qontract-reconcile 0.10.1rc1181__py3-none-any.whl → 0.10.1rc1183__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.1rc1183
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
@@ -726,7 +726,7 @@ reconcile/utils/sqs_gateway.py,sha256=XNIf3PY4UCPNufP2Ul0UJj3fKlt5larBba-VTT-41F
726
726
  reconcile/utils/state.py,sha256=W0_awkLAPX18hNOF_60o73tkPxDUylqbzYNHfl_sDsk,16386
727
727
  reconcile/utils/structs.py,sha256=LcbLEg8WxfRqM6nW7NhcWN0YeqF7SQzxOgntmLs1SgY,352
728
728
  reconcile/utils/template.py,sha256=wTvRU4AnAV_o042tD4Mwls2dwWMuk7MKnde3MaCjaYg,331
729
- reconcile/utils/terraform_client.py,sha256=LjX2U2E0Dglt2S_KA5jWQ_dVC8sPn4FEAh0xW_d6JTk,35953
729
+ reconcile/utils/terraform_client.py,sha256=IQV6JsoGH-Fy987stSJ3_A2f2ys_cIj6s9761krePi4,36035
730
730
  reconcile/utils/terrascript_aws_client.py,sha256=q6Ydbjle7K5Z3LYpoRJGXnb50ix6aGN6jLvP2vglPz8,283769
731
731
  reconcile/utils/three_way_diff_strategy.py,sha256=oQcHXd9LVhirJfoaOBoHUYuZVGfyL2voKr6KVI34zZE,4833
732
732
  reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
@@ -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.1rc1183.dist-info/METADATA,sha256=awLi7LWrtIKv-9-jSwBD1xyD2dU782uKtYHtKl89r4w,2213
886
+ qontract_reconcile-0.10.1rc1183.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
887
+ qontract_reconcile-0.10.1rc1183.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
888
+ qontract_reconcile-0.10.1rc1183.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
889
+ qontract_reconcile-0.10.1rc1183.dist-info/RECORD,,
@@ -68,6 +68,10 @@ class TerraformCommandError(CalledProcessError):
68
68
  pass
69
69
 
70
70
 
71
+ class RdsUpgradeValidationError(Exception):
72
+ pass
73
+
74
+
71
75
  class DeletionApprovalExpirationValueError(Exception):
72
76
  pass
73
77
 
@@ -218,7 +222,7 @@ class TerraformClient: # pylint: disable=too-many-public-methods
218
222
  if disable_deletions_detected:
219
223
  raise RuntimeError("Terraform plan has disabled deletions detected")
220
224
 
221
- @retry()
225
+ @retry(no_retry_exceptions=RdsUpgradeValidationError)
222
226
  def terraform_plan(
223
227
  self, spec: TerraformSpec, enable_deletion: bool
224
228
  ) -> tuple[bool, list[AccountUser], bool]:
@@ -788,21 +792,19 @@ class TerraformClient: # pylint: disable=too-many-public-methods
788
792
  None,
789
793
  )
790
794
  if target is None:
791
- logging.error(
795
+ raise RdsUpgradeValidationError(
792
796
  f"Cannot upgrade RDS instance: {resource_name} "
793
797
  f"from {before_version} to {after_version}"
794
798
  )
795
- return
796
799
  allow_major_version_upgrade = after.get(
797
800
  "allow_major_version_upgrade",
798
801
  False,
799
802
  )
800
803
  if target["IsMajorVersionUpgrade"] and not allow_major_version_upgrade:
801
- logging.error(
804
+ raise RdsUpgradeValidationError(
802
805
  "allow_major_version_upgrade is not enabled for upgrading RDS instance: "
803
806
  f"{resource_name} to a new major version."
804
807
  )
805
- return
806
808
 
807
809
  blue_green_update = after.get("blue_green_update", [])
808
810
  if blue_green_update and blue_green_update[0]["enabled"]:
@@ -859,36 +861,32 @@ class TerraformClient: # pylint: disable=too-many-public-methods
859
861
  return False
860
862
 
861
863
  if not is_supported(engine, version):
862
- logging.error(
864
+ raise RdsUpgradeValidationError(
863
865
  f"Cannot upgrade RDS instance: {resource_name}. "
864
866
  f"Engine version {version} is not supported for blue/green updates."
865
867
  )
866
- return
867
868
 
868
869
  if replica:
869
- logging.error(
870
+ raise RdsUpgradeValidationError(
870
871
  f"Cannot upgrade RDS instance: {resource_name}. "
871
872
  "Blue/green updates are not supported for instances with read replicas."
872
873
  )
873
- return
874
874
 
875
875
  if engine == "postgres" and self._aws_api is not None:
876
876
  pg_details = self._aws_api.describe_db_parameter_group(
877
877
  account_name, parameter_group, region_name
878
878
  )
879
879
  if pg_details.get("rds.logical_replication") != "1":
880
- logging.error(
880
+ raise RdsUpgradeValidationError(
881
881
  f"Cannot upgrade RDS instance: {resource_name}. "
882
882
  f"Blue/green updates require logical replication to be enabled in the Parameter group {parameter_group}."
883
883
  )
884
- return
885
884
 
886
885
  if "storage_type" in changed_fields or "allocated_storage" in changed_fields:
887
- logging.error(
886
+ raise RdsUpgradeValidationError(
888
887
  f"Cannot upgrade RDS instance: {resource_name}. "
889
888
  f"Blue/green updates are not supported when 'storage_type' or 'allocated_storage' has changed."
890
889
  )
891
- return
892
890
 
893
891
 
894
892
  class TerraformPlanFailed(Exception):
@@ -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
+ ]