Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/superlinter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
VALIDATE_ALL_CODEBASE: true
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Skip installed collection copies under .ansible/ (duplicate paths break PYTHON_MYPY and other tools).
FILTER_REGEX_EXCLUDE: '(^|/)\.ansible/'
# These are the validation we disable atm
VALIDATE_ANSIBLE: false
VALIDATE_BIOME_FORMAT: false
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ help: ## This help message

.PHONY: super-linter
super-linter: ## Runs super linter locally
rm -rf .mypy_cache
rm -rf .mypy_cache .ansible
podman run -e RUN_LOCAL=true -e USE_FIND_ALGORITHM=true \
-e FILTER_REGEX_EXCLUDE='(^|/)\\.ansible/' \
-e VALIDATE_ANSIBLE=false \
-e VALIDATE_BASH=false \
-e VALIDATE_BIOME_FORMAT=false \
Expand Down
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,80 @@ The main purpose of this collections are to:
loading local secrets files into VP secrets stores.

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

## Secrets loading

The collection distinguishes **primary** values-secret files (the usual pattern secrets) from optional **bootstrap** values-secret files (extra content loaded with the `none` backing store into the cluster, independent of `values-global.yaml` `secretStore.backend`).

### Primary values-secret (standard load)

- **Backing store** comes from `values-global.yaml`: `.global.secretStore.backend` (default `vault`). That drives parsing and whether secrets go to Vault or Kubernetes.
- **Discovery order** when `VALUES_SECRET` is unset (first existing file wins):
`~/.config/hybrid-cloud-patterns/values-secret-<pattern>.yaml`,
`~/.config/validated-patterns/values-secret-<pattern>.yaml`,
`~/values-secret-<pattern>.yaml`,
`~/values-secret.yaml`,
then `<pattern_dir>/values-secret.yaml.template`.
- When `VALUES_SECRET` is set to an existing path, that file is used for the **primary** load. If bootstrap loading already consumed that same path because it was a bootstrap-named file, the primary pass temporarily ignores `VALUES_SECRET` so the primary search can fall back to the paths above.

Files may be plain YAML or `ansible-vault` encrypted.

### Bootstrap values-secret (optional)

Bootstrap files are **never** read from `<pattern_dir>/` (no `values-secret-*-bootstrap.yaml` under the pattern tree).

Bootstrap files may be **plain YAML or `ansible-vault` encrypted**, the same as primary values-secret files: when encrypted, Ansible prompts for the vault password (or uses your usual `ansible-playbook` vault options).

When not using environment overrides, bootstrap candidates are checked in order (first existing file wins):

- `~/.config/hybrid-cloud-patterns/values-secret-<pattern>-bootstrap.yaml`
- `~/.config/validated-patterns/values-secret-<pattern>-bootstrap.yaml`
- `~/values-secret-<pattern>-bootstrap.yaml`
- `~/values-secret-bootstrap.yaml`

Bootstrap discovery precedence:

1. **`VALUES_SECRET_BOOTSTRAP`** – if set to a path that exists, that file is used for bootstrap only (any filename). Primary `VALUES_SECRET` is unchanged.
2. **`VALUES_SECRET`** – if set to an **existing** file whose name ends with `-bootstrap.yaml` or `-bootstrap.yml`, that file is used for bootstrap (and primary loading will ignore `VALUES_SECRET` for the primary file search so a separate primary file can be found).
3. Otherwise the candidate paths above are searched.

**Bootstrap is always parsed and applied with backing store `none`** (Kubernetes secret injection path), which requires schema version 2.0 or newer in the bootstrap file.

### Playbooks and flows

- **`playbooks/load_secrets.yml`**
Respects `.global.secretLoader.disabled` in `values-global.yaml`. When enabled: `cluster_pre_check`, optional **bootstrap** load (if a bootstrap file exists; **not** an error if missing), then **primary** discovery, parse, and load using the configured backend.

- **`playbooks/load_bootstrap_secrets.yml`**
Convenience wrapper: `determine_pattern_dir`, `determine_pattern_name`, then imports `load_secrets.yml` (same combined bootstrap-then-primary behavior as install).

- **`playbooks/load_bootstrap_secrets_only.yml`**
**Bootstrap only**: same pattern discovery plays and `pattern_settings`, then only bootstrap inject (with retries). **Fails** if no bootstrap file is found. Does **not** read `secretLoader.disabled` or load the primary file.

