qontract-reconcile 0.10.2.dev256__py3-none-any.whl → 0.10.2.dev258__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.
- {qontract_reconcile-0.10.2.dev256.dist-info → qontract_reconcile-0.10.2.dev258.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.2.dev256.dist-info → qontract_reconcile-0.10.2.dev258.dist-info}/RECORD +96 -95
- reconcile/aus/advanced_upgrade_service.py +1 -1
- reconcile/aus/base.py +2 -2
- reconcile/aus/version_gates/sts_version_gate_handler.py +2 -2
- reconcile/aws_account_manager/reconciler.py +22 -20
- reconcile/aws_iam_keys.py +5 -5
- reconcile/aws_iam_password_reset.py +5 -5
- reconcile/aws_saml_roles/integration.py +5 -5
- reconcile/aws_version_sync/integration.py +4 -3
- reconcile/cli.py +16 -12
- reconcile/closedbox_endpoint_monitoring_base.py +1 -0
- reconcile/database_access_manager.py +4 -4
- reconcile/dynatrace_token_provider/integration.py +2 -2
- reconcile/external_resources/manager.py +2 -2
- reconcile/external_resources/model.py +1 -1
- reconcile/external_resources/secrets_sync.py +2 -2
- reconcile/gabi_authorized_users.py +3 -3
- reconcile/github_org.py +2 -2
- reconcile/gitlab_housekeeping.py +1 -1
- reconcile/gitlab_mr_sqs_consumer.py +1 -1
- reconcile/glitchtip/integration.py +2 -2
- reconcile/jenkins_worker_fleets.py +5 -5
- reconcile/ldap_groups/integration.py +3 -3
- reconcile/ocm_clusters.py +2 -2
- reconcile/ocm_internal_notifications/integration.py +2 -2
- reconcile/ocm_labels/integration.py +3 -2
- reconcile/openshift_base.py +12 -11
- reconcile/openshift_cluster_bots.py +2 -2
- reconcile/openshift_resources_base.py +3 -3
- reconcile/openshift_rhcs_certs.py +2 -2
- reconcile/openshift_saas_deploy.py +1 -1
- reconcile/quay_membership.py +4 -4
- reconcile/rhidp/common.py +3 -2
- reconcile/run_integration.py +7 -4
- reconcile/saas_auto_promotions_manager/dependencies.py +95 -0
- reconcile/saas_auto_promotions_manager/integration.py +85 -165
- reconcile/skupper_network/integration.py +3 -3
- reconcile/slack_usergroups.py +4 -4
- reconcile/status_board.py +3 -3
- reconcile/terraform_cloudflare_dns.py +5 -5
- reconcile/terraform_cloudflare_users.py +15 -17
- reconcile/terraform_resources.py +6 -6
- reconcile/terraform_vpc_peerings.py +9 -9
- reconcile/unleash_feature_toggles/integration.py +1 -1
- reconcile/utils/aggregated_list.py +2 -2
- reconcile/utils/aws_api_typed/iam.py +2 -2
- reconcile/utils/aws_api_typed/organization.py +4 -4
- reconcile/utils/aws_api_typed/service_quotas.py +4 -4
- reconcile/utils/aws_api_typed/support.py +9 -9
- reconcile/utils/aws_helper.py +1 -1
- reconcile/utils/config.py +8 -4
- reconcile/utils/deadmanssnitch_api.py +2 -4
- reconcile/utils/glitchtip/models.py +18 -12
- reconcile/utils/gql.py +4 -4
- reconcile/utils/internal_groups/client.py +2 -2
- reconcile/utils/jinja2/utils.py +7 -3
- reconcile/utils/jjb_client.py +2 -2
- reconcile/utils/models.py +2 -1
- reconcile/utils/mr/__init__.py +3 -3
- reconcile/utils/mr/app_interface_reporter.py +2 -2
- reconcile/utils/mr/aws_access.py +5 -2
- reconcile/utils/mr/base.py +3 -3
- reconcile/utils/mr/user_maintenance.py +1 -1
- reconcile/utils/oc.py +11 -11
- reconcile/utils/oc_connection_parameters.py +4 -4
- reconcile/utils/ocm/base.py +3 -3
- reconcile/utils/ocm/products.py +8 -8
- reconcile/utils/ocm/search_filters.py +2 -2
- reconcile/utils/openshift_resource.py +21 -18
- reconcile/utils/pagerduty_api.py +5 -5
- reconcile/utils/quay_api.py +2 -2
- reconcile/utils/rosa/rosa_cli.py +1 -1
- reconcile/utils/rosa/session.py +2 -2
- reconcile/utils/runtime/desired_state_diff.py +7 -7
- reconcile/utils/saasherder/interfaces.py +1 -0
- reconcile/utils/saasherder/models.py +1 -1
- reconcile/utils/saasherder/saasherder.py +1 -1
- reconcile/utils/secret_reader.py +20 -20
- reconcile/utils/slack_api.py +5 -5
- reconcile/utils/slo_document_manager.py +6 -6
- reconcile/utils/state.py +8 -8
- reconcile/utils/terraform_client.py +3 -3
- reconcile/utils/terrascript/cloudflare_client.py +2 -2
- reconcile/utils/terrascript/cloudflare_resources.py +1 -0
- reconcile/utils/terrascript_aws_client.py +12 -11
- reconcile/utils/vault.py +22 -22
- reconcile/vault_replication.py +15 -15
- tools/cli_commands/erv2.py +3 -2
- tools/cli_commands/gpg_encrypt.py +9 -9
- tools/cli_commands/systems_and_tools.py +1 -1
- tools/qontract_cli.py +13 -14
- tools/saas_promotion_state/saas_promotion_state.py +4 -4
- tools/template_validation.py +5 -5
- {qontract_reconcile-0.10.2.dev256.dist-info → qontract_reconcile-0.10.2.dev258.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev256.dist-info → qontract_reconcile-0.10.2.dev258.dist-info}/entry_points.txt +0 -0
@@ -44,8 +44,8 @@ from reconcile.utils.terraform_client import TerraformClient
|
|
44
44
|
from reconcile.utils.terrascript.cloudflare_client import (
|
45
45
|
DEFAULT_PROVIDER_RPS,
|
46
46
|
DNSZoneShardingStrategy,
|
47
|
-
|
48
|
-
|
47
|
+
IntegrationUndefinedError,
|
48
|
+
InvalidTerraformStateError,
|
49
49
|
TerrascriptCloudflareClientFactory,
|
50
50
|
)
|
51
51
|
from reconcile.utils.terrascript.models import (
|
@@ -312,9 +312,9 @@ def build_cloudflare_terraform_config_collection(
|
|
312
312
|
integrations = tf_state.integrations
|
313
313
|
|
314
314
|
if not bucket:
|
315
|
-
raise
|
315
|
+
raise InvalidTerraformStateError("Terraform state must have bucket defined")
|
316
316
|
if not region:
|
317
|
-
raise
|
317
|
+
raise InvalidTerraformStateError("Terraform state must have region defined")
|
318
318
|
|
319
319
|
integration = None
|
320
320
|
for i in integrations:
|
@@ -323,7 +323,7 @@ def build_cloudflare_terraform_config_collection(
|
|
323
323
|
break
|
324
324
|
|
325
325
|
if not integration:
|
326
|
-
raise
|
326
|
+
raise IntegrationUndefinedError(
|
327
327
|
f"Must declare integration name under Terraform state in {zone.account.terraform_state_account.name} AWS account for {cf_account.name} Cloudflare account in app-interface"
|
328
328
|
)
|
329
329
|
|
@@ -35,15 +35,15 @@ from reconcile.utils.terraform.config_client import (
|
|
35
35
|
TerraformConfigClientCollection,
|
36
36
|
)
|
37
37
|
from reconcile.utils.terraform_client import (
|
38
|
-
|
38
|
+
TerraformApplyFailedError,
|
39
39
|
TerraformClient,
|
40
|
-
|
41
|
-
|
40
|
+
TerraformDeletionDetectedError,
|
41
|
+
TerraformPlanFailedError,
|
42
42
|
)
|
43
43
|
from reconcile.utils.terrascript.cloudflare_client import (
|
44
44
|
AccountShardingStrategy,
|
45
|
-
|
46
|
-
|
45
|
+
IntegrationUndefinedError,
|
46
|
+
InvalidTerraformStateError,
|
47
47
|
TerrascriptCloudflareClientFactory,
|
48
48
|
)
|
49
49
|
from reconcile.utils.terrascript.models import (
|
@@ -153,9 +153,6 @@ class TerraformCloudflareUsers(
|
|
153
153
|
]
|
154
154
|
|
155
155
|
self._run_terraform(
|
156
|
-
QONTRACT_INTEGRATION,
|
157
|
-
QONTRACT_INTEGRATION_VERSION,
|
158
|
-
QONTRACT_TF_PREFIX,
|
159
156
|
dry_run,
|
160
157
|
enable_deletion,
|
161
158
|
thread_pool_size,
|
@@ -165,9 +162,6 @@ class TerraformCloudflareUsers(
|
|
165
162
|
|
166
163
|
def _run_terraform(
|
167
164
|
self,
|
168
|
-
QONTRACT_INTEGRATION: str,
|
169
|
-
QONTRACT_INTEGRATION_VERSION: str,
|
170
|
-
QONTRACT_TF_PREFIX: str,
|
171
165
|
dry_run: bool,
|
172
166
|
enable_deletion: bool,
|
173
167
|
thread_pool_size: int,
|
@@ -186,11 +180,11 @@ class TerraformCloudflareUsers(
|
|
186
180
|
try:
|
187
181
|
disabled_deletions_detected, err = tf.plan(enable_deletion)
|
188
182
|
if err:
|
189
|
-
raise
|
183
|
+
raise TerraformPlanFailedError(
|
190
184
|
f"Failed to run terraform plan for integration {QONTRACT_INTEGRATION}"
|
191
185
|
)
|
192
186
|
if disabled_deletions_detected:
|
193
|
-
raise
|
187
|
+
raise TerraformDeletionDetectedError(
|
194
188
|
"Deletions detected but they are disabled"
|
195
189
|
)
|
196
190
|
|
@@ -199,7 +193,7 @@ class TerraformCloudflareUsers(
|
|
199
193
|
|
200
194
|
err = tf.apply()
|
201
195
|
if err:
|
202
|
-
raise
|
196
|
+
raise TerraformApplyFailedError(
|
203
197
|
f"Failed to run terraform apply for integration {QONTRACT_INTEGRATION}"
|
204
198
|
)
|
205
199
|
finally:
|
@@ -235,9 +229,13 @@ class TerraformCloudflareUsers(
|
|
235
229
|
integrations = tf_state.integrations
|
236
230
|
|
237
231
|
if not bucket:
|
238
|
-
raise
|
232
|
+
raise InvalidTerraformStateError(
|
233
|
+
"Terraform state must have bucket defined"
|
234
|
+
)
|
239
235
|
if not region:
|
240
|
-
raise
|
236
|
+
raise InvalidTerraformStateError(
|
237
|
+
"Terraform state must have region defined"
|
238
|
+
)
|
241
239
|
|
242
240
|
integration = None
|
243
241
|
for i in integrations:
|
@@ -246,7 +244,7 @@ class TerraformCloudflareUsers(
|
|
246
244
|
break
|
247
245
|
|
248
246
|
if not integration:
|
249
|
-
raise
|
247
|
+
raise IntegrationUndefinedError(
|
250
248
|
"Must declare integration name under Terraform state in app-interface"
|
251
249
|
)
|
252
250
|
|
reconcile/terraform_resources.py
CHANGED
@@ -200,20 +200,20 @@ def get_aws_accounts(
|
|
200
200
|
if exclude_accounts and not dry_run:
|
201
201
|
message = "--exclude-accounts is only supported in dry-run mode"
|
202
202
|
logging.error(message)
|
203
|
-
raise
|
203
|
+
raise ExcludeAccountsAndDryRunError(message)
|
204
204
|
|
205
205
|
if (exclude_accounts and include_accounts) and any(
|
206
206
|
a in exclude_accounts for a in include_accounts
|
207
207
|
):
|
208
208
|
message = "Using --exclude-accounts and --account-name with the same account is not allowed"
|
209
209
|
logging.error(message)
|
210
|
-
raise
|
210
|
+
raise ExcludeAccountsAndAccountNameError(message)
|
211
211
|
|
212
212
|
# If we are not running in dry run we don't want to run with more than one account
|
213
213
|
if include_accounts and len(include_accounts) > 1 and not dry_run:
|
214
214
|
message = "Running with multiple accounts is only supported in dry-run mode"
|
215
215
|
logging.error(message)
|
216
|
-
raise
|
216
|
+
raise MultipleAccountNamesInDryRunError(message)
|
217
217
|
|
218
218
|
accounts = queries.get_aws_accounts(terraform_state=True)
|
219
219
|
|
@@ -345,15 +345,15 @@ def populate_desired_state(
|
|
345
345
|
)
|
346
346
|
|
347
347
|
|
348
|
-
class
|
348
|
+
class ExcludeAccountsAndDryRunError(Exception):
|
349
349
|
pass
|
350
350
|
|
351
351
|
|
352
|
-
class
|
352
|
+
class ExcludeAccountsAndAccountNameError(Exception):
|
353
353
|
pass
|
354
354
|
|
355
355
|
|
356
|
-
class
|
356
|
+
class MultipleAccountNamesInDryRunError(Exception):
|
357
357
|
pass
|
358
358
|
|
359
359
|
|
@@ -28,7 +28,7 @@ QONTRACT_INTEGRATION = "terraform_vpc_peerings"
|
|
28
28
|
QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
|
29
29
|
|
30
30
|
|
31
|
-
class
|
31
|
+
class BadTerraformPeeringStateError(Exception):
|
32
32
|
pass
|
33
33
|
|
34
34
|
|
@@ -122,7 +122,7 @@ def aws_assume_roles_for_cluster_vpc_peering(
|
|
122
122
|
# accepters peering connection
|
123
123
|
infra_account = accepter_connection["awsInfrastructureManagementAccount"]
|
124
124
|
if infra_account and infra_account["name"] not in allowed_accounts:
|
125
|
-
raise
|
125
|
+
raise BadTerraformPeeringStateError(
|
126
126
|
"[account_not_allowed] "
|
127
127
|
f"account {infra_account['name']} used on the peering accepter of "
|
128
128
|
f"cluster {accepter_cluster['name']} is not listed as a "
|
@@ -135,7 +135,7 @@ def aws_assume_roles_for_cluster_vpc_peering(
|
|
135
135
|
infra_account = _get_default_management_account(accepter_cluster)
|
136
136
|
|
137
137
|
if not infra_account:
|
138
|
-
raise
|
138
|
+
raise BadTerraformPeeringStateError(
|
139
139
|
f"[no_account_available] unable to find infra account "
|
140
140
|
f"for {accepter_cluster['name']} to manage the VPC peering "
|
141
141
|
f"with {requester_cluster['name']}"
|
@@ -147,7 +147,7 @@ def aws_assume_roles_for_cluster_vpc_peering(
|
|
147
147
|
infra_account, requester_cluster, ocm, requester_connection.get("assumeRole")
|
148
148
|
)
|
149
149
|
if req_aws is None:
|
150
|
-
raise
|
150
|
+
raise BadTerraformPeeringStateError(
|
151
151
|
f"[assume_role_not_found] unable to find assume role "
|
152
152
|
f"on cluster-vpc-requester for account {infra_account['name']} and "
|
153
153
|
f"cluster {requester_cluster['name']} "
|
@@ -156,7 +156,7 @@ def aws_assume_roles_for_cluster_vpc_peering(
|
|
156
156
|
infra_account, accepter_cluster, ocm, accepter_connection.get("assumeRole")
|
157
157
|
)
|
158
158
|
if acc_aws is None:
|
159
|
-
raise
|
159
|
+
raise BadTerraformPeeringStateError(
|
160
160
|
f"[assume_role_not_found] unable to find assume role "
|
161
161
|
f"on cluster-vpc-accepter for account {infra_account['name']} and "
|
162
162
|
f"cluster {accepter_cluster['name']} "
|
@@ -192,7 +192,7 @@ def build_desired_state_single_cluster(
|
|
192
192
|
cluster_info, peer_cluster, "cluster-vpc-accepter"
|
193
193
|
)
|
194
194
|
if not peer_info:
|
195
|
-
raise
|
195
|
+
raise BadTerraformPeeringStateError(
|
196
196
|
"[no_matching_peering] could not find a matching peering "
|
197
197
|
f"connection for cluster {cluster_name}, connection "
|
198
198
|
f"{peer_connection_name}"
|
@@ -297,7 +297,7 @@ def build_desired_state_all_clusters(
|
|
297
297
|
cluster_info, ocm, awsapi, account_filter
|
298
298
|
)
|
299
299
|
desired_state.extend(items)
|
300
|
-
except (KeyError,
|
300
|
+
except (KeyError, BadTerraformPeeringStateError, aws_api.MissingARNError):
|
301
301
|
logging.exception(f"Failed to get desired state for {cluster}")
|
302
302
|
error = True
|
303
303
|
|
@@ -421,7 +421,7 @@ def build_desired_state_vpc_mesh(
|
|
421
421
|
cluster_info, ocm, awsapi, account_filter
|
422
422
|
)
|
423
423
|
desired_state.extend(items)
|
424
|
-
except (KeyError,
|
424
|
+
except (KeyError, BadTerraformPeeringStateError, aws_api.MissingARNError):
|
425
425
|
logging.exception(f"Unable to create VPC mesh for cluster {cluster}")
|
426
426
|
error = True
|
427
427
|
|
@@ -554,7 +554,7 @@ def build_desired_state_vpc(
|
|
554
554
|
cluster_info, ocm, awsapi, account_filter
|
555
555
|
)
|
556
556
|
desired_state.extend(items)
|
557
|
-
except (KeyError,
|
557
|
+
except (KeyError, BadTerraformPeeringStateError, aws_api.MissingARNError):
|
558
558
|
logging.exception(f"Unable to process {cluster_info['name']}")
|
559
559
|
error = True
|
560
560
|
|
@@ -7,7 +7,7 @@ Action = Callable[[Any, list[Any]], bool]
|
|
7
7
|
Cond = Callable[[Any], bool]
|
8
8
|
|
9
9
|
|
10
|
-
class
|
10
|
+
class RunnerError(Exception):
|
11
11
|
pass
|
12
12
|
|
13
13
|
|
@@ -88,7 +88,7 @@ class AggregatedList:
|
|
88
88
|
def dump(self) -> list[AggregatedItem]:
|
89
89
|
return list(self._dict.values())
|
90
90
|
|
91
|
-
def
|
91
|
+
def to_json(self) -> str:
|
92
92
|
return json.dumps(self.dump(), indent=4)
|
93
93
|
|
94
94
|
@staticmethod
|
@@ -20,7 +20,7 @@ class AWSUser(BaseModel):
|
|
20
20
|
path: str = Field(..., alias="Path")
|
21
21
|
|
22
22
|
|
23
|
-
class
|
23
|
+
class AWSEntityAlreadyExistsError(Exception):
|
24
24
|
"""Raised when the user already exists in IAM."""
|
25
25
|
|
26
26
|
|
@@ -41,7 +41,7 @@ class AWSApiIam:
|
|
41
41
|
user = self.client.create_user(UserName=user_name)
|
42
42
|
return AWSUser(**user["User"])
|
43
43
|
except self.client.exceptions.EntityAlreadyExistsException:
|
44
|
-
raise
|
44
|
+
raise AWSEntityAlreadyExistsError(
|
45
45
|
f"User {user_name} already exists"
|
46
46
|
) from None
|
47
47
|
|
@@ -61,11 +61,11 @@ class AWSAccount(BaseModel):
|
|
61
61
|
state: str = Field(..., alias="Status")
|
62
62
|
|
63
63
|
|
64
|
-
class
|
64
|
+
class AWSAccountCreationError(Exception):
|
65
65
|
"""Exception raised when account creation failed."""
|
66
66
|
|
67
67
|
|
68
|
-
class
|
68
|
+
class AWSAccountNotFoundError(Exception):
|
69
69
|
"""Exception raised when the account cannot be found in the specified OU."""
|
70
70
|
|
71
71
|
|
@@ -102,7 +102,7 @@ class AWSApiOrganizations:
|
|
102
102
|
)
|
103
103
|
status = AWSAccountStatus(**resp["CreateAccountStatus"])
|
104
104
|
if status.state == "FAILED":
|
105
|
-
raise
|
105
|
+
raise AWSAccountCreationError(
|
106
106
|
f"Account creation failed: {status.failure_reason}"
|
107
107
|
)
|
108
108
|
return status
|
@@ -122,7 +122,7 @@ class AWSApiOrganizations:
|
|
122
122
|
for p in resp.get("Parents", []):
|
123
123
|
if p["Type"] in {"ORGANIZATIONAL_UNIT", "ROOT"}:
|
124
124
|
return p["Id"]
|
125
|
-
raise
|
125
|
+
raise AWSAccountNotFoundError(f"Account {uid} not found!")
|
126
126
|
|
127
127
|
def move_account(self, uid: str, destination_parent_id: str) -> None:
|
128
128
|
"""Move an account to a different organizational unit."""
|
@@ -31,11 +31,11 @@ class AWSQuota(BaseModel):
|
|
31
31
|
return str(self)
|
32
32
|
|
33
33
|
|
34
|
-
class
|
34
|
+
class AWSNoSuchResourceError(Exception):
|
35
35
|
"""Raised when a resource is not found in a service quotas API call."""
|
36
36
|
|
37
37
|
|
38
|
-
class
|
38
|
+
class AWSResourceAlreadyExistsError(Exception):
|
39
39
|
"""Raised when quota increase request already exists."""
|
40
40
|
|
41
41
|
|
@@ -62,7 +62,7 @@ class AWSApiServiceQuotas:
|
|
62
62
|
)
|
63
63
|
return AWSRequestedServiceQuotaChange(**req["RequestedQuota"])
|
64
64
|
except self.client.exceptions.ResourceAlreadyExistsException:
|
65
|
-
raise
|
65
|
+
raise AWSResourceAlreadyExistsError(
|
66
66
|
f"Service quota increase request {service_code=}, {quota_code=} already exists."
|
67
67
|
) from None
|
68
68
|
|
@@ -74,6 +74,6 @@ class AWSApiServiceQuotas:
|
|
74
74
|
)
|
75
75
|
return AWSQuota(**quota["Quota"])
|
76
76
|
except self.client.exceptions.NoSuchResourceException:
|
77
|
-
raise
|
77
|
+
raise AWSNoSuchResourceError(
|
78
78
|
f"Service quota {service_code=}, {quota_code=} not found."
|
79
79
|
) from None
|
@@ -15,7 +15,7 @@ class AWSCase(BaseModel):
|
|
15
15
|
status: str
|
16
16
|
|
17
17
|
|
18
|
-
class
|
18
|
+
class SupportPlan(Enum):
|
19
19
|
BASIC = "basic"
|
20
20
|
DEVELOPER = "developer"
|
21
21
|
BUSINESS = "business"
|
@@ -53,27 +53,27 @@ class AWSApiSupport:
|
|
53
53
|
case = self.client.describe_cases(caseIdList=[case_id])["cases"][0]
|
54
54
|
return AWSCase(**case)
|
55
55
|
|
56
|
-
def get_support_level(self) ->
|
56
|
+
def get_support_level(self) -> SupportPlan:
|
57
57
|
"""Return the support level of the account."""
|
58
58
|
|
59
59
|
try:
|
60
60
|
response = self.client.describe_severity_levels(language="en")
|
61
61
|
except self.client.exceptions.ClientError as err:
|
62
62
|
if err.response["Error"]["Code"] == "SubscriptionRequiredException":
|
63
|
-
return
|
63
|
+
return SupportPlan.BASIC
|
64
64
|
raise err
|
65
65
|
|
66
66
|
severity_levels = {
|
67
67
|
level["code"].lower() for level in response["severityLevels"]
|
68
68
|
}
|
69
69
|
if "critical" in severity_levels:
|
70
|
-
return
|
70
|
+
return SupportPlan.ENTERPRISE
|
71
71
|
if "urgent" in severity_levels:
|
72
|
-
return
|
72
|
+
return SupportPlan.BUSINESS
|
73
73
|
if "high" in severity_levels:
|
74
|
-
return
|
74
|
+
return SupportPlan.BUSINESS
|
75
75
|
if "normal" in severity_levels:
|
76
|
-
return
|
76
|
+
return SupportPlan.DEVELOPER
|
77
77
|
if "low" in severity_levels:
|
78
|
-
return
|
79
|
-
return
|
78
|
+
return SupportPlan.DEVELOPER
|
79
|
+
return SupportPlan.BASIC
|
reconcile/utils/aws_helper.py
CHANGED
@@ -27,7 +27,7 @@ def get_account_uid_from_arn(arn: str) -> str:
|
|
27
27
|
|
28
28
|
def get_role_name_from_arn(arn: str) -> str:
|
29
29
|
# arn:aws:iam::12345:role/role-1 --> role-1
|
30
|
-
return arn.split("/")[-1]
|
30
|
+
return arn.split("/")[-1] # noqa: PLC0207
|
31
31
|
|
32
32
|
|
33
33
|
def is_aws_managed_resource(arn: str) -> bool:
|
reconcile/utils/config.py
CHANGED
@@ -6,11 +6,11 @@ import toml
|
|
6
6
|
_config: dict = {}
|
7
7
|
|
8
8
|
|
9
|
-
class
|
9
|
+
class ConfigNotFoundError(Exception):
|
10
10
|
pass
|
11
11
|
|
12
12
|
|
13
|
-
class
|
13
|
+
class SecretNotFoundError(Exception):
|
14
14
|
pass
|
15
15
|
|
16
16
|
|
@@ -38,7 +38,9 @@ def read(secret: Mapping[str, Any]) -> str:
|
|
38
38
|
config = config[t]
|
39
39
|
return config[field]
|
40
40
|
except Exception as e:
|
41
|
-
raise
|
41
|
+
raise SecretNotFoundError(
|
42
|
+
f"key not found in config file {path}: {e!s}"
|
43
|
+
) from None
|
42
44
|
|
43
45
|
|
44
46
|
def read_all(secret: Mapping[str, Any]) -> dict:
|
@@ -50,4 +52,6 @@ def read_all(secret: Mapping[str, Any]) -> dict:
|
|
50
52
|
config = config[t]
|
51
53
|
return config
|
52
54
|
except Exception as e:
|
53
|
-
raise
|
55
|
+
raise SecretNotFoundError(
|
56
|
+
f"secret {path} not found in config file: {e!s}"
|
57
|
+
) from None
|
@@ -11,7 +11,7 @@ BASE_URL = "https://api.deadmanssnitch.com/v1/snitches"
|
|
11
11
|
REQUEST_TIMEOUT = 60
|
12
12
|
|
13
13
|
|
14
|
-
class
|
14
|
+
class DeadManssnitchError(Exception):
|
15
15
|
pass
|
16
16
|
|
17
17
|
|
@@ -61,9 +61,7 @@ class DeadMansSnitchApi:
|
|
61
61
|
|
62
62
|
def create_snitch(self, payload: dict) -> Snitch:
|
63
63
|
if payload.get("name") is None or payload.get("interval") is None:
|
64
|
-
raise
|
65
|
-
"Invalid payload,name and interval are mandatory"
|
66
|
-
)
|
64
|
+
raise DeadManssnitchError("Invalid payload,name and interval are mandatory")
|
67
65
|
headers = {"Content-Type": "application/json"}
|
68
66
|
logging.debug("Creating new snitch with name:: %s ", payload["name"])
|
69
67
|
response = self.session.post(
|
@@ -48,8 +48,9 @@ class Team(BaseModel):
|
|
48
48
|
users: list[User] = []
|
49
49
|
|
50
50
|
@root_validator(pre=True)
|
51
|
-
def name_xor_slug_must_be_set(
|
52
|
-
cls,
|
51
|
+
def name_xor_slug_must_be_set(
|
52
|
+
cls, # noqa: N805
|
53
|
+
values: MutableMapping[str, Any],
|
53
54
|
) -> MutableMapping[str, Any]:
|
54
55
|
assert ("name" in values or "slug" in values) and not (
|
55
56
|
"name" in values and "slug" in values
|
@@ -57,8 +58,9 @@ class Team(BaseModel):
|
|
57
58
|
return values
|
58
59
|
|
59
60
|
@root_validator
|
60
|
-
def slugify(
|
61
|
-
cls,
|
61
|
+
def slugify(
|
62
|
+
cls, # noqa: N805
|
63
|
+
values: MutableMapping[str, Any],
|
62
64
|
) -> MutableMapping[str, Any]:
|
63
65
|
values["slug"] = values.get("slug") or slugify(values.get("name", ""))
|
64
66
|
values["name"] = slugify(values.get("name", "")) or values.get("slug")
|
@@ -96,8 +98,9 @@ class ProjectAlertRecipient(BaseModel):
|
|
96
98
|
use_enum_values = True
|
97
99
|
|
98
100
|
@validator("recipient_type")
|
99
|
-
def recipient_type_enforce_enum_type(
|
100
|
-
cls,
|
101
|
+
def recipient_type_enforce_enum_type(
|
102
|
+
cls, # noqa: N805
|
103
|
+
v: str | RecipientType,
|
101
104
|
) -> RecipientType:
|
102
105
|
if isinstance(v, RecipientType):
|
103
106
|
return v
|
@@ -126,8 +129,9 @@ class ProjectAlert(BaseModel):
|
|
126
129
|
allow_population_by_field_name = True
|
127
130
|
|
128
131
|
@root_validator
|
129
|
-
def empty_name(
|
130
|
-
cls,
|
132
|
+
def empty_name(
|
133
|
+
cls, # noqa: N805
|
134
|
+
values: MutableMapping[str, Any],
|
131
135
|
) -> MutableMapping[str, Any]:
|
132
136
|
# name is an empty string if the alert was created manually because it can't be set via UI
|
133
137
|
# use the pk instead.
|
@@ -159,8 +163,9 @@ class Project(BaseModel):
|
|
159
163
|
allow_population_by_field_name = True
|
160
164
|
|
161
165
|
@root_validator
|
162
|
-
def slugify(
|
163
|
-
cls,
|
166
|
+
def slugify(
|
167
|
+
cls, # noqa: N805
|
168
|
+
values: MutableMapping[str, Any],
|
164
169
|
) -> MutableMapping[str, Any]:
|
165
170
|
values["slug"] = values.get("slug") or slugify(values["name"])
|
166
171
|
return values
|
@@ -202,8 +207,9 @@ class Organization(BaseModel):
|
|
202
207
|
users: list[User] = []
|
203
208
|
|
204
209
|
@root_validator
|
205
|
-
def slugify(
|
206
|
-
cls,
|
210
|
+
def slugify(
|
211
|
+
cls, # noqa: N805
|
212
|
+
values: MutableMapping[str, Any],
|
207
213
|
) -> MutableMapping[str, Any]:
|
208
214
|
values["slug"] = values.get("slug") or slugify(values["name"])
|
209
215
|
return values
|
reconcile/utils/gql.py
CHANGED
@@ -53,7 +53,7 @@ class GqlApiError(Exception):
|
|
53
53
|
pass
|
54
54
|
|
55
55
|
|
56
|
-
class
|
56
|
+
class GqlApiIntegrationNotFoundError(Exception):
|
57
57
|
def __init__(self, integration: str):
|
58
58
|
msg = f"""
|
59
59
|
Integration not found: {integration}
|
@@ -64,7 +64,7 @@ class GqlApiIntegrationNotFound(Exception):
|
|
64
64
|
super().__init__(textwrap.dedent(msg).strip())
|
65
65
|
|
66
66
|
|
67
|
-
class
|
67
|
+
class GqlApiErrorForbiddenSchemaError(Exception):
|
68
68
|
def __init__(self, schemas: list):
|
69
69
|
msg = f"""
|
70
70
|
Forbidden schemas: {schemas}
|
@@ -115,7 +115,7 @@ class GqlApi:
|
|
115
115
|
break
|
116
116
|
|
117
117
|
if validate_schemas and not self._valid_schemas:
|
118
|
-
raise
|
118
|
+
raise GqlApiIntegrationNotFoundError(int_name)
|
119
119
|
|
120
120
|
def _init_gql_client(self) -> Client:
|
121
121
|
req_headers = None
|
@@ -170,7 +170,7 @@ class GqlApi:
|
|
170
170
|
schema for schema in query_schemas if schema not in self._valid_schemas
|
171
171
|
]
|
172
172
|
if forbidden_schemas:
|
173
|
-
raise
|
173
|
+
raise GqlApiErrorForbiddenSchemaError(forbidden_schemas)
|
174
174
|
|
175
175
|
# This is to appease mypy. This exception won't be thrown as this condition
|
176
176
|
# is already handled above with AssertionError
|
@@ -17,7 +17,7 @@ from reconcile.utils.internal_groups.models import Group
|
|
17
17
|
REQUEST_TIMEOUT = 30
|
18
18
|
|
19
19
|
|
20
|
-
class
|
20
|
+
class NotFoundError(Exception):
|
21
21
|
"""Not found exception."""
|
22
22
|
|
23
23
|
|
@@ -48,7 +48,7 @@ class InternalGroupsApi:
|
|
48
48
|
resp.raise_for_status()
|
49
49
|
except requests.exceptions.HTTPError as e:
|
50
50
|
if e.response is not None and e.response.status_code == 404:
|
51
|
-
raise
|
51
|
+
raise NotFoundError(e.response.text) from e
|
52
52
|
raise
|
53
53
|
|
54
54
|
def __enter__(self) -> Self:
|
reconcile/utils/jinja2/utils.py
CHANGED
@@ -29,8 +29,12 @@ from reconcile.utils.jinja2.filters import (
|
|
29
29
|
urlunescape,
|
30
30
|
yaml_to_dict,
|
31
31
|
)
|
32
|
-
from reconcile.utils.secret_reader import
|
33
|
-
|
32
|
+
from reconcile.utils.secret_reader import (
|
33
|
+
SecretNotFoundError,
|
34
|
+
SecretReader,
|
35
|
+
SecretReaderBase,
|
36
|
+
)
|
37
|
+
from reconcile.utils.vault import SecretFieldNotFoundError
|
34
38
|
|
35
39
|
|
36
40
|
class Jinja2TemplateError(Exception):
|
@@ -209,7 +213,7 @@ def lookup_secret(
|
|
209
213
|
if not secret_reader:
|
210
214
|
secret_reader = SecretReader(settings)
|
211
215
|
return secret_reader.read(secret)
|
212
|
-
except (
|
216
|
+
except (SecretNotFoundError, SecretFieldNotFoundError) as e:
|
213
217
|
if allow_not_found:
|
214
218
|
return None
|
215
219
|
raise FetchSecretError(e) from None
|
reconcile/utils/jjb_client.py
CHANGED
@@ -7,7 +7,7 @@ import re
|
|
7
7
|
import shutil
|
8
8
|
import subprocess
|
9
9
|
import tempfile
|
10
|
-
import xml.etree.ElementTree as
|
10
|
+
import xml.etree.ElementTree as ET
|
11
11
|
from os import path
|
12
12
|
from subprocess import (
|
13
13
|
PIPE,
|
@@ -192,7 +192,7 @@ class JJB: # pylint: disable=too-many-public-methods
|
|
192
192
|
name = "/".join(items)
|
193
193
|
raise ValueError(f"Invalid job name contains '/' in {instance}: {name}")
|
194
194
|
item = items[0]
|
195
|
-
item_type =
|
195
|
+
item_type = ET.parse(f).getroot().tag
|
196
196
|
item_type = item_type.replace("hudson.model.ListView", "view")
|
197
197
|
item_type = item_type.replace("project", "job")
|
198
198
|
logging.info([action, item_type, instance, item])
|
reconcile/utils/models.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
from collections import UserList
|
1
2
|
from collections.abc import (
|
2
3
|
Callable,
|
3
4
|
Generator,
|
@@ -81,7 +82,7 @@ def data_default_none(
|
|
81
82
|
return data
|
82
83
|
|
83
84
|
|
84
|
-
class CSV(
|
85
|
+
class CSV(UserList[str]):
|
85
86
|
"""
|
86
87
|
A pydantic custom type that converts a CSV into a list of strings. It
|
87
88
|
also supports basic validation of length constraints.
|