Skip to content

Commit 13ec2b1

Browse files
authored
Addresses Static Web App deployment issues (#85)
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
1 parent 5a2ad96 commit 13ec2b1

File tree

8 files changed

+293
-121
lines changed

8 files changed

+293
-121
lines changed

cli/azd/.vscode/cspell-azd-dictionary.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ azdtempl
1010
azdtest
1111
azsdk
1212
AZURECLI
13+
azurestaticapps
1314
azureutil
1415
byts
1516
containerapp
@@ -35,6 +36,7 @@ omitempty
3536
osutil
3637
pflag
3738
pyapp
39+
keychain
3840
restoreapp
3941
rzip
4042
sstore

cli/azd/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
- Fixed an issue where passing `--help` to `azd` would result in an error message being printed to standard error before the help was printed.
88
- [[#71]](https://github.com/Azure/azure-dev/issues/71) Fixed detection for disabled GitHub actions on new created repos.
9+
- [[#70]](https://github.com/Azure/azure-dev/issues/70) Ensure SWA app is in READY state after deployment completes
10+
- [[#53]](https://github.com/Azure/azure-dev/issues/53) SWA app is deployed to incorrect environment
911

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

cli/azd/pkg/project/service_target_staticwebapp.go

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import (
88
"fmt"
99
"log"
1010
"strings"
11+
"time"
1112

1213
"github.com/azure/azure-dev/cli/azd/pkg/azure"
1314
"github.com/azure/azure-dev/cli/azd/pkg/environment"
1415
"github.com/azure/azure-dev/cli/azd/pkg/tools"
1516
)
1617

18+
// TODO: Enhance for multi-environment support
19+
// https://github.com/Azure/azure-dev/issues/1152
20+
const DefaultStaticWebAppEnvironmentName = "default"
21+
1722
type staticWebAppTarget struct {
1823
config *ServiceConfig
1924
env *environment.Environment
@@ -31,25 +36,34 @@ func (at *staticWebAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az
3136
at.config.OutputPath = "build"
3237
}
3338

34-
staticWebAppEnvironmentName := at.env.GetEnvName()
35-
if strings.TrimSpace(staticWebAppEnvironmentName) == "" {
36-
staticWebAppEnvironmentName = "production"
37-
}
38-
39-
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())
40-
41-
// Login to get the app deployment token
42-
progress <- "Generating deployment tokens"
43-
if err := at.swa.Login(ctx, at.env.GetTenantId(), at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()); err != nil {
44-
return ServiceDeploymentResult{}, fmt.Errorf("Failed deploying static web app: %w", err)
39+
// Get the static webapp deployment token
40+
progress <- "Retrieving deployment token"
41+
deploymentToken, err := at.cli.GetStaticWebAppApiKey(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName())
42+
if err != nil {
43+
return ServiceDeploymentResult{}, fmt.Errorf("failed retrieving static web app deployment token: %w", err)
4544
}
4645

4746
// SWA performs a zip & deploy of the specified output folder and publishes it to the configured environment
48-
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())
4947
progress <- "Publishing deployment artifacts"
50-
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)
48+
res, err := at.swa.Deploy(ctx,
49+
at.config.Project.Path,
50+
at.env.GetTenantId(),
51+
at.env.GetSubscriptionId(),
52+
at.scope.ResourceGroupName(),
53+
at.scope.ResourceName(),
54+
at.config.RelativePath,
55+
at.config.OutputPath,
56+
DefaultStaticWebAppEnvironmentName,
57+
deploymentToken)
58+
59+
log.Println(res)
60+
5161
if err != nil {
52-
return ServiceDeploymentResult{}, fmt.Errorf("Failed deploying static web app: %w", err)
62+
return ServiceDeploymentResult{}, fmt.Errorf("failed deploying static web app: %w", err)
63+
}
64+
65+
if err := at.verifyDeployment(ctx, progress); err != nil {
66+
return ServiceDeploymentResult{}, err
5367
}
5468

5569
progress <- "Fetching endpoints for static web app"
@@ -71,11 +85,41 @@ func (at *staticWebAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az
7185
func (at *staticWebAppTarget) Endpoints(ctx context.Context) ([]string, error) {
7286
// TODO: Enhance for multi-environment support
7387
// https://github.com/Azure/azure-dev/issues/1152
74-
if props, err := at.cli.GetStaticWebAppProperties(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName()); err != nil {
88+
envProps, err := at.cli.GetStaticWebAppEnvironmentProperties(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), DefaultStaticWebAppEnvironmentName)
89+
if err != nil {
7590
return nil, fmt.Errorf("fetching service properties: %w", err)
76-
} else {
77-
return []string{fmt.Sprintf("https://%s/", props.DefaultHostname)}, nil
7891
}
92+
93+
return []string{fmt.Sprintf("https://%s/", envProps.Hostname)}, nil
94+
}
95+
96+
func (at *staticWebAppTarget) verifyDeployment(ctx context.Context, progress chan<- string) error {
97+
verifyMsg := "Verifying deployment"
98+
retries := 0
99+
const maxRetries = 10
100+
101+
for {
102+
progress <- verifyMsg
103+
envProps, err := at.cli.GetStaticWebAppEnvironmentProperties(ctx, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName(), DefaultStaticWebAppEnvironmentName)
104+
if err != nil {
105+
return fmt.Errorf("failed verifying static web app deployment: %w", err)
106+
}
107+
108+
if envProps.Status == "Ready" {
109+
break
110+
}
111+
112+
retries++
113+
114+
if retries >= maxRetries {
115+
return fmt.Errorf("failed verifying static web app deployment. Still in %s state", envProps.Status)
116+
}
117+
118+
verifyMsg += "."
119+
time.Sleep(5 * time.Second)
120+
}
121+
122+
return nil
79123
}
80124

81125
func NewStaticWebAppTarget(config *ServiceConfig, env *environment.Environment, scope *environment.DeploymentScope, azCli tools.AzCli, swaCli tools.SwaCli) ServiceTarget {

cli/azd/pkg/tools/azcli.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"net/http"
1313
"regexp"
14+
"strings"
1415
"time"
1516

1617
azdinternal "github.com/azure/azure-dev/cli/azd/internal"
@@ -77,6 +78,8 @@ type AzCli interface {
7778
GetAppServiceProperties(ctx context.Context, subscriptionId string, resourceGroupName string, applicationName string) (AzCliAppServiceProperties, error)
7879
GetContainerAppProperties(ctx context.Context, subscriptionId string, resourceGroupName string, applicationName string) (AzCliContainerAppProperties, error)
7980
GetStaticWebAppProperties(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (AzCliStaticWebAppProperties, error)
81+
GetStaticWebAppApiKey(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (string, error)
82+
GetStaticWebAppEnvironmentProperties(ctx context.Context, subscriptionID string, resourceGroup string, appName string, environmentName string) (AzCliStaticWebAppEnvironmentProperties, error)
8083

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

@@ -209,6 +212,11 @@ type AzCliStaticWebAppProperties struct {
209212
DefaultHostname string `json:"defaultHostname"`
210213
}
211214

215+
type AzCliStaticWebAppEnvironmentProperties struct {
216+
Hostname string `json:"hostname"`
217+
Status string `json:"status"`
218+
}
219+
212220
type AzCliLocation struct {
213221
// The human friendly name of the location (e.g. "West US 2")
214222
DisplayName string `json:"displayName"`
@@ -538,6 +546,51 @@ func (cli *azCli) GetStaticWebAppProperties(ctx context.Context, subscriptionID
538546
return staticWebAppProperties, nil
539547
}
540548

549+
func (cli *azCli) GetStaticWebAppEnvironmentProperties(ctx context.Context, subscriptionID string, resourceGroup string, appName string, environmentName string) (AzCliStaticWebAppEnvironmentProperties, error) {
550+
res, err := cli.runAzCommandWithArgs(context.Background(), executil.RunArgs{
551+
Args: []string{
552+
"staticwebapp", "environment", "show",
553+
"--subscription", subscriptionID,
554+
"--resource-group", resourceGroup,
555+
"--name", appName,
556+
"--environment", environmentName,
557+
"--output", "json",
558+
},
559+
EnrichError: true,
560+
})
561+
562+
if err != nil {
563+
return AzCliStaticWebAppEnvironmentProperties{}, fmt.Errorf("failed getting staticwebapp environment properties: %w", err)
564+
}
565+
566+
var environmentProperties AzCliStaticWebAppEnvironmentProperties
567+
if err := json.Unmarshal([]byte(res.Stdout), &environmentProperties); err != nil {
568+
return AzCliStaticWebAppEnvironmentProperties{}, fmt.Errorf("could not unmarshal output %s as an AzCliStaticWebAppEnvironmentProperties: %w", res.Stdout, err)
569+
}
570+
571+
return environmentProperties, nil
572+
}
573+
574+
func (cli *azCli) GetStaticWebAppApiKey(ctx context.Context, subscriptionID string, resourceGroup string, appName string) (string, error) {
575+
res, err := cli.runAzCommandWithArgs(context.Background(), executil.RunArgs{
576+
Args: []string{
577+
"staticwebapp", "secrets", "list",
578+
"--subscription", subscriptionID,
579+
"--resource-group", resourceGroup,
580+
"--name", appName,
581+
"--query", "properties.apiKey",
582+
"--output", "tsv",
583+
},
584+
EnrichError: true,
585+
})
586+
587+
if err != nil {
588+
return "", fmt.Errorf("failed getting staticwebapp api key: %w", err)
589+
}
590+
591+
return strings.TrimSpace(res.Stdout), nil
592+
}
593+
541594
func (cli *azCli) DeployToSubscription(ctx context.Context, subscriptionId string, deploymentName string, templateFile string, parametersFile string, location string) (AzCliDeploymentResult, error) {
542595
res, err := cli.runAzCommand(ctx, "deployment", "sub", "create", "--subscription", subscriptionId, "--name", deploymentName, "--location", location, "--template-file", templateFile, "--parameters", fmt.Sprintf("@%s", parametersFile), "--output", "json")
543596
if isNotLoggedInMessage(res.Stderr) {

cli/azd/pkg/tools/azcli_staticwebapp_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,139 @@ func Test_GetStaticWebAppProperties(t *testing.T) {
7777
require.EqualError(t, err, "failed getting staticwebapp properties: example error message")
7878
})
7979
}
80+
81+
func Test_GetStaticWebAppEnvironmentProperties(t *testing.T) {
82+
tempAZCLI := NewAzCli(NewAzCliArgs{
83+
EnableDebug: false,
84+
EnableTelemetry: true,
85+
})
86+
azcli := tempAZCLI.(*azCli)
87+
88+
ran := false
89+
90+
t.Run("NoErrors", func(t *testing.T) {
91+
azcli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) {
92+
ran = true
93+
94+
require.Equal(t, []string{
95+
"staticwebapp", "environment", "show",
96+
"--subscription", "subID",
97+
"--resource-group", "resourceGroupID",
98+
"--name", "appName",
99+
"--environment", "default",
100+
"--output", "json",
101+
}, args.Args)
102+
103+
require.True(t, args.EnrichError, "errors are enriched")
104+
105+
return executil.RunResult{
106+
Stdout: `{"hostname":"default-environment-name.azurestaticapps.net"}`,
107+
Stderr: "stderr text",
108+
// if the returned `error` is nil we don't return an error. The underlying 'exec'
109+
// returns an error if the command returns a non-zero exit code so we don't actually
110+
// need to check it.
111+
ExitCode: 1,
112+
}, nil
113+
}
114+
115+
props, err := azcli.GetStaticWebAppEnvironmentProperties(context.Background(), "subID", "resourceGroupID", "appName", "default")
116+
require.NoError(t, err)
117+
require.Equal(t, "default-environment-name.azurestaticapps.net", props.Hostname)
118+
require.True(t, ran)
119+
})
120+
121+
t.Run("Error", func(t *testing.T) {
122+
azcli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) {
123+
ran = true
124+
125+
require.Equal(t, []string{
126+
"staticwebapp", "environment", "show",
127+
"--subscription", "subID",
128+
"--resource-group", "resourceGroupID",
129+
"--name", "appName",
130+
"--environment", "default",
131+
"--output", "json",
132+
}, args.Args)
133+
134+
require.True(t, args.EnrichError, "errors are enriched")
135+
return executil.RunResult{
136+
Stdout: "",
137+
Stderr: "stderr text",
138+
ExitCode: 1,
139+
}, errors.New("example error message")
140+
}
141+
142+
props, err := azcli.GetStaticWebAppEnvironmentProperties(context.Background(), "subID", "resourceGroupID", "appName", "default")
143+
require.Equal(t, AzCliStaticWebAppEnvironmentProperties{}, props)
144+
require.True(t, ran)
145+
require.EqualError(t, err, "failed getting staticwebapp environment properties: example error message")
146+
})
147+
}
148+
149+
func Test_GetStaticWebAppApiKey(t *testing.T) {
150+
tempAZCLI := NewAzCli(NewAzCliArgs{
151+
EnableDebug: false,
152+
EnableTelemetry: true,
153+
})
154+
azcli := tempAZCLI.(*azCli)
155+
156+
ran := false
157+
158+
t.Run("NoErrors", func(t *testing.T) {
159+
azcli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) {
160+
ran = true
161+
162+
require.Equal(t, []string{
163+
"staticwebapp", "secrets", "list",
164+
"--subscription", "subID",
165+
"--resource-group", "resourceGroupID",
166+
"--name", "appName",
167+
"--query", "properties.apiKey",
168+
"--output", "tsv",
169+
}, args.Args)
170+
171+
require.True(t, args.EnrichError, "errors are enriched")
172+
173+
return executil.RunResult{
174+
Stdout: "ABC123",
175+
Stderr: "stderr text",
176+
// if the returned `error` is nil we don't return an error. The underlying 'exec'
177+
// returns an error if the command returns a non-zero exit code so we don't actually
178+
// need to check it.
179+
ExitCode: 1,
180+
}, nil
181+
}
182+
183+
apiKey, err := azcli.GetStaticWebAppApiKey(context.Background(), "subID", "resourceGroupID", "appName")
184+
require.NoError(t, err)
185+
require.Equal(t, "ABC123", apiKey)
186+
require.True(t, ran)
187+
})
188+
189+
t.Run("Error", func(t *testing.T) {
190+
azcli.runWithResultFn = func(ctx context.Context, args executil.RunArgs) (executil.RunResult, error) {
191+
ran = true
192+
193+
require.Equal(t, []string{
194+
"staticwebapp", "secrets", "list",
195+
"--subscription", "subID",
196+
"--resource-group", "resourceGroupID",
197+
"--name", "appName",
198+
"--query", "properties.apiKey",
199+
"--output", "tsv",
200+
}, args.Args)
201+
202+
require.True(t, args.EnrichError, "errors are enriched")
203+
return executil.RunResult{
204+
Stdout: "",
205+
Stderr: "stderr text",
206+
ExitCode: 1,
207+
}, errors.New("example error message")
208+
}
209+
210+
apiKey, err := azcli.GetStaticWebAppApiKey(context.Background(), "subID", "resourceGroupID", "appName")
211+
require.Equal(t, "", apiKey)
212+
require.True(t, ran)
213+
require.EqualError(t, err, "failed getting staticwebapp api key: example error message")
214+
})
215+
}

0 commit comments

Comments
 (0)