Skip to content

Commit 2f61772

Browse files
jongioCopilot
andcommitted
feat: support env and polyglot containerization for App Service
Enable two previously-blocked capabilities: 1. env: support - Apply service-level environment variables as App Settings during container deploy via UpdateAppServiceAppSettings. Merges with existing settings (preserves settings not managed by azd). Matches the containerapp target's behavior of applying env vars at deploy time. 2. Polyglot containerization (e.g., language: python + docker.path) - Add docker.path to the composite framework condition in service_manager.go so the docker wrapper builds a container image from any language source when a Dockerfile is provided. This is a one-line change that only fires when the user explicitly sets docker.path (opt-in, no impact on plain zip deploys). Schema updated: env now allowed for appservice (alongside docker/image). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 18248ee commit 2f61772

6 files changed

Lines changed: 87 additions & 36 deletions

File tree

cli/azd/pkg/azapi/webapp.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,46 @@ func (cli *AzureClient) ValidateAppServiceForContainerDeploy(
517517
return nil
518518
}
519519

520+
// UpdateAppServiceAppSettings merges the provided environment variables into the App Service's
521+
// application settings. Existing settings not in the provided map are preserved.
522+
func (cli *AzureClient) UpdateAppServiceAppSettings(
523+
ctx context.Context,
524+
subscriptionId string,
525+
resourceGroup string,
526+
appName string,
527+
envVars map[string]string,
528+
) error {
529+
client, err := cli.createWebAppsClient(ctx, subscriptionId)
530+
if err != nil {
531+
return err
532+
}
533+
534+
// Get existing app settings to merge (preserve settings not managed by azd)
535+
existing, err := client.ListApplicationSettings(ctx, resourceGroup, appName, nil)
536+
if err != nil {
537+
return fmt.Errorf("listing app settings for %s: %w", appName, err)
538+
}
539+
540+
// Merge: existing settings + new env vars (new values overwrite)
541+
merged := make(map[string]*string)
542+
if existing.Properties != nil {
543+
for k, v := range existing.Properties {
544+
merged[k] = v
545+
}
546+
}
547+
for k, v := range envVars {
548+
merged[k] = &v
549+
}
550+
551+
_, err = client.UpdateApplicationSettings(ctx, resourceGroup, appName,
552+
armappservice.StringDictionary{Properties: merged}, nil)
553+
if err != nil {
554+
return fmt.Errorf("updating app settings for %s: %w", appName, err)
555+
}
556+
557+
return nil
558+
}
559+
520560
// DeployAppServiceSlotZip deploys a zip file to a specific deployment slot.
521561
func (cli *AzureClient) DeployAppServiceSlotZip(
522562
ctx context.Context,

cli/azd/pkg/project/service_manager.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,9 +707,10 @@ func (sm *serviceManager) GetFrameworkService(ctx context.Context, serviceConfig
707707

708708
var compositeFramework CompositeFrameworkService
709709
// For hosts which run in containers, if the source project is not already a container, we need to wrap it in a docker
710-
// project that handles the containerization.
710+
// project that handles the containerization. Also applies when the user explicitly sets docker.path (opt-in
711+
// containerization for any host, e.g., appservice with a Dockerfile).
711712
requiresLanguage := serviceConfig.Language != ServiceLanguageDocker && serviceConfig.Language != ServiceLanguageNone
712-
if serviceConfig.Host.RequiresContainer() && requiresLanguage {
713+
if (serviceConfig.Host.RequiresContainer() || serviceConfig.Docker.Path != "") && requiresLanguage {
713714
if err := sm.serviceLocator.ResolveNamed(string(ServiceLanguageDocker), &compositeFramework); err != nil {
714715
return nil, fmt.Errorf(
715716
"failed resolving composite framework service for '%s', language '%s': %w",

cli/azd/pkg/project/service_target_appservice.go

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func NewAppServiceTarget(
4747

4848
// Gets the required external tools
4949
func (st *appServiceTarget) RequiredExternalTools(ctx context.Context, serviceConfig *ServiceConfig) []tools.ExternalTool {
50-
if serviceConfig.Language == ServiceLanguageDocker {
50+
if serviceConfig.Language == ServiceLanguageDocker || serviceConfig.Docker.Path != "" {
5151
return st.containerHelper.RequiredExternalTools(ctx, serviceConfig)
5252
}
5353
return []tools.ExternalTool{}
@@ -263,6 +263,25 @@ func (st *appServiceTarget) containerDeploy(
263263
}
264264
}
265265

266+
// Apply environment variables as App Settings if configured
267+
if len(serviceConfig.Environment) > 0 {
268+
progress.SetProgress(NewServiceProgress("Updating application settings"))
269+
envVars, err := serviceConfig.Environment.Expand(st.env.Getenv)
270+
if err != nil {
271+
return nil, fmt.Errorf("expanding environment variables: %w", err)
272+
}
273+
274+
if err := st.cli.UpdateAppServiceAppSettings(
275+
ctx,
276+
targetResource.SubscriptionId(),
277+
targetResource.ResourceGroupName(),
278+
targetResource.ResourceName(),
279+
envVars,
280+
); err != nil {
281+
return nil, fmt.Errorf("updating app settings for service %s: %w", serviceConfig.Name, err)
282+
}
283+
}
284+
266285
progress.SetProgress(NewServiceProgress("Fetching endpoints for app service"))
267286
endpoints, err := st.Endpoints(ctx, serviceConfig, targetResource)
268287
if err != nil {
@@ -619,12 +638,10 @@ func (st *appServiceTarget) validateTargetResource(
619638
}
620639

621640
// isContainerDeploy returns true when the service is configured for container deployment.
622-
// For App Service, container deployment requires language=docker or a pre-built image.
623-
// Polyglot containerization (non-docker language + docker.path) is not yet supported for
624-
// App Service because the composite docker framework only wraps for targets that return
625-
// RequiresContainer() == true (containerapp/aks).
641+
// For App Service, container deployment is triggered by language=docker, docker.path set
642+
// (polyglot containerization), or a pre-built image.
626643
func (st *appServiceTarget) isContainerDeploy(serviceConfig *ServiceConfig, serviceContext *ServiceContext) bool {
627-
if serviceConfig.Language == ServiceLanguageDocker {
644+
if serviceConfig.Language == ServiceLanguageDocker || serviceConfig.Docker.Path != "" {
628645
return true
629646
}
630647
if _, found := serviceContext.Package.FindFirst(WithKind(ArtifactKindContainer)); found {
@@ -633,19 +650,8 @@ func (st *appServiceTarget) isContainerDeploy(serviceConfig *ServiceConfig, serv
633650
return false
634651
}
635652

636-
// validateContainerConfig checks for unsupported container configurations and returns
637-
// actionable errors instead of silently failing.
638-
func (st *appServiceTarget) validateContainerConfig(serviceConfig *ServiceConfig) error {
639-
// Polyglot containerization (e.g., language: python + docker.path) is not yet supported
640-
// for App Service. The composite docker framework only wraps for containerapp/aks targets.
641-
if serviceConfig.Language != ServiceLanguageDocker &&
642-
serviceConfig.Language != ServiceLanguageNone &&
643-
serviceConfig.Docker.Path != "" {
644-
return fmt.Errorf(
645-
"App Service container deployment with language '%s' and docker.path is not yet supported. "+
646-
"Use 'language: docker' with a Dockerfile, or use 'host: containerapp' for polyglot "+
647-
"containerization. See https://github.com/Azure/azure-dev/issues/1608 for tracking",
648-
serviceConfig.Language)
649-
}
653+
// validateContainerConfig is a no-op; polyglot containerization is now supported via the
654+
// composite docker framework in service_manager.go.
655+
func (st *appServiceTarget) validateContainerConfig(_ *ServiceConfig) error {
650656
return nil
651657
}

cli/azd/pkg/project/service_target_appservice_test.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -416,14 +416,14 @@ func Test_appServiceTarget_Initialize(t *testing.T) {
416416
require.NoError(t, err)
417417
})
418418

419-
t.Run("PolyglotDocker_ReturnsError", func(t *testing.T) {
419+
t.Run("PolyglotDocker_NoError", func(t *testing.T) {
420+
// Polyglot containerization (python + docker.path) is now supported
420421
target := NewAppServiceTarget(nil, nil, nil, nil, nil)
421422
err := target.Initialize(t.Context(), &ServiceConfig{
422423
Language: ServiceLanguagePython,
423424
Docker: DockerProjectOptions{Path: "./Dockerfile"},
424425
})
425-
require.Error(t, err)
426-
assert.Contains(t, err.Error(), "not yet supported")
426+
require.NoError(t, err)
427427
})
428428
}
429429

@@ -687,15 +687,21 @@ func Test_appServiceTarget_RequiredExternalTools_Docker(t *testing.T) {
687687
assert.NotEmpty(t, tools, "should return docker tools for docker language")
688688
})
689689

690-
t.Run("DockerPath_WithNonDockerLanguage_ReturnsEmpty", func(t *testing.T) {
691-
// Polyglot containerization (non-docker language + docker.path) is not supported for appservice
692-
target := &appServiceTarget{}
690+
t.Run("DockerPath_WithNonDockerLanguage_DelegatesToContainerHelper", func(t *testing.T) {
691+
// Polyglot containerization (non-docker language + docker.path) is now supported
692+
mockContext := mocks.NewMockContext(t.Context())
693+
dockerCli := docker.NewCli(mockContext.CommandRunner)
694+
containerHelper := NewContainerHelper(
695+
nil, nil, nil, nil, dockerCli, nil, mockContext.Console, nil)
696+
target := &appServiceTarget{
697+
containerHelper: containerHelper,
698+
}
693699
sc := &ServiceConfig{
694700
Language: ServiceLanguagePython,
695701
Docker: DockerProjectOptions{Path: "./Dockerfile"},
696702
}
697-
tools := target.RequiredExternalTools(t.Context(), sc)
698-
assert.Empty(t, tools, "should not return docker tools for unsupported polyglot containerization")
703+
tools := target.RequiredExternalTools(*mockContext.Context, sc)
704+
assert.NotEmpty(t, tools, "should return docker tools when docker.path is set")
699705
})
700706

701707
t.Run("NonDocker_ReturnsEmpty", func(t *testing.T) {

schemas/alpha/azure.yaml.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@
450450
}
451451
},
452452
{
453-
"comment": "App Service supports docker/image for containers but not k8s, apiVersion, or env (env not yet wired for container deploy)",
453+
"comment": "App Service supports docker/image/env for containers but not Kubernetes-specific properties",
454454
"if": {
455455
"properties": {
456456
"host": { "const": "appservice" }
@@ -459,8 +459,7 @@
459459
"then": {
460460
"properties": {
461461
"k8s": false,
462-
"apiVersion": false,
463-
"env": false
462+
"apiVersion": false
464463
}
465464
}
466465
},

schemas/v1.0/azure.yaml.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@
410410
}
411411
},
412412
{
413-
"comment": "App Service supports docker/image for containers but not k8s, apiVersion, or env (env not yet wired for container deploy)",
413+
"comment": "App Service supports docker/image/env for containers but not Kubernetes-specific properties",
414414
"if": {
415415
"properties": {
416416
"host": { "const": "appservice" }
@@ -419,8 +419,7 @@
419419
"then": {
420420
"properties": {
421421
"k8s": false,
422-
"apiVersion": false,
423-
"env": false
422+
"apiVersion": false
424423
}
425424
}
426425
},

0 commit comments

Comments
 (0)