Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8268d5e
Move ADR certificate management to the certificate-authority model
Copilot Sep 4, 2026
50b7af3
Correct the DPS endpoint type and the namespace link and heal calls
Copilot Sep 4, 2026
4fdd94c
Keep the policy reference present, and allow api-version overrides
Copilot Sep 4, 2026
2b292ed
Stop depending on preview-only CLI flags, and refresh the extension
Copilot Sep 4, 2026
80b9b3f
Do not let a failed read abort the provisioning-state wait
Copilot Sep 4, 2026
8fac417
Fail on a real extension update error
Copilot Sep 4, 2026
3e40dc6
Copy existing tags safely, and keep polling through a failed read
Copilot Sep 4, 2026
8983f87
Re-submit a link whose endpoint fails while grants replicate
Copilot Sep 4, 2026
1caee47
Create the DPS with its identity, which cannot be added later
Copilot Sep 4, 2026
626513b
Grant the full role set the linking saga and CSR issuance need
Copilot Sep 4, 2026
537dc42
Reuse a custom role that already grants certificate issuance
Copilot Sep 4, 2026
0018494
Use the built-in role for certificate issuance instead of a custom one
Copilot Sep 4, 2026
610a77f
Create a GEN2 IoT Hub for certificate management
Copilot Sep 4, 2026
d6b3662
Create the IoT Hub through ARM so it can be GEN2
Copilot Sep 4, 2026
7efde7e
Use an IoT Hub api-version that is registered
Copilot Sep 4, 2026
6eb61b5
Create the namespace and its identity before the GEN2 hub
Copilot Sep 4, 2026
c816509
Let the hub identity write to the namespace, not just read it
Copilot Sep 4, 2026
e1d7b28
Say why a resource failed to provision
Copilot Sep 4, 2026
cfe820e
Give the GEN2 hub only the identity it is meant to have
Copilot Sep 4, 2026
ff81e54
Let the IoT Hub service reach the resource group again
Copilot Sep 4, 2026
bd9aa5a
Create the certificate-management hub the way that works
Copilot Sep 4, 2026
be7503c
Attach the DPS to the namespace before creating the hub
Copilot Sep 4, 2026
1a12eae
End a link attempt as soon as an endpoint fails
Copilot Sep 4, 2026
52d91c7
Wait for the grant the link reads the DPS with
Copilot Sep 4, 2026
7a839e9
Provision the way the reference E2E harness for this feature does
Copilot Sep 5, 2026
904c69d
Restore the IoT Hub api-version, and refuse a URL without one
Copilot Sep 5, 2026
d485456
Route DPS regionally, and recreate the namespace if linking exhausts
Copilot Sep 5, 2026
a7eacc9
State the token audience on ARM calls
Copilot Sep 5, 2026
c9937bd
Bind the location the data-plane push routes on, and tidy three lefto…
Copilot Sep 5, 2026
07d4296
Correct why the reconcile guards its tag copy
Copilot Sep 8, 2026
a540260
Create the IoT Hub at the api-version the reference harness uses
Copilot Sep 8, 2026
49aa3e6
Escape what goes into a URL, and name the DPS that was used
Copilot Sep 11, 2026
6b1f631
Retry a link that ARM answers with a transient failure
Copilot Sep 11, 2026
0a12e89
Do not mistake a failed read for a deletion, or one endpoint for a link
Copilot Sep 11, 2026
b9733c0
Report every link endpoint, and judge the retry on all of them
Copilot Sep 11, 2026
fd325b9
Count endpoints that exist, not the placeholder an absent group leaves
Copilot Sep 11, 2026
0d5d3be
Let a rejected link reach its recovery, and leave ordinary DPS creati…
Copilot Sep 11, 2026
cda2c70
Carry the reason in the thrown error, not just the step
Copilot Sep 11, 2026
a50b5b4
Leave ordinary enrollment creation, and the delete probe, alone
Copilot Sep 12, 2026
4ab2f5d
Drop two helpers that were not earning their place
Copilot Sep 12, 2026
4611133
Store hub FQDNs in LinkedIotHubs, not the short selection names
Copilot Sep 12, 2026
4bd1377
Drop the account-type flag nothing reads any more
Copilot Sep 15, 2026
1258b11
Merge master: keep the extension pin, drop only the ADR reason for it
Copilot Sep 15, 2026
c637f57
Check arguments before touching Azure, and close two recovery gaps
Copilot Sep 15, 2026
0b3b88b
Make the namespace delete retry real, and report what the link actual…
Copilot Sep 15, 2026
2626c91
Let the reconcile outlast the state it heals, and refuse a swapped id…
Copilot Sep 15, 2026
c411251
Re-assert namespace feature tags when reconciling a failed namespace
Copilot Sep 16, 2026
8ed475f
Stop retrying a DPS enrollment error code that cannot clear
Copilot Sep 17, 2026
81ed05c
Default to the namespace api-version that links on both test subscrip…
Copilot Sep 17, 2026
5fd8934
Merge master and move the certificate-authority work into the split m…
Copilot Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 156 additions & 4 deletions scripts/AzIotSdkTest/parts/AzureCommon.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ function Install-AzureIotCliExtension {
Stop-OnError -Step "Install Azure IoT extension"
}

