Skip to content

Commit

Permalink
Addresses Static Web App deployment issues (#85)
Browse files Browse the repository at this point in the history
Address a few different Static Web App deployment issues

- Deprecates usage of swa login. Will now manually query for deployment token and pass via --deployment-token param of swa deploy
- Defaults to default environment name regardless of azd environment name till Enhance static web apps endpoint listing with support for multiple environments. azure-dev-pr#1152 is resolved
- Adds deployment validation check to ensure environment is in "Ready" state
- Now works correctly in Linux based hosts
  • Loading branch information
wbreza authored Jul 13, 2022
1 parent 5a2ad96 commit 13ec2b1
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 121 deletions.
2 changes: 2 additions & 0 deletions cli/azd/.vscode/cspell-azd-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ azdtempl
azdtest
azsdk
AZURECLI
azurestaticapps
azureutil
byts
containerapp
Expand All @@ -35,6 +36,7 @@ omitempty
osutil
pflag
pyapp
keychain
restoreapp
rzip
sstore
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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

## 0.1.0-beta.1 (2022-07-11)

Expand Down
78 changes: 61 additions & 17 deletions cli/azd/pkg/project/service_target_staticwebapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import (
"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
Expand All @@ -31,25 +36,34 @@ 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
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)
// Get the static webapp deployment token
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)
}

// 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.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)
return ServiceDeploymentResult{}, fmt.Errorf("failed deploying static web app: %w", err)
}

if err := at.verifyDeployment(ctx, progress); err != nil {
return ServiceDeploymentResult{}, err
}

progress <- "Fetching endpoints for static web app"
Expand All @@ -71,11 +85,41 @@ 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 (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 {
Expand Down
53 changes: 53 additions & 0 deletions cli/azd/pkg/tools/azcli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io"
"net/http"
"regexp"
"strings"
"time"

azdinternal "github.com/azure/azure-dev/cli/azd/internal"
Expand Down Expand Up @@ -77,6 +78,8 @@ 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)
GetStaticWebAppEnvironmentProperties(ctx context.Context, subscriptionID string, resourceGroup string, appName string, environmentName string) (AzCliStaticWebAppEnvironmentProperties, error)

GetSignedInUserId(ctx context.Context) (string, error)

Expand Down Expand Up @@ -209,6 +212,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"`
Expand Down Expand Up @@ -538,6 +546,51 @@ 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{
"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 strings.TrimSpace(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) {
Expand Down
136 changes: 136 additions & 0 deletions cli/azd/pkg/tools/azcli_staticwebapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
Loading

0 comments on commit 13ec2b1

Please sign in to comment.