dbt-platform-helper 13.2.0__py3-none-any.whl → 13.4.0__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 (37) hide show
  1. dbt_platform_helper/COMMANDS.md +7 -2
  2. dbt_platform_helper/commands/codebase.py +29 -6
  3. dbt_platform_helper/commands/config.py +12 -314
  4. dbt_platform_helper/commands/copilot.py +10 -6
  5. dbt_platform_helper/commands/database.py +17 -9
  6. dbt_platform_helper/commands/environment.py +2 -3
  7. dbt_platform_helper/domain/codebase.py +68 -25
  8. dbt_platform_helper/domain/config.py +345 -0
  9. dbt_platform_helper/domain/copilot.py +155 -157
  10. dbt_platform_helper/domain/versioning.py +48 -7
  11. dbt_platform_helper/providers/aws/__init__.py +0 -0
  12. dbt_platform_helper/providers/aws/exceptions.py +12 -2
  13. dbt_platform_helper/providers/aws/sso_auth.py +61 -0
  14. dbt_platform_helper/providers/config.py +2 -1
  15. dbt_platform_helper/providers/config_validator.py +15 -13
  16. dbt_platform_helper/providers/ecr.py +64 -7
  17. dbt_platform_helper/providers/io.py +2 -2
  18. dbt_platform_helper/providers/parameter_store.py +47 -0
  19. dbt_platform_helper/providers/platform_config_schema.py +17 -0
  20. dbt_platform_helper/providers/semantic_version.py +15 -88
  21. dbt_platform_helper/providers/terraform_manifest.py +1 -0
  22. dbt_platform_helper/providers/version.py +82 -24
  23. dbt_platform_helper/providers/version_status.py +80 -0
  24. dbt_platform_helper/utils/aws.py +0 -135
  25. dbt_platform_helper/utils/git.py +3 -1
  26. dbt_platform_helper/utils/tool_versioning.py +0 -84
  27. {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/METADATA +2 -2
  28. {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/RECORD +32 -32
  29. {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/WHEEL +1 -1
  30. platform_helper.py +1 -1
  31. dbt_platform_helper/templates/svc/manifest-backend.yml +0 -69
  32. dbt_platform_helper/templates/svc/manifest-public.yml +0 -109
  33. dbt_platform_helper/utils/cloudfoundry.py +0 -14
  34. dbt_platform_helper/utils/files.py +0 -53
  35. dbt_platform_helper/utils/manifests.py +0 -18
  36. {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/LICENSE +0 -0
  37. {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/entry_points.txt +0 -0
@@ -5,19 +5,19 @@ import json
5
5
  from pathlib import Path
6
6
  from pathlib import PosixPath
7
7
 
8
- import click
9
- import yaml
10
- from schema import SchemaError
8
+ import botocore
9
+ import botocore.errorfactory
11
10
 
12
11
  from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
13
12
  from dbt_platform_helper.domain.copilot_environment import CopilotTemplating
14
13
  from dbt_platform_helper.providers.config import ConfigProvider
15
14
  from dbt_platform_helper.providers.files import FileProvider
15
+ from dbt_platform_helper.providers.io import ClickIOProvider
16
16
  from dbt_platform_helper.providers.kms import KMSProvider
17
+ from dbt_platform_helper.providers.parameter_store import ParameterStore
18
+ from dbt_platform_helper.providers.yaml_file import YamlFileProvider
17
19
  from dbt_platform_helper.utils.application import get_application_name
18
20
  from dbt_platform_helper.utils.application import load_application
19
- from dbt_platform_helper.utils.aws import get_aws_session_or_abort
20
- from dbt_platform_helper.utils.files import generate_override_files
21
21
  from dbt_platform_helper.utils.template import ADDON_TEMPLATE_MAP
22
22
  from dbt_platform_helper.utils.template import camel_case
23
23
  from dbt_platform_helper.utils.template import setup_templates
@@ -28,9 +28,6 @@ class Copilot:
28
28
 
29
29
  PACKAGE_DIR = Path(__file__).resolve().parent.parent
30
30
 
31
- # TODO Remove and test
32
- WAF_ACL_ARN_KEY = "waf-acl-arn"
33
-
34
31
  SERVICE_TYPES = [
35
32
  "Load Balanced Web Service",
36
33
  "Backend Service",
@@ -42,36 +39,115 @@ class Copilot:
42
39
  def __init__(
43
40
  self,
44
41
  config_provider: ConfigProvider,
42
+ parameter_provider: ParameterStore,
45
43
  file_provider: FileProvider,
46
44
  copilot_templating: CopilotTemplating,
47
45
  kms_provider: KMSProvider,
46
+ session,
47
+ io: ClickIOProvider = ClickIOProvider(),
48
+ yaml_file_provider: YamlFileProvider = YamlFileProvider,
48
49
  ):
49
50
  self.config_provider = config_provider
51
+ self.parameter_provider = parameter_provider
50
52
  self.file_provider = file_provider
51
53
  self.copilot_templating = copilot_templating
52
54
  self.kms_provider = kms_provider
55
+ self.io = io
56
+ self.yaml_file_provider = yaml_file_provider
57
+ self.session = session
58
+
59
+ def make_addons(self):
60
+ config = self.config_provider.load_and_validate_platform_config()
61
+
62
+ templates = setup_templates()
63
+ extensions = self._get_extensions()
64
+ application_name = get_application_name()
65
+
66
+ self.io.info("\n>>> Generating Terraform compatible addons CloudFormation\n")
67
+
68
+ output_dir = Path(".").absolute()
69
+ env_path = Path(f"copilot/environments/")
70
+ env_addons_path = env_path / "addons"
71
+ env_overrides_path = env_path / "overrides"
72
+
73
+ self._cleanup_old_files(extensions, output_dir, env_addons_path, env_overrides_path)
74
+ self._generate_env_overrides(output_dir)
75
+
76
+ svc_names = self._list_copilot_local_services()
77
+ base_path = Path(".")
78
+ for svc_name in svc_names:
79
+ self._generate_svc_overrides(base_path, templates, svc_name)
53
80
 
54
- def list_copilot_local_environments(self):
81
+ services = []
82
+ for ext_name, ext_data in extensions.items():
83
+ extension = {**ext_data}
84
+ addon_type = extension.pop("type")
85
+ environments = extension.pop("environments")
86
+ environment_addon_config = {
87
+ "addon_type": addon_type,
88
+ "environments": environments,
89
+ "name": extension.get("name", None) or ext_name,
90
+ "prefix": camel_case(ext_name),
91
+ "secret_name": ext_name.upper().replace("-", "_"),
92
+ **extension,
93
+ }
94
+
95
+ services.append(environment_addon_config)
96
+
97
+ service_addon_config = {
98
+ "application_name": application_name,
99
+ "name": extension.get("name", None) or ext_name,
100
+ "prefix": camel_case(ext_name),
101
+ "environments": environments,
102
+ **extension,
103
+ }
104
+
105
+ log_destination_arns = self._get_log_destination_arn()
106
+
107
+ if addon_type in ["s3", "s3-policy"]:
108
+ if extensions[ext_name].get("serve_static_content"):
109
+ continue
110
+
111
+ s3_kms_arns = self._get_s3_kms_alias_arns(application_name, environments)
112
+ for environment_name in environments:
113
+ environments[environment_name]["kms_key_arn"] = s3_kms_arns.get(
114
+ environment_name, "kms-key-not-found"
115
+ )
116
+
117
+ self._generate_service_addons(
118
+ extension,
119
+ ext_name,
120
+ addon_type,
121
+ output_dir,
122
+ service_addon_config,
123
+ templates,
124
+ log_destination_arns,
125
+ )
126
+
127
+ environments = self.config_provider.apply_environment_defaults(config)["environments"]
128
+
129
+ self.copilot_templating.generate_cross_account_s3_policies(environments, extensions)
130
+
131
+ self.io.info(templates.get_template("addon-instructions.txt").render(services=services))
132
+
133
+ def _list_copilot_local_environments(self):
55
134
  return [
56
135
  path.parent.parts[-1] for path in Path("./copilot/environments/").glob("*/manifest.yml")
57
136
  ]
58
137
 
59
- def is_service(self, path: PosixPath) -> bool:
60
- with open(path) as manifest_file:
61
- data = yaml.safe_load(manifest_file)
62
- if not data or not data.get("type"):
63
- click.echo(
64
- click.style(f"No type defined in manifest file {str(path)}; exiting", fg="red")
65
- )
66
- exit(1)
138
+ def _is_service(self, path: PosixPath) -> bool:
139
+
140
+ manifest_file = self.yaml_file_provider.load(path)
141
+ if not manifest_file or not manifest_file.get("type"):
142
+ self.io.abort_with_error(f"No type defined in manifest file {str(path)}; exiting")
67
143
 
68
- return data.get("type") in self.SERVICE_TYPES
144
+ return manifest_file.get("type") in self.SERVICE_TYPES
69
145
 
70
- def list_copilot_local_services(self):
146
+ def _list_copilot_local_services(self):
71
147
  return [
72
148
  path.parent.parts[-1]
73
149
  for path in Path("./copilot/").glob("*/manifest.yml")
74
- if self.is_service(path)
150
+ if self._is_service(path)
75
151
  ]
76
152
 
77
153
  def _validate_and_normalise_extensions_config(self, config_file, key_in_config_file=None):
@@ -93,12 +169,10 @@ class Copilot:
93
169
  def _normalise_keys(source: dict):
94
170
  return {k.replace("-", "_"): v for k, v in source.items()}
95
171
 
96
- with open(self.PACKAGE_DIR / "addon-plans.yml", "r") as fd:
97
- addon_plans = yaml.safe_load(fd)
172
+ addon_plans = self.yaml_file_provider.load(self.PACKAGE_DIR / "addon-plans.yml")
98
173
 
99
174
  # load and validate config
100
- with open(config_file, "r") as fd:
101
- config = yaml.safe_load(fd)
175
+ config = self.yaml_file_provider.load(config_file)
102
176
 
103
177
  if config and key_in_config_file:
104
178
  config = config[key_in_config_file]
@@ -110,23 +184,19 @@ class Copilot:
110
184
  errors = validate_addons(config)
111
185
 
112
186
  if errors:
113
- click.echo(click.style(f"Errors found in {config_file}:", fg="red"))
187
+ self.io.error(f"Errors found in {config_file}:")
114
188
  for addon, error in errors.items():
115
- click.echo(click.style(f"Addon '{addon}': {error}", fg="red"))
116
- exit(1)
189
+ self.io.error(f"Addon '{addon}': {error}")
190
+ self.io.abort_with_error("Invalid platform-config.yml provided, see above warnings")
117
191
 
118
- env_names = self.list_copilot_local_environments()
119
- svc_names = self.list_copilot_local_services()
192
+ env_names = self._list_copilot_local_environments()
193
+ svc_names = self._list_copilot_local_services()
120
194
 
121
195
  if not env_names:
122
- click.echo(
123
- click.style(f"No environments found in ./copilot/environments; exiting", fg="red")
124
- )
125
- exit(1)
196
+ self.io.abort_with_error("No environments found in ./copilot/environments; exiting")
126
197
 
127
198
  if not svc_names:
128
- click.echo(click.style(f"No services found in ./copilot/; exiting", fg="red"))
129
- exit(1)
199
+ self.io.abort_with_error("No services found in ./copilot/; exiting")
130
200
 
131
201
  normalised_config = {}
132
202
  config_has_errors = False
@@ -139,11 +209,8 @@ class Copilot:
139
209
  normalised_config[addon_name]["services"] = svc_names
140
210
 
141
211
  if not set(normalised_config[addon_name]["services"]).issubset(set(svc_names)):
142
- click.echo(
143
- click.style(
144
- f"Services listed in {addon_name}.services do not exist in ./copilot/",
145
- fg="red",
146
- ),
212
+ self.io.error(
213
+ f"Services listed in {addon_name}.services do not exist in ./copilot/"
147
214
  )
148
215
  config_has_errors = True
149
216
 
@@ -154,18 +221,10 @@ class Copilot:
154
221
 
155
222
  missing_envs = set(environments.keys()) - set(env_names)
156
223
  if missing_envs:
157
- click.echo(
158
- click.style(
159
- f"Environment keys listed in {addon_name} do not match those defined in ./copilot/environments.",
160
- fg="red",
161
- )
162
- ),
163
- click.echo(
164
- click.style(
165
- f" Missing environments: {', '.join(sorted(missing_envs))}",
166
- fg="white",
167
- ),
224
+ self.io.error(
225
+ f"Environment keys listed in {addon_name} do not match those defined in ./copilot/environments"
168
226
  )
227
+ self.io.error(f" Missing environments: {', '.join(sorted(missing_envs))}")
169
228
  config_has_errors = True
170
229
 
171
230
  if config_has_errors:
@@ -186,41 +245,41 @@ class Copilot:
186
245
  normalised_config[addon_name]["environments"] = normalised_environments
187
246
 
188
247
  if config_has_errors:
189
- exit(1)
248
+ self.io.abort_with_error("Configuration has errors. Exiting.")
190
249
 
191
250
  return normalised_config
192
251
 
193
- def get_log_destination_arn(self):
252
+ def _get_log_destination_arn(self):
194
253
  """Get destination arns stored in param store in projects aws
195
254
  account."""
196
- session = get_aws_session_or_abort()
197
- client = session.client("ssm", region_name="eu-west-2")
198
- response = client.get_parameters(Names=["/copilot/tools/central_log_groups"])
199
-
200
- if not response["Parameters"]:
201
- click.echo(
202
- click.style(
203
- "No aws central log group defined in Parameter Store at location /copilot/tools/central_log_groups; exiting",
204
- fg="red",
205
- )
255
+
256
+ try:
257
+ destination_arns = self.parameter_provider.get_ssm_parameter_by_name(
258
+ "/copilot/tools/central_log_groups"
259
+ )
260
+ except botocore.errorfactory.ParameterNotFound:
261
+ self.io.abort_with_error(
262
+ "No aws central log group defined in Parameter Store at location /copilot/tools/central_log_groups; exiting"
206
263
  )
207
- exit(1)
208
264
 
209
- destination_arns = json.loads(response["Parameters"][0]["Value"])
210
- return destination_arns
265
+ return json.loads(destination_arns["Value"])
211
266
 
212
267
  def _generate_svc_overrides(self, base_path, templates, name):
213
- click.echo(f"\n>>> Generating service overrides for {name}\n")
268
+ self.io.info(f"\n>>> Generating service overrides for {name}\n")
214
269
  overrides_path = base_path.joinpath(f"copilot/{name}/overrides")
215
270
  overrides_path.mkdir(parents=True, exist_ok=True)
216
271
  overrides_file = overrides_path.joinpath("cfn.patches.yml")
217
272
  overrides_file.write_text(templates.get_template("svc/overrides/cfn.patches.yml").render())
218
273
 
219
- def _get_s3_kms_alias_arns(self, session, application_name, config):
220
- application = load_application(application_name, session)
274
+ def _get_s3_kms_alias_arns(self, application_name, config):
275
+ application = load_application(application_name, self.session)
221
276
  arns = {}
222
277
 
223
278
  for environment_name in application.environments:
279
+ kms_provider = self.kms_provider(
280
+ application.environments[environment_name].session.client("kms")
281
+ )
282
+
224
283
  if environment_name not in config:
225
284
  continue
226
285
 
@@ -228,99 +287,20 @@ class Copilot:
228
287
  continue
229
288
 
230
289
  bucket_name = config[environment_name]["bucket_name"]
231
- kms_client = application.environments[environment_name].session.client("kms")
232
290
  alias_name = f"alias/{application_name}-{environment_name}-{bucket_name}-key"
233
291
 
234
292
  try:
235
- response = kms_client.describe_key(KeyId=alias_name)
236
- except kms_client.exceptions.NotFoundException:
237
- pass
293
+ response = kms_provider.describe_key(alias_name)
294
+
295
+ # Boto3 classifies all AWS service errors and exceptions as ClientError exceptions
296
+ except botocore.exceptions.ClientError as error:
297
+ if error.response["Error"]["Code"] == "NotFoundException":
298
+ pass
238
299
  else:
239
300
  arns[environment_name] = response["KeyMetadata"]["Arn"]
240
301
 
241
302
  return arns
242
303
 
243
- def make_addons(self):
244
- self.config_provider.config_file_check()
245
- try:
246
- config = self.config_provider.load_and_validate_platform_config()
247
- except SchemaError as ex:
248
- click.secho(f"Invalid `{PLATFORM_CONFIG_FILE}` file: {str(ex)}", fg="red")
249
- raise click.Abort
250
-
251
- templates = setup_templates()
252
- extensions = self._get_extensions()
253
- session = get_aws_session_or_abort()
254
-
255
- application_name = get_application_name()
256
-
257
- click.echo("\n>>> Generating Terraform compatible addons CloudFormation\n")
258
-
259
- output_dir = Path(".").absolute()
260
- env_path = Path(f"copilot/environments/")
261
- env_addons_path = env_path / "addons"
262
- env_overrides_path = env_path / "overrides"
263
-
264
- self._cleanup_old_files(extensions, output_dir, env_addons_path, env_overrides_path)
265
- self._generate_env_overrides(output_dir)
266
-
267
- svc_names = self.list_copilot_local_services()
268
- base_path = Path(".")
269
- for svc_name in svc_names:
270
- self._generate_svc_overrides(base_path, templates, svc_name)
271
-
272
- services = []
273
- for ext_name, ext_data in extensions.items():
274
- extension = {**ext_data}
275
- addon_type = extension.pop("type")
276
- environments = extension.pop("environments")
277
- environment_addon_config = {
278
- "addon_type": addon_type,
279
- "environments": environments,
280
- "name": extension.get("name", None) or ext_name,
281
- "prefix": camel_case(ext_name),
282
- "secret_name": ext_name.upper().replace("-", "_"),
283
- **extension,
284
- }
285
-
286
- services.append(environment_addon_config)
287
-
288
- service_addon_config = {
289
- "application_name": application_name,
290
- "name": extension.get("name", None) or ext_name,
291
- "prefix": camel_case(ext_name),
292
- "environments": environments,
293
- **extension,
294
- }
295
-
296
- log_destination_arns = self.get_log_destination_arn()
297
-
298
- if addon_type in ["s3", "s3-policy"]:
299
- if extensions[ext_name].get("serve_static_content"):
300
- continue
301
-
302
- s3_kms_arns = self._get_s3_kms_alias_arns(session, application_name, environments)
303
- for environment_name in environments:
304
- environments[environment_name]["kms_key_arn"] = s3_kms_arns.get(
305
- environment_name, "kms-key-not-found"
306
- )
307
-
308
- self._generate_service_addons(
309
- extension,
310
- ext_name,
311
- addon_type,
312
- output_dir,
313
- service_addon_config,
314
- templates,
315
- log_destination_arns,
316
- )
317
-
318
- environments = self.config_provider.apply_environment_defaults(config)["environments"]
319
-
320
- self.copilot_templating.generate_cross_account_s3_policies(environments, extensions)
321
-
322
- click.echo(templates.get_template("addon-instructions.txt").render(services=services))
323
-
324
304
  def _get_extensions(self):
325
305
  config = self._validate_and_normalise_extensions_config(
326
306
  self.PACKAGE_DIR / "default-extensions.yml"
@@ -331,13 +311,31 @@ class Copilot:
331
311
  config.update(project_config)
332
312
  return config
333
313
 
314
+ def _generate_override_files(self, base_path, file_path, output_dir):
315
+ def generate_files_for_dir(pattern):
316
+ for file in file_path.glob(pattern):
317
+ if file.is_file():
318
+ contents = file.read_text()
319
+ file_name = str(file).removeprefix(f"{file_path}/")
320
+ self.io.info(
321
+ self.file_provider.mkfile(
322
+ base_path,
323
+ output_dir / file_name,
324
+ contents,
325
+ overwrite=True,
326
+ )
327
+ )
328
+
329
+ generate_files_for_dir("*")
330
+ generate_files_for_dir("bin/*")
331
+
334
332
  def _generate_env_overrides(self, output_dir):
335
333
  path = "templates/env/terraform-overrides"
336
- click.echo("\n>>> Generating Environment overrides\n")
334
+ self.io.info("\n>>> Generating Environment overrides\n")
337
335
  overrides_path = output_dir.joinpath(f"copilot/environments/overrides")
338
336
  overrides_path.mkdir(parents=True, exist_ok=True)
339
337
  template_overrides_path = Path(__file__).parent.parent.joinpath(path)
340
- generate_override_files(Path("."), template_overrides_path, overrides_path)
338
+ self._generate_override_files(Path("."), template_overrides_path, overrides_path)
341
339
 
342
340
  def _generate_service_addons(
343
341
  self,
@@ -364,7 +362,7 @@ class Copilot:
364
362
  )
365
363
 
366
364
  (output_dir / service_path).mkdir(parents=True, exist_ok=True)
367
- click.echo(
365
+ self.io.info(
368
366
  self.file_provider.mkfile(
369
367
  output_dir, service_path / f"{addon_name}.yml", contents, overwrite=True
370
368
  )
@@ -9,11 +9,16 @@ from dbt_platform_helper.providers.semantic_version import (
9
9
  from dbt_platform_helper.providers.semantic_version import (
10
10
  IncompatibleMinorVersionException,
11
11
  )
12
- from dbt_platform_helper.providers.semantic_version import PlatformHelperVersionStatus
13
12
  from dbt_platform_helper.providers.semantic_version import SemanticVersion
13
+ from dbt_platform_helper.providers.version import AWSCLIInstalledVersionProvider
14
+ from dbt_platform_helper.providers.version import CopilotInstalledVersionProvider
14
15
  from dbt_platform_helper.providers.version import DeprecatedVersionFileVersionProvider
16
+ from dbt_platform_helper.providers.version import GithubLatestVersionProvider
15
17
  from dbt_platform_helper.providers.version import InstalledVersionProvider
16
- from dbt_platform_helper.providers.version import PyPiVersionProvider
18
+ from dbt_platform_helper.providers.version import PyPiLatestVersionProvider
19
+ from dbt_platform_helper.providers.version import VersionProvider
20
+ from dbt_platform_helper.providers.version_status import PlatformHelperVersionStatus
21
+ from dbt_platform_helper.providers.version_status import VersionStatus
17
22
  from dbt_platform_helper.providers.yaml_file import YamlFileProvider
18
23
 
19
24
 
@@ -38,14 +43,14 @@ class PlatformHelperVersioning:
38
43
  YamlFileProvider
39
44
  ),
40
45
  config_provider: ConfigProvider = ConfigProvider(),
41
- pypi_provider: PyPiVersionProvider = PyPiVersionProvider,
46
+ latest_version_provider: VersionProvider = PyPiLatestVersionProvider,
42
47
  installed_version_provider: InstalledVersionProvider = InstalledVersionProvider(),
43
48
  skip_versioning_checks: bool = None,
44
49
  ):
45
50
  self.io = io
46
51
  self.version_file_version_provider = version_file_version_provider
47
52
  self.config_provider = config_provider
48
- self.pypi_provider = pypi_provider
53
+ self.latest_version_provider = latest_version_provider
49
54
  self.installed_version_provider = installed_version_provider
50
55
  self.skip_versioning_checks = (
51
56
  skip_versioning_checks if skip_versioning_checks is not None else skip_version_checks()
@@ -100,11 +105,11 @@ class PlatformHelperVersioning:
100
105
  self,
101
106
  include_project_versions: bool = True,
102
107
  ) -> PlatformHelperVersionStatus:
103
- locally_installed_version = self.installed_version_provider.get_installed_tool_version(
108
+ locally_installed_version = self.installed_version_provider.get_semantic_version(
104
109
  "dbt-platform-helper"
105
110
  )
106
111
 
107
- latest_release = self.pypi_provider.get_latest_version("dbt-platform-helper")
112
+ latest_release = self.latest_version_provider.get_semantic_version("dbt-platform-helper")
108
113
 
109
114
  if not include_project_versions:
110
115
  return PlatformHelperVersionStatus(
@@ -129,7 +134,7 @@ class PlatformHelperVersioning:
129
134
  out = PlatformHelperVersionStatus(
130
135
  installed=locally_installed_version,
131
136
  latest=latest_release,
132
- deprecated_version_file=self.version_file_version_provider.get_required_version(),
137
+ deprecated_version_file=self.version_file_version_provider.get_semantic_version(),
133
138
  platform_config_default=platform_config_default,
134
139
  pipeline_overrides=pipeline_overrides,
135
140
  )
@@ -155,3 +160,39 @@ class PlatformHelperVersioning:
155
160
  raise PlatformHelperVersionNotFoundException
156
161
 
157
162
  return out
163
+
164
+
165
+ class AWSVersioning:
166
+ def __init__(
167
+ self,
168
+ latest_version_provider: VersionProvider = None,
169
+ installed_version_provider: VersionProvider = None,
170
+ ):
171
+ self.latest_version_provider = latest_version_provider or GithubLatestVersionProvider
172
+ self.installed_version_provider = (
173
+ installed_version_provider or AWSCLIInstalledVersionProvider
174
+ )
175
+
176
+ def get_version_status(self) -> VersionStatus:
177
+ return VersionStatus(
178
+ self.installed_version_provider.get_semantic_version(),
179
+ self.latest_version_provider.get_semantic_version("aws/aws-cli", True),
180
+ )
181
+
182
+
183
+ class CopilotVersioning:
184
+ def __init__(
185
+ self,
186
+ latest_version_provider: VersionProvider = None,
187
+ installed_version_provider: VersionProvider = None,
188
+ ):
189
+ self.latest_version_provider = latest_version_provider or GithubLatestVersionProvider
190
+ self.installed_version_provider = (
191
+ installed_version_provider or CopilotInstalledVersionProvider
192
+ )
193
+
194
+ def get_version_status(self) -> VersionStatus:
195
+ return VersionStatus(
196
+ self.installed_version_provider.get_semantic_version(),
197
+ self.latest_version_provider.get_semantic_version("aws/copilot-cli"),
198
+ )
File without changes
@@ -13,9 +13,9 @@ class CreateTaskTimeoutException(AWSException):
13
13
 
14
14
 
15
15
  class ImageNotFoundException(AWSException):
16
- def __init__(self, commit: str):
16
+ def __init__(self, image_ref: str):
17
17
  super().__init__(
18
- f"""The commit hash "{commit}" has not been built into an image, try the `platform-helper codebase build` command first."""
18
+ f"""An image labelled "{image_ref}" could not be found in your image repository. Try the `platform-helper codebase build` command first."""
19
19
  )
20
20
 
21
21
 
@@ -35,3 +35,13 @@ class CopilotCodebaseNotFoundException(PlatformException):
35
35
  super().__init__(
36
36
  f"""The codebase "{codebase}" either does not exist or has not been deployed."""
37
37
  )
38
+
39
+
40
+ class CreateAccessTokenException(AWSException):
41
+ def __init__(self, client_id: str):
42
+ super().__init__(f"""Failed to create access token for Client "{client_id}".""")
43
+
44
+
45
+ class UnableToRetrieveSSOAccountList(AWSException):
46
+ def __init__(self):
47
+ super().__init__("Unable to retrieve AWS SSO account list")
@@ -0,0 +1,61 @@
1
+ import botocore
2
+ from boto3 import Session
3
+
4
+ from dbt_platform_helper.providers.aws.exceptions import CreateAccessTokenException
5
+ from dbt_platform_helper.providers.aws.exceptions import UnableToRetrieveSSOAccountList
6
+ from dbt_platform_helper.utils.aws import get_aws_session_or_abort
7
+
8
+
9
+ class SSOAuthProvider:
10
+ def __init__(self, session: Session = None):
11
+ self.session = session
12
+ self.sso_oidc = self._get_client("sso-oidc")
13
+ self.sso = self._get_client("sso")
14
+
15
+ def register(self, client_name, client_type):
16
+ client = self.sso_oidc.register_client(clientName=client_name, clientType=client_type)
17
+ client_id = client.get("clientId")
18
+ client_secret = client.get("clientSecret")
19
+
20
+ return client_id, client_secret
21
+
22
+ def start_device_authorization(self, client_id, client_secret, start_url):
23
+ authz = self.sso_oidc.start_device_authorization(
24
+ clientId=client_id,
25
+ clientSecret=client_secret,
26
+ startUrl=start_url,
27
+ )
28
+ url = authz.get("verificationUriComplete")
29
+ deviceCode = authz.get("deviceCode")
30
+
31
+ return url, deviceCode
32
+
33
+ def create_access_token(self, client_id, client_secret, device_code):
34
+ try:
35
+ response = self.sso_oidc.create_token(
36
+ clientId=client_id,
37
+ clientSecret=client_secret,
38
+ grantType="urn:ietf:params:oauth:grant-type:device_code",
39
+ deviceCode=device_code,
40
+ )
41
+
42
+ return response.get("accessToken")
43
+
44
+ except botocore.exceptions.ClientError as e:
45
+ if e.response["Error"]["Code"] != "AuthorizationPendingException":
46
+ raise CreateAccessTokenException(client_id)
47
+
48
+ def list_accounts(self, access_token, max_results=100):
49
+ aws_accounts_response = self.sso.list_accounts(
50
+ accessToken=access_token,
51
+ maxResults=max_results,
52
+ )
53
+
54
+ if len(aws_accounts_response.get("accountList", [])) == 0:
55
+ raise UnableToRetrieveSSOAccountList()
56
+ return aws_accounts_response.get("accountList")
57
+
58
+ def _get_client(self, client: str):
59
+ if not self.session:
60
+ self.session = get_aws_session_or_abort()
61
+ return self.session.client(client)
@@ -64,7 +64,8 @@ class ConfigProvider:
64
64
  except FileProviderException:
65
65
  return {}
66
66
 
67
- # TODO this general function should be moved out of ConfigProvider
67
+ # TODO remove function and push logic to where this is called.
68
+ # removed usage from config domain, code is very generic and doesn't require the overhead of a function
68
69
  def config_file_check(self, path=PLATFORM_CONFIG_FILE):
69
70
  if not Path(path).exists():
70
71
  self.io.abort_with_error(