qontract-reconcile 0.10.2.dev43__py3-none-any.whl → 0.10.2.dev45__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qontract-reconcile
3
- Version: 0.10.2.dev43
3
+ Version: 0.10.2.dev45
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Project-URL: homepage, https://github.com/app-sre/qontract-reconcile
6
6
  Project-URL: repository, https://github.com/app-sre/qontract-reconcile
@@ -195,11 +195,11 @@ reconcile/endpoints_discovery/integration.py,sha256=fzy5tv4c_6WoAZGGBNJ276NVNB1O
195
195
  reconcile/endpoints_discovery/merge_request.py,sha256=_yLb4tnvoZMCko8rta2C_CvOInJa9pa3HzSmHNtjgGU,2978
196
196
  reconcile/endpoints_discovery/merge_request_manager.py,sha256=wUMsumxv8RnWaRattax4HfoRlhtVzmgro3GiJJ1C4Vc,6392
197
197
  reconcile/external_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
198
- reconcile/external_resources/aws.py,sha256=2vimGEtcwcIyVaFvt5Ab2PAAWu2hawJyCu4fuZWSobI,7783
199
- reconcile/external_resources/factories.py,sha256=ldvm3n13IdE899Ei850NSf0RQ35rzsKJBCUkROrbE5Y,5579
198
+ reconcile/external_resources/aws.py,sha256=KAs656zj4oZYpVbgWsDLXnJWJlLOZdR1JnRhikbF4x0,10712
199
+ reconcile/external_resources/factories.py,sha256=C0QHT0soEv6z99-ELAAE19S5MaMHhV0t1fSiQn0Coc4,5970
200
200
  reconcile/external_resources/integration.py,sha256=JF38M7R0Z4ADUTx57TZqSZH9k_xpPlbAxQAcGyIISuM,6925
201
201
  reconcile/external_resources/integration_secrets_sync.py,sha256=dX09O3r6KURziUYYfiki10orNjOGVma-XojhVqd0ww4,1667
202
- reconcile/external_resources/manager.py,sha256=DtxjWx34WdPjPR5TzqV4mZpN_Gn20LcNTZHBbPxqzuQ,16953
202
+ reconcile/external_resources/manager.py,sha256=ZagwLn6YQ1XmgmMN3qpuDzQsQxa4VOYl-IQPZBwCDqM,17103
203
203
  reconcile/external_resources/meta.py,sha256=noaytFzmShpzLA_ebGh7wuP45mOfHIOnnoUxivjDa1I,672
204
204
  reconcile/external_resources/metrics.py,sha256=KiBjMUaN_z0cSkF_7Ar_a8RiuiwVqjyMcVdISlxhzXE,3898
205
205
  reconcile/external_resources/model.py,sha256=EpgIgVRPUsyfHhgjHv_TLUKjzFiIQt0wUd30K0NJJpI,11826
@@ -633,7 +633,7 @@ reconcile/utils/state.py,sha256=az4tBmZ0EdbFcAGiBVUxs3cr2-BVWsuDQiNTvjjQq8s,1637
633
633
  reconcile/utils/structs.py,sha256=LcbLEg8WxfRqM6nW7NhcWN0YeqF7SQzxOgntmLs1SgY,352
634
634
  reconcile/utils/template.py,sha256=wTvRU4AnAV_o042tD4Mwls2dwWMuk7MKnde3MaCjaYg,331
635
635
  reconcile/utils/terraform_client.py,sha256=H8frsS370y8xfivKLNBD1dwlBLHvfuR6JSN_syBL5Qc,36033
636
- reconcile/utils/terrascript_aws_client.py,sha256=SNGtsG1n-IDZaI0blKLm3t3AfVNmxW-O8Y8NtX08OOc,270318
636
+ reconcile/utils/terrascript_aws_client.py,sha256=UdEM3JeTMiE0VRqtz7gcBWR-c0fouORtPFrniRJ3pao,283505
637
637
  reconcile/utils/three_way_diff_strategy.py,sha256=oQcHXd9LVhirJfoaOBoHUYuZVGfyL2voKr6KVI34zZE,4833
638
638
  reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
639
639
  reconcile/utils/vault.py,sha256=aSA8l9cJlPUHpChFGl27nSY-Mpq9FMjBo7Dcgb1BVfM,15036
@@ -751,7 +751,7 @@ tools/sd_app_sre_alert_report.py,sha256=jQpJdXVID68bSNtJNOGDh0-ei1CfEUS4Itr4MAaB
751
751
  tools/template_validation.py,sha256=qpKYaTgk0GOPGa2Ct5_5sKdwIHtCAKIBGzsMPuJU5fw,3371
752
752
  tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
