Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
85 changes: 59 additions & 26 deletions cli/azd/internal/cmd/publish.go
Original file line number Diff line number Diff line change
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
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