Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,17 @@ Advanced configuration options.
| Variable | Description | Scope Configuration Property |
|----------|-------------|------------------------------|
| **K8S_MODIFIERS** | JSON string with dynamic modifications to Kubernetes objects | `object_modifiers` |

### ServiceAccounts managed outside the scope

To run pods with an existing ServiceAccount (for example IRSA managed by
OpenTofu), set the scope capability `service_account_name`. Leave it empty to
preserve current behavior. Account-level provider defaults are not consulted
for this opt-in, so other scopes keep their existing identity. Keep managed IAM
disabled; configuring both ownership modes is rejected.

The ServiceAccount must already exist in the scope's Kubernetes namespace and
carry the desired IAM role annotation. The deployment builder only selects it
through `spec.template.spec.serviceAccountName`; the scope does not create or
delete the external ServiceAccount or its IAM role. Existing managed-IAM naming
and unconfigured defaults remain unchanged.
19 changes: 17 additions & 2 deletions k8s/deployment/build_context
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,24 @@ fi

IAM_ENABLED=$(echo "$IAM" | jq -r '.ENABLED // false')

SERVICE_ACCOUNT_NAME=""
# Externally managed ServiceAccounts (for example, IRSA declared in IaC) are
# selected for the pod without enabling the scope's role/SA lifecycle manager.
# Explicit scope opt-in: do not start using inherited account-level defaults
# that older deployments previously ignored.
EXTERNAL_SERVICE_ACCOUNT_NAME=$(echo "$CONTEXT" | jq -r '.scope.capabilities.service_account_name // empty')

if [[ "$IAM_ENABLED" == "true" ]]; then
SERVICE_ACCOUNT_NAME=""
if [[ -n "$EXTERNAL_SERVICE_ACCOUNT_NAME" ]]; then
if [[ "$IAM_ENABLED" == "true" ]]; then
log error "Configure either an external service_account_name or managed IAM, not both"
exit 1
fi
if [[ ${#EXTERNAL_SERVICE_ACCOUNT_NAME} -gt 253 || ! "$EXTERNAL_SERVICE_ACCOUNT_NAME" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ ]]; then
log error "Invalid external service_account_name: expected a Kubernetes DNS subdomain name"
exit 1
fi
SERVICE_ACCOUNT_NAME="$EXTERNAL_SERVICE_ACCOUNT_NAME"
elif [[ "$IAM_ENABLED" == "true" ]]; then
SERVICE_ACCOUNT_NAME=$(echo "$IAM" | jq -r .PREFIX)-"$SCOPE_ID"
fi

Expand Down
65 changes: 65 additions & 0 deletions k8s/deployment/tests/build_context.bats
Original file line number Diff line number Diff line change
Expand Up @@ -1302,3 +1302,68 @@ EOF

assert_equal "$(echo "$CONTEXT" | jq -r '.main_traffic_manager_port')" "10080"
}

# External ServiceAccounts are selected by the real context builder, while IAM
# role creation/deletion stays disabled for resources owned by IaC.
@test "external service account: uses the explicitly selected scope account" {
setup_full_build_context
export IAM='{"ENABLED":false}'
CONTEXT=$(echo "$CONTEXT" | jq '.scope.capabilities.service_account_name = "ui-plugins-stage"')

source "$SCRIPT"

assert_equal "$(echo "$CONTEXT" | jq -r .service_account_name)" "ui-plugins-stage"
assert_equal "$IAM_ENABLED" "false"
}

@test "external service account: ignores inherited account defaults without scope opt-in" {
setup_full_build_context
export IAM='{"ENABLED":false}'
CONTEXT=$(echo "$CONTEXT" | jq '.providers["container-orchestration"].security.service_account_name = "base-sa"')

source "$SCRIPT"

assert_equal "$(echo "$CONTEXT" | jq -r .service_account_name)" ""
}

@test "external service account: keeps managed IAM name when no external name is configured" {
setup_full_build_context
export IAM='{"ENABLED":true,"PREFIX":"managed"}'

source "$SCRIPT"

assert_equal "$(echo "$CONTEXT" | jq -r .service_account_name)" "managed-test-scope-123"
}

@test "external service account: leaves the pod default when neither mode is configured" {
setup_full_build_context
export IAM='{"ENABLED":false}'

source "$SCRIPT"

assert_equal "$(echo "$CONTEXT" | jq -r .service_account_name)" ""
}

@test "external service account: rejects conflicting lifecycle ownership" {
setup_full_build_context
export IAM='{"ENABLED":true,"PREFIX":"managed"}'
CONTEXT=$(echo "$CONTEXT" | jq '.scope.capabilities.service_account_name = "external-sa"')
export CONTEXT

run bash -c 'source "$SCRIPT"'

[ "$status" -ne 0 ]
assert_contains "$output" "either an external service_account_name or managed IAM"
}

@test "external service account: rejects names that could inject YAML" {
setup_full_build_context
export IAM='{"ENABLED":false}'
CONTEXT=$(echo "$CONTEXT" | jq '.scope.capabilities.service_account_name = "bad\nname"')
export CONTEXT

run bash -c 'source "$SCRIPT"'

[ "$status" -ne 0 ]
assert_contains "$output" "Invalid external service_account_name"
}
18 changes: 18 additions & 0 deletions k8s/specs/service-spec.json.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@
}
},
"elements":[
{
"type":"Category",
"label":"ServiceAccount",
"elements":[
{
"type":"Control",
"scope":"#/properties/service_account_name"
}
]
},
{
"type":"Category",
"label":"Resources",
Expand Down Expand Up @@ -341,6 +351,14 @@
"export":false,
"default":"docker-image"
},
"service_account_name": {
"type": "string",
"title": "Existing ServiceAccount",
"default": "",
"maxLength": 253,
"pattern": "^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$",
"description": "Use an existing Kubernetes ServiceAccount managed outside this scope. Leave empty for the default. Requires managed IAM to remain disabled."
},
"ram_memory":{
"type":"integer",
"oneOf":[
Expand Down
Loading