dbt-platform-helper 13.1.0__py3-none-any.whl → 13.1.2__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.

Potentially problematic release.


This version of dbt-platform-helper might be problematic. Click here for more details.

Files changed (30) hide show
  1. dbt_platform_helper/commands/application.py +4 -4
  2. dbt_platform_helper/commands/codebase.py +4 -4
  3. dbt_platform_helper/commands/conduit.py +4 -4
  4. dbt_platform_helper/commands/config.py +7 -5
  5. dbt_platform_helper/commands/copilot.py +12 -391
  6. dbt_platform_helper/commands/environment.py +4 -4
  7. dbt_platform_helper/commands/generate.py +1 -1
  8. dbt_platform_helper/commands/notify.py +4 -4
  9. dbt_platform_helper/commands/pipeline.py +4 -4
  10. dbt_platform_helper/commands/secrets.py +4 -4
  11. dbt_platform_helper/commands/version.py +1 -1
  12. dbt_platform_helper/domain/codebase.py +4 -9
  13. dbt_platform_helper/domain/copilot.py +394 -0
  14. dbt_platform_helper/domain/copilot_environment.py +6 -6
  15. dbt_platform_helper/domain/maintenance_page.py +193 -424
  16. dbt_platform_helper/domain/versioning.py +67 -0
  17. dbt_platform_helper/providers/io.py +14 -0
  18. dbt_platform_helper/providers/load_balancers.py +258 -43
  19. dbt_platform_helper/providers/platform_config_schema.py +3 -1
  20. dbt_platform_helper/providers/platform_helper_versioning.py +107 -0
  21. dbt_platform_helper/providers/semantic_version.py +27 -7
  22. dbt_platform_helper/providers/version.py +24 -0
  23. dbt_platform_helper/utils/application.py +14 -0
  24. dbt_platform_helper/utils/files.py +6 -0
  25. dbt_platform_helper/utils/versioning.py +11 -158
  26. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/METADATA +3 -4
  27. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/RECORD +30 -27
  28. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/LICENSE +0 -0
  29. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/WHEEL +0 -0
  30. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/entry_points.txt +0 -0
@@ -12,7 +12,9 @@ from dbt_platform_helper.platform_exception import PlatformException
12
12
  from dbt_platform_helper.providers.files import FileProvider
13
13
  from dbt_platform_helper.providers.io import ClickIOProvider
14
14
  from dbt_platform_helper.utils.application import Application
15
- from dbt_platform_helper.utils.application import ApplicationException
15
+ from dbt_platform_helper.utils.application import (
16
+ ApplicationEnvironmentNotFoundException,
17
+ )
16
18
  from dbt_platform_helper.utils.application import load_application
17
19
  from dbt_platform_helper.utils.aws import check_image_exists
18
20
  from dbt_platform_helper.utils.aws import get_aws_session_or_abort
@@ -148,7 +150,7 @@ class Codebase:
148
150
 
149
151
  application = self.load_application(app, default_session=session)
150
152
  if not application.environments.get(env):
151
- raise ApplicationEnvironmentNotFoundException(env)
153
+ raise ApplicationEnvironmentNotFoundException(application.name, env)
152
154
 
153
155
  self.check_image_exists(session, application, codebase, commit)
154
156
 
@@ -242,13 +244,6 @@ class ApplicationDeploymentNotTriggered(PlatformException):
242
244
  super().__init__(f"""Your deployment for {codebase} was not triggered.""")
243
245
 
244
246
 
245
- class ApplicationEnvironmentNotFoundException(ApplicationException):
246
- def __init__(self, environment: str):
247
- super().__init__(
248
- f"""The environment "{environment}" either does not exist or has not been deployed."""
249
- )
250
-
251
-
252
247
  class NotInCodeBaseRepositoryException(PlatformException):
253
248
  def __init__(self):
