qontract-reconcile 0.10.1rc1181__py3-none-any.whl → 0.10.1rc1182__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc1181
3
+ Version: 0.10.1rc1182
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -838,10 +838,11 @@ tools/app_interface_metrics_exporter.py,sha256=zkwkxdAUAxjdc-pzx2_oJXG25fo0Fnyd5
838
838
  tools/app_interface_reporter.py,sha256=oZPib4HPq0aZ2Zui1QGJGk6qQdfpeihujGDBnSdKyGE,17627
839
839
  tools/glitchtip_access_reporter.py,sha256=oPBnk_YoDuljU3v0FaChzOwwnk4vap1xEE67QEjzdqs,2948
840
840
  tools/glitchtip_access_revalidation.py,sha256=8kbBJk04mkq28kWoRDDkfCGIF3GRg3pJrFAh1sW0dbk,2821
841
- tools/qontract_cli.py,sha256=YMOYkmP9WZFMX8wPxoJZ5ddstK3YyXMMx6ReXnCiH0w,140707
841
+ tools/qontract_cli.py,sha256=KfmjfaCuyd8F68oY1hHd2tEf0PY0kLd6t8sWqR81nmc,142498
842
842
  tools/sd_app_sre_alert_report.py,sha256=e9vAdyenUz2f5c8-z-5WY0wv-SJ9aePKDH2r4IwB6pc,5063
843
843
  tools/template_validation.py,sha256=qpKYaTgk0GOPGa2Ct5_5sKdwIHtCAKIBGzsMPuJU5fw,3371
844
844
  tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
845
+ tools/cli_commands/container_images_report.py,sha256=PCJIzvUqiYmTdn5xJFcxHocCYp6dprrsJ_lkYdl3ET8,4417
845
846
  tools/cli_commands/erv2.py,sha256=469qdhyaf7thpPQ4hJSurvmxBqYDJsoI8H4AigQIF7U,20737
846
847
  tools/cli_commands/gpg_encrypt.py,sha256=x02JOMn834z89YSNvr5B-oJky7rR1C0begCkPh45eHk,4958
847
848
  tools/cli_commands/systems_and_tools.py,sha256=EMHOF1AtUDaoSk0bbjl6oUKYAz4rTZjIBaF-6E6GspM,16816
@@ -876,12 +877,13 @@ tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
876
877
  tools/test/conftest.py,sha256=CsDbu4otrxb7X7kXKKGyV3ZEzu3pCkgjCoCGiHNx6zc,2401
877
878
  tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvftCWEEf-g1mfXOtgCog-g,1271
878
879
  tools/test/test_erv2.py,sha256=EAS7QuJkHisRVO9bMGxm662L5B6i66wF_mT9PAjVzrU,3128
880
+ tools/test/test_get_container_images.py,sha256=L2XzfmYAd6WZ17UXNnr8Z4iwoGcCvQ0vN6gxAZ7gEws,6097
879
881
  tools/test/test_qontract_cli.py,sha256=iuzKbQ6ahinvjoQmQLBrG4shey0z-1rB6qCgS8T6dgU,5789
880
882
  tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
