Skip to content

Commit f1e6ee3

Browse files
authored
Fix: attach lockfiles to terraform modules (#21220)
#21099 made synthetic targets for lockfiles. `terraform_deployments` would then pull these in as source files. But `terraform validate` can be run against modules, and these were missing their lockfiles. This MR attaches the lockfiles to the module. `terraform_deployment` targets still pull in the lockfile, but now transitively through the module.
1 parent 5f16090 commit f1e6ee3

File tree

4 files changed

+126
-63
lines changed

4 files changed

+126
-63
lines changed

src/python/pants/backend/terraform/dependencies.py

+51-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33
from __future__ import annotations
44

5+
import os.path
56
from dataclasses import dataclass
67
from typing import Optional
78

@@ -15,7 +16,9 @@
1516
)
1617
from pants.backend.terraform.tool import TerraformProcess
1718
from pants.backend.terraform.utils import terraform_arg, terraform_relpath
19+
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
1820
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
21+
from pants.engine.fs import DigestSubset, PathGlobs
1922
from pants.engine.internals.native_engine import Address, AddressInput, Digest, MergeDigests
2023
from pants.engine.internals.selectors import Get, MultiGet
2124
from pants.engine.process import FallibleProcessResult, ProcessExecutionFailure
@@ -103,7 +106,6 @@ class TerraformInitRequest:
103106

104107
# Not initialising the backend means we won't access remote state. Useful for `validate`
105108
initialise_backend: bool = False
106-
upgrade: bool = False
107109

108110

109111
@dataclass(frozen=True)
@@ -113,8 +115,14 @@ class TerraformInitResponse:
113115
chdir: str
114116

115117

116-
@rule
117-
async def init_terraform(request: TerraformInitRequest) -> TerraformInitResponse:
118+
@dataclass(frozen=True)
119+
class TerraformUpgradeResponse:
120+
lockfile: Digest
121+
chdir: str
122+
123+
124+
async def run_terraform_init(request: TerraformInitRequest, upgrade: bool):
125+
"""Just run `terraform init`"""
118126
this_targets_dependencies = await Get(
119127
TransitiveTargets, TransitiveTargetsRequest((request.dependencies.address,))
120128
)
@@ -180,25 +188,38 @@ async def init_terraform(request: TerraformInitRequest) -> TerraformInitResponse
180188
)
181189

182190
has_lockfile = invocation_files.lockfile is not None
183-
third_party_deps = await Get(
191+
init_response = await Get(
184192
TerraformDependenciesResponse,
185193
TerraformDependenciesRequest(
186194
chdir,
187195
backend_config,
188196
has_lockfile,
189197
source_for_validate,
190198
initialise_backend=request.initialise_backend,
191-
upgrade=request.upgrade,
199+
upgrade=upgrade,
192200
),
193201
)
194202

203+
return source_files, dependencies_files, init_response, chdir
204+
205+
206+
@rule
207+
async def terraform_init(request: TerraformInitRequest) -> TerraformInitResponse:
208+
"""Run `terraform init`.
209+
210+
Returns all the initialised files, ready for execution of subsequent tasks
211+
"""
212+
source_files, dependencies_files, init_response, chdir = await run_terraform_init(
213+
request, upgrade=False
214+
)
215+
195216
all_terraform_files = await Get(
196217
Digest,
197218
MergeDigests(
198219
[
199220
source_files.snapshot.digest,
200221
dependencies_files.snapshot.digest,
201-
third_party_deps.digest,
222+
init_response.digest,
202223
]
203224
),
204225
)
@@ -208,5 +229,29 @@ async def init_terraform(request: TerraformInitRequest) -> TerraformInitResponse
208229
)
209230

210231