# What actually ended up installed. When a provisioning command goes missing
# ("'adr' is misspelled or not recognized"), this is the first thing worth
# seeing in the log.
# What actually ended up installed. When a provisioning command goes missing,
# this is the first thing worth seeing in the log.
#
# Capture the table and re-emit it with Write-Host rather than letting the
# native command write straight to the pipeline. A bare `az` call puts its
Expand Down Expand Up @@ -66,6 +65,11 @@ function Invoke-WithRetry {
Script block to run. Runs in its defining scope, so it can use the caller's
variables normally.

.PARAMETER StopOnPattern
Regular expression matched against the same captured output. When it matches, the failure is
treated as permanent even if -RetryOnPattern also matches, and no further attempt is made. Use
it where one error code covers both a transient and a permanent condition.

.PARAMETER RetryOnPattern
Regex matched against stderr. Only matching failures are retried.

Expand All @@ -84,8 +88,12 @@ function Invoke-WithRetry {
[Parameter(Mandatory = $true)][string]$Step,
[Parameter(Mandatory = $true)][scriptblock]$Command,
[Parameter(Mandatory = $true)][string]$RetryOnPattern,
[string]$StopOnPattern,
[int]$MaxAttempts = 4,
[int]$InitialDelaySeconds = 30
[int]$InitialDelaySeconds = 30,
# Throw on final failure instead of ending the run. Needed by callers that recover from the
# failure themselves; without it Stop-OnError exits the host and their catch never runs.
[switch]$ThrowOnFailure
)

$Delay = $InitialDelaySeconds
Expand Down Expand Up @@ -125,6 +133,13 @@ function Invoke-WithRetry {

$IsLastAttempt = ($Attempt -ge $MaxAttempts)
$IsRetryable = ($null -ne $StdErr) -and ($StdErr -match $RetryOnPattern)
# A service can reuse one error code for both a transient condition and a permanent one.
# Where it does, -StopOnPattern tells the permanent case apart and wins over the retry
# pattern, so a failure that cannot succeed is reported at once instead of after the
# whole backoff ladder.
if ($IsRetryable -and -not [string]::IsNullOrEmpty($StopOnPattern) -and ($StdErr -match $StopOnPattern)) {
$IsRetryable = $false
}

if ($IsLastAttempt -or -not $IsRetryable) {
if ($null -ne $Caught) {
Expand All @@ -133,6 +148,12 @@ function Invoke-WithRetry {
# Hand the command's own exit code to Stop-OnError so the failure
# is reported exactly like a non-retrying call site.
$global:LASTEXITCODE = if ($ExitCode -ne 0) { $ExitCode } else { 1 }
if ($ThrowOnFailure) {
# The error text goes in the exception, not just the log. A caller that recovers
# from a failure has to recognise WHICH failure it was, and the step name and an
# exit code do not say; the reason is only in what the command wrote to stderr.
throw "ERROR: `"$Step`" failed (exit code $LASTEXITCODE): $(($StdErr, $Caught | ?{ $_ }) -join ' ')".Trim()
}
Stop-OnError -Step $Step
return $null
}
Expand Down Expand Up @@ -234,6 +255,137 @@ function Wait-AzRoleAssignment {
}
}

function Invoke-AzRest {
<#
.SYNOPSIS
Calls an ARM endpoint with `az rest`, passing the body as a file, and returns parsed JSON.

.DESCRIPTION
`az rest --body` only reliably carries JSON when it is handed a file: under the AzureCLI@2
task an inline body is re-quoted by the shell and arrives malformed. Every ARM call in this
module therefore writes a temp file first, and this collapses that boilerplate into one place.

.PARAMETER Method
HTTP method. Defaults to GET, which takes no body.

.PARAMETER Url
Fully-qualified ARM URL, including the api-version query parameter.

.PARAMETER Body
Request payload as a hashtable. Serialized to JSON; omit for GET.

.PARAMETER AllowFailure
Return $null instead of stopping when the call fails. Used for existence and state probes, where
a failure is an answer rather than an error; stderr is suppressed in that case only, so that
callers wrapping this in Invoke-WithRetry can still see -- and match on -- a real error.
#>
param(
[string]$Method = "GET",
[Parameter(Mandatory = $true)][string]$Url,
[Hashtable]$Body = $null,
[switch]$AllowFailure
)

if ($Url -notmatch 'api-version=[^&\s]+') {
throw "URL is missing an api-version: $Url"
}

$BodyFile = $null
try {
# The token audience is stated rather than left to be derived from the URL: the CLI only
# infers it for the hosts in `az cloud show`, so a regional ARM host gets no Authorization
# header at all and the call comes back as an authentication failure.
$Arguments = @("rest", "--method", $Method, "--url", $Url,
"--resource", "https://management.azure.com/", "--only-show-errors")

if ($null -ne $Body) {
$BodyFile = New-TempFile
Set-FileContent -Path $BodyFile -Content ($Body | ConvertTo-Json -Compress -Depth 10)
$Arguments += @("--body", "@$BodyFile")
}

if ($AllowFailure) {
$Response = az @Arguments 2>$null
if ($LASTEXITCODE -ne 0) {
$global:LASTEXITCODE = 0
return $null
}
} else {
$Response = az @Arguments
if ($LASTEXITCODE -ne 0) {
# Throws rather than exits, so a call wrapped in Invoke-WithRetry can be retried;
# an unhandled throw still fails the run for every other caller.
Stop-OnError -Step "$Method $Url" -Throw
}
}

if ([string]::IsNullOrWhiteSpace($Response)) {
return $null
}

return $Response | ConvertFrom-Json
}
finally {
if ($null -ne $BodyFile) {
Remove-Item -Path $BodyFile -ErrorAction SilentlyContinue
}
}
}

function Wait-AzProvisioningState {
<#
.SYNOPSIS
Polls an ARM resource until its provisioningState is terminal, and throws unless it succeeded.

.DESCRIPTION
The ADR resources created here (namespace, certificate authorities, certificate policy) are
provisioned asynchronously: the PUT returns immediately and the outcome only shows up in
provisioningState. Creating a child before its parent is Succeeded fails, so each create waits.

.PARAMETER Url
ARM URL of the resource, including api-version.

.PARAMETER Step
Human-readable name of the resource, used in progress and error messages.

.PARAMETER TimeoutSeconds
How long to wait for a terminal state before giving up.
#>
param(
[Parameter(Mandatory = $true)][string]$Url,
[Parameter(Mandatory = $true)][string]$Step,
[int]$TimeoutSeconds = 600
)

$Deadline = (Get-Date).AddSeconds($TimeoutSeconds)

while ($true) {
# A failed read is deliberately not fatal: these resources are polled for minutes, and a
# transient ARM or CLI failure along the way says nothing about the provisioning itself.
# The state is simply unknown for this attempt, and the deadline still applies.
$Resource = Invoke-AzRest -Url $Url -AllowFailure
$State = if ($null -ne $Resource) { $Resource.properties.provisioningState } else { $null }

if ($State -eq "Succeeded") {
return
}

if ($State -eq "Failed" -or $State -eq "Canceled") {
# The resource is included because provisioningState alone does not say why: an
# asynchronous failure records its reason on the resource, and without it the only
# thing to go on is the word 'Failed'.
throw "$Step reached provisioningState '$State'. Resource: $($Resource | ConvertTo-Json -Depth 10 -Compress)"
}

if ((Get-Date) -ge $Deadline) {
throw "$Step did not reach a terminal provisioningState within $TimeoutSeconds seconds (last state: '$State')."
}

Write-Host "Waiting for $Step (provisioningState=$State)."
Start-Sleep -Seconds 10
}
}

function Merge-ResourceGroupTags {
<#
.SYNOPSIS
Expand Down
16 changes: 7 additions & 9 deletions scripts/AzIotSdkTest/parts/Common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,14 @@ function Stop-OnError {

# The azure-iot CLI extension version this repo provisions with.
#
# Provisioning needs the `az iot adr` command group, which ships only in the
# preview builds. 0.30.0 final dropped it, and because a final release outranks
# its own pre-releases, `--allow-preview` started resolving to 0.30.0 instead
# of 0.30.0b2 -- so the command group disappeared with no change on our side and
# every provisioning run began failing with:
# ADR no longer needs it: the namespace, the certificate authorities and the link
# are created through ARM directly (see New-AdrCertificateAuthority), so the
# `az iot adr` command group -- which models the retired public-preview object
# model and has no command for the one that replaced it -- is not used here.
#
# ERROR: 'adr' is misspelled or not recognized by the system.
#
# Pinning is what makes this reproducible: `--allow-preview` selects whatever
# happens to be newest, which is not a version this repo ever tested against.
# The pin stays for the OTHER reasons below, which still hold: this module reads
# `az iot hub connection-string show` for the service-side clients, and pinning
# is what makes the version reproducible rather than whatever is newest.
#
# Installed from the release wheel rather than by name, because 0.30.0b2 was
# pulled from the Azure CLI extension index and `--version` no longer resolves
Expand Down
Loading