Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/charmbracelet/glamour v0.10.0
github.com/cli/browser v1.3.0
github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817
github.com/distribution/reference v0.6.0
github.com/drone/envsubst v1.0.3
github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0
Expand Down Expand Up @@ -127,6 +128,7 @@ require (
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions cli/azd/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 h1:0nsrg//Dc7xC74H/TZ5sYR8uk4UQRNjsw8zejqH5a4Q=
github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817/go.mod h1:C/+sI4IFnEpCn6VQ3GIPEp+FrQnQw+YQP3+n+GdGq7o=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
Expand Down Expand Up @@ -248,6 +250,8 @@ github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d/g
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
Expand Down
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.

98 changes: 93 additions & 5 deletions cli/azd/pkg/project/container_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
"github.com/azure/azure-dev/cli/azd/pkg/tools/pack"
"github.com/benbjohnson/clock"
"github.com/distribution/reference"
"github.com/sethvargo/go-retry"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
Expand Down Expand Up @@ -255,8 +256,48 @@ 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")
}

if _, err := reference.Parse(image); err != nil {
return "", fmt.Errorf("parsing passthrough image: %w", err)
}

// Passthrough preserves the expanded reference exactly, including tag and digest combinations.
return image, nil
}

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 +395,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 +589,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 @@ -612,6 +668,27 @@ func (ch *ContainerHelper) Package(
}, nil
}

func validatePublishOptions(serviceConfig *ServiceConfig, options *PublishOptions) error {
if serviceConfig.Docker.ImagePassthrough && options != nil && options.Image != "" {
return fmt.Errorf("docker.imagePassthrough cannot be combined with a publish image override")
}

return nil
}

func validateImagePassthroughPackage(serviceConfig *ServiceConfig, serviceContext *ServiceContext) error {
if !serviceConfig.Docker.ImagePassthrough || serviceContext == nil {
return nil
}

artifact, found := serviceContext.Package.FindFirst(WithKind(ArtifactKindContainer))
Comment thread
m5i-work marked this conversation as resolved.
Outdated
if found && artifact.Location != "" && artifact.Metadata["imagePassthrough"] != "true" {
return fmt.Errorf("docker.imagePassthrough cannot be combined with a package image override")
}

return nil
}

// Publish pushes an image to a remote server and returns the fully qualified remote image name.
func (ch *ContainerHelper) Publish(
ctx context.Context,
Expand All @@ -630,13 +707,22 @@ func (ch *ContainerHelper) Publish(

var remoteImage string

if err := validatePublishOptions(serviceConfig, options); err != nil {
return nil, err
}
if err := validateImagePassthroughPackage(serviceConfig, serviceContext); err != nil {
return nil, err
}

// Parse PublishOptions into ImageOverride
imageOverride, err := parseImageOverride(options)
if err != nil {
return nil, err
}

if serviceConfig.Docker.RemoteBuild {
if serviceConfig.Docker.ImagePassthrough {
Comment thread
m5i-work marked this conversation as resolved.
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 +750,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
Loading
Loading