Skip to content
Open
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: 1 addition & 1 deletion cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6366,7 +6366,7 @@ const completionSpec: Fig.Spec = {
},
{
name: ['publish'],
description: 'Publish a service to a container registry.',
description: 'Publish a service image or reuse an existing passthrough image.',
options: [
{
name: ['--all'],
Expand Down
5 changes: 3 additions & 2 deletions cli/azd/cmd/testdata/TestUsage-azd-publish.snap
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@

Publish a service to a container registry.
Publish a service image or reuse an existing passthrough image.

• Supports Container App services only.
• Supports Container Apps, AKS, and extension-provided service targets.
• Target registry set by AZURE_CONTAINER_REGISTRY_ENDPOINT environment variable, docker.registry in azure.yaml, or '--to' flag.
• Use '--from-package' to publish an existing container image, otherwise azd automatically packages the container image before publishing.
• For services with docker.imagePassthrough enabled, azd reuses the configured remote image without publishing it; '--from-package' and '--to' are not supported.

Usage
azd publish <service> [flags]
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/cmd/testdata/TestUsage-azd.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Commands
deploy : Deploy your project code to Azure.
down : Delete your project's Azure resources.
provision : Provision Azure resources for your project.
publish : Publish a service to a container registry.
publish : Publish a service image or reuse an existing passthrough image.

Manage and show settings
completion : Generate shell completion scripts.
Expand Down
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
95 changes: 66 additions & 29 deletions cli/azd/internal/cmd/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func NewPublishFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions)
func NewPublishCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "publish <service>",
Short: "Publish a service to a container registry.",
Short: "Publish a service image or reuse an existing passthrough image.",
}
cmd.Args = cobra.MaximumNArgs(1)
return cmd
Expand Down Expand Up @@ -209,6 +209,23 @@ func (pa *PublishAction) Run(ctx context.Context) (*actions.ActionResult, error)
}
}

// Create publish options from flags
publishOptions := &project.PublishOptions{
Image: pa.flags.To,
}

if err := pa.projectManager.Initialize(ctx, pa.projectConfig); err != nil {
return nil, err
}

stableServices, err := pa.importManager.ServiceStableFiltered(ctx, pa.projectConfig, targetServiceName, pa.env.Getenv)
if err != nil {
return nil, err
}
if err := validateImagePassthroughPublishFlags(stableServices, pa.flags); err != nil {
return nil, err
}

if pa.flags.FromPackage != "" {
if parsedImage, err := docker.ParseContainerImage(pa.flags.FromPackage); err == nil && parsedImage.Registry != "" {
return nil, &internal.ErrorWithSuggestion{
Expand All @@ -220,15 +237,6 @@ func (pa *PublishAction) Run(ctx context.Context) (*actions.ActionResult, error)
}
}

// Create publish options from flags
publishOptions := &project.PublishOptions{
Image: pa.flags.To,
}

if err := pa.projectManager.Initialize(ctx, pa.projectConfig); err != nil {
return nil, err
}

if err := pa.projectManager.EnsureServiceTargetTools(ctx, pa.projectConfig, func(svc *project.ServiceConfig) bool {
return targetServiceName == "" || svc.Name == targetServiceName
}); err != nil {
Expand All @@ -242,16 +250,12 @@ func (pa *PublishAction) Run(ctx context.Context) (*actions.ActionResult, error)

startTime := time.Now()

stableServices, err := pa.importManager.ServiceStableFiltered(ctx, pa.projectConfig, targetServiceName, pa.env.Getenv)
if err != nil {
return nil, err
}

projectEventArgs := project.ProjectLifecycleEventArgs{
Project: pa.projectConfig,
}

publishResults := map[string]*project.ServicePublishResult{}
passthroughServiceCount := 0

err = pa.projectConfig.Invoke(ctx, project.ProjectEventPublish, projectEventArgs, func() error {
for _, svc := range stableServices {
Expand Down Expand Up @@ -299,7 +303,7 @@ func (pa *PublishAction) Run(ctx context.Context) (*actions.ActionResult, error)
}
} else {
// --from-package not set, automatically package the application
packageResult, err := async.RunWithProgress(
_, err := async.RunWithProgress(
func(packageProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message)
pa.console.ShowSpinner(ctx, progressMessage, input.Step)
Expand All @@ -313,12 +317,6 @@ func (pa *PublishAction) Run(ctx context.Context) (*actions.ActionResult, error)
pa.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}

// Append package artifacts
if err := serviceContext.Package.Add(packageResult.Artifacts...); err != nil {
pa.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}
}

publishResult, err := async.RunWithProgress(
Expand Down Expand Up @@ -352,7 +350,13 @@ func (pa *PublishAction) Run(ctx context.Context) (*actions.ActionResult, error)
}
}

pa.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err))
if svc.Docker.ImagePassthrough {
passthroughServiceCount++
stepMessage = fmt.Sprintf("Publishing service %s (using existing remote image)", svc.Name)
pa.console.StopSpinner(ctx, stepMessage, input.StepSkipped)
} else {
pa.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err))
}

