Skip to content

Commit

Permalink
Implement support for Terraform Stacks
Browse files Browse the repository at this point in the history
This commit adds initial support for Terraform Stacks files and Deployment files. This provides completion, hover, and diagnostics for Terraform Stacks and Deployment files.
  • Loading branch information
dbanck committed Jul 5, 2024
1 parent 4604f3f commit 5d3790b
Show file tree
Hide file tree
Showing 11 changed files with 1,182 additions and 0 deletions.
127 changes: 127 additions & 0 deletions internal/features/stacks/ast/stacks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ast

import (
"strings"

"github.com/hashicorp/hcl/v2"
globalAst "github.com/hashicorp/terraform-ls/internal/terraform/ast"
)

type Filename interface {
String() string
IsJSON() bool
IsIgnored() bool
}

// StackFilename is a custom type for stack configuration files
type StackFilename string

func (mf StackFilename) String() string {
return string(mf)
}

func (mf StackFilename) IsJSON() bool {
return strings.HasSuffix(string(mf), ".json")
}

func (mf StackFilename) IsIgnored() bool {
return globalAst.IsIgnoredFile(string(mf))
}

func IsStackFilename(name string) bool {
return strings.HasSuffix(name, ".tfstack.hcl") ||
strings.HasSuffix(name, ".tfstack.json")
}

// DeployFilename is a custom type for deployment files
type DeployFilename string

func (df DeployFilename) String() string {
return string(df)
}

func (df DeployFilename) IsJSON() bool {
return strings.HasSuffix(string(df), ".json")
}

func (df DeployFilename) IsIgnored() bool {
return globalAst.IsIgnoredFile(string(df))
}

func IsDeployFilename(name string) bool {
return strings.HasSuffix(name, ".tfdeploy.hcl") ||
strings.HasSuffix(name, ".tfdeploy.json")
}

// FilenameFromName returns either a StackFilename or DeployFilename based
// on the name
func FilenameFromName(name string) Filename {
if IsStackFilename(name) {
return StackFilename(name)
}
if IsDeployFilename(name) {
return DeployFilename(name)
}

return nil
}

type Files map[Filename]*hcl.File

func (sf Files) Copy() Files {
m := make(Files, len(sf))
for name, file := range sf {
m[name] = file
}
return m
}

type Diagnostics map[Filename]hcl.Diagnostics

func (sd Diagnostics) Copy() Diagnostics {
m := make(Diagnostics, len(sd))
for name, diags := range sd {
m[name] = diags
}
return m
}

// AutoloadedOnly returns only diagnostics that are not from ignored files
func (sd Diagnostics) AutoloadedOnly() Diagnostics {
diags := make(Diagnostics)
for name, f := range sd {
if !name.IsIgnored() {
diags[name] = f
}
}
return diags
}

func (sd Diagnostics) AsMap() map[string]hcl.Diagnostics {
m := make(map[string]hcl.Diagnostics, len(sd))
for name, diags := range sd {
m[name.String()] = diags
}
return m
}

func (sd Diagnostics) Count() int {
count := 0
for _, diags := range sd {
count += len(diags)
}
return count
}

type SourceDiagnostics map[globalAst.DiagnosticSource]Diagnostics

func (svd SourceDiagnostics) Count() int {
count := 0
for _, diags := range svd {
count += diags.Count()
}
return count
}
133 changes: 133 additions & 0 deletions internal/features/stacks/decoder/path_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package decoder

import (
"context"
"fmt"

"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform-ls/internal/features/stacks/ast"
"github.com/hashicorp/terraform-ls/internal/features/stacks/state"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
stackschema "github.com/hashicorp/terraform-schema/schema"
)

type PathReader struct {
StateReader StateReader
}

type StateReader interface {
List() ([]*state.StackRecord, error)
StackRecordByPath(modPath string) (*state.StackRecord, error)
}

// PathContext returns a PathContext for the given path based on the language ID
func (pr *PathReader) PathContext(path lang.Path) (*decoder.PathContext, error) {
record, err := pr.StateReader.StackRecordByPath(path.Path)
if err != nil {
return nil, err
}

switch path.LanguageID {
case ilsp.Stacks.String():
return stackPathContext(record)
case ilsp.Deploy.String():
return deployPathContext(record)
}

return nil, fmt.Errorf("unknown language ID: %q", path.LanguageID)
}

func stackPathContext(record *state.StackRecord) (*decoder.PathContext, error) {
// TODO: get Terraform version from record and use that to get the schema
// TODO: this should only work for terraform 1.8 and above
schema, err := stackschema.CoreStackSchemaForVersion(stackschema.LatestAvailableVersion)
if err != nil {
return nil, err
}

pathCtx := &decoder.PathContext{
Schema: schema,
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File, 0),
}

// TODO: Add reference origins and targets if needed

for name, f := range record.ParsedFiles {
if _, ok := name.(ast.StackFilename); ok {
pathCtx.Files[name.String()] = f
}
}

return pathCtx, nil
}

func deployPathContext(record *state.StackRecord) (*decoder.PathContext, error) {
// TODO: get Terraform version from record and use that to get the schema
// TODO: this should only work for terraform 1.8 and above
schema, err := stackschema.CoreDeploySchemaForVersion(stackschema.LatestAvailableVersion)
if err != nil {
return nil, err
}

pathCtx := &decoder.PathContext{
Schema: schema,
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File, 0),
}

// TODO: Add reference origins and targets if needed

for name, f := range record.ParsedFiles {
if _, ok := name.(ast.DeployFilename); ok {
pathCtx.Files[name.String()] = f
}
}

return pathCtx, nil
}

func (pr *PathReader) Paths(ctx context.Context) []lang.Path {
paths := make([]lang.Path, 0)

stackRecords, err := pr.StateReader.List()
if err != nil {
return paths
}

for _, record := range stackRecords {
foundStack := false
foundDeploy := false
for name := range record.ParsedFiles {
if _, ok := name.(ast.StackFilename); ok {
foundStack = true
}
if _, ok := name.(ast.DeployFilename); ok {
foundDeploy = true
}
}

if foundStack {
paths = append(paths, lang.Path{
Path: record.Path(),
LanguageID: ilsp.Stacks.String(),
})
}
if foundDeploy {
paths = append(paths, lang.Path{
Path: record.Path(),
LanguageID: ilsp.Deploy.String(),
})
}
}

return paths
}
Loading

0 comments on commit 5d3790b

Please sign in to comment.