diff --git a/crates/dev_container/src/docker.rs b/crates/dev_container/src/docker.rs index 99ce7422eee36d56e2bc53fd31d150fe2f41b16d..7931923b4219e33fa56e8fb2fb6b97c1ea89a750 100644 --- a/crates/dev_container/src/docker.rs +++ b/crates/dev_container/src/docker.rs @@ -487,10 +487,18 @@ where let s: Option = Option::deserialize(deserializer)?; match s { Some(json_string) => { + // The devcontainer metadata label can be either a JSON array (e.g. from + // image-based devcontainers) or a single JSON object (e.g. from + // docker-compose-based devcontainers created by the devcontainer CLI). + // Handle both formats. let parsed: Vec> = - serde_json_lenient::from_str(&json_string).map_err(|e| { - log::error!("Error deserializing metadata: {e}"); - serde::de::Error::custom(e) + serde_json_lenient::from_str(&json_string).or_else(|_| { + let single: HashMap = + serde_json_lenient::from_str(&json_string).map_err(|e| { + log::error!("Error deserializing metadata: {e}"); + serde::de::Error::custom(e) + })?; + Ok(vec![single]) })?; Ok(Some(parsed)) } @@ -936,6 +944,30 @@ mod test { assert_eq!(target_dir.unwrap(), "/workspaces/cli/".to_string()); } + #[test] + fn should_deserialize_object_metadata_from_docker_compose_container() { + // The devcontainer CLI writes metadata as a bare JSON object (not an array) + // when there is only one metadata entry (e.g. docker-compose with no features). + // See https://github.com/devcontainers/cli/issues/1054 + let given_config = r#" + { + "Id": "dc4e7b8ff4bf", + "Config": { + "Labels": { + "devcontainer.metadata": "{\"remoteUser\":\"ubuntu\"}" + } + } + } + "#; + let config = serde_json_lenient::from_str::(given_config).unwrap(); + + assert!(config.config.labels.metadata.is_some()); + let metadata = config.config.labels.metadata.unwrap(); + assert_eq!(metadata.len(), 1); + assert!(metadata[0].contains_key("remoteUser")); + assert_eq!(metadata[0]["remoteUser"], "ubuntu"); + } + #[test] fn should_deserialize_docker_compose_config() { let given_config = r#"