Script provisioning provider -- full provider implementation
Parent epic: #7733
Framework: #7465 | Depends on: #7734 (scaffold), #7735 (config parsing)
Overview
Implement all azdext.ProvisioningProvider lifecycle methods with real behavior, including the three core components: EnvResolver, ScriptExecutor, and OutputCollector.
Core Components
1. EnvResolver (internal/provisioning/env_resolver.go)
Resolves environment variables for each script using the 4-layer priority model:
Layer 4 (highest): secrets: map values (Key Vault refs, prompted values)
Layer 3: env: map values (${EXPRESSION} substitution)
Layer 2: azd environment values (.env + prior script outputs)
Layer 1 (lowest): OS environment variables
Key behaviors:
${EXPRESSION} substitution via osutil.ExpandableString (or drone/envsubst)
- Missing variable detection with actionable error messages (
azd env set guidance)
- Secret resolution:
akvs:// Key Vault refs -> azd env lookup -> interactive prompt -> persist to env
- Prior script outputs flow as env vars to subsequent scripts
2. ScriptExecutor (internal/provisioning/executor.go)
Runs individual shell scripts with a prepared environment:
- Shell invocation:
bash <script> or pwsh -NoProfile -NonInteractive -File <script>
- Working directory: project root (directory containing
azure.yaml)
- Streams stdout/stderr to console in real-time
- Captures exit code; non-zero fails unless
continueOnError: true
- Path containment check before execution (no escaping project root)
type ScriptResult struct {
ExitCode int
Stdout string
Stderr string
}
3. OutputCollector (internal/provisioning/output.go)
Discovers and parses outputs.json files after each script:
- Search strategy: walk up from script directory toward project root, stop at first
outputs.json
- Format:
{ "outputs": { "KEY": { "type": "string", "value": "..." } } }
- Merge: outputs accumulate across scripts (last-write-wins for duplicate keys)
- Missing file: not an error (scripts may have no outputs)
Lifecycle Method Implementations
Initialize(ctx, projectPath, options) error
EnsureEnv(ctx) (*EnsureEnvResult, error)
- If
AZURE_SUBSCRIPTION_ID missing -> show subscription picker via AzdClient.Account().GetSubscriptions() + AzdClient.Prompt().Select()
- If
AZURE_LOCATION missing -> show location picker
- Resolve all
secrets: values across all scripts (Key Vault / prompt / env lookup)
- Validate all
env: expressions resolve to non-empty values
- Persist prompted values to azd environment
- Return list of env keys that were set
Deploy(ctx, progress) (*ProvisioningDeployResult, error)
- Fetch current azd environment values (once, at start)
- For each script in
config.Provision:
- Report progress:
"Running script (N/M): <name>"
EnvResolver.Resolve() -- build merged environment
ScriptExecutor.Run() -- execute with merged env, stream output
OutputCollector.Collect() -- discover and parse outputs.json
- Merge outputs into accumulated outputs + prior output values for next script
- Report progress:
"Completed: <name>"
- Handle
continueOnError: if script fails and continueOnError: false, stop and return partial results
- Return
ProvisioningDeployResult with all accumulated outputs
Destroy(ctx, progress) (*ProvisioningDestroyResult, error)
- For each script in
config.Destroy:
- Report progress
- Resolve env, execute script
- On failure: stop unless
continueOnError
- Pass
AZD_PURGE=true when options.Purge() is true
- Return list of invalidated environment keys
Preview(ctx, progress) (*DeployPreviewResult, error)
- Return list of scripts that would execute (name, kind, path)
- Resolve env status (which variables are set vs. missing)
- Do NOT execute any scripts
State(ctx) (*ProvisioningStateResult, error)
- Return last-collected outputs from azd environment
- Empty resources list (scripts don't track individual Azure resources)
Parameters(ctx) (*ProvisioningParameterResult, error)
- Return
env: and secrets: declarations across all scripts as ProvisioningParameter list
- Include metadata: name, source (env/secret), required status
PlannedOutputs(ctx) (*PlannedOutputsResult, error)
- Return empty list (scripts cannot predict their outputs ahead of time)
Cross-Cutting Concerns
Error Handling
- Per-script failures include: script name, path, exit code, last 50 lines of stderr
- Partial results preserved (outputs from successful scripts returned even on failure)
- Descriptive messages per UX spec with actionable guidance
Security
- Path containment: all script paths validated against project root (no
.. escapes)
- Secret masking: secret values masked in all console output and logging
- No shell injection: scripts invoked as file arguments, not via shell string interpolation
Progress Reporting
- Report script start/complete/fail via
ProgressFunc
- Format:
"Running script (1/3): Setup Infrastructure", "Completed: Setup Infrastructure"
Testing
Unit Tests
| File |
Coverage |
env_resolver_test.go |
4-layer merge, expression substitution, missing vars, secret resolution |
executor_test.go |
Script execution, exit codes, stdout/stderr capture, continueOnError |
output_test.go |
outputs.json discovery, parsing, merge, missing file, malformed JSON |
provider_test.go |
Initialize validation, Deploy orchestration, Destroy flow |
Integration Test
- Full provision -> state -> destroy cycle with actual script files
- Multi-script orchestration: outputs from script N flow as env vars to script N+1
- Verify outputs appear in
DeployResult
Acceptance Criteria
Script provisioning provider -- full provider implementation
Parent epic: #7733
Framework: #7465 | Depends on: #7734 (scaffold), #7735 (config parsing)
Overview
Implement all
azdext.ProvisioningProviderlifecycle methods with real behavior, including the three core components:EnvResolver,ScriptExecutor, andOutputCollector.Core Components
1. EnvResolver (
internal/provisioning/env_resolver.go)Resolves environment variables for each script using the 4-layer priority model:
Key behaviors:
${EXPRESSION}substitution viaosutil.ExpandableString(ordrone/envsubst)azd env setguidance)akvs://Key Vault refs -> azd env lookup -> interactive prompt -> persist to env2. ScriptExecutor (
internal/provisioning/executor.go)Runs individual shell scripts with a prepared environment:
bash <script>orpwsh -NoProfile -NonInteractive -File <script>azure.yaml)continueOnError: true3. OutputCollector (
internal/provisioning/output.go)Discovers and parses
outputs.jsonfiles after each script:outputs.json{ "outputs": { "KEY": { "type": "string", "value": "..." } } }Lifecycle Method Implementations
Initialize(ctx, projectPath, options) errorParseProviderConfig()(from Script provisioning provider — config design and parsing #7735)EnsureEnv(ctx) (*EnsureEnvResult, error)AZURE_SUBSCRIPTION_IDmissing -> show subscription picker viaAzdClient.Account().GetSubscriptions()+AzdClient.Prompt().Select()AZURE_LOCATIONmissing -> show location pickersecrets:values across all scripts (Key Vault / prompt / env lookup)env:expressions resolve to non-empty valuesDeploy(ctx, progress) (*ProvisioningDeployResult, error)config.Provision:"Running script (N/M): <name>"EnvResolver.Resolve()-- build merged environmentScriptExecutor.Run()-- execute with merged env, stream outputOutputCollector.Collect()-- discover and parse outputs.json"Completed: <name>"continueOnError: if script fails andcontinueOnError: false, stop and return partial resultsProvisioningDeployResultwith all accumulated outputsDestroy(ctx, progress) (*ProvisioningDestroyResult, error)config.Destroy:continueOnErrorAZD_PURGE=truewhenoptions.Purge()is truePreview(ctx, progress) (*DeployPreviewResult, error)State(ctx) (*ProvisioningStateResult, error)Parameters(ctx) (*ProvisioningParameterResult, error)env:andsecrets:declarations across all scripts asProvisioningParameterlistPlannedOutputs(ctx) (*PlannedOutputsResult, error)Cross-Cutting Concerns
Error Handling
Security
..escapes)Progress Reporting
ProgressFunc"Running script (1/3): Setup Infrastructure","Completed: Setup Infrastructure"Testing
Unit Tests
env_resolver_test.goexecutor_test.gooutput_test.goprovider_test.goIntegration Test
DeployResultAcceptance Criteria
azd provisionwithprovider: scriptsruns configured scripts and collects outputsazd downwithprovider: scriptsruns configured destroy scriptscontinueOnError: true)AZURE_SUBSCRIPTION_ID/AZURE_LOCATIONtrigger standard pickers when missingazd provision --previewlists scripts without executing themState()returns previously-stored outputsAZD_PURGE=trueset duringazd down --purge