From d7e56223d012294176ee33627a18fe1a0668e96b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:53:45 +0000 Subject: [PATCH 1/3] Initial plan From e2d49dd48ec3063cb065f0c0149bd0746cb9a76d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:08:27 +0000 Subject: [PATCH 2/3] Fix: preserve Dapr configuration during container app deployment Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- cli/azd/pkg/containerapps/container_app.go | 20 ++- .../pkg/containerapps/container_app_test.go | 146 ++++++++++++++++++ 2 files changed, 163 insertions(+), 3 deletions(-) diff --git a/cli/azd/pkg/containerapps/container_app.go b/cli/azd/pkg/containerapps/container_app.go index 79d79f36774..08fc4b5ff51 100644 --- a/cli/azd/pkg/containerapps/container_app.go +++ b/cli/azd/pkg/containerapps/container_app.go @@ -36,6 +36,7 @@ const ( pathTemplateRevisionSuffix = "properties.template.revisionSuffix" pathTemplateContainers = "properties.template.containers" pathConfigurationActiveRevisionsMode = "properties.configuration.activeRevisionsMode" + pathConfigurationDapr = "properties.configuration.dapr" pathConfigurationSecrets = "properties.configuration.secrets" pathConfigurationIngressTraffic = "properties.configuration.ingress.traffic" pathConfigurationIngressFqdn = "properties.configuration.ingress.fqdn" @@ -168,7 +169,13 @@ func (cas *containerAppService) persistSettings( shouldPersistDomains := cas.alphaFeatureManager.IsEnabled(persistCustomDomainsFeature) shouldPersistIngressSessionAffinity := cas.alphaFeatureManager.IsEnabled(persistIngressSessionAffinity) - if !shouldPersistDomains && !shouldPersistIngressSessionAffinity { + // Preserve existing Dapr configuration when the deployment YAML does not include it. + // This prevents Dapr config set externally (e.g. via Terraform) from being removed on deploy. + objConfig := config.NewConfig(obj) + _, hasDaprConfig := objConfig.Get(pathConfigurationDapr) + shouldPreserveDapr := !hasDaprConfig + + if !shouldPersistDomains && !shouldPersistIngressSessionAffinity && !shouldPreserveDapr { return obj, nil } @@ -180,8 +187,6 @@ func (cas *containerAppService) persistSettings( return obj, nil } - objConfig := config.NewConfig(obj) - if shouldPersistDomains { customDomains, has := aca.GetSlice(pathConfigurationIngressCustomDomains) if has { @@ -200,6 +205,15 @@ func (cas *containerAppService) persistSettings( } } + if shouldPreserveDapr { + daprConfig, has := aca.Get(pathConfigurationDapr) + if has { + if err := objConfig.Set(pathConfigurationDapr, daprConfig); err != nil { + return nil, fmt.Errorf("setting dapr configuration: %w", err) + } + } + } + return objConfig.Raw(), nil } diff --git a/cli/azd/pkg/containerapps/container_app_test.go b/cli/azd/pkg/containerapps/container_app_test.go index 3cda1e9b8ae..beb6445f8af 100644 --- a/cli/azd/pkg/containerapps/container_app_test.go +++ b/cli/azd/pkg/containerapps/container_app_test.go @@ -848,3 +848,149 @@ func Test_ContainerAppJob_UpdateImage_NilContainerElement(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "nil container entry") } + +// Test_ContainerApp_DeployYaml_PreservesDaprConfig verifies that when a deployment YAML does not include +// Dapr configuration, any existing Dapr configuration on the container app is preserved. +// This ensures that Dapr configuration set externally (e.g. via Terraform) is not removed on deploy. +func Test_ContainerApp_DeployYaml_PreservesDaprConfig(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + subscriptionId := "SUBSCRIPTION_ID" + location := "eastus2" + resourceGroup := "RESOURCE_GROUP" + appName := "APP_NAME" + + // YAML does NOT include Dapr configuration + containerAppYaml := ` +location: eastus2 +name: APP_NAME +properties: + configuration: + activeRevisionsMode: Single + template: + containers: + - image: IMAGE_NAME +` + + // Existing container app has Dapr enabled + existingApp := &armappcontainers.ContainerApp{ + Location: to.Ptr(location), + Name: to.Ptr(appName), + Properties: &armappcontainers.ContainerAppProperties{ + Configuration: &armappcontainers.Configuration{ + ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle), + Dapr: &armappcontainers.Dapr{ + AppID: to.Ptr("my-app"), + AppPort: to.Ptr[int32](8080), + Enabled: to.Ptr(true), + }, + }, + Template: &armappcontainers.Template{ + Containers: []*armappcontainers.Container{ + { + Image: to.Ptr("IMAGE_NAME"), + }, + }, + }, + }, + } + + _ = mockazsdk.MockContainerAppGet(mockContext, subscriptionId, resourceGroup, appName, existingApp) + containerAppUpdateRequest := mockazsdk.MockContainerAppCreateOrUpdate( + mockContext, subscriptionId, resourceGroup, appName, existingApp, + ) + + cas := NewContainerAppService( + mockContext.SubscriptionCredentialProvider, + clock.NewMock(), + mockContext.ArmClientOptions, + mockContext.AlphaFeaturesManager, + ) + + err := cas.DeployYaml(*mockContext.Context, subscriptionId, resourceGroup, appName, []byte(containerAppYaml), nil) + require.NoError(t, err) + + var actual *armappcontainers.ContainerApp + err = mocks.ReadHttpBody(containerAppUpdateRequest.Body, &actual) + require.NoError(t, err) + + // Dapr configuration should be preserved from the existing container app + require.NotNil(t, actual.Properties.Configuration.Dapr) + require.Equal(t, "my-app", *actual.Properties.Configuration.Dapr.AppID) + require.Equal(t, int32(8080), *actual.Properties.Configuration.Dapr.AppPort) + require.Equal(t, true, *actual.Properties.Configuration.Dapr.Enabled) +} + +// Test_ContainerApp_DeployYaml_YamlDaprConfigNotOverridden verifies that when a deployment YAML already +// includes Dapr configuration, the YAML's Dapr configuration is used (not the existing app's configuration). +func Test_ContainerApp_DeployYaml_YamlDaprConfigNotOverridden(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + subscriptionId := "SUBSCRIPTION_ID" + location := "eastus2" + resourceGroup := "RESOURCE_GROUP" + appName := "APP_NAME" + + // YAML includes its own Dapr configuration + containerAppYaml := ` +location: eastus2 +name: APP_NAME +properties: + configuration: + activeRevisionsMode: Single + dapr: + appId: yaml-app + appPort: 9090 + enabled: true + template: + containers: + - image: IMAGE_NAME +` + + // Existing container app has different Dapr configuration + existingApp := &armappcontainers.ContainerApp{ + Location: to.Ptr(location), + Name: to.Ptr(appName), + Properties: &armappcontainers.ContainerAppProperties{ + Configuration: &armappcontainers.Configuration{ + ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle), + Dapr: &armappcontainers.Dapr{ + AppID: to.Ptr("existing-app"), + AppPort: to.Ptr[int32](8080), + Enabled: to.Ptr(true), + }, + }, + Template: &armappcontainers.Template{ + Containers: []*armappcontainers.Container{ + { + Image: to.Ptr("IMAGE_NAME"), + }, + }, + }, + }, + } + + _ = mockazsdk.MockContainerAppGet(mockContext, subscriptionId, resourceGroup, appName, existingApp) + containerAppUpdateRequest := mockazsdk.MockContainerAppCreateOrUpdate( + mockContext, subscriptionId, resourceGroup, appName, existingApp, + ) + + cas := NewContainerAppService( + mockContext.SubscriptionCredentialProvider, + clock.NewMock(), + mockContext.ArmClientOptions, + mockContext.AlphaFeaturesManager, + ) + + err := cas.DeployYaml(*mockContext.Context, subscriptionId, resourceGroup, appName, []byte(containerAppYaml), nil) + require.NoError(t, err) + + var actual *armappcontainers.ContainerApp + err = mocks.ReadHttpBody(containerAppUpdateRequest.Body, &actual) + require.NoError(t, err) + + // Dapr configuration from the YAML should be used, not the existing app's + require.NotNil(t, actual.Properties.Configuration.Dapr) + require.Equal(t, "yaml-app", *actual.Properties.Configuration.Dapr.AppID) + require.Equal(t, int32(9090), *actual.Properties.Configuration.Dapr.AppPort) +} From 113829d1504935a73e7de5680b4a19dc06064a1d Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 12 Mar 2026 15:12:48 -0700 Subject: [PATCH 3/3] fix: improve Dapr preservation error handling and add 404 test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handle 404 (first deploy) explicitly in persistSettings instead of swallowing all errors — proceed without persisting when app does not exist yet - Fail on non-404 errors when Dapr preservation is needed to prevent silent config wipe (correctness-critical path) - Add test for first-deploy scenario (GET 404) verifying no Dapr config is injected - Fix pre-existing cspell issues (projectpkg, agentserver) - Apply go fix modernizations (to.Ptr -> new) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/.vscode/cspell.yaml | 9 + cli/azd/pkg/containerapps/container_app.go | 20 ++- .../container_app_benchmark_test.go | 32 ++-- .../pkg/containerapps/container_app_test.go | 164 +++++++++++++----- 4 files changed, 164 insertions(+), 61 deletions(-) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 6ae1d0c06b5..724eeb533ca 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -304,6 +304,15 @@ overrides: - filename: pkg/azdext/scope_detector.go words: - fakeazure + - filename: extensions/azure.ai.agents/internal/cmd/helpers.go + words: + - projectpkg + - filename: extensions/azure.ai.agents/internal/cmd/invoke.go + words: + - agentserver + - filename: extensions/azure.ai.models/internal/cmd/custom_create.go + words: + - Qwen ignorePaths: - "**/*_test.go" - "**/mock*.go" diff --git a/cli/azd/pkg/containerapps/container_app.go b/cli/azd/pkg/containerapps/container_app.go index 08fc4b5ff51..9cdb9b3f1c2 100644 --- a/cli/azd/pkg/containerapps/container_app.go +++ b/cli/azd/pkg/containerapps/container_app.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "log" "net/http" @@ -14,6 +15,7 @@ import ( "strings" "sync" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "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/runtime" @@ -181,9 +183,19 @@ func (cas *containerAppService) persistSettings( aca, err := cas.getContainerApp(ctx, subscriptionId, resourceGroupName, appName, options) if err != nil { + // On first deploy the app doesn't exist yet (404) — proceed without persisting. + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + return obj, nil + } + + // For alpha-gated features, preserve existing behavior: log and continue. + // For Dapr preservation (correctness-critical), fail to prevent silent config wipe. + if shouldPreserveDapr { + return nil, fmt.Errorf("fetching existing container app to preserve Dapr config: %w", err) + } + log.Printf("failed getting current aca settings: %v. No settings will be persisted.", err) - // if the container app doesn't exist, there's nothing for us to update in the desired state, - // so we can just return the existing state as is. return obj, nil } @@ -712,8 +724,8 @@ func (cas *containerAppService) UpdateContainerAppJobImage( // Merge new env vars (override existing with same name) for key, value := range envVars { envMap[key] = &armappcontainers.EnvironmentVar{ - Name: to.Ptr(key), - Value: to.Ptr(value), + Name: new(key), + Value: new(value), } } diff --git a/cli/azd/pkg/containerapps/container_app_benchmark_test.go b/cli/azd/pkg/containerapps/container_app_benchmark_test.go index b9757601d79..9b16016deaf 100644 --- a/cli/azd/pkg/containerapps/container_app_benchmark_test.go +++ b/cli/azd/pkg/containerapps/container_app_benchmark_test.go @@ -165,25 +165,25 @@ func Test_AddRevision_ARMCallCount(t *testing.T) { var secrets []*armappcontainers.Secret if tt.hasSecrets { secrets = []*armappcontainers.Secret{ - {Name: to.Ptr("secret"), Value: nil}, + {Name: new("secret"), Value: nil}, } } containerApp := &armappcontainers.ContainerApp{ - Location: to.Ptr("eastus2"), + Location: new("eastus2"), Name: &appName, Properties: &armappcontainers.ContainerAppProperties{ - LatestRevisionName: to.Ptr("rev-1"), + LatestRevisionName: new("rev-1"), Configuration: &armappcontainers.Configuration{ - ActiveRevisionsMode: to.Ptr(tt.revisionMode), + ActiveRevisionsMode: new(tt.revisionMode), Secrets: secrets, Ingress: &armappcontainers.Ingress{ - Fqdn: to.Ptr("app.azurecontainerapps.io"), + Fqdn: new("app.azurecontainerapps.io"), }, }, Template: &armappcontainers.Template{ Containers: []*armappcontainers.Container{ - {Image: to.Ptr("old-image")}, + {Image: new("old-image")}, }, }, }, @@ -209,7 +209,7 @@ func Test_AddRevision_ARMCallCount(t *testing.T) { appName, &armappcontainers.SecretsCollection{ Value: []*armappcontainers.ContainerAppSecret{ - {Name: to.Ptr("secret"), Value: to.Ptr("value")}, + {Name: new("secret"), Value: new("value")}, }, }, &secretsCalls) } @@ -259,19 +259,19 @@ func Test_AddRevision_MultiRevision_CombinedPatch(t *testing.T) { appName := "APP" containerApp := &armappcontainers.ContainerApp{ - Location: to.Ptr("eastus2"), + Location: new("eastus2"), Name: &appName, Properties: &armappcontainers.ContainerAppProperties{ - LatestRevisionName: to.Ptr("rev-1"), + LatestRevisionName: new("rev-1"), Configuration: &armappcontainers.Configuration{ ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeMultiple), Ingress: &armappcontainers.Ingress{ - Fqdn: to.Ptr("app.azurecontainerapps.io"), + Fqdn: new("app.azurecontainerapps.io"), }, }, Template: &armappcontainers.Template{ Containers: []*armappcontainers.Container{ - {Image: to.Ptr("old-image")}, + {Image: new("old-image")}, }, }, }, @@ -327,19 +327,19 @@ func Benchmark_AddRevision(b *testing.B) { appName := "APP" containerApp := &armappcontainers.ContainerApp{ - Location: to.Ptr("eastus2"), + Location: new("eastus2"), Name: &appName, Properties: &armappcontainers.ContainerAppProperties{ - LatestRevisionName: to.Ptr("rev-1"), + LatestRevisionName: new("rev-1"), Configuration: &armappcontainers.Configuration{ ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle), Secrets: []*armappcontainers.Secret{ - {Name: to.Ptr("secret"), Value: nil}, + {Name: new("secret"), Value: nil}, }, }, Template: &armappcontainers.Template{ Containers: []*armappcontainers.Container{ - {Image: to.Ptr("old-image")}, + {Image: new("old-image")}, }, }, }, @@ -347,7 +347,7 @@ func Benchmark_AddRevision(b *testing.B) { secrets := &armappcontainers.SecretsCollection{ Value: []*armappcontainers.ContainerAppSecret{ - {Name: to.Ptr("secret"), Value: to.Ptr("value")}, + {Name: new("secret"), Value: new("value")}, }, } diff --git a/cli/azd/pkg/containerapps/container_app_test.go b/cli/azd/pkg/containerapps/container_app_test.go index beb6445f8af..b235180a90c 100644 --- a/cli/azd/pkg/containerapps/container_app_test.go +++ b/cli/azd/pkg/containerapps/container_app_test.go @@ -83,7 +83,7 @@ func Test_ContainerApp_AddRevision(t *testing.T) { ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle), Secrets: []*armappcontainers.Secret{ { - Name: to.Ptr("secret"), + Name: new("secret"), Value: nil, }, }, @@ -101,8 +101,8 @@ func Test_ContainerApp_AddRevision(t *testing.T) { secrets := &armappcontainers.SecretsCollection{ Value: []*armappcontainers.ContainerAppSecret{ { - Name: to.Ptr("secret"), - Value: to.Ptr("value"), + Name: new("secret"), + Value: new("value"), }, }, } @@ -152,7 +152,7 @@ func Test_ContainerApp_AddRevision_MultipleRevisionMode(t *testing.T) { ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeMultiple), Secrets: []*armappcontainers.Secret{ { - Name: to.Ptr("secret"), + Name: new("secret"), Value: nil, }, }, @@ -171,8 +171,8 @@ func Test_ContainerApp_AddRevision_MultipleRevisionMode(t *testing.T) { secrets := &armappcontainers.SecretsCollection{ Value: []*armappcontainers.ContainerAppSecret{ { - Name: to.Ptr("secret"), - Value: to.Ptr("value"), + Name: new("secret"), + Value: new("value"), }, }, } @@ -247,11 +247,11 @@ func Test_ContainerApp_AddRevision_WithEnvVars(t *testing.T) { Image: &originalImageName, Env: []*armappcontainers.EnvironmentVar{ { - Name: to.Ptr("EXISTING"), + Name: new("EXISTING"), Value: &existingValue, }, { - Name: to.Ptr("OVERRIDE"), + Name: new("OVERRIDE"), Value: &overrideValue, }, }, @@ -324,16 +324,16 @@ properties: ` expected := &armappcontainers.ContainerApp{ - Location: to.Ptr(location), - Name: to.Ptr(appName), + Location: new(location), + Name: new(appName), Properties: &armappcontainers.ContainerAppProperties{ - LatestRevisionName: to.Ptr("LATEST_REVISION_NAME"), + LatestRevisionName: new("LATEST_REVISION_NAME"), Configuration: &armappcontainers.Configuration{ ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle), Ingress: &armappcontainers.Ingress{ CustomDomains: []*armappcontainers.CustomDomain{ { - Name: to.Ptr("DOMAIN_NAME"), + Name: new("DOMAIN_NAME"), }, }, StickySessions: &armappcontainers.IngressStickySessions{ @@ -344,7 +344,7 @@ properties: Template: &armappcontainers.Template{ Containers: []*armappcontainers.Container{ { - Image: to.Ptr("IMAGE_NAME"), + Image: new("IMAGE_NAME"), }, }, }, @@ -400,14 +400,14 @@ func Test_ContainerAppJob_Get(t *testing.T) { imageName := "myregistry.azurecr.io/myimage:latest" job := &armappcontainers.Job{ - Location: to.Ptr(location), - Name: to.Ptr(jobName), + Location: new(location), + Name: new(jobName), Properties: &armappcontainers.JobProperties{ Template: &armappcontainers.JobTemplate{ Containers: []*armappcontainers.Container{ { - Name: to.Ptr(jobName), - Image: to.Ptr(imageName), + Name: new(jobName), + Image: new(imageName), }, }, }, @@ -451,14 +451,14 @@ func Test_ContainerAppJob_UpdateImage(t *testing.T) { updatedImage := "myregistry.azurecr.io/myimage:v2" job := &armappcontainers.Job{ - Location: to.Ptr(location), - Name: to.Ptr(jobName), + Location: new(location), + Name: new(jobName), Properties: &armappcontainers.JobProperties{ Template: &armappcontainers.JobTemplate{ Containers: []*armappcontainers.Container{ { - Name: to.Ptr(jobName), - Image: to.Ptr(originalImage), + Name: new(jobName), + Image: new(originalImage), }, }, }, @@ -502,7 +502,7 @@ func Test_ContainerAppJob_UpdateImage_NilContainers(t *testing.T) { jobName := "MY_JOB" job := &armappcontainers.Job{ - Name: to.Ptr(jobName), + Name: new(jobName), Properties: nil, } @@ -536,14 +536,14 @@ func Test_ContainerAppJob_CreateJobsClient_CacheHit(t *testing.T) { imageName := "myregistry.azurecr.io/myimage:latest" job := &armappcontainers.Job{ - Location: to.Ptr(location), - Name: to.Ptr(jobName), + Location: new(location), + Name: new(jobName), Properties: &armappcontainers.JobProperties{ Template: &armappcontainers.JobTemplate{ Containers: []*armappcontainers.Container{ { - Name: to.Ptr(jobName), - Image: to.Ptr(imageName), + Name: new(jobName), + Image: new(imageName), }, }, }, @@ -701,14 +701,14 @@ func Test_ContainerAppJob_UpdateImage_CustomApiVersion(t *testing.T) { customApiVersion := "2024-10-02-preview" job := &armappcontainers.Job{ - Location: to.Ptr(location), - Name: to.Ptr(jobName), + Location: new(location), + Name: new(jobName), Properties: &armappcontainers.JobProperties{ Template: &armappcontainers.JobTemplate{ Containers: []*armappcontainers.Container{ { - Name: to.Ptr(jobName), - Image: to.Ptr(originalImage), + Name: new(jobName), + Image: new(originalImage), }, }, }, @@ -816,7 +816,7 @@ func Test_ContainerAppJob_UpdateImage_NilContainerElement(t *testing.T) { jobName := "MY_JOB" job := &armappcontainers.Job{ - Name: to.Ptr(jobName), + Name: new(jobName), Properties: &armappcontainers.JobProperties{ Template: &armappcontainers.JobTemplate{ Containers: []*armappcontainers.Container{ @@ -874,21 +874,21 @@ properties: // Existing container app has Dapr enabled existingApp := &armappcontainers.ContainerApp{ - Location: to.Ptr(location), - Name: to.Ptr(appName), + Location: new(location), + Name: new(appName), Properties: &armappcontainers.ContainerAppProperties{ Configuration: &armappcontainers.Configuration{ ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle), Dapr: &armappcontainers.Dapr{ - AppID: to.Ptr("my-app"), + AppID: new("my-app"), AppPort: to.Ptr[int32](8080), - Enabled: to.Ptr(true), + Enabled: new(true), }, }, Template: &armappcontainers.Template{ Containers: []*armappcontainers.Container{ { - Image: to.Ptr("IMAGE_NAME"), + Image: new("IMAGE_NAME"), }, }, }, @@ -949,21 +949,21 @@ properties: // Existing container app has different Dapr configuration existingApp := &armappcontainers.ContainerApp{ - Location: to.Ptr(location), - Name: to.Ptr(appName), + Location: new(location), + Name: new(appName), Properties: &armappcontainers.ContainerAppProperties{ Configuration: &armappcontainers.Configuration{ ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle), Dapr: &armappcontainers.Dapr{ - AppID: to.Ptr("existing-app"), + AppID: new("existing-app"), AppPort: to.Ptr[int32](8080), - Enabled: to.Ptr(true), + Enabled: new(true), }, }, Template: &armappcontainers.Template{ Containers: []*armappcontainers.Container{ { - Image: to.Ptr("IMAGE_NAME"), + Image: new("IMAGE_NAME"), }, }, }, @@ -994,3 +994,85 @@ properties: require.Equal(t, "yaml-app", *actual.Properties.Configuration.Dapr.AppID) require.Equal(t, int32(9090), *actual.Properties.Configuration.Dapr.AppPort) } + +// Test_ContainerApp_DeployYaml_FirstDeploy_NoDaprInjected verifies that on first deploy (GET returns 404), +// no Dapr configuration is injected into the deployment payload. +func Test_ContainerApp_DeployYaml_FirstDeploy_NoDaprInjected(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + subscriptionId := "SUBSCRIPTION_ID" + resourceGroup := "RESOURCE_GROUP" + appName := "NEW_APP" + + // YAML does NOT include Dapr configuration + containerAppYaml := ` +location: eastus2 +name: NEW_APP +properties: + configuration: + activeRevisionsMode: Single + template: + containers: + - image: IMAGE_NAME +` + + // Mock GET: first call returns 404 (app doesn't exist for persistSettings), + // subsequent calls return 200 (for CreateOrUpdate polling) + getCallCount := 0 + createdApp := &armappcontainers.ContainerApp{ + Location: new("eastus2"), + Name: new("NEW_APP"), + Properties: &armappcontainers.ContainerAppProperties{ + Configuration: &armappcontainers.Configuration{ + ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle), + }, + Template: &armappcontainers.Template{ + Containers: []*armappcontainers.Container{ + {Image: new("IMAGE_NAME")}, + }, + }, + }, + } + + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodGet && strings.Contains( + request.URL.Path, + fmt.Sprintf( + "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.App/containerApps/%s", + subscriptionId, + resourceGroup, + appName, + ), + ) + }).RespondFn(func(request *http.Request) (*http.Response, error) { + getCallCount++ + if getCallCount == 1 { + return mocks.CreateEmptyHttpResponse(request, http.StatusNotFound) + } + response := armappcontainers.ContainerAppsClientGetResponse{ + ContainerApp: *createdApp, + } + return mocks.CreateHttpResponseWithBody(request, http.StatusOK, response) + }) + + containerAppUpdateRequest := mockazsdk.MockContainerAppCreateOrUpdate( + mockContext, subscriptionId, resourceGroup, appName, createdApp, + ) + + cas := NewContainerAppService( + mockContext.SubscriptionCredentialProvider, + clock.NewMock(), + mockContext.ArmClientOptions, + mockContext.AlphaFeaturesManager, + ) + + err := cas.DeployYaml(*mockContext.Context, subscriptionId, resourceGroup, appName, []byte(containerAppYaml), nil) + require.NoError(t, err) + + var actual *armappcontainers.ContainerApp + err = mocks.ReadHttpBody(containerAppUpdateRequest.Body, &actual) + require.NoError(t, err) + + // No Dapr configuration should be injected on first deploy + require.Nil(t, actual.Properties.Configuration.Dapr) +}