Azure / Azure/azure-dev

Script provisioning provider — config design and parsing

Open
#7,735 0 comments 0 reactions 1 assignee Claimed by @wbreza View on GitHub
area/provisioning enhancement
Dominant language
Go
Stars
569
Forks
364
Avg merge
2d 19h
Merged PRs (30d)
136

Description

## Script provisioning provider -- config design and parsing

**Parent epic**: #7733
**Framework**: #7465 | **Depends on**: #7734 (scaffold)

### Overview

Implement configuration parsing from `ProvisioningOptions.Config` (a `google.protobuf.Struct`) into typed Go structs, plus validation logic that runs during `Initialize()`.

### Configuration Model

Users configure the script provider in `azure.yaml`:

```yaml
infra:
provider: scripts
config:
provision:
- kind: sh
run: scripts/setup.sh
name: Setup Infrastructure
env:
AZURE_LOCATION: ${AZURE_LOCATION}
RESOURCE_GROUP: rg-${AZURE_ENV_NAME}
secrets:
DB_PASSWORD: akvs://my-vault/db-password
continueOnError: false
- kind: pwsh
run: scripts/configure-app.ps1
env:
APP_NAME: myapp-${AZURE_ENV_NAME}
destroy:
- kind: sh
run: scripts/teardown.sh
env:
RESOURCE_GROUP: rg-${AZURE_ENV_NAME}
```

### Types to Implement

File: `internal/provisioning/config.go`

```go
// ProviderConfig is the top-level configuration parsed from infra.config in azure.yaml.
type ProviderConfig struct {
Provision []*ScriptConfig `json:"provision,omitempty"`
Destroy []*ScriptConfig `json:"destroy,omitempty"`
}

// ScriptConfig defines the configuration for a single script entry.
// Field names align with HookConfig (pkg/ext/models.go) for user familiarity.
type ScriptConfig struct {
Kind string `json:"kind,omitempty"`
Shell string `json:"shell,omitempty"` // deprecated alias for Kind
Run string `json:"run"`
Name string `json:"name,omitempty"`
Env map[string]string `json:"env,omitempty"`
Secrets map[string]string `json:"secrets,omitempty"`
ContinueOnError bool `json:"continueOnError,omitempty"`
Windows *ScriptConfig `json:"windows,omitempty"`
Posix *ScriptConfig `json:"posix,omitempty"`
}
```

### Parsing Strategy

Use the JSON re-marshal pattern established in azd (`tools.UnmarshalHookConfig`):

```go
func ParseProviderConfig(raw map[string]any) (*ProviderConfig, error) {
data, err := json.Marshal(raw)
// ...
var cfg ProviderConfig
err = json.Unmarshal(data, &cfg)
// ...
}
```

The `ProvisioningOptions.Config` is a `google.protobuf.Struct`. Convert via `options.GetConfig().AsMap()` -> `ParseProviderConfig()`.

### Validation (during Initialize)

Implement `validateScriptConfig(sc *ScriptConfig, projectPath string, index int, section string) error` with these checks:

1. **`run` is non-empty** -- error: `"provision[0].run is required"`
2. **Script path is relative** -- reject absolute paths
3. **No path traversal** -- reject paths containing `..` that escape project root
4. **Script file exists** -- `os.Stat(filepath.Join(projectPath, sc.Run))`
5. **Valid kind** -- `sh`, `pwsh`, or auto-inferred from file extension (`.sh` -> `sh`, `.ps1` -> `pwsh`)
6. **Shell -> Kind mapping** -- if `Kind` is empty but `Shell` is set, map `Shell` to `Kind` (backward compat with hooks)
7. **At least one script** -- config must have at least one `provision` or `destroy` entry

Error messages should follow UX spec patterns:
```
ERROR: Invalid script configuration in provision[0]:
Script file not found: scripts/setup.sh
Resolved path: /home/user/myproject/scripts/setup.sh
```

### Platform Resolution

On Windows, if `sc.Windows != nil`, merge its fields into the base config. On Linux/macOS, use `sc.Posix`. This mirrors `HookConfig.Windows`/`HookConfig.Posix` semantics.

### Acceptance Criteria

- [ ] `ParseProviderConfig()` correctly converts `map[string]any` -> `*ProviderConfig`
- [ ] Validation catches: missing `run`, absolute paths, `..` path traversal, missing script files, unknown kinds
- [ ] `kind` auto-inferred from `.sh` -> `sh`, `.ps1` -> `pwsh` when omitted
- [ ] `shell` field maps to `kind` for backward compatibility
- [ ] Platform override merging works (Windows/Posix)
- [ ] Unit tests in `config_test.go` covering:
- Valid multi-script config
- Empty config (no provision or destroy)
- Missing script file
- Path traversal attempt (`../../../etc/passwd`)
- Unknown kind value
- Kind inference from file extension
- Shell -> Kind backward compat mapping
- [ ] Error messages include section name, index, and actionable details

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.