cli/azd/pkg/azapi/stack_deployments.go (731 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azapi
import (
"context"
"errors"
"fmt"
"log"
"maps"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/async"
"github.com/azure/azure-dev/cli/azd/pkg/azure"
"github.com/azure/azure-dev/cli/azd/pkg/cloud"
"github.com/azure/azure-dev/cli/azd/pkg/config"
"github.com/azure/azure-dev/cli/azd/pkg/convert"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/benbjohnson/clock"
"github.com/sethvargo/go-retry"
)
var FeatureDeploymentStacks = alpha.MustFeatureKey("deployment.stacks")
const (
deploymentStacksConfigKey = "DeploymentStacks"
stacksPortalUrlFragment = "#@microsoft.onmicrosoft.com/resource"
bypassOutOfSyncErrorEnvVarName = "DEPLOYMENT_STACKS_BYPASS_STACK_OUT_OF_SYNC_ERROR"
)
var defaultDeploymentStackOptions = &deploymentStackOptions{
BypassStackOutOfSyncError: to.Ptr(false),
ActionOnUnmanage: &armdeploymentstacks.ActionOnUnmanage{
ManagementGroups: to.Ptr(armdeploymentstacks.DeploymentStacksDeleteDetachEnumDelete),
ResourceGroups: to.Ptr(armdeploymentstacks.DeploymentStacksDeleteDetachEnumDelete),
Resources: to.Ptr(armdeploymentstacks.DeploymentStacksDeleteDetachEnumDelete),
},
DenySettings: &armdeploymentstacks.DenySettings{
Mode: to.Ptr(armdeploymentstacks.DenySettingsModeNone),
},
}
type StackDeployments struct {
credentialProvider account.SubscriptionCredentialProvider
armClientOptions *arm.ClientOptions
standardDeployments *StandardDeployments
cloud *cloud.Cloud
}
type deploymentStackOptions struct {
BypassStackOutOfSyncError *bool `yaml:"bypassStackOutOfSyncError,omitempty"`
ActionOnUnmanage *armdeploymentstacks.ActionOnUnmanage `yaml:"actionOnUnmanage,omitempty"`
DenySettings *armdeploymentstacks.DenySettings `yaml:"denySettings,omitempty"`
}
func NewStackDeployments(
credentialProvider account.SubscriptionCredentialProvider,
armClientOptions *arm.ClientOptions,
standardDeployments *StandardDeployments,
cloud *cloud.Cloud,
clock clock.Clock,
) *StackDeployments {
return &StackDeployments{
credentialProvider: credentialProvider,
armClientOptions: armClientOptions,
standardDeployments: standardDeployments,
cloud: cloud,
}
}
// GenerateDeploymentName creates a name to use for the deployment stack from the base name.
func (d *StackDeployments) GenerateDeploymentName(baseName string) string {
return fmt.Sprintf("azd-stack-%s", baseName)
}
func (d *StackDeployments) ListSubscriptionDeployments(
ctx context.Context,
subscriptionId string,
) ([]*ResourceDeployment, error) {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return nil, err
}
results := []*ResourceDeployment{}
pager := client.NewListAtSubscriptionPager(nil)
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, err
}
for _, deployment := range page.Value {
results = append(results, d.convertFromStackDeployment(deployment))
}
}
return results, nil
}
func (d *StackDeployments) GetSubscriptionDeployment(
ctx context.Context,
subscriptionId string,
deploymentName string,
) (*ResourceDeployment, error) {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return nil, err
}
var deploymentStack *armdeploymentstacks.DeploymentStack
err = retry.Do(
ctx,
retry.WithMaxDuration(10*time.Minute, retry.NewConstant(5*time.Second)),
func(ctx context.Context) error {
response, err := client.GetAtSubscription(ctx, deploymentName, nil)
if err != nil {
return fmt.Errorf(
"%w: '%s' in subscription '%s', Error: %w",
ErrDeploymentNotFound,
subscriptionId,
deploymentName,
err,
)
}
if response.DeploymentStack.Properties.DeploymentID == nil {
return retry.RetryableError(errors.New("deployment stack is missing ARM deployment id"))
}
deploymentStack = &response.DeploymentStack
return nil
})
if err != nil {
// If a deployment stack is not found with the given name, fallback to check for standard deployments
if errors.Is(err, ErrDeploymentNotFound) {
return d.standardDeployments.GetSubscriptionDeployment(ctx, subscriptionId, deploymentName)
}
return nil, err
}
return d.convertFromStackDeployment(deploymentStack), nil
}
func (d *StackDeployments) ListResourceGroupDeployments(
ctx context.Context,
subscriptionId string,
resourceGroupName string,
) ([]*ResourceDeployment, error) {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return nil, err
}
results := []*ResourceDeployment{}
pager := client.NewListAtResourceGroupPager(resourceGroupName, nil)
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, err
}
for _, deployment := range page.Value {
results = append(results, d.convertFromStackDeployment(deployment))
}
}
return results, nil
}
func (d *StackDeployments) GetResourceGroupDeployment(
ctx context.Context,
subscriptionId string,
resourceGroupName string,
deploymentName string,
) (*ResourceDeployment, error) {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return nil, err
}
var deploymentStack *armdeploymentstacks.DeploymentStack
err = retry.Do(
ctx,
retry.WithMaxDuration(10*time.Minute, retry.NewConstant(5*time.Second)),
func(ctx context.Context) error {
response, err := client.GetAtResourceGroup(ctx, resourceGroupName, deploymentName, nil)
if err != nil {
return fmt.Errorf(
"%w: '%s' in resource group '%s', Error: %w",
ErrDeploymentNotFound,
resourceGroupName,
deploymentName,
err,
)
}
if response.DeploymentStack.Properties.DeploymentID == nil {
return retry.RetryableError(errors.New("deployment stack is missing ARM deployment id"))
}
deploymentStack = &response.DeploymentStack
return nil
})
if err != nil {
// If a deployment stack is not found with the given name, fallback to check for standard deployments
if errors.Is(err, ErrDeploymentNotFound) {
return d.standardDeployments.GetResourceGroupDeployment(ctx, subscriptionId, resourceGroupName, deploymentName)
}
return nil, err
}
return d.convertFromStackDeployment(deploymentStack), nil
}
func (d *StackDeployments) DeployToSubscription(
ctx context.Context,
subscriptionId string,
location string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) (*ResourceDeployment, error) {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return nil, err
}
stack, err := d.stackFromArmForSubscription(ctx, subscriptionId, location, armTemplate, parameters, tags, options)
if err != nil {
return nil, err
}
poller, err := client.BeginCreateOrUpdateAtSubscription(ctx, deploymentName, stack, nil)
if err != nil {
return nil, err
}
_, err = poller.PollUntilDone(ctx, nil)
if err != nil {
deploymentError := createDeploymentError(err)
return nil, fmt.Errorf(
"deploying to subscription:\n\nDeployment Error Details:\n%w",
deploymentError,
)
}
return d.GetSubscriptionDeployment(ctx, subscriptionId, deploymentName)
}
func (d *StackDeployments) stackFromArmForResourceGroup(
ctx context.Context,
subscriptionId string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) (armdeploymentstacks.DeploymentStack, error) {
templateHash, err := d.CalculateTemplateHash(ctx, subscriptionId, armTemplate)
if err != nil {
return armdeploymentstacks.DeploymentStack{}, fmt.Errorf("failed to calculate template hash: %w", err)
}
clonedTags := maps.Clone(tags)
clonedTags[azure.TagKeyAzdDeploymentTemplateHashName] = &templateHash
stackParams := convertToStackParams(parameters)
deploymentStackOptions, err := parseDeploymentStackOptions(options)
if err != nil {
return armdeploymentstacks.DeploymentStack{}, err
}
stack := armdeploymentstacks.DeploymentStack{
Tags: clonedTags,
Properties: &armdeploymentstacks.DeploymentStackProperties{
BypassStackOutOfSyncError: deploymentStackOptions.BypassStackOutOfSyncError,
ActionOnUnmanage: deploymentStackOptions.ActionOnUnmanage,
DenySettings: deploymentStackOptions.DenySettings,
Parameters: stackParams,
Template: armTemplate,
},
}
return stack, nil
}
func (d *StackDeployments) DeployToResourceGroup(
ctx context.Context,
subscriptionId string,
resourceGroup string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) (*ResourceDeployment, error) {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return nil, err
}
stack, err := d.stackFromArmForResourceGroup(ctx, subscriptionId, armTemplate, parameters, tags, options)
if err != nil {
return nil, err
}
poller, err := client.BeginCreateOrUpdateAtResourceGroup(ctx, resourceGroup, deploymentName, stack, nil)
if err != nil {
return nil, err
}
_, err = poller.PollUntilDone(ctx, nil)
if err != nil {
deploymentError := createDeploymentError(err)
return nil, fmt.Errorf(
"deploying to resource group:\n\nDeployment Error Details:\n%w",
deploymentError,
)
}
return d.GetResourceGroupDeployment(ctx, subscriptionId, resourceGroup, deploymentName)
}
func (d *StackDeployments) ListSubscriptionDeploymentOperations(
ctx context.Context,
subscriptionId string,
deploymentName string,
) ([]*armresources.DeploymentOperation, error) {
deployment, err := d.GetSubscriptionDeployment(ctx, subscriptionId, deploymentName)
if err != nil && !errors.Is(err, ErrDeploymentNotFound) {
return nil, err
}
if deployment != nil && deployment.DeploymentId != "" {
deploymentName = filepath.Base(deployment.DeploymentId)
}
return d.standardDeployments.ListSubscriptionDeploymentOperations(ctx, subscriptionId, deploymentName)
}
func (d *StackDeployments) ListResourceGroupDeploymentOperations(
ctx context.Context,
subscriptionId string,
resourceGroupName string,
deploymentName string,
) ([]*armresources.DeploymentOperation, error) {
// The requested deployment name may be an inner deployment which will not be found in the deployment stacks.
// If this is the case continue on checking if there is a stack deployment
// If a deployment stack is found then use the deployment id of the stack
deployment, err := d.GetResourceGroupDeployment(ctx, subscriptionId, resourceGroupName, deploymentName)
if err != nil && !errors.Is(err, ErrDeploymentNotFound) {
return nil, err
}
if deployment != nil && deployment.DeploymentId != "" {
deploymentName = filepath.Base(deployment.DeploymentId)
}
return d.standardDeployments.ListResourceGroupDeploymentOperations(
ctx,
subscriptionId,
resourceGroupName,
deploymentName,
)
}
func (d *StackDeployments) WhatIfDeployToSubscription(
ctx context.Context,
subscriptionId string,
location string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
) (*armresources.WhatIfOperationResult, error) {
return nil, ErrPreviewNotSupported
}
func (d *StackDeployments) WhatIfDeployToResourceGroup(
ctx context.Context,
subscriptionId string,
resourceGroup string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
) (*armresources.WhatIfOperationResult, error) {
return nil, ErrPreviewNotSupported
}
func (d *StackDeployments) ListSubscriptionDeploymentResources(
ctx context.Context,
subscriptionId string,
deploymentName string,
) ([]*armresources.ResourceReference, error) {
deployment, err := d.GetSubscriptionDeployment(ctx, subscriptionId, deploymentName)
if err != nil {
return nil, err
}
return deployment.Resources, nil
}
func (d *StackDeployments) ListResourceGroupDeploymentResources(
ctx context.Context,
subscriptionId string,
resourceGroupName string,
deploymentName string,
) ([]*armresources.ResourceReference, error) {
deployment, err := d.GetResourceGroupDeployment(ctx, subscriptionId, resourceGroupName, deploymentName)
if err != nil {
return nil, err
}
return deployment.Resources, nil
}
func (d *StackDeployments) DeleteSubscriptionDeployment(
ctx context.Context,
subscriptionId string,
deploymentName string,
options map[string]any,
progress *async.Progress[DeleteDeploymentProgress],
) error {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return err
}
deploymentStackOptions, err := parseDeploymentStackOptions(options)
if err != nil {
return err
}
deleteOptions := &armdeploymentstacks.ClientBeginDeleteAtSubscriptionOptions{
BypassStackOutOfSyncError: deploymentStackOptions.BypassStackOutOfSyncError,
UnmanageActionManagementGroups: (*armdeploymentstacks.UnmanageActionManagementGroupMode)(
deploymentStackOptions.ActionOnUnmanage.ManagementGroups,
),
UnmanageActionResourceGroups: (*armdeploymentstacks.UnmanageActionResourceGroupMode)(
deploymentStackOptions.ActionOnUnmanage.ResourceGroups,
),
UnmanageActionResources: (*armdeploymentstacks.UnmanageActionResourceMode)(
deploymentStackOptions.ActionOnUnmanage.Resources,
),
}
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Message: fmt.Sprintf("Deleting subscription deployment stack %s", output.WithHighLightFormat(deploymentName)),
State: DeleteResourceStateInProgress,
})
poller, err := client.BeginDeleteAtSubscription(ctx, deploymentName, deleteOptions)
if err != nil {
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Message: fmt.Sprintf(
"Failed deleting subscription deployment stack %s",
output.WithHighLightFormat(deploymentName),
),
State: DeleteResourceStateFailed,
})
return err
}
_, err = poller.PollUntilDone(ctx, nil)
if err != nil {
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Message: fmt.Sprintf(
"Failed deleting subscription deployment stack %s",
output.WithHighLightFormat(deploymentName),
),
State: DeleteResourceStateFailed,
})
return err
}
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Message: fmt.Sprintf("Deleted subscription deployment stack %s", output.WithHighLightFormat(deploymentName)),
State: DeleteResourceStateSucceeded,
})
return nil
}
// parseDeploymentStackOptions parses the deployment stack options from the given options map.
// If the options map is nil, the default deployment stack options are returned.
// Default deployment stack options are:
// - BypassStackOutOfSyncError: false
// - ActionOnUnmanage: Delete for all
// - DenySettings: nil
func parseDeploymentStackOptions(options map[string]any) (*deploymentStackOptions, error) {
bypassStackOutOfSyncErrorVal, hasBypassStackOutOfSyncError := os.LookupEnv(bypassOutOfSyncErrorEnvVarName)
if options == nil && !hasBypassStackOutOfSyncError {
return defaultDeploymentStackOptions, nil
}
optionsConfig := config.NewConfig(options)
var deploymentStackOptions *deploymentStackOptions
hasDeploymentStacksConfig, err := optionsConfig.GetSection(deploymentStacksConfigKey, &deploymentStackOptions)
if err != nil {
suggestion := &internal.ErrorWithSuggestion{
Err: fmt.Errorf("failed parsing deployment stack options: %w", err),
Suggestion: "Review the 'infra.deploymentStacks' configuration section in the 'azure.yaml' file.",
}
return nil, suggestion
}
if !hasBypassStackOutOfSyncError && (!hasDeploymentStacksConfig || deploymentStackOptions == nil) {
return defaultDeploymentStackOptions, nil
}
if deploymentStackOptions == nil {
deploymentStackOptions = defaultDeploymentStackOptions
}
// The BypassStackOutOfSyncError will NOT be exposed in the `azure.yaml` for configuration
// since this option will typically only be used on a per call basis.
// The value will be read from the environment variable `DEPLOYMENT_STACKS_BYPASS_STACK_OUT_OF_SYNC_ERROR`
// If the value is a truthy value, the value will be set to true, otherwise it will be set to false (default)
if hasBypassStackOutOfSyncError {
byPassOutOfSyncError, err := strconv.ParseBool(bypassStackOutOfSyncErrorVal)
if err != nil {
log.Printf(
"Failed to parse environment variable '%s' value '%s' as a boolean. Defaulting to false.",
bypassOutOfSyncErrorEnvVarName,
bypassStackOutOfSyncErrorVal,
)
} else {
deploymentStackOptions.BypassStackOutOfSyncError = &byPassOutOfSyncError
}
}
if deploymentStackOptions.BypassStackOutOfSyncError == nil {
deploymentStackOptions.BypassStackOutOfSyncError = defaultDeploymentStackOptions.BypassStackOutOfSyncError
}
if deploymentStackOptions.ActionOnUnmanage == nil {
deploymentStackOptions.ActionOnUnmanage = defaultDeploymentStackOptions.ActionOnUnmanage
}
if deploymentStackOptions.DenySettings == nil {
deploymentStackOptions.DenySettings = defaultDeploymentStackOptions.DenySettings
}
return deploymentStackOptions, nil
}
func (d *StackDeployments) DeleteResourceGroupDeployment(
ctx context.Context,
subscriptionId,
resourceGroupName string,
deploymentName string,
options map[string]any,
progress *async.Progress[DeleteDeploymentProgress],
) error {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return err
}
deploymentStackOptions, err := parseDeploymentStackOptions(options)
if err != nil {
return err
}
deleteOptions := &armdeploymentstacks.ClientBeginDeleteAtResourceGroupOptions{
BypassStackOutOfSyncError: deploymentStackOptions.BypassStackOutOfSyncError,
UnmanageActionManagementGroups: (*armdeploymentstacks.UnmanageActionManagementGroupMode)(
deploymentStackOptions.ActionOnUnmanage.ManagementGroups,
),
UnmanageActionResourceGroups: (*armdeploymentstacks.UnmanageActionResourceGroupMode)(
deploymentStackOptions.ActionOnUnmanage.ResourceGroups,
),
UnmanageActionResources: (*armdeploymentstacks.UnmanageActionResourceMode)(
deploymentStackOptions.ActionOnUnmanage.Resources,
),
}
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Message: fmt.Sprintf("Deleting resource group deployment stack %s", output.WithHighLightFormat(deploymentName)),
State: DeleteResourceStateInProgress,
})
poller, err := client.BeginDeleteAtResourceGroup(ctx, resourceGroupName, deploymentName, deleteOptions)
if err != nil {
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Message: fmt.Sprintf(
"Failed deleting resource group deployment stack %s",
output.WithHighLightFormat(deploymentName),
),
State: DeleteResourceStateFailed,
})
return err
}
_, err = poller.PollUntilDone(ctx, nil)
if err != nil {
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Message: fmt.Sprintf(
"Failed deleting resource group deployment stack %s",
output.WithHighLightFormat(deploymentName),
),
State: DeleteResourceStateFailed,
})
return err
}
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Message: fmt.Sprintf("Deleted resource group deployment stack %s", output.WithHighLightFormat(deploymentName)),
State: DeleteResourceStateSucceeded,
})
return nil
}
func (d *StackDeployments) CalculateTemplateHash(
ctx context.Context,
subscriptionId string,
template azure.RawArmTemplate,
) (string, error) {
return d.standardDeployments.CalculateTemplateHash(ctx, subscriptionId, template)
}
func (d *StackDeployments) createClient(ctx context.Context, subscriptionId string) (*armdeploymentstacks.Client, error) {
credential, err := d.credentialProvider.CredentialForSubscription(ctx, subscriptionId)
if err != nil {
return nil, err
}
return armdeploymentstacks.NewClient(subscriptionId, credential, d.armClientOptions)
}
// Converts from an ARM Extended Deployment to Azd Generic deployment
func (d *StackDeployments) convertFromStackDeployment(deployment *armdeploymentstacks.DeploymentStack) *ResourceDeployment {
resources := []*armresources.ResourceReference{}
for _, resource := range deployment.Properties.Resources {
resources = append(resources, &armresources.ResourceReference{ID: resource.ID})
}
deploymentId := convert.ToValueWithDefault(deployment.Properties.DeploymentID, "")
return &ResourceDeployment{
Id: *deployment.ID,
Location: convert.ToValueWithDefault(deployment.Location, ""),
DeploymentId: deploymentId,
Name: *deployment.Name,
Type: *deployment.Type,
Tags: deployment.Tags,
ProvisioningState: convertFromStacksProvisioningState(*deployment.Properties.ProvisioningState),
Timestamp: *deployment.SystemData.LastModifiedAt,
TemplateHash: deployment.Tags[azure.TagKeyAzdDeploymentTemplateHashName],
Outputs: deployment.Properties.Outputs,
Resources: resources,
Dependencies: []*armresources.Dependency{},
PortalUrl: fmt.Sprintf("%s/%s/%s",
d.cloud.PortalUrlBase,
stacksPortalUrlFragment,
*deployment.ID,
),
OutputsUrl: fmt.Sprintf("%s/%s/%s/outputs",
d.cloud.PortalUrlBase,
stacksPortalUrlFragment,
*deployment.ID,
),
DeploymentUrl: fmt.Sprintf("%s/%s/%s",
d.cloud.PortalUrlBase,
cPortalUrlFragment,
url.PathEscape(deploymentId),
),
}
}
func convertFromStacksProvisioningState(
state armdeploymentstacks.DeploymentStackProvisioningState,
) DeploymentProvisioningState {
switch state {
case armdeploymentstacks.DeploymentStackProvisioningStateCanceled:
return DeploymentProvisioningStateCanceled
case armdeploymentstacks.DeploymentStackProvisioningStateCanceling:
return DeploymentProvisioningStateCanceling
case armdeploymentstacks.DeploymentStackProvisioningStateCreating:
return DeploymentProvisioningStateCreating
case armdeploymentstacks.DeploymentStackProvisioningStateDeleting:
return DeploymentProvisioningStateDeleting
case armdeploymentstacks.DeploymentStackProvisioningStateDeletingResources:
return DeploymentProvisioningStateDeletingResources
case armdeploymentstacks.DeploymentStackProvisioningStateDeploying:
return DeploymentProvisioningStateDeploying
case armdeploymentstacks.DeploymentStackProvisioningStateFailed:
return DeploymentProvisioningStateFailed
case armdeploymentstacks.DeploymentStackProvisioningStateSucceeded:
return DeploymentProvisioningStateSucceeded
case armdeploymentstacks.DeploymentStackProvisioningStateUpdatingDenyAssignments:
return DeploymentProvisioningStateUpdatingDenyAssignments
case armdeploymentstacks.DeploymentStackProvisioningStateValidating:
return DeploymentProvisioningStateValidating
case armdeploymentstacks.DeploymentStackProvisioningStateWaiting:
return DeploymentProvisioningStateWaiting
}
return DeploymentProvisioningState("")
}
// convertToStackParams converts the given ARM parameters to deployment stack parameters
func convertToStackParams(parameters azure.ArmParameters) map[string]*armdeploymentstacks.DeploymentParameter {
stackParams := map[string]*armdeploymentstacks.DeploymentParameter{}
for k, v := range parameters {
if v.KeyVaultReference != nil {
stackParams[k] = &armdeploymentstacks.DeploymentParameter{
Reference: &armdeploymentstacks.KeyVaultParameterReference{
KeyVault: &armdeploymentstacks.KeyVaultReference{
ID: &v.KeyVaultReference.KeyVault.ID,
},
SecretName: &v.KeyVaultReference.SecretName,
SecretVersion: &v.KeyVaultReference.SecretVersion,
},
}
} else {
stackParams[k] = &armdeploymentstacks.DeploymentParameter{
Value: v.Value,
}
}
}
return stackParams
}
// Preflight API validates whether the specified template is syntactically correct
// and will be accepted by Azure Resource Manager.
func (d *StackDeployments) ValidatePreflightToResourceGroup(
ctx context.Context,
subscriptionId string,
resourceGroup string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return err
}
stack, err := d.stackFromArmForResourceGroup(ctx, subscriptionId, armTemplate, parameters, tags, options)
if err != nil {
return err
}
var rawResponse *http.Response
ctx = policy.WithCaptureResponse(ctx, &rawResponse)
validateResult, err := client.BeginValidateStackAtResourceGroup(ctx, resourceGroup, deploymentName, stack, nil)
if err != nil {
return validatePreflightError(rawResponse, err, "resource group")
}
_, err = validateResult.PollUntilDone(ctx, nil)
if err != nil {
return validatePreflightError(rawResponse, err, "resource group")
}
return nil
}
func (d *StackDeployments) stackFromArmForSubscription(
ctx context.Context,
subscriptionId string,
location string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) (armdeploymentstacks.DeploymentStack, error) {
templateHash, err := d.CalculateTemplateHash(ctx, subscriptionId, armTemplate)
if err != nil {
return armdeploymentstacks.DeploymentStack{}, fmt.Errorf("failed to calculate template hash: %w", err)
}
clonedTags := maps.Clone(tags)
clonedTags[azure.TagKeyAzdDeploymentTemplateHashName] = &templateHash
stackParams := convertToStackParams(parameters)
deploymentStackOptions, err := parseDeploymentStackOptions(options)
if err != nil {
return armdeploymentstacks.DeploymentStack{}, err
}
stack := armdeploymentstacks.DeploymentStack{
Location: &location,
Tags: clonedTags,
Properties: &armdeploymentstacks.DeploymentStackProperties{
BypassStackOutOfSyncError: deploymentStackOptions.BypassStackOutOfSyncError,
ActionOnUnmanage: deploymentStackOptions.ActionOnUnmanage,
DenySettings: deploymentStackOptions.DenySettings,
Parameters: stackParams,
Template: armTemplate,
},
}
return stack, nil
}
// Preflight API validates whether the specified template is syntactically correct
// and will be accepted by Azure Resource Manager.
func (d *StackDeployments) ValidatePreflightToSubscription(
ctx context.Context,
subscriptionId string,
location string,
deploymentName string,
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) error {
client, err := d.createClient(ctx, subscriptionId)
if err != nil {
return err
}
stack, err := d.stackFromArmForSubscription(ctx, subscriptionId, location, armTemplate, parameters, tags, options)
if err != nil {
return err
}
var rawResponse *http.Response
ctx = policy.WithCaptureResponse(ctx, &rawResponse)
validateResult, err := client.BeginValidateStackAtSubscription(ctx, deploymentName, stack, nil)
if err != nil {
return validatePreflightError(rawResponse, err, "subscription")
}
_, err = validateResult.PollUntilDone(ctx, nil)
if err != nil {
return validatePreflightError(rawResponse, err, "subscription")
}
return nil
}