From f4ac6d4196b0dfefadee52a138ae83de1a7f45e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandra=20=C3=98stermark?= Date: Mon, 26 Oct 2026 11:41:22 +0100 Subject: [PATCH] fix(scaleway): set an existing secret instead of failing on Scaleway's 400 Creating a secret whose name is taken returns 400 invalid_arguments ("cannot have same secret name in same path"), not the 409 ensure_secret expected, so set on any existing secret failed. Recognize that 400 and fall back to the lookup, as 409 already did. --- CHANGELOG.md | 2 + secretspec/src/provider/scaleway.rs | 58 ++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b7f8855..1298ab03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 a compatible SecretSpec executable. ### Fixed +- Scaleway: `secretspec set` on a secret that already exists adds a new version + instead of failing with HTTP 400 "cannot have same secret name in same path". - KDBX saves use the correct `DeletionTime` XML element, keeping databases compatible with KeePass clients that strictly validate deletion metadata. diff --git a/secretspec/src/provider/scaleway.rs b/secretspec/src/provider/scaleway.rs index 2a160b02..b5e4fcbf 100644 --- a/secretspec/src/provider/scaleway.rs +++ b/secretspec/src/provider/scaleway.rs @@ -393,6 +393,16 @@ impl ScalewayProvider { self.lookup_secret_id(client, project_id, secret_path, secret_name) .await } + // Scaleway answers a name that is already taken with 400, not 409. + 400 => { + let body = read_body(response).await; + if is_duplicate_name(&body) { + self.lookup_secret_id(client, project_id, secret_path, secret_name) + .await + } else { + Err(status_error("creating secret", 400, &body)) + } + } status => Err(http_error("creating secret", status, response).await), } } @@ -460,16 +470,43 @@ fn decode_payload(item: &str, data: &str) -> Result { /// Builds an error for a non-success HTTP response, including the body. async fn http_error(action: &str, status: u16, response: reqwest::Response) -> SecretSpecError { - let body = match response.text().await { + status_error(action, status, &read_body(response).await) +} + +fn status_error(action: &str, status: u16, body: &str) -> SecretSpecError { + SecretSpecError::ProviderOperationFailed(format!( + "Scaleway returned HTTP {status} while {action}: {body}" + )) +} + +async fn read_body(response: reqwest::Response) -> String { + match response.text().await { Ok(body) => body, Err(error) => format!( "", crate::error::display_error_chain(&error) ), - }; - SecretSpecError::ProviderOperationFailed(format!( - "Scaleway returned HTTP {status} while {action}: {body}" - )) + } +} + +/// Whether a create-secret 400 is Scaleway's "name already taken in this path". +fn is_duplicate_name(body: &str) -> bool { + #[derive(Deserialize)] + struct InvalidArguments { + #[serde(default)] + details: Vec, + } + #[derive(Deserialize)] + struct ArgumentError { + argument_name: String, + help_message: String, + } + serde_json::from_str::(body).is_ok_and(|error| { + error.details.iter().any(|detail| { + detail.argument_name == "name" + && detail.help_message.contains("cannot have same secret name") + }) + }) } impl Provider for ScalewayProvider { @@ -561,6 +598,17 @@ mod tests { ScalewayConfig::try_from(&ProviderUrl::new(Url::parse(spec).unwrap())).unwrap() } + #[test] + fn duplicate_name_is_recognized() { + // verbatim from Scaleway, setting a secret that already exists + let taken = r#"{"details":[{"argument_name":"name","help_message":"cannot have same secret name in same path","reason":"format"}],"message":"invalid argument(s)","type":"invalid_arguments"}"#; + assert!(is_duplicate_name(taken)); + + let bad_path = r#"{"details":[{"argument_name":"path","help_message":"must start with /","reason":"format"}],"message":"invalid argument(s)","type":"invalid_arguments"}"#; + assert!(!is_duplicate_name(bad_path)); + assert!(!is_duplicate_name("not json")); + } + #[test] fn region_from_uri_or_default() { // An explicit host is used verbatim regardless of the environment.