Skip to content

Commit 0d2f243

Browse files
Add prepare_client_pod stage for warm client-pod bootstrap
When cifmw recreates shiftstackclient and jobs omit prepare (Pipeline B run-tests), the empty pod lacks oc, clouds.shiftstack, /etc/hosts, and resources.yml. Add a lightweight stage that restores only that subset from the PVC / install-config without re-running full prepare. Address review feedback: - Assert a clouds.yaml source exists before creating ~/.config/openstack - Bootstrap when oc or kubectl is missing - Resolve API/apps from FloatingIP with VIP/VIPs fallback for proxy IC - Run prepare_client_pod before create_installer_group - Reject original-config fallback unless clouds[user_cloud] is present Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f01bb8a commit 0d2f243

5 files changed

Lines changed: 330 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
# Lightweight client bootstrap when full "prepare" is skipped (e.g. Pipeline B
3+
# run-tests after a warm guest install). Mirrors the subset of prepare that is
4+
# lost when the shiftstackclient pod is recreated: oc, clouds.shiftstack,
5+
# /etc/hosts, and resources.yml.
6+
#
7+
# Do NOT recreate OpenStack projects, FIPs, or installer hosts here.
8+
prepare_client_pod_bootstrap_oc_dir: "{{ home_dir }}/bootstrap-oc"
9+
prepare_client_pod_bootstrap_oc_url: >-
10+
{{ openshift_mirror_url | default('https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp') }}/stable/openshift-client-linux.tar.gz
11+
prepare_client_pod_install_config: "{{ user_cloud_installation_dir }}/install-config.yaml"
12+
prepare_client_pod_metadata: "{{ ocp_installation_dir }}/metadata.json"
13+
# Same host rows as prepare/defaults etc_hosts_entries (api_ip / apps_ip at runtime).
14+
prepare_client_pod_etc_hosts_entries:
15+
- {regex: 'api\..*\.{{ ocp_base_domain }}', row: '{{ api_ip }} api.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
16+
- {regex: 'oauth-openshift.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
17+
- {regex: 'console-openshift-console.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} console-openshift-console.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
18+
- {regex: 'downloads-openshift-console.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} downloads-openshift-console.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
19+
- {regex: 'canary-openshift-ingress-canary.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} canary-openshift-ingress-canary.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
20+
- {regex: 'alertmanager-main-openshift-monitoring.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} alertmanager-main-openshift-monitoring.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
21+
- {regex: 'grafana-openshift-monitoring.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} grafana-openshift-monitoring.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
22+
- {regex: 'prometheus-k8s-openshift-monitoring.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} prometheus-k8s-openshift-monitoring.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
23+
- {regex: 'prometheus-k8s-federate-openshift-monitoring.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} prometheus-k8s-federate-openshift-monitoring.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
24+
- {regex: 'thanos-querier-openshift-monitoring.apps\..*\.{{ ocp_base_domain }}', row: '{{ apps_ip }} thanos-querier-openshift-monitoring.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}'}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
collections:
3+
- shiftstack.tools
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
# Prepare the recreated shiftstackclient / installer inventory host for warm
2+
# testing when the full prepare stage was skipped.
3+
4+
- name: Check for PVC OpenStack clouds.yaml
5+
ansible.builtin.stat:
6+
path: "{{ osp_config_dir }}/clouds.yaml"
7+
register: prepare_client_pod_pvc_clouds
8+
9+
- name: Check for original-config OpenStack clouds.yaml
10+
ansible.builtin.stat:
11+
path: "{{ home_dir }}/.original-config/openstack/clouds.yaml"
12+
register: prepare_client_pod_orig_clouds
13+
14+
- name: Fail when no OpenStack clouds.yaml source is available
15+
ansible.builtin.assert:
16+
that:
17+
- >-
18+
(prepare_client_pod_pvc_clouds.stat.exists | default(false))
19+
or (prepare_client_pod_orig_clouds.stat.exists | default(false))
20+
fail_msg: >-
21+
prepare_client_pod requires clouds.yaml at {{ osp_config_dir }}/clouds.yaml
22+
or {{ home_dir }}/.original-config/openstack/clouds.yaml to restore
23+
OpenStack client config for warm testing.
24+
25+
- name: Ensure OpenStack client config directory exists
26+
ansible.builtin.file:
27+
path: "{{ home_dir }}/.config/openstack"
28+
state: directory
29+
mode: u=rwx,g=rx,o=rx
30+
31+
- name: Restore OpenStack clouds config from PVC osp_config_dir
32+
when: prepare_client_pod_pvc_clouds.stat.exists | default(false)
33+
ansible.builtin.copy:
34+
src: "{{ osp_config_dir }}/"
35+
dest: "{{ home_dir }}/.config/openstack/"
36+
remote_src: true
37+
mode: preserve
38+
39+
- name: Load original-config clouds.yaml when PVC copy is absent
40+
when:
41+
- not (prepare_client_pod_pvc_clouds.stat.exists | default(false))
42+
- prepare_client_pod_orig_clouds.stat.exists | default(false)
43+
ansible.builtin.slurp:
44+
src: "{{ home_dir }}/.original-config/openstack/clouds.yaml"
45+
register: prepare_client_pod_orig_clouds_slurp
46+
47+
- name: Fail when original-config clouds.yaml lacks user_cloud
48+
when:
49+
- not (prepare_client_pod_pvc_clouds.stat.exists | default(false))
50+
- prepare_client_pod_orig_clouds.stat.exists | default(false)
51+
vars:
52+
_orig_clouds: >-
53+
{{
54+
prepare_client_pod_orig_clouds_slurp.content
55+
| b64decode
56+
| from_yaml
57+
}}
58+
ansible.builtin.assert:
59+
that:
60+
- user_cloud in (_orig_clouds.clouds | default({}))
61+
fail_msg: >-
62+
prepare_client_pod fallback
63+
{{ home_dir }}/.original-config/openstack/clouds.yaml does not define
64+
clouds.{{ user_cloud }}. The prepare stage writes that cloud into
65+
{{ osp_config_dir }}/clouds.yaml; restore the PVC copy or re-run prepare.
66+
67+
- name: Fall back to original-config OpenStack clouds when PVC copy is absent
68+
when:
69+
- not (prepare_client_pod_pvc_clouds.stat.exists | default(false))
70+
- prepare_client_pod_orig_clouds.stat.exists | default(false)
71+
ansible.builtin.copy:
72+
src: "{{ home_dir }}/.original-config/openstack/"
73+
dest: "{{ home_dir }}/.config/openstack/"
74+
remote_src: true
75+
mode: preserve
76+
77+
- name: Load restored clouds.yaml
78+
ansible.builtin.slurp:
79+
src: "{{ home_dir }}/.config/openstack/clouds.yaml"
80+
register: prepare_client_pod_restored_clouds_slurp
81+
82+
- name: Fail when restored clouds.yaml lacks user_cloud
83+
vars:
84+
_restored_clouds: >-
85+
{{
86+
prepare_client_pod_restored_clouds_slurp.content
87+
| b64decode
88+
| from_yaml
89+
}}
90+
ansible.builtin.assert:
91+
that:
92+
- user_cloud in (_restored_clouds.clouds | default({}))
93+
fail_msg: >-
94+
prepare_client_pod restored
95+
{{ home_dir }}/.config/openstack/clouds.yaml but clouds.{{ user_cloud }}
96+
is missing. Warm testing requires the project cloud written by prepare.
97+
98+
- name: Check for secure.yaml on destination and original-config
99+
ansible.builtin.stat:
100+
path: "{{ item }}"
101+
loop:
102+
- "{{ home_dir }}/.config/openstack/secure.yaml"
103+
- "{{ home_dir }}/.original-config/openstack/secure.yaml"
104+
register: prepare_client_pod_secure_stats
105+
106+
- name: Ensure secure.yaml is present when only available on original-config
107+
when:
108+
- not (prepare_client_pod_secure_stats.results[0].stat.exists | default(false))
109+
- prepare_client_pod_secure_stats.results[1].stat.exists | default(false)
110+
ansible.builtin.copy:
111+
src: "{{ home_dir }}/.original-config/openstack/secure.yaml"
112+
dest: "{{ home_dir }}/.config/openstack/secure.yaml"
113+
remote_src: true
114+
mode: preserve
115+
116+
- name: Check whether oc is already available
117+
ansible.builtin.command:
118+
cmd: which oc
119+
register: prepare_client_pod_oc_which
120+
changed_when: false
121+
failed_when: false
122+
123+
- name: Check whether kubectl is already available
124+
ansible.builtin.command:
125+
cmd: which kubectl
126+
register: prepare_client_pod_kubectl_which
127+
changed_when: false
128+
failed_when: false
129+
130+
- name: Bootstrap oc and kubectl from the OpenShift mirror
131+
when: >-
132+
prepare_client_pod_oc_which.rc != 0
133+
or prepare_client_pod_kubectl_which.rc != 0
134+
block:
135+
- name: Create bootstrap oc directory
136+
ansible.builtin.file:
137+
path: "{{ prepare_client_pod_bootstrap_oc_dir }}"
138+
state: directory
139+
mode: u=rwx,g=rw,o=r
140+
141+
- name: Download and extract stable oc client
142+
ansible.builtin.unarchive:
143+
src: "{{ prepare_client_pod_bootstrap_oc_url }}"
144+
dest: "{{ prepare_client_pod_bootstrap_oc_dir }}"
145+
remote_src: true
146+
include:
147+
- oc
148+
- kubectl
149+
register: prepare_client_pod_oc_download
150+
until: prepare_client_pod_oc_download is not failed
151+
retries: 3
152+
delay: 10
153+
154+
- name: Symlink oc into /usr/local/bin
155+
ansible.builtin.file:
156+
src: "{{ prepare_client_pod_bootstrap_oc_dir }}/oc"
157+
dest: /usr/local/bin/oc
158+
state: link
159+
become: true
160+
161+
- name: Symlink kubectl into /usr/bin
162+
ansible.builtin.file:
163+
src: "{{ prepare_client_pod_bootstrap_oc_dir }}/kubectl"
164+
dest: /usr/bin/kubectl
165+
state: link
166+
become: true
167+
168+
- name: Check install-config.yaml on the PVC
169+
ansible.builtin.stat:
170+
path: "{{ prepare_client_pod_install_config }}"
171+
register: prepare_client_pod_ic_stat
172+
173+
- name: Fail when install-config.yaml is missing
174+
ansible.builtin.assert:
175+
that:
176+
- prepare_client_pod_ic_stat.stat.exists | default(false)
177+
fail_msg: >-
178+
prepare_client_pod requires {{ prepare_client_pod_install_config }} on the
179+
PVC to restore guest API/apps addresses into /etc/hosts and resources.yml.
180+
181+
- name: Load install-config.yaml
182+
ansible.builtin.slurp:
183+
src: "{{ prepare_client_pod_install_config }}"
184+
register: prepare_client_pod_install_config_slurp
185+
186+
- name: Extract OpenStack platform keys from install-config
187+
vars:
188+
_ic: "{{ prepare_client_pod_install_config_slurp.content | b64decode | from_yaml }}"
189+
ansible.builtin.set_fact:
190+
prepare_client_pod_osp: "{{ _ic.platform.openstack | default({}) }}"
191+
prepare_client_pod_base_domain: "{{ _ic.baseDomain }}"
192+
prepare_client_pod_cluster_name: "{{ ocp_cluster_name }}"
193+
194+
# Prefer FloatingIP (standard IPI); fall back to VIP/VIPs used by proxy installs.
195+
- name: Resolve API accessible address (FloatingIP or VIP)
196+
vars:
197+
_fip: "{{ prepare_client_pod_osp.apiFloatingIP | default('') }}"
198+
_vip: "{{ prepare_client_pod_osp.apiVIP | default(prepare_client_pod_osp.apiVIPs | default('')) }}"
199+
ansible.builtin.set_fact:
200+
api_accessible_ip: >-
201+
{{
202+
_fip
203+
if (_fip | string | length > 0)
204+
else (
205+
_vip
206+
if (_vip is string and (_vip | length > 0))
207+
else ((_vip | list | first) | default(''))
208+
)
209+
}}
210+
211+
- name: Resolve apps/ingress accessible address (FloatingIP or VIP)
212+
vars:
213+
_fip: "{{ prepare_client_pod_osp.ingressFloatingIP | default('') }}"
214+
_vip: "{{ prepare_client_pod_osp.ingressVIP | default(prepare_client_pod_osp.ingressVIPs | default('')) }}"
215+
ansible.builtin.set_fact:
216+
apps_accessible_ip: >-
217+
{{
218+
_fip
219+
if (_fip | string | length > 0)
220+
else (
221+
_vip
222+
if (_vip is string and (_vip | length > 0))
223+
else ((_vip | list | first) | default(''))
224+
)
225+
}}
226+
227+
- name: Fail when API/apps addresses cannot be resolved from install-config
228+
ansible.builtin.assert:
229+
that:
230+
- api_accessible_ip | string | length > 0
231+
- apps_accessible_ip | string | length > 0
232+
fail_msg: >-
233+
prepare_client_pod could not resolve guest API/apps addresses from
234+
{{ prepare_client_pod_install_config }}. Expected
235+
platform.openstack.apiFloatingIP/ingressFloatingIP (standard IPI) or
236+
apiVIP/ingressVIP (or apiVIPs/ingressVIPs) for proxy deployments.
237+
238+
- name: Expose addresses for /etc/hosts template vars
239+
ansible.builtin.set_fact:
240+
api_ip: "{{ api_accessible_ip }}"
241+
apps_ip: "{{ apps_accessible_ip }}"
242+
243+
- name: Check for metadata.json
244+
ansible.builtin.stat:
245+
path: "{{ prepare_client_pod_metadata }}"
246+
register: prepare_client_pod_meta_stat
247+
248+
- name: Override cluster name from metadata.json when present
249+
when: prepare_client_pod_meta_stat.stat.exists | default(false)
250+
block:
251+
- name: Load metadata.json
252+
ansible.builtin.slurp:
253+
src: "{{ prepare_client_pod_metadata }}"
254+
register: prepare_client_pod_metadata_slurp
255+
256+
- name: Set cluster name from metadata
257+
vars:
258+
_meta: "{{ prepare_client_pod_metadata_slurp.content | b64decode | from_json }}"
259+
ansible.builtin.set_fact:
260+
prepare_client_pod_cluster_name: >-
261+
{{ _meta.clusterName | default(ocp_cluster_name) }}
262+
263+
- name: Check whether resources.yml already exists
264+
ansible.builtin.stat:
265+
path: "{{ resources_file }}"
266+
register: prepare_client_pod_resources_stat
267+
268+
- name: Write resources.yml when absent
269+
when: not (prepare_client_pod_resources_stat.stat.exists | default(false))
270+
ansible.builtin.include_role:
271+
name: shiftstack.tools.tools_register_resources_file
272+
vars:
273+
input:
274+
api_accessible_ip: "{{ api_accessible_ip }}"
275+
apps_accessible_ip: "{{ apps_accessible_ip }}"
276+
277+
- name: Restore guest API and apps entries in /etc/hosts
278+
become: true
279+
ansible.builtin.lineinfile:
280+
path: /etc/hosts
281+
regexp: "{{ item.regex }}"
282+
line: "{{ item.row }}"
283+
unsafe_writes: true
284+
vars:
285+
ocp_cluster_name: "{{ prepare_client_pod_cluster_name }}"
286+
ocp_base_domain: "{{ prepare_client_pod_base_domain }}"
287+
loop: "{{ prepare_client_pod_etc_hosts_entries }}"

‎playbooks/ocp_testing.yaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
mode: u=rwx,g=rwx,o=rwx
4242
when: "'cleanup' in stages or 'prepare' in stages"
4343

44+
# Must run before create_installer_group so OpenStack credentials exist when
45+
# server_info looks up the installer VM on a recreated client pod.
46+
- name: Prepare the shiftstackclient pod when full prepare was skipped
47+
ansible.builtin.import_playbook: plays/prepare_client_pod.yaml
48+
when: "'prepare_client_pod' in stages"
49+
4450
- name: Prepare the OpenShift environment
4551
hosts: localhost
4652
gather_facts: no
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
- name: Prepare the shiftstackclient pod for warm testing
3+
hosts: localhost
4+
gather_facts: true
5+
vars_files:
6+
- "../../configs/global.yml"
7+
tasks:
8+
- name: Bootstrap oc, clouds, hosts, and resources.yml on the client pod
9+
ansible.builtin.include_role:
10+
name: shiftstack.stages.prepare_client_pod

0 commit comments

Comments
 (0)