qontract-reconcile 0.10.2.dev465__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qontract-reconcile
3
- Version: 0.10.2.dev465
3
+ Version: 0.10.2.dev473
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Project-URL: homepage, https://github.com/app-sre/qontract-reconcile
6
6
  Project-URL: repository, https://github.com/app-sre/qontract-reconcile
@@ -82,13 +82,13 @@ reconcile/openshift_tekton_resources.py,sha256=5mUWEQqU9RpMLQZxHOb6IkbbZzx4iJnaV
82
82
  reconcile/openshift_upgrade_watcher.py,sha256=93l8X1-RHNtL89GzPg1XWivWart49l_hpAVFChvT6Wg,6643
83
83
  reconcile/openshift_users.py,sha256=lxHYrOhKntLnRy5mVZfh_8XWvwZaUgzrDNqkoon905U,5508
84
84
  reconcile/openshift_vault_secrets.py,sha256=Ax-_EBWWU1VRHYyKaUkGJkIjHGwWM3bZgjXL5CkPW8k,1883
85
- reconcile/quay_base.py,sha256=hfHv8ET6iw0GqPyncYJMRH7YFwJc5E1C9z7zET5MCjo,2327
86
- reconcile/quay_membership.py,sha256=No2sgEyTVj-hr5VPLy_xdrYAPvt-xo-CPpOt0X3x_6o,6623
85
+ reconcile/quay_base.py,sha256=4gNca076ICJ2fDoTwCgcA3L-DqmVmBgHWLCubTk78uc,2832
86
+ reconcile/quay_membership.py,sha256=ul4KcEdarri0WzH3ZBHzXup7oF9MhGLDpCyHtfxuuZ0,6844
87
87
  reconcile/quay_mirror.py,sha256=vhPL44TMEsritkiJhNQLf2Ir45RLApdO_mh3aZV0OuI,14334
88
- reconcile/quay_mirror_org.py,sha256=ltPbHuWUI8Wnl8gV4aeYmvoYFA1uXLWqlXqEPpw7Hi0,11065
89
- reconcile/quay_permissions.py,sha256=BF539lRxjpgwm88WzazklzgaCF_ipRALwbO2AdpqUqE,4388
90
- reconcile/quay_repos.py,sha256=fBleLzMtfDmTidpzbrTt8kGCy-Bk3J06EO4hhyghGnQ,7570
91
- reconcile/queries.py,sha256=L0NbIr6-M12P7xqU2s_2-tgi5BOC5QEo6Otu33WG5tI,56598
88
+ reconcile/quay_mirror_org.py,sha256=9YeOpRsuTOWxnql1oj-rbUAkb_aGSlzmHVbMyLhSalM,11092
89
+ reconcile/quay_permissions.py,sha256=wsQZ5wi2jrlTlV3Aq3dn_mJpjMlcjnjVvFNflBFBdGk,4706
90
+ reconcile/quay_repos.py,sha256=JpR9vyvDIdKfP4EwLw1c2X53LOjLZY35pJ8AvNxZIGo,7674
91
+ reconcile/queries.py,sha256=nEko5_luE4Xoj-nBI1XEX3YRcxfAObtvqZbN7LSlJVY,56597
92
92
  reconcile/query_validator.py,sha256=csOSkKxcf6ZlpchJu4ck2jLYKUN6y1l-UmSQUFHgssY,1618
93
93
  reconcile/requests_sender.py,sha256=g-tlrudvIqhneQPDMrfYF0Xsq7BSW2QcBPirl7hFM6I,4058
94
94
  reconcile/resource_scraper.py,sha256=TcMhXga7konX9x97NhpoijnDGWA-ZjdpiiXjm5qCmPk,2249
@@ -214,7 +214,7 @@ reconcile/glitchtip_project_alerts/integration.py,sha256=prje61EOuLEIZLLxlJS_YN0
214
214
  reconcile/glitchtip_project_dsn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
215
215
  reconcile/glitchtip_project_dsn/integration.py,sha256=3GgcqUM6hWhLpo9Yx5Xr9vrdexF-WNevVCNL9bJ0Upc,8162
216
216
  reconcile/gql_definitions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
