dbt-platform-helper 12.1.0__tar.gz → 12.2.0__tar.gz

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 (92) hide show
  1. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/PKG-INFO +2 -1
  2. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/COMMANDS.md +5 -4
  3. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/application.py +1 -0
  4. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/codebase.py +3 -2
  5. dbt_platform_helper-12.2.0/dbt_platform_helper/commands/conduit.py +78 -0
  6. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/secrets.py +1 -1
  7. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/constants.py +12 -2
  8. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/domain/codebase.py +1 -1
  9. dbt_platform_helper-12.2.0/dbt_platform_helper/domain/conduit.py +172 -0
  10. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/exceptions.py +33 -0
  11. dbt_platform_helper-12.2.0/dbt_platform_helper/providers/cloudformation.py +105 -0
  12. dbt_platform_helper-12.2.0/dbt_platform_helper/providers/copilot.py +144 -0
  13. dbt_platform_helper-12.2.0/dbt_platform_helper/providers/ecs.py +78 -0
  14. dbt_platform_helper-12.2.0/dbt_platform_helper/providers/secrets.py +85 -0
  15. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/addons/svc/prometheus-policy.yml +2 -0
  16. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/environments/manifest.yml +0 -1
  17. dbt_platform_helper-12.2.0/dbt_platform_helper/utils/__init__.py +0 -0
  18. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/validation.py +22 -1
  19. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/pyproject.toml +1 -1
  20. dbt_platform_helper-12.1.0/dbt_platform_helper/commands/conduit.py +0 -453
  21. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/LICENSE +0 -0
  22. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/README.md +0 -0
  23. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/__init__.py +0 -0
  24. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/addon-plans.yml +0 -0
  25. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/addons-template-map.yml +0 -0
  26. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/__init__.py +0 -0
  27. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/config.py +0 -0
  28. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/copilot.py +0 -0
  29. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/database.py +0 -0
  30. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/environment.py +0 -0
  31. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/generate.py +0 -0
  32. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/notify.py +0 -0
  33. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/pipeline.py +0 -0
  34. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/commands/version.py +0 -0
  35. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/default-extensions.yml +0 -0
  36. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/domain/__init__.py +0 -0
  37. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/domain/database_copy.py +0 -0
  38. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/domain/maintenance_page.py +0 -0
  39. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/jinja2_tags.py +0 -0
  40. {dbt_platform_helper-12.1.0/dbt_platform_helper/utils → dbt_platform_helper-12.2.0/dbt_platform_helper/providers}/__init__.py +0 -0
  41. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/providers/load_balancers.py +0 -0
  42. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/.copilot/config.yml +0 -0
  43. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/.copilot/image_build_run.sh +0 -0
  44. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/.copilot/phases/build.sh +0 -0
  45. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/.copilot/phases/install.sh +0 -0
  46. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/.copilot/phases/post_build.sh +0 -0
  47. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/.copilot/phases/pre_build.sh +0 -0
  48. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/COMMANDS.md.jinja +0 -0
  49. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/addon-instructions.txt +0 -0
  50. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/addons/README.md +0 -0
  51. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/addons/svc/appconfig-ipfilter.yml +0 -0
  52. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/addons/svc/s3-policy.yml +0 -0
  53. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/addons/svc/subscription-filter.yml +0 -0
  54. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/ci-codebuild-role-policy.json +0 -0
  55. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/create-codebuild-role.json +0 -0
  56. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/custom-codebuild-role-policy.json +0 -0
  57. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/env/manifest.yml +0 -0
  58. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/env/terraform-overrides/cfn.patches.yml +0 -0
  59. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/environment-pipelines/main.tf +0 -0
  60. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/environments/main.tf +0 -0
  61. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/manifest.yml +0 -0
  62. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/.gitignore +0 -0
  63. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/bin/override.ts +0 -0
  64. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/buildspec.deploy.yml +0 -0
  65. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/buildspec.image.yml +0 -0
  66. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/cdk.json +0 -0
  67. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/package-lock.json +0 -0
  68. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/package.json +0 -0
  69. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/stack.ts +0 -0
  70. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/tsconfig.json +0 -0
  71. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/codebase/overrides/types.ts +0 -0
  72. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/environments/buildspec.yml +0 -0
  73. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/pipelines/environments/overrides/cfn.patches.yml +0 -0
  74. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/svc/maintenance_pages/default.html +0 -0
  75. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/svc/maintenance_pages/dmas-migration.html +0 -0
  76. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/svc/maintenance_pages/migration.html +0 -0
  77. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/svc/manifest-backend.yml +0 -0
  78. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/svc/manifest-public.yml +0 -0
  79. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +0 -0
  80. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/application.py +0 -0
  81. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/arn_parser.py +0 -0
  82. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/aws.py +0 -0
  83. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/click.py +0 -0
  84. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/cloudfoundry.py +0 -0
  85. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/files.py +0 -0
  86. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/git.py +0 -0
  87. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/manifests.py +0 -0
  88. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/messages.py +0 -0
  89. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/platform_config.py +0 -0
  90. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/template.py +0 -0
  91. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/dbt_platform_helper/utils/versioning.py +0 -0
  92. {dbt_platform_helper-12.1.0 → dbt_platform_helper-12.2.0}/platform_helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbt-platform-helper
