Skip to content

Commit aa8221a

Browse files
author
Martin Jackson
committed
Expand spoke logic
1 parent 7dcf1e7 commit aa8221a

14 files changed

Lines changed: 385 additions & 90 deletions

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,32 @@ loading local secrets files into VP secrets stores.
1414
`vault_utils` can read `ssCsiWorkloadAuth` entries from clustergroup values and
1515
create Vault Kubernetes auth roles for hub and spoke workloads.
1616

17+
By default it loads **merged** clustergroup YAML from an in-cluster `ConfigMap`
18+
named `values-<main_clustergroupname>` in `openshift-gitops` (override with
19+
`vault_ss_csi_clustergroup_configmap_namespace` and
20+
`vault_ss_csi_clustergroup_configmap_name`). It looks for a data key such as
21+
`values.yaml` unless you set `vault_ss_csi_clustergroup_configmap_key`. The
22+
document must include a top-level `clusterGroup` key. If the `ConfigMap` is
23+
missing or unusable, it falls back to
24+
`pattern_dir/values-<main_clustergroupname>.yaml` when
25+
`vault_ss_csi_fallback_local_clustergroup_file` is true.
26+
1727
At the application level (`clusterGroup.applications.<app>`), the relevant
1828
inputs are:
1929

2030
- `ssCsiWorkloadAuth` (list)
2131
- `ssCsiWorkloadAuth[].serviceAccount` (required)
2232
- `ssCsiWorkloadAuth[].namespace` (optional)
23-
- `ssCsiWorkloadAuth[].cluster` (optional)
24-
- `ssCsiWorkloadAuth[].roleSlug` / `role_slug` (optional)
33+
- `ssCsiWorkloadAuth[].cluster` (optional): matching hint for **which** spoke a
34+
row applies to (managed cluster group name, `ManagedCluster` name, spoke FQDN
35+
/ `vault_path`, or `clusterGroup` label). For Vault writes, spokes are
36+
normalized to **`vault_path`** (full DNS), same as External Secrets.
37+
- `ssCsiWorkloadAuth[].roleSlug` / `role_slug` (optional): suffix only; Vault
38+
role is **`<mount>-sscsi-<slug>`** where **`<mount>`** is hub **`hub`** (or
39+
configured hub path) or the spoke **`vault_path`**. When using the
40+
**vp-sscsi-spc** chart, `spec.parameters.roleName` uses the same **mount**
41+
as `vaultKubernetesMountPath` (typically **`global.clusterDomain`** on
42+
spokes), not the short `cluster` value.
2543
- application `namespace` (optional default for entry namespace)
2644

2745
CA material management for SS CSI is not handled in this collection anymore.

playbooks/vault.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
connection: local
55
gather_facts: false
66
roles:
7-
# Resolves pattern_dir (PATTERN_DIR / PWD) and loads main.clusterGroupName as main_clustergroup
8-
# so vault_ss_csi_workload_auth can read values-<clustergroup>.yaml for ssCsiWorkloadAuth.
7+
# Resolves pattern_dir (PATTERN_DIR / PWD) and loads main.clusterGroupName as main_clustergroup.
8+
# vault_ss_csi_workload_auth prefers merged clustergroup YAML from an in-cluster ConfigMap, then file fallback.
99
- pattern_settings
1010
- find_vp_secrets
1111
- cluster_pre_check

roles/vault_utils/README.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,46 @@ This role can create Vault Kubernetes auth roles from
6060
`clusterGroup.applications.*.ssCsiWorkloadAuth` and
6161
`clusterGroup.managedClusterGroups.*.applications.*.ssCsiWorkloadAuth`.
6262

