qontract-reconcile 0.10.2.dev456__py3-none-any.whl → 0.10.2.dev473__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.

Potentially problematic release.


This version of qontract-reconcile might be problematic. Click here for more details.

@@ -17,7 +17,7 @@ from sretoolbox.container.image import (
17
17
  )
18
18
  from sretoolbox.container.skopeo import SkopeoCmdError
19
19
 
20
- from reconcile.quay_base import get_quay_api_store
20
+ from reconcile.quay_base import QuayApiStore
21
21
  from reconcile.quay_mirror import QuayMirror
22
22
  from reconcile.utils.quay_mirror import record_timestamp, sync_tag
23
23
 
@@ -45,7 +45,7 @@ class QuayMirrorOrg:
45
45
  ) -> None:
46
46
  self.dry_run = dry_run
47
47
  self.skopeo_cli = Skopeo(dry_run)
48
- self.quay_api_store = get_quay_api_store()
48
+ self.quay_api_store = QuayApiStore()
49
49
  self.compare_tags = compare_tags
50
50
  self.compare_tags_interval = compare_tags_interval
51
51
  self.orgs = orgs
@@ -71,6 +71,7 @@ class QuayMirrorOrg:
71
71
 
72
72
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
73
73
  self.session.close()
74
+ self.quay_api_store.cleanup()
74
75
 
75
76
  def run(self) -> None:
76
77
  sync_tasks = self.process_sync_tasks()
@@ -101,11 +102,9 @@ class QuayMirrorOrg:
101
102
  if self.orgs and org_key.org_name not in self.orgs:
102
103
  continue
103
104
 
104
- quay_api = org_info["api"]
105
105
  upstream_org_key = org_info["mirror"]
106
106
  assert upstream_org_key is not None
107
107
  upstream_org = self.quay_api_store[upstream_org_key]
108
- upstream_quay_api = upstream_org["api"]
109
108
 
110
109
  push_token = upstream_org["push_token"]
111
110
 
@@ -114,7 +113,10 @@ class QuayMirrorOrg:
114
113
  username = push_token["user"]
115
114
  token = push_token["token"]
116
115
 
116
+ quay_api = org_info["api"]
117
117
  org_repos = [item["name"] for item in quay_api.list_images()]
118
+
119
+ upstream_quay_api = upstream_org["api"]
118
120
  for repo in upstream_quay_api.list_images():
119
121
  if repo["name"] not in org_repos:
120
122
  continue
@@ -2,7 +2,10 @@ import logging
2
2
  import sys
3
3
  from typing import Any
4
4
 
5
- from reconcile.quay_base import OrgKey, get_quay_api_store
5
+ from reconcile.quay_base import (
6
+ OrgKey,
7
+ QuayApiStore,
8
+ )
6
9
  from reconcile.status import ExitCodes
7
10
  from reconcile.utils import gql
8
11
 
@@ -57,85 +60,88 @@ def run(dry_run: bool) -> None:
57
60
  return
58
61
 
59
62
  apps: list[dict[str, Any]] = result.get("apps") or []
60
- quay_api_store = get_quay_api_store()
61
63
  error = False
62
- for app in apps:
63
- quay_repo_configs = app.get("quayRepos")
64
- if not quay_repo_configs:
65
- continue
66
- for quay_repo_config in quay_repo_configs:
67
- instance_name = quay_repo_config["org"]["instance"]["name"]
68
- org_name = quay_repo_config["org"]["name"]
69
- org_key = OrgKey(instance_name, org_name)
70
-
71
- if not quay_repo_config["org"]["managedRepos"]:
72
- logging.error(
73
- f"[{app['name']}] Can not manage repo permissions in {org_name} "
74
- "since managedRepos is set to false."
75
- )
76
- error = True
77
- continue
78
-
79
- # processing quayRepos section
80
- logging.debug(["app", app["name"], instance_name, org_name])
81
64
 
82
- quay_api = quay_api_store[org_key]["api"]
83
- teams = quay_repo_config.get("teams")
84
- if not teams:
65
+ with QuayApiStore() as quay_api_store:
66
+ for app in apps:
67
+ quay_repo_configs = app.get("quayRepos")
68
+ if not quay_repo_configs:
85
69
  continue
