Skip to content

Commit b95cb7d

Browse files
denis256omattsson
andauthored
feat(azure): End to end Azure CICD (#6574)
* feat: add experimental azurerm remote state backend Co-Authored-By: Olof Mattsson <olof@devopsninja.nu> * feat: azure backend unit tests * chore: tests cleanup * chore: azure backend fixes * azure backend * chore: code cleanup * chore: PR comments * chore: secrets fetching * chore: venv usage * chore: secrets cleanup * erorr handling simplifications * docs cleanup * chore: azure PR cleanup * chore: tests cleanup * chore: PR comments * fix(azure): refuse cross-cloud state migration * chore: state fixes * chore: azurehelper remote state * chore: azure integraiton tests * chore: env variables setup for cicd tests * chore: azure cicd fixes * chore: azure backned fixes * chore: azure tests checks * CICD simplifications * resources cleanup * chore: azure cicd fixes * git commit -m "ci(azure): pass ARM_ACCESS_KEY so tofu init can authenticate" * chore: CI fixes * integration test cleanup * chore: ARM key update * chore: blob test update * chore: contributing update * chore(azure): address review feedback on build tags, deref, and secret quoting * chore: cicd updates * chore: build test tag update * Secrets cleanup * chore: tests cleanup * chore: PR review notes * chore: changelog cleanup * fix(ci): provide credentials to Azure internal tests --------- Co-authored-by: Olof Mattsson <olof@devopsninja.nu>
1 parent 8d2db91 commit b95cb7d

36 files changed

Lines changed: 1586 additions & 176 deletions

.github/scripts/setup/generate-secrets.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ for SECRET in $SECRETS; do
7070
printf "export TG_AZURE_TEST_STORAGE_ACCOUNT='%s'\n" "${TG_AZURE_TEST_STORAGE_ACCOUNT}" >>"$ENV_FILE"
7171
elif [[ "$SECRET" == "TG_AZURE_TEST_SUBSCRIPTION_ID" && -n "${TG_AZURE_TEST_SUBSCRIPTION_ID:-}" ]]; then
7272
printf "export TG_AZURE_TEST_SUBSCRIPTION_ID='%s'\n" "${TG_AZURE_TEST_SUBSCRIPTION_ID}" >>"$ENV_FILE"
73+
elif [[ "$SECRET" == "TG_AZURE_TEST_RESOURCE_GROUP" && -n "${TG_AZURE_TEST_RESOURCE_GROUP:-}" ]]; then
74+
printf "export TG_AZURE_TEST_RESOURCE_GROUP='%s'\n" "${TG_AZURE_TEST_RESOURCE_GROUP}" >>"$ENV_FILE"
75+
elif [[ "$SECRET" == "ARM_CLIENT_ID" && -n "${ARM_CLIENT_ID:-}" ]]; then
76+
printf "export ARM_CLIENT_ID='%s'\n" "${ARM_CLIENT_ID}" >>"$ENV_FILE"
77+
elif [[ "$SECRET" == "ARM_CLIENT_SECRET" && -n "${ARM_CLIENT_SECRET:-}" ]]; then
78+
printf "export ARM_CLIENT_SECRET='%s'\n" "${ARM_CLIENT_SECRET}" >>"$ENV_FILE"
79+
elif [[ "$SECRET" == "ARM_TENANT_ID" && -n "${ARM_TENANT_ID:-}" ]]; then
80+
printf "export ARM_TENANT_ID='%s'\n" "${ARM_TENANT_ID}" >>"$ENV_FILE"
81+
elif [[ "$SECRET" == "ARM_SUBSCRIPTION_ID" && -n "${ARM_SUBSCRIPTION_ID:-}" ]]; then
82+
printf "export ARM_SUBSCRIPTION_ID='%s'\n" "${ARM_SUBSCRIPTION_ID}" >>"$ENV_FILE"
7383
elif [[ "$SECRET" == "TG_OCI_TEST_ECR_REPOSITORY" && -n "${TG_OCI_TEST_ECR_REPOSITORY:-}" ]]; then
7484
printf "export TG_OCI_TEST_ECR_REPOSITORY='%s'\n" "${TG_OCI_TEST_ECR_REPOSITORY}" >>"$ENV_FILE"
7585
elif [[ "$SECRET" == "TG_OCI_TEST_GHCR_REPOSITORY" && -n "${TG_OCI_TEST_GHCR_REPOSITORY:-}" ]]; then

.github/workflows/integration-test.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,25 @@ jobs:
114114
AZURE_TENANT_ID,
115115
TG_AZURE_TEST_STORAGE_ACCOUNT,
116116
TG_AZURE_TEST_SUBSCRIPTION_ID,
117+
ARM_CLIENT_ID,
118+
ARM_CLIENT_SECRET,
119+
ARM_TENANT_ID,
120+
ARM_SUBSCRIPTION_ID,
121+
]
122+
- name: Azure Integration
123+
os: ubuntu
124+
target: ./test
125+
tags: azure
126+
run: ^TestAzure
127+
secrets:
128+
[
129+
TG_AZURE_TEST_STORAGE_ACCOUNT,
130+
TG_AZURE_TEST_SUBSCRIPTION_ID,
131+
TG_AZURE_TEST_RESOURCE_GROUP,
132+
ARM_CLIENT_ID,
133+
ARM_CLIENT_SECRET,
134+
ARM_TENANT_ID,
135+
ARM_SUBSCRIPTION_ID,
117136
]
118137
- name: OCI Registries
119138
os: ubuntu
@@ -242,6 +261,11 @@ jobs:
242261
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
243262
TG_AZURE_TEST_STORAGE_ACCOUNT: ${{ secrets.TG_AZURE_TEST_STORAGE_ACCOUNT }}
244263
TG_AZURE_TEST_SUBSCRIPTION_ID: ${{ secrets.TG_AZURE_TEST_SUBSCRIPTION_ID }}
264+
TG_AZURE_TEST_RESOURCE_GROUP: ${{ secrets.TG_AZURE_TEST_RESOURCE_GROUP }}
265+
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
266+
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
267+
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
268+
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
245269
TG_OCI_TEST_ECR_REPOSITORY: ${{ secrets.TG_OCI_TEST_ECR_REPOSITORY }}
246270
TG_OCI_TEST_GHCR_REPOSITORY: ${{ secrets.TG_OCI_TEST_GHCR_REPOSITORY }}
247271
TG_OCI_TEST_GHCR_USERNAME: ${{ secrets.TG_OCI_TEST_GHCR_USERNAME }}

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ linters:
202202