881
883
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
882
884
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
883
- qontract_reconcile-0.10.1rc1181.dist-info/METADATA,sha256=pDZwlb7DGvqkX4trR3vqj0NqCsBK-LPoCvo46Uufung,2213
884
- qontract_reconcile-0.10.1rc1181.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
885
- qontract_reconcile-0.10.1rc1181.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
886
- qontract_reconcile-0.10.1rc1181.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
887
- qontract_reconcile-0.10.1rc1181.dist-info/RECORD,,
885
+ qontract_reconcile-0.10.1rc1182.dist-info/METADATA,sha256=gdReu60z2THXeO2P__JA2nYHErSt6uI8EoVgnxsl5ro,2213
886
+ qontract_reconcile-0.10.1rc1182.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
887
+ qontract_reconcile-0.10.1rc1182.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
888
+ qontract_reconcile-0.10.1rc1182.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
889
+ qontract_reconcile-0.10.1rc1182.dist-info/RECORD,,
@@ -0,0 +1,129 @@
1
+ import re
2
+ from collections import defaultdict
3
+ from collections.abc import Sequence
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel
7
+ from sretoolbox.utils import threaded
8
+
9
+ from reconcile.gql_definitions.common.namespaces_minimal import NamespaceV1
10
+ from reconcile.typed_queries.app_interface_vault_settings import (
11
+ get_app_interface_vault_settings,
12
+ )
13
+ from reconcile.typed_queries.namespaces_minimal import get_namespaces_minimal
14
+ from reconcile.utils.oc_filters import filter_namespaces_by_cluster_and_namespace
15
+ from reconcile.utils.oc_map import OCMap, init_oc_map_from_namespaces
16
+ from reconcile.utils.secret_reader import create_secret_reader
17
+
18
+ IMAGE_NAME_REGEX = re.compile(r"^(?P<name>[a-zA-Z0-9][a-zA-Z0-9/_.-]+)(?:@sha256)?:.+$")
19
+
20
+
21
+ class NamespaceImages(BaseModel):
22
+ namespace_name: str
23
+ image_names: list[str]
24
+
25
+
26
+ def get_all_pods_images(
27
+ cluster_name: Sequence[str] | None = None,
28
+ namespace_name: Sequence[str] | None = None,
29
+ thread_pool_size: int = 10,
30
+ use_jump_host: bool = True,
31
+ include_pattern: str | None = None,
32
+ exclude_pattern: str | None = None,
33
+ ) -> list[dict[str, Any]]:
34
+ """Gets all the images in the clusters/namespaces given. Returns a list of dicts
35
+ with the following keys:
36
+ * name: image name
37
+ * namespaces: a comma separated list of namespaces where the instance is used
38
+ * count: number of uses of the image
39
+ """
40
+ all_namespaces = get_namespaces_minimal()
41
+ namespaces = filter_namespaces_by_cluster_and_namespace(
42
+ namespaces=all_namespaces,
43
+ cluster_names=cluster_name,
44
+ namespace_names=namespace_name,
45
+ )
46
+ vault_settings = get_app_interface_vault_settings()
47
+ secret_reader = create_secret_reader(use_vault=vault_settings.vault)
48
+ oc_map = init_oc_map_from_namespaces(
49
+ namespaces=namespaces,
50
+ integration="qontract-cli-get-namespace_images",
51
+ secret_reader=secret_reader,
52
+ use_jump_host=use_jump_host,
53
+ thread_pool_size=thread_pool_size,
54
+ init_projects=True,
55
+ )
56
+
57
+ return fetch_pods_images_from_namespaces(
58
+ namespaces=namespaces,
59
+ oc_map=oc_map,
60
+ exclude_pattern=exclude_pattern,
61
+ include_pattern=include_pattern,
62
+ thread_pool_size=thread_pool_size,
63
+ )
64
+
65
+
66
+ def fetch_pods_images_from_namespaces(
67
+ namespaces: list[NamespaceV1],
68
+ oc_map: OCMap,
69
+ include_pattern: str | None = None,
70
+ exclude_pattern: str | None = None,
71
+ thread_pool_size: int = 10,
72
+ ) -> list[dict[str, Any]]:
73
+ all_namespace_images = threaded.run(
74
+ func=_get_namespace_images,
75
+ iterable=namespaces,
76
+ thread_pool_size=thread_pool_size,
77
+ oc_map=oc_map,
78
+ )
79
+
80
+ result: defaultdict = defaultdict(_get_all_images_default)
81
+ for ni in all_namespace_images:
82
+ for name in ni.image_names:
83
+ result[name]["namespaces"].add(ni.namespace_name)
84
+ result[name]["count"] += 1
85
+
86
+ exclude_pattern_compiled: re.Pattern | None = None
87
+ if exclude_pattern:
88
+ exclude_pattern_compiled = re.compile(exclude_pattern)
89
+
90
+ include_pattern_compiled: re.Pattern | None = None
91
+ if include_pattern:
92
+ include_pattern_compiled = re.compile(include_pattern)
93
+
94
+ result_filtered_flattened: list[dict[str, Any]] = []
95
+ for name, value in result.items():
96
+ if include_pattern_compiled and not include_pattern_compiled.match(name):
97
+ continue
98
+ if exclude_pattern_compiled and exclude_pattern_compiled.match(name):
99
+ continue
100
+
101
+ result_filtered_flattened.append({
102
+ "name": name,
103
+ "namespaces": ",".join(sorted(value["namespaces"])),
104
+ "count": value["count"],
105
+ })
106
+
107
+ return result_filtered_flattened
108
+
109
+
110
+ def _get_all_images_default() -> dict[str, Any]:
111
+ return {"namespaces": set(), "count": 0}
112
+
113
+
114
+ def _get_namespace_images(ns: NamespaceV1, oc_map: OCMap) -> NamespaceImages:
115
+ image_names = []
116
+ oc = oc_map.get_cluster(ns.cluster.name)
117
+ pod_items = oc.get_items("Pod", namespace=ns.name)
118
+ for pod in pod_items:
119
+ containers = pod.get("spec", {}).get("containers", [])
120
+ containers.extend(pod.get("spec", {}).get("initContainers", []))
121
+
122
+ for c in containers:
123
+ if m := IMAGE_NAME_REGEX.match(c["image"]):
124
+ image_names.append(m.group("name"))
125
+
126
+ return NamespaceImages(
127
+ namespace_name=ns.name,
128
+ image_names=image_names,
129
+ )
tools/qontract_cli.py CHANGED
@@ -63,9 +63,14 @@ from reconcile.checkpoint import report_invalid_metadata
63
63
  from reconcile.cli import (
64
64
  TERRAFORM_VERSION,
65
65
  TERRAFORM_VERSION_REGEX,
66
+ cluster_name,
66
67
  config_file,
68
+ namespace_name,
67
69
  use_jump_host,
68
70
  )
