Skip to content

Commit a2e8a0a

Browse files
committed
feat(phase-2): fix admin service-account create defaults for rustfs
1 parent 210c415 commit a2e8a0a

3 files changed

Lines changed: 94 additions & 18 deletions

File tree

crates/cli/src/commands/admin/mod.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,19 @@ pub async fn execute(cmd: AdminCommands, output_config: OutputConfig) -> ExitCod
5959
}
6060
}
6161

62+
fn normalize_admin_alias(alias_name: &str) -> &str {
63+
let normalized_alias = alias_name.trim_end_matches('/');
64+
if normalized_alias.is_empty() {
65+
alias_name
66+
} else {
67+
normalized_alias
68+
}
69+
}
70+
6271
/// Helper to get AdminClient from an alias name
6372
pub fn get_admin_client(alias_name: &str, formatter: &Formatter) -> Result<AdminClient, ExitCode> {
73+
let alias_lookup_name = normalize_admin_alias(alias_name);
74+
6475
let alias_manager = match AliasManager::new() {
6576
Ok(am) => am,
6677
Err(e) => {
@@ -69,7 +80,7 @@ pub fn get_admin_client(alias_name: &str, formatter: &Formatter) -> Result<Admin
6980
}
7081
};
7182

72-
let alias = match alias_manager.get(alias_name) {
83+
let alias = match alias_manager.get(alias_lookup_name) {
7384
Ok(a) => a,
7485
Err(rc_core::Error::AliasNotFound(_)) => {
7586
formatter.error(&format!("Alias '{}' not found", alias_name));
@@ -146,4 +157,19 @@ mod tests {
146157
_ => panic!("Unexpected command parsing result"),
147158
}
148159
}
160+
161+
#[test]
162+
fn test_normalize_admin_alias_trailing_slash() {
163+
assert_eq!(normalize_admin_alias("local/"), "local");
164+
}
165+
166+
#[test]
167+
fn test_normalize_admin_alias_without_trailing_slash() {
168+
assert_eq!(normalize_admin_alias("local"), "local");
169+
}
170+
171+
#[test]
172+
fn test_normalize_admin_alias_only_slash() {
173+
assert_eq!(normalize_admin_alias("/"), "/");
174+
}
149175
}

crates/cli/src/commands/admin/service_account.rs

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use crate::exit_code::ExitCode;
1010
use crate::output::Formatter;
1111
use rc_core::admin::{AdminApi, CreateServiceAccountRequest, ServiceAccount};
1212

13+
const DEFAULT_SERVICE_ACCOUNT_EXPIRY: &str = "9999-12-31T23:59:59Z";
14+
1315
/// Service account management subcommands
1416
#[derive(Subcommand, Debug)]
1517
pub enum ServiceAccountCommands {
@@ -205,14 +207,7 @@ async fn execute_create(args: CreateArgs, formatter: &Formatter) -> ExitCode {
205207
None
206208
};
207209

208-
let request = CreateServiceAccountRequest {
209-
policy,
210-
expiry: args.expiry,
211-
name: args.name,
212-
description: args.description,
213-
access_key: args.access_key.clone(),
214-
secret_key: args.secret_key.clone(),
215-
};
210+
let request = build_create_service_account_request(&args, policy);
216211

217212
match client.create_service_account(request).await {
218213
Ok(sa) => {
@@ -244,6 +239,24 @@ async fn execute_create(args: CreateArgs, formatter: &Formatter) -> ExitCode {
244239
}
245240
}
246241

242+
fn build_create_service_account_request(
243+
args: &CreateArgs,
244+
policy: Option<String>,
245+
) -> CreateServiceAccountRequest {
246+
CreateServiceAccountRequest {
247+
policy,
248+
expiry: args
249+
.expiry
250+
.clone()
251+
.or_else(|| Some(DEFAULT_SERVICE_ACCOUNT_EXPIRY.to_string())),
252+
// Keep existing CLI UX where positional ACCESS_KEY is enough.
253+
name: args.name.clone().or_else(|| Some(args.access_key.clone())),
254+
description: args.description.clone(),
255+
access_key: args.access_key.clone(),
256+
secret_key: args.secret_key.clone(),
257+
}
258+
}
259+
247260
async fn execute_info(args: InfoArgs, formatter: &Formatter) -> ExitCode {
248261
let client = match get_admin_client(&args.alias, formatter) {
249262
Ok(c) => c,
@@ -346,4 +359,43 @@ mod tests {
346359
assert!(info.secret_key.is_some());
347360
assert_eq!(info.parent_user, Some("admin".to_string()));
348361
}
362+
363+
#[test]
364+
fn test_build_create_request_uses_access_key_as_default_name() {
365+
let args = CreateArgs {
366+
alias: "local".to_string(),
367+
access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
368+
secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
369+
name: None,
370+
description: None,
371+
policy: None,
372+
expiry: None,
373+
};
374+
375+
let request = build_create_service_account_request(&args, None);
376+
assert_eq!(request.name.as_deref(), Some("AKIAIOSFODNN7EXAMPLE"));
377+
assert_eq!(
378+
request.expiry.as_deref(),
379+
Some(DEFAULT_SERVICE_ACCOUNT_EXPIRY)
380+
);
381+
}
382+
383+
#[test]
384+
fn test_build_create_request_keeps_explicit_name() {
385+
let args = CreateArgs {
386+
alias: "local".to_string(),
387+
access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
388+
secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
389+
name: Some("manual-name".to_string()),
390+
description: Some("demo".to_string()),
391+
policy: None,
392+
expiry: Some("2030-01-01T00:00:00Z".to_string()),
393+
};
394+
395+
let request = build_create_service_account_request(&args, Some("{\"x\":1}".to_string()));
396+
assert_eq!(request.name.as_deref(), Some("manual-name"));
397+
assert_eq!(request.description.as_deref(), Some("demo"));
398+
assert_eq!(request.expiry.as_deref(), Some("2030-01-01T00:00:00Z"));
399+
assert_eq!(request.policy.as_deref(), Some("{\"x\":1}"));
400+
}
349401
}

crates/core/src/admin/types.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,10 @@ pub struct CreateServiceAccountRequest {
315315
#[serde(skip_serializing_if = "Option::is_none")]
316316
pub policy: Option<String>,
317317

318-
/// Expiration time (ISO 8601). The `expiration` field must be present in the request body; use null when no expiration is set.
318+
/// Expiration time (ISO 8601)
319+
///
320+
/// RustFS latest rejects `"expiration": null`, so omit this field when unset.
321+
#[serde(skip_serializing_if = "Option::is_none")]
319322
#[serde(rename = "expiration")]
320323
pub expiry: Option<String>,
321324

@@ -487,7 +490,7 @@ mod tests {
487490
}
488491

489492
#[test]
490-
fn test_create_service_account_request_includes_expiration() {
493+
fn test_create_service_account_request_omits_expiration_when_none() {
491494
let request = CreateServiceAccountRequest {
492495
policy: None,
493496
expiry: None,
@@ -498,15 +501,10 @@ mod tests {
498501
};
499502

500503
let json = serde_json::to_string(&request).unwrap();
501-
// expiration field must always be present even when None
502504
assert!(
503-
json.contains("\"expiration\""),
504-
"JSON must contain expiration field, got: {json}"
505+
!json.contains("\"expiration\""),
506+
"JSON must not contain expiration field when unset, got: {json}"
505507
);
506-
507-
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
508-
assert!(parsed.get("expiration").is_some());
509-
assert!(parsed["expiration"].is_null());
510508
}
511509

512510
#[test]

0 commit comments

Comments
 (0)