Skip to content

Commit 18ebb46

Browse files
committed
feat(telco-kpis): Implement unified lockdowns role with hub capture and parse
Implements a unified lockdowns role for hub/spoke operator lockdown management, enabling repeatable OpenShift cluster deployments with locked operator versions. Key Features: 1. Unified Role Structure (lockdowns) - Mode-based dispatcher (hub/spoke) with action (parse/capture) - Common utilities: download, validation, symlink generation - Separate task files for hub and spoke operations - Jinja2 templates for lockdown JSON generation 2. Hub Lockdown Capture - Queries cluster for OCP version, Subscriptions, OperatorGroups, CatalogSources - Detects architecture via skopeo (x86_64/arm64, multi-arch support) - Maps mirrored catalog names to upstream (cs-redhat-operator-index-* → redhat-operators) - Enriches FBC operators with mirroring metadata: * fbc_iib_repo: 'latest' * ocp_operator_mirror_fbc_image_base: quay.io/redhat-user-workloads/telco-5g-tenant/{catalog}-{version} - Generates dual-format OCP pull specs (digest + tag) - Outputs timestamped lockdown JSON with cluster metadata 3. Hub Lockdown Parse - Downloads lockdown JSON from URI (GitLab, Gitea, file://) - GitLab symlink resolver handles multi-hop symlink chains: * lockdown-hub-x86_64.json → lockdown-hub-4.21-x86_64.json → ../4.21/actual.json * Detects JSON vs symlink text, resolves relative paths * Max 5 hops protection - Uses slurp module (not lookup) for SSH compatibility (Ansible controller vs remote host) - Validates lockdown structure (required fields, nested hierarchy) - Transforms operators for upstream compatibility: * Adds 'nsname' field from 'namespace' (upstream role requirement) - Sets facts: hub_lockdown_operators, hub_ocp_pull_spec, hub_ocp_version 4. Telco-KPIs Wrapper Playbook (deploy-ocp-operators.yml) - Three-phase workflow: * Phase 1: Parse lockdown (if hub_lockdown_uri provided) OR use parameters * Phase 2: Call upstream deploy-ocp-operators.yml with transformed operators * Phase 3: Capture lockdown (if generate_hub_lockdown requested) - Integrates with Jenkins install-hub-operators job - Supports both lockdown mode and parameter mode 5. Lockdown JSON Format - Nested structure: {hub: {ocp: {...}, operators: [...], metadata: {...}}} - OCP pull_spec with both digest (immutable) and tag (human-readable) - Operators include: name, namespace, catalog, channel, subscription_name, installed_csv, install_plan_approval, og_name, og_spec - FBC operators include additional: fbc_iib_repo, ocp_operator_mirror_fbc_image_base - Metadata: cluster_name, capture_timestamp (ISO8601) 6. Testing - Molecule test suites for hub parse and capture - 10 test scenarios covering parse, validation, symlinks, comparisons - Fixtures for direct JSON, 1-hop/2-hop symlinks, invalid JSON Implementation Details: - Uses kubernetes.core.k8s_info for cluster queries (no k8s_exec) - Hardcoded FBC metadata for relaxed repeatability (vs reading CatalogSource state) - Directory existence checks before template writes - Variable scoping fixes for lockdown_artifact_dir across playbook phases Fixes: - Jenkins builds #75-77, #79, #81, #84, #85 failures with hub lockdown URI - GitLab symlink resolution (/-/raw/ endpoint returns text, not target) - Ansible execution context (lookup vs slurp on remote hosts) - FBC operator mirroring metadata requirements - Field name mismatches (namespace vs nsname) Files: - playbooks/telco-kpis/deploy-ocp-operators.yml (new wrapper) - playbooks/telco-kpis/roles/lockdowns/ (unified role) - playbooks/telco-kpis/roles/lockdowns/tasks/{main,hub,spoke,common} - playbooks/telco-kpis/roles/lockdowns/templates/{hub,spoke,resolve-gitlab-symlinks} - playbooks/telco-kpis/roles/lockdowns/molecule/hub/default/ (tests) Integration: - Jenkins: jobs/Telco-KPIs/install-hub-operators.Jenkinsfile - GitLab: ran/dev-kpi-pipeline/-/tree/prow-lockdowns/hub/ - Upstream: playbooks/deploy-ocp-operators.yml (imports this wrapper) Related: - Legacy branch: ipa-telco-kpis-prow-migration-20260619-before-deploy-ocp-operators-untouched - Design docs: docs/designs/operator-lockdown-*.md - README: playbooks/telco-kpis/roles/lockdowns/README.md
1 parent b4fcb3c commit 18ebb46

30 files changed

Lines changed: 2510 additions & 1 deletion

playbooks/deploy-ocp-operators.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@
201201
ansible.builtin.include_role:
202202
name: ocp_operator_mirror
203203
vars:
204-
ocp_operator_mirror_registry_url: disconnected.registry.local:5000
204+
ocp_operator_mirror_registry_url: "{{ disconnected_registry_url }}:{{ disconnected_registry_port }}"
205205
ocp_operator_mirror_registry_user: "{{ ansible_user }}"
206206
ocp_operator_mirror_registry_password: "{{ ansible_password }}"
207207
ocp_operator_mirror_pull_secret: "{{ pull_secret_string | b64decode }}"
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
# Telco-KPIs wrapper for deploy-ocp-operators.yml
3+
#
4+
# This wrapper adds hub operator lockdown support to the upstream deploy-ocp-operators.yml playbook:
5+
# 1. Parse lockdown JSON if hub_lockdown_uri provided (extract operators list)
6+
# 2. Call upstream deploy-ocp-operators.yml with normalized operators variable
7+
# 3. Generate lockdown JSON if generate_hub_lockdown requested (capture cluster state)
8+
#
9+
# Usage:
10+
# # Without lockdown (uses HUB_OPERATORS parameter):
11+
# ansible-playbook playbooks/telco-kpis/deploy-ocp-operators.yml \
12+
# --extra-vars 'kubeconfig=/tmp/kubeconfig version=4.21 operators=[...]'
13+
#
14+
# # With lockdown URI (operators extracted from lockdown):
15+
# ansible-playbook playbooks/telco-kpis/deploy-ocp-operators.yml \
16+
# --extra-vars 'kubeconfig=/tmp/kubeconfig hub_lockdown_uri=https://gitea.../hub-lockdown.json'
17+
#
18+
# # Generate lockdown after installation:
19+
# ansible-playbook playbooks/telco-kpis/deploy-ocp-operators.yml \
20+
# --extra-vars 'kubeconfig=/tmp/kubeconfig version=4.21 operators=[...] generate_hub_lockdown=true hub_cluster=dev-kpi-01'
21+
22+
- name: Phase 1 - Parse Hub Lockdown (if provided)
23+
hosts: bastion
24+
gather_facts: true
25+
tasks:
26+
- name: Hub lockdown integration
27+
when:
28+
- hub_lockdown_uri is defined
29+
- hub_lockdown_uri | length > 0
30+
block:
31+
- name: Parse hub lockdown JSON using unified lockdowns role
32+
ansible.builtin.include_role:
33+
name: lockdowns
34+
vars:
35+
lockdown_mode: hub
36+
lockdown_action: parse
37+
38+
- name: Pass lockdown operators to upstream (no transformation needed)
39+
ansible.builtin.set_fact:
40+
operators: "{{ hub_lockdown_operators }}"
41+
version: "{{ hub_ocp_version }}"
42+
43+
- name: Display hub lockdown override
44+
ansible.builtin.debug:
45+
msg:
46+
- "=========================================="
47+
- "Hub Lockdown Mode: ENABLED"
48+
- "=========================================="
49+
- "Lockdown URI: {{ hub_lockdown_uri }}"
50+
- "OCP Version: {{ version }}"
51+
- "Operators Count: {{ operators | length }}"
52+
- "=========================================="
53+
- "NOTE: operators and version from lockdown override parameters"
54+
- "=========================================="
55+
56+
- name: Validate required variables
57+
ansible.builtin.assert:
58+
that:
59+
- operators is defined
60+
- version is defined or (hub_lockdown_uri is defined and hub_lockdown_uri | length > 0)
61+
- kubeconfig is defined
62+
fail_msg: >-
63+
Required variables: operators, version (or hub_lockdown_uri), kubeconfig
64+
success_msg: "Required variables validated"
65+
66+
- name: Apply known operator deployment workarounds (before operator installation)
67+
when:
68+
- not (mirror_only | default(false) | bool)
69+
ansible.builtin.include_role:
70+
name: workarounds
71+
vars:
72+
workarounds_talm_manifestwork_crd_operators: "{{ operators }}"
73+
74+
# Phase 2 - Call Upstream Operator Deployment
75+
- name: Phase 2 - Deploy Operators
76+
ansible.builtin.import_playbook: ../deploy-ocp-operators.yml
77+
# Variables passed through from caller or set by Phase 1:
78+
# - operators (from HUB_OPERATORS param OR hub lockdown)
79+
# - version (from OCP_VERSION param OR hub lockdown)
80+
# - disconnected (default: true for telco-kpis)
81+
# - mirror_only (default: false)
82+
# - kubeconfig
83+
84+
# Phase 3 - Generate Hub Lockdown (if requested)
85+
- name: Phase 3 - Capture Hub Lockdown
86+
hosts: bastion
87+
gather_facts: true
88+
tasks:
89+
- name: Generate hub lockdown JSON
90+
when:
91+
- generate_hub_lockdown is defined
92+
- generate_hub_lockdown | bool
93+
block:
94+
- name: Validate capture parameters
95+
ansible.builtin.assert:
96+
that:
97+
- hub_cluster is defined
98+
- kubeconfig is defined
99+
fail_msg: "hub_cluster and kubeconfig required for lockdown capture"
100+
success_msg: "Capturing lockdown for cluster: {{ hub_cluster }}"
101+
102+
- name: Set lockdown artifact directory (if not using Jenkins-provided path)
103+
when: hub_lockdown_output_file is not defined
104+
ansible.builtin.set_fact:
105+
lockdown_artifact_dir: "{{ lookup('env', 'ARTIFACT_DIR') | default('/artifacts', true) }}"
106+
107+
- name: Set lockdown output path
108+
ansible.builtin.set_fact:
109+
hub_lockdown_output_path: >-
110+
{{
111+
hub_lockdown_output_file
112+
if (hub_lockdown_output_file is defined)
113+
else (
114+
lockdown_artifact_dir + '/hub-lockdown-' + hub_cluster + '-' +
115+
(lookup('env', 'BUILD_NUMBER') | default(ansible_date_time.epoch, true)) + '.json'
116+
)
117+
}}
118+
119+
- name: Set lockdown artifact directory from output path
120+
ansible.builtin.set_fact:
121+
lockdown_artifact_dir: "{{ hub_lockdown_output_path | dirname }}"
122+
123+
- name: Capture hub lockdown using unified lockdowns role
124+
ansible.builtin.include_role:
125+
name: lockdowns
126+
vars:
127+
lockdown_mode: hub
128+
lockdown_action: capture
129+
hub_lockdown_output_file: "{{ hub_lockdown_output_path }}"
130+
131+
- name: Generate symlink for latest lockdown
132+
ansible.builtin.include_role:
133+
name: lockdowns
134+
tasks_from: common/generate-symlink.yml
135+
vars:
136+
lockdown_output_file: "{{ hub_lockdown_output_path }}"
137+
lockdown_mode: hub
138+
139+
- name: Display lockdown capture result
140+
ansible.builtin.debug:
141+
msg:
142+
- "=========================================="
143+
- "Hub Lockdown Captured Successfully"
144+
- "=========================================="
145+
- "Output File: {{ hub_lockdown_output_path }}"
146+
- "Symlink: {{ lockdown_artifact_dir }}/hub-lockdown-latest.json"
147+
- "=========================================="
148+
- "Upload this file to Gitea for future deployments"
149+
- "=========================================="
150+
151+
- name: Compare lockdown if both input and generated exist
152+
when:
153+
- generate_hub_lockdown is defined
154+
- generate_hub_lockdown | bool
155+
- hub_lockdown_data is defined
156+
block:
157+
- name: Write parsed input lockdown to temp file for comparison
158+
ansible.builtin.copy:
159+
content: "{{ hub_lockdown_data | to_nice_json }}"
160+
dest: /tmp/hub-lockdown-input.json
161+
mode: '0644'
162+
163+
- name: Compare input vs generated lockdown
164+
ansible.builtin.shell: |
165+
if command -v jd &> /dev/null; then
166+
jd -set /tmp/hub-lockdown-input.json {{ hub_lockdown_output_path }}
167+
elif command -v jq &> /dev/null; then
168+
echo "=== Input Lockdown ==="
169+
jq -S . /tmp/hub-lockdown-input.json > /tmp/input-sorted.json
170+
echo "=== Generated Lockdown ==="
171+
jq -S . {{ hub_lockdown_output_path }} > /tmp/generated-sorted.json
172+
diff -u /tmp/input-sorted.json /tmp/generated-sorted.json || true
173+
else
174+
echo "WARNING: Neither jd nor jq available for comparison"
175+
echo "Install jd (https://github.com/josephburnett/jd) for better diffs"
176+
fi
177+
register: lockdown_diff
178+
changed_when: false
179+
failed_when: false
180+
181+
- name: Display lockdown comparison result
182+
ansible.builtin.debug:
183+
msg: "{{ lockdown_diff.stdout_lines }}"
184+
when: lockdown_diff.stdout_lines is defined
185+
186+
- name: Save diff to artifacts
187+
ansible.builtin.copy:
188+
content: "{{ lockdown_diff.stdout }}"
189+
dest: "{{ lockdown_artifact_dir }}/hub-lockdown-diff.txt"
190+
mode: '0644'
191+
when: lockdown_diff.stdout is defined
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
.PHONY: test test-hub test-hub-parse test-hub-capture test-spoke test-hub-local test-spoke-local prepare converge verify clean help
2+
3+
# Get absolute path to eco-ci-cd root (5 levels up from role dir)
4+
ECO_CI_CD_ROOT := $(shell cd ../../../.. && pwd)
5+
6+
# Default target
7+
test: test-hub-parse test-hub-capture
8+
9+
# Run hub parse tests in container
10+
test-hub-parse:
11+
@echo "=========================================="
12+
@echo " Running Hub PARSE Tests"
13+
@echo "=========================================="
14+
@podman run --rm --platform linux/amd64 \
15+
-v $(ECO_CI_CD_ROOT):/eco-ci-cd:Z \
16+
-w /eco-ci-cd/playbooks/telco-kpis/roles/lockdowns \
17+
-e ANSIBLE_ROLES_PATH=/eco-ci-cd/playbooks/telco-kpis/roles \
18+
--entrypoint /bin/sh \
19+
quay.io/ccardenosa/eco-ci-cd:latest \
20+
-c "ansible-playbook -i localhost, -c local molecule/hub/default/test.yml"
21+
@echo ""
22+
@echo "✓ Hub parse tests passed!"
23+
24+
# Run hub capture tests in container
25+
test-hub-capture:
26+
@echo "=========================================="
27+
@echo " Running Hub CAPTURE Tests"
28+
@echo "=========================================="
29+
@podman run --rm --platform linux/amd64 \
30+
-v $(ECO_CI_CD_ROOT):/eco-ci-cd:Z \
31+
-w /eco-ci-cd/playbooks/telco-kpis/roles/lockdowns \
32+
-e ANSIBLE_ROLES_PATH=/eco-ci-cd/playbooks/telco-kpis/roles \
33+
--entrypoint /bin/sh \
34+
quay.io/ccardenosa/eco-ci-cd:latest \
35+
-c "ansible-playbook -i localhost, -c local molecule/hub/default/test-capture.yml"
36+
@echo ""
37+
@echo "✓ Hub capture tests passed!"
38+
39+
# Run all hub tests (parse + capture)
40+
test-hub: test-hub-parse test-hub-capture
41+
@echo ""
42+
@echo "✓ All hub tests passed!"
43+
44+
# Run spoke tests in container (when implemented)
45+
test-spoke:
46+
@echo "=========================================="
47+
@echo " Running Lockdowns Role Spoke Tests"
48+
@echo "=========================================="
49+
@echo "TODO: Spoke tests not yet implemented"
50+
51+
# Run individual hub test phases (for debugging - requires local ansible)
52+
prepare-hub:
53+
@ansible-playbook -i localhost, -c local molecule/hub/default/prepare.yml
54+
55+
converge-hub:
56+
@ansible-playbook -i localhost, -c local molecule/hub/default/converge.yml
57+
58+
verify-hub:
59+
@ansible-playbook -i localhost, -c local molecule/hub/default/verify.yml
60+
61+
# Clean up test artifacts
62+
clean:
63+
@rm -rf /tmp/molecule-tests
64+
@rm -f /tmp/hub-lockdown.json /tmp/spoke-lockdown.json
65+
@echo "✓ Test artifacts cleaned"
66+
67+
# Help target
68+
help:
69+
@echo "Lockdowns Role Tests"
70+
@echo ""
71+
@echo "Usage:"
72+
@echo " make test Run all hub tests (parse + capture)"
73+
@echo " make test-hub Run all hub tests (parse + capture)"
74+
@echo " make test-hub-parse Run hub parse tests only"
75+
@echo " make test-hub-capture Run hub capture tests only"
76+
@echo " make test-spoke Run spoke tests (TODO)"
77+
@echo " make prepare-hub Run hub prepare phase only (requires ansible)"
78+
@echo " make converge-hub Run hub converge phase only (requires ansible)"
79+
@echo " make verify-hub Run hub verify phase only (requires ansible)"
80+
@echo " make clean Remove test artifacts"
81+
@echo ""
82+
@echo "Hub Parse Tests:"
83+
@echo " 1. prepare - Creates mock hub lockdown JSON file"
84+
@echo " 2. converge - Runs role parse action (read JSON)"
85+
@echo " 3. verify - Validates parse results (10 assertions)"
86+
@echo ""
87+
@echo "Hub Capture Tests:"
88+
@echo " 1. prepare - Sets up mock k8s_info responses"
89+
@echo " 2. converge - Runs capture logic (generate JSON)"
90+
@echo " 3. verify - Validates generated JSON (10 assertions)"

0 commit comments

Comments
 (0)