232+
@rule
233+
async def terraform_upgrade_lockfile(request: TerraformInitRequest) -> TerraformUpgradeResponse:
234+
"""Run `terraform init -upgrade`. Returns just the lockfile.
235+
236+
This split exists because the new and old lockfile will conflict if merging digests
237+
"""
238+
239+
_, _, init_response, chdir = await run_terraform_init(request, upgrade=True)
240+
241+
lockfile = await Get(
242+
Digest,
243+
DigestSubset(
244+
init_response.digest,
245+
PathGlobs(
246+
(os.path.join(chdir, ".terraform.lock.hcl"),),
247+
glob_match_error_behavior=GlobMatchErrorBehavior.error,
248+
description_of_origin="upgrade terraform lockfile with `terraform init`",
249+
),
250+
),
251+
)
252+
253+
return TerraformUpgradeResponse(lockfile, chdir)
254+
255+
211256
def rules():
212257
return collect_rules()

src/python/pants/backend/terraform/dependency_inference.py

+63-38
Original file line numberDiff line numberDiff line change
@@ -110,50 +110,16 @@ async def setup_process_for_parse_terraform_module_sources(
110110

111111
@dataclass(frozen=True)
112112
class TerraformModuleDependenciesInferenceFieldSet(FieldSet):
113-
required_fields = (TerraformModuleSourcesField,)
113+
required_fields = (TerraformModuleSourcesField, TerraformDependenciesField)
114114

115115
sources: TerraformModuleSourcesField
116+
dependencies: TerraformDependenciesField
116117

117118

118119
class InferTerraformModuleDependenciesRequest(InferDependenciesRequest):
119120
infer_from = TerraformModuleDependenciesInferenceFieldSet
120121

121122

122-
@rule
123-
async def infer_terraform_module_dependencies(
124-
request: InferTerraformModuleDependenciesRequest,
125-
) -> InferredDependencies:
126-
hydrated_sources = await Get(HydratedSources, HydrateSourcesRequest(request.field_set.sources))
127-
128-
paths = OrderedSet(
129-
filename for filename in hydrated_sources.snapshot.files if filename.endswith(".tf")
130-
)
131-
result = await Get(
132-
ProcessResult,
133-
ParseTerraformModuleSources(
134-
sources_digest=hydrated_sources.snapshot.digest,
135-
paths=tuple(paths),
136-
),
137-
)
138-
candidate_spec_paths = [line for line in result.stdout.decode("utf-8").split("\n") if line]
139-
140-
# For each path, see if there is a `terraform_module` target at the specified spec_path.
141-
candidate_targets = await Get(
142-
Targets,
143-
RawSpecs(
144-
dir_globs=tuple(DirGlobSpec(path) for path in candidate_spec_paths),
145-
unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,
146-
description_of_origin="the `terraform_module` dependency inference rule",
147-
),
148-
)
149-
# TODO: Need to either implement the standard ambiguous dependency logic or ban >1 terraform_module
150-
# per directory.
151-
terraform_module_addresses = [
152-
tgt.address for tgt in candidate_targets if tgt.has_field(TerraformModuleSourcesField)
153-
]
154-
return InferredDependencies(terraform_module_addresses)
155-
156-
157123
@dataclass(frozen=True)
158124
class TerraformDeploymentDependenciesInferenceFieldSet(TerraformDeploymentFieldSet):
159125
pass
@@ -260,6 +226,66 @@ def identify_terraform_backend_and_vars(
260226
return TerraformDeploymentInvocationFiles(backend_targets, vars_targets, lockfile)
261227

262228

229+
async def _infer_dependencies_from_sources(
230+
request: InferTerraformModuleDependenciesRequest,
231+
) -> list[Address]:
232+
"""Parse the source code for references to other modules."""
233+
hydrated_sources = await Get(HydratedSources, HydrateSourcesRequest(request.field_set.sources))
234+
paths = OrderedSet(
235+
filename for filename in hydrated_sources.snapshot.files if filename.endswith(".tf")
236+
)
237+
result = await Get(
238+
ProcessResult,
239+
ParseTerraformModuleSources(
240+
sources_digest=hydrated_sources.snapshot.digest,
241+
paths=tuple(paths),
242+
),
243+
)
244+
candidate_spec_paths = [line for line in result.stdout.decode("utf-8").split("\n") if line]
245+
# For each path, see if there is a `terraform_module` target at the specified spec_path.
246+
candidate_targets = await Get(
247+
Targets,
248+
RawSpecs(
249+
dir_globs=tuple(DirGlobSpec(path) for path in candidate_spec_paths),
250+
unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,
251+
description_of_origin="the `terraform_module` dependency inference rule",
252+
),
253+
)
254+
# TODO: Need to either implement the standard ambiguous dependency logic or ban >1 terraform_module
255+
# per directory.
256+
terraform_module_addresses = [
257+
tgt.address for tgt in candidate_targets if tgt.has_field(TerraformModuleSourcesField)
258+
]
259+
return terraform_module_addresses
260+
261+
262+
async def _infer_lockfile(request: InferTerraformModuleDependenciesRequest) -> list[Address]:
263+
"""Pull in the lockfile for a Terraform module.
264+
265+
This is necessary for `terraform validate`.
266+
"""
267+
invocation_files = await Get(
268+
TerraformDeploymentInvocationFiles,
269+
TerraformDeploymentInvocationFilesRequest(
270+
request.field_set.address, request.field_set.dependencies
271+
),
272+
)
273+
if invocation_files.lockfile:
274+
return [invocation_files.lockfile.address]
275+
else:
276+
return []
277+
278+
279+
@rule
280+
async def infer_terraform_module_dependencies(
281+
request: InferTerraformModuleDependenciesRequest,
282+
) -> InferredDependencies:
283+
terraform_module_addresses = await _infer_dependencies_from_sources(request)
284+
lockfile_address = await _infer_lockfile(request)
285+
286+
return InferredDependencies([*terraform_module_addresses, *lockfile_address])
287+
288+
263289
@rule
264290
async def infer_terraform_deployment_dependencies(
265291
request: InferTerraformDeploymentDependenciesRequest,
@@ -276,8 +302,7 @@ async def infer_terraform_deployment_dependencies(
276302
)
277303
deps.extend(e.address for e in invocation_files.backend_configs)
278304
deps.extend(e.address for e in invocation_files.vars_files)
279-
if invocation_files.lockfile:
280-
deps.append(invocation_files.lockfile.address)
305+
# lockfile is attached to the module itself
281306

282307
return InferredDependencies(deps)
283308

src/python/pants/backend/terraform/dependency_inference_test.py

+7
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
TerraformHcl2Parser,
1414
TerraformModuleDependenciesInferenceFieldSet,
1515
)
16+
from pants.backend.terraform.goals.lockfiles import rules as terraform_lockfile_rules
1617
from pants.backend.terraform.target_types import (
1718
TerraformBackendTarget,
1819
TerraformDeploymentTarget,
20+
TerraformLockfileTarget,
1921
TerraformModuleTarget,
2022
TerraformVarFileTarget,
2123
)
@@ -43,10 +45,12 @@ def rule_runner() -> RuleRunner:
4345
TerraformDeploymentTarget,
4446
TerraformBackendTarget,
4547
TerraformVarFileTarget,
48+
TerraformLockfileTarget,
4649
],
4750
rules=[
4851
*external_tool.rules(),
4952
*source_files.rules(),
53+
*terraform_lockfile_rules(),
5054
*dependency_inference.rules(),
5155
QueryRule(InferredDependencies, [InferTerraformModuleDependenciesRequest]),
5256
QueryRule(InferredDependencies, [InferTerraformDeploymentDependenciesRequest]),
@@ -68,6 +72,7 @@ def test_dependency_inference_module(rule_runner: RuleRunner) -> None:
6872
"src/tf/modules/foo/versions.tf": "",
6973
"src/tf/modules/foo/bar/BUILD": "terraform_module()\n",
7074
"src/tf/modules/foo/bar/versions.tf": "",
75+
"src/tf/resources/grok/.terraform.lock.hcl": "",
7176
"src/tf/resources/grok/subdir/BUILD": "terraform_module()\n",
7277
"src/tf/resources/grok/subdir/versions.tf": "",
7378
"src/tf/resources/grok/BUILD": "terraform_module()\n",
@@ -107,6 +112,7 @@ def test_dependency_inference_module(rule_runner: RuleRunner) -> None:
107112
Address("src/tf/modules/foo"),
108113
Address("src/tf/modules/foo/bar"),
109114
Address("src/tf/resources/grok/subdir"),
115+
Address("src/tf/resources/grok", target_name=".terraform.lock.hcl"),
110116
]
111117
),
112118
)
@@ -142,6 +148,7 @@ def test_dependency_inference_autoinfered_files(rule_runner: RuleRunner) -> None
142148
"src/tf/main.tf": "",
143149
"src/tf/main.tfvars": "",
144150
"src/tf/main.tfbackend": "",
151+
"src/tf/.terraform.lock.hcl": "",
145152
}
146153
)
147154
target = rule_runner.get_target(Address("src/tf", target_name="deployment"))

