Skip to content

Commit

Permalink
Deployment Stacks: Expose configurable settings for `actionOnUnmanage…
Browse files Browse the repository at this point in the history
…` and `denySettings` (#4331)

Add new optional deploymentStacks section for the infra provider to pass settings for deployment stacks.
  • Loading branch information
wbreza authored Oct 2, 2024
1 parent 2fd5bb3 commit 5575aeb
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 52 deletions.
4 changes: 4 additions & 0 deletions cli/azd/pkg/azapi/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ type DeploymentService interface {
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) (*ResourceDeployment, error)
DeployToResourceGroup(
ctx context.Context,
Expand All @@ -153,6 +154,7 @@ type DeploymentService interface {
armTemplate azure.RawArmTemplate,
parameters azure.ArmParameters,
tags map[string]*string,
options map[string]any,
) (*ResourceDeployment, error)
ListSubscriptionDeploymentOperations(
ctx context.Context,
Expand Down Expand Up @@ -185,13 +187,15 @@ type DeploymentService interface {
ctx context.Context,
subscriptionId string,
deploymentName string,
options map[string]any,
progress *async.Progress[DeleteDeploymentProgress],
) error
DeleteResourceGroupDeployment(
ctx context.Context,
subscriptionId,
resourceGroupName string,
deploymentName string,
options map[string]any,
progress *async.Progress[DeleteDeploymentProgress],
) error
}
Expand Down
187 changes: 148 additions & 39 deletions cli/azd/pkg/azapi/stack_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ import (
"context"
"errors"
"fmt"
"log"
"maps"
"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/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"
Expand All @@ -27,16 +32,36 @@ import (
var FeatureDeploymentStacks = alpha.MustFeatureKey("deployment.stacks")

const (
stacksPortalUrlFragment = "#@microsoft.onmicrosoft.com/resource"
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,
Expand Down Expand Up @@ -215,6 +240,7 @@ func (d *StackDeployments) DeployToSubscription(
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 {
Expand All @@ -236,23 +262,20 @@ func (d *StackDeployments) DeployToSubscription(
}
}

deleteBehavior := armdeploymentstacks.DeploymentStacksDeleteDetachEnumDelete
deploymentStackOptions, err := parseDeploymentStackOptions(options)
if err != nil {
return nil, err
}

stack := armdeploymentstacks.DeploymentStack{
Location: &location,
Tags: clonedTags,
Properties: &armdeploymentstacks.DeploymentStackProperties{
BypassStackOutOfSyncError: to.Ptr(false),
ActionOnUnmanage: &armdeploymentstacks.ActionOnUnmanage{
Resources: &deleteBehavior,
ManagementGroups: &deleteBehavior,
ResourceGroups: &deleteBehavior,
},
DenySettings: &armdeploymentstacks.DenySettings{
Mode: to.Ptr(armdeploymentstacks.DenySettingsModeNone),
},
Parameters: stackParams,
Template: armTemplate,
BypassStackOutOfSyncError: deploymentStackOptions.BypassStackOutOfSyncError,
ActionOnUnmanage: deploymentStackOptions.ActionOnUnmanage,
DenySettings: deploymentStackOptions.DenySettings,
Parameters: stackParams,
Template: armTemplate,
},
}
poller, err := client.BeginCreateOrUpdateAtSubscription(ctx, deploymentName, stack, nil)
Expand All @@ -276,6 +299,7 @@ func (d *StackDeployments) DeployToResourceGroup(
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 {
Expand All @@ -297,22 +321,19 @@ func (d *StackDeployments) DeployToResourceGroup(
}
}

deleteBehavior := armdeploymentstacks.DeploymentStacksDeleteDetachEnumDelete
deploymentStackOptions, err := parseDeploymentStackOptions(options)
if err != nil {
return nil, err
}

stack := armdeploymentstacks.DeploymentStack{
Tags: clonedTags,
Properties: &armdeploymentstacks.DeploymentStackProperties{
BypassStackOutOfSyncError: to.Ptr(false),
ActionOnUnmanage: &armdeploymentstacks.ActionOnUnmanage{
Resources: &deleteBehavior,
ManagementGroups: &deleteBehavior,
ResourceGroups: &deleteBehavior,
},
DenySettings: &armdeploymentstacks.DenySettings{
Mode: to.Ptr(armdeploymentstacks.DenySettingsModeNone),
},
Parameters: stackParams,
Template: armTemplate,
BypassStackOutOfSyncError: deploymentStackOptions.BypassStackOutOfSyncError,
ActionOnUnmanage: deploymentStackOptions.ActionOnUnmanage,
DenySettings: deploymentStackOptions.DenySettings,
Parameters: stackParams,
Template: armTemplate,
},
}
poller, err := client.BeginCreateOrUpdateAtResourceGroup(ctx, resourceGroup, deploymentName, stack, nil)
Expand Down Expand Up @@ -423,19 +444,30 @@ 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
}

// Delete all resource groups & resources within the deployment stack
options := armdeploymentstacks.ClientBeginDeleteAtSubscriptionOptions{
BypassStackOutOfSyncError: to.Ptr(false),
UnmanageActionManagementGroups: to.Ptr(armdeploymentstacks.UnmanageActionManagementGroupModeDelete),
UnmanageActionResourceGroups: to.Ptr(armdeploymentstacks.UnmanageActionResourceGroupModeDelete),
UnmanageActionResources: to.Ptr(armdeploymentstacks.UnmanageActionResourceModeDelete),
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{
Expand All @@ -444,7 +476,7 @@ func (d *StackDeployments) DeleteSubscriptionDeployment(
State: DeleteResourceStateInProgress,
})

poller, err := client.BeginDeleteAtSubscription(ctx, deploymentName, &options)
poller, err := client.BeginDeleteAtSubscription(ctx, deploymentName, deleteOptions)
if err != nil {
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Expand Down Expand Up @@ -481,24 +513,101 @@ func (d *StackDeployments) DeleteSubscriptionDeployment(
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
}

// Delete all resource groups & resources within the deployment stack
options := armdeploymentstacks.ClientBeginDeleteAtResourceGroupOptions{
BypassStackOutOfSyncError: to.Ptr(false),
UnmanageActionManagementGroups: to.Ptr(armdeploymentstacks.UnmanageActionManagementGroupModeDelete),
UnmanageActionResourceGroups: to.Ptr(armdeploymentstacks.UnmanageActionResourceGroupModeDelete),
UnmanageActionResources: to.Ptr(armdeploymentstacks.UnmanageActionResourceModeDelete),
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{
Expand All @@ -507,7 +616,7 @@ func (d *StackDeployments) DeleteResourceGroupDeployment(
State: DeleteResourceStateInProgress,
})

poller, err := client.BeginDeleteAtResourceGroup(ctx, resourceGroupName, deploymentName, &options)
poller, err := client.BeginDeleteAtResourceGroup(ctx, resourceGroupName, deploymentName, deleteOptions)
if err != nil {
progress.SetProgress(DeleteDeploymentProgress{
Name: deploymentName,
Expand Down
Loading

0 comments on commit 5575aeb

Please sign in to comment.