fogies 0.0.0.dev4__tar.gz → 0.0.0.dev5__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.
Files changed (31) hide show
  1. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/PKG-INFO +5 -2
  2. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/pyproject.toml +21 -11
  3. fogies-0.0.0.dev5/src/fogies/retry.py +102 -0
  4. fogies-0.0.0.dev5/src/fogies/tasks/format.py +65 -0
  5. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tasks/poetry.py +10 -3
  6. fogies-0.0.0.dev5/src/fogies/tasks/terraform.py +173 -0
  7. fogies-0.0.0.dev5/src/fogies/tasks/terraform_backend.py +180 -0
  8. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tasks/test.py +1 -1
  9. fogies-0.0.0.dev5/src/fogies/terraform/alb.py +22 -0
  10. fogies-0.0.0.dev5/src/fogies/terraform/backend.py +191 -0
  11. fogies-0.0.0.dev5/src/fogies/terraform/ecr.py +23 -0
  12. fogies-0.0.0.dev5/src/fogies/terraform/hosted_zone.py +14 -0
  13. fogies-0.0.0.dev5/src/fogies/terraform/network.py +16 -0
  14. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tools/aws_environ.py +26 -6
  15. fogies-0.0.0.dev5/src/fogies/tools/boto.py +71 -0
  16. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tools/command.py +35 -4
  17. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tools/ollama.py +43 -48
  18. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tools/terraform.py +503 -445
  19. fogies-0.0.0.dev5/src/fogies/tools/terraform_backend.py +88 -0
  20. fogies-0.0.0.dev5/src/fogies/typing.py +17 -0
  21. fogies-0.0.0.dev4/fogies_paths.py +0 -21
  22. fogies-0.0.0.dev4/src/fogies/tasks/format.py +0 -37
  23. fogies-0.0.0.dev4/src/fogies/terraform/backend.py +0 -17
  24. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/README.md +0 -0
  25. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/__init__.py +0 -0
  26. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/py.typed +0 -0
  27. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tasks/__init__.py +0 -0
  28. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tasks/lint.py +0 -0
  29. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/terraform/__init__.py +0 -0
  30. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tools/__init__.py +0 -0
  31. {fogies-0.0.0.dev4 → fogies-0.0.0.dev5}/src/fogies/tools/environ.py +0 -0
@@ -1,16 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fogies
3
- Version: 0.0.0.dev4
3
+ Version: 0.0.0.dev5
4
4
  Summary:
5
5
  Author: James Fogarty
6
6
  Author-email: jayfo@users.noreply.github.com
7
7
  Requires-Python: >=3.14,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.14
10
+ Requires-Dist: boto3 (>=1.42.68,<2.0.0)
10
11
  Requires-Dist: filelock (>=3.25.2,<4.0.0)
11
- Requires-Dist: ollama (>=0.6.1,<0.7.0)
12
+ Requires-Dist: ollama (>=0.6.2,<0.7.0)
12
13
  Requires-Dist: psutil (>=7.2.2,<8.0.0)
13
14
  Requires-Dist: pydantic (>=2.12.5,<3.0.0)
15
+ Requires-Dist: tenacity (>=9.1.4,<10.0.0)
16
+ Requires-Dist: tomli-w (>=1.2.0,<2.0.0)
14
17
  Description-Content-Type: text/markdown
15
18
 
16
19
  # pyfogies
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "fogies"
3
3
  # Version follows semantic versioning <https://semver.org/>.
4
- version = "0.0.0-dev.4"
4
+ version = "0.0.0-dev.5"
5
5
  description = ""
