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.
@@ -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
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
17
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
18
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
19
+ [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
20
+ [![Actions status](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions)
21
+ [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy)
22
+ [![PyPI Version](https://img.shields.io/pypi/v/ephemeral-pulumi-deploy.svg)](https://pypi.org/project/ephemeral-pulumi-deploy/)
23
+ [![Downloads](https://pepy.tech/badge/ephemeral-pulumi-deploy)](https://pepy.tech/project/ephemeral-pulumi-deploy)
24
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ephemeral-pulumi-deploy.svg)](https://pypi.org/project/ephemeral-pulumi-deploy/)
25
+ [![Codecov](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy/branch/main/graph/badge.svg)](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy)
26
+ [![Documentation Status](https://readthedocs.org/projects/ephemeral-pulumi-deploy/badge/?version=latest)](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
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
2
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
3
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
4
+ [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
5
+ [![Actions status](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions)
6
+ [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy)
7
+ [![PyPI Version](https://img.shields.io/pypi/v/ephemeral-pulumi-deploy.svg)](https://pypi.org/project/ephemeral-pulumi-deploy/)
8
+ [![Downloads](https://pepy.tech/badge/ephemeral-pulumi-deploy)](https://pepy.tech/project/ephemeral-pulumi-deploy)
9
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ephemeral-pulumi-deploy.svg)](https://pypi.org/project/ephemeral-pulumi-deploy/)
10
+ [![Codecov](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy/branch/main/graph/badge.svg)](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy)
11
+ [![Documentation Status](https://readthedocs.org/projects/ephemeral-pulumi-deploy/badge/?version=latest)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
@@ -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
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
17
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
18
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
19
+ [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
20
+ [![Actions status](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy/actions)
21
+ [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/LabAutomationAndScreening/ephemeral-pulumi-deploy)
22
+ [![PyPI Version](https://img.shields.io/pypi/v/ephemeral-pulumi-deploy.svg)](https://pypi.org/project/ephemeral-pulumi-deploy/)
23
+ [![Downloads](https://pepy.tech/badge/ephemeral-pulumi-deploy)](https://pepy.tech/project/ephemeral-pulumi-deploy)
24
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ephemeral-pulumi-deploy.svg)](https://pypi.org/project/ephemeral-pulumi-deploy/)
25
+ [![Codecov](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy/branch/main/graph/badge.svg)](https://codecov.io/gh/LabAutomationAndScreening/ephemeral-pulumi-deploy)
26
+ [![Documentation Status](https://readthedocs.org/projects/ephemeral-pulumi-deploy/badge/?version=latest)](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,4 @@
1
+ pulumi>=3.147.0
2
+ pulumi-aws>=6.66.3
3
+ pulumi-aws-native>=1.24.0
4
+ boto3>=1.36.3