217
- reconcile/gql_definitions/introspection.json,sha256=t_E8pSEQnQWn0rV-lMmdBnC5G6_KgF8qm0Og0VcdBYA,2430149
217
+ reconcile/gql_definitions/introspection.json,sha256=liDjRAOJ0_ZN7bagOacrd1yGlybaxK8V9rQTflnecxo,2429961
218
218
  reconcile/gql_definitions/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
219
219
  reconcile/gql_definitions/acs/acs_instances.py,sha256=VySMcnWddg-jXj-bj_ddLIwLX3u1GSFUm02H8rJDBYU,2167
220
220
  reconcile/gql_definitions/acs/acs_policies.py,sha256=jEV1U8j4VYL9ih17JSK1tiz2s_1CegVECmXU-NVEQvA,4333
@@ -307,7 +307,7 @@ reconcile/gql_definitions/endpoints_discovery/apps.py,sha256=p3hvzvrtkCCQfQoJ3mi
307
307
  reconcile/gql_definitions/external_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
308
308
  reconcile/gql_definitions/external_resources/aws_accounts.py,sha256=bRzfuPDLJvVJRx7IzqAJKnqpd7SBWdj3trI1rNPeYnU,2033
309
309
  reconcile/gql_definitions/external_resources/external_resources_modules.py,sha256=w07PFh526GaYnZRe-SH92MaxA-aeD2TDT2kG_3Da_HE,3241
310
- reconcile/gql_definitions/external_resources/external_resources_namespaces.py,sha256=jA2Q_j8-zEL57jJ10rb4xyUzQyOJZdKyHFquxQVd0pw,46606
310
+ reconcile/gql_definitions/external_resources/external_resources_namespaces.py,sha256=DGKKoJK7rOng2FBan8vxunLdlysX9Hb9_GTPsm2wyf8,46616
311
311
  reconcile/gql_definitions/external_resources/external_resources_settings.py,sha256=RvAMgipgH3MoLfWaqCPaYUy8GS3v0Dr4Cod17OsaNx0,3567
312
312
  reconcile/gql_definitions/external_resources/fragments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
313
313
  reconcile/gql_definitions/external_resources/fragments/external_resources_module_overrides.py,sha256=jjABgAhVx7LO3NJd9l90JH8s-_TJFvduBZRbdVquLfY,1313
@@ -427,7 +427,7 @@ reconcile/gql_definitions/terraform_repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5
427
427
  reconcile/gql_definitions/terraform_repo/terraform_repo.py,sha256=vocF7fpLk4Rdz1maZ2Hi_d5l4bt3kuL2HyDGvFIUu8M,3868
428
428
  reconcile/gql_definitions/terraform_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
429
429
  reconcile/gql_definitions/terraform_resources/database_access_manager.py,sha256=17CC1Wk65HtBn45Bo6iGsFNaLOnKD03MV3QQtixFtPw,4808
430
- reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py,sha256=q-X_7yCtlAXR1eTOfb9V1q9vWQymrP8lg5lKUOwkWrk,44754
430
+ reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py,sha256=oaLnYx6FtG33zDtE2TN8_iPXFfP8mRvPivYtoPo_8M8,44764
431
431
  reconcile/gql_definitions/terraform_tgw_attachments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
432
432
  reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py,sha256=VVWXcTGrHlZ8xqAf7p9Ygocwkm7dWZRaO_B6d_SamCc,2719
433
433
  reconcile/gql_definitions/unleash_feature_toggles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -507,7 +507,7 @@ reconcile/templates/rosa-classic-cluster-creation.sh.j2,sha256=7VlxlKpqIA9Pq7SMn
507
507
  reconcile/templates/rosa-hcp-cluster-creation.sh.j2,sha256=UbLexFWBsDbSUUe3-5S5aLaH1u_t8ikZnoKd5QTk_ro,2376
508
508
  reconcile/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
509
509
  reconcile/templating/renderer.py,sha256=nR1CM3DDOoLOH7UOxO0PMDkOmhiYFbGriKcjDA-Z8j8,14810
510
- reconcile/templating/validator.py,sha256=5f9f35PCHOOdjb7KZquL2YdabyuAUokPDa4xutSEHIQ,5360
510
+ reconcile/templating/validator.py,sha256=cyvSNsQqheIBl78HHDu4u_H82rt0zC8_YZL5BtT_Y1w,5266
511
511
  reconcile/templating/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