71
+ from reconcile.cli import (
72
+ threaded as thread_pool_size,
73
+ )
69
74
  from reconcile.gql_definitions.advanced_upgrade_service.aus_clusters import (
70
75
  query as aus_clusters_query,
71
76
  )
@@ -136,7 +141,9 @@ from reconcile.utils.oc import (
136
141
  OC_Map,
137
142
  OCLogMsg,
138
143
  )
139
- from reconcile.utils.oc_map import init_oc_map_from_clusters
144
+ from reconcile.utils.oc_map import (
145
+ init_oc_map_from_clusters,
146
+ )
140
147
  from reconcile.utils.ocm import OCM_PRODUCT_ROSA, OCMMap
141
148
  from reconcile.utils.ocm_base_client import init_ocm_base_client
142
149
  from reconcile.utils.output import print_output
@@ -4313,5 +4320,68 @@ def migrate(ctx, dry_run: bool, skip_build: bool) -> None:
4313
4320
  rich_print(f"[b red]Please remove the temporary directory ({tempdir}) manually!")
4314
4321
 
4315
4322
 
4323
+ @get.command(help="Get all container images in app-interface defined namespaces")
4324
+ @cluster_name
4325
+ @namespace_name
4326
+ @thread_pool_size()
4327
+ @use_jump_host()
4328
+ @click.option("--exclude-pattern", help="Exclude images that match this pattern")
4329
+ @click.option("--include-pattern", help="Only include images that match this pattern")
4330
+ @click.pass_context
4331
+ def container_images(
4332
+ ctx,
4333
+ cluster_name,
4334
+ namespace_name,
4335
+ thread_pool_size,
4336
+ use_jump_host,
4337
+ exclude_pattern,
4338
+ include_pattern,
4339
+ ):
4340
+ from tools.cli_commands.container_images_report import get_all_pods_images
4341
+
4342
+ results = get_all_pods_images(
4343
+ cluster_name=cluster_name,
4344
+ namespace_name=namespace_name,
4345
+ thread_pool_size=thread_pool_size,
4346
+ use_jump_host=use_jump_host,
4347
+ exclude_pattern=exclude_pattern,
4348
+ include_pattern=include_pattern,
4349
+ )
4350
+
4351
+ if ctx.obj["options"]["output"] == "md":
4352
+ json_table = {
4353
+ "filter": True,
4354
+ "fields": [
4355
+ {"key": "name", "sortable": True},
4356
+ {"key": "namespaces", "sortable": True},
4357
+ {"key": "count", "sortable": True},
4358
+ ],
4359
+ "items": results,
4360
+ }
4361
+
4362
+ print(
4363
+ f"""
4364
+ You can view the source of this Markdown to extract the JSON data.
4365
+
4366
+ {len(results)} container images found.
4367
+
4368
+ exclude-pattern = {exclude_pattern}
4369
+ include-pattern = {include_pattern}
4370
+
4371
+ ```json:table
4372
+ {json.dumps(json_table)}
4373
+ ```
4374
+ """
4375
+ )
4376
+ else:
4377
+ columns = [
4378
+ "name",
4379
+ "namespaces",
4380
+ "count",
4381
+ ]
4382
+ ctx.obj["options"]["sort"] = False
4383
+ print_output(ctx.obj["options"], results, columns)
4384
+
4385
+
4316
4386
  if __name__ == "__main__":
