Skip to content

Commit

Permalink
Introduce validation of *.tfvars files (#1413)
Browse files Browse the repository at this point in the history
* Introduce validation of tfvars files

* clarify existing job name/type

* add tests
  • Loading branch information
radeksimko authored Sep 25, 2023
1 parent 8b0c012 commit daaa726
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 16 deletions.
1 change: 1 addition & 0 deletions internal/decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func varsPathContext(mod *state.Module) (*decoder.PathContext, error) {
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File),
Validators: varsValidators,
}

for _, origin := range mod.VarsRefOrigins {
Expand Down
5 changes: 5 additions & 0 deletions internal/decoder/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ var moduleValidators = []validator.Validator{
validator.UnexpectedAttribute{},
validator.UnexpectedBlock{},
}

var varsValidators = []validator.Validator{
validator.UnexpectedAttribute{},
validator.UnexpectedBlock{},
}
24 changes: 22 additions & 2 deletions internal/indexer/document_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ func (idx *Indexer) DocumentChanged(ctx context.Context, modHandle document.DirH
}
ids = append(ids, parseVarsId)

validationOptions, err := lsctx.ValidationOptions(ctx)
if err != nil {
return ids, err
}

if validationOptions.EarlyValidation {
_, err = idx.jobStore.EnqueueJob(ctx, job.Job{
Dir: modHandle,
Func: func(ctx context.Context) error {
return module.SchemaVariablesValidation(ctx, idx.modStore, idx.schemaStore, modHandle.Path())
},
Type: op.OpTypeSchemaVarsValidation.String(),
DependsOn: append(modIds, parseVarsId),
IgnoreState: true,
})
if err != nil {
return ids, err
}
}

varsRefsId, err := idx.jobStore.EnqueueJob(ctx, job.Job{
Dir: modHandle,
Func: func(ctx context.Context) error {
Expand Down Expand Up @@ -131,9 +151,9 @@ func (idx *Indexer) decodeModule(ctx context.Context, modHandle document.DirHand
_, err = idx.jobStore.EnqueueJob(ctx, job.Job{
Dir: modHandle,
Func: func(ctx context.Context) error {
return module.SchemaValidation(ctx, idx.modStore, idx.schemaStore, modHandle.Path())
return module.SchemaModuleValidation(ctx, idx.modStore, idx.schemaStore, modHandle.Path())
},
Type: op.OpTypeSchemaValidation.String(),
Type: op.OpTypeSchemaModuleValidation.String(),
DependsOn: job.IDs{metaId},
IgnoreState: ignoreState,
})
Expand Down
21 changes: 21 additions & 0 deletions internal/indexer/document_open.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"

"github.com/hashicorp/go-multierror"
lsctx "github.com/hashicorp/terraform-ls/internal/context"
"github.com/hashicorp/terraform-ls/internal/document"
"github.com/hashicorp/terraform-ls/internal/job"
"github.com/hashicorp/terraform-ls/internal/terraform/exec"
Expand Down Expand Up @@ -72,6 +73,26 @@ func (idx *Indexer) DocumentOpened(ctx context.Context, modHandle document.DirHa
}
ids = append(ids, parseVarsId)

validationOptions, err := lsctx.ValidationOptions(ctx)
if err != nil {
return ids, err
}

if validationOptions.EarlyValidation {
_, err = idx.jobStore.EnqueueJob(ctx, job.Job{
Dir: modHandle,
Func: func(ctx context.Context) error {
return module.SchemaVariablesValidation(ctx, idx.modStore, idx.schemaStore, modHandle.Path())
},
Type: op.OpTypeSchemaVarsValidation.String(),
DependsOn: append(modIds, parseVarsId),
IgnoreState: true,
})
if err != nil {
return ids, err
}
}

varsRefsId, err := idx.jobStore.EnqueueJob(ctx, job.Job{
Dir: modHandle,
Func: func(ctx context.Context) error {
Expand Down
2 changes: 1 addition & 1 deletion internal/state/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ func (s *ModuleStore) SetModuleDiagnosticsState(path string, source ast.Diagnost
func (s *ModuleStore) UpdateVarsDiagnostics(path string, source ast.DiagnosticSource, diags ast.VarsDiags) error {
txn := s.db.Txn(true)
txn.Defer(func() {
s.SetVarsDiagnosticsState(path, ast.HCLParsingSource, op.OpStateLoaded)
s.SetVarsDiagnosticsState(path, source, op.OpStateLoaded)
})
defer txn.Abort()

Expand Down
69 changes: 66 additions & 3 deletions internal/terraform/module/module_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ func DecodeVarsReferences(ctx context.Context, modStore *state.ModuleStore, sche
return rErr
}

func SchemaValidation(ctx context.Context, modStore *state.ModuleStore, schemaReader state.SchemaReader, modPath string) error {
func SchemaModuleValidation(ctx context.Context, modStore *state.ModuleStore, schemaReader state.SchemaReader, modPath string) error {
mod, err := modStore.ModuleByPath(modPath)
if err != nil {
return err
Expand All @@ -688,6 +688,7 @@ func SchemaValidation(ctx context.Context, modStore *state.ModuleStore, schemaRe
ModuleReader: modStore,
SchemaReader: schemaReader,
})

d.SetContext(idecoder.DecoderContext(ctx))

moduleDecoder, err := d.Path(lang.Path{
Expand All @@ -700,8 +701,7 @@ func SchemaValidation(ctx context.Context, modStore *state.ModuleStore, schemaRe

var rErr error
rpcContext := lsctx.RPCContext(ctx)
isSingleFileChange := rpcContext.Method == "textDocument/didChange"
if isSingleFileChange {
if rpcContext.Method == "textDocument/didChange" && lsctx.IsLanguageId(ctx, ilsp.Terraform.String()) {
filename := path.Base(rpcContext.URI)
// We only revalidate a single file that changed
var fileDiags hcl.Diagnostics
Expand Down Expand Up @@ -731,6 +731,69 @@ func SchemaValidation(ctx context.Context, modStore *state.ModuleStore, schemaRe
return rErr
}

func SchemaVariablesValidation(ctx context.Context, modStore *state.ModuleStore, schemaReader state.SchemaReader, modPath string) error {
mod, err := modStore.ModuleByPath(modPath)
if err != nil {
return err
}

// Avoid validation if it is already in progress or already finished
if mod.VarsDiagnosticsState[ast.SchemaValidationSource] != op.OpStateUnknown && !job.IgnoreState(ctx) {
return job.StateNotChangedErr{Dir: document.DirHandleFromPath(modPath)}
}

err = modStore.SetVarsDiagnosticsState(modPath, ast.SchemaValidationSource, op.OpStateLoading)
if err != nil {
return err
}

d := decoder.NewDecoder(&idecoder.PathReader{
ModuleReader: modStore,
SchemaReader: schemaReader,
})

d.SetContext(idecoder.DecoderContext(ctx))

moduleDecoder, err := d.Path(lang.Path{
Path: modPath,
LanguageID: ilsp.Tfvars.String(),
})
if err != nil {
return err
}

var rErr error
rpcContext := lsctx.RPCContext(ctx)
if rpcContext.Method == "textDocument/didChange" && lsctx.IsLanguageId(ctx, ilsp.Tfvars.String()) {
filename := path.Base(rpcContext.URI)
// We only revalidate a single file that changed
var fileDiags hcl.Diagnostics
fileDiags, rErr = moduleDecoder.ValidateFile(ctx, filename)

varsDiags, ok := mod.VarsDiagnostics[ast.SchemaValidationSource]
if !ok {
varsDiags = make(ast.VarsDiags)
}
varsDiags[ast.VarsFilename(filename)] = fileDiags

sErr := modStore.UpdateVarsDiagnostics(modPath, ast.SchemaValidationSource, varsDiags)
if sErr != nil {
return sErr
}
} else {
// We validate the whole module, e.g. on open
var diags lang.DiagnosticsMap
diags, rErr = moduleDecoder.Validate(ctx)

sErr := modStore.UpdateVarsDiagnostics(modPath, ast.SchemaValidationSource, ast.VarsDiagsFromMap(diags))
if sErr != nil {
return sErr
}
}

return rErr
}

func ReferenceValidation(ctx context.Context, modStore *state.ModuleStore, schemaReader state.SchemaReader, modPath string) error {
mod, err := modStore.ModuleByPath(modPath)
if err != nil {
Expand Down
117 changes: 113 additions & 4 deletions internal/terraform/module/module_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/hashicorp/terraform-ls/internal/document"
"github.com/hashicorp/terraform-ls/internal/filesystem"
"github.com/hashicorp/terraform-ls/internal/job"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
"github.com/hashicorp/terraform-ls/internal/registry"
"github.com/hashicorp/terraform-ls/internal/state"
"github.com/hashicorp/terraform-ls/internal/terraform/ast"
Expand Down Expand Up @@ -969,7 +970,7 @@ var randomSchemaJSON = `{
}
}`

func TestSchemaValidation_FullModule(t *testing.T) {
func TestSchemaModuleValidation_FullModule(t *testing.T) {
ctx := context.Background()
ss, err := state.NewStateStore()
if err != nil {
Expand All @@ -996,7 +997,8 @@ func TestSchemaValidation_FullModule(t *testing.T) {
Method: "textDocument/didOpen",
URI: "file:///test/variables.tf",
})
err = SchemaValidation(ctx, ss.Modules, ss.ProviderSchemas, modPath)
ctx = lsctx.WithLanguageId(ctx, ilsp.Terraform.String())
err = SchemaModuleValidation(ctx, ss.Modules, ss.ProviderSchemas, modPath)
if err != nil {
t.Fatal(err)
}
Expand All @@ -1013,7 +1015,7 @@ func TestSchemaValidation_FullModule(t *testing.T) {
}
}

func TestSchemaValidation_SingleFile(t *testing.T) {
func TestSchemaModuleValidation_SingleFile(t *testing.T) {
ctx := context.Background()
ss, err := state.NewStateStore()
if err != nil {
Expand All @@ -1040,7 +1042,8 @@ func TestSchemaValidation_SingleFile(t *testing.T) {
Method: "textDocument/didChange",
URI: "file:///test/variables.tf",
})
err = SchemaValidation(ctx, ss.Modules, ss.ProviderSchemas, modPath)
ctx = lsctx.WithLanguageId(ctx, ilsp.Terraform.String())
err = SchemaModuleValidation(ctx, ss.Modules, ss.ProviderSchemas, modPath)
if err != nil {
t.Fatal(err)
}
Expand All @@ -1056,3 +1059,109 @@ func TestSchemaValidation_SingleFile(t *testing.T) {
t.Fatalf("expected %d diagnostics, %d given", expectedCount, diagsCount)
}
}

func TestSchemaVarsValidation_FullModule(t *testing.T) {
ctx := context.Background()
ss, err := state.NewStateStore()
if err != nil {
t.Fatal(err)
}

testData, err := filepath.Abs("testdata")
if err != nil {
t.Fatal(err)
}
modPath := filepath.Join(testData, "invalid-tfvars")

err = ss.Modules.Add(modPath)
if err != nil {
t.Fatal(err)
}

fs := filesystem.NewFilesystem(ss.DocumentStore)
err = ParseModuleConfiguration(ctx, fs, ss.Modules, modPath)
if err != nil {
t.Fatal(err)
}
err = LoadModuleMetadata(ctx, ss.Modules, modPath)
if err != nil {
t.Fatal(err)
}
err = ParseVariables(ctx, fs, ss.Modules, modPath)
if err != nil {
t.Fatal(err)
}
ctx = lsctx.WithRPCContext(ctx, lsctx.RPCContextData{
Method: "textDocument/didOpen",
URI: "file:///test/terraform.tfvars",
})
ctx = lsctx.WithLanguageId(ctx, ilsp.Tfvars.String())
err = SchemaVariablesValidation(ctx, ss.Modules, ss.ProviderSchemas, modPath)
if err != nil {
t.Fatal(err)
}

mod, err := ss.Modules.ModuleByPath(modPath)
if err != nil {
t.Fatal(err)
}

expectedCount := 2
diagsCount := mod.VarsDiagnostics[ast.SchemaValidationSource].Count()
if diagsCount != expectedCount {
t.Fatalf("expected %d diagnostics, %d given", expectedCount, diagsCount)
}
}

func TestSchemaVarsValidation_SingleFile(t *testing.T) {
ctx := context.Background()
ss, err := state.NewStateStore()
if err != nil {
t.Fatal(err)
}

testData, err := filepath.Abs("testdata")
if err != nil {
t.Fatal(err)
}
modPath := filepath.Join(testData, "invalid-tfvars")

err = ss.Modules.Add(modPath)
if err != nil {
t.Fatal(err)
}

fs := filesystem.NewFilesystem(ss.DocumentStore)
err = ParseModuleConfiguration(ctx, fs, ss.Modules, modPath)
if err != nil {
t.Fatal(err)
}
err = LoadModuleMetadata(ctx, ss.Modules, modPath)
if err != nil {
t.Fatal(err)
}
err = ParseVariables(ctx, fs, ss.Modules, modPath)
if err != nil {
t.Fatal(err)
}
ctx = lsctx.WithRPCContext(ctx, lsctx.RPCContextData{
Method: "textDocument/didChange",
URI: "file:///test/terraform.tfvars",
})
ctx = lsctx.WithLanguageId(ctx, ilsp.Tfvars.String())
err = SchemaVariablesValidation(ctx, ss.Modules, ss.ProviderSchemas, modPath)
if err != nil {
t.Fatal(err)
}

mod, err := ss.Modules.ModuleByPath(modPath)
if err != nil {
t.Fatal(err)
}

expectedCount := 1
diagsCount := mod.VarsDiagnostics[ast.SchemaValidationSource].Count()
if diagsCount != expectedCount {
t.Fatalf("expected %d diagnostics, %d given", expectedCount, diagsCount)
}
}
11 changes: 6 additions & 5 deletions internal/terraform/module/operation/op_type_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion internal/terraform/module/operation/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const (
OpTypeGetModuleDataFromRegistry
OpTypeParseProviderVersions
OpTypePreloadEmbeddedSchema
OpTypeSchemaValidation
OpTypeSchemaModuleValidation
OpTypeSchemaVarsValidation
OpTypeReferenceValidation
OpTypeTerraformValidate
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
noot = "noot"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
foo = "foo"
bar = "noot"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
variable "foo" {}

0 comments on commit daaa726

Please sign in to comment.