6
6
  authors = [
7
7
  {name = "James Fogarty",email = "jayfo@users.noreply.github.com"}
@@ -12,39 +12,49 @@ requires-python = ">=3.14,<4.0"
12
12
  [tool.poetry]
13
13
  packages = [
14
14
  {include = "fogies", from = "src"},
15
- {include = "fogies_paths.py", from = "."},
16
15
  ]
17
16
  include = [
18
17
  {path = "src/fogies/py.typed", format = ["sdist", "wheel"]},
19
18
  ]
20
19
 
21
20
  [tool.poetry.dependencies]
22
- pydantic = "^2.12.5"
23
- ollama = "^0.6.1"
21
+ # ^ allows all minor and patch releases.
22
+ # Example: ^1.2.3 will match 1.x.x versions >=1.2.3 and <2.0.0.
23
+ # ~ allows only patch releases.
24
+ # Example: ~1.2.3 will match versions >=1.2.3 and <1.3.0.
25
+ boto3 = "^1.42.68"
24
26
  filelock = "^3.25.2"
27
+ ollama = "^0.6.2"
25
28
  psutil = "^7.2.2"
29
+ pydantic = "^2.12.5"
30
+ tenacity = "^9.1.4"
31
+ tomli-w = "^1.2.0"
26
32
 
27
33
  [tool.poetry.group.dev.dependencies]
28
34
  # ^ allows all minor and patch releases.
29
- # Example: ^1.2.3 will match 1.x.x versions >=1.2.3 but <2.0.0.
35
+ # Example: ^1.2.3 will match 1.x.x versions >=1.2.3 and <2.0.0.
30
36
  # ~ allows only patch releases.
31
- # Example: ~1.2.3 will match versions >=1.2.3 but <1.3.0.
37
+ # Example: ~1.2.3 will match versions >=1.2.3 and <1.3.0.
32
38
  basedpyright = "^1.36.1"
33
- black = "~25.12.0" # Black does not semver.
34
- boto3 = "^1.42.68"
35
- boto3-stubs-full = "^1.42.68"
39
+ black = "~26.5.1" # Black does not semver.
40
+ boto3-stubs = {version = "^1.43.60", extras = ["s3"]}
36
41
  invoke = "^2.2.1"
37
- isort = "^7.0.0"
42
+ isort = "^8.0.1"
38
43
  poetry = "^2.2.1"
39
44
  pytest = "^9.0.2"
45
+ requests = "~2.33.0"
40
46
  semver = "^3.0.4"
47
+ types-requests = "~2.33.0.20260712"
41
48
 
42
49
  [tool.basedpyright]
43
50
  typeCheckingMode = "recommended"
44
51
  pythonVersion = "3.14"
45
- include = ["fogies_paths.py", "src", "tasks", "tests"]
52
+ include = ["src", "tasks", "tests"]
46
53
  allowedUntypedLibraries = ["invoke"]
47
54
 
55
+ [tool.black]
56
+ target-version = ["py314"]
57
+
48
58
  [build-system]
49
59
  requires = ["poetry-core>=2.0.0,<3.0.0"]
50
60
  build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,102 @@
1
+ """Shared retry policies built on tenacity.
2
+
3
+ Call sites should use one of the named profiles below rather than inventing
4
+ timeout/interval/attempt values locally. Add a new profile here if a case
5
+ genuinely needs different numbers.
6
+
7
+ Pass *log_path* to a profile to append a line for each retried failure. This
8
+ is for retries whose cause isn't already well understood and is worth
9
+ investigating; well-understood retries (e.g. polling for a resource to come
10
+ up) don't need it. Nothing is written, and no file is created, unless a
11
+ retry actually happens.
12
+ """
13
+
14
+ import pathlib
15
+ from collections.abc import Callable
16
+ from datetime import datetime
17
+ from typing import TypeVar
18
+
19
+ import tenacity
20
+
21
+ _T = TypeVar("_T")
22
+
23
+
24
+ def _log_retry(log_path: pathlib.Path) -> Callable[[tenacity.RetryCallState], None]:
25
+ def log_it(retry_state: tenacity.RetryCallState) -> None:
26
+ outcome = retry_state.outcome
27
+ assert outcome is not None and outcome.failed
28
+ exception = outcome.exception()
29
+ assert exception is not None
30
+ line = "{} attempt {} raised {}: {}\n".format(
31
+ datetime.now().isoformat(timespec="seconds"),
32
+ retry_state.attempt_number,
33
+ type(exception).__name__,
34
+ exception,
35
+ )
36
+ with log_path.open("a", encoding="utf-8") as f:
37
+ _ = f.write(line)
38
+
39
+ return log_it
40
+
41
+
42
+ def readiness_poll(
43
+ *,
44
+ exceptions: type[BaseException] | tuple[type[BaseException], ...],
45
+ timeout: float,
46
+ poll_interval: float,
47
+ reraise: bool = True,
48
+ ) -> tenacity.Retrying:
49
+ """Retry policy for polling until a resource becomes ready or *timeout* elapses.
50
+
51
+ Retries only on *exceptions*; any other exception propagates immediately.
52
+ On giving up, re-raises the last *exceptions* instance unless *reraise* is
53
+ False, in which case a `tenacity.RetryError` is raised instead.
54
+ """
55
+ return tenacity.Retrying(
56
+ retry=tenacity.retry_if_exception_type(exceptions),
57
+ wait=tenacity.wait_fixed(poll_interval),
58
+ stop=tenacity.stop_after_delay(timeout),
59
+ reraise=reraise,
60
+ )
61
+
62
+
63
+ def readiness_poll_short(
64
+ *,
65
+ exceptions: type[BaseException] | tuple[type[BaseException], ...],
66
+ reraise: bool = True,
67
+ ) -> tenacity.Retrying:
68
+ """Readiness poll profile for something expected ready within seconds."""
69
+ return readiness_poll(
70
+ exceptions=exceptions,
71
+ timeout=10.0,
72
+ poll_interval=0.1,
73
+ reraise=reraise,
74
+ )
75
+
76
+
77
+ def readiness_poll_long(
78
+ *,
79
+ exceptions: type[BaseException] | tuple[type[BaseException], ...],
80
+ reraise: bool = True,
81
+ ) -> tenacity.Retrying:
82
+ """Readiness poll profile for something that can take minutes to become ready."""
83
+ return readiness_poll(
84
+ exceptions=exceptions,
85
+ timeout=300.0,
86
+ poll_interval=5.0,
87
+ reraise=reraise,
88
+ )
89
+
90
+
91
+ def retry_transient(
92
+ *,
93
+ exceptions: type[BaseException] | tuple[type[BaseException], ...],
94
+ log_path: pathlib.Path | None = None,
95
+ ) -> Callable[[Callable[[], _T]], Callable[[], _T]]:
96
+ """Retry profile for a flaky one-off operation, with no backoff."""
97
+ return tenacity.retry(
98
+ retry=tenacity.retry_if_exception_type(exceptions),
99
+ stop=tenacity.stop_after_attempt(5),
100
+ reraise=True,
101
+ before_sleep=_log_retry(log_path) if log_path is not None else None,
102
+ )
@@ -0,0 +1,65 @@
1
+ """
2
+ Tasks for applying code formatting.
3
+ """
4
+
5
+ import pathlib
6
+ from typing import Callable, cast
7
+
8
+ from invoke.context import Context
9
+ from invoke.tasks import Task, task
10
+
11
+ from fogies.tools.command import CommandParams
12
+ from fogies.tools.terraform import terraform
13
+
14
+
15
+ def get_task_format(
16
+ *,
17
+ fmt_black: bool = False,
18
+ fmt_isort: bool = False,
19
+ fmt_terraform: bool = False,
20
+ terraform_binary_cache_path: pathlib.Path | None = None,
21
+ ) -> Task[Callable[[Context], None]]:
22
+ if fmt_terraform and terraform_binary_cache_path is None:
23
+ raise ValueError(
24
+ "terraform_binary_cache_path is required when fmt_terraform is set"
25
+ )
26
+ if not (fmt_black or fmt_isort or fmt_terraform):
27
+ raise ValueError("At least one fmt flag must be set.")
28
+
29
+ @task(name="format") # pyright: ignore[reportUntypedFunctionDecorator]
30
+ def task_format(context: Context) -> None:
31
+ """
32
+ Apply code formatting.
33
+ """
34
+ # isort before black: black should see the post-sort import layout.
35
+ if fmt_isort:
36
+ _ = context.run(
37
+ command=" ".join(
38
+ [
39
+ "isort",
40
+ ".",
41
+ ]
42
+ ),
43
+ echo=True,
44
+ )
45
+
46
+ if fmt_black:
47
+ _ = context.run(
48
+ command=" ".join(
49
+ [
50
+ "black",
51
+ ".",
52
+ ]
53
+ ),
54
+ echo=True,
55
+ )
56
+
57
+ if fmt_terraform:
58
+ assert terraform_binary_cache_path is not None
59
+ with terraform(binary_cache_path=terraform_binary_cache_path) as tf:
60
+ _ = tf.format(
61
+ command_params=CommandParams(context=context),
62
+ path=pathlib.Path("."),
63
+ )
64
+
65
+ return cast(Task[Callable[[Context], None]], task_format)
@@ -36,17 +36,24 @@ def get_task_publish(*, path_secrets_poetry: Path) -> Task[Callable[[Context], N
36
36
  """
37
37
  Publish package to PyPI.
38
38
  """
39
+ if not path_secrets_poetry.exists():
40
+ raise FileNotFoundError(
41
+ "Poetry secrets file '{}' does not exist.\nSee provided template.".format(
42
+ path_secrets_poetry
43
+ )
44
+ )
45
+
39
46
  with path_secrets_poetry.open("rb") as handle:
40
47
  secrets_poetry = tomllib.load(handle)
41
48
 
42
- api_key: str = cast(str, secrets_poetry["api"]["api_key"])
49
+ api_key: str = cast(str, secrets_poetry["pypi"]["api_key"])
43
50
 
44
51
  _ = context.run(
45
52
  command=" ".join(
46
53
  [
47
54
  "poetry",
48
55
  "publish",
49
- "--username {}".format("__token__"),
56
+ "--username __token__",
50
57
  "--password {}".format(api_key),
51
58
  ]
52
59
  ),
@@ -56,7 +63,7 @@ def get_task_publish(*, path_secrets_poetry: Path) -> Task[Callable[[Context], N
56
63
  return cast(Task[Callable[[Context], None]], task_publish)
57
64
 
58
65
 
59
- def get_collection(path_secrets_poetry: Path) -> Collection:
66
+ def get_collection(*, path_secrets_poetry: Path) -> Collection:
60
67
  """
61
68
  Get a collection of tasks.
62
69
  """
@@ -0,0 +1,173 @@
1
+ """Generic tasks for applying and destroying a Terraform configuration."""
2
+
3
+ import pathlib
4
+ from contextlib import ExitStack
5
+ from typing import Callable, cast
6
+
7
+ from invoke.context import Context
8
+ from invoke.tasks import Task, task
9
+
10
+ from fogies.terraform.backend import BackendConfig
11
+ from fogies.tools.aws_environ import AwsEnvironContextManager
12
+ from fogies.tools.command import CommandParams
13
+ from fogies.tools.terraform import (
14
+ ApplyParams,
15
+ DestroyParams,
16
+ InitParams,
17
+ TfbackendContextManager,
18
+ TfvarsContextManager,
19
+ terraform,
20
+ )
21
+
22
+
23
+ def get_task_apply(
24
+ *,
25
+ binary_cache_path: pathlib.Path,
26
+ module_path: pathlib.Path,
27
+ aws_environ: AwsEnvironContextManager | None = None,
28
+ backend: BackendConfig | None = None,
29
+ backend_status_path: pathlib.Path | None = None,
30
+ tfbackend: TfbackendContextManager | None = None,
31
+ tfvars: TfvarsContextManager | None = None,
32
+ default_init: bool = True,
33
+ default_init_upgrade: bool = False,
34
+ default_init_reconfigure: bool = False,
35
+ default_apply_auto_approve: bool = False,
36
+ ) -> Task[Callable[[Context, bool, bool, bool, bool], None]]:
37
+ if backend is not None and tfbackend is None:
38
+ raise ValueError("tfbackend is required when backend is set")
39
+ if (backend_status_path is None) != (backend is None):
40
+ raise ValueError("backend_status_path and backend must be provided together")
41
+
42
+ @task(name="apply") # pyright: ignore[reportUntypedFunctionDecorator]
43
+ def task_apply(
44
+ context: Context,
45
+ init: bool = default_init,
46
+ init_upgrade: bool = default_init_upgrade,
47
+ init_reconfigure: bool = default_init_reconfigure,
48
+ apply_auto_approve: bool = default_apply_auto_approve,
49
+ ) -> None:
50
+ """
51
+ Apply a Terraform configuration.
52
+
53
+ Flags:
54
+ --init Run terraform init before apply (downloads providers, sets up backend).
55
+ --init-upgrade Pass -upgrade to init; checks for newer provider versions within constraints.
56
+ --init-reconfigure Pass -reconfigure to init; re-initializes backend from scratch.
57
+ --apply-auto-approve Skip Terraform's interactive confirmation prompt.
58
+
59
+ --init-upgrade and --init-reconfigure require --init.
60
+ """
61
+ if init_upgrade and not init:
62
+ raise ValueError("--init-upgrade requires --init")
63
+ if init_reconfigure and not init:
64
+ raise ValueError("--init-reconfigure requires --init")
65
+
66
+ command_params = CommandParams(context=context)
67
+ init_params = (
68
+ InitParams(upgrade=init_upgrade, reconfigure=init_reconfigure)
69
+ if init
70
+ else None
71
+ )
72
+
73
+ with ExitStack() as stack:
74
+ if aws_environ is not None:
75
+ _ = stack.enter_context(aws_environ)
76
+
77
+ tfbackend_path = stack.enter_context(tfbackend) if tfbackend else None
78
+ tfvars_path = stack.enter_context(tfvars) if tfvars else None
79
+
80
+ _ = stack.enter_context(
81
+ terraform(
82
+ binary_cache_path=binary_cache_path,
83
+ command_params=command_params,
84
+ module_path=module_path,
85
+ backend=backend,
86
+ backend_status_path=backend_status_path,
87
+ tfbackend_path=tfbackend_path,
88
+ tfvars_path=tfvars_path,
89
+ init_on_entry=init,
90
+ init_params=init_params,
91
+ apply_on_entry=True,
92
+ apply_params=ApplyParams(auto_approve=apply_auto_approve),
93
+ destroy_on_exit=False,
94
+ )
95
+ )
96
+
97
+ return cast(Task[Callable[[Context, bool, bool, bool, bool], None]], task_apply)
98
+
99
+
100
+ def get_task_destroy(
101
+ *,
102
+ binary_cache_path: pathlib.Path,
103
+ module_path: pathlib.Path,
104
+ aws_environ: AwsEnvironContextManager | None = None,
105
+ backend: BackendConfig | None = None,
106
+ backend_status_path: pathlib.Path | None = None,
107
+ tfbackend: TfbackendContextManager | None = None,
108
+ tfvars: TfvarsContextManager | None = None,
109
+ default_init: bool = True,
110
+ default_init_upgrade: bool = False,
111
+ default_init_reconfigure: bool = False,
112
+ default_destroy_auto_approve: bool = False,
113
+ ) -> Task[Callable[[Context, bool, bool, bool, bool], None]]:
114
+ if backend is not None and tfbackend is None:
115
+ raise ValueError("tfbackend is required when backend is set")
116
+ if (backend_status_path is None) != (backend is None):
117
+ raise ValueError("backend_status_path and backend must be provided together")
118
+
119
+ @task(name="destroy") # pyright: ignore[reportUntypedFunctionDecorator]
120
+ def task_destroy(
121
+ context: Context,
122
+ init: bool = default_init,
123
+ init_upgrade: bool = default_init_upgrade,
124
+ init_reconfigure: bool = default_init_reconfigure,
125
+ destroy_auto_approve: bool = default_destroy_auto_approve,
126
+ ) -> None:
127
+ """
128
+ Destroy a Terraform configuration.
129
+
130
+ Flags:
131
+ --init Run terraform init before destroy (downloads providers, sets up backend).
132
+ --init-upgrade Pass -upgrade to init; checks for newer provider versions within constraints.
133
+ --init-reconfigure Pass -reconfigure to init; re-initializes backend from scratch.
134
+ --destroy-auto-approve Skip Terraform's interactive confirmation prompt.
135
+
136
+ --init-upgrade and --init-reconfigure require --init.
137
+ """
138
+ if init_upgrade and not init:
139
+ raise ValueError("--init-upgrade requires --init")
140
+ if init_reconfigure and not init:
141
+ raise ValueError("--init-reconfigure requires --init")
142
+
143
+ command_params = CommandParams(context=context)
144
+ init_params = (
145
+ InitParams(upgrade=init_upgrade, reconfigure=init_reconfigure)
146
+ if init
147
+ else None
148
+ )
149
+
150
+ with ExitStack() as stack:
151
+ if aws_environ is not None:
152
+ _ = stack.enter_context(aws_environ)
153
+
154
+ tfbackend_path = stack.enter_context(tfbackend) if tfbackend else None
155
+ tfvars_path = stack.enter_context(tfvars) if tfvars else None
156
+
157
+ _ = stack.enter_context(
158
+ terraform(
159
+ binary_cache_path=binary_cache_path,
160
+ command_params=command_params,
161
+ module_path=module_path,
162
+ backend=backend,
163
+ backend_status_path=backend_status_path,
164
+ tfbackend_path=tfbackend_path,
165
+ tfvars_path=tfvars_path,
166
+ init_on_entry=init,
167
+ init_params=init_params,
168
+ destroy_on_exit=True,
169
+ destroy_params=DestroyParams(auto_approve=destroy_auto_approve),
170
+ )
171
+ )
172
+
173
+ return cast(Task[Callable[[Context, bool, bool, bool, bool], None]], task_destroy)
@@ -0,0 +1,180 @@
1
+ """Tasks for applying and destroying a Terraform backend configuration."""
2
+
3
+ import pathlib
4
+ from collections.abc import Callable
5
+ from contextlib import ExitStack
6
+ from typing import cast
7
+
8
+ from invoke.context import Context
9
+ from invoke.tasks import Task, task
10
+
11
+ from fogies.terraform.backend import BackendOutput
12
+ from fogies.tools.aws_environ import AwsEnvironContextManager
13
+ from fogies.tools.command import CommandParams
14
+ from fogies.tools.terraform import (
15
+ ApplyParams,
16
+ DestroyParams,
17
+ InitParams,
18
+ TerraformOutputModel,
19
+ TfvarsContextManager,
20
+ )
21
+ from fogies.tools.terraform_backend import terraform_backend
22
+
23
+
24
+ def get_task_backend_apply(
25
+ *,
26
+ binary_cache_path: pathlib.Path,
27
+ module_path: pathlib.Path,
28
+ aws_environ: AwsEnvironContextManager | None = None,
29
+ backend_status_path: pathlib.Path,
30
+ tfbackend_path: pathlib.Path | None = None,
31
+ tfvars: TfvarsContextManager | None = None,
32
+ output_model: type[TerraformOutputModel],
33
+ output_model_get_backend: Callable[[TerraformOutputModel], BackendOutput],
34
+ default_init: bool = True,
35
+ default_init_upgrade: bool = False,
36
+ default_init_reconfigure: bool = False,
37
+ default_apply_auto_approve: bool = False,
38
+ default_output: bool = False,
39
+ ) -> Task[Callable[[Context, bool, bool, bool, bool, bool], None]]:
40
+ @task(name="apply") # pyright: ignore[reportUntypedFunctionDecorator]
41
+ def task_apply(
42
+ context: Context,
43
+ init: bool = default_init,
44
+ init_upgrade: bool = default_init_upgrade,
45
+ init_reconfigure: bool = default_init_reconfigure,
46
+ apply_auto_approve: bool = default_apply_auto_approve,
47
+ output: bool = default_output,
48
+ ) -> None:
49
+ """
50
+ Apply a Terraform backend configuration, and record it as applied.
51
+
52
+ Flags:
53
+ --init Run terraform init before apply (downloads providers, sets up backend).
54
+ --init-upgrade Pass -upgrade to init; checks for newer provider versions within constraints.
55
+ --init-reconfigure Pass -reconfigure to init; re-initializes backend from scratch.
56
+ --apply-auto-approve Skip Terraform's interactive confirmation prompt.
57
+ --output Print terraform output as JSON after apply.
58
+
59
+ --init-upgrade and --init-reconfigure require --init.
60
+ """
61
+ if init_upgrade and not init:
62
+ raise ValueError("--init-upgrade requires --init")
63
+ if init_reconfigure and not init:
64
+ raise ValueError("--init-reconfigure requires --init")
65
+
66
+ command_params = CommandParams(context=context)
67
+ init_params = (
68
+ InitParams(upgrade=init_upgrade, reconfigure=init_reconfigure)
69
+ if init
70
+ else None
71
+ )
72
+
73
+ with ExitStack() as stack:
74
+ if aws_environ is not None:
75
+ _ = stack.enter_context(aws_environ)
76
+
77
+ tfvars_path = stack.enter_context(tfvars) if tfvars else None
78
+
79
+ output_result = stack.enter_context(
80
+ terraform_backend(
81
+ binary_cache_path=binary_cache_path,
82
+ command_params=command_params,
83
+ module_path=module_path,
84
+ backend_status_path=backend_status_path,
85
+ tfbackend_path=tfbackend_path,
86
+ tfvars_path=tfvars_path,
87
+ init_on_entry=init,
88
+ init_params=init_params,
89
+ apply_on_entry=True,
90
+ apply_params=ApplyParams(auto_approve=apply_auto_approve),
91
+ destroy_on_exit=False,
92
+ output_model=output_model,
93
+ output_model_get_backend=output_model_get_backend,
94
+ )
95
+ )
96
+
97
+ if output:
98
+ print(output_model_get_backend(output_result).model_dump_json(indent=2))
99
+
100
+ return cast(
101
+ Task[Callable[[Context, bool, bool, bool, bool, bool], None]], task_apply
102
+ )
103
+
104
+
105
+ def get_task_backend_destroy(
106
+ *,
107
+ binary_cache_path: pathlib.Path,
108
+ module_path: pathlib.Path,
109
+ aws_environ: AwsEnvironContextManager | None = None,
110
+ backend_status_path: pathlib.Path,
111
+ tfbackend_path: pathlib.Path | None = None,
112
+ tfvars: TfvarsContextManager | None = None,
113
+ output_model: type[TerraformOutputModel],
114
+ output_model_get_backend: Callable[[TerraformOutputModel], BackendOutput],
115
+ default_init: bool = True,
116
+ default_init_upgrade: bool = False,
117
+ default_init_reconfigure: bool = False,
118
+ default_destroy_auto_approve: bool = False,
119
+ ) -> Task[Callable[[Context, bool, bool, bool, bool], None]]:
120
+ @task(name="destroy") # pyright: ignore[reportUntypedFunctionDecorator]
121
+ def task_destroy(
122
+ context: Context,
123
+ init: bool = default_init,
124
+ init_upgrade: bool = default_init_upgrade,
125
+ init_reconfigure: bool = default_init_reconfigure,
126
+ destroy_auto_approve: bool = default_destroy_auto_approve,
127
+ ) -> None:
128
+ """
129
+ Destroy a Terraform backend configuration, and record it as destroyed.
130
+
131
+ Reads the backend module's own output, verifies every state it
132
+ declares is empty of resources, then clears the bucket's state
133
+ objects before destroying it. Refuses (raises) if any declared state
134
+ still has resources, regardless of how this task is invoked.
135
+
136
+ Flags:
137
+ --init Run terraform init before destroy (downloads providers, sets up backend).
138
+ --init-upgrade Pass -upgrade to init; checks for newer provider versions within constraints.
139
+ --init-reconfigure Pass -reconfigure to init; re-initializes backend from scratch.
140
+ --destroy-auto-approve Skip Terraform's interactive confirmation prompt.
141
+
142
+ --init-upgrade and --init-reconfigure require --init.
143
+ """
144
+ if init_upgrade and not init:
145
+ raise ValueError("--init-upgrade requires --init")
146
+ if init_reconfigure and not init:
147
+ raise ValueError("--init-reconfigure requires --init")
148
+
149
+ command_params = CommandParams(context=context)
150
+ init_params = (
151
+ InitParams(upgrade=init_upgrade, reconfigure=init_reconfigure)
152
+ if init
153
+ else None
154
+ )
155
+
156
+ with ExitStack() as stack:
157
+ if aws_environ is not None:
158
+ _ = stack.enter_context(aws_environ)
159
+
160
+ tfvars_path = stack.enter_context(tfvars) if tfvars else None
161
+
162
+ _ = stack.enter_context(
163
+ terraform_backend(
164
+ binary_cache_path=binary_cache_path,
165
+ command_params=command_params,
166
+ module_path=module_path,
167
+ backend_status_path=backend_status_path,
168
+ tfbackend_path=tfbackend_path,
169
+ tfvars_path=tfvars_path,
170
+ init_on_entry=init,
171
+ init_params=init_params,
172
+ apply_on_entry=False,
173
+ destroy_on_exit=True,
174
+ destroy_params=DestroyParams(auto_approve=destroy_auto_approve),
175
+ output_model=output_model,
176
+ output_model_get_backend=output_model_get_backend,
177
+ )
178
+ )
179
+
180
+ return cast(Task[Callable[[Context, bool, bool, bool, bool], None]], task_destroy)
@@ -10,7 +10,7 @@ from invoke.context import Context
10
10
  from invoke.tasks import Task, task
11
11
 
12
12
 
13
- def get_task_test(path_tests: str | None = None) -> Task[Callable[[Context], None]]:
13
+ def get_task_test(*, path_tests: str | None = None) -> Task[Callable[[Context], None]]:
14
14
  @task(name="test") # pyright: ignore[reportUntypedFunctionDecorator]
15
15
  def task_test(context: Context) -> None:
16
16
  """
@@ -0,0 +1,22 @@
1
+ """Pydantic models for Terraform ALB module variables and output."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class AlbVars(BaseModel):
7
+ region: str
8
+ name: str
9
+ subnet_ids: list[str]
10
+ security_group_ids: list[str]
11
+ self_signed_certificate: bool = False
12
+ certificate_arn: str | None = None
13
+ tags: dict[str, str] = {}
14
+
15
+
16
+ class AlbOutput(BaseModel):
17
+ alb_arn: str
18
+ alb_dns_name: str
19
+ alb_zone_id: str
20
+ listener_http_arn: str
21
+ listener_https_arn: str
22
+ certificate_pem: str | None