Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/azd/grpc/proto/models.proto
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ message DockerProjectOptions {
string tag = 7;
bool remote_build = 8;
repeated string build_args = 9;
reserved 10;
reserved "local_fallback";
Comment thread
JeffreyCA marked this conversation as resolved.
}

// ServiceContext defines the shared pipeline state across all phases of the service lifecycle
Expand Down
17 changes: 17 additions & 0 deletions cli/azd/pkg/project/container_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/pkg/tools/docker"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
Expand Down Expand Up @@ -599,6 +600,22 @@ func (ch *ContainerHelper) Publish(

if serviceConfig.Docker.RemoteBuild {
remoteImage, err = ch.runRemoteBuild(ctx, serviceConfig, targetResource, env, progress, imageOverride)
if err != nil {
Comment thread
spboyer marked this conversation as resolved.
// Check if a local container runtime (Docker/Podman) is available before falling back
if dockerErr := ch.docker.CheckInstalled(ctx); dockerErr != nil {
return nil, fmt.Errorf(
"remote build failed: %w\n\nLocal fallback unavailable: %w",
err, dockerErr)
}

ch.console.MessageUxItem(ctx, &ux.WarningMessage{
Description: fmt.Sprintf(
"Remote build failed: %s\nFalling back to local Docker build.", err),
HidePrefix: false,
})
remoteImage, err = ch.publishLocalImage(
ctx, serviceConfig, serviceContext, env, progress, imageOverride)
}
} else if useDotnetPublishForDockerBuild(serviceConfig) {
remoteImage, err = ch.runDotnetPublish(ctx, serviceConfig, targetResource, env, progress)
} else {
Expand Down
96 changes: 96 additions & 0 deletions cli/azd/pkg/project/container_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1271,3 +1271,99 @@ func Test_ContainerHelper_Publish(t *testing.T) {
})
}
}

func Test_ContainerHelper_Publish_RemoteBuildLocalFallback(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
mockResults := setupDockerMocks(mockContext)
env := environment.NewWithValues("dev", map[string]string{})
dockerCli := docker.NewCli(mockContext.CommandRunner)
dotnetCli := dotnet.NewCli(mockContext.CommandRunner)

// Mock Docker availability checks for local fallback
mockContext.CommandRunner.MockToolInPath("docker", nil)

mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker --version")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
Stdout: "Docker version 20.10.17, build 100c701",
ExitCode: 0,
}, nil
})

mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, "docker ps")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
Stdout: "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES",
ExitCode: 0,
}, nil
})

mockContainerRegistryService := &mockContainerRegistryService{}
setupContainerRegistryMocks(mockContext, &mockContainerRegistryService.Mock)

containerHelper := NewContainerHelper(
clock.NewMock(),
mockContainerRegistryService,
nil,
mockContext.CommandRunner,
dockerCli,
dotnetCli,
mockContext.Console,
cloud.AzurePublic(),
)

serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript)
serviceConfig.Docker.Registry = osutil.NewExpandableString("contoso.azurecr.io")
serviceConfig.Docker.RemoteBuild = true
serviceConfig.Docker.Platform = "linux/arm64"

dockerArtifact := &Artifact{
Kind: ArtifactKindContainer,
Location: "my-project/my-service:azd-deploy-0",
LocationKind: LocationKindLocal,
Metadata: map[string]string{
"imageHash": "IMAGE_ID",
"sourceImage": "",
"targetImage": "my-project/my-service:azd-deploy-0",
},
}

serviceContext := &ServiceContext{
Package: ArtifactCollection{dockerArtifact},
}

targetResource := environment.NewTargetResource(
"SUBSCRIPTION_ID",
"RESOURCE_GROUP",
"CONTAINER_APP",
"Microsoft.App/containerApps",
)

publishResult, err := logProgress(
t, func(progress *async.Progress[ServiceProgress]) (*ServicePublishResult, error) {
return containerHelper.Publish(
*mockContext.Context, serviceConfig, serviceContext, targetResource, env, progress, &PublishOptions{})
},
)

require.NoError(t, err)
require.Len(t, publishResult.Artifacts, 1)
expectedImage := "contoso.azurecr.io/my-project/my-service:azd-deploy-0"
require.Equal(t, expectedImage,
publishResult.Artifacts[0].Metadata["remoteImage"])

_, dockerPushCalled := mockResults["docker-push"]
require.True(t, dockerPushCalled)

warningFound := false
for _, line := range mockContext.Console.Output() {
if strings.Contains(line, "Remote build failed:") &&
strings.Contains(line, "Falling back to local Docker build.") {
warningFound = true
break
}
}
require.True(t, warningFound)
}
2 changes: 1 addition & 1 deletion cli/azd/pkg/project/framework_service_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type DockerProjectOptions struct {
Registry osutil.ExpandableString `yaml:"registry,omitempty" json:"registry"`
Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"`
Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"`
RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"`
RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"`
BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"`
// not supported from azure.yaml directly yet. Adding it for Aspire to use it, initially.
// Aspire would pass the secret keys, which are env vars that azd will set just to run docker build.
Expand Down
2 changes: 1 addition & 1 deletion schemas/v1.0/azure.yaml.json
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,7 @@
"remoteBuild": {
"type": "boolean",
"title": "Optional. Whether to build the image remotely",
"description": "If set to true, the image will be built remotely using the Azure Container Registry remote build feature. If set to false, the image will be built locally using Docker."
"description": "If set to true, the image will be built remotely using the Azure Container Registry remote build feature. If the remote build fails, azd automatically falls back to building locally using Docker or Podman if available. If set to false, the image will be built locally."
Comment thread
spboyer marked this conversation as resolved.
}
}
},
Expand Down
Loading