203203
- linters:
204204
- forbidigo
205-
path: ^(internal/cli/commands/browse/tui/model\.go|internal/cli/commands/catalog/cli\.go|internal/cli/commands/catalog/tui/form\.go|internal/cli/commands/catalog/tui/tags_layout\.go|internal/cli/commands/commands\.go|internal/cli/commands/hcl/validate/validate\.go|internal/cli/commands/help/cli\.go|internal/cli/commands/render/render\.go|internal/cli/commands/scaffold/cli\.go|internal/cli/commands/scaffold/scaffold\.go)$
205+
path: ^(internal/cli/commands/browse/tui/model\.go|internal/cli/commands/catalog/cli\.go|internal/view/tui/form/form\.go|internal/cli/commands/catalog/tui/tags_layout\.go|internal/cli/commands/commands\.go|internal/cli/commands/hcl/validate/validate\.go|internal/cli/commands/help/cli\.go|internal/cli/commands/render/render\.go|internal/cli/commands/scaffold/cli\.go|internal/cli/commands/scaffold/scaffold\.go)$
206206
# Env-var-backed flags resolve against the process environment rather
207207
# than the venv's Env map, so every flag parsed from an env var bypasses
208208
# the seam.

docs/src/content/docs/05-community/01-contributing.mdx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,61 @@ gcloud projects add-iam-policy-binding <gcp-project> \
268268
--role="roles/iam.serviceAccountTokenCreator"
269269
```
270270

271+
#### Azure tests
272+
273+
Terragrunt also includes end-to-end tests for the Azure Storage (`azurerm`) remote-state backend. These are prefixed with `TestAzure*` and use the `azure` build tag, because they create and destroy real Azure resources. The `internal/azurehelper` tests use the same build tag; their live round-trip skips when credentials are absent.
274+
275+
```bash
276+
GOFLAGS='-tags=azure' go test ./internal/azurehelper/...
277+
GOFLAGS='-tags=azure' go test -run 'TestAzure*' ./test
278+
```
279+
280+
Unlike some other suites, these fail rather than skip when credentials are missing, so a misconfigured environment is visible instead of silently reporting success.
281+
282+
To successfully run the Azure tests, you must set the following environment variables:
283+
284+
- `TG_AZURE_TEST_SUBSCRIPTION_ID`: The subscription hosting the test storage account. `ARM_SUBSCRIPTION_ID` or `AZURE_SUBSCRIPTION_ID` also work.
285+
- `TG_AZURE_TEST_STORAGE_ACCOUNT`: The name of a pre-existing storage account the tests create containers in. `ARM_STORAGE_ACCOUNT_NAME` also works.
286+
- `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`: Service principal credentials. The `ARM_*` spelling of each is also accepted, and these can be omitted entirely when authenticating with `az login`.
287+
288+
With ARM-capable authentication, the resource group is looked up from the storage account. Set `TG_AZURE_TEST_RESOURCE_GROUP` when using an access key or SAS token, because data-plane credentials cannot perform that lookup.
289+
290+
```bash
291+
export TG_AZURE_TEST_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
292+
export TG_AZURE_TEST_STORAGE_ACCOUNT="tgteststate"
293+
az login
294+
```
295+
296+
The identity used for Azure tests must hold the following roles:
297+
298+
- `Storage Blob Data Contributor`, because the tests authorize blob access with Microsoft Entra rather than an account key.
299+
- `Storage Account Contributor`, because blob versioning and soft delete are management-plane properties of the account. Without it, `TestAzureBackendVersioningConverges` cannot converge them.
300+
301+
You can assign these roles using the following Azure CLI commands:
302+
303+
```bash
304+
RG="/subscriptions/<subscription-id>/resourceGroups/<resource-group>"
305+
306+
az role assignment create \
307+
--assignee "<client-id>" \
308+
--role "Storage Blob Data Contributor" \
309+
--scope "$RG"
310+
311+
az role assignment create \
312+
--assignee "<client-id>" \
313+
--role "Storage Account Contributor" \
314+
--scope "$RG"
315+
```
316+
317+
Scoping both roles to the resource group also lets the tests resolve it from the storage account name, so no subscription-wide assignment is needed.
318+
319+
An account key (`ARM_ACCESS_KEY`) authenticates the blob data plane and is enough for
320+
most of the suite, but it cannot reach the management plane at all, so
321+
`TestAzureBackendVersioningConverges` skips under it. Only an Entra identity runs the
322+
whole suite.
323+
324+
The tests create and delete blob containers with unique names on each run; they never create or delete the storage account itself.
325+
271326
#### Race tests
272327

273328
Given that Terragrunt is a tool that frequently involves concurrently running multiple things at once, there's always a risk for race conditions to occur. As such, there are dedicated tests that are run with the `-race` flag in CI to use golang's built-in tooling for identifying race conditions.

docs/src/data/experiments/azure-backend.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ To transition the `azure-backend` feature to a stable release, the following mus
8888
- [x] `internal/azurehelper` package wrapping the Azure SDK with a builder pattern matching `awshelper`/`gcphelper`.
8989
- [x] Bootstrap of storage accounts and blob containers, including versioning and soft delete convergence.
9090
- [ ] Optional RBAC role assignment for `use_azuread_auth` during bootstrap (the data-plane role must currently be granted manually).
91-
- [x] Delete and migrate operations for state blobs and containers with confirmation prompts.
91+
- [x] Delete operations for state blobs and containers, with confirmation prompts, and state migration within a storage account.
9292
- [ ] Direct state file reads from Azure blobs for `--dependency-fetch-output-from-state`.
9393
- [x] Documentation covering authentication methods, configuration keys, and troubleshooting.
94-
- [ ] End-to-end live coverage against a real subscription for resource group and storage account bootstrap, policy convergence, backend delete, and state migration. The existing live blob round-trip covers only the data-plane helper; hermetic unit tests ship behind the `azure` build tag.
94+
- [x] End-to-end live coverage against a real subscription for container bootstrap, blob versioning convergence, and backend delete, behind the `azure` build tag.
95+
- [ ] End-to-end live coverage for resource group and storage account creation, soft-delete retention convergence, and state migration. The current live tests assume a pre-existing storage account.
9596
- [ ] Community feedback on real-world usage.

internal/azurehelper/blob_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,9 @@ func TestBlobClient_CopyBlob_RequiresArgs(t *testing.T) {
184184
func TestBlob_LiveRoundTrip(t *testing.T) {
185185
t.Parallel()
186186

187-
env := venv.OSVenv().Env
188-
account := env["TG_AZURE_TEST_STORAGE_ACCOUNT"]
189-
sub := env["TG_AZURE_TEST_SUBSCRIPTION_ID"]
187+
testVenv := venv.OSVenv()
188+
account := testVenv.Env["TG_AZURE_TEST_STORAGE_ACCOUNT"]
189+
sub := testVenv.Env["TG_AZURE_TEST_SUBSCRIPTION_ID"]
190190

191191
if account == "" || sub == "" {
192192
t.Skip("TG_AZURE_TEST_STORAGE_ACCOUNT and TG_AZURE_TEST_SUBSCRIPTION_ID are required for live test")
@@ -199,8 +199,9 @@ func TestBlob_LiveRoundTrip(t *testing.T) {
199199
WithSessionConfig(&azurehelper.AzureSessionConfig{
200200
SubscriptionID: sub,
201201
StorageAccountName: account,
202-
UseAzureADAuth: true,
202+
UseAzureADAuth: new(true),
203203
}).
204+
WithVenv(testVenv).
204205
Build(log.New())
205206
require.NoError(t, err, "Build config")
206207

0 commit comments

Comments
 (0)