-
Notifications
You must be signed in to change notification settings - Fork 332
fix(provision): support ${VAR} substitution in infra.deploymentStacks #9238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
vhvb1989
wants to merge
6
commits into
main
Choose a base branch
from
expandable-strings-infra-layers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b4608b7
fix(provision): support ${VAR} substitution in infra.deploymentStacks
vhvb1989 4d5d3d1
fix: address Copilot review feedback (iteration 1)
vhvb1989 ed0f030
fix: address Copilot review feedback (iteration 2)
vhvb1989 b6cd306
fix: address Copilot review feedback (iteration 3)
vhvb1989 229d2a9
Merge remote-tracking branch 'origin/main' into expandable-strings-in…
vhvb1989 2c6548a
test: raise coverage for deployment stacks changes
vhvb1989 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
198 changes: 198 additions & 0 deletions
198
cli/azd/pkg/infra/provisioning/bicep/deployment_stacks.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package bicep | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/azure/azure-dev/cli/azd/pkg/alpha" | ||
| "github.com/azure/azure-dev/cli/azd/pkg/azapi" | ||
| "github.com/azure/azure-dev/cli/azd/pkg/convert" | ||
| "github.com/azure/azure-dev/cli/azd/pkg/osutil" | ||
| ) | ||
|
|
||
| // deploymentStacksEnabled reports whether the Deployment Stacks alpha feature is active. This | ||
| // mirrors the deployment-service selection in cmd/container.go: only when the feature is enabled | ||
| // is the stacks deployment service used (and the deployment-stacks options actually consumed). | ||
| func (p *BicepProvider) deploymentStacksEnabled() bool { | ||
| var featureManager *alpha.FeatureManager | ||
| if err := p.serviceLocator.Resolve(&featureManager); err != nil { | ||
| return false | ||
| } | ||
|
|
||
| return featureManager.IsEnabled(azapi.FeatureDeploymentStacks) | ||
| } | ||
|
|
||
| // hasActiveDeploymentStacksConfig reports whether an effective deployment-stacks configuration is | ||
| // present (config supplied AND the alpha feature enabled). When true, the deployment-state shortcut | ||
| // must be bypassed: the stacks deny/unmanage settings — including ${VAR}-resolved deny lists — can | ||
| // change independently of the ARM template and parameters that the state hash covers. Skipping the | ||
| // deployment would otherwise leave stale deny settings on the stack and would also bypass the | ||
| // ${VAR} resolution/validation performed while building the options map. | ||
| func (p *BicepProvider) hasActiveDeploymentStacksConfig() bool { | ||
| return p.options.DeploymentStacks != nil && p.deploymentStacksEnabled() | ||
| } | ||
|
|
||
| // useDeploymentStateShortcut reports whether the deployment-state shortcut (which skips a | ||
| // redeploy when the template and parameters are unchanged) may be used. It returns false when | ||
| // deployment-state tracking is disabled (--no-state), when the parameters hash could not be | ||
| // computed, or when an active deployment-stacks configuration is present. In the stacks case the | ||
| // deny/unmanage settings — including ${VAR}-resolved deny lists — can change independently of the | ||
| // template+parameters the state hash covers, so the shortcut must be bypassed to avoid leaving | ||
| // stale settings on the stack and to preserve ${VAR} resolution/validation. | ||
| func (p *BicepProvider) useDeploymentStateShortcut(parametersHashErr error) bool { | ||
| if p.ignoreDeploymentState || parametersHashErr != nil { | ||
| return false | ||
| } | ||
|
|
||
| return !p.hasActiveDeploymentStacksConfig() | ||
| } | ||
|
|
||
| // resolveDeploymentStacksMap resolves the typed deployment-stacks configuration into a | ||
| // camelCase map[string]any consumable by the deployment-stacks API layer | ||
| // (azapi.parseDeploymentStackOptions). It performs ${VAR} environment-variable substitution | ||
| // on denySettings.excludedPrincipals and denySettings.excludedActions, resolving values from | ||
| // plan-time layer outputs (VirtualEnv) first and then the azd environment. | ||
| // | ||
| // When includeDenySettings is false the denySettings block is omitted entirely (and therefore | ||
| // not resolved). The stack delete APIs only consume actionOnUnmanage and the bypass flag, so the | ||
| // destroy path passes false to avoid failing `azd down` when a ${VAR} referenced only by the deny | ||
| // lists is no longer available. | ||
| // | ||
| // It returns nil when no deployment-stacks configuration is present, in which case the caller | ||
| // should omit the DeploymentStacks key entirely so the API layer applies its defaults. | ||
| func (p *BicepProvider) resolveDeploymentStacksMap(includeDenySettings bool) (map[string]any, error) { | ||
| cfg := p.options.DeploymentStacks | ||
| if cfg == nil { | ||
| return nil, nil | ||
| } | ||
|
|
||
| result := map[string]any{} | ||
|
|
||
| if cfg.ActionOnUnmanage != nil { | ||
| actionOnUnmanage := map[string]any{} | ||
| if cfg.ActionOnUnmanage.Resources != "" { | ||
| actionOnUnmanage["resources"] = cfg.ActionOnUnmanage.Resources | ||
| } | ||
| if cfg.ActionOnUnmanage.ResourceGroups != "" { | ||
| actionOnUnmanage["resourceGroups"] = cfg.ActionOnUnmanage.ResourceGroups | ||
| } | ||
| if cfg.ActionOnUnmanage.ManagementGroups != "" { | ||
| actionOnUnmanage["managementGroups"] = cfg.ActionOnUnmanage.ManagementGroups | ||
| } | ||
| result["actionOnUnmanage"] = actionOnUnmanage | ||
| } | ||
|
|
||
| if includeDenySettings && cfg.DenySettings != nil { | ||
| denySettings := map[string]any{} | ||
| if cfg.DenySettings.Mode != "" { | ||
| denySettings["mode"] = cfg.DenySettings.Mode | ||
| } | ||
| if cfg.DenySettings.ApplyToChildScopes != nil { | ||
| denySettings["applyToChildScopes"] = *cfg.DenySettings.ApplyToChildScopes | ||
| } | ||
|
|
||
| excludedActions, err := p.resolveDeploymentStacksValues(cfg.DenySettings.ExcludedActions) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if excludedActions != nil { | ||
| denySettings["excludedActions"] = excludedActions | ||
| } | ||
|
|
||
| excludedPrincipals, err := p.resolveDeploymentStacksValues(cfg.DenySettings.ExcludedPrincipals) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if excludedPrincipals != nil { | ||
| denySettings["excludedPrincipals"] = excludedPrincipals | ||
| } | ||
|
|
||
| result["denySettings"] = denySettings | ||
| } | ||
|
|
||
| return result, nil | ||
| } | ||
|
|
||
| // resolveDeploymentStacksValues evaluates ${VAR} references in each value, resolving from the | ||
| // plan-time layer outputs (VirtualEnv) first and then the azd environment. A value that resolves | ||
| // to an empty string is treated as a misconfiguration (a blank deny-list entry, for example an | ||
| // empty excluded principal, is almost always a mistake) and returns an error. Shell-style default | ||
| // expressions such as ${VAR:-fallback} are honored: Envsubst applies the default before this check, | ||
| // so a reference with a usable default yields a non-blank value and is accepted. | ||
| func (p *BicepProvider) resolveDeploymentStacksValues(values []osutil.ExpandableString) ([]string, error) { | ||
| if len(values) == 0 { | ||
| return nil, nil | ||
| } | ||
|
|
||
| resolved := make([]string, 0, len(values)) | ||
| for _, value := range values { | ||
| var lookedUpEmpty []string | ||
| substituted, err := value.Envsubst(func(name string) string { | ||
| if p.options.VirtualEnv != nil { | ||
| if v, has := p.options.VirtualEnv[name]; has { | ||
| return v | ||
| } | ||
| } | ||
|
|
||
| v, ok := p.env.LookupEnv(name) | ||
| if !ok || v == "" { | ||
| lookedUpEmpty = append(lookedUpEmpty, name) | ||
| } | ||
| return v | ||
| }) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("resolving deploymentStacks value: %w", err) | ||
| } | ||
|
|
||
| if strings.TrimSpace(substituted) == "" { | ||
| if len(lookedUpEmpty) > 0 { | ||
| return nil, fmt.Errorf( | ||
| "deploymentStacks references unset environment variable(s): %s", | ||
| strings.Join(lookedUpEmpty, ", ")) | ||
| } | ||
| return nil, fmt.Errorf("deploymentStacks value resolved to an empty string") | ||
| } | ||
|
|
||
| resolved = append(resolved, substituted) | ||
| } | ||
|
|
||
| return resolved, nil | ||
| } | ||
|
|
||
| // deploymentOptionsMap builds the generic options map handed to the deployment API layer, | ||
| // with the deployment-stacks configuration resolved (including ${VAR} substitution). Standard | ||
| // (non-stack) deployments ignore this map; stack deployments read only the DeploymentStacks key. | ||
| // | ||
| // Resolution is skipped entirely when the Deployment Stacks alpha feature is inactive, so an | ||
| // otherwise-valid standard provision can't be failed by an unavailable ${VAR} in an inactive | ||
| // deploymentStacks block. includeDenySettings is false on the destroy path, where the deny lists | ||
| // are not consumed by the stack delete APIs. | ||
| func (p *BicepProvider) deploymentOptionsMap(includeDenySettings bool) (map[string]any, error) { | ||
| optionsMap, err := convert.ToMap(p.options) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // The typed DeploymentStacksConfig is not JSON-serializable in a form the API layer can | ||
| // consume (ExpandableString has no exported fields), so always drop whatever convert.ToMap | ||
| // produced for it and only re-add a resolved map when stacks are actually in play. | ||
| delete(optionsMap, "DeploymentStacks") | ||
|
|
||
| if !p.deploymentStacksEnabled() { | ||
| return optionsMap, nil | ||
| } | ||
|
|
||
| stacks, err := p.resolveDeploymentStacksMap(includeDenySettings) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if stacks != nil { | ||
| optionsMap["DeploymentStacks"] = stacks | ||
| } | ||
|
|
||
| return optionsMap, nil | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.