fogies 0.0.0.dev10__tar.gz → 0.0.0.dev11__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.
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/PKG-INFO +1 -1
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/pyproject.toml +1 -1
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/terraform.py +49 -21
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/backend.py +73 -35
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/aws_environ.py +8 -5
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/terraform.py +51 -24
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/README.md +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/__init__.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/aws_access_key.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/boto_clients.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/download_verified.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/includes/templates/aws.toml.template +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/includes/templates/backend_status.toml.template +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/includes/templates/poetry.toml.template +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/py.typed +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/pyfogies.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/ready_poll.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/retry.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/__init__.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/access_key.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/format.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/lint.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/poetry.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/terraform_backend.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/test.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/templates.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/__init__.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/alb.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/cloudwatch.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/ecr.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/ecs.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/hosted_zone.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/network.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/__init__.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/boto.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/claude_sync.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/command.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/environ.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/go.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/ollama.py +0 -0
- {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/terraform_backend.py +0 -0
|
@@ -26,7 +26,7 @@ def get_task_apply(
|
|
|
26
26
|
binary_cache_path: pathlib.Path,
|
|
27
27
|
module_path: pathlib.Path,
|
|
28
28
|
aws_environ_factory: AwsEnvironFactory | None = None,
|
|
29
|
-
|
|
29
|
+
backend_config: BackendConfig | None = None,
|
|
30
30
|
backend_status_path: pathlib.Path | None = None,
|
|
31
31
|
tfbackend_factory: TfbackendFactory | None = None,
|
|
32
32
|
tfvars_factory: TfvarsFactory | None = None,
|
|
@@ -35,10 +35,18 @@ def get_task_apply(
|
|
|
35
35
|
default_init_reconfigure: bool = False,
|
|
36
36
|
default_apply_auto_approve: bool = False,
|
|
37
37
|
) -> Task[Callable[[Context, bool, bool, bool, bool], None]]:
|
|
38
|
-
if
|
|
39
|
-
raise ValueError("tfbackend_factory is required when
|
|
40
|
-
if (
|
|
41
|
-
|
|
38
|
+
if backend_config is not None and tfbackend_factory is None:
|
|
39
|
+
raise ValueError("tfbackend_factory is required when backend_config is set")
|
|
40
|
+
if (backend_config is not None or tfbackend_factory is not None) and (
|
|
41
|
+
aws_environ_factory is None
|
|
42
|
+
):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"aws_environ_factory is required when backend_config or tfbackend_factory is set"
|
|
45
|
+
)
|
|
46
|
+
if (backend_status_path is None) != (backend_config is None):
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"backend_status_path and backend_config must be provided together"
|
|
49
|
+
)
|
|
42
50
|
|
|
43
51
|
@task(
|
|
44
52
|
name="apply",
|
|
@@ -72,11 +80,16 @@ def get_task_apply(
|
|
|
72
80
|
)
|
|
73
81
|
|
|
74
82
|
with ExitStack() as stack:
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
aws_environ = (
|
|
84
|
+
stack.enter_context(aws_environ_factory())
|
|
85
|
+
if aws_environ_factory
|
|
86
|
+
else None
|
|
87
|
+
)
|
|
77
88
|
|
|
78
89
|
tfbackend_path = (
|
|
79
|
-
stack.enter_context(tfbackend_factory())
|
|
90
|
+
stack.enter_context(tfbackend_factory(aws_environ))
|
|
91
|
+
if tfbackend_factory and aws_environ
|
|
92
|
+
else None
|
|
80
93
|
)
|
|
81
94
|
tfvars_path = (
|
|
82
95
|
stack.enter_context(tfvars_factory()) if tfvars_factory else None
|
|
@@ -87,7 +100,8 @@ def get_task_apply(
|
|
|
87
100
|
binary_cache_path=binary_cache_path,
|
|
88
101
|
command_params=command_params,
|
|
89
102
|
module_path=module_path,
|
|
90
|
-
|
|
103
|
+
backend_config=backend_config,
|
|
104
|
+
aws_environ=aws_environ,
|
|
91
105
|
backend_status_path=backend_status_path,
|
|
92
106
|
tfbackend_path=tfbackend_path,
|
|
93
107
|
tfvars_path=tfvars_path,
|
|
@@ -107,7 +121,7 @@ def get_task_destroy(
|
|
|
107
121
|
binary_cache_path: pathlib.Path,
|
|
108
122
|
module_path: pathlib.Path,
|
|
109
123
|
aws_environ_factory: AwsEnvironFactory | None = None,
|
|
110
|
-
|
|
124
|
+
backend_config: BackendConfig | None = None,
|
|
111
125
|
backend_status_path: pathlib.Path | None = None,
|
|
112
126
|
tfbackend_factory: TfbackendFactory | None = None,
|
|
113
127
|
tfvars_factory: TfvarsFactory | None = None,
|
|
@@ -116,10 +130,18 @@ def get_task_destroy(
|
|
|
116
130
|
default_init_reconfigure: bool = False,
|
|
117
131
|
default_destroy_auto_approve: bool = False,
|
|
118
132
|
) -> Task[Callable[[Context, bool, bool, bool, bool], None]]:
|
|
119
|
-
if
|
|
120
|
-
raise ValueError("tfbackend_factory is required when
|
|
121
|
-
if (
|
|
122
|
-
|
|
133
|
+
if backend_config is not None and tfbackend_factory is None:
|
|
134
|
+
raise ValueError("tfbackend_factory is required when backend_config is set")
|
|
135
|
+
if (backend_config is not None or tfbackend_factory is not None) and (
|
|
136
|
+
aws_environ_factory is None
|
|
137
|
+
):
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"aws_environ_factory is required when backend_config or tfbackend_factory is set"
|
|
140
|
+
)
|
|
141
|
+
if (backend_status_path is None) != (backend_config is None):
|
|
142
|
+
raise ValueError(
|
|
143
|
+
"backend_status_path and backend_config must be provided together"
|
|
144
|
+
)
|
|
123
145
|
|
|
124
146
|
@task(
|
|
125
147
|
name="destroy",
|
|
@@ -153,11 +175,16 @@ def get_task_destroy(
|
|
|
153
175
|
)
|
|
154
176
|
|
|
155
177
|
with ExitStack() as stack:
|
|
156
|
-
|
|
157
|
-
|
|
178
|
+
aws_environ = (
|
|
179
|
+
stack.enter_context(aws_environ_factory())
|
|
180
|
+
if aws_environ_factory
|
|
181
|
+
else None
|
|
182
|
+
)
|
|
158
183
|
|
|
159
184
|
tfbackend_path = (
|
|
160
|
-
stack.enter_context(tfbackend_factory())
|
|
185
|
+
stack.enter_context(tfbackend_factory(aws_environ))
|
|
186
|
+
if tfbackend_factory and aws_environ
|
|
187
|
+
else None
|
|
161
188
|
)
|
|
162
189
|
tfvars_path = (
|
|
163
190
|
stack.enter_context(tfvars_factory()) if tfvars_factory else None
|
|
@@ -168,7 +195,8 @@ def get_task_destroy(
|
|
|
168
195
|
binary_cache_path=binary_cache_path,
|
|
169
196
|
command_params=command_params,
|
|
170
197
|
module_path=module_path,
|
|
171
|
-
|
|
198
|
+
backend_config=backend_config,
|
|
199
|
+
aws_environ=aws_environ,
|
|
172
200
|
backend_status_path=backend_status_path,
|
|
173
201
|
tfbackend_path=tfbackend_path,
|
|
174
202
|
tfvars_path=tfvars_path,
|
|
@@ -188,7 +216,7 @@ def get_collection(
|
|
|
188
216
|
binary_cache_path: pathlib.Path,
|
|
189
217
|
module_path: pathlib.Path,
|
|
190
218
|
aws_environ_factory: AwsEnvironFactory | None = None,
|
|
191
|
-
|
|
219
|
+
backend_config: BackendConfig | None = None,
|
|
192
220
|
backend_status_path: pathlib.Path | None = None,
|
|
193
221
|
tfbackend_factory: TfbackendFactory | None = None,
|
|
194
222
|
tfvars_factory: TfvarsFactory | None = None,
|
|
@@ -200,7 +228,7 @@ def get_collection(
|
|
|
200
228
|
binary_cache_path=binary_cache_path,
|
|
201
229
|
module_path=module_path,
|
|
202
230
|
aws_environ_factory=aws_environ_factory,
|
|
203
|
-
|
|
231
|
+
backend_config=backend_config,
|
|
204
232
|
backend_status_path=backend_status_path,
|
|
205
233
|
tfbackend_factory=tfbackend_factory,
|
|
206
234
|
tfvars_factory=tfvars_factory,
|
|
@@ -211,7 +239,7 @@ def get_collection(
|
|
|
211
239
|
binary_cache_path=binary_cache_path,
|
|
212
240
|
module_path=module_path,
|
|
213
241
|
aws_environ_factory=aws_environ_factory,
|
|
214
|
-
|
|
242
|
+
backend_config=backend_config,
|
|
215
243
|
backend_status_path=backend_status_path,
|
|
216
244
|
tfbackend_factory=tfbackend_factory,
|
|
217
245
|
tfvars_factory=tfvars_factory,
|
|
@@ -14,68 +14,94 @@ from fogies.tools.boto import s3_delete_keys
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class BackendVars(BaseModel):
|
|
17
|
-
|
|
17
|
+
backend_name: str
|
|
18
18
|
region: str
|
|
19
|
-
|
|
19
|
+
state_names: list[str]
|
|
20
20
|
tags: dict[str, str] = {}
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
# S3 limits the length of a bucket name.
|
|
24
|
+
_BUCKET_NAME_MAX_LENGTH = 63
|
|
25
|
+
|
|
26
|
+
|
|
23
27
|
class BackendConfig(BaseModel):
|
|
24
|
-
"""Connection
|
|
28
|
+
"""Connection config for a single state within an S3 Terraform backend.
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
The bucket name includes the AWS account ID, which is not stored in code.
|
|
31
|
+
A config is safe to create at import time; bucket_name() is given the
|
|
32
|
+
account ID (e.g. from AwsEnviron) when the bucket is actually needed. This
|
|
33
|
+
lets a tenant point at an already-applied backend without knowing about
|
|
34
|
+
any other state sharing its bucket.
|
|
29
35
|
"""
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
state_name: str
|
|
38
|
+
backend_name: str
|
|
33
39
|
region: str
|
|
34
40
|
key: str
|
|
35
41
|
|
|
36
42
|
@staticmethod
|
|
37
|
-
def for_state(
|
|
38
|
-
|
|
43
|
+
def for_state(
|
|
44
|
+
*, backend_name: str, region: str, state_name: str
|
|
45
|
+
) -> "BackendConfig":
|
|
46
|
+
"""Return config for a state, without applying the backend module.
|
|
39
47
|
|
|
40
|
-
Mirrors the
|
|
48
|
+
Mirrors the key naming the backend Terraform module derives
|
|
41
49
|
internally, so a tenant can point at an already-applied backend without
|
|
42
50
|
knowing about any other state sharing its bucket.
|
|
43
51
|
"""
|
|
44
52
|
return BackendConfig(
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
state_name=state_name,
|
|
54
|
+
backend_name=backend_name,
|
|
47
55
|
region=region,
|
|
48
|
-
key="{}/terraform.tfstate".format(
|
|
56
|
+
key="{}/terraform.tfstate".format(state_name),
|
|
49
57
|
)
|
|
50
58
|
|
|
59
|
+
def bucket_name(self, *, account_id: str) -> str:
|
|
60
|
+
"""Return the name of the backend bucket in the given account.
|
|
61
|
+
|
|
62
|
+
Mirrors the bucket naming of the backend Terraform module. Raises
|
|
63
|
+
ValueError if the backend name leaves no room for the account and
|
|
64
|
+
region suffix.
|
|
65
|
+
"""
|
|
66
|
+
bucket_name = "{}-{}-{}-an".format(self.backend_name, account_id, self.region)
|
|
67
|
+
if len(bucket_name) > _BUCKET_NAME_MAX_LENGTH:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"Backend name '{}' makes a bucket name longer than {} characters".format(
|
|
70
|
+
self.backend_name,
|
|
71
|
+
_BUCKET_NAME_MAX_LENGTH,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return bucket_name
|
|
75
|
+
|
|
51
76
|
|
|
52
77
|
class BackendOutput(BaseModel):
|
|
78
|
+
backend_name: str
|
|
53
79
|
bucket_name: str
|
|
54
80
|
region: str
|
|
55
81
|
state_keys: dict[str, str]
|
|
56
82
|
|
|
57
|
-
def config(self, *,
|
|
83
|
+
def config(self, *, state_name: str) -> BackendConfig:
|
|
58
84
|
"""Return connection config for one of this backend's declared states.
|
|
59
85
|
|
|
60
|
-
Raises ValueError if
|
|
86
|
+
Raises ValueError if state_name is not declared as part of the backend.
|
|
61
87
|
"""
|
|
62
|
-
if
|
|
88
|
+
if state_name not in self.state_keys:
|
|
63
89
|
raise ValueError(
|
|
64
90
|
"State '{}' is not declared as part of backend. Declared states: {}.".format(
|
|
65
|
-
|
|
91
|
+
state_name,
|
|
66
92
|
", ".join(sorted(self.state_keys)),
|
|
67
93
|
)
|
|
68
94
|
)
|
|
69
95
|
|
|
70
96
|
return BackendConfig(
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
state_name=state_name,
|
|
98
|
+
backend_name=self.backend_name,
|
|
73
99
|
region=self.region,
|
|
74
|
-
key=self.state_keys[
|
|
100
|
+
key=self.state_keys[state_name],
|
|
75
101
|
)
|
|
76
102
|
|
|
77
|
-
def __getitem__(self,
|
|
78
|
-
return self.config(
|
|
103
|
+
def __getitem__(self, state_name: str) -> BackendConfig:
|
|
104
|
+
return self.config(state_name=state_name)
|
|
79
105
|
|
|
80
106
|
|
|
81
107
|
def backend_delete_state_objects(*, output: BackendOutput) -> None:
|
|
@@ -111,15 +137,23 @@ def backend_delete_state_objects(*, output: BackendOutput) -> None:
|
|
|
111
137
|
)
|
|
112
138
|
|
|
113
139
|
|
|
114
|
-
def backend_state_resources(*, config: BackendConfig) -> list[object]:
|
|
140
|
+
def backend_state_resources(*, config: BackendConfig, account_id: str) -> list[object]:
|
|
115
141
|
"""Return config's resources from the backend bucket.
|
|
116
142
|
|
|
117
143
|
Returns an empty list if the state's object is absent (never applied) or
|
|
118
144
|
has no resources.
|
|
119
145
|
"""
|
|
120
|
-
|
|
146
|
+
return _state_resources(
|
|
147
|
+
bucket_name=config.bucket_name(account_id=account_id),
|
|
148
|
+
region=config.region,
|
|
149
|
+
key=config.key,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _state_resources(*, bucket_name: str, region: str, key: str) -> list[object]:
|
|
154
|
+
client = boto_client_s3(region=region)
|
|
121
155
|
try:
|
|
122
|
-
response = client.get_object(Bucket=
|
|
156
|
+
response = client.get_object(Bucket=bucket_name, Key=key)
|
|
123
157
|
except client.exceptions.NoSuchKey:
|
|
124
158
|
return []
|
|
125
159
|
|
|
@@ -134,10 +168,14 @@ def backend_states_with_resources(*, output: BackendOutput) -> dict[str, list[ob
|
|
|
134
168
|
Empty if every declared state is empty.
|
|
135
169
|
"""
|
|
136
170
|
states_with_resources: dict[str, list[object]] = {}
|
|
137
|
-
for
|
|
138
|
-
resources =
|
|
171
|
+
for state_name in output.state_keys:
|
|
172
|
+
resources = _state_resources(
|
|
173
|
+
bucket_name=output.bucket_name,
|
|
174
|
+
region=output.region,
|
|
175
|
+
key=output.state_keys[state_name],
|
|
176
|
+
)
|
|
139
177
|
if resources:
|
|
140
|
-
states_with_resources[
|
|
178
|
+
states_with_resources[state_name] = resources
|
|
141
179
|
return states_with_resources
|
|
142
180
|
|
|
143
181
|
|
|
@@ -167,8 +205,8 @@ class BackendStatus(BaseModel):
|
|
|
167
205
|
|
|
168
206
|
status = BackendStatus.load(path=path)
|
|
169
207
|
status.backend.applied = True
|
|
170
|
-
applied = status.states.get(
|
|
171
|
-
status.states[
|
|
208
|
+
applied = status.states.get(state_name, BackendStatusEntry()).applied
|
|
209
|
+
status.states[state_name] = BackendStatusEntry(applied=True)
|
|
172
210
|
status.save(path=path)
|
|
173
211
|
"""
|
|
174
212
|
|
|
@@ -212,10 +250,10 @@ class BackendStatus(BaseModel):
|
|
|
212
250
|
if "states" not in doc:
|
|
213
251
|
doc["states"] = tomlkit.table(is_super_table=True)
|
|
214
252
|
states_table = cast(Table, doc["states"])
|
|
215
|
-
for
|
|
216
|
-
if
|
|
217
|
-
states_table[
|
|
218
|
-
state_table = cast(Table, states_table[
|
|
253
|
+
for state_name, entry in self.states.items():
|
|
254
|
+
if state_name not in states_table:
|
|
255
|
+
states_table[state_name] = tomlkit.table()
|
|
256
|
+
state_table = cast(Table, states_table[state_name])
|
|
219
257
|
state_table["applied"] = entry.applied
|
|
220
258
|
|
|
221
259
|
with path.open("w", encoding="utf-8", newline="\n") as f:
|
|
@@ -26,6 +26,7 @@ class AwsProfile(BaseModel):
|
|
|
26
26
|
|
|
27
27
|
class AwsEnviron(BaseModel):
|
|
28
28
|
profile: str
|
|
29
|
+
account_id: str
|
|
29
30
|
username: str
|
|
30
31
|
aws_access_key_id: str
|
|
31
32
|
|
|
@@ -127,11 +128,11 @@ def aws_environ_from_profile(
|
|
|
127
128
|
yielding, raising ValueError immediately rather than letting some later,
|
|
128
129
|
unrelated AWS call fail confusingly. The extra round trip is minor next
|
|
129
130
|
to the time lost misdiagnosing an unclear downstream error. That same
|
|
130
|
-
call's identity
|
|
131
|
-
need a second STS call just to learn
|
|
131
|
+
call's identity also supplies the yielded account ID and username, so
|
|
132
|
+
callers never need a second STS call just to learn them.
|
|
132
133
|
|
|
133
|
-
Yields an :class:`AwsEnviron` describing which profile,
|
|
134
|
-
and key ID are active.
|
|
134
|
+
Yields an :class:`AwsEnviron` describing which profile, account ID, IAM
|
|
135
|
+
username, and key ID are active.
|
|
135
136
|
"""
|
|
136
137
|
variables: dict[str, str] = {
|
|
137
138
|
"AWS_ACCESS_KEY_ID": profile.aws_access_key_id,
|
|
@@ -151,6 +152,7 @@ def aws_environ_from_profile(
|
|
|
151
152
|
username = _username_from_iam_arn(identity["Arn"])
|
|
152
153
|
yield AwsEnviron(
|
|
153
154
|
profile=profile.name,
|
|
155
|
+
account_id=identity["Account"],
|
|
154
156
|
username=username,
|
|
155
157
|
aws_access_key_id=profile.aws_access_key_id,
|
|
156
158
|
)
|
|
@@ -172,7 +174,8 @@ def aws_environ_from_config(
|
|
|
172
174
|
config file. See aws_environ_from_profile() for credential validation
|
|
173
175
|
and for raise_if_env_exists/raise_if_env_changed defaults.
|
|
174
176
|
|
|
175
|
-
Yields an :class:`AwsEnviron` describing which profile
|
|
177
|
+
Yields an :class:`AwsEnviron` describing which profile, account ID, IAM
|
|
178
|
+
username, and key ID are active.
|
|
176
179
|
"""
|
|
177
180
|
aws_profile = load_aws_profile_from_config(
|
|
178
181
|
config_path=config_path, profile_name=profile_name
|
|
@@ -18,6 +18,7 @@ from fogies.terraform.backend import (
|
|
|
18
18
|
BackendStatusEntry,
|
|
19
19
|
backend_state_resources,
|
|
20
20
|
)
|
|
21
|
+
from fogies.tools.aws_environ import AwsEnviron
|
|
21
22
|
from fogies.tools.command import CommandParams, command_run
|
|
22
23
|
|
|
23
24
|
|
|
@@ -93,8 +94,9 @@ TfvarsContextManager = AbstractContextManager[TfvarsPath]
|
|
|
93
94
|
# by @contextlib.contextmanager can only be entered once, so anything that
|
|
94
95
|
# might run more than once per process (e.g. an invoke task) must build its
|
|
95
96
|
# own fresh instance each time instead of reusing one captured at definition
|
|
96
|
-
# time.
|
|
97
|
-
|
|
97
|
+
# time. A tfbackend factory is given the AWS environment the task runs in,
|
|
98
|
+
# whose account a backend's bucket name depends on.
|
|
99
|
+
TfbackendFactory = Callable[[AwsEnviron], TfbackendContextManager]
|
|
98
100
|
TfvarsFactory = Callable[[], TfvarsContextManager]
|
|
99
101
|
|
|
100
102
|
|
|
@@ -102,7 +104,8 @@ TfvarsFactory = Callable[[], TfvarsContextManager]
|
|
|
102
104
|
def terraform_tfbackend(
|
|
103
105
|
*,
|
|
104
106
|
path: pathlib.Path,
|
|
105
|
-
|
|
107
|
+
backend_config: BackendConfig,
|
|
108
|
+
aws_environ: AwsEnviron,
|
|
106
109
|
delete_on_exit: bool = True,
|
|
107
110
|
) -> Generator[TfbackendPath]:
|
|
108
111
|
"""Write S3 backend configuration to a file and yield the path.
|
|
@@ -110,7 +113,7 @@ def terraform_tfbackend(
|
|
|
110
113
|
The file is written as flat key/value entries, one per line, e.g.:
|
|
111
114
|
|
|
112
115
|
region = "us-west-2"
|
|
113
|
-
bucket = "pyfogies-test-backend-session-
|
|
116
|
+
bucket = "pyfogies-test-backend-session-111122223333-us-west-2-an"
|
|
114
117
|
key = "test-state-a/terraform.tfstate"
|
|
115
118
|
use_lockfile = true
|
|
116
119
|
"""
|
|
@@ -118,9 +121,13 @@ def terraform_tfbackend(
|
|
|
118
121
|
raise ValueError("Path '{}' must end with '.tfbackend'".format(path))
|
|
119
122
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
120
123
|
with path.open("w", encoding="utf-8", newline="\n") as f:
|
|
121
|
-
_ = f.write('region = "{}"\n'.format(
|
|
122
|
-
_ = f.write(
|
|
123
|
-
|
|
124
|
+
_ = f.write('region = "{}"\n'.format(backend_config.region))
|
|
125
|
+
_ = f.write(
|
|
126
|
+
'bucket = "{}"\n'.format(
|
|
127
|
+
backend_config.bucket_name(account_id=aws_environ.account_id)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
_ = f.write('key = "{}"\n'.format(backend_config.key))
|
|
124
131
|
_ = f.write("use_lockfile = true\n")
|
|
125
132
|
try:
|
|
126
133
|
yield TfbackendPath(path)
|
|
@@ -358,7 +365,8 @@ def terraform(
|
|
|
358
365
|
binary_cache_path: pathlib.Path,
|
|
359
366
|
command_params: CommandParams | None = None,
|
|
360
367
|
module_path: pathlib.Path | None = None,
|
|
361
|
-
|
|
368
|
+
backend_config: BackendConfig | None = None,
|
|
369
|
+
aws_environ: AwsEnviron | None = None,
|
|
362
370
|
backend_status_path: pathlib.Path | None = None,
|
|
363
371
|
tfbackend_path: pathlib.Path | None = None,
|
|
364
372
|
tfvars_path: pathlib.Path | None = None,
|
|
@@ -377,11 +385,12 @@ def terraform(
|
|
|
377
385
|
*tfbackend_path* is optional for init and, when set, is passed as
|
|
378
386
|
-backend-config=<path>.
|
|
379
387
|
|
|
380
|
-
*backend_status_path* and *
|
|
381
|
-
whether
|
|
388
|
+
*backend_status_path* and *backend_config*, when provided together, check
|
|
389
|
+
whether backend_config.state_name has any resources in the backend bucket after a
|
|
382
390
|
successful apply_on_entry, and again after a successful destroy_on_exit,
|
|
383
391
|
recording the result in backend_status_path's backend status file. See
|
|
384
|
-
fogies.terraform.backend.
|
|
392
|
+
fogies.terraform.backend. *aws_environ* is the AWS environment of the
|
|
393
|
+
backend bucket's account, and is required when *backend_config* is set.
|
|
385
394
|
|
|
386
395
|
*tfvars_path* is optional for apply. If *destroy_on_exit* is true, run
|
|
387
396
|
destroy when exiting the context; requires *command_params* and
|
|
@@ -409,8 +418,12 @@ def terraform(
|
|
|
409
418
|
raise ValueError("apply_on_entry requires command_params and module_path")
|
|
410
419
|
if destroy_on_exit and (command_params is None or module_path is None):
|
|
411
420
|
raise ValueError("destroy_on_exit requires command_params and module_path")
|
|
412
|
-
if (backend_status_path is None) != (
|
|
413
|
-
raise ValueError(
|
|
421
|
+
if (backend_status_path is None) != (backend_config is None):
|
|
422
|
+
raise ValueError(
|
|
423
|
+
"backend_status_path and backend_config must be provided together"
|
|
424
|
+
)
|
|
425
|
+
if backend_config is not None and aws_environ is None:
|
|
426
|
+
raise ValueError("aws_environ is required when backend_config is set")
|
|
414
427
|
|
|
415
428
|
exe_name = "terraform_{}.exe".format(version.replace(".", "_"))
|
|
416
429
|
exe_path = binary_cache_path / exe_name
|
|
@@ -458,10 +471,15 @@ def terraform(
|
|
|
458
471
|
apply_params=apply_params,
|
|
459
472
|
)
|
|
460
473
|
if backend_status_path is not None:
|
|
461
|
-
assert
|
|
474
|
+
assert backend_config is not None
|
|
475
|
+
assert aws_environ is not None
|
|
462
476
|
backend_status = BackendStatus.load(path=backend_status_path)
|
|
463
|
-
backend_status.states[
|
|
464
|
-
applied=bool(
|
|
477
|
+
backend_status.states[backend_config.state_name] = BackendStatusEntry(
|
|
478
|
+
applied=bool(
|
|
479
|
+
backend_state_resources(
|
|
480
|
+
config=backend_config, account_id=aws_environ.account_id
|
|
481
|
+
)
|
|
482
|
+
)
|
|
465
483
|
)
|
|
466
484
|
backend_status.save(path=backend_status_path)
|
|
467
485
|
|
|
@@ -477,10 +495,15 @@ def terraform(
|
|
|
477
495
|
destroy_params=destroy_params,
|
|
478
496
|
)
|
|
479
497
|
if backend_status_path is not None:
|
|
480
|
-
assert
|
|
498
|
+
assert backend_config is not None
|
|
499
|
+
assert aws_environ is not None
|
|
481
500
|
backend_status = BackendStatus.load(path=backend_status_path)
|
|
482
|
-
backend_status.states[
|
|
483
|
-
applied=bool(
|
|
501
|
+
backend_status.states[backend_config.state_name] = BackendStatusEntry(
|
|
502
|
+
applied=bool(
|
|
503
|
+
backend_state_resources(
|
|
504
|
+
config=backend_config, account_id=aws_environ.account_id
|
|
505
|
+
)
|
|
506
|
+
)
|
|
484
507
|
)
|
|
485
508
|
backend_status.save(path=backend_status_path)
|
|
486
509
|
|
|
@@ -493,7 +516,8 @@ def terraform_output(
|
|
|
493
516
|
binary_cache_path: pathlib.Path,
|
|
494
517
|
command_params: CommandParams,
|
|
495
518
|
module_path: pathlib.Path,
|
|
496
|
-
|
|
519
|
+
backend_config: BackendConfig | None = None,
|
|
520
|
+
aws_environ: AwsEnviron | None = None,
|
|
497
521
|
backend_status_path: pathlib.Path | None = None,
|
|
498
522
|
tfbackend_path: pathlib.Path | None = None,
|
|
499
523
|
tfvars_path: pathlib.Path | None = None,
|
|
@@ -515,7 +539,8 @@ def terraform_output(
|
|
|
515
539
|
binary_cache_path: pathlib.Path,
|
|
516
540
|
command_params: CommandParams,
|
|
517
541
|
module_path: pathlib.Path,
|
|
518
|
-
|
|
542
|
+
backend_config: BackendConfig | None = None,
|
|
543
|
+
aws_environ: AwsEnviron | None = None,
|
|
519
544
|
backend_status_path: pathlib.Path | None = None,
|
|
520
545
|
tfbackend_path: pathlib.Path | None = None,
|
|
521
546
|
tfvars_path: pathlib.Path | None = None,
|
|
@@ -536,7 +561,8 @@ def terraform_output(
|
|
|
536
561
|
binary_cache_path: pathlib.Path,
|
|
537
562
|
command_params: CommandParams,
|
|
538
563
|
module_path: pathlib.Path,
|
|
539
|
-
|
|
564
|
+
backend_config: BackendConfig | None = None,
|
|
565
|
+
aws_environ: AwsEnviron | None = None,
|
|
540
566
|
backend_status_path: pathlib.Path | None = None,
|
|
541
567
|
tfbackend_path: pathlib.Path | None = None,
|
|
542
568
|
tfvars_path: pathlib.Path | None = None,
|
|
@@ -560,7 +586,7 @@ def terraform_output(
|
|
|
560
586
|
None then, as the Literal[True] overload above tells callers; the assert
|
|
561
587
|
below is what actually enforces that assumption at runtime.
|
|
562
588
|
|
|
563
|
-
*backend_status_path* and *
|
|
589
|
+
*backend_status_path*, *backend_config* and *aws_environ* are passed through to terraform();
|
|
564
590
|
see its docstring.
|
|
565
591
|
"""
|
|
566
592
|
with terraform(
|
|
@@ -568,7 +594,8 @@ def terraform_output(
|
|
|
568
594
|
binary_cache_path=binary_cache_path,
|
|
569
595
|
command_params=command_params,
|
|
570
596
|
module_path=module_path,
|
|
571
|
-
|
|
597
|
+
backend_config=backend_config,
|
|
598
|
+
aws_environ=aws_environ,
|
|
572
599
|
backend_status_path=backend_status_path,
|
|
573
600
|
tfbackend_path=tfbackend_path,
|
|
574
601
|
tfvars_path=tfvars_path,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/includes/templates/backend_status.toml.template
RENAMED
|
File without changes
|
{fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/includes/templates/poetry.toml.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|