Skip to content
Open
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
3 changes: 3 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ overrides:
- filename: pkg/project/service_target_dotnet_containerapp.go
words:
- IMAGENAME
- filename: internal/cmd/deploy.go
words:
- gctx
ignorePaths:
- "**/*_test.go"
- "**/mock*.go"
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/extensions/azure.coding-agent/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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 @@ -169,6 +170,7 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
Expand Down
326 changes: 247 additions & 79 deletions cli/azd/internal/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"log"
"os"
"strings"
"sync"
"time"

"github.com/azure/azure-dev/cli/azd/cmd/actions"
Expand All @@ -30,6 +31,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
)

type DeployFlags struct {
Expand Down Expand Up @@ -236,107 +238,75 @@ func (da *DeployAction) Run(ctx context.Context) (*actions.ActionResult, error)
}

deployResults := map[string]*project.ServiceDeployResult{}
var deployResultsMutex sync.Mutex

err = da.projectConfig.Invoke(ctx, project.ProjectEventDeploy, projectEventArgs, func() error {
for _, svc := range stableServices {
stepMessage := fmt.Sprintf("Deploying service %s", svc.Name)
da.console.ShowSpinner(ctx, stepMessage, input.Step)
// Separate services into container apps and non-container apps
var containerAppServices []*project.ServiceConfig
var otherServices []*project.ServiceConfig

for _, svc := range stableServices {
// Skip this service if both cases are true:
// 1. The user specified a service name
// 2. This service is not the one the user specified
if targetServiceName != "" && targetServiceName != svc.Name {
da.console.StopSpinner(ctx, stepMessage, input.StepSkipped)
continue
}

if alphaFeatureId, isAlphaFeature := alpha.IsFeatureKey(string(svc.Host)); isAlphaFeature {
// alpha feature on/off detection for host is done during initialization.
// This is just for displaying the warning during deployment.
da.console.WarnForFeature(ctx, alphaFeatureId)
// Check if this is a container app service
if svc.Host == project.ContainerAppTarget || svc.Host == project.DotNetContainerAppTarget {
containerAppServices = append(containerAppServices, svc)
} else {
otherServices = append(otherServices, svc)
}
}

// Initialize service context for tracking artifacts across operations
serviceContext := project.NewServiceContext()

if da.flags.fromPackage != "" {
// --from-package set, skip packaging and create package artifact
err = serviceContext.Package.Add(&project.Artifact{
Kind: determineArtifactKind(da.flags.fromPackage),
Location: da.flags.fromPackage,
LocationKind: project.LocationKindLocal,
})
// Deploy non-container app services sequentially first
for _, svc := range otherServices {
if err := da.deployService(ctx, svc, targetServiceName, deployResults); err != nil {
return err
}
}

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}
} else {
// --from-package not set, automatically package the application
_, err := async.RunWithProgress(
func(packageProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) {
return da.serviceManager.Package(ctx, svc, serviceContext, progress, nil)
},
)

// do not stop progress here as next step is to publish
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}
// Deploy container app services in parallel
if len(containerAppServices) > 0 {
// Show initial message about parallel deployment
serviceNames := make([]string, len(containerAppServices))
for i, svc := range containerAppServices {
serviceNames[i] = svc.Name
}
da.console.Message(ctx, fmt.Sprintf(
"Deploying %d container app services in parallel: %s",
len(containerAppServices), strings.Join(serviceNames, ", ")))

g, gctx := errgroup.WithContext(ctx)

for _, svc := range containerAppServices {
svc := svc // capture loop variable
g.Go(func() error {
deployResult, err := da.deployServiceWithProgress(gctx, svc, targetServiceName)
if err != nil {
return err
}

_, err := async.RunWithProgress(
func(publishProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Publishing service %s (%s)", svc.Name, publishProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePublishResult, error) {
return da.serviceManager.Publish(ctx, svc, serviceContext, progress, nil)
},
)

// do not stop progress here as next step is to deploy
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
deployResultsMutex.Lock()
deployResults[svc.Name] = deployResult
deployResultsMutex.Unlock()

return nil
})
}

deployResult, err := async.RunWithProgress(
func(deployProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Deploying service %s (%s)", svc.Name, deployProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceDeployResult, error) {
return da.serviceManager.Deploy(ctx, svc, serviceContext, progress)
},
)

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
if err := g.Wait(); err != nil {
return err
}

// clean up for packages automatically created in temp dir
if da.flags.fromPackage == "" {
for _, artifact := range serviceContext.Package {
if strings.HasPrefix(artifact.Location, os.TempDir()) {
if err := os.RemoveAll(artifact.Location); err != nil {
log.Printf("failed to remove temporary package: %s : %s", artifact.Location, err)
}
}
// Report deploy outputs for all parallel deployments after completion
for _, svc := range containerAppServices {
if result, ok := deployResults[svc.Name]; ok {
da.console.MessageUxItem(ctx, result.Artifacts)
}
}

da.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err))
deployResults[svc.Name] = deployResult

// report deploy outputs
da.console.MessageUxItem(ctx, deployResult.Artifacts)
}

return nil
Expand Down Expand Up @@ -405,3 +375,201 @@ func GetCmdDeployHelpFooter(*cobra.Command) string {
),
})
}