86
- repos = quay_repo_config["items"]
87
- for repo in repos:
88
- repo_name = repo["name"]
89
-
90
- # processing repo section
91
- logging.debug(["repo", repo_name])
92
-
93
- for team in teams:
94
- permissions = team["permissions"]
95
- role = team["role"]
96
- for permission in permissions:
97
- if permission["service"] != "quay-membership":
98
- logging.warning(
99
- "wrong service kind, should be quay-membership"
70
+ for quay_repo_config in quay_repo_configs:
71
+ instance_name = quay_repo_config["org"]["instance"]["name"]
72
+ org_name = quay_repo_config["org"]["name"]
73
+ org_key = OrgKey(instance_name, org_name)
74
+
75
+ if not quay_repo_config["org"]["managedRepos"]:
76
+ logging.error(
77
+ f"[{app['name']}] Can not manage repo permissions in {org_name} "
78
+ "since managedRepos is set to false."
79
+ )
80
+ error = True
81
+ continue
82
+
83
+ # processing quayRepos section
84
+ logging.debug(["app", app["name"], instance_name, org_name])
85
+
86
+ org_data = quay_api_store[org_key]
87
+ teams = quay_repo_config.get("teams")
88
+ if not teams:
89
+ continue
90
+ repos = quay_repo_config["items"]
91
+ quay_api = org_data["api"]
92
+
93
+ for repo in repos:
94
+ repo_name = repo["name"]
95
+
96
+ # processing repo section
97
+ logging.debug(["repo", repo_name])
98
+
99
+ for team in teams:
100
+ permissions = team["permissions"]
101
+ role = team["role"]
102
+ for permission in permissions:
103
+ if permission["service"] != "quay-membership":
104
+ logging.warning(
105
+ "wrong service kind, should be quay-membership"
106
+ )
107
+ continue
108
+
109
+ perm_org_key = OrgKey(
110
+ permission["quayOrg"]["instance"]["name"],
111
+ permission["quayOrg"]["name"],
100
112
  )
101
- continue
102
-
103
- perm_org_key = OrgKey(
104
- permission["quayOrg"]["instance"]["name"],
105
- permission["quayOrg"]["name"],
106
- )
107
-
108
- if perm_org_key != org_key:
109
- logging.warning(f"wrong org, should be {org_key}")
110
- continue
111
113
 
112
- team_name = permission["team"]
113
-
114
- # processing team section
115
- logging.debug(["team", team_name])
116
- try:
117
- current_role = quay_api.get_repo_team_permissions(
118
- repo_name, team_name
119
- )
120
- if current_role != role:
121
- logging.info([
122
- "update_role",
123
- org_key,
124
- repo_name,
125
- team_name,
126
- role,
127
- ])
128
- if not dry_run:
129
- quay_api.set_repo_team_permissions(
130
- repo_name, team_name, role
131
- )
132
- except Exception:
133
- error = True
134
- logging.exception(
135
- "could not manage repo permissions: "
136
- f"repo name: {repo_name}, "
137
- f"team name: {team_name}"
138
- )
114
+ if perm_org_key != org_key:
115
+ logging.warning(f"wrong org, should be {org_key}")
116
+ continue
117
+
118
+ team_name = permission["team"]
119
+
120
+ # processing team section
121
+ logging.debug(["team", team_name])
122
+ try:
123
+ current_role = quay_api.get_repo_team_permissions(
124
+ repo_name, team_name
125
+ )
126
+ if current_role != role:
127
+ logging.info([
128
+ "update_role",
129
+ org_key,
130
+ repo_name,
131
+ team_name,
132
+ role,
133
+ ])
134
+ if not dry_run:
135
+ quay_api.set_repo_team_permissions(
136
+ repo_name, team_name, role
137
+ )
138
+ except Exception:
139
+ error = True
140
+ logging.exception(
141
+ "could not manage repo permissions: "
142
+ f"repo name: {repo_name}, "
143
+ f"team name: {team_name}"
144
+ )
139
145
 
140
146
  if error:
141
147
  sys.exit(ExitCodes.ERROR)
reconcile/quay_repos.py CHANGED
@@ -3,18 +3,14 @@ from __future__ import annotations
3
3
  import logging
4
4
  import sys
5
5
  from collections import namedtuple
6
- from typing import TYPE_CHECKING
7
6
 
8
7
  from reconcile.quay_base import (
9
8
  OrgKey,
10
- get_quay_api_store,
9
+ QuayApiStore,
11
10
  )
12
11
  from reconcile.status import ExitCodes
13
12
  from reconcile.utils import gql
14
13
 
15
- if TYPE_CHECKING:
16
- from reconcile.quay_base import QuayApiStore
17
-
18
14
  QUAY_REPOS_QUERY = """
19
15
  {
20
16
  apps: apps_v1 {
@@ -51,7 +47,6 @@ def fetch_current_state(quay_api_store: QuayApiStore) -> list[RepoInfo]:
51
47
  continue
52
48
 
53
49
  quay_api = org_info["api"]
54
-
55
50
  for repo in quay_api.list_images():
56
51
  name = repo["name"]
57
52
  public = repo["is_public"]
@@ -150,7 +145,8 @@ def act_delete(
150
145
  current_repo.name,
151
146
  ])
152
147
  if not dry_run:
153
- api = quay_api_store[current_repo.org_key]["api"]
148
+ org_data = quay_api_store[current_repo.org_key]
149
+ api = org_data["api"]
154
150
  api.repo_delete(current_repo.name)
155
151
 
156
152
 
@@ -164,7 +160,8 @@ def act_create(
164
160
  desired_repo.name,
165
161
  ])
166
162
  if not dry_run:
167
- api = quay_api_store[desired_repo.org_key]["api"]
163
+ org_data = quay_api_store[desired_repo.org_key]
164
+ api = org_data["api"]
168
165
  api.repo_create(
169
166
  desired_repo.name, desired_repo.description, desired_repo.public
170
167
  )
@@ -180,7 +177,8 @@ def act_description(
180
177
  desired_repo.description,
181
178
  ])
182
179
  if not dry_run:
183
- api = quay_api_store[desired_repo.org_key]["api"]
180
+ org_data = quay_api_store[desired_repo.org_key]
181
+ api = org_data["api"]
184
182
  api.repo_update_description(desired_repo.name, desired_repo.description)
185
183
 
186
184
 
@@ -194,7 +192,8 @@ def act_public(
194
192
  desired_repo.name,
195
193
  ])
196
194
  if not dry_run:
197
- api = quay_api_store[desired_repo.org_key]["api"]
195
+ org_data = quay_api_store[desired_repo.org_key]
196
+ api = org_data["api"]
198
197
  if desired_repo.public:
199
198
  api.repo_make_public(desired_repo.name)
200
199
  else:
@@ -223,32 +222,31 @@ def act(
223
222
 
224
223
 
225
224
  def run(dry_run: bool) -> None:
226
- quay_api_store = get_quay_api_store()
227
-
228
- # consistency checks
229
- for org_key, org_info in quay_api_store.items():
230
- if org_info.get("mirror"):
231
- # ensure there are no circular mirror dependencies
232
- mirror_org_key = org_info["mirror"]
233
- assert mirror_org_key is not None
234
- mirror_org = quay_api_store[mirror_org_key]
235
- if mirror_org.get("mirror"):
236
- logging.error(
237
- f"{mirror_org_key.instance}/"
238
- + f"{mirror_org_key.org_name} "
239
- + "can't have mirrors and be a mirror"
240
- )
241
- sys.exit(ExitCodes.ERROR)
225
+ with QuayApiStore() as quay_api_store:
226
+ # consistency checks
227
+ for org_key, org_info in quay_api_store.items():
228
+ if org_info.get("mirror"):
229
+ # ensure there are no circular mirror dependencies
230
+ mirror_org_key = org_info["mirror"]
231
+ assert mirror_org_key is not None
232
+ mirror_org = quay_api_store[mirror_org_key]
233
+ if mirror_org.get("mirror"):
234
+ logging.error(
235
+ f"{mirror_org_key.instance}/"
236
+ + f"{mirror_org_key.org_name} "
237
+ + "can't have mirrors and be a mirror"
238
+ )
239
+ sys.exit(ExitCodes.ERROR)
242
240
 
243
- # ensure no org defines `managedRepos` and `mirror` at the same
244
- if org_info.get("managedRepos"):
245
- logging.error(
246
- f"{org_key.instance}/{org_key.org_name} "
247
- + "has defined mirror and managedRepos"
248
- )
249
- sys.exit(ExitCodes.ERROR)
241
+ # ensure no org defines `managedRepos` and `mirror` at the same
242
+ if org_info.get("managedRepos"):
243
+ logging.error(
244
+ f"{org_key.instance}/{org_key.org_name} "
245
+ + "has defined mirror and managedRepos"
246
+ )
247
+ sys.exit(ExitCodes.ERROR)
250
248
 
251
- # run integration
252
- current_state = fetch_current_state(quay_api_store)
253
- desired_state = fetch_desired_state(quay_api_store)
254
- act(dry_run, quay_api_store, current_state, desired_state)
249
+ # run integration
250
+ current_state = fetch_current_state(quay_api_store)
251
+ desired_state = fetch_desired_state(quay_api_store)
252
+ act(dry_run, quay_api_store, current_state, desired_state)
reconcile/queries.py CHANGED
@@ -1815,7 +1815,7 @@ def get_review_repos() -> list[dict[str, str]]:
1815
1815
  return [
1816
1816
  {"url": c["url"], "name": c["name"]}
1817
1817
  for c in code_components
1818
- if c["showInReviewQueue"] is not None
1818
+ if c.get("showInReviewQueue", False)
1819
1819
  ]
1820
1820
 
1821
1821
 
@@ -141,11 +141,11 @@ class TemplateValidatorIntegration(QontractReconcileIntegration):
141
141
  if diffs:
142
142
  for diff in diffs:
143
143
  logging.error(f"template: {diff.template}, test: {diff.test}")
144
- # This log should never be added except for local debugging.
145
- # Credentials could be leaked, i.e. creating an MR with a diff,
146
- # using a template, that uses the vault function.
147
- # Use template-validator CLI instead.
148
144
  # logging.debug(diff.diff)
145
+
146
+ logging.error(
147
+ "The diff is never logged to avoid accidental credential leaks. Use template-validator CLI locally for debugging templates."
148
+ )
149
149
  raise ValueError("Template validation failed")
150
150
 
151
151
  @property
@@ -1,5 +1,6 @@
1
1
  import re
2
2
  import string
3
+ from enum import StrEnum
3
4
 
4
5
  from pydantic import BaseModel
5
6
 
@@ -9,6 +10,12 @@ PROMOTION_DATA_SEPARATOR = "**DO NOT MANUALLY CHANGE ANYTHING BELOW THIS LINE**"
9
10
  VERSION = "0.1.0"
10
11
  LABEL = "terraform-vpc-resources"
11
12
 
13
+
14
+ class Action(StrEnum):
15
+ CREATE = "create"
16
+ UPDATE = "update"
17
+
18
+
12
19
  VERSION_REF = "tf_vpc_resources_version"
13
20
  ACCOUNT_REF = "account"
14
21
  COMPILED_REGEXES = {
@@ -53,5 +60,8 @@ class Renderer:
53
60
  def render_description(self, account: str) -> str:
54
61
  return DESC.safe_substitute(account=account)
55
62
 
56
- def render_title(self, account: str) -> str:
57
- return f"[auto] VPC data file creation to {account}"
63
+ def render_title(self, account: str, action: Action) -> str:
64
+ return f"[auto] {action} VPC data file for {account}"
65
+
66
+ def render_update_title(self, account: str) -> str:
67
+ return f"[auto] VPC data file update for {account}"
@@ -5,6 +5,7 @@ from pydantic import BaseModel
5
5
 
6
6
  from reconcile.terraform_vpc_resources.merge_request import (
7
7
  LABEL,
8
+ Action,
8
9
  Info,
9
10
  Renderer,
10
11
  )
@@ -28,6 +29,7 @@ class VPCRequestMR(MergeRequestBase):
28
29
  vpc_tmpl_file_path: str,
29
30
  vpc_tmpl_file_content: str,
30
31
  labels: list[str],
32
+ action: Action,
31
33
  ):
32
34
  super().__init__()
33
35
  self._title = title
@@ -35,6 +37,7 @@ class VPCRequestMR(MergeRequestBase):
35
37
  self._vpc_tmpl_file_path = vpc_tmpl_file_path
36
38
  self._vpc_tmpl_file_content = vpc_tmpl_file_content
37
39
  self.labels = labels
40
+ self._action = action
38
41
 
39
42
  @property
40
43
  def title(self) -> str:
@@ -45,12 +48,21 @@ class VPCRequestMR(MergeRequestBase):
45
48
  return self._description
46
49
 
47
50
  def process(self, gitlab_cli: GitLabApi) -> None:
48
- gitlab_cli.create_file(
49
- branch_name=self.branch,
50
- file_path=self._vpc_tmpl_file_path,
51
- commit_message="add vpc datafile",
52
- content=self._vpc_tmpl_file_content,
53
- )
51
+ # Create or update file based on whether it already exists
52
+ if self._action == Action.UPDATE:
53
+ gitlab_cli.update_file(
54
+ branch_name=self.branch,
55
+ file_path=self._vpc_tmpl_file_path,
56
+ commit_message="update vpc datafile",
57
+ content=self._vpc_tmpl_file_content,
58
+ )
59
+ else:
60
+ gitlab_cli.create_file(
61
+ branch_name=self.branch,
62
+ file_path=self._vpc_tmpl_file_path,
63
+ commit_message="add vpc datafile",
64
+ content=self._vpc_tmpl_file_content,
65
+ )
54
66
 
55
67
 
56
68
  class MrData(BaseModel):
@@ -73,26 +85,37 @@ class MergeRequestManager(MergeRequestManagerBase[Info]):
73
85
  self._renderer = renderer
74
86
  self._auto_merge_enabled = auto_merge_enabled
75
87
 
76
- def create_merge_request(self, data: MrData) -> None:
77
- """Open a new MR, if not already present, for a VPC datafile and close any outdated before."""
78
- if not self._housekeeping_ran:
79
- self.housekeeping()
80
-
88
+ def _create_action(self, data: MrData) -> Action | None:
81
89
  if self._merge_request_already_exists({"account": data.account}):
82
90
  logging.info("MR already exists for %s", data.account)
83
91
  return None
84
-
85
92
  try:
86
- self._vcs.get_file_content_from_app_interface_ref(file_path=data.path)
87
- # the file exists, nothing to do
88
- return None
93
+ existing_content = self._vcs.get_file_content_from_app_interface_ref(
94
+ file_path=data.path
95
+ )
89
96
  except GitlabGetError as e:
90
- if e.response_code != 404:
91
- raise
97
+ if e.response_code == 404:
98
+ return Action.CREATE
99
+ raise
100
+
101
+ if existing_content.strip() != data.content.strip():
102
+ return Action.UPDATE
103
+
104
+ logging.info("VPC data file exists and is up-to-date for %s", data.account)
105
+ return None
106
+
107
+ def create_merge_request(self, data: MrData) -> None:
108
+ """Open a new MR for VPC datafile updates, or update existing if changed."""
109
+ if not self._housekeeping_ran:
110
+ self.housekeeping()
111
+ action = self._create_action(data)
112
+ if action is None:
113
+ return
92
114
 
93
115
  description = self._renderer.render_description(account=data.account)
94
- title = self._renderer.render_title(account=data.account)
95
- logging.info("Open MR for %s", data.account)
116
+ title = self._renderer.render_title(account=data.account, action=action)
117
+
118
+ logging.info("Open MR for %s (%s)", data.account, action)
96
119
  mr_labels = [LABEL]
97
120
  if self._auto_merge_enabled:
98
121
  mr_labels.append(AUTO_MERGE)
@@ -103,5 +126,6 @@ class MergeRequestManager(MergeRequestManagerBase[Info]):
103
126
  description=description,
104
127
  vpc_tmpl_file_content=data.content,
105
128
  labels=mr_labels,
129
+ action=action,
106
130
  )
107
131
  )
@@ -83,7 +83,7 @@ class SaasResourceTemplateTarget(
83
83
  if used_for_security_is_enabled():
84
84
  # When USED_FOR_SECURITY is enabled, use blake2s without digest_size and truncate to 20 bytes
85
85
  # This is needed for FIPS compliance where digest_size parameter is not supported
86
- return hashlib.blake2s(data).digest()[:20].hex()
86
+ return hashlib.sha256(data).digest()[:20].hex()
87
87
  else:
88
88
  # Default behavior: use blake2s with digest_size=20
89
89
  return hashlib.blake2s(data, digest_size=20).hexdigest()
@@ -157,6 +157,8 @@ class ExternalResourceSpec:
157
157
  tags["cost-center"] = cost_center
158
158
  if service_phase := self.namespace["environment"].get("servicePhase"):
159
159
  tags["service-phase"] = service_phase
160
+ if cost_center := self.namespace["environment"].get("costCenter"):
161
+ tags["cost-center"] = cost_center
160
162
 
161
163
  resource_tags_str = self.resource.get("tags")
162
164
  if resource_tags_str: