Skip to content

ci: stage MSAL Go pipeline + add Managed Identity IMDS & Azure Arc E2E tests - #641

Merged
Gladwin Johnson VR (gladjohn) merged 1 commit into
mainfrom
gladjohn/mi-e2e-imds
Jul 28, 2026
Merged

Gladwin Johnson VR (gladjohn) merged 1 commit into
mainfrom
gladjohn/mi-e2e-imds

Conversation

@gladjohn

@gladjohn Gladwin Johnson VR (gladjohn) commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Restructures the MSAL Go CI pipeline into discrete stages (modeled on MSAL .NET) and adds Managed Identity E2E tests for both IMDS (VM) and Azure Arc, running on the same self-hosted agents MSAL .NET uses.

1. Pipeline staging (ado/build_test.yaml)

The previous single job is split into stages, each reporting independently in Azure DevOps:

Stage Pool Runs
Build ubuntu-latest go build ./apps/...
Unit Tests (dependsOn Build) ubuntu-latest go test -race -short across apps/...
Integration Tests (dependsOn Build) ubuntu-latest Key Vault + cert + go test -race ./apps/tests/integration/...
MI E2E – IMDS MISEManagedIdentity IMDS managed-identity token test
MI E2E – Azure Arc MISEAZUREARC Azure Arc managed-identity token test

The two MI E2E stages are independent (dependsOn: []) and run on every PR.