- **`playbooks/display_secrets_info.yml`**
Loads and displays parsed primary secrets (using the backend from `values-global`). If a bootstrap file exists, also parses and displays it with backing store `none`. Missing bootstrap is not an error.

Typical usage passes the pattern checkout as `pattern_dir` (for example `-e pattern_dir=/path/to/pattern`). If you omit it, the same resolution as `pattern_settings` applies: `PATTERN_DIR`, then `PWD`, then the `pwd` command.

`playbooks/install.yml` imports `load_secrets.yml` after the pattern install playbook, so the combined bootstrap-then-primary flow runs during install when secret loading is enabled.

### Bootstrap retries

Bootstrap **inject** retries (parse plus Kubernetes apply) are controlled on the role defaults / extra-vars:

- `vp_secrets_bootstrap_retry_max` (default `20`)
- `vp_secrets_bootstrap_retry_delay` (seconds between attempts, default `30`)

These apply to the optional bootstrap phase inside `load_secrets` and to `load_bootstrap_secrets_only.yml`.

Per-secret namespace readiness (before each `kubernetes.core.k8s` apply) uses role defaults on `k8s_secret_utils`:

- `k8s_secret_namespace_check_retries` (default `5`) and `k8s_secret_namespace_check_delay` (seconds between attempts, default `45`).

If the namespace still does not exist after those attempts, the inject fails and the **outer** bootstrap retry re-runs parse plus all secret injections from the start.

### Roles (implementation notes)

- `roles/load_secrets/tasks/main.yml` implements the **combined** flow (optional bootstrap, then primary).
- `roles/load_secrets/tasks/bootstrap_only.yml` is used only when you invoke the `load_secrets` role with `tasks_from: bootstrap_only.yml` (as `load_bootstrap_secrets_only.yml` does).
- `roles/find_vp_secrets` resolves primary files (`tasks/main.yml`) and optional bootstrap discovery (`tasks/find_optional_bootstrap.yml`).
19 changes: 13 additions & 6 deletions playbooks/determine_pattern_dir.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
---
# Resolves pattern_dir the same way as the pattern_settings role (extra-vars, PATTERN_DIR, PWD, pwd),
# then fails if still unset. Used by display_secrets_info, load_values_global, load_bootstrap_secrets*, etc.
- name: Determine pattern dir
hosts: localhost
connection: local
gather_facts: false
become: false
vars:
pattern_dir: ''
tasks:
- name: Fail if directory is not set
- name: Resolve pattern_dir from extra-vars, PATTERN_DIR, PWD, or pwd
ansible.builtin.include_role:
name: pattern_settings
tasks_from: resolve_overrides.yml

- name: Fail if pattern directory is not set after resolution
ansible.builtin.fail:
msg: "pattern_dir variable must be set"
when: pattern_dir | length == 0
msg: >-
pattern_dir is not set. Pass -e pattern_dir=/path/to/pattern, export PATTERN_DIR to that path,
or run the playbook from the pattern directory so PWD is correct.
when: pattern_dir | default('') | string | trim | length == 0

- name: Set pattern_dir fact for future plays
ansible.builtin.set_fact:
pattern_dir: '{{ pattern_dir }}'
pattern_dir: "{{ pattern_dir | string | trim }}"
26 changes: 24 additions & 2 deletions playbooks/display_secrets_info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
vars:
hide_sensitive_output: false
tasks:
# Set the VALUES_SECRET environment variable to the file to parse
# Primary file: VALUES_SECRET. Bootstrap file: VALUES_SECRET_BOOTSTRAP, bootstrap-named VALUES_SECRET, or search paths.
- name: Find and decrypt secrets if needed
ansible.builtin.include_role:
name: find_vp_secrets
Expand All @@ -36,6 +36,28 @@
secrets_backing_store: "{{ secrets_backing_store }}"
register: secrets_results

- name: Display secrets data
- name: Display primary secrets data
ansible.builtin.debug:
var: secrets_results

- name: Snapshot primary secrets for optional bootstrap display
ansible.builtin.set_fact:
_primary_values_secrets_data_snapshot: "{{ values_secrets_data }}"

