Skip to content

Commit fc4a6f2

Browse files
furkhatFurkhat Kasymov Genii UuluValyaB
authored
onboarding with workload identity authentication (#126)
* onboarding with workload identity authentication * tests * run tests in PR * fix mock data in tests --------- Co-authored-by: Furkhat Kasymov Genii Uulu <furkhat@cast.ai> Co-authored-by: Valentyna Bukhalova <valentyna@cast.ai>
1 parent 1e22a98 commit fc4a6f2

File tree

8 files changed

+801
-10
lines changed

8 files changed

+801
-10
lines changed

.github/workflows/test.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Run Tests
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
push:
8+
branches:
9+
- main
10+
11+
jobs:
12+
tofu_test:
13+
strategy:
14+
matrix:
15+
version: ["1.10", latest]
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Set up OpenTofu
23+
uses: opentofu/setup-opentofu@v1
24+
with:
25+
tofu_version: ${{ matrix.version }}
26+
27+
- name: Print tofu version
28+
run: tofu version
29+
30+
- name: OpenTofu init
31+
run: tofu init || tofu init -upgrade
32+
33+
- name: Run OpenTofu tests
34+
run: tofu test -verbose

iam.tf

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
locals {
2-
role_name = "CastAKSRole-${var.aks_cluster_name}-tf"
3-
app_name = substr("CAST AI ${var.aks_cluster_name}-${var.resource_group}", 0, 64)
2+
role_name = "CastAKSRole-${var.aks_cluster_name}-tf"
3+
app_name = substr("CAST AI ${var.aks_cluster_name}-${var.resource_group}", 0, 64)
4+
federated_identity_name = substr("castai-${var.aks_cluster_name}-${var.resource_group}", 0, 64)
5+
}
6+
7+
data "azurerm_kubernetes_cluster" "castai" {
8+
count = var.authentication_method == "workload_identity" ? 1 : 0
9+
name = var.aks_cluster_name
10+
resource_group_name = var.resource_group
411
}
512

613
// Azure RM
@@ -60,22 +67,22 @@ resource "azurerm_role_definition" "castai" {
6067
}
6168

6269
resource "azurerm_role_assignment" "castai_resource_group" {
63-
principal_id = azuread_service_principal.castai.object_id
70+
principal_id = var.authentication_method == "client_secret" ? azuread_service_principal.castai[0].object_id : azurerm_user_assigned_identity.this[0].principal_id
6471
role_definition_id = azurerm_role_definition.castai.role_definition_resource_id
6572
description = "castai role assignment for resource group ${var.resource_group}"
6673
scope = "/subscriptions/${var.subscription_id}/resourceGroups/${var.resource_group}"
6774
}
6875

6976
resource "azurerm_role_assignment" "castai_node_resource_group" {
70-
principal_id = azuread_service_principal.castai.object_id
77+
principal_id = var.authentication_method == "client_secret" ? azuread_service_principal.castai[0].object_id : azurerm_user_assigned_identity.this[0].principal_id
7178
role_definition_id = azurerm_role_definition.castai.role_definition_resource_id
7279
description = "castai role assignment for resource group ${var.aks_cluster_name}"
7380
scope = "/subscriptions/${var.subscription_id}/resourceGroups/${var.node_resource_group}"
7481
}
7582

7683
resource "azurerm_role_assignment" "castai_additional_resource_groups" {
7784
for_each = toset(var.additional_resource_groups)
78-
principal_id = azuread_service_principal.castai.object_id
85+
principal_id = var.authentication_method == "client_secret" ? azuread_service_principal.castai[0].object_id : azurerm_user_assigned_identity.this[0].principal_id
7986
description = "castai role assignment for resource group ${each.key}"
8087
role_definition_id = azurerm_role_definition.castai.role_definition_resource_id
8188
scope = each.key
@@ -86,16 +93,58 @@ resource "azurerm_role_assignment" "castai_additional_resource_groups" {
8693
data "azuread_client_config" "current" {}
8794

8895
resource "azuread_application" "castai" {
96+
count = var.authentication_method == "client_secret" ? 1 : 0
8997
display_name = local.app_name
9098
owners = (var.azuread_owners == null ? [data.azuread_client_config.current.object_id] : var.azuread_owners)
9199
}
92100

93101
resource "azuread_application_password" "castai" {
94-
application_id = azuread_application.castai.id
102+
count = var.authentication_method == "client_secret" ? 1 : 0
103+
application_id = azuread_application.castai[0].id
95104
}
96105

97106
resource "azuread_service_principal" "castai" {
98-
client_id = azuread_application.castai.client_id
107+
count = var.authentication_method == "client_secret" ? 1 : 0
108+
client_id = azuread_application.castai[0].client_id
99109
app_role_assignment_required = false
100110
owners = (var.azuread_owners == null ? [data.azuread_client_config.current.object_id] : var.azuread_owners)
101111
}
112+
113+
# State migration for existing users upgrading to authentication_method variable
114+
moved {
115+
from = azuread_application.castai
116+
to = azuread_application.castai[0]
117+
}
118+
119+
moved {
120+
from = azuread_application_password.castai
121+
to = azuread_application_password.castai[0]
122+
}
123+
124+
moved {
125+
from = azuread_service_principal.castai
126+
to = azuread_service_principal.castai[0]
127+
}
128+
129+
// Workload Identity
130+
131+
data "castai_impersonation_service_account" "this" {
132+
count = var.authentication_method == "workload_identity" ? 1 : 0
133+
}
134+
135+
resource "azurerm_user_assigned_identity" "this" {
136+
count = var.authentication_method == "workload_identity" ? 1 : 0
137+
name = "${var.aks_cluster_name}-castai-identity"
138+
resource_group_name = var.resource_group
139+
location = data.azurerm_kubernetes_cluster.castai[0].location
140+
}
141+
142+
resource "azurerm_federated_identity_credential" "this" {
143+
count = var.authentication_method == "workload_identity" ? 1 : 0
144+
name = local.federated_identity_name
145+
resource_group_name = var.resource_group
146+
audience = ["api://AzureADTokenExchange"]
147+
issuer = "https://accounts.google.com"
148+
parent_id = azurerm_user_assigned_identity.this[0].id
149+
subject = data.castai_impersonation_service_account.this[0].id
150+
}

main.tf

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ resource "castai_aks_cluster" "castai_cluster" {
99
region = var.aks_cluster_region
1010
subscription_id = var.subscription_id
1111
tenant_id = var.tenant_id
12-
client_id = azuread_application.castai.client_id
13-
client_secret = azuread_application_password.castai.value
12+
client_id = var.authentication_method == "client_secret" ? azuread_application.castai[0].client_id : azurerm_user_assigned_identity.this[0].client_id
13+
client_secret = var.authentication_method == "client_secret" ? azuread_application_password.castai[0].value : null
14+
federation_id = var.authentication_method == "workload_identity" ? azurerm_user_assigned_identity.this[0].principal_id : null
1415

1516
node_resource_group = var.node_resource_group
1617
delete_nodes_on_disconnect = var.delete_nodes_on_disconnect
@@ -35,7 +36,9 @@ resource "castai_aks_cluster" "castai_cluster" {
3536
azurerm_role_assignment.castai_additional_resource_groups,
3637
azuread_application.castai,
3738
azuread_application_password.castai,
38-
azuread_service_principal.castai
39+
azuread_service_principal.castai,
40+
azurerm_user_assigned_identity.this,
41+
azurerm_federated_identity_credential.this
3942
]
4043
}
4144

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
mock_provider "azurerm" {
2+
mock_data "azurerm_kubernetes_cluster" {
3+
defaults = {
4+
location = "eastus"
5+
fqdn = "test-cluster.eastus.azmk8s.io"
6+
network_profile = [{
7+
pod_cidr = "10.244.0.0/16"
8+
service_cidr = "10.0.0.0/16"
9+
dns_service_ip = "10.0.0.10"
10+
docker_bridge_cidr = "172.17.0.1/16"
11+
load_balancer_sku = "standard"
12+
network_plugin = "azure"
13+
network_policy = "azure"
14+
}]
15+
}
16+
}
17+
}
18+
19+
mock_provider "azuread" {
20+
mock_data "azuread_client_config" {
21+
defaults = {
22+
object_id = "00000000-0000-0000-0000-000000000000"
23+
}
24+
}
25+
26+
mock_resource "azuread_application" {
27+
defaults = {
28+
client_id = "22222222-2222-2222-2222-222222222222"
29+
id = "/applications/22222222-2222-2222-2222-222222222222"
30+
object_id = "33333333-3333-3333-3333-333333333333"
31+
}
32+
}
33+
34+
mock_resource "azuread_application_password" {
35+
defaults = {
36+
id = "/applications/22222222-2222-2222-2222-222222222222/passwords/44444444-4444-4444-4444-444444444444"
37+
value = "mock-password-value"
38+
}
39+
}
40+
41+
mock_resource "azuread_service_principal" {
42+
defaults = {
43+
id = "/servicePrincipals/55555555-5555-5555-5555-555555555555"
44+
object_id = "55555555-5555-5555-5555-555555555555"
45+
}
46+
}
47+
}
48+
49+
mock_provider "castai" {
50+
mock_resource "castai_aks_cluster" {
51+
defaults = {
52+
id = "88888888-8888-8888-8888-888888888888"
53+
}
54+
}
55+
56+
mock_resource "castai_node_configuration" {
57+
defaults = {
58+
id = "99999999-9999-9999-9999-999999999999"
59+
}
60+
}
61+
62+
mock_data "castai_impersonation_service_account" {
63+
defaults = {
64+
id = "system:serviceaccount:castai-agent:castai-agent"
65+
}
66+
}
67+
}
68+
69+
mock_provider "helm" {}
70+
mock_provider "null" {}
71+
72+
variables {
73+
aks_cluster_name = "test-cluster"
74+
aks_cluster_region = "eastus"
75+
subscription_id = "00000000-0000-0000-0000-000000000000"
76+
tenant_id = "11111111-1111-1111-1111-111111111111"
77+
resource_group = "test-rg"
78+
node_resource_group = "test-node-rg"
79+
castai_api_token = "test-token-12345"
80+
default_node_configuration = "default"
81+
node_configurations = {
82+
default = {
83+
name = "default"
84+
subnets = ["/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"]
85+
}
86+
}
87+
}
88+
89+
run "verify_default_authentication_method" {
90+
command = plan
91+
92+
assert {
93+
condition = var.authentication_method == "client_secret"
94+
error_message = "Default authentication_method should be 'client_secret'"
95+
}
96+
}
97+
98+
run "verify_client_secret_resources_created" {
99+
command = plan
100+
101+
assert {
102+
condition = length([for r in azuread_application.castai : r]) == 1
103+
error_message = "Azure AD application should be created for client_secret auth"
104+
}
105+
106+
assert {
107+
condition = length([for r in azuread_application_password.castai : r]) == 1
108+
error_message = "Azure AD application password should be created for client_secret auth"
109+
}
110+
111+
assert {
112+
condition = length([for r in azuread_service_principal.castai : r]) == 1
113+
error_message = "Azure AD service principal should be created for client_secret auth"
114+
}
115+
}
116+
117+
run "verify_workload_identity_resources_not_created" {
118+
command = plan
119+
120+
assert {
121+
condition = length([for r in azurerm_user_assigned_identity.this : r]) == 0
122+
error_message = "Managed identity should NOT be created for client_secret auth"
123+
}
124+
125+
assert {
126+
condition = length([for r in azurerm_federated_identity_credential.this : r]) == 0
127+
error_message = "Federated identity credential should NOT be created for client_secret auth"
128+
}
129+
130+
assert {
131+
condition = length([for r in data.azurerm_kubernetes_cluster.castai : r]) == 0
132+
error_message = "AKS cluster data source should NOT be queried for client_secret auth"
133+
}
134+
135+
assert {
136+
condition = length([for r in data.castai_impersonation_service_account.this : r]) == 0
137+
error_message = "CAST AI impersonation service account should NOT be queried for client_secret auth"
138+
}
139+
}
140+
141+
run "verify_cluster_configuration_client_secret" {
142+
command = plan
143+
144+
assert {
145+
condition = castai_aks_cluster.castai_cluster.federation_id == null
146+
error_message = "federation_id should be null for client_secret auth"
147+
}
148+
}

0 commit comments

Comments
 (0)