Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions cli/azd/grpc/proto/models.proto
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ message DockerProjectOptions {
bool remote_build = 8;
repeated string build_args = 9;
string network = 10;
bool image_passthrough = 11;
}

// ServiceContext defines the shared pipeline state across all phases of the service lifecycle
Expand Down
39 changes: 24 additions & 15 deletions cli/azd/pkg/azdext/models.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 66 additions & 5 deletions cli/azd/pkg/project/container_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,47 @@ func (ch *ContainerHelper) LocalImageTag(
return configuredImage.Local(), nil
}

func (ch *ContainerHelper) RequiredExternalTools(ctx context.Context, serviceConfig *ServiceConfig) []tools.ExternalTool {
func resolveImagePassthrough(
serviceConfig *ServiceConfig,
env *environment.Environment,
) (string, error) {
if !serviceConfig.Docker.ImagePassthrough {
return "", nil
}
if serviceConfig.Docker.RemoteBuild {
return "", fmt.Errorf("docker.imagePassthrough cannot be combined with docker.remoteBuild")
}

image, err := serviceConfig.Image.Envsubst(env.Getenv)
if err != nil {
return "", fmt.Errorf("substituting environment variables in passthrough image: %w", err)
}
if strings.TrimSpace(image) == "" {
return "", fmt.Errorf("docker.imagePassthrough requires the service image property")
}

parsed, err := docker.ParseContainerImage(image)
if err != nil {
return "", fmt.Errorf("parsing passthrough image: %w", err)
}
return parsed.Remote(), nil
Comment thread
m5i-work marked this conversation as resolved.
Outdated
}

func imagePassthroughArtifact(image string) *Artifact {
return &Artifact{
Kind: ArtifactKindContainer,
Location: image,
LocationKind: LocationKindRemote,
Metadata: map[string]string{
"imagePassthrough": "true",
"remoteImage": image,
"sourceImage": image,
},
}
}

func (ch *ContainerHelper) RequiredExternalTools(ctx context.Context, serviceConfig *ServiceConfig) []tools.ExternalTool {
if serviceConfig.Docker.ImagePassthrough || serviceConfig.Docker.RemoteBuild {
return []tools.ExternalTool{}
}

Expand Down Expand Up @@ -354,6 +393,12 @@ func (ch *ContainerHelper) Build(
env *environment.Environment,
progress *async.Progress[ServiceProgress],
) (*ServiceBuildResult, error) {
if serviceConfig.Docker.ImagePassthrough {
if _, err := resolveImagePassthrough(serviceConfig, env); err != nil {
return nil, err
}
return &ServiceBuildResult{}, nil
}
if serviceConfig.Docker.RemoteBuild || useDotnetPublishForDockerBuild(serviceConfig) {
return &ServiceBuildResult{}, nil
}
Expand Down Expand Up @@ -542,6 +587,15 @@ func (ch *ContainerHelper) Package(
env *environment.Environment,
progress *async.Progress[ServiceProgress],
) (*ServicePackageResult, error) {
if serviceConfig.Docker.ImagePassthrough {
image, err := resolveImagePassthrough(serviceConfig, env)
if err != nil {
return nil, err
}
return &ServicePackageResult{
Artifacts: ArtifactCollection{imagePassthroughArtifact(image)},
}, nil
}
if serviceConfig.Docker.RemoteBuild || useDotnetPublishForDockerBuild(serviceConfig) {
return &ServicePackageResult{}, nil
}
Expand Down Expand Up @@ -636,7 +690,12 @@ func (ch *ContainerHelper) Publish(
return nil, err
}

if serviceConfig.Docker.RemoteBuild {
if serviceConfig.Docker.ImagePassthrough {
Comment thread
m5i-work marked this conversation as resolved.
if imageOverride != nil {
return nil, fmt.Errorf("docker.imagePassthrough cannot be combined with a publish image override")
Comment thread
m5i-work marked this conversation as resolved.
Outdated
}
remoteImage, err = resolveImagePassthrough(serviceConfig, env)
} else if serviceConfig.Docker.RemoteBuild {
remoteImage, err = ch.runRemoteBuild(ctx, serviceConfig, targetResource, env, progress, imageOverride)
if err != nil {
// Check if a local container runtime (Docker/Podman) is available before falling back
Expand Down Expand Up @@ -664,13 +723,15 @@ func (ch *ContainerHelper) Publish(
}

// Create publish artifact with remote image reference
metadata := map[string]string{"remoteImage": remoteImage}
if serviceConfig.Docker.ImagePassthrough {
metadata["imagePassthrough"] = "true"
}
publishArtifact := &Artifact{
Kind: ArtifactKindContainer,
Location: remoteImage,
LocationKind: LocationKindRemote, // Remote after publish
Metadata: map[string]string{
"remoteImage": remoteImage,
},
Metadata: metadata,
}

return &ServicePublishResult{
Expand Down
90 changes: 90 additions & 0 deletions cli/azd/pkg/project/container_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,65 @@ func (m *mockContainerRegistryService) FindContainerRegistryResourceGroup(
args := m.Called(ctx, subscriptionId, registryName)
return args.String(0), args.Error(1)
}
func TestResolveImagePassthrough(t *testing.T) {
t.Parallel()

tests := []struct {
name string
image string
docker DockerProjectOptions
env map[string]string
want string
wantErr bool
errContains string
}{
{name: "disabled"},
{
name: "expands service image",
image: "${PRIVATE_REGISTRY}/team/agent:v1",
docker: DockerProjectOptions{ImagePassthrough: true},
env: map[string]string{"PRIVATE_REGISTRY": "private.example.com"},
want: "private.example.com/team/agent:v1",
},
{
name: "requires service image",
docker: DockerProjectOptions{ImagePassthrough: true},
wantErr: true,
errContains: "requires the service image property",
},
{
name: "conflicts with remote build",
image: "private.example.com/team/agent:v1",
docker: DockerProjectOptions{
ImagePassthrough: true,
RemoteBuild: true,
},
wantErr: true,
errContains: "cannot be combined",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
serviceConfig := &ServiceConfig{
Image: osutil.NewExpandableString(tt.image),
Docker: tt.docker,
}
got, err := resolveImagePassthrough(
serviceConfig,
environment.NewWithValues("test", tt.env),
)
if tt.wantErr {
require.ErrorContains(t, err, tt.errContains)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, got)
})
}
}

func Test_ContainerHelper_Publish(t *testing.T) {
tests := []struct {
name string
Expand All @@ -1292,6 +1351,7 @@ func Test_ContainerHelper_Publish(t *testing.T) {
targetImage string
publishOptions *PublishOptions
expectedRemoteImage string
imagePassthrough bool
expectDockerLoginCalled bool
expectDockerPullCalled bool
expectDockerTagCalled bool
Expand Down Expand Up @@ -1359,6 +1419,30 @@ func Test_ContainerHelper_Publish(t *testing.T) {
expectedRemoteImage: "nginx",
expectError: false,
},
{
name: "Image passthrough with configured destination registry",
image: "private.example.com/team/agent:v1",
registry: osutil.NewExpandableString("contoso.azurecr.io"),
imagePassthrough: true,
publishOptions: &PublishOptions{},
expectDockerLoginCalled: false,
expectDockerPullCalled: false,
expectDockerTagCalled: false,
expectDockerPushCalled: false,
expectedRemoteImage: "private.example.com/team/agent:v1",
expectError: false,
},
{
name: "Image passthrough rejects publish image override",
image: "private.example.com/team/agent:v1",
imagePassthrough: true,
publishOptions: &PublishOptions{Image: "other.example.com/team/agent:v2"},
expectDockerLoginCalled: false,
expectDockerPullCalled: false,
expectDockerTagCalled: false,
expectDockerPushCalled: false,
expectError: true,
},
{
name: "With publish options overwrite",
project: "./src/api",
Expand Down Expand Up @@ -1460,6 +1544,7 @@ func Test_ContainerHelper_Publish(t *testing.T) {
serviceConfig.Image = osutil.NewExpandableString(tt.image)
serviceConfig.RelativePath = tt.project
serviceConfig.Docker.Registry = tt.registry
serviceConfig.Docker.ImagePassthrough = tt.imagePassthrough

packageOutput := &ServicePackageResult{
Artifacts: ArtifactCollection{
Expand Down Expand Up @@ -1522,7 +1607,12 @@ func Test_ContainerHelper_Publish(t *testing.T) {
require.Len(t, publishResult.Artifacts, 1)
artifact := publishResult.Artifacts[0]
require.Equal(t, ArtifactKindContainer, artifact.Kind)
require.Equal(t, LocationKindRemote, artifact.LocationKind)
require.Equal(t, tt.expectedRemoteImage, artifact.Location)
require.Equal(t, tt.expectedRemoteImage, artifact.Metadata["remoteImage"])
if tt.imagePassthrough {
require.Equal(t, "true", artifact.Metadata["imagePassthrough"])
}
}
})
}
Expand Down
21 changes: 11 additions & 10 deletions cli/azd/pkg/project/framework_service_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ import (
)

type DockerProjectOptions struct {
Path string `yaml:"path,omitempty" json:"path,omitempty"`
Context string `yaml:"context,omitempty" json:"context,omitempty"`
Platform string `yaml:"platform,omitempty" json:"platform,omitempty"`
Target string `yaml:"target,omitempty" json:"target,omitempty"`
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"`
Network string `yaml:"network,omitempty" json:"network,omitempty"`
BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"`
Path string `yaml:"path,omitempty" json:"path,omitempty"`
Context string `yaml:"context,omitempty" json:"context,omitempty"`
Platform string `yaml:"platform,omitempty" json:"platform,omitempty"`
Target string `yaml:"target,omitempty" json:"target,omitempty"`
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"`
ImagePassthrough bool `yaml:"imagePassthrough,omitempty" json:"imagePassthrough,omitempty"`
Network string `yaml:"network,omitempty" json:"network,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.
BuildSecrets []string `yaml:"-" json:"-"`
Expand Down
Loading
Loading