- name: Discover optional bootstrap values-secret file
ansible.builtin.include_role:
name: find_vp_secrets
tasks_from: find_optional_bootstrap.yml

- name: Parse bootstrap secrets data (none backend)
no_log: '{{ hide_sensitive_output }}'
parse_secrets_info:
values_secrets_plaintext: "{{ values_secrets_bootstrap_data }}"
secrets_backing_store: none
register: bootstrap_secrets_results
when: vp_bootstrap_secrets_present | default(false) | bool

- name: Display bootstrap secrets data
ansible.builtin.debug:
var: bootstrap_secrets_results
when: vp_bootstrap_secrets_present | default(false) | bool
11 changes: 11 additions & 0 deletions playbooks/load_bootstrap_secrets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
# Post-install alias: runs the same secrets load as load_secrets.yml (optional bootstrap, then primary).
# Optional extra-vars: vp_secrets_bootstrap_retry_max, vp_secrets_bootstrap_retry_delay (seconds).
- name: Determine pattern directory
ansible.builtin.import_playbook: ./determine_pattern_dir.yml

- name: Determine pattern name
ansible.builtin.import_playbook: ./determine_pattern_name.yml

- name: Load secrets (optional bootstrap then standard)
ansible.builtin.import_playbook: ./load_secrets.yml
23 changes: 23 additions & 0 deletions playbooks/load_bootstrap_secrets_only.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
# Load only bootstrap values-secret files (none backend). Does not load the primary values-secret
# or honor secretLoader.disabled from values-global. Fails if no bootstrap file exists.
# Optional extra-vars: vp_secrets_bootstrap_retry_max, vp_secrets_bootstrap_retry_delay (seconds).
- name: Determine pattern directory
ansible.builtin.import_playbook: ./determine_pattern_dir.yml

- name: Determine pattern name
ansible.builtin.import_playbook: ./determine_pattern_name.yml

- name: Load bootstrap secrets only
hosts: localhost
connection: local
gather_facts: false
become: false
roles:
- pattern_settings

tasks:
- name: Run bootstrap-only secrets load
ansible.builtin.include_role:
name: load_secrets
tasks_from: bootstrap_only.yml
6 changes: 5 additions & 1 deletion roles/cluster_pre_check/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
---
- name: Check if the kubernetes python module is usable from ansible
ansible.builtin.command: "{{ ansible_python_interpreter }} -c 'import kubernetes'"
ansible.builtin.command:
argv:
- "{{ ansible_python_interpreter | default(ansible_playbook_python | default('python3')) }}"
- "-c"
- "import kubernetes"
changed_when: false

- name: Check if KUBECONFIG is correctly set
Expand Down
114 changes: 114 additions & 0 deletions roles/find_vp_secrets/tasks/find_optional_bootstrap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
# Sets values_secrets_bootstrap_data when a bootstrap values-secret file exists; otherwise no-op.
# Expects: pattern_name, and _primary_values_secrets_data_snapshot when restoring after read.
- name: Clear bootstrap secrets facts from any prior play
ansible.builtin.set_fact:
values_secrets_bootstrap_data: ''
vp_bootstrap_secrets_present: false
found_bootstrap_file: ''
vp_bootstrap_loaded_via_values_secret_env: false

# Dedicated bootstrap override (any path). Does not affect primary VALUES_SECRET resolution.
- name: Read VALUES_SECRET_BOOTSTRAP for optional bootstrap discovery
ansible.builtin.set_fact:
bootstrap_dedicated_env_values_secret: "{{ lookup('ansible.builtin.env', 'VALUES_SECRET_BOOTSTRAP') | default('', true) }}"

- name: Check if VALUES_SECRET_BOOTSTRAP points to an existing file
ansible.builtin.stat:
path: "{{ bootstrap_dedicated_env_values_secret }}"
register: bootstrap_dedicated_file_stat
when: bootstrap_dedicated_env_values_secret | default('') | string | length > 0

- name: Fail if VALUES_SECRET_BOOTSTRAP is set but file is missing
ansible.builtin.fail:
msg: >-
VALUES_SECRET_BOOTSTRAP is set to {{ bootstrap_dedicated_env_values_secret }} but that path does not exist.
when:
- bootstrap_dedicated_env_values_secret | default('') | string | length > 0
- bootstrap_dedicated_file_stat.stat is defined
- not bootstrap_dedicated_file_stat.stat.exists

