diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/FeedResponseListValidator.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/FeedResponseListValidator.java index c997fc715d03..1201dfa45d67 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/FeedResponseListValidator.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/FeedResponseListValidator.java @@ -144,6 +144,30 @@ public void validate(List> feedList) { return this; } + /** + * Asserts the results CONTAIN all of the expected resource IDs, allowing additional + * unexpected results. Unlike {@link #exactlyContainsInAnyOrder(List)} this does not + * require the result set to be limited to the expected IDs, so it is safe for + * account-global reads (e.g. readAllDatabases) that run against shared test accounts + * where other resources may exist concurrently. + */ + public Builder containsResourceIds(List expectedIds) { + validators.add(new FeedResponseListValidator() { + @Override + public void validate(List> feedList) { + List actualIds = feedList + .stream() + .flatMap(f -> f.getResults().stream()) + .map(r -> getResource(r).getResourceId()) + .collect(Collectors.toList()); + assertThat(actualIds) + .describedAs("Resource IDs of results") + .containsAll(expectedIds); + } + }); + return this; + } + public Builder exactlyContainsIdsInAnyOrder(List expectedIds) { validators.add(new FeedResponseListValidator() { @Override diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/ReadFeedDatabasesTest.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/ReadFeedDatabasesTest.java index 94466d8d99b6..eac37d1eddf3 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/ReadFeedDatabasesTest.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/ReadFeedDatabasesTest.java @@ -23,7 +23,6 @@ public class ReadFeedDatabasesTest extends TestSuiteBase { private List createdDatabases = new ArrayList<>(); - private List allDatabases = new ArrayList<>(); private CosmosAsyncClient client; @@ -38,11 +37,13 @@ public void readDatabases() throws Exception { CosmosPagedFlux feedObservable = client.readAllDatabases(); - int expectedPageSize = (allDatabases.size() + maxItemCount - 1) / maxItemCount; + // readAllDatabases is an account-global read. Live tests share a fixed account, so + // other test runs may create/delete databases concurrently. Assert only that the + // databases created by this test are present (containment) rather than asserting the + // exact account-wide set/count/page-count, which would be non-deterministic. FeedResponseListValidator validator = new FeedResponseListValidator.Builder() - .totalSize(allDatabases.size()) - .exactlyContainsInAnyOrder(allDatabases.stream().map(d -> d.getResourceId()).collect(Collectors.toList())) - .numberOfPages(expectedPageSize) + .containsResourceIds(createdDatabases.stream().map(d -> d.getResourceId()).collect(Collectors.toList())) + .numberOfPagesIsGreaterThanOrEqualTo(1) .pageSatisfy(0, new FeedResponseValidator.Builder() .requestChargeGreaterThanOrEqualTo(1.0).build()) .build(); @@ -53,13 +54,9 @@ public void readDatabases() throws Exception { @BeforeClass(groups = { "query" }, timeOut = SETUP_TIMEOUT) public void before_ReadFeedDatabasesTest() throws URISyntaxException { client = getClientBuilder().buildAsyncClient(); - allDatabases = client.readAllDatabases() - .collectList() - .block(); for(int i = 0; i < 5; i++) { createdDatabases.add(createDatabase(client)); } - allDatabases.addAll(createdDatabases); } public CosmosDatabaseProperties createDatabase(CosmosAsyncClient client) { diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/ReadFeedOffersTest.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/ReadFeedOffersTest.java index f316029bbc84..89d1a09ebd98 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/ReadFeedOffersTest.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/ReadFeedOffersTest.java @@ -45,7 +45,8 @@ public class ReadFeedOffersTest extends TestSuiteBase { public final String databaseId = DatabaseForTest.generateId(); private Database createdDatabase; - private List allOffers = new ArrayList<>(); + private final List createdCollections = new ArrayList<>(); + private List expectedOffers = new ArrayList<>(); private AsyncDocumentClient client; @@ -78,13 +79,13 @@ public void readOffers() throws Exception { Flux> feedObservable = client.readOffers(dummyState); - int maxItemCount = ModelBridgeInternal.getMaxItemCountFromQueryRequestOptions(options); - int expectedPageSize = (allOffers.size() + maxItemCount - 1) / maxItemCount; - + // readOffers is an account-global read. Live tests share a fixed account, so + // other test runs may create/delete containers (and thus offers) concurrently. + // Assert only that the offers for the containers created by this test are present + // (containment) rather than asserting the exact account-wide set/count/page-count. FeedResponseListValidator validator = new FeedResponseListValidator.Builder() - .totalSize(allOffers.size()) - .exactlyContainsInAnyOrder(allOffers.stream().map(d -> d.getResourceId()).collect(Collectors.toList())) - .numberOfPages(expectedPageSize) + .containsResourceIds(expectedOffers.stream().map(d -> d.getResourceId()).collect(Collectors.toList())) + .numberOfPagesIsGreaterThanOrEqualTo(1) .pageSatisfy(0, new FeedResponseValidator.Builder() .requestChargeGreaterThanOrEqualTo(1.0).build()) .build(); @@ -98,7 +99,7 @@ public void before_ReadFeedOffersTest() { createdDatabase = createDatabase(client, databaseId); for(int i = 0; i < 3; i++) { - createCollections(client); + createdCollections.add(createCollections(client)); } QueryFeedOperationState offerDummyState = TestUtils.createDummyQueryFeedOperationState( @@ -109,12 +110,18 @@ public void before_ReadFeedOffersTest() { ); try { - allOffers = client.readOffers(offerDummyState) + List createdCollectionRids = createdCollections.stream() + .map(DocumentCollection::getResourceId) + .collect(Collectors.toList()); + expectedOffers = client.readOffers(offerDummyState) .map(FeedResponse::getResults) .collectList() .map(list -> list.stream().flatMap(Collection::stream).collect(Collectors.toList())) .single() - .block(); + .block() + .stream() + .filter(o -> createdCollectionRids.contains(o.getOfferResourceId())) + .collect(Collectors.toList()); } finally { safeClose(offerDummyState); } diff --git a/sdk/cosmos/live-platform-matrix.json b/sdk/cosmos/live-platform-matrix.json index baa999d6f14a..7650c2989607 100644 --- a/sdk/cosmos/live-platform-matrix.json +++ b/sdk/cosmos/live-platform-matrix.json @@ -24,7 +24,12 @@ "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Strong' }": "", "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Session'; enablePartitionMerge = $true }": "", "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Session'; enableMultipleRegions = $true; enablePartitionMerge = $true}": "", - "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Strong'; enableMultipleRegions = $true }": "" + "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Strong'; enableMultipleRegions = $true }": "", + "single-session": "", + "single-session-pmerge": "", + "single-strong": "", + "multiregion-strong": "", + "multimaster-multiregion-session": "" }, "include": [ { @@ -32,12 +37,14 @@ "Session": { "DESIRED_CONSISTENCY": "Session", "ACCOUNT_CONSISTENCY": "Session", - "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Session'; enablePartitionMerge = $true }" + "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Session'; enablePartitionMerge = $true }", + "AccountSelector": "single-session-pmerge" }, "Strong": { "DESIRED_CONSISTENCY": "Strong", "ACCOUNT_CONSISTENCY": "Strong", - "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Strong' }" + "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Strong' }", + "AccountSelector": "single-strong" } }, "AdditionalArgs": [ @@ -45,7 +52,10 @@ ], "ProfileFlag": "-Pe2e", "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } } }, { @@ -58,18 +68,35 @@ "-DargLine=\"-Dio.netty.handler.ssl.noOpenSsl=true\"" ], "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "single-session" }, { - "DESIRED_CONSISTENCIES": [ "[\"Strong\", \"Session\"]", "[\"BoundedStaleness\"]", "[\"ConsistentPrefix\"]" ], + "DESIRED_CONSISTENCIES": [ + "[\"Strong\", \"Session\"]", + "[\"BoundedStaleness\"]", + "[\"ConsistentPrefix\"]" + ], "ACCOUNT_CONSISTENCY": "Strong", "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Strong' }", "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Pcfp-split", "-Psplit", "-Pquery", "-Pfast" ], + "ProfileFlag": [ + "-Pcfp-split", + "-Psplit", + "-Pquery", + "-Pfast" + ], "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "single-strong" }, { "DESIRED_CONSISTENCY": "BoundedStaleness", @@ -78,28 +105,44 @@ "ProfileFlag": "-Pe2e", "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Strong' }", "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "single-strong" }, { "DESIRED_CONSISTENCIES": "[\"Strong\", \"Session\"]", "ACCOUNT_CONSISTENCY": "Strong", "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Pdirect" ], + "ProfileFlag": [ + "-Pdirect" + ], "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Strong' }", "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "single-strong" }, { "DESIRED_CONSISTENCIES": "[\"Session\"]", "ACCOUNT_CONSISTENCY": "Session", "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Plong" ], + "ProfileFlag": [ + "-Plong" + ], "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Session'; enablePartitionMerge = $true }", "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "single-session-pmerge" }, { "DESIRED_CONSISTENCIES": "[\"Session\"]", @@ -111,11 +154,17 @@ } }, "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Pmulti-master" ], + "ProfileFlag": [ + "-Pmulti-master" + ], "AdditionalArgs": "\"-DCOSMOS.PARTITION_LEVEL_CIRCUIT_BREAKER_DEFAULT_CONFIG_OPT_IN=TRUE\"", "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "multimaster-multiregion-session" }, { "DESIRED_CONSISTENCIES": "[\"Session\"]", @@ -127,11 +176,17 @@ } }, "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Pfi-multi-master" ], + "ProfileFlag": [ + "-Pfi-multi-master" + ], "AdditionalArgs": "\"-DCOSMOS.PARTITION_LEVEL_CIRCUIT_BREAKER_DEFAULT_CONFIG_OPT_IN=TRUE\"", "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "multimaster-multiregion-session" }, { "DESIRED_CONSISTENCIES": "[\"Session\"]", @@ -143,11 +198,17 @@ } }, "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Pmulti-master" ], + "ProfileFlag": [ + "-Pmulti-master" + ], "AdditionalArgs": "\"-DCOSMOS.PARTITION_LEVEL_CIRCUIT_BREAKER_DEFAULT_CONFIG_OPT_IN=FALSE\"", "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "multimaster-multiregion-session" }, { "DESIRED_CONSISTENCIES": "[\"Session\"]", @@ -159,11 +220,17 @@ } }, "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Pfi-multi-master" ], + "ProfileFlag": [ + "-Pfi-multi-master" + ], "AdditionalArgs": "\"-DCOSMOS.PARTITION_LEVEL_CIRCUIT_BREAKER_DEFAULT_CONFIG_OPT_IN=FALSE\"", "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "multimaster-multiregion-session" }, { "DESIRED_CONSISTENCIES": "[\"Session\"]", @@ -175,10 +242,24 @@ } }, "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Pcfp-split", "-Psplit", "-Pquery", "-Pflaky-multi-master", "-Pcircuit-breaker-misc-direct", "-Pcircuit-breaker-misc-gateway", "-Pcircuit-breaker-read-all-read-many", "-Pfast", "-Pdirect" ], + "ProfileFlag": [ + "-Pcfp-split", + "-Psplit", + "-Pquery", + "-Pflaky-multi-master", + "-Pcircuit-breaker-misc-direct", + "-Pcircuit-breaker-misc-gateway", + "-Pcircuit-breaker-read-all-read-many", + "-Pfast", + "-Pdirect" + ], "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "multimaster-multiregion-session" }, { "DESIRED_CONSISTENCIES": "[\"Strong\"]", @@ -189,10 +270,16 @@ } }, "PROTOCOLS": "[\"Tcp\"]", - "ProfileFlag": [ "-Pmulti-region-strong" ], + "ProfileFlag": [ + "-Pmulti-region-strong" + ], "Agent": { - "ubuntu": { "OSVmImage": "env:LINUXVMIMAGE", "Pool": "env:LINUXPOOL" } - } + "ubuntu": { + "OSVmImage": "env:LINUXVMIMAGE", + "Pool": "env:LINUXPOOL" + } + }, + "AccountSelector": "multiregion-strong" } ] } diff --git a/sdk/cosmos/pipeline/README.md b/sdk/cosmos/pipeline/README.md new file mode 100644 index 000000000000..ebc4ce959c48 --- /dev/null +++ b/sdk/cosmos/pipeline/README.md @@ -0,0 +1,95 @@ +# 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. + +## 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. | + +## 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 + `{ 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, + looks up the selected account, and exports `ACCOUNT_HOST`, `ACCOUNT_KEY` (and + `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/`](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. + +All Cosmos live-test matrix legs run on **linux** agents, so the parser is bash + jq. + +## Selectors (from the test matrices) + +`single-session`, `single-session-pmerge`, `single-strong`, `multiregion-strong`, +`multimaster-multiregion-session`, `multiregion-tc-session`, `gsi-single-session`, +`kafka-session`. + +## Wiring a stage (tests.yml / kafka.yml) + +Use the reusable steps template in `PreTestRunSteps` and set +`DisableAzureResourceCreation: true` so no per-run provisioning/service-connection deploy +happens: + +```yaml +DisableAzureResourceCreation: true +PreTestRunSteps: + - template: /sdk/cosmos/pipeline/resolve-test-account-steps.yml + parameters: + AccountSelector: multimaster-multiregion-session # literal, or $(AccountSelector) from the matrix +``` + +`ACCOUNT_HOST`/`ACCOUNT_KEY` set by the resolve step are consumed by `TestConfigurations` +via environment variables; no test-code change is required. The key is emitted with the +azure-sdk double-set convention (`_ACCOUNT_KEY` secret to mask it in logs + a plain +`ACCOUNT_KEY` so it reaches the Maven task's env). + +### Per-leg selection (multi-account stages) + +The main `Cosmos_live_test` stage spans many consistency/topology configs, so each leg of +`live-platform-matrix.json` carries an `AccountSelector` field and the stage passes +`AccountSelector: $(AccountSelector)`. Single-account stages pass a literal selector. + +### Currently wired + +- `Cosmos_live_test` (main) — per-leg selector. +- `Cosmos_Live_Test_Http2` — `multimaster-multiregion-session`. + +NOT yet wired (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`). + +### Secret upkeep + +Every account in the secret carries a `secondaryKey` — `AzureKeyCredentialTest` (in the +`-Pfast` legs) calls `TestConfigurations.SECONDARY_MASTER_KEY`, so a missing secondary key +would fall back to the emulator key and fail against a live account. + +## Secret variable + +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 +``` diff --git a/sdk/cosmos/pipeline/account-provisioning/New-CosmosLiveTestAccounts.ps1 b/sdk/cosmos/pipeline/account-provisioning/New-CosmosLiveTestAccounts.ps1 new file mode 100644 index 000000000000..23eb93634098 --- /dev/null +++ b/sdk/cosmos/pipeline/account-provisioning/New-CosmosLiveTestAccounts.ps1 @@ -0,0 +1,238 @@ +<# +.SYNOPSIS + (Re)creates the fixed Cosmos DB accounts used by the azure-sdk-for-java Cosmos live + tests and outputs the cosmos-live-test-accounts 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 cosmos-live-test-accounts JSON and emit it (to stdout, + and to -OutputPath if provided). + + This script does NOT touch Key Vault. Update the cosmos-live-test-accounts 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 + Primary/write region for the accounts. Defaults to 'West Central US'. + +.PARAMETER SecondaryLocation + Secondary region used for multi-region accounts. Defaults to 'Central US'. + +.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 = 'West Central US', + + [string] $SecondaryLocation = 'Central US', + + [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 + +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 $Location" + $null = New-AzResourceGroup -Name $ResourceGroupName -Location $Location + } +} else { + Write-Info "Resource group $ResourceGroupName already exists" +} + +# --- Helper: build the -LocationObject list for an account ------------------- +function New-LocationObjects([bool] $multiRegion) { + $locations = @( + New-AzCosmosDBLocationObject -LocationName $Location -FailoverPriority 0 -IsZoneRedundant $false + ) + if ($multiRegion) { + $locations += New-AzCosmosDBLocationObject -LocationName $SecondaryLocation -FailoverPriority 1 -IsZoneRedundant $false + } + return $locations +} + +# --- 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 + $locations = New-LocationObjects $multiRegion + + $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 + } + if ($capabilities.Count -gt 0) { $params['Capabilities'] = $capabilities } + $null = New-AzCosmosDBAccount @params + } + } else { + # Account already exists. Reconcile capabilities: Cosmos capabilities are additive + # and cannot be removed, so we only add any desired capability that is missing + # (e.g. EnableNoSQLVectorSearch). This makes the script idempotent for capability + # changes on already-provisioned accounts. + $existingCaps = @() + if ($existing.Capabilities) { $existingCaps = @($existing.Capabilities | ForEach-Object { $_.Name }) } + $missingCaps = @($capabilities | Where-Object { $existingCaps -notcontains $_ }) + if ($missingCaps.Count -gt 0) { + $mergedCaps = @($existingCaps + $missingCaps | Select-Object -Unique) + if ($PSCmdlet.ShouldProcess($accountName, "Add capabilities [$($missingCaps -join ', ')]")) { + Write-Info "Account '$accountName' exists (selector=$selector); adding missing capabilities: $($missingCaps -join ', ')" + # Capabilities cannot be set via Update-AzCosmosDBAccount, so PATCH the account + # through ARM. Capabilities are additive; send the full merged list. + $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)" + } + } + } else { + Write-Info "Cosmos account '$accountName' already exists (selector=$selector); capabilities up to date" + } + } + + # 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) + } + $regions = if ($multiRegion) { @($Location, $SecondaryLocation) } else { @($Location) } + $entry['regions'] = [string[]]$regions + + $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 cosmos-live-test-accounts 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 new file mode 100644 index 000000000000..ecd5e7289c06 --- /dev/null +++ b/sdk/cosmos/pipeline/account-provisioning/README.md @@ -0,0 +1,61 @@ +# 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 +(`cosmos-live-test-accounts`) 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). | + +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. + +Then **update the Key Vault secret / ADO variable manually** with the contents of +`accounts.json` (paste the JSON into the `cosmos-live-test-accounts` 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 `cosmos-live-test-accounts` secret with the new JSON. +5. Java pipelines pick up the refreshed values on their next run — no YAML edits. 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 new file mode 100644 index 000000000000..3f8343def278 --- /dev/null +++ b/sdk/cosmos/pipeline/account-provisioning/cosmos-live-test-accounts.definition.json @@ -0,0 +1,107 @@ +{ + "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 the cosmos-live-test-accounts 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). NOTE: preferredLocations values mirror the existing pipeline PREFERRED_LOCATIONS and may name a region outside the account's provisioned regions (client falls back). 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": "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": "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, + "enablePartitionMerge": false, + "thinClient": false, + "preferredLocations": [ + "East US 2" + ], + "includeSecondaryKey": true + }, + { + "name": "kafka-session", + "defaultConsistencyLevel": "Session", + "enableMultipleWriteLocations": false, + "enableMultipleRegions": false, + "enablePartitionMerge": false, + "thinClient": false, + "includeSecondaryKey": true + } + ], + "regionDefaults": { + "singleRegion": [ + "West Central US" + ], + "multiRegion": [ + "West Central US", + "Central US" + ] + } +} diff --git a/sdk/cosmos/pipeline/cspell.json b/sdk/cosmos/pipeline/cspell.json new file mode 100644 index 000000000000..aee66641a828 --- /dev/null +++ b/sdk/cosmos/pipeline/cspell.json @@ -0,0 +1,11 @@ +{ + "version": "0.2", + "words": [ + "esac", + "issecret", + "pmerge", + "Pfast", + "sdkci", + "WHATIF" + ] +} diff --git a/sdk/cosmos/pipeline/live-test-accounts.sample.json b/sdk/cosmos/pipeline/live-test-accounts.sample.json new file mode 100644 index 000000000000..0f6e61279350 --- /dev/null +++ b/sdk/cosmos/pipeline/live-test-accounts.sample.json @@ -0,0 +1,95 @@ +{ + "version": 1, + "accounts": { + "single-session": { + "endpoint": "https://REPLACE-single-session.documents.azure.com:443/", + "key": "REPLACE_KEY", + "regions": [ + "West 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": [ + "West 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": [ + "West 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": [ + "West Central US", + "Central US" + ], + "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": [ + "West Central US", + "Central US" + ], + "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": [ + "West Central US", + "Central US" + ], + "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": [ + "East US 2" + ], + "preferredLocations": [ + "East US 2" + ], + "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": [ + "West 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 new file mode 100644 index 000000000000..0ffaf67aff56 --- /dev/null +++ b/sdk/cosmos/pipeline/live-test-accounts.schema.json @@ -0,0 +1,68 @@ +{ + "$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 one Key Vault secret / ADO variable (cosmos-live-test-accounts) 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 exported as PREFERRED_LOCATIONS.", + "items": { "type": "string" } + }, + "consistency": { + "type": "string", + "description": "Account default consistency level (exported as ACCOUNT_CONSISTENCY).", + "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 new file mode 100755 index 000000000000..6d46493e9f77 --- /dev/null +++ b/sdk/cosmos/pipeline/resolve-cosmos-test-account.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# +# Resolves a single Cosmos live-test account from the one JSON secret +# (cosmos-live-test-accounts) and exports ACCOUNT_HOST / ACCOUNT_KEY (and optional +# SECONDARY_ACCOUNT_KEY / PREFERRED_LOCATIONS / ACCOUNT_CONSISTENCY) for the tests. +# +# 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 +# (the value of the cosmos-live-test-accounts secret). +# 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 +# emitting Azure DevOps ##vso logging commands (used for local tests). +# +# Exit codes: 0 success; non-zero on any validation failure. +set -euo pipefail + +fail() { echo "ERROR: $*" >&2; exit 1; } + +command -v jq >/dev/null 2>&1 || fail "jq is required but not found on PATH." + +json="${COSMOS_TEST_ACCOUNTS_JSON:-}" +selector="${COSMOS_ACCOUNT_SELECTOR:-}" +local_mode="${COSMOS_ACCOUNTS_LOCAL:-false}" + +[ -n "$json" ] || fail "COSMOS_TEST_ACCOUNTS_JSON is empty. Wire the cosmos-live-test-accounts secret to this variable." +[ -n "$selector" ] || fail "COSMOS_ACCOUNT_SELECTOR is empty. Set it to a logical account name." + +echo "$json" | jq empty 2>/dev/null || fail "COSMOS_TEST_ACCOUNTS_JSON is not valid JSON." + +version="$(echo "$json" | jq -r '.version // empty')" +[ "$version" = "1" ] || fail "Unsupported or missing schema version '$version' (parser supports: 1)." + +if [ "$(echo "$json" | jq --arg s "$selector" 'has("accounts") and (.accounts | has($s))')" != "true" ]; then + available="$(echo "$json" | jq -r '.accounts | keys | join(", ")' 2>/dev/null || echo '')" + fail "Account selector '$selector' not found. Available: ${available}" +fi + +acct="$(echo "$json" | jq -c --arg s "$selector" '.accounts[$s]')" + +endpoint="$(echo "$acct" | jq -r '.endpoint // empty')" +key="$(echo "$acct" | jq -r '.key // empty')" +secondary_key="$(echo "$acct" | jq -r '.secondaryKey // empty')" +consistency="$(echo "$acct" | jq -r '.consistency // empty')" +preferred="$(echo "$acct" | jq -r '(.preferredLocations // []) | join(",")')" + +[ -n "$endpoint" ] || fail "Account '$selector' is missing required 'endpoint'." +[ -n "$key" ] || fail "Account '$selector' is missing required 'key'." +case "$endpoint" in + https://*) : ;; + *) fail "Account '$selector' endpoint must start with https:// (got '$endpoint')." ;; +esac + +emit_public() { # name value + if [ "$local_mode" = "true" ]; then + echo "$1=$2" + else + echo "##vso[task.setvariable variable=$1;issecret=false]$2" + fi +} +# Emit a secret using the azure-sdk double-set convention: register the literal value as +# a secret (variable _NAME) so the log scrubber masks it everywhere, AND set a plain +# variable NAME so it still auto-exports as an environment variable to the Maven test +# task. Marking a variable issecret=true alone would prevent env propagation. +emit_secret() { # name value + if [ "$local_mode" = "true" ]; then + echo "$1=$2" + else + echo "##vso[task.setvariable variable=_$1;issecret=true]$2" + echo "##vso[task.setvariable variable=$1;issecret=false]$2" + fi +} + +# Only ACCOUNT_HOST / ACCOUNT_KEY (+ optional SECONDARY_ACCOUNT_KEY) are emitted. +# ACCOUNT_CONSISTENCY and PREFERRED_LOCATIONS are intentionally NOT set here: they are +# controlled per matrix leg (DESIRED_CONSISTENCIES / ACCOUNT_CONSISTENCY / PREFERRED_LOCATIONS) +# and the fixed accounts provisioned to match, so emitting them here would clobber +# the matrix values. +emit_public ACCOUNT_HOST "$endpoint" +emit_secret ACCOUNT_KEY "$key" +[ -n "$secondary_key" ] && emit_secret SECONDARY_ACCOUNT_KEY "$secondary_key" + +# Masked, secret-free summary for logs. +echo "Resolved Cosmos test account '$selector': endpoint=$endpoint consistency=${consistency:-} preferredLocations=${preferred:-} key=***" >&2 diff --git a/sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh b/sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh new file mode 100755 index 000000000000..4fae6ab599e2 --- /dev/null +++ b/sdk/cosmos/pipeline/resolve-cosmos-test-account.tests.sh @@ -0,0 +1,61 @@ +#!/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/resolve-test-account-steps.yml b/sdk/cosmos/pipeline/resolve-test-account-steps.yml new file mode 100644 index 000000000000..1fade3956749 --- /dev/null +++ b/sdk/cosmos/pipeline/resolve-test-account-steps.yml @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# +# Reusable pre-test step that resolves one fixed Cosmos live-test account from the single +# JSON secret and exports ACCOUNT_HOST / ACCOUNT_KEY (+ optional SECONDARY_ACCOUNT_KEY) +# for the test run. Drop this into a stage's PreTestRunSteps. +# +# The account JSON lives in the ADO variable group secret referenced by AccountsJsonVar +# (default: the Cosmos user-administered variable sub-config-cosmos-azure-cloud-test-resources). +# +# AccountSelector selects which logical account to use. It may be a literal (for +# single-account stages) or a macro like $(AccountSelector) sourced from the matrix leg +# (for the main multi-account stage). +parameters: + - name: AccountSelector + type: string + - name: AccountsJsonVar + type: string + default: 'sub-config-cosmos-azure-cloud-test-resources' + +steps: + - task: Bash@3 + displayName: 'Resolve fixed Cosmos test account (${{ parameters.AccountSelector }})' + inputs: + filePath: $(Build.SourcesDirectory)/sdk/cosmos/pipeline/resolve-cosmos-test-account.sh + env: + COSMOS_TEST_ACCOUNTS_JSON: $(${{ parameters.AccountsJsonVar }}) + COSMOS_ACCOUNT_SELECTOR: ${{ parameters.AccountSelector }} diff --git a/sdk/cosmos/tests.yml b/sdk/cosmos/tests.yml index a03d1638ab89..836b90c5a21b 100644 --- a/sdk/cosmos/tests.yml +++ b/sdk/cosmos/tests.yml @@ -9,6 +9,15 @@ extends: CloudConfig: Public: ServiceConnection: azure-sdk-tests-cosmos + # Cosmos live tests run against fixed, self-owned accounts (RG sdk-ci) rather than + # provisioning an account per run. The resolve pre-step reads the single JSON + # secret (sub-config-cosmos-azure-cloud-test-resources) and selects the account + # for each matrix leg via $(AccountSelector), exporting ACCOUNT_HOST/ACCOUNT_KEY. + DisableAzureResourceCreation: true + PreTestRunSteps: + - template: /sdk/cosmos/pipeline/resolve-test-account-steps.yml + parameters: + AccountSelector: $(AccountSelector) MatrixConfigs: - Name: Cosmos_live_test Path: sdk/cosmos/live-platform-matrix.json @@ -41,6 +50,12 @@ extends: CloudConfig: Public: ServiceConnection: azure-sdk-tests-cosmos + # Fixed self-owned account (RG sdk-ci); no per-run provisioning. + DisableAzureResourceCreation: true + PreTestRunSteps: + - template: /sdk/cosmos/pipeline/resolve-test-account-steps.yml + parameters: + AccountSelector: multimaster-multiregion-session MatrixConfigs: - Name: Cosmos_live_test_http2 Path: sdk/cosmos/live-http2-platform-matrix.json