From 25e55e41e49bfa37f0170a207c60c7d8cce43a70 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 23 Oct 2026 12:46:43 -0700 Subject: [PATCH 1/3] Add scoped conditional PUT contract for Chaos 2026-11-01 Add optional wildcard create-only/update-only headers and explicit 412 only to workspace, scenario and configuration PUTs. Preserve retained contracts and existing success/LRO behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...figurations_CreateOrUpdate_CreateOnly.json | 44 +++++ ...figurations_CreateOrUpdate_UpdateOnly.json | 40 +++++ .../Scenarios_CreateOrUpdate_CreateOnly.json | 74 ++++++++ .../Scenarios_CreateOrUpdate_UpdateOnly.json | 74 ++++++++ .../Workspaces_CreateOrUpdate_CreateOnly.json | 48 +++++ .../Workspaces_CreateOrUpdate_UpdateOnly.json | 44 +++++ .../Chaos/put-preconditions.tsp | 49 +++++ .../Microsoft.Chaos/Chaos/readme.md | 58 ++++++ .../Microsoft.Chaos/Chaos/scenario.tsp | 8 +- .../Chaos/scenarioConfiguration.tsp | 8 +- ...figurations_CreateOrUpdate_CreateOnly.json | 44 +++++ ...figurations_CreateOrUpdate_UpdateOnly.json | 40 +++++ .../Scenarios_CreateOrUpdate_CreateOnly.json | 74 ++++++++ .../Scenarios_CreateOrUpdate_UpdateOnly.json | 74 ++++++++ .../Workspaces_CreateOrUpdate_CreateOnly.json | 48 +++++ .../Workspaces_CreateOrUpdate_UpdateOnly.json | 44 +++++ .../Chaos/stable/2026-11-01/openapi.json | 78 +++++++- .../Chaos/tests/put-preconditions.test.mjs | 168 ++++++++++++++++++ .../Microsoft.Chaos/Chaos/workspace.tsp | 8 +- 19 files changed, 1021 insertions(+), 4 deletions(-) create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_CreateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_UpdateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_CreateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_UpdateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/put-preconditions.tsp create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_CreateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_UpdateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_CreateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_UpdateOnly.json create mode 100644 specification/chaos/resource-manager/Microsoft.Chaos/Chaos/tests/put-preconditions.test.mjs diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json new file mode 100644 index 000000000000..23d6f9c3b0f6 --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json @@ -0,0 +1,44 @@ +{ + "title": "Create a scenario configuration only when absent, or return 412 when it already exists", + "operationId": "ScenarioConfigurations_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "scenarioName": "exampleScenario", + "scenarioConfigurationName": "exampleConfiguration", + "If-None-Match": "*", + "resource": { + "properties": { + "scenarioId": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario" + } + } + }, + "responses": { + "201": { + "headers": { + "Azure-AsyncOperation": "https://management.azure.com/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/providers/Microsoft.Chaos/locations/westus2/operationStatuses/4bdadd97-207c-4de8-9bba-08339ae099c7?api-version=2026-11-01", + "Retry-After": 10 + }, + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario/configurations/exampleConfiguration", + "name": "exampleConfiguration", + "type": "Microsoft.Chaos/workspaces/scenarios/configurations", + "properties": { + "scenarioId": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario", + "provisioningState": "Creating" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The scenario configuration already exists. The create-only precondition failed.", + "target": "If-None-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json new file mode 100644 index 000000000000..3f27ae93216e --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json @@ -0,0 +1,40 @@ +{ + "title": "Update a scenario configuration only when present, or return 412 when it does not exist", + "operationId": "ScenarioConfigurations_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "scenarioName": "exampleScenario", + "scenarioConfigurationName": "exampleConfiguration", + "If-Match": "*", + "resource": { + "properties": { + "scenarioId": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario" + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario/configurations/exampleConfiguration", + "name": "exampleConfiguration", + "type": "Microsoft.Chaos/workspaces/scenarios/configurations", + "properties": { + "scenarioId": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario", + "provisioningState": "Succeeded" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The scenario configuration does not exist. The update-only precondition failed.", + "target": "If-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_CreateOnly.json new file mode 100644 index 000000000000..b03b26bbe598 --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_CreateOnly.json @@ -0,0 +1,74 @@ +{ + "title": "Create a scenario only when absent, or return 412 when it already exists", + "operationId": "Scenarios_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "scenarioName": "exampleScenario", + "If-None-Match": "*", + "resource": { + "properties": { + "parameters": [], + "actions": [ + { + "name": "restart", + "actionId": "microsoft-compute-shutdown/2.0", + "duration": "PT5M", + "selectors": [ + { + "name": "logicalZone", + "value": "1" + }, + { + "name": "location", + "value": "eastus" + } + ] + } + ] + } + } + }, + "responses": { + "201": { + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario", + "name": "exampleScenario", + "type": "Microsoft.Chaos/workspaces/scenarios", + "properties": { + "version": "1.0", + "parameters": [], + "actions": [ + { + "name": "restart", + "actionId": "microsoft-compute-shutdown/2.0", + "duration": "PT5M", + "selectors": [ + { + "name": "logicalZone", + "value": "1" + }, + { + "name": "location", + "value": "eastus" + } + ] + } + ], + "provisioningState": "Succeeded" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The scenario already exists. The create-only precondition failed.", + "target": "If-None-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_UpdateOnly.json new file mode 100644 index 000000000000..53422b8f8595 --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_UpdateOnly.json @@ -0,0 +1,74 @@ +{ + "title": "Update a scenario only when present, or return 412 when it does not exist", + "operationId": "Scenarios_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "scenarioName": "exampleScenario", + "If-Match": "*", + "resource": { + "properties": { + "parameters": [], + "actions": [ + { + "name": "restart", + "actionId": "microsoft-compute-shutdown/2.0", + "duration": "PT5M", + "selectors": [ + { + "name": "logicalZone", + "value": "1" + }, + { + "name": "location", + "value": "eastus" + } + ] + } + ] + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario", + "name": "exampleScenario", + "type": "Microsoft.Chaos/workspaces/scenarios", + "properties": { + "version": "1.0", + "parameters": [], + "actions": [ + { + "name": "restart", + "actionId": "microsoft-compute-shutdown/2.0", + "duration": "PT5M", + "selectors": [ + { + "name": "logicalZone", + "value": "1" + }, + { + "name": "location", + "value": "eastus" + } + ] + } + ], + "provisioningState": "Succeeded" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The scenario does not exist. The update-only precondition failed.", + "target": "If-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_CreateOnly.json new file mode 100644 index 000000000000..8b78508b30ac --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_CreateOnly.json @@ -0,0 +1,48 @@ +{ + "title": "Create a workspace only when absent, or return 412 when it already exists", + "operationId": "Workspaces_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "If-None-Match": "*", + "resource": { + "location": "westus2", + "properties": { + "scopes": [ + "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG" + ] + } + } + }, + "responses": { + "201": { + "headers": { + "Azure-AsyncOperation": "https://management.azure.com/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/providers/Microsoft.Chaos/locations/westus2/operationStatuses/4bdadd97-207c-4de8-9bba-08339ae099c7?api-version=2026-11-01", + "Retry-After": 10 + }, + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace", + "name": "exampleWorkspace", + "type": "Microsoft.Chaos/workspaces", + "location": "westus2", + "properties": { + "scopes": [ + "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG" + ], + "provisioningState": "Creating" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The workspace already exists. The create-only precondition failed.", + "target": "If-None-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_UpdateOnly.json new file mode 100644 index 000000000000..ec713f86e3cd --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_UpdateOnly.json @@ -0,0 +1,44 @@ +{ + "title": "Update a workspace only when present, or return 412 when it does not exist", + "operationId": "Workspaces_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "If-Match": "*", + "resource": { + "location": "westus2", + "properties": { + "scopes": [ + "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG" + ] + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace", + "name": "exampleWorkspace", + "type": "Microsoft.Chaos/workspaces", + "location": "westus2", + "properties": { + "scopes": [ + "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG" + ], + "provisioningState": "Succeeded" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The workspace does not exist. The update-only precondition failed.", + "target": "If-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/put-preconditions.tsp b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/put-preconditions.tsp new file mode 100644 index 000000000000..bba201d6d236 --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/put-preconditions.tsp @@ -0,0 +1,49 @@ +import "@azure-tools/typespec-azure-resource-manager"; +import "@typespec/http"; +import "@typespec/versioning"; + +using Azure.ResourceManager; +using TypeSpec.Http; +using TypeSpec.Versioning; + +namespace Microsoft.Chaos; + +/** Optional conditions for creating or updating a workspace, scenario, or scenario configuration. */ +model PutPreconditionHeaders { + /** + * Use '*' to create the resource only if it does not exist. If the resource exists, the request fails with 412 Precondition Failed without changing it. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching. + */ + @added(Versions.v2026_11_01) + @header("If-None-Match") + ifNoneMatch?: string; + + /** + * Use '*' to update the resource only if it exists. If the resource does not exist, the request fails with 412 Precondition Failed without creating it. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching. + */ + @added(Versions.v2026_11_01) + @header("If-Match") + ifMatch?: string; +} + +/** An unexpected error response. */ +#suppress "@azure-tools/typespec-client-generator-core/use-union-hierarchy" "HTTP error envelopes select status codes, not a client model inheritance hierarchy; both responses use the existing common Azure error body." +union PutPreconditionError { + ErrorResponse, + + /** An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started. */ + @added(Versions.v2026_11_01) + preconditionFailed: PreconditionFailedResult, +} + +/** An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started. */ +@added(Versions.v2026_11_01) +@error +model PreconditionFailedResult { + /** The HTTP status code. */ + @statusCode + statusCode: 412; + + /** The Azure error describing the failed precondition. */ + @body + body: Azure.ResourceManager.CommonTypes.ErrorResponse; +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md index 1e7a77f5813e..a5fdc76c61b6 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md @@ -4,6 +4,37 @@ This is the AutoRest configuration file for chaos. +## Conditional PUT proposal for 2026-11-01 + +Only `Workspaces_CreateOrUpdate`, `Scenarios_CreateOrUpdate`, and +`ScenarioConfigurations_CreateOrUpdate` add optional `If-None-Match` and +`If-Match` request headers in the upcoming 2026-11-01 contract. +`If-None-Match: *` is create-only: an existing resource returns 412. +`If-Match: *` is update-only: a missing resource returns 412. +A failed precondition returns the standard Azure error body before any resource +change or asynchronous operation starts. Successful requests retain the existing +200/201 responses and polling contract. Omitting both headers preserves +unconditional create-or-update behavior. + +Evaluate applicable conditions in +[HTTP precondition order](https://www.rfc-editor.org/rfc/rfc9110.html#section-13.2.2). +If both wildcard headers are supplied, no resource state satisfies both and the +request fails with 412. Preserve already supported validators; other effective +conditions must be honored or rejected with 412 when they cannot be verified, +never silently ignored. The RFC-required exceptions still apply: +`If-Unmodified-Since` is ignored when `If-Match` is present, its date is invalid, +or no modification date is available. This proposal adds no date-validator +parameter, entity-tag matching platform, response ETag, or `etag` body property. + +This is a spec-only contract proposal, not evidence of runtime implementation +or API approval. It does not change retained API versions, Connections, V1 +resources, DELETE, SDKs, Portal/CLI defaults, or resource ownership. +The design follows the +[Azure conditional request guidance](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#conditional-requests) +and uses +[versioned parameters](https://azure.github.io/typespec-azure/docs/howtos/versioning/06-evolving-apis/) +on the existing ARM operation templates. + ## Workspace customer-managed keys in 2026-11-01 Workspace PUT, PATCH, GET, and list responses support `properties.encryption`. @@ -1308,6 +1339,33 @@ See configuration in [readme.csharp.md](./readme.csharp.md) ```yaml directive: + - suppress: RESPONSE_STATUS_CODE_NOT_IN_EXAMPLE + from: + - stable/2026-11-01/openapi.json + - stable\2026-11-01\openapi.json + where: + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}"].put.responses["200"] + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}"].put.responses["201"] + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}"].put.responses["412"] + reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200, update-only cannot return 201, and unconditional examples have no failed condition to return 412. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and 412; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example status completeness is suppressed, not request or response validation. + - suppress: RESPONSE_STATUS_CODE_NOT_IN_EXAMPLE + from: + - stable/2026-11-01/openapi.json + - stable\2026-11-01\openapi.json + where: + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}"].put.responses["200"] + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}"].put.responses["201"] + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}"].put.responses["412"] + reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200, update-only cannot return 201, and unconditional examples have no failed condition to return 412. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and 412; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example status completeness is suppressed, not request or response validation. + - suppress: RESPONSE_STATUS_CODE_NOT_IN_EXAMPLE + from: + - stable/2026-11-01/openapi.json + - stable\2026-11-01\openapi.json + where: + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}/configurations/{scenarioConfigurationName}"].put.responses["200"] + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}/configurations/{scenarioConfigurationName}"].put.responses["201"] + - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}/configurations/{scenarioConfigurationName}"].put.responses["412"] + reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200, update-only cannot return 201, and unconditional examples have no failed condition to return 412. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and 412; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example status completeness is suppressed, not request or response validation. - suppress: TopLevelResourcesListBySubscription where: $.definitions.target from: targets.json diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenario.tsp b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenario.tsp index 9ad1a0ec3703..f394ede4b832 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenario.tsp +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenario.tsp @@ -3,6 +3,7 @@ import "@azure-tools/typespec-azure-resource-manager"; import "@typespec/rest"; import "@typespec/http"; import "./scenario.models.tsp"; +import "./put-preconditions.tsp"; using Azure.ResourceManager; using TypeSpec.Versioning; @@ -20,7 +21,12 @@ interface Scenarios { /** * Create or update a scenario. */ - createOrUpdate is ArmResourceCreateOrReplaceSync; + #suppress "@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes" "Conditional PUT explicitly returns HTTP 412 as required for failed preconditions; 200, 201 and default responses remain unchanged." + createOrUpdate is ArmResourceCreateOrReplaceSync< + Scenario, + Parameters = PutPreconditionHeaders, + Error = PutPreconditionError + >; /** * Delete a scenario. diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenarioConfiguration.tsp b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenarioConfiguration.tsp index 6ac08fb70c78..713cc9647732 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenarioConfiguration.tsp +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenarioConfiguration.tsp @@ -8,6 +8,7 @@ import "./scenarioRun.models.tsp"; import "./scenarioRun.tsp"; import "./common.models.tsp"; import "./lro-helpers.tsp"; +import "./put-preconditions.tsp"; using Azure.ResourceManager; using Azure.Core; @@ -30,7 +31,12 @@ interface ScenarioConfigurations { /** * Create or update a scenario definition. */ - createOrUpdate is ArmResourceCreateOrReplaceAsync; + #suppress "@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes" "Conditional PUT explicitly returns HTTP 412 as required for failed preconditions; 200, 201 and default responses remain unchanged." + createOrUpdate is ArmResourceCreateOrReplaceAsync< + ScenarioConfiguration, + Parameters = PutPreconditionHeaders, + Error = PutPreconditionError + >; /** * Delete a scenario definition. diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json new file mode 100644 index 000000000000..23d6f9c3b0f6 --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json @@ -0,0 +1,44 @@ +{ + "title": "Create a scenario configuration only when absent, or return 412 when it already exists", + "operationId": "ScenarioConfigurations_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "scenarioName": "exampleScenario", + "scenarioConfigurationName": "exampleConfiguration", + "If-None-Match": "*", + "resource": { + "properties": { + "scenarioId": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario" + } + } + }, + "responses": { + "201": { + "headers": { + "Azure-AsyncOperation": "https://management.azure.com/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/providers/Microsoft.Chaos/locations/westus2/operationStatuses/4bdadd97-207c-4de8-9bba-08339ae099c7?api-version=2026-11-01", + "Retry-After": 10 + }, + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario/configurations/exampleConfiguration", + "name": "exampleConfiguration", + "type": "Microsoft.Chaos/workspaces/scenarios/configurations", + "properties": { + "scenarioId": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario", + "provisioningState": "Creating" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The scenario configuration already exists. The create-only precondition failed.", + "target": "If-None-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json new file mode 100644 index 000000000000..3f27ae93216e --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json @@ -0,0 +1,40 @@ +{ + "title": "Update a scenario configuration only when present, or return 412 when it does not exist", + "operationId": "ScenarioConfigurations_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "scenarioName": "exampleScenario", + "scenarioConfigurationName": "exampleConfiguration", + "If-Match": "*", + "resource": { + "properties": { + "scenarioId": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario" + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario/configurations/exampleConfiguration", + "name": "exampleConfiguration", + "type": "Microsoft.Chaos/workspaces/scenarios/configurations", + "properties": { + "scenarioId": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario", + "provisioningState": "Succeeded" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The scenario configuration does not exist. The update-only precondition failed.", + "target": "If-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_CreateOnly.json new file mode 100644 index 000000000000..b03b26bbe598 --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_CreateOnly.json @@ -0,0 +1,74 @@ +{ + "title": "Create a scenario only when absent, or return 412 when it already exists", + "operationId": "Scenarios_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "scenarioName": "exampleScenario", + "If-None-Match": "*", + "resource": { + "properties": { + "parameters": [], + "actions": [ + { + "name": "restart", + "actionId": "microsoft-compute-shutdown/2.0", + "duration": "PT5M", + "selectors": [ + { + "name": "logicalZone", + "value": "1" + }, + { + "name": "location", + "value": "eastus" + } + ] + } + ] + } + } + }, + "responses": { + "201": { + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario", + "name": "exampleScenario", + "type": "Microsoft.Chaos/workspaces/scenarios", + "properties": { + "version": "1.0", + "parameters": [], + "actions": [ + { + "name": "restart", + "actionId": "microsoft-compute-shutdown/2.0", + "duration": "PT5M", + "selectors": [ + { + "name": "logicalZone", + "value": "1" + }, + { + "name": "location", + "value": "eastus" + } + ] + } + ], + "provisioningState": "Succeeded" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The scenario already exists. The create-only precondition failed.", + "target": "If-None-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_UpdateOnly.json new file mode 100644 index 000000000000..53422b8f8595 --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_UpdateOnly.json @@ -0,0 +1,74 @@ +{ + "title": "Update a scenario only when present, or return 412 when it does not exist", + "operationId": "Scenarios_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "scenarioName": "exampleScenario", + "If-Match": "*", + "resource": { + "properties": { + "parameters": [], + "actions": [ + { + "name": "restart", + "actionId": "microsoft-compute-shutdown/2.0", + "duration": "PT5M", + "selectors": [ + { + "name": "logicalZone", + "value": "1" + }, + { + "name": "location", + "value": "eastus" + } + ] + } + ] + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace/scenarios/exampleScenario", + "name": "exampleScenario", + "type": "Microsoft.Chaos/workspaces/scenarios", + "properties": { + "version": "1.0", + "parameters": [], + "actions": [ + { + "name": "restart", + "actionId": "microsoft-compute-shutdown/2.0", + "duration": "PT5M", + "selectors": [ + { + "name": "logicalZone", + "value": "1" + }, + { + "name": "location", + "value": "eastus" + } + ] + } + ], + "provisioningState": "Succeeded" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The scenario does not exist. The update-only precondition failed.", + "target": "If-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_CreateOnly.json new file mode 100644 index 000000000000..8b78508b30ac --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_CreateOnly.json @@ -0,0 +1,48 @@ +{ + "title": "Create a workspace only when absent, or return 412 when it already exists", + "operationId": "Workspaces_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "If-None-Match": "*", + "resource": { + "location": "westus2", + "properties": { + "scopes": [ + "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG" + ] + } + } + }, + "responses": { + "201": { + "headers": { + "Azure-AsyncOperation": "https://management.azure.com/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/providers/Microsoft.Chaos/locations/westus2/operationStatuses/4bdadd97-207c-4de8-9bba-08339ae099c7?api-version=2026-11-01", + "Retry-After": 10 + }, + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace", + "name": "exampleWorkspace", + "type": "Microsoft.Chaos/workspaces", + "location": "westus2", + "properties": { + "scopes": [ + "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG" + ], + "provisioningState": "Creating" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The workspace already exists. The create-only precondition failed.", + "target": "If-None-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_UpdateOnly.json new file mode 100644 index 000000000000..ec713f86e3cd --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_UpdateOnly.json @@ -0,0 +1,44 @@ +{ + "title": "Update a workspace only when present, or return 412 when it does not exist", + "operationId": "Workspaces_CreateOrUpdate", + "parameters": { + "api-version": "2026-11-01", + "subscriptionId": "6b052e15-03d3-4f17-b2e1-be7f07588291", + "resourceGroupName": "exampleRG", + "workspaceName": "exampleWorkspace", + "If-Match": "*", + "resource": { + "location": "westus2", + "properties": { + "scopes": [ + "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG" + ] + } + } + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG/providers/Microsoft.Chaos/workspaces/exampleWorkspace", + "name": "exampleWorkspace", + "type": "Microsoft.Chaos/workspaces", + "location": "westus2", + "properties": { + "scopes": [ + "/subscriptions/6b052e15-03d3-4f17-b2e1-be7f07588291/resourceGroups/exampleRG" + ], + "provisioningState": "Succeeded" + } + } + }, + "412": { + "body": { + "error": { + "code": "PreconditionFailed", + "message": "The workspace does not exist. The update-only precondition failed.", + "target": "If-Match" + } + } + } + } +} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/openapi.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/openapi.json index b6f67d709754..d123fda13db2 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/openapi.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/openapi.json @@ -2254,6 +2254,12 @@ "minLength": 1, "pattern": "^[^<>%&:?#/\\\\]+$" }, + { + "$ref": "#/parameters/PutPreconditionHeaders.ifNoneMatch" + }, + { + "$ref": "#/parameters/PutPreconditionHeaders.ifMatch" + }, { "name": "resource", "in": "body", @@ -2288,6 +2294,13 @@ } } }, + "412": { + "description": "An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v6/types.json#/definitions/ErrorResponse" + }, + "x-ms-error-response": true + }, "default": { "description": "An unexpected error response.", "schema": { @@ -2296,6 +2309,9 @@ } }, "x-ms-examples": { + "Create a workspace only when absent, or return 412 when it already exists": { + "$ref": "./examples/Workspaces_CreateOrUpdate_CreateOnly.json" + }, "Create or update a Microsoft-managed Workspace with empty encryption settings": { "$ref": "./examples/Workspaces_CreateOrUpdate_EmptyEncryption.json" }, @@ -2307,6 +2323,9 @@ }, "Create/update a workspace in a resource group.": { "$ref": "./examples/Workspaces_CreateOrUpdate.json" + }, + "Update a workspace only when present, or return 412 when it does not exist": { + "$ref": "./examples/Workspaces_CreateOrUpdate_UpdateOnly.json" } }, "x-ms-long-running-operation-options": { @@ -3339,6 +3358,12 @@ "minLength": 1, "pattern": "^[^<>%&:?#/\\\\]+$" }, + { + "$ref": "#/parameters/PutPreconditionHeaders.ifNoneMatch" + }, + { + "$ref": "#/parameters/PutPreconditionHeaders.ifMatch" + }, { "name": "resource", "in": "body", @@ -3362,6 +3387,13 @@ "$ref": "#/definitions/Scenario" } }, + "412": { + "description": "An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v6/types.json#/definitions/ErrorResponse" + }, + "x-ms-error-response": true + }, "default": { "description": "An unexpected error response.", "schema": { @@ -3373,11 +3405,17 @@ "Create a scenario from a built-in scenario template version.": { "$ref": "./examples/Scenarios_CreateOrUpdate_From_Template.json" }, + "Create a scenario only when absent, or return 412 when it already exists": { + "$ref": "./examples/Scenarios_CreateOrUpdate_CreateOnly.json" + }, "Create or update a scenario that continues past suppressed action failures.": { "$ref": "./examples/Scenarios_CreateOrUpdate_With_Failure_Behavior.json" }, "Create or update a scenario.": { "$ref": "./examples/Scenarios_CreateOrUpdate.json" + }, + "Update a scenario only when present, or return 412 when it does not exist": { + "$ref": "./examples/Scenarios_CreateOrUpdate_UpdateOnly.json" } } }, @@ -3605,6 +3643,12 @@ "minLength": 1, "pattern": "^[^<>%&:?#/\\\\]+$" }, + { + "$ref": "#/parameters/PutPreconditionHeaders.ifNoneMatch" + }, + { + "$ref": "#/parameters/PutPreconditionHeaders.ifMatch" + }, { "name": "resource", "in": "body", @@ -3639,6 +3683,13 @@ } } }, + "412": { + "description": "An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started.", + "schema": { + "$ref": "../../../../../../common-types/resource-management/v6/types.json#/definitions/ErrorResponse" + }, + "x-ms-error-response": true + }, "default": { "description": "An unexpected error response.", "schema": { @@ -3647,11 +3698,17 @@ } }, "x-ms-examples": { + "Create a scenario configuration only when absent, or return 412 when it already exists": { + "$ref": "./examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json" + }, "Create or update a scenario configuration with physical zone targeting.": { "$ref": "./examples/ScenarioConfigurations_CreateOrUpdate_With_Physical_Zones.json" }, "Create or update a scenario configuration.": { "$ref": "./examples/ScenarioConfigurations_CreateOrUpdate.json" + }, + "Update a scenario configuration only when present, or return 412 when it does not exist": { + "$ref": "./examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json" } }, "x-ms-long-running-operation-options": { @@ -9003,5 +9060,24 @@ } } }, - "parameters": {} + "parameters": { + "PutPreconditionHeaders.ifMatch": { + "name": "If-Match", + "in": "header", + "description": "Use '*' to update the resource only if it exists. If the resource does not exist, the request fails with 412 Precondition Failed without creating it. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching.", + "required": false, + "type": "string", + "x-ms-parameter-location": "method", + "x-ms-client-name": "ifMatch" + }, + "PutPreconditionHeaders.ifNoneMatch": { + "name": "If-None-Match", + "in": "header", + "description": "Use '*' to create the resource only if it does not exist. If the resource exists, the request fails with 412 Precondition Failed without changing it. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching.", + "required": false, + "type": "string", + "x-ms-parameter-location": "method", + "x-ms-client-name": "ifNoneMatch" + } + } } diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/tests/put-preconditions.test.mjs b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/tests/put-preconditions.test.mjs new file mode 100644 index 000000000000..482824a5f360 --- /dev/null +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/tests/put-preconditions.test.mjs @@ -0,0 +1,168 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +const readJson = (name) => + JSON.parse(readFileSync(path.join(root, name), "utf8")); +const spec = readJson("stable/2026-11-01/openapi.json"); +const operationIds = [ + "Workspaces_CreateOrUpdate", + "Scenarios_CreateOrUpdate", + "ScenarioConfigurations_CreateOrUpdate", +]; +const headerNames = ["If-None-Match", "If-Match"]; +const parameters = (operation) => + operation.parameters.map((parameter) => + parameter.$ref?.startsWith("#/parameters/") + ? spec.parameters[parameter.$ref.slice("#/parameters/".length)] + : parameter, + ); +const operations = Object.values(spec.paths).flatMap((route) => + Object.entries(route).filter(([, operation]) => operation.operationId), +); +const scopedOperations = operations.filter(([, operation]) => + operationIds.includes(operation.operationId), +); + +test("only the three selected PUT operations expose optional wildcard conditions and 412", () => { + assert.equal(scopedOperations.length, 3); + for (const [method, operation] of operations) { + const headers = parameters(operation).filter((parameter) => + headerNames.includes(parameter.name), + ); + if (!operationIds.includes(operation.operationId)) { + assert.deepEqual(headers, [], operation.operationId); + assert.equal( + operation.responses["412"], + undefined, + operation.operationId, + ); + continue; + } + assert.equal(method, "put"); + assert.deepEqual( + headers.map((header) => header.name), + headerNames, + ); + for (const header of headers) { + assert.equal(header.in, "header"); + assert.equal(header.required, false); + assert.equal(header.type, "string"); + assert.equal(header.default, undefined); + assert.equal(header.enum, undefined); + assert.match(header.description, /Use '\*'/); + assert.match(header.description, /must not be silently ignored/); + assert.match( + header.description, + /does not introduce entity-tag matching/, + ); + } + const failure = operation.responses["412"]; + assert.equal(failure["x-ms-error-response"], true); + assert.deepEqual(failure.schema, operation.responses.default.schema); + assert.equal(failure.headers, undefined); + assert.match( + failure.description, + /No resource change or asynchronous operation is started/, + ); + } +}); + +test("create-only and update-only examples are wired, cover success and 412, and are emitted unchanged", () => { + for (const [, operation] of scopedOperations) { + for (const [suffix, header, status] of [ + ["CreateOnly", "If-None-Match", "201"], + ["UpdateOnly", "If-Match", "200"], + ]) { + const name = `${operation.operationId}_${suffix}.json`; + const source = readJson(`examples/2026-11-01/${name}`); + assert.equal(source.operationId, operation.operationId); + assert.equal(source.parameters["api-version"], "2026-11-01"); + assert.equal(source.parameters[header], "*"); + assert.equal( + source.parameters[headerNames.find((name) => name !== header)], + undefined, + ); + assert.deepEqual(Object.keys(source.responses), [status, "412"]); + assert.equal( + source.responses["412"].body.error.code, + "PreconditionFailed", + ); + assert.equal(source.responses["412"].body.error.target, header); + assert.equal(source.responses["412"].headers, undefined); + assert.ok( + Object.values(operation["x-ms-examples"]).some( + (example) => example.$ref === `./examples/${name}`, + ), + name, + ); + assert.deepEqual( + readFileSync(path.join(root, `examples/2026-11-01/${name}`)), + readFileSync(path.join(root, `stable/2026-11-01/examples/${name}`)), + ); + if ( + status === "201" && + operation.operationId !== "Scenarios_CreateOrUpdate" + ) { + assert.ok(source.responses[status].headers["Azure-AsyncOperation"]); + } else { + assert.equal(source.responses[status].headers, undefined); + assert.equal( + source.responses[status].body.properties.provisioningState, + "Succeeded", + ); + } + } + const unconditional = readJson( + `examples/2026-11-01/${operation.operationId}.json`, + ); + for (const header of headerNames) + assert.equal(unconditional.parameters[header], undefined); + } +}); + +test( + "the GA contract differs from the baseline only by the authorized headers, 412 and example links", + { skip: !process.env.CHAOS_PRECONDITION_BASE_REF }, + () => { + const repo = execFileSync("git", ["rev-parse", "--show-toplevel"], { + cwd: root, + encoding: "utf8", + }).trim(); + const relative = path.relative(repo, root).split(path.sep).join("/"); + const baseline = JSON.parse( + execFileSync( + "git", + [ + "show", + `${process.env.CHAOS_PRECONDITION_BASE_REF}:${relative}/stable/2026-11-01/openapi.json`, + ], + { cwd: repo, maxBuffer: 20 * 1024 * 1024, encoding: "utf8" }, + ), + ); + const actual = structuredClone(spec); + for (const route of Object.values(actual.paths)) { + for (const operation of Object.values(route)) { + if (!operationIds.includes(operation.operationId)) continue; + operation.parameters = operation.parameters.filter( + (parameter) => + !parameter.$ref?.startsWith("#/parameters/PutPreconditionHeaders."), + ); + delete operation.responses["412"]; + for (const [title, example] of Object.entries( + operation["x-ms-examples"], + )) { + if (/_(CreateOnly|UpdateOnly)\.json$/.test(example.$ref)) + delete operation["x-ms-examples"][title]; + } + } + } + delete actual.parameters["PutPreconditionHeaders.ifNoneMatch"]; + delete actual.parameters["PutPreconditionHeaders.ifMatch"]; + assert.deepEqual(actual, baseline); + }, +); diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/workspace.tsp b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/workspace.tsp index 9723250dd9c3..4079d016823c 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/workspace.tsp +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/workspace.tsp @@ -6,6 +6,7 @@ import "./workspace.models.tsp"; import "./workspaceEvaluation.models.tsp"; import "./workspaceDiscovery.models.tsp"; import "./lro-helpers.tsp"; +import "./put-preconditions.tsp"; using Azure.ResourceManager; using Azure.Core; @@ -26,7 +27,12 @@ interface Workspaces { /** * Create or update a Workspace resource. */ - createOrUpdate is ArmResourceCreateOrReplaceAsync; + #suppress "@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes" "Conditional PUT explicitly returns HTTP 412 as required for failed preconditions; 200, 201 and default responses remain unchanged." + createOrUpdate is ArmResourceCreateOrReplaceAsync< + Workspace, + Parameters = PutPreconditionHeaders, + Error = PutPreconditionError + >; /** * Update a Workspace. From 9b64e3e78998cd9e3c81343384da045a8e21dab3 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 23 Oct 2026 13:21:57 -0700 Subject: [PATCH 2/3] Use standard ARM default errors for conditional Chaos PUTs Preserve HTTP 412 PreconditionFailed semantics and November-only headers while removing explicit error-status scaffolding and obsolete suppressions. Use default-key error examples and prove actual 412 validation with negative controls. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...figurations_CreateOrUpdate_CreateOnly.json | 2 +- ...figurations_CreateOrUpdate_UpdateOnly.json | 2 +- .../Scenarios_CreateOrUpdate_CreateOnly.json | 2 +- .../Scenarios_CreateOrUpdate_UpdateOnly.json | 2 +- .../Workspaces_CreateOrUpdate_CreateOnly.json | 2 +- .../Workspaces_CreateOrUpdate_UpdateOnly.json | 2 +- .../Chaos/put-preconditions.tsp | 29 +---- .../Microsoft.Chaos/Chaos/readme.md | 20 ++-- .../Microsoft.Chaos/Chaos/scenario.tsp | 4 +- .../Chaos/scenarioConfiguration.tsp | 4 +- ...figurations_CreateOrUpdate_CreateOnly.json | 2 +- ...figurations_CreateOrUpdate_UpdateOnly.json | 2 +- .../Scenarios_CreateOrUpdate_CreateOnly.json | 2 +- .../Scenarios_CreateOrUpdate_UpdateOnly.json | 2 +- .../Workspaces_CreateOrUpdate_CreateOnly.json | 2 +- .../Workspaces_CreateOrUpdate_UpdateOnly.json | 2 +- .../Chaos/stable/2026-11-01/openapi.json | 25 +---- .../Chaos/tests/put-preconditions.test.mjs | 103 +++++++++++++++--- .../Microsoft.Chaos/Chaos/workspace.tsp | 4 +- 19 files changed, 116 insertions(+), 97 deletions(-) diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json index 23d6f9c3b0f6..266de79f36b3 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json @@ -31,7 +31,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json index 3f27ae93216e..dc212413ef7e 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json @@ -27,7 +27,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_CreateOnly.json index b03b26bbe598..1933dd60393c 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_CreateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_CreateOnly.json @@ -61,7 +61,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_UpdateOnly.json index 53422b8f8595..bb6a7408421c 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_UpdateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Scenarios_CreateOrUpdate_UpdateOnly.json @@ -61,7 +61,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_CreateOnly.json index 8b78508b30ac..c55dac9bed65 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_CreateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_CreateOnly.json @@ -35,7 +35,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_UpdateOnly.json index ec713f86e3cd..21e6dd66b598 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_UpdateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/examples/2026-11-01/Workspaces_CreateOrUpdate_UpdateOnly.json @@ -31,7 +31,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/put-preconditions.tsp b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/put-preconditions.tsp index bba201d6d236..ffee6f2dc932 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/put-preconditions.tsp +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/put-preconditions.tsp @@ -1,8 +1,6 @@ -import "@azure-tools/typespec-azure-resource-manager"; import "@typespec/http"; import "@typespec/versioning"; -using Azure.ResourceManager; using TypeSpec.Http; using TypeSpec.Versioning; @@ -11,39 +9,16 @@ namespace Microsoft.Chaos; /** Optional conditions for creating or updating a workspace, scenario, or scenario configuration. */ model PutPreconditionHeaders { /** - * Use '*' to create the resource only if it does not exist. If the resource exists, the request fails with 412 Precondition Failed without changing it. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching. + * Use '*' to create the resource only if it does not exist. If the resource exists, the request fails with HTTP 412 and error code PreconditionFailed in the standard Azure error body. A failed condition starts no resource change or asynchronous operation. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching. */ @added(Versions.v2026_11_01) @header("If-None-Match") ifNoneMatch?: string; /** - * Use '*' to update the resource only if it exists. If the resource does not exist, the request fails with 412 Precondition Failed without creating it. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching. + * Use '*' to update the resource only if it exists. If the resource does not exist, the request fails with HTTP 412 and error code PreconditionFailed in the standard Azure error body. A failed condition starts no resource change or asynchronous operation. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching. */ @added(Versions.v2026_11_01) @header("If-Match") ifMatch?: string; } - -/** An unexpected error response. */ -#suppress "@azure-tools/typespec-client-generator-core/use-union-hierarchy" "HTTP error envelopes select status codes, not a client model inheritance hierarchy; both responses use the existing common Azure error body." -union PutPreconditionError { - ErrorResponse, - - /** An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started. */ - @added(Versions.v2026_11_01) - preconditionFailed: PreconditionFailedResult, -} - -/** An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started. */ -@added(Versions.v2026_11_01) -@error -model PreconditionFailedResult { - /** The HTTP status code. */ - @statusCode - statusCode: 412; - - /** The Azure error describing the failed precondition. */ - @body - body: Azure.ResourceManager.CommonTypes.ErrorResponse; -} diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md index a5fdc76c61b6..cba1acddd4dc 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md @@ -12,8 +12,14 @@ Only `Workspaces_CreateOrUpdate`, `Scenarios_CreateOrUpdate`, and `If-None-Match: *` is create-only: an existing resource returns 412. `If-Match: *` is update-only: a missing resource returns 412. A failed precondition returns the standard Azure error body before any resource -change or asynchronous operation starts. Successful requests retain the existing -200/201 responses and polling contract. Omitting both headers preserves +change or asynchronous operation starts, with error code `PreconditionFailed`. +The public ARM response map remains `200`, `201`, and `default`; actual HTTP 412 +failures use the existing `default` error schema, not a success response. +The conditional examples use `responses.default` for those HTTP 412 failures, +identified by their titles and `PreconditionFailed` error code. Regression tests +also validate their error bodies with actual status 412 through OAV. +Successful requests retain the existing 200/201 responses and polling contract. +Omitting both headers preserves unconditional create-or-update behavior. Evaluate applicable conditions in @@ -1339,6 +1345,7 @@ See configuration in [readme.csharp.md](./readme.csharp.md) ```yaml directive: + # Locked OAV matches literal path suffixes; both forms keep Windows and POSIX scoped to November. - suppress: RESPONSE_STATUS_CODE_NOT_IN_EXAMPLE from: - stable/2026-11-01/openapi.json @@ -1346,8 +1353,7 @@ directive: where: - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}"].put.responses["200"] - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}"].put.responses["201"] - - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}"].put.responses["412"] - reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200, update-only cannot return 201, and unconditional examples have no failed condition to return 412. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and 412; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example status completeness is suppressed, not request or response validation. + reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200 and update-only cannot return 201. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and actual HTTP 412 through the default error schema; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example success-status completeness is suppressed, not request or response validation. - suppress: RESPONSE_STATUS_CODE_NOT_IN_EXAMPLE from: - stable/2026-11-01/openapi.json @@ -1355,8 +1361,7 @@ directive: where: - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}"].put.responses["200"] - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}"].put.responses["201"] - - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}"].put.responses["412"] - reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200, update-only cannot return 201, and unconditional examples have no failed condition to return 412. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and 412; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example status completeness is suppressed, not request or response validation. + reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200 and update-only cannot return 201. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and actual HTTP 412 through the default error schema; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example success-status completeness is suppressed, not request or response validation. - suppress: RESPONSE_STATUS_CODE_NOT_IN_EXAMPLE from: - stable/2026-11-01/openapi.json @@ -1364,8 +1369,7 @@ directive: where: - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}/configurations/{scenarioConfigurationName}"].put.responses["200"] - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}/configurations/{scenarioConfigurationName}"].put.responses["201"] - - $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Chaos/workspaces/{workspaceName}/scenarios/{scenarioName}/configurations/{scenarioConfigurationName}"].put.responses["412"] - reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200, update-only cannot return 201, and unconditional examples have no failed condition to return 412. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and 412; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example status completeness is suppressed, not request or response validation. + reason: Each conditional example must show only outcomes possible for its request. Create-only cannot return 200 and update-only cannot return 201. The CreateOnly and UpdateOnly examples collectively cover 200, 201 and actual HTTP 412 through the default error schema; put-preconditions.test.mjs verifies their wiring and coverage. Only per-example success-status completeness is suppressed, not request or response validation. - suppress: TopLevelResourcesListBySubscription where: $.definitions.target from: targets.json diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenario.tsp b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenario.tsp index f394ede4b832..08f5692efc1c 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenario.tsp +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenario.tsp @@ -21,11 +21,9 @@ interface Scenarios { /** * Create or update a scenario. */ - #suppress "@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes" "Conditional PUT explicitly returns HTTP 412 as required for failed preconditions; 200, 201 and default responses remain unchanged." createOrUpdate is ArmResourceCreateOrReplaceSync< Scenario, - Parameters = PutPreconditionHeaders, - Error = PutPreconditionError + Parameters = PutPreconditionHeaders >; /** diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenarioConfiguration.tsp b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenarioConfiguration.tsp index 713cc9647732..35039a50c690 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenarioConfiguration.tsp +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/scenarioConfiguration.tsp @@ -31,11 +31,9 @@ interface ScenarioConfigurations { /** * Create or update a scenario definition. */ - #suppress "@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes" "Conditional PUT explicitly returns HTTP 412 as required for failed preconditions; 200, 201 and default responses remain unchanged." createOrUpdate is ArmResourceCreateOrReplaceAsync< ScenarioConfiguration, - Parameters = PutPreconditionHeaders, - Error = PutPreconditionError + Parameters = PutPreconditionHeaders >; /** diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json index 23d6f9c3b0f6..266de79f36b3 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_CreateOnly.json @@ -31,7 +31,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json index 3f27ae93216e..dc212413ef7e 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/ScenarioConfigurations_CreateOrUpdate_UpdateOnly.json @@ -27,7 +27,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_CreateOnly.json index b03b26bbe598..1933dd60393c 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_CreateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_CreateOnly.json @@ -61,7 +61,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_UpdateOnly.json index 53422b8f8595..bb6a7408421c 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_UpdateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Scenarios_CreateOrUpdate_UpdateOnly.json @@ -61,7 +61,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_CreateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_CreateOnly.json index 8b78508b30ac..c55dac9bed65 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_CreateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_CreateOnly.json @@ -35,7 +35,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_UpdateOnly.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_UpdateOnly.json index ec713f86e3cd..21e6dd66b598 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_UpdateOnly.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/examples/Workspaces_CreateOrUpdate_UpdateOnly.json @@ -31,7 +31,7 @@ } } }, - "412": { + "default": { "body": { "error": { "code": "PreconditionFailed", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/openapi.json b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/openapi.json index d123fda13db2..1970ed1597bc 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/openapi.json +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/stable/2026-11-01/openapi.json @@ -2294,13 +2294,6 @@ } } }, - "412": { - "description": "An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started.", - "schema": { - "$ref": "../../../../../../common-types/resource-management/v6/types.json#/definitions/ErrorResponse" - }, - "x-ms-error-response": true - }, "default": { "description": "An unexpected error response.", "schema": { @@ -3387,13 +3380,6 @@ "$ref": "#/definitions/Scenario" } }, - "412": { - "description": "An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started.", - "schema": { - "$ref": "../../../../../../common-types/resource-management/v6/types.json#/definitions/ErrorResponse" - }, - "x-ms-error-response": true - }, "default": { "description": "An unexpected error response.", "schema": { @@ -3683,13 +3669,6 @@ } } }, - "412": { - "description": "An effective request precondition failed or could not be verified. No resource change or asynchronous operation is started.", - "schema": { - "$ref": "../../../../../../common-types/resource-management/v6/types.json#/definitions/ErrorResponse" - }, - "x-ms-error-response": true - }, "default": { "description": "An unexpected error response.", "schema": { @@ -9064,7 +9043,7 @@ "PutPreconditionHeaders.ifMatch": { "name": "If-Match", "in": "header", - "description": "Use '*' to update the resource only if it exists. If the resource does not exist, the request fails with 412 Precondition Failed without creating it. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching.", + "description": "Use '*' to update the resource only if it exists. If the resource does not exist, the request fails with HTTP 412 and error code PreconditionFailed in the standard Azure error body. A failed condition starts no resource change or asynchronous operation. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching.", "required": false, "type": "string", "x-ms-parameter-location": "method", @@ -9073,7 +9052,7 @@ "PutPreconditionHeaders.ifNoneMatch": { "name": "If-None-Match", "in": "header", - "description": "Use '*' to create the resource only if it does not exist. If the resource exists, the request fails with 412 Precondition Failed without changing it. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching.", + "description": "Use '*' to create the resource only if it does not exist. If the resource exists, the request fails with HTTP 412 and error code PreconditionFailed in the standard Azure error body. A failed condition starts no resource change or asynchronous operation. Omit both condition headers for an unconditional create or update. Other effective conditions must be honored if supported or rejected with 412 if they cannot be verified; they must not be silently ignored. This contract does not introduce entity-tag matching.", "required": false, "type": "string", "x-ms-parameter-location": "method", diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/tests/put-preconditions.test.mjs b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/tests/put-preconditions.test.mjs index 482824a5f360..5feda6261fdc 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/tests/put-preconditions.test.mjs +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/tests/put-preconditions.test.mjs @@ -4,8 +4,13 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; +import { LiveValidator } from "oav/dist/lib/liveValidation/liveValidator.js"; const root = fileURLToPath(new URL("../", import.meta.url)); +const repo = execFileSync("git", ["rev-parse", "--show-toplevel"], { + cwd: root, + encoding: "utf8", +}).trim(); const readJson = (name) => JSON.parse(readFileSync(path.join(root, name), "utf8")); const spec = readJson("stable/2026-11-01/openapi.json"); @@ -28,7 +33,7 @@ const scopedOperations = operations.filter(([, operation]) => operationIds.includes(operation.operationId), ); -test("only the three selected PUT operations expose optional wildcard conditions and 412", () => { +test("only the three selected PUT operations expose optional wildcard conditions with standard ARM errors", () => { assert.equal(scopedOperations.length, 3); for (const [method, operation] of operations) { const headers = parameters(operation).filter((parameter) => @@ -56,19 +61,30 @@ test("only the three selected PUT operations expose optional wildcard conditions assert.equal(header.enum, undefined); assert.match(header.description, /Use '\*'/); assert.match(header.description, /must not be silently ignored/); + assert.match( + header.description, + /HTTP 412 and error code PreconditionFailed/, + ); + assert.match( + header.description, + /A failed condition starts no resource change or asynchronous operation/, + ); assert.match( header.description, /does not introduce entity-tag matching/, ); } - const failure = operation.responses["412"]; - assert.equal(failure["x-ms-error-response"], true); - assert.deepEqual(failure.schema, operation.responses.default.schema); + assert.deepEqual(Object.keys(operation.responses), [ + "200", + "201", + "default", + ]); + const failure = operation.responses.default; + assert.deepEqual(failure.schema, { + $ref: "../../../../../../common-types/resource-management/v6/types.json#/definitions/ErrorResponse", + }); assert.equal(failure.headers, undefined); - assert.match( - failure.description, - /No resource change or asynchronous operation is started/, - ); + assert.equal(failure.description, "An unexpected error response."); } }); @@ -87,13 +103,16 @@ test("create-only and update-only examples are wired, cover success and 412, and source.parameters[headerNames.find((name) => name !== header)], undefined, ); - assert.deepEqual(Object.keys(source.responses), [status, "412"]); + assert.deepEqual(Object.keys(source.responses), [status, "default"]); + assert.match(source.title, /return 412/); assert.equal( - source.responses["412"].body.error.code, + source.responses.default.body.error.code, "PreconditionFailed", ); - assert.equal(source.responses["412"].body.error.target, header); - assert.equal(source.responses["412"].headers, undefined); + assert.equal(source.responses.default.body.error.target, header); + assert.equal(source.responses.default.headers, undefined); + assert.equal(operation.responses["412"], undefined); + assert.ok(operation.responses.default.schema, "412 is modeled by default"); assert.ok( Object.values(operation["x-ms-examples"]).some( (example) => example.$ref === `./examples/${name}`, @@ -125,14 +144,63 @@ test("create-only and update-only examples are wired, cover success and 412, and } }); +test("OAV validates actual HTTP 412 examples against the default error schema", async () => { + const validator = new LiveValidator({ + swaggerPaths: [path.join(root, "stable/2026-11-01/openapi.json")], + directory: repo, + git: { shouldClone: false }, + loadValidatorInBackground: false, + loadValidatorInInitialize: true, + isArmCall: true, + }); + await validator.initialize(); + let validated = 0; + for (const [route, methods] of Object.entries(spec.paths)) { + const operation = methods.put; + if (!operationIds.includes(operation?.operationId)) continue; + for (const suffix of ["CreateOnly", "UpdateOnly"]) { + const example = readJson( + `examples/2026-11-01/${operation.operationId}_${suffix}.json`, + ); + const url = + "https://management.azure.com" + + route.replace(/\{([^}]+)\}/g, (_, name) => + encodeURIComponent(example.parameters[name]), + ) + + "?api-version=2026-11-01"; + const response = { + statusCode: "412", + headers: { "content-type": "application/json" }, + body: example.responses.default.body, + }; + const result = await validator.validateLiveResponse(response, { + url, + method: "PUT", + }); + assert.equal(result.runtimeException, undefined); + assert.equal(result.operationInfo.operationId, operation.operationId); + assert.equal(result.isSuccessful, true, JSON.stringify(result.errors)); + assert.deepEqual(result.errors, []); + + const invalid = structuredClone(response); + invalid.body.error.code = 412; + const rejected = await validator.validateLiveResponse(invalid, { + url, + method: "PUT", + }); + assert.equal(rejected.runtimeException, undefined); + assert.equal(rejected.isSuccessful, false); + assert.ok(rejected.errors.length > 0, "invalid error body must be rejected"); + validated++; + } + } + assert.equal(validated, 6); +}); + test( - "the GA contract differs from the baseline only by the authorized headers, 412 and example links", + "the GA contract differs from the baseline only by the authorized headers and example links", { skip: !process.env.CHAOS_PRECONDITION_BASE_REF }, () => { - const repo = execFileSync("git", ["rev-parse", "--show-toplevel"], { - cwd: root, - encoding: "utf8", - }).trim(); const relative = path.relative(repo, root).split(path.sep).join("/"); const baseline = JSON.parse( execFileSync( @@ -152,7 +220,6 @@ test( (parameter) => !parameter.$ref?.startsWith("#/parameters/PutPreconditionHeaders."), ); - delete operation.responses["412"]; for (const [title, example] of Object.entries( operation["x-ms-examples"], )) { diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/workspace.tsp b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/workspace.tsp index 4079d016823c..6479eaf324b7 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/workspace.tsp +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/workspace.tsp @@ -27,11 +27,9 @@ interface Workspaces { /** * Create or update a Workspace resource. */ - #suppress "@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes" "Conditional PUT explicitly returns HTTP 412 as required for failed preconditions; 200, 201 and default responses remain unchanged." createOrUpdate is ArmResourceCreateOrReplaceAsync< Workspace, - Parameters = PutPreconditionHeaders, - Error = PutPreconditionError + Parameters = PutPreconditionHeaders >; /** From 4257caa567b4e015a8a34f9eb212852cfd16fb33 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 23 Oct 2026 15:13:57 -0700 Subject: [PATCH 3/3] Clarify November conditional PUT contract wording Make the contract overview declarative without asserting runtime rollout or API approval. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../resource-manager/Microsoft.Chaos/Chaos/readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md index cba1acddd4dc..dc9edda222ca 100644 --- a/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md +++ b/specification/chaos/resource-manager/Microsoft.Chaos/Chaos/readme.md @@ -4,11 +4,11 @@ This is the AutoRest configuration file for chaos. -## Conditional PUT proposal for 2026-11-01 +## Conditional PUT in 2026-11-01 Only `Workspaces_CreateOrUpdate`, `Scenarios_CreateOrUpdate`, and -`ScenarioConfigurations_CreateOrUpdate` add optional `If-None-Match` and -`If-Match` request headers in the upcoming 2026-11-01 contract. +`ScenarioConfigurations_CreateOrUpdate` support optional `If-None-Match` and +`If-Match` request headers in the 2026-11-01 contract. `If-None-Match: *` is create-only: an existing resource returns 412. `If-Match: *` is update-only: a missing resource returns 412. A failed precondition returns the standard Azure error body before any resource @@ -29,10 +29,10 @@ request fails with 412. Preserve already supported validators; other effective conditions must be honored or rejected with 412 when they cannot be verified, never silently ignored. The RFC-required exceptions still apply: `If-Unmodified-Since` is ignored when `If-Match` is present, its date is invalid, -or no modification date is available. This proposal adds no date-validator +or no modification date is available. These conditional headers add no date-validator parameter, entity-tag matching platform, response ETag, or `etag` body property. -This is a spec-only contract proposal, not evidence of runtime implementation +This section specifies the API contract; it does not establish runtime rollout or API approval. It does not change retained API versions, Connections, V1 resources, DELETE, SDKs, Portal/CLI defaults, or resource ownership. The design follows the