From 9625ff1c8b0c5328937f149ce03c20ca27d1429e Mon Sep 17 00:00:00 2001 From: tvaron3 Date: Fri, 16 Oct 2026 12:47:24 -0400 Subject: [PATCH 1/3] Avoid redundant provisioning in fixed-account Cosmos test stages Disable per-run ARM provisioning for six thin-client and GSI stages while preserving fixed credentials, authentication, test profiles, and cleanup. Add offline pipeline regression coverage and document the account contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ead4fdd6-1296-47c8-bd5c-1cdfe90128ee --- sdk/cosmos/pipeline/README.md | 33 +++- .../pipeline/test_fixed_account_pipeline.py | 144 ++++++++++++++++++ sdk/cosmos/tests.yml | 12 ++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 sdk/cosmos/pipeline/test_fixed_account_pipeline.py diff --git a/sdk/cosmos/pipeline/README.md b/sdk/cosmos/pipeline/README.md index 7a38ce949a218..eb96bd734d3d7 100644 --- a/sdk/cosmos/pipeline/README.md +++ b/sdk/cosmos/pipeline/README.md @@ -79,11 +79,34 @@ The main `Cosmos_live_test` stage spans many consistency/topology configs, so ea - `Cosmos_live_test` (main) — per-leg selector. - `Cosmos_Live_Test_Http2` — `multimaster-session-http2`. -NOT yet wired (need follow-up): the thin-client stages and GSI (their accounts need +NOT yet wired to the JSON selector (need follow-up): the thin-client stages and GSI (their accounts need thin-client / GSI-preview enablement that plain account creation doesn't do), Kafka (entangled with AAD — belongs in the separate AAD pipeline), and Spring (uses its own test-resources with `AZURE_SPRING_TENANT_ID`). +### Fixed thin-client and GSI accounts + +These stages do not use the JSON selector. They pass fixed credentials to Maven as +`ACCOUNT_HOST`/`ACCOUNT_KEY` and to the post-job database cleanup: + +| Stage (`Cosmos_Live_Test_` prefix) | Endpoint variable | Key variable | +| --- | --- | --- | +| `Http2NetworkFault`, `ThinClient`, `ThinClient_EndpointProbe` | `thinclient-test-endpoint` | `thinclient-test-key` | +| `ThinClient_MultiRegion` | `thin-client-canary-multi-region-session-endpoint` | `thin-client-canary-multi-region-session-key` | +| `ThinClient_MultiMaster` | `thin-client-canary-multi-writer-session-endpoint` | `thin-client-canary-multi-writer-session-key` | +| `GSI` | `gsi-pipeline-uri` | `gsi-pipeline-key` | + +Their selected profiles use these accounts for setup and tests; they do not require +ARM-generated `NEW_ACCOUNT_*` or secondary-key outputs. This includes the query and +consistency-override legs of `ThinClient` and both transport legs of `Http2NetworkFault`. + +All six stages set `DisableAzureResourceCreation: true` to skip per-run ARM resource +configuration, deployment, and removal. Maven feed authentication and the federated +test runner remain enabled. The legacy `ArmTemplateParameters` in their matrices are +unused while provisioning is disabled; consistency and other test inputs still apply. +The endpoint-probe stage still leaves `COSMOS.THINCLIENT_ENABLED` unset, and the +network-fault stage retains its iptables setup and per-profile transport selection. + ### Secret upkeep Every account in the secret carries a `secondaryKey` — `AzureKeyCredentialTest` (in the @@ -103,3 +126,11 @@ regenerated JSON from `New-CosmosLiveTestAccounts.ps1`. ```bash bash sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh ``` + +Check all six stages' provisioning opt-outs, fixed test/cleanup credentials, +profile selection, transport settings, and authentication configuration offline (Python 3 with PyYAML, +also used by `eng/automation/requirement.txt`): + +```bash +python3 sdk/cosmos/pipeline/test_fixed_account_pipeline.py +``` diff --git a/sdk/cosmos/pipeline/test_fixed_account_pipeline.py b/sdk/cosmos/pipeline/test_fixed_account_pipeline.py new file mode 100644 index 0000000000000..6d698287503cc --- /dev/null +++ b/sdk/cosmos/pipeline/test_fixed_account_pipeline.py @@ -0,0 +1,144 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# cspell:ignore DACCOUNT + +"""Offline configuration checks for fixed-account thin-client and GSI stages.""" + +import json +from pathlib import Path +import unittest + +import yaml + + +COSMOS = Path(__file__).resolve().parents[1] +REPO = COSMOS.parents[1] +TEMPLATE = "/eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml" +# Stage suffix -> endpoint variable, key variable, matrix filename, selected profiles. +STAGES = { + "Http2NetworkFault": ( + "thinclient-test-endpoint", "thinclient-test-key", + "live-http2-network-fault-platform-matrix.json", + ["manual-http-network-fault", "manual-http-network-fault-compute"], + ), + "ThinClient": ( + "thinclient-test-endpoint", "thinclient-test-key", + "live-thinclient-platform-matrix.json", + ["thinclient", "query", "consistency-overrides"], + ), + "ThinClient_EndpointProbe": ( + "thinclient-test-endpoint", "thinclient-test-key", + "live-thinclient-endpoint-probe-platform-matrix.json", + ["thinclient-endpoint-probe"], + ), + "ThinClient_MultiRegion": ( + "thin-client-canary-multi-region-session-endpoint", "thin-client-canary-multi-region-session-key", + "live-thinclient-multi-region-platform-matrix.json", + ["fi-thinclient-multi-region"], + ), + "ThinClient_MultiMaster": ( + "thin-client-canary-multi-writer-session-endpoint", "thin-client-canary-multi-writer-session-key", + "live-thinclient-multi-master-platform-matrix.json", + ["fi-thinclient-multi-master"], + ), + "GSI": ( + "gsi-pipeline-uri", "gsi-pipeline-key", + "live-gsi-platform-matrix.json", ["gsi"], + ), +} + + +class FixedAccountPipelineTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + pipeline = yaml.safe_load((COSMOS / "tests.yml").read_text(encoding="utf-8")) + cls.stages = {} + for suffix in STAGES: + name = f"Cosmos_Live_Test_{suffix}" + stages = [ + stage for stage in pipeline["extends"]["parameters"]["stages"] + if stage["parameters"].get("TestName") == name + ] + if len(stages) != 1: + raise AssertionError(f"Expected exactly one {name} stage, found {len(stages)}") + cls.stages[suffix] = stages[0] + + def test_fixed_account_disables_arm_provisioning(self): + for name, stage in self.stages.items(): + with self.subTest(stage=name): + self.assertEqual(stage["template"], TEMPLATE) + self.assertIs(stage["parameters"].get("DisableAzureResourceCreation"), True) + + def test_test_runner_keeps_fixed_credentials_and_profiles(self): + for name, (host, key, matrix_file, profiles) in STAGES.items(): + with self.subTest(stage=name): + parameters = self.stages[name]["parameters"] + args = self.additional_args(name) + self.assertIn(f"-DACCOUNT_HOST=$({host})", args) + self.assertIn(f"-DACCOUNT_KEY=$({key})", args) + self.assertEqual(parameters["TestGoals"], "verify") + self.assertIn("$(ProfileFlag)", parameters["TestOptions"]) + self.assertIn("$(AdditionalArgs)", parameters["TestOptions"]) + + self.assertEqual(parameters["MatrixConfigs"][0]["Path"], f"sdk/cosmos/{matrix_file}") + matrix = json.loads((COSMOS / matrix_file).read_text(encoding="utf-8")) + self.assertTrue(matrix["include"]) + actual_profiles = [] + for leg in matrix["include"]: + actual_profiles.extend(leg["ProfileFlag"]) + self.assertEqual(leg["ACCOUNT_CONSISTENCY"], "Session") + self.assertCountEqual(actual_profiles, [f"-P{profile}" for profile in profiles]) + + def test_cleanup_keeps_fixed_credentials(self): + for name, (host, key, _, _) in STAGES.items(): + with self.subTest(stage=name): + cleanup = [ + step for step in self.stages[name]["parameters"]["PostSteps"] + if step.get("template") == "/sdk/cosmos/cleanup-test-resources.yml" + ] + self.assertEqual(len(cleanup), 1) + self.assertEqual(cleanup[0]["parameters"]["AccountHost"], f"$({host})") + self.assertEqual(cleanup[0]["parameters"]["AccountKey"], f"$({key})") + + def test_service_connection_and_federated_auth_are_preserved(self): + template = yaml.safe_load((REPO / TEMPLATE.lstrip("/")).read_text(encoding="utf-8")) + defaults = {parameter["name"]: parameter["default"] for parameter in template["parameters"]} + for name, stage in self.stages.items(): + with self.subTest(stage=name): + parameters = stage["parameters"] + self.assertEqual( + parameters["CloudConfig"]["Public"]["ServiceConnection"], + "azure-sdk-tests-cosmos", + ) + self.assertIs(parameters.get("UseFederatedAuth", defaults["UseFederatedAuth"]), True) + + def test_transport_selection_is_preserved(self): + for name in STAGES: + with self.subTest(stage=name): + args = self.additional_args(name) + thinclient_flags = [arg for arg in args if arg.startswith("-DCOSMOS.THINCLIENT_ENABLED=")] + http2_flags = [arg for arg in args if arg.startswith("-DCOSMOS.HTTP2_ENABLED=")] + if name in ("Http2NetworkFault", "GSI"): + self.assertEqual(thinclient_flags, []) + self.assertEqual(http2_flags, []) + elif name == "ThinClient_EndpointProbe": + self.assertEqual(thinclient_flags, []) + self.assertEqual(http2_flags, ["-DCOSMOS.HTTP2_ENABLED=true"]) + else: + self.assertEqual(thinclient_flags, ["-DCOSMOS.THINCLIENT_ENABLED=true"]) + self.assertEqual(http2_flags, ["-DCOSMOS.HTTP2_ENABLED=true"]) + + def test_network_fault_setup_is_preserved(self): + steps = self.stages["Http2NetworkFault"]["parameters"]["PreSteps"] + self.assertTrue(any("apt-get install -y -qq iptables" in step.get("script", "") for step in steps)) + + def additional_args(self, name): + variables = { + variable["name"]: variable["value"] + for variable in self.stages[name]["parameters"]["AdditionalVariables"] + } + return variables["AdditionalArgs"].split() + + +if __name__ == "__main__": + unittest.main() diff --git a/sdk/cosmos/tests.yml b/sdk/cosmos/tests.yml index 9c18b5a8de249..37fe67686346d 100644 --- a/sdk/cosmos/tests.yml +++ b/sdk/cosmos/tests.yml @@ -99,6 +99,8 @@ extends: - template: /eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml parameters: TestName: 'Cosmos_Live_Test_Http2NetworkFault' + # Tests and cleanup use a fixed account, not ARM-created accounts. + DisableAzureResourceCreation: true # Long lived shared account: nothing tears it down at the end of the run, so leaked # databases stay forever. Delete this job's databases even if the job failed. PostSteps: @@ -144,6 +146,8 @@ extends: - template: /eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml parameters: TestName: 'Cosmos_Live_Test_ThinClient' + # Tests and cleanup use a fixed account, not ARM-created accounts. + DisableAzureResourceCreation: true # Long lived shared account: nothing tears it down at the end of the run, so leaked # databases stay forever. Delete this job's databases even if the job failed. PostSteps: @@ -183,6 +187,8 @@ extends: - template: /eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml parameters: TestName: 'Cosmos_Live_Test_ThinClient_EndpointProbe' + # Tests and cleanup use a fixed account, not ARM-created accounts. + DisableAzureResourceCreation: true # Long lived shared account: nothing tears it down at the end of the run, so leaked # databases stay forever. Delete this job's databases even if the job failed. PostSteps: @@ -226,6 +232,8 @@ extends: - template: /eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml parameters: TestName: 'Cosmos_Live_Test_ThinClient_MultiRegion' + # Tests and cleanup use a fixed account, not ARM-created accounts. + DisableAzureResourceCreation: true # Long lived shared account: nothing tears it down at the end of the run, so leaked # databases stay forever. Delete this job's databases even if the job failed. PostSteps: @@ -265,6 +273,8 @@ extends: - template: /eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml parameters: TestName: 'Cosmos_Live_Test_ThinClient_MultiMaster' + # Tests and cleanup use the fixed thin-client canary account, not ARM-created accounts. + DisableAzureResourceCreation: true # Long lived shared account: nothing tears it down at the end of the run, so leaked # databases stay forever. Delete this job's databases even if the job failed. PostSteps: @@ -304,6 +314,8 @@ extends: - template: /eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml parameters: TestName: 'Cosmos_Live_Test_GSI' + # Tests and cleanup use a fixed account, not ARM-created accounts. + DisableAzureResourceCreation: true # Long lived shared account: nothing tears it down at the end of the run, so leaked # databases stay forever. Delete this job's databases even if the job failed. PostSteps: From dbe5f9ede6245288355d2ca2f9b7f9ae3e1fff40 Mon Sep 17 00:00:00 2001 From: tvaron3 Date: Fri, 16 Oct 2026 20:04:05 -0400 Subject: [PATCH 2/3] Remove local-only Cosmos pipeline tooling and retain CI resolvers Remove manual provisioning assets and offline-only test scripts, schema, and sample. Preserve the CI credential-resolution template and script, spelling configuration, and documented secret contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ead4fdd6-1296-47c8-bd5c-1cdfe90128ee --- sdk/cosmos/pipeline/README.md | 62 ++-- .../New-CosmosLiveTestAccounts.ps1 | 324 ------------------ .../pipeline/account-provisioning/README.md | 136 -------- .../cosmos-live-test-accounts.definition.json | 245 ------------- .../pipeline/live-test-accounts.sample.json | 216 ------------ .../pipeline/live-test-accounts.schema.json | 68 ---- .../pipeline/resolve-cosmos-test-account.sh | 2 +- .../resolve-cosmos-test-account.tests.sh | 61 ---- .../pipeline/test_fixed_account_pipeline.py | 144 -------- 9 files changed, 35 insertions(+), 1223 deletions(-) delete mode 100644 sdk/cosmos/pipeline/account-provisioning/New-CosmosLiveTestAccounts.ps1 delete mode 100644 sdk/cosmos/pipeline/account-provisioning/README.md delete mode 100644 sdk/cosmos/pipeline/account-provisioning/cosmos-live-test-accounts.definition.json delete mode 100644 sdk/cosmos/pipeline/live-test-accounts.sample.json delete mode 100644 sdk/cosmos/pipeline/live-test-accounts.schema.json delete mode 100755 sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh delete mode 100644 sdk/cosmos/pipeline/test_fixed_account_pipeline.py diff --git a/sdk/cosmos/pipeline/README.md b/sdk/cosmos/pipeline/README.md index eb96bd734d3d7..7045a130d76a8 100644 --- a/sdk/cosmos/pipeline/README.md +++ b/sdk/cosmos/pipeline/README.md @@ -1,23 +1,21 @@ # Cosmos live-test fixed accounts -This directory holds the **single-secret JSON configuration** used to point the Cosmos -Java live tests at fixed, self-owned Cosmos DB accounts (no central EngSys -provisioner). It is the Track A mechanism from the retargeting plan. +This directory holds the CI runtime resolver used to point the Cosmos Java live tests +at fixed, self-owned Cosmos DB accounts. Account provisioning and local-only tooling +are not part of this directory. ## Files | File | Purpose | | --- | --- | -| `live-test-accounts.schema.json` | JSON schema (draft-07) for the single secret. | -| `live-test-accounts.sample.json` | Example with all logical accounts (placeholder values). | | `resolve-cosmos-test-account.sh` | Pre-step parser: reads the JSON secret + a selector, exports `ACCOUNT_HOST`/`ACCOUNT_KEY`. | -| `resolve-cosmos-test-account.tests.sh` | Local tests for the parser (no ADO required). | | `resolve-test-account-steps.yml` | Reusable `PreTestRunSteps` template that runs the parser for a selector. | +| `cspell.json` | Spelling configuration for this directory's CI files and documentation. | ## How it works 1. A **single ADO variable-group secret** (`sub-config-cosmos-azure-cloud-test-resources`) - holds a JSON document (see schema) mapping a **logical account name** to its + holds a version 1 JSON document mapping a **logical account name** to its `{ endpoint, key, secondaryKey, ... }`. 2. Each pipeline stage passes a **selector** (the logical name it needs). 3. A **pre-step** runs `resolve-cosmos-test-account.sh`, which validates the JSON, @@ -25,14 +23,35 @@ provisioner). It is the Track A mechanism from the retargeting plan. `SECONDARY_ACCOUNT_KEY` when present) for the tests to read via `TestConfigurations`. It does NOT set `ACCOUNT_CONSISTENCY`/`PREFERRED_LOCATIONS` — those are matrix-controlled per leg. -4. The account provisioning script (in [`account-provisioning/`](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/cosmos/pipeline/account-provisioning)) - regenerates the accounts + JSON on every ephemeral-tenant rotation - (`New-CosmosLiveTestAccounts.ps1` creates the accounts and outputs the JSON). The - secret is then updated manually with that JSON, so pipelines pick up refreshed - endpoints/keys automatically. +4. Account provisioning is managed separately. After account or tenant rotation, update + the secret with the current endpoints and keys so pipelines pick up refreshed + credentials automatically. All Cosmos live-test matrix legs run on **linux** agents, so the parser is bash + jq. +## Secret format + +The resolver requires `version: 1`, an `accounts` object containing the selected +logical name, a nonempty HTTPS `endpoint`, and a nonempty `key`. Supply `secondaryKey` +for profiles that exercise key rotation. Other account metadata is ignored by the +resolver; consistency and preferred locations remain matrix-controlled. + +```json +{ + "version": 1, + "accounts": { + "single-session": { + "endpoint": "https://REPLACE.documents.azure.com:443/", + "key": "REPLACE_PRIMARY_KEY", + "secondaryKey": "REPLACE_SECONDARY_KEY" + } + } +} +``` + +This example contains placeholders only. Store real account keys in the secret, not +in source files or logs. + ## Selectors (from the test matrices) `single-session`, `single-session-pmerge`, `single-strong`, `single-session-split`, @@ -118,19 +137,6 @@ would fall back to the emulator key and fail against a live account. The JSON lives in the ADO variable-group secret `sub-config-cosmos-azure-cloud-test-resources` (Cosmos "user administered" variable group). The resolve steps template reads it by default. Refresh it manually whenever the -accounts are re-provisioned (e.g. after an ephemeral-tenant rotation) by pasting the -regenerated JSON from `New-CosmosLiveTestAccounts.ps1`. - -## Local test - -```bash -bash sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh -``` - -Check all six stages' provisioning opt-outs, fixed test/cleanup credentials, -profile selection, transport settings, and authentication configuration offline (Python 3 with PyYAML, -also used by `eng/automation/requirement.txt`): - -```bash -python3 sdk/cosmos/pipeline/test_fixed_account_pipeline.py -``` +accounts are re-provisioned (e.g. after an ephemeral-tenant rotation), using the format +above and including every selector used by the test matrices. Do not print or commit +the secret's contents. diff --git a/sdk/cosmos/pipeline/account-provisioning/New-CosmosLiveTestAccounts.ps1 b/sdk/cosmos/pipeline/account-provisioning/New-CosmosLiveTestAccounts.ps1 deleted file mode 100644 index ecd8035e89a10..0000000000000 --- a/sdk/cosmos/pipeline/account-provisioning/New-CosmosLiveTestAccounts.ps1 +++ /dev/null @@ -1,324 +0,0 @@ -<# -.SYNOPSIS - (Re)creates the fixed Cosmos DB accounts used by the azure-sdk-for-java Cosmos live - tests and outputs the sub-config-cosmos-azure-cloud-test-resources JSON (endpoints + keys). - -.DESCRIPTION - The Java Cosmos live tests run against fixed, self-owned accounts (Track A of the - live-test retargeting). Because the ephemeral tenant is deleted/recreated roughly - every 90 days, this script is re-run after each rotation to: - 1. Ensure the resource group (default: sdk-ci) exists (created if missing). - 2. Create (idempotently) one Cosmos account per entry in the definition file, - with the requested consistency / multi-write / multi-region / thin-client / - partition-merge configuration. - 3. Read each account's endpoint + primary (and optional secondary) key. - 4. Assemble the versioned account JSON and emit it (to stdout, - and to -OutputPath if provided). - - This script does NOT touch Key Vault. Update the - sub-config-cosmos-azure-cloud-test-resources secret / ADO variable manually with the - JSON it outputs. - - Uses the Az PowerShell modules (Az.Accounts, Az.Resources, Az.CosmosDB). - -.PARAMETER SubscriptionId - Subscription hosting the resource group and the Cosmos accounts. - -.PARAMETER ResourceGroupName - Resource group for the accounts. Created if it does not exist. Defaults to 'sdk-ci'. - -.PARAMETER Location - Optional override for the primary/write region. When omitted, the primary region comes - from the definition's regionDefaults (single source of truth; matches test-resources.json). - -.PARAMETER SecondaryLocation - Optional override for the secondary region of multi-region accounts. When omitted, it - comes from the definition's regionDefaults.multiRegion. - -.PARAMETER DefinitionPath - Path to the account definition JSON. Defaults to the file next to this script. - -.PARAMETER AccountNamePrefix - Prefix for the globally-unique Cosmos account names. Defaults to 'sdkci'. - -.PARAMETER OutputPath - Optional path to write the assembled JSON to. The JSON is always also written to - stdout. NOTE: the JSON contains account keys - treat any file you write as a secret. - -.EXAMPLE - # Create/refresh accounts and write the JSON to a file, then update the secret manually - ./New-CosmosLiveTestAccounts.ps1 -SubscriptionId -OutputPath ./accounts.json - -.EXAMPLE - # Dry run - creates nothing, prints the assembled JSON with keys stubbed - ./New-CosmosLiveTestAccounts.ps1 -SubscriptionId -WhatIf - -.NOTES - Requires: PowerShell 7+, Az modules, and Contributor on the subscription. - Idempotent: safe to re-run. -#> -[CmdletBinding(SupportsShouldProcess = $true)] -param( - [Parameter(Mandatory = $true)] - [string] $SubscriptionId, - - [string] $ResourceGroupName = 'sdk-ci', - - [string] $Location, - - [string] $SecondaryLocation, - - [string] $DefinitionPath = (Join-Path $PSScriptRoot 'cosmos-live-test-accounts.definition.json'), - - [ValidatePattern('^[a-z0-9]{1,10}$')] - [string] $AccountNamePrefix = 'sdkci', - - [string] $OutputPath -) - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version Latest - -function Write-Info($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } - -# --- Prerequisites ----------------------------------------------------------- -foreach ($m in @('Az.Accounts', 'Az.Resources', 'Az.CosmosDB')) { - if (-not (Get-Module -ListAvailable -Name $m)) { - throw "Required module '$m' is not installed. Install with: Install-Module $m -Scope CurrentUser" - } -} - -if (-not (Test-Path $DefinitionPath)) { throw "Definition file not found: $DefinitionPath" } -$definition = Get-Content -Raw -Path $DefinitionPath | ConvertFrom-Json - -# Regions are defined once in the definition's regionDefaults so the provisioned topology -# matches sdk/cosmos/test-resources.json (the ARM template the old flow used). The optional -# -Location / -SecondaryLocation params override the primary/secondary region for ad-hoc runs. -if (-not ($definition.PSObject.Properties.Name -contains 'regionDefaults')) { - throw "Definition '$DefinitionPath' is missing 'regionDefaults' (singleRegion / multiRegion)." -} -$singleRegionList = @($definition.regionDefaults.singleRegion) -$multiRegionList = @($definition.regionDefaults.multiRegion) -if ($singleRegionList.Count -lt 1 -or $multiRegionList.Count -lt 2) { - throw "regionDefaults must provide singleRegion (>=1) and multiRegion (>=2) entries." -} -if ($Location) { - $singleRegionList[0] = $Location - $multiRegionList[0] = $Location -} -if ($SecondaryLocation) { - $multiRegionList[1] = $SecondaryLocation -} -$primaryRegion = $multiRegionList[0] - -Write-Info "Selecting subscription $SubscriptionId" -$null = Set-AzContext -Subscription $SubscriptionId - -# --- Resource group (create if missing) -------------------------------------- -if (-not (Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue)) { - if ($PSCmdlet.ShouldProcess($ResourceGroupName, 'Create resource group')) { - Write-Info "Creating resource group $ResourceGroupName in $primaryRegion" - $null = New-AzResourceGroup -Name $ResourceGroupName -Location $primaryRegion - } -} else { - Write-Info "Resource group $ResourceGroupName already exists" -} - -# --- Helper: build the -LocationObject list from a region list -------------- -# First region is the write region (failoverPriority 0); the rest are read regions. -function New-LocationObjects([string[]] $regionList) { - $locations = @() - for ($i = 0; $i -lt $regionList.Count; $i++) { - $locations += New-AzCosmosDBLocationObject -LocationName $regionList[$i] -FailoverPriority $i -IsZoneRedundant $false - } - return ,$locations -} - -# Adds any missing capabilities to an existing account via ARM PATCH. -# -# Capabilities are handled here, never via New-/Update-AzCosmosDBAccount: -# - Update-AzCosmosDBAccount cannot set them at all. -# - New-AzCosmosDBAccount -Capabilities is silently ignored by some Az.CosmosDB versions -# (observed on Az 12.2.0: accounts came up with no capabilities at all, which failed -# every vector-search test until the script was run a second time). -# Reconciling after the account exists makes the outcome independent of module behaviour, -# so a fresh tenant rotation is a single pass. -# -# Cosmos capabilities are additive and cannot be removed, so we only ever add, and always -# send the full merged list. -function Sync-AccountCapability { - # SupportsShouldProcess so -WhatIf propagates from the caller and this never PATCHes on a dry run. - [CmdletBinding(SupportsShouldProcess = $true)] - param( - [Parameter(Mandatory)] [string] $AccountName, - [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $DesiredCapabilities, - [Parameter(Mandatory)] [string] $ResourceGroupName, - [Parameter(Mandatory)] [string] $SubscriptionId, - [string] $Selector - ) - - if ($DesiredCapabilities.Count -eq 0) { return } - - $account = Get-AzCosmosDBAccount -ResourceGroupName $ResourceGroupName -Name $AccountName -ErrorAction SilentlyContinue - if (-not $account) { - # Only reachable under -WhatIf, where the account was never actually created. - return - } - - $existingCaps = @() - if ($account.Capabilities) { $existingCaps = @($account.Capabilities | ForEach-Object { $_.Name }) } - - $missingCaps = @($DesiredCapabilities | Where-Object { $existingCaps -notcontains $_ }) - if ($missingCaps.Count -eq 0) { - Write-Info "Cosmos account '$AccountName' (selector=$Selector); capabilities up to date" - return - } - - $mergedCaps = @($existingCaps + $missingCaps | Select-Object -Unique) - if (-not $PSCmdlet.ShouldProcess($AccountName, "Add capabilities [$($missingCaps -join ', ')]")) { return } - - Write-Info "Account '$AccountName' (selector=$Selector); adding missing capabilities: $($missingCaps -join ', ')" - $resourceId = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.DocumentDB/databaseAccounts/$AccountName" - $body = @{ - properties = @{ - capabilities = @($mergedCaps | ForEach-Object { @{ name = $_ } }) - } - } | ConvertTo-Json -Depth 6 - - $resp = Invoke-AzRestMethod -Method PATCH -Path "$($resourceId)?api-version=2024-11-15" -Payload $body - if ($resp.StatusCode -ge 300) { - throw "Failed to add capabilities to '$AccountName' (HTTP $($resp.StatusCode)): $($resp.Content)" - } - - # PATCH returns before the capability is durably applied; confirm it landed so a fresh - # provisioning run cannot silently produce accounts the tests then fail against. - $deadline = (Get-Date).AddMinutes(5) - while ((Get-Date) -lt $deadline) { - Start-Sleep -Seconds 10 - $check = Get-AzCosmosDBAccount -ResourceGroupName $ResourceGroupName -Name $AccountName -ErrorAction SilentlyContinue - $nowCaps = @() - if ($check -and $check.Capabilities) { $nowCaps = @($check.Capabilities | ForEach-Object { $_.Name }) } - $stillMissing = @($DesiredCapabilities | Where-Object { $nowCaps -notcontains $_ }) - if ($stillMissing.Count -eq 0) { return } - } - - throw "Capabilities [$($missingCaps -join ', ')] did not apply to '$AccountName' within 5 minutes" -} - -# --- Create / update each account, then collect endpoint + keys -------------- -$secret = [ordered]@{ - version = 1 - accounts = [ordered]@{} -} - -foreach ($acct in $definition.accounts) { - $selector = $acct.name - $accountName = ("{0}-{1}" -f $AccountNamePrefix, $selector).ToLower() - if ($accountName.Length -gt 44) { - throw "Generated account name '$accountName' exceeds 44 chars. Shorten AccountNamePrefix or the selector '$selector'." - } - - $multiRegion = [bool]$acct.enableMultipleRegions - $multiWrite = [bool]$acct.enableMultipleWriteLocations - # An account may pin its own regions when the defaults do not suit it - the GSI account, for - # example, must live in East US 2 because live-gsi-platform-matrix.json sets - # PREFERRED_LOCATIONS=["East US 2"] on a single-region account, and a preferred region the account - # does not have leaves the client with nothing to prefer. - $regionList = if ($acct.PSObject.Properties.Name -contains 'regions' -and $acct.regions) { - @($acct.regions) - } elseif ($multiRegion) { - $multiRegionList - } else { - $singleRegionList - } - $locations = New-LocationObjects $regionList - - $capabilities = @() - if ($acct.PSObject.Properties.Name -contains 'capabilities') { - # Any account-level capabilities (e.g. thin-client) come from the definition so we - # never ship a guessed capability name. Leave empty if none are specified. - $capabilities = @($acct.capabilities) - } - - $existing = Get-AzCosmosDBAccount -ResourceGroupName $ResourceGroupName -Name $accountName -ErrorAction SilentlyContinue - if (-not $existing) { - if ($PSCmdlet.ShouldProcess($accountName, "Create Cosmos account [$selector]")) { - Write-Info "Creating Cosmos account '$accountName' (selector=$selector, consistency=$($acct.defaultConsistencyLevel), multiWrite=$multiWrite, multiRegion=$multiRegion)" - $params = @{ - ResourceGroupName = $ResourceGroupName - Name = $accountName - LocationObject = $locations - DefaultConsistencyLevel = $acct.defaultConsistencyLevel - EnableMultipleWriteLocations = $multiWrite - ApiKind = 'GlobalDocumentDB' - } - if ($acct.defaultConsistencyLevel -eq 'BoundedStaleness') { - $params['MaxStalenessIntervalInSeconds'] = 5 - $params['MaxStalenessPrefix'] = 100 - } - if ($acct.PSObject.Properties.Name -contains 'enablePartitionMerge' -and $acct.enablePartitionMerge) { - $params['EnablePartitionMerge'] = $true - } - $null = New-AzCosmosDBAccount @params - } - } else { - Write-Info "Cosmos account '$accountName' already exists (selector=$selector)" - } - - # Reconcile capabilities on both paths. Deliberately not passed to New-AzCosmosDBAccount: - # some Az.CosmosDB versions ignore -Capabilities silently, which produced accounts with no - # capabilities at all and needed a second run of this script to fix. - Sync-AccountCapability ` - -AccountName $accountName ` - -DesiredCapabilities $capabilities ` - -ResourceGroupName $ResourceGroupName ` - -SubscriptionId $SubscriptionId ` - -Selector $selector - - # Read endpoint + keys. Under -WhatIf (dry run) never read real keys — stub them so a - # preview never emits secrets, even for already-provisioned accounts. - if ($WhatIfPreference) { - $endpoint = "https://$accountName.documents.azure.com:443/" - $primary = 'WHATIF_KEY' - $secondary = 'WHATIF_SECONDARY_KEY' - } else { - $account = Get-AzCosmosDBAccount -ResourceGroupName $ResourceGroupName -Name $accountName - $endpoint = $account.DocumentEndpoint - $keys = Get-AzCosmosDBAccountKey -ResourceGroupName $ResourceGroupName -Name $accountName -Type 'Keys' - $primary = $keys.PrimaryMasterKey - $secondary = $keys.SecondaryMasterKey - } - - $entry = [ordered]@{ - endpoint = $endpoint - key = $primary - consistency = $acct.defaultConsistencyLevel - multiWrite = $multiWrite - } - if ($acct.PSObject.Properties.Name -contains 'includeSecondaryKey' -and $acct.includeSecondaryKey) { - $entry['secondaryKey'] = $secondary - } - if ($acct.PSObject.Properties.Name -contains 'thinClient' -and $acct.thinClient) { - $entry['thinClient'] = $true - } - if ($acct.PSObject.Properties.Name -contains 'preferredLocations') { - $entry['preferredLocations'] = [string[]]@($acct.preferredLocations) - } - $entry['regions'] = [string[]]$regionList - - $secret.accounts[$selector] = $entry -} - -# --- Emit the assembled JSON ------------------------------------------------- -$secretJson = $secret | ConvertTo-Json -Depth 8 - -if ($OutputPath) { - if ($PSCmdlet.ShouldProcess($OutputPath, 'Write accounts JSON to file')) { - Set-Content -Path $OutputPath -Value $secretJson -NoNewline - Write-Info "Wrote accounts JSON to '$OutputPath' ($($secret.accounts.Count) accounts). Contains keys - treat as secret." - } -} - -Write-Info "Assembled $($secret.accounts.Count) accounts. Update the sub-config-cosmos-azure-cloud-test-resources secret manually with this JSON." -# Emit the JSON to stdout so it can be captured/redirected. -Write-Output $secretJson diff --git a/sdk/cosmos/pipeline/account-provisioning/README.md b/sdk/cosmos/pipeline/account-provisioning/README.md deleted file mode 100644 index 817393d64e2c7..0000000000000 --- a/sdk/cosmos/pipeline/account-provisioning/README.md +++ /dev/null @@ -1,136 +0,0 @@ -# Cosmos live-test account provisioning - -`New-CosmosLiveTestAccounts.ps1` provisions the fixed Cosmos DB accounts used by the -**azure-sdk-for-java** Cosmos live tests and outputs the single JSON document -stored in `sub-config-cosmos-azure-cloud-test-resources`, which the pipelines read. - -- **`New-CosmosLiveTestAccounts.ps1`** — creates the resource group (if missing) + the - accounts, and **outputs** the accounts JSON (endpoints + keys). It does not touch Key - Vault; publish the JSON to the secret manually (see below). - -## Why - -The Java Cosmos live tests are moving off the central EngSys on-the-fly provisioner to -**fixed, self-owned accounts** (no service-connection / tenant dependency for the -key-based tests). Because the ephemeral tenant is deleted and recreated roughly every -**90 days**, this script is re-run after each rotation to recreate the accounts and -regenerate the fresh endpoints/keys. - -## Files - -| File | Purpose | -| --- | --- | -| `New-CosmosLiveTestAccounts.ps1` | Creates `sdk-ci` RG (if missing) + accounts; outputs accounts JSON. | -| `cosmos-live-test-accounts.definition.json` | Desired accounts (logical selector + config). | - -An account entry may set `"regions": [...]` to pin its own regions when `regionDefaults` does not -suit it. `gsi-single-session` uses this to sit in **East US 2**, because -`live-gsi-platform-matrix.json` runs it single-region with `PREFERRED_LOCATIONS=["East US 2"]`, and a -preferred region the account does not have leaves the client with nothing to prefer. - -The emitted JSON conforms to the schema at -`../live-test-accounts.schema.json`, which the pipeline pre-step -`../resolve-cosmos-test-account.sh` parses. - -## Prerequisites - -- PowerShell 7+ -- Az modules: `Az.Accounts`, `Az.Resources`, `Az.CosmosDB` -- Contributor on the subscription that hosts `sdk-ci` -- Signed in: `Connect-AzAccount -Tenant -Subscription ` - -## Usage - -```powershell -# Create/refresh accounts and write the JSON to a file -# (contains keys - treat as secret, delete after publishing) -./New-CosmosLiveTestAccounts.ps1 -SubscriptionId -OutputPath ./accounts.json - -# Dry run: create nothing, print the assembled JSON with keys stubbed -./New-CosmosLiveTestAccounts.ps1 -SubscriptionId -WhatIf -``` - -Idempotent: existing accounts are left in place and missing capabilities are added; the -JSON is regenerated with current endpoints/keys. A single run is enough for a fresh tenant - -capabilities are applied by ARM PATCH after the account exists, and verified, rather than being -passed to `New-AzCosmosDBAccount` (some Az.CosmosDB versions ignore `-Capabilities` silently, -which previously produced accounts with no capabilities and required a second run). - -The multi-master accounts are separated by contention domain: - -| Selector | Workload | -| --- | --- | -| `multimaster-multiregion-session` | General query/direct and multi-master tests | -| `multimaster-session-control` | Control-plane-heavy `fast` tests | -| `multimaster-session-http2` | HTTP/2 fast/query/circuit-breaker tests | -| `multimaster-session-circuit` | Flaky and circuit-breaker tests | -| `multimaster-multiregion-session-fi` | Fault-injection tests | -| `multimaster-multiregion-session-split` | Multi-master partition split tests | -| `single-session-cfp-split` | Single-region Session change-feed split tests | -| `single-strong-cfp-split` | Single-region Strong change-feed split tests | -| `multimaster-session-cfp-split` | Multi-master change-feed split tests | - -Then **update the Key Vault secret / ADO variable manually** with the contents of -`accounts.json` (paste the JSON into the `sub-config-cosmos-azure-cloud-test-resources` secret). - -## Rotation runbook - -1. Ephemeral tenant is recreated (~every 90 days). -2. `Connect-AzAccount -Tenant -Subscription `. -3. Run the account script (above) to recreate any missing `sdk-ci` accounts and - regenerate `accounts.json`. -4. Manually update the `sub-config-cosmos-azure-cloud-test-resources` secret with the new JSON. -5. Java pipelines pick up the refreshed values on their next run — no YAML edits. - -## Incident-response key rotation - -If either key set may have been disclosed, do not wait for the next tenant rotation: - -1. Cancel or pause active Cosmos live-test runs so no job continues using a key while it - is being revoked. -2. Connect to the subscription and regenerate both master keys for every fixed account: - - ```powershell - $subscriptionId = '' - $resourceGroupName = 'sdk-ci' - $accountNamePrefix = 'sdkci' - $definitionPath = './cosmos-live-test-accounts.definition.json' - - Connect-AzAccount -Tenant -Subscription $subscriptionId - $definition = Get-Content -Raw $definitionPath | ConvertFrom-Json - foreach ($account in $definition.accounts) { - $accountName = ('{0}-{1}' -f $accountNamePrefix, $account.name).ToLowerInvariant() - foreach ($keyKind in @('primary', 'secondary')) { - New-AzCosmosDBAccountKey ` - -ResourceGroupName $resourceGroupName ` - -Name $accountName ` - -KeyKind $keyKind ` - -Confirm:$false | Out-Null - } - } - ``` - -3. Regenerate the complete JSON without printing it to the terminal: - - ```powershell - $accountsPath = Join-Path ([System.IO.Path]::GetTempPath()) 'cosmos-live-test-accounts.json' - ./New-CosmosLiveTestAccounts.ps1 ` - -SubscriptionId $subscriptionId ` - -ResourceGroupName $resourceGroupName ` - -AccountNamePrefix $accountNamePrefix ` - -DefinitionPath $definitionPath ` - -OutputPath $accountsPath > $null - ``` - -4. Immediately replace the `sub-config-cosmos-azure-cloud-test-resources` Key Vault - secret / ADO variable with the contents of `$accountsPath`. -5. Delete the temporary file even if publication or validation fails: - - ```powershell - Remove-Item -LiteralPath $accountsPath -Force - ``` - - Do not paste the JSON into terminals, chats, build logs, or incident notes. Remove any - other copies created by shell transcripts, redirected output, or local backups. -6. Queue a fresh Cosmos live-test run, verify account resolution and key-dependent tests, - and record the rotation in the incident timeline. diff --git a/sdk/cosmos/pipeline/account-provisioning/cosmos-live-test-accounts.definition.json b/sdk/cosmos/pipeline/account-provisioning/cosmos-live-test-accounts.definition.json deleted file mode 100644 index afbc217211973..0000000000000 --- a/sdk/cosmos/pipeline/account-provisioning/cosmos-live-test-accounts.definition.json +++ /dev/null @@ -1,245 +0,0 @@ -{ - "comment": "Desired fixed Cosmos accounts for the Java SDK live tests. Consumed by New-CosmosLiveTestAccounts.ps1 to (re)create accounts in the sdk-ci resource group and to emit JSON for the sub-config-cosmos-azure-cloud-test-resources Key Vault secret. Account names here are the logical selectors used by the azure-sdk-for-java tests.yml / kafka.yml stages. accountNamePrefix + name form the actual Cosmos account name (must be globally unique, <=44 chars, lowercase); the region is intentionally NOT encoded in the name. regionDefaults defines the provisioned topology and mirrors sdk/cosmos/test-resources.json (singleRegion: Central US; multiRegion: Central US write + East US 2), so preferredLocations values match a region that actually exists on the multi-region accounts. NOTE: thinClient=true only tags the emitted secret; enabling thin-client on the account itself requires the correct account 'capabilities' entry / federation config once confirmed - add it to a per-account \"capabilities\": [\"\"] array.", - "accounts": [ - { - "name": "single-session", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "single-session-pmerge", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "enablePartitionMerge": true, - "thinClient": false, - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "single-strong", - "defaultConsistencyLevel": "Strong", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "single-session-split", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "single-session-cfp-split", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "single-strong-split", - "defaultConsistencyLevel": "Strong", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "single-strong-cfp-split", - "defaultConsistencyLevel": "Strong", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multiregion-strong", - "defaultConsistencyLevel": "Strong", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multimaster-multiregion-session", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": true, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": false, - "preferredLocations": [ - "East US 2" - ], - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multimaster-session-control", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": true, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": false, - "preferredLocations": [ - "East US 2" - ], - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multimaster-session-http2", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": true, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": false, - "preferredLocations": [ - "East US 2" - ], - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multimaster-session-circuit", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": true, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": false, - "preferredLocations": [ - "East US 2" - ], - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multimaster-multiregion-session-fi", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": true, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": false, - "preferredLocations": [ - "East US 2" - ], - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multimaster-multiregion-session-split", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": true, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": false, - "preferredLocations": [ - "East US 2" - ], - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multimaster-session-cfp-split", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": true, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": false, - "preferredLocations": [ - "East US 2" - ], - "includeSecondaryKey": true, - "capabilities": [ - "EnableNoSQLVectorSearch" - ] - }, - { - "name": "multiregion-tc-session", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": true, - "enablePartitionMerge": false, - "thinClient": true, - "includeSecondaryKey": true - }, - { - "name": "gsi-single-session", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "regions": [ - "East US 2" - ], - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true - }, - { - "name": "kafka-session", - "defaultConsistencyLevel": "Session", - "enableMultipleWriteLocations": false, - "enableMultipleRegions": false, - "enablePartitionMerge": false, - "thinClient": false, - "includeSecondaryKey": true - } - ], - "regionDefaults": { - "singleRegion": [ - "Central US" - ], - "multiRegion": [ - "Central US", - "East US 2" - ] - } -} diff --git a/sdk/cosmos/pipeline/live-test-accounts.sample.json b/sdk/cosmos/pipeline/live-test-accounts.sample.json deleted file mode 100644 index 3f6a41d5c28fe..0000000000000 --- a/sdk/cosmos/pipeline/live-test-accounts.sample.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "version": 1, - "accounts": { - "single-session": { - "endpoint": "https://REPLACE-single-session.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US" - ], - "consistency": "Session", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "single-session-pmerge": { - "endpoint": "https://REPLACE-single-session-pmerge.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US" - ], - "consistency": "Session", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "single-strong": { - "endpoint": "https://REPLACE-single-strong.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US" - ], - "consistency": "Strong", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "single-session-split": { - "endpoint": "https://REPLACE-single-session-split.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US" - ], - "consistency": "Session", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "single-session-cfp-split": { - "endpoint": "https://REPLACE-single-session-cfp-split.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US" - ], - "consistency": "Session", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "single-strong-split": { - "endpoint": "https://REPLACE-single-strong-split.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US" - ], - "consistency": "Strong", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "single-strong-cfp-split": { - "endpoint": "https://REPLACE-single-strong-cfp-split.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US" - ], - "consistency": "Strong", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multiregion-strong": { - "endpoint": "https://REPLACE-multiregion-strong.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "consistency": "Strong", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multimaster-multiregion-session": { - "endpoint": "https://REPLACE-multimaster-multiregion-session.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "preferredLocations": [ - "East US 2" - ], - "consistency": "Session", - "multiWrite": true, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multimaster-session-control": { - "endpoint": "https://REPLACE-multimaster-session-control.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "preferredLocations": [ - "East US 2" - ], - "consistency": "Session", - "multiWrite": true, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multimaster-session-http2": { - "endpoint": "https://REPLACE-multimaster-session-http2.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "preferredLocations": [ - "East US 2" - ], - "consistency": "Session", - "multiWrite": true, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multimaster-session-circuit": { - "endpoint": "https://REPLACE-multimaster-session-circuit.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "preferredLocations": [ - "East US 2" - ], - "consistency": "Session", - "multiWrite": true, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multimaster-multiregion-session-fi": { - "endpoint": "https://REPLACE-multimaster-multiregion-session-fi.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "preferredLocations": [ - "East US 2" - ], - "consistency": "Session", - "multiWrite": true, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multimaster-multiregion-session-split": { - "endpoint": "https://REPLACE-multimaster-multiregion-session-split.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "preferredLocations": [ - "East US 2" - ], - "consistency": "Session", - "multiWrite": true, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multimaster-session-cfp-split": { - "endpoint": "https://REPLACE-multimaster-session-cfp-split.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "preferredLocations": [ - "East US 2" - ], - "consistency": "Session", - "multiWrite": true, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "multiregion-tc-session": { - "endpoint": "https://REPLACE-multiregion-tc-session.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US", - "East US 2" - ], - "consistency": "Session", - "multiWrite": false, - "thinClient": true, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "gsi-single-session": { - "endpoint": "https://REPLACE-gsi-single-session.documents.azure.com:443/", - "key": "REPLACE_KEY", - "regions": [ - "Central US" - ], - "consistency": "Session", - "multiWrite": false, - "secondaryKey": "REPLACE_SECONDARY_KEY" - }, - "kafka-session": { - "endpoint": "https://REPLACE-kafka-session.documents.azure.com:443/", - "key": "REPLACE_KEY", - "secondaryKey": "REPLACE_SECONDARY_KEY", - "regions": [ - "Central US" - ], - "consistency": "Session", - "multiWrite": false - } - } -} diff --git a/sdk/cosmos/pipeline/live-test-accounts.schema.json b/sdk/cosmos/pipeline/live-test-accounts.schema.json deleted file mode 100644 index 5f252d9a0d4df..0000000000000 --- a/sdk/cosmos/pipeline/live-test-accounts.schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://azure.github.io/azure-sdk-for-java/cosmos/live-test-accounts.schema.json", - "title": "Cosmos live-test fixed accounts config", - "description": "Single-secret JSON describing the fixed Cosmos DB accounts used by the Java live tests. Stored as the Key Vault secret / ADO variable sub-config-cosmos-azure-cloud-test-resources and refreshed by the account-creation script on each ephemeral-tenant rotation. Each pipeline stage selects one account by its logical name.", - "type": "object", - "required": ["version", "accounts"], - "additionalProperties": false, - "properties": { - "version": { - "type": "integer", - "description": "Schema version. The parser rejects versions it does not understand.", - "enum": [1] - }, - "accounts": { - "type": "object", - "description": "Map of logical account selector -> account descriptor.", - "minProperties": 1, - "additionalProperties": { "$ref": "#/definitions/account" } - } - }, - "definitions": { - "account": { - "type": "object", - "required": ["endpoint", "key"], - "additionalProperties": false, - "properties": { - "endpoint": { - "type": "string", - "description": "documentEndpoint of the Cosmos account (https://.documents.azure.com:443/).", - "pattern": "^https://" - }, - "key": { - "type": "string", - "description": "Primary master key. Treated as a secret by the parser.", - "minLength": 1 - }, - "secondaryKey": { - "type": "string", - "description": "Optional secondary master key (exported as SECONDARY_ACCOUNT_KEY)." - }, - "regions": { - "type": "array", - "description": "Regions the account is provisioned in (informational / documentation).", - "items": { "type": "string" } - }, - "preferredLocations": { - "type": "array", - "description": "Optional preferred locations for the provisioned account. Runtime PREFERRED_LOCATIONS remains matrix-controlled and is not exported by the resolver.", - "items": { "type": "string" } - }, - "consistency": { - "type": "string", - "description": "Account default consistency level. Runtime ACCOUNT_CONSISTENCY remains matrix-controlled and is not exported by the resolver.", - "enum": ["Strong", "BoundedStaleness", "Session", "ConsistentPrefix", "Eventual"] - }, - "multiWrite": { - "type": "boolean", - "description": "True if the account has multiple write locations (multi-master)." - }, - "thinClient": { - "type": "boolean", - "description": "True if the account is thin-client enabled." - } - } - } - } -} diff --git a/sdk/cosmos/pipeline/resolve-cosmos-test-account.sh b/sdk/cosmos/pipeline/resolve-cosmos-test-account.sh index ba27bb4ec01bf..5f9cec73bf9b7 100755 --- a/sdk/cosmos/pipeline/resolve-cosmos-test-account.sh +++ b/sdk/cosmos/pipeline/resolve-cosmos-test-account.sh @@ -8,7 +8,7 @@ # All Cosmos live-test matrix legs run on linux agents, so this is bash + jq. # # Inputs (environment variables): -# COSMOS_TEST_ACCOUNTS_JSON Raw JSON matching live-test-accounts.schema.json +# COSMOS_TEST_ACCOUNTS_JSON Versioned account JSON as documented in README.md # (the value of sub-config-cosmos-azure-cloud-test-resources). # COSMOS_ACCOUNT_SELECTOR Logical account name to select (e.g. multimaster-multiregion-session). # COSMOS_ACCOUNTS_LOCAL Optional. When "true", prints KEY=VALUE to stdout instead of diff --git a/sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh b/sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh deleted file mode 100755 index 4fae6ab599e2b..0000000000000 --- a/sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# -# Local tests for resolve-cosmos-test-account.sh (no ADO required). -# Run: bash sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh -set -uo pipefail - -HERE="$(cd "$(dirname "$0")" && pwd)" -SCRIPT="$HERE/resolve-cosmos-test-account.sh" -SAMPLE="$HERE/live-test-accounts.sample.json" - -pass=0; fail=0 -ok() { echo " PASS: $1"; pass=$((pass+1)); } -no() { echo " FAIL: $1"; fail=$((fail+1)); } - -run() { # selector json -> sets OUT, RC (local mode) - OUT="$(COSMOS_ACCOUNTS_LOCAL=true COSMOS_ACCOUNT_SELECTOR="$1" COSMOS_TEST_ACCOUNTS_JSON="$2" bash "$SCRIPT" 2>/dev/null)" - RC=$? -} - -SAMPLE_JSON="$(cat "$SAMPLE")" - -echo "Test 1: resolves a valid selector and exports host+key (no consistency/preferred)" -run "multimaster-multiregion-session" "$SAMPLE_JSON" -if [ $RC -eq 0 ] && echo "$OUT" | grep -q "^ACCOUNT_HOST=https://REPLACE-multimaster-multiregion-session" \ - && echo "$OUT" | grep -q "^ACCOUNT_KEY=REPLACE_KEY" \ - && ! echo "$OUT" | grep -q "^ACCOUNT_CONSISTENCY=" \ - && ! echo "$OUT" | grep -q "^PREFERRED_LOCATIONS="; then ok "resolved host+key only"; else no "resolve valid selector (rc=$RC): $OUT"; fi - -echo "Test 2: exports SECONDARY_ACCOUNT_KEY when present" -run "kafka-session" "$SAMPLE_JSON" -if [ $RC -eq 0 ] && echo "$OUT" | grep -q "^SECONDARY_ACCOUNT_KEY=REPLACE_SECONDARY_KEY"; then ok "secondary key exported"; else no "secondary key (rc=$RC): $OUT"; fi - -echo "Test 3: unknown selector fails with non-zero" -run "does-not-exist" "$SAMPLE_JSON" -if [ $RC -ne 0 ]; then ok "unknown selector rejected"; else no "unknown selector should fail: $OUT"; fi - -echo "Test 4: invalid JSON fails" -run "single-session" "{not json" -if [ $RC -ne 0 ]; then ok "invalid json rejected"; else no "invalid json should fail"; fi - -echo "Test 5: unsupported version fails" -run "single-session" '{"version":99,"accounts":{"single-session":{"endpoint":"https://x","key":"k"}}}' -if [ $RC -ne 0 ]; then ok "bad version rejected"; else no "bad version should fail"; fi - -echo "Test 6: missing key fails" -run "x" '{"version":1,"accounts":{"x":{"endpoint":"https://x"}}}' -if [ $RC -ne 0 ]; then ok "missing key rejected"; else no "missing key should fail"; fi - -echo "Test 7: non-https endpoint fails" -run "x" '{"version":1,"accounts":{"x":{"endpoint":"http://x","key":"k"}}}' -if [ $RC -ne 0 ]; then ok "non-https rejected"; else no "non-https should fail"; fi - -echo "Test 8: empty selector fails" -OUT="$(COSMOS_ACCOUNTS_LOCAL=true COSMOS_ACCOUNT_SELECTOR="" COSMOS_TEST_ACCOUNTS_JSON="$SAMPLE_JSON" bash "$SCRIPT" 2>/dev/null)"; RC=$? -if [ $RC -ne 0 ]; then ok "empty selector rejected"; else no "empty selector should fail"; fi - -echo "" -echo "Results: $pass passed, $fail failed" -[ $fail -eq 0 ] diff --git a/sdk/cosmos/pipeline/test_fixed_account_pipeline.py b/sdk/cosmos/pipeline/test_fixed_account_pipeline.py deleted file mode 100644 index 6d698287503cc..0000000000000 --- a/sdk/cosmos/pipeline/test_fixed_account_pipeline.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# cspell:ignore DACCOUNT - -"""Offline configuration checks for fixed-account thin-client and GSI stages.""" - -import json -from pathlib import Path -import unittest - -import yaml - - -COSMOS = Path(__file__).resolve().parents[1] -REPO = COSMOS.parents[1] -TEMPLATE = "/eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml" -# Stage suffix -> endpoint variable, key variable, matrix filename, selected profiles. -STAGES = { - "Http2NetworkFault": ( - "thinclient-test-endpoint", "thinclient-test-key", - "live-http2-network-fault-platform-matrix.json", - ["manual-http-network-fault", "manual-http-network-fault-compute"], - ), - "ThinClient": ( - "thinclient-test-endpoint", "thinclient-test-key", - "live-thinclient-platform-matrix.json", - ["thinclient", "query", "consistency-overrides"], - ), - "ThinClient_EndpointProbe": ( - "thinclient-test-endpoint", "thinclient-test-key", - "live-thinclient-endpoint-probe-platform-matrix.json", - ["thinclient-endpoint-probe"], - ), - "ThinClient_MultiRegion": ( - "thin-client-canary-multi-region-session-endpoint", "thin-client-canary-multi-region-session-key", - "live-thinclient-multi-region-platform-matrix.json", - ["fi-thinclient-multi-region"], - ), - "ThinClient_MultiMaster": ( - "thin-client-canary-multi-writer-session-endpoint", "thin-client-canary-multi-writer-session-key", - "live-thinclient-multi-master-platform-matrix.json", - ["fi-thinclient-multi-master"], - ), - "GSI": ( - "gsi-pipeline-uri", "gsi-pipeline-key", - "live-gsi-platform-matrix.json", ["gsi"], - ), -} - - -class FixedAccountPipelineTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - pipeline = yaml.safe_load((COSMOS / "tests.yml").read_text(encoding="utf-8")) - cls.stages = {} - for suffix in STAGES: - name = f"Cosmos_Live_Test_{suffix}" - stages = [ - stage for stage in pipeline["extends"]["parameters"]["stages"] - if stage["parameters"].get("TestName") == name - ] - if len(stages) != 1: - raise AssertionError(f"Expected exactly one {name} stage, found {len(stages)}") - cls.stages[suffix] = stages[0] - - def test_fixed_account_disables_arm_provisioning(self): - for name, stage in self.stages.items(): - with self.subTest(stage=name): - self.assertEqual(stage["template"], TEMPLATE) - self.assertIs(stage["parameters"].get("DisableAzureResourceCreation"), True) - - def test_test_runner_keeps_fixed_credentials_and_profiles(self): - for name, (host, key, matrix_file, profiles) in STAGES.items(): - with self.subTest(stage=name): - parameters = self.stages[name]["parameters"] - args = self.additional_args(name) - self.assertIn(f"-DACCOUNT_HOST=$({host})", args) - self.assertIn(f"-DACCOUNT_KEY=$({key})", args) - self.assertEqual(parameters["TestGoals"], "verify") - self.assertIn("$(ProfileFlag)", parameters["TestOptions"]) - self.assertIn("$(AdditionalArgs)", parameters["TestOptions"]) - - self.assertEqual(parameters["MatrixConfigs"][0]["Path"], f"sdk/cosmos/{matrix_file}") - matrix = json.loads((COSMOS / matrix_file).read_text(encoding="utf-8")) - self.assertTrue(matrix["include"]) - actual_profiles = [] - for leg in matrix["include"]: - actual_profiles.extend(leg["ProfileFlag"]) - self.assertEqual(leg["ACCOUNT_CONSISTENCY"], "Session") - self.assertCountEqual(actual_profiles, [f"-P{profile}" for profile in profiles]) - - def test_cleanup_keeps_fixed_credentials(self): - for name, (host, key, _, _) in STAGES.items(): - with self.subTest(stage=name): - cleanup = [ - step for step in self.stages[name]["parameters"]["PostSteps"] - if step.get("template") == "/sdk/cosmos/cleanup-test-resources.yml" - ] - self.assertEqual(len(cleanup), 1) - self.assertEqual(cleanup[0]["parameters"]["AccountHost"], f"$({host})") - self.assertEqual(cleanup[0]["parameters"]["AccountKey"], f"$({key})") - - def test_service_connection_and_federated_auth_are_preserved(self): - template = yaml.safe_load((REPO / TEMPLATE.lstrip("/")).read_text(encoding="utf-8")) - defaults = {parameter["name"]: parameter["default"] for parameter in template["parameters"]} - for name, stage in self.stages.items(): - with self.subTest(stage=name): - parameters = stage["parameters"] - self.assertEqual( - parameters["CloudConfig"]["Public"]["ServiceConnection"], - "azure-sdk-tests-cosmos", - ) - self.assertIs(parameters.get("UseFederatedAuth", defaults["UseFederatedAuth"]), True) - - def test_transport_selection_is_preserved(self): - for name in STAGES: - with self.subTest(stage=name): - args = self.additional_args(name) - thinclient_flags = [arg for arg in args if arg.startswith("-DCOSMOS.THINCLIENT_ENABLED=")] - http2_flags = [arg for arg in args if arg.startswith("-DCOSMOS.HTTP2_ENABLED=")] - if name in ("Http2NetworkFault", "GSI"): - self.assertEqual(thinclient_flags, []) - self.assertEqual(http2_flags, []) - elif name == "ThinClient_EndpointProbe": - self.assertEqual(thinclient_flags, []) - self.assertEqual(http2_flags, ["-DCOSMOS.HTTP2_ENABLED=true"]) - else: - self.assertEqual(thinclient_flags, ["-DCOSMOS.THINCLIENT_ENABLED=true"]) - self.assertEqual(http2_flags, ["-DCOSMOS.HTTP2_ENABLED=true"]) - - def test_network_fault_setup_is_preserved(self): - steps = self.stages["Http2NetworkFault"]["parameters"]["PreSteps"] - self.assertTrue(any("apt-get install -y -qq iptables" in step.get("script", "") for step in steps)) - - def additional_args(self, name): - variables = { - variable["name"]: variable["value"] - for variable in self.stages[name]["parameters"]["AdditionalVariables"] - } - return variables["AdditionalArgs"].split() - - -if __name__ == "__main__": - unittest.main() From 34d2b93a82627cf839b77a17ee143c71911964ab Mon Sep 17 00:00:00 2001 From: tvaron3 Date: Mon, 26 Oct 2026 17:14:07 -0400 Subject: [PATCH 3/3] Trigger fresh CI for Cosmos provisioning PR Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ead4fdd6-1296-47c8-bd5c-1cdfe90128ee