ephemeral-pulumi-deploy 0.0.1__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.
- ephemeral_pulumi_deploy-0.0.1/LICENSE +21 -0
- ephemeral_pulumi_deploy-0.0.1/PKG-INFO +42 -0
- ephemeral_pulumi_deploy-0.0.1/README.md +27 -0
- ephemeral_pulumi_deploy-0.0.1/pyproject.toml +58 -0
- ephemeral_pulumi_deploy-0.0.1/setup.cfg +4 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy/__init__.py +9 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy/cli.py +74 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy/py.typed +0 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy/utils.py +274 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy.egg-info/PKG-INFO +42 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy.egg-info/SOURCES.txt +12 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy.egg-info/dependency_links.txt +1 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy.egg-info/requires.txt +4 -0
- ephemeral_pulumi_deploy-0.0.1/src/ephemeral_pulumi_deploy.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 LabAutomationAndScreening
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: ephemeral-pulumi-deploy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Library for facilitating easy deployment of ephemeral pulumi stacks
|
|
5
|
+
Author: Eli Fine
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Requires-Python: >=3.12.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: pulumi>=3.147.0
|
|
12
|
+
Requires-Dist: pulumi-aws>=6.66.3
|
|
13
|
+
Requires-Dist: pulumi-aws-native>=1.24.0
|
|
14
|
+
Requires-Dist: boto3>=1.36.3
|
|
15
|
+
|
|
16
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
17
|
+
[](https://github.com/astral-sh/ruff)
|
|
18
|
+
[](https://github.com/astral-sh/uv)
|
|
19
|
+
[](https://microsoft.github.io/pyright/)
|
|
20
|
+
[](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions)
|
|
21
|
+
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy)
|
|
22
|
+
[](https://pypi.org/project/ephemeral-pulumi-deploy/)
|
|
23
|
+
[](https://pepy.tech/project/ephemeral-pulumi-deploy)
|
|
24
|
+
[](https://pypi.org/project/ephemeral-pulumi-deploy/)
|
|
25
|
+
[](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy)
|
|
26
|
+
[](https://ephemeral-pulumi-deploy.readthedocs.io/en/latest/?badge=latest)
|
|
27
|
+
|
|
28
|
+
# Usage
|
|
29
|
+
Documentation is hosted on [ReadTheDocs](https://ephemeral-pulumi-deploy.readthedocs.io/en/latest/?badge=latest).
|
|
30
|
+
|
|
31
|
+
# Development
|
|
32
|
+
This project has a dev container. If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Updating from the template
|
|
41
|
+
This repository uses a copier template. To pull in the latest updates from the template, use the command:
|
|
42
|
+
`copier update --trust --conflict rej --defaults`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
2
|
+
[](https://github.com/astral-sh/ruff)
|
|
3
|
+
[](https://github.com/astral-sh/uv)
|
|
4
|
+
[](https://microsoft.github.io/pyright/)
|
|
5
|
+
[](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions)
|
|
6
|
+
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy)
|
|
7
|
+
[](https://pypi.org/project/ephemeral-pulumi-deploy/)
|
|
8
|
+
[](https://pepy.tech/project/ephemeral-pulumi-deploy)
|
|
9
|
+
[](https://pypi.org/project/ephemeral-pulumi-deploy/)
|
|
10
|
+
[](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy)
|
|
11
|
+
[](https://ephemeral-pulumi-deploy.readthedocs.io/en/latest/?badge=latest)
|
|
12
|
+
|
|
13
|
+
# Usage
|
|
14
|
+
Documentation is hosted on [ReadTheDocs](https://ephemeral-pulumi-deploy.readthedocs.io/en/latest/?badge=latest).
|
|
15
|
+
|
|
16
|
+
# Development
|
|
17
|
+
This project has a dev container. If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Updating from the template
|
|
26
|
+
This repository uses a copier template. To pull in the latest updates from the template, use the command:
|
|
27
|
+
`copier update --trust --conflict rej --defaults`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ephemeral-pulumi-deploy"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Library for facilitating easy deployment of ephemeral pulumi stacks"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Eli Fine"},
|
|
7
|
+
|
|
8
|
+
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12.7"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
|
|
21
|
+
"pulumi>=3.147.0",
|
|
22
|
+
"pulumi-aws>=6.66.3",
|
|
23
|
+
"pulumi-aws-native>=1.24.0",
|
|
24
|
+
"boto3>=1.36.3",
|
|
25
|
+
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
# Specific to this template
|
|
32
|
+
|
|
33
|
+
"boto3-stubs[all]>=1.36.3",
|
|
34
|
+
"pytest-mock>=3.14",
|
|
35
|
+
|
|
36
|
+
# Managed by upstream template
|
|
37
|
+
"pyright>=1.1.393",
|
|
38
|
+
"pytest>=8.3.4",
|
|
39
|
+
"pytest-cov>=6.0.0",
|
|
40
|
+
"pytest-randomly>=3.16.0",
|
|
41
|
+
"sphinx==8.1.3",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools]
|
|
45
|
+
license-files = [] # kludge until this bug is fixed https://github.com/pypa/setuptools/issues/4759
|
|
46
|
+
|
|
47
|
+
[tool.uv]
|
|
48
|
+
package = true
|
|
49
|
+
|
|
50
|
+
[[tool.uv.index]]
|
|
51
|
+
name = "pypi"
|
|
52
|
+
url = "https://pypi.org/simple/"
|
|
53
|
+
publish-url = "https://upload.pypi.org/legacy/"
|
|
54
|
+
|
|
55
|
+
[[tool.uv.index]]
|
|
56
|
+
name = "testpypi"
|
|
57
|
+
url = "https://test.pypi.org/simple/"
|
|
58
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from . import cli
|
|
2
|
+
from . import utils
|
|
3
|
+
from .cli import run_cli
|
|
4
|
+
from .utils import append_resource_suffix
|
|
5
|
+
from .utils import get_aws_account_id
|
|
6
|
+
from .utils import get_config
|
|
7
|
+
from .utils import get_config_str
|
|
8
|
+
|
|
9
|
+
__all__ = ["append_resource_suffix", "cli", "get_aws_account_id", "get_config", "get_config_str", "run_cli", "utils"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from pulumi.automation import PulumiFn
|
|
9
|
+
|
|
10
|
+
from .utils import PROTECTED_ENVS
|
|
11
|
+
from .utils import get_env_from_cli_input
|
|
12
|
+
from .utils import get_stack
|
|
13
|
+
from .utils import parser
|
|
14
|
+
from .utils import result_to_str
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StackKwargs(TypedDict):
|
|
20
|
+
diff: bool
|
|
21
|
+
on_output: Callable[[str], None]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_cli(*, stack_config: dict[str, Any], pulumi_program: PulumiFn) -> None:
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
stack_name = args.stack.replace(
|
|
27
|
+
"/", "-"
|
|
28
|
+
) # replace characters sometimes used in git branch names (for test/feature branches) that are incompatible with Pulumi and/or AWS resource naming
|
|
29
|
+
|
|
30
|
+
env = get_env_from_cli_input(stack_name)
|
|
31
|
+
|
|
32
|
+
if (env in PROTECTED_ENVS) and args.destroy:
|
|
33
|
+
logger.error(f"Stack {stack_name} can't be destroyed, because it's not a test/dev stack.")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
stack = get_stack(
|
|
37
|
+
stack_name=stack_name,
|
|
38
|
+
pulumi_program=pulumi_program,
|
|
39
|
+
stack_config=stack_config,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# if destroy then teardown and exit
|
|
43
|
+
if args.destroy:
|
|
44
|
+
destroy_response = stack.destroy()
|
|
45
|
+
destroy_response_str = result_to_str(destroy_response)
|
|
46
|
+
logger.info(destroy_response_str)
|
|
47
|
+
# I see no reason not to completely remove the stack and history after destroying it. There is no returned output from the command https://github.com/pulumi/pulumi/blob/06ba63bb57e90706c1550861b785075ae860144a/sdk/python/lib/pulumi/automation/_local_workspace.py#L277
|
|
48
|
+
stack.workspace.remove_stack(stack_name)
|
|
49
|
+
sys.exit(0)
|
|
50
|
+
up_and_preview_kwargs: StackKwargs = {
|
|
51
|
+
"diff": True,
|
|
52
|
+
"on_output": print,
|
|
53
|
+
# 'on_event': print # TODO: figure out how to log these? Seems too verbose to print to stdout though
|
|
54
|
+
}
|
|
55
|
+
if args.apply:
|
|
56
|
+
up_response = stack.up(**up_and_preview_kwargs)
|
|
57
|
+
up_response_str = result_to_str(up_response)
|
|
58
|
+
logger.info(up_response_str)
|
|
59
|
+
else: # plan only
|
|
60
|
+
# TODO: Make use of this feature to guarantee the plan is what is actually executed https://www.pulumi.com/blog/announcing-public-preview-update-plans/
|
|
61
|
+
preview_response = stack.preview(**up_and_preview_kwargs)
|
|
62
|
+
preview_response_str = result_to_str(preview_response)
|
|
63
|
+
logger.info(preview_response_str)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
custom_exit_code = os.environ["CUSTOM_PULUMI_OPERATION_EXIT_CODE"]
|
|
67
|
+
if not custom_exit_code.isnumeric():
|
|
68
|
+
raise NotImplementedError(f"Exit codes should always be set to integers, but received {custom_exit_code}")
|
|
69
|
+
print( # noqa: T201 # TODO: figure out a way to log this that also shows up in stdout in CI
|
|
70
|
+
f"Exiting with non-zero custom error code {custom_exit_code}. Look back through output to find the reason."
|
|
71
|
+
)
|
|
72
|
+
sys.exit(int(custom_exit_code))
|
|
73
|
+
except KeyError:
|
|
74
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
import pulumi
|
|
8
|
+
import pulumi.runtime
|
|
9
|
+
import pulumi_aws
|
|
10
|
+
from pulumi.automation import ConfigValue
|
|
11
|
+
from pulumi.automation import LocalWorkspaceOptions
|
|
12
|
+
from pulumi.automation import ProjectBackend
|
|
13
|
+
from pulumi.automation import ProjectRuntimeInfo
|
|
14
|
+
from pulumi.automation import ProjectSettings
|
|
15
|
+
from pulumi.automation import PulumiFn
|
|
16
|
+
from pulumi.automation import Stack
|
|
17
|
+
from pulumi.automation import StackSettings
|
|
18
|
+
from pulumi.automation import create_or_select_stack
|
|
19
|
+
from pulumi.automation._stack import BaseResult
|
|
20
|
+
from pulumi_aws_native import TagArgs
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
PROTECTED_ENVS = ("stag", "staging", "prod", "production")
|
|
24
|
+
RESOURCE_SUFFIX_DELIMITER = "--"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_backend_url(
|
|
28
|
+
*,
|
|
29
|
+
backend_bucket: str,
|
|
30
|
+
aws_account_id: str,
|
|
31
|
+
repo_name: str,
|
|
32
|
+
pulumi_project_name: str,
|
|
33
|
+
bucket_region: str = "us-east-1",
|
|
34
|
+
) -> str:
|
|
35
|
+
"""Create the backend URL to store the state."""
|
|
36
|
+
return f"s3://{backend_bucket}/{aws_account_id}/{repo_name}/{pulumi_project_name}?region={bucket_region}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
AWS_ACCOUNT_ID_LENGTH = 12
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def format_aws_account_id(account_id: str | int) -> str:
|
|
43
|
+
"""Ensure 12 digits, including leading zeros."""
|
|
44
|
+
aws_account_id = str(account_id).zfill(AWS_ACCOUNT_ID_LENGTH)
|
|
45
|
+
if len(aws_account_id) != AWS_ACCOUNT_ID_LENGTH or not aws_account_id.isdigit():
|
|
46
|
+
raise ValueError( # noqa: TRY003 # this doesn't warrant a custom exception
|
|
47
|
+
f"AWS account id should be {AWS_ACCOUNT_ID_LENGTH} digits, but was {aws_account_id}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return aws_account_id
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_aws_account_id() -> str:
|
|
54
|
+
return format_aws_account_id(pulumi_aws.get_caller_identity().account_id)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_aws_region() -> str:
|
|
58
|
+
region = str(pulumi_aws.config.region)
|
|
59
|
+
if not region:
|
|
60
|
+
raise ValueError("Could not determine AWS region") # noqa: TRY003 # this doesn't warrant a custom exception
|
|
61
|
+
return region
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
SAFE_MAX_AWS_NAME_LENGTH = 56
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def append_resource_suffix(
|
|
68
|
+
resource_name: str = "",
|
|
69
|
+
) -> str:
|
|
70
|
+
"""Append the suffix to the resource name.
|
|
71
|
+
|
|
72
|
+
{resource_name}--{project_name}--{stack_name}
|
|
73
|
+
|
|
74
|
+
- case is preserved, since that has conventions for things like IAM Roles
|
|
75
|
+
- however, some AWS resources (e.g. S3 buckets) require names to be lowercase
|
|
76
|
+
- maximum length allowed in the template is 56
|
|
77
|
+
- most AWS names are a maximum 63 characters
|
|
78
|
+
- lambdas are 140 though, so longer limits can be supplied
|
|
79
|
+
- Pulumi reserves 7 random characters as a suffix, leaving 56.
|
|
80
|
+
- the stack name is trimmed to 7 characters to help ensure a fit.
|
|
81
|
+
"""
|
|
82
|
+
stack_name = pulumi.get_stack()[:7]
|
|
83
|
+
project_name = pulumi.get_project()
|
|
84
|
+
if resource_name:
|
|
85
|
+
resource_name = RESOURCE_SUFFIX_DELIMITER.join((resource_name, project_name, stack_name.lower()))
|
|
86
|
+
else:
|
|
87
|
+
resource_name = RESOURCE_SUFFIX_DELIMITER.join((project_name, stack_name.lower()))
|
|
88
|
+
|
|
89
|
+
if len(resource_name) > SAFE_MAX_AWS_NAME_LENGTH:
|
|
90
|
+
raise ValueError( # noqa: TRY003 # this doesn't warrant a custom exception
|
|
91
|
+
f"Error creating aws resource name from template.\n{resource_name} is too long: {len(resource_name)} characters."
|
|
92
|
+
)
|
|
93
|
+
return resource_name
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def result_to_str(pulumi_result: BaseResult) -> str:
|
|
97
|
+
"""Convert to something printable."""
|
|
98
|
+
return f"stdout:\n{pulumi_result.stdout}\n\nstderr:\n{pulumi_result.stderr}\n"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_env_from_cli_input(cli_stack_name: str) -> str:
|
|
102
|
+
"""Return the environment tier from the command line input stack name."""
|
|
103
|
+
name = cli_stack_name.lower()
|
|
104
|
+
if name.startswith("test"):
|
|
105
|
+
return "test"
|
|
106
|
+
if name.startswith("prod"):
|
|
107
|
+
return "prod"
|
|
108
|
+
if name.startswith("mod"):
|
|
109
|
+
return "modl"
|
|
110
|
+
if name.startswith("stag"):
|
|
111
|
+
return "stag"
|
|
112
|
+
|
|
113
|
+
return "dev"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_config(key: str) -> str | int | dict[str, Any]:
|
|
117
|
+
"""Get the configuration value as a string.
|
|
118
|
+
|
|
119
|
+
For reasons unknown, the `pulumi.runtime.config` returns a JSON string with `'value':str` and `'secret':bool` as a dictionary, instead of just the
|
|
120
|
+
value.
|
|
121
|
+
"""
|
|
122
|
+
json_str = pulumi.runtime.get_config(key)
|
|
123
|
+
if json_str is None:
|
|
124
|
+
raise KeyError(f"The key {key} was not found in the Pulumi config.") # noqa: TRY003 # this doesn't warrant a custom exception
|
|
125
|
+
if not isinstance(json_str, str):
|
|
126
|
+
raise NotImplementedError(
|
|
127
|
+
f"get_config is always supposed to return a string. But {json_str} was type {type(json_str)}"
|
|
128
|
+
)
|
|
129
|
+
try:
|
|
130
|
+
json_dict = json.loads(json_str)
|
|
131
|
+
except json.decoder.JSONDecodeError:
|
|
132
|
+
# Not totally sure how this is happening, but sometimes the exact string value already is returned and not a JSON-formatted string, so assuming we should just directly return the value
|
|
133
|
+
return json_str
|
|
134
|
+
if not isinstance(json_dict, dict):
|
|
135
|
+
raise NotImplementedError(
|
|
136
|
+
f"The config key {key} JSON should always parse to a dictionary, but it was found to be {json_dict} which is {type(json_dict)}. Original retrieved JSON was {json_str}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
"value" in json_dict
|
|
141
|
+
): # if the 'value' key is present, assume this is an actual attribute. Otherwise assume it's a nested dictionary
|
|
142
|
+
value = json_dict["value"] # type: ignore[reportUnknownVariableType] # TODO: understand this, so there can be better typing
|
|
143
|
+
if not isinstance(value, int | str):
|
|
144
|
+
raise NotImplementedError(
|
|
145
|
+
f"The value for config key {key} should always be a string or int, but it was found to be {value} which is {type(value)}. Original retrieved JSON was {json_str}" # type: ignore[reportUnknownArgumentType] # TODO: understand this, so there can be better typing
|
|
146
|
+
)
|
|
147
|
+
return value
|
|
148
|
+
assert isinstance(json_dict, str | int | dict)
|
|
149
|
+
return json_dict # type: ignore[reportUnknownVariableType] # TODO: understand this, so there can be better typing
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_config_aws_account_id(key: str) -> str:
|
|
153
|
+
value = get_config(key)
|
|
154
|
+
if not isinstance(value, str | int):
|
|
155
|
+
raise NotImplementedError(
|
|
156
|
+
f"The value for {key} should always be a string or int, but {value} was type {type(value)}."
|
|
157
|
+
)
|
|
158
|
+
return format_aws_account_id(value)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_config_str(key: str) -> str:
|
|
162
|
+
value = get_config(key)
|
|
163
|
+
if not isinstance(value, str):
|
|
164
|
+
raise NotImplementedError(f"The value for {key} should always be a string, but {value} was type {type(value)}.")
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_config_bool(key: str) -> bool:
|
|
169
|
+
value = get_config(key)
|
|
170
|
+
if not isinstance(value, str):
|
|
171
|
+
raise NotImplementedError(f"The value for {key} should always be a string, but {value} was type {type(value)}.")
|
|
172
|
+
return value in ("1", "True")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_config_int(key: str) -> int:
|
|
176
|
+
value = get_config(key)
|
|
177
|
+
if isinstance(value, int):
|
|
178
|
+
return value
|
|
179
|
+
if not isinstance(value, str):
|
|
180
|
+
raise NotImplementedError(
|
|
181
|
+
f"The value for {key} should initially always be a string, but {value} was type {type(value)}."
|
|
182
|
+
)
|
|
183
|
+
return int(value)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_stack(
|
|
187
|
+
*, stack_name: str, pulumi_program: PulumiFn, stack_config: dict[str, Any], aws_home_region: str = "us-east-1"
|
|
188
|
+
) -> Stack:
|
|
189
|
+
env = get_env_from_cli_input(stack_name)
|
|
190
|
+
project_name = stack_config["proj:pulumi_project_name"]
|
|
191
|
+
github_repo_name = stack_config["proj:github_repo_name"]
|
|
192
|
+
stack_config["proj:env"] = ConfigValue(value=env)
|
|
193
|
+
|
|
194
|
+
fully_qualified_stack_name = f"{project_name}/{stack_name}"
|
|
195
|
+
|
|
196
|
+
session = boto3.Session()
|
|
197
|
+
sts_client = session.client("sts")
|
|
198
|
+
ssm_client = session.client("ssm", region_name=aws_home_region)
|
|
199
|
+
|
|
200
|
+
account_id = sts_client.get_caller_identity()["Account"]
|
|
201
|
+
# account_id is a `str` when returned from `boto` and ConfigValue stores a `str`. However it is somehow an `int` when fetched back by get_config.
|
|
202
|
+
# This is a problem when the account_id is prefixed with zeros.
|
|
203
|
+
stack_config["proj:aws_account_id"] = ConfigValue(value=account_id)
|
|
204
|
+
backend_bucket_param = ssm_client.get_parameter(Name="/org-managed/infra-state-bucket-name")["Parameter"]
|
|
205
|
+
assert "Value" in backend_bucket_param, f"Expected 'Value' in {backend_bucket_param}"
|
|
206
|
+
backend_bucket = backend_bucket_param["Value"]
|
|
207
|
+
stack_config["proj:backend_bucket_name"] = ConfigValue(value=backend_bucket)
|
|
208
|
+
|
|
209
|
+
kms_key_id_param = ssm_client.get_parameter(Name="/org-managed/infra-state-kms-key-arn")["Parameter"]
|
|
210
|
+
assert "Value" in kms_key_id_param, f"Expected 'Value' in {kms_key_id_param}"
|
|
211
|
+
kms_key_id = kms_key_id_param["Value"]
|
|
212
|
+
stack_config["proj:kms_key_id"] = ConfigValue(value=kms_key_id)
|
|
213
|
+
|
|
214
|
+
secrets_provider = f"awskms:///{kms_key_id}?region={aws_home_region}" # TODO: add context parameters https://www.pulumi.com/docs/iac/concepts/secrets/
|
|
215
|
+
logger.info("Stack is: %s", fully_qualified_stack_name)
|
|
216
|
+
project_runtime_info = ProjectRuntimeInfo( # I have no idea what this does or if it is necessary, but this was copied off of a template
|
|
217
|
+
name="python", options={"virtualenv": "venv"}
|
|
218
|
+
)
|
|
219
|
+
backend_url = generate_backend_url(
|
|
220
|
+
backend_bucket=backend_bucket,
|
|
221
|
+
aws_account_id=account_id,
|
|
222
|
+
repo_name=github_repo_name,
|
|
223
|
+
pulumi_project_name=project_name,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
project_backend = ProjectBackend(url=backend_url)
|
|
227
|
+
project_settings = ProjectSettings(name=project_name, runtime=project_runtime_info, backend=project_backend)
|
|
228
|
+
stack_settings = StackSettings(
|
|
229
|
+
secrets_provider=secrets_provider,
|
|
230
|
+
config=stack_config,
|
|
231
|
+
)
|
|
232
|
+
workspace_options = LocalWorkspaceOptions(
|
|
233
|
+
secrets_provider=secrets_provider, # Since secrets_provider is already in the ProjectSettings, unclear if it's needed in both places or if just one spot would be better
|
|
234
|
+
project_settings=project_settings,
|
|
235
|
+
stack_settings={stack_name: stack_settings},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return create_or_select_stack(
|
|
239
|
+
stack_name,
|
|
240
|
+
project_name=project_name,
|
|
241
|
+
program=pulumi_program,
|
|
242
|
+
opts=workspace_options,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def common_tags() -> dict[str, str]:
|
|
247
|
+
"""Create common tags that all resources should have."""
|
|
248
|
+
return {
|
|
249
|
+
"git-repository-url": get_config_str("proj:git_repository_url"),
|
|
250
|
+
"managed-by": "pulumi",
|
|
251
|
+
"stack-name": pulumi.get_stack(),
|
|
252
|
+
"pulumi-project-name": pulumi.get_project(),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def common_tags_native() -> list[TagArgs]:
|
|
257
|
+
"""Generate tags in the format expected in AWS Native."""
|
|
258
|
+
tags = common_tags()
|
|
259
|
+
native_tags: list[TagArgs] = []
|
|
260
|
+
for key, value in tags.items():
|
|
261
|
+
native_tags.append(TagArgs(key=key, value=value))
|
|
262
|
+
return native_tags
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
parser = argparse.ArgumentParser(description="pulumi-auto-deploy")
|
|
266
|
+
_ = parser.add_argument(
|
|
267
|
+
"--stack",
|
|
268
|
+
required=True,
|
|
269
|
+
type=str,
|
|
270
|
+
help="Pulumi stack name.",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
_ = parser.add_argument("--apply", action="store_true")
|
|
274
|
+
_ = parser.add_argument("--destroy", action="store_true")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: ephemeral-pulumi-deploy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Library for facilitating easy deployment of ephemeral pulumi stacks
|
|
5
|
+
Author: Eli Fine
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Requires-Python: >=3.12.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: pulumi>=3.147.0
|
|
12
|
+
Requires-Dist: pulumi-aws>=6.66.3
|
|
13
|
+
Requires-Dist: pulumi-aws-native>=1.24.0
|
|
14
|
+
Requires-Dist: boto3>=1.36.3
|
|
15
|
+
|
|
16
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
17
|
+
[](https://github.com/astral-sh/ruff)
|
|
18
|
+
[](https://github.com/astral-sh/uv)
|
|
19
|
+
[](https://microsoft.github.io/pyright/)
|
|
20
|
+
[](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions)
|
|
21
|
+
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy)
|
|
22
|
+
[](https://pypi.org/project/ephemeral-pulumi-deploy/)
|
|
23
|
+
[](https://pepy.tech/project/ephemeral-pulumi-deploy)
|
|
24
|
+
[](https://pypi.org/project/ephemeral-pulumi-deploy/)
|
|
25
|
+
[](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy)
|
|
26
|
+
[](https://ephemeral-pulumi-deploy.readthedocs.io/en/latest/?badge=latest)
|
|
27
|
+
|
|
28
|
+
# Usage
|
|
29
|
+
Documentation is hosted on [ReadTheDocs](https://ephemeral-pulumi-deploy.readthedocs.io/en/latest/?badge=latest).
|
|
30
|
+
|
|
31
|
+
# Development
|
|
32
|
+
This project has a dev container. If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Updating from the template
|
|
41
|
+
This repository uses a copier template. To pull in the latest updates from the template, use the command:
|
|
42
|
+
`copier update --trust --conflict rej --defaults`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/ephemeral_pulumi_deploy/__init__.py
|
|
5
|
+
src/ephemeral_pulumi_deploy/cli.py
|
|
6
|
+
src/ephemeral_pulumi_deploy/py.typed
|
|
7
|
+
src/ephemeral_pulumi_deploy/utils.py
|
|
8
|
+
src/ephemeral_pulumi_deploy.egg-info/PKG-INFO
|
|
9
|
+
src/ephemeral_pulumi_deploy.egg-info/SOURCES.txt
|
|
10
|
+
src/ephemeral_pulumi_deploy.egg-info/dependency_links.txt
|
|
11
|
+
src/ephemeral_pulumi_deploy.egg-info/requires.txt
|
|
12
|
+
src/ephemeral_pulumi_deploy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ephemeral_pulumi_deploy
|