diff --git a/cli/azd/pkg/apphost/service_ingress_configurer.go b/cli/azd/pkg/apphost/service_ingress_configurer.go index f51dcde0240..7c45d7513cb 100644 --- a/cli/azd/pkg/apphost/service_ingress_configurer.go +++ b/cli/azd/pkg/apphost/service_ingress_configurer.go @@ -37,8 +37,9 @@ func (adc *IngressSelector) SelectPublicServices(ctx context.Context) ([]string, "it is running in. Selecting a service here will also allow it to be reached from the Internet.") exposed, err := adc.console.MultiSelect(ctx, input.ConsoleOptions{ - Message: "Select which services to expose to the Internet", - Options: services, + Message: "Select which services to expose to the Internet", + Options: services, + DefaultValue: []string{}, }) if err != nil { return nil, err diff --git a/cli/azd/pkg/azdo/pipeline.go b/cli/azd/pkg/azdo/pipeline.go index 57304f43fe3..a9a9366ed6e 100644 --- a/cli/azd/pkg/azdo/pipeline.go +++ b/cli/azd/pkg/azdo/pipeline.go @@ -93,7 +93,8 @@ func CreatePipeline( credentials *azcli.AzureCredentials, env *environment.Environment, console input.Console, - provisioningProvider provisioning.Options) (*build.BuildDefinition, error) { + provisioningProvider provisioning.Options, + additionalSecrets map[string]string) (*build.BuildDefinition, error) { client, err := build.NewClient(ctx, connection) if err != nil { @@ -110,7 +111,7 @@ func CreatePipeline( // Pipeline is already created. It uses the same connection but // we need to update the variables and secrets as they // might have been updated - buildDefinitionVariables, err := getDefinitionVariables(env, credentials, provisioningProvider) + buildDefinitionVariables, err := getDefinitionVariables(env, credentials, provisioningProvider, additionalSecrets) if err != nil { return nil, err } @@ -132,7 +133,7 @@ func CreatePipeline( } createDefinitionArgs, err := createAzureDevPipelineArgs( - ctx, projectId, name, repoName, credentials, env, queue, provisioningProvider) + ctx, projectId, name, repoName, credentials, env, queue, provisioningProvider, additionalSecrets) if err != nil { return nil, err } @@ -148,7 +149,8 @@ func CreatePipeline( func getDefinitionVariables( env *environment.Environment, credentials *azcli.AzureCredentials, - provisioningProvider provisioning.Options) (*map[string]build.BuildDefinitionVariable, error) { + provisioningProvider provisioning.Options, + additionalSecrets map[string]string) (*map[string]build.BuildDefinitionVariable, error) { rawCredential, err := json.Marshal(credentials) if err != nil { return nil, err @@ -184,6 +186,11 @@ Visit %s for more information on configuring Terraform remote state`, variables[key] = createBuildDefinitionVariable(value, false, true) } } + + for key, value := range additionalSecrets { + variables[key] = createBuildDefinitionVariable(value, true, false) + } + return &variables, nil } @@ -197,6 +204,7 @@ func createAzureDevPipelineArgs( env *environment.Environment, queue *taskagent.TaskAgentQueue, provisioningProvider provisioning.Options, + additionalSecrets map[string]string, ) (*build.CreateDefinitionArgs, error) { repoType := "tfsgit" @@ -233,7 +241,7 @@ func createAzureDevPipelineArgs( trigger, } - buildDefinitionVariables, err := getDefinitionVariables(env, credentials, provisioningProvider) + buildDefinitionVariables, err := getDefinitionVariables(env, credentials, provisioningProvider, additionalSecrets) if err != nil { return nil, err } diff --git a/cli/azd/pkg/environment/environment.go b/cli/azd/pkg/environment/environment.go index e5640dcdc3c..9c375e0c4c7 100644 --- a/cli/azd/pkg/environment/environment.go +++ b/cli/azd/pkg/environment/environment.go @@ -5,7 +5,9 @@ package environment import ( "context" + "encoding/json" "fmt" + "log" "os" "regexp" "strings" @@ -57,19 +59,36 @@ type Environment struct { Config config.Config } +const AzdInitialEnvironmentConfigName = "AZD_INITIAL_ENVIRONMENT_CONFIG" + // New returns a new environment with the specified name. func New(name string) *Environment { env := &Environment{ name: name, dotenv: make(map[string]string), deletedKeys: make(map[string]struct{}), - Config: config.NewEmptyConfig(), + Config: getInitialConfig(), } env.SetEnvName(name) return env } +func getInitialConfig() config.Config { + initialConfig := os.Getenv(AzdInitialEnvironmentConfigName) + if initialConfig == "" { + return config.NewEmptyConfig() + } + + var initConfig map[string]any + if err := json.Unmarshal([]byte(initialConfig), &initConfig); err != nil { + log.Println("Failed to unmarshal initial config", err, "Using empty config.") + return config.NewEmptyConfig() + } + + return config.NewConfig(initConfig) +} + // NewWithValues returns an ephemeral environment (i.e. not backed by a data store) with a set // of values. Useful for testing. The name parameter is added to the environment with the // AZURE_ENV_NAME key, replacing an existing value in the provided values map. A nil values is diff --git a/cli/azd/pkg/environment/environment_test.go b/cli/azd/pkg/environment/environment_test.go index d3822d6ce51..02cabae9ca3 100644 --- a/cli/azd/pkg/environment/environment_test.go +++ b/cli/azd/pkg/environment/environment_test.go @@ -5,6 +5,7 @@ package environment import ( "context" + "encoding/json" "os" "path/filepath" "testing" @@ -196,6 +197,70 @@ func TestRoundTripNumberWithLeadingZeros(t *testing.T) { require.Equal(t, "01", env2.dotenv["TEST"]) } +const configSample = `{ + "infra": { + "parameters": { + "bro": "xms" + } + }, + "services": { + "app": { + "config": { + "exposedServices": [ + "webapp" + ] + } + } + } + }` + +func TestInitialEnvState(t *testing.T) { + + // expected config + var configEncode map[string]any + err := json.Unmarshal([]byte(configSample), &configEncode) + require.NoError(t, err) + configBytes, err := json.Marshal(configEncode) + require.NoError(t, err) + + // Set up the environment variable + t.Setenv(AzdInitialEnvironmentConfigName, string(configBytes)) + + // Create the environment + env := New("test") + + // pull config back and compare against expected + config := env.Config.Raw() + require.Equal(t, configEncode, config) +} + +func TestInitialEnvStateWithError(t *testing.T) { + + // Set up the environment variable + t.Setenv(AzdInitialEnvironmentConfigName, "not{}valid{}json") + + // Create the environment + env := New("test") + + // pull unexpectedConfig back and compare + unexpectedConfig := env.Config.Raw() + expected := config.NewEmptyConfig().Raw() + require.Equal(t, expected, unexpectedConfig) +} + +func TestInitialEnvStateEmpty(t *testing.T) { + + // expected config + expected := config.NewEmptyConfig().Raw() + + // Create the environment + env := New("test") + + // pull config back and compare against expected + config := env.Config.Raw() + require.Equal(t, expected, config) +} + func Test_fixupUnquotedDotenv(t *testing.T) { test := map[string]string{ "TEST_SHOULD_NOT_QUOTE": "1", diff --git a/cli/azd/pkg/pipeline/azdo_provider.go b/cli/azd/pkg/pipeline/azdo_provider.go index c3596a7bf60..a27c4bd7a3a 100644 --- a/cli/azd/pkg/pipeline/azdo_provider.go +++ b/cli/azd/pkg/pipeline/azdo_provider.go @@ -757,6 +757,7 @@ func (p *AzdoCiProvider) configurePipeline( ctx context.Context, repoDetails *gitRepositoryDetails, provisioningProvider provisioning.Options, + additionalSecrets map[string]string, ) (CiPipeline, error) { details := repoDetails.details.(*AzdoRepositoryDetails) @@ -782,6 +783,7 @@ func (p *AzdoCiProvider) configurePipeline( p.Env, p.console, provisioningProvider, + additionalSecrets, ) if err != nil { return nil, err diff --git a/cli/azd/pkg/pipeline/github_provider.go b/cli/azd/pkg/pipeline/github_provider.go index 859e04f2d26..e03a0e09d27 100644 --- a/cli/azd/pkg/pipeline/github_provider.go +++ b/cli/azd/pkg/pipeline/github_provider.go @@ -625,7 +625,16 @@ func (p *GitHubCiProvider) configurePipeline( ctx context.Context, repoDetails *gitRepositoryDetails, provisioningProvider provisioning.Options, + additionalSecrets map[string]string, ) (CiPipeline, error) { + + repoSlug := repoDetails.owner + "/" + repoDetails.repoName + for key, value := range additionalSecrets { + if err := p.ghCli.SetSecret(ctx, repoSlug, key, value); err != nil { + return nil, fmt.Errorf("failed setting %s secret: %w", key, err) + } + } + return &workflow{ repoDetails: repoDetails, }, nil diff --git a/cli/azd/pkg/pipeline/pipeline.go b/cli/azd/pkg/pipeline/pipeline.go index df376f3230d..cd2066f87b3 100644 --- a/cli/azd/pkg/pipeline/pipeline.go +++ b/cli/azd/pkg/pipeline/pipeline.go @@ -96,6 +96,7 @@ type CiProvider interface { ctx context.Context, repoDetails *gitRepositoryDetails, provisioningProvider provisioning.Options, + additionalSecrets map[string]string, ) (CiPipeline, error) // configureConnection use the credential to set up the connection from the pipeline // to Azure diff --git a/cli/azd/pkg/pipeline/pipeline_manager.go b/cli/azd/pkg/pipeline/pipeline_manager.go index c8e2553833a..ddaa75b56f3 100644 --- a/cli/azd/pkg/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/pipeline/pipeline_manager.go @@ -5,6 +5,7 @@ package pipeline import ( "context" + "encoding/json" "errors" "fmt" "log" @@ -330,8 +331,20 @@ func (pm *PipelineManager) Configure(ctx context.Context) (result *PipelineConfi return result, err } + // Adding environment.AzdInitialEnvironmentConfigName as a secret to the pipeline as the base configuration for + // whenever a new environment is created. This means loading the local environment config into a pipeline secret which + // azd will use to restore the the config on CI + localEnvConfig, err := json.Marshal(pm.env.Config.Raw()) + if err != nil { + return result, fmt.Errorf("failed to marshal environment config: %w", err) + } + + additionalSecrets := map[string]string{ + environment.AzdInitialEnvironmentConfigName: string(localEnvConfig), + } + // config pipeline handles setting or creating the provider pipeline to be used - ciPipeline, err := pm.ciProvider.configurePipeline(ctx, gitRepoInfo, infra.Options) + ciPipeline, err := pm.ciProvider.configurePipeline(ctx, gitRepoInfo, infra.Options, additionalSecrets) if err != nil { return result, err } diff --git a/cli/azd/pkg/project/dotnet_importer.go b/cli/azd/pkg/project/dotnet_importer.go index 38f25783492..2be34f710c7 100644 --- a/cli/azd/pkg/project/dotnet_importer.go +++ b/cli/azd/pkg/project/dotnet_importer.go @@ -262,7 +262,9 @@ func (ai *DotNetImporter) readManifest(ctx context.Context, svcConfig *ServiceCo return cached, nil } + ai.console.ShowSpinner(ctx, "Analyzing Aspire Application (this might take a moment...)", input.Step) manifest, err := apphost.ManifestFromAppHost(ctx, svcConfig.Path(), ai.dotnetCli) + ai.console.StopSpinner(ctx, "", input.Step) if err != nil { return nil, err }