Skip to content

Commit 8bbdc7b

Browse files
author
Martin Jackson
committed
Merge branch 'bootstrap_secrets_single_file' into feature/sscsi-vp-proxy-cluster-ca-chart
2 parents 74657c3 + 392baaf commit 8bbdc7b

38 files changed

Lines changed: 988 additions & 190 deletions

.github/workflows/jsonschema.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ jobs:
3232
- name: Verify secrets json schema
3333
run: |
3434
set -e
35-
for i in values-secret-v2-base values-secret-v2-generic-onlygenerate values-secret-v2-block-yamlstring; do echo "$i"; check-jsonschema --fill-defaults --schemafile ./roles/vault_utils/values-secrets.v2.schema.json "tests/unit/v2/$i.yaml"; done
35+
for i in values-secret-v2-base values-secret-v2-generic-onlygenerate values-secret-v2-block-yamlstring values-secret-v2-bootstrap-mixed; do echo "$i"; check-jsonschema --fill-defaults --schemafile ./roles/vault_utils/values-secrets.v2.schema.json "tests/unit/v2/$i.yaml"; done

.github/workflows/superlinter.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ jobs:
2727
VALIDATE_ALL_CODEBASE: true
2828
DEFAULT_BRANCH: main
2929
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
# Skip installed collection copies under .ansible/ (duplicate paths break PYTHON_MYPY and other tools).
31+
FILTER_REGEX_EXCLUDE: '(^|/)\.ansible/'
3032
# These are the validation we disable atm
3133
VALIDATE_ANSIBLE: false
3234
VALIDATE_BIOME_FORMAT: false

.github/workflows/trigger-utility-imperative-container-builds.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
steps:
1414
- name: Generate GitHub App token
1515
id: generate-token
16-
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
16+
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
1717
with:
1818
app-id: ${{ secrets.GH_WORKFLOW_AUTOMATION_CLIENT_ID }}
1919
private-key: ${{ secrets.GH_WORKFLOW_AUTOMATION_PRIVATE_KEY }}

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ help: ## This help message
77

88
.PHONY: super-linter
99
super-linter: ## Runs super linter locally
10-
rm -rf .mypy_cache
11-
rm -rf .ansible
10+
rm -rf .mypy_cache .ansible
1211
podman run -e RUN_LOCAL=true -e USE_FIND_ALGORITHM=true \
12+
-e FILTER_REGEX_EXCLUDE='(^|/)\\.ansible/' \
1313
-e VALIDATE_ANSIBLE=false \
1414
-e VALIDATE_BASH=false \
1515
-e VALIDATE_BIOME_FORMAT=false \
@@ -49,4 +49,4 @@ test: ansible-sanitytest ansible-unittest
4949
.PHONY: check-jsonschema
5050
check-jsonschema: ## Runs check-jsonschema against all unit test files except known broken ones
5151
set -e; \
52-
for i in values-secret-v2-base values-secret-v2-generic-onlygenerate values-secret-v2-block-yamlstring; do echo "$$i"; check-jsonschema --schemafile ./roles/vault_utils/values-secrets.v2.schema.json "tests/unit/v2/$$i.yaml"; done
52+
for i in values-secret-v2-base values-secret-v2-generic-onlygenerate values-secret-v2-block-yamlstring values-secret-v2-bootstrap-mixed; do echo "$$i"; check-jsonschema --schemafile ./roles/vault_utils/values-secrets.v2.schema.json "tests/unit/v2/$$i.yaml"; done

README.md

Lines changed: 91 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -9,84 +9,94 @@ loading local secrets files into VP secrets stores.
99

1010
2. Help manage imperative and other utility functions of the cluster
1111

12-
## SS CSI workload auth notes
13-
14-
SS CSI task files live in **`roles/vault_utils/tasks/ss_csi/`**; paths below match **`include_tasks`** from the role (**`ss_csi/<file>`** relative to **`tasks/`**).
15-
16-
`vault_utils` can read `ssCsiWorkloadAuth` entries from clustergroup values and
17-
create Vault Kubernetes auth roles for hub and spoke workloads.
18-
19-
### Parsing (load YAML)
20-
21-
With **`vault_ss_csi_aggregate_clustergroup_sources`** true (default), SS CSI
22-
uses the **`clustergroup_discovery`** role to determine stems: **main** from
23-
`values-global.yaml`, then **managed** names from `clusterGroup.managedClusterGroups`
24-
in the main `values-<main>.yaml|yml`. For **each** stem it loads a document from
25-
the in-cluster **`ConfigMap` `values-<stem>`** (namespace
26-
`openshift-gitops` by default), then falls back to **`pattern_dir/values-<stem>.yaml|yml`**
27-
when enabled. ConfigMap data keys follow **`vault_ss_csi_clustergroup_configmap_key`**
28-
and **`vault_ss_csi_clustergroup_configmap_key_candidates`**. Each document must
29-
include **`clusterGroup`**. Stems are merged in **`clustergroup_load_order`**
30-
(main first, then managed stems sorted) so later sources override duplicate
31-
`clusterGroup.applications` keys. Set **`vault_ss_csi_aggregate_clustergroup_sources`**
32-
to false to load only the **main** document (legacy: single ConfigMap or
33-
`values-<main>.yaml`).
34-
35-
### Extraction (find `ssCsiWorkloadAuth`)
36-
37-
The role builds **`_vault_ss_csi_apps_by_stem`** (per-stem `clusterGroup.applications`)
38-
and a merged **`clusterGroup.managedClusterGroups`**. It collects:
39-
40-
- **`clusterGroup.applications.*.ssCsiWorkloadAuth`** — per stem; omit **`cluster`**
41-
in values: the **main** stem resolves to **hub**; **managed** stems resolve to
42-
that **stem name** so entries under `values-<managed>.yaml` stay spoke-scoped.
43-
- **`clusterGroup.managedClusterGroups.*.applications.*.ssCsiWorkloadAuth`**
44-
from the merged map; omit **`cluster`** and the row targets that managed group
45-
(**`name`**, else the group map key).
46-
47-
### Projection (Vault roles)
48-
49-
Rows are appended to **`_ss_csi_all_entries`**, split into hub vs spoke using
50-
the computed **`cluster`** field (from stem or managed group when omitted in YAML), then **hub** identities get Vault Kubernetes
51-
auth roles via **`ss_csi/vault_ss_csi_apply_one_hub_sscsi_role.yaml`**. Spoke rows are
52-
normalized to **`vault_path`** later in the play (**`ss_csi/vault_ss_csi_normalize_spoke_entries_to_vault_path.yaml`**
53-
during **`vault_spokes_init`**) and roles are written on each spoke mount
54-
(**`ss_csi/vault_ss_csi_apply_one_spoke_sscsi_role.yaml`**). Role names use
55-
**`<mount>-sscsi-<slug>`**; slugs come from **`ss_csi/vault_ss_csi_compute_role_slug.yaml`**.
56-
57-
To **inspect** stems and files locally, run **`playbooks/list_clustergroups.yml`**
58-
or **`playbooks/parse_clustergroup_values.yml`** (see **`roles/clustergroup_discovery/README.md`**).
59-
60-
At the application level (`clusterGroup.applications.<app>`), the relevant
61-
inputs are:
62-
63-
- `ssCsiWorkloadAuth` (list)
64-
- `ssCsiWorkloadAuth[].serviceAccount` (required)
65-
- `ssCsiWorkloadAuth[].namespace` (optional)
66-
- Omit **`cluster`** in pattern YAML; hub vs spoke comes from **which file or
67-
`managedClusterGroups` branch** defines the list (see extraction above). Spoke
68-
handling still normalizes to **`vault_path`** (full DNS), same as External Secrets.
69-
- `ssCsiWorkloadAuth[].roleSlug` / `role_slug` (optional): suffix only; Vault
70-
role is **`<mount>-sscsi-<slug>`** where **`<mount>`** is hub **`hub`** (or
71-
configured hub path) or the spoke **`vault_path`**. When using the
72-
**vp-sscsi-spc** chart, `spec.parameters.roleName` uses the same **mount**
73-
as `vaultKubernetesMountPath` (typically **`global.clusterDomain`** on
74-
spokes), not a short clustergroup label.
75-
- application `namespace` (optional default for entry namespace)
76-
77-
CA material management for SS CSI is not handled in this collection anymore.
78-
Provide CA distribution using a separate chart or platform mechanism.
79-
80-
For the complete flow and task ordering, see
81-
`secrets-initialization-and-vault-unseal.md`.
82-
83-
## Pattern repository directory (`pattern_dir`)
84-
85-
Playbooks need the path to your pattern Git checkout (where `values-global.yaml`
86-
and related files live). Resolution order: extra var `pattern_dir`, environment
87-
variable `PATTERN_DIR`, then `PWD` and `pwd`.
88-
89-
When running from the imperative container or another fixed working directory,
90-
pass the repository root explicitly, for example `-e pattern_dir=/git/repo` (or add
91-
equivalent extra vars via `clusterGroup.imperative.extraPlaybookArgs` in the
92-
clustergroup chart).
12+
## Secrets loading
13+
14+
Secrets are loaded from a **single primary** values-secret file (plus optional `values-secret.yaml.template` under the
15+
pattern tree as a last-resort discovery path). There are **no** separate `*-bootstrap.yaml` files or `VALUES_SECRET_BOOTSTRAP`
16+
paths; early cluster bootstrap uses **per-entry** `bootstrap` fields on v2 secrets in that same primary file.
17+
18+
### Primary values-secret
19+
20+
- **Backing store** comes from `values-global.yaml`: `.global.secretStore.backend` (default `vault`). That drives parsing
21+
and whether secrets go to Vault or Kubernetes.
22+
- **Discovery order** when `VALUES_SECRET` is unset (first existing file wins):
23+
`~/.config/hybrid-cloud-patterns/values-secret-<pattern>.yaml`,
24+
`~/.config/validated-patterns/values-secret-<pattern>.yaml`,
25+
`~/values-secret-<pattern>.yaml`,
26+
`~/values-secret.yaml`,
27+
then `<pattern_dir>/values-secret.yaml.template`.
28+
- When `VALUES_SECRET` is set to an existing path, that file is used for the primary load.
29+
30+
Files may be plain YAML or `ansible-vault` encrypted.
31+
32+
### Per-secret `bootstrap` in v2 primary files
33+
34+
On schema **2.0** primary values-secret files, each secret may set `bootstrap`:
35+
36+
- **`bootstrap: true`** (or string equivalents such as `yes`, `both`) — the secret is included in the **early**
37+
Kubernetes inject pass (`none` backend) and is **also** parsed in the **primary** pass into the configured backend
38+
(Vault or Kubernetes as in `values-global.yaml`). It must not use `onMissingValue: generate` on any field (the early
39+
pass cannot generate in Vault).
40+
- **`bootstrap: only`** (or `early`) — the secret is **only** in the early inject pass; the primary pass **omits** it.
41+
- **Unset / false** — normal primary-only secret.
42+
43+
Invalid `bootstrap` scalars fail parsing with a clear error.
44+
45+
Early inject runs **before** the primary backend load: during `playbooks/install.yml`, immediately after the
46+
pattern-install manifests are applied (`operator_deploy.yml`), then again inside `load_secrets` unless that early pass
47+
already completed (duplicate inject is skipped).
48+
49+
### Playbooks and flows
50+
51+
- **`playbooks/load_secrets.yml`**
52+
Respects `.global.secretLoader.disabled` in `values-global.yaml`. When enabled: `cluster_pre_check`, primary file
53+
discovery, early Kubernetes inject for bootstrap-tagged v2 entries (when present), then parse and load the rest into
54+
the configured backend.
55+
56+
- **`playbooks/load_bootstrap_secrets.yml`**
57+
Convenience wrapper: `determine_pattern_dir`, `determine_pattern_name`, then imports `load_secrets.yml` (same behavior
58+
as install).
59+
60+
- **`playbooks/load_bootstrap_secrets_only.yml`**
61+
**Early bootstrap inject only**: same pattern discovery plays and `pattern_settings`, then only the Kubernetes inject
62+
for bootstrap-tagged secrets in the primary file (with retries). **Fails** if no primary file exists or there are no
63+
bootstrap-tagged v2 entries. Does **not** read `secretLoader.disabled` or load into Vault / primary backend.
64+
65+
- **`playbooks/display_secrets_info.yml`**
66+
Loads and displays parsed secrets (using the backend from `values-global`). For v2 files with any bootstrap-tagged
67+
entries, output is split into **`early_bootstrap_inject`** (none backend, early K8s view; includes `bootstrap: true`
68+
and `bootstrap: only`) and **`primary_backend`** (configured backend; includes normal secrets and **`bootstrap: true`**
69+
again so dual-mode entries appear in both groups). Otherwise a single parse is shown as before.
70+
71+
Typical usage passes the pattern checkout as `pattern_dir` (for example `-e pattern_dir=/path/to/pattern`). If you omit
72+
it, the same resolution as `pattern_settings` applies: `PATTERN_DIR`, then `PWD`, then the `pwd` command.
73+
74+
`playbooks/install.yml` imports `load_secrets.yml` after the pattern install playbook. When secret loading is enabled,
75+
early bootstrap inject from the primary file runs at the end of `operator_deploy.yml` (right after apply), then
76+
`load_secrets.yml` continues without repeating that inject when it already succeeded.
77+
78+
### Early bootstrap inject retries
79+
80+
Outer retries (parse plus Kubernetes apply) are controlled on the role defaults / extra-vars:
81+
82+
- `vp_secrets_bootstrap_retry_max` (default `20`)
83+
- `vp_secrets_bootstrap_retry_delay` (seconds between attempts, default `30`)
84+
85+
These apply to the early inject path inside `load_secrets` and to `load_bootstrap_secrets_only.yml`.
86+
87+
Per-secret namespace readiness (before each `kubernetes.core.k8s` apply) uses role defaults on `k8s_secret_utils`:
88+
89+
- `k8s_secret_namespace_check_retries` (default `5`) and `k8s_secret_namespace_check_delay` (seconds between attempts, default `45`).
90+
91+
If the namespace still does not exist after those attempts, the inject fails and the **outer** retry re-runs parse plus
92+
all secret injections from the start.
93+
94+
### Roles (implementation notes)
95+
96+
- `roles/load_secrets/tasks/main.yml` implements the **combined** flow (early inject from primary file, then primary
97+
backend load).
98+
- `roles/load_secrets/tasks/bootstrap_only.yml` is used only when you invoke the `load_secrets` role with
99+
`tasks_from: bootstrap_only.yml` (as `load_bootstrap_secrets_only.yml` does).
100+
- `roles/find_vp_secrets` resolves the primary file (`tasks/main.yml`).
101+
- v2 parsing and phase filters (`bootstrap_only`, `exclude_bootstrap`, `all`) are implemented in
102+
`plugins/module_utils/parse_secrets_v2.py` (single `bootstrap` normalizer: off / dual / early-only).
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
---
2+
# Resolves pattern_dir the same way as the pattern_settings role (extra-vars, PATTERN_DIR, PWD, pwd),
3+
# then fails if still unset. Used by display_secrets_info, load_values_global, load_bootstrap_secrets*, etc.
24
- name: Determine pattern dir
35
hosts: localhost
46
connection: local
57
gather_facts: false
68
become: false
7-
vars:
8-
pattern_dir: ''
99
tasks:
10-
- name: Fail if directory is not set
10+
- name: Resolve pattern_dir from extra-vars, PATTERN_DIR, PWD, or pwd
11+
ansible.builtin.include_role:
12+
name: pattern_settings
13+
tasks_from: resolve_overrides.yml
14+
15+
- name: Fail if pattern directory is not set after resolution
1116
ansible.builtin.fail:
12-
msg: "pattern_dir variable must be set"
13-
when: pattern_dir | length == 0
17+
msg: >-
18+
pattern_dir is not set. Pass -e pattern_dir=/path/to/pattern, export PATTERN_DIR to that path,
19+
or run the playbook from the pattern directory so PWD is correct.
20+
when: pattern_dir | default('') | string | trim | length == 0
1421

1522
- name: Set pattern_dir fact for future plays
1623
ansible.builtin.set_fact:
17-
pattern_dir: '{{ pattern_dir }}'
24+
pattern_dir: "{{ pattern_dir | string | trim }}"

playbooks/display_secrets_info.yml

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,70 @@
2929
ansible.builtin.set_fact:
3030
secrets_yaml: "{{ values_secrets_data if values_secrets_data is not string else values_secrets_data | from_yaml }}"
3131

32-
- name: Parse secrets data
32+
- name: Detect inline bootstrap secrets in primary v2 file
33+
ansible.builtin.set_fact:
34+
_vp_has_inline_bootstrap_secrets: >-
35+
{{
36+
(secrets_yaml.version | default('2.0')) is version('2.0', '>=')
37+
and (
38+
(secrets_yaml.secrets | default([])
39+
| selectattr('bootstrap', 'defined')
40+
| selectattr('bootstrap')
41+
| list
42+
| length) > 0
43+
)
44+
}}
45+
46+
- name: Parse secrets data (v2 with bootstrap — two display groups)
47+
when: _vp_has_inline_bootstrap_secrets | bool
48+
block:
49+
- name: Parse early-bootstrap inject portion for display (none backend)
50+
no_log: '{{ hide_sensitive_output }}'
51+
parse_secrets_info:
52+
values_secrets_plaintext: "{{ values_secrets_data }}"
53+
secrets_backing_store: none
54+
secrets_parse_filter: bootstrap_only
55+
register: _display_bootstrap_parse
56+
57+
- name: Parse primary-backend portion for display (configured backend)
58+
no_log: '{{ hide_sensitive_output }}'
59+
parse_secrets_info:
60+
values_secrets_plaintext: "{{ values_secrets_data }}"
61+
secrets_backing_store: "{{ secrets_backing_store }}"
62+
secrets_parse_filter: exclude_bootstrap
63+
register: _display_primary_parse
64+
65+
- name: Build two-group secrets display (dual bootstrap entries appear in both)
66+
ansible.builtin.set_fact:
67+
secrets_results:
68+
early_bootstrap_inject:
69+
parsed_secrets: "{{ _display_bootstrap_parse.parsed_secrets }}"
70+
kubernetes_secret_objects: "{{ _display_bootstrap_parse.kubernetes_secret_objects }}"
71+
vault_policies: "{{ _display_bootstrap_parse.vault_policies | default({}) }}"
72+
unique_vault_prefixes: "{{ _display_bootstrap_parse.unique_vault_prefixes | default([]) }}"
73+
backing_store: none
74+
primary_backend:
75+
parsed_secrets: "{{ _display_primary_parse.parsed_secrets }}"
76+
kubernetes_secret_objects: "{{ _display_primary_parse.kubernetes_secret_objects }}"
77+
vault_policies: "{{ _display_primary_parse.vault_policies | default({}) }}"
78+
secret_store_namespace: "{{ _display_primary_parse.secret_store_namespace }}"
79+
unique_vault_prefixes: "{{ _display_primary_parse.unique_vault_prefixes | default([]) }}"
80+
secrets_backing_store: "{{ secrets_backing_store }}"
81+
82+
# Do not register: secrets_results here — a skipped task still overwrites the register
83+
# and would wipe the two-group set_fact when bootstrap secrets are present.
84+
- name: Parse secrets data (single phase)
85+
when: not (_vp_has_inline_bootstrap_secrets | bool)
3386
no_log: '{{ hide_sensitive_output }}'
3487
parse_secrets_info:
3588
values_secrets_plaintext: "{{ values_secrets_data }}"
3689
secrets_backing_store: "{{ secrets_backing_store }}"
37-
register: secrets_results
90+
register: _display_single_phase_parse
91+
92+
- name: Set secrets_results from single-phase parse
93+
when: not (_vp_has_inline_bootstrap_secrets | bool)
94+
ansible.builtin.set_fact:
95+
secrets_results: "{{ _display_single_phase_parse }}"
3896

