Skip to content

Commit dc026dc

Browse files
jongioCopilot
andcommitted
refactor: deploy only updates image, validates IaC owns config
Address vhvb1989's design feedback: azd deploy should only mutate the running artifact (container image), not infrastructure configuration. This aligns with how the containerapp target works. Changes: - Rename UpdateAppServiceContainerConfig -> UpdateAppServiceContainerImage (only sets linuxFxVersion, does NOT set acrUseManagedIdentityCreds) - Add ValidateAppServiceForContainerDeploy that checks the site is Linux with an existing DOCKER| linuxFxVersion before deploying. Returns actionable error messages directing users to fix their IaC if the site is not configured for containers. - containerDeploy now validates first, then updates only the image ref - Tests assert acrUseManagedIdentityCreds is NOT in the request body Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b2cd0f1 commit dc026dc

4 files changed

Lines changed: 87 additions & 33 deletions

File tree

cli/azd/pkg/azapi/webapp.go

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,10 @@ func (cli *AzureClient) GetAppServiceSlots(
420420
return slots, nil
421421
}
422422

423-
// UpdateAppServiceContainerConfig updates the container configuration for a Linux Web App for Containers.
424-
// It sets linuxFxVersion to "DOCKER|<imageName>" and enables ACR managed identity authentication.
425-
func (cli *AzureClient) UpdateAppServiceContainerConfig(
423+
// UpdateAppServiceContainerImage updates the container image for a Linux Web App for Containers.
424+
// It only sets linuxFxVersion to "DOCKER|<imageName>"; infrastructure configuration (ACR auth,
425+
// managed identity) must be set via IaC (bicep/terraform), not at deploy time.
426+
func (cli *AzureClient) UpdateAppServiceContainerImage(
426427
ctx context.Context,
427428
subscriptionId string,
428429
resourceGroup string,
@@ -438,21 +439,20 @@ func (cli *AzureClient) UpdateAppServiceContainerConfig(
438439
_, err = client.Update(ctx, resourceGroup, appName, armappservice.SitePatchResource{
439440
Properties: &armappservice.SitePatchResourceProperties{
440441
SiteConfig: &armappservice.SiteConfig{
441-
LinuxFxVersion: &linuxFxVersion,
442-
AcrUseManagedIdentityCreds: new(true),
442+
LinuxFxVersion: &linuxFxVersion,
443443
},
444444
},
445445
}, nil)
446446
if err != nil {
447-
return fmt.Errorf("updating container config for app service %s: %w", appName, err)
447+
return fmt.Errorf("updating container image for app service %s: %w", appName, err)
448448
}
449449

450450
return nil
451451
}
452452

453-
// UpdateAppServiceSlotContainerConfig updates the container configuration for a deployment slot.
454-
// It sets linuxFxVersion to "DOCKER|<imageName>" and enables ACR managed identity authentication.
455-
func (cli *AzureClient) UpdateAppServiceSlotContainerConfig(
453+
// UpdateAppServiceSlotContainerImage updates the container image for a deployment slot.
454+
// It only sets linuxFxVersion; infrastructure configuration must be set via IaC.
455+
func (cli *AzureClient) UpdateAppServiceSlotContainerImage(
456456
ctx context.Context,
457457
subscriptionId string,
458458
resourceGroup string,
@@ -469,13 +469,49 @@ func (cli *AzureClient) UpdateAppServiceSlotContainerConfig(
469469
_, err = client.UpdateSlot(ctx, resourceGroup, appName, slotName, armappservice.SitePatchResource{
470470
Properties: &armappservice.SitePatchResourceProperties{
471471
SiteConfig: &armappservice.SiteConfig{
472-
LinuxFxVersion: &linuxFxVersion,
473-
AcrUseManagedIdentityCreds: new(true),
472+
LinuxFxVersion: &linuxFxVersion,
474473
},
475474
},
476475
}, nil)
477476
if err != nil {
478-
return fmt.Errorf("updating container config for app service %s slot %s: %w", appName, slotName, err)
477+
return fmt.Errorf("updating container image for app service %s slot %s: %w", appName, slotName, err)
478+
}
479+
480+
return nil
481+
}
482+
483+
// ValidateAppServiceForContainerDeploy checks that the App Service is configured for container
484+
// deployment (Linux kind with an existing DOCKER| linuxFxVersion). Returns an error with
485+
// actionable suggestions if the site is not ready for container deployment.
486+
func (cli *AzureClient) ValidateAppServiceForContainerDeploy(
487+
ctx context.Context,
488+
subscriptionId string,
489+
resourceGroup string,
490+
appName string,
491+
) error {
492+
response, err := cli.appService(ctx, subscriptionId, resourceGroup, appName)
493+
if err != nil {
494+
return err
495+
}
496+
497+
if response.Kind == nil || !strings.Contains(*response.Kind, "linux") {
498+
return fmt.Errorf(
499+
"app service '%s' is not configured as a Linux app. "+
500+
"Container deployment requires a Linux App Service Plan. "+
501+
"Set 'kind: linux' in your bicep/terraform configuration",
502+
appName)
503+
}
504+
505+
if response.Properties == nil || response.Properties.SiteConfig == nil ||
506+
response.Properties.SiteConfig.LinuxFxVersion == nil ||
507+
!strings.HasPrefix(strings.ToUpper(*response.Properties.SiteConfig.LinuxFxVersion), "DOCKER|") {
508+
return fmt.Errorf(
509+
"app service '%s' is not configured for container deployment. "+
510+
"Ensure your infrastructure sets linuxFxVersion to a DOCKER| image "+
511+
"and configures ACR access (e.g., acrUseManagedIdentityCreds) in bicep/terraform. "+
512+
"azd deploy only updates the image reference; infrastructure configuration "+
513+
"must be provisioned first via 'azd provision'",
514+
appName)
479515
}
480516

481517
return nil

cli/azd/pkg/azapi/webapp_test.go

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func Test_AzureClient_GetAppServiceSlotProperties(t *testing.T) {
192192
assert.Contains(t, props.HostNames, "my-app-staging.azurewebsites.net")
193193
}
194194

195-
func Test_AzureClient_UpdateAppServiceContainerConfig(t *testing.T) {
195+
func Test_AzureClient_UpdateAppServiceContainerImage(t *testing.T) {
196196
mockCtx := mocks.NewMockContext(t.Context())
197197
client := newAzureClientFromMockContext(mockCtx)
198198

@@ -213,24 +213,23 @@ func Test_AzureClient_UpdateAppServiceContainerConfig(t *testing.T) {
213213
Properties: &armappservice.SiteProperties{
214214
DefaultHostName: new("my-app.azurewebsites.net"),
215215
SiteConfig: &armappservice.SiteConfig{
216-
LinuxFxVersion: new("DOCKER|myregistry.azurecr.io/myapp:v1"),
217-
AcrUseManagedIdentityCreds: new(true),
216+
LinuxFxVersion: new("DOCKER|myregistry.azurecr.io/myapp:v1"),
218217
},
219218
},
220219
})
221220
})
222221

223-
err := client.UpdateAppServiceContainerConfig(
222+
err := client.UpdateAppServiceContainerImage(
224223
*mockCtx.Context, "SUB", "RG", "my-app", "myregistry.azurecr.io/myapp:v1")
225224
require.NoError(t, err)
226225
assert.NotEmpty(t, capturedBody, "Update should have been called with a body")
227226
assert.Contains(t, capturedBody, "DOCKER|myregistry.azurecr.io/myapp:v1",
228227
"body should contain the correct linuxFxVersion")
229-
assert.Contains(t, capturedBody, "acrUseManagedIdentityCreds",
230-
"body should enable ACR managed identity")
228+
assert.NotContains(t, capturedBody, "acrUseManagedIdentityCreds",
229+
"should NOT set acrUseManagedIdentityCreds (IaC responsibility)")
231230
}
232231

233-
func Test_AzureClient_UpdateAppServiceContainerConfig_Error(t *testing.T) {
232+
func Test_AzureClient_UpdateAppServiceContainerImage_Error(t *testing.T) {
234233
mockCtx := mocks.NewMockContext(t.Context())
235234
client := newAzureClientFromMockContext(mockCtx)
236235

@@ -241,13 +240,13 @@ func Test_AzureClient_UpdateAppServiceContainerConfig_Error(t *testing.T) {
241240
return mocks.CreateEmptyHttpResponse(req, http.StatusInternalServerError)
242241
})
243242

244-
err := client.UpdateAppServiceContainerConfig(
243+
err := client.UpdateAppServiceContainerImage(
245244
*mockCtx.Context, "SUB", "RG", "my-app", "myregistry.azurecr.io/myapp:v1")
246245
require.Error(t, err)
247-
assert.Contains(t, err.Error(), "updating container config")
246+
assert.Contains(t, err.Error(), "updating container image")
248247
}
249248

250-
func Test_AzureClient_UpdateAppServiceSlotContainerConfig(t *testing.T) {
249+
func Test_AzureClient_UpdateAppServiceSlotContainerImage(t *testing.T) {
251250
mockCtx := mocks.NewMockContext(t.Context())
252251
client := newAzureClientFromMockContext(mockCtx)
253252

@@ -265,19 +264,18 @@ func Test_AzureClient_UpdateAppServiceSlotContainerConfig(t *testing.T) {
265264
Properties: &armappservice.SiteProperties{
266265
DefaultHostName: new("my-app-staging.azurewebsites.net"),
267266
SiteConfig: &armappservice.SiteConfig{
268-
LinuxFxVersion: new("DOCKER|myregistry.azurecr.io/myapp:v1"),
269-
AcrUseManagedIdentityCreds: new(true),
267+
LinuxFxVersion: new("DOCKER|myregistry.azurecr.io/myapp:v1"),
270268
},
271269
},
272270
})
273271
})
274272

275-
err := client.UpdateAppServiceSlotContainerConfig(
273+
err := client.UpdateAppServiceSlotContainerImage(
276274
*mockCtx.Context, "SUB", "RG", "my-app", "staging", "myregistry.azurecr.io/myapp:v1")
277275
require.NoError(t, err)
278276
assert.NotEmpty(t, capturedBody, "UpdateSlot should have been called with a body")
279277
assert.Contains(t, capturedBody, "DOCKER|myregistry.azurecr.io/myapp:v1",
280278
"body should contain the correct linuxFxVersion")
281-
assert.Contains(t, capturedBody, "acrUseManagedIdentityCreds",
282-
"body should enable ACR managed identity")
279+
assert.NotContains(t, capturedBody, "acrUseManagedIdentityCreds",
280+
"should NOT set acrUseManagedIdentityCreds (IaC responsibility)")
283281
}

cli/azd/pkg/project/service_target_appservice.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,18 @@ func (st *appServiceTarget) containerDeploy(
213213
imageName string,
214214
progress *async.Progress[ServiceProgress],
215215
) (*ServiceDeployResult, error) {
216+
// Validate the App Service is configured for container deployment (Linux + DOCKER| linuxFxVersion).
217+
// Infrastructure configuration (ACR auth, managed identity) must be set via IaC, not at deploy time.
218+
progress.SetProgress(NewServiceProgress("Validating container configuration"))
219+
if err := st.cli.ValidateAppServiceForContainerDeploy(
220+
ctx,
221+
targetResource.SubscriptionId(),
222+
targetResource.ResourceGroupName(),
223+
targetResource.ResourceName(),
224+
); err != nil {
225+
return nil, err
226+
}
227+
216228
// Determine deployment targets (main app or slots) using the same logic as zip deploy
217229
deployTargets, err := st.determineDeploymentTargets(ctx, serviceConfig, targetResource, progress)
218230
if err != nil {
@@ -222,22 +234,22 @@ func (st *appServiceTarget) containerDeploy(
222234
for _, target := range deployTargets {
223235
var progressMsg string
224236
if target.SlotName == "" {
225-
progressMsg = "Updating container configuration"
237+
progressMsg = "Updating container image"
226238
} else {
227-
progressMsg = fmt.Sprintf("Updating container configuration for slot '%s'", target.SlotName)
239+
progressMsg = fmt.Sprintf("Updating container image for slot '%s'", target.SlotName)
228240
}
229241
progress.SetProgress(NewServiceProgress(progressMsg))
230242

231243
if target.SlotName == "" {
232-
err = st.cli.UpdateAppServiceContainerConfig(
244+
err = st.cli.UpdateAppServiceContainerImage(
233245
ctx,
234246
targetResource.SubscriptionId(),
235247
targetResource.ResourceGroupName(),
236248
targetResource.ResourceName(),
237249
imageName,
238250
)
239251
} else {
240-
err = st.cli.UpdateAppServiceSlotContainerConfig(
252+
err = st.cli.UpdateAppServiceSlotContainerImage(
241253
ctx,
242254
targetResource.SubscriptionId(),
243255
targetResource.ResourceGroupName(),

cli/azd/pkg/project/service_target_appservice_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,16 +495,20 @@ func Test_appServiceTarget_Deploy_ContainerPath(t *testing.T) {
495495
return mocks.CreateHttpResponseWithBody(request, http.StatusOK, site)
496496
})
497497

498-
// Mock GetAppServiceProperties (for Endpoints)
498+
// Mock GetAppServiceProperties (for Validation + Endpoints)
499499
mockContext.HttpClient.When(func(request *http.Request) bool {
500500
return request.Method == http.MethodGet &&
501501
strings.Contains(request.URL.Path, "/sites/WEB_APP_NAME") &&
502502
!strings.Contains(request.URL.Path, "/slots")
503503
}).RespondFn(func(request *http.Request) (*http.Response, error) {
504504
response := armappservice.WebAppsClientGetResponse{
505505
Site: armappservice.Site{
506+
Kind: new("app,linux,container"),
506507
Properties: &armappservice.SiteProperties{
507508
DefaultHostName: new("webapp.azurewebsites.net"),
509+
SiteConfig: &armappservice.SiteConfig{
510+
LinuxFxVersion: new("DOCKER|placeholder:latest"),
511+
},
508512
},
509513
},
510514
}
@@ -587,16 +591,20 @@ func Test_appServiceTarget_Deploy_ContainerSlotPath(t *testing.T) {
587591
return mocks.CreateHttpResponseWithBody(request, http.StatusOK, site)
588592
})
589593

590-
// Mock GetAppServiceProperties (for Endpoints)
594+
// Mock GetAppServiceProperties (for Validation + Endpoints)
591595
mockContext.HttpClient.When(func(request *http.Request) bool {
592596
return request.Method == http.MethodGet &&
593597
strings.Contains(request.URL.Path, "/sites/WEB_APP_NAME") &&
594598
!strings.Contains(request.URL.Path, "/slots")
595599
}).RespondFn(func(request *http.Request) (*http.Response, error) {
596600
response := armappservice.WebAppsClientGetResponse{
597601
Site: armappservice.Site{
602+
Kind: new("app,linux,container"),
598603
Properties: &armappservice.SiteProperties{
599604
DefaultHostName: new("webapp.azurewebsites.net"),
605+
SiteConfig: &armappservice.SiteConfig{
606+
LinuxFxVersion: new("DOCKER|placeholder:latest"),
607+
},
600608
},
601609
},
602610
}

0 commit comments

Comments
 (0)