512
512
  reconcile/templating/lib/merge_request_manager.py,sha256=MaEI9Vrtblb5LIpaa394ACut0Rq_gzxJTfNVSgG113o,5234
513
513
  reconcile/templating/lib/model.py,sha256=YVUIXuPny3_kpFgBMSud8q_ndY5o882wKiX0l0A14L4,481
@@ -518,8 +518,8 @@ reconcile/terraform_init/merge_request.py,sha256=3CYtgSd7Q9zjKg4wsDz437EPCRfGeZZ
518
518
  reconcile/terraform_init/merge_request_manager.py,sha256=TQmtHq4DH-xgyYvuRyGu7VEgjPU2Yjj-uexIy-L7i88,3098
519
519
  reconcile/terraform_vpc_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
520
520
  reconcile/terraform_vpc_resources/integration.py,sha256=q5il4l4Bd9fmCQePy4XSy5R3nokVvcz1v2znwadelhU,9703
521
- reconcile/terraform_vpc_resources/merge_request.py,sha256=loRymUigCIvaaT0s_NzktZchh-DGRQnCICdBSCAcFPY,1503
522
- reconcile/terraform_vpc_resources/merge_request_manager.py,sha256=6jfwgbqXEFQlgLM6zmModpOkQX8wqddpoE0pZJL1Acc,3256
521
+ reconcile/terraform_vpc_resources/merge_request.py,sha256=MFRG7JQojmCTgr-9rLXVotcjH9mMIZZ0-kPai9UDJWA,1732
522
+ reconcile/terraform_vpc_resources/merge_request_manager.py,sha256=erCC9aY-gWMCVeLrFxG5_q3G2p3iVBS8eeYIUZR_6_o,4123
523
523
  reconcile/typed_queries/__init__.py,sha256=rRk4CyslLsBr4vAh1pIPgt6s3P4R1M9NSEPLnyQgBpk,61
524
524
  reconcile/typed_queries/alerting_services_settings.py,sha256=YKQd60O_2C_H103nLrYgcUInndM2vFypqW_NO706L2E,833
525
525
  reconcile/typed_queries/app_interface_custom_messages.py,sha256=bgSAJEzqee8aiPVCj_bIIqb4VTkrF0-vti1dos7ebEg,684
@@ -644,7 +644,7 @@ reconcile/utils/password_validator.py,sha256=knR6jJGc-v44v-hhQFvpYrEubuFfCCc3Qly
644
644
  reconcile/utils/prometheus.py,sha256=Ad0rwLbxRuuYjHwkwJloHEdK0bvy42h-p-HIT1DhDhs,3832
645
645
  reconcile/utils/promotion_state.py,sha256=4NTBswYkzxlJIIkMz4j92dOj-jn0m36DKQg8CqZEyo8,3910
646
646
  reconcile/utils/promtool.py,sha256=YnqwMAzsQVGuBZ1j9zy3UcVPFQVJgBMLzQkxhK_KFkU,3079
647
- reconcile/utils/quay_api.py,sha256=ZWjfjzFnIsbKRDcdAnP9tWQezclf53I7VWZJ0gbF2kE,8260
647
+ reconcile/utils/quay_api.py,sha256=zlzM-1_GsVSQVd2z0SMwx3uv5wqzZ7JUKE5ZOqQPKXc,7728
648
648
  reconcile/utils/quay_mirror.py,sha256=dpWCNv5lITwIk6Q9RkmqaQKHNk_JPy27UQEribJ7E-U,1324
649
649
  reconcile/utils/raw_github_api.py,sha256=ZUDtOxdMSMs-Z0noKi0pyMtXHi5V2nCMFDB5JIM_oQ0,3057
650
650
  reconcile/utils/repo_owners.py,sha256=c6Z-U5TkiRPvuhr_zYWvZG9HZGzoT-l-d2PJ33lGflE,6507
@@ -802,7 +802,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
802
802
  tools/saas_promotion_state/saas_promotion_state.py,sha256=uQv2QJAmUXP1g2GPIH30WTlvL9soY6m9lefpZEVDM5w,3965
803
803
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
804
804
  tools/sre_checkpoints/util.py,sha256=KcYVfa3UmJHVP_ocgrKe8NkrO5IDB9aWEDydSokPcRk,975