63+
Clustergroup values are loaded for SS CSI in this order (see
64+
`tasks/vault_ss_csi_load_clustergroup_values.yaml`):
65+
66+
1. In-cluster `ConfigMap` (default: namespace `openshift-gitops`, name
67+
`values-<main_clustergroupname>`, YAML under a `values.yaml`-style data key),
68+
when `vault_ss_csi_clustergroup_values_from_configmap` is true. The parsed
69+
document must define `clusterGroup`.
70+
2. Local file `vault_ss_csi_cluster_values_file`, or
71+
`pattern_dir/values-<main_clustergroupname>.yaml`, when
72+
`vault_ss_csi_fallback_local_clustergroup_file` is true.
73+
74+
Override defaults with `vault_ss_csi_clustergroup_configmap_namespace`,
75+
`vault_ss_csi_clustergroup_configmap_name`, `vault_ss_csi_clustergroup_configmap_key`,
76+
and `vault_ss_csi_clustergroup_configmap_key_candidates` as needed for your pattern.
77+
78+
Vault Kubernetes auth **role names** use the form **auth mount + `-sscsi-` + slug**. They must satisfy
79+
Vault path rules (non-empty slug, no trailing `-`, bounded length on some versions).
80+
This role derives `slug` from optional `roleSlug`, or from `vault_ss_csi_role_slug_mode`
81+
(`hash` or `stable_slug`), and shortens to a SHA-1 prefix when
82+
`vault_ss_csi_kubernetes_auth_role_name_max_length` would be exceeded (set to `0`
83+
for no limit). If an older Vault returns **400 invalid role name**, use `hash` mode,
84+
set a short explicit `roleSlug`, or lower `vault_ss_csi_kubernetes_auth_role_name_max_length`.
85+
6386
For each `ssCsiWorkloadAuth` entry:
6487

6588
- required: `serviceAccount`
6689
- optional: `namespace`, `cluster`, `roleSlug` (or `role_slug`)
6790

91+
For spokes, `cluster` in values can be the **managed cluster group** name (default), the ACM **`ManagedCluster` name**, the spoke **FQDN** (`vault_path`, same as Vault/ESO), or **`metadata.labels.clusterGroup`**. During `vault_spokes_init`, rows are **normalized** so spoke Vault roles always use **`vault_path`** (full cluster DNS name) as the cluster id, matching ESO and the Kubernetes auth mount path on the spoke.
92+
93+
**Charts (vp-sscsi-spc):** `SecretProviderClass` workload auth should use the same
94+
idea: with `roleSlug` set, the chart emits **`roleName: <vaultKubernetesMountPath>-sscsi-<roleSlug>`**
95+
where **`vaultKubernetesMountPath`** is the hub mount or **`global.clusterDomain`**
96+
on the spoke. You do not need to duplicate the spoke FQDN in `ssCsiWorkloadAuth.cluster`
97+
for the CSI role name; keep `cluster` as a matcher for Ansible (short name or FQDN).
98+
6899
Application-level `namespace` is used as the default when an entry does not set
69100
`namespace`.
70101

71-
Example:
102+
Example (hub):
72103

73104
```yaml
74105
clusterGroup:
@@ -78,6 +109,23 @@ clusterGroup:
78109
ssCsiWorkloadAuth:
79110
- serviceAccount: my-app-sa
80111
cluster: hub
112+
roleSlug: my-app-my-app-sa-my-app
113+
```
114+
115+
Example (spoke row in hub values under `managedClusterGroups` — `cluster` may be the group name; Vault and vp-sscsi-spc still use the spoke FQDN as mount and role prefix):
116+
117+
```yaml
118+
clusterGroup:
119+
managedClusterGroups:
120+
exampleRegion:
121+
name: group-one
122+
applications:
123+
my-app:
124+
namespace: my-app-namespace
125+
ssCsiWorkloadAuth:
126+
- serviceAccount: my-app-sa
127+
cluster: group-one
128+
roleSlug: my-app-my-app-sa-my-app
81129
```
82130

83131
SS CSI CA material management is external to this role. Use a separate chart or

