Skip to content

Commit 38f1714

Browse files
committed
[env_op_images] Add pulled images report to env_op_images role
Cross-references pod images against ICSP/IDMS mirror rules to report which images have a mirror configured and which pull directly from the original registry. Co-authored-by: Cursor <cursor@cursor.com> Signed-off-by: nemarjan <nemarjan@redhat.com>
1 parent 70f4bb9 commit 38f1714

3 files changed

Lines changed: 238 additions & 0 deletions

File tree

roles/env_op_images/defaults/main.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@
2121
cifmw_env_op_images_dir: "{{ cifmw_basedir }}"
2222
cifmw_env_op_images_file: operator_images.yaml
2323
cifmw_env_op_images_dryrun: false
24+
25+
cifmw_env_op_images_pulled_report_file: pulled_images_report.yaml
26+
cifmw_env_op_images_pulled_report_namespaces:
27+
- "{{ cifmw_openstack_namespace | default('openstack') }}"
28+
- "{{ operator_namespace | default('openstack-operators') }}"

roles/env_op_images/tasks/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,6 @@
155155
dest: "{{ cifmw_env_op_images_dir }}/artifacts/{{ cifmw_env_op_images_file }}"
156156
content: "{{ _content | to_nice_yaml }}"
157157
mode: "0644"
158+
159+
- name: Generate pulled images registry report
160+
ansible.builtin.include_tasks: pulled_images_report.yml
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
---
2+
# Copyright Red Hat, Inc.
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
# Pulled-images report (policy view, not node-verified pulls):
18+
# - Load cluster ImageContentSourcePolicy + ImageDigestMirrorSet objects via oc.
19+
# - Flatten them into {source, mirror} pairs (repository prefix -> mirror ref).
20+
# - For each container/initContainer in selected namespaces, compare the pod
21+
# status image string to those prefixes: first matching rule yields
22+
# expected_pull_basis mirror and expected_pull_location from the mirror host;
23+
# otherwise basis source and location from the image ref host.
24+
# Matching uses image (reference string), not imageID; pod status often keeps
25+
# the upstream registry name even when the runtime used a mirror.
26+
27+
- name: Report pulled image registries per pod
28+
when:
29+
- cifmw_openshift_kubeconfig is defined
30+
environment:
31+
KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
32+
PATH: "{{ cifmw_path }}"
33+
block:
34+
- name: Ensure artifacts directory exists
35+
ansible.builtin.file:
36+
path: "{{ cifmw_env_op_images_dir }}/artifacts"
37+
state: directory
38+
mode: "0755"
39+
40+
# Legacy OpenShift mirror CRD; empty or error is OK (parsed as no items).
41+
- name: Get ICSP mirror rules
42+
ansible.builtin.command:
43+
cmd: oc get imagecontentsourcepolicy -o json
44+
register: _pulled_report_icsp
45+
failed_when: false
46+
47+
# Preferred / current mirror CRD alongside ICSP.
48+
- name: Get IDMS mirror rules
49+
ansible.builtin.command:
50+
cmd: oc get imagedigestmirrorset -o json
51+
register: _pulled_report_idms
52+
failed_when: false
53+
54+
# One flat list of rules for templating: each mirror list entry becomes
55+
# its own row so prefix matching can use the same loop for all mirrors.
56+
- name: Build source-to-mirror mapping from ICSP/IDMS
57+
vars:
58+
_icsp_items: >-
59+
{{ (_pulled_report_icsp.stdout | default('{}') | from_json).get('items', []) }}
60+
_idms_items: >-
61+
{{ (_pulled_report_idms.stdout | default('{}') | from_json).get('items', []) }}
62+
_mappings: >-
63+
{% set maps = [] %}
64+
{# ICSP spec.repositoryDigestMirrors: source repo + mirrors[] #}
65+
{% for icsp in _icsp_items %}
66+
{% for rdm in icsp.spec.get('repositoryDigestMirrors', []) %}
67+
{% if rdm.source is defined and rdm.source %}
68+
{% for m in rdm.get('mirrors', []) %}
69+
{% set _ = maps.append({'source': rdm.source, 'mirror': m}) %}
70+
{% endfor %}
71+
{% endif %}
72+
{% endfor %}
73+
{% endfor %}
74+
{# IDMS spec.imageDigestMirrors: same shape for matching purposes #}
75+
{% for idms in _idms_items %}
76+
{% for rdm in idms.spec.get('imageDigestMirrors', []) %}
77+
{% if rdm.source is defined and rdm.source %}
78+
{% for m in rdm.get('mirrors', []) %}
79+
{% set _ = maps.append({'source': rdm.source, 'mirror': m}) %}
80+
{% endfor %}
81+
{% endif %}
82+
{% endfor %}
83+
{% endfor %}
84+
{{ maps }}
85+
ansible.builtin.set_fact:
86+
_pulled_report_mirror_mappings: "{{ _mappings | trim | from_yaml }}"
87+
88+
- name: Report ICSP/IDMS mirror rules found
89+
when: _pulled_report_mirror_mappings | length > 0
90+
ansible.builtin.debug:
91+
msg: >-
92+
{{ item.source }} -> {{ item.mirror }}
93+
loop: "{{ _pulled_report_mirror_mappings }}"
94+
loop_control:
95+
label: "{{ item.source }}"
96+
97+
- name: Warn if no ICSP/IDMS mirror rules found
98+
when: _pulled_report_mirror_mappings | length == 0
99+
ansible.builtin.debug:
100+
msg: >-
101+
No ICSP or IDMS mirror rules found on the cluster.
102+
All rows will have expected_pull_basis: source and expected_pull_location from the image ref.
103+
104+
# Namespaces come from role default / caller; failed namespaces skip via failed_when: false.
105+
- name: Get pods per namespace
106+
kubernetes.core.k8s_info:
107+
kubeconfig: "{{ cifmw_openshift_kubeconfig }}"
108+
api_key: "{{ cifmw_openshift_token | default(omit) }}"
109+
context: "{{ cifmw_openshift_context | default(omit) }}"
110+
kind: Pod
111+
namespace: "{{ item }}"
112+
register: _pulled_report_pods
113+
loop: "{{ cifmw_env_op_images_pulled_report_namespaces | unique }}"
114+
loop_control:
115+
label: "{{ item }}"
116+
failed_when: false
117+
118+
# Flatten all loop results, then one report row per container + initContainer.
119+
- name: Build per-pod pulled images report
120+
vars:
121+
_report: >-
122+
{% set entries = [] %}
123+
{% set all_pods = _pulled_report_pods.results |
124+
map(attribute='resources', default=[]) | flatten %}
125+
{% for pod in all_pods %}
126+
{% set pod_labels = pod.metadata.labels | default({}) %}
127+
{% set operator_name = pod_labels.get('openstack.org/operator-name', '') %}
128+
{% set app_label = pod_labels.get('app', pod_labels.get('app.kubernetes.io/name', '')) %}
129+
{# Build a single list so regular and init containers share one loop #}
130+
{% set image_statuses = [] %}
131+
{% for cs in pod.status.containerStatuses | default([]) %}
132+
{% set _ = image_statuses.append({'cs': cs, 'type': 'container'}) %}
133+
{% endfor %}
134+
{% for cs in pod.status.initContainerStatuses | default([]) %}
135+
{% set _ = image_statuses.append({'cs': cs, 'type': 'init_container'}) %}
136+
{% endfor %}
137+
{% for entry in image_statuses %}
138+
{% set cs = entry.cs %}
139+
{% set image = cs.image | default('unknown') %}
140+
{% set image_id = cs.imageID | default('') %}
141+
{% set image_repo = image.split('@')[0].split(':')[0] %}
142+
{% set image_name = image_repo.split('/')[-1] %}
143+
{# Default: host = first path segment of image ref; basis = source #}
144+
{% set match = namespace(host=image.split('/')[0], expected_pull_basis='source') %}
145+
{# First mapping where image startswith source: use mirror registry host #}
146+
{% for m in _pulled_report_mirror_mappings if m.source is defined and m.source and image.startswith(m.source) %}
147+
{% if loop.first %}
148+
{% set match.host = m.mirror.split('/')[0] %}
149+
{% set match.expected_pull_basis = 'mirror' %}
150+
{% endif %}
151+
{% endfor %}
152+
{% set _ = entries.append({
153+
'namespace': pod.metadata.namespace,
154+
'pod': pod.metadata.name,
155+
'container': cs.name,
156+
'container_type': entry.type,
157+
'operator': operator_name if operator_name else 'N/A',
158+
'app': app_label if app_label else image_name,
159+
'image': image,
160+
'image_id': image_id,
161+
'expected_pull_location': match.host,
162+
'expected_pull_basis': match.expected_pull_basis
163+
}) %}
164+
{% endfor %}
165+
{% endfor %}
166+
{{ entries }}
167+
ansible.builtin.set_fact:
168+
_pulled_images_report: "{{ _report | trim | from_yaml }}"
169+
170+
# Counts for the YAML artifact summary section.
171+
- name: Build report summary
172+
vars:
173+
_total: "{{ _pulled_images_report | length }}"
174+
_basis_mirror: >-
175+
{{ _pulled_images_report |
176+
selectattr('expected_pull_basis', 'equalto', 'mirror') | list | length }}
177+
_basis_source: >-
178+
{{ _pulled_images_report |
179+
selectattr('expected_pull_basis', 'equalto', 'source') | list | length }}
180+
_mirror_rules: "{{ _pulled_report_mirror_mappings | length }}"
181+
ansible.builtin.set_fact:
182+
_pulled_report_summary:
183+
mirror_rules_found: "{{ _mirror_rules | int }}"
184+
mirror_rules: "{{ _pulled_report_mirror_mappings }}"
185+
total_containers: "{{ _total | int }}"
186+
containers_expected_basis_source: "{{ _basis_source | int }}"
187+
containers_expected_basis_mirror: "{{ _basis_mirror | int }}"
188+
189+
- name: Save pulled images report to artifacts
190+
vars:
191+
_full_report:
192+
summary: "{{ _pulled_report_summary }}"
193+
images: "{{ _pulled_images_report }}"
194+
ansible.builtin.copy:
195+
dest: >-
196+
{{
197+
(cifmw_env_op_images_dir, 'artifacts',
198+
cifmw_env_op_images_pulled_report_file) | path_join
199+
}}
200+
content: "{{ _full_report | to_nice_yaml }}"
201+
mode: "0644"
202+
203+
# Console visibility: split rows by how ICSP/IDMS classification turned out.
204+
- name: Images with expected_pull_basis source
205+
ansible.builtin.debug:
206+
msg: >-
207+
[{{ item.app }}] {{ item.image }} ->
208+
expected pull location {{ item.expected_pull_location }}
209+
({{ item.container_type }}: {{ item.container }}
210+
| operator: {{ item.operator }}
211+
| pod: {{ item.namespace }}/{{ item.pod }})
212+
loop: >-
213+
{{ _pulled_images_report |
214+
selectattr('expected_pull_basis', 'equalto', 'source') | list }}
215+
loop_control:
216+
label: "{{ item.app }}/{{ item.container }}"
217+
218+
- name: Images with expected_pull_basis mirror
219+
ansible.builtin.debug:
220+
msg: >-
221+
[{{ item.app }}] {{ item.image }} ->
222+
expected pull location {{ item.expected_pull_location }}
223+
({{ item.container_type }}: {{ item.container }}
224+
| operator: {{ item.operator }}
225+
| pod: {{ item.namespace }}/{{ item.pod }})
226+
loop: >-
227+
{{ _pulled_images_report |
228+
selectattr('expected_pull_basis', 'equalto', 'mirror') | list }}
229+
loop_control:
230+
label: "{{ item.app }}/{{ item.container }}"

0 commit comments

Comments
 (0)