publishResults[svc.Name] = publishResult
pa.console.MessageUxItem(ctx, publishResult.Artifacts)
Expand All @@ -376,14 +380,43 @@ func (pa *PublishAction) Run(ctx context.Context) (*actions.ActionResult, error)
}
}

resultHeader := fmt.Sprintf("Your application was published in %s.", ux.DurationAsText(since(startTime)))
if passthroughServiceCount == len(publishResults) && passthroughServiceCount > 0 {
resultHeader = "No images were published. Existing remote images are configured for deployment."
} else if passthroughServiceCount > 0 {
resultHeader += " Existing remote images were used for image passthrough services."
}

return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: fmt.Sprintf("Your application was published in %s.",
ux.DurationAsText(since(startTime))),
},
Message: &actions.ResultMessage{Header: resultHeader},
}, nil
}

func validateImagePassthroughPublishFlags(services []*project.ServiceConfig, flags *PublishFlags) error {
for _, svc := range services {
if !svc.Docker.ImagePassthrough {
continue
}
if flags.FromPackage != "" {
return fmt.Errorf(
"--from-package is not supported by azd publish for image passthrough service %q; "+
"use azd deploy %s --from-package <remote-image> to override its image",
svc.Name,
svc.Name,
)
}
if flags.To != "" {
return fmt.Errorf(
"--to is not supported by azd publish for image passthrough service %q; "+
"disable docker.imagePassthrough to publish the image",
svc.Name,
)
}
}

return nil
}

// supportsPublish checks if the service host supports publishing.
func (pa *PublishAction) supportsPublish(ctx context.Context, serviceConfig *project.ServiceConfig) bool {
// Built-in container targets support publish
Expand Down Expand Up @@ -432,9 +465,9 @@ func determineArtifactKind(fromPackage string) project.ArtifactKind {

func GetCmdPublishHelpDescription(*cobra.Command) string {
return generateCmdHelpDescription(
"Publish a service to a container registry.",
"Publish a service image or reuse an existing passthrough image.",
[]string{
formatHelpNote("Supports Container App services only."),
formatHelpNote("Supports Container Apps, AKS, and extension-provided service targets."),
formatHelpNote(
//nolint:lll
"Target registry set by AZURE_CONTAINER_REGISTRY_ENDPOINT environment variable, docker.registry in azure.yaml, or '--to' flag.",
Expand All @@ -443,6 +476,10 @@ func GetCmdPublishHelpDescription(*cobra.Command) string {
//nolint:lll
"Use '--from-package' to publish an existing container image, otherwise azd automatically packages the container image before publishing.",
),
formatHelpNote(
//nolint:lll
"For services with docker.imagePassthrough enabled, azd reuses the configured remote image without publishing it; '--from-package' and '--to' are not supported.",
),
})
}

Expand Down
62 changes: 62 additions & 0 deletions cli/azd/internal/cmd/publish_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/stretchr/testify/require"
)

func TestValidateImagePassthroughPublishFlags(t *testing.T) {
passthroughService := &project.ServiceConfig{
Name: "api",
Docker: project.DockerProjectOptions{
ImagePassthrough: true,
},
}
normalService := &project.ServiceConfig{Name: "web"}

tests := []struct {
name string
services []*project.ServiceConfig
flags *PublishFlags
errorContains string
}{
{
name: "passthrough without overrides",
services: []*project.ServiceConfig{passthroughService},
flags: &PublishFlags{},
},
{
name: "normal service with from package",
services: []*project.ServiceConfig{normalService},
flags: &PublishFlags{FromPackage: "api:v1"},
},
{
name: "passthrough with from package",
services: []*project.ServiceConfig{passthroughService},
flags: &PublishFlags{FromPackage: "registry.example.com/team/api:v2"},
errorContains: "--from-package is not supported by azd publish",
},
{
name: "passthrough with destination override",
services: []*project.ServiceConfig{passthroughService},
flags: &PublishFlags{To: "registry.example.com/team/api:v2"},
errorContains: "--to is not supported by azd publish",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateImagePassthroughPublishFlags(tt.services, tt.flags)
if tt.errorContains == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tt.errorContains)
}
})
}
}
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.

3 changes: 3 additions & 0 deletions cli/azd/pkg/project/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const (

// MetadataKeyNote adds a note line below the artifact output.
MetadataKeyNote = "note"

// MetadataKeyImagePassthrough identifies a remote container image that azd reused without publishing.
MetadataKeyImagePassthrough = "imagePassthrough"
)

// ArtifactKind represents well-known artifact types in the Azure Developer CLI
Expand Down
Loading
Loading