3
- Version: 12.1.0
3
+ Version: 12.2.0
4
4
  Summary: Set of tools to help transfer applications/services from GOV.UK PaaS to DBT PaaS augmenting AWS Copilot.
5
5
  License: MIT
6
6
  Author: Department for Business and Trade Platform Team
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.9
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
15
16
  Requires-Dist: Jinja2 (==3.1.4)
16
17
  Requires-Dist: PyYAML (==6.0.1)
17
18
  Requires-Dist: aiohttp (>=3.8.4,<4.0.0)
@@ -256,7 +256,8 @@ platform-helper codebase deploy --app <application> --env <environment> --codeba
256
256
 
257
257
  [↩ Parent](#platform-helper)
258
258
 
259
- Create a conduit connection to an addon.
259
+ Opens a shell for a given addon_name create a conduit connection to
260
+ interact with postgres, opensearch or redis.
260
261
 
261
262
  ## Usage
262
263
 
@@ -272,11 +273,11 @@ platform-helper conduit <addon_name>
272
273
  ## Options
273
274
 
274
275
  - `--app <text>`
275
- - AWS application name
276
+ - Application name
276
277
  - `--env <text>`
277
- - AWS environment name
278
+ - Environment name
278
279
  - `--access <choice>` _Defaults to read._
279
- - Allow write or admin access to database addons
280
+ - Allow read, write or admin access to the database addons.
280
281
  - `--help <boolean>` _Defaults to False._
281
282
  - Show this message and exit.
282
283
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
 
3
3
  # application commands are deprecated, do not spend time refactoring them
4
+ # Service teams are trained to use them as a replacement for cf app(s)
4
5
 
5
6
  import time
6
7
  from datetime import datetime
@@ -30,7 +30,7 @@ def prepare():
30
30
  try:
31
31
  Codebase().prepare()
32
32
  except NotInCodeBaseRepositoryError:
33
- # TODO print error attached to exception
33
+ # TODO: Set exception message in the exceptions and just output the message in the command code
34
34
  click.secho(
35
35
  "You are in the deploy repository; make sure you are in the application codebase repository.",
36
36
  fg="red",
@@ -109,6 +109,7 @@ def deploy(app, env, codebase, commit):
109
109
  try:
110
110
  Codebase().deploy(app, env, codebase, commit)
111
111
  except ApplicationNotFoundError:
112
+ # TODO: Set exception message in the exceptions and just output the message in the command code
112
113
  click.secho(
113
114
  f"""The account "{os.environ.get("AWS_PROFILE")}" does not contain the application "{app}"; ensure you have set the environment variable "AWS_PROFILE" correctly.""",
114
115
  fg="red",
@@ -120,9 +121,9 @@ def deploy(app, env, codebase, commit):
120
121
  fg="red",
121
122
  )
122
123
  raise click.Abort
123
- # TODO: don't hide json decode error
124
124
  except (
125
125
  CopilotCodebaseNotFoundError,
126
+ # TODO: Catch this error earlier and throw a more meaningful error, maybe it's CopilotCodebaseNotFoundError?
126
127
  json.JSONDecodeError,
127
128
  ):
128
129
  click.secho(
@@ -0,0 +1,78 @@
1
+ import click
2
+
3
+ from dbt_platform_helper.constants import CONDUIT_ADDON_TYPES
4
+ from dbt_platform_helper.domain.conduit import Conduit
5
+ from dbt_platform_helper.exceptions import AddonNotFoundError
6
+ from dbt_platform_helper.exceptions import AddonTypeMissingFromConfigError
7
+ from dbt_platform_helper.exceptions import CreateTaskTimeoutError
8
+ from dbt_platform_helper.exceptions import InvalidAddonTypeError
9
+ from dbt_platform_helper.exceptions import NoClusterError
10
+ from dbt_platform_helper.exceptions import ParameterNotFoundError
11
+ from dbt_platform_helper.providers.secrets import SecretNotFoundError
12
+ from dbt_platform_helper.utils.application import load_application
13
+ from dbt_platform_helper.utils.click import ClickDocOptCommand
14
+ from dbt_platform_helper.utils.versioning import (
15
+ check_platform_helper_version_needs_update,
16
+ )
17
+
18
+ CONDUIT_ACCESS_OPTIONS = ["read", "write", "admin"]
19
+
20
+
21
+ @click.command(cls=ClickDocOptCommand)
22
+ @click.argument("addon_name", type=str, required=True)
23
+ @click.option("--app", help="Application name", required=True)
24
+ @click.option("--env", help="Environment name", required=True)
25
+ @click.option(
26
+ "--access",
27
+ default="read",
28
+ type=click.Choice(CONDUIT_ACCESS_OPTIONS),
29
+ help="Allow read, write or admin access to the database addons.",
30
+ )
31
+ def conduit(addon_name: str, app: str, env: str, access: str):
32
+ """Opens a shell for a given addon_name create a conduit connection to
33
+ interact with postgres, opensearch or redis."""
34
+ check_platform_helper_version_needs_update()
35
+ application = load_application(app)
36
+
37
+ try:
38
+ Conduit(application).start(env, addon_name, access)
39
+ except NoClusterError:
40
+ # TODO: Set exception message in the exceptions and just output the message in the command code, should be able to catch all errors in one block
41
+ click.secho(f"""No ECS cluster found for "{app}" in "{env}" environment.""", fg="red")
42
+ exit(1)
43
+ except SecretNotFoundError as err:
44
+ click.secho(
45
+ f"""No secret called "{err}" for "{app}" in "{env}" environment.""",
46
+ fg="red",
47
+ )
48
+ exit(1)
49
+ except CreateTaskTimeoutError:
50
+ click.secho(
51
+ f"""Client ({addon_name}) ECS task has failed to start for "{app}" in "{env}" environment.""",
52
+ fg="red",
53
+ )
54
+ exit(1)
55
+ except ParameterNotFoundError:
56
+ click.secho(
57
+ f"""No parameter called "/copilot/applications/{app}/environments/{env}/addons". Try deploying the "{app}" "{env}" environment.""",
58
+ fg="red",
59
+ )
60
+ exit(1)
61
+ except AddonNotFoundError:
62
+ click.secho(
63
+ f"""Addon "{addon_name}" does not exist.""",
64
+ fg="red",
65
+ )
66
+ exit(1)
67
+ except InvalidAddonTypeError as err:
68
+ click.secho(
69
+ f"""Addon type "{err.addon_type}" is not supported, we support: {", ".join(CONDUIT_ADDON_TYPES)}.""",
70
+ fg="red",
71
+ )
72
+ exit(1)
73
+ except AddonTypeMissingFromConfigError:
74
+ click.secho(
75
+ f"""The configuration for the addon {addon_name}, is missconfigured and missing the addon type.""",
76
+ fg="red",
77
+ )
78
+ exit(1)
@@ -102,7 +102,7 @@ def list(app, env):
102
102
  params = dict(Path=path, Recursive=False, WithDecryption=True, MaxResults=10)
103
103
  secrets = []
104
104
 
105
- # TODO: refactor shared code with get_ssm_secret_names
105
+ # TODO: refactor shared code with get_ssm_secret_names - Check if this is still valid
106
106
  while True:
107
107
  response = client.get_parameters_by_path(**params)
108
108
 
@@ -1,6 +1,16 @@
1
1
  PLATFORM_CONFIG_FILE = "platform-config.yml"
2
2
  PLATFORM_HELPER_VERSION_FILE = ".platform-helper-version"
3
- CODEBASE_PIPELINES_KEY = "codebase_pipelines"
4
- ENVIRONMENTS_KEY = "environments"
5
3
  DEFAULT_TERRAFORM_PLATFORM_MODULES_VERSION = "5"
6
4
  PLATFORM_HELPER_CACHE_FILE = ".platform-helper-config-cache.yml"
5
+
6
+ # Keys
7
+ CODEBASE_PIPELINES_KEY = "codebase_pipelines"
8
+ ENVIRONMENTS_KEY = "environments"
9
+
10
+ # Conduit
11
+ CONDUIT_ADDON_TYPES = [
12
+ "opensearch",
13
+ "postgres",
14
+ "redis",
15
+ ]
16
+ CONDUIT_DOCKER_IMAGE_LOCATION = "public.ecr.aws/uktrade/tunnel"
@@ -195,7 +195,6 @@ class Codebase:
195
195
 
196
196
  self.echo_fn("")
197
197
 
198
- # TODO return empty list without exception
199
198
  def __get_codebases(self, application, ssm_client):
200
199
  parameters = ssm_client.get_parameters_by_path(
201
200
  Path=f"/copilot/applications/{application.name}/codebases",
@@ -205,6 +204,7 @@ class Codebase:
205
204
  codebases = [json.loads(p["Value"]) for p in parameters]
206
205
 
207
206
  if not codebases:
207
+ # TODO Is this really an error? Or just no codebases so we could return an empty list?
208
208
  raise NoCopilotCodebasesFoundError
209
209
  return codebases
210
210
 
@@ -0,0 +1,172 @@
1
+ import subprocess
2
+ from collections.abc import Callable
3
+
4
+ import click
5
+
6
+ from dbt_platform_helper.exceptions import ECSAgentNotRunning
7
+ from dbt_platform_helper.providers.cloudformation import (
8
+ add_stack_delete_policy_to_task_role,
9
+ )
10
+ from dbt_platform_helper.providers.cloudformation import update_conduit_stack_resources
11
+ from dbt_platform_helper.providers.cloudformation import (
12
+ wait_for_cloudformation_to_reach_status,
13
+ )
14
+ from dbt_platform_helper.providers.copilot import connect_to_addon_client_task
15
+ from dbt_platform_helper.providers.copilot import create_addon_client_task
16
+ from dbt_platform_helper.providers.copilot import create_postgres_admin_task
17
+ from dbt_platform_helper.providers.ecs import ecs_exec_is_available
18
+ from dbt_platform_helper.providers.ecs import get_cluster_arn
19
+ from dbt_platform_helper.providers.ecs import get_ecs_task_arns
20
+ from dbt_platform_helper.providers.ecs import get_or_create_task_name
21
+ from dbt_platform_helper.providers.secrets import get_addon_type
22
+ from dbt_platform_helper.providers.secrets import get_parameter_name
23
+ from dbt_platform_helper.utils.application import Application
24
+ from dbt_platform_helper.utils.messages import abort_with_error
25
+
26
+
27
+ class Conduit:
28
+ def __init__(
29
+ self,
30
+ application: Application,
31
+ echo_fn: Callable[[str], str] = click.secho,
32
+ subprocess_fn: subprocess = subprocess,
33
+ get_ecs_task_arns_fn=get_ecs_task_arns,
34
+ connect_to_addon_client_task_fn=connect_to_addon_client_task,
35
+ create_addon_client_task_fn=create_addon_client_task,
36
+ create_postgres_admin_task_fn=create_postgres_admin_task,
37
+ get_addon_type_fn=get_addon_type,
38
+ ecs_exec_is_available_fn=ecs_exec_is_available,
39
+ get_cluster_arn_fn=get_cluster_arn,
40
+ get_parameter_name_fn=get_parameter_name,
41
+ get_or_create_task_name_fn=get_or_create_task_name,
42
+ add_stack_delete_policy_to_task_role_fn=add_stack_delete_policy_to_task_role,
43
+ update_conduit_stack_resources_fn=update_conduit_stack_resources,
44
+ wait_for_cloudformation_to_reach_status_fn=wait_for_cloudformation_to_reach_status,
45
+ abort_fn=abort_with_error,
46
+ ):
47
+
48
+ self.application = application
49
+ self.subprocess_fn = subprocess_fn
50
+ self.echo_fn = echo_fn
51
+ self.get_ecs_task_arns_fn = get_ecs_task_arns_fn
52
+ self.connect_to_addon_client_task_fn = connect_to_addon_client_task_fn
53
+ self.create_addon_client_task_fn = create_addon_client_task_fn
54
+ self.create_postgres_admin_task = create_postgres_admin_task_fn
55
+ self.get_addon_type_fn = get_addon_type_fn
56
+ self.ecs_exec_is_available_fn = ecs_exec_is_available_fn
57
+ self.get_cluster_arn_fn = get_cluster_arn_fn
58
+ self.get_parameter_name_fn = get_parameter_name_fn
59
+ self.get_or_create_task_name_fn = get_or_create_task_name_fn
60
+ self.add_stack_delete_policy_to_task_role_fn = add_stack_delete_policy_to_task_role_fn
61
+ self.update_conduit_stack_resources_fn = update_conduit_stack_resources_fn
62
+ self.wait_for_cloudformation_to_reach_status_fn = wait_for_cloudformation_to_reach_status_fn
63
+ self.abort_fn = abort_fn
64
+
65
+ def start(self, env: str, addon_name: str, access: str = "read"):
66
+ clients = self._initialise_clients(env)
67
+ addon_type, cluster_arn, parameter_name, task_name = self._get_addon_details(
68
+ env, addon_name, access
69
+ )
70
+
71
+ self.echo_fn(f"Checking if a conduit task is already running for {addon_type}")
72
+ task_arn = self.get_ecs_task_arns_fn(clients["ecs"], cluster_arn, task_name)
73
+ if not task_arn:
74
+ self.echo_fn("Creating conduit task")
75
+ self.create_addon_client_task_fn(
76
+ clients["iam"],
77
+ clients["ssm"],
78
+ clients["secrets_manager"],
79
+ self.subprocess_fn,
80
+ self.application,
81
+ env,
82
+ addon_type,
83
+ addon_name,
84
+ task_name,
85
+ access,
86
+ )
87
+
88
+ self.echo_fn("Updating conduit task")
89
+ self._update_stack_resources(
90
+ clients["cloudformation"],
91
+ clients["iam"],
92
+ clients["ssm"],
93
+ self.application.name,
94
+ env,
95
+ addon_type,
96
+ addon_name,
97
+ task_name,
98
+ parameter_name,
99
+ access,
100
+ )
101
+
102
+ task_arn = self.get_ecs_task_arns_fn(clients["ecs"], cluster_arn, task_name)
103
+
104
+ else:
105
+ self.echo_fn("Conduit task already running")
106
+
107
+ self.echo_fn(f"Checking if exec is available for conduit task...")
108
+
109
+ try:
110
+ self.ecs_exec_is_available_fn(clients["ecs"], cluster_arn, task_arn)
111
+ except ECSAgentNotRunning:
112
+ self.abort_fn('ECS exec agent never reached "RUNNING" status')
113
+
114
+ self.echo_fn("Connecting to conduit task")
115
+ self.connect_to_addon_client_task_fn(
116
+ clients["ecs"], self.subprocess_fn, self.application.name, env, cluster_arn, task_name
117
+ )
118
+
119
+ def _initialise_clients(self, env):
120
+ return {
121
+ "ecs": self.application.environments[env].session.client("ecs"),
122
+ "iam": self.application.environments[env].session.client("iam"),
123
+ "ssm": self.application.environments[env].session.client("ssm"),
124
+ "cloudformation": self.application.environments[env].session.client("cloudformation"),
125
+ "secrets_manager": self.application.environments[env].session.client("secretsmanager"),
126
+ }
127
+
128
+ def _get_addon_details(self, env, addon_name, access):
129
+ ssm_client = self.application.environments[env].session.client("ssm")
130
+ ecs_client = self.application.environments[env].session.client("ecs")
131
+
132
+ addon_type = self.get_addon_type_fn(ssm_client, self.application.name, env, addon_name)
133
+ cluster_arn = self.get_cluster_arn_fn(ecs_client, self.application.name, env)
134
+ parameter_name = self.get_parameter_name_fn(
135
+ self.application.name, env, addon_type, addon_name, access
136
+ )
137
+ task_name = self.get_or_create_task_name_fn(
138
+ ssm_client, self.application.name, env, addon_name, parameter_name
139
+ )
140
+
141
+ return addon_type, cluster_arn, parameter_name, task_name
142
+
143
+ def _update_stack_resources(
144
+ self,
145
+ cloudformation_client,
146
+ iam_client,
147
+ ssm_client,
148
+ app_name,
149
+ env,
150
+ addon_type,
151
+ addon_name,
152
+ task_name,
153
+ parameter_name,
154
+ access,
155
+ ):
156
+ self.add_stack_delete_policy_to_task_role_fn(cloudformation_client, iam_client, task_name)
157
+ stack_name = self.update_conduit_stack_resources_fn(
158
+ cloudformation_client,
159
+ iam_client,
160
+ ssm_client,
161
+ app_name,
162
+ env,
163
+ addon_type,
164
+ addon_name,
165
+ task_name,
166
+ parameter_name,
167
+ access,
168
+ )
169
+ self.echo_fn("Waiting for conduit task update to complete...")
170
+ self.wait_for_cloudformation_to_reach_status_fn(
171
+ cloudformation_client, "stack_update_complete", stack_name
172
+ )
@@ -20,6 +20,31 @@ class IncompatibleMinorVersion(ValidationException):
20
20
  self.check_version = check_version
21
21
 
22
22
 
23
+ class NoClusterError(AWSException):
24
+ pass
25
+
26
+
27
+ class CreateTaskTimeoutError(AWSException):
28
+ pass
29
+
30
+
31
+ class ParameterNotFoundError(AWSException):
32
+ pass
33
+
34
+
35
+ class AddonNotFoundError(AWSException):
36
+ pass
37
+
38
+
39
+ class InvalidAddonTypeError(AWSException):
40
+ def __init__(self, addon_type):
41
+ self.addon_type = addon_type
42
+
43
+
44
+ class AddonTypeMissingFromConfigError(AWSException):
45
+ pass
46
+
47
+
23
48
  class CopilotCodebaseNotFoundError(Exception):
24
49
  pass
25
50
 
@@ -46,3 +71,11 @@ class ApplicationNotFoundError(Exception):
46
71
 
47
72
  class ApplicationEnvironmentNotFoundError(Exception):
48
73
  pass
74
+
75
+
76
+ class SecretNotFoundError(AWSException):
77
+ pass
78
+
79
+
80
+ class ECSAgentNotRunning(AWSException):
81
+ pass
@@ -0,0 +1,105 @@
1
+ import json
2
+
3
+ from cfn_tools import dump_yaml
4
+ from cfn_tools import load_yaml
5
+
6
+
7
+ def add_stack_delete_policy_to_task_role(cloudformation_client, iam_client, task_name: str):
8
+
9
+ stack_name = f"task-{task_name}"
10
+ stack_resources = cloudformation_client.list_stack_resources(StackName=stack_name)[
11
+ "StackResourceSummaries"
12
+ ]
13
+
14
+ for resource in stack_resources:
15
+ if resource["LogicalResourceId"] == "DefaultTaskRole":
16
+ task_role_name = resource["PhysicalResourceId"]
17
+ iam_client.put_role_policy(
18
+ RoleName=task_role_name,
19
+ PolicyName="DeleteCloudFormationStack",
20
+ PolicyDocument=json.dumps(
21
+ {
22
+ "Version": "2012-10-17",
23
+ "Statement": [
24
+ {
25
+ "Action": ["cloudformation:DeleteStack"],
26
+ "Effect": "Allow",
27
+ "Resource": f"arn:aws:cloudformation:*:*:stack/{stack_name}/*",
28
+ },
29
+ ],
30
+ },
31
+ ),
32
+ )
33
+
34
+
35
+ def update_conduit_stack_resources(
36
+ cloudformation_client,
37
+ iam_client,
38
+ ssm_client,
39
+ application_name: str,
40
+ env: str,
41
+ addon_type: str,
42
+ addon_name: str,
43
+ task_name: str,
44
+ parameter_name: str,
45
+ access: str,
46
+ ):
47
+
48
+ conduit_stack_name = f"task-{task_name}"
49
+ template = cloudformation_client.get_template(StackName=conduit_stack_name)
50
+ template_yml = load_yaml(template["TemplateBody"])
51
+ template_yml["Resources"]["LogGroup"]["DeletionPolicy"] = "Retain"
52
+ template_yml["Resources"]["TaskNameParameter"] = load_yaml(
53
+ f"""
54
+ Type: AWS::SSM::Parameter
55
+ Properties:
56
+ Name: {parameter_name}
57
+ Type: String
58
+ Value: {task_name}
59
+ """
60
+ )
61
+
62
+ log_filter_role_arn = iam_client.get_role(RoleName="CWLtoSubscriptionFilterRole")["Role"]["Arn"]
63
+
64
+ destination_log_group_arns = json.loads(
65
+ ssm_client.get_parameter(Name="/copilot/tools/central_log_groups")["Parameter"]["Value"]
66
+ )
67
+
68
+ destination_arn = destination_log_group_arns["dev"]
69
+ if env.lower() in ("prod", "production"):
70
+ destination_arn = destination_log_group_arns["prod"]
71
+
72
+ template_yml["Resources"]["SubscriptionFilter"] = load_yaml(
73
+ f"""
74
+ Type: AWS::Logs::SubscriptionFilter
75
+ DeletionPolicy: Retain
76
+ Properties:
77
+ RoleArn: {log_filter_role_arn}
78
+ LogGroupName: /copilot/{task_name}
79
+ FilterName: /copilot/conduit/{application_name}/{env}/{addon_type}/{addon_name}/{task_name.rsplit("-", 1)[1]}/{access}
80
+ FilterPattern: ''
81
+ DestinationArn: {destination_arn}
82
+ """
83
+ )
84
+
85
+ params = []
86
+ if "Parameters" in template_yml:
87
+ for param in template_yml["Parameters"]:
88
+ # TODO testing missed in codecov, update test to assert on method call below with params including ExistingParameter from cloudformation template.
89
+ params.append({"ParameterKey": param, "UsePreviousValue": True})
90
+
91
+ cloudformation_client.update_stack(
92
+ StackName=conduit_stack_name,
93
+ TemplateBody=dump_yaml(template_yml),
94
+ Parameters=params,
95
+ Capabilities=["CAPABILITY_IAM"],
96
+ )
97
+
98
+ return conduit_stack_name
99
+
100
+
101
+ # TODO Catch errors and raise a more human friendly Exception is the CloudFormation stack goes into a "unhappy" state, e.g. ROLLBACK_IN_PROGRESS. Currently we get things like botocore.exceptions.WaiterError: Waiter StackUpdateComplete failed: Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" we matched expected path: "UPDATE_ROLLBACK_COMPLETE" at least once
102
+ def wait_for_cloudformation_to_reach_status(cloudformation_client, stack_status, stack_name):
103
+
104
+ waiter = cloudformation_client.get_waiter(stack_status)
105
+ waiter.wait(StackName=stack_name, WaiterConfig={"Delay": 5, "MaxAttempts": 20})
@@ -0,0 +1,144 @@
1
+ import json
2
+ import time
3
+
4
+ from botocore.exceptions import ClientError
5
+
6
+ from dbt_platform_helper.constants import CONDUIT_DOCKER_IMAGE_LOCATION
7
+ from dbt_platform_helper.exceptions import CreateTaskTimeoutError
8
+ from dbt_platform_helper.providers.ecs import get_ecs_task_arns
9
+ from dbt_platform_helper.providers.secrets import get_connection_secret_arn
10
+ from dbt_platform_helper.providers.secrets import (
11
+ get_postgres_connection_data_updated_with_master_secret,
12
+ )
13
+ from dbt_platform_helper.utils.application import Application
14
+ from dbt_platform_helper.utils.messages import abort_with_error
15
+
16
+
17
+ def create_addon_client_task(
18
+ iam_client,
19
+ ssm_client,
20
+ secrets_manager_client,
21
+ subprocess,
22
+ application: Application,
23
+ env: str,
24
+ addon_type: str,
25
+ addon_name: str,
26
+ task_name: str,
27
+ access: str,
28
+ ):
29
+ secret_name = f"/copilot/{application.name}/{env}/secrets/{_normalise_secret_name(addon_name)}"
30
+
31
+ if addon_type == "postgres":
32
+ if access == "read":
33
+ secret_name += "_READ_ONLY_USER"
34
+ elif access == "write":
35
+ secret_name += "_APPLICATION_USER"
36
+ elif access == "admin":
37
+ create_postgres_admin_task(
38
+ ssm_client,
39
+ secrets_manager_client,
40
+ subprocess,
41
+ application,
42
+ addon_name,
43
+ addon_type,
44
+ env,
45
+ secret_name,
46
+ task_name,
47
+ )
48
+ return
49
+ elif addon_type == "redis" or addon_type == "opensearch":
50
+ secret_name += "_ENDPOINT"
51
+
52
+ role_name = f"{addon_name}-{application.name}-{env}-conduitEcsTask"
53
+
54
+ try:
55
+ iam_client.get_role(RoleName=role_name)
56
+ execution_role = f"--execution-role {role_name} "
57
+ except ClientError as ex:
58
+ execution_role = ""
59
+ # We cannot check for botocore.errorfactory.NoSuchEntityException as botocore generates that class on the fly as part of errorfactory.
60
+ # factory. Checking the error code is the recommended way of handling these exceptions.
61
+ if ex.response.get("Error", {}).get("Code", None) != "NoSuchEntity":
62
+ # TODO Raise an exception to be caught at the command layer
63
+ abort_with_error(
64
+ f"cannot obtain Role {role_name}: {ex.response.get('Error', {}).get('Message', '')}"
65
+ )
66
+
67
+ subprocess.call(
68
+ f"copilot task run --app {application.name} --env {env} "
69
+ f"--task-group-name {task_name} "
70
+ f"{execution_role}"
71
+ f"--image {CONDUIT_DOCKER_IMAGE_LOCATION}:{addon_type} "
72
+ f"--secrets CONNECTION_SECRET={get_connection_secret_arn(ssm_client,secrets_manager_client, secret_name)} "
73
+ "--platform-os linux "
74
+ "--platform-arch arm64",
75
+ shell=True,
76
+ )
77
+
78
+
79
+ def create_postgres_admin_task(
80
+ ssm_client,
81
+ secrets_manager_client,
82
+ subprocess,
83
+ app: Application,
84
+ addon_name: str,
85
+ addon_type: str,
86
+ env: str,
87
+ secret_name: str,
88
+ task_name: str,
89
+ ):
90
+ read_only_secret_name = secret_name + "_READ_ONLY_USER"
91
+ master_secret_name = (
92
+ f"/copilot/{app.name}/{env}/secrets/{_normalise_secret_name(addon_name)}_RDS_MASTER_ARN"
93
+ )
94
+ master_secret_arn = ssm_client.get_parameter(Name=master_secret_name, WithDecryption=True)[
95
+ "Parameter"
96
+ ]["Value"]
97
+ connection_string = json.dumps(
98
+ get_postgres_connection_data_updated_with_master_secret(
99
+ ssm_client, secrets_manager_client, read_only_secret_name, master_secret_arn
100
+ )
101
+ )
102
+
103
+ subprocess.call(
104
+ f"copilot task run --app {app.name} --env {env} "
105
+ f"--task-group-name {task_name} "
106
+ f"--image {CONDUIT_DOCKER_IMAGE_LOCATION}:{addon_type} "
107
+ f"--env-vars CONNECTION_SECRET='{connection_string}' "
108
+ "--platform-os linux "
109
+ "--platform-arch arm64",
110
+ shell=True,
111
+ )
112
+
113
+
114
+ def connect_to_addon_client_task(
115
+ ecs_client,
116
+ subprocess,
117
+ application_name,
118
+ env,
119
+ cluster_arn,
120
+ task_name,
121
+ addon_client_is_running_fn=get_ecs_task_arns,
122
+ ):
123
+ running = False
124
+ tries = 0
125
+ while tries < 15 and not running:
126
+ tries += 1
127
+ if addon_client_is_running_fn(ecs_client, cluster_arn, task_name):
128
+ subprocess.call(
129
+ "copilot task exec "
130
+ f"--app {application_name} --env {env} "
131
+ f"--name {task_name} "
132
+ f"--command bash",
133
+ shell=True,
134
+ )
135
+ running = True
136
+
137
+ time.sleep(1)
138
+
139
+ if not running:
140
+ raise CreateTaskTimeoutError
141
+
142
+
143
+ def _normalise_secret_name(addon_name: str) -> str:
144
+ return addon_name.replace("-", "_").upper()