cli/azd/pkg/project/service_target_staticwebapp.go (175 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package project
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/azure/azure-dev/cli/azd/pkg/async"
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
"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"
"github.com/azure/azure-dev/cli/azd/pkg/tools/swa"
)
// TODO: Enhance for multi-environment support
// https://github.com/Azure/azure-dev/issues/1152
const DefaultStaticWebAppEnvironmentName = "default"
type staticWebAppTarget struct {
env *environment.Environment
cli *azapi.AzureClient
swa *swa.Cli
}
// NewStaticWebAppTarget creates a new instance of the Static Web App target
func NewStaticWebAppTarget(
env *environment.Environment,
azCli *azapi.AzureClient,
swaCli *swa.Cli,
) ServiceTarget {
return &staticWebAppTarget{
env: env,
cli: azCli,
swa: swaCli,
}
}
// Gets the required external tools for the Static Web App target
func (at *staticWebAppTarget) RequiredExternalTools(ctx context.Context, serviceConfig *ServiceConfig) []tools.ExternalTool {
return []tools.ExternalTool{at.swa}
}
// Initializes the static web app target
func (at *staticWebAppTarget) Initialize(ctx context.Context, serviceConfig *ServiceConfig) error {
return nil
}
// Sets the build output that will be consumed for the deploy operation
func (at *staticWebAppTarget) Package(
ctx context.Context,
serviceConfig *ServiceConfig,
packageOutput *ServicePackageResult,
progress *async.Progress[ServiceProgress],
) (*ServicePackageResult, error) {
if usingSwaConfig(packageOutput) {
// The swa framework service does not set a packageOutput.PackagePath during package b/c the output
// is governed by the swa-cli.config.json file.
return packageOutput, nil
}
// packageOutput.PackagePath != "" -> This means swa framework service is not used to build/deploy and there
// is no a swa-cli.config.json file to govern the output.
// In this case, the serviceConfig.OutputPath (azure.yaml -> service -> dist) defines where is the output from
// the language framework service.
// If serviceConfig.OutputPath is not defined, azd should use the package path defined by the language framework
// service.
packagePath := serviceConfig.OutputPath
if strings.TrimSpace(packagePath) == "" {
packagePath = packageOutput.PackagePath
}
return &ServicePackageResult{
Build: packageOutput.Build,
PackagePath: packagePath,
}, nil
}
func usingSwaConfig(packageResult *ServicePackageResult) bool {
// The swa framework service does not set a packageOutput.PackagePath during package b/c the output
// is governed by the swa-cli.config.json file.
return packageResult.PackagePath == ""
}
// Deploys the packaged build output using the SWA CLI
func (at *staticWebAppTarget) Deploy(
ctx context.Context,
serviceConfig *ServiceConfig,
packageOutput *ServicePackageResult,
targetResource *environment.TargetResource,
progress *async.Progress[ServiceProgress],
) (*ServiceDeployResult, error) {
if err := at.validateTargetResource(targetResource); err != nil {
return nil, fmt.Errorf("validating target resource: %w", err)
}
// Get the static webapp deployment token
progress.SetProgress(NewServiceProgress("Retrieving deployment token"))
deploymentToken, err := at.cli.GetStaticWebAppApiKey(
ctx,
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
targetResource.ResourceName(),
)
if err != nil {
return nil, fmt.Errorf("failed retrieving static web app deployment token: %w", err)
}
// SWA performs a zip & deploy of the specified output folder and deploys it to the configured environment
progress.SetProgress(NewServiceProgress("swa cli deploy"))
dOptions := swa.DeployOptions{}
cwd := serviceConfig.Path()
if !usingSwaConfig(packageOutput) {
dOptions.AppFolderPath = serviceConfig.RelativePath
dOptions.OutputRelativeFolderPath = packageOutput.PackagePath
cwd = serviceConfig.Project.Path
}
res, err := at.swa.Deploy(ctx,
cwd,
at.env.GetTenantId(),
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
targetResource.ResourceName(),
DefaultStaticWebAppEnvironmentName,
*deploymentToken,
dOptions)
log.Println(res)
if err != nil {
return nil, fmt.Errorf("failed deploying static web app: %w", err)
}
progress.SetProgress(NewServiceProgress("Verifying deployment"))
if err := at.verifyDeployment(ctx, targetResource); err != nil {
return nil, err
}
progress.SetProgress(NewServiceProgress("Fetching endpoints for static web app"))
endpoints, err := at.Endpoints(ctx, serviceConfig, targetResource)
if err != nil {
return nil, err
}
sdr := NewServiceDeployResult(
azure.StaticWebAppRID(
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
targetResource.ResourceName(),
),
StaticWebAppTarget,
res,
endpoints,
)
sdr.Package = packageOutput
return sdr, nil
}
// Gets the endpoints for the static web app
func (at *staticWebAppTarget) Endpoints(
ctx context.Context,
serviceConfig *ServiceConfig,
targetResource *environment.TargetResource,
) ([]string, error) {
// TODO: Enhance for multi-environment support
// https://github.com/Azure/azure-dev/issues/1152
if envProps, err := at.cli.GetStaticWebAppEnvironmentProperties(
ctx,
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
targetResource.ResourceName(),
DefaultStaticWebAppEnvironmentName,
); err != nil {
return nil, fmt.Errorf("fetching service properties: %w", err)
} else {
return []string{fmt.Sprintf("https://%s/", envProps.Hostname)}, nil
}
}
func (at *staticWebAppTarget) validateTargetResource(
targetResource *environment.TargetResource,
) error {
if !strings.EqualFold(targetResource.ResourceType(), string(azapi.AzureResourceTypeStaticWebSite)) {
return resourceTypeMismatchError(
targetResource.ResourceName(),
targetResource.ResourceType(),
azapi.AzureResourceTypeStaticWebSite,
)
}
return nil
}
func (at *staticWebAppTarget) verifyDeployment(ctx context.Context, targetResource *environment.TargetResource) error {
retries := 0
const maxRetries = 10
for {
envProps, err := at.cli.GetStaticWebAppEnvironmentProperties(
ctx,
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
targetResource.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)
}
time.Sleep(5 * time.Second)
}
return nil
}