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.
Files changed (41) hide show
  1. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/PKG-INFO +1 -1
  2. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/pyproject.toml +1 -1
  3. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/terraform.py +49 -21
  4. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/backend.py +73 -35
  5. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/aws_environ.py +8 -5
  6. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/terraform.py +51 -24
  7. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/README.md +0 -0
  8. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/__init__.py +0 -0
  9. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/aws_access_key.py +0 -0
  10. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/boto_clients.py +0 -0
  11. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/download_verified.py +0 -0
  12. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/includes/templates/aws.toml.template +0 -0
  13. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/includes/templates/backend_status.toml.template +0 -0
  14. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/includes/templates/poetry.toml.template +0 -0
  15. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/py.typed +0 -0
  16. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/pyfogies.py +0 -0
  17. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/ready_poll.py +0 -0
  18. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/retry.py +0 -0
  19. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/__init__.py +0 -0
  20. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/access_key.py +0 -0
  21. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/format.py +0 -0
  22. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/lint.py +0 -0
  23. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/poetry.py +0 -0
  24. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/terraform_backend.py +0 -0
  25. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tasks/test.py +0 -0
  26. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/templates.py +0 -0
  27. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/__init__.py +0 -0
  28. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/alb.py +0 -0
  29. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/cloudwatch.py +0 -0
  30. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/ecr.py +0 -0
  31. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/ecs.py +0 -0
  32. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/hosted_zone.py +0 -0
  33. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/terraform/network.py +0 -0
  34. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/__init__.py +0 -0
  35. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/boto.py +0 -0
  36. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/claude_sync.py +0 -0
  37. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/command.py +0 -0
  38. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/environ.py +0 -0
  39. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/go.py +0 -0
  40. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/ollama.py +0 -0
  41. {fogies-0.0.0.dev10 → fogies-0.0.0.dev11}/src/fogies/tools/terraform_backend.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fogies
3
- Version: 0.0.0.dev10
3
+ Version: 0.0.0.dev11
4
4
  Summary:
5
5
  Author: James Fogarty
6
6
  Author-email: jayfo@users.noreply.github.com
@@ -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.10"
4
+ version = "0.0.0-dev.11"
5
5
  description = ""
6
6
  authors = [
7
7
  {name = "James Fogarty",email = "jayfo@users.noreply.github.com"}
@@ -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
- backend: BackendConfig | None = None,
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 backend is not None and tfbackend_factory is None:
39
- raise ValueError("tfbackend_factory is required when backend is set")
40
- if (backend_status_path is None) != (backend is None):
41
- raise ValueError("backend_status_path and backend must be provided together")
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
- if aws_environ_factory is not None:
76
- _ = stack.enter_context(aws_environ_factory())
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()) if tfbackend_factory else None
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
- backend=backend,
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
- backend: BackendConfig | None = None,
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 backend is not None and tfbackend_factory is None:
120
- raise ValueError("tfbackend_factory is required when backend is set")
121
- if (backend_status_path is None) != (backend is None):
122
- raise ValueError("backend_status_path and backend must be provided together")
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
- if aws_environ_factory is not None:
157
- _ = stack.enter_context(aws_environ_factory())
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()) if tfbackend_factory else None
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
- backend=backend,
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
- backend: BackendConfig | None = None,
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
- backend=backend,
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
- backend=backend,
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
- name: str
17
+ backend_name: str
18
18
  region: str
19
- states: list[str]
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 info for a single state within an S3 Terraform backend.
28
+ """Connection config for a single state within an S3 Terraform backend.
25
29
 
26
- Unlike a Terraform backend module's output, this does not describe every
27
- state sharing a backend bucket. It describes only the one state a
28
- consumer wants to connect to.
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
- state: str
32
- bucket_name: str
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(*, name: str, region: str, state: str) -> "BackendConfig":
38
- """Return connection config for a state, without applying the backend module.
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 bucket and key naming the backend Terraform module derives
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
- state=state,
46
- bucket_name="{}-bucket-{}".format(name, region),
53
+ state_name=state_name,
54
+ backend_name=backend_name,
47
55
  region=region,
48
- key="{}/terraform.tfstate".format(state),
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, *, state: str) -> BackendConfig:
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 state is not declared as part of the backend.
86
+ Raises ValueError if state_name is not declared as part of the backend.
61
87
  """
62
- if state not in self.state_keys:
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
- state,
91
+ state_name,
66
92
  ", ".join(sorted(self.state_keys)),
67
93
  )
68
94
  )
69
95
 
70
96
  return BackendConfig(
71
- state=state,
72
- bucket_name=self.bucket_name,
97
+ state_name=state_name,
98
+ backend_name=self.backend_name,
73
99
  region=self.region,
74
- key=self.state_keys[state],
100
+ key=self.state_keys[state_name],
75
101
  )
76
102
 
77
- def __getitem__(self, state: str) -> BackendConfig:
78
- return self.config(state=state)
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
- client = boto_client_s3(region=config.region)
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=config.bucket_name, Key=config.key)
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 state in output.state_keys:
138
- resources = backend_state_resources(config=output.config(state=state))
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[state] = 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(name, BackendStatusEntry()).applied
171
- status.states[name] = BackendStatusEntry(applied=True)
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 name, entry in self.states.items():
216
- if name not in states_table:
217
- states_table[name] = tomlkit.table()
218
- state_table = cast(Table, states_table[name])
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 ARN also supplies the yielded username, so callers never
131
- need a second STS call just to learn it.
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, IAM username,
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 and key ID are active.
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
- TfbackendFactory = Callable[[], TfbackendContextManager]
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
- backend: BackendConfig,
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-bucket-us-west-2"
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(backend.region))
122
- _ = f.write('bucket = "{}"\n'.format(backend.bucket_name))
123
- _ = f.write('key = "{}"\n'.format(backend.key))
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
- backend: BackendConfig | None = None,
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 *backend*, when provided together, check
381
- whether backend.state has any resources in the backend bucket after a
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) != (backend is None):
413
- raise ValueError("backend_status_path and backend must be provided together")
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 backend is not None
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[backend.state] = BackendStatusEntry(
464
- applied=bool(backend_state_resources(config=backend))
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 backend is not None
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[backend.state] = BackendStatusEntry(
483
- applied=bool(backend_state_resources(config=backend))
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
- backend: BackendConfig | None = None,
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
- backend: BackendConfig | None = None,
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
- backend: BackendConfig | None = None,
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 *backend* are passed through to terraform();
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
- backend=backend,
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