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.
- dbt_platform_helper/COMMANDS.md +7 -2
- dbt_platform_helper/commands/codebase.py +29 -6
- dbt_platform_helper/commands/config.py +12 -314
- dbt_platform_helper/commands/copilot.py +10 -6
- dbt_platform_helper/commands/database.py +17 -9
- dbt_platform_helper/commands/environment.py +2 -3
- dbt_platform_helper/domain/codebase.py +68 -25
- dbt_platform_helper/domain/config.py +345 -0
- dbt_platform_helper/domain/copilot.py +155 -157
- dbt_platform_helper/domain/versioning.py +48 -7
- dbt_platform_helper/providers/aws/__init__.py +0 -0
- dbt_platform_helper/providers/aws/exceptions.py +12 -2
- dbt_platform_helper/providers/aws/sso_auth.py +61 -0
- dbt_platform_helper/providers/config.py +2 -1
- dbt_platform_helper/providers/config_validator.py +15 -13
- dbt_platform_helper/providers/ecr.py +64 -7
- dbt_platform_helper/providers/io.py +2 -2
- dbt_platform_helper/providers/parameter_store.py +47 -0
- dbt_platform_helper/providers/platform_config_schema.py +17 -0
- dbt_platform_helper/providers/semantic_version.py +15 -88
- dbt_platform_helper/providers/terraform_manifest.py +1 -0
- dbt_platform_helper/providers/version.py +82 -24
- dbt_platform_helper/providers/version_status.py +80 -0
- dbt_platform_helper/utils/aws.py +0 -135
- dbt_platform_helper/utils/git.py +3 -1
- dbt_platform_helper/utils/tool_versioning.py +0 -84
- {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/METADATA +2 -2
- {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/RECORD +32 -32
- {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/WHEEL +1 -1
- platform_helper.py +1 -1
- dbt_platform_helper/templates/svc/manifest-backend.yml +0 -69
- dbt_platform_helper/templates/svc/manifest-public.yml +0 -109
- dbt_platform_helper/utils/cloudfoundry.py +0 -14
- dbt_platform_helper/utils/files.py +0 -53
- dbt_platform_helper/utils/manifests.py +0 -18
- {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-13.2.0.dist-info → dbt_platform_helper-13.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -17,7 +17,10 @@ class ConfigValidatorError(PlatformException):
|
|
|
17
17
|
class ConfigValidator:
|
|
18
18
|
|
|
19
19
|
def __init__(
|
|
20
|
-
self,
|
|
20
|
+
self,
|
|
21
|
+
validations: Callable[[dict], None] = None,
|
|
22
|
+
io: ClickIOProvider = ClickIOProvider(),
|
|
23
|
+
session: boto3.Session = None,
|
|
21
24
|
):
|
|
22
25
|
self.validations = validations or [
|
|
23
26
|
self.validate_supported_redis_versions,
|
|
@@ -28,6 +31,7 @@ class ConfigValidator:
|
|
|
28
31
|
self.validate_database_migration_input_sources,
|
|
29
32
|
]
|
|
30
33
|
self.io = io
|
|
34
|
+
self.session = session
|
|
31
35
|
|
|
32
36
|
def run_validations(self, config: dict):
|
|
33
37
|
for validation in self.validations:
|
|
@@ -76,10 +80,15 @@ class ConfigValidator:
|
|
|
76
80
|
f"{extension_type} version for environment {version_failure['environment']} is not in the list of supported {extension_type} versions: {supported_extension_versions}. Provided Version: {version_failure['version']}",
|
|
77
81
|
)
|
|
78
82
|
|
|
83
|
+
def _get_client(self, service_name: str):
|
|
84
|
+
if self.session:
|
|
85
|
+
return self.session.client(service_name)
|
|
86
|
+
return boto3.client(service_name)
|
|
87
|
+
|
|
79
88
|
def validate_supported_redis_versions(self, config):
|
|
80
89
|
return self._validate_extension_supported_versions(
|
|
81
90
|
config=config,
|
|
82
|
-
aws_provider=Redis(
|
|
91
|
+
aws_provider=Redis(self._get_client("elasticache")),
|
|
83
92
|
extension_type="redis", # TODO this is information which can live in the RedisProvider
|
|
84
93
|
version_key="engine", # TODO this is information which can live in the RedisProvider
|
|
85
94
|
)
|
|
@@ -87,7 +96,7 @@ class ConfigValidator:
|
|
|
87
96
|
def validate_supported_opensearch_versions(self, config):
|
|
88
97
|
return self._validate_extension_supported_versions(
|
|
89
98
|
config=config,
|
|
90
|
-
aws_provider=Opensearch(
|
|
99
|
+
aws_provider=Opensearch(self._get_client("opensearch")),
|
|
91
100
|
extension_type="opensearch", # TODO this is information which can live in the OpensearchProvider
|
|
92
101
|
version_key="engine", # TODO this is information which can live in the OpensearchProvider
|
|
93
102
|
)
|
|
@@ -206,21 +215,14 @@ class ConfigValidator:
|
|
|
206
215
|
f"database_copy 'to' parameter must be a valid environment ({all_envs_string}) but was '{to_env}' in extension '{extension_name}'."
|
|
207
216
|
)
|
|
208
217
|
|
|
218
|
+
# TODO - The from_account and to_account properties are deprecated and will be removed when terraform-platform-modules is merged with platform-tools
|
|
209
219
|
if from_account != to_account:
|
|
210
|
-
if "from_account"
|
|
211
|
-
errors.append(
|
|
212
|
-
f"Environments '{from_env}' and '{to_env}' are in different AWS accounts. The 'from_account' parameter must be present."
|
|
213
|
-
)
|
|
214
|
-
elif section["from_account"] != from_account:
|
|
220
|
+
if "from_account" in section and section["from_account"] != from_account:
|
|
215
221
|
errors.append(
|
|
216
222
|
f"Incorrect value for 'from_account' for environment '{from_env}'"
|
|
217
223
|
)
|
|
218
224
|
|
|
219
|
-
if "to_account"
|
|
220
|
-
errors.append(
|
|
221
|
-
f"Environments '{from_env}' and '{to_env}' are in different AWS accounts. The 'to_account' parameter must be present."
|
|
222
|
-
)
|
|
223
|
-
elif section["to_account"] != to_account:
|
|
225
|
+
if "to_account" in section and section["to_account"] != to_account:
|
|
224
226
|
errors.append(
|
|
225
227
|
f"Incorrect value for 'to_account' for environment '{to_env}'"
|
|
226
228
|
)
|
|
@@ -1,20 +1,77 @@
|
|
|
1
|
+
import botocore
|
|
1
2
|
from boto3 import Session
|
|
2
3
|
|
|
4
|
+
from dbt_platform_helper.providers.aws.exceptions import ImageNotFoundException
|
|
5
|
+
from dbt_platform_helper.providers.aws.exceptions import RepositoryNotFoundException
|
|
6
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
7
|
+
from dbt_platform_helper.utils.application import Application
|
|
3
8
|
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
class ECRProvider:
|
|
7
|
-
def __init__(self, session: Session = None):
|
|
12
|
+
def __init__(self, session: Session = None, click_io: ClickIOProvider = ClickIOProvider()):
|
|
8
13
|
self.session = session
|
|
9
|
-
self.
|
|
10
|
-
|
|
11
|
-
def _get_client(self):
|
|
12
|
-
if not self.session:
|
|
13
|
-
self.session = get_aws_session_or_abort()
|
|
14
|
-
return self.session.client("ecr")
|
|
14
|
+
self.click_io = click_io
|
|
15
15
|
|
|
16
16
|
def get_ecr_repo_names(self) -> list[str]:
|
|
17
17
|
out = []
|
|
18
18
|
for page in self._get_client().get_paginator("describe_repositories").paginate():
|
|
19
19
|
out.extend([repo["repositoryName"] for repo in page.get("repositories", {})])
|
|
20
20
|
return out
|
|
21
|
+
|
|
22
|
+
def get_image_details(
|
|
23
|
+
self, application: Application, codebase: str, image_ref: str
|
|
24
|
+
) -> list[dict]:
|
|
25
|
+
"""Check if image exists in AWS ECR, and return a list of dictionaries
|
|
26
|
+
containing image metadata."""
|
|
27
|
+
|
|
28
|
+
repository = f"{application.name}/{codebase}"
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
image_info = self._get_client().describe_images(
|
|
32
|
+
repositoryName=repository,
|
|
33
|
+
imageIds=[{"imageTag": image_ref}],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self._check_image_details_exists(image_info, image_ref)
|
|
37
|
+
|
|
38
|
+
return image_info.get("imageDetails")
|
|
39
|
+
except botocore.exceptions.ClientError as e:
|
|
40
|
+
if e.response["Error"]["Code"] == "ImageNotFoundException":
|
|
41
|
+
raise ImageNotFoundException(image_ref)
|
|
42
|
+
if e.response["Error"]["Code"] == "RepositoryNotFoundException":
|
|
43
|
+
raise RepositoryNotFoundException(repository)
|
|
44
|
+
|
|
45
|
+
def find_commit_tag(self, image_details: list[dict], image_ref: str) -> str:
|
|
46
|
+
"""Loop through imageTags list to query for an image tag starting with
|
|
47
|
+
'commit-', and return that value if found."""
|
|
48
|
+
|
|
49
|
+
if image_ref.startswith("commit-"):
|
|
50
|
+
return image_ref
|
|
51
|
+
|
|
52
|
+
if image_details:
|
|
53
|
+
for image in image_details:
|
|
54
|
+
image_tags = image.get("imageTags", {})
|
|
55
|
+
for tag in image_tags:
|
|
56
|
+
if tag.startswith("commit-"):
|
|
57
|
+
self.click_io.info(
|
|
58
|
+
f'INFO: The tag "{image_ref}" is not a unique, commit-specific tag. Deploying the corresponding commit tag "{tag}" instead.'
|
|
59
|
+
)
|
|
60
|
+
return tag
|
|
61
|
+
self.click_io.warn(
|
|
62
|
+
f'WARNING: The AWS ECR image "{image_ref}" has no associated commit tag so deploying "{image_ref}". Note this could result in images with unintended or incompatible changes being deployed if new ECS Tasks for your service.'
|
|
63
|
+
)
|
|
64
|
+
return image_ref
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _check_image_details_exists(image_info: dict, image_ref: str):
|
|
68
|
+
"""Error handling for any unexpected scenario where AWS ECR returns a
|
|
69
|
+
malformed response."""
|
|
70
|
+
|
|
71
|
+
if "imageDetails" not in image_info:
|
|
72
|
+
raise ImageNotFoundException(image_ref)
|
|
73
|
+
|
|
74
|
+
def _get_client(self):
|
|
75
|
+
if not self.session:
|
|
76
|
+
self.session = get_aws_session_or_abort()
|
|
77
|
+
return self.session.client("ecr")
|
|
@@ -13,8 +13,8 @@ class ClickIOProvider:
|
|
|
13
13
|
def error(self, message: str):
|
|
14
14
|
click.secho(f"Error: {message}", fg="red")
|
|
15
15
|
|
|
16
|
-
def info(self, message: str):
|
|
17
|
-
click.secho(message)
|
|
16
|
+
def info(self, message: str, **kwargs):
|
|
17
|
+
click.secho(message, **kwargs)
|
|
18
18
|
|
|
19
19
|
def input(self, message: str) -> str:
|
|
20
20
|
return click.prompt(message)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
|
|
3
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ParameterStore:
|
|
7
|
+
def __init__(self, ssm_client: boto3.client):
|
|
8
|
+
self.ssm_client = ssm_client
|
|
9
|
+
|
|
10
|
+
def get_ssm_parameter_by_name(self, parameter_name: str) -> dict:
|
|
11
|
+
"""
|
|
12
|
+
Retrieves the latest version of a parameter from parameter store for a
|
|
13
|
+
given name/arn.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
path (str): The parameter name to retrieve the parameter value for.
|
|
17
|
+
Returns:
|
|
18
|
+
dict: A dictionary representation of your ssm parameter
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
return self.ssm_client.get_parameter(Name=parameter_name)["Parameter"]
|
|
22
|
+
|
|
23
|
+
def get_ssm_parameters_by_path(self, path: str) -> list:
|
|
24
|
+
"""
|
|
25
|
+
Retrieves all SSM parameters for a given path from parameter store.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
path (str): The parameter path to retrieve the parameters for. e.g. /copilot/applications/
|
|
29
|
+
Returns:
|
|
30
|
+
list: A list of dictionaries containing all SSM parameters under the provided path.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
parameters = []
|
|
34
|
+
paginator = self.ssm_client.get_paginator("get_parameters_by_path")
|
|
35
|
+
page_iterator = paginator.paginate(Path=path, Recursive=True)
|
|
36
|
+
|
|
37
|
+
for page in page_iterator:
|
|
38
|
+
parameters.extend(page.get("Parameters", []))
|
|
39
|
+
|
|
40
|
+
if parameters:
|
|
41
|
+
return parameters
|
|
42
|
+
else:
|
|
43
|
+
raise ParameterNotFoundForPathException()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ParameterNotFoundForPathException(PlatformException):
|
|
47
|
+
"""Exception raised when no parameters are found for a given path."""
|
|
@@ -28,6 +28,7 @@ class PlatformConfigSchema:
|
|
|
28
28
|
PlatformConfigSchema.__monitoring_schema(),
|
|
29
29
|
PlatformConfigSchema.__opensearch_schema(),
|
|
30
30
|
PlatformConfigSchema.__postgres_schema(),
|
|
31
|
+
PlatformConfigSchema.__datadog_schema(),
|
|
31
32
|
PlatformConfigSchema.__prometheus_policy_schema(),
|
|
32
33
|
PlatformConfigSchema.__redis_schema(),
|
|
33
34
|
PlatformConfigSchema.__s3_bucket_schema(),
|
|
@@ -48,6 +49,7 @@ class PlatformConfigSchema:
|
|
|
48
49
|
"postgres": Schema(PlatformConfigSchema.__postgres_schema()),
|
|
49
50
|
"prometheus-policy": Schema(PlatformConfigSchema.__prometheus_policy_schema()),
|
|
50
51
|
"redis": Schema(PlatformConfigSchema.__redis_schema()),
|
|
52
|
+
"datadog": Schema(PlatformConfigSchema.__datadog_schema()),
|
|
51
53
|
"s3": Schema(PlatformConfigSchema.__s3_bucket_schema()),
|
|
52
54
|
"s3-policy": Schema(PlatformConfigSchema.__s3_bucket_policy_schema()),
|
|
53
55
|
"subscription-filter": PlatformConfigSchema.__no_configuration_required_schema(
|
|
@@ -434,6 +436,21 @@ class PlatformConfigSchema:
|
|
|
434
436
|
|
|
435
437
|
return True
|
|
436
438
|
|
|
439
|
+
@staticmethod
|
|
440
|
+
def __datadog_schema() -> dict:
|
|
441
|
+
return {
|
|
442
|
+
"type": "datadog",
|
|
443
|
+
Optional("environments"): {
|
|
444
|
+
Optional(PlatformConfigSchema.__valid_environment_name()): {
|
|
445
|
+
"team_name": str,
|
|
446
|
+
"contact_name": str,
|
|
447
|
+
"contact_email": str,
|
|
448
|
+
"documentation_url": str,
|
|
449
|
+
"services_to_monitor": list,
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
|
|
437
454
|
@staticmethod
|
|
438
455
|
def __s3_bucket_schema() -> dict:
|
|
439
456
|
def _valid_s3_bucket_arn(key):
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
from dataclasses import field
|
|
4
|
-
from typing import Dict
|
|
5
|
-
from typing import Optional
|
|
6
2
|
from typing import Union
|
|
7
3
|
|
|
8
|
-
from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
|
|
9
|
-
from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_FILE
|
|
10
4
|
from dbt_platform_helper.providers.validation import ValidationException
|
|
11
5
|
|
|
12
6
|
|
|
@@ -42,9 +36,13 @@ class SemanticVersion:
|
|
|
42
36
|
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
|
|
43
37
|
|
|
44
38
|
def __eq__(self, other) -> bool:
|
|
39
|
+
if other is None:
|
|
40
|
+
return False
|
|
45
41
|
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
|
|
46
42
|
|
|
47
43
|
def validate_compatibility_with(self, other):
|
|
44
|
+
if other is None:
|
|
45
|
+
raise ValidationException("Cannot compare NoneType")
|
|
48
46
|
if (self.major == 0 and other.major == 0) and (
|
|
49
47
|
self.minor != other.minor or self.patch != other.patch
|
|
50
48
|
):
|
|
@@ -57,93 +55,22 @@ class SemanticVersion:
|
|
|
57
55
|
raise IncompatibleMinorVersionException(str(self), str(other))
|
|
58
56
|
|
|
59
57
|
@staticmethod
|
|
60
|
-
def
|
|
58
|
+
def _cast_to_int_with_fallback(input, fallback=-1):
|
|
59
|
+
try:
|
|
60
|
+
return int(input)
|
|
61
|
+
except ValueError:
|
|
62
|
+
return fallback
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_string(self, version_string: Union[str, None]):
|
|
61
66
|
if version_string is None:
|
|
62
67
|
return None
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
version_segments = re.split(r"[.\-]", version_plain)
|
|
69
|
+
version_segments = re.split(r"[.\-]", version_string.replace("v", ""))
|
|
66
70
|
|
|
67
71
|
if len(version_segments) != 3:
|
|
68
72
|
return None
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
for index, segment in enumerate(version_segments):
|
|
72
|
-
try:
|
|
73
|
-
output_version[index] = int(segment)
|
|
74
|
-
except ValueError:
|
|
75
|
-
output_version[index] = -1
|
|
74
|
+
major, minor, patch = [self._cast_to_int_with_fallback(s) for s in version_segments]
|
|
76
75
|
|
|
77
|
-
return SemanticVersion(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@dataclass
|
|
81
|
-
class VersionStatus:
|
|
82
|
-
installed: SemanticVersion = None
|
|
83
|
-
latest: SemanticVersion = None
|
|
84
|
-
|
|
85
|
-
def __str__(self):
|
|
86
|
-
attrs = {
|
|
87
|
-
key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
|
|
88
|
-
}
|
|
89
|
-
attrs_str = ", ".join(f"{key}: {value}" for key, value in attrs.items())
|
|
90
|
-
return f"{self.__class__.__name__}: {attrs_str}"
|
|
91
|
-
|
|
92
|
-
def is_outdated(self):
|
|
93
|
-
return self.installed != self.latest
|
|
94
|
-
|
|
95
|
-
def validate(self):
|
|
96
|
-
pass
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@dataclass
|
|
100
|
-
class PlatformHelperVersionStatus(VersionStatus):
|
|
101
|
-
installed: Optional[SemanticVersion] = None
|
|
102
|
-
latest: Optional[SemanticVersion] = None
|
|
103
|
-
deprecated_version_file: Optional[SemanticVersion] = None
|
|
104
|
-
platform_config_default: Optional[SemanticVersion] = None
|
|
105
|
-
pipeline_overrides: Optional[Dict[str, str]] = field(default_factory=dict)
|
|
106
|
-
|
|
107
|
-
def __str__(self):
|
|
108
|
-
semantic_version_attrs = {
|
|
109
|
-
key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
class_str = ", ".join(f"{key}: {value}" for key, value in semantic_version_attrs.items())
|
|
113
|
-
|
|
114
|
-
if self.pipeline_overrides.items():
|
|
115
|
-
pipeline_overrides_str = "pipeline_overrides: " + ", ".join(
|
|
116
|
-
f"{key}: {value}" for key, value in self.pipeline_overrides.items()
|
|
117
|
-
)
|
|
118
|
-
class_str = ", ".join([class_str, pipeline_overrides_str])
|
|
119
|
-
|
|
120
|
-
return f"{self.__class__.__name__}: {class_str}"
|
|
121
|
-
|
|
122
|
-
def validate(self) -> dict:
|
|
123
|
-
if self.platform_config_default and not self.deprecated_version_file:
|
|
124
|
-
return {}
|
|
125
|
-
|
|
126
|
-
warnings = []
|
|
127
|
-
errors = []
|
|
128
|
-
|
|
129
|
-
missing_default_version_message = f"Create a section in the root of '{PLATFORM_CONFIG_FILE}':\n\ndefault_versions:\n platform-helper: "
|
|
130
|
-
deprecation_message = (
|
|
131
|
-
f"Please delete '{PLATFORM_HELPER_VERSION_FILE}' as it is now deprecated."
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
if self.platform_config_default and self.deprecated_version_file:
|
|
135
|
-
warnings.append(deprecation_message)
|
|
136
|
-
|
|
137
|
-
if not self.platform_config_default and self.deprecated_version_file:
|
|
138
|
-
warnings.append(deprecation_message)
|
|
139
|
-
warnings.append(f"{missing_default_version_message}{self.deprecated_version_file}\n")
|
|
140
|
-
|
|
141
|
-
if not self.platform_config_default and not self.deprecated_version_file:
|
|
142
|
-
message = f"Cannot get dbt-platform-helper version from '{PLATFORM_CONFIG_FILE}'.\n"
|
|
143
|
-
message += f"{missing_default_version_message}{self.installed}\n"
|
|
144
|
-
errors.append(message)
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
"warnings": warnings,
|
|
148
|
-
"errors": errors,
|
|
149
|
-
}
|
|
76
|
+
return SemanticVersion(major, minor, patch)
|
|
@@ -128,6 +128,7 @@ class TerraformManifestProvider:
|
|
|
128
128
|
"codebase": "${each.key}",
|
|
129
129
|
"repository": "${each.value.repository}",
|
|
130
130
|
"deploy_repository": f"{deploy_repository}",
|
|
131
|
+
"deploy_repository_branch": '${lookup(each.value, "deploy_repository_branch", "main")}',
|
|
131
132
|
"additional_ecr_repository": '${lookup(each.value, "additional_ecr_repository", null)}',
|
|
132
133
|
"pipelines": '${lookup(each.value, "pipelines", [])}',
|
|
133
134
|
"services": "${each.value.services}",
|
|
@@ -1,17 +1,31 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
1
3
|
from abc import ABC
|
|
4
|
+
from abc import abstractmethod
|
|
2
5
|
from importlib.metadata import PackageNotFoundError
|
|
3
6
|
from importlib.metadata import version
|
|
4
7
|
from pathlib import Path
|
|
8
|
+
from typing import Union
|
|
5
9
|
|
|
6
|
-
import
|
|
10
|
+
from requests import Session
|
|
11
|
+
from requests.adapters import HTTPAdapter
|
|
12
|
+
from urllib3.util import Retry
|
|
7
13
|
|
|
8
14
|
from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_FILE
|
|
9
15
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
16
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
10
17
|
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
11
18
|
from dbt_platform_helper.providers.yaml_file import FileProviderException
|
|
12
19
|
from dbt_platform_helper.providers.yaml_file import YamlFileProvider
|
|
13
20
|
|
|
14
21
|
|
|
22
|
+
def set_up_retry():
|
|
23
|
+
session = Session()
|
|
24
|
+
retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[403, 500, 502, 503, 504])
|
|
25
|
+
session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
26
|
+
return session
|
|
27
|
+
|
|
28
|
+
|
|
15
29
|
class InstalledVersionProviderException(PlatformException):
|
|
16
30
|
pass
|
|
17
31
|
|
|
@@ -25,50 +39,67 @@ class InstalledToolNotFoundException(InstalledVersionProviderException):
|
|
|
25
39
|
|
|
26
40
|
|
|
27
41
|
class VersionProvider(ABC):
|
|
28
|
-
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get_semantic_version() -> SemanticVersion:
|
|
44
|
+
raise NotImplementedError("Must be implemented in subclasses")
|
|
29
45
|
|
|
30
46
|
|
|
31
47
|
class InstalledVersionProvider:
|
|
32
48
|
@staticmethod
|
|
33
|
-
def
|
|
49
|
+
def get_semantic_version(tool_name: str) -> SemanticVersion:
|
|
34
50
|
try:
|
|
35
51
|
return SemanticVersion.from_string(version(tool_name))
|
|
36
52
|
except PackageNotFoundError:
|
|
37
53
|
raise InstalledToolNotFoundException(tool_name)
|
|
38
54
|
|
|
39
55
|
|
|
40
|
-
|
|
41
|
-
# TODO Alternatively use the gitpython package?
|
|
42
|
-
class GithubVersionProvider(VersionProvider):
|
|
56
|
+
class GithubLatestVersionProvider(VersionProvider):
|
|
43
57
|
@staticmethod
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
def get_semantic_version(
|
|
59
|
+
repo_name: str, tags: bool = False, request_session=set_up_retry(), io=ClickIOProvider()
|
|
60
|
+
) -> Union[SemanticVersion, None]:
|
|
61
|
+
|
|
62
|
+
semantic_version = None
|
|
63
|
+
try:
|
|
64
|
+
if tags:
|
|
65
|
+
response = request_session.get(f"https://api.github.com/repos/{repo_name}/tags")
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
versions = [SemanticVersion.from_string(v["name"]) for v in response.json()]
|
|
68
|
+
versions.sort(reverse=True)
|
|
69
|
+
semantic_version = versions[0]
|
|
70
|
+
else:
|
|
71
|
+
package_info = request_session.get(
|
|
72
|
+
f"https://api.github.com/repos/{repo_name}/releases/latest"
|
|
73
|
+
).json()
|
|
74
|
+
semantic_version = SemanticVersion.from_string(package_info["tag_name"])
|
|
75
|
+
except Exception as e:
|
|
76
|
+
io.error(f"Exception occured when calling Github with:\n{str(e)}")
|
|
55
77
|
|
|
78
|
+
return semantic_version
|
|
56
79
|
|
|
57
|
-
|
|
80
|
+
|
|
81
|
+
class PyPiLatestVersionProvider(VersionProvider):
|
|
58
82
|
@staticmethod
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
def get_semantic_version(
|
|
84
|
+
project_name: str, request_session=set_up_retry(), io=ClickIOProvider()
|
|
85
|
+
) -> Union[SemanticVersion, None]:
|
|
86
|
+
semantic_version = None
|
|
87
|
+
try:
|
|
88
|
+
package_info = request_session.get(f"https://pypi.org/pypi/{project_name}/json").json()
|
|
89
|
+
released_versions = package_info["releases"].keys()
|
|
90
|
+
parsed_released_versions = [SemanticVersion.from_string(v) for v in released_versions]
|
|
91
|
+
parsed_released_versions.sort(reverse=True)
|
|
92
|
+
semantic_version = parsed_released_versions[0]
|
|
93
|
+
except Exception as e:
|
|
94
|
+
io.error(f"Exception occured when calling PyPi with:\n{str(e)}")
|
|
95
|
+
return semantic_version
|
|
65
96
|
|
|
66
97
|
|
|
67
98
|
class DeprecatedVersionFileVersionProvider(VersionProvider):
|
|
68
99
|
def __init__(self, file_provider: YamlFileProvider):
|
|
69
100
|
self.file_provider = file_provider or YamlFileProvider
|
|
70
101
|
|
|
71
|
-
def
|
|
102
|
+
def get_semantic_version(self) -> Union[SemanticVersion, None]:
|
|
72
103
|
deprecated_version_file = Path(PLATFORM_HELPER_VERSION_FILE)
|
|
73
104
|
try:
|
|
74
105
|
loaded_version = self.file_provider.load(deprecated_version_file)
|
|
@@ -76,3 +107,30 @@ class DeprecatedVersionFileVersionProvider(VersionProvider):
|
|
|
76
107
|
except FileProviderException:
|
|
77
108
|
version_from_file = None
|
|
78
109
|
return version_from_file
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AWSCLIInstalledVersionProvider(VersionProvider):
|
|
113
|
+
@staticmethod
|
|
114
|
+
def get_semantic_version() -> Union[SemanticVersion, None]:
|
|
115
|
+
installed_aws_version = None
|
|
116
|
+
try:
|
|
117
|
+
response = subprocess.run("aws --version", capture_output=True, shell=True)
|
|
118
|
+
matched = re.match(r"aws-cli/([0-9.]+)", response.stdout.decode("utf8"))
|
|
119
|
+
installed_aws_version = matched.group(1)
|
|
120
|
+
except (ValueError, AttributeError):
|
|
121
|
+
pass
|
|
122
|
+
return SemanticVersion.from_string(installed_aws_version)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class CopilotInstalledVersionProvider(VersionProvider):
|
|
126
|
+
@staticmethod
|
|
127
|
+
def get_semantic_version() -> Union[SemanticVersion, None]:
|
|
128
|
+
copilot_version = None
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
response = subprocess.run("copilot --version", capture_output=True, shell=True)
|
|
132
|
+
[copilot_version] = re.findall(r"[0-9.]+", response.stdout.decode("utf8"))
|
|
133
|
+
except ValueError:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
return SemanticVersion.from_string(copilot_version)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from dataclasses import field
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
|
|
7
|
+
from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_FILE
|
|
8
|
+
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class VersionStatus:
|
|
13
|
+
installed: SemanticVersion = None
|
|
14
|
+
latest: SemanticVersion = None
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
attrs = {
|
|
18
|
+
key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
|
|
19
|
+
}
|
|
20
|
+
attrs_str = ", ".join(f"{key}: {value}" for key, value in attrs.items())
|
|
21
|
+
return f"{self.__class__.__name__}: {attrs_str}"
|
|
22
|
+
|
|
23
|
+
def is_outdated(self):
|
|
24
|
+
return self.installed != self.latest
|
|
25
|
+
|
|
26
|
+
def validate(self):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PlatformHelperVersionStatus(VersionStatus):
|
|
32
|
+
installed: Optional[SemanticVersion] = None
|
|
33
|
+
latest: Optional[SemanticVersion] = None
|
|
34
|
+
deprecated_version_file: Optional[SemanticVersion] = None
|
|
35
|
+
platform_config_default: Optional[SemanticVersion] = None
|
|
36
|
+
pipeline_overrides: Optional[Dict[str, str]] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
semantic_version_attrs = {
|
|
40
|
+
key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class_str = ", ".join(f"{key}: {value}" for key, value in semantic_version_attrs.items())
|
|
44
|
+
|
|
45
|
+
if self.pipeline_overrides.items():
|
|
46
|
+
pipeline_overrides_str = "pipeline_overrides: " + ", ".join(
|
|
47
|
+
f"{key}: {value}" for key, value in self.pipeline_overrides.items()
|
|
48
|
+
)
|
|
49
|
+
class_str = ", ".join([class_str, pipeline_overrides_str])
|
|
50
|
+
|
|
51
|
+
return f"{self.__class__.__name__}: {class_str}"
|
|
52
|
+
|
|
53
|
+
def validate(self) -> dict:
|
|
54
|
+
if self.platform_config_default and not self.deprecated_version_file:
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
warnings = []
|
|
58
|
+
errors = []
|
|
59
|
+
|
|
60
|
+
missing_default_version_message = f"Create a section in the root of '{PLATFORM_CONFIG_FILE}':\n\ndefault_versions:\n platform-helper: "
|
|
61
|
+
deprecation_message = (
|
|
62
|
+
f"Please delete '{PLATFORM_HELPER_VERSION_FILE}' as it is now deprecated."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if self.platform_config_default and self.deprecated_version_file:
|
|
66
|
+
warnings.append(deprecation_message)
|
|
67
|
+
|
|
68
|
+
if not self.platform_config_default and self.deprecated_version_file:
|
|
69
|
+
warnings.append(deprecation_message)
|
|
70
|
+
warnings.append(f"{missing_default_version_message}{self.deprecated_version_file}\n")
|
|
71
|
+
|
|
72
|
+
if not self.platform_config_default and not self.deprecated_version_file:
|
|
73
|
+
message = f"Cannot get dbt-platform-helper version from '{PLATFORM_CONFIG_FILE}'.\n"
|
|
74
|
+
message += f"{missing_default_version_message}{self.installed}\n"
|
|
75
|
+
errors.append(message)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"warnings": warnings,
|
|
79
|
+
"errors": errors,
|
|
80
|
+
}
|