805
- qontract_reconcile-0.10.2.dev465.dist-info/METADATA,sha256=ScW_C4JCmkdyTIXDqZrqfFVmZtjNv3mNn_WklW7ZtTo,24948
806
- qontract_reconcile-0.10.2.dev465.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
807
- qontract_reconcile-0.10.2.dev465.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
808
- qontract_reconcile-0.10.2.dev465.dist-info/RECORD,,
805
+ qontract_reconcile-0.10.2.dev473.dist-info/METADATA,sha256=cqVPSqOjgcxHnzId0KJ1x9lHen_cPLUwfkBQjZe6ULI,24948
806
+ qontract_reconcile-0.10.2.dev473.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
807
+ qontract_reconcile-0.10.2.dev473.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
808
+ qontract_reconcile-0.10.2.dev473.dist-info/RECORD,,
@@ -1184,7 +1184,7 @@ class AppV1(ConfiguredBaseModel):
1184
1184
  path: str = Field(..., alias="path")
1185
1185
  name: str = Field(..., alias="name")
1186
1186
  app_code: str = Field(..., alias="appCode")
1187
- cost_center: str = Field(..., alias="costCenter")
1187
+ cost_center: Optional[str] = Field(..., alias="costCenter")
1188
1188
 
1189
1189
 
1190
1190
  class ClusterSpecV1(ConfiguredBaseModel):
@@ -16708,13 +16708,9 @@
16708
16708
  "description": null,
16709
16709
  "args": [],
16710
16710
  "type": {
16711
- "kind": "NON_NULL",
16712
- "name": null,
16713
- "ofType": {
16714
- "kind": "SCALAR",
16715
- "name": "String",
16716
- "ofType": null
16717
- }
16711
+ "kind": "SCALAR",
16712
+ "name": "String",
16713
+ "ofType": null
16718
16714
  },
16719
16715
  "isDeprecated": false,
16720
16716
  "deprecationReason": null
@@ -1117,7 +1117,7 @@ class EnvironmentV1(ConfiguredBaseModel):
1117
1117
  class AppV1(ConfiguredBaseModel):
1118
1118
  name: str = Field(..., alias="name")
1119
1119
  app_code: str = Field(..., alias="appCode")
1120
- cost_center: str = Field(..., alias="costCenter")
1120
+ cost_center: Optional[str] = Field(..., alias="costCenter")
1121
1121
 
1122
1122
 
1123
1123
  class ClusterSpecV1(ConfiguredBaseModel):
reconcile/quay_base.py CHANGED
@@ -1,4 +1,4 @@
1
- from collections import namedtuple
1
+ from collections import UserDict, namedtuple
2
2
  from typing import Any, TypedDict
3
3
 
4
4
  from reconcile import queries
@@ -10,22 +10,34 @@ OrgKey = namedtuple("OrgKey", ["instance", "org_name"])
10
10
 
11
11
  class OrgInfo(TypedDict):
12
12
  url: str
13
- api: QuayApi
14
13
  push_token: dict[str, str] | None
15
14
  teams: list[str]
16
15
  managedRepos: bool
17
16
  mirror: OrgKey | None
18
17
  mirror_filters: dict[str, Any]
18
+ api: QuayApi
19
+
19
20
 
21
+ class QuayApiStore(UserDict[OrgKey, OrgInfo]):
22
+ def __init__(self) -> None:
23
+ super().__init__(get_quay_api_store())
20
24
 
21
- QuayApiStore = dict[OrgKey, OrgInfo]
25
+ def cleanup(self) -> None:
26
+ """Close all QuayApi sessions."""
27
+ for org_info in self.data.values():
28
+ org_info["api"].cleanup()
22
29
 
30
+ def __enter__(self) -> "QuayApiStore":
31
+ return self
23
32
 
24
- def get_quay_api_store() -> QuayApiStore:
33
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
34
+ self.cleanup()
35
+
36
+
37
+ def get_quay_api_store() -> dict[OrgKey, OrgInfo]:
25
38
  """
26
39
  Returns a dictionary with a key for each Quay organization
27
40
  managed in app-interface.
28
- Each key contains an initiated QuayApi instance.
29
41
  """
30
42
  quay_orgs = queries.get_quay_orgs()
31
43
  settings = queries.get_app_interface_settings()
@@ -61,14 +73,21 @@ def get_quay_api_store() -> QuayApiStore:
61
73
  else:
62
74
  push_token = None