3997
- name: Display secrets data
4098
ansible.builtin.debug:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
# Post-install alias: runs the same secrets load as load_secrets.yml (early bootstrap-tagged inject
3+
# from the primary file when present, then primary backend load).
4+
# Optional extra-vars: vp_secrets_bootstrap_retry_max, vp_secrets_bootstrap_retry_delay (seconds).
5+
- name: Determine pattern directory
6+
ansible.builtin.import_playbook: ./determine_pattern_dir.yml
7+
8+
- name: Determine pattern name
9+
ansible.builtin.import_playbook: ./determine_pattern_name.yml
10+
11+
- name: Load secrets (optional bootstrap then standard)
12+
ansible.builtin.import_playbook: ./load_secrets.yml
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
# Inject only bootstrap-tagged secrets from the primary values-secret file (none backend, with retries).
3+
# Does not load secrets into Vault or the primary Kubernetes backend. Does not honor
4+
# secretLoader.disabled from values-global. Fails if no primary file exists or there are no
5+
# bootstrap-tagged v2 entries.
6+
# Optional extra-vars: vp_secrets_bootstrap_retry_max, vp_secrets_bootstrap_retry_delay (seconds).
7+
- name: Determine pattern directory
8+
ansible.builtin.import_playbook: ./determine_pattern_dir.yml
9+
10+
- name: Determine pattern name
11+
ansible.builtin.import_playbook: ./determine_pattern_name.yml
12+
13+
- name: Load bootstrap secrets only
14+
hosts: localhost
15+
connection: local
16+
gather_facts: false
17+
become: false
18+
roles:
19+
- pattern_settings
20+
21+
tasks:
22+
- name: Run bootstrap-only secrets load
23+
ansible.builtin.include_role:
24+
name: load_secrets
25+
tasks_from: bootstrap_only.yml

0 commit comments

Comments
 (0)