753
753
  tools/cli_commands/container_images_report.py,sha256=8fG9XU-eEhJ7hKCdQzBcdPpvIJR-8WGkHOgFEulpfYQ,5213
754
- tools/cli_commands/erv2.py,sha256=iqihq908iJYIQoaNq3Iofp5SQP8S-DhTurzGdnEragU,23218
754
+ tools/cli_commands/erv2.py,sha256=VxUlNXllo947UwmtvS-42IeI9x_t_X3MHrrSI3K_GRo,23274
755
755
  tools/cli_commands/gpg_encrypt.py,sha256=NhzwN49UN7P5_FJgTUN5A4BIwNbFokIE4lwDax2iP5k,4891
756
756
  tools/cli_commands/systems_and_tools.py,sha256=EMHOF1AtUDaoSk0bbjl6oUKYAz4rTZjIBaF-6E6GspM,16816
757
757
  tools/cli_commands/cost_report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -773,7 +773,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
773
773
  tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
774
774
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
775
775
  tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
776
- qontract_reconcile-0.10.2.dev43.dist-info/METADATA,sha256=dR0wcgtr249AUyR4Ry3LQDzF8VgP0FQO4AEs_sCbJLM,24665
777
- qontract_reconcile-0.10.2.dev43.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
778
- qontract_reconcile-0.10.2.dev43.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
779
- qontract_reconcile-0.10.2.dev43.dist-info/RECORD,,
776
+ qontract_reconcile-0.10.2.dev45.dist-info/METADATA,sha256=weP8FCL2ss8xcapzIrtxBfdAV3gAjUFV5fhHpfGe3nY,24665
777
+ qontract_reconcile-0.10.2.dev45.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
778
+ qontract_reconcile-0.10.2.dev45.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
779
+ qontract_reconcile-0.10.2.dev45.dist-info/RECORD,,
@@ -1,9 +1,11 @@
1
+ import re
1
2
  from abc import ABC, abstractmethod
2
3
  from typing import Any
3
4
 
4
5
  from reconcile.external_resources.model import (
5
6
  ExternalResource,
6
7
  ExternalResourceKey,
8
+ ExternalResourceModuleConfiguration,
7
9
  ExternalResourcesInventory,
8
10
  )
9
11
  from reconcile.utils.external_resource_spec import (
@@ -21,10 +23,18 @@ class AWSResourceFactory(ABC):
21
23
  self.secret_reader = secret_reader
22
24
 
23
25
  @abstractmethod
24
- def resolve(self, spec: ExternalResourceSpec) -> dict[str, Any]: ...
26
+ def resolve(
27
+ self,
28
+ spec: ExternalResourceSpec,
29
+ module_conf: ExternalResourceModuleConfiguration,
30
+ ) -> dict[str, Any]: ...
25
31
 
26
32
  @abstractmethod
27
- def validate(self, resource: ExternalResource) -> None: ...
33
+ def validate(
34
+ self,
35
+ resource: ExternalResource,
36
+ module_conf: ExternalResourceModuleConfiguration,
37
+ ) -> None: ...
28
38
 
29
39
  def find_linked_resources(
30
40
  self, spec: ExternalResourceSpec
@@ -35,10 +45,18 @@ class AWSResourceFactory(ABC):
35
45
 
36
46
 
37
47
  class AWSDefaultResourceFactory(AWSResourceFactory):
38
- def resolve(self, spec: ExternalResourceSpec) -> dict[str, Any]:
48
+ def resolve(
49
+ self,
50
+ spec: ExternalResourceSpec,
51
+ module_conf: ExternalResourceModuleConfiguration,
52
+ ) -> dict[str, Any]:
39
53
  return ResourceValueResolver(spec=spec, identifier_as_value=True).resolve()
40
54
 
41
- def validate(self, resource: ExternalResource) -> None: ...
55
+ def validate(
56
+ self,
57
+ resource: ExternalResource,
58
+ module_conf: ExternalResourceModuleConfiguration,
59
+ ) -> None: ...
42
60
 
43
61
 
44
62
  class AWSElasticacheFactory(AWSDefaultResourceFactory):
@@ -49,7 +67,11 @@ class AWSElasticacheFactory(AWSDefaultResourceFactory):
49
67
  "aws", provisioner, "elasticache", identifier
50
68
  )
51
69
 
52
- def resolve(self, spec: ExternalResourceSpec) -> dict[str, Any]:
70
+ def resolve(
71
+ self,
72
+ spec: ExternalResourceSpec,
73
+ module_conf: ExternalResourceModuleConfiguration,
74
+ ) -> dict[str, Any]:
53
75
  """Resolve the elasticache resource specification and translate some attributes to AWS >= 5.60.0 provider format."""
54
76
  rvr = ResourceValueResolver(spec=spec, identifier_as_value=True)
55
77
  data = rvr.resolve()
@@ -70,7 +92,11 @@ class AWSElasticacheFactory(AWSDefaultResourceFactory):
70
92
 
71
93
  return data
72
94
 
73
- def validate(self, resource: ExternalResource) -> None:
95
+ def validate(
96
+ self,
97
+ resource: ExternalResource,
98
+ module_conf: ExternalResourceModuleConfiguration,
99
+ ) -> None:
74
100
  """Validate the elasticache resource specification."""
75
101
  data = resource.data
76
102
  if data.get("parameter_group"):
@@ -96,6 +122,8 @@ class AWSElasticacheFactory(AWSDefaultResourceFactory):
96
122
 
97
123
 
98
124
  class AWSRdsFactory(AWSDefaultResourceFactory):
125
+ TIMEOUT_RE = re.compile(r"^\d+m$")
126
+
99
127
  def _get_source_db_spec(
100
128
  self, provisioner: str, identifier: str
101
129
  ) -> ExternalResourceSpec:
@@ -110,7 +138,11 @@ class AWSRdsFactory(AWSDefaultResourceFactory):
110
138
  "aws", provisioner, "kms", identifier
111
139
  )