4317
4387
  root() # pylint: disable=no-value-for-parameter
@@ -0,0 +1,187 @@
1
+ import pytest
2
+ from pytest_mock import MockerFixture
3
+
4
+ from reconcile.gql_definitions.common.namespaces_minimal import ClusterV1, NamespaceV1
5
+ from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
6
+ from reconcile.test.fixtures import Fixtures
7
+ from reconcile.utils.oc import OCNative
8
+ from reconcile.utils.oc_map import OCMap
9
+ from tools.cli_commands.container_images_report import (
10
+ fetch_pods_images_from_namespaces,
11
+ )
12
+
13
+ fxt = Fixtures("container_images_report")
14
+
15
+
16
+ @pytest.fixture
17
+ def observability_pods() -> list[dict]:
18
+ return fxt.get_anymarkup("app-sre-observability-stage-pods.yaml")
19
+
20
+
21
+ @pytest.fixture
22
+ def pipeline_pods() -> list[dict]:
23
+ return fxt.get_anymarkup("app-sre-pipelines-pods.yaml")
24
+
25
+
26
+ @pytest.fixture
27
+ def namespaces() -> list[NamespaceV1]:
28
+ return [
29
+ NamespaceV1(
30
+ name="app-sre-observability-stage",
31
+ delete=None,
32
+ labels="{}",
33
+ clusterAdmin=None,
34
+ cluster=ClusterV1(
35
+ name="appsres09ue1",
36
+ serverUrl="https://api.appsres09ue1.24ep.p3.openshiftapps.com:443",
37
+ insecureSkipTLSVerify=None,
38
+ jumpHost=None,
39
+ automationToken=VaultSecret(
40
+ path="app-sre/integrations-output/openshift-cluster-bots/appsres09ue1",
41
+ field="token",
42
+ version=None,
43
+ format=None,
44
+ ),
45
+ clusterAdminAutomationToken=VaultSecret(
46
+ path="app-sre/integrations-output/openshift-cluster-bots/appsres09ue1-cluster-admin",
47
+ field="token",
48
+ version=None,
49
+ format=None,
50
+ ),
51
+ internal=True,
52
+ disable=None,
53
+ ),
54
+ ),
55
+ NamespaceV1(
56
+ name="app-sre-pipelines",
57
+ delete=None,
58
+ labels='{"provider": "tekton"}',
59
+ clusterAdmin=None,
60
+ cluster=ClusterV1(
61
+ name="appsres09ue1",
62
+ serverUrl="https://api.appsres09ue1.24ep.p3.openshiftapps.com:443",
63
+ insecureSkipTLSVerify=None,
64
+ jumpHost=None,
65
+ automationToken=VaultSecret(
66
+ path="app-sre/integrations-output/openshift-cluster-bots/appsres09ue1",
67
+ field="token",
68
+ version=None,
69
+ format=None,
70
+ ),
71
+ clusterAdminAutomationToken=VaultSecret(
72
+ path="app-sre/integrations-output/openshift-cluster-bots/appsres09ue1-cluster-admin",
73
+ field="token",
74
+ version=None,
75
+ format=None,
76
+ ),
77
+ internal=True,
78
+ disable=None,
79
+ ),
80
+ ),
81
+ ]
82
+
83
+
84
+ @pytest.fixture
85
+ def oc(
86
+ mocker: MockerFixture,
87
+ observability_pods: list[dict],
88
+ pipeline_pods: list[dict],
89
+ ) -> OCNative:
90
+ oc = mocker.patch("reconcile.utils.oc.OCNative", autospec=True)
91
+ oc.get_items.side_effect = [observability_pods, pipeline_pods]
92
+ return oc
93
+
94
+
95
+ @pytest.fixture
96
+ def oc_map(mocker: MockerFixture, oc: OCNative) -> OCMap:
97
+ oc_map = mocker.patch("reconcile.utils.oc_map.OCMap", autospec=True)
98
+ oc_map.get_cluster.return_value = oc
99
+ return oc_map
100
+
101
+
102
+ # convert a list of dicts into a set of tuples to use it in assertions
103
+ def _to_set(list_of_dicts: list[dict]) -> set[tuple]:
104
+ return {tuple(d.items()) for d in list_of_dicts}
105
+
106
+
107
+ def testfetch_no_filter(namespaces: list[NamespaceV1], oc_map: OCMap) -> None:
108
+ images = fetch_pods_images_from_namespaces(
109
+ namespaces=namespaces,
110
+ oc_map=oc_map,
111
+ thread_pool_size=2,
112
+ )
113
+
114
+ assert _to_set(images) == _to_set([
115
+ {
116
+ "name": "quay.io/prometheus/blackbox-exporter",
117
+ "namespaces": "app-sre-observability-stage",
118
+ "count": 1,
119
+ },
120
+ {
121
+ "name": "quay.io/redhat-services-prod/app-sre-tenant/gitlab-project-exporter-main/gitlab-project-exporter-main",
122
+ "namespaces": "app-sre-observability-stage",
123
+ "count": 1,
124
+ },
125
+ {
126
+ "name": "quay.io/app-sre/internal-redhat-ca",
127
+ "namespaces": "app-sre-observability-stage,app-sre-pipelines",
128
+ "count": 3,
129
+ },
130
+ {
131
+ "name": "quay.io/app-sre/clamav",
132
+ "namespaces": "app-sre-pipelines",
133
+ "count": 1,
134
+ },
135
+ {
136
+ "name": "quay.io/redhat-appstudio/clamav-db",
137
+ "namespaces": "app-sre-pipelines",
138
+ "count": 1,
139
+ },
140
+ {
141
+ "name": "registry.redhat.io/openshift-pipelines/pipelines-entrypoint-rhel8",
142
+ "namespaces": "app-sre-pipelines",
143
+ "count": 3,
144
+ },
145
+ {
146
+ "name": "quay.io/redhatproductsecurity/rapidast",
147
+ "namespaces": "app-sre-pipelines",
148
+ "count": 1,
149
+ },
150
+ ])
151
+
152
+
153
+ def testfetch_exclude_pattern(namespaces: list[NamespaceV1], oc_map: OCMap) -> None:
154
+ images = fetch_pods_images_from_namespaces(
155
+ namespaces=namespaces,
156
+ oc_map=oc_map,
157
+ thread_pool_size=2,
158
+ exclude_pattern="quay.io/redhat|quay.io/app-sre",
159
+ )
160
+ assert _to_set(images) == _to_set([
161
+ {
162
+ "name": "quay.io/prometheus/blackbox-exporter",
163
+ "namespaces": "app-sre-observability-stage",
164
+ "count": 1,
165
+ },
166
+ {
167
+ "name": "registry.redhat.io/openshift-pipelines/pipelines-entrypoint-rhel8",
168
+ "namespaces": "app-sre-pipelines",
169
+ "count": 3,
170
+ },
171
+ ])
172
+
173
+
174
+ def testfetch_include_pattern(namespaces: list[NamespaceV1], oc_map: OCMap) -> None:
175
+ images = fetch_pods_images_from_namespaces(
176
+ namespaces=namespaces,
177
+ oc_map=oc_map,
178
+ thread_pool_size=2,
179
+ include_pattern="^registry.redhat.io",
180
+ )
181
+ assert images == [
182
+ {
183
+ "name": "registry.redhat.io/openshift-pipelines/pipelines-entrypoint-rhel8",
184
+ "namespaces": "app-sre-pipelines",
185
+ "count": 3,
186
+ },
187
+ ]