Skip to content

Commit 8d33e73

Browse files
jpogranansgarm
andauthored
(TF-18673) Validate Stack and Deployment files for unreferenced origins (#1797)
* Validate Stack and Deployment files for unreferenced origins This commit adds a new validation to check for unreferenced origins in stack and deployment files. This validation is performed after the reference targets and origins are decoded. The validation checks for references to variables and local values that do not have a corresponding target. The validation is performed for variables, local values, providers and identity_tokens only, as components can have unknown schema. * refactor: stop validating components as we see false positives for referenced outputs that don't match the type constraint for where they're used --------- Co-authored-by: Ansgar Mertens <[email protected]>
1 parent 1d4fe76 commit 8d33e73

File tree

6 files changed

+160
-6
lines changed

6 files changed

+160
-6
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ENHANCEMENTS
2+
body: Validate Stack and Deployment files for unreferenced origins
3+
time: 2024-08-15T13:51:08.906805-04:00
4+
custom:
5+
Issue: "1797"
6+
Repository: terraform-ls
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package validations
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"slices"
10+
11+
"github.com/hashicorp/hcl-lang/decoder"
12+
"github.com/hashicorp/hcl-lang/lang"
13+
"github.com/hashicorp/hcl-lang/reference"
14+
"github.com/hashicorp/hcl/v2"
15+
)
16+
17+
func UnreferencedOrigins(ctx context.Context, pathCtx *decoder.PathContext) lang.DiagnosticsMap {
18+
diagsMap := make(lang.DiagnosticsMap)
19+
20+
for _, origin := range pathCtx.ReferenceOrigins {
21+
localOrigin, ok := origin.(reference.LocalOrigin)
22+
if !ok {
23+
// We avoid reporting on other origin types.
24+
//
25+
// DirectOrigin is represented as module's source
26+
// and we already validate existence of the local module
27+
// and avoiding linking to a non-existent module in terraform-schema
28+
// https://github.com/hashicorp/terraform-schema/blob/b39f3de0/schema/module_schema.go#L212-L232
29+
//
30+
// PathOrigin is represented as module inputs
31+
// and we can validate module inputs more meaningfully
32+
// as attributes in body (module block), e.g. raise that
33+
// an input is required or unknown, rather than "reference"
34+
// lacking a corresponding target.
35+
continue
36+
}
37+
38+
address := localOrigin.Address()
39+
40+
if len(address) > 2 {
41+
// We temporarily ignore references with more than 2 segments
42+
// as these indicate references to complex types
43+
// which we do not fully support yet.
44+
// TODO: revisit as part of https://github.com/hashicorp/terraform-ls/issues/653
45+
46+
// However, we still want to validate references to component provider and identity_token
47+
// for Stacks. This is relatively safe as we know the structure of the references
48+
// and can validate them without needing to know the schema of the referenced object.
49+
// TODO: revisit after user feedback
50+
supported := []string{"provider", "identity_token"}
51+
if !slices.Contains(supported, address[0].String()) {
52+
continue
53+
}
54+
}
55+
56+
// we only initially validate variables, providers, and identity_tokens
57+
// resources can have unknown schema and will be researched at a later point
58+
// TODO: revisit as part of https://github.com/hashicorp/terraform-ls/issues/1364
59+
supported := []string{"var", "provider", "identity_token"}
60+
firstStep := address[0].String()
61+
if !slices.Contains(supported, firstStep) {
62+
continue
63+
}
64+
65+
_, ok = pathCtx.ReferenceTargets.Match(localOrigin)
66+
if !ok {
67+
// target not found
68+
fileName := origin.OriginRange().Filename
69+
d := &hcl.Diagnostic{
70+
Severity: hcl.DiagError,
71+
Summary: fmt.Sprintf("No declaration found for %q", address),
72+
Subject: origin.OriginRange().Ptr(),
73+
}
74+
diagsMap[fileName] = diagsMap[fileName].Append(d)
75+
76+
continue
77+
}
78+
}
79+
80+
return diagsMap
81+
}

