dbt-platform-helper 13.0.1__py3-none-any.whl → 13.0.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 (26) hide show
  1. dbt_platform_helper/COMMANDS.md +2 -2
  2. dbt_platform_helper/commands/config.py +26 -33
  3. dbt_platform_helper/commands/generate.py +2 -2
  4. dbt_platform_helper/commands/version.py +30 -30
  5. dbt_platform_helper/domain/config_validator.py +10 -5
  6. dbt_platform_helper/domain/copilot_environment.py +10 -9
  7. dbt_platform_helper/domain/maintenance_page.py +32 -7
  8. dbt_platform_helper/domain/terraform_environment.py +17 -61
  9. dbt_platform_helper/providers/config.py +11 -1
  10. dbt_platform_helper/providers/files.py +13 -12
  11. dbt_platform_helper/providers/semantic_version.py +126 -0
  12. dbt_platform_helper/providers/terraform_manifest.py +117 -26
  13. dbt_platform_helper/providers/validation.py +0 -14
  14. dbt_platform_helper/providers/version.py +36 -0
  15. dbt_platform_helper/providers/yaml_file.py +5 -3
  16. dbt_platform_helper/utils/application.py +3 -2
  17. dbt_platform_helper/utils/versioning.py +152 -221
  18. {dbt_platform_helper-13.0.1.dist-info → dbt_platform_helper-13.0.2.dist-info}/METADATA +1 -1
  19. {dbt_platform_helper-13.0.1.dist-info → dbt_platform_helper-13.0.2.dist-info}/RECORD +23 -24
  20. platform_helper.py +2 -2
  21. dbt_platform_helper/domain/test_platform_terraform_manifest_generator.py +0 -100
  22. dbt_platform_helper/templates/environments/main.tf +0 -46
  23. dbt_platform_helper/utils/platform_config.py +0 -20
  24. {dbt_platform_helper-13.0.1.dist-info → dbt_platform_helper-13.0.2.dist-info}/LICENSE +0 -0
  25. {dbt_platform_helper-13.0.1.dist-info → dbt_platform_helper-13.0.2.dist-info}/WHEEL +0 -0
  26. {dbt_platform_helper-13.0.1.dist-info → dbt_platform_helper-13.0.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,126 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+ from dataclasses import field
4
+ from typing import Dict
5
+ from typing import Optional
6
+ from typing import Union
7
+
8
+ from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
9
+ from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_FILE
10
+ from dbt_platform_helper.providers.validation import ValidationException
11
+
12
+
13
+ class IncompatibleMajorVersionException(ValidationException):
14
+ def __init__(self, app_version: str, check_version: str):
15
+ super().__init__()
16
+ self.app_version = app_version
17
+ self.check_version = check_version
18
+
19
+
20
+ class IncompatibleMinorVersionException(ValidationException):
21
+ def __init__(self, app_version: str, check_version: str):
22
+ super().__init__()
23
+ self.app_version = app_version
24
+ self.check_version = check_version
25
+
26
+
27
+ class SemanticVersion:
28
+ def __init__(self, major, minor, patch):
29
+ self.major = major
30
+ self.minor = minor
31
+ self.patch = patch
32
+
33
+ def __str__(self) -> str:
34
+ if self.major is None:
35
+ return "unknown"
36
+ return ".".join([str(s) for s in [self.major, self.minor, self.patch]])
37
+
38
+ def __lt__(self, other) -> bool:
39
+ return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
40
+
41
+ def __eq__(self, other) -> bool:
42
+ return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
43
+
44
+ def validate_compatibility_with(self, other):
45
+ if (self.major == 0 and other.major == 0) and (
46
+ self.minor != other.minor or self.patch != other.patch
47
+ ):
48
+ raise IncompatibleMajorVersionException(str(self), str(other))
49
+
50
+ if self.major != other.major:
51
+ raise IncompatibleMajorVersionException(str(self), str(other))
52
+
53
+ if self.minor != other.minor:
54
+ raise IncompatibleMinorVersionException(str(self), str(other))
55
+
56
+ @staticmethod
57
+ def from_string(version_string: Union[str, None]):
58
+ if version_string is None:
59
+ return None
60
+
61
+ version_plain = version_string.replace("v", "")
62
+ version_segments = re.split(r"[.\-]", version_plain)
63
+
64
+ if len(version_segments) != 3:
65
+ return None
66
+
67
+ output_version = [0, 0, 0]
68
+ for index, segment in enumerate(version_segments):
69
+ try:
70
+ output_version[index] = int(segment)
71
+ except ValueError:
72
+ output_version[index] = -1
73
+
74
+ return SemanticVersion(output_version[0], output_version[1], output_version[2])
75
+
76
+
77
+ class VersionStatus:
78
+ def __init__(
79
+ self, local_version: SemanticVersion = None, latest_release: SemanticVersion = None
80
+ ):
81
+ self.local = local_version
82
+ self.latest = latest_release
83
+
84
+ def is_outdated(self):
85
+ return self.local != self.latest
86
+
87
+ def warn(self):
88
+ pass
89
+
90
+
91
+ @dataclass
92
+ class PlatformHelperVersionStatus(VersionStatus):
93
+ local: Optional[SemanticVersion] = None
94
+ latest: Optional[SemanticVersion] = None
95
+ deprecated_version_file: Optional[SemanticVersion] = None
96
+ platform_config_default: Optional[SemanticVersion] = None
97
+ pipeline_overrides: Optional[Dict[str, str]] = field(default_factory=dict)
98
+
99
+ def warn(self) -> dict:
100
+ if self.platform_config_default and not self.deprecated_version_file:
101
+ return {}
102
+
103
+ warnings = []
104
+ errors = []
105
+
106
+ missing_default_version_message = f"Create a section in the root of '{PLATFORM_CONFIG_FILE}':\n\ndefault_versions:\n platform-helper: "
107
+ deprecation_message = (
108
+ f"Please delete '{PLATFORM_HELPER_VERSION_FILE}' as it is now deprecated."
109
+ )
110
+
111
+ if self.platform_config_default and self.deprecated_version_file:
112
+ warnings.append(deprecation_message)
113
+
114
+ if not self.platform_config_default and self.deprecated_version_file:
115
+ warnings.append(deprecation_message)
116
+ warnings.append(f"{missing_default_version_message}{self.deprecated_version_file}\n")
117
+
118
+ if not self.platform_config_default and not self.deprecated_version_file:
119
+ message = f"Cannot get dbt-platform-helper version from '{PLATFORM_CONFIG_FILE}'.\n"
120
+ message += f"{missing_default_version_message}{self.local}\n"
121
+ errors.append(message)
122
+
123
+ return {
124
+ "warnings": warnings,
125
+ "errors": errors,
126
+ }
@@ -2,21 +2,20 @@ import json
2
2
  from datetime import datetime
3
3
  from importlib.metadata import version
4
4
  from pathlib import Path
5
- from typing import Callable
6
-
7
- import click
8
5
 
9
6
  from dbt_platform_helper.constants import SUPPORTED_AWS_PROVIDER_VERSION
10
7
  from dbt_platform_helper.constants import SUPPORTED_TERRAFORM_VERSION
8
+ from dbt_platform_helper.providers.config import ConfigProvider
11
9
  from dbt_platform_helper.providers.files import FileProvider
10
+ from dbt_platform_helper.providers.io import ClickIOProvider
12
11
 
13
12
 
14
13
  class TerraformManifestProvider:
15
14
  def __init__(
16
- self, file_provider: FileProvider = FileProvider(), echo: Callable[[str], None] = click.echo
15
+ self, file_provider: FileProvider = FileProvider(), io: ClickIOProvider = ClickIOProvider()
17
16
  ):
18
17
  self.file_provider = file_provider
19
- self.echo = echo
18
+ self.io = io
20
19
 
21
20
  def generate_codebase_pipeline_config(
22
21
  self,
@@ -24,28 +23,50 @@ class TerraformManifestProvider:
24
23
  terraform_platform_modules_version: str,
25
24
  ecr_imports: dict[str, str],
26
25
  ):
27
- default_account = (
28
- platform_config.get("environments", {})
29
- .get("*", {})
30
- .get("accounts", {})
31
- .get("deploy", {})
32
- .get("name")
33
- )
26
+ default_account = self._get_account_for_env("*", platform_config)
27
+ state_key_suffix = f"{platform_config['application']}-codebase-pipelines"
28
+
34
29
  terraform = {}
35
30
  self._add_header(terraform)
36
- self._add_locals(terraform)
31
+ self._add_codebase_pipeline_locals(terraform)
37
32
  self._add_provider(terraform, default_account)
38
- self._add_backend(terraform, platform_config, default_account)
33
+ self._add_backend(terraform, platform_config, default_account, state_key_suffix)
39
34
  self._add_codebase_pipeline_module(terraform, terraform_platform_modules_version)
40
35
  self._add_imports(terraform, ecr_imports)
36
+ self._write_terraform_json(terraform, "terraform/codebase-pipelines")
41
37
 
42
- message = self.file_provider.mkfile(
43
- str(Path(".").absolute()),
44
- "terraform/codebase-pipelines/main.tf.json",
45
- json.dumps(terraform, indent=2),
46
- True,
38
+ def generate_environment_config(
39
+ self,
40
+ platform_config: dict,
41
+ env: str,
42
+ terraform_platform_modules_version: str,
43
+ ):
44
+ platform_config = ConfigProvider.apply_environment_defaults(platform_config)
45
+ account = self._get_account_for_env(env, platform_config)
46
+
47
+ application_name = platform_config["application"]
48
+ state_key_suffix = f"{platform_config['application']}-{env}"
49
+ env_dir = f"terraform/environments/{env}"
50
+
51
+ terraform = {}
52
+ self._add_header(terraform)
53
+ self._add_environment_locals(terraform, application_name)
54
+ self._add_backend(terraform, platform_config, account, state_key_suffix)
55
+ self._add_extensions_module(terraform, terraform_platform_modules_version, env)
56
+ self._add_moved(terraform, platform_config)
57
+ self._ensure_no_hcl_manifest_file(env_dir)
58
+ self._write_terraform_json(terraform, env_dir)
59
+
60
+ @staticmethod
61
+ def _get_account_for_env(env, platform_config):
62
+ account = (
63
+ platform_config.get("environments", {})
64
+ .get(env, {})
65
+ .get("accounts", {})
66
+ .get("deploy", {})
67
+ .get("name")
47
68
  )
48
- self.echo(message)
69
+ return account
49
70
 
50
71
  @staticmethod
51
72
  def _add_header(terraform: dict):
@@ -56,7 +77,7 @@ class TerraformManifestProvider:
56
77
  terraform["//"] = f"{version_header} {warning}"
57
78
 
58
79
  @staticmethod
59
- def _add_locals(terraform: dict):
80
+ def _add_codebase_pipeline_locals(terraform: dict):
60
81
  terraform["locals"] = {
61
82
  "platform_config": '${yamldecode(file("../../platform-config.yml"))}',
62
83
  "application": '${local.platform_config["application"]}',
@@ -73,17 +94,17 @@ class TerraformManifestProvider:
73
94
  terraform["provider"]["aws"]["shared_credentials_files"] = ["~/.aws/config"]
74
95
 
75
96
  @staticmethod
76
- def _add_backend(terraform: dict, platform_config: dict, default_account: str):
97
+ def _add_backend(terraform: dict, platform_config: dict, account: str, state_key_suffix: str):
77
98
  terraform["terraform"] = {
78
99
  "required_version": SUPPORTED_TERRAFORM_VERSION,
79
100
  "backend": {
80
101
  "s3": {
81
- "bucket": f"terraform-platform-state-{default_account}",
82
- "key": f"tfstate/application/{platform_config['application']}-codebase-pipelines.tfstate",
102
+ "bucket": f"terraform-platform-state-{account}",
103
+ "key": f"tfstate/application/{state_key_suffix}.tfstate",
83
104
  "region": "eu-west-2",
84
105
  "encrypt": True,
85
- "kms_key_id": f"alias/terraform-platform-state-s3-key-{default_account}",
86
- "dynamodb_table": f"terraform-platform-lockdb-{default_account}",
106
+ "kms_key_id": f"alias/terraform-platform-state-s3-key-{account}",
107
+ "dynamodb_table": f"terraform-platform-lockdb-{account}",
87
108
  }
88
109
  },
89
110
  "required_providers": {
@@ -110,6 +131,13 @@ class TerraformManifestProvider:
110
131
  }
111
132
  }
112
133
 
134
+ @staticmethod
135
+ def _add_extensions_module(terraform: dict, terraform_platform_modules_version: str, env: str):
136
+ source = f"git::https://github.com/uktrade/terraform-platform-modules.git//extensions?depth=1&ref={terraform_platform_modules_version}"
137
+ terraform["module"] = {
138
+ "extensions": {"source": source, "args": "${local.args}", "environment": env}
139
+ }
140
+
113
141
  @staticmethod
114
142
  def _add_imports(terraform: dict, ecr_imports: dict[str, str]):
115
143
  if ecr_imports:
@@ -118,3 +146,66 @@ class TerraformManifestProvider:
118
146
  "id": "${each.value}",
119
147
  "to": "module.codebase-pipelines[each.key].aws_ecr_repository.this",
120
148
  }
149
+
150
+ @staticmethod
151
+ def _add_environment_locals(terraform: dict, app: str):
152
+ terraform["locals"] = {
153
+ "config": '${yamldecode(file("../../../platform-config.yml"))}',
154
+ "environments": '${local.config["environments"]}',
155
+ "env_config": '${{for name, config in local.environments: name => merge(lookup(local.environments, "*", {}), config)}}',
156
+ "args": {
157
+ "application": app,
158
+ "services": '${local.config["extensions"]}',
159
+ "env_config": "${local.env_config}",
160
+ },
161
+ }
162
+
163
+ @staticmethod
164
+ def _add_moved(terraform, platform_config):
165
+ extensions_comment = "Moved extensions-tf to just extensions - this block tells terraform this. Can be removed once all services have moved to the new naming."
166
+ terraform["moved"] = [
167
+ {
168
+ "//": extensions_comment,
169
+ "from": "module.extensions-tf",
170
+ "to": "module.extensions",
171
+ }
172
+ ]
173
+
174
+ extensions = platform_config.get("extensions", {})
175
+ s3_extension_names = [
176
+ extension_name
177
+ for extension_name, extension in extensions.items()
178
+ if extension["type"] == "s3"
179
+ ]
180
+ s3_comment = "S3 bucket resources are now indexed. Can be removed once all services have moved to terraform-platform-modules 5.x."
181
+
182
+ for name in s3_extension_names:
183
+ resources = [
184
+ "aws_s3_bucket_server_side_encryption_configuration.encryption-config",
185
+ "aws_s3_bucket_policy.bucket-policy",
186
+ "aws_kms_key.kms-key",
187
+ "aws_kms_alias.s3-bucket",
188
+ ]
189
+ moves = [f'module.extensions.module.s3["{name}"].{resource}' for resource in resources]
190
+ for move in moves:
191
+ terraform["moved"].append(
192
+ {
193
+ "//": s3_comment,
194
+ "from": move,
195
+ "to": f"{move}[0]",
196
+ }
197
+ )
198
+
199
+ def _write_terraform_json(self, terraform: dict, env_dir: str):
200
+ message = self.file_provider.mkfile(
201
+ str(Path(env_dir).absolute()),
202
+ "main.tf.json",
203
+ json.dumps(terraform, indent=2),
204
+ True,
205
+ )
206
+ self.io.info(message)
207
+
208
+ def _ensure_no_hcl_manifest_file(self, env_dir):
209
+ message = self.file_provider.delete_file(env_dir, "main.tf")
210
+ if message:
211
+ self.io.info(f"Manifest has moved to main.tf.json. {message}")
@@ -3,17 +3,3 @@ from dbt_platform_helper.platform_exception import PlatformException
3
3
 
4
4
  class ValidationException(PlatformException):
5
5
  pass
6
-
7
-
8
- class IncompatibleMajorVersionException(ValidationException):
9
- def __init__(self, app_version: str, check_version: str):
10
- super().__init__()
11
- self.app_version = app_version
12
- self.check_version = check_version
13
-
14
-
15
- class IncompatibleMinorVersionException(ValidationException):
16
- def __init__(self, app_version: str, check_version: str):
17
- super().__init__()
18
- self.app_version = app_version
19
- self.check_version = check_version
@@ -0,0 +1,36 @@
1
+ from abc import ABC
2
+
3
+ import requests
4
+
5
+ from dbt_platform_helper.providers.semantic_version import SemanticVersion
6
+
7
+
8
+ class VersionProvider(ABC):
9
+ pass
10
+
11
+
12
+ # TODO add timeouts and exception handling for requests
13
+ # TODO Alternatively use the gitpython package?
14
+ class GithubVersionProvider(VersionProvider):
15
+ @staticmethod
16
+ def get_latest_version(repo_name: str, tags: bool = False) -> SemanticVersion:
17
+ if tags:
18
+ tags_list = requests.get(f"https://api.github.com/repos/{repo_name}/tags").json()
19
+ versions = [SemanticVersion.from_string(v["name"]) for v in tags_list]
20
+ versions.sort(reverse=True)
21
+ return versions[0]
22
+
23
+ package_info = requests.get(
24
+ f"https://api.github.com/repos/{repo_name}/releases/latest"
25
+ ).json()
26
+ return SemanticVersion.from_string(package_info["tag_name"])
27
+
28
+
29
+ class PyPiVersionProvider(VersionProvider):
30
+ @staticmethod
31
+ def get_latest_version(project_name: str) -> SemanticVersion:
32
+ package_info = requests.get(f"https://pypi.org/pypi/{project_name}/json").json()
33
+ released_versions = package_info["releases"].keys()
34
+ parsed_released_versions = [SemanticVersion.from_string(v) for v in released_versions]
35
+ parsed_released_versions.sort(reverse=True)
36
+ return parsed_released_versions[0]
@@ -19,11 +19,13 @@ class FileNotFoundException(FileProviderException):
19
19
 
20
20
 
21
21
  class InvalidYamlException(YamlFileProviderException):
22
- pass
22
+ def __init__(self, path: str):
23
+ super().__init__(f"""{path} is not valid YAML.""")
23
24
 
24
25
 
25
26
  class DuplicateKeysException(YamlFileProviderException):
26
- pass
27
+ def __init__(self, duplicate_keys: str):
28
+ super().__init__(f"""Duplicate keys found in your config file: {duplicate_keys}.""")
27
29
 
28
30
 
29
31
  class YamlFileProvider:
@@ -39,7 +41,7 @@ class YamlFileProvider:
39
41
  try:
40
42
  yaml_content = yaml.safe_load(Path(path).read_text())
41
43
  except ParserError:
42
- raise InvalidYamlException(f"{path} is not valid YAML.")
44
+ raise InvalidYamlException(path)
43
45
 
44
46
  if not yaml_content:
45
47
  return {}
@@ -10,11 +10,11 @@ import boto3
10
10
 
11
11
  from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
12
12
  from dbt_platform_helper.platform_exception import PlatformException
13
+ from dbt_platform_helper.providers.config import ConfigProvider
13
14
  from dbt_platform_helper.utils.aws import get_aws_session_or_abort
14
15
  from dbt_platform_helper.utils.aws import get_profile_name_from_account_id
15
16
  from dbt_platform_helper.utils.aws import get_ssm_secrets
16
17
  from dbt_platform_helper.utils.messages import abort_with_error
17
- from dbt_platform_helper.utils.platform_config import load_unvalidated_config_file
18
18
 
19
19
 
20
20
  @dataclass
@@ -125,8 +125,9 @@ def load_application(app=None, default_session=None) -> Application:
125
125
 
126
126
  def get_application_name(abort=abort_with_error):
127
127
  if Path(PLATFORM_CONFIG_FILE).exists():
128
+ config = ConfigProvider()
128
129
  try:
129
- app_config = load_unvalidated_config_file()
130
+ app_config = config.load_unvalidated_config_file()
130
131
  return app_config["application"]
131
132
  except KeyError:
132
133
  abort(