-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement support for Terraform Stacks
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
Showing
11 changed files
with
1,182 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.