Skip to content

Commit 17d2fb3

Browse files
authored
Run live Workload Identity (AKS) test in weekly pipeline (Azure#22627)
1 parent afea09b commit 17d2fb3

File tree

7 files changed

+199
-35
lines changed

7 files changed

+199
-35
lines changed

sdk/azidentity/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# live test artifacts
2+
Dockerfile
3+
k8s.yaml
4+
sshkey*

sdk/azidentity/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "go",
44
"TagPrefix": "go/azidentity",
5-
"Tag": "go/azidentity_db4a26f583"
5+
"Tag": "go/azidentity_0a700f9ea1"
66
}

sdk/azidentity/test-resources-post.ps1

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44
# IMPORTANT: Do not invoke this file directly. Please instead run eng/common/TestResources/New-TestResources.ps1 from the repository root.
55

66
param (
7-
[hashtable] $AdditionalParameters = @{},
8-
[hashtable] $DeploymentOutputs
7+
[hashtable] $AdditionalParameters = @{},
8+
[hashtable] $DeploymentOutputs
99
)
1010

11-
if (!$AdditionalParameters['deployResources']) {
11+
$ErrorActionPreference = 'Stop'
12+
$PSNativeCommandUseErrorActionPreference = $true
13+
14+
if ($CI) {
15+
if (!$AdditionalParameters['deployResources']) {
1216
Write-Host "Skipping post-provisioning script because resources weren't deployed"
1317
return
18+
}
19+
az login --service-principal -u $DeploymentOutputs['AZIDENTITY_CLIENT_ID'] -p $DeploymentOutputs['AZIDENTITY_CLIENT_SECRET'] --tenant $DeploymentOutputs['AZIDENTITY_TENANT_ID']
20+
az account set --subscription $DeploymentOutputs['AZIDENTITY_SUBSCRIPTION_ID']
1421
}
1522

16-
$ErrorActionPreference = 'Stop'
17-
$PSNativeCommandUseErrorActionPreference = $true
18-
1923
Write-Host "Building container"
20-
$image = "azidentity-managed-id-test"
24+
$image = "$($DeploymentOutputs['AZIDENTITY_ACR_LOGIN_SERVER'])/azidentity-managed-id-test"
2125
Set-Content -Path "$PSScriptRoot/Dockerfile" -Value @"
2226
FROM mcr.microsoft.com/oss/go/microsoft/golang:latest as builder
2327
ENV GOARCH=amd64 GOWORK=off
@@ -34,16 +38,64 @@ CMD ["./managed-id-test"]
3438
"@
3539
# build from sdk/azidentity because we need that dir in the context (because the test app uses local azidentity)
3640
docker build -t $image "$PSScriptRoot"
41+
az acr login -n $DeploymentOutputs['AZIDENTITY_ACR_NAME']
42+
docker push $image
3743

38-
az login --service-principal -u $DeploymentOutputs['AZIDENTITY_CLIENT_ID'] -p $DeploymentOutputs['AZIDENTITY_CLIENT_SECRET'] --tenant $DeploymentOutputs['AZIDENTITY_TENANT_ID']
39-
az account set --subscription $DeploymentOutputs['AZIDENTITY_SUBSCRIPTION_ID']
44+
$rg = $DeploymentOutputs['AZIDENTITY_RESOURCE_GROUP']
4045

4146
# Azure Functions deployment: copy the Windows binary from the Docker image, deploy it in a zip
4247
Write-Host "Deploying to Azure Functions"
4348
$container = docker create $image
4449
docker cp ${container}:managed-id-test.exe "$PSScriptRoot/testdata/managed-id-test/"
4550
docker rm -v $container
4651
Compress-Archive -Path "$PSScriptRoot/testdata/managed-id-test/*" -DestinationPath func.zip -Force
47-
az functionapp deploy -g $DeploymentOutputs['AZIDENTITY_RESOURCE_GROUP'] -n $DeploymentOutputs['AZIDENTITY_FUNCTION_NAME'] --src-path func.zip --type zip
52+
az functionapp deploy -g $rg -n $DeploymentOutputs['AZIDENTITY_FUNCTION_NAME'] --src-path func.zip --type zip
53+
54+
Write-Host "Creating federated identity"
55+
$aksName = $DeploymentOutputs['AZIDENTITY_AKS_NAME']
56+
$idName = $DeploymentOutputs['AZIDENTITY_USER_ASSIGNED_IDENTITY_NAME']
57+
$issuer = az aks show -g $rg -n $aksName --query "oidcIssuerProfile.issuerUrl" -otsv
58+
$podName = "azidentity-test"
59+
$serviceAccountName = "workload-identity-sa"
60+
az identity federated-credential create -g $rg --identity-name $idName --issuer $issuer --name $idName --subject system:serviceaccount:default:$serviceAccountName
61+
Write-Host "Deploying to AKS"
62+
az aks get-credentials -g $rg -n $aksName
63+
az aks update --attach-acr $DeploymentOutputs['AZIDENTITY_ACR_NAME'] -g $rg -n $aksName
64+
Set-Content -Path "$PSScriptRoot/k8s.yaml" -Value @"
65+
apiVersion: v1
66+
kind: ServiceAccount
67+
metadata:
68+
annotations:
69+
azure.workload.identity/client-id: $($DeploymentOutputs['AZIDENTITY_USER_ASSIGNED_IDENTITY_CLIENT_ID'])
70+
name: $serviceAccountName
71+
namespace: default
72+
---
73+
apiVersion: v1
74+
kind: Pod
75+
metadata:
76+
name: $podName
77+
namespace: default
78+
labels:
79+
app: $podName
80+
azure.workload.identity/use: "true"
81+
spec:
82+
serviceAccountName: $serviceAccountName
83+
containers:
84+
- name: $podName
85+
image: $image
86+
env:
87+
- name: AZIDENTITY_STORAGE_NAME
88+
value: $($DeploymentOutputs['AZIDENTITY_STORAGE_NAME_USER_ASSIGNED'])
89+
- name: AZIDENTITY_USE_WORKLOAD_IDENTITY
90+
value: "true"
91+
- name: FUNCTIONS_CUSTOMHANDLER_PORT
92+
value: "80"
93+
nodeSelector:
94+
kubernetes.io/os: linux
95+
"@
96+
kubectl apply -f "$PSScriptRoot/k8s.yaml"
97+
Write-Host "##vso[task.setvariable variable=AZIDENTITY_POD_NAME;]$podName"
4898

49-
az logout
99+
if ($CI) {
100+
az logout
101+
}

sdk/azidentity/test-resources-pre.ps1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ param (
1212
$RemainingArguments
1313
)
1414

15+
if (-not (Test-Path "$PSScriptRoot/sshkey.pub")) {
16+
ssh-keygen -t rsa -b 4096 -f "$PSScriptRoot/sshkey" -N '' -C ''
17+
}
18+
$templateFileParameters['sshPubKey'] = Get-Content "$PSScriptRoot/sshkey.pub"
19+
1520
if (!$CI) {
1621
# TODO: Remove this once auto-cloud config downloads are supported locally
1722
Write-Host "Skipping cert setup in local testing mode"

sdk/azidentity/test-resources.bicep

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
@description('Kubernetes cluster admin user name.')
5+
param adminUser string = 'azureuser'
6+
47
@minLength(6)
58
@maxLength(23)
69
@description('The base resource name.')
@@ -9,6 +12,8 @@ param baseName string = resourceGroup().name
912
@description('Whether to deploy resources. When set to false, this file deploys nothing.')
1013
param deployResources bool = false
1114

15+
param sshPubKey string = ''
16+
1217
@description('The location of the resource. By default, this is the same as the resource group.')
1318
param location string = resourceGroup().location
1419

@@ -18,7 +23,7 @@ var blobContributor = subscriptionResourceId('Microsoft.Authorization/roleDefini
1823
resource sa 'Microsoft.Storage/storageAccounts@2021-08-01' = if (deployResources) {
1924
kind: 'StorageV2'
2025
location: location
21-
name: baseName
26+
name: 'sa${uniqueString(baseName)}'
2227
properties: {
2328
accessTier: 'Hot'
2429
}
@@ -30,7 +35,7 @@ resource sa 'Microsoft.Storage/storageAccounts@2021-08-01' = if (deployResources
3035
resource saUserAssigned 'Microsoft.Storage/storageAccounts@2021-08-01' = if (deployResources) {
3136
kind: 'StorageV2'
3237
location: location
33-
name: '${baseName}2'
38+
name: 'sa2${uniqueString(baseName)}'
3439
properties: {
3540
accessTier: 'Hot'
3641
}
@@ -64,6 +69,17 @@ resource blobRoleFunc 'Microsoft.Authorization/roleAssignments@2022-04-01' = if
6469
scope: sa
6570
}
6671

72+
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = if (deployResources) {
73+
location: location
74+
name: uniqueString(resourceGroup().id)
75+
properties: {
76+
adminUserEnabled: true
77+
}
78+
sku: {
79+
name: 'Basic'
80+
}
81+
}
82+
6783
resource farm 'Microsoft.Web/serverfarms@2021-03-01' = if (deployResources) {
6884
kind: 'app'
6985
location: location
@@ -135,7 +151,57 @@ resource azfunc 'Microsoft.Web/sites@2021-03-01' = if (deployResources) {
135151
}
136152
}
137153

154+
resource aks 'Microsoft.ContainerService/managedClusters@2023-06-01' = if (deployResources) {
155+
name: baseName
156+
location: location
157+
identity: {
158+
type: 'SystemAssigned'
159+
}
160+
properties: {
161+
agentPoolProfiles: [
162+
{
163+
count: 1
164+
enableAutoScaling: false
165+
kubeletDiskType: 'OS'
166+
mode: 'System'
167+
name: 'agentpool'
168+
osDiskSizeGB: 128
169+
osDiskType: 'Managed'
170+
osSKU: 'Ubuntu'
171+
osType: 'Linux'
172+
type: 'VirtualMachineScaleSets'
173+
vmSize: 'Standard_D2s_v3'
174+
}
175+
]
176+
dnsPrefix: 'identitytest'
177+
enableRBAC: true
178+
linuxProfile: {
179+
adminUsername: adminUser
180+
ssh: {
181+
publicKeys: [
182+
{
183+
keyData: sshPubKey
184+
}
185+
]
186+
}
187+
}
188+
oidcIssuerProfile: {
189+
enabled: true
190+
}
191+
securityProfile: {
192+
workloadIdentity: {
193+
enabled: true
194+
}
195+
}
196+
}
197+
}
198+
199+
output AZIDENTITY_ACR_LOGIN_SERVER string = deployResources ? containerRegistry.properties.loginServer : ''
200+
output AZIDENTITY_ACR_NAME string = deployResources ? containerRegistry.name : ''
201+
output AZIDENTITY_AKS_NAME string = deployResources ? aks.name : ''
138202
output AZIDENTITY_FUNCTION_NAME string = deployResources ? azfunc.name : ''
139203
output AZIDENTITY_STORAGE_NAME string = deployResources ? sa.name : ''
140204
output AZIDENTITY_STORAGE_NAME_USER_ASSIGNED string = deployResources ? saUserAssigned.name : ''
141205
output AZIDENTITY_USER_ASSIGNED_IDENTITY string = deployResources ? usermgdid.id : ''
206+
output AZIDENTITY_USER_ASSIGNED_IDENTITY_CLIENT_ID string = deployResources ? usermgdid.properties.clientId : ''
207+
output AZIDENTITY_USER_ASSIGNED_IDENTITY_NAME string = deployResources ? usermgdid.name : ''

sdk/azidentity/testdata/managed-id-test/main.go

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,38 @@ import (
1818

1919
var (
2020
config = struct {
21-
id string
22-
storageName string
21+
// resourceID of a managed identity permitted to list blobs in the account specified by storageNameUserAssigned.
22+
resourceID string
23+
// storageName is the name of a storage account accessible by the default or system-assigned identity
24+
storageName string
25+
// storageNameUserAssigned is the name of a storage account accessible by the identity specified by
26+
// resourceID. The default or system-assigned identity shouldn't have any permission for this account.
2327
storageNameUserAssigned string
28+
// workloadID determines whether this app tests ManagedIdentityCredential or WorkloadIdentityCredential.
29+
// When true, the app ignores resourceID and storageNameUserAssigned.
30+
workloadID bool
2431
}{
25-
id: os.Getenv("AZIDENTITY_USER_ASSIGNED_IDENTITY"),
32+
resourceID: os.Getenv("AZIDENTITY_USER_ASSIGNED_IDENTITY"),
2633
storageName: os.Getenv("AZIDENTITY_STORAGE_NAME"),
2734
storageNameUserAssigned: os.Getenv("AZIDENTITY_STORAGE_NAME_USER_ASSIGNED"),
35+
workloadID: os.Getenv("AZIDENTITY_USE_WORKLOAD_IDENTITY") != "",
2836
}
2937

3038
missingConfig string
3139
)
3240

41+
func credential(resourceID string) (azcore.TokenCredential, error) {
42+
if config.workloadID {
43+
// the identity is determined by service account configuration
44+
return azidentity.NewWorkloadIdentityCredential(nil)
45+
}
46+
opts := azidentity.ManagedIdentityCredentialOptions{}
47+
if resourceID != "" {
48+
opts.ID = azidentity.ResourceID(resourceID)
49+
}
50+
return azidentity.NewManagedIdentityCredential(&opts)
51+
}
52+
3353
func listContainers(account string, cred azcore.TokenCredential) error {
3454
url := fmt.Sprintf("https://%s.blob.core.windows.net", account)
3555
log.Printf("listing containers in %s", url)
@@ -48,20 +68,14 @@ func handler(w http.ResponseWriter, r *http.Request) {
4868
return
4969
}
5070

51-
cred, err := azidentity.NewManagedIdentityCredential(nil)
52-
if err != nil {
53-
w.WriteHeader(http.StatusInternalServerError)
54-
fmt.Fprint(w, err)
55-
log.Print(err)
56-
return
57-
}
58-
err = listContainers(config.storageName, cred)
71+
cred, err := credential("")
5972
if err == nil {
60-
cred, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
61-
ID: azidentity.ResourceID(config.id),
62-
})
63-
if err == nil {
64-
err = listContainers(config.storageNameUserAssigned, cred)
73+
err = listContainers(config.storageName, cred)
74+
if !config.workloadID && err == nil {
75+
cred, err = credential(config.resourceID)
76+
if err == nil {
77+
err = listContainers(config.storageNameUserAssigned, cred)
78+
}
6579
}
6680
}
6781

@@ -77,14 +91,19 @@ func handler(w http.ResponseWriter, r *http.Request) {
7791

7892
func main() {
7993
v := []string{}
80-
if config.id == "" {
81-
v = append(v, "AZIDENTITY_USER_ASSIGNED_IDENTITY")
82-
}
8394
if config.storageName == "" {
8495
v = append(v, "AZIDENTITY_STORAGE_NAME")
8596
}
86-
if config.storageNameUserAssigned == "" {
87-
v = append(v, "AZIDENTITY_STORAGE_NAME_USER_ASSIGNED")
97+
if config.workloadID {
98+
log.Print("Testing WorkloadIdentityCredential")
99+
} else {
100+
log.Print("Testing ManagedIdentityCredential")
101+
if config.resourceID == "" {
102+
v = append(v, "AZIDENTITY_USER_ASSIGNED_IDENTITY")
103+
}
104+
if config.storageNameUserAssigned == "" {
105+
v = append(v, "AZIDENTITY_STORAGE_NAME_USER_ASSIGNED")
106+
}
88107
}
89108
if len(v) > 0 {
90109
missingConfig = strings.Join(v, ", ")

sdk/azidentity/workload_identity_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"fmt"
1717
"net/http"
1818
"os"
19+
"os/exec"
1920
"path/filepath"
2021
"strconv"
2122
"strings"
@@ -25,6 +26,7 @@ import (
2526
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
2627
"github.com/golang-jwt/jwt/v5"
2728
"github.com/google/uuid"
29+
"github.com/stretchr/testify/require"
2830
)
2931

3032
func assertion(cert *x509.Certificate, key crypto.PrivateKey) (string, error) {
@@ -46,6 +48,22 @@ func assertion(cert *x509.Certificate, key crypto.PrivateKey) (string, error) {
4648
}
4749

4850
func TestWorkloadIdentityCredential_Live(t *testing.T) {
51+
// This test triggers the managed identity test app deployed to Azure Kubernetes Service.
52+
// See the bicep file and test resources scripts for details.
53+
// It triggers the app with kubectl because the test subscription prohibits opening ports to the internet.
54+
pod := os.Getenv("AZIDENTITY_POD_NAME")
55+
if pod == "" {
56+
t.Skip("set AZIDENTITY_POD_NAME to run this test")
57+
}
58+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
59+
defer cancel()
60+
cmd := exec.CommandContext(ctx, "kubectl", "exec", pod, "--", "wget", "-qO-", "localhost")
61+
b, err := cmd.CombinedOutput()
62+
require.NoError(t, err)
63+
require.EqualValues(t, "test passed", b)
64+
}
65+
66+
func TestWorkloadIdentityCredential_Recorded(t *testing.T) {
4967
cert, err := os.ReadFile(liveSP.pemPath)
5068
if err != nil {
5169
t.Fatal(err)

0 commit comments

Comments
 (0)