From bea4acd768c8757ee81b4e308ac0ccd1f3731119 Mon Sep 17 00:00:00 2001 From: Johan Carlin Date: Wed, 22 Apr 2026 12:48:32 +0200 Subject: [PATCH] Handle podman inspect templates without Variant field --- cmd/devcontainer/src/runtime/compose/tests.rs | 16 +++++ .../src/runtime/container/uid_update.rs | 40 +++++++++++- .../src/runtime/container/uid_update/tests.rs | 63 +++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/cmd/devcontainer/src/runtime/compose/tests.rs b/cmd/devcontainer/src/runtime/compose/tests.rs index f50046eaf..d886d6c82 100644 --- a/cmd/devcontainer/src/runtime/compose/tests.rs +++ b/cmd/devcontainer/src/runtime/compose/tests.rs @@ -64,6 +64,22 @@ fn compose_project_name_defaults_to_workspace_devcontainer() { let _ = fs::remove_dir_all(root); } +#[test] +fn compose_project_name_defaults_to_compose_working_dir_basename() { + let root = unique_temp_dir("devcontainer-compose-test"); + let compose_file = root.join("docker-compose.yml"); + fs::create_dir_all(&root).expect("compose dir"); + fs::write(&compose_file, "services:\n app:\n image: alpine:3.20\n").expect("compose"); + + let project_name = compose_project_name(&[compose_file]).expect("project name"); + + assert_eq!( + project_name, + root.file_name().unwrap().to_string_lossy().to_lowercase() + ); + let _ = fs::remove_dir_all(root); +} + #[test] fn compose_name_from_file_reads_top_level_name() { let root = unique_temp_dir("devcontainer-compose-test"); diff --git a/cmd/devcontainer/src/runtime/container/uid_update.rs b/cmd/devcontainer/src/runtime/container/uid_update.rs index 496ba4882..9bd3c454c 100644 --- a/cmd/devcontainer/src/runtime/container/uid_update.rs +++ b/cmd/devcontainer/src/runtime/container/uid_update.rs @@ -16,6 +16,8 @@ use super::super::paths::unique_temp_path; const UID_UPDATE_IMAGE_INSPECT_FORMAT: &str = "{{.Config.User}}\n{{.Os}}/{{.Architecture}}{{if .Variant}}/{{.Variant}}{{end}}"; +const UID_UPDATE_IMAGE_INSPECT_FORMAT_NO_VARIANT: &str = + "{{.Config.User}}\n{{.Os}}/{{.Architecture}}"; #[derive(Debug, Eq, PartialEq)] struct UidUpdateDetails { @@ -263,13 +265,44 @@ fn inspect_image_details_for_uid_update_once( )?; if result.status_code != 0 { let error = engine::stderr_or_stdout(&result); + if is_missing_variant_template_error(&error) { + return inspect_image_details_without_variant(args, image_name); + } if is_missing_local_image_inspect_error(&error) { return Ok(None); } return Err(error); } - let mut lines = result.stdout.lines(); + parse_image_inspect_details(&result.stdout) +} + +fn inspect_image_details_without_variant( + args: &[String], + image_name: &str, +) -> Result, String> { + let result = engine::run_engine( + args, + vec![ + "image".to_string(), + "inspect".to_string(), + "--format".to_string(), + UID_UPDATE_IMAGE_INSPECT_FORMAT_NO_VARIANT.to_string(), + image_name.to_string(), + ], + )?; + if result.status_code != 0 { + let error = engine::stderr_or_stdout(&result); + if is_missing_local_image_inspect_error(&error) { + return Ok(None); + } + return Err(error); + } + parse_image_inspect_details(&result.stdout) +} + +fn parse_image_inspect_details(stdout: &str) -> Result, String> { + let mut lines = stdout.lines(); let user = lines .next() .map(str::trim) @@ -298,6 +331,11 @@ fn is_missing_local_image_inspect_error(error: &str) -> bool { error.contains("no such image") || error.contains("image not known") } +fn is_missing_variant_template_error(error: &str) -> bool { + let error = error.to_ascii_lowercase(); + error.contains("can't evaluate field variant") +} + fn uid_update_image_name(workspace_folder: &Path, image_name: &str) -> String { let local_image_name = uid_update_local_image_name(workspace_folder); let base_image_name = if image_name.starts_with(&local_image_name) { diff --git a/cmd/devcontainer/src/runtime/container/uid_update/tests.rs b/cmd/devcontainer/src/runtime/container/uid_update/tests.rs index 5370b41b8..b6d1d5c06 100644 --- a/cmd/devcontainer/src/runtime/container/uid_update/tests.rs +++ b/cmd/devcontainer/src/runtime/container/uid_update/tests.rs @@ -255,6 +255,48 @@ fn prepare_up_image_prefixes_local_podman_base_images_with_localhost() { ))); } +#[test] +fn prepare_up_image_retries_image_inspect_without_variant_template_for_podman() { + let fixture = FakeEngineFixture::new(); + fixture.write("image-inspect-with-variant.exit", "1\n"); + fixture.write( + "image-inspect-with-variant.stderr", + "Error: template: inspect:2:30: executing \"inspect\" at <.Variant>: can't evaluate field Variant in type interface {}\n", + ); + fixture.write( + "image-inspect-without-variant.stdout", + &image_inspect_output("node", Some("linux/amd64")), + ); + + let workspace = fixture.root.join("workspace"); + fs::create_dir_all(&workspace).expect("workspace dir"); + let resolved = resolved_config( + json!({ + "remoteUser": "vscode" + }), + &workspace, + ); + + let updated_image = prepare_up_image_for_platform( + &resolved, + &fixture.args_with_podman_name(), + "ghcr.io/example/app:latest", + true, + ) + .expect("prepare up image"); + + assert!(updated_image.ends_with("-uid")); + let invocations = fixture.invocations(); + assert!(invocations.contains(&format!( + "image inspect --format {} ghcr.io/example/app:latest", + super::UID_UPDATE_IMAGE_INSPECT_FORMAT + ))); + assert!(invocations.contains(&format!( + "image inspect --format {} ghcr.io/example/app:latest", + super::UID_UPDATE_IMAGE_INSPECT_FORMAT_NO_VARIANT + ))); +} + #[test] fn prepare_up_image_uses_compose_service_user_for_uid_update_selection() { let fixture = FakeEngineFixture::new(); @@ -333,6 +375,27 @@ case "$COMMAND" in shift || true case "$SUBCOMMAND" in inspect) + if [ "${*}" != "${*#*'.Variant'*}" ]; then + if [ -f "$ROOT/image-inspect-with-variant.stdout" ]; then + cat "$ROOT/image-inspect-with-variant.stdout" + fi + if [ -f "$ROOT/image-inspect-with-variant.stderr" ]; then + cat "$ROOT/image-inspect-with-variant.stderr" >&2 + fi + if [ -f "$ROOT/image-inspect-with-variant.exit" ]; then + exit "$(tr -d '\n' < "$ROOT/image-inspect-with-variant.exit")" + fi + else + if [ -f "$ROOT/image-inspect-without-variant.stdout" ]; then + cat "$ROOT/image-inspect-without-variant.stdout" + fi + if [ -f "$ROOT/image-inspect-without-variant.stderr" ]; then + cat "$ROOT/image-inspect-without-variant.stderr" >&2 + fi + if [ -f "$ROOT/image-inspect-without-variant.exit" ]; then + exit "$(tr -d '\n' < "$ROOT/image-inspect-without-variant.exit")" + fi + fi if [ -f "$ROOT/pulled" ]; then if [ -f "$ROOT/image-inspect-after-pull.stdout" ]; then cat "$ROOT/image-inspect-after-pull.stdout"