// deployService deploys a single service sequentially
func (da *DeployAction) deployService(
ctx context.Context,
svc *project.ServiceConfig,
targetServiceName string,
deployResults map[string]*project.ServiceDeployResult,
) error {
stepMessage := fmt.Sprintf("Deploying service %s", svc.Name)
da.console.ShowSpinner(ctx, stepMessage, input.Step)

if alphaFeatureId, isAlphaFeature := alpha.IsFeatureKey(string(svc.Host)); isAlphaFeature {
// alpha feature on/off detection for host is done during initialization.
// This is just for displaying the warning during deployment.
da.console.WarnForFeature(ctx, alphaFeatureId)
}

// Initialize service context for tracking artifacts across operations
serviceContext := project.NewServiceContext()

if da.flags.fromPackage != "" {
// --from-package set, skip packaging and create package artifact
err := serviceContext.Package.Add(&project.Artifact{
Kind: determineArtifactKind(da.flags.fromPackage),
Location: da.flags.fromPackage,
LocationKind: project.LocationKindLocal,
})

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}
} else {
// --from-package not set, automatically package the application
_, err := async.RunWithProgress(
func(packageProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) {
return da.serviceManager.Package(ctx, svc, serviceContext, progress, nil)
},
)

// do not stop progress here as next step is to publish
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}
}

_, err := async.RunWithProgress(
func(publishProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Publishing service %s (%s)", svc.Name, publishProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePublishResult, error) {
return da.serviceManager.Publish(ctx, svc, serviceContext, progress, nil)
},
)

// do not stop progress here as next step is to deploy
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}

deployResult, err := async.RunWithProgress(
func(deployProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Deploying service %s (%s)", svc.Name, deployProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceDeployResult, error) {
return da.serviceManager.Deploy(ctx, svc, serviceContext, progress)
},
)

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}

// clean up for packages automatically created in temp dir
if da.flags.fromPackage == "" {
for _, artifact := range serviceContext.Package {
if strings.HasPrefix(artifact.Location, os.TempDir()) {
if err := os.RemoveAll(artifact.Location); err != nil {
log.Printf("failed to remove temporary package: %s : %s", artifact.Location, err)
}
}
}
}

da.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err))
deployResults[svc.Name] = deployResult

// report deploy outputs
da.console.MessageUxItem(ctx, deployResult.Artifacts)

return nil
}

// deployServiceWithProgress deploys a single service with progress reporting
// This function shows spinners and progress messages for each deployment phase
func (da *DeployAction) deployServiceWithProgress(
ctx context.Context,
svc *project.ServiceConfig,
targetServiceName string,
) (*project.ServiceDeployResult, error) {
stepMessage := fmt.Sprintf("Deploying service %s", svc.Name)
da.console.ShowSpinner(ctx, stepMessage, input.Step)

if alphaFeatureId, isAlphaFeature := alpha.IsFeatureKey(string(svc.Host)); isAlphaFeature {
// alpha feature on/off detection for host is done during initialization.
// This is just for displaying the warning during deployment.
da.console.WarnForFeature(ctx, alphaFeatureId)
}

// Initialize service context for tracking artifacts across operations
serviceContext := project.NewServiceContext()

if da.flags.fromPackage != "" {
// --from-package set, skip packaging and create package artifact
err := serviceContext.Package.Add(&project.Artifact{
Kind: determineArtifactKind(da.flags.fromPackage),
Location: da.flags.fromPackage,
LocationKind: project.LocationKindLocal,
})

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return nil, fmt.Errorf("failed to add package artifact for service %s: %w", svc.Name, err)
}
} else {
// --from-package not set, automatically package the application
_, err := async.RunWithProgress(
func(packageProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) {
return da.serviceManager.Package(ctx, svc, serviceContext, progress, nil)
},
)

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return nil, fmt.Errorf("failed to package service %s: %w", svc.Name, err)
}
}

// Publish the service
_, err := async.RunWithProgress(
func(publishProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Publishing service %s (%s)", svc.Name, publishProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePublishResult, error) {
return da.serviceManager.Publish(ctx, svc, serviceContext, progress, nil)
},
)

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return nil, fmt.Errorf("failed to publish service %s: %w", svc.Name, err)
}

// Deploy the service
deployResult, err := async.RunWithProgress(
func(deployProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Deploying service %s (%s)", svc.Name, deployProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceDeployResult, error) {
return da.serviceManager.Deploy(ctx, svc, serviceContext, progress)
},
)

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return nil, fmt.Errorf("failed to deploy service %s: %w", svc.Name, err)
}

// clean up for packages automatically created in temp dir
if da.flags.fromPackage == "" {
for _, artifact := range serviceContext.Package {
if strings.HasPrefix(artifact.Location, os.TempDir()) {
if err := os.RemoveAll(artifact.Location); err != nil {
log.Printf("failed to remove temporary package: %s : %s", artifact.Location, err)
}
}
}
}

da.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err))

return deployResult, nil
}
Loading
Loading