From 4cb4199e3bd8d8e0eb06b4ab44a84ec5fa8405a7 Mon Sep 17 00:00:00 2001 From: Denis O Date: Thu, 5 Dec 2024 17:03:10 +0000 Subject: [PATCH 01/59] Unit struct update --- config/unit.go | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 config/unit.go diff --git a/config/unit.go b/config/unit.go new file mode 100644 index 0000000000..b769074450 --- /dev/null +++ b/config/unit.go @@ -0,0 +1,8 @@ +package config + +// Unit represents a list of units. +type Unit struct { + Name string `cty:"name" hcl:",label"` + Source string `hcl:"source,attr" cty:"source"` + Path string `hcl:"source,attr" cty:"source"` +} From b7530ca15e552dcd7d9f31ec42ec82e85fc3dd76 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 6 Dec 2024 19:30:31 +0000 Subject: [PATCH 02/59] Add stack parsing --- config/unit.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/unit.go b/config/unit.go index b769074450..1fce64326a 100644 --- a/config/unit.go +++ b/config/unit.go @@ -1,5 +1,11 @@ package config +// StackConfigFile represents the structure of terragrunt stack file +type StackConfigFile struct { + Locals *terragruntLocal `cty:"locals" hcl:"locals,block"` + Units []*Unit `cty:"unit" hcl:"unit,block"` +} + // Unit represents a list of units. type Unit struct { Name string `cty:"name" hcl:",label"` From 47cd72128c6035825ca52c3632fedff7ff4465c0 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 6 Dec 2024 19:34:48 +0000 Subject: [PATCH 03/59] Add cty serialization --- config/unit.go | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/config/unit.go b/config/unit.go index 1fce64326a..078e5b0a78 100644 --- a/config/unit.go +++ b/config/unit.go @@ -1,6 +1,13 @@ package config -// StackConfigFile represents the structure of terragrunt stack file +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" +) + +// StackConfigFile represents the structure of terragrunt.stack.hcl stack file type StackConfigFile struct { Locals *terragruntLocal `cty:"locals" hcl:"locals,block"` Units []*Unit `cty:"unit" hcl:"unit,block"` @@ -12,3 +19,27 @@ type Unit struct { Source string `hcl:"source,attr" cty:"source"` Path string `hcl:"source,attr" cty:"source"` } + +// ToCtyValue converts StackConfigFile to cty.Value +func (s *StackConfigFile) ToCtyValue() (cty.Value, error) { + return gocty.ToCtyValue(s, cty.Object(map[string]cty.Type{ + "locals": cty.Object(map[string]cty.Type{ + // Define locals structure here + }), + "unit": cty.List(cty.Object(map[string]cty.Type{ + "name": cty.String, + "source": cty.String, + "path": cty.String, + })), + })) +} + +// FromCtyValue converts cty.Value back to StackConfigFile +func FromCtyValue(v cty.Value) (*StackConfigFile, error) { + var config StackConfigFile + err := gocty.FromCtyValue(v, &config) + if err != nil { + return nil, fmt.Errorf("failed to decode cty value: %w", err) + } + return &config, nil +} From 46414209826039bf0790e092b4416b95b0ac6604 Mon Sep 17 00:00:00 2001 From: Denis O Date: Mon, 9 Dec 2024 20:50:02 +0000 Subject: [PATCH 04/59] stock go --- config/stack.go | 7 +++++++ config/unit.go | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 config/stack.go diff --git a/config/stack.go b/config/stack.go new file mode 100644 index 0000000000..ebed88bbfc --- /dev/null +++ b/config/stack.go @@ -0,0 +1,7 @@ +package config + +// StackConfigFile represents the structure of terragrunt.stack.hcl stack file +type StackConfigFile struct { + Locals *terragruntLocal `cty:"locals" hcl:"locals,block"` + Units []*Unit `cty:"unit" hcl:"unit,block"` +} diff --git a/config/unit.go b/config/unit.go index 078e5b0a78..38eea40925 100644 --- a/config/unit.go +++ b/config/unit.go @@ -7,12 +7,6 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ) -// StackConfigFile represents the structure of terragrunt.stack.hcl stack file -type StackConfigFile struct { - Locals *terragruntLocal `cty:"locals" hcl:"locals,block"` - Units []*Unit `cty:"unit" hcl:"unit,block"` -} - // Unit represents a list of units. type Unit struct { Name string `cty:"name" hcl:",label"` From 97ec9e5155ba66ca0c19d1f73f0a5c9ff9de860d Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 10 Dec 2024 19:01:22 +0000 Subject: [PATCH 05/59] Stack config path --- config/config.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index 746d969dc1..f72b491a6c 100644 --- a/config/config.go +++ b/config/config.go @@ -36,9 +36,10 @@ import ( ) const ( - DefaultTerragruntConfigPath = "terragrunt.hcl" - DefaultTerragruntJSONConfigPath = "terragrunt.hcl.json" - FoundInFile = "found_in_file" + DefaultTerragruntConfigPath = "terragrunt.hcl" + DefaultTerragruntStackConfigPath = "terragrunt.stack.hcl" + DefaultTerragruntJSONConfigPath = "terragrunt.hcl.json" + FoundInFile = "found_in_file" iamRoleCacheName = "iamRoleCache" From 7c9f8bd5a2af5d6c7fd85262dccd8117ba56abfe Mon Sep 17 00:00:00 2001 From: Denis O Date: Mon, 16 Dec 2024 21:31:46 +0000 Subject: [PATCH 06/59] unit struct clone --- config/unit.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config/unit.go b/config/unit.go index 38eea40925..eccf98be09 100644 --- a/config/unit.go +++ b/config/unit.go @@ -37,3 +37,15 @@ func FromCtyValue(v cty.Value) (*StackConfigFile, error) { } return &config, nil } + +// Clone creates a deep copy of Unit. +func (u *Unit) Clone() *Unit { + if u == nil { + return nil + } + return &Unit{ + Name: u.Name, + Source: u.Source, + Path: u.Path, + } +} From 8928af0fd9df6bd03c20f482efcdec64239b20dd Mon Sep 17 00:00:00 2001 From: Denis O Date: Mon, 16 Dec 2024 21:50:24 +0000 Subject: [PATCH 07/59] stack cli --- cli/commands/stack/action.go | 1 + cli/commands/stack/command.go | 1 + 2 files changed, 2 insertions(+) create mode 100644 cli/commands/stack/action.go create mode 100644 cli/commands/stack/command.go diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go new file mode 100644 index 0000000000..91312a0c02 --- /dev/null +++ b/cli/commands/stack/action.go @@ -0,0 +1 @@ +package stack diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go new file mode 100644 index 0000000000..91312a0c02 --- /dev/null +++ b/cli/commands/stack/command.go @@ -0,0 +1 @@ +package stack From bd6139392454226a9b29bb61fd973dd0df59cf08 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 18 Dec 2024 14:51:18 +0000 Subject: [PATCH 08/59] stack cli update --- cli/commands/stack/action.go | 15 +++++++++++++++ cli/commands/stack/command.go | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 91312a0c02..11c4dfab5f 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -1 +1,16 @@ package stack + +import ( + "context" + + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/options" +) + +func Run(ctx context.Context, opts *options.TerragruntOptions) error { + if opts.TerraformCommand == "" { + return errors.New(MissingCommand{}) + } + + return runTerraform(ctx, opts, new(Target)) +} diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go index 91312a0c02..69b0649451 100644 --- a/cli/commands/stack/command.go +++ b/cli/commands/stack/command.go @@ -1 +1,5 @@ package stack + +const ( + CommandName = "stack" +) From fd83fb42b34613599d6b93d8160d80808ff5a591 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 18 Dec 2024 15:00:21 +0000 Subject: [PATCH 09/59] Stack cli commands --- cli/commands/stack/command.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go index 69b0649451..757dfc69a9 100644 --- a/cli/commands/stack/command.go +++ b/cli/commands/stack/command.go @@ -1,5 +1,27 @@ package stack +import ( + "github.com/gruntwork-io/terragrunt/options" + "github.com/gruntwork-io/terragrunt/pkg/cli" +) + const ( CommandName = "stack" ) + +func NewFlags(opts *options.TerragruntOptions) cli.Flags { + + return nil +} + +func NewCommand(opts *options.TerragruntOptions) *cli.Command { + return &cli.Command{ + Name: CommandName, + Usage: "Terragrunt stack commands.", + DisallowUndefinedFlags: true, + Flags: NewFlags(opts).Sort(), + Action: func(ctx *cli.Context) error { + return Run(ctx.Context, opts.OptionsFromContext(ctx)) + }, + } +} From 14d20882f64b8bc4442289e24c3669f5b07bf83b Mon Sep 17 00:00:00 2001 From: Denis O Date: Thu, 19 Dec 2024 13:36:57 +0000 Subject: [PATCH 10/59] stack update --- cli/app.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/app.go b/cli/app.go index abf639173c..51c11f65d6 100644 --- a/cli/app.go +++ b/cli/app.go @@ -8,6 +8,8 @@ import ( "path/filepath" "sort" + "github.com/gruntwork-io/terragrunt/cli/commands/stack" + "github.com/gruntwork-io/terragrunt/engine" "github.com/gruntwork-io/terragrunt/internal/os/exec" "github.com/gruntwork-io/terragrunt/internal/os/signal" @@ -154,6 +156,7 @@ func TerragruntCommands(opts *options.TerragruntOptions) cli.Commands { outputmodulegroups.NewCommand(opts), // output-module-groups catalog.NewCommand(opts), // catalog scaffold.NewCommand(opts), // scaffold + stack.NewCommand(opts), // stack graph.NewCommand(opts), // graph hclvalidate.NewCommand(opts), // hclvalidate } From 472aeb99cecef621ffce6d8c3e9394b53d4e2bf9 Mon Sep 17 00:00:00 2001 From: Denis O Date: Thu, 19 Dec 2024 13:49:34 +0000 Subject: [PATCH 11/59] action update --- cli/commands/stack/action.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 11c4dfab5f..9a7e9712a6 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -9,8 +9,8 @@ import ( func Run(ctx context.Context, opts *options.TerragruntOptions) error { if opts.TerraformCommand == "" { - return errors.New(MissingCommand{}) + return errors.New("No terraform command specified") } - return runTerraform(ctx, opts, new(Target)) + return Run(ctx, opts) } From def9b27890206f15c52c425296c338e7966bc91f Mon Sep 17 00:00:00 2001 From: Denis O Date: Thu, 19 Dec 2024 13:53:44 +0000 Subject: [PATCH 12/59] Add cli flags --- cli/commands/stack/command.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go index 757dfc69a9..4b487af0dc 100644 --- a/cli/commands/stack/command.go +++ b/cli/commands/stack/command.go @@ -1,6 +1,7 @@ package stack import ( + "github.com/gruntwork-io/terragrunt/cli/commands" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/cli" ) @@ -10,8 +11,10 @@ const ( ) func NewFlags(opts *options.TerragruntOptions) cli.Flags { - - return nil + return cli.Flags{ + commands.NewNoIncludeRootFlag(opts), + commands.NewRootFileNameFlag(opts), + } } func NewCommand(opts *options.TerragruntOptions) *cli.Command { From f32fb864b561e3609e85b0875d3fd3e5a63733d2 Mon Sep 17 00:00:00 2001 From: Denis O Date: Thu, 19 Dec 2024 14:11:12 +0000 Subject: [PATCH 13/59] stack command --- cli/commands/stack/action.go | 6 +++--- cli/commands/stack/command.go | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 9a7e9712a6..3dfce0a289 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -7,10 +7,10 @@ import ( "github.com/gruntwork-io/terragrunt/options" ) -func Run(ctx context.Context, opts *options.TerragruntOptions) error { - if opts.TerraformCommand == "" { +func Run(ctx context.Context, opts *options.TerragruntOptions, command string) error { + if command == "" { return errors.New("No terraform command specified") } - return Run(ctx, opts) + return nil } diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go index 4b487af0dc..5913f0e335 100644 --- a/cli/commands/stack/command.go +++ b/cli/commands/stack/command.go @@ -24,7 +24,8 @@ func NewCommand(opts *options.TerragruntOptions) *cli.Command { DisallowUndefinedFlags: true, Flags: NewFlags(opts).Sort(), Action: func(ctx *cli.Context) error { - return Run(ctx.Context, opts.OptionsFromContext(ctx)) + command := ctx.Args().Get(0) + return Run(ctx.Context, opts.OptionsFromContext(ctx), command) }, } } From 46f57c2470be22214f95f4ddfb9f30c74b6d1c8a Mon Sep 17 00:00:00 2001 From: Denis O Date: Thu, 19 Dec 2024 14:24:13 +0000 Subject: [PATCH 14/59] tg generate --- cli/commands/stack/action.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 3dfce0a289..11133ad376 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -7,10 +7,26 @@ import ( "github.com/gruntwork-io/terragrunt/options" ) -func Run(ctx context.Context, opts *options.TerragruntOptions, command string) error { - if command == "" { - return errors.New("No terraform command specified") +const ( + generate = "generate" +) + +func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string) error { + if subCommand == "" { + return errors.New("No subCommand specified") + } + + switch subCommand { + case generate: + { + return generateStack(ctx, opts) + } } return nil } + +func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { + + return nil +} From 65dc02d8f1267a152da7f7b39badd0babf1efd56 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 20 Dec 2024 17:27:02 +0000 Subject: [PATCH 15/59] Config parse update --- cli/commands/stack/command.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go index 5913f0e335..a5ba4809f4 100644 --- a/cli/commands/stack/command.go +++ b/cli/commands/stack/command.go @@ -1,7 +1,6 @@ package stack import ( - "github.com/gruntwork-io/terragrunt/cli/commands" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/cli" ) @@ -11,10 +10,7 @@ const ( ) func NewFlags(opts *options.TerragruntOptions) cli.Flags { - return cli.Flags{ - commands.NewNoIncludeRootFlag(opts), - commands.NewRootFileNameFlag(opts), - } + return cli.Flags{} } func NewCommand(opts *options.TerragruntOptions) *cli.Command { From bf26ab23abe5656793d07d01d9bd488a5d0aa3a1 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 20 Dec 2024 17:32:20 +0000 Subject: [PATCH 16/59] stacks parsing --- .../stacks/basic/terragrunt.stack.hcl | 20 +++++++++++++++++++ .../stacks/basic/units/chick/terragrunt.hcl | 0 .../stacks/basic/units/chicken/terragrunt.hcl | 0 .../stacks/basic/units/father/terragrunt.hcl | 0 .../stacks/basic/units/mother/terragrunt.hcl | 0 5 files changed, 20 insertions(+) create mode 100644 test/fixtures/stacks/basic/terragrunt.stack.hcl create mode 100644 test/fixtures/stacks/basic/units/chick/terragrunt.hcl create mode 100644 test/fixtures/stacks/basic/units/chicken/terragrunt.hcl create mode 100644 test/fixtures/stacks/basic/units/father/terragrunt.hcl create mode 100644 test/fixtures/stacks/basic/units/mother/terragrunt.hcl diff --git a/test/fixtures/stacks/basic/terragrunt.stack.hcl b/test/fixtures/stacks/basic/terragrunt.stack.hcl new file mode 100644 index 0000000000..8b63f53ef7 --- /dev/null +++ b/test/fixtures/stacks/basic/terragrunt.stack.hcl @@ -0,0 +1,20 @@ +unit "mother" { + source = "units/chicken" + path = "mother" +} + +unit "father" { + source = "units/chicken" + path = "father" +} + +unit "chick_1" { + source = "units/chick" + path = "chicks/chick-1" +} + +unit "chick_2" { + source = "units/chick" + path = "chicks/chick-2" +} + diff --git a/test/fixtures/stacks/basic/units/chick/terragrunt.hcl b/test/fixtures/stacks/basic/units/chick/terragrunt.hcl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/stacks/basic/units/chicken/terragrunt.hcl b/test/fixtures/stacks/basic/units/chicken/terragrunt.hcl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/stacks/basic/units/father/terragrunt.hcl b/test/fixtures/stacks/basic/units/father/terragrunt.hcl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/stacks/basic/units/mother/terragrunt.hcl b/test/fixtures/stacks/basic/units/mother/terragrunt.hcl new file mode 100644 index 0000000000..e69de29bb2 From 231303f549f3aaa569a6db1528b4f5b8f0c52c3b Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 20 Dec 2024 17:44:42 +0000 Subject: [PATCH 17/59] stack config file parsing --- config/stack.go | 28 ++++++++++++++++++++++++++++ options/options.go | 2 ++ 2 files changed, 30 insertions(+) diff --git a/config/stack.go b/config/stack.go index ebed88bbfc..f002429c3c 100644 --- a/config/stack.go +++ b/config/stack.go @@ -1,7 +1,35 @@ package config +import ( + "context" + + "github.com/gruntwork-io/terragrunt/config/hclparse" + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/options" +) + // StackConfigFile represents the structure of terragrunt.stack.hcl stack file type StackConfigFile struct { Locals *terragruntLocal `cty:"locals" hcl:"locals,block"` Units []*Unit `cty:"unit" hcl:"unit,block"` } + +func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.TerragruntOptions) (*StackConfigFile, error) { + terragruntOptions.Logger.Debugf("Reading Terragrunt stack config file at %s", terragruntOptions.TerragrungStackConfigPath) + + parseCtx := NewParsingContext(ctx, terragruntOptions) + + file, err := hclparse.NewParser(parseCtx.ParserOptions...).ParseFromFile(terragruntOptions.TerragrungStackConfigPath) + if err != nil { + return nil, errors.New(err) + } + + evalParsingContext, err := createTerragruntEvalContext(parseCtx, file.ConfigPath) + + config := &StackConfigFile{} + if err := file.Decode(&config, evalParsingContext); err != nil { + return nil, err + } + + return config, nil +} diff --git a/options/options.go b/options/options.go index 99924fb7dd..39ab638c97 100644 --- a/options/options.go +++ b/options/options.go @@ -82,6 +82,8 @@ type TerragruntOptions struct { // Location of the Terragrunt config file TerragruntConfigPath string + TerragrungStackConfigPath string + // Location of the original Terragrunt config file. This is primarily useful when one Terragrunt config is being // read from another: e.g., if /terraform-code/terragrunt.hcl calls read_terragrunt_config("/foo/bar.hcl"), // and within bar.hcl, you call get_original_terragrunt_dir(), you'll get back /terraform-code. From a532787de1443b6839d49101a690cb29cf3f41d2 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 20 Dec 2024 17:59:07 +0000 Subject: [PATCH 18/59] update terragrunt path --- cli/commands/stack/action.go | 38 +++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 11133ad376..52d3300816 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -2,13 +2,20 @@ package stack import ( "context" + "fmt" + "os" + "path/filepath" + + "github.com/gruntwork-io/terragrunt/config" + getter "github.com/hashicorp/go-getter/v2" "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" ) const ( - generate = "generate" + generate = "generate" + stackCacheDir = ".terragrunt-stack" ) func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string) error { @@ -27,6 +34,35 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string } func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { + //TODO: update stack path + opts.TerragrungStackConfigPath = filepath.Join(opts.WorkingDir, "terragrunt.stack.hcl") + stackFile, err := config.ReadStackConfigFile(ctx, opts) + if err != nil { + return err + } + + if err := processStackFile(ctx, stackFile); err != nil { + return err + } + + return nil +} +func processStackFile(ctx context.Context, stackFile *config.StackConfigFile) error { + if err := os.MkdirAll(stackCacheDir, 0755); err != nil { + return errors.New(fmt.Errorf("failed to create base directory: %w", err)) + } + + for _, unit := range stackFile.Units { + destPath := filepath.Join(stackCacheDir, unit.Path) + + if err := os.MkdirAll(destPath, 0755); err != nil { + return errors.New(fmt.Errorf("failed to create destination directory '%s': %w", destPath, err)) + } + + if _, err := getter.GetAny(ctx, unit.Source, destPath); err != nil { + return errors.New(fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, destPath, err)) + } + } return nil } From ed16d24cc3148a4d774f7a173a927467919fad73 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 20 Dec 2024 18:18:49 +0000 Subject: [PATCH 19/59] action config update --- cli/commands/stack/action.go | 9 +++++---- config/stack.go | 2 +- config/unit.go | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 52d3300816..7db64e2784 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -41,19 +41,20 @@ func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { return err } - if err := processStackFile(ctx, stackFile); err != nil { + if err := processStackFile(ctx, opts, stackFile); err != nil { return err } return nil } -func processStackFile(ctx context.Context, stackFile *config.StackConfigFile) error { - if err := os.MkdirAll(stackCacheDir, 0755); err != nil { +func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stackFile *config.StackConfigFile) error { + baseDir := filepath.Join(opts.WorkingDir, stackCacheDir) + if err := os.MkdirAll(baseDir, 0755); err != nil { return errors.New(fmt.Errorf("failed to create base directory: %w", err)) } for _, unit := range stackFile.Units { - destPath := filepath.Join(stackCacheDir, unit.Path) + destPath := filepath.Join(baseDir, unit.Path) if err := os.MkdirAll(destPath, 0755); err != nil { return errors.New(fmt.Errorf("failed to create destination directory '%s': %w", destPath, err)) diff --git a/config/stack.go b/config/stack.go index f002429c3c..1a2e947936 100644 --- a/config/stack.go +++ b/config/stack.go @@ -27,7 +27,7 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr evalParsingContext, err := createTerragruntEvalContext(parseCtx, file.ConfigPath) config := &StackConfigFile{} - if err := file.Decode(&config, evalParsingContext); err != nil { + if err := file.Decode(config, evalParsingContext); err != nil { return nil, err } diff --git a/config/unit.go b/config/unit.go index eccf98be09..4b555f5169 100644 --- a/config/unit.go +++ b/config/unit.go @@ -11,7 +11,7 @@ import ( type Unit struct { Name string `cty:"name" hcl:",label"` Source string `hcl:"source,attr" cty:"source"` - Path string `hcl:"source,attr" cty:"source"` + Path string `hcl:"path,attr" cty:"path"` } // ToCtyValue converts StackConfigFile to cty.Value From b050d2d22acd6edb15ef74c51497e23f618209d6 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 20 Dec 2024 18:58:07 +0000 Subject: [PATCH 20/59] symbol links --- cli/commands/stack/action.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 7db64e2784..dd98f8b2a0 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -55,13 +55,24 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac for _, unit := range stackFile.Units { destPath := filepath.Join(baseDir, unit.Path) + src := unit.Source + src, err := filepath.Abs(src) + if err != nil { + return errors.New(fmt.Errorf("failed to get absolute path for source '%s': %w", unit.Source, err)) + } + opts.Logger.Infof("Processing unit: %s (%s) to %s", unit.Name, src, destPath) - if err := os.MkdirAll(destPath, 0755); err != nil { - return errors.New(fmt.Errorf("failed to create destination directory '%s': %w", destPath, err)) + req := &getter.Request{ + Src: src, + Dst: destPath, + GetMode: getter.ModeAny, + Copy: true, + DisableSymlinks: true, + Umask: 0755, } - if _, err := getter.GetAny(ctx, unit.Source, destPath); err != nil { - return errors.New(fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, destPath, err)) + if _, err := getter.DefaultClient.Get(ctx, req); err != nil { + return fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, destPath, err) } } From f76ae529a325ebb3ddb158feb5a6b49bd8d1ab89 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 20 Dec 2024 20:10:48 +0000 Subject: [PATCH 21/59] Units parsing --- cli/commands/stack/action.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index dd98f8b2a0..5692a5087b 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -53,25 +53,28 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac return errors.New(fmt.Errorf("failed to create base directory: %w", err)) } + //client := getter.Client{ + // Getters: getter.Getters, + // Decompressors: getter.Decompressors, + // DisableSymlinks: true, + //} + for _, unit := range stackFile.Units { destPath := filepath.Join(baseDir, unit.Path) - src := unit.Source - src, err := filepath.Abs(src) + dest, err := filepath.Abs(destPath) if err != nil { - return errors.New(fmt.Errorf("failed to get absolute path for source '%s': %w", unit.Source, err)) + return errors.New(fmt.Errorf("failed to get absolute path for destination '%s': %w", destPath, err)) } - opts.Logger.Infof("Processing unit: %s (%s) to %s", unit.Name, src, destPath) - req := &getter.Request{ - Src: src, - Dst: destPath, - GetMode: getter.ModeAny, - Copy: true, - DisableSymlinks: true, - Umask: 0755, + src := unit.Source + src, err = filepath.Abs(src) + if err != nil { + opts.Logger.Warnf("failed to get absolute path for source '%s': %v", unit.Source, err) + src = unit.Source } + opts.Logger.Infof("Processing unit: %s (%s) to %s", unit.Name, src, dest) - if _, err := getter.DefaultClient.Get(ctx, req); err != nil { + if _, err := getter.GetAny(ctx, dest, src); err != nil { return fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, destPath, err) } } From 8542090ff724fe7dac9d2a3280700b4bdf55d017 Mon Sep 17 00:00:00 2001 From: Denis O Date: Fri, 20 Dec 2024 21:50:35 +0000 Subject: [PATCH 22/59] Symbol link issues --- cli/commands/stack/action.go | 143 ++++++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 10 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 5692a5087b..9f66d66896 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -3,11 +3,13 @@ package stack import ( "context" "fmt" + "io" + "net/url" "os" "path/filepath" "github.com/gruntwork-io/terragrunt/config" - getter "github.com/hashicorp/go-getter/v2" + getter "github.com/hashicorp/go-getter" "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" @@ -53,17 +55,11 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac return errors.New(fmt.Errorf("failed to create base directory: %w", err)) } - //client := getter.Client{ - // Getters: getter.Getters, - // Decompressors: getter.Decompressors, - // DisableSymlinks: true, - //} - for _, unit := range stackFile.Units { destPath := filepath.Join(baseDir, unit.Path) dest, err := filepath.Abs(destPath) if err != nil { - return errors.New(fmt.Errorf("failed to get absolute path for destination '%s': %w", destPath, err)) + return errors.New(fmt.Errorf("failed to get absolute path for destination '%s': %w", dest, err)) } src := unit.Source @@ -72,10 +68,137 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac opts.Logger.Warnf("failed to get absolute path for source '%s': %v", unit.Source, err) src = unit.Source } + opts.Logger.Infof("Processing unit: %s (%s) to %s", unit.Name, src, dest) - if _, err := getter.GetAny(ctx, dest, src); err != nil { - return fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, destPath, err) + client := &getter.Client{ + Src: src, + Dst: dest, + Mode: getter.ClientModeAny, + Dir: true, + DisableSymlinks: true, + Options: []getter.ClientOption{ + getter.WithInsecure(), + getter.WithContext(ctx), + getter.WithGetters(map[string]getter.Getter{ + "file": &CustomFileProvider{}, + }), + }, + } + if err := client.Get(); err != nil { + return fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, dest, err) + } + } + + return nil +} + +type CustomFileProvider struct { + client *getter.Client +} + +// Get implements downloading functionality +func (p *CustomFileProvider) Get(dst string, u *url.URL) error { + src := u.Path + + // Check if source exists + fi, err := os.Stat(src) + if err != nil { + return err + } + + if fi.IsDir() { + return p.copyDir(src, dst) + } + return p.copyFile(src, dst) +} + +// GetFile implements single file download +func (p *CustomFileProvider) GetFile(dst string, u *url.URL) error { + return p.copyFile(u.Path, dst) +} + +// ClientMode determines if we're getting a directory or single file +func (p *CustomFileProvider) ClientMode(u *url.URL) (getter.ClientMode, error) { + fi, err := os.Stat(u.Path) + if err != nil { + return getter.ClientModeInvalid, err + } + + if fi.IsDir() { + return getter.ClientModeDir, nil + } + return getter.ClientModeFile, nil +} + +// SetClient sets the client for this provider +func (p *CustomFileProvider) SetClient(c *getter.Client) { + p.client = c +} + +func (p *CustomFileProvider) copyFile(src, dst string) error { + // Create destination directory if it doesn't exist + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return fmt.Errorf("failed to create destination directory: %v", err) + } + + // Open source file + srcFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file: %v", err) + } + defer srcFile.Close() + + // Create destination file + dstFile, err := os.Create(dst) + if err != nil { + return fmt.Errorf("failed to create destination file: %v", err) + } + defer dstFile.Close() + + // Copy the contents + if _, err := io.Copy(dstFile, srcFile); err != nil { + return fmt.Errorf("failed to copy file contents: %v", err) + } + + // Copy file mode + srcInfo, err := os.Stat(src) + if err != nil { + return fmt.Errorf("failed to stat source file: %v", err) + } + + return os.Chmod(dst, srcInfo.Mode()) +} + +func (p *CustomFileProvider) copyDir(src, dst string) error { + // Create the destination directory + srcInfo, err := os.Stat(src) + if err != nil { + return fmt.Errorf("failed to stat source directory: %v", err) + } + + if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil { + return fmt.Errorf("failed to create destination directory: %v", err) + } + + // Read directory contents + entries, err := os.ReadDir(src) + if err != nil { + return fmt.Errorf("failed to read source directory: %v", err) + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + if err := p.copyDir(srcPath, dstPath); err != nil { + return err + } + } else { + if err := p.copyFile(srcPath, dstPath); err != nil { + return err + } } } From 6024cc097952b877a34efdea2b3c0f67b3d04845 Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 18:00:35 +0000 Subject: [PATCH 23/59] Add basic stack tests --- cli/commands/stack/action.go | 14 +++++++------- test/integration_stacks_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 test/integration_stacks_test.go diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 9f66d66896..4b2612823f 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -16,13 +16,14 @@ import ( ) const ( - generate = "generate" - stackCacheDir = ".terragrunt-stack" + generate = "generate" + stackCacheDir = ".terragrunt-stack" + defaultStackFile = "terragrunt.stack.hcl" ) func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string) error { if subCommand == "" { - return errors.New("No subCommand specified") + return errors.New("No command specified") } switch subCommand { @@ -36,15 +37,14 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string } func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { - //TODO: update stack path - opts.TerragrungStackConfigPath = filepath.Join(opts.WorkingDir, "terragrunt.stack.hcl") + opts.TerragrungStackConfigPath = filepath.Join(opts.WorkingDir, defaultStackFile) stackFile, err := config.ReadStackConfigFile(ctx, opts) if err != nil { - return err + return errors.New(err) } if err := processStackFile(ctx, opts, stackFile); err != nil { - return err + return errors.New(err) } return nil diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go new file mode 100644 index 0000000000..be86b45d36 --- /dev/null +++ b/test/integration_stacks_test.go @@ -0,0 +1,24 @@ +package test_test + +import ( + "fmt" + "testing" + + "github.com/gruntwork-io/terragrunt/test/helpers" + "github.com/gruntwork-io/terragrunt/util" +) + +const ( + testFixtureStacksBasic = "fixtures/stacks/basic" +) + +func TestStacksGenerateBasic(t *testing.T) { + t.Parallel() + + helpers.CleanupTerraformFolder(t, testFixtureStacksBasic) + tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic) + rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksBasic) + + helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) + +} From 04ae059fa172037df4db2b41bfe71b7e64441a0f Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 18:24:20 +0000 Subject: [PATCH 24/59] Stacks file update --- cli/commands/stack/action.go | 75 +++++++++++++++--------------------- 1 file changed, 31 insertions(+), 44 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 4b2612823f..e6f52941ee 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -63,11 +63,6 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } src := unit.Source - src, err = filepath.Abs(src) - if err != nil { - opts.Logger.Warnf("failed to get absolute path for source '%s': %v", unit.Source, err) - src = unit.Source - } opts.Logger.Infof("Processing unit: %s (%s) to %s", unit.Name, src, dest) @@ -81,30 +76,28 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac getter.WithInsecure(), getter.WithContext(ctx), getter.WithGetters(map[string]getter.Getter{ - "file": &CustomFileProvider{}, + "file": &StacksFileProvider{}, }), }, } if err := client.Get(); err != nil { - return fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, dest, err) + return errors.New(fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, dest, err)) } } return nil } -type CustomFileProvider struct { +type StacksFileProvider struct { client *getter.Client } // Get implements downloading functionality -func (p *CustomFileProvider) Get(dst string, u *url.URL) error { +func (p *StacksFileProvider) Get(dst string, u *url.URL) error { src := u.Path - - // Check if source exists fi, err := os.Stat(src) if err != nil { - return err + return errors.New(fmt.Errorf("source path error: %w", err)) } if fi.IsDir() { @@ -114,15 +107,15 @@ func (p *CustomFileProvider) Get(dst string, u *url.URL) error { } // GetFile implements single file download -func (p *CustomFileProvider) GetFile(dst string, u *url.URL) error { +func (p *StacksFileProvider) GetFile(dst string, u *url.URL) error { return p.copyFile(u.Path, dst) } // ClientMode determines if we're getting a directory or single file -func (p *CustomFileProvider) ClientMode(u *url.URL) (getter.ClientMode, error) { +func (p *StacksFileProvider) ClientMode(u *url.URL) (getter.ClientMode, error) { fi, err := os.Stat(u.Path) if err != nil { - return getter.ClientModeInvalid, err + return getter.ClientModeInvalid, errors.New(err) } if fi.IsDir() { @@ -132,59 +125,52 @@ func (p *CustomFileProvider) ClientMode(u *url.URL) (getter.ClientMode, error) { } // SetClient sets the client for this provider -func (p *CustomFileProvider) SetClient(c *getter.Client) { +func (p *StacksFileProvider) SetClient(c *getter.Client) { p.client = c } -func (p *CustomFileProvider) copyFile(src, dst string) error { - // Create destination directory if it doesn't exist +func (p *StacksFileProvider) copyFile(src, dst string) error { if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { - return fmt.Errorf("failed to create destination directory: %v", err) + return errors.New(err) } - // Open source file srcFile, err := os.Open(src) if err != nil { - return fmt.Errorf("failed to open source file: %v", err) + return errors.New(err) } defer srcFile.Close() - // Create destination file - dstFile, err := os.Create(dst) + srcInfo, err := srcFile.Stat() if err != nil { - return fmt.Errorf("failed to create destination file: %v", err) + return errors.New(err) } - defer dstFile.Close() - // Copy the contents - if _, err := io.Copy(dstFile, srcFile); err != nil { - return fmt.Errorf("failed to copy file contents: %v", err) + dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, srcInfo.Mode()) + if err != nil { + return errors.New(err) } + defer dstFile.Close() - // Copy file mode - srcInfo, err := os.Stat(src) - if err != nil { - return fmt.Errorf("failed to stat source file: %v", err) + if _, err := io.Copy(dstFile, srcFile); err != nil { + return errors.New(err) } - return os.Chmod(dst, srcInfo.Mode()) + return nil } -func (p *CustomFileProvider) copyDir(src, dst string) error { - // Create the destination directory +func (p *StacksFileProvider) copyDir(src, dst string) error { srcInfo, err := os.Stat(src) if err != nil { - return fmt.Errorf("failed to stat source directory: %v", err) + return errors.New(err) } if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil { - return fmt.Errorf("failed to create destination directory: %v", err) + return errors.New(err) } - // Read directory contents entries, err := os.ReadDir(src) if err != nil { - return fmt.Errorf("failed to read source directory: %v", err) + return errors.New(err) } for _, entry := range entries { @@ -193,12 +179,13 @@ func (p *CustomFileProvider) copyDir(src, dst string) error { if entry.IsDir() { if err := p.copyDir(srcPath, dstPath); err != nil { - return err - } - } else { - if err := p.copyFile(srcPath, dstPath); err != nil { - return err + return errors.New(err) } + continue + } + + if err := p.copyFile(srcPath, dstPath); err != nil { + return errors.New(err) } } From 4c1b80ab3ac69ba2b1b7d19b2e002a4bb2887bc5 Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 19:13:56 +0000 Subject: [PATCH 25/59] Local path detection --- cli/commands/stack/action.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index e6f52941ee..d72747d7f3 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -63,7 +63,15 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } src := unit.Source - + // set absolute path for source if it's not an absolute path or URL + if !filepath.IsAbs(unit.Source) && !isURL(unit.Source) { + src = filepath.Join(opts.WorkingDir, unit.Source) + src, err = filepath.Abs(src) + if err != nil { + opts.Logger.Warnf("failed to get absolute path for source '%s': %v", unit.Source, err) + src = unit.Source + } + } opts.Logger.Infof("Processing unit: %s (%s) to %s", unit.Name, src, dest) client := &getter.Client{ @@ -88,6 +96,11 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac return nil } +func isURL(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} + type StacksFileProvider struct { client *getter.Client } From 1fa7a7a64170c48d7c15c645bb148a036cbe45a1 Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 19:22:21 +0000 Subject: [PATCH 26/59] Update setting getters --- cli/commands/stack/action.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index d72747d7f3..da55fe9848 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -83,11 +83,20 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac Options: []getter.ClientOption{ getter.WithInsecure(), getter.WithContext(ctx), - getter.WithGetters(map[string]getter.Getter{ - "file": &StacksFileProvider{}, - }), }, } + + // setting custom getters + client.Getters = map[string]getter.Getter{} + for getterName, getterValue := range getter.Getters { + // setting custom getter for file to not use symlinks + if getterName == "file" { + client.Getters[getterName] = &StacksFileProvider{} + } else { + client.Getters[getterName] = getterValue + } + } + if err := client.Get(); err != nil { return errors.New(fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, dest, err)) } From 8eb6e1276d8dbd592be8c5fee8ec1327b306c60f Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 19:26:40 +0000 Subject: [PATCH 27/59] Add locals evaluation --- .../stacks/locals/terragrunt.stack.hcl | 25 +++++++++++++++++++ .../stacks/locals/units/chick/terragrunt.hcl | 0 .../locals/units/chicken/terragrunt.hcl | 0 .../stacks/locals/units/father/terragrunt.hcl | 0 .../stacks/locals/units/mother/terragrunt.hcl | 0 test/integration_stacks_test.go | 12 ++++++++- 6 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/stacks/locals/terragrunt.stack.hcl create mode 100644 test/fixtures/stacks/locals/units/chick/terragrunt.hcl create mode 100644 test/fixtures/stacks/locals/units/chicken/terragrunt.hcl create mode 100644 test/fixtures/stacks/locals/units/father/terragrunt.hcl create mode 100644 test/fixtures/stacks/locals/units/mother/terragrunt.hcl diff --git a/test/fixtures/stacks/locals/terragrunt.stack.hcl b/test/fixtures/stacks/locals/terragrunt.stack.hcl new file mode 100644 index 0000000000..e5175d0e98 --- /dev/null +++ b/test/fixtures/stacks/locals/terragrunt.stack.hcl @@ -0,0 +1,25 @@ +locals { + chicken = "units/chicken" + chick = "units/chick" +} + +unit "mother" { + source = local.chicken + path = "mother" +} + +unit "father" { + source = local.chicken + path = "father" +} + +unit "chick_1" { + source = local.chick + path = "chicks/chick-1" +} + +unit "chick_2" { + source = local.chick + path = "chicks/chick-2" +} + diff --git a/test/fixtures/stacks/locals/units/chick/terragrunt.hcl b/test/fixtures/stacks/locals/units/chick/terragrunt.hcl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/stacks/locals/units/chicken/terragrunt.hcl b/test/fixtures/stacks/locals/units/chicken/terragrunt.hcl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/stacks/locals/units/father/terragrunt.hcl b/test/fixtures/stacks/locals/units/father/terragrunt.hcl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/stacks/locals/units/mother/terragrunt.hcl b/test/fixtures/stacks/locals/units/mother/terragrunt.hcl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go index be86b45d36..7519f7a25d 100644 --- a/test/integration_stacks_test.go +++ b/test/integration_stacks_test.go @@ -9,7 +9,8 @@ import ( ) const ( - testFixtureStacksBasic = "fixtures/stacks/basic" + testFixtureStacksBasic = "fixtures/stacks/basic" + testFixtureStacksLocals = "fixtures/stacks/locals" ) func TestStacksGenerateBasic(t *testing.T) { @@ -20,5 +21,14 @@ func TestStacksGenerateBasic(t *testing.T) { rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksBasic) helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) +} + +func TestStacksGenerateLocals(t *testing.T) { + t.Parallel() + helpers.CleanupTerraformFolder(t, testFixtureStacksLocals) + tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocals) + rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksLocals) + + helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) } From d411aa4e590a9c2ebd23b9768becde1173a3e68a Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 19:39:37 +0000 Subject: [PATCH 28/59] Stack local evaluation --- config/stack.go | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/config/stack.go b/config/stack.go index 1a2e947936..73dc90f07c 100644 --- a/config/stack.go +++ b/config/stack.go @@ -3,6 +3,8 @@ package config import ( "context" + "github.com/zclconf/go-cty/cty" + "github.com/gruntwork-io/terragrunt/config/hclparse" "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" @@ -24,8 +26,44 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr return nil, errors.New(err) } - evalParsingContext, err := createTerragruntEvalContext(parseCtx, file.ConfigPath) + localsBlock, err := file.Blocks(MetadataLocals, false) + if err != nil { + return nil, err + } + attrs, err := localsBlock[0].JustAttributes() + if err != nil { + return nil, err + } + evaluatedLocals := map[string]cty.Value{} + evaluated := true + for iterations := 0; len(attrs) > 0 && evaluated; iterations++ { + if iterations > MaxIter { + // Reached maximum supported iterations, which is most likely an infinite loop bug so cut the iteration + // short an return an error. + return nil, errors.New(MaxIterError{}) + } + + var err error + attrs, evaluatedLocals, evaluated, err = attemptEvaluateLocals( + parseCtx, + file, + attrs, + evaluatedLocals, + ) + + if err != nil { + parseCtx.TerragruntOptions.Logger.Debugf("Encountered error while evaluating locals in file %s", terragruntOptions.TerragrungStackConfigPath) + return nil, err + } + } + localsAsCtyVal, err := convertValuesMapToCtyVal(evaluatedLocals) + if err != nil { + return nil, err + } + parseCtx.Locals = &localsAsCtyVal + + evalParsingContext, err := createTerragruntEvalContext(parseCtx, file.ConfigPath) config := &StackConfigFile{} if err := file.Decode(config, evalParsingContext); err != nil { return nil, err From c342600367a09f1f417420ccb339c08a17ac48e7 Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 19:57:47 +0000 Subject: [PATCH 29/59] Stacks remote test --- cli/commands/stack/action.go | 4 ++++ test/fixtures/stacks/remote/terragrunt.stack.hcl | 14 ++++++++++++++ test/integration_stacks_test.go | 11 +++++++++++ 3 files changed, 29 insertions(+) create mode 100644 test/fixtures/stacks/remote/terragrunt.stack.hcl diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index da55fe9848..d38868ec4a 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -7,6 +7,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "github.com/gruntwork-io/terragrunt/config" getter "github.com/hashicorp/go-getter" @@ -106,6 +107,9 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } func isURL(str string) bool { + if strings.Contains(str, "//") { + return true + } u, err := url.Parse(str) return err == nil && u.Scheme != "" && u.Host != "" } diff --git a/test/fixtures/stacks/remote/terragrunt.stack.hcl b/test/fixtures/stacks/remote/terragrunt.stack.hcl new file mode 100644 index 0000000000..df6c391a0d --- /dev/null +++ b/test/fixtures/stacks/remote/terragrunt.stack.hcl @@ -0,0 +1,14 @@ +locals { + version = "v0.68.4" +} + +unit "app1" { + source = "github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=${local.version}" + path = "app1" +} + +unit "app2" { + source = "github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=${local.version}" + path = "app2" +} + diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go index 7519f7a25d..7a2968d13d 100644 --- a/test/integration_stacks_test.go +++ b/test/integration_stacks_test.go @@ -11,6 +11,7 @@ import ( const ( testFixtureStacksBasic = "fixtures/stacks/basic" testFixtureStacksLocals = "fixtures/stacks/locals" + testFixtureStacksRemote = "fixtures/stacks/remote" ) func TestStacksGenerateBasic(t *testing.T) { @@ -32,3 +33,13 @@ func TestStacksGenerateLocals(t *testing.T) { helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) } + +func TestStacksGenerateRemote(t *testing.T) { + t.Parallel() + + helpers.CleanupTerraformFolder(t, testFixtureStacksRemote) + tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksRemote) + rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksRemote) + + helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) +} From 0da61dad4d9b0303d9d3f3136965434a0a193026 Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 20:07:15 +0000 Subject: [PATCH 30/59] Stack generate --- cli/commands/stack/action.go | 2 +- config/stack.go | 38 +++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index d38868ec4a..d39ba1376a 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -99,7 +99,7 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } if err := client.Get(); err != nil { - return errors.New(fmt.Errorf("failed to fetch source '%s' to destination '%s': %w", unit.Source, dest, err)) + return errors.New(err) } } diff --git a/config/stack.go b/config/stack.go index 73dc90f07c..e0ccd48128 100644 --- a/config/stack.go +++ b/config/stack.go @@ -26,13 +26,30 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr return nil, errors.New(err) } + if err := processLocals(terragruntOptions, parseCtx, file); err != nil { + return nil, errors.New(err) + } + + evalParsingContext, err := createTerragruntEvalContext(parseCtx, file.ConfigPath) + config := &StackConfigFile{} + if err := file.Decode(config, evalParsingContext); err != nil { + return nil, err + } + + return config, nil +} + +func processLocals(terragruntOptions *options.TerragruntOptions, parseCtx *ParsingContext, file *hclparse.File) error { localsBlock, err := file.Blocks(MetadataLocals, false) if err != nil { - return nil, err + return errors.New(err) + } + if len(localsBlock) == 0 { + return nil } attrs, err := localsBlock[0].JustAttributes() if err != nil { - return nil, err + return errors.New(err) } evaluatedLocals := map[string]cty.Value{} evaluated := true @@ -41,7 +58,7 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr if iterations > MaxIter { // Reached maximum supported iterations, which is most likely an infinite loop bug so cut the iteration // short an return an error. - return nil, errors.New(MaxIterError{}) + return errors.New(MaxIterError{}) } var err error @@ -53,21 +70,14 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr ) if err != nil { - parseCtx.TerragruntOptions.Logger.Debugf("Encountered error while evaluating locals in file %s", terragruntOptions.TerragrungStackConfigPath) - return nil, err + terragruntOptions.Logger.Debugf("Encountered error while evaluating locals in file %s", terragruntOptions.TerragrungStackConfigPath) + return errors.New(err) } } localsAsCtyVal, err := convertValuesMapToCtyVal(evaluatedLocals) if err != nil { - return nil, err + return errors.New(err) } parseCtx.Locals = &localsAsCtyVal - - evalParsingContext, err := createTerragruntEvalContext(parseCtx, file.ConfigPath) - config := &StackConfigFile{} - if err := file.Decode(config, evalParsingContext); err != nil { - return nil, err - } - - return config, nil + return nil } From cb1800b9916ffa6ef3af04c9c7571bf025709990 Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 20:17:52 +0000 Subject: [PATCH 31/59] Add basic stack generate cli --- docs/_docs/04_reference/cli-options.md | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index 12dcfad713..49474cd5b9 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -32,6 +32,7 @@ This page documents the CLI commands and options available with Terragrunt: - [scaffold](#scaffold) - [catalog](#catalog) - [graph](#graph) + - [stack](#stack) - [CLI options](#cli-options) - [terragrunt-check](#terragrunt-check) - [terragrunt-config](#terragrunt-config) @@ -741,6 +742,44 @@ Notes: - destroy will be executed only on subset of services dependent from `eks-service-3` +### stack + +The `terragrunt stack` commands provide a new interface for managing collections of Terragrunt units defined in `terragrunt.stack.hcl` files. +These commands simplify the process of handling multiple infrastructure units by grouping them into a "stack," reducing code duplication and streamlining operations across environments. + +The `terragrunt stack generate` command is used to generate a stack of Terragrunt `hcl` files based on the configuration provided in the `terragrunt.stack.hcl` file. + +### Example +Given the following `terragrunt.stack.hcl` configuration: +```hcl +locals { + version = "v0.68.4" +} + +unit "app1" { + source = "github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=${local.version}" + path = "app1" +} + +unit "app2" { + source = "github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=${local.version}" + path = "app2" +} + +``` +Running: +```bash +terragrunt stack generate +``` +Will create the following directory structure: +``` +.terragrunt-stack/ +├── app1/ +│ └── terragrunt.hcl +└── app2/ + └── terragrunt.hcl +``` + ## CLI options Terragrunt forwards all options to OpenTofu/Terraform. The only exceptions are `--version` and arguments that start with the From 4e9372f0c8fa4b0bf0e383f494b90fdf1c8f71b0 Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 20:31:40 +0000 Subject: [PATCH 32/59] Cleanup --- cli/commands/stack/action.go | 13 ++++++------- config/stack.go | 10 +++++----- test/integration_stacks_test.go | 6 +++--- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index d39ba1376a..73ee917a60 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -27,11 +27,8 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string return errors.New("No command specified") } - switch subCommand { - case generate: - { - return generateStack(ctx, opts) - } + if subCommand == generate { + return generateStack(ctx, opts) } return nil @@ -52,7 +49,8 @@ func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { } func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stackFile *config.StackConfigFile) error { baseDir := filepath.Join(opts.WorkingDir, stackCacheDir) - if err := os.MkdirAll(baseDir, 0755); err != nil { + const dirPerm = 0755 + if err := os.MkdirAll(baseDir, dirPerm); err != nil { return errors.New(fmt.Errorf("failed to create base directory: %w", err)) } @@ -156,7 +154,8 @@ func (p *StacksFileProvider) SetClient(c *getter.Client) { } func (p *StacksFileProvider) copyFile(src, dst string) error { - if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + const dirPerm = 0755 + if err := os.MkdirAll(filepath.Dir(dst), dirPerm); err != nil { return errors.New(err) } diff --git a/config/stack.go b/config/stack.go index e0ccd48128..24ff037a8d 100644 --- a/config/stack.go +++ b/config/stack.go @@ -19,18 +19,18 @@ type StackConfigFile struct { func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.TerragruntOptions) (*StackConfigFile, error) { terragruntOptions.Logger.Debugf("Reading Terragrunt stack config file at %s", terragruntOptions.TerragrungStackConfigPath) - parseCtx := NewParsingContext(ctx, terragruntOptions) + parser := NewParsingContext(ctx, terragruntOptions) - file, err := hclparse.NewParser(parseCtx.ParserOptions...).ParseFromFile(terragruntOptions.TerragrungStackConfigPath) + file, err := hclparse.NewParser(parser.ParserOptions...).ParseFromFile(terragruntOptions.TerragrungStackConfigPath) if err != nil { return nil, errors.New(err) } - if err := processLocals(terragruntOptions, parseCtx, file); err != nil { + if err := processLocals(parser, terragruntOptions, file); err != nil { return nil, errors.New(err) } - evalParsingContext, err := createTerragruntEvalContext(parseCtx, file.ConfigPath) + evalParsingContext, err := createTerragruntEvalContext(parser, file.ConfigPath) config := &StackConfigFile{} if err := file.Decode(config, evalParsingContext); err != nil { return nil, err @@ -39,7 +39,7 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr return config, nil } -func processLocals(terragruntOptions *options.TerragruntOptions, parseCtx *ParsingContext, file *hclparse.File) error { +func processLocals(parseCtx *ParsingContext, terragruntOptions *options.TerragruntOptions, file *hclparse.File) error { localsBlock, err := file.Blocks(MetadataLocals, false) if err != nil { return errors.New(err) diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go index 7a2968d13d..fdbfb38591 100644 --- a/test/integration_stacks_test.go +++ b/test/integration_stacks_test.go @@ -21,7 +21,7 @@ func TestStacksGenerateBasic(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksBasic) - helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) + helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-working-dir %s", rootPath)) } func TestStacksGenerateLocals(t *testing.T) { @@ -31,7 +31,7 @@ func TestStacksGenerateLocals(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocals) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksLocals) - helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) + helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-working-dir %s", rootPath)) } func TestStacksGenerateRemote(t *testing.T) { @@ -41,5 +41,5 @@ func TestStacksGenerateRemote(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksRemote) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksRemote) - helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) + helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-working-dir %s", rootPath)) } From a2b2186ee9149b7b46ad2dceca9e2d105243f836 Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 20:33:31 +0000 Subject: [PATCH 33/59] Markdown link --- docs/_docs/04_reference/cli-options.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index 49474cd5b9..04252795c6 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -749,8 +749,8 @@ These commands simplify the process of handling multiple infrastructure units by The `terragrunt stack generate` command is used to generate a stack of Terragrunt `hcl` files based on the configuration provided in the `terragrunt.stack.hcl` file. -### Example Given the following `terragrunt.stack.hcl` configuration: + ```hcl locals { version = "v0.68.4" @@ -767,11 +767,15 @@ unit "app2" { } ``` + Running: + ```bash terragrunt stack generate ``` + Will create the following directory structure: + ``` .terragrunt-stack/ ├── app1/ From 0d52b1e4b91003525c376aab396ee47904643e5a Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 20:35:16 +0000 Subject: [PATCH 34/59] tree update --- docs/_docs/04_reference/cli-options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index 04252795c6..aa05198611 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -776,7 +776,7 @@ terragrunt stack generate Will create the following directory structure: -``` +```tree .terragrunt-stack/ ├── app1/ │ └── terragrunt.hcl From fc61aa1d4c1758af1da10edf2677e2ed5fdb4b0c Mon Sep 17 00:00:00 2001 From: Denis O Date: Tue, 7 Jan 2025 20:50:56 +0000 Subject: [PATCH 35/59] stack generate cleanup --- cli/commands/stack/action.go | 14 ++++++++++++-- cli/commands/stack/command.go | 1 + config/stack.go | 26 ++++++++++++++++++++------ config/unit.go | 3 +++ test/integration_stacks_test.go | 7 +++---- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 73ee917a60..f2fe1bd3c9 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -20,6 +20,7 @@ const ( generate = "generate" stackCacheDir = ".terragrunt-stack" defaultStackFile = "terragrunt.stack.hcl" + dirPerm = 0755 ) func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string) error { @@ -37,6 +38,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { opts.TerragrungStackConfigPath = filepath.Join(opts.WorkingDir, defaultStackFile) stackFile, err := config.ReadStackConfigFile(ctx, opts) + if err != nil { return errors.New(err) } @@ -49,7 +51,6 @@ func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { } func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stackFile *config.StackConfigFile) error { baseDir := filepath.Join(opts.WorkingDir, stackCacheDir) - const dirPerm = 0755 if err := os.MkdirAll(baseDir, dirPerm); err != nil { return errors.New(fmt.Errorf("failed to create base directory: %w", err)) } @@ -57,6 +58,7 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac for _, unit := range stackFile.Units { destPath := filepath.Join(baseDir, unit.Path) dest, err := filepath.Abs(destPath) + if err != nil { return errors.New(fmt.Errorf("failed to get absolute path for destination '%s': %w", dest, err)) } @@ -66,11 +68,13 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac if !filepath.IsAbs(unit.Source) && !isURL(unit.Source) { src = filepath.Join(opts.WorkingDir, unit.Source) src, err = filepath.Abs(src) + if err != nil { opts.Logger.Warnf("failed to get absolute path for source '%s': %v", unit.Source, err) src = unit.Source } } + opts.Logger.Infof("Processing unit: %s (%s) to %s", unit.Name, src, dest) client := &getter.Client{ @@ -87,6 +91,7 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac // setting custom getters client.Getters = map[string]getter.Getter{} + for getterName, getterValue := range getter.Getters { // setting custom getter for file to not use symlinks if getterName == "file" { @@ -108,7 +113,9 @@ func isURL(str string) bool { if strings.Contains(str, "//") { return true } + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" } @@ -120,6 +127,7 @@ type StacksFileProvider struct { func (p *StacksFileProvider) Get(dst string, u *url.URL) error { src := u.Path fi, err := os.Stat(src) + if err != nil { return errors.New(fmt.Errorf("source path error: %w", err)) } @@ -127,6 +135,7 @@ func (p *StacksFileProvider) Get(dst string, u *url.URL) error { if fi.IsDir() { return p.copyDir(src, dst) } + return p.copyFile(src, dst) } @@ -145,6 +154,7 @@ func (p *StacksFileProvider) ClientMode(u *url.URL) (getter.ClientMode, error) { if fi.IsDir() { return getter.ClientModeDir, nil } + return getter.ClientModeFile, nil } @@ -154,7 +164,6 @@ func (p *StacksFileProvider) SetClient(c *getter.Client) { } func (p *StacksFileProvider) copyFile(src, dst string) error { - const dirPerm = 0755 if err := os.MkdirAll(filepath.Dir(dst), dirPerm); err != nil { return errors.New(err) } @@ -206,6 +215,7 @@ func (p *StacksFileProvider) copyDir(src, dst string) error { if err := p.copyDir(srcPath, dstPath); err != nil { return errors.New(err) } + continue } diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go index a5ba4809f4..3eed8ff104 100644 --- a/cli/commands/stack/command.go +++ b/cli/commands/stack/command.go @@ -1,3 +1,4 @@ +// Package stack provides the command to stack. package stack import ( diff --git a/config/stack.go b/config/stack.go index 24ff037a8d..e63ce3eaad 100644 --- a/config/stack.go +++ b/config/stack.go @@ -25,32 +25,41 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr if err != nil { return nil, errors.New(err) } - + // nolint:contextcheck if err := processLocals(parser, terragruntOptions, file); err != nil { return nil, errors.New(err) } - + // nolint:contextcheck evalParsingContext, err := createTerragruntEvalContext(parser, file.ConfigPath) + if err != nil { + return nil, errors.New(err) + } + config := &StackConfigFile{} if err := file.Decode(config, evalParsingContext); err != nil { - return nil, err + return nil, errors.New(err) } return config, nil } -func processLocals(parseCtx *ParsingContext, terragruntOptions *options.TerragruntOptions, file *hclparse.File) error { +func processLocals(parser *ParsingContext, terragruntOptions *options.TerragruntOptions, file *hclparse.File) error { localsBlock, err := file.Blocks(MetadataLocals, false) + if err != nil { return errors.New(err) } + if len(localsBlock) == 0 { return nil } + attrs, err := localsBlock[0].JustAttributes() + if err != nil { return errors.New(err) } + evaluatedLocals := map[string]cty.Value{} evaluated := true @@ -62,8 +71,9 @@ func processLocals(parseCtx *ParsingContext, terragruntOptions *options.Terragru } var err error + // nolint:contextcheck attrs, evaluatedLocals, evaluated, err = attemptEvaluateLocals( - parseCtx, + parser, file, attrs, evaluatedLocals, @@ -74,10 +84,14 @@ func processLocals(parseCtx *ParsingContext, terragruntOptions *options.Terragru return errors.New(err) } } + localsAsCtyVal, err := convertValuesMapToCtyVal(evaluatedLocals) + if err != nil { return errors.New(err) } - parseCtx.Locals = &localsAsCtyVal + + parser.Locals = &localsAsCtyVal + return nil } diff --git a/config/unit.go b/config/unit.go index 4b555f5169..436984f695 100644 --- a/config/unit.go +++ b/config/unit.go @@ -32,9 +32,11 @@ func (s *StackConfigFile) ToCtyValue() (cty.Value, error) { func FromCtyValue(v cty.Value) (*StackConfigFile, error) { var config StackConfigFile err := gocty.FromCtyValue(v, &config) + if err != nil { return nil, fmt.Errorf("failed to decode cty value: %w", err) } + return &config, nil } @@ -43,6 +45,7 @@ func (u *Unit) Clone() *Unit { if u == nil { return nil } + return &Unit{ Name: u.Name, Source: u.Source, diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go index fdbfb38591..46995b4fd6 100644 --- a/test/integration_stacks_test.go +++ b/test/integration_stacks_test.go @@ -1,7 +1,6 @@ package test_test import ( - "fmt" "testing" "github.com/gruntwork-io/terragrunt/test/helpers" @@ -21,7 +20,7 @@ func TestStacksGenerateBasic(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksBasic) - helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-working-dir %s", rootPath)) + helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) } func TestStacksGenerateLocals(t *testing.T) { @@ -31,7 +30,7 @@ func TestStacksGenerateLocals(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocals) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksLocals) - helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-working-dir %s", rootPath)) + helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir %s"+rootPath) } func TestStacksGenerateRemote(t *testing.T) { @@ -41,5 +40,5 @@ func TestStacksGenerateRemote(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksRemote) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksRemote) - helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt stack generate --terragrunt-working-dir %s", rootPath)) + helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir %s"+rootPath) } From 3c5e3d9381884c2493a9582a5e62cef9cc38a13d Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 14:20:19 +0000 Subject: [PATCH 36/59] typo fix --- cli/commands/stack/action.go | 2 +- config/stack.go | 6 +++--- options/options.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index f2fe1bd3c9..b13333db8e 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -36,7 +36,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string } func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { - opts.TerragrungStackConfigPath = filepath.Join(opts.WorkingDir, defaultStackFile) + opts.TerragruntStackConfigPath = filepath.Join(opts.WorkingDir, defaultStackFile) stackFile, err := config.ReadStackConfigFile(ctx, opts) if err != nil { diff --git a/config/stack.go b/config/stack.go index e63ce3eaad..3726b52b3f 100644 --- a/config/stack.go +++ b/config/stack.go @@ -17,11 +17,11 @@ type StackConfigFile struct { } func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.TerragruntOptions) (*StackConfigFile, error) { - terragruntOptions.Logger.Debugf("Reading Terragrunt stack config file at %s", terragruntOptions.TerragrungStackConfigPath) + terragruntOptions.Logger.Debugf("Reading Terragrunt stack config file at %s", terragruntOptions.TerragruntStackConfigPath) parser := NewParsingContext(ctx, terragruntOptions) - file, err := hclparse.NewParser(parser.ParserOptions...).ParseFromFile(terragruntOptions.TerragrungStackConfigPath) + file, err := hclparse.NewParser(parser.ParserOptions...).ParseFromFile(terragruntOptions.TerragruntStackConfigPath) if err != nil { return nil, errors.New(err) } @@ -80,7 +80,7 @@ func processLocals(parser *ParsingContext, terragruntOptions *options.Terragrunt ) if err != nil { - terragruntOptions.Logger.Debugf("Encountered error while evaluating locals in file %s", terragruntOptions.TerragrungStackConfigPath) + terragruntOptions.Logger.Debugf("Encountered error while evaluating locals in file %s", terragruntOptions.TerragruntStackConfigPath) return errors.New(err) } } diff --git a/options/options.go b/options/options.go index f0b9dc6f1a..c336d08318 100644 --- a/options/options.go +++ b/options/options.go @@ -82,7 +82,7 @@ type TerragruntOptions struct { // Location of the Terragrunt config file TerragruntConfigPath string - TerragrungStackConfigPath string + TerragruntStackConfigPath string // Location of the original Terragrunt config file. This is primarily useful when one Terragrunt config is being // read from another: e.g., if /terraform-code/terragrunt.hcl calls read_terragrunt_config("/foo/bar.hcl"), From 8519fd410dc8a4ac7881fb722afab35e1eae39e8 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 19:34:37 +0000 Subject: [PATCH 37/59] Merged unit and stack files --- config/stack.go | 7 +++++++ config/unit.go | 54 ------------------------------------------------- 2 files changed, 7 insertions(+), 54 deletions(-) delete mode 100644 config/unit.go diff --git a/config/stack.go b/config/stack.go index 3726b52b3f..7989575ebe 100644 --- a/config/stack.go +++ b/config/stack.go @@ -16,6 +16,13 @@ type StackConfigFile struct { Units []*Unit `cty:"unit" hcl:"unit,block"` } +// Unit represent unit from stack file. +type Unit struct { + Name string `cty:"name" hcl:",label"` + Source string `hcl:"source,attr" cty:"source"` + Path string `hcl:"path,attr" cty:"path"` +} + func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.TerragruntOptions) (*StackConfigFile, error) { terragruntOptions.Logger.Debugf("Reading Terragrunt stack config file at %s", terragruntOptions.TerragruntStackConfigPath) diff --git a/config/unit.go b/config/unit.go deleted file mode 100644 index 436984f695..0000000000 --- a/config/unit.go +++ /dev/null @@ -1,54 +0,0 @@ -package config - -import ( - "fmt" - - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/gocty" -) - -// Unit represents a list of units. -type Unit struct { - Name string `cty:"name" hcl:",label"` - Source string `hcl:"source,attr" cty:"source"` - Path string `hcl:"path,attr" cty:"path"` -} - -// ToCtyValue converts StackConfigFile to cty.Value -func (s *StackConfigFile) ToCtyValue() (cty.Value, error) { - return gocty.ToCtyValue(s, cty.Object(map[string]cty.Type{ - "locals": cty.Object(map[string]cty.Type{ - // Define locals structure here - }), - "unit": cty.List(cty.Object(map[string]cty.Type{ - "name": cty.String, - "source": cty.String, - "path": cty.String, - })), - })) -} - -// FromCtyValue converts cty.Value back to StackConfigFile -func FromCtyValue(v cty.Value) (*StackConfigFile, error) { - var config StackConfigFile - err := gocty.FromCtyValue(v, &config) - - if err != nil { - return nil, fmt.Errorf("failed to decode cty value: %w", err) - } - - return &config, nil -} - -// Clone creates a deep copy of Unit. -func (u *Unit) Clone() *Unit { - if u == nil { - return nil - } - - return &Unit{ - Name: u.Name, - Source: u.Source, - Path: u.Path, - } -} From b6316b6b91c8948cb26b8ed64c1267a27bc907ef Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 19:37:33 +0000 Subject: [PATCH 38/59] Add error for no command --- cli/commands/stack/action.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index b13333db8e..85cccf43d0 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -32,7 +32,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string return generateStack(ctx, opts) } - return nil + return errors.New(fmt.Errorf("unknown command: %s", subCommand)) } func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { @@ -75,7 +75,7 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } } - opts.Logger.Infof("Processing unit: %s (%s) to %s", unit.Name, src, dest) + opts.Logger.Debugf("Processing unit: %s (%s) to %s", unit.Name, src, dest) client := &getter.Client{ Src: src, From 9cb576ab47d61ef40fc5e689bf16231d993c41ef Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 19:45:15 +0000 Subject: [PATCH 39/59] Add check for / and . --- cli/commands/stack/action.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 85cccf43d0..819e5bd836 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -84,7 +84,6 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac Dir: true, DisableSymlinks: true, Options: []getter.ClientOption{ - getter.WithInsecure(), getter.WithContext(ctx), }, } @@ -110,6 +109,11 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } func isURL(str string) bool { + // check if string starts with / or . + if strings.HasPrefix(str, "/") || strings.HasPrefix(str, ".") { + return false + } + if strings.Contains(str, "//") { return true } From a9a8ff4f63676fc9f1b47550f702de213676ce06 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 20:05:55 +0000 Subject: [PATCH 40/59] Stack fetching --- config/stack.go | 5 +++++ test/integration_stacks_test.go | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/stack.go b/config/stack.go index 7989575ebe..b88bd300f5 100644 --- a/config/stack.go +++ b/config/stack.go @@ -2,6 +2,7 @@ package config import ( "context" + "fmt" "github.com/zclconf/go-cty/cty" @@ -61,6 +62,10 @@ func processLocals(parser *ParsingContext, terragruntOptions *options.Terragrunt return nil } + if len(localsBlock) > 1 { + return errors.New(fmt.Errorf("only one locals block is allowed in a terragrunt stack file, but found %d in %s", len(localsBlock), file.ConfigPath)) + } + attrs, err := localsBlock[0].JustAttributes() if err != nil { diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go index 46995b4fd6..3f2d876a8d 100644 --- a/test/integration_stacks_test.go +++ b/test/integration_stacks_test.go @@ -30,7 +30,7 @@ func TestStacksGenerateLocals(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocals) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksLocals) - helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir %s"+rootPath) + helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) } func TestStacksGenerateRemote(t *testing.T) { @@ -40,5 +40,5 @@ func TestStacksGenerateRemote(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksRemote) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksRemote) - helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir %s"+rootPath) + helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) } From c5426e42ddab4d8c563271cce141ff9dacd04598 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 20:14:40 +0000 Subject: [PATCH 41/59] Locals paths importing --- cli/commands/stack/action.go | 10 ++-------- .../stacks/basic/terragrunt.stack.hcl | 8 ++++---- .../stacks/locals-error/terragrunt.stack.hcl | 7 +++++++ .../stacks/locals/terragrunt.stack.hcl | 4 ++-- test/integration_stacks_test.go | 20 ++++++++++++++++--- 5 files changed, 32 insertions(+), 17 deletions(-) create mode 100644 test/fixtures/stacks/locals-error/terragrunt.stack.hcl diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 819e5bd836..3feb7c5781 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -109,18 +109,12 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } func isURL(str string) bool { - // check if string starts with / or . + // as convention, using / or . for local paths if strings.HasPrefix(str, "/") || strings.HasPrefix(str, ".") { return false } - if strings.Contains(str, "//") { - return true - } - - u, err := url.Parse(str) - - return err == nil && u.Scheme != "" && u.Host != "" + return true } type StacksFileProvider struct { diff --git a/test/fixtures/stacks/basic/terragrunt.stack.hcl b/test/fixtures/stacks/basic/terragrunt.stack.hcl index 8b63f53ef7..86c95025d6 100644 --- a/test/fixtures/stacks/basic/terragrunt.stack.hcl +++ b/test/fixtures/stacks/basic/terragrunt.stack.hcl @@ -1,20 +1,20 @@ unit "mother" { - source = "units/chicken" + source = "./units/chicken" path = "mother" } unit "father" { - source = "units/chicken" + source = "./units/chicken" path = "father" } unit "chick_1" { - source = "units/chick" + source = "./units/chick" path = "chicks/chick-1" } unit "chick_2" { - source = "units/chick" + source = "./units/chick" path = "chicks/chick-2" } diff --git a/test/fixtures/stacks/locals-error/terragrunt.stack.hcl b/test/fixtures/stacks/locals-error/terragrunt.stack.hcl new file mode 100644 index 0000000000..b8c0c843fa --- /dev/null +++ b/test/fixtures/stacks/locals-error/terragrunt.stack.hcl @@ -0,0 +1,7 @@ +locals { + chicken = "units/chicken" +} + +locals { + chick = "units/chick" +} diff --git a/test/fixtures/stacks/locals/terragrunt.stack.hcl b/test/fixtures/stacks/locals/terragrunt.stack.hcl index e5175d0e98..f61c97c2b1 100644 --- a/test/fixtures/stacks/locals/terragrunt.stack.hcl +++ b/test/fixtures/stacks/locals/terragrunt.stack.hcl @@ -1,6 +1,6 @@ locals { - chicken = "units/chicken" - chick = "units/chick" + chicken = "./units/chicken" + chick = "./units/chick" } unit "mother" { diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go index 3f2d876a8d..0d83916c7a 100644 --- a/test/integration_stacks_test.go +++ b/test/integration_stacks_test.go @@ -3,14 +3,17 @@ package test_test import ( "testing" + "github.com/stretchr/testify/require" + "github.com/gruntwork-io/terragrunt/test/helpers" "github.com/gruntwork-io/terragrunt/util" ) const ( - testFixtureStacksBasic = "fixtures/stacks/basic" - testFixtureStacksLocals = "fixtures/stacks/locals" - testFixtureStacksRemote = "fixtures/stacks/remote" + testFixtureStacksBasic = "fixtures/stacks/basic" + testFixtureStacksLocals = "fixtures/stacks/locals" + testFixtureStacksLocalsError = "fixtures/stacks/locals-error" + testFixtureStacksRemote = "fixtures/stacks/remote" ) func TestStacksGenerateBasic(t *testing.T) { @@ -33,6 +36,17 @@ func TestStacksGenerateLocals(t *testing.T) { helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) } +func TestStacksGenerateLocalsError(t *testing.T) { + t.Parallel() + + helpers.CleanupTerraformFolder(t, testFixtureStacksLocalsError) + tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocalsError) + rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksLocalsError) + + _, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) + require.Error(t, err) +} + func TestStacksGenerateRemote(t *testing.T) { t.Parallel() From 48d19ec44a8112b85423500d94ca828991583be9 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 20:24:01 +0000 Subject: [PATCH 42/59] hcl and cty formatting --- config/stack.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/stack.go b/config/stack.go index b88bd300f5..7ef30513e4 100644 --- a/config/stack.go +++ b/config/stack.go @@ -13,15 +13,15 @@ import ( // StackConfigFile represents the structure of terragrunt.stack.hcl stack file type StackConfigFile struct { - Locals *terragruntLocal `cty:"locals" hcl:"locals,block"` - Units []*Unit `cty:"unit" hcl:"unit,block"` + Locals *terragruntLocal `hcl:"locals,block" cty:"locals"` + Units []*Unit `hcl:"unit,block" cty:"unit"` } // Unit represent unit from stack file. type Unit struct { - Name string `cty:"name" hcl:",label"` + Name string `hcl:",label" cty:"name"` Source string `hcl:"source,attr" cty:"source"` - Path string `hcl:"path,attr" cty:"path"` + Path string `hcl:"path,attr" cty:"path"` } func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.TerragruntOptions) (*StackConfigFile, error) { @@ -78,7 +78,7 @@ func processLocals(parser *ParsingContext, terragruntOptions *options.Terragrunt for iterations := 0; len(attrs) > 0 && evaluated; iterations++ { if iterations > MaxIter { // Reached maximum supported iterations, which is most likely an infinite loop bug so cut the iteration - // short an return an error. + // short and return an error. return errors.New(MaxIterError{}) } From acad34efd1a2ec46971e8cfcdad7193accd9c584 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 20:29:20 +0000 Subject: [PATCH 43/59] Markdown update --- docs/_docs/04_reference/cli-options.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index aa05198611..2ca48aea85 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -744,10 +744,10 @@ Notes: ### stack -The `terragrunt stack` commands provide a new interface for managing collections of Terragrunt units defined in `terragrunt.stack.hcl` files. +The `terragrunt stack` commands provide an interface for managing collections of Terragrunt units defined in `terragrunt.stack.hcl` files. These commands simplify the process of handling multiple infrastructure units by grouping them into a "stack," reducing code duplication and streamlining operations across environments. -The `terragrunt stack generate` command is used to generate a stack of Terragrunt `hcl` files based on the configuration provided in the `terragrunt.stack.hcl` file. +The `terragrunt stack generate` command is used to generate a stack of `terragrunt.hcl` files based on the configuration provided in the `terragrunt.stack.hcl` file. Given the following `terragrunt.stack.hcl` configuration: From 56c1ab0f9f1bf426efa674fbd02a3e0632d9ff0d Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 20:45:24 +0000 Subject: [PATCH 44/59] Added go-getter detector --- cli/commands/stack/action.go | 40 ++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 3feb7c5781..6263d221e2 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -62,23 +62,9 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac if err != nil { return errors.New(fmt.Errorf("failed to get absolute path for destination '%s': %w", dest, err)) } - src := unit.Source - // set absolute path for source if it's not an absolute path or URL - if !filepath.IsAbs(unit.Source) && !isURL(unit.Source) { - src = filepath.Join(opts.WorkingDir, unit.Source) - src, err = filepath.Abs(src) - - if err != nil { - opts.Logger.Warnf("failed to get absolute path for source '%s': %v", unit.Source, err) - src = unit.Source - } - } - - opts.Logger.Debugf("Processing unit: %s (%s) to %s", unit.Name, src, dest) client := &getter.Client{ - Src: src, Dst: dest, Mode: getter.ClientModeAny, Dir: true, @@ -100,6 +86,21 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } } + // set absolute path for source if it's not an absolute path or URL + if !filepath.IsAbs(unit.Source) && !isURL(client, unit.Source) { + src = filepath.Join(opts.WorkingDir, unit.Source) + src, err = filepath.Abs(src) + + if err != nil { + opts.Logger.Warnf("failed to get absolute path for source '%s': %v", unit.Source, err) + src = unit.Source + } + } + + opts.Logger.Debugf("Processing unit: %s (%s) to %s", unit.Name, src, dest) + + client.Src = src + if err := client.Get(); err != nil { return errors.New(err) } @@ -108,12 +109,15 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac return nil } -func isURL(str string) bool { - // as convention, using / or . for local paths - if strings.HasPrefix(str, "/") || strings.HasPrefix(str, ".") { +func isURL(client *getter.Client, str string) bool { + value, err := getter.Detect(str, client.Dst, getter.Detectors) + if err != nil { + return false + } + // check if starts with file:// + if strings.HasPrefix(value, "file://") { return false } - return true } From 0b2d9d15ac9e14907995fc7e28dc47b3531a2430 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 20:47:37 +0000 Subject: [PATCH 45/59] Markdown update --- docs/_docs/04_reference/cli-options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index 2ca48aea85..dbe75fb03f 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -745,7 +745,7 @@ Notes: ### stack The `terragrunt stack` commands provide an interface for managing collections of Terragrunt units defined in `terragrunt.stack.hcl` files. -These commands simplify the process of handling multiple infrastructure units by grouping them into a "stack," reducing code duplication and streamlining operations across environments. +These commands simplify the process of handling multiple infrastructure units by grouping them into a "stack", reducing code duplication and streamlining operations across environments. The `terragrunt stack generate` command is used to generate a stack of `terragrunt.hcl` files based on the configuration provided in the `terragrunt.stack.hcl` file. From c63ff05d5da4823c39781daa73e3188c2f693a03 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 20:50:06 +0000 Subject: [PATCH 46/59] Test stack update --- test/fixtures/stacks/basic/terragrunt.stack.hcl | 4 ++-- test/fixtures/stacks/locals/terragrunt.stack.hcl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/fixtures/stacks/basic/terragrunt.stack.hcl b/test/fixtures/stacks/basic/terragrunt.stack.hcl index 86c95025d6..ac2ebea9f7 100644 --- a/test/fixtures/stacks/basic/terragrunt.stack.hcl +++ b/test/fixtures/stacks/basic/terragrunt.stack.hcl @@ -1,5 +1,5 @@ unit "mother" { - source = "./units/chicken" + source = "units/chicken" path = "mother" } @@ -14,7 +14,7 @@ unit "chick_1" { } unit "chick_2" { - source = "./units/chick" + source = "units/chick" path = "chicks/chick-2" } diff --git a/test/fixtures/stacks/locals/terragrunt.stack.hcl b/test/fixtures/stacks/locals/terragrunt.stack.hcl index f61c97c2b1..e5175d0e98 100644 --- a/test/fixtures/stacks/locals/terragrunt.stack.hcl +++ b/test/fixtures/stacks/locals/terragrunt.stack.hcl @@ -1,6 +1,6 @@ locals { - chicken = "./units/chicken" - chick = "./units/chick" + chicken = "units/chicken" + chick = "units/chick" } unit "mother" { From 5860985c66bbf7014894f2665cd5d98699fac89c Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 21:09:27 +0000 Subject: [PATCH 47/59] lint fixes --- cli/commands/stack/action.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 6263d221e2..51c7752b94 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -62,7 +62,6 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac if err != nil { return errors.New(fmt.Errorf("failed to get absolute path for destination '%s': %w", dest, err)) } - src := unit.Source client := &getter.Client{ Dst: dest, @@ -86,6 +85,9 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac } } + // fetching unit source + src := unit.Source + // set absolute path for source if it's not an absolute path or URL if !filepath.IsAbs(unit.Source) && !isURL(client, unit.Source) { src = filepath.Join(opts.WorkingDir, unit.Source) @@ -118,6 +120,7 @@ func isURL(client *getter.Client, str string) bool { if strings.HasPrefix(value, "file://") { return false } + return true } From f48243e3e2ed5188e7d2faba4c0e9ea5d85e7de9 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 21:11:35 +0000 Subject: [PATCH 48/59] Strict lint update --- config/stack.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/stack.go b/config/stack.go index 7ef30513e4..0e899ec822 100644 --- a/config/stack.go +++ b/config/stack.go @@ -33,11 +33,11 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr if err != nil { return nil, errors.New(err) } - // nolint:contextcheck + //nolint:contextcheck if err := processLocals(parser, terragruntOptions, file); err != nil { return nil, errors.New(err) } - // nolint:contextcheck + //nolint:contextcheck evalParsingContext, err := createTerragruntEvalContext(parser, file.ConfigPath) if err != nil { return nil, errors.New(err) @@ -83,7 +83,7 @@ func processLocals(parser *ParsingContext, terragruntOptions *options.Terragrunt } var err error - // nolint:contextcheck + //nolint:contextcheck attrs, evaluatedLocals, evaluated, err = attemptEvaluateLocals( parser, file, From 1b644736d1a533a3312539d31a8ffbc9067fee40 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 21:33:47 +0000 Subject: [PATCH 49/59] Lint fixes --- cli/commands/stack/action.go | 32 +++++++++++++++++--------------- cli/commands/stack/command.go | 6 +++++- config/config.go | 7 +++---- config/stack.go | 32 +++++++++++++++++--------------- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 51c7752b94..0421ef8c61 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -23,6 +23,7 @@ const ( dirPerm = 0755 ) +// Run runs the stack command. func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string) error { if subCommand == "" { return errors.New("No command specified") @@ -32,7 +33,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string return generateStack(ctx, opts) } - return errors.New(fmt.Errorf("unknown command: %s", subCommand)) + return errors.New("unknown command: " + subCommand) } func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { @@ -79,7 +80,7 @@ func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stac for getterName, getterValue := range getter.Getters { // setting custom getter for file to not use symlinks if getterName == "file" { - client.Getters[getterName] = &StacksFileProvider{} + client.Getters[getterName] = &stacksFileProvider{} } else { client.Getters[getterName] = getterValue } @@ -124,33 +125,34 @@ func isURL(client *getter.Client, str string) bool { return true } -type StacksFileProvider struct { +// stacksFileProvider is a custom getter for file:// protocol. +type stacksFileProvider struct { client *getter.Client } -// Get implements downloading functionality -func (p *StacksFileProvider) Get(dst string, u *url.URL) error { +// Get implements downloading functionality. +func (p *stacksFileProvider) Get(dst string, u *url.URL) error { src := u.Path - fi, err := os.Stat(src) + file, err := os.Stat(src) if err != nil { return errors.New(fmt.Errorf("source path error: %w", err)) } - if fi.IsDir() { + if file.IsDir() { return p.copyDir(src, dst) } return p.copyFile(src, dst) } -// GetFile implements single file download -func (p *StacksFileProvider) GetFile(dst string, u *url.URL) error { +// GetFile implements single file download. +func (p *stacksFileProvider) GetFile(dst string, u *url.URL) error { return p.copyFile(u.Path, dst) } -// ClientMode determines if we're getting a directory or single file -func (p *StacksFileProvider) ClientMode(u *url.URL) (getter.ClientMode, error) { +// ClientMode determines if we're getting a directory or single file. +func (p *stacksFileProvider) ClientMode(u *url.URL) (getter.ClientMode, error) { fi, err := os.Stat(u.Path) if err != nil { return getter.ClientModeInvalid, errors.New(err) @@ -163,12 +165,12 @@ func (p *StacksFileProvider) ClientMode(u *url.URL) (getter.ClientMode, error) { return getter.ClientModeFile, nil } -// SetClient sets the client for this provider -func (p *StacksFileProvider) SetClient(c *getter.Client) { +// SetClient sets the client for this provider. +func (p *stacksFileProvider) SetClient(c *getter.Client) { p.client = c } -func (p *StacksFileProvider) copyFile(src, dst string) error { +func (p *stacksFileProvider) copyFile(src, dst string) error { if err := os.MkdirAll(filepath.Dir(dst), dirPerm); err != nil { return errors.New(err) } @@ -197,7 +199,7 @@ func (p *StacksFileProvider) copyFile(src, dst string) error { return nil } -func (p *StacksFileProvider) copyDir(src, dst string) error { +func (p *stacksFileProvider) copyDir(src, dst string) error { srcInfo, err := os.Stat(src) if err != nil { return errors.New(err) diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go index 3eed8ff104..96c02d7ffa 100644 --- a/cli/commands/stack/command.go +++ b/cli/commands/stack/command.go @@ -7,13 +7,16 @@ import ( ) const ( + // CommandName stack command name. CommandName = "stack" ) -func NewFlags(opts *options.TerragruntOptions) cli.Flags { +// NewFlags builds the flags for stack. +func NewFlags(_ *options.TerragruntOptions) cli.Flags { return cli.Flags{} } +// NewCommand builds the command for stack. func NewCommand(opts *options.TerragruntOptions) *cli.Command { return &cli.Command{ Name: CommandName, @@ -22,6 +25,7 @@ func NewCommand(opts *options.TerragruntOptions) *cli.Command { Flags: NewFlags(opts).Sort(), Action: func(ctx *cli.Context) error { command := ctx.Args().Get(0) + return Run(ctx.Context, opts.OptionsFromContext(ctx), command) }, } diff --git a/config/config.go b/config/config.go index 80369892c6..c124d95085 100644 --- a/config/config.go +++ b/config/config.go @@ -37,10 +37,9 @@ import ( ) const ( - DefaultTerragruntConfigPath = "terragrunt.hcl" - DefaultTerragruntStackConfigPath = "terragrunt.stack.hcl" - DefaultTerragruntJSONConfigPath = "terragrunt.hcl.json" - FoundInFile = "found_in_file" + DefaultTerragruntConfigPath = "terragrunt.hcl" + DefaultTerragruntJSONConfigPath = "terragrunt.hcl.json" + FoundInFile = "found_in_file" iamRoleCacheName = "iamRoleCache" diff --git a/config/stack.go b/config/stack.go index 0e899ec822..453bbbc043 100644 --- a/config/stack.go +++ b/config/stack.go @@ -11,30 +11,31 @@ import ( "github.com/gruntwork-io/terragrunt/options" ) -// StackConfigFile represents the structure of terragrunt.stack.hcl stack file +// StackConfigFile represents the structure of terragrunt.stack.hcl stack file. type StackConfigFile struct { - Locals *terragruntLocal `hcl:"locals,block" cty:"locals"` - Units []*Unit `hcl:"unit,block" cty:"unit"` + Locals *terragruntLocal `hcl:"locals,block"` + Units []*Unit `hcl:"unit,block"` } // Unit represent unit from stack file. type Unit struct { - Name string `hcl:",label" cty:"name"` - Source string `hcl:"source,attr" cty:"source"` - Path string `hcl:"path,attr" cty:"path"` + Name string `hcl:",label"` + Source string `hcl:"source,attr"` + Path string `hcl:"path,attr"` } -func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.TerragruntOptions) (*StackConfigFile, error) { - terragruntOptions.Logger.Debugf("Reading Terragrunt stack config file at %s", terragruntOptions.TerragruntStackConfigPath) +// ReadStackConfigFile reads the terragrunt.stack.hcl file. +func ReadStackConfigFile(ctx context.Context, opts *options.TerragruntOptions) (*StackConfigFile, error) { + opts.Logger.Debugf("Reading Terragrunt stack config file at %s", opts.TerragruntStackConfigPath) - parser := NewParsingContext(ctx, terragruntOptions) + parser := NewParsingContext(ctx, opts) - file, err := hclparse.NewParser(parser.ParserOptions...).ParseFromFile(terragruntOptions.TerragruntStackConfigPath) + file, err := hclparse.NewParser(parser.ParserOptions...).ParseFromFile(opts.TerragruntStackConfigPath) if err != nil { return nil, errors.New(err) } //nolint:contextcheck - if err := processLocals(parser, terragruntOptions, file); err != nil { + if err := processLocals(parser, opts, file); err != nil { return nil, errors.New(err) } //nolint:contextcheck @@ -51,7 +52,7 @@ func ReadStackConfigFile(ctx context.Context, terragruntOptions *options.Terragr return config, nil } -func processLocals(parser *ParsingContext, terragruntOptions *options.TerragruntOptions, file *hclparse.File) error { +func processLocals(parser *ParsingContext, opts *options.TerragruntOptions, file *hclparse.File) error { localsBlock, err := file.Blocks(MetadataLocals, false) if err != nil { @@ -63,7 +64,8 @@ func processLocals(parser *ParsingContext, terragruntOptions *options.Terragrunt } if len(localsBlock) > 1 { - return errors.New(fmt.Errorf("only one locals block is allowed in a terragrunt stack file, but found %d in %s", len(localsBlock), file.ConfigPath)) + return errors.New(fmt.Sprintf("only one locals block is allowed %s stack file, "+ + "but found %d ", file.ConfigPath, len(localsBlock))) } attrs, err := localsBlock[0].JustAttributes() @@ -83,7 +85,6 @@ func processLocals(parser *ParsingContext, terragruntOptions *options.Terragrunt } var err error - //nolint:contextcheck attrs, evaluatedLocals, evaluated, err = attemptEvaluateLocals( parser, file, @@ -92,7 +93,8 @@ func processLocals(parser *ParsingContext, terragruntOptions *options.Terragrunt ) if err != nil { - terragruntOptions.Logger.Debugf("Encountered error while evaluating locals in file %s", terragruntOptions.TerragruntStackConfigPath) + opts.Logger.Debugf("Encountered error while evaluating locals in file %s", opts.TerragruntStackConfigPath) + return errors.New(err) } } From 744c6fe2165b72c9102f50e304aa070dbc7afb71 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 21:36:54 +0000 Subject: [PATCH 50/59] Updated remote clone --- test/fixtures/stacks/remote/terragrunt.stack.hcl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/fixtures/stacks/remote/terragrunt.stack.hcl b/test/fixtures/stacks/remote/terragrunt.stack.hcl index df6c391a0d..bf0a4c728e 100644 --- a/test/fixtures/stacks/remote/terragrunt.stack.hcl +++ b/test/fixtures/stacks/remote/terragrunt.stack.hcl @@ -1,14 +1,14 @@ locals { - version = "v0.68.4" + version = "v0.6.0" } unit "app1" { - source = "github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=${local.version}" + source = "github.com/gruntwork-io/terraform-google-sql.git//modules/cloud-sql?ref=${local.version}" path = "app1" } unit "app2" { - source = "github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=${local.version}" + source = "github.com/gruntwork-io/terraform-google-sql.git//modules/cloud-sql?ref=${local.version}" path = "app2" } From 3d71d5114ba65bb9f90e33a094bab236d95f661e Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 22:07:23 +0000 Subject: [PATCH 51/59] Setting stack behind stack flag --- cli/commands/stack/action.go | 8 ++++++++ docs/_docs/04_reference/experiments.md | 26 ++++++++++++++++++++++++++ internal/experiment/experiment.go | 4 ++++ test/integration_stacks_test.go | 8 ++++---- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 0421ef8c61..68fe225972 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strings" + "github.com/gruntwork-io/terragrunt/internal/experiment" + "github.com/gruntwork-io/terragrunt/config" getter "github.com/hashicorp/go-getter" @@ -25,6 +27,12 @@ const ( // Run runs the stack command. func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string) error { + + stacksEnabled := opts.Experiments[experiment.Stacks] + if !stacksEnabled.Enabled { + return errors.New("stacks experiment is not enabled use --experiment stacks to enable it") + } + if subCommand == "" { return errors.New("No command specified") } diff --git a/docs/_docs/04_reference/experiments.md b/docs/_docs/04_reference/experiments.md index 6ad063d26b..bdcc664d2d 100644 --- a/docs/_docs/04_reference/experiments.md +++ b/docs/_docs/04_reference/experiments.md @@ -65,6 +65,7 @@ You can also enable multiple experiments at once with a comma delimited list. The following strict mode controls are available: - [symlinks](#symlinks) +- [stacks](#stacks) ### symlinks @@ -86,3 +87,28 @@ To stabilize this feature, the following need to be resolved, at a minimum: - [ ] Add integration tests for all filesystem flags to confirm support with symlinks (or document the fact that they cannot be supported). - [ ] Ensure that MacOS integration tests still work. See [#3616](https://github.com/gruntwork-io/terragrunt/issues/3616). - [ ] Add integration tests for MacOS in CI. + +### stacks + +Support for Terragrunt stacks. + +#### What it does + +Enable `stack` command to manage Terragrunt stacks. + +#### How to provide feedback + +Share your experience with the `stack` command in the [Stacks](https://github.com/gruntwork-io/terragrunt/issues/3313) RFC. +Feedback is crucial for ensuring the feature meets real-world use cases. Please include: +- Details about your infrastructure's complexity and use case. +- Any bugs or issues encountered (including logs or stack traces if possible). +- Suggestions for additional improvements or enhancements. + +#### Criteria for stabilization + +To transition the `stacks` feature to a stable release, the following must be addressed: + +- [ ] Add support for `stack run *` and `stack output` commands to extend stack-level operations. +- [ ] Integration testing for recursive stack handling across typical workflows, ensuring smooth transitions during `plan`, `apply`, and `destroy` operations. +- [ ] Confirm compatibility with parallelism flags (e.g., `--parallel`), especially for stacks with dependencies. +- [ ] Ensure that error handling and failure recovery strategies work as intended across large and nested stacks. diff --git a/internal/experiment/experiment.go b/internal/experiment/experiment.go index 36abce36d4..34a4cb0e7c 100644 --- a/internal/experiment/experiment.go +++ b/internal/experiment/experiment.go @@ -21,6 +21,9 @@ func NewExperiments() Experiments { Symlinks: Experiment{ Name: Symlinks, }, + Stacks: Experiment{ + Name: Stacks, + }, } } @@ -42,6 +45,7 @@ func (e Experiment) String() string { const ( // Symlinks is the experiment that allows symlinks to be used in Terragrunt configurations. Symlinks = "symlinks" + Stacks = "stacks" ) const ( diff --git a/test/integration_stacks_test.go b/test/integration_stacks_test.go index 0d83916c7a..5267c0f58a 100644 --- a/test/integration_stacks_test.go +++ b/test/integration_stacks_test.go @@ -23,7 +23,7 @@ func TestStacksGenerateBasic(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksBasic) - helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) + helpers.RunTerragrunt(t, "terragrunt stack generate --experiment stacks --terragrunt-working-dir "+rootPath) } func TestStacksGenerateLocals(t *testing.T) { @@ -33,7 +33,7 @@ func TestStacksGenerateLocals(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocals) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksLocals) - helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) + helpers.RunTerragrunt(t, "terragrunt stack generate --experiment stacks --terragrunt-working-dir "+rootPath) } func TestStacksGenerateLocalsError(t *testing.T) { @@ -43,7 +43,7 @@ func TestStacksGenerateLocalsError(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocalsError) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksLocalsError) - _, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) + _, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack generate --experiment stacks --terragrunt-working-dir "+rootPath) require.Error(t, err) } @@ -54,5 +54,5 @@ func TestStacksGenerateRemote(t *testing.T) { tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksRemote) rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksRemote) - helpers.RunTerragrunt(t, "terragrunt stack generate --terragrunt-working-dir "+rootPath) + helpers.RunTerragrunt(t, "terragrunt stack generate --experiment stacks --terragrunt-working-dir "+rootPath) } From 254e95ca54d2d676fd2dc05631ae0207928ab2f6 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 22:17:32 +0000 Subject: [PATCH 52/59] Markdown update --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9d879a4f79..4329847a1a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,6 +128,8 @@ run_markdownlint: &run_markdownlint command: | markdownlint \ --disable 'MD013' \ + # disable duplicate duplicate headings + --disable 'MD024' \ -- \ docs From b0cc2db6f75633292e8ded443b2cd2d346a3b768 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 22:19:10 +0000 Subject: [PATCH 53/59] lint update --- .circleci/config.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4329847a1a..6b0e190170 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,9 +127,8 @@ run_markdownlint: &run_markdownlint name: Run markdownlint command: | markdownlint \ - --disable 'MD013' \ - # disable duplicate duplicate headings - --disable 'MD024' \ + # MD024 - disable duplicate duplicate headings + --disable 'MD013' 'MD024' \ -- \ docs From 9f6987add15c60662845d4c62e22107226552c12 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 22:21:05 +0000 Subject: [PATCH 54/59] Disable update --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b0e190170..c555e8613a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,8 +127,7 @@ run_markdownlint: &run_markdownlint name: Run markdownlint command: | markdownlint \ - # MD024 - disable duplicate duplicate headings - --disable 'MD013' 'MD024' \ + --disable MD013 MD024 \ -- \ docs From 3288ce9baad9d7e85c4d429648933b5981f59f1d Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 22:24:36 +0000 Subject: [PATCH 55/59] Markdown cleanup --- docs/_docs/04_reference/experiments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_docs/04_reference/experiments.md b/docs/_docs/04_reference/experiments.md index bdcc664d2d..73bd3260b2 100644 --- a/docs/_docs/04_reference/experiments.md +++ b/docs/_docs/04_reference/experiments.md @@ -98,9 +98,9 @@ Enable `stack` command to manage Terragrunt stacks. #### How to provide feedback -Share your experience with the `stack` command in the [Stacks](https://github.com/gruntwork-io/terragrunt/issues/3313) RFC. +Share your experience with the `stack` command in the [Stacks](https://github.com/gruntwork-io/terragrunt/issues/3313) RFC. Feedback is crucial for ensuring the feature meets real-world use cases. Please include: -- Details about your infrastructure's complexity and use case. + - Any bugs or issues encountered (including logs or stack traces if possible). - Suggestions for additional improvements or enhancements. From 6611b233065f30a1593fbdb759e5eac478725364 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 22:25:20 +0000 Subject: [PATCH 56/59] stacks experiment update --- internal/experiment/experiment.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/experiment/experiment.go b/internal/experiment/experiment.go index 34a4cb0e7c..bd90da17c6 100644 --- a/internal/experiment/experiment.go +++ b/internal/experiment/experiment.go @@ -45,7 +45,8 @@ func (e Experiment) String() string { const ( // Symlinks is the experiment that allows symlinks to be used in Terragrunt configurations. Symlinks = "symlinks" - Stacks = "stacks" + // Stacks is the experiment that allows stacks to be used in Terragrunt. + Stacks = "stacks" ) const ( From 54d95a8884c23ed4c0c8c64d4dec532c8486ef52 Mon Sep 17 00:00:00 2001 From: Denis O Date: Wed, 8 Jan 2025 22:36:57 +0000 Subject: [PATCH 57/59] Lint fixes --- cli/commands/stack/action.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 68fe225972..212bf41be5 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -27,7 +27,6 @@ const ( // Run runs the stack command. func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string) error { - stacksEnabled := opts.Experiments[experiment.Stacks] if !stacksEnabled.Enabled { return errors.New("stacks experiment is not enabled use --experiment stacks to enable it") From 2d526f9ce8f295d5754e51856999a5cc35f26c7e Mon Sep 17 00:00:00 2001 From: Denis O Date: Thu, 9 Jan 2025 16:12:50 +0000 Subject: [PATCH 58/59] Simplified stack generate CLI --- cli/commands/stack/action.go | 15 +++------------ cli/commands/stack/command.go | 15 ++++++++++++--- config/stack.go | 3 +-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/cli/commands/stack/action.go b/cli/commands/stack/action.go index 212bf41be5..be21ffbbbc 100644 --- a/cli/commands/stack/action.go +++ b/cli/commands/stack/action.go @@ -19,28 +19,19 @@ import ( ) const ( - generate = "generate" stackCacheDir = ".terragrunt-stack" defaultStackFile = "terragrunt.stack.hcl" dirPerm = 0755 ) -// Run runs the stack command. -func Run(ctx context.Context, opts *options.TerragruntOptions, subCommand string) error { +// RunGenerate runs the stack command. +func RunGenerate(ctx context.Context, opts *options.TerragruntOptions) error { stacksEnabled := opts.Experiments[experiment.Stacks] if !stacksEnabled.Enabled { return errors.New("stacks experiment is not enabled use --experiment stacks to enable it") } - if subCommand == "" { - return errors.New("No command specified") - } - - if subCommand == generate { - return generateStack(ctx, opts) - } - - return errors.New("unknown command: " + subCommand) + return generateStack(ctx, opts) } func generateStack(ctx context.Context, opts *options.TerragruntOptions) error { diff --git a/cli/commands/stack/command.go b/cli/commands/stack/command.go index 96c02d7ffa..6ea8504850 100644 --- a/cli/commands/stack/command.go +++ b/cli/commands/stack/command.go @@ -9,6 +9,7 @@ import ( const ( // CommandName stack command name. CommandName = "stack" + generate = "generate" ) // NewFlags builds the flags for stack. @@ -23,10 +24,18 @@ func NewCommand(opts *options.TerragruntOptions) *cli.Command { Usage: "Terragrunt stack commands.", DisallowUndefinedFlags: true, Flags: NewFlags(opts).Sort(), - Action: func(ctx *cli.Context) error { - command := ctx.Args().Get(0) + Subcommands: cli.Commands{ + &cli.Command{ + Name: "generate", + Usage: "Generate the stack file.", + Action: func(ctx *cli.Context) error { + return RunGenerate(ctx.Context, opts.OptionsFromContext(ctx)) - return Run(ctx.Context, opts.OptionsFromContext(ctx), command) + }, + }, + }, + Action: func(ctx *cli.Context) error { + return cli.ShowCommandHelp(ctx, generate) }, } } diff --git a/config/stack.go b/config/stack.go index 453bbbc043..a95956e17a 100644 --- a/config/stack.go +++ b/config/stack.go @@ -64,8 +64,7 @@ func processLocals(parser *ParsingContext, opts *options.TerragruntOptions, file } if len(localsBlock) > 1 { - return errors.New(fmt.Sprintf("only one locals block is allowed %s stack file, "+ - "but found %d ", file.ConfigPath, len(localsBlock))) + return errors.New(fmt.Sprintf("up to one locals block is allowed per stack file, but found %d in %s", len(localsBlock), file.ConfigPath)) } attrs, err := localsBlock[0].JustAttributes() From 4ae51d55d731070fdd4df28a549427003653f453 Mon Sep 17 00:00:00 2001 From: Denis O Date: Thu, 9 Jan 2025 16:17:33 +0000 Subject: [PATCH 59/59] Unit path updated --- test/fixtures/stacks/remote/terragrunt.stack.hcl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/fixtures/stacks/remote/terragrunt.stack.hcl b/test/fixtures/stacks/remote/terragrunt.stack.hcl index bf0a4c728e..918f211a09 100644 --- a/test/fixtures/stacks/remote/terragrunt.stack.hcl +++ b/test/fixtures/stacks/remote/terragrunt.stack.hcl @@ -1,14 +1,14 @@ locals { - version = "v0.6.0" + version = "2d526f9ce8f295d5754e51856999a5cc35f26c7e" } unit "app1" { - source = "github.com/gruntwork-io/terraform-google-sql.git//modules/cloud-sql?ref=${local.version}" + source = "github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/basic/units/chick?ref=${local.version}" path = "app1" } unit "app2" { - source = "github.com/gruntwork-io/terraform-google-sql.git//modules/cloud-sql?ref=${local.version}" + source = "github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/basic/units/chick?ref=${local.version}" path = "app2" }