roles/vault_utils/defaults/main.yml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,34 @@ vault_csi_role_ttl: "15m"
7272
# clusterGroup.applications.<app>
7373
# or under clusterGroup.managedClusterGroups.<group>.applications.<app>
7474
# (see vault_ss_csi_* tasks). Example element:
75-
# { serviceAccount: my-sa, namespace: my-ns, cluster: <spoke>, optional roleSlug: stable suffix }
75+
# { serviceAccount: my-sa, namespace: my-ns, cluster: <match hint>, optional roleSlug: suffix }
76+
# cluster: which cluster the row targets (MCG name, ManagedCluster name, vault_path/FQDN, or clusterGroup label);
77+
# spoke rows are normalized to vault_path before Vault role writes (same id as ESO). Vault role name is always
78+
# <mount>-sscsi-<slug> where mount is hub or vault_path (vp-sscsi-spc uses the same mount for roleName).
7679
# namespace defaults to the application namespace; cluster defaults to hub for hub apps, or to
7780
# managedClusterGroup.name (else the group YAML key) for applications declared under managedClusterGroups.
7881
vault_ss_csi_from_applications: true
79-
# Override path to values-<clustergroup>.yaml; empty uses pattern_dir/values-{{ main_clustergroupname }}.yaml
82+
# Prefer merged clustergroup values from an in-cluster ConfigMap (reflects GitOps overrides).
83+
vault_ss_csi_clustergroup_values_from_configmap: true
84+
# Namespace containing the clustergroup values ConfigMap (OpenShift GitOps default).
85+
vault_ss_csi_clustergroup_configmap_namespace: openshift-gitops
86+
# If empty, the ConfigMap name defaults to values-<main_clustergroupname> (same stem as values-<cg>.yaml).
87+
vault_ss_csi_clustergroup_configmap_name: ""
88+
# If empty, try keys in vault_ss_csi_clustergroup_configmap_key_candidates in order.
89+
vault_ss_csi_clustergroup_configmap_key: ""
90+
vault_ss_csi_clustergroup_configmap_key_candidates:
91+
- values.yaml
92+
- helm-values.yaml
93+
- values.yml
94+
# When the ConfigMap is missing or does not contain a parseable clusterGroup document, slurp local file.
95+
vault_ss_csi_fallback_local_clustergroup_file: true
96+
# Override path to values-<clustergroup>.yaml; empty uses pattern_dir/values-{{ main_clustergroupname }}.yaml (fallback only)
8097
vault_ss_csi_cluster_values_file: ""
8198
vault_ss_csi_role_ttl: "15m"
8299
# How Vault names Kubernetes auth roles: auth/<mount>/role/<mount>-sscsi-<slug>
83100
# - hash: legacy SHA1 of namespace|serviceAccount|app (hub) or vault_path|... (spoke)
84101
# - stable_slug: hub-sscsi-<ns>-<sa>-<app> (sanitized); spokes prefix sanitized vault_path
85102
# Per-entry override wins: ssCsiWorkloadAuth[].roleSlug (suffix only; still prefixed with <mount>-sscsi-)
86103
vault_ss_csi_role_slug_mode: hash
104+
# Full role name is <auth_mount>-sscsi-<slug>. Cap length for older Vault (HTTP 400 invalid role name); 0 = no limit.
105+
vault_ss_csi_kubernetes_auth_role_name_max_length: 256
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
- name: Compute Vault role slug for hub SS CSI identity
3+
ansible.builtin.include_tasks: vault_ss_csi_compute_role_slug.yaml
4+
vars:
5+
ss_csi_mount_prefix: "{{ vault_hub }}"
6+
ss_csi_hash_input: "{{ (item.namespace | default('', true)) ~ '|' ~ (item.serviceAccount | default('', true)) ~ '|' ~ (item.app | default('', true)) }}"
7+
8+
- name: Configure hub Vault Kubernetes auth role for SS CSI workload identity
9+
kubernetes.core.k8s_exec:
10+
namespace: "{{ vault_ns }}"
11+
pod: "{{ vault_pod }}"
12+
command: >
13+
vault write auth/"{{ vault_hub }}"/role/"{{ vault_hub }}-sscsi-{{ _role_slug }}"
14+
bound_service_account_names="{{ item.serviceAccount }}"
15+
bound_service_account_namespaces="{{ item.namespace }}"
16+
policies="{{ _merged_hub_policies | join(',') }}"
17+
ttl="{{ vault_ss_csi_role_ttl }}"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
- name: Compute Vault role slug for spoke SS CSI identity
3+
ansible.builtin.include_tasks: vault_ss_csi_compute_role_slug.yaml
4+
vars:
5+
ss_csi_mount_prefix: "{{ vault_spoke_cluster_loop.value.vault_path }}"
6+
ss_csi_hash_input: >-
7+
{{
8+
(vault_spoke_cluster_loop.value.vault_path | string)
9+
~ '|' ~ (item.namespace | default('', true))
10+
~ '|' ~ (item.serviceAccount | default('', true))
11+
~ '|' ~ (item.app | default('', true))
12+
}}
13+
14+
- name: Configure Vault SS CSI role on spoke {{ vault_spoke_cluster_loop.key }}
15+
kubernetes.core.k8s_exec:
16+
namespace: "{{ vault_ns }}"
17+
pod: "{{ vault_pod }}"
18+
command: >
19+
vault write auth/{{ vault_spoke_cluster_loop.value.vault_path }}/role/{{ vault_spoke_cluster_loop.value.vault_path }}-sscsi-{{ _role_slug }}
20+
bound_service_account_names="{{ item.serviceAccount }}"
21+
bound_service_account_namespaces="{{ item.namespace }}"
22+
policies="default,{{ vault_global_policy }}-secret,{{ vault_pushsecrets_policy }}-secret,{{ vault_spoke_cluster_loop.value.vault_path }}-secret"
23+
ttl="{{ vault_spoke_ttl }}"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
# Sets _role_slug for Vault Kubernetes auth role name: <ss_csi_mount_prefix>-sscsi-<slug>
3+
# Requires: item (ssCsiWorkloadAuth row), ss_csi_mount_prefix, ss_csi_hash_input (string for SHA1).
4+
5+
- name: Derive SS CSI Kubernetes auth role slug
6+
ansible.builtin.set_fact:
7+
_role_slug: "{{ _ss_csi_final_slug }}"
8+
vars:
9+
_raw_slug: "{{ item.roleSlug | default(item.role_slug | default('', true), true) | string | trim }}"
10+
_sanitized_custom: >-
11+
{{ _raw_slug | lower | regex_replace('[^a-z0-9-]+', '-') | regex_replace('-+', '-') | trim('-') }}
12+
_stable_ns: "{{ item.namespace | default('', true) | lower | regex_replace('[^a-z0-9-]+', '-') | regex_replace('-+', '-') | trim('-') }}"
13+
_stable_sa: "{{ item.serviceAccount | default('', true) | lower | regex_replace('[^a-z0-9-]+', '-') | regex_replace('-+', '-') | trim('-') }}"
14+
_stable_app: "{{ item.app | default('', true) | lower | regex_replace('[^a-z0-9-]+', '-') | regex_replace('-+', '-') | trim('-') }}"
15+
_stable_joined: "{{ [_stable_ns, _stable_sa, _stable_app] | reject('equalto', '') | list | join('-') | regex_replace('-+', '-') | trim('-') }}"
16+
_hash_slug: "{{ ss_csi_hash_input | hash('sha1') }}"
17+
_max_len: "{{ vault_ss_csi_kubernetes_auth_role_name_max_length | default(256) | int }}"
18+
_prefix: "{{ ss_csi_mount_prefix | string }}"
19+
_mode: "{{ vault_ss_csi_role_slug_mode | default('hash') | lower }}"
20+
_candidate: >-
21+
{{
22+
(_sanitized_custom | length > 0)
23+
| ternary(
24+
_sanitized_custom,
25+
(((_mode == 'stable_slug') and (_stable_joined | length > 0))
26+
| ternary(_stable_joined, _hash_slug))
27+
)
28+
}}
29+
_prefix_len: "{{ _prefix | length }}"
30+
_candidate_len: "{{ _candidate | length }}"
31+
_needs_shorten: >-
32+
{{
33+
(_max_len | int > 0)
34+
and ((_prefix_len | int) + 7 + (_candidate_len | int) > (_max_len | int))
35+
}}
36+
_budget: >-
37+
{{
38+
([(_max_len | int) - (_prefix_len | int) - 7, 8] | max)
39+
if (_max_len | int > 0) else 40
40+
}}
41+
_hash_take: "{{ [_budget | int, 40] | min }}"
42+
_short_hash: "{{ _hash_slug[0 : (_hash_take | int)] }}"
43+
_ss_csi_final_slug: >-
44+
{{
45+
(_needs_shorten | bool)
46+
| ternary(
47+
_short_hash,
48+
(((_candidate | length) > 0) | ternary(_candidate, _hash_slug))
49+
)
50+
}}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
# Load merged clustergroup values for SS CSI: prefer in-cluster ConfigMap, then local file.
3+
# Sets _vault_ss_csi_values_root (mapping with .clusterGroup) and _vault_ss_csi_values_source when successful.
4+
# Requires: main_clustergroupname; pattern_dir for file fallback.
5+
6+
- name: Initialize SS CSI clustergroup values load facts
7+
ansible.builtin.set_fact:
8+
_vault_ss_csi_values_source: ""
9+
_ss_csi_cm_data: {}
10+
_ss_csi_cm_yaml_key: ""
11+
12+
- name: Compute SS CSI clustergroup ConfigMap object name
13+
ansible.builtin.set_fact:
14+
_ss_csi_cg_cm_name: >-
15+
{{
16+
(vault_ss_csi_clustergroup_configmap_name | default('', true) | string | trim | length > 0)
17+
| ternary(
18+
vault_ss_csi_clustergroup_configmap_name | string | trim,
19+
'values-' ~ (main_clustergroupname | string | trim)
20+
)
21+
}}
22+
when:
23+
- main_clustergroupname is defined
24+
- main_clustergroupname | string | trim | length > 0
25+
26+
- name: Read clustergroup values ConfigMap from cluster (SS CSI)
27+
kubernetes.core.k8s_info:
28+
api_version: v1
29+
kind: ConfigMap
30+
name: "{{ _ss_csi_cg_cm_name }}"
31+
namespace: "{{ vault_ss_csi_clustergroup_configmap_namespace }}"
32+
register: _vault_ss_csi_cg_cm
33+
failed_when: false
34+
when:
35+
- vault_ss_csi_clustergroup_values_from_configmap | default(true) | bool
36+
- _ss_csi_cg_cm_name is defined
37+
38+
- name: Set ConfigMap .data for SS CSI clustergroup parse
39+
ansible.builtin.set_fact:
40+
_ss_csi_cm_data: "{{ _vault_ss_csi_cg_cm.resources[0].data | default({}) }}"
41+
when:
42+
- vault_ss_csi_clustergroup_values_from_configmap | default(true) | bool
43+
- _vault_ss_csi_cg_cm is defined
44+
- not (_vault_ss_csi_cg_cm.failed | default(false))
45+
- (_vault_ss_csi_cg_cm.resources | default([]) | length) > 0
46+
47+
- name: Build ordered ConfigMap data key candidates for clustergroup YAML
48+
ansible.builtin.set_fact:
49+
_ss_csi_cm_key_candidates: >-
50+
{{
51+
(
52+
([vault_ss_csi_clustergroup_configmap_key | default('', true) | string | trim]
53+
| reject('equalto', '') | list)
54+
+ (vault_ss_csi_clustergroup_configmap_key_candidates | default([]))
55+
) | unique | list
56+
}}
57+
when:
58+
- _ss_csi_cm_data is defined
59+
- (_ss_csi_cm_data | default({}) | length) > 0
60+
61+
- name: Pick first ConfigMap data key present in candidates (SS CSI)
62+
ansible.builtin.set_fact:
63+
_ss_csi_cm_yaml_key: "{{ item }}"
64+
loop: "{{ _ss_csi_cm_key_candidates | default([]) }}"
65+
when:
66+
- (_ss_csi_cm_yaml_key | default('') | string | length) == 0
67+
- item in (_ss_csi_cm_data | default({}))
68+
69+
- name: Parse YAML from ConfigMap data (SS CSI)
70+
block:
71+
- name: Decode YAML string from ConfigMap key
72+
ansible.builtin.set_fact:
73+
_vault_ss_csi_cm_values_candidate: "{{ _ss_csi_cm_data[_ss_csi_cm_yaml_key] | trim | from_yaml }}"
74+
rescue:
75+
- name: Note ConfigMap YAML parse failure (SS CSI)
76+
ansible.builtin.set_fact:
77+
_vault_ss_csi_cm_values_candidate: {}
78+
79+
- name: Accept clustergroup values from ConfigMap when clusterGroup is present (SS CSI)
80+
ansible.builtin.set_fact:
81+
_vault_ss_csi_values_root: "{{ _vault_ss_csi_cm_values_candidate }}"
82+
_vault_ss_csi_values_source: >-
83+
configmap {{ vault_ss_csi_clustergroup_configmap_namespace }}/{{ _ss_csi_cg_cm_name }} key={{ _ss_csi_cm_yaml_key }}
84+
when:
85+
- _vault_ss_csi_cm_values_candidate is defined
86+
- _vault_ss_csi_cm_values_candidate is mapping
87+
- _vault_ss_csi_cm_values_candidate.clusterGroup is defined
88+
- _ss_csi_cm_yaml_key is defined
89+
- _ss_csi_cm_yaml_key | string | length > 0
90+
91+
- name: Resolve path to clustergroup values file for SS CSI (fallback)
92+
ansible.builtin.set_fact:
93+
_vault_ss_csi_values_path: "{{ vault_ss_csi_cluster_values_file | default('', true) | trim }}"
94+
95+
- name: Default clustergroup values path from pattern_dir (SS CSI fallback)
96+
ansible.builtin.set_fact:
97+
_vault_ss_csi_values_path: "{{ pattern_dir }}/values-{{ main_clustergroupname }}.yaml"
98+
when:
99+
- (_vault_ss_csi_values_path | default('', true) | length) == 0
100+
- pattern_dir is defined
101+
- pattern_dir | length > 0
102+
- main_clustergroupname is defined
103+
- main_clustergroupname | string | trim | length > 0
104+
105+
- name: Stat clustergroup values file for SS CSI (fallback)
106+
ansible.builtin.stat:
107+
path: "{{ _vault_ss_csi_values_path }}"
108+
register: _vault_ss_csi_values_stat
109+
when:
110+
- vault_ss_csi_fallback_local_clustergroup_file | default(true) | bool
111+
- _vault_ss_csi_values_root is not defined
112+
- _vault_ss_csi_values_path is defined
113+
- _vault_ss_csi_values_path | length > 0
114+
115+
- name: Load clustergroup values YAML from local file (SS CSI fallback)
116+
ansible.builtin.slurp:
117+
src: "{{ _vault_ss_csi_values_path }}"
118+
register: _vault_ss_csi_values_slurp
119+
when:
120+
- vault_ss_csi_fallback_local_clustergroup_file | default(true) | bool
121+
- _vault_ss_csi_values_root is not defined
122+
- _vault_ss_csi_values_stat is defined
123+
- _vault_ss_csi_values_stat.stat.exists | default(false)
124+
125+
- name: Decode clustergroup values root from local file (SS CSI fallback)
126+
ansible.builtin.set_fact:
127+
_vault_ss_csi_values_root: "{{ (_vault_ss_csi_values_slurp.content | b64decode | from_yaml) }}"
128+
_vault_ss_csi_values_source: "file {{ _vault_ss_csi_values_path }}"
129+
when:
130+
- vault_ss_csi_fallback_local_clustergroup_file | default(true) | bool
131+
- _vault_ss_csi_values_slurp is defined
132+
- _vault_ss_csi_values_slurp.content is defined

0 commit comments

Comments
 (0)