diff --git a/cli/azd/pkg/project/container_helper.go b/cli/azd/pkg/project/container_helper.go index 257e75ee9aa..52c6b2d941e 100644 --- a/cli/azd/pkg/project/container_helper.go +++ b/cli/azd/pkg/project/container_helper.go @@ -84,6 +84,11 @@ func NewContainerHelper( } } +// ContainerEngine returns the detected container engine name ("docker" or "podman"). +func (ch *ContainerHelper) ContainerEngine() string { + return ch.docker.ContainerEngine() +} + // DockerfileBuilder returns a new DockerfileBuilder instance for building Dockerfiles programmatically. func (ch *ContainerHelper) DockerfileBuilder() *DockerfileBuilder { return NewDockerfileBuilder() @@ -1017,7 +1022,8 @@ func (ch *ContainerHelper) runDotnetPublish( imageName, dockerCreds.LoginServer, dockerCreds.Username, - dockerCreds.Password) + dockerCreds.Password, + ch.ContainerEngine()) if err != nil { return "", fmt.Errorf("publishing container: %w", err) } diff --git a/cli/azd/pkg/project/service_target_dotnet_containerapp.go b/cli/azd/pkg/project/service_target_dotnet_containerapp.go index ffea0c4eca6..205d6616f5e 100644 --- a/cli/azd/pkg/project/service_target_dotnet_containerapp.go +++ b/cli/azd/pkg/project/service_target_dotnet_containerapp.go @@ -664,7 +664,8 @@ func (at *dotnetContainerAppTarget) prepareDotNetProjectImage( imageName, dockerCreds.LoginServer, dockerCreds.Username, - dockerCreds.Password) + dockerCreds.Password, + at.containerHelper.ContainerEngine()) if err != nil { return nil, fmt.Errorf("publishing container: %w", err) } @@ -700,6 +701,7 @@ func (at *dotnetContainerAppTarget) prepareDotNetMultiStageImage( serviceConfig.Path(), "Release", localImageTag, + at.containerHelper.ContainerEngine(), ) if err != nil { return nil, fmt.Errorf("building local container: %w", err) diff --git a/cli/azd/pkg/tools/docker/docker.go b/cli/azd/pkg/tools/docker/docker.go index c670fe2f24c..71dec1cd9d1 100644 --- a/cli/azd/pkg/tools/docker/docker.go +++ b/cli/azd/pkg/tools/docker/docker.go @@ -35,6 +35,13 @@ type Cli struct { containerEngine string // "docker" or "podman", detected during CheckInstalled } +// ContainerEngine returns the detected container engine name ("docker" or "podman"). +// CheckInstalled() should be called first to detect and set the container engine. +// If not set, defaults to "docker" for backward compatibility. +func (d *Cli) ContainerEngine() string { + return d.getContainerEngine() +} + // getContainerEngine returns the container engine command to use ("docker" or "podman"). // CheckInstalled() should be called first to detect and set the container engine. // If not set, defaults to "docker" for backward compatibility. diff --git a/cli/azd/pkg/tools/dotnet/dotnet.go b/cli/azd/pkg/tools/dotnet/dotnet.go index a7c9eaf1024..5a2562b026d 100644 --- a/cli/azd/pkg/tools/dotnet/dotnet.go +++ b/cli/azd/pkg/tools/dotnet/dotnet.go @@ -228,7 +228,7 @@ func (cli *Cli) PublishAppHostManifest( // BuildContainerLocal runs `dotnet publish` with `/t:PublishContainer` to build a container image locally // without pushing to a registry. Returns the port number and image name. func (cli *Cli) BuildContainerLocal( - ctx context.Context, project, configuration, imageName string, + ctx context.Context, project, configuration, imageName, containerEngine string, ) (int, string, error) { if !strings.Contains(imageName, ":") { imageName = fmt.Sprintf("%s:latest", imageName) @@ -258,6 +258,7 @@ func (cli *Cli) BuildContainerLocal( "--getProperty:GeneratedContainerConfiguration", ) + runArgs = appendContainerEngine(runArgs, containerEngine) runArgs = appendArtifactsPath(ctx, runArgs) result, err := cli.commandRunner.Run(ctx, runArgs) @@ -274,7 +275,7 @@ func (cli *Cli) BuildContainerLocal( } func (cli *Cli) PublishContainer( - ctx context.Context, project, configuration, imageName, server, username, password string, + ctx context.Context, project, configuration, imageName, server, username, password, containerEngine string, ) (int, error) { if !strings.Contains(imageName, ":") { imageName = fmt.Sprintf("%s:latest", imageName) @@ -304,6 +305,7 @@ func (cli *Cli) PublishContainer( "--getProperty:GeneratedContainerConfiguration", ) + runArgs = appendContainerEngine(runArgs, containerEngine) runArgs = appendArtifactsPath(ctx, runArgs) runArgs = runArgs.WithEnv([]string{ @@ -327,6 +329,18 @@ func (cli *Cli) PublishContainer( return port, nil } +// appendContainerEngine appends -p:ContainerEngine= to the dotnet publish command +// when the container engine is not "docker" (the .NET SDK default). This ensures the +// .NET SDK uses the correct container runtime (e.g., Podman) for building and pushing images. +func appendContainerEngine(runArgs exec.RunArgs, containerEngine string) exec.RunArgs { + if containerEngine != "" && containerEngine != "docker" { + runArgs = runArgs.AppendParams( + fmt.Sprintf("-p:ContainerEngine=%s", containerEngine), + ) + } + return runArgs +} + // appendArtifactsPath checks the context for a per-service artifacts path // (set by [ContextWithArtifactsPath]) and, if present, appends // --artifacts-path to the dotnet publish command. This redirects all diff --git a/cli/azd/pkg/tools/dotnet/dotnet_commands_test.go b/cli/azd/pkg/tools/dotnet/dotnet_commands_test.go index 5863750a18b..d1a334d9fb2 100644 --- a/cli/azd/pkg/tools/dotnet/dotnet_commands_test.go +++ b/cli/azd/pkg/tools/dotnet/dotnet_commands_test.go @@ -408,7 +408,7 @@ func Test_Cli_BuildContainerLocal(t *testing.T) { return exec.NewRunResult(0, successContainerOutput, ""), nil }) - port, image, err := cli.BuildContainerLocal(t.Context(), "p.csproj", "Release", "myrepo") + port, image, err := cli.BuildContainerLocal(t.Context(), "p.csproj", "Release", "myrepo", "") require.NoError(t, err) require.Equal(t, 8080, port) require.Equal(t, "myrepo:latest", image) @@ -431,7 +431,7 @@ func Test_Cli_BuildContainerLocal(t *testing.T) { }) proj := filepath.Join("some", "dir", "apphost.cs") - _, _, err := cli.BuildContainerLocal(t.Context(), proj, "Release", "img:v1") + _, _, err := cli.BuildContainerLocal(t.Context(), proj, "Release", "img:v1", "") require.NoError(t, err) require.Equal(t, filepath.Dir(proj), captured.Cwd) require.Equal(t, "apphost.cs", captured.Args[1]) @@ -446,7 +446,7 @@ func Test_Cli_BuildContainerLocal(t *testing.T) { return exec.RunResult{}, errors.New("nope") }) - _, _, err := cli.BuildContainerLocal(t.Context(), "p.csproj", "Release", "img") + _, _, err := cli.BuildContainerLocal(t.Context(), "p.csproj", "Release", "img", "") require.Error(t, err) require.Contains(t, err.Error(), "dotnet publish on project 'p.csproj' failed") }) @@ -457,7 +457,7 @@ func Test_Cli_BuildContainerLocal(t *testing.T) { runner.When(matchDotnetArg0("publish")). Respond(exec.NewRunResult(0, "", "")) - _, _, err := cli.BuildContainerLocal(t.Context(), "p.csproj", "Release", "img") + _, _, err := cli.BuildContainerLocal(t.Context(), "p.csproj", "Release", "img", "") require.Error(t, err) require.Contains(t, err.Error(), "failed to get dotnet target port") }) @@ -477,7 +477,7 @@ func Test_Cli_PublishContainer(t *testing.T) { }) port, err := cli.PublishContainer( - t.Context(), "p.csproj", "Release", "repo", "registry.example.com", "user", "secret", + t.Context(), "p.csproj", "Release", "repo", "registry.example.com", "user", "secret", "", ) require.NoError(t, err) require.Equal(t, 8080, port) @@ -500,7 +500,7 @@ func Test_Cli_PublishContainer(t *testing.T) { }) proj := filepath.Join("d", "apphost.cs") - _, err := cli.PublishContainer(t.Context(), proj, "Release", "img", "r", "u", "p") + _, err := cli.PublishContainer(t.Context(), proj, "Release", "img", "r", "u", "p", "") require.NoError(t, err) require.Equal(t, filepath.Dir(proj), captured.Cwd) require.Equal(t, "apphost.cs", captured.Args[1]) @@ -515,7 +515,7 @@ func Test_Cli_PublishContainer(t *testing.T) { return exec.RunResult{}, errors.New("boom") }) - _, err := cli.PublishContainer(t.Context(), "p.csproj", "Release", "img", "r", "u", "p") + _, err := cli.PublishContainer(t.Context(), "p.csproj", "Release", "img", "r", "u", "p", "") require.Error(t, err) require.Contains(t, err.Error(), "dotnet publish on project 'p.csproj' failed") }) @@ -526,7 +526,7 @@ func Test_Cli_PublishContainer(t *testing.T) { runner.When(matchDotnetArg0("publish")). Respond(exec.NewRunResult(0, "", "")) - _, err := cli.PublishContainer(t.Context(), "p.csproj", "Release", "img", "r", "u", "p") + _, err := cli.PublishContainer(t.Context(), "p.csproj", "Release", "img", "r", "u", "p", "") require.Error(t, err) require.Contains(t, err.Error(), "failed to get dotnet target port") }) @@ -546,7 +546,7 @@ func Test_Cli_ArtifactsPathContext(t *testing.T) { }) ctx := ContextWithArtifactsPath(t.Context(), "/tmp/artifacts-svc1") - _, err := cli.PublishContainer(ctx, "p.csproj", "Release", "img", "r", "u", "p") + _, err := cli.PublishContainer(ctx, "p.csproj", "Release", "img", "r", "u", "p", "") require.NoError(t, err) joined := strings.Join(captured.Args, " ") require.Contains(t, joined, "--artifacts-path") @@ -564,7 +564,7 @@ func Test_Cli_ArtifactsPathContext(t *testing.T) { }) ctx := ContextWithArtifactsPath(t.Context(), "/tmp/artifacts-svc2") - _, _, err := cli.BuildContainerLocal(ctx, "p.csproj", "Release", "img") + _, _, err := cli.BuildContainerLocal(ctx, "p.csproj", "Release", "img", "") require.NoError(t, err) joined := strings.Join(captured.Args, " ") require.Contains(t, joined, "--artifacts-path") @@ -581,13 +581,89 @@ func Test_Cli_ArtifactsPathContext(t *testing.T) { return exec.NewRunResult(0, successContainerOutput, ""), nil }) - _, err := cli.PublishContainer(t.Context(), "p.csproj", "Release", "img", "r", "u", "p") + _, err := cli.PublishContainer(t.Context(), "p.csproj", "Release", "img", "r", "u", "p", "") require.NoError(t, err) joined := strings.Join(captured.Args, " ") require.NotContains(t, joined, "--artifacts-path") }) } +func Test_Cli_ContainerEngine(t *testing.T) { + t.Parallel() + + t.Run("podman engine appends ContainerEngine property", func(t *testing.T) { + t.Parallel() + cli, runner := newCliWithMock(t) + var captured exec.RunArgs + runner.When(matchDotnetArg0("publish")). + RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + captured = args + return exec.NewRunResult(0, successContainerOutput, ""), nil + }) + + _, _, err := cli.BuildContainerLocal( + t.Context(), "p.csproj", "Release", "img", "podman", + ) + require.NoError(t, err) + joined := strings.Join(captured.Args, " ") + require.Contains(t, joined, "-p:ContainerEngine=podman") + }) + + t.Run("docker engine does not append ContainerEngine property", func(t *testing.T) { + t.Parallel() + cli, runner := newCliWithMock(t) + var captured exec.RunArgs + runner.When(matchDotnetArg0("publish")). + RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + captured = args + return exec.NewRunResult(0, successContainerOutput, ""), nil + }) + + _, _, err := cli.BuildContainerLocal( + t.Context(), "p.csproj", "Release", "img", "docker", + ) + require.NoError(t, err) + joined := strings.Join(captured.Args, " ") + require.NotContains(t, joined, "-p:ContainerEngine=") + }) + + t.Run("empty engine does not append ContainerEngine property", func(t *testing.T) { + t.Parallel() + cli, runner := newCliWithMock(t) + var captured exec.RunArgs + runner.When(matchDotnetArg0("publish")). + RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + captured = args + return exec.NewRunResult(0, successContainerOutput, ""), nil + }) + + _, _, err := cli.BuildContainerLocal( + t.Context(), "p.csproj", "Release", "img", "", + ) + require.NoError(t, err) + joined := strings.Join(captured.Args, " ") + require.NotContains(t, joined, "-p:ContainerEngine=") + }) + + t.Run("podman engine on PublishContainer", func(t *testing.T) { + t.Parallel() + cli, runner := newCliWithMock(t) + var captured exec.RunArgs + runner.When(matchDotnetArg0("publish")). + RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + captured = args + return exec.NewRunResult(0, successContainerOutput, ""), nil + }) + + _, err := cli.PublishContainer( + t.Context(), "p.csproj", "Release", "img", "r", "u", "p", "podman", + ) + require.NoError(t, err) + joined := strings.Join(captured.Args, " ") + require.Contains(t, joined, "-p:ContainerEngine=podman") + }) +} + func Test_Cli_getTargetPort_Branches(t *testing.T) { t.Parallel()