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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@ cli/azd/extensions/microsoft.azd.concurx/concurx
cli/azd/extensions/microsoft.azd.concurx/concurx.exe
cli/azd/extensions/azure.appservice/azureappservice
cli/azd/extensions/azure.appservice/azureappservice.exe

.squad/
9 changes: 9 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,15 @@ const completionSpec: Fig.Spec = {
},
],
},
{
name: ['--timeout'],
description: 'Maximum time in seconds to wait for deployment to complete (default: 1200)',
args: [
{
name: 'timeout',
},
],
},
],
args: {
name: 'service',
Expand Down
1 change: 1 addition & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-deploy.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Flags
--all : Deploys all services that are listed in azure.yaml
-e, --environment string : The name of the environment to use.
--from-package string : Deploys the packaged service located at the provided path. Supports zipped file packages (file path) or container images (image tag).
--timeout int : Maximum time in seconds to wait for deployment to complete (default: 1200)

Global Flags
-C, --cwd string : Sets the current working directory.
Expand Down
123 changes: 114 additions & 9 deletions cli/azd/internal/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"log"
"os"
"strconv"
"strings"
"time"

Expand All @@ -35,11 +36,15 @@ import (
type DeployFlags struct {
ServiceName string
All bool
Timeout int
fromPackage string
flagSet *pflag.FlagSet
global *internal.GlobalCommandOptions
*internal.EnvFlag
}

const defaultDeployTimeoutSeconds = 1200

func (d *DeployFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
d.BindNonCommon(local, global)
d.bindCommon(local, global)
Expand All @@ -63,6 +68,7 @@ func (d *DeployFlags) BindNonCommon(
func (d *DeployFlags) bindCommon(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
d.EnvFlag = &internal.EnvFlag{}
d.EnvFlag.Bind(local, global)
d.flagSet = local

local.BoolVar(
&d.All,
Expand All @@ -77,6 +83,12 @@ func (d *DeployFlags) bindCommon(local *pflag.FlagSet, global *internal.GlobalCo
//nolint:lll
"Deploys the packaged service located at the provided path. Supports zipped file packages (file path) or container images (image tag).",
)
local.IntVar(
&d.Timeout,
"timeout",
defaultDeployTimeoutSeconds,
fmt.Sprintf("Maximum time in seconds to wait for deployment to complete (default: %d)", defaultDeployTimeoutSeconds),
)
}

func (d *DeployFlags) SetCommon(envFlag *internal.EnvFlag) {
Expand All @@ -92,11 +104,21 @@ func NewDeployFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *

func NewDeployFlagsFromEnvAndOptions(envFlag *internal.EnvFlag, global *internal.GlobalCommandOptions) *DeployFlags {
return &DeployFlags{
Timeout: defaultDeployTimeoutSeconds,
EnvFlag: envFlag,
global: global,
}
}

func (d *DeployFlags) timeoutChanged() bool {
if d.flagSet == nil {
return false
}

timeoutFlag := d.flagSet.Lookup("timeout")
return timeoutFlag != nil && timeoutFlag.Changed
}

func NewDeployCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "deploy <service>",
Expand Down Expand Up @@ -239,6 +261,10 @@ func (da *DeployAction) Run(ctx context.Context) (*actions.ActionResult, error)
}

deployResults := map[string]*project.ServiceDeployResult{}
deployTimeouts, err := da.resolveDeployTimeouts(stableServices)
if err != nil {
return nil, err
}

err = da.projectConfig.Invoke(ctx, project.ProjectEventDeploy, projectEventArgs, func() error {
for _, svc := range stableServices {
Expand Down Expand Up @@ -301,18 +327,48 @@ func (da *DeployAction) Run(ctx context.Context) (*actions.ActionResult, error)
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)
},
)
deployTimeout := deployTimeouts[svc.Name]
var deployResult *project.ServiceDeployResult
var deployCtx context.Context
func() {
var deployCancel context.CancelFunc
deployCtx, deployCancel = context.WithTimeout(ctx, deployTimeout)
defer deployCancel()

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(deployCtx, svc, serviceContext, progress)
},
)
}()

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
if deployCtx.Err() == context.DeadlineExceeded {
warnMsg := fmt.Sprintf(
"Deployment of service '%s' exceeded the azd wait timeout."+
" azd has stopped waiting, but the deployment may"+
" still be running in Azure.",
svc.Name,
)
da.console.MessageUxItem(ctx, &ux.WarningMessage{
Description: warnMsg,
Hints: []string{"Check the Azure Portal for current deployment status."},
})

return fmt.Errorf(
"deployment of service '%s' timed out after %d seconds. To increase, use --timeout, "+
"AZD_DEPLOY_TIMEOUT env var, or deployTimeout in azure.yaml. Note: azd has stopped "+
"waiting, but the deployment may still be running in Azure. Check the Azure Portal for "+
"current deployment status.",
svc.Name,
int(deployTimeout.Seconds()),
)
}
return err
}

Expand Down Expand Up @@ -377,6 +433,55 @@ func (da *DeployAction) Run(ctx context.Context) (*actions.ActionResult, error)
}, nil
}

func (da *DeployAction) resolveDeployTimeouts(services []*project.ServiceConfig) (map[string]time.Duration, error) {
deployTimeouts := make(map[string]time.Duration, len(services))

for _, serviceConfig := range services {
timeout, err := da.resolveDeployTimeout(serviceConfig)
if err != nil {
return nil, err
}

deployTimeouts[serviceConfig.Name] = timeout
}

return deployTimeouts, nil
}

func (da *DeployAction) resolveDeployTimeout(serviceConfig *project.ServiceConfig) (time.Duration, error) {
if da.flags.timeoutChanged() {
if da.flags.Timeout <= 0 {
return 0, errors.New("invalid value for --timeout: must be greater than 0 seconds")
}

return time.Duration(da.flags.Timeout) * time.Second, nil
}

if envVal, ok := os.LookupEnv("AZD_DEPLOY_TIMEOUT"); ok {
seconds, err := strconv.Atoi(envVal)
if err != nil {
return 0, fmt.Errorf("invalid AZD_DEPLOY_TIMEOUT value '%s': must be an integer number of seconds", envVal)
}
if seconds <= 0 {
return 0, fmt.Errorf("invalid AZD_DEPLOY_TIMEOUT value '%d': must be greater than 0 seconds", seconds)
}
return time.Duration(seconds) * time.Second, nil
}

if serviceConfig.DeployTimeout != nil {
if *serviceConfig.DeployTimeout <= 0 {
return 0, fmt.Errorf(
"invalid deployTimeout for service '%s': must be greater than 0 seconds",
serviceConfig.Name,
)
}

return time.Duration(*serviceConfig.DeployTimeout) * time.Second, nil
}

return time.Duration(defaultDeployTimeoutSeconds) * time.Second, nil
}

func GetCmdDeployHelpDescription(*cobra.Command) string {
return generateCmdHelpDescription("Deploy application to Azure.", []string{
formatHelpNote(
Expand Down
Loading
Loading