2. Managed Identity E2E tests (apps/tests/e2e/, build tag e2e)

  • TestManagedIdentityImds — acquires an ARM token (https://management.azure.com) over IMDS for the system-assigned identity and each user-assigned binding: client ID, resource ID, object ID.
  • TestManagedIdentityAzureArc — acquires an ARM token for the system-assigned identity (Azure Arc supports SAMI only).
  • Both assert the first call reaches the identity provider and the second is served from the token cache — mirroring the MSAL .NET IMDS/Arc E2E tests and reusing the same lab identities.
  • Each stage selects its test via go test -run, so neither runs the other's environment-specific test.
  • The tests are behind the e2e build tag, so normal go build/go test excludes them.

3. Test reporting + locked tooling (build/tools/)

Why gotestsum? The stages run tests through gotestsum (rather than the plain Go@0 test task) so each stage publishes JUnit test results to the ADO test UI via PublishTestResults@2 — you get per-test pass/fail, not just a job exit code.

Why a separate module? gotestsum must be installed, and the obvious go install gotest.tools/gotestsum@<version> trips SonarCloud S8545 ("Go dependencies should be locked to verified versions"). To fix that, build/tools is a small dedicated module that pins the tool via committed go.mod + go.sum, installed with a lock-file-enforcing command:

(cd build/tools && go install gotest.tools/gotestsum)   # no @version — resolved from the committed lock file
  • go.mod — pins gotest.tools/gotestsum v1.12.2 (newest release compatible with the pipeline's Go 1.22.3).
  • tools.go — build-tagged (//go:build tools), never compiled into any binary; blank-imports gotestsum's command packages so go mod tidy keeps go.sum complete (standard Go "tools module" pattern).
  • go.sum — the lock file (checksums of gotestsum + transitive deps).

It's kept as its own module so the tooling's dependencies stay out of the published MSAL library's dependency graph. This is a Sonar/reporting concern only — unrelated to the Arc network issue below.

4. Air-gapped Azure Arc agent

The MISEAZUREARC agent has no outbound Go connectivity — the Go downloader and curl to go.dev / proxy.golang.org are TLS-reset by egress inspection, so GoTool@0 and on-demand go install / module downloads fail on that pool (they work fine on the ubuntu-latest and IMDS agents). The Arc stage therefore:

  • uses a pre-baked Go toolchain (C:\Go) and a pre-baked Go module cache (C:\gomodcache), running go test with GOPROXY=off (fully offline);
  • falls back to warming the cache from go.sum via curl when no pre-baked cache is present.

The module set (8 modules) is derived from go.sum and was verified to build the E2E test offline.

Validation

  • go vet -tags e2e ./apps/tests/e2e/... — clean; both tests compile & list.
  • gofmt -l — clean.
  • build/tools go.sum is go mod tidy-stable; lock-enforcing install verified locally.
  • Offline build proven: GOPROXY=off + pre-baked cache builds the E2E test.
  • All msal-go-ci stages green: Build, Unit, Integration, MI E2E – IMDS, MI E2E – Azure Arc; SonarCloud gate passing.

Follow-up

Bake C:\Go + C:\gomodcache into the MISEAZUREARC (and, if desired, MISEManagedIdentity) agent image so the offline cache is durable across reimaging.

@gladjohn Gladwin Johnson VR (gladjohn) changed the title test(managedidentity): add IMDS v1 managed identity E2E tests + pipeline stage ci: stage MSAL Go pipeline + add Managed Identity IMDS E2E tests Jul 28, 2026
@gladjohn
Gladwin Johnson VR (gladjohn) changed the base branch from gladjohn/ci-stage-build-test to main July 28, 2026 04:31
Comment thread ado/build_test.yaml Fixed
Comment thread ado/build_test.yaml Fixed
@gladjohn
Gladwin Johnson VR (gladjohn) marked this pull request as ready for review July 28, 2026 04:42
@gladjohn Gladwin Johnson VR (gladjohn) changed the title ci: stage MSAL Go pipeline + add Managed Identity IMDS E2E tests ci: stage MSAL Go pipeline + add Managed Identity IMDS & Azure Arc E2E tests Jul 28, 2026
…E tests

Restructure ado/build_test.yaml from a single job into discrete stages
(modeled on MSAL .NET) and add Managed Identity E2E coverage for both IMDS
(VM) and Azure Arc.

Pipeline stages:
- Build
- Unit Tests (dependsOn Build)
- Integration Tests (dependsOn Build)
- MI E2E - IMDS      (self-hosted MISEManagedIdentity pool)
- MI E2E - Azure Arc (self-hosted MISEAZUREARC pool)

Managed Identity E2E tests (apps/tests/e2e, build tag "e2e"):
- TestManagedIdentityImds: acquires an ARM token over IMDS for the
  system-assigned identity and each user-assigned binding (client ID,
  resource ID, object ID).
- TestManagedIdentityAzureArc: acquires an ARM token for the system-assigned
  identity (Arc supports SAMI only).
- Both assert the first call hits the identity provider and the second is
  served from the token cache. They reuse the same lab identities as the
  MSAL .NET IMDS/Arc E2E tests. Each stage selects its test via "go test -run".

Locked tooling (build/tools): gotestsum is installed from a dedicated module
with committed go.mod + go.sum via a lock-file-enforcing command, satisfying
SonarCloud S8545 (no unpinned "go install pkg@version").

Air-gapped Arc agent: the MISEAZUREARC agent has no outbound Go connectivity,
so the Arc stage uses a pre-baked Go toolchain (C:\Go) and a pre-baked Go
module cache (C:\gomodcache), running "go test" with GOPROXY=off; it falls
back to warming the cache via curl from go.sum when no pre-baked cache exists.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2d02b5ca-22b5-4b48-840a-de79786b3f9d
@sonarqubecloud

Copy link
Copy Markdown

@gladjohn
Gladwin Johnson VR (gladjohn) merged commit b49594a into main Jul 28, 2026
15 checks passed
Gladwin Johnson VR (gladjohn) added a commit that referenced this pull request Aug 2, 2026
Move the Azure Arc non-existent user-assigned identity E2E test into the
build-tagged e2e suite (apps/tests/e2e/managedidentity_arc_e2e_test.go) that runs
on the MISEAZUREARC pool, following the pattern from #641, and remove the ad-hoc
self-gating version from the unit test file. The test requests an unassigned UAMI
and asserts a 404 / identity_not_found with no token, mirroring MSAL.NET's
AcquireToken_ForNonExistentUami_OnAzureArc_Fails. Broaden the Arc E2E stage -run
filter so both Arc E2E tests execute.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
Gladwin Johnson VR (gladjohn) added a commit that referenced this pull request Aug 10, 2026
….NET #6128 parity) (#644)

* feat(managedidentity): Azure Arc user-assigned managed identity fail-closed support

Port of MSAL.NET #6128. Remove the client-side block that rejected user-assigned
managed identities on Azure Arc, forward the client_id / mi_res_id / object_id
selector on the Arc token request, and validate the identity the agent echoes in
the token response against the requested selector (case-insensitive; accepts both
mi_res_id and msi_res_id). Fail closed (error, no token) when the echo is absent or
does not match, so a legacy agent's silent auto-SAMI is never returned for a UAMI
request. System-assigned is unaffected; a 404 surfaces as a normal service error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* test(managedidentity): reduce Arc test cognitive complexity; assert 404 in Arc UAMI E2E

- Extract the per-case bodies of TestAzureArcUserAssignedHonored and
  TestAzureArcUserAssignedNotHonoredFailsClosed into helpers to drop the
  loop/closure nesting flagged by SonarCloud (go:S3776, complexity 17/16 > 15).
- Tighten TestAzureArcUserAssignedNonExistentE2E to assert a 404 /
  identity_not_found for a non-existent UAMI, matching MSAL.NET's
  AcquireToken_ForNonExistentUami_OnAzureArc_Fails.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* test(e2e): add Azure Arc non-existent UAMI E2E test to the e2e suite

Move the Azure Arc non-existent user-assigned identity E2E test into the
build-tagged e2e suite (apps/tests/e2e/managedidentity_arc_e2e_test.go) that runs
on the MISEAZUREARC pool, following the pattern from #641, and remove the ad-hoc
self-gating version from the unit test file. The test requests an unassigned UAMI
and asserts a 404 / identity_not_found with no token, mirroring MSAL.NET's
AcquireToken_ForNonExistentUami_OnAzureArc_Fails. Broaden the Arc E2E stage -run
filter so both Arc E2E tests execute.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* test(managedidentity): add Azure Arc UAMI cache-partitioning test

Port .NET's AzureArcUserAssignedManagedIdentityCacheIsPartitionedByIdentity:
assert the token cache is partitioned by the requested identity so a cache lookup
never returns a token for a different identity. SAMI and each of the three
user-assigned selectors get a distinct token; the first acquisition per identity
is a cache miss (identity provider) and the second returns that identity's own
token from cache (with no queued HTTP responses, so a miss would panic).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* test(managedidentity): assert Arc UAMI cache keys, not just behavior

Address review: in addition to the behavioral round-trip proof, assert the
managed-identity token cache keys directly (via Manager.Marshal) so the test
matches .NET's RecordAccess client-id check. There must be exactly one
access-token entry per identity, and each identity's identifier (the
system-assigned default and each user-assigned client/resource/object id) must
key exactly one entry, which catches a key collision that behavior alone
might mask.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* refactor(managedidentity): rename miQueryParameterResourceIdIMDS to miQueryParameterMsiResourceId

The constant holds the msi_res_id resource-id spelling and is no longer IMDS-only:
verifyAzureArcUserAssignedIdentity accepts it as one of the two resource-id
spellings Azure Arc may echo. Name it after the value (the msi_res_id parameter)
rather than a single source, per PR review feedback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* fix(managedidentity): send msi_res_id (not mi_res_id) for Azure Arc resource-id UAMI

Live testing against the Azure Connected Machine agent (1.67, user-assigned preview)
showed the Arc token endpoint only honors the IMDS 'msi_res_id' spelling for the
resource-id selector: a 'mi_res_id' request is silently ignored and returns the
system-assigned identity, so a resource-id UAMI always failed closed. Send
'msi_res_id' on the Arc request (matching IMDS); the echo verification still accepts
either spelling as a safety net. Update the happy-path/cache-partition tests to the
msi_res_id request+echo and repurpose the echo-spelling test to cover the mi_res_id
echo fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* test(e2e): accept fail-closed message (not just 404) for non-existent Arc UAMI

A non-existent user-assigned identity returns no token on any Azure Arc agent, but the
surfaced error differs by agent: a user-assigned-aware agent returns 404
(identity_not_found), while a legacy agent ignores the selector and returns the
system-assigned identity, which MSAL rejects (fail closed). Accept either error and
correct the comment that claimed every agent returns 404, per review feedback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* test(managedidentity): de-duplicate Arc honored-path helpers (SonarCloud)

Extract the shared Azure Arc happy-path setup into assertArcEchoHonored so both
assertArcUserAssignedHonored and the mi_res_id echo safety-net test are thin wrappers,
removing the duplicated mock-setup block flagged by the SonarCloud duplication gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* test(managedidentity): route Arc helpers through one mock-setup helper (SonarCloud)

Extract newArcChallengeClient so assertArcEchoHonored, assertArcUserAssignedFailsClosed,
and assertArcAcquireFromIdP no longer each repeat the Azure Arc 401-challenge + 200-body
mock setup. This removes the remaining duplicated block flagged by the SonarCloud
duplication gate on new code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

* refactor(managedidentity): extract shared user-assigned selector helper (SonarCloud)

The msi_res_id fix made the Azure Arc request-building switch byte-for-byte identical to
the IMDS one, which SonarCloud flagged as duplicated code on new code (20.5%). Extract
setUserAssignedQueryParam and route IMDS, Azure Arc, and verifyAzureArcUserAssignedIdentity
through it, so the type switch over the identity kind exists exactly once. Behavior is
unchanged; App Service keeps its own mi_res_id spelling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants