Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion cli/azd/cmd/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,37 @@ func (p *pipelineConfigAction) Run(ctx context.Context) (*actions.ActionResult,
layers := infra.Options.GetLayers()
allParameters := []provisioning.Parameter{}

// virtualEnv contains all accumulated outputs from previous layers
virtualEnv := map[string]string{}

for _, layer := range layers {
if len(layers) > 1 {
// update current environment with accumulated outputs
layer.VirtualEnv = virtualEnv
}

err = p.provisioningManager.Initialize(ctx, p.projectConfig.Path, layer)
if err != nil {
return nil, err
}

// Pull provider specific parameters
providerParameters, err := p.provisioningManager.Parameters(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get parameters for provider %s: %w", pipelineProviderName, err)
}

allParameters = append(allParameters, providerParameters...)

outputs, err := p.provisioningManager.PlannedOutputs(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get outputs for provider %s: %w", pipelineProviderName, err)
}

// Save current outputs
for _, output := range outputs {
// save a dummy value that is easily looked at
virtualEnv[output.Name] = fmt.Sprintf("%s--%s", layer.Name, output.Name)
}
}

p.manager.SetParameters(allParameters)
Expand Down
5 changes: 5 additions & 0 deletions cli/azd/pkg/devcenter/provision_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,8 @@ func (p *ProvisionProvider) Parameters(ctx context.Context) ([]provisioning.Para
// not supported (no-op)
return nil, nil
}

func (p *ProvisionProvider) PlannedOutputs(ctx context.Context) ([]provisioning.PlannedOutput, error) {
// not supported (no-op)
return nil, nil
}
64 changes: 58 additions & 6 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go

Large diffs are not rendered by default.

122 changes: 122 additions & 0 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"io"
"maps"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -1691,6 +1693,7 @@ func TestArrayParameterViaEnvVarSimple(t *testing.T) {
"ServicePrincipal",
"testParam",
env,
nil,
)

require.Nil(t, err)
Expand All @@ -1701,6 +1704,18 @@ func TestArrayParameterViaEnvVarSimple(t *testing.T) {

func createBicepProviderWithEnv(
t *testing.T, mockContext *mocks.MockContext, armTemplate azure.ArmTemplate, envVars map[string]string) *BicepProvider {
return createBicepProviderWithEnvAndMode(t, mockContext, armTemplate, envVars, provisioning.ModeDeploy)
}

func createBicepProviderWithEnvAndMode(
t *testing.T,
mockContext *mocks.MockContext,
armTemplate azure.ArmTemplate,
envVars map[string]string,
mode provisioning.Mode,
) *BicepProvider {
t.Helper()

bicepBytes, _ := json.Marshal(armTemplate)

mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool {
Expand All @@ -1721,6 +1736,7 @@ func createBicepProviderWithEnv(
options := provisioning.Options{
Path: "infra",
Module: "main",
Mode: mode,
}

baseEnvVars := map[string]string{
Expand Down Expand Up @@ -1802,6 +1818,7 @@ func TestObjectParameterEnvSubst(t *testing.T) {
"ServicePrincipal",
"testParam",
env,
nil,
)

require.Nil(t, err)
Expand Down Expand Up @@ -1901,6 +1918,7 @@ func TestHelperEvalParamEnvSubst(t *testing.T) {
"ServicePrincipal",
"testParam",
env,
nil,
)

require.Nil(t, err)
Expand All @@ -1910,3 +1928,107 @@ func TestHelperEvalParamEnvSubst(t *testing.T) {
require.Contains(t, substResult.mappedEnvVars, "VAR2")
require.False(t, substResult.hasUnsetEnvVar)
}

func TestEvalParamEnvSubstUsesVirtualEnv(t *testing.T) {
env := environment.NewWithValues("test-env", map[string]string{})
virtualKey := "AZD_TEST_VIRTUAL_LAYER_OUTPUT"
virtualValue := "layer1--WEBSITE_URL"
virtualEnv := map[string]string{virtualKey: virtualValue}

testCases := []struct {
name string
value string
want string
}{
{
name: "simple substitution",
value: "${AZD_TEST_VIRTUAL_LAYER_OUTPUT}",
want: "layer1--WEBSITE_URL",
},
{
name: "mixed expression substitution",
value: "prefix-${AZD_TEST_VIRTUAL_LAYER_OUTPUT}-suffix",
want: "prefix-layer1--WEBSITE_URL-suffix",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, substResult, err := evalParamEnvSubst(
tc.value,
"principal-id",
"ServicePrincipal",
"testParam",
env,
virtualEnv,
)

require.NoError(t, err)
require.Equal(t, tc.want, result)
require.True(t, substResult.hasVirtualEnvVar)
require.False(t, substResult.hasUnsetEnvVar)
require.Contains(t, substResult.mappedEnvVars, virtualKey)
})
}
}

func TestEnsureParametersSkipsVirtualEnvMappedRequiredParameters(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())

armTemplate := azure.ArmTemplate{
Schema: "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
ContentVersion: "1.0.0.0",
Parameters: azure.ArmTemplateParameterDefinitions{
"environmentName": {Type: "string", DefaultValue: "test-env"},
"location": {Type: "string", DefaultValue: "westus2"},
"dependentValue": {Type: "string"},
"compositeValue": {Type: "string"},
},
Outputs: azure.ArmTemplateOutputs{},
}

infraProvider := createBicepProviderWithEnvAndMode(
t,
mockContext,
armTemplate,
map[string]string{},
provisioning.ModeDestroy,
)

tmpInfraDir := filepath.Join(t.TempDir(), "infra")
require.NoError(t, os.MkdirAll(tmpInfraDir, 0o755))

const virtualEnvKey = "AZD_TEST_VIRTUAL_LAYER_OUTPUT"
require.NoError(t, os.WriteFile(filepath.Join(tmpInfraDir, "main.parameters.json"), []byte(`{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"dependentValue": {
"value": "${AZD_TEST_VIRTUAL_LAYER_OUTPUT}"
},
"compositeValue": {
"value": "prefix-${AZD_TEST_VIRTUAL_LAYER_OUTPUT}-suffix"
}
}
}`), 0o600))

infraProvider.path = filepath.Join(tmpInfraDir, "main.bicep")
infraProvider.options.VirtualEnv = map[string]string{
virtualEnvKey: "layer1--WEBSITE_URL",
}

compileResult, err := infraProvider.compileBicep(*mockContext.Context)
require.NoError(t, err)

loadResult, err := infraProvider.loadParameters(*mockContext.Context, &compileResult.Template)
require.NoError(t, err)
require.Contains(t, loadResult.virtualMapping, "dependentValue")
require.Contains(t, loadResult.virtualMapping, "compositeValue")
require.NotContains(t, loadResult.parameters, "dependentValue")
require.NotContains(t, loadResult.parameters, "compositeValue")

configuredParameters, err := infraProvider.ensureParameters(*mockContext.Context, compileResult.Template)
require.NoError(t, err)
require.NotContains(t, configuredParameters, "dependentValue")
require.NotContains(t, configuredParameters, "compositeValue")
}
8 changes: 8 additions & 0 deletions cli/azd/pkg/infra/provisioning/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ func (m *Manager) Parameters(ctx context.Context) ([]Parameter, error) {
return m.provider.Parameters(ctx)
}

// PlannedOutputs returns the list of outputs in the current plan.
func (m *Manager) PlannedOutputs(ctx context.Context) ([]PlannedOutput, error) {
if m.provider == nil {
panic("called PlannedOutputs() with provider not initialized. Make sure to call manager.Initialize() first.")
}
return m.provider.PlannedOutputs(ctx)
}

// Gets the latest deployment details for the specified scope
func (m *Manager) State(ctx context.Context, options *StateOptions) (*StateResult, error) {
result, err := m.provider.State(ctx, options)
Expand Down
13 changes: 13 additions & 0 deletions cli/azd/pkg/infra/provisioning/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ type Options struct {
IgnoreDeploymentState bool `yaml:"-"`
// The mode in which the deployment is being run.
Mode Mode `yaml:"-"`
// Environment variables that should be considered as resolved when prompting for parameters.
//
// This is used when planning multiple layers, and would be set to plan-time outputs
// from previous layers.
VirtualEnv map[string]string `yaml:"-"`
}

// GetWithDefaults merges the provided infra options with the default provisioning options
Expand Down Expand Up @@ -179,6 +184,13 @@ type Parameter struct {
UsingEnvVarMapping bool
}

// PlannedOutput represents a plan-time output.
// It does not contain the actual output value.
type PlannedOutput struct {
// The name of the planned output
Name string
}

type Provider interface {
Name() string
Initialize(ctx context.Context, projectPath string, options Options) error
Expand All @@ -188,4 +200,5 @@ type Provider interface {
Destroy(ctx context.Context, options DestroyOptions) (*DestroyResult, error)
EnsureEnv(ctx context.Context) error
Parameters(ctx context.Context) ([]Parameter, error)
PlannedOutputs(ctx context.Context) ([]PlannedOutput, error)
}
Original file line number Diff line number Diff line change
Expand Up @@ -811,3 +811,8 @@ func (t *TerraformProvider) Parameters(ctx context.Context) ([]provisioning.Para
// not supported (no-op)
return nil, nil
}

func (t *TerraformProvider) PlannedOutputs(ctx context.Context) ([]provisioning.PlannedOutput, error) {
// not supported (no-op)
return nil, nil
}
5 changes: 5 additions & 0 deletions cli/azd/pkg/infra/provisioning/test/test_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ func (p *TestProvider) Parameters(ctx context.Context) ([]provisioning.Parameter
return nil, nil
}

func (p *TestProvider) PlannedOutputs(ctx context.Context) ([]provisioning.PlannedOutput, error) {
// not supported (no-op)
return nil, nil
}

func NewTestProvider(
envManager environment.Manager,
env *environment.Environment,
Expand Down
Loading