Rust implementation of OCI Distribution Spec with granular access control
at main 290 lines 7.6 kB view raw
1use clap::{Parser, Subcommand}; 2use reqwest::blocking::Client; 3use serde_json::json; 4use std::process; 5 6#[derive(Parser)] 7#[command(name = "grainctl")] 8#[command(about = "CLI tool for administering the grain OCI registry", long_about = None)] 9#[command(version)] 10struct Cli { 11 #[command(subcommand)] 12 command: Commands, 13} 14 15#[derive(Subcommand)] 16enum Commands { 17 /// User management 18 User { 19 #[command(subcommand)] 20 command: UserCommands, 21 }, 22 23 /// Run garbage collection 24 Gc { 25 #[arg(long, default_value = "false")] 26 dry_run: bool, 27 28 #[arg(long, default_value = "24")] 29 grace_period_hours: u64, 30 31 #[arg(long, env = "GRAIN_URL")] 32 url: String, 33 34 #[arg(long, env = "GRAIN_ADMIN_USER")] 35 username: String, 36 37 #[arg(long, env = "GRAIN_ADMIN_PASSWORD")] 38 password: String, 39 }, 40} 41 42#[derive(Subcommand)] 43enum UserCommands { 44 /// List all users 45 List { 46 #[arg(long, env = "GRAIN_URL")] 47 url: String, 48 49 #[arg(long, env = "GRAIN_ADMIN_USER")] 50 username: String, 51 52 #[arg(long, env = "GRAIN_ADMIN_PASSWORD")] 53 password: String, 54 }, 55 56 /// Create a new user 57 Create { 58 /// Username for the new user 59 user: String, 60 61 /// Password for the new user 62 #[arg(long)] 63 pass: String, 64 65 #[arg(long, env = "GRAIN_URL")] 66 url: String, 67 68 #[arg(long, env = "GRAIN_ADMIN_USER")] 69 username: String, 70 71 #[arg(long, env = "GRAIN_ADMIN_PASSWORD")] 72 password: String, 73 }, 74 75 /// Delete a user 76 Delete { 77 /// Username to delete 78 user: String, 79 80 #[arg(long, env = "GRAIN_URL")] 81 url: String, 82 83 #[arg(long, env = "GRAIN_ADMIN_USER")] 84 username: String, 85 86 #[arg(long, env = "GRAIN_ADMIN_PASSWORD")] 87 password: String, 88 }, 89 90 /// Add permission to a user 91 AddPermission { 92 /// Target username 93 user: String, 94 95 /// Repository pattern (e.g., "myorg/myrepo" or "myorg/*") 96 #[arg(long)] 97 repository: String, 98 99 /// Tag pattern (e.g., "latest" or "v*") 100 #[arg(long)] 101 tag: String, 102 103 /// Actions (comma-separated: pull,push,delete) 104 #[arg(long)] 105 actions: String, 106 107 #[arg(long, env = "GRAIN_URL")] 108 url: String, 109 110 #[arg(long, env = "GRAIN_ADMIN_USER")] 111 username: String, 112 113 #[arg(long, env = "GRAIN_ADMIN_PASSWORD")] 114 password: String, 115 }, 116} 117 118fn main() { 119 let cli = Cli::parse(); 120 121 if let Err(e) = execute_command(&cli.command) { 122 eprintln!("Error: {}", e); 123 process::exit(1); 124 } 125} 126 127fn execute_command(cmd: &Commands) -> Result<(), Box<dyn std::error::Error>> { 128 match cmd { 129 Commands::User { command } => execute_user_command(command), 130 Commands::Gc { 131 dry_run, 132 grace_period_hours, 133 url, 134 username, 135 password, 136 } => execute_gc_command(*dry_run, *grace_period_hours, url, username, password), 137 } 138} 139 140fn execute_user_command(cmd: &UserCommands) -> Result<(), Box<dyn std::error::Error>> { 141 let client = Client::new(); 142 143 match cmd { 144 UserCommands::List { 145 url, 146 username, 147 password, 148 } => { 149 let response = client 150 .get(format!("{}/admin/users", url)) 151 .basic_auth(username, Some(password)) 152 .send()?; 153 154 if !response.status().is_success() { 155 let status = response.status(); 156 let text = response 157 .text() 158 .unwrap_or_else(|_| String::from("No response body")); 159 return Err(format!("{} - {}", status, text).into()); 160 } 161 162 let users: serde_json::Value = response.json()?; 163 println!("{}", serde_json::to_string_pretty(&users)?); 164 Ok(()) 165 } 166 167 UserCommands::Create { 168 user, 169 pass, 170 url, 171 username, 172 password, 173 } => { 174 let body = json!({ 175 "username": user, 176 "password": pass, 177 "permissions": [] 178 }); 179 180 let response = client 181 .post(format!("{}/admin/users", url)) 182 .basic_auth(username, Some(password)) 183 .json(&body) 184 .send()?; 185 186 if !response.status().is_success() { 187 let status = response.status(); 188 let text = response 189 .text() 190 .unwrap_or_else(|_| String::from("No response body")); 191 return Err(format!("{} - {}", status, text).into()); 192 } 193 194 println!("User '{}' created successfully", user); 195 Ok(()) 196 } 197 198 UserCommands::Delete { 199 user, 200 url, 201 username, 202 password, 203 } => { 204 let response = client 205 .delete(format!("{}/admin/users/{}", url, user)) 206 .basic_auth(username, Some(password)) 207 .send()?; 208 209 if !response.status().is_success() { 210 let status = response.status(); 211 let text = response 212 .text() 213 .unwrap_or_else(|_| String::from("No response body")); 214 return Err(format!("{} - {}", status, text).into()); 215 } 216 217 println!("User '{}' deleted successfully", user); 218 Ok(()) 219 } 220 221 UserCommands::AddPermission { 222 user, 223 repository, 224 tag, 225 actions, 226 url, 227 username, 228 password, 229 } => { 230 let actions_vec: Vec<String> = 231 actions.split(',').map(|s| s.trim().to_string()).collect(); 232 233 let body = json!({ 234 "repository": repository, 235 "tag": tag, 236 "actions": actions_vec 237 }); 238 239 let response = client 240 .post(format!("{}/admin/users/{}/permissions", url, user)) 241 .basic_auth(username, Some(password)) 242 .json(&body) 243 .send()?; 244 245 if !response.status().is_success() { 246 let status = response.status(); 247 let text = response 248 .text() 249 .unwrap_or_else(|_| String::from("No response body")); 250 return Err(format!("{} - {}", status, text).into()); 251 } 252 253 println!( 254 "Permission added to user '{}': {} on {}:{}", 255 user, actions, repository, tag 256 ); 257 Ok(()) 258 } 259 } 260} 261 262fn execute_gc_command( 263 dry_run: bool, 264 grace_period_hours: u64, 265 url: &str, 266 username: &str, 267 password: &str, 268) -> Result<(), Box<dyn std::error::Error>> { 269 let client = Client::new(); 270 271 let response = client 272 .post(format!( 273 "{}/admin/gc?dry_run={}&grace_period_hours={}", 274 url, dry_run, grace_period_hours 275 )) 276 .basic_auth(username, Some(password)) 277 .send()?; 278 279 if !response.status().is_success() { 280 let status = response.status(); 281 let text = response 282 .text() 283 .unwrap_or_else(|_| String::from("No response body")); 284 return Err(format!("{} - {}", status, text).into()); 285 } 286 287 let stats: serde_json::Value = response.json()?; 288 println!("{}", serde_json::to_string_pretty(&stats)?); 289 Ok(()) 290}