112
140
 
113
- def resolve(self, spec: ExternalResourceSpec) -> dict[str, Any]:
141
+ def resolve(
142
+ self,
143
+ spec: ExternalResourceSpec,
144
+ module_conf: ExternalResourceModuleConfiguration,
145
+ ) -> dict[str, Any]:
114
146
  rvr = ResourceValueResolver(spec=spec, identifier_as_value=True)
115
147
  data = rvr.resolve()
116
148
 
@@ -126,7 +158,7 @@ class AWSRdsFactory(AWSDefaultResourceFactory):
126
158
  sourcedb_spec = self._get_source_db_spec(
127
159
  spec.provisioner_name, data["replica_source"]
128
160
  )
129
- sourcedb = self.resolve(sourcedb_spec)
161
+ sourcedb = self.resolve(sourcedb_spec, module_conf)
130
162
  sourcedb_region = (
131
163
  sourcedb.get("region", None)
132
164
  or sourcedb_spec.provisioner["resources_default_region"]
@@ -142,9 +174,59 @@ class AWSRdsFactory(AWSDefaultResourceFactory):
142
174
  spec.provisioner_name, kms_key_id
143
175
  ).identifier
144
176
 
177
+ # If not timeouts are set, set default timeouts according to the module reconcile timeout configuration
178
+ # 5 minutes are substracted to let terraform finish gracefully before the Job is killed.
179
+ if "timeouts" not in data:
180
+ data["timeouts"] = {
181
+ "create": f"{module_conf.reconcile_timeout_minutes - 5}m",
182
+ "update": f"{module_conf.reconcile_timeout_minutes - 5}m",
183
+ "delete": f"{module_conf.reconcile_timeout_minutes - 5}m",
184
+ }
145
185
  return data
146
186
 
147
- def validate(self, resource: ExternalResource) -> None: ...
187
+ def _get_timeout_minutes(
188
+ self,
189
+ timeout: str,
190
+ ) -> int:
191
+ if not re.match(AWSRdsFactory.TIMEOUT_RE, timeout):
192
+ raise ValueError(
193
+ f"Invalid RDS instance timeout format: {timeout}. Specify timeout in minutes(m)."
194
+ )
195
+ return int(timeout[:-1])
196
+
197
+ def _validate_timeouts(
198
+ self,
199
+ resource: ExternalResource,
200
+ module_conf: ExternalResourceModuleConfiguration,
201
+ ) -> None:
202
+ timeouts = resource.data.get("timeouts")
203
+ if not timeouts:
204
+ return
205
+
206
+ if not isinstance(timeouts, dict):
207
+ raise ValueError(
208
+ "Timeouts must be a dictionary with 'create', 'update' and/or 'delete' keys."
209
+ )
210
+
211
+ allowed_keys = {"create", "update", "delete"}
212
+ if unknown_keys := timeouts.keys() - allowed_keys:
213
+ raise ValueError(
214
+ f"Timeouts must be a dictionary with 'create', 'update' and/or 'delete' keys. Offending keys: {unknown_keys}."
215
+ )
216
+
217
+ for option, timeout in timeouts.items():
218
+ timeout_minutes = self._get_timeout_minutes(timeout)
219
+ if timeout_minutes >= module_conf.reconcile_timeout_minutes:
220
+ raise ValueError(
221
+ f"RDS instance {option} timeout value {timeout_minutes} must be lower than the module reconcile_timeout_minutes value {module_conf.reconcile_timeout_minutes}."
222
+ )
223
+
224
+ def validate(
225
+ self,
226
+ resource: ExternalResource,
227
+ module_conf: ExternalResourceModuleConfiguration,
228
+ ) -> None:
229
+ self._validate_timeouts(resource, module_conf)
148
230
 