254
249
  super().__init__(
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env python
2
+
3
+ import copy
4
+ import json
5
+ from pathlib import Path
6
+ from pathlib import PosixPath
7
+
8
+ import click
9
+ import yaml
10
+ from schema import SchemaError
11
+
12
+ from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
13
+ from dbt_platform_helper.domain.copilot_environment import CopilotTemplating
14
+ from dbt_platform_helper.providers.config import ConfigProvider
15
+ from dbt_platform_helper.providers.files import FileProvider
16
+ from dbt_platform_helper.utils.application import get_application_name
17
+ from dbt_platform_helper.utils.application import load_application
18
+ from dbt_platform_helper.utils.aws import get_aws_session_or_abort
19
+ from dbt_platform_helper.utils.files import generate_override_files
20
+ from dbt_platform_helper.utils.template import ADDON_TEMPLATE_MAP
21
+ from dbt_platform_helper.utils.template import camel_case
22
+ from dbt_platform_helper.utils.template import setup_templates
23
+ from dbt_platform_helper.utils.validation import validate_addons
24
+
25
+
26
+ class Copilot:
27
+
28
+ PACKAGE_DIR = Path(__file__).resolve().parent.parent
29
+
30
+ # TODO Remove and test
31
+ WAF_ACL_ARN_KEY = "waf-acl-arn"
32
+
33
+ SERVICE_TYPES = [
34
+ "Load Balanced Web Service",
35
+ "Backend Service",
36
+ "Request-Driven Web Service",
37
+ "Static Site",
38
+ "Worker Service",
39
+ ]
40
+
41
+ def __init__(
42
+ self,
43
+ config_provider: ConfigProvider,
44
+ file_provider: FileProvider,
45
+ copilot_templating: CopilotTemplating,
46
+ ):
47
+ self.config_provider = config_provider
48
+ self.file_provider = file_provider
49
+ self.copilot_templating = copilot_templating
50
+
51
+ def list_copilot_local_environments(self):
52
+ return [
53
+ path.parent.parts[-1] for path in Path("./copilot/environments/").glob("*/manifest.yml")
54
+ ]
55
+
56
+ def is_service(self, path: PosixPath) -> bool:
57
+ with open(path) as manifest_file:
58
+ data = yaml.safe_load(manifest_file)
59
+ if not data or not data.get("type"):
60
+ click.echo(
61
+ click.style(f"No type defined in manifest file {str(path)}; exiting", fg="red")
62
+ )
63
+ exit(1)
64
+
65
+ return data.get("type") in self.SERVICE_TYPES
66
+
67
+ def list_copilot_local_services(self):
68
+ return [
69
+ path.parent.parts[-1]
70
+ for path in Path("./copilot/").glob("*/manifest.yml")
71
+ if self.is_service(path)
72
+ ]
73
+
74
+ def _validate_and_normalise_extensions_config(self, config_file, key_in_config_file=None):
75
+ """Load a config file, validate it against the extensions schemas and
76
+ return the normalised config dict."""
77
+
78
+ def _lookup_plan(addon_type, env_conf):
79
+ plan = env_conf.pop("plan", None)
80
+ conf = addon_plans[addon_type][plan] if plan else {}
81
+
82
+ # Make a copy of the addon plan config so subsequent
83
+ # calls do not override the root object
84
+ conf = conf.copy()
85
+
86
+ conf.update(env_conf)
87
+
88
+ return conf
89
+
90
+ def _normalise_keys(source: dict):
91
+ return {k.replace("-", "_"): v for k, v in source.items()}
92
+
93
+ with open(self.PACKAGE_DIR / "addon-plans.yml", "r") as fd:
94
+ addon_plans = yaml.safe_load(fd)
95
+
96
+ # load and validate config
97
+ with open(config_file, "r") as fd:
98
+ config = yaml.safe_load(fd)
99
+
100
+ if config and key_in_config_file:
101
+ config = config[key_in_config_file]
102
+
103
+ # empty file
104
+ if not config:
105
+ return {}
106
+
107
+ errors = validate_addons(config)
108
+
109
+ if errors:
110
+ click.echo(click.style(f"Errors found in {config_file}:", fg="red"))
111
+ for addon, error in errors.items():
112
+ click.echo(click.style(f"Addon '{addon}': {error}", fg="red"))
113
+ exit(1)
114
+
115
+ env_names = self.list_copilot_local_environments()
116
+ svc_names = self.list_copilot_local_services()
117
+
118
+ if not env_names:
119
+ click.echo(
120
+ click.style(f"No environments found in ./copilot/environments; exiting", fg="red")
121
+ )
122
+ exit(1)
123
+
124
+ if not svc_names:
125
+ click.echo(click.style(f"No services found in ./copilot/; exiting", fg="red"))
126
+ exit(1)
127
+
128
+ normalised_config = {}
129
+ config_has_errors = False
130
+ for addon_name, addon_config in config.items():
131
+ addon_type = addon_config["type"]
132
+ normalised_config[addon_name] = copy.deepcopy(addon_config)
133
+
134
+ if "services" in normalised_config[addon_name]:
135
+ if normalised_config[addon_name]["services"] == "__all__":
136
+ normalised_config[addon_name]["services"] = svc_names
137
+
138
+ if not set(normalised_config[addon_name]["services"]).issubset(set(svc_names)):
139
+ click.echo(
140
+ click.style(
141
+ f"Services listed in {addon_name}.services do not exist in ./copilot/",
142
+ fg="red",
143
+ ),
144
+ )
145
+ config_has_errors = True
146
+
147
+ environments = normalised_config[addon_name].pop("environments", {})
148
+ default = environments.pop("*", environments.pop("default", {}))
149
+
150
+ initial = _lookup_plan(addon_type, default)
151
+
152
+ missing_envs = set(environments.keys()) - set(env_names)
153
+ if missing_envs:
154
+ click.echo(
155
+ click.style(
156
+ f"Environment keys listed in {addon_name} do not match those defined in ./copilot/environments.",
157
+ fg="red",
158
+ )
159
+ ),
160
+ click.echo(
161
+ click.style(
162
+ f" Missing environments: {', '.join(sorted(missing_envs))}",
163
+ fg="white",
164
+ ),
165
+ )
166
+ config_has_errors = True
167
+
168
+ if config_has_errors:
169
+ continue
170
+
171
+ normalised_environments = {}
172
+
173
+ for env in env_names:
174
+ normalised_environments[env] = _normalise_keys(initial)
175
+
176
+ for env_name, env_config in environments.items():
177
+ if env_config is None:
178
+ env_config = {}
179
+ normalised_environments[env_name].update(
180
+ _lookup_plan(addon_type, _normalise_keys(env_config))
181
+ )
182
+
183
+ normalised_config[addon_name]["environments"] = normalised_environments
184
+
185
+ if config_has_errors:
186
+ exit(1)
187
+
188
+ return normalised_config
189
+
190
+ def get_log_destination_arn(self):
191
+ """Get destination arns stored in param store in projects aws
192
+ account."""
193
+ session = get_aws_session_or_abort()
194
+ client = session.client("ssm", region_name="eu-west-2")
195
+ response = client.get_parameters(Names=["/copilot/tools/central_log_groups"])
196
+
197
+ if not response["Parameters"]:
198
+ click.echo(
199
+ click.style(
200
+ "No aws central log group defined in Parameter Store at location /copilot/tools/central_log_groups; exiting",
201
+ fg="red",
202
+ )
203
+ )
204
+ exit(1)
205
+
206
+ destination_arns = json.loads(response["Parameters"][0]["Value"])
207
+ return destination_arns
208
+
209
+ def _generate_svc_overrides(self, base_path, templates, name):
210
+ click.echo(f"\n>>> Generating service overrides for {name}\n")
211
+ overrides_path = base_path.joinpath(f"copilot/{name}/overrides")
212
+ overrides_path.mkdir(parents=True, exist_ok=True)
213
+ overrides_file = overrides_path.joinpath("cfn.patches.yml")
214
+ overrides_file.write_text(templates.get_template("svc/overrides/cfn.patches.yml").render())
215
+
216
+ def _get_s3_kms_alias_arns(self, session, application_name, config):
217
+ application = load_application(application_name, session)
218
+ arns = {}
219
+
220
+ for environment_name in application.environments:
221
+ if environment_name not in config:
222
+ continue
223
+
224
+ if "bucket_name" not in config[environment_name]:
225
+ continue
226
+
227
+ bucket_name = config[environment_name]["bucket_name"]
228
+ kms_client = application.environments[environment_name].session.client("kms")
229
+ alias_name = f"alias/{application_name}-{environment_name}-{bucket_name}-key"
230
+
231
+ try:
232
+ response = kms_client.describe_key(KeyId=alias_name)
233
+ except kms_client.exceptions.NotFoundException:
234
+ pass
235
+ else:
236
+ arns[environment_name] = response["KeyMetadata"]["Arn"]
237
+
238
+ return arns
239
+
240
+ def make_addons(self):
241
+ self.config_provider.config_file_check()
242
+ try:
243
+ config = self.config_provider.load_and_validate_platform_config()
244
+ except SchemaError as ex:
245
+ click.secho(f"Invalid `{PLATFORM_CONFIG_FILE}` file: {str(ex)}", fg="red")
246
+ raise click.Abort
247
+
248
+ templates = setup_templates()
249
+ extensions = self._get_extensions()
250
+ session = get_aws_session_or_abort()
251
+
252
+ application_name = get_application_name()
253
+
254
+ click.echo("\n>>> Generating Terraform compatible addons CloudFormation\n")
255
+
256
+ output_dir = Path(".").absolute()
257
+ env_path = Path(f"copilot/environments/")
258
+ env_addons_path = env_path / "addons"
259
+ env_overrides_path = env_path / "overrides"
260
+
261
+ self._cleanup_old_files(extensions, output_dir, env_addons_path, env_overrides_path)
262
+ self._generate_env_overrides(output_dir)
263
+
264
+ svc_names = self.list_copilot_local_services()
265
+ base_path = Path(".")
266
+ for svc_name in svc_names:
267
+ self._generate_svc_overrides(base_path, templates, svc_name)
268
+
269
+ services = []
270
+ for ext_name, ext_data in extensions.items():
271
+ extension = {**ext_data}
272
+ addon_type = extension.pop("type")
273
+ environments = extension.pop("environments")
274
+ environment_addon_config = {
275
+ "addon_type": addon_type,
276
+ "environments": environments,
277
+ "name": extension.get("name", None) or ext_name,
278
+ "prefix": camel_case(ext_name),
279
+ "secret_name": ext_name.upper().replace("-", "_"),
280
+ **extension,
281
+ }
282
+
283
+ services.append(environment_addon_config)
284
+
285
+ service_addon_config = {
286
+ "application_name": application_name,
287
+ "name": extension.get("name", None) or ext_name,
288
+ "prefix": camel_case(ext_name),
289
+ "environments": environments,
290
+ **extension,
291
+ }
292
+
293
+ log_destination_arns = self.get_log_destination_arn()
294
+
295
+ if addon_type in ["s3", "s3-policy"]:
296
+ if extensions[ext_name].get("serve_static_content"):
297
+ continue
298
+
299
+ s3_kms_arns = self._get_s3_kms_alias_arns(session, application_name, environments)
300
+ for environment_name in environments:
301
+ environments[environment_name]["kms_key_arn"] = s3_kms_arns.get(
302
+ environment_name, "kms-key-not-found"
303
+ )
304
+
305
+ self._generate_service_addons(
306
+ extension,
307
+ ext_name,
308
+ addon_type,
309
+ output_dir,
310
+ service_addon_config,
311
+ templates,
312
+ log_destination_arns,
313
+ )
314
+
315
+ environments = self.config_provider.apply_environment_defaults(config)["environments"]
316
+
317
+ self.copilot_templating.generate_cross_account_s3_policies(environments, extensions)
318
+
319
+ click.echo(templates.get_template("addon-instructions.txt").render(services=services))
320
+
321
+ def _get_extensions(self):
322
+ config = self._validate_and_normalise_extensions_config(
323
+ self.PACKAGE_DIR / "default-extensions.yml"
324
+ )
325
+ project_config = self._validate_and_normalise_extensions_config(
326
+ PLATFORM_CONFIG_FILE, "extensions"
327
+ )
328
+ config.update(project_config)
329
+ return config
330
+
331
+ def _generate_env_overrides(self, output_dir):
332
+ path = "templates/env/terraform-overrides"
333
+ click.echo("\n>>> Generating Environment overrides\n")
334
+ overrides_path = output_dir.joinpath(f"copilot/environments/overrides")
335
+ overrides_path.mkdir(parents=True, exist_ok=True)
336
+ template_overrides_path = Path(__file__).parent.parent.joinpath(path)
337
+ generate_override_files(Path("."), template_overrides_path, overrides_path)
338
+
339
+ def _generate_service_addons(
340
+ self,
341
+ addon_config,
342
+ addon_name,
343
+ addon_type,
344
+ output_dir,
345
+ service_addon_config,
346
+ templates,
347
+ log_destination_arns,
348
+ ):
349
+ # generate svc addons
350
+ for addon_template in ADDON_TEMPLATE_MAP.get(addon_type, []):
351
+ template = templates.get_template(addon_template)
352
+
353
+ for svc in addon_config.get("services", []):
354
+ service_path = Path(f"copilot/{svc}/addons/")
355
+
356
+ contents = template.render(
357
+ {
358
+ "addon_config": service_addon_config,
359
+ "log_destination": log_destination_arns,
360
+ }
361
+ )
362
+
363
+ (output_dir / service_path).mkdir(parents=True, exist_ok=True)
364
+ click.echo(
365
+ self.file_provider.mkfile(
366
+ output_dir, service_path / f"{addon_name}.yml", contents, overwrite=True
367
+ )
368
+ )
369
+
370
+ def _cleanup_old_files(self, config, output_dir, env_addons_path, env_overrides_path):
371
+ def _rmdir(path):
372
+ if not path.exists():
373
+ return
374
+ for f in path.iterdir():
375
+ if f.is_file():
376
+ f.unlink()
377
+ if f.is_dir():
378
+ _rmdir(f)
379
+ f.rmdir()
380
+
381
+ _rmdir(output_dir / env_addons_path)
382
+ _rmdir(output_dir / env_overrides_path)
383
+
384
+ all_services = set()
385
+ for services in [v["services"] for v in config.values() if "services" in v]:
386
+ all_services.update(services)
387
+
388
+ for service in all_services:
389
+ svc_addons_dir = Path(output_dir, "copilot", service, "addons")
390
+ if not svc_addons_dir.exists():
391
+ continue
392
+ for f in svc_addons_dir.iterdir():
393
+ if f.is_file():
394
+ f.unlink()
@@ -10,9 +10,7 @@ from dbt_platform_helper.providers.cloudformation import CloudFormation
10
10
  from dbt_platform_helper.providers.config import ConfigProvider
11
11
  from dbt_platform_helper.providers.files import FileProvider
12
12
  from dbt_platform_helper.providers.io import ClickIOProvider
13
- from dbt_platform_helper.providers.load_balancers import (
14
- get_https_certificate_for_application,
15
- )
13
+ from dbt_platform_helper.providers.load_balancers import LoadBalancerProvider
16
14
  from dbt_platform_helper.providers.vpc import Vpc
17
15
  from dbt_platform_helper.providers.vpc import VpcNotFoundForNameException
18
16
  from dbt_platform_helper.providers.vpc import VpcProvider
@@ -27,9 +25,10 @@ class CopilotEnvironment:
27
25
  config_provider: ConfigProvider,
28
26
  vpc_provider: VpcProvider = None,
29
27
  cloudformation_provider: CloudFormation = None,
30
- session: Session = None, # TODO - this is a temporary fix, will fall away once the Loadbalancer provider is in place.
28
+ session: Session = None, # TODO - this is a temporary fix, will fall away once _get_environment_vpc is updated.
31
29
  copilot_templating=None,
32
30
  io: ClickIOProvider = ClickIOProvider(),
31
+ load_balancer_provider: LoadBalancerProvider = LoadBalancerProvider,
33
32
  ):
34
33
  self.config_provider = config_provider
35
34
  self.vpc_provider = vpc_provider
@@ -38,6 +37,7 @@ class CopilotEnvironment:
38
37
  )
39
38
  self.io = io
40
39
  self.session = session
40
+ self.load_balancer = load_balancer_provider(session)
41
41
  self.cloudformation_provider = cloudformation_provider
42
42
 
43
43
  def generate(self, environment_name: str) -> None:
@@ -56,8 +56,8 @@ class CopilotEnvironment:
56
56
 
57
57
  app_name = platform_config["application"]
58
58
 
59
- certificate_arn = get_https_certificate_for_application(
60
- self.session, app_name, environment_name
59
+ certificate_arn = self.load_balancer.get_https_certificate_for_application(
60
+ app_name, environment_name
61
61
  )
62
62
 
63
63
  vpc = self._get_environment_vpc(