diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 0c2d440ed42..4ceabd322e7 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -404,7 +404,7 @@ overrides: - filename: extensions/azure.ai.models/internal/cmd/custom_create.go words: - Qwen - - filename: pkg/infra/provisioning/bicep/local_preflight.go + - filename: pkg/infra/provisioning/bicep/provision_validation.go words: - actioned - filename: pkg/project/project.go diff --git a/cli/azd/CHANGELOG.md b/cli/azd/CHANGELOG.md index 9f15a429fbc..c188bbbf6d4 100644 --- a/cli/azd/CHANGELOG.md +++ b/cli/azd/CHANGELOG.md @@ -6,6 +6,8 @@ ### Breaking Changes +- [[#7113]](https://github.com/Azure/azure-dev/issues/7113) Rename azd's client-side "preflight" feature to **provision validation** to avoid confusion with the Azure ARM Preflight API. The local validation toggle moves from `provision.preflight` to a new `validation.provision` config key (`azd config set validation.provision off`), and `provision.preflight` now controls **only** the server-side ARM preflight call. Provisioning telemetry events/fields are renamed from `validation.preflight*` to `validation.provision*`, the validation outcome values `aborted_by_errors`/`aborted_by_user` are renamed to `canceled_by_errors`/`canceled_by_user`, and the extension validation `check_type` is renamed from `local-preflight` to `provision`. Users who previously set `provision.preflight off` to disable azd's local checks should now also set `validation.provision off`. + ### Bugs Fixed ### Other Changes diff --git a/cli/azd/cmd/init_test.go b/cli/azd/cmd/init_test.go index 24b47f8006e..b9549f964d1 100644 --- a/cli/azd/cmd/init_test.go +++ b/cli/azd/cmd/init_test.go @@ -1060,7 +1060,7 @@ func Test_EnvSetSecretAction_VaultNotProvisioned_Cancel(t *testing.T) { _, err := action.Run(t.Context()) require.Error(t, err) - assert.Contains(t, err.Error(), "operation cancelled by user") + assert.Contains(t, err.Error(), "operation canceled by user") } func Test_EnvSetSecretAction_VaultNotProvisioned_SelectError(t *testing.T) { @@ -1559,7 +1559,7 @@ func Test_EnvSetSecretAction_VaultDefinedButNotProvisioned(t *testing.T) { _, err := action.Run(t.Context()) require.Error(t, err) - require.Contains(t, err.Error(), "cancelled") + require.Contains(t, err.Error(), "canceled") } func Test_SelectDistinctExtension_ZeroMatches(t *testing.T) { diff --git a/cli/azd/cmd/middleware/error_test.go b/cli/azd/cmd/middleware/error_test.go index 5cebdd317f6..2d2e736a04d 100644 --- a/cli/azd/cmd/middleware/error_test.go +++ b/cli/azd/cmd/middleware/error_test.go @@ -509,7 +509,7 @@ func Test_ShouldSkipAgentHandling_ControlFlow(t *testing.T) { t.Run("Wrapped ErrAbortedByUser is skipped", func(t *testing.T) { t.Parallel() - wrapped := fmt.Errorf("preflight declined: %w", internal.ErrAbortedByUser) + wrapped := fmt.Errorf("validation declined: %w", internal.ErrAbortedByUser) require.True(t, shouldSkipAgentHandling(wrapped)) }) diff --git a/cli/azd/docs/design/local-preflight-validation.md b/cli/azd/docs/design/provision-validation.md similarity index 60% rename from cli/azd/docs/design/local-preflight-validation.md rename to cli/azd/docs/design/provision-validation.md index 3761f2eac85..3f1ca2c2495 100644 --- a/cli/azd/docs/design/local-preflight-validation.md +++ b/cli/azd/docs/design/provision-validation.md @@ -1,33 +1,38 @@ -# Local Preflight Validation +# Provision Validation -Local preflight validation is a client-side check that runs automatically before every `azd provision` deployment. It analyzes the compiled ARM template and a Bicep deployment snapshot to detect common issues — such as missing permissions. +Local provision validation is a client-side check that runs automatically before every `azd provision` deployment. It analyzes the compiled ARM template and a Bicep deployment snapshot to detect common issues — such as missing permissions. ## When It Runs -The preflight pipeline executes inside `BicepProvider.Deploy()`, after the Bicep module has been compiled and parameters resolved, but **before** the template is sent to Azure for server-side validation or deployment. +The validation pipeline executes inside `BicepProvider.Deploy()`, after the Bicep module has been compiled and parameters resolved, but **before** the template is sent to Azure for server-side validation or deployment. ``` azd provision │ ├── Compile Bicep module → ARM template + parameters - ├── ► Local preflight validation ← runs here + ├── ► Local provision validation ← runs here │ ├── Parse ARM template (schema, contentVersion, resources) │ ├── Generate Bicep snapshot (resolved resource graph) │ ├── Analyze resources (derive properties) │ └── Run registered check functions - ├── Server-side preflight (Azure ValidatePreflight API) + ├── Server-side ARM preflight (Azure ValidatePreflight API) └── Deploy ``` -The user can disable preflight entirely by setting `provision.preflight` to `"off"` in their azd user configuration: +The user can disable local provision validation by setting `validation.provision` to `"off"` in their azd user configuration: ```bash -azd config set provision.preflight off +azd config set validation.provision off ``` +> **Note:** `validation.provision` controls only azd's *local* (client-side) provision +> validation described in this document. The separate `provision.preflight` config key +> controls the *server-side* ARM preflight call (`azd config set provision.preflight off`). +> The two are independent. + ## Bicep Snapshots -Local preflight depends on the `bicep snapshot` command (available in modern Bicep CLI versions). The snapshot produces a **fully resolved deployment graph**: all template expressions are evaluated, conditions are applied, copy loops are expanded, and nested deployments are flattened into a single flat list of predicted resources. +Local provision validation depends on the `bicep snapshot` command (available in modern Bicep CLI versions). The snapshot produces a **fully resolved deployment graph**: all template expressions are evaluated, conditions are applied, copy loops are expanded, and nested deployments are flattened into a single flat list of predicted resources. ### Why Snapshots Instead of Manual Parsing @@ -65,14 +70,14 @@ Advantages of snapshots over manual template parsing: │ ▼ ┌──────────────────────────┐ -│ preflight-*.bicepparam │ (temporary) +│ validation-*.bicepparam │ (temporary) └──────────┬───────────────┘ │ bicep snapshot --subscription-id ... --resource-group ... │ ▼ ┌──────────────────────────┐ -│ preflight-*.snapshot.json│ +│ validation-*.snapshot.json│ │ { │ │ "predictedResources": │ │ [ │ @@ -85,34 +90,38 @@ Advantages of snapshots over manual template parsing: ## Check Pipeline -The preflight system uses a pluggable pipeline of check functions. Each check receives a `validationContext` containing: +The validation system uses a pluggable pipeline of check functions. Each check receives a `validationContext` containing: - **`Console`** — for user interaction (prompts, messages). - **`Props`** — derived properties from the resource analysis (e.g. `HasRoleAssignments`). - **`ResourcesSnapshot`** — the raw JSON from `bicep snapshot`. - **`SnapshotResources`** — the parsed `[]armTemplateResource` list from the snapshot. -Checks are registered via `AddCheck()` before calling `validate()`. They run in registration order and each returns either: +Checks are registered via `AddCheck()` before calling `validate()`. They run in registration order. Each check is a `ProvisionValidationCheck{RuleID, Fn}`, where `Fn` returns: -- `nil` — nothing to report (check passed). -- `*PreflightCheckResult` with a `Severity` (`PreflightCheckWarning` or `PreflightCheckError`) and a `Message`. +- `nil` (or an empty slice) — nothing to report (check passed). +- `[]ProvisionValidationCheckResult`, each with a `Severity` (`ProvisionValidationCheckWarning` or `ProvisionValidationCheckError`) and a `Message`. A single check may emit multiple results. ### Adding a New Check -To add a new preflight check: +To add a new validation check: ```go -localPreflight.AddCheck(func(ctx context.Context, valCtx *validationContext) (*PreflightCheckResult, error) { - // Inspect valCtx.SnapshotResources, valCtx.Props, etc. - for _, res := range valCtx.SnapshotResources { - if strings.EqualFold(res.Type, "Microsoft.SomeProvider/problematicResource") { - return &PreflightCheckResult{ - Severity: PreflightCheckWarning, - Message: "This resource type requires additional configuration.", - }, nil +validator.AddCheck(ProvisionValidationCheck{ + RuleID: "some_provider_problematic_resource", + Fn: func(ctx context.Context, valCtx *validationContext) ([]ProvisionValidationCheckResult, error) { + // Inspect valCtx.SnapshotResources, valCtx.Props, etc. + var results []ProvisionValidationCheckResult + for _, res := range valCtx.SnapshotResources { + if strings.EqualFold(res.Type, "Microsoft.SomeProvider/problematicResource") { + results = append(results, ProvisionValidationCheckResult{ + Severity: ProvisionValidationCheckWarning, + Message: "This resource type requires additional configuration.", + }) + } } - } - return nil, nil // nothing to report + return results, nil // empty slice = nothing to report + }, }) ``` @@ -124,13 +133,13 @@ localPreflight.AddCheck(func(ctx context.Context, valCtx *validationContext) (*P ## UX Presentation -Results are displayed using the `PreflightReport` UX component (`pkg/output/ux/preflight_report.go`), which implements the standard `UxItem` interface. The report groups and orders findings: all warnings appear first, followed by all errors. Each entry is prefixed with the standard azd status icons. +Results are displayed using the `ProvisionValidationReport` UX component (`pkg/output/ux/provision_validation_report.go`), which implements the standard `UxItem` interface. The report groups and orders findings: all warnings appear first, followed by all errors. Each entry is prefixed with the standard azd status icons. ## Scenarios ### Scenario 1: No Issues Found -All registered checks pass. No output is printed from the preflight step. The deployment proceeds directly to server-side validation and then Azure deployment. +All registered checks pass. No output is printed from the validation step. The deployment proceeds directly to server-side validation and then Azure deployment. ``` Validating deployment (✓) Done: @@ -150,29 +159,28 @@ to create role assignments (Microsoft.Authorization/roleAssignments/write) on subscription sub-456. The deployment includes role assignments and will fail without this permission. -? Preflight validation found warnings that may cause the deployment - to fail. Do you want to continue? (Y/n) +? Proceed with provisioning despite the warnings above? (Y/n) ``` -If the user confirms (or accepts the default), deployment proceeds normally. If the user declines, the operation is aborted with a zero exit code (an intentional abort, not a failure). +If the user confirms (or accepts the default), deployment proceeds normally. If the user declines, the operation is canceled with a zero exit code (an intentional cancel, not a failure). ### Scenario 3: Errors Only -One or more checks return errors. The errors are displayed and the deployment is **immediately aborted** — the user is not prompted. The CLI exits with a zero exit code. +One or more checks return errors. The errors are displayed and the deployment is **immediately canceled** — the user is not prompted. The CLI exits with a zero exit code. ``` Validating deployment (x) Failed: critical configuration error detected in template -preflight validation detected errors, deployment aborted +Validation detected errors, provisioning canceled. ``` -Note: the exit code is **zero** because the preflight validation **successfully** detected problems and intentionally aborted the deployment. This is not an unexpected internal failure — the CLI completed its task (validating and reporting errors) without encountering any execution errors itself. +Note: the exit code is **zero** because the provision validation **successfully** detected problems and intentionally canceled the deployment. This is not an unexpected internal failure — the CLI completed its task (validating and reporting errors) without encountering any execution errors itself. ### Scenario 4: Warnings and Errors -When the report contains both warnings and errors, warnings are listed first and errors second. Because errors are present the deployment is aborted immediately — the warning prompt is skipped. +When the report contains both warnings and errors, warnings are listed first and errors second. Because errors are present the deployment is canceled immediately — the warning prompt is skipped. ``` Validating deployment @@ -183,29 +191,29 @@ role assignments on this subscription. (x) Failed: required parameter 'storageAccountName' is missing from the deployment. -preflight validation detected errors, deployment aborted +Validation detected errors, provisioning canceled. ``` ### Scenario 5: Check Function Returns an Error -If a check function itself fails (returns a Go `error` rather than a `*PreflightCheckResult`), this is treated as an infrastructure failure. The CLI reports it as a hard error and exits with a non-zero code. This is distinct from a check returning a result with `PreflightCheckError` severity — that case means "we successfully detected a problem in the template", while an error return means "something went wrong while trying to run the check". +If a check function itself fails (returns a Go `error` rather than `[]ProvisionValidationCheckResult`), this is treated as an infrastructure failure. The CLI reports it as a hard error and exits with a non-zero code. This is distinct from a check returning a result with `ProvisionValidationCheckError` severity — that case means "we successfully detected a problem in the template", while an error return means "something went wrong while trying to run the check". ``` -ERROR: local preflight validation failed: preflight check failed: +ERROR: local provision validation failed: validation check failed: ``` ## Exit Code Behavior The exit code distinguishes between **successful operation** (the CLI did what it was supposed to do) and **internal failure** (the CLI could not complete its task). -Preflight validation detecting errors and aborting the deployment is a **successful outcome** — the CLI performed the validation and correctly prevented a bad deployment. Only failures in the validation machinery itself produce a non-zero exit code. +Provision validation detecting errors and canceling provisioning is a **successful outcome** — the CLI performed the validation and correctly prevented a bad deployment. Only failures in the validation machinery itself produce a non-zero exit code. | Outcome | Exit Code | Rationale | |---|---|---| | No issues | 0 | Deployment proceeds and succeeds. | | Warnings only, user continues | 0 | User acknowledged warnings; deployment proceeds. | -| Warnings only, user declines | 0 | User chose to abort; intentional, not a failure. | -| Errors detected | 0 | Validation successfully detected problems and aborted the deployment. | +| Warnings only, user declines | 0 | User chose to cancel; intentional, not a failure. | +| Errors detected | 0 | Validation successfully detected problems and canceled provisioning. | | Check function error | 1 | Internal failure running a check (the `validate` function returned a non-nil error). | ## File Layout @@ -213,34 +221,34 @@ Preflight validation detecting errors and aborting the deployment is a **success ``` pkg/ ├── infra/provisioning/bicep/ -│ ├── local_preflight.go # Core pipeline, ARM types, parseTemplate, analyzeResources -│ ├── local_preflight_test.go # Unit tests for parsing, analysis, check pipeline +│ ├── provision_validation.go # Core pipeline, ARM types, parseTemplate, analyzeResources +│ ├── provision_validation_test.go # Unit tests for parsing, analysis, check pipeline │ ├── role_assignment_check_test.go # Tests for the role assignment check │ ├── generate_bicep_param_test.go # Tests for .bicepparam generation -│ └── bicep_provider.go # validatePreflight() integration, checkRoleAssignmentPermissions +│ └── bicep_provider.go # validateProvision() integration, checkRoleAssignmentPermissions ├── infra/provisioning/ │ └── validation_dispatcher.go # ValidationCheckDispatcher interface (DI decoupling) ├── output/ux/ -│ ├── preflight_report.go # PreflightReport UxItem -│ └── preflight_report_test.go # Tests for PreflightReport +│ ├── provision_validation_report.go # ProvisionValidationReport UxItem +│ └── provision_validation_report_test.go # Tests for ProvisionValidationReport └── tools/bicep/ └── bicep.go # Snapshot() method, SnapshotOptions builder ``` ## Extension-Provided Checks -Extensions can contribute validation checks to the local preflight pipeline using -the `validation-provider` capability. This allows extensions to inspect the Bicep -deployment data (ARM template, snapshot, parameters, location) and return additional -warnings or errors that are merged into the preflight report. +Extensions can contribute validation checks to the local provision validation +pipeline using the `validation-provider` capability. This allows extensions to +inspect the Bicep deployment data (ARM template, snapshot, parameters, location) and +return additional warnings or errors that are merged into the validation report. ### How It Works 1. The extension declares `validation-provider` in its `extension.yaml` capabilities. 2. During startup, the extension registers one or more checks with a `check_type` - (e.g., `"local-preflight"`) and a stable `rule_id`. -3. When `BicepProvider.validatePreflight()` runs, after the built-in checks complete, - it dispatches to all extension-registered checks matching `check_type: "local-preflight"`. + (e.g., `"provision"`) and a stable `rule_id`. +3. When `BicepProvider.validateProvision()` runs, after the built-in checks complete, + it dispatches to all extension-registered checks matching `check_type: "provision"`. 4. Each extension check receives a context map with: - `resources_snapshot` — Bicep snapshot JSON (`predictedResources`) - `predicted_resources` — Parsed resource array from the snapshot @@ -248,7 +256,7 @@ warnings or errors that are merged into the preflight report. - `arm_parameters` — Resolved ARM parameters JSON - `env_location` — Azure location string 5. The extension returns `ValidationCheckResult` items (severity, message, suggestion, links) - which are appended to the preflight report. + which are appended to the validation report. ### Extension Code Example @@ -256,7 +264,7 @@ warnings or errors that are merged into the preflight report. // In your extension's listen command: host := azdext.NewExtensionHost(azdClient). WithValidationCheck(azdext.ValidationCheckRegistration{ - CheckType: "local-preflight", + CheckType: "provision", RuleID: "my_naming_rule", Factory: func() azdext.ValidationCheckProvider { return &MyNamingCheck{} @@ -293,6 +301,6 @@ described above. ### Future Check Types The `check_type` field is designed for extensibility. Currently only -`"local-preflight"` is supported, but future check types (e.g., `"project-config"`, +`"provision"` is supported, but future check types (e.g., `"project-config"`, `"auth"`) can be added without changing the protocol. Each check type defines its own context keys. diff --git a/cli/azd/docs/environment-variables.md b/cli/azd/docs/environment-variables.md index a5b8830c3f7..8b423f21387 100644 --- a/cli/azd/docs/environment-variables.md +++ b/cli/azd/docs/environment-variables.md @@ -54,7 +54,7 @@ integration. | `AZD_DEPLOY_CONCURRENCY` | Maximum number of services to deploy in parallel during `azd deploy`. Only takes effect when at least one service declares `uses:` targeting another service; without `uses:` edges, services deploy sequentially in alphabetical order for backward compatibility (see [concurrency model](concurrency-model.md)). Parsed as a positive integer; clamped to a maximum of `64`. When unset, concurrency is unlimited (bounded only by the number of services). | | `AZD_DEPLOY_TIMEOUT` | Timeout for deployment operations, parsed as an integer number of seconds (for example, `1200`). Defaults to `1200` seconds (20 minutes). | | `AZD_PROVISION_CONCURRENCY` | Maximum number of infrastructure layers to provision in parallel during `azd provision`. Parsed as a positive integer; clamped to a maximum of `64`. When unset, concurrency is unlimited (bounded only by the dependency graph). | -| `AZD_DEPLOYMENT_ID_FILE` | Absolute path of a file where `azd` writes ARM deployment IDs in NDJSON format (one JSON line per layer) during `azd provision` or `azd up`. The file is truncated at the start of each provisioning run, and each infrastructure layer appends one line as its ARM deployment starts. Each line has the shape `{"deploymentId":"/subscriptions/.../deployments/","layer":""}` — the `layer` field is empty for non-layered (single-module) provisioning. Consumers should tail/watch the file and parse each line independently; unknown fields must be ignored for forward compatibility. The path must be absolute (relative paths are ignored); the containing directory must already exist and be writable. Lines are only appended when an ARM deployment is actually started — runs short-circuited by the deployment-state cache or aborted by preflight do not produce output. A process-wide mutex serializes writes so each line is always complete. If the file cannot be written (for example, the parent directory does not exist, the path is not writable, or the path points to a directory rather than a file), provisioning continues and the failure is recorded via the standard log; that output is only visible when `--debug` or `AZD_DEBUG_LOG` is enabled. On Windows, consumers should use a file-watcher pattern that does not keep a read handle open, otherwise new appends may fail. Only Bicep deployments are supported. | +| `AZD_DEPLOYMENT_ID_FILE` | Absolute path of a file where `azd` writes ARM deployment IDs in NDJSON format (one JSON line per layer) during `azd provision` or `azd up`. The file is truncated at the start of each provisioning run, and each infrastructure layer appends one line as its ARM deployment starts. Each line has the shape `{"deploymentId":"/subscriptions/.../deployments/","layer":""}` — the `layer` field is empty for non-layered (single-module) provisioning. Consumers should tail/watch the file and parse each line independently; unknown fields must be ignored for forward compatibility. The path must be absolute (relative paths are ignored); the containing directory must already exist and be writable. Lines are only appended when an ARM deployment is actually started — runs short-circuited by the deployment-state cache or canceled by provision validation do not produce output. A process-wide mutex serializes writes so each line is always complete. If the file cannot be written (for example, the parent directory does not exist, the path is not writable, or the path points to a directory rather than a file), provisioning continues and the failure is recorded via the standard log; that output is only visible when `--debug` or `AZD_DEBUG_LOG` is enabled. On Windows, consumers should use a file-watcher pattern that does not keep a read handle open, otherwise new appends may fail. Only Bicep deployments are supported. | | `AZD_UP_CONCURRENCY` | Maximum number of steps to run in parallel during `azd up`. Parsed as a positive integer; clamped to a maximum of `64`. Falls back to `AZD_DEPLOY_CONCURRENCY` when unset. When both are unset, concurrency is unlimited. | | `AZD_DEPLOY_{SERVICE}_SLOT_NAME` | Sets the App Service deployment slot target for a service. Replace `{SERVICE}` with the uppercase service name (hyphens become underscores). Set to `production` to deploy to the main app, or a slot name (e.g., `staging`). When slots exist and this is not set, `--no-prompt` mode fails with an error listing available targets. | | `AZD_DEPLOY_{SERVICE}_SKIP_STATUS_CHECK` | If `true`, skips runtime deployment status tracking for the named Linux App Service after zip deploy. Useful when the target web app is intentionally stopped. Parsed as a boolean (`true`/`false`/`1`/`0`). `{SERVICE}` follows the same naming rules as `AZD_DEPLOY_{SERVICE}_SLOT_NAME`. | diff --git a/cli/azd/docs/extensions/extension-framework.md b/cli/azd/docs/extensions/extension-framework.md index 6cb390088c1..78c138eadbb 100644 --- a/cli/azd/docs/extensions/extension-framework.md +++ b/cli/azd/docs/extensions/extension-framework.md @@ -946,7 +946,7 @@ Extensions can provide AI agent tools through the Model Context Protocol, enabli Extensions can contribute validation checks to azd's validation pipeline. Currently supported check types: -- **`local-preflight`** — Checks run during `azd provision` before deployment. The +- **`provision`** — Checks run during `azd provision` before deployment. The extension receives the Bicep snapshot, ARM template, ARM parameters, and Azure location as context. @@ -958,7 +958,7 @@ changes. ```go host := azdext.NewExtensionHost(azdClient). WithValidationCheck(azdext.ValidationCheckRegistration{ - CheckType: "local-preflight", + CheckType: "provision", RuleID: "my_naming_rule", Factory: func() azdext.ValidationCheckProvider { return &MyNamingCheck{} @@ -966,7 +966,7 @@ host := azdext.NewExtensionHost(azdClient). }) ``` -See [`local-preflight-validation.md`](../design/local-preflight-validation.md#extension-provided-checks) +See [`provision-validation.md`](../design/provision-validation.md#extension-provided-checks) for full details on the check interface and context keys. #### Future Considerations @@ -1078,7 +1078,7 @@ Extensions can declare the following capabilities in their manifest: - **`service-target-provider`**: Provide custom service deployment targets - **`framework-service-provider`**: Provide custom language frameworks and build systems - **`provisioning-provider`**: Provide a custom infrastructure provisioning experience (alternative to Bicep / Terraform) -- **`validation-provider`**: Contribute validation checks to azd's preflight and future validation pipelines +- **`validation-provider`**: Contribute validation checks to azd's provision and future validation pipelines - **`metadata`**: Provide comprehensive metadata about commands and configuration schemas #### Complete Extension Manifest Example diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go index 12f5453d99e..510da320f25 100644 --- a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go +++ b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go @@ -39,7 +39,7 @@ func newListenCommand() *cobra.Command { return project.NewDemoProvisioningProvider(azdClient) }). WithValidationCheck(azdext.ValidationCheckRegistration{ - CheckType: "local-preflight", + CheckType: "provision", RuleID: "demo_warning", Factory: func() azdext.ValidationCheckProvider { return project.NewDemoValidationCheck() diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/project/demo_validation_check_test.go b/cli/azd/extensions/microsoft.azd.demo/internal/project/demo_validation_check_test.go index 03739b8ef14..25ac94e6e7b 100644 --- a/cli/azd/extensions/microsoft.azd.demo/internal/project/demo_validation_check_test.go +++ b/cli/azd/extensions/microsoft.azd.demo/internal/project/demo_validation_check_test.go @@ -16,11 +16,11 @@ func TestDemoValidationCheck_AlwaysReturnsWarning(t *testing.T) { t.Run("no ARM template", func(t *testing.T) { valCtx := &azdext.ValidationContext{ ContextID: "test-ctx", - CheckType: "local-preflight", + CheckType: "provision", Data: map[string][]byte{}, } req := &azdext.ValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "demo_warning", ContextId: "test-ctx", } @@ -42,13 +42,13 @@ func TestDemoValidationCheck_AlwaysReturnsWarning(t *testing.T) { resourcesJSON := []byte(`[{"type": "a"}, {"type": "b"}, {"type": "c"}]`) valCtx := &azdext.ValidationContext{ ContextID: "test-ctx-2", - CheckType: "local-preflight", + CheckType: "provision", Data: map[string][]byte{ azdext.ValidationContextPredictedResources: resourcesJSON, }, } req := &azdext.ValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "demo_warning", ContextId: "test-ctx-2", } diff --git a/cli/azd/grpc/proto/validation.proto b/cli/azd/grpc/proto/validation.proto index 57b3178481b..b2d3e7f3075 100644 --- a/cli/azd/grpc/proto/validation.proto +++ b/cli/azd/grpc/proto/validation.proto @@ -32,7 +32,7 @@ message ValidationMessage { // RegisterValidationCheckRequest is sent by extensions to register a validation check. message RegisterValidationCheckRequest { // check_type identifies the validation context this check targets - // (e.g. "local-preflight", "project-config"). Future check types can be + // (e.g. "provision", "project-config"). Future check types can be // added without changing the protocol. string check_type = 1; // rule_id is a stable, unique identifier for this check rule, diff --git a/cli/azd/internal/cmd/provision_graph.go b/cli/azd/internal/cmd/provision_graph.go index a5d81947cc9..71fcc79fa77 100644 --- a/cli/azd/internal/cmd/provision_graph.go +++ b/cli/azd/internal/cmd/provision_graph.go @@ -38,11 +38,11 @@ import ( "go.uber.org/multierr" ) -// errPreflightAbortedByUser is a sentinel returned from [provisionSingleLayer] -// when the underlying provider reports [provisioning.PreflightAbortedSkipped]. +// errProvisionValidationCanceledByUser is a sentinel returned from [provisionSingleLayer] +// when the underlying provider reports [provisioning.ProvisionValidationCanceledSkipped]. // The caller translates it to [internal.ErrAbortedByUser] at the action -// boundary so the user sees a friendly "Provisioning was cancelled." message. -var errPreflightAbortedByUser = errors.New("provisioning aborted by user during preflight") +// boundary so the user sees a friendly "Provisioning was canceled." message. +var errProvisionValidationCanceledByUser = errors.New("provisioning canceled by user during validation") // provisionLayersGraph is the single execution entry point for // [ProvisionAction]. It dispatches to one of four disjoint paths: @@ -152,11 +152,11 @@ func (p *ProvisionAction) provisionLayersGraph( return hookErr } - if deployResult.SkippedReason == provisioning.PreflightAbortedSkipped { + if deployResult.SkippedReason == provisioning.ProvisionValidationCanceledSkipped { // Return the internal sentinel; wrapProvisionError at the - // outer boundary emits the "Provisioning was cancelled." + // outer boundary emits the "Provisioning was canceled." // UX message and translates to internal.ErrAbortedByUser. - return errPreflightAbortedByUser + return errProvisionValidationCanceledByUser } skipped := deployResult.SkippedReason == provisioning.DeploymentStateSkipped @@ -287,7 +287,7 @@ func (p *ProvisionAction) provisionLayersGraph( if result.Error != nil { // Peel the scheduler's `step "X" failed:` prefix and run the // underlying error through the same wrapping used by the sequential - // path (OpenAI / Responsible AI translation, preflight-abort → + // path (OpenAI / Responsible AI translation, validation-cancel → // ErrAbortedByUser, state dump on provider failure, etc.). return nil, p.wrapProvisionError(ctx, unwrapStepErrors(result)) } @@ -635,7 +635,7 @@ func (p *ProvisionAction) logProvisionGraphTimings(result *exegraph.RunResult) { // wrapProvisionError replicates the sequential path's error-wrapping logic // (provision.go:382-435): JSON state dump on failure, OpenAI access wrapper, -// Responsible AI wrapper, and the preflight-aborted translation. +// Responsible AI wrapper, and the validation-canceled translation. func (p *ProvisionAction) wrapProvisionError(ctx context.Context, err error) error { return wrapProvisionError(ctx, err, provisionErrorDeps{ console: p.console, @@ -663,10 +663,10 @@ type provisionErrorDeps struct { // provisionManager (the same one used for the primary layer) so that the // JSON state dump on failure has something to render. func wrapProvisionError(ctx context.Context, err error, deps provisionErrorDeps) error { - // Preflight-aborted → ErrAbortedByUser with success message. - if errors.Is(err, errPreflightAbortedByUser) { + // Validation-canceled → ErrAbortedByUser with success message. + if errors.Is(err, errProvisionValidationCanceledByUser) { deps.console.MessageUxItem(ctx, &ux.ActionResult{ - SuccessMessage: "Provisioning was cancelled.", + SuccessMessage: "Provisioning was canceled.", }) return internal.ErrAbortedByUser } @@ -850,8 +850,8 @@ func provisionSingleLayer( // parallel and B's clone may pre-date A's reload. // // Returns the raw [provisioning.DeployResult] so callers can record skip -// semantics; on [provisioning.PreflightAbortedSkipped] it returns -// [errPreflightAbortedByUser] so [ProvisionAction] can translate it to +// semantics; on [provisioning.ProvisionValidationCanceledSkipped] it returns +// [errProvisionValidationCanceledByUser] so [ProvisionAction] can translate it to // [internal.ErrAbortedByUser]. func runProvisionSingleLayer( ctx context.Context, @@ -961,8 +961,8 @@ func runProvisionSingleLayer( return nil, fmt.Errorf("deploying layer %s: %w", stepName, err) } - if deployResult.SkippedReason == provisioning.PreflightAbortedSkipped { - return deployResult, errPreflightAbortedByUser + if deployResult.SkippedReason == provisioning.ProvisionValidationCanceledSkipped { + return deployResult, errProvisionValidationCanceledByUser } // ── Step 4: Env merge ── diff --git a/cli/azd/internal/cmd/provision_test.go b/cli/azd/internal/cmd/provision_test.go index 4b36dd719a7..1fde6eb1004 100644 --- a/cli/azd/internal/cmd/provision_test.go +++ b/cli/azd/internal/cmd/provision_test.go @@ -104,12 +104,12 @@ func (p *mockProvider) PlannedOutputs(_ context.Context) ([]provisioning.Planned return nil, nil } -// TestProvisionAction_PreflightAborted verifies that when the user declines -// preflight warnings, ProvisionAction.Run returns ErrAbortedByUser and does NOT +// TestProvisionAction_ValidationCanceled verifies that when the user declines +// validation warnings, ProvisionAction.Run returns ErrAbortedByUser and does NOT // attempt to read deployResult.Deployment.Outputs (which would nil-panic). // // Regression test for https://github.com/Azure/azure-dev/issues/7305 -func TestProvisionAction_PreflightAborted(t *testing.T) { +func TestProvisionAction_ValidationCanceled(t *testing.T) { t.Parallel() // Set up a temp project with a minimal infra directory so ImportManager works. projectDir := t.TempDir() @@ -117,10 +117,10 @@ func TestProvisionAction_PreflightAborted(t *testing.T) { require.NoError(t, os.MkdirAll(infraDir, 0o755)) require.NoError(t, os.WriteFile(filepath.Join(infraDir, "main.bicep"), []byte("targetScope = 'subscription'\n"), 0o600)) - // Mock provider that simulates preflight abort (user said No). + // Mock provider that simulates validation cancel (user said No). provider := &mockProvider{ deployResult: &provisioning.DeployResult{ - SkippedReason: provisioning.PreflightAbortedSkipped, + SkippedReason: provisioning.ProvisionValidationCanceledSkipped, }, } diff --git a/cli/azd/internal/errors.go b/cli/azd/internal/errors.go index 2303f6885c8..d6391584b14 100644 --- a/cli/azd/internal/errors.go +++ b/cli/azd/internal/errors.go @@ -78,11 +78,11 @@ var ( var ( ErrNoArgsProvided = errors.New("required arguments not provided") ErrInvalidArgValue = errors.New("invalid argument value") - ErrOperationCancelled = errors.New("operation cancelled by user") + ErrOperationCancelled = errors.New("operation canceled by user") - // ErrAbortedByUser indicates the user intentionally declined to proceed (e.g. preflight warnings). + // ErrAbortedByUser indicates the user intentionally declined to proceed (e.g. provision validation warnings). // This is not a failure — the CLI should exit with code 0. - ErrAbortedByUser = errors.New("operation aborted by user") + ErrAbortedByUser = errors.New("operation canceled by user") ) // Config errors diff --git a/cli/azd/internal/grpcserver/validation_service_test.go b/cli/azd/internal/grpcserver/validation_service_test.go index a08b1c48c5f..6b436df4f16 100644 --- a/cli/azd/internal/grpcserver/validation_service_test.go +++ b/cli/azd/internal/grpcserver/validation_service_test.go @@ -89,7 +89,7 @@ func TestValidationService_DispatchChecks_NoChecks(t *testing.T) { svc := &ValidationService{} results, ruleIDs, err := svc.DispatchChecks( - t.Context(), "local-preflight", nil, + t.Context(), "provision", nil, ) require.NoError(t, err) require.Nil(t, results) @@ -107,10 +107,10 @@ func TestValidationService_OnRegisterRequest_Validations(t *testing.T) { wantErr bool }{ {"empty_check_type", "", "rule1", true}, - {"empty_rule_id", "local-preflight", "", true}, + {"empty_rule_id", "provision", "", true}, {"whitespace_check_type", " ", "rule1", true}, - {"whitespace_rule_id", "local-preflight", " ", true}, - {"valid", "local-preflight", "rule1", false}, + {"whitespace_rule_id", "provision", " ", true}, + {"valid", "provision", "rule1", false}, } for _, tt := range tests { @@ -150,7 +150,7 @@ func TestValidationService_OnRegisterRequest_DuplicateRejection(t *testing.T) { resp, err := svc.onRegisterRequest( t.Context(), &azdext.RegisterValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "unique_rule", }, ext, @@ -165,7 +165,7 @@ func TestValidationService_OnRegisterRequest_DuplicateRejection(t *testing.T) { _, err = svc.onRegisterRequest( t.Context(), &azdext.RegisterValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "unique_rule", }, ext, @@ -181,13 +181,13 @@ func TestValidationService_DispatchChecks_NoMatchingType(t *testing.T) { svc := &ValidationService{} ext := newTestValidationExtension() - // Register a check for "local-preflight" + // Register a check for "provision" var registered []validationCheckEntry mu := &sync.Mutex{} _, _ = svc.onRegisterRequest( t.Context(), &azdext.RegisterValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "test_rule", }, ext, @@ -222,7 +222,7 @@ func TestSendContextChunks_EmptyContext(t *testing.T) { ) err := sendContextChunks( - t.Context(), broker, "ctx-1", "local-preflight", + t.Context(), broker, "ctx-1", "provision", map[string][]byte{}, ) require.NoError(t, err) @@ -273,7 +273,7 @@ func TestSendContextChunks_SmallData(t *testing.T) { } err := sendContextChunks( - ctx, broker, "ctx-123", "local-preflight", contextData, + ctx, broker, "ctx-123", "provision", contextData, ) require.NoError(t, err) cancel() @@ -344,13 +344,13 @@ func TestValidationService_DispatchChecks_WithBroker(t *testing.T) { svc := &ValidationService{} svc.checks = []validationCheckEntry{ { - CheckType: "local-preflight", + CheckType: "provision", RuleID: "ext_rule_1", Extension: ext, Broker: broker, }, { - CheckType: "local-preflight", + CheckType: "provision", RuleID: "ext_rule_2", Extension: ext, Broker: broker, @@ -362,7 +362,7 @@ func TestValidationService_DispatchChecks_WithBroker(t *testing.T) { } results, ruleIDs, err := svc.DispatchChecks( - ctx, "local-preflight", contextData, + ctx, "provision", contextData, ) require.NoError(t, err) require.Len(t, results, 2, "should have 2 results (one per check)") diff --git a/cli/azd/internal/tracing/events/events.go b/cli/azd/internal/tracing/events/events.go index 7eda92f0044..5e89fb7cbf7 100644 --- a/cli/azd/internal/tracing/events/events.go +++ b/cli/azd/internal/tracing/events/events.go @@ -43,11 +43,11 @@ const ( CopilotSessionEvent = "copilot.session" ) -// Preflight validation events. +// Provision validation events. const ( - // PreflightValidationEvent tracks the local preflight validation operation - // and its outcome (passed, warnings accepted, aborted). - PreflightValidationEvent = "validation.preflight" + // ProvisionValidationEvent tracks the local provision validation operation + // and its outcome (passed, warnings accepted, canceled). + ProvisionValidationEvent = "validation.provision" ) // Hook execution events. diff --git a/cli/azd/internal/tracing/fields/fields.go b/cli/azd/internal/tracing/fields/fields.go index 429f158b3a7..f836f13aad6 100644 --- a/cli/azd/internal/tracing/fields/fields.go +++ b/cli/azd/internal/tracing/fields/fields.go @@ -645,59 +645,59 @@ var ( } ) -// Preflight validation related fields +// Provision validation related fields var ( - // PreflightOutcomeKey records the outcome of preflight validation. + // ProvisionValidationOutcomeKey records the outcome of provision validation. // - // Example: "passed", "warnings_accepted", "aborted_by_errors", - // "aborted_by_user", "skipped", "error" - PreflightOutcomeKey = AttributeKey{ - Key: attribute.Key("validation.preflight.outcome"), + // Example: "passed", "warnings_accepted", "canceled_by_errors", + // "canceled_by_user", "skipped", "error" + ProvisionValidationOutcomeKey = AttributeKey{ + Key: attribute.Key("validation.provision.outcome"), Classification: SystemMetadata, Purpose: FeatureInsight, } - // PreflightDiagnosticsKey records the list of diagnostic IDs emitted by preflight checks. + // ProvisionValidationDiagnosticsKey records the list of diagnostic IDs emitted by validation checks. // // Example: ["role_assignment_missing", "role_assignment_conditional"] - PreflightDiagnosticsKey = AttributeKey{ - Key: attribute.Key("validation.preflight.diagnostics"), + ProvisionValidationDiagnosticsKey = AttributeKey{ + Key: attribute.Key("validation.provision.diagnostics"), Classification: SystemMetadata, Purpose: FeatureInsight, } - // PreflightRulesKey records the list of rule IDs that were executed. + // ProvisionValidationRulesKey records the list of rule IDs that were executed. // // Example: ["role_assignment_permissions"] - PreflightRulesKey = AttributeKey{ - Key: attribute.Key("validation.preflight.rules"), + ProvisionValidationRulesKey = AttributeKey{ + Key: attribute.Key("validation.provision.rules"), Classification: SystemMetadata, Purpose: FeatureInsight, } - // PreflightWarningCountKey records the number of warnings produced by preflight validation. - PreflightWarningCountKey = AttributeKey{ - Key: attribute.Key("validation.preflight.warning.count"), + // ProvisionValidationWarningCountKey records the number of warnings produced by provision validation. + ProvisionValidationWarningCountKey = AttributeKey{ + Key: attribute.Key("validation.provision.warning.count"), Classification: SystemMetadata, Purpose: FeatureInsight, IsMeasurement: true, } - // PreflightErrorCountKey records the number of errors produced by preflight validation. - PreflightErrorCountKey = AttributeKey{ - Key: attribute.Key("validation.preflight.error.count"), + // ProvisionValidationErrorCountKey records the number of errors produced by provision validation. + ProvisionValidationErrorCountKey = AttributeKey{ + Key: attribute.Key("validation.provision.error.count"), Classification: SystemMetadata, Purpose: FeatureInsight, IsMeasurement: true, } - // PreflightExtensionRulesKey records the list of rule IDs from extension-provided - // validation checks that were executed. Separate from PreflightRulesKey (core rules) + // ProvisionValidationExtensionRulesKey records the list of rule IDs from extension-provided + // validation checks that were executed. Separate from ProvisionValidationRulesKey (core rules) // to distinguish the source of checks in telemetry. // // Example: ["todo_resource_name", "naming_convention"] - PreflightExtensionRulesKey = AttributeKey{ - Key: attribute.Key("validation.preflight.extension_rules"), + ProvisionValidationExtensionRulesKey = AttributeKey{ + Key: attribute.Key("validation.provision.extension_rules"), Classification: SystemMetadata, Purpose: FeatureInsight, } diff --git a/cli/azd/magefile.go b/cli/azd/magefile.go index 16bb9858d17..cb27189ad31 100644 --- a/cli/azd/magefile.go +++ b/cli/azd/magefile.go @@ -694,15 +694,15 @@ var excludedPlaybackTests = map[string]string{ // Recordings affected by feat/exegraph: the graph-driven up/provision path // introduces legitimate new HTTP interactions (layer hash probes, resource-group // existence checks). Must be re-recorded with live Azure credentials before merge. - "Test_DeploymentStacks": "needs re-record for feat/exegraph graph-driven provision", - "Test_CLI_ProvisionState": "needs re-record for feat/exegraph graph-driven provision", - "Test_CLI_InfraCreateAndDeleteUpperCase": "needs re-record for feat/exegraph graph-driven provision", - "Test_CLI_PreflightQuota_Sub_DefaultCapacity": "stale recording; missing extension registry + resource group interactions", - "Test_CLI_PreflightQuota_Sub_InvalidModelName": "stale recording; missing extension registry + resource group interactions", - "Test_CLI_PreflightQuota_Sub_DifferentLocation": "stale recording; missing extension registry + resource group interactions", - "Test_CLI_PreflightQuota_RG_DefaultCapacity": "stale recording; missing extension registry + resource group interactions", - "Test_CLI_PreflightQuota_RG_InvalidVersion": "stale recording; missing extension registry + resource group interactions", - "Test_CLI_PreflightQuota_RG_InvalidModelName": "stale recording; missing extension registry + resource group interactions", + "Test_DeploymentStacks": "needs re-record for feat/exegraph graph-driven provision", + "Test_CLI_ProvisionState": "needs re-record for feat/exegraph graph-driven provision", + "Test_CLI_InfraCreateAndDeleteUpperCase": "needs re-record for feat/exegraph graph-driven provision", + "Test_CLI_ProvisionValidationQuota_Sub_DefaultCapacity": "stale recording; missing extension registry + resource group interactions", + "Test_CLI_ProvisionValidationQuota_Sub_InvalidModelName": "stale recording; missing extension registry + resource group interactions", + "Test_CLI_ProvisionValidationQuota_Sub_DifferentLocation": "stale recording; missing extension registry + resource group interactions", + "Test_CLI_ProvisionValidationQuota_RG_DefaultCapacity": "stale recording; missing extension registry + resource group interactions", + "Test_CLI_ProvisionValidationQuota_RG_InvalidVersion": "stale recording; missing extension registry + resource group interactions", + "Test_CLI_ProvisionValidationQuota_RG_InvalidModelName": "stale recording; missing extension registry + resource group interactions", } // discoverPlaybackTests scans the recordings directory for .yaml files and diff --git a/cli/azd/pkg/azdext/extension_host.go b/cli/azd/pkg/azdext/extension_host.go index 760469bdb63..13dbb21e1e0 100644 --- a/cli/azd/pkg/azdext/extension_host.go +++ b/cli/azd/pkg/azdext/extension_host.go @@ -190,7 +190,7 @@ func (er *ExtensionHost) WithProvisioningProvider( } // WithValidationCheck registers a validation check to be wired when Run is invoked. -// The checkType identifies the validation context (e.g. "local-preflight"), +// The checkType identifies the validation context (e.g. "provision"), // and the ruleID is a stable identifier for this check. func (er *ExtensionHost) WithValidationCheck( reg ValidationCheckRegistration, diff --git a/cli/azd/pkg/azdext/validation.pb.go b/cli/azd/pkg/azdext/validation.pb.go index 5e3a44fd9e8..937ab4e1111 100644 --- a/cli/azd/pkg/azdext/validation.pb.go +++ b/cli/azd/pkg/azdext/validation.pb.go @@ -237,7 +237,7 @@ func (*ValidationMessage_PrepareValidationContextResponse) isValidationMessage_M type RegisterValidationCheckRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // check_type identifies the validation context this check targets - // (e.g. "local-preflight", "project-config"). Future check types can be + // (e.g. "provision", "project-config"). Future check types can be // added without changing the protocol. CheckType string `protobuf:"bytes,1,opt,name=check_type,json=checkType,proto3" json:"check_type,omitempty"` // rule_id is a stable, unique identifier for this check rule, diff --git a/cli/azd/pkg/azdext/validation_manager_integration_test.go b/cli/azd/pkg/azdext/validation_manager_integration_test.go index fcd8a0a601a..e6723309225 100644 --- a/cli/azd/pkg/azdext/validation_manager_integration_test.go +++ b/cli/azd/pkg/azdext/validation_manager_integration_test.go @@ -174,11 +174,11 @@ func TestValidationManager_Register_Integration(t *testing.T) { // Register a check (exercises Register end-to-end over the stream). factory := func() ValidationCheckProvider { return &mockProvider{} } - err := mgr.Register(ctx, factory, "local-preflight", "rule_1") + err := mgr.Register(ctx, factory, "provision", "rule_1") require.NoError(t, err) // Registering the same rule again should fail locally (duplicate). - err = mgr.Register(ctx, factory, "local-preflight", "rule_1") + err = mgr.Register(ctx, factory, "provision", "rule_1") require.Error(t, err) require.Contains(t, err.Error(), "already registered") } @@ -200,13 +200,13 @@ func TestValidationManager_Register_ServerError(t *testing.T) { require.NoError(t, mgr.Ready(ctx)) factory := func() ValidationCheckProvider { return &mockProvider{} } - err := mgr.Register(ctx, factory, "local-preflight", "rule_err") + err := mgr.Register(ctx, factory, "provision", "rule_err") require.Error(t, err) require.Contains(t, err.Error(), "registration rejected by core") // The factory should have been rolled back so a retry path is clean. mgr.mu.RLock() - _, exists := mgr.factories[validationCheckKey{CheckType: "local-preflight", RuleID: "rule_err"}] + _, exists := mgr.factories[validationCheckKey{CheckType: "provision", RuleID: "rule_err"}] mgr.mu.RUnlock() require.False(t, exists, "factory should be removed after failed registration") } @@ -223,7 +223,7 @@ func TestValidationManager_Register_EmptyArgs(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "check type cannot be empty") - err = mgr.Register(ctx, factory, "local-preflight", "") + err = mgr.Register(ctx, factory, "provision", "") require.Error(t, err) require.Contains(t, err.Error(), "rule ID cannot be empty") } @@ -250,7 +250,7 @@ func TestValidationManager_Dispatch_Integration(t *testing.T) { factory := func() ValidationCheckProvider { return &recordingProvider{invoked: invoked} } - require.NoError(t, mgr.Register(ctx, factory, "local-preflight", "rule_dispatch")) + require.NoError(t, mgr.Register(ctx, factory, "provision", "rule_dispatch")) // The server pushes a check request; the manager should invoke the provider. select { diff --git a/cli/azd/pkg/azdext/validation_provider.go b/cli/azd/pkg/azdext/validation_provider.go index 52cbf4e0c0a..cc8ed2eaacb 100644 --- a/cli/azd/pkg/azdext/validation_provider.go +++ b/cli/azd/pkg/azdext/validation_provider.go @@ -40,7 +40,7 @@ type PredictedResource struct { type ValidationContext struct { // ContextID is the unique identifier for this context delivery. ContextID string - // CheckType identifies the validation context (e.g. "local-preflight"). + // CheckType identifies the validation context (e.g. "provision"). CheckType string // Data is the reassembled context map (key → full value). Data map[string][]byte @@ -95,7 +95,7 @@ func (c *ValidationContext) EnvLocation() (string, bool) { // ValidationCheckProvider is the extension-side interface for a validation check. // Extensions implement this to provide custom checks that run during the azd -// validation pipeline (e.g. local-preflight during provisioning). +// validation pipeline (e.g. provision checks during provisioning). type ValidationCheckProvider interface { // Validate runs the check against the provided context and returns results. Validate( @@ -110,7 +110,7 @@ type ValidationCheckProviderFactory func() ValidationCheckProvider // ValidationCheckRegistration describes a validation check to register with azd core. type ValidationCheckRegistration struct { - // CheckType identifies the validation context (e.g. "local-preflight"). + // CheckType identifies the validation context (e.g. "provision"). CheckType string // RuleID is a stable, unique identifier for this check rule. RuleID string @@ -118,23 +118,23 @@ type ValidationCheckRegistration struct { Factory ValidationCheckProviderFactory } -// --- Context key constants for "local-preflight" checks --- +// --- Context key constants for "provision" checks --- const ( // ValidationContextResourcesSnapshot is the key for the raw Bicep - // snapshot JSON in a "local-preflight" check context. + // snapshot JSON in a "provision" check context. ValidationContextResourcesSnapshot = "resources_snapshot" // ValidationContextPredictedResources is the key for the JSON array of // predicted resources extracted from the Bicep snapshot. Each element is // a resource object with type, name, location, properties, etc. ValidationContextPredictedResources = "predicted_resources" // ValidationContextARMTemplate is the key for the compiled ARM - // template JSON in a "local-preflight" check context. + // template JSON in a "provision" check context. ValidationContextARMTemplate = "arm_template" // ValidationContextARMParameters is the key for the resolved ARM - // parameters JSON in a "local-preflight" check context. + // parameters JSON in a "provision" check context. ValidationContextARMParameters = "arm_parameters" // ValidationContextEnvLocation is the key for the Azure deployment - // location string in a "local-preflight" check context. + // location string in a "provision" check context. ValidationContextEnvLocation = "env_location" ) diff --git a/cli/azd/pkg/azdext/validation_test.go b/cli/azd/pkg/azdext/validation_test.go index 6413a32e305..7e715866383 100644 --- a/cli/azd/pkg/azdext/validation_test.go +++ b/cli/azd/pkg/azdext/validation_test.go @@ -51,7 +51,7 @@ func TestValidationEnvelope_GetInnerMessage(t *testing.T) { msg: &ValidationMessage{ MessageType: &ValidationMessage_RegisterValidationCheckRequest{ RegisterValidationCheckRequest: &RegisterValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "test_rule", }, }, @@ -70,7 +70,7 @@ func TestValidationEnvelope_GetInnerMessage(t *testing.T) { msg: &ValidationMessage{ MessageType: &ValidationMessage_ValidationCheckRequest{ ValidationCheckRequest: &ValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "test_rule", ContextId: "ctx-123", }, @@ -99,7 +99,7 @@ func TestValidationEnvelope_GetInnerMessage(t *testing.T) { MessageType: &ValidationMessage_PrepareValidationContextChunk{ PrepareValidationContextChunk: &PrepareValidationContextChunk{ ContextId: "ctx-1", - CheckType: "local-preflight", + CheckType: "provision", Key: "arm_template", Data: []byte("data"), }, @@ -144,7 +144,7 @@ func TestValidationEnvelope_ProgressNotSupported(t *testing.T) { func TestValidationContext_Helpers(t *testing.T) { valCtx := &ValidationContext{ ContextID: "ctx-1", - CheckType: "local-preflight", + CheckType: "provision", Data: map[string][]byte{ ValidationContextResourcesSnapshot: []byte(`{"predictedResources":[]}`), ValidationContextARMTemplate: []byte(`{"resources":[]}`), @@ -325,7 +325,7 @@ func TestValidationManager_GetOrCreateProvider(t *testing.T) { instances: make(map[validationCheckKey]ValidationCheckProvider), } - key := validationCheckKey{CheckType: "local-preflight", RuleID: "test_rule"} + key := validationCheckKey{CheckType: "provision", RuleID: "test_rule"} // No factory registered — should error _, err := mgr.getOrCreateProvider(key) @@ -362,7 +362,7 @@ func TestValidationManager_OnPrepareContextChunk(t *testing.T) { // Send incomplete chunk resp, err := mgr.onPrepareContextChunk(t.Context(), &PrepareValidationContextChunk{ ContextId: "ctx-1", - CheckType: "local-preflight", + CheckType: "provision", Key: "arm_template", Data: []byte("hello"), ChunkIndex: 0, @@ -380,7 +380,7 @@ func TestValidationManager_OnPrepareContextChunk(t *testing.T) { // Send final chunk resp, err = mgr.onPrepareContextChunk(t.Context(), &PrepareValidationContextChunk{ ContextId: "ctx-1", - CheckType: "local-preflight", + CheckType: "provision", Key: "env_location", Data: []byte("eastus"), ChunkIndex: 0, @@ -408,7 +408,7 @@ func TestValidationManager_OnValidationCheck(t *testing.T) { assemblers: make(map[string]*contextAssembler), } - key := validationCheckKey{CheckType: "local-preflight", RuleID: "test_rule"} + key := validationCheckKey{CheckType: "provision", RuleID: "test_rule"} mgr.factories[key] = func() ValidationCheckProvider { return &mockProvider{ results: []*ValidationCheckResult{ @@ -424,13 +424,13 @@ func TestValidationManager_OnValidationCheck(t *testing.T) { // Cache a context mgr.cachedContexts["ctx-abc"] = &ValidationContext{ ContextID: "ctx-abc", - CheckType: "local-preflight", + CheckType: "provision", Data: map[string][]byte{"env_location": []byte("westus2")}, } mgr.contextRefCounts["ctx-abc"] = 0 resp, err := mgr.onValidationCheck(t.Context(), &ValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "test_rule", ContextId: "ctx-abc", }) @@ -456,13 +456,13 @@ func TestValidationManager_OnValidationCheck_NilResponse(t *testing.T) { assemblers: make(map[string]*contextAssembler), } - key := validationCheckKey{CheckType: "local-preflight", RuleID: "nil_rule"} + key := validationCheckKey{CheckType: "provision", RuleID: "nil_rule"} mgr.factories[key] = func() ValidationCheckProvider { return &mockProvider{results: nil} } resp, err := mgr.onValidationCheck(t.Context(), &ValidationCheckRequest{ - CheckType: "local-preflight", + CheckType: "provision", RuleId: "nil_rule", ContextId: "no-such-ctx", }) diff --git a/cli/azd/pkg/extensions/registry.go b/cli/azd/pkg/extensions/registry.go index 6748994b77f..6ce47938ca7 100644 --- a/cli/azd/pkg/extensions/registry.go +++ b/cli/azd/pkg/extensions/registry.go @@ -55,7 +55,7 @@ const ( // Provision provider enables extensions to provide a custom provisioning experience ProvisioningProviderCapability CapabilityType = "provisioning-provider" // Validation provider enables extensions to contribute validation checks - // to azd's validation pipeline (e.g. local-preflight checks during provisioning) + // to azd's validation pipeline (e.g. provision checks during provisioning) ValidationProviderCapability CapabilityType = "validation-provider" ) diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index 199717e87f1..84527c82fd4 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -70,11 +70,11 @@ const ( apiVersionResourceGroupExistence = "2025-03-01" ) -// extensionValidationTimeout bounds how long core preflight waits for +// extensionValidationTimeout bounds how long core validation waits for // extension-provided validation checks to complete. If an extension's check // blocks or never responds, dispatch is abandoned once this deadline elapses -// and preflight continues with the core results. This preserves the guarantee -// that extensions cannot hang core preflight. +// and validation continues with the core results. This preserves the guarantee +// that extensions cannot hang core validation. const extensionValidationTimeout = 60 * time.Second // Azure reserved resource name words. @@ -903,20 +903,26 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult, return nil, err } - // Check if preflight validation is disabled via config - skipPreflight := false + // Provision validation runs two independent, config-gated steps: + // - validation.provision=off → skip azd's local (client-side) provision validation. + // - provision.preflight=off → skip the server-side ARM provision validation call. + skipValidation := false + skipArmPreflight := false var userConfigManager config.UserConfigManager if err := p.serviceLocator.Resolve(&userConfigManager); err == nil { if userConfig, err := userConfigManager.Load(); err == nil { + if val, exists := userConfig.GetString("validation.provision"); exists && val == "off" { + skipValidation = true + } if val, exists := userConfig.GetString("provision.preflight"); exists && val == "off" { - skipPreflight = true + skipArmPreflight = true } } } - if !skipPreflight { + if !skipValidation || !skipArmPreflight { p.console.ShowSpinner(ctx, "Validating deployment", input.Step) - abort, preflightErr := p.validatePreflight( + canceled, validationErr := p.validateProvision( ctx, deployment, p.path, @@ -924,16 +930,18 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult, planned.Parameters, deploymentTags, optionsMap, + skipValidation, + skipArmPreflight, ) - if preflightErr != nil { + if validationErr != nil { p.console.StopSpinner(ctx, "Validating deployment", input.StepFailed) - return nil, preflightErr + return nil, validationErr } - if abort { - // Preflight detected issues and the deployment was intentionally aborted. + if canceled { + // Validation detected issues and provisioning was intentionally canceled. // This is a successful operation (exit code 0), not an internal failure. p.console.StopSpinner(ctx, "Validating deployment", input.StepSkipped) - return &provisioning.DeployResult{SkippedReason: provisioning.PreflightAbortedSkipped}, nil + return &provisioning.DeployResult{SkippedReason: provisioning.ProvisionValidationCanceledSkipped}, nil } p.console.StopSpinner(ctx, "", input.StepDone) } @@ -1008,7 +1016,7 @@ func (p *BicepProvider) Deploy(ctx context.Context) (*provisioning.DeployResult, // that we are actually about to start the ARM deployment. Doing this here (rather // than immediately after generating the deployment object) avoids advertising a // deployment ID that never exists in Azure when the run short-circuits via the - // deployment-state cache or is aborted by preflight validation. + // deployment-state cache or is canceled by provision validation. writeDeploymentIdFile(deployment, p.layer) deployCtx, interruptStarted, interruptCh, markDeployCompleted, interruptCleanup := @@ -2552,22 +2560,32 @@ func (p *BicepProvider) convertToDeployment(bicepTemplate azure.ArmTemplate) pro return result } -// Preflight validation outcome constants for telemetry. +// Provision validation outcome constants for telemetry. const ( - preflightOutcomePassed = "passed" - preflightOutcomeWarningsAccepted = "warnings_accepted" - preflightOutcomeAbortedByErrors = "aborted_by_errors" - preflightOutcomeAbortedByUser = "aborted_by_user" - preflightOutcomeSkipped = "skipped" - preflightOutcomeError = "error" + provisionValidationOutcomePassed = "passed" + provisionValidationOutcomeWarningsAccepted = "warnings_accepted" + provisionValidationOutcomeCanceledByErrors = "canceled_by_errors" + provisionValidationOutcomeCanceledByUser = "canceled_by_user" + provisionValidationOutcomeSkipped = "skipped" + provisionValidationOutcomeError = "error" ) -// validatePreflight runs client-side preflight validation on the ARM template. -// It returns (abort, err) where: -// - abort=true, err=nil: checks detected issues and the deployment should be skipped (exit code 0). -// - abort=false, err!=nil: the validation itself failed to run (exit code 1). -// - abort=false, err=nil: validation passed, proceed with deployment. -func (p *BicepProvider) validatePreflight( +// validateProvision runs azd's pre-deployment provision validation on the ARM template. +// +// It performs up to two independent steps, each individually skippable via config: +// - local (client-side) provision validation (skipped when skipLocalValidation is true) +// - server-side ARM provision validation (skipped when skipArmPreflight is true) +// +// Only the local step is traced under the `validation.provision` telemetry event; the +// server-side ARM call runs outside that span so its failures/latency are not attributed +// to azd's local validation event. +// +// It returns (canceled, err) where: +// - canceled=true, err=nil: validation detected issues and provisioning should be skipped +// (exit code 0). +// - canceled=false, err!=nil: the validation itself failed to run (exit code 1). +// - canceled=false, err=nil: validation passed, proceed with provisioning. +func (p *BicepProvider) validateProvision( ctx context.Context, target infra.Deployment, modulePath string, @@ -2575,13 +2593,67 @@ func (p *BicepProvider) validatePreflight( armParameters azure.ArmParameters, tags map[string]*string, options map[string]any, -) (abort bool, err error) { - ctx, span := tracing.Start(ctx, events.PreflightValidationEvent) + skipLocalValidation bool, + skipArmPreflight bool, +) (canceled bool, err error) { + // Local (client-side) provision validation, traced under the validation.provision event. + canceled, err = p.traceLocalProvisionValidation( + ctx, target, modulePath, armTemplate, armParameters, skipLocalValidation) + if err != nil || canceled { + return canceled, err + } + + // Server-side ARM provision validation. Skipped independently via provision.preflight=off. + // This runs outside the validation.provision span on purpose so ARM preflight errors are + // not attributed to azd's local validation telemetry. + if skipArmPreflight { + return false, nil + } + return false, target.ValidatePreflight(ctx, armTemplate, armParameters, tags, options) +} + +// traceLocalProvisionValidation runs (or skips) azd's local provision validation within the +// `validation.provision` telemetry span and records the outcome attributes. It returns the +// same (canceled, err) semantics as validateProvision for the local step. +func (p *BicepProvider) traceLocalProvisionValidation( + ctx context.Context, + target infra.Deployment, + modulePath string, + armTemplate azure.RawArmTemplate, + armParameters azure.ArmParameters, + skipLocalValidation bool, +) (canceled bool, err error) { + ctx, span := tracing.Start(ctx, events.ProvisionValidationEvent) defer func() { span.EndWithStatus(err) }() - // Run local preflight validation before sending to Azure. + if skipLocalValidation { + p.setProvisionValidationOutcome(span, provisionValidationOutcomeSkipped, []string{}) + // Emit the same empty/zero attribute shape as the other skipped paths so downstream + // telemetry queries see a consistent set of validation.provision.* fields. + span.SetAttributes(fields.ProvisionValidationRulesKey.StringSlice([]string{})) + span.SetAttributes(fields.ProvisionValidationDiagnosticsKey.StringSlice([]string{})) + span.SetAttributes(fields.ProvisionValidationWarningCountKey.Int(0)) + span.SetAttributes(fields.ProvisionValidationErrorCountKey.Int(0)) + return false, nil + } + + return p.runLocalProvisionValidation(ctx, span, target, modulePath, armTemplate, armParameters) +} + +// runLocalProvisionValidation executes azd's local (client-side) provision validation +// pipeline and reports findings to the user. It returns (canceled, err) using the same +// semantics as validateProvision (canceled=true means provisioning should be skipped with +// exit code 0). +func (p *BicepProvider) runLocalProvisionValidation( + ctx context.Context, + span tracing.Span, + target infra.Deployment, + modulePath string, + armTemplate azure.RawArmTemplate, + armParameters azure.ArmParameters, +) (canceled bool, err error) { // Local validation catches common issues without requiring a network round-trip. // Resolve the environment location for RG-scoped deployments: prefer the actual // resource group location (if the RG already exists), then fall back to AZURE_LOCATION. @@ -2590,35 +2662,35 @@ func (p *BicepProvider) validatePreflight( if envLocation == "" { envLocation = strings.ToLower(p.env.GetLocation()) } - localPreflight := newLocalArmPreflight( + validator := newProvisionValidator( modulePath, p.bicepCli, target, envLocation) // Register the role assignment permission check so it runs as part of the - // local preflight pipeline. The check inspects whether the template contains + // validation pipeline. The check inspects whether the template contains // Microsoft.Authorization/roleAssignments and, if so, verifies the current // principal has the required write permission. - localPreflight.AddCheck(PreflightCheck{ + validator.AddCheck(ProvisionValidationCheck{ RuleID: "role_assignment_permissions", Fn: p.checkRoleAssignmentPermissions, }) - localPreflight.AddCheck(PreflightCheck{ + validator.AddCheck(ProvisionValidationCheck{ RuleID: "ai_model_quota", Fn: p.checkAiModelQuota, }) - localPreflight.AddCheck(PreflightCheck{ + validator.AddCheck(ProvisionValidationCheck{ RuleID: "reserved_resource_names", Fn: p.checkReservedResourceNames, }) - valCtx, results, err := localPreflight.validate(ctx, p.console, armTemplate, armParameters) + valCtx, results, err := validator.validate(ctx, p.console, armTemplate, armParameters) if err != nil { - p.setPreflightOutcome(span, preflightOutcomeError, nil) - return false, fmt.Errorf("local preflight validation failed: %w", err) + p.setProvisionValidationOutcome(span, provisionValidationOutcomeError, nil) + return false, fmt.Errorf("local provision validation failed: %w", err) } - // Dispatch to extension-provided validation checks for "local-preflight". + // Dispatch to extension-provided validation checks for the "provision" check type. // Extensions register checks via the validation-provider capability. // The dispatcher is optional — if no extensions are loaded, skip silently. var extRuleIDs []string @@ -2630,11 +2702,11 @@ func (p *BicepProvider) validatePreflight( checkContext := valCtx.extensionContext(armTemplate, armParameters) // Bound extension dispatch so a blocked or unresponsive extension check - // cannot hang core preflight. A timeout surfaces as extErr below and is - // treated as a non-fatal skip (logged; preflight continues). + // cannot hang core validation. A timeout surfaces as extErr below and is + // treated as a non-fatal skip (logged; validation continues). dispatchCtx, cancelDispatch := context.WithTimeout(ctx, extensionValidationTimeout) extResults, invokedRuleIDs, extErr := dispatcher.DispatchChecks( - dispatchCtx, "local-preflight", checkContext, + dispatchCtx, "provision", checkContext, ) cancelDispatch() if extErr != nil { @@ -2649,18 +2721,18 @@ func (p *BicepProvider) validatePreflight( } extRuleIDs = append(extRuleIDs, invokedRuleIDs...) for _, extResult := range extResults { - severity := PreflightCheckWarning + severity := ProvisionValidationCheckWarning if extResult.Severity == azdext.ValidationCheckSeverity_VALIDATION_CHECK_SEVERITY_ERROR { - severity = PreflightCheckError + severity = ProvisionValidationCheckError } - links := make([]ux.PreflightReportLink, len(extResult.Links)) + links := make([]ux.ProvisionValidationReportLink, len(extResult.Links)) for i, l := range extResult.Links { - links[i] = ux.PreflightReportLink{ + links[i] = ux.ProvisionValidationReportLink{ Title: l.Text, URL: l.Url, } } - results = append(results, PreflightCheckResult{ + results = append(results, ProvisionValidationCheckResult{ Severity: severity, DiagnosticID: extResult.DiagnosticId, Message: extResult.Message, @@ -2674,20 +2746,20 @@ func (p *BicepProvider) validatePreflight( // because validate() may skip checks entirely (e.g. when the bicep snapshot // is unavailable). A nil result with nil error means checks were skipped. if results == nil { - p.setPreflightOutcome(span, preflightOutcomeSkipped, nil) + p.setProvisionValidationOutcome(span, provisionValidationOutcomeSkipped, nil) // No rules actually executed; record an empty slice for telemetry. - span.SetAttributes(fields.PreflightRulesKey.StringSlice([]string{})) + span.SetAttributes(fields.ProvisionValidationRulesKey.StringSlice([]string{})) } else { - ruleIDs := make([]string, len(localPreflight.checks)) - for i, check := range localPreflight.checks { + ruleIDs := make([]string, len(validator.checks)) + for i, check := range validator.checks { ruleIDs[i] = check.RuleID } - span.SetAttributes(fields.PreflightRulesKey.StringSlice(ruleIDs)) + span.SetAttributes(fields.ProvisionValidationRulesKey.StringSlice(ruleIDs)) } // Record extension-provided rule IDs separately for telemetry attribution. if len(extRuleIDs) > 0 { - span.SetAttributes(fields.PreflightExtensionRulesKey.StringSlice(extRuleIDs)) + span.SetAttributes(fields.ProvisionValidationExtensionRulesKey.StringSlice(extRuleIDs)) } // Compute telemetry metrics from the results. @@ -2697,22 +2769,22 @@ func (p *BicepProvider) validatePreflight( if result.DiagnosticID != "" { diagnosticIDs = append(diagnosticIDs, result.DiagnosticID) } - if result.Severity == PreflightCheckError { + if result.Severity == ProvisionValidationCheckError { errorCount++ } else { warningCount++ } } - span.SetAttributes(fields.PreflightDiagnosticsKey.StringSlice(diagnosticIDs)) - span.SetAttributes(fields.PreflightWarningCountKey.Int(warningCount)) - span.SetAttributes(fields.PreflightErrorCountKey.Int(errorCount)) + span.SetAttributes(fields.ProvisionValidationDiagnosticsKey.StringSlice(diagnosticIDs)) + span.SetAttributes(fields.ProvisionValidationWarningCountKey.Int(warningCount)) + span.SetAttributes(fields.ProvisionValidationErrorCountKey.Int(errorCount)) - // Build a UX report from the preflight results and display it. + // Build a UX report from the validation results and display it. if len(results) > 0 { - report := &ux.PreflightReport{} + report := &ux.ProvisionValidationReport{} for _, result := range results { - report.Items = append(report.Items, ux.PreflightReportItem{ - IsError: result.Severity == PreflightCheckError, + report.Items = append(report.Items, ux.ProvisionValidationReportItem{ + IsError: result.Severity == ProvisionValidationCheckError, DiagnosticID: result.DiagnosticID, Message: result.Message, Suggestion: result.Suggestion, @@ -2723,61 +2795,61 @@ func (p *BicepProvider) validatePreflight( if report.HasErrors() { // Errors were already displayed by the UX report above. The validation - // successfully detected problems and the deployment is intentionally aborted. + // successfully detected problems and provisioning is intentionally canceled. // This is not an internal failure, so no error is returned (exit code 0). - p.console.Message(ctx, "Preflight validation detected errors, deployment aborted.") - p.setPreflightOutcome(span, preflightOutcomeAbortedByErrors, diagnosticIDs) + p.console.Message(ctx, "Validation detected errors, provisioning canceled.") + p.setProvisionValidationOutcome(span, provisionValidationOutcomeCanceledByErrors, diagnosticIDs) return true, nil } if report.HasWarnings() { p.console.Message(ctx, "") continueDeployment, promptErr := p.console.Confirm(ctx, input.ConsoleOptions{ - Message: "Proceed with deployment despite the warnings above?", + Message: "Proceed with provisioning despite the warnings above?", DefaultValue: true, }) if promptErr != nil { - p.setPreflightOutcome( - span, preflightOutcomeError, diagnosticIDs, + p.setProvisionValidationOutcome( + span, provisionValidationOutcomeError, diagnosticIDs, ) return false, fmt.Errorf( - "prompting for preflight confirmation: %w", promptErr, + "prompting for validation confirmation: %w", promptErr, ) } if !continueDeployment { - // User chose not to continue — this is an intentional abort, not a failure. - p.setPreflightOutcome(span, preflightOutcomeAbortedByUser, diagnosticIDs) + // User chose not to continue — this is an intentional cancel, not a failure. + p.setProvisionValidationOutcome(span, provisionValidationOutcomeCanceledByUser, diagnosticIDs) return true, nil } - p.setPreflightOutcome(span, preflightOutcomeWarningsAccepted, diagnosticIDs) + p.setProvisionValidationOutcome(span, provisionValidationOutcomeWarningsAccepted, diagnosticIDs) } } else if results != nil { - p.setPreflightOutcome(span, preflightOutcomePassed, nil) + p.setProvisionValidationOutcome(span, provisionValidationOutcomePassed, nil) } - return false, target.ValidatePreflight(ctx, armTemplate, armParameters, tags, options) + return false, nil } -// setPreflightOutcome records the preflight outcome on both the span and as a usage-level +// setProvisionValidationOutcome records the validation outcome on both the span and as a usage-level // attribute so it can be correlated with the overall deployment result. -func (p *BicepProvider) setPreflightOutcome( +func (p *BicepProvider) setProvisionValidationOutcome( span tracing.Span, outcome string, diagnosticIDs []string, ) { - span.SetAttributes(fields.PreflightOutcomeKey.String(outcome)) + span.SetAttributes(fields.ProvisionValidationOutcomeKey.String(outcome)) // Set usage-level attributes so the parent command span (cmd.provision / cmd.up) can - // correlate preflight outcome with the final deployment result. This enables tracking + // correlate validation outcome with the final deployment result. This enables tracking // false positives (warnings_accepted + deploy succeeds) and true positives // (warnings_accepted + deploy fails). tracing.SetUsageAttributes( - fields.PreflightOutcomeKey.String(outcome), - fields.PreflightDiagnosticsKey.StringSlice(diagnosticIDs), + fields.ProvisionValidationOutcomeKey.String(outcome), + fields.ProvisionValidationDiagnosticsKey.StringSlice(diagnosticIDs), ) } -// checkRoleAssignmentPermissions is a PreflightCheckFn that verifies the current principal +// checkRoleAssignmentPermissions is a ProvisionValidationCheckFn that verifies the current principal // has Microsoft.Authorization/roleAssignments/write permission when the template contains // role assignments. The PermissionsService is resolved lazily via the service locator so it // is only instantiated when actually needed. @@ -2788,7 +2860,7 @@ func (p *BicepProvider) setPreflightOutcome( // See https://github.com/Azure/azure-dev/issues/7173 for the broader fix. func (p *BicepProvider) checkRoleAssignmentPermissions( ctx context.Context, valCtx *validationContext, -) ([]PreflightCheckResult, error) { +) ([]ProvisionValidationCheckResult, error) { if !valCtx.Props.HasRoleAssignments { return nil, nil } @@ -2827,8 +2899,8 @@ func (p *BicepProvider) checkRoleAssignmentPermissions( } if !hasPermission.HasPermission { - return []PreflightCheckResult{{ - Severity: PreflightCheckWarning, + return []ProvisionValidationCheckResult{{ + Severity: ProvisionValidationCheckWarning, DiagnosticID: "role_assignment_missing", Message: fmt.Sprintf( "Principal %s lacks role assignment"+ @@ -2854,8 +2926,8 @@ func (p *BicepProvider) checkRoleAssignmentPermissions( } if hasPermission.Conditional { - return []PreflightCheckResult{{ - Severity: PreflightCheckWarning, + return []ProvisionValidationCheckResult{{ + Severity: ProvisionValidationCheckWarning, DiagnosticID: "role_assignment_conditional", Message: fmt.Sprintf( "Principal %s has conditional role"+ @@ -2880,11 +2952,11 @@ func (p *BicepProvider) checkRoleAssignmentPermissions( // checkReservedResourceNames inspects predicted resource names and warns when a // resource name segment matches Azure's published reserved-word restrictions. // All violations are reported so users can resolve them in a single pass -// instead of rediscovering new violations on each preflight re-run. +// instead of rediscovering new violations on each validation re-run. func (p *BicepProvider) checkReservedResourceNames( _ context.Context, valCtx *validationContext, -) ([]PreflightCheckResult, error) { - var results []PreflightCheckResult +) ([]ProvisionValidationCheckResult, error) { + var results []ProvisionValidationCheckResult const docsLink = "https://learn.microsoft.com/azure/azure-resource-manager/templates/error-reserved-resource-name" @@ -2898,8 +2970,8 @@ func (p *BicepProvider) checkReservedResourceNames( resourceType := output.WithGrayFormat( "(%s)", resource.Type) - results = append(results, PreflightCheckResult{ - Severity: PreflightCheckWarning, + results = append(results, ProvisionValidationCheckResult{ + Severity: ProvisionValidationCheckWarning, DiagnosticID: "reserved_resource_name", Message: fmt.Sprintf( "Resource %s %s %s the"+ @@ -2910,7 +2982,7 @@ func (p *BicepProvider) checkReservedResourceNames( resourceName, resourceType, v.matchType, v.reservedWord, ), - Links: []ux.PreflightReportLink{ + Links: []ux.ProvisionValidationReportLink{ { URL: docsLink, Title: "Reserved resource name errors", @@ -2928,7 +3000,7 @@ func (p *BicepProvider) checkReservedResourceNames( // Returns a warning for each deployment that would exceed the available quota. func (p *BicepProvider) checkAiModelQuota( ctx context.Context, valCtx *validationContext, -) ([]PreflightCheckResult, error) { +) ([]ProvisionValidationCheckResult, error) { if len(valCtx.Props.CognitiveDeployments) == 0 { return nil, nil } @@ -2945,7 +3017,7 @@ func (p *BicepProvider) checkAiModelQuota( } // Use the pre-resolved fallback location from the validation context. - // This was already resolved in validatePreflight from the actual RG + // This was already resolved in validateProvision from the actual RG // location or AZURE_LOCATION, so we avoid a duplicate API call. fallbackLocation := strings.ToLower(valCtx.EnvLocation) @@ -2966,7 +3038,7 @@ func (p *BicepProvider) checkAiModelQuota( byLocation[loc] = append(byLocation[loc], dep) } - var results []PreflightCheckResult + var results []ProvisionValidationCheckResult for _, loc := range slices.Sorted(maps.Keys(byLocation)) { deps := byLocation[loc] @@ -3020,8 +3092,8 @@ func (p *BicepProvider) checkAiModelQuota( details = fmt.Sprintf( " (%s)", strings.Join(detailParts, ", ")) } - results = append(results, PreflightCheckResult{ - Severity: PreflightCheckWarning, + results = append(results, ProvisionValidationCheckResult{ + Severity: ProvisionValidationCheckWarning, DiagnosticID: "ai_model_not_found", Message: fmt.Sprintf( "Model %s%s not found in %s\n"+ @@ -3034,7 +3106,7 @@ func (p *BicepProvider) checkAiModelQuota( ), Suggestion: "Verify the model name, SKU," + " and version are correct.", - Links: []ux.PreflightReportLink{ + Links: []ux.ProvisionValidationReportLink{ { URL: "https://learn.microsoft.com/" + "azure/ai-services/openai/" + @@ -3114,8 +3186,8 @@ func (p *BicepProvider) checkAiModelQuota( ) } - results = append(results, PreflightCheckResult{ - Severity: PreflightCheckWarning, + results = append(results, ProvisionValidationCheckResult{ + Severity: ProvisionValidationCheckWarning, DiagnosticID: "ai_model_quota_exceeded", Message: fmt.Sprintf( "Insufficient quota for model %s %s"+ @@ -3130,7 +3202,7 @@ func (p *BicepProvider) checkAiModelQuota( remaining, ), Suggestion: suggestion, - Links: []ux.PreflightReportLink{ + Links: []ux.ProvisionValidationReportLink{ { URL: "https://learn.microsoft.com/azure/quotas/quickstart-increase-quota-portal", Title: "Increase Azure subscription quotas", @@ -3184,7 +3256,7 @@ type reservedNameViolation struct { // found in resourceName. A single resource can produce multiple violations // (for example "LoginMicrosoftApp" triggers both the LOGIN prefix and the // MICROSOFT substring rule), so all matches are returned to avoid fix-rerun -// cycles during preflight. +// cycles during validation. // // Only the trailing `/`-delimited segment of the name is evaluated. For child // resources, ARM emits the name as `/` (and `/...` diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_reserved_names_test.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_reserved_names_test.go index 1a9bc3ab51e..1cccd7ef446 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_reserved_names_test.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_reserved_names_test.go @@ -271,7 +271,7 @@ func TestCheckReservedResourceNames(t *testing.T) { require.Len(t, results, 4) for _, r := range results { - require.Equal(t, PreflightCheckWarning, r.Severity) + require.Equal(t, ProvisionValidationCheckWarning, r.Severity) require.Equal(t, "reserved_resource_name", r.DiagnosticID) } diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go index 5889b9d96db..7204057b393 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go @@ -2073,32 +2073,32 @@ func TestHelperEvalParamEnvSubst(t *testing.T) { require.Contains(t, substResult.mappedEnvVars, "VAR2") require.False(t, substResult.hasUnsetEnvVar) } -func TestSetPreflightOutcome_SetsSpanAndUsageAttributes(t *testing.T) { +func TestSetProvisionValidationOutcome_SetsSpanAndUsageAttributes(t *testing.T) { span := &mocktracing.Span{} provider := &BicepProvider{} diagnosticIDs := []string{"role_assignment_missing", "role_assignment_conditional"} - provider.setPreflightOutcome(span, preflightOutcomeWarningsAccepted, diagnosticIDs) + provider.setProvisionValidationOutcome(span, provisionValidationOutcomeWarningsAccepted, diagnosticIDs) // Verify outcome is set on the span directly. - outcomeAttr := findSpanAttribute(span.Attributes, "validation.preflight.outcome") + outcomeAttr := findSpanAttribute(span.Attributes, "validation.provision.outcome") require.NotNil(t, outcomeAttr, "expected outcome attribute on span") - require.Equal(t, preflightOutcomeWarningsAccepted, outcomeAttr.Value.AsString()) + require.Equal(t, provisionValidationOutcomeWarningsAccepted, outcomeAttr.Value.AsString()) // Verify usage-level attributes are set for parent command span correlation. usageAttrs := tracing.GetUsageAttributes() - usageOutcome := findSpanAttribute(usageAttrs, "validation.preflight.outcome") + usageOutcome := findSpanAttribute(usageAttrs, "validation.provision.outcome") require.NotNil(t, usageOutcome, "expected outcome in usage attributes") - require.Equal(t, preflightOutcomeWarningsAccepted, usageOutcome.Value.AsString()) + require.Equal(t, provisionValidationOutcomeWarningsAccepted, usageOutcome.Value.AsString()) usageDiag := findSpanAttribute( - usageAttrs, "validation.preflight.diagnostics", + usageAttrs, "validation.provision.diagnostics", ) require.NotNil(t, usageDiag, "expected diagnostics in usage attributes") require.Equal(t, diagnosticIDs, usageDiag.Value.AsStringSlice()) } -func TestSetPreflightOutcome_AllOutcomeValues(t *testing.T) { +func TestSetProvisionValidationOutcome_AllOutcomeValues(t *testing.T) { tests := []struct { name string outcome string @@ -2106,32 +2106,32 @@ func TestSetPreflightOutcome_AllOutcomeValues(t *testing.T) { }{ { name: "passed", - outcome: preflightOutcomePassed, + outcome: provisionValidationOutcomePassed, diagnosticIDs: nil, }, { name: "warnings accepted", - outcome: preflightOutcomeWarningsAccepted, + outcome: provisionValidationOutcomeWarningsAccepted, diagnosticIDs: []string{"role_assignment_missing"}, }, { name: "aborted by errors", - outcome: preflightOutcomeAbortedByErrors, + outcome: provisionValidationOutcomeCanceledByErrors, diagnosticIDs: []string{"role_assignment_missing"}, }, { name: "aborted by user", - outcome: preflightOutcomeAbortedByUser, + outcome: provisionValidationOutcomeCanceledByUser, diagnosticIDs: []string{"role_assignment_conditional"}, }, { name: "skipped", - outcome: preflightOutcomeSkipped, + outcome: provisionValidationOutcomeSkipped, diagnosticIDs: nil, }, { name: "error", - outcome: preflightOutcomeError, + outcome: provisionValidationOutcomeError, diagnosticIDs: nil, }, } @@ -2141,10 +2141,10 @@ func TestSetPreflightOutcome_AllOutcomeValues(t *testing.T) { span := &mocktracing.Span{} provider := &BicepProvider{} - provider.setPreflightOutcome(span, tt.outcome, tt.diagnosticIDs) + provider.setProvisionValidationOutcome(span, tt.outcome, tt.diagnosticIDs) outcomeAttr := findSpanAttribute( - span.Attributes, "validation.preflight.outcome", + span.Attributes, "validation.provision.outcome", ) require.NotNil(t, outcomeAttr) require.Equal(t, tt.outcome, outcomeAttr.Value.AsString()) @@ -3269,11 +3269,11 @@ func TestConvertIntAndJsonHelpers(t *testing.T) { }) } -// TestNewLocalArmPreflightAndAddCheck covers the constructor and AddCheck append path. -func TestNewLocalArmPreflightAndAddCheck(t *testing.T) { +// TestNewProvisionValidatorAndAddCheck covers the constructor and AddCheck append path. +func TestNewProvisionValidatorAndAddCheck(t *testing.T) { t.Parallel() - pf := newLocalArmPreflight("infra/main.bicep", nil, nil, "westus2") + pf := newProvisionValidator("infra/main.bicep", nil, nil, "westus2") require.NotNil(t, pf) require.Equal(t, "infra/main.bicep", pf.modulePath) require.Equal(t, "westus2", pf.envLocation) @@ -3281,11 +3281,11 @@ func TestNewLocalArmPreflightAndAddCheck(t *testing.T) { require.Empty(t, pf.checks) // AddCheck appends; verify count grows. - noopFn := func(ctx context.Context, valCtx *validationContext) ([]PreflightCheckResult, error) { + noopFn := func(ctx context.Context, valCtx *validationContext) ([]ProvisionValidationCheckResult, error) { return nil, nil } - pf.AddCheck(PreflightCheck{RuleID: "rule1", Fn: noopFn}) - pf.AddCheck(PreflightCheck{RuleID: "rule2", Fn: noopFn}) + pf.AddCheck(ProvisionValidationCheck{RuleID: "rule1", Fn: noopFn}) + pf.AddCheck(ProvisionValidationCheck{RuleID: "rule2", Fn: noopFn}) require.Len(t, pf.checks, 2) } @@ -3343,7 +3343,7 @@ func TestDeploymentStateErrors(t *testing.T) { }) } -// TestValidateErrors exercises the error/skip paths of localArmPreflight.validate +// TestValidateErrors exercises the error/skip paths of provisionValidator.validate // without depending on a real Bicep snapshot. func TestValidateErrors(t *testing.T) { t.Parallel() @@ -3354,7 +3354,7 @@ func TestValidateErrors(t *testing.T) { prepareBicepMocks(mockContext) p := createBicepProvider(t, mockContext) - pre := newLocalArmPreflight("main.bicep", p.bicepCli, nil, "eastus2") + pre := newProvisionValidator("main.bicep", p.bicepCli, nil, "eastus2") // Pass invalid JSON to trigger parseTemplate error. _, _, err := pre.validate( t.Context(), @@ -3386,7 +3386,7 @@ func TestValidateErrors(t *testing.T) { `"resources":[{"type":"Microsoft.Resources/deployments",` + `"name":"x","apiVersion":"2020-10-01"}]}`) - pre := newLocalArmPreflight("nonexistent.bicepparam", p.bicepCli, nil, "") + pre := newProvisionValidator("nonexistent.bicepparam", p.bicepCli, nil, "") _, results, err := pre.validate( t.Context(), mockContext.Console, @@ -3421,7 +3421,7 @@ func TestValidateErrors(t *testing.T) { moduleDir := t.TempDir() modulePath := moduleDir + "/main.bicep" - pre := newLocalArmPreflight(modulePath, p.bicepCli, nil, "eastus2") + pre := newProvisionValidator(modulePath, p.bicepCli, nil, "eastus2") _, results, err := pre.validate( t.Context(), mockContext.Console, diff --git a/cli/azd/pkg/infra/provisioning/bicep/local_preflight.go b/cli/azd/pkg/infra/provisioning/bicep/provision_validation.go similarity index 92% rename from cli/azd/pkg/infra/provisioning/bicep/local_preflight.go rename to cli/azd/pkg/infra/provisioning/bicep/provision_validation.go index 54fc59e3776..1e8f2fa2228 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/local_preflight.go +++ b/cli/azd/pkg/infra/provisioning/bicep/provision_validation.go @@ -253,20 +253,20 @@ type armTemplate struct { Outputs map[string]armTemplateOutputDef `json:"outputs,omitempty"` } -// PreflightCheckSeverity indicates the severity level of a preflight check result. -type PreflightCheckSeverity int +// ProvisionValidationCheckSeverity indicates the severity level of a validation check result. +type ProvisionValidationCheckSeverity int const ( - // PreflightCheckWarning indicates a non-blocking issue that should be reported to the user. - PreflightCheckWarning PreflightCheckSeverity = iota - // PreflightCheckError indicates a blocking issue that should prevent deployment. - PreflightCheckError + // ProvisionValidationCheckWarning indicates a non-blocking issue that should be reported to the user. + ProvisionValidationCheckWarning ProvisionValidationCheckSeverity = iota + // ProvisionValidationCheckError indicates a blocking issue that should prevent deployment. + ProvisionValidationCheckError ) -// PreflightCheckResult holds the outcome of a single preflight check function. -type PreflightCheckResult struct { +// ProvisionValidationCheckResult holds the outcome of a single validation check function. +type ProvisionValidationCheckResult struct { // Severity indicates whether this result is a warning or a blocking error. - Severity PreflightCheckSeverity + Severity ProvisionValidationCheckSeverity // DiagnosticID is a unique, stable identifier for this specific finding type // (e.g. "role_assignment_missing"). Used in telemetry to correlate actioned // warnings with deployment outcomes and to track error frequency over time. @@ -277,10 +277,10 @@ type PreflightCheckResult struct { // It should be dynamically generated with context-specific advice when possible. Suggestion string // Links is an optional list of reference links related to the finding. - Links []ux.PreflightReportLink + Links []ux.ProvisionValidationReportLink } -// validationContext provides the data and utilities available to preflight check functions. +// validationContext provides the data and utilities available to validation check functions. // It acts as a bag of convenient values that checks may inspect to produce their results. type validationContext struct { // Console provides user interaction capabilities (prompts, messages). @@ -337,35 +337,35 @@ type snapshotResult struct { PredictedResources []armTemplateResource `json:"predictedResources"` } -// PreflightCheckFn is a function that performs a single preflight validation check. +// ProvisionValidationCheckFn is a function that performs a single provision validation check. // It receives the execution context and a validationContext containing the console, // analyzed resource properties, and the deployment snapshot. // It returns zero or more results describing findings (or nil/empty if there is // nothing to report) and an error if the check itself failed to execute. -type PreflightCheckFn func( +type ProvisionValidationCheckFn func( ctx context.Context, valCtx *validationContext, -) ([]PreflightCheckResult, error) +) ([]ProvisionValidationCheckResult, error) -// PreflightCheck pairs a unique rule identifier with its check function. +// ProvisionValidationCheck pairs a unique rule identifier with its check function. // The RuleID is a stable, unique string used in telemetry to identify which rule // produced a result (e.g. for crash tracking). Each rule may emit results with // different DiagnosticIDs to distinguish specific finding types. -type PreflightCheck struct { +type ProvisionValidationCheck struct { // RuleID is a unique, stable identifier for the rule (e.g. "role_assignment_permissions"). RuleID string // Fn is the check function that performs the validation. - Fn PreflightCheckFn + Fn ProvisionValidationCheckFn } -// localArmPreflight provides local (client-side) validation of an ARM template before deployment. +// provisionValidator provides local (client-side) validation of an ARM template before deployment. // It parses the template and parameters to build a comprehensive view of all resources that would // be deployed, enabling early detection of issues without making Azure API calls. // // Callers can register additional check functions via AddCheck before calling validate. Each // registered function is invoked with the analyzed resource properties, and the results are // collected and returned alongside the resource properties. -type localArmPreflight struct { +type provisionValidator struct { // modulePath is the absolute path to the source Bicep module (e.g. /project/infra/main.bicep). modulePath string // bicepCli is the Bicep CLI wrapper used to run bicep commands such as snapshot. @@ -377,18 +377,18 @@ type localArmPreflight struct { // deployments, enabling Bicep to resolve resourceGroup().location. This is looked up from // the actual resource group (when it exists) or falls back to AZURE_LOCATION. envLocation string - checks []PreflightCheck + checks []ProvisionValidationCheck } -// newLocalArmPreflight creates a new instance of localArmPreflight. +// newProvisionValidator creates a new instance of provisionValidator. // modulePath is the path to the source Bicep module file (e.g. "infra/main.bicep"). // bicepCli is the Bicep CLI wrapper used to invoke bicep commands. // target is the deployment scope used to populate snapshot options; it may be nil. // envLocation is the resolved location for RG deployments (from RG lookup or AZURE_LOCATION). -func newLocalArmPreflight( +func newProvisionValidator( modulePath string, bicepCli *bicep.Cli, target infra.Deployment, envLocation string, -) *localArmPreflight { - return &localArmPreflight{ +) *provisionValidator { + return &provisionValidator{ modulePath: modulePath, bicepCli: bicepCli, target: target, @@ -396,22 +396,22 @@ func newLocalArmPreflight( } } -// AddCheck registers a preflight check to be executed during validate. +// AddCheck registers a validation check to be executed during validate. // Checks are invoked in the order they are added. -func (l *localArmPreflight) AddCheck(check PreflightCheck) { +func (l *provisionValidator) AddCheck(check ProvisionValidationCheck) { l.checks = append(l.checks, check) } -// validate performs local preflight validation on the given ARM template and parameters. +// validate performs local provision validation on the given ARM template and parameters. // It parses the template, resolves parameters, analyzes the resources, and then runs all // registered check functions. It returns the validation context (for extension dispatch), // the collected results from all checks, and an error if template parsing fails. -func (l *localArmPreflight) validate( +func (l *provisionValidator) validate( ctx context.Context, console input.Console, armTemplate azure.RawArmTemplate, armParameters azure.ArmParameters, -) (*validationContext, []PreflightCheckResult, error) { +) (*validationContext, []ProvisionValidationCheckResult, error) { _, err := l.parseTemplate(armTemplate) if err != nil { return nil, nil, fmt.Errorf("parsing ARM template: %w", err) @@ -429,7 +429,7 @@ func (l *localArmPreflight) validate( bicepParamContent := generateBicepParam(bicepFileName, armParameters) - tmpFile, err := os.CreateTemp(moduleDir, "preflight-*.bicepparam") + tmpFile, err := os.CreateTemp(moduleDir, "validation-*.bicepparam") if err != nil { return nil, nil, fmt.Errorf("creating temp bicepparam file: %w", err) } @@ -469,10 +469,10 @@ func (l *localArmPreflight) validate( // The snapshot contains the fully resolved deployment graph with expressions evaluated, // conditions applied, and copy loops expanded. // If the snapshot fails (e.g., older Bicep binary without snapshot support), skip local - // preflight rather than blocking the deployment. + // validation rather than blocking the deployment. data, err := l.bicepCli.Snapshot(ctx, bicepParamFile, snapshotOpts) if err != nil { - log.Printf("local preflight: skipping checks, bicep snapshot unavailable: %v", err) + log.Printf("provision validation: skipping checks, bicep snapshot unavailable: %v", err) return nil, nil, nil } @@ -493,11 +493,11 @@ func (l *localArmPreflight) validate( // Initialize to a non-nil empty slice so the caller can distinguish "checks ran // but found nothing" (empty slice) from "checks were skipped" (nil). - results := []PreflightCheckResult{} + results := []ProvisionValidationCheckResult{} for _, check := range l.checks { checkResults, err := check.Fn(ctx, valCtx) if err != nil { - return valCtx, results, fmt.Errorf("preflight check %q failed: %w", check.RuleID, err) + return valCtx, results, fmt.Errorf("validation check %q failed: %w", check.RuleID, err) } results = append(results, checkResults...) } @@ -506,7 +506,7 @@ func (l *localArmPreflight) validate( } // parseTemplate unmarshals a raw ARM template into the parser's own armTemplate structure. -func (l *localArmPreflight) parseTemplate(raw azure.RawArmTemplate) (*armTemplate, error) { +func (l *provisionValidator) parseTemplate(raw azure.RawArmTemplate) (*armTemplate, error) { var tmpl armTemplate if err := json.Unmarshal(raw, &tmpl); err != nil { return nil, fmt.Errorf("unmarshalling ARM template JSON: %w", err) @@ -618,7 +618,7 @@ func toBicepValue(v any) string { } } -// resourcesProperties contains derived properties from analyzing the collected preflight resources. +// resourcesProperties contains derived properties from analyzing the collected validation resources. type resourcesProperties struct { // HasRoleAssignments indicates whether the deployment includes one or more // Microsoft.Authorization/roleAssignments resources. diff --git a/cli/azd/pkg/infra/provisioning/bicep/local_preflight_test.go b/cli/azd/pkg/infra/provisioning/bicep/provision_validation_test.go similarity index 82% rename from cli/azd/pkg/infra/provisioning/bicep/local_preflight_test.go rename to cli/azd/pkg/infra/provisioning/bicep/provision_validation_test.go index 834e343253d..aa8e0bebd01 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/local_preflight_test.go +++ b/cli/azd/pkg/infra/provisioning/bicep/provision_validation_test.go @@ -24,8 +24,8 @@ func TestParseTemplate_ValidTemplate(t *testing.T) { raw, err := json.Marshal(template) require.NoError(t, err) - preflight := &localArmPreflight{} - parsed, err := preflight.parseTemplate(azure.RawArmTemplate(raw)) + validator := &provisionValidator{} + parsed, err := validator.parseTemplate(azure.RawArmTemplate(raw)) require.NoError(t, err) require.NotNil(t, parsed) @@ -36,8 +36,8 @@ func TestParseTemplate_ValidTemplate(t *testing.T) { func TestParseTemplate_MissingSchema(t *testing.T) { raw := []byte(`{"contentVersion": "1.0.0.0", "resources": [{"type": "Microsoft.Resources/resourceGroups"}]}`) - preflight := &localArmPreflight{} - _, err := preflight.parseTemplate(azure.RawArmTemplate(raw)) + validator := &provisionValidator{} + _, err := validator.parseTemplate(azure.RawArmTemplate(raw)) require.Error(t, err) require.Contains(t, err.Error(), "missing required '$schema'") @@ -50,8 +50,8 @@ func TestParseTemplate_MissingContentVersion(t *testing.T) { schema, ) - preflight := &localArmPreflight{} - _, err := preflight.parseTemplate(azure.RawArmTemplate(raw)) + validator := &provisionValidator{} + _, err := validator.parseTemplate(azure.RawArmTemplate(raw)) require.Error(t, err) require.Contains(t, err.Error(), "missing required 'contentVersion'") @@ -64,16 +64,16 @@ func TestParseTemplate_NoResources(t *testing.T) { schema, ) - preflight := &localArmPreflight{} - _, err := preflight.parseTemplate(azure.RawArmTemplate(raw)) + validator := &provisionValidator{} + _, err := validator.parseTemplate(azure.RawArmTemplate(raw)) require.Error(t, err) require.Contains(t, err.Error(), "no resources") } func TestParseTemplate_InvalidJSON(t *testing.T) { - preflight := &localArmPreflight{} - _, err := preflight.parseTemplate(azure.RawArmTemplate([]byte(`{}`))) + validator := &provisionValidator{} + _, err := validator.parseTemplate(azure.RawArmTemplate([]byte(`{}`))) require.Error(t, err) require.Contains(t, err.Error(), "missing required") @@ -84,17 +84,17 @@ func TestRegisteredChecks_RunInOrder(t *testing.T) { Props: resourcesProperties{}, } - var checks []PreflightCheck + var checks []ProvisionValidationCheck // Add a warning check - checks = append(checks, PreflightCheck{ + checks = append(checks, ProvisionValidationCheck{ RuleID: "warning_rule", Fn: func( ctx context.Context, valCtx *validationContext, - ) ([]PreflightCheckResult, error) { - return []PreflightCheckResult{{ - Severity: PreflightCheckWarning, + ) ([]ProvisionValidationCheckResult, error) { + return []ProvisionValidationCheckResult{{ + Severity: ProvisionValidationCheckWarning, DiagnosticID: "warning_diag", Message: "this is a warning", }}, nil @@ -102,32 +102,32 @@ func TestRegisteredChecks_RunInOrder(t *testing.T) { }) // Add a check that returns nil (no finding) - checks = append(checks, PreflightCheck{ + checks = append(checks, ProvisionValidationCheck{ RuleID: "nil_rule", Fn: func( ctx context.Context, valCtx *validationContext, - ) ([]PreflightCheckResult, error) { + ) ([]ProvisionValidationCheckResult, error) { return nil, nil }, }) // Add an error check - checks = append(checks, PreflightCheck{ + checks = append(checks, ProvisionValidationCheck{ RuleID: "error_rule", Fn: func( ctx context.Context, valCtx *validationContext, - ) ([]PreflightCheckResult, error) { - return []PreflightCheckResult{{ - Severity: PreflightCheckError, + ) ([]ProvisionValidationCheckResult, error) { + return []ProvisionValidationCheckResult{{ + Severity: ProvisionValidationCheckError, DiagnosticID: "error_diag", Message: "this is an error", }}, nil }, }) - var results []PreflightCheckResult + var results []ProvisionValidationCheckResult for _, check := range checks { checkResults, err := check.Fn(t.Context(), valCtx) require.NoError(t, err) @@ -135,24 +135,24 @@ func TestRegisteredChecks_RunInOrder(t *testing.T) { } require.Len(t, results, 2) - require.Equal(t, PreflightCheckWarning, results[0].Severity) + require.Equal(t, ProvisionValidationCheckWarning, results[0].Severity) require.Equal(t, "warning_diag", results[0].DiagnosticID) require.Equal(t, "this is a warning", results[0].Message) - require.Equal(t, PreflightCheckError, results[1].Severity) + require.Equal(t, ProvisionValidationCheckError, results[1].Severity) require.Equal(t, "error_diag", results[1].DiagnosticID) require.Equal(t, "this is an error", results[1].Message) } -func TestPreflightCheck_DiagnosticIDPropagation(t *testing.T) { +func TestProvisionValidationCheck_DiagnosticIDPropagation(t *testing.T) { valCtx := &validationContext{ Props: resourcesProperties{}, } - check := PreflightCheck{ + check := ProvisionValidationCheck{ RuleID: "test_rule", - Fn: func(ctx context.Context, valCtx *validationContext) ([]PreflightCheckResult, error) { - return []PreflightCheckResult{{ - Severity: PreflightCheckWarning, + Fn: func(ctx context.Context, valCtx *validationContext) ([]ProvisionValidationCheckResult, error) { + return []ProvisionValidationCheckResult{{ + Severity: ProvisionValidationCheckWarning, DiagnosticID: "test_diagnostic_id", Message: "test message", }}, nil @@ -166,17 +166,17 @@ func TestPreflightCheck_DiagnosticIDPropagation(t *testing.T) { require.Equal(t, "test_rule", check.RuleID) } -func TestPreflightCheck_AddCheckStoresRuleID(t *testing.T) { - preflight := &localArmPreflight{} - preflight.AddCheck(PreflightCheck{ +func TestProvisionValidationCheck_AddCheckStoresRuleID(t *testing.T) { + validator := &provisionValidator{} + validator.AddCheck(ProvisionValidationCheck{ RuleID: "failing_rule", - Fn: func(ctx context.Context, valCtx *validationContext) ([]PreflightCheckResult, error) { + Fn: func(ctx context.Context, valCtx *validationContext) ([]ProvisionValidationCheckResult, error) { return nil, fmt.Errorf("something went wrong") }, }) - require.Len(t, preflight.checks, 1) - require.Equal(t, "failing_rule", preflight.checks[0].RuleID) + require.Len(t, validator.checks, 1) + require.Equal(t, "failing_rule", validator.checks[0].RuleID) } func TestArmField_TypedValue(t *testing.T) { diff --git a/cli/azd/pkg/infra/provisioning/bicep/role_assignment_check_test.go b/cli/azd/pkg/infra/provisioning/bicep/role_assignment_check_test.go index 8642e6ac8b7..50e28812943 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/role_assignment_check_test.go +++ b/cli/azd/pkg/infra/provisioning/bicep/role_assignment_check_test.go @@ -10,18 +10,18 @@ import ( "github.com/stretchr/testify/require" ) -func TestPreflightCheckFn_SkipsWhenNoRoleAssignments(t *testing.T) { +func TestProvisionValidationCheckFn_SkipsWhenNoRoleAssignments(t *testing.T) { called := false - checkFn := PreflightCheckFn(func( + checkFn := ProvisionValidationCheckFn(func( ctx context.Context, valCtx *validationContext, - ) ([]PreflightCheckResult, error) { + ) ([]ProvisionValidationCheckResult, error) { called = true if !valCtx.Props.HasRoleAssignments { return nil, nil } - return []PreflightCheckResult{{ - Severity: PreflightCheckError, + return []ProvisionValidationCheckResult{{ + Severity: ProvisionValidationCheckError, Message: "missing permissions", }}, nil }) @@ -36,16 +36,16 @@ func TestPreflightCheckFn_SkipsWhenNoRoleAssignments(t *testing.T) { require.Nil(t, result) } -func TestPreflightCheckFn_ReportsErrorWhenRoleAssignments(t *testing.T) { - checkFn := PreflightCheckFn(func( +func TestProvisionValidationCheckFn_ReportsErrorWhenRoleAssignments(t *testing.T) { + checkFn := ProvisionValidationCheckFn(func( ctx context.Context, valCtx *validationContext, - ) ([]PreflightCheckResult, error) { + ) ([]ProvisionValidationCheckResult, error) { if !valCtx.Props.HasRoleAssignments { return nil, nil } - return []PreflightCheckResult{{ - Severity: PreflightCheckError, + return []ProvisionValidationCheckResult{{ + Severity: ProvisionValidationCheckError, Message: "missing role assignment permissions", }}, nil }) @@ -57,6 +57,6 @@ func TestPreflightCheckFn_ReportsErrorWhenRoleAssignments(t *testing.T) { results, err := checkFn(t.Context(), valCtx) require.NoError(t, err) require.Len(t, results, 1) - require.Equal(t, PreflightCheckError, results[0].Severity) + require.Equal(t, ProvisionValidationCheckError, results[0].Severity) require.Contains(t, results[0].Message, "missing role assignment permissions") } diff --git a/cli/azd/pkg/infra/provisioning/manager.go b/cli/azd/pkg/infra/provisioning/manager.go index 5d9b81caf0c..3ad5f740644 100644 --- a/cli/azd/pkg/infra/provisioning/manager.go +++ b/cli/azd/pkg/infra/provisioning/manager.go @@ -105,8 +105,8 @@ func (m *Manager) Deploy(ctx context.Context) (*DeployResult, error) { skippedDueToDeploymentState := deployResult.SkippedReason == DeploymentStateSkipped - if deployResult.SkippedReason == PreflightAbortedSkipped { - // Preflight intentionally aborted the deployment. There is no Deployment to process. + if deployResult.SkippedReason == ProvisionValidationCanceledSkipped { + // Validation intentionally canceled provisioning. There is no Deployment to process. return deployResult, nil } diff --git a/cli/azd/pkg/infra/provisioning/provider.go b/cli/azd/pkg/infra/provisioning/provider.go index a27d5d52923..bca336cdbb2 100644 --- a/cli/azd/pkg/infra/provisioning/provider.go +++ b/cli/azd/pkg/infra/provisioning/provider.go @@ -218,8 +218,8 @@ func (o *Options) validateLayers() error { type SkippedReasonType string const ( - DeploymentStateSkipped SkippedReasonType = "deployment State" - PreflightAbortedSkipped SkippedReasonType = "preflight aborted" + DeploymentStateSkipped SkippedReasonType = "deployment state" + ProvisionValidationCanceledSkipped SkippedReasonType = "provision validation canceled" ) type DeployResult struct { diff --git a/cli/azd/pkg/output/ux/preflight_report.go b/cli/azd/pkg/output/ux/provision_validation_report.go similarity index 75% rename from cli/azd/pkg/output/ux/preflight_report.go rename to cli/azd/pkg/output/ux/provision_validation_report.go index 250b74e7ff9..daeb5e62300 100644 --- a/cli/azd/pkg/output/ux/preflight_report.go +++ b/cli/azd/pkg/output/ux/provision_validation_report.go @@ -11,8 +11,8 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/output" ) -// PreflightReportItem represents a single finding from preflight validation. -type PreflightReportItem struct { +// ProvisionValidationReportItem represents a single finding from provision validation. +type ProvisionValidationReportItem struct { // IsError is true for blocking errors, false for warnings. IsError bool // DiagnosticID is a unique, stable identifier for this finding type (e.g. @@ -23,11 +23,11 @@ type PreflightReportItem struct { // Suggestion is an optional actionable recommendation for resolving the issue. Suggestion string // Links is an optional list of reference links related to the finding. - Links []PreflightReportLink + Links []ProvisionValidationReportLink } -// PreflightReportLink represents a reference link attached to a preflight report item. -type PreflightReportLink struct { +// ProvisionValidationReportLink represents a reference link attached to a validation report item. +type ProvisionValidationReportLink struct { // URL is the link target. URL string // Title is the display text for terminal hyperlinks (optional). @@ -35,13 +35,13 @@ type PreflightReportLink struct { Title string } -// PreflightReport displays the results of local preflight validation. +// ProvisionValidationReport displays the results of local provision validation. // Warnings are shown first, followed by errors. Each entry is separated by a blank line. -type PreflightReport struct { - Items []PreflightReportItem +type ProvisionValidationReport struct { + Items []ProvisionValidationReportItem } -func (r *PreflightReport) ToString(currentIndentation string) string { +func (r *ProvisionValidationReport) ToString(currentIndentation string) string { warnings, errors := r.partition() if len(warnings) == 0 && len(errors) == 0 { return "" @@ -74,7 +74,7 @@ func (r *PreflightReport) ToString(currentIndentation string) string { // The first line is prefixed with the status indicator (e.g. "(!) Warning:"). // Continuation lines in the message are indented at the same level as the prefix. func writeItem( - sb *strings.Builder, indent string, prefix string, item PreflightReportItem, + sb *strings.Builder, indent string, prefix string, item ProvisionValidationReportItem, ) { if item.Message == "" { return @@ -104,16 +104,16 @@ func writeItem( } } -func (r *PreflightReport) MarshalJSON() ([]byte, error) { +func (r *ProvisionValidationReport) MarshalJSON() ([]byte, error) { warnings, errors := r.partition() return json.Marshal(output.EventForMessage( - fmt.Sprintf("preflight: %d warning(s), %d error(s)", + fmt.Sprintf("provision validation: %d warning(s), %d error(s)", len(warnings), len(errors)))) } // HasErrors returns true if the report contains at least one error-level item. -func (r *PreflightReport) HasErrors() bool { +func (r *ProvisionValidationReport) HasErrors() bool { for _, item := range r.Items { if item.IsError { return true @@ -123,7 +123,7 @@ func (r *PreflightReport) HasErrors() bool { } // HasWarnings returns true if the report contains at least one warning-level item. -func (r *PreflightReport) HasWarnings() bool { +func (r *ProvisionValidationReport) HasWarnings() bool { for _, item := range r.Items { if !item.IsError { return true @@ -133,7 +133,7 @@ func (r *PreflightReport) HasWarnings() bool { } // partition splits items into warnings and errors, preserving order within each group. -func (r *PreflightReport) partition() (warnings, errors []PreflightReportItem) { +func (r *ProvisionValidationReport) partition() (warnings, errors []ProvisionValidationReportItem) { for _, item := range r.Items { if item.IsError { errors = append(errors, item) diff --git a/cli/azd/pkg/output/ux/preflight_report_test.go b/cli/azd/pkg/output/ux/provision_validation_report_test.go similarity index 76% rename from cli/azd/pkg/output/ux/preflight_report_test.go rename to cli/azd/pkg/output/ux/provision_validation_report_test.go index e0d0e36d590..7c8522c243c 100644 --- a/cli/azd/pkg/output/ux/preflight_report_test.go +++ b/cli/azd/pkg/output/ux/provision_validation_report_test.go @@ -13,16 +13,16 @@ import ( "github.com/azure/azure-dev/cli/azd/test/snapshot" ) -func TestPreflightReport_EmptyItems(t *testing.T) { - report := &PreflightReport{} +func TestProvisionValidationReport_EmptyItems(t *testing.T) { + report := &ProvisionValidationReport{} require.Empty(t, report.ToString("")) require.False(t, report.HasErrors()) require.False(t, report.HasWarnings()) } -func TestPreflightReport_WarningsOnly(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_WarningsOnly(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ {IsError: false, Message: "first warning"}, {IsError: false, Message: "second warning"}, }, @@ -37,9 +37,9 @@ func TestPreflightReport_WarningsOnly(t *testing.T) { require.True(t, report.HasWarnings()) } -func TestPreflightReport_ErrorsOnly(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_ErrorsOnly(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ {IsError: true, Message: "critical error"}, }, } @@ -52,9 +52,9 @@ func TestPreflightReport_ErrorsOnly(t *testing.T) { require.False(t, report.HasWarnings()) } -func TestPreflightReport_WarningsBeforeErrors(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_WarningsBeforeErrors(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ {IsError: true, Message: "an error"}, {IsError: false, Message: "a warning"}, }, @@ -70,9 +70,9 @@ func TestPreflightReport_WarningsBeforeErrors(t *testing.T) { require.True(t, report.HasWarnings()) } -func TestPreflightReport_MarshalJSON(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_MarshalJSON(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ {IsError: false, Message: "w1"}, {IsError: true, Message: "e1"}, }, @@ -84,9 +84,9 @@ func TestPreflightReport_MarshalJSON(t *testing.T) { require.Contains(t, string(data), "1 error(s)") } -func TestPreflightReport_WarningWithSuggestion(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_WarningWithSuggestion(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, Message: "insufficient quota for model gpt-4o", @@ -106,14 +106,14 @@ func TestPreflightReport_WarningWithSuggestion(t *testing.T) { require.Greater(t, suggIdx, warnIdx, "suggestion should appear after warning message") } -func TestPreflightReport_WarningWithLinks(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_WarningWithLinks(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, Message: "model not found", Suggestion: "Verify the model name.", - Links: []PreflightReportLink{ + Links: []ProvisionValidationReportLink{ {URL: "https://example.com/models", Title: "Supported models"}, {URL: "https://example.com/raw-link"}, }, @@ -130,9 +130,9 @@ func TestPreflightReport_WarningWithLinks(t *testing.T) { require.Contains(t, result, "https://example.com/raw-link") } -func TestPreflightReport_NoSuggestion(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_NoSuggestion(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ {IsError: false, Message: "simple warning"}, }, } @@ -142,9 +142,9 @@ func TestPreflightReport_NoSuggestion(t *testing.T) { require.NotContains(t, result, "Suggestion:") } -func TestPreflightReport_MarshalJSON_Envelope(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_MarshalJSON_Envelope(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ {IsError: false, Message: "w1", Suggestion: "fix it"}, {IsError: true, Message: "e1"}, }, @@ -166,9 +166,9 @@ func TestPreflightReport_MarshalJSON_Envelope(t *testing.T) { require.Contains(t, parsed.Data.Message, "1 error(s)") } -func TestPreflightReport_Indentation(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_Indentation(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ {IsError: false, Message: "indented warning"}, }, } @@ -178,9 +178,9 @@ func TestPreflightReport_Indentation(t *testing.T) { require.Contains(t, result, "indented warning") } -func TestPreflightReport_MultiLineMessageIndentation(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_MultiLineMessageIndentation(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, Message: "Model \"gpt-4o\" not found in eastus2\n" + @@ -201,9 +201,9 @@ func TestPreflightReport_MultiLineMessageIndentation(t *testing.T) { require.Contains(t, lines[1], "Model not found in AI model catalog.") } -func TestPreflightReport_MultiLineWithSuggestion(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_MultiLineWithSuggestion(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, Message: "Insufficient quota for model \"gpt-4o\" in eastus2\n" + @@ -234,35 +234,35 @@ func indexOf(s, substr string) int { return -1 } -func TestPreflightReport_WriteItem_EdgeCases(t *testing.T) { +func TestProvisionValidationReport_WriteItem_EdgeCases(t *testing.T) { tests := []struct { name string - item PreflightReportItem + item ProvisionValidationReportItem contains []string excludes []string }{ { name: "empty message", - item: PreflightReportItem{Message: ""}, + item: ProvisionValidationReportItem{Message: ""}, excludes: []string{"Warning"}, }, { name: "trailing newline in message", - item: PreflightReportItem{ + item: ProvisionValidationReportItem{ Message: "title line\n", }, contains: []string{"title line"}, }, { name: "consecutive newlines in message", - item: PreflightReportItem{ + item: ProvisionValidationReportItem{ Message: "first\n\nthird", }, contains: []string{"first", "third"}, }, { name: "nil links slice", - item: PreflightReportItem{ + item: ProvisionValidationReportItem{ Message: "msg", Links: nil, }, @@ -271,16 +271,16 @@ func TestPreflightReport_WriteItem_EdgeCases(t *testing.T) { }, { name: "empty links slice", - item: PreflightReportItem{ + item: ProvisionValidationReportItem{ Message: "msg", - Links: []PreflightReportLink{}, + Links: []ProvisionValidationReportLink{}, }, contains: []string{"msg"}, excludes: []string{"•"}, }, { name: "empty suggestion string", - item: PreflightReportItem{ + item: ProvisionValidationReportItem{ Message: "msg", Suggestion: "", }, @@ -289,7 +289,7 @@ func TestPreflightReport_WriteItem_EdgeCases(t *testing.T) { }, { name: "message with leading newline", - item: PreflightReportItem{ + item: ProvisionValidationReportItem{ Message: "\nleading newline", }, contains: []string{"leading newline"}, @@ -298,8 +298,8 @@ func TestPreflightReport_WriteItem_EdgeCases(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{tt.item}, + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{tt.item}, } result := report.ToString(" ") for _, s := range tt.contains { @@ -315,9 +315,9 @@ func TestPreflightReport_WriteItem_EdgeCases(t *testing.T) { // Snapshot tests — one per diagnostic type, plus one combined report. // Update snapshots with: UPDATE_SNAPSHOTS=true go test ./pkg/output/ux/... -func TestPreflightReport_Snapshot_RoleAssignmentMissing(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_Snapshot_RoleAssignmentMissing(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, DiagnosticID: "role_assignment_missing", @@ -340,9 +340,9 @@ func TestPreflightReport_Snapshot_RoleAssignmentMissing(t *testing.T) { snapshot.SnapshotT(t, report.ToString(" ")) } -func TestPreflightReport_Snapshot_RoleAssignmentConditional(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_Snapshot_RoleAssignmentConditional(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, DiagnosticID: "role_assignment_conditional", @@ -358,9 +358,9 @@ func TestPreflightReport_Snapshot_RoleAssignmentConditional(t *testing.T) { snapshot.SnapshotT(t, report.ToString(" ")) } -func TestPreflightReport_Snapshot_ReservedResourceName(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_Snapshot_ReservedResourceName(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, DiagnosticID: "reserved_resource_name", @@ -369,7 +369,7 @@ func TestPreflightReport_Snapshot_ReservedResourceName(t *testing.T) { " contains the reserved word \"login\"\n" + "Azure does not allow reserved words in" + " resource names. The deployment will fail.", - Links: []PreflightReportLink{ + Links: []ProvisionValidationReportLink{ { URL: "https://learn.microsoft.com/azure/" + "azure-resource-manager/templates/" + @@ -383,9 +383,9 @@ func TestPreflightReport_Snapshot_ReservedResourceName(t *testing.T) { snapshot.SnapshotT(t, report.ToString(" ")) } -func TestPreflightReport_Snapshot_AiModelNotFound(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_Snapshot_AiModelNotFound(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, DiagnosticID: "ai_model_not_found", @@ -395,7 +395,7 @@ func TestPreflightReport_Snapshot_AiModelNotFound(t *testing.T) { " Provisioning will likely fail.", Suggestion: "Verify the model name, SKU," + " and version are correct.", - Links: []PreflightReportLink{ + Links: []ProvisionValidationReportLink{ { URL: "https://learn.microsoft.com/azure/ai-services/openai/concepts/models", Title: "Azure OpenAI supported models and regions", @@ -407,9 +407,9 @@ func TestPreflightReport_Snapshot_AiModelNotFound(t *testing.T) { snapshot.SnapshotT(t, report.ToString(" ")) } -func TestPreflightReport_Snapshot_AiModelQuotaExceeded(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_Snapshot_AiModelQuotaExceeded(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, DiagnosticID: "ai_model_quota_exceeded", @@ -421,7 +421,7 @@ func TestPreflightReport_Snapshot_AiModelQuotaExceeded(t *testing.T) { " azd env set AZURE_LOCATION ." + " You can also request a quota increase" + " in the Azure portal.", - Links: []PreflightReportLink{ + Links: []ProvisionValidationReportLink{ { URL: "https://learn.microsoft.com/azure/quotas/quickstart-increase-quota-portal", Title: "Increase Azure subscription quotas", @@ -433,9 +433,9 @@ func TestPreflightReport_Snapshot_AiModelQuotaExceeded(t *testing.T) { snapshot.SnapshotT(t, report.ToString(" ")) } -func TestPreflightReport_Snapshot_AllWarningsCombined(t *testing.T) { - report := &PreflightReport{ - Items: []PreflightReportItem{ +func TestProvisionValidationReport_Snapshot_AllWarningsCombined(t *testing.T) { + report := &ProvisionValidationReport{ + Items: []ProvisionValidationReportItem{ { IsError: false, DiagnosticID: "role_assignment_missing", @@ -462,7 +462,7 @@ func TestPreflightReport_Snapshot_AllWarningsCombined(t *testing.T) { " Provisioning will likely fail.", Suggestion: "Verify the model name, SKU," + " and version are correct.", - Links: []PreflightReportLink{ + Links: []ProvisionValidationReportLink{ { URL: "https://learn.microsoft.com/azure/ai-services/openai/concepts/models", Title: "Azure OpenAI supported models and regions", @@ -480,7 +480,7 @@ func TestPreflightReport_Snapshot_AllWarningsCombined(t *testing.T) { " azd env set AZURE_LOCATION ." + " You can also request a quota increase" + " in the Azure portal.", - Links: []PreflightReportLink{ + Links: []ProvisionValidationReportLink{ { URL: "https://learn.microsoft.com/azure/quotas/quickstart-increase-quota-portal", Title: "Increase Azure subscription quotas", diff --git a/cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_AiModelNotFound.snap b/cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_AiModelNotFound.snap similarity index 100% rename from cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_AiModelNotFound.snap rename to cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_AiModelNotFound.snap diff --git a/cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_AiModelQuotaExceeded.snap b/cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_AiModelQuotaExceeded.snap similarity index 100% rename from cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_AiModelQuotaExceeded.snap rename to cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_AiModelQuotaExceeded.snap diff --git a/cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_AllWarningsCombined.snap b/cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_AllWarningsCombined.snap similarity index 100% rename from cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_AllWarningsCombined.snap rename to cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_AllWarningsCombined.snap diff --git a/cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_ReservedResourceName.snap b/cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_ReservedResourceName.snap similarity index 100% rename from cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_ReservedResourceName.snap rename to cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_ReservedResourceName.snap diff --git a/cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_RoleAssignmentConditional.snap b/cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_RoleAssignmentConditional.snap similarity index 100% rename from cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_RoleAssignmentConditional.snap rename to cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_RoleAssignmentConditional.snap diff --git a/cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_RoleAssignmentMissing.snap b/cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_RoleAssignmentMissing.snap similarity index 100% rename from cli/azd/pkg/output/ux/testdata/TestPreflightReport_Snapshot_RoleAssignmentMissing.snap rename to cli/azd/pkg/output/ux/testdata/TestProvisionValidationReport_Snapshot_RoleAssignmentMissing.snap diff --git a/cli/azd/resources/config_options.yaml b/cli/azd/resources/config_options.yaml index cce7303c712..6f00d3dc421 100644 --- a/cli/azd/resources/config_options.yaml +++ b/cli/azd/resources/config_options.yaml @@ -7,7 +7,12 @@ type: string example: "eastus" - key: provision.preflight - description: "Controls whether ARM preflight validation runs before deployment. Set to 'off' to skip validation and reduce deployment time." + description: "Controls whether the server-side ARM preflight validation call runs before deployment. Set to 'off' to skip it and reduce deployment time." + type: string + allowedValues: ["on", "off"] + example: "on" +- key: validation.provision + description: "Controls whether azd's local (client-side) provision validation runs before deployment. Set to 'off' to skip azd's pre-deployment checks (e.g. role assignment, AI model quota, reserved names)." type: string allowedValues: ["on", "off"] example: "on" diff --git a/cli/azd/test/functional/preflight_quota_test.go b/cli/azd/test/functional/provision_validation_quota_test.go similarity index 79% rename from cli/azd/test/functional/preflight_quota_test.go rename to cli/azd/test/functional/provision_validation_quota_test.go index 75be7bebd49..90f07d731df 100644 --- a/cli/azd/test/functional/preflight_quota_test.go +++ b/cli/azd/test/functional/provision_validation_quota_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" ) -// Test_CLI_PreflightQuota_RG_DefaultCapacity verifies that the ai_model_quota preflight +// Test_CLI_ProvisionValidationQuota_RG_DefaultCapacity verifies that the ai_model_quota validation // check fires a quota warning for RG-scoped deployments when capacity is absurdly high. -func Test_CLI_PreflightQuota_RG_DefaultCapacity(t *testing.T) { +func Test_CLI_ProvisionValidationQuota_RG_DefaultCapacity(t *testing.T) { t.Parallel() ctx, cancel := newTestContext(t) defer cancel() @@ -38,7 +38,7 @@ func Test_CLI_PreflightQuota_RG_DefaultCapacity(t *testing.T) { _, err = cli.RunCommandWithStdIn(ctx, stdinForInit(envName), "init") require.NoError(t, err) - // Persist AZURE_LOCATION to the azd environment so the preflight check + // Persist AZURE_LOCATION to the azd environment so the validation check // can resolve it for RG-scoped deployments where the RG doesn't exist yet. _, err = cli.RunCommand(ctx, "env", "set", "AZURE_LOCATION", "eastus2") require.NoError(t, err) @@ -46,12 +46,12 @@ func Test_CLI_PreflightQuota_RG_DefaultCapacity(t *testing.T) { // Provision with default params (capacity=99999) — expect quota warning, answer No. result, err := cli.RunCommandWithStdIn( ctx, - stdinForRGProvisionWithPreflightNo(), + stdinForRGProvisionWithValidationNo(), "provision", ) require.NoError(t, err) // The user declined the warning, so azd should stop before provisioning. - // In this flow, declining the preflight warning is expected to return successfully, + // In this flow, declining the validation warning is expected to return successfully, // and the output should contain the quota warning. output := result.Stdout + result.Stderr require.Contains(t, output, "Insufficient quota", @@ -60,9 +60,9 @@ func Test_CLI_PreflightQuota_RG_DefaultCapacity(t *testing.T) { "expected actionable suggestion in output") } -// Test_CLI_PreflightQuota_RG_InvalidModelName verifies a warning when the model name +// Test_CLI_ProvisionValidationQuota_RG_InvalidModelName verifies a warning when the model name // doesn't exist in the Azure AI catalog. -func Test_CLI_PreflightQuota_RG_InvalidModelName(t *testing.T) { +func Test_CLI_ProvisionValidationQuota_RG_InvalidModelName(t *testing.T) { t.Parallel() ctx, cancel := newTestContext(t) defer cancel() @@ -89,7 +89,7 @@ func Test_CLI_PreflightQuota_RG_InvalidModelName(t *testing.T) { result, err := cli.RunCommandWithStdIn( ctx, - stdinForRGProvisionWithPreflightNo(), + stdinForRGProvisionWithValidationNo(), "provision", ) require.NoError(t, err) @@ -99,9 +99,9 @@ func Test_CLI_PreflightQuota_RG_InvalidModelName(t *testing.T) { require.Contains(t, output, "gpt-nonexistent-model") } -// Test_CLI_PreflightQuota_RG_InvalidVersion verifies a warning when the model version +// Test_CLI_ProvisionValidationQuota_RG_InvalidVersion verifies a warning when the model version // is not available in the catalog. -func Test_CLI_PreflightQuota_RG_InvalidVersion(t *testing.T) { +func Test_CLI_ProvisionValidationQuota_RG_InvalidVersion(t *testing.T) { t.Parallel() ctx, cancel := newTestContext(t) defer cancel() @@ -128,7 +128,7 @@ func Test_CLI_PreflightQuota_RG_InvalidVersion(t *testing.T) { result, err := cli.RunCommandWithStdIn( ctx, - stdinForRGProvisionWithPreflightNo(), + stdinForRGProvisionWithValidationNo(), "provision", ) require.NoError(t, err) @@ -137,9 +137,9 @@ func Test_CLI_PreflightQuota_RG_InvalidVersion(t *testing.T) { "expected model-not-found warning for invalid version") } -// Test_CLI_PreflightQuota_Sub_DefaultCapacity verifies the quota check for +// Test_CLI_ProvisionValidationQuota_Sub_DefaultCapacity verifies the quota check for // subscription-scoped deployments. -func Test_CLI_PreflightQuota_Sub_DefaultCapacity(t *testing.T) { +func Test_CLI_ProvisionValidationQuota_Sub_DefaultCapacity(t *testing.T) { t.Parallel() ctx, cancel := newTestContext(t) defer cancel() @@ -161,7 +161,7 @@ func Test_CLI_PreflightQuota_Sub_DefaultCapacity(t *testing.T) { result, err := cli.RunCommandWithStdIn( ctx, - stdinForProvisionWithPreflightNo(), + stdinForProvisionWithValidationNo(), "provision", ) require.NoError(t, err) @@ -170,9 +170,9 @@ func Test_CLI_PreflightQuota_Sub_DefaultCapacity(t *testing.T) { "expected quota exceeded warning in output") } -// Test_CLI_PreflightQuota_Sub_InvalidModelName verifies model-not-found for +// Test_CLI_ProvisionValidationQuota_Sub_InvalidModelName verifies model-not-found for // subscription-scoped deployments with a bad model name. -func Test_CLI_PreflightQuota_Sub_InvalidModelName(t *testing.T) { +func Test_CLI_ProvisionValidationQuota_Sub_InvalidModelName(t *testing.T) { t.Parallel() ctx, cancel := newTestContext(t) defer cancel() @@ -196,7 +196,7 @@ func Test_CLI_PreflightQuota_Sub_InvalidModelName(t *testing.T) { result, err := cli.RunCommandWithStdIn( ctx, - stdinForProvisionWithPreflightNo(), + stdinForProvisionWithValidationNo(), "provision", ) require.NoError(t, err) @@ -206,9 +206,9 @@ func Test_CLI_PreflightQuota_Sub_InvalidModelName(t *testing.T) { require.Contains(t, output, "gpt-555-turbo") } -// Test_CLI_PreflightQuota_Sub_DifferentLocation verifies quota checking against +// Test_CLI_ProvisionValidationQuota_Sub_DifferentLocation verifies quota checking against // a different location than the primary deployment location. -func Test_CLI_PreflightQuota_Sub_DifferentLocation(t *testing.T) { +func Test_CLI_ProvisionValidationQuota_Sub_DifferentLocation(t *testing.T) { t.Parallel() ctx, cancel := newTestContext(t) defer cancel() @@ -232,7 +232,7 @@ func Test_CLI_PreflightQuota_Sub_DifferentLocation(t *testing.T) { result, err := cli.RunCommandWithStdIn( ctx, - stdinForProvisionWithPreflightNo(), + stdinForProvisionWithValidationNo(), "provision", ) require.NoError(t, err) @@ -242,28 +242,28 @@ func Test_CLI_PreflightQuota_Sub_DifferentLocation(t *testing.T) { "expected quota check against the override location") } -// stdinForProvisionWithPreflightNo provides stdin for subscription-scoped provision that: +// stdinForProvisionWithValidationNo provides stdin for subscription-scoped provision that: // 1. Accepts default subscription // 2. Accepts default location -// 3. Answers "No" to the preflight warning prompt -func stdinForProvisionWithPreflightNo() string { +// 3. Answers "No" to the validation warning prompt +func stdinForProvisionWithValidationNo() string { return strings.Join([]string{ "", // choose subscription (default) "", // choose location (default) - "n", // decline preflight warning + "n", // decline validation warning }, "\n") } -// stdinForRGProvisionWithPreflightNo provides stdin for resource-group-scoped provision: +// stdinForRGProvisionWithValidationNo provides stdin for resource-group-scoped provision: // 1. Accepts default subscription // 2. Accepts default resource group (create new) // 3. Accepts default resource group name -// 4. Answers "No" to the preflight warning prompt -func stdinForRGProvisionWithPreflightNo() string { +// 4. Answers "No" to the validation warning prompt +func stdinForRGProvisionWithValidationNo() string { return strings.Join([]string{ "", // choose subscription (default) "", // choose resource group (default = create new) "", // accept default resource group name - "n", // decline preflight warning + "n", // decline validation warning }, "\n") } diff --git a/cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_RG_DefaultCapacity.yaml b/cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_RG_DefaultCapacity.yaml similarity index 100% rename from cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_RG_DefaultCapacity.yaml rename to cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_RG_DefaultCapacity.yaml diff --git a/cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_RG_InvalidModelName.yaml b/cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_RG_InvalidModelName.yaml similarity index 100% rename from cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_RG_InvalidModelName.yaml rename to cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_RG_InvalidModelName.yaml diff --git a/cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_RG_InvalidVersion.yaml b/cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_RG_InvalidVersion.yaml similarity index 100% rename from cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_RG_InvalidVersion.yaml rename to cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_RG_InvalidVersion.yaml diff --git a/cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_Sub_DefaultCapacity.yaml b/cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_Sub_DefaultCapacity.yaml similarity index 100% rename from cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_Sub_DefaultCapacity.yaml rename to cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_Sub_DefaultCapacity.yaml diff --git a/cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_Sub_DifferentLocation.yaml b/cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_Sub_DifferentLocation.yaml similarity index 100% rename from cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_Sub_DifferentLocation.yaml rename to cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_Sub_DifferentLocation.yaml diff --git a/cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_Sub_InvalidModelName.yaml b/cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_Sub_InvalidModelName.yaml similarity index 100% rename from cli/azd/test/functional/testdata/recordings/Test_CLI_PreflightQuota_Sub_InvalidModelName.yaml rename to cli/azd/test/functional/testdata/recordings/Test_CLI_ProvisionValidationQuota_Sub_InvalidModelName.yaml diff --git a/cli/azd/test/functional/testdata/samples/ai-quota/README.md b/cli/azd/test/functional/testdata/samples/ai-quota/README.md index e762c057287..27f71475c7f 100644 --- a/cli/azd/test/functional/testdata/samples/ai-quota/README.md +++ b/cli/azd/test/functional/testdata/samples/ai-quota/README.md @@ -1,7 +1,7 @@ -# AI Model Quota Preflight Test Samples +# AI Model Quota Provision Validation Test Samples -These samples exercise the `ai_model_quota` preflight validation check added to the -Bicep provider. They are used by functional tests in `preflight_quota_test.go`. +These samples exercise the `ai_model_quota` provision validation check added to the +Bicep provider. They are used by functional tests in `provision_validation_quota_test.go`. ## Samples diff --git a/cli/azd/test/functional/testdata/samples/ai-quota/rg-deployment/infra/main.bicep b/cli/azd/test/functional/testdata/samples/ai-quota/rg-deployment/infra/main.bicep index 616ae894bc1..79e5fed08a3 100644 --- a/cli/azd/test/functional/testdata/samples/ai-quota/rg-deployment/infra/main.bicep +++ b/cli/azd/test/functional/testdata/samples/ai-quota/rg-deployment/infra/main.bicep @@ -1,4 +1,4 @@ -// Minimal resource-group scoped template for testing AI model quota preflight checks. +// Minimal resource-group scoped template for testing AI model quota validation checks. // All model parameters are configurable to exercise different quota validation scenarios. targetScope = 'resourceGroup' diff --git a/cli/azd/test/functional/testdata/samples/ai-quota/sub-deployment/infra/main.bicep b/cli/azd/test/functional/testdata/samples/ai-quota/sub-deployment/infra/main.bicep index fb4e0ccb7de..0e5b43a1e14 100644 --- a/cli/azd/test/functional/testdata/samples/ai-quota/sub-deployment/infra/main.bicep +++ b/cli/azd/test/functional/testdata/samples/ai-quota/sub-deployment/infra/main.bicep @@ -1,4 +1,4 @@ -// Subscription-scoped template for testing AI model quota preflight checks. +// Subscription-scoped template for testing AI model quota validation checks. // Creates a resource group and deploys cognitive services with model deployments. targetScope = 'subscription' diff --git a/docs/architecture/provisioning-pipeline.md b/docs/architecture/provisioning-pipeline.md index 9c607377ce2..111984a86ba 100644 --- a/docs/architecture/provisioning-pipeline.md +++ b/docs/architecture/provisioning-pipeline.md @@ -9,7 +9,7 @@ The provisioning pipeline creates or updates Azure infrastructure from Infrastru ## Pipeline Stages ```text -IaC Templates → Compilation → Preflight Checks → Deployment → State Tracking +IaC Templates → Compilation → Provision Validation → Deployment → State Tracking ``` ### 1. Template Compilation @@ -21,23 +21,24 @@ azd supports two IaC providers: For Bicep, azd can generate a fully resolved deployment snapshot using `bicep snapshot`, which evaluates expressions, applies conditions, expands copy loops, and flattens nested deployments. -### 2. Preflight Checks +### 2. Provision Validation -Client-side validation that runs after compilation but before deployment: +Client-side (local) validation that runs after compilation but before deployment: - **Role assignment permissions** — Checks if the user has required RBAC roles for role assignments in the template - **AI model quota** — Validates that sufficient AI model capacity is available in the target region - **Reserved resource names** — Warns when predicted resource names collide with Azure reserved or restricted names -The preflight framework is pluggable — new checks can be added via `AddCheck()`. +The validation framework is pluggable — new checks can be added via `AddCheck()`. **UX behavior:** - No issues → proceed silently - Warnings only → display and prompt to continue -- Errors → display and abort +- Errors → display and cancel -Disable with: `azd config set provision.preflight off` +Disable local validation with: `azd config set validation.provision off`. +(The separate `azd config set provision.preflight off` disables only the server-side ARM preflight call.) ### 3. Deployment @@ -71,4 +72,4 @@ After deployment, azd stores a hash of the template and parameters. On subsequen ## Detailed Reference - [Provision State](../../cli/azd/docs/provision-state.md) — Hash-based change detection -- [Local Preflight Validation](../../cli/azd/docs/design/local-preflight-validation.md) — Preflight check design +- [Provision Validation](../../cli/azd/docs/design/provision-validation.md) — Provision validation design diff --git a/docs/architecture/system-overview.md b/docs/architecture/system-overview.md index 487f5327ee0..f863b3047b1 100644 --- a/docs/architecture/system-overview.md +++ b/docs/architecture/system-overview.md @@ -59,10 +59,10 @@ container.MustRegisterSingleton(func(dep *Dependency) *MyService { ### Infrastructure Provisioning -The provisioning pipeline compiles IaC templates, runs preflight checks, deploys to Azure, and tracks state: +The provisioning pipeline compiles IaC templates, runs local provision validation, deploys to Azure, and tracks state: 1. Compile Bicep/Terraform templates -2. Run local preflight validation (permissions, AI model quotas, reserved resource names) +2. Run local provision validation (permissions, AI model quotas, reserved resource names) 3. Submit deployment to Azure Resource Manager 4. Track provision state hash for change detection diff --git a/docs/concepts/glossary.md b/docs/concepts/glossary.md index 7c176f2961e..86836ad5275 100644 --- a/docs/concepts/glossary.md +++ b/docs/concepts/glossary.md @@ -46,9 +46,9 @@ The process of creating or updating Azure infrastructure from IaC templates. Tri A hash-based mechanism that tracks whether the IaC template has changed since the last deployment. Provisioning is skipped when the template hash matches the previous deployment, unless `--no-state` is passed. -### Preflight Checks +### Provision Validation -Client-side validation that runs after Bicep compilation but before deployment. Validates role assignment permissions, AI model quotas, and reserved resource names to surface issues early. +Client-side (local) validation that runs after Bicep compilation but before deployment. Disabled with `azd config set validation.provision off`. Validates role assignment permissions, AI model quotas, and reserved resource names to surface issues early. ## Extensions diff --git a/docs/reference/telemetry-data.md b/docs/reference/telemetry-data.md index 60d02375ee4..be2709404ef 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -107,7 +107,7 @@ Commands follow the pattern `cmd.` where spaces become dots. | Event | Description | |-------|-------------| | `tools.pack.build` | Cloud Native Buildpacks build | -| `validation.preflight` | Local preflight validation | +| `validation.provision` | Local provision validation | | `hooks.exec` | Lifecycle hook execution | | `aks.postprovision.skip` | AKS postprovision hook skipped | | `deploy.appservice.zip` | App Service zip deployment | @@ -358,16 +358,16 @@ Set **only when an external command-line tool invocation fails**, during error c
-Preflight Validation +Provision Validation | Field Key | Type | Description | |-----------|------|-------------| -| `validation.preflight.outcome` | string | `passed`, `warnings_accepted`, `aborted_by_errors`, `aborted_by_user`, `skipped`, `error` | -| `validation.preflight.diagnostics` | string[] | Diagnostic IDs emitted | -| `validation.preflight.rules` | string[] | Rule IDs executed | -| `validation.preflight.extension_rules` | string[] | Rule IDs executed from extension-provided validation checks | -| `validation.preflight.warning.count` | measurement | Number of warnings | -| `validation.preflight.error.count` | measurement | Number of errors | +| `validation.provision.outcome` | string | `passed`, `warnings_accepted`, `canceled_by_errors`, `canceled_by_user`, `skipped`, `error` | +| `validation.provision.diagnostics` | string[] | Diagnostic IDs emitted | +| `validation.provision.rules` | string[] | Rule IDs executed | +| `validation.provision.extension_rules` | string[] | Rule IDs executed from extension-provided validation checks | +| `validation.provision.warning.count` | measurement | Number of warnings | +| `validation.provision.error.count` | measurement | Number of errors |
diff --git a/docs/specs/exegraph/spec.md b/docs/specs/exegraph/spec.md index de2a54b99cc..e5eee80a2f3 100644 --- a/docs/specs/exegraph/spec.md +++ b/docs/specs/exegraph/spec.md @@ -156,7 +156,7 @@ In-memory `sync.Map` cache keyed by SHA-256 of the full Bicep file tree: shared manager before concurrent steps start, so CI runs are race-free 5. All step failures flow through `wrapProvisionError(ctx, unwrapStepErrors(result))` at the outer boundary: the scheduler's `step "X" failed:` prefix is stripped, and - preflight-abort / JSON state dump / OpenAI-access / Responsible-AI wrappers are + validation-cancel / JSON state dump / OpenAI-access / Responsible-AI wrappers are applied exactly once 6. `FailFast` error policy 7. Concurrency limit configurable via `AZD_PROVISION_CONCURRENCY` env var diff --git a/docs/specs/metrics-audit/feature-telemetry-matrix.md b/docs/specs/metrics-audit/feature-telemetry-matrix.md index 8c4b8c841b0..3ceb0f5e7fb 100644 --- a/docs/specs/metrics-audit/feature-telemetry-matrix.md +++ b/docs/specs/metrics-audit/feature-telemetry-matrix.md @@ -34,7 +34,7 @@ These commands emit attributes or events beyond the global middleware span. | `mcp start` | Per-tool spans via `tracing.Start` with `mcp.client.name`, `mcp.client.version` | MCP event prefix `mcp.*` | | `tool install` / `tool upgrade` / `tool uninstall` / `tool check` / `tool list` / `tool show` | `tool.id`, `tool.ids`, `tool.dry_run`, `tool.install.strategy`, `tool.install.success`, `tool.install.success_count`, `tool.install.failure_count`, `tool.install.failed_ids`, `tool.install.duration_ms`, `tool.upgrade.from_version`, `tool.upgrade.to_version`, `tool.check.updates_available` | Comprehensive coverage in `cli/azd/cmd/tool.go`; install/upgrade emit `tools.pack.build` spans for pack-based tools | | `copilot` (agent) | `copilot.initialize` event (model + reasoning config), `copilot.session` event (session create/resume) | Emitted from `internal/agent/copilot_agent.go`; covers the experimental copilot agent surface | -| `provision` | `validation.preflight` event (preflight outcome + 5 fields), 8 `arm.*` events (subscription / resource-group deploy / stack-deploy / what-if / validate), `aks.postprovision.skip`, per-layer `provision.layer.*` counts (`count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count`) when multi-layer infra is used | Telemetry added across `internal/cmd/provision_*.go` and the ARM deployment client | +| `provision` | `validation.provision` event (provision validation outcome + 5 fields), 8 `arm.*` events (subscription / resource-group deploy / stack-deploy / what-if / validate), `aks.postprovision.skip`, per-layer `provision.layer.*` counts (`count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count`) when multi-layer infra is used | Telemetry added across `internal/cmd/provision_*.go` and the ARM deployment client | | `deploy` / `publish` / `package` | `deploy.appservice.zip` event (zip-deploy outcome), `container.credentials` / `container.publish` / `container.remotebuild` events for container-based services | Per-service-target instrumentation; container events emitted from container-app and ACR push paths | | `hooks run` (and all hook-running commands) | `hooks.exec` event with `hooks.name` (hashed unless built-in lifecycle name), `hooks.type` (project / service / **layer**), `hooks.kind` (script runtime — `sh` / `pwsh` / `js` / `ts` / `python` / `dotnet`) | `hooks.type=layer` was added with multi-layer provision; pre/post is encoded in `hooks.name` (e.g., `prebuild` / `postbuild`); emitted from the hooks runner on every lifecycle command | @@ -72,7 +72,7 @@ These commands emit attributes or events beyond the global middleware span. | **Core Lifecycle** | | | | | | | `restore` | — | ✅ | ❌ | ❌ | Via hooks middleware | | `build` | — | ✅ | ❌ | ❌ | Via hooks middleware | -| `provision` | — | ✅ | ✅ | ✅ | Emits `validation.preflight`, 8 `arm.*` events, `aks.postprovision.skip`, and per-layer `provision.layer.*` counts (`count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count`) for multi-layer infra | +| `provision` | — | ✅ | ✅ | ✅ | Emits `validation.provision`, 8 `arm.*` events, `aks.postprovision.skip`, and per-layer `provision.layer.*` counts (`count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count`) for multi-layer infra | | `package` | — | ✅ | ✅ | ✅ | Via hooks middleware; container service targets emit `container.credentials`, `container.publish`, `container.remotebuild` events | | `deploy` | — | ✅ | ✅ | ✅ | App Service zip-deploy emits `deploy.appservice.zip` (`deploy.appservice.linux`, `deploy.appservice.attempt`); container service targets emit `container.*` events | | `publish` | — | ✅ | ✅ | ✅ | Same as `deploy` (alias behavior) | @@ -126,7 +126,7 @@ command-specific telemetry fields provide analytical value beyond the command na | Tool ID | `tool.id` / `tool.ids` | `tool *` | Identifies which managed tool (e.g., bicep, gh, kubectl) the command acted on | | Tool install metrics | `tool.install.*` | `tool install`, `tool upgrade`, `tool uninstall`, first-run middleware | Success count, failure count, duration, strategy — quantitative install health | | Tool upgrade versions | `tool.upgrade.from_version`, `tool.upgrade.to_version` | `tool upgrade` | Tracks adoption of new tool versions | -| Preflight outcome | `validation.preflight.outcome` (+ peer fields) | `provision` | Distinguishes passed / warnings-accepted / aborted local validation | +| Provision validation outcome | `validation.provision.outcome` (+ peer fields) | `provision` | Distinguishes passed / warnings-accepted / canceled local validation | | ARM deployment events | `arm.deploy.*`, `arm.stack.deploy.*`, `arm.whatif.*`, `arm.validate.*` | `provision` | Distinguishes deployment scope (subscription vs resource-group) and operation kind (deploy / stack / what-if / validate) | | Container events | `container.credentials`, `container.publish`, `container.remotebuild` | `package`, `deploy` | Per-stage container lifecycle for container-based services | | Multi-layer provision | `provision.layer.*` | `provision` | Layer-graph shape counts for multi-layer infra: `count`, `max_parallel`, `safe_fallback_count`, `explicit_dependson_count` (integer measurements; no duration or outcome) | @@ -156,7 +156,7 @@ privacy review covers every emission point. |-----------|---------|--------|----------------|-------| | **Tool first-run middleware** | Wraps every interactive command | (none — enriches the active span) | `tool.firstrun.outcome`, `tool.firstrun.skip_reason`, `tool.firstrun.opt_in`, `tool.firstrun.tools_detected`, `tool.firstrun.tools_offered`, `tool.firstrun.tools_selected`, `tool.firstrun.tools_selected_names`, `tool.firstrun.tools_deselected_names`, `tool.firstrun.install_success_count`, `tool.firstrun.install_failure_count`, `tool.firstrun.install_failed_ids`, `tool.firstrun.install_duration_ms` | Records the first-run consent + tool-install flow; outcome key replaces deprecated boolean `tool.firstrun.completed` | | **Hooks execution middleware** | Every lifecycle command (provision/deploy/up/down/restore/build/package/publish) | `hooks.exec` | `hooks.name` (hashed unless built-in lifecycle name), `hooks.type` (project / service / layer), `hooks.kind` (script runtime — `sh` / `pwsh` / `js` / `ts` / `python` / `dotnet`) | Layer-scope hooks added with multi-layer provision; pre/post is encoded in `hooks.name` (e.g., `prebuild` / `postbuild`), not in `hooks.kind` | -| **Preflight validation** | `provision` (prior to ARM deploy) | `validation.preflight` | `validation.preflight.outcome`, plus 4 peer fields covering warnings/errors counts and abort reason | Local-only validation; outcome captures passed / warnings-accepted / aborted | +| **Provision validation** | `provision` (prior to ARM deploy) | `validation.provision` | `validation.provision.outcome`, plus 4 peer fields covering warnings/errors counts and cancel reason | Local-only validation; outcome captures passed / warnings-accepted / canceled | | **ARM deployment client** | `provision` (any Bicep flow) | `arm.deploy.subscription`, `arm.deploy.resourcegroup`, `arm.stack.deploy.subscription`, `arm.stack.deploy.resourcegroup`, `arm.whatif.subscription`, `arm.whatif.resourcegroup`, `arm.validate.subscription`, `arm.validate.resourcegroup` | ARM operation status + duration | Per-call instrumentation in the ARM client; covers regular + stack deployments at both scopes | | **Multi-layer provision** | `provision` (when `infra.layers[]` is configured in `azure.yaml`) | (none — enriches the `provision` span) | `provision.layer.count`, `provision.layer.max_parallel`, `provision.layer.safe_fallback_count`, `provision.layer.explicit_dependson_count` | All four are integer measurements emitted from `internal/cmd/provision_graph.go`; no per-layer duration or outcome attribute is emitted | | **Execution graph (scheduler)** | `up`, `provision`, `deploy`, `package`, `publish`, `down` | `exegraph.run`, `exegraph.step` | `exegraph.step.count`, `exegraph.max_concurrency`, `exegraph.error_policy`, `exegraph.step.name` (hashed), `exegraph.step.deps` (hashed slice), `exegraph.step.tags` (raw — hardcoded literals only), `exegraph.step.timeout_s` | Step names embed user-defined service / layer names from `azure.yaml`; both `name` and `deps` use `fields.StringHashed` / `fields.StringSliceHashed` | diff --git a/docs/specs/metrics-audit/telemetry-schema.md b/docs/specs/metrics-audit/telemetry-schema.md index 6f61eb2fd3b..c1e3a87768b 100644 --- a/docs/specs/metrics-audit/telemetry-schema.md +++ b/docs/specs/metrics-audit/telemetry-schema.md @@ -21,7 +21,7 @@ OpenTelemetry span name or event name. | `ExtensionPromoteEvent` | `ext.promote` | Extension registry promotion (e.g., dev → main) | | `CopilotInitializeEvent` | `copilot.initialize` | Copilot initialization event | | `CopilotSessionEvent` | `copilot.session` | Copilot session lifecycle event | -| `PreflightValidationEvent` | `validation.preflight` | Local preflight validation outcome | +| `ProvisionValidationEvent` | `validation.provision` | Local provision validation outcome | | `HooksExecEvent` | `hooks.exec` | Lifecycle hook execution | | `AksPostprovisionSkipEvent` | `aks.postprovision.skip` | AKS postprovision hook skipped (cluster not yet available) | | `ArmDeploySubscriptionEvent` | `arm.deploy.subscription` | ARM subscription-scope deploy | @@ -323,15 +323,15 @@ no file paths, no user-identifiable data, no raw error text. | Upgrade to version | `tool.upgrade.to_version` | SystemMetadata | FeatureInsight | New version after a successful upgrade | | Updates available | `tool.check.updates_available` | SystemMetadata | FeatureInsight | **Measurement** — number of installed tools with an available update | -### Preflight Validation +### Provision Validation | Field | OTel Key | Classification | Purpose | Notes | |-------|----------|----------------|---------|-------| -| Outcome | `validation.preflight.outcome` | SystemMetadata | FeatureInsight | Values: `passed`, `warnings_accepted`, `aborted_by_errors`, `aborted_by_user`, `skipped`, `error` | -| Diagnostic IDs | `validation.preflight.diagnostics` | SystemMetadata | FeatureInsight | List of diagnostic IDs emitted by preflight checks (fixed code-defined enum, e.g. `role_assignment_missing`, `role_assignment_conditional`) | -| Rule IDs | `validation.preflight.rules` | SystemMetadata | FeatureInsight | List of rule IDs that were executed (fixed code-defined enum, e.g. `role_assignment_permissions`) | -| Warning count | `validation.preflight.warning.count` | SystemMetadata | FeatureInsight | **Measurement** | -| Error count | `validation.preflight.error.count` | SystemMetadata | FeatureInsight | **Measurement** | +| Outcome | `validation.provision.outcome` | SystemMetadata | FeatureInsight | Values: `passed`, `warnings_accepted`, `canceled_by_errors`, `canceled_by_user`, `skipped`, `error` | +| Diagnostic IDs | `validation.provision.diagnostics` | SystemMetadata | FeatureInsight | List of diagnostic IDs emitted by validation checks (fixed code-defined enum, e.g. `role_assignment_missing`, `role_assignment_conditional`) | +| Rule IDs | `validation.provision.rules` | SystemMetadata | FeatureInsight | List of rule IDs that were executed (fixed code-defined enum, e.g. `role_assignment_permissions`) | +| Warning count | `validation.provision.warning.count` | SystemMetadata | FeatureInsight | **Measurement** | +| Error count | `validation.provision.error.count` | SystemMetadata | FeatureInsight | **Measurement** | ### Provision