internal/features/stacks/events.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,20 @@ func (f *StacksFeature) decodeStack(ctx context.Context, dir document.DirHandle,
289289
}
290290
}
291291

292+
_, err = f.stateStore.JobStore.EnqueueJob(ctx, job.Job{
293+
Dir: dir,
294+
Func: func(ctx context.Context) error {
295+
296+
return jobs.ReferenceValidation(ctx, f.store, f.moduleFeature, dir.Path())
297+
},
298+
Type: operation.OpTypeReferenceStackValidation.String(),
299+
DependsOn: job.IDs{refOriginsId, refTargetsId},
300+
IgnoreState: ignoreState,
301+
})
302+
if err != nil {
303+
return deferIds, err
304+
}
305+
292306
return deferIds, nil
293307
},
294308
})

internal/features/stacks/jobs/validation.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/hashicorp/terraform-ls/internal/document"
1616
"github.com/hashicorp/terraform-ls/internal/features/stacks/ast"
1717
stackDecoder "github.com/hashicorp/terraform-ls/internal/features/stacks/decoder"
18+
"github.com/hashicorp/terraform-ls/internal/features/stacks/decoder/validations"
1819
"github.com/hashicorp/terraform-ls/internal/features/stacks/state"
1920
"github.com/hashicorp/terraform-ls/internal/job"
2021
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
@@ -116,3 +117,53 @@ func SchemaStackValidation(ctx context.Context, stackStore *state.StackStore, mo
116117

117118
return rErr
118119
}
120+
121+
// ReferenceValidation does validation based on (mis)matched
122+
// reference origins and targets, to flag up "orphaned" references.
123+
//
124+
// It relies on [DecodeReferenceTargets] and [DecodeReferenceOrigins]
125+
// to supply both origins and targets to compare.
126+
func ReferenceValidation(ctx context.Context, stackStore *state.StackStore, moduleFeature stackDecoder.ModuleReader, stackPath string) error {
127+
record, err := stackStore.StackRecordByPath(stackPath)
128+
if err != nil {
129+
return err
130+
}
131+
132+
// Avoid validation if it is already in progress or already finished
133+
if record.DiagnosticsState[globalAst.ReferenceValidationSource] != operation.OpStateUnknown && !job.IgnoreState(ctx) {
134+
return job.StateNotChangedErr{Dir: document.DirHandleFromPath(stackPath)}
135+
}
136+
137+
err = stackStore.SetDiagnosticsState(stackPath, globalAst.ReferenceValidationSource, operation.OpStateLoading)
138+
if err != nil {
139+
return err
140+
}
141+
142+
pathReader := &stackDecoder.PathReader{
143+
StateReader: stackStore,
144+
ModuleReader: moduleFeature,
145+
}
146+
147+
stackDecoder, err := pathReader.PathContext(lang.Path{
148+
Path: stackPath,
149+
LanguageID: ilsp.Stacks.String(),
150+
})
151+
if err != nil {
152+
return err
153+
}
154+
155+
deployDecoder, err := pathReader.PathContext(lang.Path{
156+
Path: stackPath,
157+
LanguageID: ilsp.Deploy.String(),
158+
})
159+
if err != nil {
160+
return err
161+
}
162+
163+
diags := validations.UnreferencedOrigins(ctx, stackDecoder)
164+
165+
deployDiags := validations.UnreferencedOrigins(ctx, deployDecoder)
166+
diags = diags.Extend(deployDiags)
167+
168+
return stackStore.UpdateDiagnostics(stackPath, globalAst.ReferenceValidationSource, ast.DiagnosticsFromMap(diags))
169+
}

internal/terraform/module/operation/op_type_string.go

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/terraform/module/operation/operation.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
OpTypeSchemaStackValidation
3737
OpTypeSchemaVarsValidation
3838
OpTypeReferenceValidation
39+
OpTypeReferenceStackValidation
3940
OpTypeTerraformValidate
4041
OpTypeParseStackConfiguration
4142
OpTypeLoadStackMetadata

0 commit comments

Comments
 (0)