Move ADR certificate management to the certificate-authority model - #447
Ewerton Scaboro da Silva (ewertons) merged 50 commits into
Conversation
Certificate management provisioning no longer works: the ADR object model it targeted was public preview and has been replaced. A certificate policy used to hang off a namespace credential and be referenced from an enrollment by a single name. It now hangs off an issuing CA, and an enrollment references it by three names that must travel together. The hub and DPS used to be pointed at a namespace and a shared user-assigned identity when they were created; the relationship is now expressed on the namespace, as endpoints linked to it afterwards, with each resource authenticating as its own system-assigned identity. - Create the root -> issuing CA -> certificate policy chain, after linking, so ADR has a hub to sync the issuing CA certificate to. A policy created under a root is rejected with PolicyRequiresIssuingCa. - Link the hub and DPS to the namespace and wait for the endpoints, retrying while the namespace identity's grants replicate and healing a namespace left Failed by an otherwise successful link. - Write enrollments over the DPS service API, carrying the three policy names. The CLI cannot express them: it only ever offered --credential-policy. - Push the linked namespace into the DPS data plane with a tags-only update. Committing a link does not push scale-unit configuration, so without it every enrollment write fails with errorCode 400004. - Skip linked-hub creation under certificate management, where ADR chooses the provisioning targets and the DPS's own list is read-only. - Read linked hubs from the namespace endpoints. They were read from the DPS, which is captured before the hub is attached, so the list was always empty. - Drop the extension version pin, the credential sync, the Contributor grant to the IoT Hub first-party application and the Onboarding role, all of which belonged to the previous model. TestEnvironmentInfo.AzureAdrPolicyName is replaced by AdrPolicy, and the generated test configuration exports the namespace and certificate authority names alongside ADR_CERT_MGMT_POLICY_NAME. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The ADR namespace linking payload appears to use the IoT Hub endpoint type for the DPS endpoint, which is likely to break provisioning links at runtime.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates the PowerShell provisioning/test module to use the post-public-preview Azure Device Registry (ADR) certificate-authority model for certificate management, replacing the previous CLI-driven ADR surface area with direct ARM + DPS service-API calls.
Changes:
- Removes the azure-iot CLI extension version pin and routes ADR operations through
az rest(ARM) instead ofaz iot adr. - Adds ADR provisioning/linking helpers (namespace creation, endpoint linking, CA chain + policy creation) and waits/polling utilities.
- Writes DPS enrollments via the DPS service REST API to carry the three-part ADR policy reference (namespace/CA/policy).
File summaries
| File | Description |
|---|---|
| scripts/Azure.Iot.Sdk.Test.psm1 | Reworks ADR certificate management provisioning and DPS enrollment creation to match the new certificate-authority model. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The DPS provisioning endpoint was sent with the IoT Hub resource type, which mis-models the link. Three further problems in the same function, found while confirming that one: - The hub messaging endpoint was missing its provisioning availability, so it was linked but not offered as a provisioning target. - The link was submitted as a properties-only update. The saga starts on a namespace write carrying location and identity. - Reconciling a namespace left Failed re-sent the endpoints, which is rejected as immutable, and judged the result on the first read, which still reports Failed for a while. It is now a tags-only update, polled to Succeeded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The updated E2E config generators dereference TestEnvInfo.AdrPolicy.* unconditionally, which will throw when certificate management isn’t enabled (AdrPolicy is null), breaking config generation for non-ADR environments.
Review details
Suppressed comments (7)
Previously missed (2) — in code that hasn't changed since the last review.
scripts/Azure.Iot.Sdk.Test.psm1:1577
- The ADR/DPS
api-versionconstants are future-dated relative to the current date (e.g.2026-11-02-preview). If these versions aren't available in the target cloud/subscription yet, everyaz restcall will fail with an invalid/unsupported api-version error. Consider allowing these to be overridden via environment variables (or module parameters) so runs can be unblocked without code changes.
scripts/Azure.Iot.Sdk.Test.psm1:3150 TestEnvInfo.AdrPolicycan be$nullwhen certificate management wasn't enabled (e.g., environments created without-EnableCertificateManagementor JSON loaded from older configs). These lines dereference.NamespaceName/etc unconditionally, which will throw and prevent config generation even for non-ADR runs. Consider emitting empty strings whenAdrPolicyis null (matching the previous string-based behavior).
This issue also appears in the following locations of the same file:
- line 3179
- line 3256
- line 3278
- line 3340
- line 3370
scripts/Azure.Iot.Sdk.Test.psm1:3181
- Same null-dereference issue as the PowerShell block above: this bash config generation path also assumes
TestEnvInfo.AdrPolicyis non-null and will throw when it isn't. Emit empty strings whenAdrPolicyis null to keep config generation working for non-ADR environments.
"export ADR_CERT_MGMT_NAMESPACE_NAME=`"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"export ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"export ADR_CERT_MGMT_POLICY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
scripts/Azure.Iot.Sdk.Test.psm1:3258
New-AzIotNetSDKE2ETestConfigdereferencesTestEnvInfo.AdrPolicy.*unconditionally. If the environment was created without certificate management,AdrPolicyis$nulland config generation will throw. Use empty strings (or conditionally omit these variables) whenAdrPolicyis not set.
"`$env:ADR_CERT_MGMT_NAMESPACE_NAME = `"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"`$env:ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME = `"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"`$env:ADR_CERT_MGMT_POLICY_NAME = `"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
scripts/Azure.Iot.Sdk.Test.psm1:3280
- Same null-dereference issue as the PowerShell block above: the bash path assumes
TestEnvInfo.AdrPolicyis non-null and will throw otherwise. Emit empty strings whenAdrPolicyis$nullto preserve backward compatibility.
"export ADR_CERT_MGMT_NAMESPACE_NAME=`"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"export ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"export ADR_CERT_MGMT_POLICY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
scripts/Azure.Iot.Sdk.Test.psm1:3342
New-AzIotPythonSDKE2ETestConfigdereferencesTestEnvInfo.AdrPolicy.*unconditionally. IfAdrPolicyis$null(non-certificate-management environment), this will throw and block config generation. Consider emitting empty strings whenAdrPolicyis not present.
"`$env:ADR_CERT_MGMT_NAMESPACE_NAME = `"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"`$env:ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME = `"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"`$env:ADR_CERT_MGMT_POLICY_NAME = `"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
scripts/Azure.Iot.Sdk.Test.psm1:3372
- Same null-dereference issue as the PowerShell block above: this bash block also assumes
TestEnvInfo.AdrPolicyis non-null and will throw otherwise. Emit empty strings whenAdrPolicyis$null.
"export ADR_CERT_MGMT_NAMESPACE_NAME=`"$($TestEnvInfo.AdrPolicy.NamespaceName)`""
"export ADR_CERT_MGMT_CERTIFICATE_AUTHORITY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificateAuthorityName)`""
"export ADR_CERT_MGMT_POLICY_NAME=`"$($TestEnvInfo.AdrPolicy.CertificatePolicyName)`""
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
The generated test configuration reads the three policy names directly. With no certificate management the reference was null, which renders as empty strings normally but throws under Set-StrictMode, so config generation could break for environments that never asked for ADR. An environment now always carries a reference; it is simply incomplete when certificate management is off. Callers already gate on IsComplete(), so nothing else changes, and the emitted values match the previous empty strings. The ADR and DPS api-versions can also be overridden from the environment, so a cloud or region where one is not registered can be unblocked without a code change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed both points from the second review in 4fdd94c. Null Future-dated api-versions. These are the versions the certificate-authority model exists at, so they cannot be lowered, but they are now overridable via |
There was a problem hiding this comment.
🔵 Needs a closer look
It makes large, high-impact changes to live Azure provisioning behavior (preview ARM api-versions, role/linking sagas, and DPS service-API enrollment writes) that require human validation in a real subscription.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
Removing the version pin means the stable azure-iot extension is installed, and 'az iot dps create' accepts identity flags only in preview builds of it. The DPS was being created with --mi-system-assigned, which the stable extension rejects as an unrecognized argument, so provisioning would have failed on exactly the version the unpinned install now selects. The DPS is created plain again and its system-assigned identity is turned on through ARM, alongside the rest of the ADR wiring, and only when certificate management is enabled. The IoT Hub keeps --mi-system-assigned, which is a core CLI flag rather than an extension one. An extension that is already installed is also updated rather than left as it is, so a long-lived agent cannot keep provisioning with a stale one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
There is a confirmed null-dereference bug in the new ARM provisioning-state wait loop, and the default ADR/DPS API versions are dated in the future and may break provisioning without overrides.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
scripts/Azure.Iot.Sdk.Test.psm1:501
- Wait-AzProvisioningState calls
(Invoke-AzRest -AllowFailure).properties.provisioningStatedirectly. When Invoke-AzRest returns $null (any transient ARM/CLI failure), this will throw a null-reference and abort the wait loop, defeating the purpose of -AllowFailure.
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
Wait-AzProvisioningState read the provisioning state straight off the result of a call it had explicitly allowed to fail. These resources are polled for minutes, and a transient ARM or CLI failure along the way says nothing about the provisioning itself, so the failed read is now treated as an unknown state for that attempt and the wait continues against its deadline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the latest review in 80b9b3f. Null dereference in Future-dated api-versions. These cannot be lowered: the certificate-authority resource tree exists only at Local verification: module parses and imports, 43 assertions pass across the unit and flow suites. Live provisioning against a subscription remains the outstanding validation, as noted in the description. |
There was a problem hiding this comment.
🔵 Needs a closer look
The defaults include future-dated api-version strings and there is at least one reliability issue around masking az extension update failures that should be addressed before merging.
Review details
Suppressed comments (1)
scripts/Azure.Iot.Sdk.Test.psm1:167
Install-AzureIotCliExtensionunconditionally suppressesaz extension updatestderr and then forces$LASTEXITCODE = 0. This hides real update failures (e.g., transient network issues, extension install corruption), which can later surface as confusing provisioning errors while the log claims the extension was updated. It’s safer to ignore only the known "already up to date" failure mode and fail fast for other errors.
# Already up to date is reported as a failure, so the exit code is reset rather than checked.
Write-Host "Azure IoT extension $($Extension.version) found; updating."
az extension update --name azure-iot --only-show-errors 2>$null | Out-Null
$global:LASTEXITCODE = 0
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
The update suppressed stderr and reset the exit code, on the belief that being already up to date is reported as a failure. It is not: with no newer version available the command warns and returns successfully. The suppression therefore bought nothing and hid genuine failures, which would have surfaced later as confusing provisioning errors against a log claiming the extension was updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed in 8fac417. Masked Future-dated api-versions. As above, these cannot be lowered without reverting to the retired model, and all three are overridable from the environment. |
There was a problem hiding this comment.
🔵 Needs a closer look
There are confirmed null-handling bugs in tags-only PATCH flows (which can break reconciliation/dataplane sync) and a polling robustness issue that can fail provisioning on transient ARM reads.
Review details
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
scripts/Azure.Iot.Sdk.Test.psm1:1704
Connect-AdrNamespacepolls for endpointlinkingStatefor up to 15 minutes, but a single transient failure to read the namespace viaInvoke-AzRestwill currently stop provisioning immediately. This is inconsistent withWait-AzProvisioningState, which treats transient ARM/CLI read failures as non-fatal during polling.
scripts/Azure.Iot.Sdk.Test.psm1:1732- In the namespace reconciliation path,
$Namespace.tagscan be$nullwhen the resource has no tags; calling.PSObject.Propertieson$nullwill throw and prevent recovery fromprovisioningState=Failed. Guard the tag enumeration so the reconcile PATCH always succeeds.
This issue also appears on line 1834 of the same file.
scripts/Azure.Iot.Sdk.Test.psm1:1837
Sync-DpsAdrConfigurationbuilds$Tagsby enumerating(Invoke-AzRest ...).tags.PSObject.Properties, but.tagscan be$nullwhen no tags are set, causing the best-effort sync to always fail early (and skip the PATCH that triggers the dataplane push). Guard the tag enumeration so the PATCH is attempted even with no existing tags.
try {
$Tags = @{}
(Invoke-AzRest -Url $Url).tags.PSObject.Properties | %{ $Tags[$_.Name] = $_.Value }
$Tags["AdrDataplaneSyncUtc"] = (Get-Date).ToUniversalTime().ToString("o")
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
Both tags-only updates copied the resource's current tags by piping the tags property into ForEach-Object. Piping a null still runs the body once, with a null key, so on a resource carrying no tags the copy threw. Neither the ADR namespace nor the DPS is created with tags, so this was the normal case, not an edge one: the namespace reconcile that recovers a link left Failed would throw instead of recovering, and the DPS data-plane push would fail before it was attempted, leaving every enrollment write to fail with errorCode 400004. The namespace link poll also gave up the whole run on a single failed read, over a window of up to fifteen minutes. It now treats a failed read the same way the provisioning-state wait does: an attempt spent, not a failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Its only consumer was the old certificate-management role-assignment branch, which is gone, so leaving the assignment suggested the behaviour still varies with how the caller signed in. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the suppressed finding from the last review in the latest commit: |
master repinned the extension and now installs it from the release wheel. This branch had removed the pin on the grounds that ADR no longer needs the preview `az iot adr` group, which is true but was not the only reason to pin: 0.32.0b1 returns the device-facing hostname from `az iot hub connection-string show`, and this module reads that string five times for the service-side clients. So the pin stays exactly as master has it, including remove-and-reinstall rather than update, which would have moved off the pinned version. Only the ADR justification is rewritten to say that part is no longer why.
|
Merged master in; the branch was conflicting. The conflict was the extension pin. This branch had removed it because ADR no longer needs the preview So the pin is kept exactly as master has it, including remove-and-reinstall rather than update — my version updated to whatever was newest, which would have moved off the pin. Only the ADR justification is rewritten to say that part is no longer why it is pinned. Tests updated to assert the pinned wheel is installed and not updated away from. |
There was a problem hiding this comment.
🔵 Needs a closer look
Address the three moderate issues before approval.
Review details
Suppressed comments (3)
scripts/Azure.Iot.Sdk.Test.psm1:2895
- The DPS identity's
IotHubDataContributorRoleIdgrant on$AzureIoTHub.idis created above, but this wait block only verifies the DPS identity's roles on the namespace. Because device registration runs through the DPS managed identity and provider RBAC is eventually consistent, the function can return and the first provisioning attempt can race this unpropagated hub grant; include the hub-scope data-contributor role in the propagation wait (or otherwise retry that operation).
@{ Assignee = $DpsPrincipalId; Role = $script:ContributorRoleId; Scope = $AdrNamespaceId },
@{ Assignee = $AdrNamespacePrincipalId; Role = $script:ContributorRoleId; Scope = $AdrNamespaceId },
@{ Assignee = $DpsPrincipalId; Role = $script:AdrContributorRoleId; Scope = $AdrNamespaceId },
@{ Assignee = $DpsPrincipalId; Role = $script:IotHubDataContributorRoleId; Scope = $AzureIoTHub.id },
scripts/Azure.Iot.Sdk.Test.psm1:1677
- The initial DELETE is issued with
-AllowFailure, so a transient 5xx/CLI failure is discarded and the loop only polls the still-present namespace for 10 minutes; it never retries the deletion. In the recovery path this turns a recoverable delete outage into a timeout instead of recreating the namespace. Retry the DELETE on transient errors (while treating a confirmed 404 as already deleted) before entering the confirmation loop.
}
scripts/Azure.Iot.Sdk.Test.psm1:2797
- This validation is reached only after Azure login, CLI extension installation, and resource-group creation/update, so an invalid
-EnableCertificateManagement -NoDpsinvocation still mutates Azure before throwing and may leave a newly created resource group behind. Move the mutually-exclusive-parameter check to the start ofNew-AzIotTestEnvironment, before any provisioning side effects.
# Created and tagged in one call, so the group is never observable in an untagged
# state that the leftover-resource cleanup would have to skip forever.
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
Three things a review caught: - the -NoDps / -EnableCertificateManagement conflict was rejected only after login, extension install and resource-group creation, so a bad invocation mutated Azure and could leave a resource group behind. It is checked before any of that now. - the namespace delete discarded a transient failure, leaving the confirmation loop to poll a namespace nothing had asked to remove; a brief outage became the full ten-minute timeout instead of a recreate. The DELETE is retried on a transient failure. - the propagation wait covered the DPS identity's namespace grants but not its IoT Hub Data Contributor grant, which device registration runs on. Waiting for the namespace ones and not that one leaves the first registration racing it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the three suppressed findings from the last review in the latest commit.
Local gates green. |
There was a problem hiding this comment.
🟡 Changes recommended
The deletion retry path suppresses failures, and the retained extension pin conflicts with the documented change.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 2
- Review effort level: Lite
…ly sees The DELETE retry added earlier never fired: -AllowFailure resets the exit code, so the retry helper saw success every time. It uses the throwing form now, and a not-found is treated as already deleted rather than propagated. Two diagnostics, prompted by the observation that an endpoint can sit in a section this code never read: - the `updating` endpoint section is reported alongside messaging and provisioning, so an endpoint that is missing from the two we submit can be told apart from one that moved; - the namespace principal is compared against the one its role grants were made against. If a write ever replaced it, the grants point at a principal that no longer exists, and ADR reports that as the linked resource being unreadable -- the same message as a grant that has not replicated. The comparison rides the read the poll already does, so it costs no extra call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved provisioning-state and DPS synchronization error handling issues remain, and live provisioning has not been validated.
Review details
Suppressed comments (1)
scripts/Azure.Iot.Sdk.Test.psm1:2048
- This catch covers failures from the initial DPS GET and the PATCH as well as a timeout while an accepted PATCH is settling. The module's own description says the link commit does not update the data plane, so a permanent GET/PATCH failure means the subsequent enrollment retries cannot repair the missing namespace configuration and will all end in 400004 after a long delay. Only swallow a post-acceptance settling timeout; propagate a failed read or rejected PATCH immediately so the real provisioning error is visible.
catch {
Write-Host "DPS ADR configuration push did not settle cleanly ($($_.Exception.Message)); continuing, as enrollment creation waits for the data plane anyway."
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
|
Tested the region hypothesis and two diagnostic gaps in one run (build subscription, resources in
Also fixed this round, from review: the namespace DELETE retry added earlier was a no-op ( Local gates: 20 unit and 42 flow assertions. Teardown succeeded; no resources left behind. |
There was a problem hiding this comment.
🟡 Changes recommended
A critical identity mismatch can leave the environment unusable, and reconciliation mishandles a pre-existing Failed state.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
scripts/Azure.Iot.Sdk.Test.psm1:1936
- The reconciliation path cannot tolerate the transient
Failedstate it explicitly documents:Wait-AzProvisioningStatetreats anyprovisioningState = Failedas terminal and throws on the first poll. If the namespace still reports its pre-reconcileFailedstate after this tags-only PATCH, setup aborts instead of waiting for the reconciliation to reachSucceeded; use a reconciliation-specific poll that ignores that pre-existing state until the update settles.
Invoke-AzRest -Method PATCH -Url $Url -Body @{ tags = $Tags } | Out-Null
Wait-AzProvisioningState -Url $Url -Step "ADR namespace reconcile" -TimeoutSeconds 300
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
…entity Two things a review caught: - the namespace reconcile waited with a helper that treats the first Failed read as terminal, which is precisely the state the reconcile exists to clear. The comment above it already said the state lingers after the PATCH is accepted, so the heal threw on the condition it was added to fix and setup never reached the certificate authority. It now waits through Failed until it clears or the deadline passes. - a namespace identity that no longer matches the principal its grants were made against was logged and accepted. The link can still report success, but every later ADR call then runs as an identity holding nothing. It is raised instead, on a message the recovery cycle recognises, so the namespace is recreated and regranted rather than carried forward unusable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tested the provider re-registration hypothesis (run 35030268243, build subscription,
The re-registration completed successfully — the pipeline identity has the rights for it — and changed nothing. So "features registered but never exposed on the provider" is eliminated for this subscription. That run did not reach linking, for an unrelated reason worth recording: the DPS never left So the link itself is still untested against re-registration, but the hypothesis it was meant to test is already ruled out by the registration states being unchanged. Also fixed this round, from review: the namespace reconcile waited with a helper that treats the first Local gates: 20 unit and 42 flow assertions. Teardown succeeded. |
There was a problem hiding this comment.
🔵 Needs a closer look
A moderate failure-handling issue remains, and live provisioning has not been validated.
Review details
Suppressed comments (1)
scripts/Azure.Iot.Sdk.Test.psm1:2066
- This catch treats a definitive control-plane failure (for example an authorization/API-version error or the DPS resource reaching
Failed) the same as a transient timeout and continues. Enrollment retries only recognize403000/400004, so the original sync failure is then hidden or reported much later as a misleading data-plane error. Keep the best-effort path for transient/settling failures, but rethrow non-retryable failures so the run stops with the actual cause.
catch {
Write-Host "DPS ADR configuration push did not settle cleanly ($($_.Exception.Message)); continuing, as enrollment creation waits for the data plane anyway."
}
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
The reconcile PATCH carried over whatever tags the namespace already had. The feature tag on the namespace selects which identity the endpoint link authorizes against, so a namespace reaching the reconcile without it would be healed into a state the link still cannot use. Re-assert the tags instead of only copying them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DPS returns 400004 both for a namespace its data plane has not picked up yet, which clears on its own, and for an api-version it does not support, which never does. The enrollment retry treated both as transient and walked the full backoff ladder on the permanent one, taking about 31 minutes to report a request that was rejected outright on the first attempt. Adds -StopOnPattern to Invoke-WithRetry, matched against the same captured output and taking precedence over -RetryOnPattern, and uses it to exclude the unsupported-api-version case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tions Linking at 2026-11-02-preview succeeded on one subscription and failed every attempt on another with LinkableResourceNotReady, which was indistinguishable from unreplicated role assignments. 2026-11-01-preview links first attempt on both, with no other change: provisioning now completes on the subscription where it had never linked, and still completes on the one where it did. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…odule master moved the framework out of the single scripts/Azure.Iot.Sdk.Test.psm1 into scripts/AzIotSdkTest/ as a manifested module, leaving that path a shim. This branch had rewritten the old file, so every change had to move with it. The split changed no code, so the parts are a contiguous partition of the file this branch started from. Each change was carried to the part that now owns the lines it touched: AzureCommon (ARM/REST helpers and retry), Dps (certificate authority, policy and enrollment), Models, Provisioning, TestConfig, Common. The shim is master's, unchanged. No part boundary moved, no export was added or removed, and the module's diff against master is the same 1092 insertions and 184 deletions this branch carried before the move. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Certificate management provisioning no longer works: the ADR object model it targeted was public preview and has been replaced.
A certificate policy used to hang off a namespace credential and be referenced from an enrollment by a single name. It now hangs off an issuing CA, and an enrollment references it by three names that must travel together. The hub and DPS used to be pointed at a namespace and a shared user-assigned identity when they were created; the relationship is now expressed on the namespace, as endpoints linked to it afterwards, with each resource authenticating as its own system-assigned identity.
Changes
PolicyRequiresIssuingCa.Failedby an otherwise successful link.--credential-policy, and theaz iot adrgroup ships only in preview builds and still models the retired shape.errorCode 400004.IH409313).az iot hub connection-string show, which this module reads for the service-side clients. Only the ADR justification for the pin is removed, not the pin.TestEnvironmentInfo.AzureAdrPolicyNameis replaced byAdrPolicy, and the generated test configuration exports the namespace and certificate authority names alongsideADR_CERT_MGMT_POLICY_NAME. No other file in the repository referenced the old property.Verification
Run locally against PowerShell 7.4.6; the module parses and imports cleanly, and 37 assertions pass across two suites:
TestEnvironmentInfoJSON round-trip, and the DPS SAS token checked against an independent implementation of the same HMAC-SHA256 construction.credentialPolicyName, and no retired command or flag is used.Live provisioning against a subscription has not been run and is the remaining validation: create an environment with
-EnableCertificateManagement, confirm the chain and endpoints reachSucceeded, and confirm the first enrollment write is not rejected.