Skip to content

Commit

Permalink
Added hclvalidate command (#3248)
Browse files Browse the repository at this point in the history
* chore: configstack code refactoring

* fix: tests

* fix: tests

* fix: hclparse options

* fix: parse config

* fix: unit test

* fix: unit test

* chore: print diagnostics in a human-friendly format

* fix: lint

* chore: fix unit tests, improve locals diagnostic

* fix: unit test

* chore: add integration test

* fix: fixture

* chore: disable SONAR for unit tests

* fix: test

* chore: sonar properties

* chore: code improvements

* chore: update sonarcloud properties

* chore: update docs

* fix: grammar

* chore: update docs

* chore: custom errors renaming

* chore: code improvements

* fix: review commetns
  • Loading branch information
levkohimins authored Jul 11, 2024
1 parent e47fa39 commit e37d71e
Show file tree
Hide file tree
Showing 70 changed files with 4,770 additions and 3,310 deletions.
4 changes: 4 additions & 0 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Source File Exclusions: Patterns used to exclude some source files from analysis.
sonar.exclusions=**/*_test.go
# Test File Inclusions: Patterns used to include some test files and only these ones in analysis.
sonar.test.inclusions=**/*_test.go
7 changes: 7 additions & 0 deletions cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"golang.org/x/text/language"

"github.com/gruntwork-io/terragrunt/cli/commands/graph"
"github.com/gruntwork-io/terragrunt/cli/commands/hclvalidate"

"github.com/gruntwork-io/terragrunt/cli/commands/scaffold"

Expand Down Expand Up @@ -97,6 +98,11 @@ func (app *App) RunContext(ctx context.Context, args []string) error {
log.Infof("%s signal received. Gracefully shutting down... (it can take up to %v)", cases.Title(language.English).String(signal.String()), shell.SignalForwardingDelay)
cancel()

shell.RegisterSignalHandler(func(signal os.Signal) {
log.Infof("Second %s signal received, force shutting down...", cases.Title(language.English).String(signal.String()))
os.Exit(1)
})

time.Sleep(forceExitInterval)
log.Infof("Failed to gracefully shutdown within %v, force shutting down...", forceExitInterval)
os.Exit(1)
Expand Down Expand Up @@ -139,6 +145,7 @@ func terragruntCommands(opts *options.TerragruntOptions) cli.Commands {
catalog.NewCommand(opts), // catalog
scaffold.NewCommand(opts), // scaffold
graph.NewCommand(opts), // graph
hclvalidate.NewCommand(opts), // hclvalidate
}

sort.Sort(cmds)
Expand Down
2 changes: 1 addition & 1 deletion cli/commands/graph-dependencies/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// Run graph dependencies prints the dependency graph to stdout
func Run(ctx context.Context, opts *options.TerragruntOptions) error {
stack, err := configstack.FindStackInSubfolders(ctx, opts, nil)
stack, err := configstack.FindStackInSubfolders(ctx, opts)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cli/commands/graph/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ func graph(ctx context.Context, opts *options.TerragruntOptions, cfg *config.Ter
rootOptions := opts.Clone(rootDir)
rootOptions.WorkingDir = rootDir

stack, err := configstack.FindStackInSubfolders(ctx, rootOptions, nil)
stack, err := configstack.FindStackInSubfolders(ctx, rootOptions)
if err != nil {
return err
}
dependentModules := configstack.ListStackDependentModules(stack)
dependentModules := stack.ListStackDependentModules()

workDir := opts.WorkingDir
modulesToInclude := dependentModules[workDir]
Expand Down
6 changes: 3 additions & 3 deletions cli/commands/hclfmt/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func formatTgHCL(opts *options.TerragruntOptions, tgHclFile string) error {
}
contents := []byte(contentsStr)

err = checkErrors(opts.Logger, contents, tgHclFile)
err = checkErrors(opts.Logger, opts.DisableLogColors, contents, tgHclFile)
if err != nil {
opts.Logger.Errorf("Error parsing %s", tgHclFile)
return err
Expand Down Expand Up @@ -128,10 +128,10 @@ func formatTgHCL(opts *options.TerragruntOptions, tgHclFile string) error {
}

// checkErrors takes in the contents of a hcl file and looks for syntax errors.
func checkErrors(logger *logrus.Entry, contents []byte, tgHclFile string) error {
func checkErrors(logger *logrus.Entry, disableColor bool, contents []byte, tgHclFile string) error {
parser := hclparse.NewParser()
_, diags := parser.ParseHCL(contents, tgHclFile)
diagWriter := util.GetDiagnosticsWriter(logger, parser)
diagWriter := util.GetDiagnosticsWriter(logger, parser, disableColor)
err := diagWriter.WriteDiagnostics(diags)
if err != nil {
return errors.WithStackTrace(err)
Expand Down
66 changes: 66 additions & 0 deletions cli/commands/hclvalidate/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package hclvalidate

import (
"context"

"github.com/gruntwork-io/terragrunt/config"
"github.com/gruntwork-io/terragrunt/config/hclparse"
"github.com/gruntwork-io/terragrunt/configstack"
"github.com/gruntwork-io/terragrunt/internal/view"
"github.com/gruntwork-io/terragrunt/internal/view/diagnostic"
"github.com/gruntwork-io/terragrunt/options"
"github.com/hashicorp/hcl/v2"
)

func Run(ctx context.Context, opts *Options) (er error) {
var diags diagnostic.Diagnostics

parseOptions := []hclparse.Option{
hclparse.WithDiagnosticsHandler(func(file *hcl.File, hclDiags hcl.Diagnostics) (hcl.Diagnostics, error) {
for _, hclDiag := range hclDiags {
if !diags.Contains(hclDiag) {
newDiag := diagnostic.NewDiagnostic(file, hclDiag)
diags = append(diags, newDiag)
}
}
return nil, nil
}),
}

opts.SkipOutput = true
opts.NonInteractive = true
opts.RunTerragrunt = func(ctx context.Context, opts *options.TerragruntOptions) error {
_, err := config.ReadTerragruntConfig(ctx, opts, parseOptions)
return err
}

stack, err := configstack.FindStackInSubfolders(ctx, opts.TerragruntOptions, configstack.WithParseOptions(parseOptions))
if err != nil {
return err
}

stackErr := stack.Run(ctx, opts.TerragruntOptions)

if len(diags) > 0 {
if err := writeDiagnostics(opts, diags); err != nil {
return err
}
}

return stackErr
}

func writeDiagnostics(opts *Options, diags diagnostic.Diagnostics) error {
render := view.NewHumanRender(opts.DisableLogColors)
if opts.JSONOutput {
render = view.NewJSONRender()
}

writer := view.NewWriter(opts.Writer, render)

if opts.InvalidConfigPath {
return writer.InvalidConfigPath(diags)
}

return writer.Diagnostics(diags)
}
47 changes: 47 additions & 0 deletions cli/commands/hclvalidate/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// `hclvalidate` command recursively looks for hcl files in the directory tree starting at workingDir, and validates them
// based on the language style guides provided by Hashicorp. This is done using the official hcl2 library.

package hclvalidate

import (
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/pkg/cli"
)

const (
CommandName = "hclvalidate"

InvalidFlagName = "terragrunt-hclvalidate-invalid"
InvalidEnvVarName = "TERRAGRUNT_HCLVALIDATE_INVALID"

JSONOutputFlagName = "terragrunt-hclvalidate-json"
JSONOutputEnvVarName = "TERRAGRUNT_HCLVALIDATE_JSON"
)

func NewFlags(opts *Options) cli.Flags {
return cli.Flags{
&cli.BoolFlag{
Name: InvalidFlagName,
EnvVar: InvalidEnvVarName,
Usage: "Show a list of files with invalid configuration.",
Destination: &opts.InvalidConfigPath,
},
&cli.BoolFlag{
Name: JSONOutputFlagName,
EnvVar: JSONOutputEnvVarName,
Destination: &opts.JSONOutput,
Usage: "Output the result in JSON format.",
},
}
}

func NewCommand(generalOpts *options.TerragruntOptions) *cli.Command {
opts := NewOptions(generalOpts)

return &cli.Command{
Name: CommandName,
Usage: "Find all hcl files from the config stack and validate them.",
Flags: NewFlags(opts).Sort(),
Action: func(ctx *cli.Context) error { return Run(ctx, opts) },
}
}
16 changes: 16 additions & 0 deletions cli/commands/hclvalidate/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package hclvalidate

import "github.com/gruntwork-io/terragrunt/options"

type Options struct {
*options.TerragruntOptions

InvalidConfigPath bool
JSONOutput bool
}

func NewOptions(general *options.TerragruntOptions) *Options {
return &Options{
TerragruntOptions: general,
}
}
2 changes: 1 addition & 1 deletion cli/commands/output-module-groups/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func Run(ctx context.Context, opts *options.TerragruntOptions) error {
stack, err := configstack.FindStackInSubfolders(ctx, opts, nil)
stack, err := configstack.FindStackInSubfolders(ctx, opts)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cli/commands/run-all/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions) error {
}
}

stack, err := configstack.FindStackInSubfolders(ctx, opts, nil)
stack, err := configstack.FindStackInSubfolders(ctx, opts)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion cli/commands/scaffold/action_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scaffold

import (
"context"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -81,7 +82,7 @@ func TestDefaultTemplateVariables(t *testing.T) {
opts, err := options.NewTerragruntOptionsForTest(filepath.Join(outputDir, "terragrunt.hcl"))
require.NoError(t, err)

cfg, err := config.ReadTerragruntConfig(opts)
cfg, err := config.ReadTerragruntConfig(context.Background(), opts, config.DefaultParserOptions(opts))
require.NoError(t, err)
require.NotEmpty(t, cfg.Inputs)
require.Equal(t, 1, len(cfg.Inputs))
Expand Down
2 changes: 1 addition & 1 deletion cli/commands/terraform/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func runTerraform(ctx context.Context, terragruntOptions *options.TerragruntOpti
return err
}

terragruntConfig, err := config.ReadTerragruntConfig(terragruntOptions)
terragruntConfig, err := config.ReadTerragruntConfig(ctx, terragruntOptions, config.DefaultParserOptions(terragruntOptions))
if err != nil {
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}
Expand Down
4 changes: 0 additions & 4 deletions cli/provider_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,6 @@ func (cache *ProviderCache) TerraformCommandHook(ctx context.Context, opts *opti
cloneOpts.WorkingDir = opts.WorkingDir
maps.Copy(cloneOpts.Env, env)

if util.FirstArg(args) == terraform.CommandNameProviders && util.SecondArg(args) == terraform.CommandNameLock {
return &shell.CmdOutput{}, nil
}

if skipRunTargetCommand {
return &shell.CmdOutput{}, nil
}
Expand Down
27 changes: 18 additions & 9 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/gruntwork-io/terragrunt/internal/cache"
"github.com/gruntwork-io/terragrunt/shell"
"github.com/gruntwork-io/terragrunt/telemetry"

"github.com/mitchellh/mapstructure"
Expand Down Expand Up @@ -72,7 +73,7 @@ var (

DefaultParserOptions = func(opts *options.TerragruntOptions) []hclparse.Option {
return []hclparse.Option{
hclparse.WithLogger(opts.Logger),
hclparse.WithLogger(opts.Logger, opts.DisableLogColors),
hclparse.WithFileUpdate(updateBareIncludeBlock),
}
}
Expand Down Expand Up @@ -680,23 +681,23 @@ func isTerragruntModuleDir(path string, terragruntOptions *options.TerragruntOpt
}

// Read the Terragrunt config file from its default location
func ReadTerragruntConfig(terragruntOptions *options.TerragruntOptions) (*TerragruntConfig, error) {
func ReadTerragruntConfig(ctx context.Context, terragruntOptions *options.TerragruntOptions, parserOptions []hclparse.Option) (*TerragruntConfig, error) {
terragruntOptions.Logger.Debugf("Reading Terragrunt config file at %s", terragruntOptions.TerragruntConfigPath)

ctx := NewParsingContext(context.Background(), terragruntOptions)
return ParseConfigFile(terragruntOptions, ctx, terragruntOptions.TerragruntConfigPath, nil)
ctx = shell.ContextWithTerraformCommandHook(ctx, nil)
parcingCtx := NewParsingContext(ctx, terragruntOptions).WithParseOption(parserOptions)
return ParseConfigFile(parcingCtx, terragruntOptions.TerragruntConfigPath, nil)
}

var hclCache = cache.NewCache[*hclparse.File]()

// Parse the Terragrunt config file at the given path. If the include parameter is not nil, then treat this as a config
// included in some other config file when resolving relative paths.
func ParseConfigFile(opts *options.TerragruntOptions, ctx *ParsingContext, configPath string, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {

func ParseConfigFile(ctx *ParsingContext, configPath string, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {
var config *TerragruntConfig
err := telemetry.Telemetry(ctx, opts, "parse_config_file", map[string]interface{}{
err := telemetry.Telemetry(ctx, ctx.TerragruntOptions, "parse_config_file", map[string]interface{}{
"config_path": configPath,
"working_dir": opts.WorkingDir,
"working_dir": ctx.TerragruntOptions.WorkingDir,
}, func(childCtx context.Context) error {
childKey := "nil"
if includeFromChild != nil {
Expand All @@ -716,7 +717,7 @@ func ParseConfigFile(opts *options.TerragruntOptions, ctx *ParsingContext, confi
return err
}
var file *hclparse.File
var cacheKey = fmt.Sprintf("parse-config-%v-%v-%v-%v-%v-%v", configPath, childKey, decodeListKey, opts.WorkingDir, dir, fileInfo.ModTime().UnixMicro())
var cacheKey = fmt.Sprintf("parse-config-%v-%v-%v-%v-%v-%v", configPath, childKey, decodeListKey, ctx.TerragruntOptions.WorkingDir, dir, fileInfo.ModTime().UnixMicro())
if cacheConfig, found := hclCache.Get(cacheKey); found {
file = cacheConfig
} else {
Expand Down Expand Up @@ -888,7 +889,9 @@ func decodeAsTerragruntConfigFile(ctx *ParsingContext, file *hclparse.File, eval
return nil, err
}
ctx.TerragruntOptions.Logger.Warnf("Failed to decode inputs %v", diagErr)
}

if terragruntConfig.Inputs != nil {
inputs, err := updateUnknownCtyValValues(terragruntConfig.Inputs)
if err != nil {
return nil, err
Expand Down Expand Up @@ -1143,6 +1146,12 @@ func convertToTerragruntConfig(ctx *ParsingContext, configPath string, terragrun
}

if ctx.Locals != nil && *ctx.Locals != cty.NilVal {
locals, err := updateUnknownCtyValValues(ctx.Locals)
if err != nil {
return nil, err
}
ctx.Locals = locals

localsParsed, err := parseCtyValueToMap(*ctx.Locals)
if err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions config/config_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ func getWorkingDir(ctx *ParsingContext) (string, error) {
FuncNameGetWorkingDir: wrapVoidToEmptyStringAsFuncImpl(),
}

terragruntConfig, err := ParseConfigFile(ctx.TerragruntOptions, ctx, ctx.TerragruntOptions.TerragruntConfigPath, nil)
terragruntConfig, err := ParseConfigFile(ctx, ctx.TerragruntOptions.TerragruntConfigPath, nil)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -594,7 +594,7 @@ func readTerragruntConfig(ctx *ParsingContext, configPath string, defaultVal *ct

// We update the ctx of terragruntOptions to the config being read in.
ctx = ctx.WithTerragruntOptions(ctx.TerragruntOptions.Clone(targetConfig))
config, err := ParseConfigFile(ctx.TerragruntOptions, ctx, targetConfig, nil)
config, err := ParseConfigFile(ctx, targetConfig, nil)
if err != nil {
return cty.NilVal, err
}
Expand Down
8 changes: 4 additions & 4 deletions config/config_partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,15 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
return nil, err
}
ctx.TerragruntOptions.Logger.Warnf("Failed to decode inputs %v", diagErr)
}

inputs, err := updateUnknownCtyValValues(decoded.Inputs)
if decoded.Inputs != nil {
val, err := updateUnknownCtyValValues(decoded.Inputs)
if err != nil {
return nil, err
}
decoded.Inputs = inputs
}
decoded.Inputs = val

if decoded.Inputs != nil {
inputs, err := parseCtyValueToMap(*decoded.Inputs)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1356,7 +1356,7 @@ func BenchmarkReadTerragruntConfig(b *testing.B) {

b.ResetTimer()
b.StartTimer()
actual, err := ReadTerragruntConfig(terragruntOptions)
actual, err := ReadTerragruntConfig(context.Background(), terragruntOptions, DefaultParserOptions(terragruntOptions))
b.StopTimer()
require.NoError(b, err)
require.NotNil(b, actual)
Expand Down
Loading

0 comments on commit e37d71e

Please sign in to comment.