Script provisioning provider — full provider implementation
- Dominant language
- Go
- Stars
- 569
- Forks
- 364
- Avg merge
- 2d 22h
- Merged PRs (30d)
- 139
Description
## 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 ` 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)
```go
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`
- Parse config via `ParseProviderConfig()` (from #7735)
- Validate script paths, kinds, shell availability
- Store project path, options, and parsed config
#### `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`:
1. Report progress: `"Running script (N/M): <name>"`
2. `EnvResolver.Resolve()` -- build merged environment
3. `ScriptExecutor.Run()` -- execute with merged env, stream output
4. `OutputCollector.Collect()` -- discover and parse outputs.json
5. Merge outputs into accumulated outputs + prior output values for next script
6. 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`:
1. Report progress
2. Resolve env, execute script
3. 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
- [ ] `azd provision` with `provider: scripts` runs configured scripts and collects outputs
- [ ] `azd down` with `provider: scripts` runs configured destroy scripts
- [ ] Multi-script orchestration works: outputs from script N available as env vars to script N+1
- [ ] Script stdout/stderr streams to console in real-time
- [ ] Non-zero exit code fails the operation (unless `continueOnError: true`)
- [ ] Partial results returned on failure (outputs from successful scripts preserved)
- [ ] Secrets masked in all console output
- [ ] `AZURE_SUBSCRIPTION_ID` / `AZURE_LOCATION` trigger standard pickers when missing
- [ ] `azd provision --preview` lists scripts without executing them
- [ ] `State()` returns previously-stored outputs
- [ ] `AZD_PURGE=true` set during `azd down --purge`
- [ ] Unit tests for EnvResolver, ScriptExecutor, OutputCollector, Provider
- [ ] Integration test: provision -> state -> destroy cycle
Contributor guide
Assessment
This issue has not been assessed yet.