qontract-reconcile 0.10.1rc763__py3-none-any.whl → 0.10.1rc765__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.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/RECORD +27 -31
- reconcile/external_resources/aws.py +85 -0
- reconcile/external_resources/factories.py +133 -0
- reconcile/external_resources/integration.py +95 -0
- reconcile/external_resources/manager.py +350 -0
- reconcile/external_resources/meta.py +4 -0
- reconcile/external_resources/metrics.py +20 -0
- reconcile/external_resources/model.py +244 -0
- reconcile/external_resources/reconciler.py +249 -0
- reconcile/external_resources/secrets_sync.py +229 -0
- reconcile/external_resources/state.py +246 -0
- reconcile/saas_auto_promotions_manager/meta.py +1 -1
- reconcile/saas_auto_promotions_manager/subscriber.py +52 -2
- reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py +4 -0
- reconcile/test/saas_auto_promotions_manager/conftest.py +63 -0
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py +0 -37
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_desired_state.py +20 -14
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/conftest.py +0 -43
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_multiple_namespaces.py +4 -11
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_single_namespace.py +12 -19
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_content_single_target.py +6 -12
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/test_json_path_selector.py +8 -15
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/data_keys.py +0 -4
- reconcile/test/saas_auto_promotions_manager/subscriber/conftest.py +0 -89
- reconcile/test/saas_auto_promotions_manager/subscriber/data_keys.py +0 -11
- reconcile/test/saas_auto_promotions_manager/subscriber/test_content_hash.py +0 -130
- reconcile/test/saas_auto_promotions_manager/subscriber/test_diff.py +0 -161
- reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_channels_config_hash.py +0 -218
- reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_channels_moving_ref.py +0 -216
- reconcile/test/saas_auto_promotions_manager/subscriber/test_multiple_publishers_moving_ref.py +0 -129
- reconcile/test/saas_auto_promotions_manager/subscriber/test_single_channel_with_single_publisher.py +0 -330
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/__init__.py +0 -0
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_multiple_publishers_for_single_channel.py +0 -68
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_use_target_config_hash.py +0 -62
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_with_auto_promote.py +0 -73
- reconcile/test/saas_auto_promotions_manager/utils/saas_files_inventory/test_saas_files_without_auto_promote.py +0 -64
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc763.dist-info → qontract_reconcile-0.10.1rc765.dist-info}/top_level.txt +0 -0
- /reconcile/{test/saas_auto_promotions_manager/subscriber → external_resources}/__init__.py +0 -0
@@ -0,0 +1,350 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
from collections.abc import Iterable
|
4
|
+
from datetime import datetime, timezone
|
5
|
+
from enum import Enum
|
6
|
+
|
7
|
+
from sretoolbox.utils import threaded
|
8
|
+
|
9
|
+
from reconcile.external_resources.factories import (
|
10
|
+
AWSExternalResourceFactory,
|
11
|
+
ExternalResourceFactory,
|
12
|
+
ModuleProvisionDataFactory,
|
13
|
+
ObjectFactory,
|
14
|
+
TerraformModuleProvisionDataFactory,
|
15
|
+
setup_aws_resource_factories,
|
16
|
+
)
|
17
|
+
from reconcile.external_resources.metrics import ExternalResourcesReconcileErrorsGauge
|
18
|
+
from reconcile.external_resources.model import (
|
19
|
+
Action,
|
20
|
+
ExternalResource,
|
21
|
+
ExternalResourceKey,
|
22
|
+
ExternalResourceModuleConfiguration,
|
23
|
+
ExternalResourcesInventory,
|
24
|
+
ExternalResourceValidationError,
|
25
|
+
ModuleInventory,
|
26
|
+
Reconciliation,
|
27
|
+
)
|
28
|
+
from reconcile.external_resources.reconciler import ExternalResourcesReconciler
|
29
|
+
from reconcile.external_resources.secrets_sync import InClusterSecretsReconciler
|
30
|
+
from reconcile.external_resources.state import (
|
31
|
+
ExternalResourcesStateDynamoDB,
|
32
|
+
ExternalResourceState,
|
33
|
+
ReconcileStatus,
|
34
|
+
ResourceStatus,
|
35
|
+
)
|
36
|
+
from reconcile.gql_definitions.external_resources.external_resources_settings import (
|
37
|
+
ExternalResourcesSettingsV1,
|
38
|
+
)
|
39
|
+
from reconcile.utils import metrics
|
40
|
+
from reconcile.utils.external_resource_spec import (
|
41
|
+
ExternalResourceSpec,
|
42
|
+
)
|
43
|
+
from reconcile.utils.secret_reader import SecretReaderBase
|
44
|
+
|
45
|
+
FLAG_RESOURCE_MANAGED_BY_ERV2 = "managed_by_erv2"
|
46
|
+
|
47
|
+
|
48
|
+
def setup_factories(
|
49
|
+
settings: ExternalResourcesSettingsV1,
|
50
|
+
module_inventory: ModuleInventory,
|
51
|
+
er_inventory: ExternalResourcesInventory,
|
52
|
+
secret_reader: SecretReaderBase,
|
53
|
+
) -> ObjectFactory[ExternalResourceFactory]:
|
54
|
+
tf_factory = TerraformModuleProvisionDataFactory(settings=settings)
|
55
|
+
|
56
|
+
aws_provision_factories = ObjectFactory[ModuleProvisionDataFactory]()
|
57
|
+
aws_provision_factories.register_factory("terraform", tf_factory)
|
58
|
+
aws_provision_factories.register_factory("cdktf", tf_factory)
|
59
|
+
|
60
|
+
of = ObjectFactory[ExternalResourceFactory]()
|
61
|
+
of.register_factory(
|
62
|
+
"aws",
|
63
|
+
AWSExternalResourceFactory(
|
64
|
+
module_inventory=module_inventory,
|
65
|
+
er_inventory=er_inventory,
|
66
|
+
secret_reader=secret_reader,
|
67
|
+
provision_factories=aws_provision_factories,
|
68
|
+
resource_factories=setup_aws_resource_factories(
|
69
|
+
er_inventory, secret_reader
|
70
|
+
),
|
71
|
+
),
|
72
|
+
)
|
73
|
+
return of
|
74
|
+
|
75
|
+
|
76
|
+
class ReconcileAction(str, Enum):
|
77
|
+
NOOP = "NOOP"
|
78
|
+
APPLY_NOT_EXISTS = "Resource does not exist"
|
79
|
+
APPLY_ERROR = "Resource status in ERROR state"
|
80
|
+
APPLY_SPEC_CHANGED = "Resource spec has changed"
|
81
|
+
APPLY_DRIFT_DETECTION = "Resource drift detection run"
|
82
|
+
DESTROY_CREATED = "Resource no longer exists in the configuration"
|
83
|
+
DESTROY_ERROR = "Resource status in ERROR state"
|
84
|
+
|
85
|
+
|
86
|
+
class ExternalResourcesManager:
|
87
|
+
def __init__(
|
88
|
+
self,
|
89
|
+
state_manager: ExternalResourcesStateDynamoDB,
|
90
|
+
settings: ExternalResourcesSettingsV1,
|
91
|
+
module_inventory: ModuleInventory,
|
92
|
+
reconciler: ExternalResourcesReconciler,
|
93
|
+
secret_reader: SecretReaderBase,
|
94
|
+
er_inventory: ExternalResourcesInventory,
|
95
|
+
factories: ObjectFactory[ExternalResourceFactory],
|
96
|
+
secrets_reconciler: InClusterSecretsReconciler,
|
97
|
+
thread_pool_size: int,
|
98
|
+
) -> None:
|
99
|
+
self.state_mgr = state_manager
|
100
|
+
self.settings = settings
|
101
|
+
self.module_inventory = module_inventory
|
102
|
+
self.reconciler = reconciler
|
103
|
+
self.er_inventory = er_inventory
|
104
|
+
self.factories = factories
|
105
|
+
self.secret_reader = secret_reader
|
106
|
+
self.secrets_reconciler = secrets_reconciler
|
107
|
+
self.errors: dict[ExternalResourceKey, ExternalResourceValidationError] = {}
|
108
|
+
self.thread_pool_size = thread_pool_size
|
109
|
+
|
110
|
+
def _get_reconcile_action(
|
111
|
+
self, reconciliation: Reconciliation, state: ExternalResourceState
|
112
|
+
) -> ReconcileAction:
|
113
|
+
if reconciliation.action == Action.APPLY:
|
114
|
+
match state.resource_status:
|
115
|
+
case ResourceStatus.NOT_EXISTS:
|
116
|
+
return ReconcileAction.APPLY_NOT_EXISTS
|
117
|
+
case ResourceStatus.ERROR:
|
118
|
+
return ReconcileAction.APPLY_ERROR
|
119
|
+
case ResourceStatus.CREATED:
|
120
|
+
if (
|
121
|
+
reconciliation.resource_hash
|
122
|
+
!= state.reconciliation.resource_hash
|
123
|
+
):
|
124
|
+
return ReconcileAction.APPLY_SPEC_CHANGED
|
125
|
+
elif (
|
126
|
+
(datetime.now(state.ts.tzinfo) - state.ts).total_seconds()
|
127
|
+
> reconciliation.module_configuration.reconcile_drift_interval_minutes
|
128
|
+
* 60
|
129
|
+
):
|
130
|
+
return ReconcileAction.APPLY_DRIFT_DETECTION
|
131
|
+
elif reconciliation.action == Action.DESTROY:
|
132
|
+
match state.resource_status:
|
133
|
+
case ResourceStatus.CREATED:
|
134
|
+
return ReconcileAction.DESTROY_CREATED
|
135
|
+
case ResourceStatus.ERROR:
|
136
|
+
return ReconcileAction.DESTROY_ERROR
|
137
|
+
|
138
|
+
return ReconcileAction.NOOP
|
139
|
+
|
140
|
+
def _resource_needs_reconciliation(
|
141
|
+
self,
|
142
|
+
reconciliation: Reconciliation,
|
143
|
+
state: ExternalResourceState,
|
144
|
+
) -> bool:
|
145
|
+
reconcile_action = self._get_reconcile_action(reconciliation, state)
|
146
|
+
reconcile = reconcile_action != ReconcileAction.NOOP
|
147
|
+
if reconcile:
|
148
|
+
logging.info(
|
149
|
+
"Reconciling: Status: [%s], Action: [%s], reason: [%s], key:[%s]",
|
150
|
+
state.resource_status.value,
|
151
|
+
reconciliation.action.value,
|
152
|
+
reconcile_action.value,
|
153
|
+
reconciliation.key,
|
154
|
+
)
|
155
|
+
return reconcile
|
156
|
+
|
157
|
+
def _get_desired_objects_reconciliations(self) -> set[Reconciliation]:
|
158
|
+
r: set[Reconciliation] = set()
|
159
|
+
for key, spec in self.er_inventory.items():
|
160
|
+
module = self.module_inventory.get_from_spec(spec)
|
161
|
+
|
162
|
+
try:
|
163
|
+
resource = self._build_external_resource(spec, self.er_inventory)
|
164
|
+
except ExternalResourceValidationError as e:
|
165
|
+
k = ExternalResourceKey.from_spec(spec)
|
166
|
+
self.errors[k] = e
|
167
|
+
continue
|
168
|
+
|
169
|
+
reconciliation = Reconciliation(
|
170
|
+
key=key,
|
171
|
+
resource_hash=resource.hash(),
|
172
|
+
input=self._serialize_resource_input(resource),
|
173
|
+
action=Action.APPLY,
|
174
|
+
module_configuration=ExternalResourceModuleConfiguration.resolve_configuration(
|
175
|
+
module, spec
|
176
|
+
),
|
177
|
+
)
|
178
|
+
r.add(reconciliation)
|
179
|
+
return r
|
180
|
+
|
181
|
+
def _get_deleted_objects_reconciliations(self) -> set[Reconciliation]:
|
182
|
+
desired_keys = set(self.er_inventory.keys())
|
183
|
+
state_resource_keys = self.state_mgr.get_all_resource_keys()
|
184
|
+
deleted_keys = state_resource_keys - desired_keys
|
185
|
+
r: set[Reconciliation] = set()
|
186
|
+
for key in deleted_keys:
|
187
|
+
state = self.state_mgr.get_external_resource_state(key)
|
188
|
+
reconciliation = Reconciliation(
|
189
|
+
key=key,
|
190
|
+
resource_hash=state.reconciliation.resource_hash,
|
191
|
+
module_configuration=state.reconciliation.module_configuration,
|
192
|
+
input=state.reconciliation.input,
|
193
|
+
action=Action.DESTROY,
|
194
|
+
)
|
195
|
+
r.add(reconciliation)
|
196
|
+
return r
|
197
|
+
|
198
|
+
def _update_in_progress_state(
|
199
|
+
self, r: Reconciliation, state: ExternalResourceState
|
200
|
+
) -> bool:
|
201
|
+
"""Gets the resource reconciliation state from the Job and updates the
|
202
|
+
Resource state accordingly. It also returns if the target outputs secret needs
|
203
|
+
to be reconciled.
|
204
|
+
|
205
|
+
:param r: Reconciliation object
|
206
|
+
:param state: State object
|
207
|
+
:return: True/False if target Secret needs to be reconciled.
|
208
|
+
"""
|
209
|
+
need_secret_sync = False
|
210
|
+
|
211
|
+
if state.resource_status not in set([
|
212
|
+
ResourceStatus.DELETE_IN_PROGRESS,
|
213
|
+
ResourceStatus.IN_PROGRESS,
|
214
|
+
]):
|
215
|
+
return need_secret_sync
|
216
|
+
|
217
|
+
logging.info(
|
218
|
+
"Reconciliation In progress. Action: %s, Key:%s",
|
219
|
+
state.reconciliation.action,
|
220
|
+
state.reconciliation.key,
|
221
|
+
)
|
222
|
+
|
223
|
+
# Need to check the reconciliation set in the state, not the desired one
|
224
|
+
# as the reconciliation object might be from a previous desired state
|
225
|
+
error = False
|
226
|
+
match self.reconciler.get_resource_reconcile_status(state.reconciliation):
|
227
|
+
case ReconcileStatus.SUCCESS:
|
228
|
+
logging.info(
|
229
|
+
"Reconciliation ended SUCCESSFULLY. Action: %s, key:%s",
|
230
|
+
r.action.value,
|
231
|
+
r.key,
|
232
|
+
)
|
233
|
+
if r.action == Action.APPLY:
|
234
|
+
state.resource_status = ResourceStatus.CREATED
|
235
|
+
state.reconciliation_errors = 0
|
236
|
+
self.state_mgr.set_external_resource_state(state)
|
237
|
+
need_secret_sync = True
|
238
|
+
elif r.action == Action.DESTROY:
|
239
|
+
state.resource_status = ResourceStatus.DELETED
|
240
|
+
self.state_mgr.del_external_resource_state(r.key)
|
241
|
+
case ReconcileStatus.ERROR:
|
242
|
+
logging.info(
|
243
|
+
"Reconciliation ended with ERROR: Action:%s, Key:%s",
|
244
|
+
r.action.value,
|
245
|
+
r.key,
|
246
|
+
)
|
247
|
+
error = True
|
248
|
+
case ReconcileStatus.NOT_EXISTS:
|
249
|
+
logging.info(
|
250
|
+
"Reconciliation should exist but it doesn't. Marking as ERROR to retrigger: Action:%s, Key:%s",
|
251
|
+
r.action.value,
|
252
|
+
r.key,
|
253
|
+
)
|
254
|
+
error = True
|
255
|
+
if error:
|
256
|
+
state.resource_status = ResourceStatus.ERROR
|
257
|
+
state.reconciliation_errors += 1
|
258
|
+
self.state_mgr.set_external_resource_state(state)
|
259
|
+
|
260
|
+
return need_secret_sync
|
261
|
+
|
262
|
+
def _update_state(self, r: Reconciliation, state: ExternalResourceState) -> None:
|
263
|
+
state.ts = datetime.now(timezone.utc)
|
264
|
+
if r.action == Action.APPLY:
|
265
|
+
state.resource_status = ResourceStatus.IN_PROGRESS
|
266
|
+
elif r.action == Action.DESTROY:
|
267
|
+
state.resource_status = ResourceStatus.DELETE_IN_PROGRESS
|
268
|
+
state.reconciliation = r
|
269
|
+
self.state_mgr.set_external_resource_state(state)
|
270
|
+
|
271
|
+
def _need_secret_sync(
|
272
|
+
self, r: Reconciliation, state: ExternalResourceState
|
273
|
+
) -> bool:
|
274
|
+
return (
|
275
|
+
r.action == Action.APPLY and state.resource_status == ResourceStatus.CREATED
|
276
|
+
)
|
277
|
+
|
278
|
+
def _sync_secrets(self, keys: Iterable[ExternalResourceKey]) -> None:
|
279
|
+
specs = [spec for key in keys if (spec := self.er_inventory.get(key))]
|
280
|
+
self.secrets_reconciler.sync_secrets(specs=specs)
|
281
|
+
|
282
|
+
def _build_external_resource(
|
283
|
+
self, spec: ExternalResourceSpec, er_inventory: ExternalResourcesInventory
|
284
|
+
) -> ExternalResource:
|
285
|
+
f = self.factories.get_factory(spec.provision_provider)
|
286
|
+
resource = f.create_external_resource(spec)
|
287
|
+
f.validate_external_resource(resource)
|
288
|
+
return resource
|
289
|
+
|
290
|
+
def _serialize_resource_input(self, resource: ExternalResource) -> str:
|
291
|
+
return json.dumps(
|
292
|
+
resource.dict(exclude={"data": {FLAG_RESOURCE_MANAGED_BY_ERV2}})
|
293
|
+
)
|
294
|
+
|
295
|
+
def handle_resources(self) -> None:
|
296
|
+
desired_r = self._get_desired_objects_reconciliations()
|
297
|
+
deleted_r = self._get_deleted_objects_reconciliations()
|
298
|
+
to_sync: set[ExternalResourceKey] = set()
|
299
|
+
for r in desired_r.union(deleted_r):
|
300
|
+
state = self.state_mgr.get_external_resource_state(r.key)
|
301
|
+
need_sync = self._update_in_progress_state(r, state)
|
302
|
+
if need_sync:
|
303
|
+
to_sync.add(r.key)
|
304
|
+
|
305
|
+
metrics.set_gauge(
|
306
|
+
ExternalResourcesReconcileErrorsGauge(
|
307
|
+
provision_provider=r.key.provision_provider,
|
308
|
+
provisioner_name=r.key.provisioner_name,
|
309
|
+
provider=r.key.provider,
|
310
|
+
identifier=r.key.identifier,
|
311
|
+
),
|
312
|
+
float(state.reconciliation_errors),
|
313
|
+
)
|
314
|
+
|
315
|
+
if self._resource_needs_reconciliation(reconciliation=r, state=state):
|
316
|
+
self.reconciler.reconcile_resource(reconciliation=r)
|
317
|
+
self._update_state(r, state)
|
318
|
+
|
319
|
+
if to_sync:
|
320
|
+
self._sync_secrets(keys=to_sync)
|
321
|
+
|
322
|
+
def handle_dry_run_resources(self) -> None:
|
323
|
+
desired_r = self._get_desired_objects_reconciliations()
|
324
|
+
deleted_r = self._get_deleted_objects_reconciliations()
|
325
|
+
reconciliations = desired_r.union(deleted_r)
|
326
|
+
triggered = set[Reconciliation]()
|
327
|
+
|
328
|
+
for r in reconciliations:
|
329
|
+
state = self.state_mgr.get_external_resource_state(key=r.key)
|
330
|
+
if (
|
331
|
+
r.action == Action.APPLY
|
332
|
+
and state.reconciliation.resource_hash != r.resource_hash
|
333
|
+
) or r.action == Action.DESTROY:
|
334
|
+
triggered.add(r)
|
335
|
+
|
336
|
+
threaded.run(
|
337
|
+
self.reconciler.reconcile_resource,
|
338
|
+
triggered,
|
339
|
+
thread_pool_size=self.thread_pool_size,
|
340
|
+
)
|
341
|
+
|
342
|
+
results = self.reconciler.wait_for_reconcile_list_completion(
|
343
|
+
triggered, check_interval_seconds=10, timeout_seconds=-1
|
344
|
+
)
|
345
|
+
|
346
|
+
for r in triggered:
|
347
|
+
self.reconciler.get_resource_reconcile_logs(reconciliation=r)
|
348
|
+
|
349
|
+
if ReconcileStatus.ERROR in [rs for rs in results.values()]:
|
350
|
+
raise Exception("Some Resources have reconciliation errors.")
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
from reconcile.utils.metrics import (
|
4
|
+
GaugeMetric,
|
5
|
+
)
|
6
|
+
|
7
|
+
|
8
|
+
class ExternalResourcesBaseMetric(BaseModel):
|
9
|
+
integration = "external_resources"
|
10
|
+
|
11
|
+
|
12
|
+
class ExternalResourcesReconcileErrorsGauge(ExternalResourcesBaseMetric, GaugeMetric):
|
13
|
+
provision_provider: str
|
14
|
+
provisioner_name: str
|
15
|
+
provider: str
|
16
|
+
identifier: str
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def name(cls) -> str:
|
20
|
+
return "external_resources_reconcile_status"
|
@@ -0,0 +1,244 @@
|
|
1
|
+
import base64
|
2
|
+
import hashlib
|
3
|
+
import json
|
4
|
+
from abc import (
|
5
|
+
ABC,
|
6
|
+
)
|
7
|
+
from collections.abc import (
|
8
|
+
Iterable,
|
9
|
+
Iterator,
|
10
|
+
MutableMapping,
|
11
|
+
)
|
12
|
+
from enum import Enum
|
13
|
+
from typing import Any
|
14
|
+
|
15
|
+
from pydantic import BaseModel
|
16
|
+
|
17
|
+
from reconcile.gql_definitions.external_resources.external_resources_modules import (
|
18
|
+
ExternalResourcesModuleV1,
|
19
|
+
)
|
20
|
+
from reconcile.gql_definitions.external_resources.external_resources_namespaces import (
|
21
|
+
NamespaceTerraformProviderResourceAWSV1,
|
22
|
+
NamespaceTerraformResourceRDSV1,
|
23
|
+
NamespaceTerraformResourceRoleV1,
|
24
|
+
NamespaceV1,
|
25
|
+
)
|
26
|
+
from reconcile.utils.exceptions import FetchResourceError
|
27
|
+
from reconcile.utils.external_resource_spec import (
|
28
|
+
ExternalResourceSpec,
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
class ExternalResourceValidationError(Exception):
|
33
|
+
errors: list[str] = []
|
34
|
+
|
35
|
+
def add_validation_error(self, msg: str) -> None:
|
36
|
+
self.errors.append(msg)
|
37
|
+
|
38
|
+
|
39
|
+
class ExternalResourceKey(BaseModel, frozen=True):
|
40
|
+
provision_provider: str
|
41
|
+
provisioner_name: str
|
42
|
+
provider: str
|
43
|
+
identifier: str
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def from_spec(spec: ExternalResourceSpec) -> "ExternalResourceKey":
|
47
|
+
return ExternalResourceKey(
|
48
|
+
provision_provider=spec.provision_provider,
|
49
|
+
provisioner_name=spec.provisioner_name,
|
50
|
+
identifier=spec.identifier,
|
51
|
+
provider=spec.provider,
|
52
|
+
)
|
53
|
+
|
54
|
+
def hash(self) -> str:
|
55
|
+
return hashlib.md5(
|
56
|
+
json.dumps(self.dict(), sort_keys=True).encode("utf-8")
|
57
|
+
).hexdigest()
|
58
|
+
|
59
|
+
@property
|
60
|
+
def state_path(self) -> str:
|
61
|
+
return f"{self.provision_provider}/{self.provisioner_name}/{self.provider}/{self.identifier}"
|
62
|
+
|
63
|
+
|
64
|
+
class ExternalResourcesInventory(MutableMapping):
|
65
|
+
_inventory: dict[ExternalResourceKey, ExternalResourceSpec] = {}
|
66
|
+
|
67
|
+
def __init__(self, namespaces: Iterable[NamespaceV1]) -> None:
|
68
|
+
desired_providers = [
|
69
|
+
(p, ns)
|
70
|
+
for ns in namespaces
|
71
|
+
for p in ns.external_resources or []
|
72
|
+
if isinstance(p, NamespaceTerraformProviderResourceAWSV1) and p.resources
|
73
|
+
]
|
74
|
+
|
75
|
+
desired_specs = [
|
76
|
+
ExternalResourceSpec(
|
77
|
+
provision_provider=p.provider,
|
78
|
+
provisioner=p.provisioner.dict(),
|
79
|
+
resource=r.dict(),
|
80
|
+
namespace=ns.dict(),
|
81
|
+
)
|
82
|
+
for (p, ns) in desired_providers
|
83
|
+
for r in p.resources
|
84
|
+
if isinstance(
|
85
|
+
r, (NamespaceTerraformResourceRDSV1, NamespaceTerraformResourceRoleV1)
|
86
|
+
)
|
87
|
+
and r.managed_by_erv2
|
88
|
+
]
|
89
|
+
|
90
|
+
for spec in desired_specs:
|
91
|
+
# self.set(ExternalResourceKey.from_spec(spec), spec)
|
92
|
+
self._inventory[ExternalResourceKey.from_spec(spec)] = spec
|
93
|
+
|
94
|
+
def __getitem__(self, key: ExternalResourceKey) -> ExternalResourceSpec | None:
|
95
|
+
return self._inventory[key]
|
96
|
+
|
97
|
+
def __setitem__(self, key: ExternalResourceKey, spec: ExternalResourceSpec) -> None:
|
98
|
+
self._inventory[key] = spec
|
99
|
+
|
100
|
+
def __delitem__(self, key: ExternalResourceKey) -> None:
|
101
|
+
del self._inventory[key]
|
102
|
+
|
103
|
+
def __iter__(self) -> Iterator[ExternalResourceKey]:
|
104
|
+
return iter(self._inventory)
|
105
|
+
|
106
|
+
def __len__(self) -> int:
|
107
|
+
return len(self._inventory)
|
108
|
+
|
109
|
+
def get_inventory_spec(
|
110
|
+
self, provision_provider: str, provisioner: str, provider: str, identifier: str
|
111
|
+
) -> ExternalResourceSpec:
|
112
|
+
"""Convinience method to find referenced specs in the inventory. For example, finding a sourcedb reference for an RDS instance."""
|
113
|
+
key = ExternalResourceKey(
|
114
|
+
provision_provider=provision_provider,
|
115
|
+
provisioner_name=provisioner,
|
116
|
+
provider=provider,
|
117
|
+
identifier=identifier,
|
118
|
+
)
|
119
|
+
try:
|
120
|
+
return self._inventory[key]
|
121
|
+
except KeyError:
|
122
|
+
msg = f"Resource spec not found: Provider {provider}, Id: {identifier}"
|
123
|
+
raise FetchResourceError(msg)
|
124
|
+
|
125
|
+
|
126
|
+
class Action(str, Enum):
|
127
|
+
DESTROY: str = "Destroy"
|
128
|
+
APPLY: str = "Apply"
|
129
|
+
|
130
|
+
|
131
|
+
class ExternalResourceModuleKey(BaseModel, frozen=True):
|
132
|
+
provision_provider: str
|
133
|
+
provider: str
|
134
|
+
|
135
|
+
|
136
|
+
class ModuleInventory:
|
137
|
+
inventory: dict[ExternalResourceModuleKey, ExternalResourcesModuleV1]
|
138
|
+
|
139
|
+
def __init__(
|
140
|
+
self, inventory: dict[ExternalResourceModuleKey, ExternalResourcesModuleV1]
|
141
|
+
):
|
142
|
+
self.inventory = inventory
|
143
|
+
|
144
|
+
def get_from_external_resource_key(
|
145
|
+
self, key: ExternalResourceKey
|
146
|
+
) -> ExternalResourcesModuleV1:
|
147
|
+
return self.inventory[
|
148
|
+
ExternalResourceModuleKey(
|
149
|
+
provision_provider=key.provision_provider, provider=key.provider
|
150
|
+
)
|
151
|
+
]
|
152
|
+
|
153
|
+
def get_from_spec(self, spec: ExternalResourceSpec) -> ExternalResourcesModuleV1:
|
154
|
+
return self.inventory[
|
155
|
+
ExternalResourceModuleKey(
|
156
|
+
provision_provider=spec.provision_provider, provider=spec.provider
|
157
|
+
)
|
158
|
+
]
|
159
|
+
|
160
|
+
|
161
|
+
def load_module_inventory(
|
162
|
+
modules: Iterable[ExternalResourcesModuleV1],
|
163
|
+
) -> ModuleInventory:
|
164
|
+
return ModuleInventory({
|
165
|
+
ExternalResourceModuleKey(
|
166
|
+
provision_provider=m.provision_provider, provider=m.provider
|
167
|
+
): m
|
168
|
+
for m in modules
|
169
|
+
})
|
170
|
+
|
171
|
+
|
172
|
+
class ExternalResourceModuleConfiguration(BaseModel, frozen=True):
|
173
|
+
image: str = ""
|
174
|
+
version: str = ""
|
175
|
+
reconcile_drift_interval_minutes: int = -1000
|
176
|
+
reconcile_timeout_minutes: int = -1000
|
177
|
+
|
178
|
+
@property
|
179
|
+
def image_version(self) -> str:
|
180
|
+
return f"{self.image}:{self.version}"
|
181
|
+
|
182
|
+
@staticmethod
|
183
|
+
def resolve_configuration(
|
184
|
+
module: ExternalResourcesModuleV1, spec: ExternalResourceSpec
|
185
|
+
) -> "ExternalResourceModuleConfiguration":
|
186
|
+
# TODO: Modify resource schemas to include this attributes
|
187
|
+
data = {
|
188
|
+
"image": module.image,
|
189
|
+
"version": module.default_version,
|
190
|
+
"reconcile_drift_interval_minutes": module.reconcile_drift_interval_minutes,
|
191
|
+
"reconcile_timeout_minutes": module.reconcile_timeout_minutes,
|
192
|
+
}
|
193
|
+
return ExternalResourceModuleConfiguration.parse_obj(data)
|
194
|
+
|
195
|
+
|
196
|
+
class Reconciliation(BaseModel, frozen=True):
|
197
|
+
key: ExternalResourceKey
|
198
|
+
resource_hash: str = ""
|
199
|
+
input: str = ""
|
200
|
+
action: Action = Action.APPLY
|
201
|
+
module_configuration: ExternalResourceModuleConfiguration = (
|
202
|
+
ExternalResourceModuleConfiguration()
|
203
|
+
)
|
204
|
+
|
205
|
+
|
206
|
+
class ModuleProvisionData(ABC, BaseModel):
|
207
|
+
pass
|
208
|
+
|
209
|
+
|
210
|
+
class TerraformModuleProvisionData(ModuleProvisionData):
|
211
|
+
"""Specific Provision Options for modules based on Terraform or CDKTF"""
|
212
|
+
|
213
|
+
tf_state_bucket: str
|
214
|
+
tf_state_region: str
|
215
|
+
tf_state_dynamodb_table: str
|
216
|
+
tf_state_key: str
|
217
|
+
|
218
|
+
|
219
|
+
class ExternalResourceProvision(BaseModel):
|
220
|
+
"""External resource app-interface attributes. They are not part of the resource but are needed
|
221
|
+
for annotating secrets or other stuff
|
222
|
+
"""
|
223
|
+
|
224
|
+
provision_provider: str
|
225
|
+
provisioner: str
|
226
|
+
provider: str
|
227
|
+
identifier: str
|
228
|
+
target_cluster: str
|
229
|
+
target_namespace: str
|
230
|
+
target_secret_name: str
|
231
|
+
module_provision_data: ModuleProvisionData
|
232
|
+
|
233
|
+
|
234
|
+
class ExternalResource(BaseModel):
|
235
|
+
data: dict[str, Any]
|
236
|
+
provision: ExternalResourceProvision
|
237
|
+
|
238
|
+
def hash(self) -> str:
|
239
|
+
return hashlib.md5(
|
240
|
+
json.dumps(self.data, sort_keys=True).encode("utf-8")
|
241
|
+
).hexdigest()
|
242
|
+
|
243
|
+
def serialize_input(self) -> str:
|
244
|
+
return base64.b64encode(json.dumps(self.dict()).encode()).decode()
|