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
reconcile/utils/secret_reader.py
CHANGED
@@ -18,11 +18,11 @@ from reconcile.utils import (
|
|
18
18
|
from reconcile.utils.vault import VaultClient
|
19
19
|
|
20
20
|
|
21
|
-
class
|
21
|
+
class VaultForbiddenError(Exception):
|
22
22
|
pass
|
23
23
|
|
24
24
|
|
25
|
-
class
|
25
|
+
class SecretNotFoundError(Exception):
|
26
26
|
pass
|
27
27
|
|
28
28
|
|
@@ -162,11 +162,11 @@ class VaultSecretReader(SecretReaderBase):
|
|
162
162
|
)
|
163
163
|
)
|
164
164
|
except Forbidden:
|
165
|
-
raise
|
165
|
+
raise VaultForbiddenError(
|
166
166
|
f"permission denied reading vault secret at {path}"
|
167
167
|
) from None
|
168
|
-
except vault.
|
169
|
-
raise
|
168
|
+
except vault.SecretNotFoundError as e:
|
169
|
+
raise SecretNotFoundError(*e.args) from e
|
170
170
|
return data
|
171
171
|
|
172
172
|
def _read(
|
@@ -181,8 +181,8 @@ class VaultSecretReader(SecretReaderBase):
|
|
181
181
|
version=version,
|
182
182
|
)
|
183
183
|
)
|
184
|
-
except vault.
|
185
|
-
raise
|
184
|
+
except vault.SecretNotFoundError as e:
|
185
|
+
raise SecretNotFoundError(*e.args) from e
|
186
186
|
return data
|
187
187
|
|
188
188
|
|
@@ -203,8 +203,8 @@ class ConfigSecretReader(SecretReaderBase):
|
|
203
203
|
version=version,
|
204
204
|
)
|
205
205
|
)
|
206
|
-
except config.
|
207
|
-
raise
|
206
|
+
except config.SecretNotFoundError as e:
|
207
|
+
raise SecretNotFoundError(*e.args) from e
|
208
208
|
return data
|
209
209
|
|
210
210
|
def _read_all(
|
@@ -219,8 +219,8 @@ class ConfigSecretReader(SecretReaderBase):
|
|
219
219
|
version=version,
|
220
220
|
)
|
221
221
|
)
|
222
|
-
except config.
|
223
|
-
raise
|
222
|
+
except config.SecretNotFoundError as e:
|
223
|
+
raise SecretNotFoundError(*e.args) from e
|
224
224
|
return data
|
225
225
|
|
226
226
|
|
@@ -279,13 +279,13 @@ class SecretReader(SecretReaderBase):
|
|
279
279
|
if self.settings and self.settings.get("vault"):
|
280
280
|
try:
|
281
281
|
data = self.vault_client.read(params) # type: ignore[attr-defined] # mypy doesn't recognize the VaultClient.__new__ method
|
282
|
-
except vault.
|
283
|
-
raise
|
282
|
+
except vault.SecretNotFoundError as e:
|
283
|
+
raise SecretNotFoundError(*e.args) from e
|
284
284
|
else:
|
285
285
|
try:
|
286
286
|
data = config.read(params)
|
287
|
-
except config.
|
288
|
-
raise
|
287
|
+
except config.SecretNotFoundError as e:
|
288
|
+
raise SecretNotFoundError(*e.args) from e
|
289
289
|
|
290
290
|
return data
|
291
291
|
|
@@ -314,15 +314,15 @@ class SecretReader(SecretReaderBase):
|
|
314
314
|
try:
|
315
315
|
data = self.vault_client.read_all(params) # type: ignore[attr-defined] # mypy doesn't recognize the VaultClient.__new__ method
|
316
316
|
except Forbidden:
|
317
|
-
raise
|
317
|
+
raise VaultForbiddenError(
|
318
318
|
f"permission denied reading vault secret at {path}"
|
319
319
|
) from None
|
320
|
-
except vault.
|
321
|
-
raise
|
320
|
+
except vault.SecretNotFoundError as e:
|
321
|
+
raise SecretNotFoundError(*e.args) from e
|
322
322
|
else:
|
323
323
|
try:
|
324
324
|
data = config.read_all(params)
|
325
|
-
except config.
|
326
|
-
raise
|
325
|
+
except config.SecretNotFoundError as e:
|
326
|
+
raise SecretNotFoundError(*e.args) from e
|
327
327
|
|
328
328
|
return data
|
reconcile/utils/slack_api.py
CHANGED
@@ -28,11 +28,11 @@ MAX_RETRIES = 5
|
|
28
28
|
TIMEOUT = 30
|
29
29
|
|
30
30
|
|
31
|
-
class
|
31
|
+
class UserNotFoundError(Exception):
|
32
32
|
pass
|
33
33
|
|
34
34
|
|
35
|
-
class
|
35
|
+
class UsergroupNotFoundError(Exception):
|
36
36
|
pass
|
37
37
|
|
38
38
|
|
@@ -296,7 +296,7 @@ class SlackApi:
|
|
296
296
|
def get_usergroup_id(self, handle: str) -> str | None:
|
297
297
|
try:
|
298
298
|
return self.get_usergroup(handle)["id"]
|
299
|
-
except
|
299
|
+
except UsergroupNotFoundError:
|
300
300
|
return None
|
301
301
|
|
302
302
|
def _initiate_usergroups(self) -> None:
|
@@ -317,7 +317,7 @@ class SlackApi:
|
|
317
317
|
self._initiate_usergroups()
|
318
318
|
usergroup = [g for g in self.usergroups if g["handle"] == handle]
|
319
319
|
if len(usergroup) != 1:
|
320
|
-
raise
|
320
|
+
raise UsergroupNotFoundError(handle)
|
321
321
|
return usergroup[0]
|
322
322
|
|
323
323
|
def create_usergroup(self, handle: str) -> str:
|
@@ -398,7 +398,7 @@ class SlackApi:
|
|
398
398
|
result = self._sc.users_lookupByEmail(email=f"{user_name}@{mail_address}")
|
399
399
|
except SlackApiError as e:
|
400
400
|
if e.response["error"] == "users_not_found":
|
401
|
-
raise
|
401
|
+
raise UserNotFoundError(e.response["error"]) from None
|
402
402
|
raise
|
403
403
|
|
404
404
|
return result["user"]["id"]
|
@@ -24,15 +24,15 @@ DEFAULT_RETRIES = 3
|
|
24
24
|
DEFAULT_THREAD_POOL_SIZE = 10
|
25
25
|
|
26
26
|
|
27
|
-
class
|
27
|
+
class EmptySLOResultError(Exception):
|
28
28
|
pass
|
29
29
|
|
30
30
|
|
31
|
-
class
|
31
|
+
class EmptySLOValueError(Exception):
|
32
32
|
pass
|
33
33
|
|
34
34
|
|
35
|
-
class
|
35
|
+
class InvalidSLOValueError(Exception):
|
36
36
|
pass
|
37
37
|
|
38
38
|
|
@@ -104,13 +104,13 @@ class PrometheusClient(ApiBase):
|
|
104
104
|
def _extract_current_slo_value(self, data: dict[str, Any]) -> float:
|
105
105
|
result = data["data"]["result"]
|
106
106
|
if not result:
|
107
|
-
raise
|
107
|
+
raise EmptySLOResultError("prometheus returned empty result")
|
108
108
|
slo_value = result[0]["value"]
|
109
109
|
if not slo_value:
|
110
|
-
raise
|
110
|
+
raise EmptySLOValueError("prometheus returned empty SLO value")
|
111
111
|
slo_value = float(slo_value[1])
|
112
112
|
if isnan(slo_value):
|
113
|
-
raise
|
113
|
+
raise InvalidSLOValueError("slo value should be a number")
|
114
114
|
return slo_value
|
115
115
|
|
116
116
|
|
reconcile/utils/state.py
CHANGED
@@ -39,7 +39,7 @@ from reconcile.utils.secret_reader import (
|
|
39
39
|
)
|
40
40
|
|
41
41
|
|
42
|
-
class
|
42
|
+
class StateInaccessibleError(Exception):
|
43
43
|
pass
|
44
44
|
|
45
45
|
|
@@ -180,7 +180,7 @@ def acquire_state_settings(
|
|
180
180
|
state_bucket_account_name, query_func=query_func
|
181
181
|
)
|
182
182
|
if not account:
|
183
|
-
raise
|
183
|
+
raise StateInaccessibleError(
|
184
184
|
f"The AWS account {state_bucket_account_name} that holds the state bucket can't be found in app-interface."
|
185
185
|
)
|
186
186
|
secret = secret_reader.read_all_secret(account.automation_token)
|
@@ -203,11 +203,11 @@ def acquire_state_settings(
|
|
203
203
|
access_key_id=secret["aws_access_key_id"],
|
204
204
|
secret_access_key=secret["aws_secret_access_key"],
|
205
205
|
)
|
206
|
-
raise
|
206
|
+
raise StateInaccessibleError(
|
207
207
|
f"The app-interface state provider {ai_settings.provider} is not supported."
|
208
208
|
)
|
209
209
|
|
210
|
-
raise
|
210
|
+
raise StateInaccessibleError(
|
211
211
|
"app-interface state must be configured to use stateful integrations. "
|
212
212
|
"use one of the following options to provide state config: "
|
213
213
|
"* env vars APP_INTERFACE_STATE_BUCKET, APP_INTERFACE_STATE_BUCKET_REGION, APP_INTERFACE_STATE_AWS_PROFILE and AWS_CONFIG (hosting the requested profile) \n"
|
@@ -218,7 +218,7 @@ def acquire_state_settings(
|
|
218
218
|
)
|
219
219
|
|
220
220
|
|
221
|
-
class
|
221
|
+
class AbortStateTransactionError(Exception):
|
222
222
|
"""Raise to abort a state transaction."""
|
223
223
|
|
224
224
|
|
@@ -249,7 +249,7 @@ class State:
|
|
249
249
|
try:
|
250
250
|
self.client.head_bucket(Bucket=self.bucket)
|
251
251
|
except ClientError as details:
|
252
|
-
raise
|
252
|
+
raise StateInaccessibleError(
|
253
253
|
f"Bucket {self.bucket} is not accessible - {details!s}"
|
254
254
|
) from None
|
255
255
|
|
@@ -299,7 +299,7 @@ class State:
|
|
299
299
|
if error_code == "404":
|
300
300
|
return False, {}
|
301
301
|
|
302
|
-
raise
|
302
|
+
raise StateInaccessibleError(
|
303
303
|
f"Can not access state key {key_path} "
|
304
304
|
f"in bucket {self.bucket} - {details!s}"
|
305
305
|
) from None
|
@@ -436,7 +436,7 @@ class State:
|
|
436
436
|
state_obj = TransactionStateObj(key, value=current_value)
|
437
437
|
try:
|
438
438
|
yield state_obj
|
439
|
-
except
|
439
|
+
except AbortStateTransactionError:
|
440
440
|
return
|
441
441
|
else:
|
442
442
|
if state_obj.changed and state_obj.value != current_value:
|
@@ -913,13 +913,13 @@ class TerraformClient: # pylint: disable=too-many-public-methods
|
|
913
913
|
)
|
914
914
|
|
915
915
|
|
916
|
-
class
|
916
|
+
class TerraformPlanFailedError(Exception):
|
917
917
|
pass
|
918
918
|
|
919
919
|
|
920
|
-
class
|
920
|
+
class TerraformApplyFailedError(Exception):
|
921
921
|
pass
|
922
922
|
|
923
923
|
|
924
|
-
class
|
924
|
+
class TerraformDeletionDetectedError(Exception):
|
925
925
|
pass
|
@@ -302,9 +302,9 @@ def _get_terraform_s3_state_key_name(
|
|
302
302
|
return sharding_strategy.get_object_key(integration)
|
303
303
|
|
304
304
|
|
305
|
-
class
|
305
|
+
class IntegrationUndefinedError(Exception):
|
306
306
|
pass
|
307
307
|
|
308
308
|
|
309
|
-
class
|
309
|
+
class InvalidTerraformStateError(Exception):
|
310
310
|
pass
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# ruff: noqa: N801
|
1
2
|
import base64
|
2
3
|
import enum
|
3
4
|
import json
|
@@ -303,7 +304,7 @@ AWS_US_GOV_ELB_ACCOUNT_IDS = {
|
|
303
304
|
}
|
304
305
|
|
305
306
|
|
306
|
-
class
|
307
|
+
class OutputResourceNameNotUniqueError(Exception):
|
307
308
|
def __init__(self, namespace, duplicates):
|
308
309
|
self.namespace, self.duplicates = namespace, duplicates
|
309
310
|
super().__init__(
|
@@ -319,7 +320,7 @@ class RDSParameterGroupValidationError(Exception):
|
|
319
320
|
pass
|
320
321
|
|
321
322
|
|
322
|
-
class
|
323
|
+
class StateInaccessibleError(Exception):
|
323
324
|
pass
|
324
325
|
|
325
326
|
|
@@ -1659,7 +1660,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
1659
1660
|
name_counter = Counter(spec.output_resource_name for spec in specs)
|
1660
1661
|
duplicates = [name for name, count in name_counter.items() if count > 1]
|
1661
1662
|
if duplicates:
|
1662
|
-
raise
|
1663
|
+
raise OutputResourceNameNotUniqueError(
|
1663
1664
|
namespace_info.get("name"), duplicates
|
1664
1665
|
)
|
1665
1666
|
for spec in specs:
|
@@ -4566,7 +4567,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
4566
4567
|
publishing_options blocks which will be further used
|
4567
4568
|
by the consumer.
|
4568
4569
|
"""
|
4569
|
-
|
4570
|
+
es_log_group_retention_days = 90
|
4570
4571
|
tf_resources = []
|
4571
4572
|
publishing_options = []
|
4572
4573
|
|
@@ -4590,7 +4591,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
4590
4591
|
log_group_values = {
|
4591
4592
|
"name": log_type_identifier,
|
4592
4593
|
"tags": values["tags"],
|
4593
|
-
"retention_in_days":
|
4594
|
+
"retention_in_days": es_log_group_retention_days,
|
4594
4595
|
}
|
4595
4596
|
region = values.get("region") or self.default_regions.get(account)
|
4596
4597
|
if self._multiregion_account(account):
|
@@ -5066,12 +5067,12 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
5066
5067
|
+ "does not have required key [certificate]"
|
5067
5068
|
)
|
5068
5069
|
|
5069
|
-
|
5070
|
+
ca_certificate = secret_data.get("caCertificate", None)
|
5071
|
+
if ca_certificate is not None:
|
5072
|
+
values["certificate_chain"] = ca_certificate
|
5070
5073
|
|
5071
5074
|
values["private_key"] = key
|
5072
5075
|
values["certificate_body"] = certificate
|
5073
|
-
if caCertificate is not None:
|
5074
|
-
values["certificate_chain"] = caCertificate
|
5075
5076
|
|
5076
5077
|
domain = common_values.get("domain", None)
|
5077
5078
|
if domain is not None:
|
@@ -5115,9 +5116,9 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
5115
5116
|
output_name = output_prefix + "__certificate"
|
5116
5117
|
output_value = certificate
|
5117
5118
|
tf_resources.append(Output(output_name, value=output_value, sensitive=True))
|
5118
|
-
if
|
5119
|
+
if ca_certificate is not None:
|
5119
5120
|
output_name = output_prefix + "__caCertificate"
|
5120
|
-
output_value =
|
5121
|
+
output_value = ca_certificate
|
5121
5122
|
tf_resources.append(
|
5122
5123
|
Output(output_name, value=output_value, sensitive=True)
|
5123
5124
|
)
|
@@ -6016,7 +6017,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
|
|
6016
6017
|
try:
|
6017
6018
|
s3_client.head_bucket(Bucket=bucket_name)
|
6018
6019
|
except ClientError as details:
|
6019
|
-
raise
|
6020
|
+
raise StateInaccessibleError(
|
6020
6021
|
f"Bucket {bucket_name} is not accessible - {details!s}"
|
6021
6022
|
) from None
|
6022
6023
|
|
reconcile/utils/vault.py
CHANGED
@@ -18,27 +18,27 @@ LOG = logging.getLogger(__name__)
|
|
18
18
|
VAULT_AUTO_REFRESH_INTERVAL = int(os.getenv("VAULT_AUTO_REFRESH_INTERVAL") or 600)
|
19
19
|
|
20
20
|
|
21
|
-
class
|
21
|
+
class PathAccessForbiddenError(Exception):
|
22
22
|
pass
|
23
23
|
|
24
24
|
|
25
|
-
class
|
25
|
+
class SecretNotFoundError(Exception):
|
26
26
|
pass
|
27
27
|
|
28
28
|
|
29
|
-
class
|
29
|
+
class SecretAccessForbiddenError(Exception):
|
30
30
|
pass
|
31
31
|
|
32
32
|
|
33
|
-
class
|
33
|
+
class SecretVersionIsNoneError(Exception):
|
34
34
|
pass
|
35
35
|
|
36
36
|
|
37
|
-
class
|
37
|
+
class SecretVersionNotFoundError(Exception):
|
38
38
|
pass
|
39
39
|
|
40
40
|
|
41
|
-
class
|
41
|
+
class SecretFieldNotFoundError(Exception):
|
42
42
|
pass
|
43
43
|
|
44
44
|
|
@@ -189,7 +189,7 @@ class _VaultClient:
|
|
189
189
|
data = secret_data
|
190
190
|
|
191
191
|
if data is None:
|
192
|
-
raise
|
192
|
+
raise SecretNotFoundError
|
193
193
|
|
194
194
|
return data, version
|
195
195
|
|
@@ -223,7 +223,7 @@ class _VaultClient:
|
|
223
223
|
read_path = "/".join(path_split[1:])
|
224
224
|
if version is None:
|
225
225
|
msg = f"version can not be null for secret with path '{path}'."
|
226
|
-
raise
|
226
|
+
raise SecretVersionIsNoneError(msg)
|
227
227
|
if version == SECRET_VERSION_LATEST:
|
228
228
|
# https://github.com/hvac/hvac/blob/
|
229
229
|
# ec048ded30d21c13c21cfa950d148c8bfc1467b0/
|
@@ -237,12 +237,12 @@ class _VaultClient:
|
|
237
237
|
)
|
238
238
|
except InvalidPath:
|
239
239
|
msg = f"version '{version}' not found for secret with path '{path}'."
|
240
|
-
raise
|
240
|
+
raise SecretVersionNotFoundError(msg) from None
|
241
241
|
except hvac.exceptions.Forbidden:
|
242
242
|
msg = f"permission denied accessing secret '{path}'"
|
243
|
-
raise
|
243
|
+
raise SecretAccessForbiddenError(msg) from None
|
244
244
|
if secret is None or "data" not in secret or "data" not in secret["data"]:
|
245
|
-
raise
|
245
|
+
raise SecretNotFoundError(path)
|
246
246
|
|
247
247
|
data = secret["data"]["data"]
|
248
248
|
secret_version = secret["data"]["metadata"]["version"]
|
@@ -253,10 +253,10 @@ class _VaultClient:
|
|
253
253
|
secret = self._client.read(path)
|
254
254
|
except hvac.exceptions.Forbidden:
|
255
255
|
msg = f"permission denied accessing secret '{path}'"
|
256
|
-
raise
|
256
|
+
raise SecretAccessForbiddenError(msg) from None
|
257
257
|
|
258
258
|
if secret is None or "data" not in secret:
|
259
|
-
raise
|
259
|
+
raise SecretNotFoundError(path)
|
260
260
|
|
261
261
|
return secret["data"]
|
262
262
|
|
@@ -285,7 +285,7 @@ class _VaultClient:
|
|
285
285
|
data = self._read_v1(secret_path, secret_field)
|
286
286
|
|
287
287
|
if data is None:
|
288
|
-
raise
|
288
|
+
raise SecretNotFoundError
|
289
289
|
|
290
290
|
return (
|
291
291
|
base64.b64decode(data).decode("utf-8")
|
@@ -298,7 +298,7 @@ class _VaultClient:
|
|
298
298
|
try:
|
299
299
|
secret_field = data[field]
|
300
300
|
except KeyError:
|
301
|
-
raise
|
301
|
+
raise SecretFieldNotFoundError(f"{path}/{field} ({version})") from None
|
302
302
|
return secret_field
|
303
303
|
|
304
304
|
def _read_v1(self, path, field):
|
@@ -306,7 +306,7 @@ class _VaultClient:
|
|
306
306
|
try:
|
307
307
|
secret_field = data[field]
|
308
308
|
except KeyError:
|
309
|
-
raise
|
309
|
+
raise SecretFieldNotFoundError(f"{path}/{field}") from None
|
310
310
|
return secret_field
|
311
311
|
|
312
312
|
@retry()
|
@@ -345,7 +345,7 @@ class _VaultClient:
|
|
345
345
|
if current_data == data and not force:
|
346
346
|
logging.debug(f"current data is up-to-date, skipping {path}")
|
347
347
|
return
|
348
|
-
except
|
348
|
+
except SecretVersionNotFoundError:
|
349
349
|
# if the secret is not found we need to write it
|
350
350
|
logging.debug(f"secret not found in {path}, will create it")
|
351
351
|
|
@@ -358,14 +358,14 @@ class _VaultClient:
|
|
358
358
|
self._read_all_v2.cache_clear()
|
359
359
|
except hvac.exceptions.Forbidden:
|
360
360
|
msg = f"permission denied accessing secret '{path}'"
|
361
|
-
raise
|
361
|
+
raise SecretAccessForbiddenError(msg) from None
|
362
362
|
|
363
363
|
def _write_v1(self, path, data):
|
364
364
|
try:
|
365
365
|
self._client.write(path, **data)
|
366
366
|
except hvac.exceptions.Forbidden:
|
367
367
|
msg = f"permission denied accessing secret '{path}'"
|
368
|
-
raise
|
368
|
+
raise SecretAccessForbiddenError(msg) from None
|
369
369
|
|
370
370
|
def _list_kv2(self, path: str) -> dict:
|
371
371
|
try:
|
@@ -376,14 +376,14 @@ class _VaultClient:
|
|
376
376
|
return response
|
377
377
|
except hvac.exceptions.Forbidden:
|
378
378
|
msg = f"permission denied accessing path '{path}'"
|
379
|
-
raise
|
379
|
+
raise PathAccessForbiddenError(msg) from None
|
380
380
|
|
381
381
|
def _list(self, path: str) -> dict:
|
382
382
|
try:
|
383
383
|
return self._client.list(path)
|
384
384
|
except hvac.exceptions.Forbidden:
|
385
385
|
msg = f"permission denied accessing path '{path}'"
|
386
|
-
raise
|
386
|
+
raise PathAccessForbiddenError(msg) from None
|
387
387
|
|
388
388
|
def list(self, path: str) -> list[str]:
|
389
389
|
"""Returns a list of secrets in a given path."""
|
@@ -420,7 +420,7 @@ class _VaultClient:
|
|
420
420
|
self._client.delete(path)
|
421
421
|
except hvac.exceptions.Forbidden:
|
422
422
|
msg = f"permission denied accessing secret '{path}'"
|
423
|
-
raise
|
423
|
+
raise SecretAccessForbiddenError(msg) from None
|
424
424
|
|
425
425
|
|
426
426
|
class VaultClient:
|
reconcile/vault_replication.py
CHANGED
@@ -26,9 +26,9 @@ from reconcile.gql_definitions.vault_policies.vault_policies import (
|
|
26
26
|
)
|
27
27
|
from reconcile.utils import gql
|
28
28
|
from reconcile.utils.vault import (
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
SecretAccessForbiddenError,
|
30
|
+
SecretNotFoundError,
|
31
|
+
SecretVersionNotFoundError,
|
32
32
|
VaultClient,
|
33
33
|
_VaultClient,
|
34
34
|
)
|
@@ -37,15 +37,15 @@ QONTRACT_INTEGRATION = "vault-replication"
|
|
37
37
|
SECRET_PATH_PATTERN = re.compile(r"^[\w/-]+?(?P<folder>/\*?)?$")
|
38
38
|
|
39
39
|
|
40
|
-
class
|
40
|
+
class VaultInvalidPathsError(Exception):
|
41
41
|
pass
|
42
42
|
|
43
43
|
|
44
|
-
class
|
44
|
+
class VaultInvalidAuthMethodError(Exception):
|
45
45
|
pass
|
46
46
|
|
47
47
|
|
48
|
-
class
|
48
|
+
class VaultInvalidPolicyError(Exception):
|
49
49
|
pass
|
50
50
|
|
51
51
|
|
@@ -64,7 +64,7 @@ def deep_copy_versions(
|
|
64
64
|
|
65
65
|
try:
|
66
66
|
secret, src_version = source_vault.read_all_with_version(secret_dict)
|
67
|
-
except (
|
67
|
+
except (SecretNotFoundError, SecretVersionNotFoundError):
|
68
68
|
# Handle the case where the difference between the source and destination
|
69
69
|
# versions is greater than the number of versions in the source vault.
|
70
70
|
# By default the secret engines store up to 10 versions of a secret.
|
@@ -119,7 +119,7 @@ def copy_vault_secret(
|
|
119
119
|
|
120
120
|
try:
|
121
121
|
source_data, version = source_vault.read_all_with_version(secret_dict)
|
122
|
-
except
|
122
|
+
except SecretAccessForbiddenError:
|
123
123
|
# Raise exception if we can't read the secret from the source vault.
|
124
124
|
# This is likely to be related to the approle permissions.
|
125
125
|
logging.error([
|
@@ -128,7 +128,7 @@ def copy_vault_secret(
|
|
128
128
|
path,
|
129
129
|
])
|
130
130
|
raise
|
131
|
-
except
|
131
|
+
except SecretNotFoundError:
|
132
132
|
# If the secret is present in vault, but there are no versions of it
|
133
133
|
# we want to be aware of it, but not cause a failure of the complete
|
134
134
|
# integration
|
@@ -161,7 +161,7 @@ def copy_vault_secret(
|
|
161
161
|
current_source_version=version,
|
162
162
|
path=path,
|
163
163
|
)
|
164
|
-
except (
|
164
|
+
except (SecretVersionNotFoundError, SecretNotFoundError):
|
165
165
|
logging.info(["replicate_vault_secret", "Secret not found", path])
|
166
166
|
# Handle v1 secrets where version is None and we don't need to deep sync.
|
167
167
|
if version is None:
|
@@ -194,7 +194,7 @@ def check_invalid_paths(
|
|
194
194
|
# Exit if we have paths not present in the policy that needs to be replicated
|
195
195
|
# this is to prevent to replicate secrets that are not allowed.
|
196
196
|
logging.error(["replicate_vault_secret", "Invalid paths", invalid_paths])
|
197
|
-
raise
|
197
|
+
raise VaultInvalidPathsError
|
198
198
|
|
199
199
|
|
200
200
|
def list_invalid_paths(
|
@@ -236,7 +236,7 @@ def get_policy_secret_list(
|
|
236
236
|
match = SECRET_PATH_PATTERN.match(path)
|
237
237
|
if not match:
|
238
238
|
logging.error(["get_policy_secret_list", "Invalid path to replicate", path])
|
239
|
-
raise
|
239
|
+
raise VaultInvalidPathsError
|
240
240
|
|
241
241
|
if match.group("folder"):
|
242
242
|
# Remove the * at the end of the path because list method expects
|
@@ -304,7 +304,7 @@ def get_vault_credentials(
|
|
304
304
|
VaultInstanceV1_VaultReplicationConfigV1_VaultInstanceAuthV1_VaultInstanceAuthApproleV1,
|
305
305
|
):
|
306
306
|
# Exit if the auth method is not approle as is the only one supported
|
307
|
-
raise
|
307
|
+
raise VaultInvalidAuthMethodError
|
308
308
|
|
309
309
|
role_id = {
|
310
310
|
"path": vault_auth.role_id.path,
|
@@ -359,7 +359,7 @@ def replicate_paths(
|
|
359
359
|
if path.policy is None:
|
360
360
|
# Exit if the replication config is empty, this should never happen
|
361
361
|
# as policy is a required field in the schema but makes mypy happy.
|
362
|
-
raise
|
362
|
+
raise VaultInvalidPolicyError(
|
363
363
|
"Policy is required when using policy provider"
|
364
364
|
)
|
365
365
|
policy_paths = get_policy_paths(
|
@@ -402,7 +402,7 @@ def get_secrets_from_templated_path(path: str, vault_list: Iterable[str]) -> lis
|
|
402
402
|
suffix = cap_groups.group(3)
|
403
403
|
else:
|
404
404
|
# Exit if the path is not a valid formatted template on the secret path
|
405
|
-
raise
|
405
|
+
raise VaultInvalidPathsError
|
406
406
|
|
407
407
|
secret_start, secret_end = _get_start_end_secret(path)
|
408
408
|
|
tools/cli_commands/erv2.py
CHANGED
@@ -39,6 +39,9 @@ from reconcile.utils import gql
|
|
39
39
|
from reconcile.utils.exceptions import FetchResourceError
|
40
40
|
from reconcile.utils.secret_reader import SecretReaderBase
|
41
41
|
|
42
|
+
UP = "\x1b[1A"
|
43
|
+
CLEAR = "\x1b[2K"
|
44
|
+
|
42
45
|
|
43
46
|
def progress_spinner() -> Progress:
|
44
47
|
"""Display shiny progress spinner."""
|
@@ -56,8 +59,6 @@ def pause_progress_spinner(progress: Progress | None) -> Iterator:
|
|
56
59
|
"""Pause the progress spinner."""
|
57
60
|
if progress:
|
58
61
|
progress.stop()
|
59
|
-
UP = "\x1b[1A"
|
60
|
-
CLEAR = "\x1b[2K"
|
61
62
|
for task in progress.tasks:
|
62
63
|
if task.finished:
|
63
64
|
continue
|