Skip to content

feat: forward service env to service targets and make it round-trip safe#9018

Open
JeffreyCA wants to merge 1 commit into
Azure:hui/forward-service-envfrom
JeffreyCA:jeffreyca/service-env
Open

feat: forward service env to service targets and make it round-trip safe#9018
JeffreyCA wants to merge 1 commit into
Azure:hui/forward-service-envfrom
JeffreyCA:jeffreyca/service-env

Conversation

@JeffreyCA

Copy link
Copy Markdown
Contributor

This PR builds on #8936 (forward service env to extensions, tracked by #9008) and hardens the service-level env forwarding so it is complete, environment-aware, and safe to round-trip back into azure.yaml.

Context

#8936 forwarded expanded service env to framework providers, but left gaps that made it incomplete and unsafe for extensions that persist config.

  • Service target providers still received a ServiceConfig with no environment.
  • Get / GetResolvedServices resolved against the default environment (ignoring -e), and nested conversions dropped the resolver, so GetProject expanded service env to empty strings.
  • Incoming values were wrapped as templates on the way back in, so a value like pa$$word could be reinterpreted, and an AddService round-trip baked resolved values into azure.yaml, discarding the author's ${VAR} templates.

Changes

Service target providers now receive resolved env, via a shared serviceConfigToProto(lazyEnv, serviceConfig) helper used by both external provider types.

Environment resolution is now uniform and honors -e. Get / GetResolvedServices resolve through a single envResolver() backed by the session lazyEnv, the resolver is propagated into nested service conversions, and a missing environment expands to empty strings instead of failing.

Round-trips back into azure.yaml are safe. Incoming values are wrapped with osutil.NewLiteralExpandableString (escaping $ and \), and AddService preserves the original ${VAR} template for every env entry whose expanded value is unchanged.

Docs explain reading versus authoring. ServiceConfig.environment carries expanded values for providers to read, while raw ${VAR} templates are authored through the env path of the service config RPCs (GetServiceConfigSection / SetServiceConfigSection, or the Value variants).

Behavior at a glance

Scenario On #8936 With this PR
Service target provider reads serviceConfig.Environment not populated resolved against the active environment
GetProject service-level env expands to empty strings expanded against the active environment
Get / GetResolvedServices environment default environment, ignores -e active session environment, honors -e
AddService round-trip of an unchanged env entry template lost or baked to its resolved value ${VAR} template preserved
Env value containing $ (for example pa$$word) can be reinterpreted preserved verbatim

Using it as an extension author

A provider reads already-expanded values from serviceConfig.Environment:

// serviceConfig.Environment is resolved against the active azd environment (honors -e).
endpoint := serviceConfig.Environment["API_ENDPOINT"]

Given env: { API_ENDPOINT: ${SERVICE_API_ENDPOINT} }, a read-modify-write through ServiceConfig keeps the template intact (unchanged entries are not baked to their resolved values):

resp, _ := client.Project().Get(ctx, &azdext.EmptyRequest{})
svc := resp.Project.Services["api"]
svc.Host = "containerapp" // change something unrelated to env
_, _ = client.Project().AddService(ctx, &azdext.AddServiceRequest{Service: svc})

To author or edit a raw ${VAR} template, use the service config RPCs instead of AddService, since ServiceConfig.environment only carries expanded values (escape a literal $ as $$):

_, _ = client.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{
    ServiceName: "api",
    Path:        "env.API_ENDPOINT",
    Value:       structpb.NewStringValue("${SERVICE_API_ENDPOINT}"),
})

Testing

  • Mapper coverage for forward, reverse, round-trip, and nested conversions, including env values that reference the environment, static values, and values containing $.
  • osutil.NewLiteralExpandableString coverage for template syntax, $, backslashes, and UNC paths.
  • Framework and service target provider conversions, including per-call resolution and the empty-values fallback.
  • Project service coverage for environment-aware resolution and template preservation on AddService.

Correctness:
- Propagate the caller's resolver into nested ServiceConfig conversions in
  the ProjectConfig -> proto mapping so project-level payloads
  (ProjectService.Get, event Project payloads) expand service env values
  instead of blanking them; wrap nested conversion errors with the service
  name so one malformed service is diagnosable.
- Escape '$' and '\' when reverse-mapping extension-supplied env values
  (new osutil.NewLiteralExpandableString) so literal values survive later
  envsubst expansion instead of being reinterpreted as templates.
- Preserve unchanged ${VAR} env templates in azure.yaml when a service
  config round-trips through ProjectService.AddService, so read-modify-write
  extension flows no longer bake expanded values into the persisted file.
- Resolve the environment lazily per conversion in external framework and
  service target providers (shared serviceConfigToProto helper) instead of
  freezing a possibly-nil env at first registration; log env-load failures
  instead of silently expanding everything to empty strings.
- Use the session environment (honoring -e/--environment) in
  ProjectService.Get and GetResolvedServices instead of the persisted
  default environment, matching the framework/service-target/event paths.
- Log the previously swallowed conversion error in RequiredExternalTools.
- Add the environment field to the extension scaffolding models.proto so
  scaffolded extensions can consume it.

Cleanup:
- Remove the now-unused lazyEnvManager dependency from projectService.
- Consolidate the duplicated framework service registration tests into one
  table-driven test; env expansion behavior is asserted in pkg/project.
- Use Expand with error assertions and $-containing fixtures in mapper
  tests so escaping regressions fail visibly.
- Document expansion and write-back semantics in the extension framework
  docs.
@github-actions github-actions Bot added area/extensions Extensions (general) ext-x microsoft.azd.extensions developer extension (azd x) labels Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/extensions Extensions (general) ext-x microsoft.azd.extensions developer extension (azd x)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant