Skip to content

Commit

Permalink
Enable early validation for Terraform Stacks
Browse files Browse the repository at this point in the history
This change enables early validation for Terraform Stacks in tfstack. hcl and tfdeploy.hcl files.

Available validations:

- Missing required attributes
- Deprecated attributes
- Deprecated blocks
- Unexpected attributes
- Unexpected blocks
- Maximum number of blocks
- Minimum number of blocks
- Block labels length

This adds a new job to validate the schema of the these files. The job is enqueued when the `EnableEnhancedValidation` option is set to true.
  • Loading branch information
jpogran committed Jul 24, 2024
1 parent d9eef93 commit f9b99ca
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 0 deletions.
9 changes: 9 additions & 0 deletions internal/features/stacks/ast/stacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ func (sd Diagnostics) Count() int {
return count
}


func DiagnosticsFromMap(m map[string]hcl.Diagnostics) Diagnostics {
mf := make(Diagnostics, len(m))
for name, file := range m {
mf[FilenameFromName(name)] = file
}
return mf
}

type SourceDiagnostics map[globalAst.DiagnosticSource]Diagnostics

func (svd SourceDiagnostics) Count() int {
Expand Down
2 changes: 2 additions & 0 deletions internal/features/stacks/decoder/path_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func stackPathContext(record *state.StackRecord) (*decoder.PathContext, error) {
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File, 0),
Validators: stackValidators,
}

// TODO: Add reference origins and targets if needed
Expand Down Expand Up @@ -90,6 +91,7 @@ func deployPathContext(record *state.StackRecord) (*decoder.PathContext, error)
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File, 0),
Validators: stackValidators,
}

// TODO: Add reference origins and targets if needed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validations

import (
"context"
"fmt"

"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl-lang/schemacontext"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)

type MissingRequiredAttribute struct{}

func (mra MissingRequiredAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) {
var diags hcl.Diagnostics
if HasUnknownRequiredAttributes(ctx) {
return ctx, diags
}

switch nodeType := node.(type) {
case *hclsyntax.Block:
// Providers are excluded from the validation for the time being
// due to complexity around required attributes with dynamic defaults
// See https://github.com/hashicorp/vscode-terraform/issues/1616
nestingLvl, nestingOk := schemacontext.BlockNestingLevel(ctx)
if nodeType.Type == "provider" && (nestingOk && nestingLvl == 0) {
ctx = WithUnknownRequiredAttributes(ctx)
}
case *hclsyntax.Body:
if nodeSchema == nil {
return ctx, diags
}

bodySchema := nodeSchema.(*schema.BodySchema)
if bodySchema.Attributes == nil {
return ctx, diags
}

for name, attr := range bodySchema.Attributes {
if attr.IsRequired {
_, ok := nodeType.Attributes[name]
if !ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Required attribute %q not specified", name),
Detail: fmt.Sprintf("An attribute named %q is required here", name),
Subject: nodeType.SrcRange.Ptr(),
})
}
}
}
}

return ctx, diags
}

type unknownRequiredAttrsCtxKey struct{}

func HasUnknownRequiredAttributes(ctx context.Context) bool {
_, ok := ctx.Value(unknownRequiredAttrsCtxKey{}).(bool)
return ok
}

func WithUnknownRequiredAttributes(ctx context.Context) context.Context {
return context.WithValue(ctx, unknownRequiredAttrsCtxKey{}, true)
}
20 changes: 20 additions & 0 deletions internal/features/stacks/decoder/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package decoder

import (
"github.com/hashicorp/hcl-lang/validator"
"github.com/hashicorp/terraform-ls/internal/features/stacks/decoder/validations"
)

var stackValidators = []validator.Validator{
validator.BlockLabelsLength{},
validator.DeprecatedAttribute{},
validator.DeprecatedBlock{},
validator.MaxBlocks{},
validator.MinBlocks{},
validations.MissingRequiredAttribute{},
validator.UnexpectedAttribute{},
validator.UnexpectedBlock{},
}
21 changes: 21 additions & 0 deletions internal/features/stacks/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"

lsctx "github.com/hashicorp/terraform-ls/internal/context"
"github.com/hashicorp/terraform-ls/internal/document"
"github.com/hashicorp/terraform-ls/internal/eventbus"
"github.com/hashicorp/terraform-ls/internal/features/stacks/ast"
Expand Down Expand Up @@ -234,6 +235,26 @@ func (f *StacksFeature) decodeStack(ctx context.Context, dir document.DirHandle,
}
ids = append(ids, eSchemaId)

validationOptions, err := lsctx.ValidationOptions(ctx)
if err != nil {
return ids, err
}
if validationOptions.EnableEnhancedValidation {
_, err = f.stateStore.JobStore.EnqueueJob(ctx, job.Job{
Dir: dir,
Func: func(ctx context.Context) error {
return jobs.SchemaModuleValidation(ctx, f.store, dir.Path())
},
Type: operation.OpTypeSchemaModuleValidation.String(),
DependsOn: ids,
IgnoreState: ignoreState,
})
if err != nil {
return ids, err
}

}

// TODO: Implement the following functions where appropriate to stacks
// Future: decodeDeclaredModuleCalls(ctx, dir, ignoreState)
// Future: DecodeReferenceTargets(ctx, f.Store, f.rootFeature, path)
Expand Down
86 changes: 86 additions & 0 deletions internal/features/stacks/jobs/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package jobs

import (
"context"
"path"

"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl/v2"
lsctx "github.com/hashicorp/terraform-ls/internal/context"
idecoder "github.com/hashicorp/terraform-ls/internal/decoder"
"github.com/hashicorp/terraform-ls/internal/document"
"github.com/hashicorp/terraform-ls/internal/features/stacks/ast"
sdecoder "github.com/hashicorp/terraform-ls/internal/features/stacks/decoder"
"github.com/hashicorp/terraform-ls/internal/features/stacks/state"
"github.com/hashicorp/terraform-ls/internal/job"
lsp "github.com/hashicorp/terraform-ls/internal/lsp"
globalAst "github.com/hashicorp/terraform-ls/internal/terraform/ast"
"github.com/hashicorp/terraform-ls/internal/terraform/module/operation"
)

func SchemaModuleValidation(ctx context.Context, stackStore *state.StackStore, stackPath string) error {
rpcContext := lsctx.DocumentContext(ctx)
isMatchingLanguageId := rpcContext.LanguageID == lsp.Stacks.String() || rpcContext.LanguageID == lsp.Deploy.String()
if !isMatchingLanguageId {
return nil
}

record, err := stackStore.StackRecordByPath(stackPath)
if err != nil {
return err
}

// Avoid validation if it is already in progress or already finished
if record.DiagnosticsState[globalAst.SchemaValidationSource] != operation.OpStateUnknown && !job.IgnoreState(ctx) {
return job.StateNotChangedErr{Dir: document.DirHandleFromPath(stackPath)}
}

err = stackStore.SetDiagnosticsState(stackPath, globalAst.SchemaValidationSource, operation.OpStateLoading)
if err != nil {
return err
}

d := decoder.NewDecoder(&sdecoder.PathReader{
StateReader: stackStore,
})
d.SetContext(idecoder.DecoderContext(ctx))

decoder, err := d.Path(lang.Path{
Path: stackPath,
LanguageID: rpcContext.LanguageID,
})
if err != nil {
return err
}

var rErr error
if rpcContext.Method == "textDocument/didChange" {
filename := path.Base(rpcContext.URI)

var fileDiags hcl.Diagnostics
fileDiags, rErr = decoder.ValidateFile(ctx, filename)

diags, ok := record.Diagnostics[globalAst.SchemaValidationSource]
if !ok {
diags = make(ast.Diagnostics)
}
diags[ast.FilenameFromName(filename)] = fileDiags

sErr := stackStore.UpdateDiagnostics(stackPath, globalAst.SchemaValidationSource, diags)
if sErr != nil {
return sErr
}
} else {
// We validate the whole stack
var diags lang.DiagnosticsMap
diags, rErr = decoder.Validate(ctx)

sErr := stackStore.UpdateDiagnostics(stackPath, globalAst.SchemaValidationSource, ast.DiagnosticsFromMap(diags))
if sErr != nil {
return sErr
}
}

return rErr
}

0 comments on commit f9b99ca

Please sign in to comment.