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.
- {qontract_reconcile-0.10.2.dev465.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.2.dev465.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/RECORD +17 -17
- reconcile/gql_definitions/external_resources/external_resources_namespaces.py +1 -1
- reconcile/gql_definitions/introspection.json +3 -7
- reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +1 -1
- reconcile/quay_base.py +25 -6
- reconcile/quay_membership.py +35 -28
- reconcile/quay_mirror_org.py +6 -4
- reconcile/quay_permissions.py +81 -75
- reconcile/quay_repos.py +35 -37
- reconcile/queries.py +1 -1
- reconcile/templating/validator.py +4 -4
- reconcile/terraform_vpc_resources/merge_request.py +12 -2
- reconcile/terraform_vpc_resources/merge_request_manager.py +43 -19
- reconcile/utils/quay_api.py +74 -87
- {qontract_reconcile-0.10.2.dev465.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev465.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/entry_points.txt +0 -0
{qontract_reconcile-0.10.2.dev465.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qontract-reconcile
|
|
3
|
-
Version: 0.10.2.
|
|
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
|
{qontract_reconcile-0.10.2.dev465.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/RECORD
RENAMED
|
@@ -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=
|
|
86
|
-
reconcile/quay_membership.py,sha256=
|
|
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=
|
|
89
|
-
reconcile/quay_permissions.py,sha256=
|
|
90
|
-
reconcile/quay_repos.py,sha256=
|
|
91
|
-
reconcile/queries.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
522
|
-
reconcile/terraform_vpc_resources/merge_request_manager.py,sha256=
|
|
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=
|
|
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.
|
|
806
|
-
qontract_reconcile-0.10.2.
|
|
807
|
-
qontract_reconcile-0.10.2.
|
|
808
|
-
qontract_reconcile-0.10.2.
|
|
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": "
|
|
16712
|
-
"name":
|
|
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
|
-
|
|
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
|
|
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
|
reconcile/quay_membership.py
CHANGED
|
@@ -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
|
|
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
|
-
{
|
|
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
|
|
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 =
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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)
|
reconcile/quay_mirror_org.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
reconcile/quay_permissions.py
CHANGED
|
@@ -2,7 +2,10 @@ import logging
|
|
|
2
2
|
import sys
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from reconcile.quay_base import
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
@@ -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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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(
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
91
|
-
|
|
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
|
-
|
|
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
|
)
|
reconcile/utils/quay_api.py
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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"
|
|
65
|
-
|
|
66
|
-
|
|
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 =
|
|
84
|
+
url_team = (
|
|
85
|
+
f"/api/v1/organization/{self.organization}/team/{team}/members/{user}"
|
|
86
|
+
)
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
99
|
+
raise
|
|
83
100
|
|
|
84
|
-
url_org = f"
|
|
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"
|
|
100
|
-
|
|
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"
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
218
|
+
f"/api/v1/repository/{self.organization}/"
|
|
233
219
|
+ f"{repo_name}/permissions/team/{team}"
|
|
234
220
|
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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"
|
|
238
|
+
f"/api/v1/repository/{self.organization}/"
|
|
249
239
|
+ f"{repo_name}/permissions/team/{team}"
|
|
250
240
|
)
|
|
251
241
|
body = {"role": role}
|
|
252
|
-
|
|
253
|
-
url, json=body, headers=self.auth_header, timeout=self._timeout
|
|
254
|
-
)
|
|
255
|
-
r.raise_for_status()
|
|
242
|
+
self._put(url, data=body)
|
{qontract_reconcile-0.10.2.dev465.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|