From b1c201922f66ac5c2f99b05b4ac8aa866a754a5b Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 12 Jul 2022 09:37:39 -0700 Subject: [PATCH 1/7] Query for deployment token before deployment --- .../project/service_target_staticwebapp.go | 9 ++++---- cli/azd/pkg/tools/azcli.go | 21 +++++++++++++++++++ cli/azd/pkg/tools/swa.go | 7 ++++--- cli/azd/test/samples/funcapp/requirements.txt | 4 ++-- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/cli/azd/pkg/project/service_target_staticwebapp.go b/cli/azd/pkg/project/service_target_staticwebapp.go index e226dcbde54..584d5ba8fae 100644 --- a/cli/azd/pkg/project/service_target_staticwebapp.go +++ b/cli/azd/pkg/project/service_target_staticwebapp.go @@ -39,15 +39,16 @@ func (at *staticWebAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az log.Printf("Logging into SWA CLI: TenantId: %s, SubscriptionId: %s, ResourceGroup: %s, ResourceName: %s", at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()) // Login to get the app deployment token - progress <- "Generating deployment tokens" - if err := at.swa.Login(ctx, at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()); err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("Failed deploying static web app: %w", err) + progress <- "Retrieving deployment tokens" + deploymentToken, err := at.cli.GetStaticWebAppApiKey(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()) + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("Failed retrieving static web app deployment token: %w", err) } // SWA performs a zip & deploy of the specified output folder and publishes it to the configured environment log.Printf("Deploying SWA app: TenantId: %s, SubscriptionId: %s, ResourceGroup: %s, ResourceName: %s", at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()) progress <- "Publishing deployment artifacts" - res, err := at.swa.Deploy(ctx, at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), at.config.RelativePath, at.config.OutputPath, staticWebAppEnvironmentName) + res, err := at.swa.Deploy(ctx, at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), at.config.RelativePath, at.config.OutputPath, staticWebAppEnvironmentName, deploymentToken) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("Failed deploying static web app: %w", err) } diff --git a/cli/azd/pkg/tools/azcli.go b/cli/azd/pkg/tools/azcli.go index ea3a0778f89..7101d70d82a 100644 --- a/cli/azd/pkg/tools/azcli.go +++ b/cli/azd/pkg/tools/azcli.go @@ -77,6 +77,7 @@ type AzCli interface { GetAppServiceProperties(ctx context.Context, subscriptionId string, resourceGroupName string, applicationName string) (AzCliAppServiceProperties, error) GetContainerAppProperties(ctx context.Context, subscriptionId string, resourceGroupName string, applicationName string) (AzCliContainerAppProperties, error) GetStaticWebAppProperties(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (AzCliStaticWebAppProperties, error) + GetStaticWebAppApiKey(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (string, error) GetSignedInUserId(ctx context.Context) (string, error) @@ -538,6 +539,26 @@ func (cli *azCli) GetStaticWebAppProperties(ctx context.Context, subscriptionID return staticWebAppProperties, nil } +func (cli *azCli) GetStaticWebAppApiKey(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (string, error) { + res, err := cli.runAzCommandWithArgs(context.Background(), executil.RunArgs{ + Args: []string{ + "staticwebapp", "secrets", "list", + "--subscription", subscriptionID, + "--resource-group", resourceGroup, + "--name", appName, + "--query", "properties.apiKey", + "--output", "tsv", + }, + EnrichError: true, + }) + + if err != nil { + return "", fmt.Errorf("failed getting staticwebapp api key: %w", err) + } + + return res.Stdout, nil +} + func (cli *azCli) DeployToSubscription(ctx context.Context, subscriptionId string, deploymentName string, templateFile string, parametersFile string, location string) (AzCliDeploymentResult, error) { res, err := cli.runAzCommand(ctx, "deployment", "sub", "create", "--subscription", subscriptionId, "--name", deploymentName, "--location", location, "--template-file", templateFile, "--parameters", fmt.Sprintf("@%s", parametersFile), "--output", "json") if isNotLoggedInMessage(res.Stderr) { diff --git a/cli/azd/pkg/tools/swa.go b/cli/azd/pkg/tools/swa.go index 19b5b31c666..446a21a38d4 100644 --- a/cli/azd/pkg/tools/swa.go +++ b/cli/azd/pkg/tools/swa.go @@ -21,7 +21,7 @@ type SwaCli interface { Login(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string) error Build(ctx context.Context, appFolderPath string, outputRelativeFolderPath string) error - Deploy(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string) (string, error) + Deploy(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string, deploymentToken string) (string, error) } type swaCli struct { @@ -56,7 +56,7 @@ func (cli *swaCli) Build(ctx context.Context, appFolderPath string, outputRelati return nil } -func (cli *swaCli) Deploy(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string) (string, error) { +func (cli *swaCli) Deploy(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string, deploymentToken string) (string, error) { res, err := cli.executeCommand(ctx, appFolderPath, "deploy", "--tenant-id", tenantId, @@ -65,7 +65,8 @@ func (cli *swaCli) Deploy(ctx context.Context, tenantId string, subscriptionId s "--app-name", appName, "--app-location", ".", "--output-location", outputRelativeFolderPath, - "--env", environment) + "--env", environment, + "--deployment-token", deploymentToken) if err != nil { return "", fmt.Errorf("swa deploy: %s: %w", res.String(), err) diff --git a/cli/azd/test/samples/funcapp/requirements.txt b/cli/azd/test/samples/funcapp/requirements.txt index 4dbffc6b68d..6bb1e59d0a8 100644 --- a/cli/azd/test/samples/funcapp/requirements.txt +++ b/cli/azd/test/samples/funcapp/requirements.txt @@ -1,3 +1,3 @@ -# Do not include azure-functions-worker as it may conflict with the Azure Functions platform - +# Do not include azure-functions-worker as it may conflict with the Azure Functions platform + azure-functions \ No newline at end of file From 5fdda43e18f11d6b97c19477194341e4ff91bc04 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 12 Jul 2022 14:46:44 -0700 Subject: [PATCH 2/7] Fixed issues with SWA environment names and status check --- .../project/service_target_staticwebapp.go | 54 ++++--- cli/azd/pkg/tools/azcli.go | 31 ++++ cli/azd/pkg/tools/azcli_staticwebapp_test.go | 136 ++++++++++++++++++ cli/azd/pkg/tools/swa.go | 3 + cli/azd/pkg/tools/swa_test.go | 10 +- 5 files changed, 214 insertions(+), 20 deletions(-) diff --git a/cli/azd/pkg/project/service_target_staticwebapp.go b/cli/azd/pkg/project/service_target_staticwebapp.go index 584d5ba8fae..9b720870fde 100644 --- a/cli/azd/pkg/project/service_target_staticwebapp.go +++ b/cli/azd/pkg/project/service_target_staticwebapp.go @@ -6,14 +6,18 @@ package project import ( "context" "fmt" - "log" "strings" + "time" "github.com/azure/azure-dev/cli/azd/pkg/azure" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/tools" ) +// TODO: Enhance for multi-environment support +// https://github.com/Azure/azure-dev/issues/1152 +const DefaultStaticWebAppEnvironmentName = "default" + type staticWebAppTarget struct { config *ServiceConfig env *environment.Environment @@ -31,26 +35,43 @@ func (at *staticWebAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az at.config.OutputPath = "build" } - staticWebAppEnvironmentName := at.env.GetEnvName() - if strings.TrimSpace(staticWebAppEnvironmentName) == "" { - staticWebAppEnvironmentName = "production" - } - - log.Printf("Logging into SWA CLI: TenantId: %s, SubscriptionId: %s, ResourceGroup: %s, ResourceName: %s", at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()) - - // Login to get the app deployment token + // Get the static webapp deployment token progress <- "Retrieving deployment tokens" deploymentToken, err := at.cli.GetStaticWebAppApiKey(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()) if err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("Failed retrieving static web app deployment token: %w", err) + return ServiceDeploymentResult{}, fmt.Errorf("failed retrieving static web app deployment token: %w", err) } // SWA performs a zip & deploy of the specified output folder and publishes it to the configured environment - log.Printf("Deploying SWA app: TenantId: %s, SubscriptionId: %s, ResourceGroup: %s, ResourceName: %s", at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()) progress <- "Publishing deployment artifacts" - res, err := at.swa.Deploy(ctx, at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), at.config.RelativePath, at.config.OutputPath, staticWebAppEnvironmentName, deploymentToken) + res, err := at.swa.Deploy(ctx, at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), at.config.RelativePath, at.config.OutputPath, DefaultStaticWebAppEnvironmentName, deploymentToken) if err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("Failed deploying static web app: %w", err) + return ServiceDeploymentResult{}, fmt.Errorf("failed deploying static web app: %w", err) + } + + verifyMsg := "Verifying deployment" + retries := 0 + const maxRetries = 10 + + for { + progress <- verifyMsg + envProps, err := at.cli.GetStaticWebAppEnvironmentProperties(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), DefaultStaticWebAppEnvironmentName) + if err != nil { + return ServiceDeploymentResult{}, fmt.Errorf("failed verifying static web app deployment: %w", err) + } + + if envProps.Status == "Ready" { + break + } + + retries++ + + if retries >= maxRetries { + return ServiceDeploymentResult{}, fmt.Errorf("failed verifying static web app deployment. Still in %s state", envProps.Status) + } + + verifyMsg += "." + time.Sleep(2 * time.Second) } progress <- "Fetching endpoints for static web app" @@ -72,11 +93,12 @@ func (at *staticWebAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az func (at *staticWebAppTarget) Endpoints(ctx context.Context) ([]string, error) { // TODO: Enhance for multi-environment support // https://github.com/Azure/azure-dev/issues/1152 - if props, err := at.cli.GetStaticWebAppProperties(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()); err != nil { + envProps, err := at.cli.GetStaticWebAppEnvironmentProperties(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), DefaultStaticWebAppEnvironmentName) + if err != nil { return nil, fmt.Errorf("fetching service properties: %w", err) - } else { - return []string{fmt.Sprintf("https://%s/", props.DefaultHostname)}, nil } + + return []string{fmt.Sprintf("https://%s/", envProps.Hostname)}, nil } func NewStaticWebAppTarget(config *ServiceConfig, env *environment.Environment, scope *environment.DeploymentScope, azCli tools.AzCli, swaCli tools.SwaCli) ServiceTarget { diff --git a/cli/azd/pkg/tools/azcli.go b/cli/azd/pkg/tools/azcli.go index 7101d70d82a..6d11b5927f9 100644 --- a/cli/azd/pkg/tools/azcli.go +++ b/cli/azd/pkg/tools/azcli.go @@ -78,6 +78,7 @@ type AzCli interface { GetContainerAppProperties(ctx context.Context, subscriptionId string, resourceGroupName string, applicationName string) (AzCliContainerAppProperties, error) GetStaticWebAppProperties(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (AzCliStaticWebAppProperties, error) GetStaticWebAppApiKey(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (string, error) + GetStaticWebAppEnvironmentProperties(ctx context.Context, subscriptionID string, resourceGroup string, appName string, environmentName string) (AzCliStaticWebAppEnvironmentProperties, error) GetSignedInUserId(ctx context.Context) (string, error) @@ -210,6 +211,11 @@ type AzCliStaticWebAppProperties struct { DefaultHostname string `json:"defaultHostname"` } +type AzCliStaticWebAppEnvironmentProperties struct { + Hostname string `json:"hostname"` + Status string `json:"status"` +} + type AzCliLocation struct { // The human friendly name of the location (e.g. "West US 2") DisplayName string `json:"displayName"` @@ -539,6 +545,31 @@ func (cli *azCli) GetStaticWebAppProperties(ctx context.Context, subscriptionID return staticWebAppProperties, nil } +func (cli *azCli) GetStaticWebAppEnvironmentProperties(ctx context.Context, subscriptionID string, resourceGroup string, appName string, environmentName string) (AzCliStaticWebAppEnvironmentProperties, error) { + res, err := cli.runAzCommandWithArgs(context.Background(), executil.RunArgs{ + Args: []string{ + "staticwebapp", "environment", "show", + "--subscription", subscriptionID, + "--resource-group", resourceGroup, + "--name", appName, + "--environment", environmentName, + "--output", "json", + }, + EnrichError: true, + }) + + if err != nil { + return AzCliStaticWebAppEnvironmentProperties{}, fmt.Errorf("failed getting staticwebapp environment properties: %w", err) + } + + var environmentProperties AzCliStaticWebAppEnvironmentProperties + if err := json.Unmarshal([]byte(res.Stdout), &environmentProperties); err != nil { + return AzCliStaticWebAppEnvironmentProperties{}, fmt.Errorf("could not unmarshal output %s as an AzCliStaticWebAppEnvironmentProperties: %w", res.Stdout, err) + } + + return environmentProperties, nil +} + func (cli *azCli) GetStaticWebAppApiKey(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (string, error) { res, err := cli.runAzCommandWithArgs(context.Background(), executil.RunArgs{ Args: []string{ diff --git a/cli/azd/pkg/tools/azcli_staticwebapp_test.go b/cli/azd/pkg/tools/azcli_staticwebapp_test.go index e4247bac8bd..cb5428dde01 100644 --- a/cli/azd/pkg/tools/azcli_staticwebapp_test.go +++ b/cli/azd/pkg/tools/azcli_staticwebapp_test.go @@ -77,3 +77,139 @@ func Test_GetStaticWebAppProperties(t *testing.T) { require.EqualError(t, err, "failed getting staticwebapp properties: example error message") }) } + +func Test_GetStaticWebAppEnvironmentProperties(t *testing.T) { + tempAZCLI := NewAzCli(NewAzCliArgs{ + EnableDebug: false, + EnableTelemetry: true, + }) + azcli := tempAZCLI.(*azCli) + + ran := false + + t.Run("NoErrors", func(t *testing.T) { + azcli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, []string{ + "staticwebapp", "environment", "show", + "--subscription", "subID", + "--resource-group", "resourceGroupID", + "--name", "appName", + "--environment", "default", + "--output", "json", + }, args.Args) + + require.True(t, args.EnrichError, "errors are enriched") + + return executil.RunResult{ + Stdout: `{"hostname":"default-environment-name.azurestaticapps.net"}`, + Stderr: "stderr text", + // if the returned `error` is nil we don't return an error. The underlying 'exec' + // returns an error if the command returns a non-zero exit code so we don't actually + // need to check it. + ExitCode: 1, + }, nil + } + + props, err := azcli.GetStaticWebAppEnvironmentProperties(context.Background(), "subID", "resourceGroupID", "appName", "default") + require.NoError(t, err) + require.Equal(t, "default-environment-name.azurestaticapps.net", props.Hostname) + require.True(t, ran) + }) + + t.Run("Error", func(t *testing.T) { + azcli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, []string{ + "staticwebapp", "environment", "show", + "--subscription", "subID", + "--resource-group", "resourceGroupID", + "--name", "appName", + "--environment", "default", + "--output", "json", + }, args.Args) + + require.True(t, args.EnrichError, "errors are enriched") + return executil.RunResult{ + Stdout: "", + Stderr: "stderr text", + ExitCode: 1, + }, errors.New("example error message") + } + + props, err := azcli.GetStaticWebAppEnvironmentProperties(context.Background(), "subID", "resourceGroupID", "appName", "default") + require.Equal(t, AzCliStaticWebAppEnvironmentProperties{}, props) + require.True(t, ran) + require.EqualError(t, err, "failed getting staticwebapp environment properties: example error message") + }) +} + +func Test_GetStaticWebAppApiKey(t *testing.T) { + tempAZCLI := NewAzCli(NewAzCliArgs{ + EnableDebug: false, + EnableTelemetry: true, + }) + azcli := tempAZCLI.(*azCli) + + ran := false + + t.Run("NoErrors", func(t *testing.T) { + azcli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, []string{ + "staticwebapp", "secrets", "list", + "--subscription", "subID", + "--resource-group", "resourceGroupID", + "--name", "appName", + "--query", "properties.apiKey", + "--output", "tsv", + }, args.Args) + + require.True(t, args.EnrichError, "errors are enriched") + + return executil.RunResult{ + Stdout: "ABC123", + Stderr: "stderr text", + // if the returned `error` is nil we don't return an error. The underlying 'exec' + // returns an error if the command returns a non-zero exit code so we don't actually + // need to check it. + ExitCode: 1, + }, nil + } + + apiKey, err := azcli.GetStaticWebAppApiKey(context.Background(), "subID", "resourceGroupID", "appName") + require.NoError(t, err) + require.Equal(t, "ABC123", apiKey) + require.True(t, ran) + }) + + t.Run("Error", func(t *testing.T) { + azcli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { + ran = true + + require.Equal(t, []string{ + "staticwebapp", "secrets", "list", + "--subscription", "subID", + "--resource-group", "resourceGroupID", + "--name", "appName", + "--query", "properties.apiKey", + "--output", "tsv", + }, args.Args) + + require.True(t, args.EnrichError, "errors are enriched") + return executil.RunResult{ + Stdout: "", + Stderr: "stderr text", + ExitCode: 1, + }, errors.New("example error message") + } + + apiKey, err := azcli.GetStaticWebAppApiKey(context.Background(), "subID", "resourceGroupID", "appName") + require.Equal(t, "", apiKey) + require.True(t, ran) + require.EqualError(t, err, "failed getting staticwebapp api key: example error message") + }) +} diff --git a/cli/azd/pkg/tools/swa.go b/cli/azd/pkg/tools/swa.go index 446a21a38d4..dcbff4175fb 100644 --- a/cli/azd/pkg/tools/swa.go +++ b/cli/azd/pkg/tools/swa.go @@ -6,6 +6,7 @@ package tools import ( "context" "fmt" + "log" "github.com/azure/azure-dev/cli/azd/pkg/executil" ) @@ -57,6 +58,8 @@ func (cli *swaCli) Build(ctx context.Context, appFolderPath string, outputRelati } func (cli *swaCli) Deploy(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string, deploymentToken string) (string, error) { + log.Printf("SWA Deploy: TenantId: %s, SubscriptionId: %s, ResourceGroup: %s, ResourceName: %s, Environment: %s", tenantId, subscriptionId, resourceGroup, appName, environment) + res, err := cli.executeCommand(ctx, appFolderPath, "deploy", "--tenant-id", tenantId, diff --git a/cli/azd/pkg/tools/swa_test.go b/cli/azd/pkg/tools/swa_test.go index 2cda221dc21..416ce812c19 100644 --- a/cli/azd/pkg/tools/swa_test.go +++ b/cli/azd/pkg/tools/swa_test.go @@ -150,7 +150,8 @@ func Test_SwaDeploy(t *testing.T) { "--app-name", "appName", "--app-location", ".", "--output-location", "build", - "--env", "production", + "--env", "default", + "--deployment-token", "deploymentToken", }, args.Args) return executil.RunResult{ @@ -163,7 +164,7 @@ func Test_SwaDeploy(t *testing.T) { }, nil } - _, err := swacli.Deploy(context.Background(), "tenantID", "subscriptionID", "resourceGroupID", "appName", "./appFolderPath", "build", "production") + _, err := swacli.Deploy(context.Background(), "tenantID", "subscriptionID", "resourceGroupID", "appName", "./appFolderPath", "build", "default", "deploymentToken") require.NoError(t, err) require.True(t, ran) }) @@ -182,7 +183,8 @@ func Test_SwaDeploy(t *testing.T) { "--app-name", "appName", "--app-location", ".", "--output-location", "build", - "--env", "production", + "--env", "default", + "--deployment-token", "deploymentToken", }, args.Args) return executil.RunResult{ @@ -192,7 +194,7 @@ func Test_SwaDeploy(t *testing.T) { }, errors.New("example error message") } - _, err := swacli.Deploy(context.Background(), "tenantID", "subscriptionID", "resourceGroupID", "appName", "./appFolderPath", "build", "production") + _, err := swacli.Deploy(context.Background(), "tenantID", "subscriptionID", "resourceGroupID", "appName", "./appFolderPath", "build", "default", "deploymentToken") require.True(t, ran) require.EqualError(t, err, "swa deploy: exit code: 1, stdout: stdout text, stderr: stderr text: example error message") }) From d77b7f316814066aafbde982e1d6f31027ed40c7 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 12 Jul 2022 14:55:01 -0700 Subject: [PATCH 3/7] Addresses cspell issues --- cli/azd/.vscode/cspell-azd-dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index ec26a8a1c1f..63f0058916c 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -10,6 +10,7 @@ azdtempl azdtest azsdk AZURECLI +azurestaticapps azureutil byts containerapp From 4c744aac923db0f63ee4c5ad699983828f243eb2 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 12 Jul 2022 15:43:02 -0700 Subject: [PATCH 4/7] Deprecate use of swal login, adds --no-use-keychain --- cli/azd/pkg/tools/swa.go | 16 +-------- cli/azd/pkg/tools/swa_test.go | 62 ++--------------------------------- 2 files changed, 3 insertions(+), 75 deletions(-) diff --git a/cli/azd/pkg/tools/swa.go b/cli/azd/pkg/tools/swa.go index dcbff4175fb..cade41ddbfc 100644 --- a/cli/azd/pkg/tools/swa.go +++ b/cli/azd/pkg/tools/swa.go @@ -20,7 +20,6 @@ func NewSwaCli() SwaCli { type SwaCli interface { ExternalTool - Login(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string) error Build(ctx context.Context, appFolderPath string, outputRelativeFolderPath string) error Deploy(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string, deploymentToken string) (string, error) } @@ -30,20 +29,6 @@ type swaCli struct { runWithResultFn func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) } -func (cli *swaCli) Login(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string) error { - res, err := cli.executeCommand(ctx, ".", "login", - "--tenant-id", tenantId, - "--subscription-id", subscriptionId, - "--resource-group", resourceGroup, - "--app-name", appName) - - if err != nil { - return fmt.Errorf("swa login: %s: %w", res.String(), err) - } - - return nil -} - func (cli *swaCli) Build(ctx context.Context, appFolderPath string, outputRelativeFolderPath string) error { res, err := cli.executeCommand(ctx, appFolderPath, "build", @@ -69,6 +54,7 @@ func (cli *swaCli) Deploy(ctx context.Context, tenantId string, subscriptionId s "--app-location", ".", "--output-location", outputRelativeFolderPath, "--env", environment, + "--no-use-keychain", "--deployment-token", deploymentToken) if err != nil { diff --git a/cli/azd/pkg/tools/swa_test.go b/cli/azd/pkg/tools/swa_test.go index 416ce812c19..31c0593be81 100644 --- a/cli/azd/pkg/tools/swa_test.go +++ b/cli/azd/pkg/tools/swa_test.go @@ -12,66 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -func Test_SwaLogin(t *testing.T) { - tempSwaCli := NewSwaCli() - swacli := tempSwaCli.(*swaCli) - - ran := false - - t.Run("NoErrors", func(t *testing.T) { - swacli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { - ran = true - - require.Equal(t, []string{ - "-y", "@azure/static-web-apps-cli", - "login", - "--tenant-id", "tenantID", - "--subscription-id", "subscriptionID", - "--resource-group", "resourceGroupID", - "--app-name", "appName", - }, args.Args) - - return executil.RunResult{ - Stdout: "", - Stderr: "", - // if the returned `error` is nil we don't return an error. The underlying 'exec' - // returns an error if the command returns a non-zero exit code so we don't actually - // need to check it. - ExitCode: 1, - }, nil - } - - err := swacli.Login(context.Background(), "tenantID", "subscriptionID", "resourceGroupID", "appName") - require.NoError(t, err) - require.True(t, ran) - }) - - t.Run("Error", func(t *testing.T) { - swacli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { - ran = true - - require.Equal(t, []string{ - "-y", "@azure/static-web-apps-cli", - "login", - "--tenant-id", "tenantID", - "--subscription-id", "subscriptionID", - "--resource-group", "resourceGroupID", - "--app-name", "appName", - }, args.Args) - - return executil.RunResult{ - Stdout: "stdout text", - Stderr: "stderr text", - ExitCode: 1, - }, errors.New("example error message") - } - - err := swacli.Login(context.Background(), "tenantID", "subscriptionID", "resourceGroupID", "appName") - require.True(t, ran) - require.EqualError(t, err, "swa login: exit code: 1, stdout: stdout text, stderr: stderr text: example error message") - }) -} - func Test_SwaBuild(t *testing.T) { tempSwaCli := NewSwaCli() swacli := tempSwaCli.(*swaCli) @@ -151,6 +91,7 @@ func Test_SwaDeploy(t *testing.T) { "--app-location", ".", "--output-location", "build", "--env", "default", + "--no-use-keychain", "--deployment-token", "deploymentToken", }, args.Args) @@ -184,6 +125,7 @@ func Test_SwaDeploy(t *testing.T) { "--app-location", ".", "--output-location", "build", "--env", "default", + "--no-use-keychain", "--deployment-token", "deploymentToken", }, args.Args) From 386baa411beaf1fcee336a53d92a2dedf11bbafc Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 13 Jul 2022 09:53:30 -0700 Subject: [PATCH 5/7] Addresses issues with swa deploy and deployment token --- .../project/service_target_staticwebapp.go | 71 ++++++++++++------- cli/azd/pkg/tools/azcli.go | 3 +- cli/azd/pkg/tools/swa.go | 25 +++---- cli/azd/pkg/tools/swa_test.go | 24 +++---- 4 files changed, 73 insertions(+), 50 deletions(-) diff --git a/cli/azd/pkg/project/service_target_staticwebapp.go b/cli/azd/pkg/project/service_target_staticwebapp.go index 9b720870fde..8122430ff75 100644 --- a/cli/azd/pkg/project/service_target_staticwebapp.go +++ b/cli/azd/pkg/project/service_target_staticwebapp.go @@ -6,6 +6,7 @@ package project import ( "context" "fmt" + "log" "strings" "time" @@ -36,7 +37,7 @@ func (at *staticWebAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az } // Get the static webapp deployment token - progress <- "Retrieving deployment tokens" + progress <- "Retrieving deployment token" deploymentToken, err := at.cli.GetStaticWebAppApiKey(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()) if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed retrieving static web app deployment token: %w", err) @@ -44,34 +45,25 @@ func (at *staticWebAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az // SWA performs a zip & deploy of the specified output folder and publishes it to the configured environment progress <- "Publishing deployment artifacts" - res, err := at.swa.Deploy(ctx, at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), at.config.RelativePath, at.config.OutputPath, DefaultStaticWebAppEnvironmentName, deploymentToken) + res, err := at.swa.Deploy(ctx, + at.config.Project.Path, + at.env.GetTenantId(), + at.env.GetSubscriptionId(), + at.scope.ResourceGroupName(), + at.scope.ResourceName(), + at.config.RelativePath, + at.config.OutputPath, + DefaultStaticWebAppEnvironmentName, + deploymentToken) + + log.Println(res) + if err != nil { return ServiceDeploymentResult{}, fmt.Errorf("failed deploying static web app: %w", err) } - verifyMsg := "Verifying deployment" - retries := 0 - const maxRetries = 10 - - for { - progress <- verifyMsg - envProps, err := at.cli.GetStaticWebAppEnvironmentProperties(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), DefaultStaticWebAppEnvironmentName) - if err != nil { - return ServiceDeploymentResult{}, fmt.Errorf("failed verifying static web app deployment: %w", err) - } - - if envProps.Status == "Ready" { - break - } - - retries++ - - if retries >= maxRetries { - return ServiceDeploymentResult{}, fmt.Errorf("failed verifying static web app deployment. Still in %s state", envProps.Status) - } - - verifyMsg += "." - time.Sleep(2 * time.Second) + if err := at.verifyDeployment(ctx, progress); err != nil { + return ServiceDeploymentResult{}, err } progress <- "Fetching endpoints for static web app" @@ -101,6 +93,35 @@ func (at *staticWebAppTarget) Endpoints(ctx context.Context) ([]string, error) { return []string{fmt.Sprintf("https://%s/", envProps.Hostname)}, nil } +func (at *staticWebAppTarget) verifyDeployment(ctx context.Context, progress chan<- string) error { + verifyMsg := "Verifying deployment" + retries := 0 + const maxRetries = 10 + + for { + progress <- verifyMsg + envProps, err := at.cli.GetStaticWebAppEnvironmentProperties(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), DefaultStaticWebAppEnvironmentName) + if err != nil { + return fmt.Errorf("failed verifying static web app deployment: %w", err) + } + + if envProps.Status == "Ready" { + break + } + + retries++ + + if retries >= maxRetries { + return fmt.Errorf("failed verifying static web app deployment. Still in %s state", envProps.Status) + } + + verifyMsg += "." + time.Sleep(5 * time.Second) + } + + return nil +} + func NewStaticWebAppTarget(config *ServiceConfig, env *environment.Environment, scope *environment.DeploymentScope, azCli tools.AzCli, swaCli tools.SwaCli) ServiceTarget { return &staticWebAppTarget{ config: config, diff --git a/cli/azd/pkg/tools/azcli.go b/cli/azd/pkg/tools/azcli.go index 6d11b5927f9..f5b8e678370 100644 --- a/cli/azd/pkg/tools/azcli.go +++ b/cli/azd/pkg/tools/azcli.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "regexp" + "strings" "time" azdinternal "github.com/azure/azure-dev/cli/azd/internal" @@ -587,7 +588,7 @@ func (cli *azCli) GetStaticWebAppApiKey(ctx context.Context, subscriptionID stri return "", fmt.Errorf("failed getting staticwebapp api key: %w", err) } - return res.Stdout, nil + return strings.TrimSpace(res.Stdout), nil } func (cli *azCli) DeployToSubscription(ctx context.Context, subscriptionId string, deploymentName string, templateFile string, parametersFile string, location string) (AzCliDeploymentResult, error) { diff --git a/cli/azd/pkg/tools/swa.go b/cli/azd/pkg/tools/swa.go index cade41ddbfc..1e8d4d80506 100644 --- a/cli/azd/pkg/tools/swa.go +++ b/cli/azd/pkg/tools/swa.go @@ -20,8 +20,8 @@ func NewSwaCli() SwaCli { type SwaCli interface { ExternalTool - Build(ctx context.Context, appFolderPath string, outputRelativeFolderPath string) error - Deploy(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string, deploymentToken string) (string, error) + Build(ctx context.Context, cwd string, appFolderPath string, outputRelativeFolderPath string) error + Deploy(ctx context.Context, cwd string, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string, deploymentToken string) (string, error) } type swaCli struct { @@ -29,10 +29,10 @@ type swaCli struct { runWithResultFn func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) } -func (cli *swaCli) Build(ctx context.Context, appFolderPath string, outputRelativeFolderPath string) error { +func (cli *swaCli) Build(ctx context.Context, cwd string, appFolderPath string, outputRelativeFolderPath string) error { res, err := cli.executeCommand(ctx, - appFolderPath, "build", - "--app-location", ".", + cwd, "build", + "--app-location", appFolderPath, "--output-location", outputRelativeFolderPath) if err != nil { @@ -42,16 +42,16 @@ func (cli *swaCli) Build(ctx context.Context, appFolderPath string, outputRelati return nil } -func (cli *swaCli) Deploy(ctx context.Context, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string, deploymentToken string) (string, error) { +func (cli *swaCli) Deploy(ctx context.Context, cwd string, tenantId string, subscriptionId string, resourceGroup string, appName string, appFolderPath string, outputRelativeFolderPath string, environment string, deploymentToken string) (string, error) { log.Printf("SWA Deploy: TenantId: %s, SubscriptionId: %s, ResourceGroup: %s, ResourceName: %s, Environment: %s", tenantId, subscriptionId, resourceGroup, appName, environment) res, err := cli.executeCommand(ctx, - appFolderPath, "deploy", + cwd, "deploy", "--tenant-id", tenantId, "--subscription-id", subscriptionId, "--resource-group", resourceGroup, "--app-name", appName, - "--app-location", ".", + "--app-location", appFolderPath, "--output-location", outputRelativeFolderPath, "--env", environment, "--no-use-keychain", @@ -61,7 +61,7 @@ func (cli *swaCli) Deploy(ctx context.Context, tenantId string, subscriptionId s return "", fmt.Errorf("swa deploy: %s: %w", res.String(), err) } - return res.Stdout, nil + return res.Stdout + res.Stderr, nil } func (cli *swaCli) CheckInstalled(_ context.Context) (bool, error) { @@ -81,8 +81,9 @@ func (cli *swaCli) executeCommand(ctx context.Context, cwd string, args ...strin finalArgs := append(defaultArgs, args...) return cli.runWithResultFn(ctx, executil.RunArgs{ - Cmd: "npx", - Args: finalArgs, - Cwd: cwd, + Cmd: "npx", + Args: finalArgs, + Cwd: cwd, + EnrichError: true, }) } diff --git a/cli/azd/pkg/tools/swa_test.go b/cli/azd/pkg/tools/swa_test.go index 31c0593be81..eacb4e99919 100644 --- a/cli/azd/pkg/tools/swa_test.go +++ b/cli/azd/pkg/tools/swa_test.go @@ -22,11 +22,11 @@ func Test_SwaBuild(t *testing.T) { swacli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { ran = true - require.Equal(t, "./appFolderPath", args.Cwd) + require.Equal(t, "./projectPath", args.Cwd) require.Equal(t, []string{ "-y", "@azure/static-web-apps-cli", "build", - "--app-location", ".", + "--app-location", "service/path", "--output-location", "build", }, args.Args) @@ -40,7 +40,7 @@ func Test_SwaBuild(t *testing.T) { }, nil } - err := swacli.Build(context.Background(), "./appFolderPath", "build") + err := swacli.Build(context.Background(), "./projectPath", "service/path", "build") require.NoError(t, err) require.True(t, ran) }) @@ -49,11 +49,11 @@ func Test_SwaBuild(t *testing.T) { swacli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { ran = true - require.Equal(t, "./appFolderPath", args.Cwd) + require.Equal(t, "./projectPath", args.Cwd) require.Equal(t, []string{ "-y", "@azure/static-web-apps-cli", "build", - "--app-location", ".", + "--app-location", "service/path", "--output-location", "build", }, args.Args) @@ -64,7 +64,7 @@ func Test_SwaBuild(t *testing.T) { }, errors.New("example error message") } - err := swacli.Build(context.Background(), "./appFolderPath", "build") + err := swacli.Build(context.Background(), "./projectPath", "service/path", "build") require.True(t, ran) require.EqualError(t, err, "swa build: exit code: 1, stdout: stdout text, stderr: stderr text: example error message") }) @@ -80,7 +80,7 @@ func Test_SwaDeploy(t *testing.T) { swacli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { ran = true - require.Equal(t, "./appFolderPath", args.Cwd) + require.Equal(t, "./projectPath", args.Cwd) require.Equal(t, []string{ "-y", "@azure/static-web-apps-cli", "deploy", @@ -88,7 +88,7 @@ func Test_SwaDeploy(t *testing.T) { "--subscription-id", "subscriptionID", "--resource-group", "resourceGroupID", "--app-name", "appName", - "--app-location", ".", + "--app-location", "service/path", "--output-location", "build", "--env", "default", "--no-use-keychain", @@ -105,7 +105,7 @@ func Test_SwaDeploy(t *testing.T) { }, nil } - _, err := swacli.Deploy(context.Background(), "tenantID", "subscriptionID", "resourceGroupID", "appName", "./appFolderPath", "build", "default", "deploymentToken") + _, err := swacli.Deploy(context.Background(), "./projectPath", "tenantID", "subscriptionID", "resourceGroupID", "appName", "service/path", "build", "default", "deploymentToken") require.NoError(t, err) require.True(t, ran) }) @@ -114,7 +114,7 @@ func Test_SwaDeploy(t *testing.T) { swacli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) { ran = true - require.Equal(t, "./appFolderPath", args.Cwd) + require.Equal(t, "./projectPath", args.Cwd) require.Equal(t, []string{ "-y", "@azure/static-web-apps-cli", "deploy", @@ -122,7 +122,7 @@ func Test_SwaDeploy(t *testing.T) { "--subscription-id", "subscriptionID", "--resource-group", "resourceGroupID", "--app-name", "appName", - "--app-location", ".", + "--app-location", "service/path", "--output-location", "build", "--env", "default", "--no-use-keychain", @@ -136,7 +136,7 @@ func Test_SwaDeploy(t *testing.T) { }, errors.New("example error message") } - _, err := swacli.Deploy(context.Background(), "tenantID", "subscriptionID", "resourceGroupID", "appName", "./appFolderPath", "build", "default", "deploymentToken") + _, err := swacli.Deploy(context.Background(), "./projectPath", "tenantID", "subscriptionID", "resourceGroupID", "appName", "service/path", "build", "default", "deploymentToken") require.True(t, ran) require.EqualError(t, err, "swa deploy: exit code: 1, stdout: stdout text, stderr: stderr text: example error message") }) From 4f3fdbc4877767680664e4164ccfade43c50d4c5 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 13 Jul 2022 10:04:51 -0700 Subject: [PATCH 6/7] Addresses cspell issues --- cli/azd/.vscode/cspell-azd-dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index 63f0058916c..24dcab93eaa 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -36,6 +36,7 @@ omitempty osutil pflag pyapp +keychain restoreapp rzip sstore From f9dcdb27d44cfbb05ca369066c5716330cd0d4c7 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 13 Jul 2022 13:13:39 -0700 Subject: [PATCH 7/7] Updates changelog --- cli/azd/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/CHANGELOG.md b/cli/azd/CHANGELOG.md index 0e2e5a5f8d4..6a972902c46 100644 --- a/cli/azd/CHANGELOG.md +++ b/cli/azd/CHANGELOG.md @@ -10,6 +10,8 @@ - Fixed an issue where passing `--help` to `azd` would result in an error message being printed to standard error before the help was printed. - [[#71]](https://github.com/Azure/azure-dev/issues/71) Fixed detection for disabled GitHub actions on new created repos. +- [[#70]](https://github.com/Azure/azure-dev/issues/70) Ensure SWA app is in READY state after deployment completes +- [[#53]](https://github.com/Azure/azure-dev/issues/53) SWA app is deployed to incorrect environment ### Other Changes