149
231
  def find_linked_resources(
150
232
  self, spec: ExternalResourceSpec
@@ -166,7 +248,11 @@ class AWSMskFactory(AWSDefaultResourceFactory):
166
248
  "aws", provisioner, "msk", identifier
167
249
  )
168
250
 
169
- def resolve(self, spec: ExternalResourceSpec) -> dict[str, Any]:
251
+ def resolve(
252
+ self,
253
+ spec: ExternalResourceSpec,
254
+ module_conf: ExternalResourceModuleConfiguration,
255
+ ) -> dict[str, Any]:
170
256
  rvr = ResourceValueResolver(spec=spec, identifier_as_value=True)
171
257
  data = rvr.resolve()
172
258
  data["output_prefix"] = spec.output_prefix
@@ -188,7 +274,11 @@ class AWSMskFactory(AWSDefaultResourceFactory):
188
274
  del data["users"]
189
275
  return data
190
276
 
191
- def validate(self, resource: ExternalResource) -> None:
277
+ def validate(
278
+ self,
279
+ resource: ExternalResource,
280
+ module_conf: ExternalResourceModuleConfiguration,
281
+ ) -> None:
192
282
  data = resource.data
193
283
  if (
194
284
  data["number_of_broker_nodes"]
@@ -15,6 +15,7 @@ from reconcile.external_resources.meta import QONTRACT_INTEGRATION
15
15
  from reconcile.external_resources.model import (
16
16
  ExternalResource,
17
17
  ExternalResourceKey,
18
+ ExternalResourceModuleConfiguration,
18
19
  ExternalResourceProvision,
19
20
  ExternalResourcesInventory,
20
21
  ModuleInventory,
@@ -55,11 +56,19 @@ class ObjectFactory(Generic[T]):
55
56
 
56
57
  class ExternalResourceFactory(ABC):
57
58
  @abstractmethod
58
- def create_external_resource(self, spec: ExternalResourceSpec) -> ExternalResource:
59
+ def create_external_resource(
60
+ self,
61
+ spec: ExternalResourceSpec,
62
+ module_conf: ExternalResourceModuleConfiguration,
63
+ ) -> ExternalResource:
59
64
  pass
60
65
 
61
66
  @abstractmethod
62
- def validate_external_resource(self, resource: ExternalResource) -> None:
67
+ def validate_external_resource(
68
+ self,
69
+ resource: ExternalResource,
70
+ module_conf: ExternalResourceModuleConfiguration,
71
+ ) -> None:
63
72
  pass
64
73
 
65
74
  def find_linked_resources(
@@ -121,9 +130,13 @@ class AWSExternalResourceFactory(ExternalResourceFactory):
121
130
  self.er_inventory = er_inventory
122
131
  self.secret_reader = secret_reader
123
132
 
124
- def create_external_resource(self, spec: ExternalResourceSpec) -> ExternalResource:
133
+ def create_external_resource(
134
+ self,
135
+ spec: ExternalResourceSpec,
136
+ module_conf: ExternalResourceModuleConfiguration,
137
+ ) -> ExternalResource:
125
138
  f = self.resource_factories.get_factory(spec.provider)
126
- data = f.resolve(spec)
139
+ data = f.resolve(spec, module_conf)
127
140
  data["tags"] = spec.tags(integration=QONTRACT_INTEGRATION)
128
141
  data["default_tags"] = AWS_DEFAULT_TAGS
129
142
 
@@ -152,9 +165,13 @@ class AWSExternalResourceFactory(ExternalResourceFactory):
152
165
 
153
166
  return ExternalResource(data=data, provision=provision)
154
167
 
155
- def validate_external_resource(self, resource: ExternalResource) -> None:
168
+ def validate_external_resource(
169
+ self,
170
+ resource: ExternalResource,
171
+ module_conf: ExternalResourceModuleConfiguration,
172
+ ) -> None:
156
173
  f = self.resource_factories.get_factory(resource.provision.provider)
157
- f.validate(resource)
174
+ f.validate(resource, module_conf)
158
175
 
159
176
  def find_linked_resources(
160
177
  self, spec: ExternalResourceSpec
@@ -197,8 +197,11 @@ class ExternalResourcesManager:
197
197
  if spec.marked_to_delete:
198
198
  continue
199
199
  module = self.module_inventory.get_from_spec(spec)
200
+ module_conf = ExternalResourceModuleConfiguration.resolve_configuration(
201
+ module, spec, self.settings
202
+ )
200
203
  try:
201
- resource = self._build_external_resource(spec)
204
+ resource = self._build_external_resource(spec, module_conf)
202
205
  except ExternalResourceValidationError as e:
203
206
  self.errors[key] = e
204
207
  continue
@@ -208,9 +211,7 @@ class ExternalResourcesManager:
208
211
  resource_hash=resource.hash(),
209
212
  input=resource.json(),
210
213
  action=Action.APPLY,
211
- module_configuration=ExternalResourceModuleConfiguration.resolve_configuration(
212
- module, spec, self.settings
213
- ),
214
+ module_configuration=module_conf,
214
215
  linked_resources=self._find_linked_resources(spec),
215
216
  )
216
217
  r.add(reconciliation)
@@ -362,10 +363,14 @@ class ExternalResourcesManager:
362
363
  )
363
364
  self.state_mgr.update_resource_status(key, ResourceStatus.CREATED)
364
365
 
365
- def _build_external_resource(self, spec: ExternalResourceSpec) -> ExternalResource:
366
+ def _build_external_resource(
367
+ self,
368
+ spec: ExternalResourceSpec,
369
+ module_conf: ExternalResourceModuleConfiguration,
370
+ ) -> ExternalResource:
366
371
  f = self.factories.get_factory(spec.provision_provider)
367
- resource = f.create_external_resource(spec)
368
- f.validate_external_resource(resource)
372
+ resource = f.create_external_resource(spec, module_conf)
373
+ f.validate_external_resource(resource, module_conf)
369
374
  return resource
370
375
 
371
376
  def _find_linked_resources(
@@ -77,6 +77,8 @@ from terrascript.resource import (
77
77
  aws_ec2_transit_gateway_vpc_attachment,
78
78
  aws_ec2_transit_gateway_vpc_attachment_accepter,
79
79
  aws_ecr_repository,
80
+ aws_elasticache_parameter_group,
81
+ aws_elasticache_replication_group,
80
82
  aws_elasticsearch_domain,
81
83
  aws_iam_access_key,
82
84
  aws_iam_group,
@@ -104,6 +106,8 @@ from terrascript.resource import (
104
106
  aws_lb_listener_rule,
105
107
  aws_lb_target_group,
106
108
  aws_lb_target_group_attachment,
109
+ aws_msk_cluster,
110
+ aws_msk_configuration,
107
111
  aws_ram_principal_association,
108
112
  aws_ram_resource_association,
109
113
  aws_ram_resource_share,
@@ -1641,6 +1645,8 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
1641
1645
  self.populate_tf_resource_rds(spec)
1642
1646
  elif provider == "s3":
1643
1647
  self.populate_tf_resource_s3(spec)
1648
+ elif provider == "elasticache":
1649
+ self.populate_tf_resource_elasticache(spec)
1644
1650
  elif provider == "aws-iam-service-account":
1645
1651
  self.populate_tf_resource_service_account(spec, ocm_map=ocm_map)
1646
1652
  elif provider == "secrets-manager-service-account":
@@ -1683,6 +1689,8 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
1683
1689
  self.populate_tf_resource_rosa_authenticator(spec)
1684
1690
  elif provider == "rosa-authenticator-vpce":
1685
1691
  self.populate_tf_resource_rosa_authenticator_vpce(spec)
1692
+ elif provider == "msk":
1693
+ self.populate_tf_resource_msk(spec)
1686
1694
  else:
1687
1695
  raise UnknownProviderError(provider)
1688
1696
 
@@ -2483,6 +2491,94 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
2483
2491
 
2484
2492
  return bucket_tf_resource
2485
2493
 
2494
+ def populate_tf_resource_elasticache(self, spec):
2495
+ account = spec.provisioner_name
2496
+ identifier = spec.identifier
2497
+ values = self.init_values(spec)
2498
+ output_prefix = spec.output_prefix
2499
+ values.setdefault("replication_group_id", values["identifier"])
2500
+ values.pop("identifier", None)
2501
+
2502
+ tf_resources = []
2503
+ self.init_common_outputs(tf_resources, spec)
2504
+
2505
+ default_region = self.default_regions.get(account)
2506
+ desired_region = values.pop("region", default_region)
2507
+
2508
+ provider = ""
2509
+ if desired_region is not None and self._multiregion_account(account):
2510
+ provider = "aws." + desired_region
2511
+ values["provider"] = provider
2512
+
2513
+ if not values.get("apply_immediately"):
2514
+ values["apply_immediately"] = False
2515
+
2516
+ parameter_group = values.get("parameter_group")
2517
+ # Assume that cluster enabled is false if parameter group unset
2518
+ pg_cluster_enabled = False
2519
+
2520
+ if parameter_group:
2521
+ pg_values = self.get_values(parameter_group)
2522
+ pg_name = pg_values["name"]
2523
+ pg_identifier = pg_name
2524
+
2525
+ # If the desired region is not the same as the default region
2526
+ # we append the region to the identifier to make it unique
2527
+ # in the terraform config
2528
+ if desired_region is not None and desired_region != default_region:
2529
+ pg_identifier = f"{pg_name}-{desired_region}"
2530
+
2531
+ pg_values["parameter"] = pg_values.pop("parameters")
2532
+ for param in pg_values["parameter"]:
2533
+ if param["name"] == "cluster-enabled" and param["value"] == "yes":
2534
+ pg_cluster_enabled = True
2535
+
2536
+ if self._multiregion_account(account) and len(provider) > 0:
2537
+ pg_values["provider"] = provider
2538
+ pg_tf_resource = aws_elasticache_parameter_group(pg_identifier, **pg_values)
2539
+ tf_resources.append(pg_tf_resource)
2540
+ values["depends_on"] = [
2541
+ f"aws_elasticache_parameter_group.{pg_identifier}",
2542
+ ]
2543
+ values["parameter_group_name"] = pg_name
2544
+ values.pop("parameter_group", None)
2545
+
2546
+ auth_token = spec.get_secret_field("db.auth_token")
2547
+ if not auth_token:
2548
+ auth_token = self.generate_random_password()
2549
+
2550
+ if values.get("transit_encryption_enabled", False):
2551
+ values["auth_token"] = auth_token
2552
+
2553
+ # elasticache replication group
2554
+ # Ref: https://www.terraform.io/docs/providers/aws/r/
2555
+ # elasticache_replication_group.html
2556
+ tf_resource = aws_elasticache_replication_group(identifier, **values)
2557
+ tf_resources.append(tf_resource)
2558
+ # elasticache outputs
2559
+ # we want the outputs to be formed into an OpenShift Secret
2560
+ # with the following fields
2561
+ # db.endpoint
2562
+ output_name = output_prefix + "__db_endpoint"
2563
+ # https://docs.aws.amazon.com/AmazonElastiCache/
2564
+ # latest/red-ug/Endpoints.html
2565
+ if pg_cluster_enabled:
2566
+ output_value = "${" + tf_resource.configuration_endpoint_address + "}"
2567
+ else:
2568
+ output_value = "${" + tf_resource.primary_endpoint_address + "}"
2569
+ tf_resources.append(Output(output_name, value=output_value))
2570
+ # db.port
2571
+ output_name = output_prefix + "__db_port"
2572
+ output_value = "${" + str(tf_resource.port) + "}"
2573
+ tf_resources.append(Output(output_name, value=output_value))
2574
+ # db.auth_token
2575
+ if values.get("transit_encryption_enabled", False):
2576
+ output_name = output_prefix + "__db_auth_token"
2577
+ output_value = values["auth_token"]
2578
+ tf_resources.append(Output(output_name, value=output_value, sensitive=True))
2579
+
2580
+ self.add_resources(account, tf_resources)
2581
+
2486
2582
  def populate_tf_resource_service_account(self, spec, ocm_map=None):
2487
2583
  account = spec.provisioner_name
2488
2584
  identifier = spec.identifier
@@ -4191,6 +4287,24 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
4191
4287
  values["db_name"] = db_name
4192
4288
  if values.get("replica_source"):
4193
4289
  values.pop("db_name", None)
4290
+ elif spec.provider == "elasticache":
4291
+ if description := values.pop("replication_group_description", None):
4292
+ values["description"] = description
4293
+ if num_cache_clusters := values.pop("number_cache_clusters", None):
4294
+ values["num_cache_clusters"] = num_cache_clusters
4295
+ if cluster_mode := values.pop("cluster_mode", {}):
4296
+ for k, v in cluster_mode.items():
4297
+ values[k] = v
4298
+ values.pop("availability_zones", None)
4299
+ elif spec.provider == "msk":
4300
+ if ebs_volume_size := values.get("broker_node_group_info", {}).pop(
4301
+ "ebs_volume_size", None
4302
+ ):
4303
+ values["broker_node_group_info"].setdefault(
4304
+ "storage_info", {}
4305
+ ).setdefault("ebs_storage_info", {})[
4306
+ "volume_size"
4307
+ ] = ebs_volume_size
4194
4308
 
4195
4309
  return values
4196
4310
 
@@ -6542,6 +6656,205 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
6542
6656
 
6543
6657
  self.add_resources(account, tf_resources)
6544
6658
 
6659
+ def populate_tf_resource_msk(self, spec):
6660
+ account = spec.provisioner_name
6661
+ values = self.init_values(spec)
6662
+ output_prefix = spec.output_prefix
6663
+ tf_resources = []
6664
+ resource_id = spec.identifier
6665
+
6666
+ del values["identifier"]
6667
+ values.setdefault("cluster_name", spec.identifier)
6668
+
6669
+ # common
6670
+ self.init_common_outputs(tf_resources, spec)
6671
+
6672
+ # validations
6673
+ if (
6674
+ values["number_of_broker_nodes"]
6675
+ % len(values["broker_node_group_info"]["client_subnets"])
6676
+ != 0
6677
+ ):
6678
+ raise ValueError(
6679
+ "number_of_broker_nodes must be a multiple of the number of specified client subnets."
6680
+ )
6681
+
6682
+ scram_enabled = (
6683
+ values.get("client_authentication", {}).get("sasl", {}).get("scram", False)
6684
+ )
6685
+ scram_users = {}
6686
+ if scram_enabled:
6687
+ if not spec.resource.get("users", []):
6688
+ raise ValueError(
6689
+ "users attribute must be given when client_authentication.sasl.scram is enabled."
6690
+ )
6691
+ scram_users = {
6692
+ user["name"]: self.secret_reader.read_all(user["secret"])
6693
+ for user in spec.resource["users"]
6694
+ }
6695
+ # validate user objects
6696
+ for user, secret in scram_users.items():
6697
+ if secret.keys() != {"password", "username"}:
6698
+ raise ValueError(
6699
+ f"MSK user '{user}' secret must contain only 'username' and 'password' keys!"
6700
+ )
6701
+
6702
+ # resource - msk config
6703
+ # unique msk config resource name enables "create_before_destroy" lifecycle
6704
+ # which is required when changing version which requires a resource replacement
6705
+ msk_version_str = values["kafka_version"].replace(".", "-")
6706
+ msk_config_name = f"{resource_id}-{msk_version_str}"
6707
+ msk_config = aws_msk_configuration(
6708
+ msk_config_name,
6709
+ name=msk_config_name,
6710
+ kafka_versions=[values["kafka_version"]],
6711
+ server_properties=values["server_properties"],
6712
+ # lifecycle create_before_destroy is required to ensure that the config is created
6713
+ # before it is assigned to the cluster
6714
+ lifecycle={
6715
+ "create_before_destroy": True,
6716
+ },
6717
+ )
6718
+ tf_resources.append(msk_config)
6719
+ values.pop("server_properties", None)
6720
+
6721
+ # resource - cluster
6722
+ values["configuration_info"] = {
6723
+ "arn": "${" + msk_config.arn + "}",
6724
+ "revision": "${" + msk_config.latest_revision + "}",
6725
+ }
6726
+ msk_cluster = aws_msk_cluster(resource_id, **values)
6727
+ tf_resources.append(msk_cluster)
6728
+
6729
+ # resource - cloudwatch
6730
+ if (
6731
+ values.get("logging_info", {})
6732
+ .get("broker_logs", {})
6733
+ .get("cloudwatch_logs", {})
6734
+ .get("enabled", False)
6735
+ ):
6736
+ log_group_values = {
6737
+ "name": f"{resource_id}-msk-broker-logs",
6738
+ "tags": values["tags"],
6739
+ "retention_in_days": values["logging_info"]["broker_logs"][
6740
+ "cloudwatch_logs"
6741
+ ]["retention_in_days"],
6742
+ }
6743
+ log_group_tf_resource = aws_cloudwatch_log_group(
6744
+ resource_id, **log_group_values
6745
+ )
6746
+ tf_resources.append(log_group_tf_resource)
6747
+ del values["logging_info"]["broker_logs"]["cloudwatch_logs"][
6748
+ "retention_in_days"
6749
+ ]
6750
+ values["logging_info"]["broker_logs"]["cloudwatch_logs"]["log_group"] = (
6751
+ log_group_tf_resource.name
6752
+ )
6753
+
6754
+ # resource - secret manager for SCRAM client credentials
6755
+ if scram_enabled and scram_users:
6756
+ scram_secrets: list[
6757
+ tuple[aws_secretsmanager_secret, aws_secretsmanager_secret_version]
6758
+ ] = []
6759
+
6760
+ # kms
6761
+ kms_values = {
6762
+ "description": "KMS key for MSK SCRAM credentials",
6763
+ "tags": values["tags"],
6764
+ }
6765
+ kms_key = aws_kms_key(resource_id, **kms_values)
6766
+ tf_resources.append(kms_key)
6767
+
6768
+ kms_key_alias = aws_kms_alias(
6769
+ resource_id,
6770
+ name=f"alias/{resource_id}-msk-scram",
6771
+ target_key_id="${" + kms_key.arn + "}",
6772
+ )
6773
+ tf_resources.append(kms_key_alias)
6774
+
6775
+ for user, secret in scram_users.items():
6776
+ secret_identifier = f"AmazonMSK_{resource_id}-{user}"
6777
+
6778
+ secret_values = {
6779
+ "name": secret_identifier,
6780
+ "tags": values["tags"],
6781
+ "kms_key_id": "${" + kms_key.arn + "}",
6782
+ }
6783
+ secret_resource = aws_secretsmanager_secret(
6784
+ secret_identifier, **secret_values
6785
+ )
6786
+ tf_resources.append(secret_resource)
6787
+
6788
+ version_values = {
6789
+ "secret_id": "${" + secret_resource.arn + "}",
6790
+ "secret_string": json.dumps(secret, sort_keys=True),
6791
+ }
6792
+ version_resource = aws_secretsmanager_secret_version(
6793
+ secret_identifier, **version_values
6794
+ )
6795
+ tf_resources.append(version_resource)
6796
+
6797
+ secret_policy_values = {
6798
+ "secret_arn": "${" + secret_resource.arn + "}",
6799
+ "policy": json.dumps({
6800
+ "Version": "2012-10-17",
6801
+ "Statement": [
6802
+ {
6803
+ "Sid": "AWSKafkaResourcePolicy",
6804
+ "Effect": "Allow",
6805
+ "Principal": {"Service": "kafka.amazonaws.com"},
6806
+ "Action": "secretsmanager:getSecretValue",
6807
+ "Resource": "${" + secret_resource.arn + "}",
6808
+ }
6809
+ ],
6810
+ }),
6811
+ }
6812
+ secret_policy = aws_secretsmanager_secret_policy(
6813
+ secret_identifier, **secret_policy_values
6814
+ )
6815
+ tf_resources.append(secret_policy)
6816
+ scram_secrets.append((secret_resource, version_resource))
6817
+
6818
+ # create ONE scram secret association for each secret created above
6819
+ scram_secret_association_values = {
6820
+ "cluster_arn": "${" + msk_cluster.arn + "}",
6821
+ "secret_arn_list": ["${" + s.arn + "}" for s, _ in scram_secrets],
6822
+ "depends_on": self.get_dependencies([v for _, v in scram_secrets]),
6823
+ }
6824
+ scram_secret_association = aws_msk_scram_secret_association(
6825
+ resource_id, **scram_secret_association_values
6826
+ )
6827
+ tf_resources.append(scram_secret_association)
6828
+
6829
+ # outputs
6830
+ tf_resources += [
6831
+ Output(
6832
+ output_prefix + "__zookeeper_connect_string",
6833
+ value="${" + msk_cluster.zookeeper_connect_string + "}",
6834
+ ),
6835
+ Output(
6836
+ output_prefix + "__zookeeper_connect_string_tls",
6837
+ value="${" + msk_cluster.zookeeper_connect_string_tls + "}",
6838
+ ),
6839
+ Output(
6840
+ output_prefix + "__bootstrap_brokers",
6841
+ value="${" + msk_cluster.bootstrap_brokers + "}",
6842
+ ),
6843
+ Output(
6844
+ output_prefix + "__bootstrap_brokers_tls",
6845
+ value="${" + msk_cluster.bootstrap_brokers_tls + "}",
6846
+ ),
6847
+ Output(
6848
+ output_prefix + "__bootstrap_brokers_sasl_iam",
6849
+ value="${" + msk_cluster.bootstrap_brokers_sasl_iam + "}",
6850
+ ),
6851
+ Output(
6852
+ output_prefix + "__bootstrap_brokers_sasl_scram",
6853
+ value="${" + msk_cluster.bootstrap_brokers_sasl_scram + "}",
6854
+ ),
6855
+ ]
6856
+ self.add_resources(account, tf_resources)
6857
+
6545
6858
  def populate_saml_idp(self, account_name: str, name: str, metadata: str) -> None:
6546
6859
  saml_idp = aws_iam_saml_provider(
6547
6860
  f"{account_name}-{name}", name=name, saml_metadata_document=metadata
@@ -118,13 +118,13 @@ class Erv2Cli:
118
118
  self._er_settings, m_inventory, er_inventory, self._secret_reader
119
119
  )
120
120
  f = factories.get_factory(spec.provision_provider)
121
- self._resource = f.create_external_resource(spec)
122
- f.validate_external_resource(self._resource)
123
121
  self._module_configuration = (
124
122
  ExternalResourceModuleConfiguration.resolve_configuration(
125
123
  m_inventory.get_from_spec(spec), spec, self._er_settings
126
124
  )
127
125
  )
126
+ self._resource = f.create_external_resource(spec, self._module_configuration)
127
+ f.validate_external_resource(self._resource, self._module_configuration)
128
128
 
129
129
  @property
130
130
  def input_data(self) -> str: