qontract-reconcile 0.10.1rc863__py3-none-any.whl → 0.10.1rc865__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.1rc863.dist-info → qontract_reconcile-0.10.1rc865.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc863.dist-info → qontract_reconcile-0.10.1rc865.dist-info}/RECORD +17 -16
- reconcile/cli.py +18 -0
- reconcile/external_resources/integration.py +6 -1
- reconcile/external_resources/integration_secrets_sync.py +47 -0
- reconcile/external_resources/manager.py +35 -9
- reconcile/external_resources/meta.py +9 -0
- reconcile/external_resources/secrets_sync.py +267 -67
- reconcile/external_resources/state.py +10 -0
- reconcile/gitlab_permissions.py +3 -1
- reconcile/gql_definitions/external_resources/external_resources_modules.py +2 -0
- reconcile/gql_definitions/external_resources/external_resources_settings.py +2 -0
- reconcile/utils/external_resource_spec.py +5 -0
- reconcile/utils/openshift_resource.py +4 -0
- {qontract_reconcile-0.10.1rc863.dist-info → qontract_reconcile-0.10.1rc865.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc863.dist-info → qontract_reconcile-0.10.1rc865.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc863.dist-info → qontract_reconcile-0.10.1rc865.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc863.dist-info → qontract_reconcile-0.10.1rc865.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
3
|
+
Version: 0.10.1rc865
|
4
4
|
Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
|
5
5
|
Home-page: https://github.com/app-sre/qontract-reconcile
|
6
6
|
Author: Red Hat App-SRE Team
|
{qontract_reconcile-0.10.1rc863.dist-info → qontract_reconcile-0.10.1rc865.dist-info}/RECORD
RENAMED
@@ -10,7 +10,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
|
|
10
10
|
reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
|
11
11
|
reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
|
12
12
|
reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
|
13
|
-
reconcile/cli.py,sha256=
|
13
|
+
reconcile/cli.py,sha256=zGgAkhK8Pl8NIKtnPr8fz5ewHa1_SSMJYtrvPa-ip6I,105151
|
14
14
|
reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
|
15
15
|
reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
|
16
16
|
reconcile/dashdotdb_base.py,sha256=R2JuwiXAEYAFiCtnztM_IIr1rtVzPpaWAmgxuDa2FgY,4813
|
@@ -35,7 +35,7 @@ reconcile/gitlab_labeler.py,sha256=IxE1XM5o4rDOFuR4cM2yAHTy4Uzdg3Nyz2mp7b8Fx1g,4
|
|
35
35
|
reconcile/gitlab_members.py,sha256=M6LwFOrwgvl1NNdOJa1mrQFUon-bEVv1AyhGeLed454,8443
|
36
36
|
reconcile/gitlab_mr_sqs_consumer.py,sha256=O46mdziPgGOndbU-0_UJKJVUaiEoVzJPEgKm4_UvYoI,2571
|
37
37
|
reconcile/gitlab_owners.py,sha256=sn9njaKOtqcvnhi2qtm-faAfAR4zNqflbSuusA9RUuI,13456
|
38
|
-
reconcile/gitlab_permissions.py,sha256=
|
38
|
+
reconcile/gitlab_permissions.py,sha256=YaLiMqrn9YOM5e7o2NJVg6vtILyBXTryTScA4RhTyPo,3279
|
39
39
|
reconcile/gitlab_projects.py,sha256=K3tFf_aD1W4Ijp5q-9Qek3kwFGEWPcZ1kd7tzFJ4GyQ,1781
|
40
40
|
reconcile/integrations_manager.py,sha256=J_VV-HINI7YNav2NPIolePZkll-7VBuBXWAyMNhsM_Q,9535
|
41
41
|
reconcile/jenkins_base.py,sha256=0Gocu3fU2YTltaxBlbDQOUvP-7CP2OSQV1ZRwtWeVXw,875
|
@@ -184,14 +184,15 @@ reconcile/dynatrace_token_provider/metrics.py,sha256=xiKkl8fTEBQaXJelGCPNTZhHAWd
|
|
184
184
|
reconcile/external_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
185
185
|
reconcile/external_resources/aws.py,sha256=JvjKaABy2Pg8u8Lq82Acv4zMvpE3_qGKes7OG-zlHOM,2956
|
186
186
|
reconcile/external_resources/factories.py,sha256=bLboXX5Dq0xN60mtDGNjCOLC6HlKofXMWQxVbRwMMwo,4485
|
187
|
-
reconcile/external_resources/integration.py,sha256=
|
188
|
-
reconcile/external_resources/
|
189
|
-
reconcile/external_resources/
|
187
|
+
reconcile/external_resources/integration.py,sha256=p07tnD4pww9QgFlHT18RRn929BL7zKzRHRvHQ_ETRAU,5113
|
188
|
+
reconcile/external_resources/integration_secrets_sync.py,sha256=cMEZhgCvABAMf-DWF051L6CRnJQdfbsISA_b1xuS940,1670
|
189
|
+
reconcile/external_resources/manager.py,sha256=d1YuCZWOnUV3nfZ0HJI-IIG5qef7eQ4ct7hSTXcFK4M,14576
|
190
|
+
reconcile/external_resources/meta.py,sha256=Z5guBceyaGyAzzA9kVb0-WaNpSli368NVUnWulxtMb4,551
|
190
191
|
reconcile/external_resources/metrics.py,sha256=m2TIOao2N7pD6k45driFbBGVCC_N7ai44m-lLPfa5qk,454
|
191
192
|
reconcile/external_resources/model.py,sha256=FJUb7rHU2l7YSAv-t4QaacL9pqheFBxhPydWSPqu3vY,7413
|
192
193
|
reconcile/external_resources/reconciler.py,sha256=E50X_lnOD0OWYXMzyZld1P6dCFJFYjHGyICWff9bxlc,9323
|
193
|
-
reconcile/external_resources/secrets_sync.py,sha256=
|
194
|
-
reconcile/external_resources/state.py,sha256=
|
194
|
+
reconcile/external_resources/secrets_sync.py,sha256=xFQ_ObWl29btbxzEJSkndvHBPcsVpSTkmUlWcUGzZ70,15132
|
195
|
+
reconcile/external_resources/state.py,sha256=fA_CzT4oNie4wnaImwW-W1duWEOKFyS1omcnMyYwx2Q,9644
|
195
196
|
reconcile/glitchtip/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
196
197
|
reconcile/glitchtip/integration.py,sha256=Y7ofQg_xCt3dOln3pjeXp7rAnwohCgD2zcUAb-Hciis,8375
|
197
198
|
reconcile/glitchtip/reconciler.py,sha256=nUvDv7qG1ly0cA16MmlL6NV71yl1mJYLT2mui7lmi0Y,12402
|
@@ -274,9 +275,9 @@ reconcile/gql_definitions/dynatrace_token_provider/__init__.py,sha256=47DEQpj8HB
|
|
274
275
|
reconcile/gql_definitions/dynatrace_token_provider/dynatrace_bootstrap_tokens.py,sha256=5gTuAnR2rnx2k6Rn7FMEAzw6GCZ6F5HZbqkmJ9-3NI4,2244
|
275
276
|
reconcile/gql_definitions/external_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
276
277
|
reconcile/gql_definitions/external_resources/aws_accounts.py,sha256=XR69j9dpTQ0gv8y-AZN7AJ0dPvO-wbHscyCDgrax6Bk,2046
|
277
|
-
reconcile/gql_definitions/external_resources/external_resources_modules.py,sha256=
|
278
|
+
reconcile/gql_definitions/external_resources/external_resources_modules.py,sha256=g2KB2wRnb8zF7xCmDJJFmiRdE4z4aYa9HtY3vCBVwMA,2441
|
278
279
|
reconcile/gql_definitions/external_resources/external_resources_namespaces.py,sha256=UyOAUY1rROenjTz6y-uSEFjrEwhh-lPsIQPbi6EQLFg,40915
|
279
|
-
reconcile/gql_definitions/external_resources/external_resources_settings.py,sha256=
|
280
|
+
reconcile/gql_definitions/external_resources/external_resources_settings.py,sha256=Hw9n_90BPG6Lnt2PT3mHc6p0KEm2CxKxvSGRFc_Dhus,2982
|
280
281
|
reconcile/gql_definitions/fragments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
281
282
|
reconcile/gql_definitions/fragments/aus_organization.py,sha256=uBKbTuBa3CZmTXR5HOcGhRcu2U9kM93KbYmoWTxcpB0,4767
|
282
283
|
reconcile/gql_definitions/fragments/aws_account_common.py,sha256=3-7ZAP6GSff7Z2Syz2VQCLY4IySqBOSVmceaRiVNQpw,2385
|
@@ -647,7 +648,7 @@ reconcile/utils/environ.py,sha256=VnW3zp6Un_UJn5BU4FU8RfhuqtZp0s-VeuuHnqC_WcQ,51
|
|
647
648
|
reconcile/utils/exceptions.py,sha256=DwfnWUpVOotpP79RWZ2pycmG6nKCL00RBIeZLYkQPW4,635
|
648
649
|
reconcile/utils/expiration.py,sha256=BXwKE50sNIV-Lszke97fxitNkLxYszoOLW1LBgp_yqg,1246
|
649
650
|
reconcile/utils/extended_early_exit.py,sha256=QSktrmfw37zSRMNk930tDbQsVeKxaPPPD43e79DGwZw,6754
|
650
|
-
reconcile/utils/external_resource_spec.py,sha256=
|
651
|
+
reconcile/utils/external_resource_spec.py,sha256=OqEwhMgdpiWm9CnlpZjCMzqFmBDVfFDNYkcxvLmBwXM,6922
|
651
652
|
reconcile/utils/external_resources.py,sha256=ZnBjQlMpiuCX0Ivm77eIMB4Um_RuAsoMtc8IyuvIxI4,7590
|
652
653
|
reconcile/utils/filtering.py,sha256=zZnHH0u0SaTDyzuFXZ_mREURGLvjEqQIQy4z-7QBVlc,419
|
653
654
|
reconcile/utils/git.py,sha256=BdxXFgQ1XOZpS-4qb3qMsKTCFDG8MlE26rv1jAhvCkM,1560
|
@@ -678,7 +679,7 @@ reconcile/utils/oc_connection_parameters.py,sha256=85slrnDigYwYmzhyceVkMElWzFArp
|
|
678
679
|
reconcile/utils/oc_filters.py,sha256=R2Lf3fo0jQCeE62Ygeo_KN24XbAosq0QbjimYG6qHI4,1402
|
679
680
|
reconcile/utils/oc_map.py,sha256=nT69J5pdPeIDnIYjD9fwY6GkE3BMQCf-AF0rmHJuUNw,9068
|
680
681
|
reconcile/utils/ocm_base_client.py,sha256=X8qkPXfpfJdBKBtFv7zyGD33HNAEBJL8owf-ykrt-Ts,6469
|
681
|
-
reconcile/utils/openshift_resource.py,sha256=
|
682
|
+
reconcile/utils/openshift_resource.py,sha256=zA2hOhhvAb6v_NAcge_pem2EL1G7JMGolEfvFoHMgvM,24952
|
682
683
|
reconcile/utils/openssl.py,sha256=QVvhzhpChq_4Daf_5wE1qeZJr4thg3DDjJPn4bOPD4E,365
|
683
684
|
reconcile/utils/output.py,sha256=I_kXYyPcN1mlZmX16ZnLNGkhhwnal640GIdIaGJd4wE,2026
|
684
685
|
reconcile/utils/pagerduty_api.py,sha256=fcSAUez6w51woDvbm0plJW2qSw6_NXQs1Fit_KTNitc,7653
|
@@ -836,8 +837,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
|
|
836
837
|
tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jrss,4941
|
837
838
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
838
839
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
839
|
-
qontract_reconcile-0.10.
|
840
|
-
qontract_reconcile-0.10.
|
841
|
-
qontract_reconcile-0.10.
|
842
|
-
qontract_reconcile-0.10.
|
843
|
-
qontract_reconcile-0.10.
|
840
|
+
qontract_reconcile-0.10.1rc865.dist-info/METADATA,sha256=hKbGCp2EG6vMoZ5lyFHsA9xtBs6aXsG0AINh0fD9w5A,2273
|
841
|
+
qontract_reconcile-0.10.1rc865.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
842
|
+
qontract_reconcile-0.10.1rc865.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
|
843
|
+
qontract_reconcile-0.10.1rc865.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
844
|
+
qontract_reconcile-0.10.1rc865.dist-info/RECORD,,
|
reconcile/cli.py
CHANGED
@@ -3712,6 +3712,24 @@ def external_resources(
|
|
3712
3712
|
)
|
3713
3713
|
|
3714
3714
|
|
3715
|
+
@integration.command(
|
3716
|
+
short_help="Syncs External Resources Secrets from Vault to Clusters"
|
3717
|
+
)
|
3718
|
+
@click.pass_context
|
3719
|
+
@threaded(default=5)
|
3720
|
+
def external_resources_secrets_sync(
|
3721
|
+
ctx,
|
3722
|
+
thread_pool_size: int,
|
3723
|
+
):
|
3724
|
+
import reconcile.external_resources.integration_secrets_sync
|
3725
|
+
|
3726
|
+
run_integration(
|
3727
|
+
reconcile.external_resources.integration_secrets_sync,
|
3728
|
+
ctx.obj,
|
3729
|
+
thread_pool_size,
|
3730
|
+
)
|
3731
|
+
|
3732
|
+
|
3715
3733
|
def get_integration_cli_meta() -> dict[str, IntegrationMeta]:
|
3716
3734
|
"""
|
3717
3735
|
returns all integrations known to cli.py via click introspection
|
@@ -124,7 +124,12 @@ def run(
|
|
124
124
|
dry_run_job_suffix=dry_run_job_suffix,
|
125
125
|
),
|
126
126
|
secrets_reconciler=build_incluster_secrets_reconciler(
|
127
|
-
workers_cluster,
|
127
|
+
workers_cluster,
|
128
|
+
workers_namespace,
|
129
|
+
secret_reader,
|
130
|
+
vault_path=er_settings.vault_secrets_path,
|
131
|
+
thread_pool_size=thread_pool_size,
|
132
|
+
dry_run=dry_run,
|
128
133
|
),
|
129
134
|
)
|
130
135
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
from reconcile.external_resources.model import (
|
2
|
+
ExternalResourcesInventory,
|
3
|
+
load_module_inventory,
|
4
|
+
)
|
5
|
+
from reconcile.external_resources.secrets_sync import VaultSecretsReconciler
|
6
|
+
from reconcile.typed_queries.app_interface_vault_settings import (
|
7
|
+
get_app_interface_vault_settings,
|
8
|
+
)
|
9
|
+
from reconcile.typed_queries.external_resources import (
|
10
|
+
get_modules,
|
11
|
+
get_namespaces,
|
12
|
+
get_settings,
|
13
|
+
)
|
14
|
+
from reconcile.utils.openshift_resource import ResourceInventory
|
15
|
+
from reconcile.utils.secret_reader import create_secret_reader
|
16
|
+
from reconcile.utils.semver_helper import make_semver
|
17
|
+
|
18
|
+
QONTRACT_INTEGRATION = "external_resources_secrets_sync"
|
19
|
+
QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
|
20
|
+
|
21
|
+
|
22
|
+
def run(dry_run: bool, thread_pool_size: int) -> None:
|
23
|
+
"""Integration that syncs External Resources Outputs Secrets from Vault into
|
24
|
+
the target clusters
|
25
|
+
"""
|
26
|
+
vault_settings = get_app_interface_vault_settings()
|
27
|
+
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
28
|
+
er_settings = get_settings()[0]
|
29
|
+
|
30
|
+
namespaces = [ns for ns in get_namespaces() if ns.external_resources]
|
31
|
+
er_inventory = ExternalResourcesInventory(namespaces)
|
32
|
+
m_inventory = load_module_inventory(get_modules())
|
33
|
+
|
34
|
+
to_sync_specs = [
|
35
|
+
spec
|
36
|
+
for key, spec in er_inventory.items()
|
37
|
+
if m_inventory.get_from_external_resource_key(key).outputs_secret_sync
|
38
|
+
]
|
39
|
+
|
40
|
+
reconciler = VaultSecretsReconciler(
|
41
|
+
ri=ResourceInventory(),
|
42
|
+
secrets_reader=secret_reader,
|
43
|
+
vault_path=er_settings.vault_secrets_path,
|
44
|
+
thread_pool_size=thread_pool_size,
|
45
|
+
dry_run=dry_run,
|
46
|
+
)
|
47
|
+
reconciler.sync_secrets(to_sync_specs)
|
@@ -212,7 +212,7 @@ class ExternalResourcesManager:
|
|
212
212
|
ResourceStatus.DELETE_IN_PROGRESS,
|
213
213
|
ResourceStatus.IN_PROGRESS,
|
214
214
|
]):
|
215
|
-
return
|
215
|
+
return False
|
216
216
|
|
217
217
|
logging.info(
|
218
218
|
"Reconciliation In progress. Action: %s, Key:%s",
|
@@ -231,7 +231,7 @@ class ExternalResourcesManager:
|
|
231
231
|
r.key,
|
232
232
|
)
|
233
233
|
if r.action == Action.APPLY:
|
234
|
-
state.resource_status = ResourceStatus.
|
234
|
+
state.resource_status = ResourceStatus.PENDING_SECRET_SYNC
|
235
235
|
state.reconciliation_errors = 0
|
236
236
|
self.state_mgr.set_external_resource_state(state)
|
237
237
|
need_secret_sync = True
|
@@ -275,9 +275,31 @@ class ExternalResourcesManager:
|
|
275
275
|
r.action == Action.APPLY and state.resource_status == ResourceStatus.CREATED
|
276
276
|
)
|
277
277
|
|
278
|
-
def _sync_secrets(
|
279
|
-
|
280
|
-
|
278
|
+
def _sync_secrets(
|
279
|
+
self,
|
280
|
+
to_sync_keys: Iterable[ExternalResourceKey],
|
281
|
+
) -> None:
|
282
|
+
specs = [
|
283
|
+
spec for key in set(to_sync_keys) if (spec := self.er_inventory.get(key))
|
284
|
+
]
|
285
|
+
|
286
|
+
sync_error_spec_keys = {
|
287
|
+
ExternalResourceKey.from_spec(spec)
|
288
|
+
for spec in self.secrets_reconciler.sync_secrets(specs=specs)
|
289
|
+
}
|
290
|
+
|
291
|
+
for key in to_sync_keys:
|
292
|
+
if key in sync_error_spec_keys:
|
293
|
+
logging.error(
|
294
|
+
"Outputs secret for key can not be reconciled. Key: %s", key
|
295
|
+
)
|
296
|
+
else:
|
297
|
+
logging.debug(
|
298
|
+
"Outputs secret for key has been reconciled. Marking resource as %s. Key: %s",
|
299
|
+
ResourceStatus.CREATED,
|
300
|
+
key,
|
301
|
+
)
|
302
|
+
self.state_mgr.update_resource_status(key, ResourceStatus.CREATED)
|
281
303
|
|
282
304
|
def _build_external_resource(
|
283
305
|
self, spec: ExternalResourceSpec, er_inventory: ExternalResourcesInventory
|
@@ -295,12 +317,12 @@ class ExternalResourcesManager:
|
|
295
317
|
def handle_resources(self) -> None:
|
296
318
|
desired_r = self._get_desired_objects_reconciliations()
|
297
319
|
deleted_r = self._get_deleted_objects_reconciliations()
|
298
|
-
|
320
|
+
to_sync_keys: set[ExternalResourceKey] = set()
|
299
321
|
for r in desired_r.union(deleted_r):
|
300
322
|
state = self.state_mgr.get_external_resource_state(r.key)
|
301
323
|
need_sync = self._update_in_progress_state(r, state)
|
302
324
|
if need_sync:
|
303
|
-
|
325
|
+
to_sync_keys.add(r.key)
|
304
326
|
|
305
327
|
metrics.set_gauge(
|
306
328
|
ExternalResourcesReconcileErrorsGauge(
|
@@ -316,8 +338,12 @@ class ExternalResourcesManager:
|
|
316
338
|
self.reconciler.reconcile_resource(reconciliation=r)
|
317
339
|
self._update_state(r, state)
|
318
340
|
|
319
|
-
|
320
|
-
|
341
|
+
pending_sync_keys = self.state_mgr.get_keys_by_status(
|
342
|
+
ResourceStatus.PENDING_SECRET_SYNC
|
343
|
+
)
|
344
|
+
|
345
|
+
if to_sync_keys or pending_sync_keys:
|
346
|
+
self._sync_secrets(to_sync_keys=to_sync_keys | pending_sync_keys)
|
321
347
|
|
322
348
|
def handle_dry_run_resources(self) -> None:
|
323
349
|
desired_r = self._get_desired_objects_reconciliations()
|
@@ -2,3 +2,12 @@ from reconcile.utils.semver_helper import make_semver
|
|
2
2
|
|
3
3
|
QONTRACT_INTEGRATION = "external_resources"
|
4
4
|
QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
|
5
|
+
|
6
|
+
|
7
|
+
SECRET_ANN_PREFIX = "external-resources"
|
8
|
+
SECRET_ANN_PROVISION_PROVIDER = SECRET_ANN_PREFIX + "/provision_provider"
|
9
|
+
SECRET_ANN_PROVISIONER = SECRET_ANN_PREFIX + "/provisioner_name"
|
10
|
+
SECRET_ANN_PROVIDER = SECRET_ANN_PREFIX + "/provider"
|
11
|
+
SECRET_ANN_IDENTIFIER = SECRET_ANN_PREFIX + "/identifier"
|
12
|
+
SECRET_UPDATED_AT = SECRET_ANN_PREFIX + "/updated_at"
|
13
|
+
SECRET_UPDATED_AT_TIMEFORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
@@ -1,17 +1,27 @@
|
|
1
1
|
import base64
|
2
2
|
import json
|
3
|
+
import logging
|
3
4
|
from abc import abstractmethod
|
4
5
|
from collections.abc import Iterable, Mapping
|
6
|
+
from datetime import datetime, timezone
|
5
7
|
from hashlib import shake_128
|
6
|
-
from typing import Any, Optional
|
8
|
+
from typing import Any, Optional
|
7
9
|
|
8
10
|
from pydantic import BaseModel
|
9
|
-
from sretoolbox.utils import
|
11
|
+
from sretoolbox.utils import threaded
|
10
12
|
|
11
13
|
from reconcile.external_resources.meta import (
|
12
14
|
QONTRACT_INTEGRATION,
|
13
15
|
QONTRACT_INTEGRATION_VERSION,
|
16
|
+
SECRET_ANN_IDENTIFIER,
|
17
|
+
SECRET_ANN_PROVIDER,
|
18
|
+
SECRET_ANN_PROVISION_PROVIDER,
|
19
|
+
SECRET_ANN_PROVISIONER,
|
20
|
+
SECRET_UPDATED_AT,
|
21
|
+
SECRET_UPDATED_AT_TIMEFORMAT,
|
14
22
|
)
|
23
|
+
from reconcile.external_resources.model import ExternalResourceKey
|
24
|
+
from reconcile.openshift_base import ApplyOptions, apply_action
|
15
25
|
from reconcile.typed_queries.clusters_minimal import get_clusters_minimal
|
16
26
|
from reconcile.utils.differ import diff_mappings
|
17
27
|
from reconcile.utils.external_resource_spec import (
|
@@ -22,7 +32,7 @@ from reconcile.utils.oc import (
|
|
22
32
|
)
|
23
33
|
from reconcile.utils.oc_map import OCMap, init_oc_map_from_clusters
|
24
34
|
from reconcile.utils.openshift_resource import OpenshiftResource, ResourceInventory
|
25
|
-
from reconcile.utils.secret_reader import SecretReaderBase
|
35
|
+
from reconcile.utils.secret_reader import SecretNotFound, SecretReaderBase
|
26
36
|
from reconcile.utils.three_way_diff_strategy import three_way_diff_using_hash
|
27
37
|
from reconcile.utils.vault import (
|
28
38
|
VaultClient,
|
@@ -39,54 +49,103 @@ class VaultSecret(BaseModel):
|
|
39
49
|
q_format: Optional[str]
|
40
50
|
|
41
51
|
|
52
|
+
class SecretHelper:
|
53
|
+
@staticmethod
|
54
|
+
def get_comparable_secret(resource: OpenshiftResource) -> OpenshiftResource:
|
55
|
+
metadata = {k: v for k, v in resource.body["metadata"].items()}
|
56
|
+
metadata["annotations"] = {
|
57
|
+
k: v
|
58
|
+
for k, v in metadata.get("annotations", {}).items()
|
59
|
+
if k != SECRET_UPDATED_AT
|
60
|
+
}
|
61
|
+
new = OpenshiftResource(
|
62
|
+
body=resource.body | {"metadata": metadata},
|
63
|
+
integration=resource.integration,
|
64
|
+
integration_version=resource.integration_version,
|
65
|
+
error_details=resource.error_details,
|
66
|
+
caller_name=resource.caller_name,
|
67
|
+
validate_k8s_object=False,
|
68
|
+
)
|
69
|
+
|
70
|
+
return new
|
71
|
+
|
72
|
+
@staticmethod
|
73
|
+
def is_newer(a: OpenshiftResource, b: OpenshiftResource) -> bool:
|
74
|
+
try:
|
75
|
+
# ISO8601 with the same TZ can be compared as strings.
|
76
|
+
return a.annotations[SECRET_UPDATED_AT] > b.annotations[SECRET_UPDATED_AT]
|
77
|
+
except (KeyError, ValueError) as e:
|
78
|
+
logging.debug("Error comparing timestamps %s", e)
|
79
|
+
return False
|
80
|
+
|
81
|
+
@staticmethod
|
82
|
+
def compare(current: OpenshiftResource, desired: OpenshiftResource) -> bool:
|
83
|
+
if SECRET_UPDATED_AT not in current.annotations:
|
84
|
+
logging.debug(
|
85
|
+
"Current does not have the optimistic locking annotation. Apply"
|
86
|
+
)
|
87
|
+
return False
|
88
|
+
# if current is newer; don't apply
|
89
|
+
if SecretHelper.is_newer(current, desired):
|
90
|
+
logging.debug("Current Secret is newer than Desired: Don't Apply")
|
91
|
+
return True
|
92
|
+
cmp_current = SecretHelper.get_comparable_secret(current)
|
93
|
+
cmp_desired = SecretHelper.get_comparable_secret(desired)
|
94
|
+
|
95
|
+
return three_way_diff_using_hash(cmp_current, cmp_desired)
|
96
|
+
|
97
|
+
|
42
98
|
class SecretsReconciler:
|
43
99
|
def __init__(
|
44
100
|
self,
|
45
101
|
ri: ResourceInventory,
|
46
102
|
secrets_reader: SecretReaderBase,
|
47
|
-
|
48
|
-
|
103
|
+
thread_pool_size: int,
|
104
|
+
dry_run: bool,
|
49
105
|
) -> None:
|
50
106
|
self.secrets_reader = secrets_reader
|
51
107
|
self.ri = ri
|
52
|
-
self.
|
53
|
-
self.
|
108
|
+
self.thread_pool_size = thread_pool_size
|
109
|
+
self.dry_run = dry_run
|
54
110
|
|
55
111
|
@abstractmethod
|
56
112
|
def _populate_secret_data(self, specs: Iterable[ExternalResourceSpec]) -> None:
|
57
113
|
raise NotImplementedError()
|
58
114
|
|
59
|
-
def
|
115
|
+
def _annotate(self, spec: ExternalResourceSpec) -> None:
|
60
116
|
try:
|
61
117
|
annotations = json.loads(spec.resource["annotations"])
|
62
118
|
except Exception:
|
63
119
|
annotations = {}
|
64
|
-
|
65
|
-
annotations[
|
66
|
-
annotations[
|
67
|
-
annotations[
|
68
|
-
annotations[
|
69
|
-
|
120
|
+
annotations[SECRET_ANN_PROVISION_PROVIDER] = spec.provision_provider
|
121
|
+
annotations[SECRET_ANN_PROVISIONER] = spec.provisioner_name
|
122
|
+
annotations[SECRET_ANN_PROVIDER] = spec.provider
|
123
|
+
annotations[SECRET_ANN_IDENTIFIER] = spec.identifier
|
124
|
+
annotations[SECRET_UPDATED_AT] = spec.metadata[SECRET_UPDATED_AT]
|
70
125
|
spec.resource["annotations"] = json.dumps(annotations)
|
71
126
|
|
72
|
-
def
|
127
|
+
def _specs_with_secret(
|
73
128
|
self,
|
74
|
-
ri: ResourceInventory,
|
75
129
|
specs: Iterable[ExternalResourceSpec],
|
130
|
+
) -> Iterable[ExternalResourceSpec]:
|
131
|
+
return [spec for spec in specs if spec.secret]
|
132
|
+
|
133
|
+
def _add_secret_to_ri(
|
134
|
+
self,
|
135
|
+
spec: ExternalResourceSpec,
|
76
136
|
) -> None:
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
)
|
137
|
+
self.ri.initialize_resource_type(
|
138
|
+
spec.cluster_name, spec.namespace_name, "Secret"
|
139
|
+
)
|
140
|
+
self.ri.add_desired(
|
141
|
+
spec.cluster_name,
|
142
|
+
spec.namespace_name,
|
143
|
+
"Secret",
|
144
|
+
name=spec.output_resource_name,
|
145
|
+
value=spec.build_oc_secret(
|
146
|
+
QONTRACT_INTEGRATION, QONTRACT_INTEGRATION_VERSION
|
147
|
+
).annotate(),
|
148
|
+
)
|
90
149
|
|
91
150
|
def _init_ocmap(self, specs: Iterable[ExternalResourceSpec]) -> OCMap:
|
92
151
|
return init_oc_map_from_clusters(
|
@@ -99,54 +158,117 @@ class SecretsReconciler:
|
|
99
158
|
integration=QONTRACT_INTEGRATION,
|
100
159
|
)
|
101
160
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
161
|
+
def sync_secrets(
|
162
|
+
self, specs: Iterable[ExternalResourceSpec]
|
163
|
+
) -> list[ExternalResourceSpec]:
|
164
|
+
"""Sync outputs secrets to target clusters.
|
165
|
+
Logic:
|
166
|
+
Vault To Cluster:
|
167
|
+
If current is newer; don't apply.
|
168
|
+
If other changes; apply and Recycle Pods
|
169
|
+
Desired can not be newer than current.
|
170
|
+
External reosurce to Cluster (last reconciliation):
|
171
|
+
If updated_at annotation is the only change; Don't update
|
172
|
+
If other changes; Update Secret and Recycle Pods
|
173
|
+
Current can not be newer then Desired
|
109
174
|
|
110
|
-
|
175
|
+
:param specs: Specs that need sync the outputs secret to the target cluster
|
176
|
+
:return: specs that produced errors when syncing secrets to clusters.
|
177
|
+
"""
|
111
178
|
self._populate_secret_data(specs)
|
112
|
-
|
113
|
-
self.
|
114
|
-
ocmap = self._init_ocmap(
|
115
|
-
|
116
|
-
|
179
|
+
|
180
|
+
to_sync_specs = [spec for spec in self._specs_with_secret(specs)]
|
181
|
+
ocmap = self._init_ocmap(to_sync_specs)
|
182
|
+
|
183
|
+
for spec in to_sync_specs:
|
184
|
+
self._annotate(spec)
|
185
|
+
self._add_secret_to_ri(spec)
|
186
|
+
|
187
|
+
threaded.run(
|
188
|
+
self.reconcile_data,
|
189
|
+
self.ri,
|
190
|
+
thread_pool_size=self.thread_pool_size,
|
191
|
+
ocmap=ocmap,
|
192
|
+
)
|
193
|
+
|
194
|
+
if self.ri.has_error_registered():
|
195
|
+
# Return all specs as error if there are errors.
|
196
|
+
# There is no a clear way to kwno which specs failed.
|
197
|
+
return list(specs)
|
198
|
+
else:
|
199
|
+
return []
|
117
200
|
|
118
201
|
def reconcile_data(
|
119
202
|
self,
|
120
203
|
ri_item: tuple[str, str, str, Mapping[str, Any]],
|
121
|
-
ri: ResourceInventory,
|
122
204
|
ocmap: OCMap,
|
123
205
|
) -> None:
|
124
206
|
cluster, namespace, kind, data = ri_item
|
125
207
|
oc = ocmap.get_cluster(cluster)
|
126
208
|
names = list(data["desired"].keys())
|
127
209
|
|
210
|
+
logging.debug(
|
211
|
+
"Getting Secrets from cluster/namespace %s/%s", cluster, namespace
|
212
|
+
)
|
128
213
|
items = oc.get_items("Secret", namespace=namespace, resource_names=names)
|
214
|
+
|
129
215
|
for item in items:
|
130
|
-
|
216
|
+
current = OpenshiftResource(
|
131
217
|
body=item,
|
132
218
|
integration=QONTRACT_INTEGRATION,
|
133
219
|
integration_version=QONTRACT_INTEGRATION_VERSION,
|
134
220
|
)
|
135
|
-
|
221
|
+
|
222
|
+
self.ri.add_current(
|
223
|
+
cluster, namespace, kind, name=current.name, value=current
|
224
|
+
)
|
136
225
|
|
137
226
|
diff = diff_mappings(
|
138
|
-
data["current"], data["desired"], equal=
|
227
|
+
data["current"], data["desired"], equal=SecretHelper.compare
|
139
228
|
)
|
140
|
-
|
229
|
+
|
230
|
+
items_to_update = [item.desired for item in diff.change.values()] + list(
|
141
231
|
diff.add.values()
|
142
232
|
)
|
143
|
-
|
233
|
+
|
234
|
+
self.apply_action(ocmap, cluster, namespace, items_to_update)
|
144
235
|
|
145
236
|
def apply_action(
|
146
|
-
self,
|
237
|
+
self,
|
238
|
+
ocmap: OCMap,
|
239
|
+
cluster: str,
|
240
|
+
namespace: str,
|
241
|
+
items: Iterable[OpenshiftResource],
|
147
242
|
) -> None:
|
148
|
-
|
149
|
-
|
243
|
+
options = ApplyOptions(
|
244
|
+
dry_run=self.dry_run,
|
245
|
+
no_dry_run_skip_compare=False,
|
246
|
+
wait_for_namespace=False,
|
247
|
+
recycle_pods=True,
|
248
|
+
take_over=False,
|
249
|
+
override_enable_deletion=False,
|
250
|
+
caller=None,
|
251
|
+
all_callers=None,
|
252
|
+
privileged=None,
|
253
|
+
enable_deletion=None,
|
254
|
+
)
|
255
|
+
for item in items:
|
256
|
+
logging.debug(
|
257
|
+
"Updating Secret Cluster: %s, Namespace: %s, Secret: %s",
|
258
|
+
cluster,
|
259
|
+
namespace,
|
260
|
+
item.name,
|
261
|
+
)
|
262
|
+
|
263
|
+
apply_action(
|
264
|
+
ocmap,
|
265
|
+
self.ri,
|
266
|
+
cluster,
|
267
|
+
namespace,
|
268
|
+
"Secret",
|
269
|
+
item,
|
270
|
+
options=options,
|
271
|
+
)
|
150
272
|
|
151
273
|
|
152
274
|
class InClusterSecretsReconciler(SecretsReconciler):
|
@@ -159,56 +281,105 @@ class InClusterSecretsReconciler(SecretsReconciler):
|
|
159
281
|
cluster: str,
|
160
282
|
namespace: str,
|
161
283
|
oc: OCCli,
|
284
|
+
thread_pool_size: int,
|
285
|
+
dry_run: bool,
|
162
286
|
):
|
163
|
-
super().__init__(ri, secrets_reader,
|
287
|
+
super().__init__(ri, secrets_reader, thread_pool_size, dry_run)
|
164
288
|
|
165
289
|
self.cluster = cluster
|
166
290
|
self.namespace = namespace
|
167
291
|
self.oc = oc
|
168
292
|
self.source_secrets: list[str] = []
|
293
|
+
self.vault_client = vault_client
|
294
|
+
self.vault_path = vault_path
|
169
295
|
|
170
296
|
def _get_spec_hash(self, spec: ExternalResourceSpec) -> str:
|
171
297
|
secret_key = f"{spec.provision_provider}-{spec.provisioner_name}-{spec.provider}-{spec.identifier}"
|
172
298
|
return shake_128(secret_key.encode("utf-8")).hexdigest(16)
|
173
299
|
|
300
|
+
def _get_spec_outputs_secret_name(self, spec: ExternalResourceSpec) -> str:
|
301
|
+
return "external-resources-output-" + self._get_spec_hash(spec)
|
302
|
+
|
174
303
|
def _populate_secret_data(self, specs: Iterable[ExternalResourceSpec]) -> None:
|
175
304
|
if not specs:
|
176
305
|
return
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
}
|
306
|
+
|
307
|
+
secrets_map = {self._get_spec_outputs_secret_name(spec): spec for spec in specs}
|
308
|
+
|
181
309
|
secrets = self.oc.get_items(
|
182
310
|
"Secret", namespace=self.namespace, resource_names=list(secrets_map.keys())
|
183
311
|
)
|
312
|
+
|
184
313
|
for secret in secrets:
|
185
314
|
secret_name = secret["metadata"]["name"]
|
186
315
|
spec = secrets_map[secret_name]
|
187
316
|
data = dict[str, str]()
|
188
317
|
for k, v in secret["data"].items():
|
189
318
|
decoded = base64.b64decode(v).decode("utf-8")
|
319
|
+
|
190
320
|
if decoded.startswith("__vault__:"):
|
191
321
|
_secret_ref = json.loads(decoded.replace("__vault__:", ""))
|
192
322
|
secret_ref = VaultSecret(**_secret_ref)
|
193
323
|
data[k] = self.secrets_reader.read_secret(secret_ref)
|
194
324
|
else:
|
195
325
|
data[k] = decoded
|
326
|
+
|
327
|
+
spec.metadata[SECRET_UPDATED_AT] = datetime.now(timezone.utc).strftime(
|
328
|
+
SECRET_UPDATED_AT_TIMEFORMAT
|
329
|
+
)
|
196
330
|
spec.secret = data
|
197
331
|
|
198
|
-
|
332
|
+
def _delete_source_secret(self, spec: ExternalResourceSpec) -> None:
|
333
|
+
secret_name = self._get_spec_outputs_secret_name(spec)
|
334
|
+
logging.debug("Deleting secret " + secret_name)
|
335
|
+
self.oc.delete(namespace=self.namespace, kind="Secret", name=secret_name)
|
336
|
+
|
337
|
+
def _write_secret_to_vault(self, spec: ExternalResourceSpec) -> None:
|
338
|
+
secret_path = f"{self.vault_path}/{spec.cluster_name}/{spec.namespace_name}/{spec.identifier}"
|
339
|
+
secret = {k: str(v) for k, v in spec.secret.items()}
|
340
|
+
secret[SECRET_UPDATED_AT] = spec.metadata[SECRET_UPDATED_AT]
|
341
|
+
desired_secret = {"path": secret_path, "data": secret}
|
342
|
+
self.vault_client.write(desired_secret, decode_base64=False) # type: ignore[attr-defined]
|
199
343
|
|
200
|
-
def
|
201
|
-
|
202
|
-
|
203
|
-
|
344
|
+
def sync_secrets(
|
345
|
+
self, specs: Iterable[ExternalResourceSpec]
|
346
|
+
) -> list[ExternalResourceSpec]:
|
347
|
+
try:
|
348
|
+
specs_with_error = super().sync_secrets(specs)
|
349
|
+
except Exception as e:
|
350
|
+
# There is no an easy way to map which secrets have not been reconciled with the specs. If the sync
|
351
|
+
# fails at this stage all the involved specs will be retried in the next iteration
|
352
|
+
logging.error(
|
353
|
+
"Error syncing Secrets to clusters. "
|
354
|
+
"All specs reconciled in this iteration are marked as pending secret synchronization\n%s",
|
355
|
+
e,
|
356
|
+
)
|
357
|
+
return list(specs)
|
358
|
+
|
359
|
+
for spec in self._specs_with_secret(specs):
|
360
|
+
try:
|
361
|
+
self._write_secret_to_vault(spec)
|
362
|
+
self._delete_source_secret(spec)
|
363
|
+
except Exception as e:
|
364
|
+
key = ExternalResourceKey.from_spec(spec)
|
365
|
+
logging.error(
|
366
|
+
"Error writing Secret to Vault or deleting the source secret: Key: %s, Secret: %s\n%s",
|
367
|
+
key,
|
368
|
+
self._get_spec_outputs_secret_name(spec),
|
369
|
+
e,
|
370
|
+
)
|
371
|
+
specs_with_error.append(spec)
|
204
372
|
|
205
|
-
|
206
|
-
super().sync_secrets(specs)
|
207
|
-
self._delete_source_secrets()
|
373
|
+
return specs_with_error
|
208
374
|
|
209
375
|
|
210
376
|
def build_incluster_secrets_reconciler(
|
211
|
-
cluster: str,
|
377
|
+
cluster: str,
|
378
|
+
namespace: str,
|
379
|
+
secrets_reader: SecretReaderBase,
|
380
|
+
vault_path: str,
|
381
|
+
thread_pool_size: int,
|
382
|
+
dry_run: bool,
|
212
383
|
) -> InClusterSecretsReconciler:
|
213
384
|
ri = ResourceInventory()
|
214
385
|
ocmap = init_oc_map_from_clusters(
|
@@ -217,13 +388,42 @@ def build_incluster_secrets_reconciler(
|
|
217
388
|
integration=QONTRACT_INTEGRATION,
|
218
389
|
)
|
219
390
|
oc = ocmap.get_cluster(cluster)
|
220
|
-
vault_client = VaultClient()
|
221
391
|
return InClusterSecretsReconciler(
|
222
392
|
cluster=cluster,
|
223
393
|
namespace=namespace,
|
224
394
|
ri=ri,
|
225
395
|
oc=oc,
|
226
396
|
vault_path=vault_path,
|
227
|
-
vault_client=
|
397
|
+
vault_client=VaultClient(),
|
228
398
|
secrets_reader=secrets_reader,
|
399
|
+
thread_pool_size=thread_pool_size,
|
400
|
+
dry_run=dry_run,
|
229
401
|
)
|
402
|
+
|
403
|
+
|
404
|
+
class VaultSecretsReconciler(SecretsReconciler):
|
405
|
+
def __init__(
|
406
|
+
self,
|
407
|
+
ri: ResourceInventory,
|
408
|
+
secrets_reader: SecretReaderBase,
|
409
|
+
vault_path: str,
|
410
|
+
thread_pool_size: int,
|
411
|
+
dry_run: bool,
|
412
|
+
):
|
413
|
+
super().__init__(ri, secrets_reader, thread_pool_size, dry_run)
|
414
|
+
self.secrets_reader = secrets_reader
|
415
|
+
self.vault_path = vault_path
|
416
|
+
|
417
|
+
def _populate_secret_data(self, specs: Iterable[ExternalResourceSpec]) -> None:
|
418
|
+
threaded.run(self._read_secret, specs, self.thread_pool_size)
|
419
|
+
|
420
|
+
def _read_secret(self, spec: ExternalResourceSpec) -> None:
|
421
|
+
secret_path = f"{self.vault_path}/{spec.cluster_name}/{spec.namespace_name}/{spec.identifier}"
|
422
|
+
try:
|
423
|
+
logging.debug("Reading Secret %s", secret_path)
|
424
|
+
data = self.secrets_reader.read_all({"path": secret_path})
|
425
|
+
spec.metadata[SECRET_UPDATED_AT] = data[SECRET_UPDATED_AT]
|
426
|
+
del data[SECRET_UPDATED_AT]
|
427
|
+
spec.secret = data
|
428
|
+
except SecretNotFound:
|
429
|
+
logging.info("Error getting secret from vault, skipping. [%s]", secret_path)
|
@@ -35,6 +35,7 @@ class ResourceStatus(str, Enum):
|
|
35
35
|
IN_PROGRESS: str = "IN_PROGRESS"
|
36
36
|
DELETE_IN_PROGRESS: str = "DELETE_IN_PROGRESS"
|
37
37
|
ERROR: str = "ERROR"
|
38
|
+
PENDING_SECRET_SYNC: str = "PENDING_SECRET_SYNC"
|
38
39
|
|
39
40
|
|
40
41
|
class ExternalResourceState(BaseModel):
|
@@ -236,6 +237,15 @@ class ExternalResourcesStateDynamoDB:
|
|
236
237
|
def get_all_resource_keys(self) -> set[ExternalResourceKey]:
|
237
238
|
return {k for k in self.partial_resources.keys()}
|
238
239
|
|
240
|
+
def get_keys_by_status(
|
241
|
+
self, resource_status: ResourceStatus
|
242
|
+
) -> set[ExternalResourceKey]:
|
243
|
+
return {
|
244
|
+
k
|
245
|
+
for k, v in self.partial_resources.items()
|
246
|
+
if v.resource_status == resource_status
|
247
|
+
}
|
248
|
+
|
239
249
|
def update_resource_status(
|
240
250
|
self, key: ExternalResourceKey, status: ResourceStatus
|
241
251
|
) -> None:
|
reconcile/gitlab_permissions.py
CHANGED
@@ -77,9 +77,11 @@ def share_project_with_group_members(
|
|
77
77
|
|
78
78
|
|
79
79
|
def share_project_with_group(gl: GitLabApi, repos: list[str], dry_run: bool) -> None:
|
80
|
+
# get repos not owned by app-sre
|
81
|
+
non_app_sre_projects = {repo for repo in repos if "/app-sre/" not in repo}
|
80
82
|
group_id, shared_projects = gl.get_group_id_and_shared_projects(APP_SRE_GROUP_NAME)
|
81
83
|
shared_project_repos = {project["web_url"] for project in shared_projects}
|
82
|
-
repos_to_share =
|
84
|
+
repos_to_share = non_app_sre_projects - shared_project_repos
|
83
85
|
for repo in repos_to_share:
|
84
86
|
gl.share_project_with_group(repo_url=repo, group_id=group_id, dry_run=dry_run)
|
85
87
|
|
@@ -28,6 +28,7 @@ query ExternalResourcesModules {
|
|
28
28
|
default_version
|
29
29
|
reconcile_drift_interval_minutes
|
30
30
|
reconcile_timeout_minutes
|
31
|
+
outputs_secret_sync
|
31
32
|
}
|
32
33
|
}
|
33
34
|
"""
|
@@ -47,6 +48,7 @@ class ExternalResourcesModuleV1(ConfiguredBaseModel):
|
|
47
48
|
default_version: str = Field(..., alias="default_version")
|
48
49
|
reconcile_drift_interval_minutes: str = Field(..., alias="reconcile_drift_interval_minutes")
|
49
50
|
reconcile_timeout_minutes: str = Field(..., alias="reconcile_timeout_minutes")
|
51
|
+
outputs_secret_sync: bool = Field(..., alias="outputs_secret_sync")
|
50
52
|
|
51
53
|
|
52
54
|
class ExternalResourcesModulesQueryData(ConfiguredBaseModel):
|
@@ -35,6 +35,7 @@ query ExternalResourcesSettings {
|
|
35
35
|
tf_state_bucket
|
36
36
|
tf_state_region
|
37
37
|
tf_state_dynamodb_table
|
38
|
+
vault_secrets_path
|
38
39
|
}
|
39
40
|
}
|
40
41
|
"""
|
@@ -67,6 +68,7 @@ class ExternalResourcesSettingsV1(ConfiguredBaseModel):
|
|
67
68
|
tf_state_bucket: Optional[str] = Field(..., alias="tf_state_bucket")
|
68
69
|
tf_state_region: Optional[str] = Field(..., alias="tf_state_region")
|
69
70
|
tf_state_dynamodb_table: Optional[str] = Field(..., alias="tf_state_dynamodb_table")
|
71
|
+
vault_secrets_path: str = Field(..., alias="vault_secrets_path")
|
70
72
|
|
71
73
|
|
72
74
|
class ExternalResourcesSettingsQueryData(ConfiguredBaseModel):
|
@@ -87,6 +87,11 @@ class ExternalResourceSpec:
|
|
87
87
|
resource: MutableMapping[str, Any]
|
88
88
|
namespace: Mapping[str, Any]
|
89
89
|
secret: Mapping[str, str] = field(init=False, default_factory=lambda: {})
|
90
|
+
# Metadata is used for processing data that shuold not be included in the secret data
|
91
|
+
# e.g: ERV2 adds a updated_at attribute that acts as optimistic lock.
|
92
|
+
metadata: MutableMapping[str, str] = field(
|
93
|
+
init=False, compare=False, repr=False, hash=False, default_factory=lambda: {}
|
94
|
+
)
|
90
95
|
|
91
96
|
@property
|
92
97
|
def provider(self) -> str:
|
@@ -14,6 +14,7 @@ from typing import (
|
|
14
14
|
import semver
|
15
15
|
from pydantic import BaseModel
|
16
16
|
|
17
|
+
from reconcile.external_resources.meta import SECRET_UPDATED_AT
|
17
18
|
from reconcile.utils.metrics import GaugeMetric
|
18
19
|
|
19
20
|
SECRET_MAX_KEY_LENGTH = 253
|
@@ -526,6 +527,9 @@ class OpenshiftResource:
|
|
526
527
|
# remove qontract specific params
|
527
528
|
for a in QONTRACT_ANNOTATIONS:
|
528
529
|
annotations.pop(a, None)
|
530
|
+
|
531
|
+
# Remove external resources annotation used for optimistic locking
|
532
|
+
annotations.pop(SECRET_UPDATED_AT, None)
|
529
533
|
return body
|
530
534
|
|
531
535
|
@staticmethod
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc863.dist-info → qontract_reconcile-0.10.1rc865.dist-info}/top_level.txt
RENAMED
File without changes
|