- name: Use VALUES_SECRET_BOOTSTRAP as bootstrap secrets file
ansible.builtin.set_fact:
found_bootstrap_file: "{{ bootstrap_dedicated_file_stat.stat.path }}"
vp_bootstrap_loaded_via_values_secret_env: false
when:
- bootstrap_dedicated_env_values_secret | default('') | string | length > 0
- bootstrap_dedicated_file_stat.stat is defined
- bootstrap_dedicated_file_stat.stat.exists

# Legacy: VALUES_SECRET only if its basename looks like a bootstrap file (primary search still uses VALUES_SECRET otherwise).
- name: Read VALUES_SECRET for optional bootstrap discovery
ansible.builtin.set_fact:
bootstrap_custom_env_values_secret: "{{ lookup('ansible.builtin.env', 'VALUES_SECRET') }}"
when: (found_bootstrap_file | default('') | string | length) == 0

- name: Decide if VALUES_SECRET names a bootstrap file
ansible.builtin.set_fact:
_bootstrap_env_is_bootstrap_named: >-
{{
(bootstrap_custom_env_values_secret | default('') | string | length > 0)
and (bootstrap_custom_env_values_secret | regex_search('-bootstrap\.ya?ml$') is not none)
}}
when: (found_bootstrap_file | default('') | string | length) == 0

- name: Check if VALUES_SECRET points to an existing file (bootstrap)
ansible.builtin.stat:
path: "{{ bootstrap_custom_env_values_secret }}"
register: bootstrap_custom_file_values_secret
when:
- (found_bootstrap_file | default('') | string | length) == 0
- bootstrap_custom_env_values_secret | default('') | length > 0
- _bootstrap_env_is_bootstrap_named | default(false) | bool

- name: Use VALUES_SECRET as bootstrap secrets file
ansible.builtin.set_fact:
found_bootstrap_file: "{{ bootstrap_custom_file_values_secret.stat.path }}"
vp_bootstrap_loaded_via_values_secret_env: true
when:
- (found_bootstrap_file | default('') | string | length) == 0
- bootstrap_custom_env_values_secret | default('') | length > 0
- _bootstrap_env_is_bootstrap_named | default(false) | bool
- bootstrap_custom_file_values_secret.stat is defined
- bootstrap_custom_file_values_secret.stat.exists

- name: Build bootstrap values-secret candidate paths
ansible.builtin.set_fact:
_vp_bootstrap_secret_candidates:
- "~/.config/hybrid-cloud-patterns/values-secret-{{ pattern_name }}-bootstrap.yaml"
- "~/.config/validated-patterns/values-secret-{{ pattern_name }}-bootstrap.yaml"
- "~/values-secret-{{ pattern_name }}-bootstrap.yaml"
- "~/values-secret-bootstrap.yaml"
when: (found_bootstrap_file | default('') | string | length) == 0

- name: Stat bootstrap candidate paths
ansible.builtin.stat:
path: "{{ item }}"
loop: "{{ _vp_bootstrap_secret_candidates }}"
register: _vp_bootstrap_stat_results
when: (found_bootstrap_file | default('') | string | length) == 0

- name: Pick first existing bootstrap secrets file from candidates
ansible.builtin.set_fact:
found_bootstrap_file: "{{ (_vp_bootstrap_stat_results.results | default([]) | selectattr('stat.exists') | map(attribute='item') | list | first) | default('') }}"
when:
- (found_bootstrap_file | default('') | string | length) == 0
- _vp_bootstrap_stat_results.results is defined

- name: Read bootstrap secrets when a bootstrap file was found
when: (found_bootstrap_file | default('') | string | length) > 0
block:
- name: Load bootstrap secrets from file
ansible.builtin.include_tasks: read_secret_from_path.yml
vars:
found_file: "{{ found_bootstrap_file }}"

- name: Publish bootstrap secrets data for display
ansible.builtin.set_fact:
values_secrets_bootstrap_data: "{{ values_secrets_data }}"
vp_bootstrap_secrets_present: true

- name: Restore primary values_secrets_data after bootstrap read
ansible.builtin.set_fact:
values_secrets_data: "{{ _primary_values_secrets_data_snapshot }}"
when: _primary_values_secrets_data_snapshot is defined
Loading