Skip to content

Commit 297253f

Browse files
authored
Add admin CLI commands (#29)
* Add admin CLI commands - Add admin subcommand with start, stop, stats, and CRUD operations - Add admin service functions with Bearer token authentication - New commands: - popcorn admin start/stop - control job acceptance - popcorn admin stats - view server statistics - popcorn admin get-submission/delete-submission - popcorn admin create-leaderboard/delete-leaderboard Requires POPCORN_ADMIN_TOKEN environment variable for authentication. * Simplify create-leaderboard to match kernelbot API - Only require directory as positional arg (e.g., "identity_py") - Accept --gpu multiple times for multiple GPU types - Name and deadline auto-derived by server * Simplify create-leaderboard to only require directory - Remove --gpu argument (GPUs now come from task.yml) - Remove unimplemented LoadCompetition stub - Service only sends directory in payload * Add update-problems admin command Adds CLI support for updating problems from a GitHub repository, mirroring the Discord /admin update-problems command. Supports --problem-set, --repository, --branch, and --force options. * Clarify CLI support for Discord functionalities Removed 'almost' from the description of CLI capabilities.
1 parent a270241 commit 297253f

File tree

4 files changed

+373
-2
lines changed

4 files changed

+373
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Interested in new kernel competitions? Join [discord.gg/gpumode](https://discord
8989

9090
## Discover Problems
9191

92-
The CLI supports (almost) everything Discord does, so you can also discover which leaderboards are available. To make discovery more pleasant we also offer a TUI experience.
92+
The CLI supports everything Discord does, so you can also discover which leaderboards are available. To make discovery more pleasant we also offer a TUI experience.
9393

9494
```bash
9595
popcorn-cli submit <submission-file>

src/cmd/admin.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use anyhow::{anyhow, Result};
2+
use clap::Subcommand;
3+
use std::env;
4+
5+
use crate::service;
6+
7+
#[derive(Subcommand, Debug)]
8+
pub enum AdminAction {
9+
/// Start accepting jobs on the server
10+
Start,
11+
/// Stop accepting jobs on the server
12+
Stop,
13+
/// Get server statistics
14+
Stats {
15+
/// Only show stats for the last 24 hours
16+
#[arg(long)]
17+
last_day: bool,
18+
},
19+
/// Get a submission by ID
20+
GetSubmission {
21+
/// The submission ID to retrieve
22+
id: i64,
23+
},
24+
/// Delete a submission by ID
25+
DeleteSubmission {
26+
/// The submission ID to delete
27+
id: i64,
28+
},
29+
/// Create a dev leaderboard from a problem directory (requires gpus in task.yml)
30+
CreateLeaderboard {
31+
/// Problem directory name (e.g., "identity_py")
32+
directory: String,
33+
},
34+
/// Delete a leaderboard
35+
DeleteLeaderboard {
36+
/// Name of the leaderboard to delete
37+
name: String,
38+
/// Force deletion even if there are submissions
39+
#[arg(long)]
40+
force: bool,
41+
},
42+
/// Update problems from a GitHub repository (mirrors Discord /admin update-problems)
43+
UpdateProblems {
44+
/// Problem set name (e.g., "nvidia", "pmpp_v2"). If not specified, updates all.
45+
#[arg(long)]
46+
problem_set: Option<String>,
47+
48+
/// Repository in format "owner/repo" (default: gpu-mode/reference-kernels)
49+
#[arg(long, default_value = "gpu-mode/reference-kernels")]
50+
repository: String,
51+
52+
/// Branch to pull from (default: main)
53+
#[arg(long, default_value = "main")]
54+
branch: String,
55+
56+
/// Force update even if task definition changed significantly
57+
#[arg(long)]
58+
force: bool,
59+
},
60+
}
61+
62+
fn get_admin_token() -> Result<String> {
63+
env::var("POPCORN_ADMIN_TOKEN").map_err(|_| {
64+
anyhow!(
65+
"POPCORN_ADMIN_TOKEN environment variable is not set.\n\
66+
Set it to your admin token to use admin commands:\n\
67+
export POPCORN_ADMIN_TOKEN=your_token_here"
68+
)
69+
})
70+
}
71+
72+
pub async fn handle_admin(action: AdminAction) -> Result<()> {
73+
let admin_token = get_admin_token()?;
74+
let client = service::create_admin_client(&admin_token)?;
75+
76+
match action {
77+
AdminAction::Start => {
78+
let result = service::admin_start(&client).await?;
79+
println!("Server started accepting jobs");
80+
println!("{}", serde_json::to_string_pretty(&result)?);
81+
}
82+
AdminAction::Stop => {
83+
let result = service::admin_stop(&client).await?;
84+
println!("Server stopped accepting jobs");
85+
println!("{}", serde_json::to_string_pretty(&result)?);
86+
}
87+
AdminAction::Stats { last_day } => {
88+
let result = service::admin_stats(&client, last_day).await?;
89+
println!("{}", serde_json::to_string_pretty(&result)?);
90+
}
91+
AdminAction::GetSubmission { id } => {
92+
let result = service::admin_get_submission(&client, id).await?;
93+
println!("{}", serde_json::to_string_pretty(&result)?);
94+
}
95+
AdminAction::DeleteSubmission { id } => {
96+
let result = service::admin_delete_submission(&client, id).await?;
97+
println!("Deleted submission {}", id);
98+
println!("{}", serde_json::to_string_pretty(&result)?);
99+
}
100+
AdminAction::CreateLeaderboard { directory } => {
101+
let result = service::admin_create_leaderboard(&client, &directory).await?;
102+
let name = result["leaderboard"].as_str().unwrap_or(&directory);
103+
println!("Created leaderboard '{}'", name);
104+
println!("{}", serde_json::to_string_pretty(&result)?);
105+
}
106+
AdminAction::DeleteLeaderboard { name, force } => {
107+
let result = service::admin_delete_leaderboard(&client, &name, force).await?;
108+
println!("Deleted leaderboard '{}'", name);
109+
println!("{}", serde_json::to_string_pretty(&result)?);
110+
}
111+
AdminAction::UpdateProblems {
112+
problem_set,
113+
repository,
114+
branch,
115+
force,
116+
} => {
117+
println!(
118+
"Updating problems from {}/tree/{}{}...",
119+
repository,
120+
branch,
121+
problem_set
122+
.as_ref()
123+
.map(|ps| format!(" (problem set: {})", ps))
124+
.unwrap_or_default()
125+
);
126+
let result = service::admin_update_problems(
127+
&client,
128+
problem_set.as_deref(),
129+
&repository,
130+
&branch,
131+
force,
132+
)
133+
.await?;
134+
135+
// Pretty print the results
136+
if let Some(created) = result.get("created").and_then(|v| v.as_array()) {
137+
if !created.is_empty() {
138+
println!("\nCreated {} leaderboard(s):", created.len());
139+
for name in created {
140+
println!(" + {}", name.as_str().unwrap_or("unknown"));
141+
}
142+
}
143+
}
144+
if let Some(updated) = result.get("updated").and_then(|v| v.as_array()) {
145+
if !updated.is_empty() {
146+
println!("\nUpdated {} leaderboard(s):", updated.len());
147+
for name in updated {
148+
println!(" ~ {}", name.as_str().unwrap_or("unknown"));
149+
}
150+
}
151+
}
152+
if let Some(skipped) = result.get("skipped").and_then(|v| v.as_array()) {
153+
if !skipped.is_empty() {
154+
println!("\nSkipped {} leaderboard(s):", skipped.len());
155+
for item in skipped {
156+
let name = item.get("name").and_then(|n| n.as_str()).unwrap_or("unknown");
157+
let reason = item
158+
.get("reason")
159+
.and_then(|r| r.as_str())
160+
.unwrap_or("no changes");
161+
println!(" - {} ({})", name, reason);
162+
}
163+
}
164+
}
165+
if let Some(errors) = result.get("errors").and_then(|v| v.as_array()) {
166+
if !errors.is_empty() {
167+
println!("\nErrors ({}):", errors.len());
168+
for item in errors {
169+
let name = item.get("name").and_then(|n| n.as_str()).unwrap_or("unknown");
170+
let error = item.get("error").and_then(|e| e.as_str()).unwrap_or("unknown");
171+
println!(" ! {}: {}", name, error);
172+
}
173+
}
174+
}
175+
}
176+
}
177+
178+
Ok(())
179+
}

src/cmd/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ use serde_yaml;
66
use std::fs::File;
77
use std::path::PathBuf;
88

9+
mod admin;
910
mod auth;
1011
mod submit;
1112

13+
pub use admin::AdminAction;
14+
1215
#[derive(Serialize, Deserialize, Debug, Default)]
1316
struct Config {
1417
cli_id: Option<String>,
@@ -105,6 +108,11 @@ enum Commands {
105108
#[arg(long)]
106109
no_tui: bool,
107110
},
111+
/// Admin commands (requires POPCORN_ADMIN_TOKEN env var)
112+
Admin {
113+
#[command(subcommand)]
114+
action: AdminAction,
115+
},
108116
}
109117

110118
pub async fn execute(cli: Cli) -> Result<()> {
@@ -142,7 +150,7 @@ pub async fn execute(cli: Cli) -> Result<()> {
142150

143151
// Use filepath from Submit command first, fallback to top-level filepath
144152
let final_filepath = filepath.or(cli.filepath);
145-
153+
146154
if no_tui {
147155
submit::run_submit_plain(
148156
final_filepath, // Resolved filepath
@@ -165,6 +173,9 @@ pub async fn execute(cli: Cli) -> Result<()> {
165173
.await
166174
}
167175
}
176+
Some(Commands::Admin { action }) => {
177+
admin::handle_admin(action).await
178+
}
168179
None => {
169180
// Check if any of the submission-related flags were used at the top level
170181
if cli.gpu.is_some() || cli.leaderboard.is_some() || cli.mode.is_some() {

0 commit comments

Comments
 (0)