63
75
 
76
+ # Create QuayApi instance for this org
77
+ api = QuayApi(
78
+ token=token,
79
+ organization=org_name,
80
+ base_url=base_url,
81
+ )
82
+
64
83
  org_info: OrgInfo = {
65
84
  "url": base_url,
66
- "api": QuayApi(token, org_name, base_url=base_url),
67
85
  "push_token": push_token,
68
86
  "teams": org_data.get("managedTeams") or [],
69
87
  "managedRepos": bool(org_data.get("managedRepos")),
70
88
  "mirror": mirror,
71
89
  "mirror_filters": mirror_filters,
90
+ "api": api,
72
91
  }
73
92
 
74
93
  store[org_key] = org_info
@@ -10,7 +10,7 @@ from reconcile.gql_definitions.quay_membership.quay_membership import (
10
10
  PermissionQuayOrgTeamV1,
11
11
  UserV1,
12
12
  )
13
- from reconcile.quay_base import QuayApiStore, get_quay_api_store
13
+ from reconcile.quay_base import QuayApiStore
14
14
  from reconcile.status import ExitCodes
15
15
  from reconcile.utils import (
16
16
  expiration,
@@ -58,10 +58,11 @@ def fetch_current_state(quay_api_store: QuayApiStore) -> AggregatedList:
58
58
  state = AggregatedList()
59
59
 
60
60
  for org_key, org_data in quay_api_store.items():
61
- quay_api = org_data["api"]
62
61
  teams = org_data["teams"]
63
62
  if not teams:
64
63
  continue
64
+
65
+ quay_api = org_data["api"]
65
66
  for team in teams:
66
67
  try:
67
68
  members = quay_api.list_team_members(team)
@@ -77,7 +78,11 @@ def fetch_current_state(quay_api_store: QuayApiStore) -> AggregatedList:
77
78
  # Teams are only added to the state if they exist so that
78
79
  # there is a proper diff between the desired and current state.
79
80
  state.add(
80
- {"service": "quay-membership", "org": org_key, "team": team},
81
+ {
82
+ "service": "quay-membership",
83
+ "org": org_key.org_name,
84
+ "team": team,
85
+ },
81
86
  members,
82
87
  )
83
88
  return state
@@ -117,9 +122,10 @@ class RunnerAction:
117
122
  def action(params: dict, items: list) -> bool:
118
123
  org = params["org"]
119
124
  team = params["team"]
125
+ org_data = self.quay_api_store[org]
126
+ quay_api = org_data["api"]
120
127
 
121
128
  missing_users = False
122
- quay_api = self.quay_api_store[org]["api"]
123
129
  for member in items:
124
130
  logging.info([label, member, org, team])
125
131
  user_exists = quay_api.user_exists(member)
@@ -147,10 +153,11 @@ class RunnerAction:
147
153
  def action(params: dict, items: list) -> bool:
148
154
  org = params["org"]
149
155
  team = params["team"]
156
+ org_data = self.quay_api_store[org]
150
157
 
151
158
  # Ensure all quay org/teams are declared as dependencies in a
152
159
  # `/dependencies/quay-org-1.yml` datafile.
153
- if team not in self.quay_api_store[org]["teams"]:
160
+ if team not in org_data["teams"]:
154
161
  raise RunnerError(
155
162
  f"Quay team {team} is not defined as a "
156
163
  f"managedTeam in the {org} org."
@@ -159,7 +166,7 @@ class RunnerAction:
159
166
  logging.info([label, org, team])
160
167
 
161
168
  if not self.dry_run:
162
- quay_api = self.quay_api_store[org]["api"]
169
+ quay_api = org_data["api"]
163
170
  quay_api.create_or_update_team(team)
164
171
 
165
172
  return True
@@ -172,12 +179,13 @@ class RunnerAction:
172
179
  def action(params: dict, items: list) -> bool:
173
180
  org = params["org"]
174
181
  team = params["team"]
182
+ org_data = self.quay_api_store[org]
175
183
 
184
+ quay_api = org_data["api"]
176
185
  if self.dry_run:
177
186
  for member in items:
178
187
  logging.info([label, member, org, team])
179
188
  else:
180
- quay_api = self.quay_api_store[org]["api"]
181
189
  for member in items:
182
190
  logging.info([label, member, org, team])
183
191
  quay_api.remove_user_from_team(member, team)
@@ -188,24 +196,23 @@ class RunnerAction:
188
196
 
189
197
 
190
198
  def run(dry_run: bool) -> None:
191
- quay_api_store = get_quay_api_store()
192
-
193
- current_state = fetch_current_state(quay_api_store)
194
- desired_state = fetch_desired_state()
195
-
196
- # calculate diff
197
- diff = current_state.diff(desired_state)
198
- logging.debug("State diff: %s", diff)
199
-
200
- # Run actions
201
- runner_action = RunnerAction(dry_run, quay_api_store)
202
- runner = AggregatedDiffRunner(diff)
203
-
204
- runner.register("insert", runner_action.create_team())
205
- runner.register("update-insert", runner_action.add_to_team())
206
- runner.register("update-delete", runner_action.del_from_team())
207
- runner.register("delete", runner_action.del_from_team())
208
-
209
- status = runner.run()
210
- if not status:
211
- sys.exit(ExitCodes.ERROR)
199
+ with QuayApiStore() as quay_api_store:
200
+ current_state = fetch_current_state(quay_api_store)
201
+ desired_state = fetch_desired_state()
202
+
203
+ # calculate diff
204
+ diff = current_state.diff(desired_state)
205
+ logging.debug("State diff: %s", diff)
206
+
207
+ # Run actions
208
+ runner_action = RunnerAction(dry_run, quay_api_store)
209
+ runner = AggregatedDiffRunner(diff)
210
+
211
+ runner.register("insert", runner_action.create_team())
212
+ runner.register("update-insert", runner_action.add_to_team())
213
+ runner.register("update-delete", runner_action.del_from_team())
214
+ runner.register("delete", runner_action.del_from_team())
215
+
216
+ status = runner.run()
217
+ if not status:
218
+ sys.exit(ExitCodes.ERROR)
@@ -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
  )
@@ -1,13 +1,16 @@
1
+ import contextlib
1
2
  from typing import Any
2
3
 
3
4
  import requests
4
5
 
6
+ from reconcile.utils.rest_api_base import ApiBase, BearerTokenAuth
7
+
5
8
 
6
9
  class QuayTeamNotFoundError(Exception):
7
10
  pass
8
11
 
9
12
 
10
- class QuayApi:
13
+ class QuayApi(ApiBase):
11
14
  LIMIT_FOLLOWS = 15
12
15
 
13
16
  def __init__(
@@ -17,14 +20,18 @@ class QuayApi:
17
20
  base_url: str = "quay.io",
18
21
  timeout: int = 60,
19
22
  ) -> None:
20
- self.token = token
23
+ # Support both hostname (e.g., "quay.io") and full URLs (e.g., "http://localhost:12345")
24
+ if base_url.startswith(("http://", "https://")):
25
+ host = base_url
26
+ else:
27
+ host = f"https://{base_url}"
28
+ super().__init__(
29
+ host=host,
30
+ auth=BearerTokenAuth(token),
31
+ read_timeout=timeout,
32
+ )
21
33
  self.organization = organization
22
- self.auth_header = {"Authorization": "Bearer %s" % (token,)}
23
34
  self.team_members: dict[str, Any] = {}
24
- self.api_url = f"https://{base_url}/api/v1"
25
-
26
- self._timeout = timeout
27
- """Timeout to use for HTTP calls to Quay (seconds)."""
28
35
 
29
36
  def list_team_members(self, team: str, **kwargs: Any) -> list[dict]:
30
37
  """
@@ -38,19 +45,20 @@ class QuayApi:
38
45
  if cache_members:
39
46
  return cache_members
40
47
 
41
- url = f"{self.api_url}/organization/{self.organization}/team/{team}/members?includePending=true"
42
-
43
- r = requests.get(url, headers=self.auth_header, timeout=self._timeout)
44
- if r.status_code == 404:
45
- raise QuayTeamNotFoundError(
46
- f"team {team} is not found in "
47
- f"org {self.organization}. "
48
- f"contact org owner to create the "
49
- f"team manually."
50
- )
51
- r.raise_for_status()
52
-
53
- body = r.json()
48
+ url = f"/api/v1/organization/{self.organization}/team/{team}/members"
49
+ params = {"includePending": "true"}
50
+
51
+ try:
52
+ body = self._get(url, params=params)
53
+ except requests.exceptions.HTTPError as e:
54
+ if e.response.status_code == 404:
55
+ raise QuayTeamNotFoundError(
56
+ f"team {team} is not found in "
57
+ f"org {self.organization}. "
58
+ f"contact org owner to create the "
59
+ f"team manually."
60
+ ) from e
61
+ raise
54
62
 
55
63
  # Using a set because members may be repeated
56
64
  members = {member["name"] for member in body["members"]}
@@ -61,30 +69,37 @@ class QuayApi:
61
69
  return members_list
62
70
 
63
71
  def user_exists(self, user: str) -> bool:
64
- url = f"{self.api_url}/users/{user}"
65
- r = requests.get(url, headers=self.auth_header, timeout=self._timeout)
66
- return r.ok
72
+ url = f"/api/v1/users/{user}"
73
+ try:
74
+ self._get(url)
75
+ return True
76
+ except requests.exceptions.HTTPError:
77
+ return False
67
78
 
68
79
  def remove_user_from_team(self, user: str, team: str) -> bool:
69
80
  """Deletes an user from a team.
70
81
 
71
82
  :raises HTTPError if there are any problems with the request
72
83
  """
73
- url_team = f"{self.api_url}/organization/{self.organization}/team/{team}/members/{user}"
84
+ url_team = (
85
+ f"/api/v1/organization/{self.organization}/team/{team}/members/{user}"
86
+ )
74
87
 
75
- r = requests.delete(url_team, headers=self.auth_header, timeout=self._timeout)
76
- if not r.ok:
77
- message = r.json().get("message", "")
88
+ try:
89
+ self._delete(url_team)
90
+ except requests.exceptions.HTTPError as e:
91
+ message = ""
92
+ if e.response is not None:
93
+ with contextlib.suppress(ValueError, AttributeError):
94
+ message = e.response.json().get("message", "")
78
95
 
79
96
  expected_message = f"User {user} does not belong to team {team}"
80
97
 
81
98
  if message != expected_message:
82
- r.raise_for_status()
99
+ raise
83
100
 
84
- url_org = f"{self.api_url}/organization/{self.organization}/members/{user}"
85
-
86
- r = requests.delete(url_org, headers=self.auth_header, timeout=self._timeout)
87
- r.raise_for_status()
101
+ url_org = f"/api/v1/organization/{self.organization}/members/{user}"
102
+ self._delete(url_org)
88
103
 
89
104
  return True
90
105
 
@@ -96,9 +111,8 @@ class QuayApi:
96
111
  if user in self.list_team_members(team, cache=True):
97
112
  return True
98
113
 
99
- url = f"{self.api_url}/organization/{self.organization}/team/{team}/members/{user}"
100
- r = requests.put(url, headers=self.auth_header, timeout=self._timeout)
101
- r.raise_for_status()
114
+ url = f"/api/v1/organization/{self.organization}/team/{team}/members/{user}"
115
+ self._put(url)
102
116
  return True
103
117
 
104
118
  def create_or_update_team(
@@ -115,17 +129,14 @@ class QuayApi:
115
129
  :raises HTTPError: unsuccessful attempt to create the team
116
130
  """
117
131
 
118
- url = f"{self.api_url}/organization/{self.organization}/team/{team}"
132
+ url = f"/api/v1/organization/{self.organization}/team/{team}"
119
133
 
120
134
  payload = {"role": role}
121
135
 
122
136
  if description:
123
137
  payload.update({"description": description})
124
138
 
125
- r = requests.put(
126
- url, headers=self.auth_header, json=payload, timeout=self._timeout
127
- )
128
- r.raise_for_status()
139
+ self._put(url, data=payload)
129
140
 
130
141
  def list_images(
131
142
  self, images: list | None = None, page: str | None = None, count: int = 0
@@ -140,7 +151,7 @@ class QuayApi:
140
151
  if count > self.LIMIT_FOLLOWS:
141
152
  raise ValueError("Too many page follows")
142
153
 
143
- url = f"{self.api_url}/repository"
154
+ url = "/api/v1/repository"
144
155
 
145
156
  # params
146
157
  params = {"namespace": self.organization}
@@ -148,13 +159,7 @@ class QuayApi:
148
159
  params["next_page"] = page
149
160
 
150
161
  # perform request
151
- r = requests.get(
152
- url, params=params, headers=self.auth_header, timeout=self._timeout
153
- )
154
- r.raise_for_status()
155
-
156
- # read body
157
- body = r.json()
162
+ body = self._get(url, params=params)
158
163
  repositories = body.get("repositories", [])
159
164
  next_page = body.get("next_page")
160
165
 
@@ -176,7 +181,7 @@ class QuayApi:
176
181
  """
177
182
  visibility = "public" if public else "private"
178
183
 
179
- url = f"{self.api_url}/repository"
184
+ url = "/repository"
180
185
 
181
186
  params = {
182
187
  "repo_kind": "image",
@@ -186,29 +191,16 @@ class QuayApi:
186
191
  "description": description,
187
192
  }
188
193
 
189
- # perform request
190
- r = requests.post(
191
- url, json=params, headers=self.auth_header, timeout=self._timeout
192
- )
193
- r.raise_for_status()
194
+ self._post(url, data=params)
194
195
 
195
196
  def repo_delete(self, repo_name: str) -> None:
196
- url = f"{self.api_url}/repository/{self.organization}/{repo_name}"
197
-
198
- # perform request
199
- r = requests.delete(url, headers=self.auth_header, timeout=self._timeout)
200
- r.raise_for_status()
197
+ url = f"/api/v1/repository/{self.organization}/{repo_name}"
198
+ self._delete(url)
201
199
 
202
200
  def repo_update_description(self, repo_name: str, description: str) -> None:
203
- url = f"{self.api_url}/repository/{self.organization}/{repo_name}"
204
-
201
+ url = f"/api/v1/repository/{self.organization}/{repo_name}"
205
202
  params = {"description": description}
206
-
207
- # perform request
208
- r = requests.put(
209
- url, json=params, headers=self.auth_header, timeout=self._timeout
210
- )
211
- r.raise_for_status()
203
+ self._put(url, data=params)
212
204
 
213
205
  def repo_make_public(self, repo_name: str) -> None:
214
206
  self._repo_change_visibility(repo_name, "public")
@@ -217,39 +209,34 @@ class QuayApi:
217
209
  self._repo_change_visibility(repo_name, "private")
218
210
 
219
211
  def _repo_change_visibility(self, repo_name: str, visibility: str) -> None:
220
- url = f"{self.api_url}/repository/{self.organization}/{repo_name}/changevisibility"
221
-
212
+ url = f"/api/v1/repository/{self.organization}/{repo_name}/changevisibility"
222
213
  params = {"visibility": visibility}
223
-
224
- # perform request
225
- r = requests.post(
226
- url, json=params, headers=self.auth_header, timeout=self._timeout
227
- )
228
- r.raise_for_status()
214
+ self._post(url, data=params)
229
215
 
230
216
  def get_repo_team_permissions(self, repo_name: str, team: str) -> str | None:
231
217
  url = (
232
- f"{self.api_url}/repository/{self.organization}/"
218
+ f"/api/v1/repository/{self.organization}/"
233
219
  + f"{repo_name}/permissions/team/{team}"
234
220
  )
235
- r = requests.get(url, headers=self.auth_header, timeout=self._timeout)
236
- if not r.ok:
237
- message = r.json().get("message")
221
+ try:
222
+ body = self._get(url)
223
+ return body.get("role") or None
224
+ except requests.exceptions.HTTPError as e:
225
+ message = ""
226
+ if e.response is not None:
227
+ with contextlib.suppress(ValueError, AttributeError):
228
+ message = e.response.json().get("message", "")
229
+
238
230
  expected_message = "Team does not have permission for repo."
239
231
  if message == expected_message:
240
232
  return None
241
233
 
242
- r.raise_for_status()
243
-
244
- return r.json().get("role") or None
234
+ raise
245
235
 
246
236
  def set_repo_team_permissions(self, repo_name: str, team: str, role: str) -> None:
247
237
  url = (
248
- f"{self.api_url}/repository/{self.organization}/"
238
+ f"/api/v1/repository/{self.organization}/"
249
239
  + f"{repo_name}/permissions/team/{team}"
250
240
  )
251
241
  body = {"role": role}
252
- r = requests.put(
253
- url, json=body, headers=self.auth_header, timeout=self._timeout
254
- )
255
- r.raise_for_status()
242
+ self._put(url, data=body)