src/python/pants/backend/terraform/goals/lockfiles.py

+5-19
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
from dataclasses import dataclass
55
from pathlib import Path
66

7-
from pants.backend.terraform.dependencies import TerraformInitRequest, TerraformInitResponse
7+
from pants.backend.terraform.dependencies import TerraformInitRequest, TerraformUpgradeResponse
88
from pants.backend.terraform.target_types import (
99
TerraformDependenciesField,
1010
TerraformLockfileTarget,
1111
TerraformModuleSourcesField,
1212
TerraformModuleTarget,
1313
TerraformRootModuleField,
1414
)
15-
from pants.backend.terraform.tool import TerraformProcess
1615
from pants.core.goals.generate_lockfiles import (
1716
GenerateLockfile,
1817
GenerateLockfileResult,
@@ -27,7 +26,6 @@
2726
from pants.engine.internals.selectors import Get, MultiGet
2827
from pants.engine.internals.synthetic_targets import SyntheticAddressMaps, SyntheticTargetsRequest
2928
from pants.engine.internals.target_adaptor import TargetAdaptor
30-
from pants.engine.process import ProcessResult
3129
from pants.engine.rules import collect_rules, rule
3230
from pants.engine.target import AllTargets, Targets
3331
from pants.engine.unions import UnionRule
@@ -101,32 +99,20 @@ async def generate_lockfile_from_sources(
10199
) -> GenerateLockfileResult:
102100
"""Generate a Terraform lockfile by running `terraform providers lock` on the sources."""
103101
initialised_terraform = await Get(
104-
TerraformInitResponse,
102+
TerraformUpgradeResponse,
105103
TerraformInitRequest(
106104
TerraformRootModuleField(
107105
lockfile_request.target.address.spec, lockfile_request.target.address
108106
),
109107
lockfile_request.target[TerraformDependenciesField],
110108
initialise_backend=False,
111-
upgrade=True,
112-
),
113-
)
114-
result = await Get(
115-
ProcessResult,
116-
TerraformProcess(
117-
args=(
118-
"providers",
119-
"lock",
120-
),
121-
input_digest=initialised_terraform.sources_and_deps,
122-
output_files=(".terraform.lock.hcl",),
123-
description=f"Update terraform lockfile for {lockfile_request.resolve_name}",
124-
chdir=initialised_terraform.chdir,
125109
),
126110
)
127111

128112
return GenerateLockfileResult(
129-
result.output_digest, lockfile_request.resolve_name, lockfile_request.lockfile_dest
113+
initialised_terraform.lockfile,
114+
lockfile_request.resolve_name,
115+
lockfile_request.lockfile_dest,
130116
)
131117

132118

0 commit comments

Comments
 (0)