Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stacks: run #3762

Merged
merged 22 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions cli/commands/stack/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"path/filepath"
"strings"

runall "github.com/gruntwork-io/terragrunt/cli/commands/run-all"

"github.com/gruntwork-io/terragrunt/internal/experiment"

"github.com/gruntwork-io/terragrunt/config"
Expand All @@ -19,7 +21,7 @@ import (
)

const (
stackCacheDir = ".terragrunt-stack"
stackDir = ".terragrunt-stack"
defaultStackFile = "terragrunt.stack.hcl"
dirPerm = 0755
)
Expand All @@ -34,8 +36,31 @@ func RunGenerate(ctx context.Context, opts *options.TerragruntOptions) error {
return generateStack(ctx, opts)
}

// Run execute stack command.
func Run(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 err := RunGenerate(ctx, opts); err != nil {
return err
}

// prepare options for execution
// navigate to stack directory
opts.WorkingDir = filepath.Join(opts.WorkingDir, stackDir)
// remove 0 element from args
opts.TerraformCliArgs = opts.TerraformCliArgs[1:]
opts.TerraformCommand = opts.TerraformCliArgs[0]
opts.OriginalTerraformCommand = strings.Join(opts.TerraformCliArgs, " ")

return runall.Run(ctx, opts)
}

func generateStack(ctx context.Context, opts *options.TerragruntOptions) error {
opts.TerragruntStackConfigPath = filepath.Join(opts.WorkingDir, defaultStackFile)
opts.Logger.Infof("Generating stack from %s", opts.TerragruntStackConfigPath)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be in info?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without an info message, it is not clear what is happening, the user doesn't see any message and it is not clear which units got processed

stackFile, err := config.ReadStackConfigFile(ctx, opts)

if err != nil {
Expand All @@ -48,13 +73,16 @@ func generateStack(ctx context.Context, opts *options.TerragruntOptions) error {

return nil
}

func processStackFile(ctx context.Context, opts *options.TerragruntOptions, stackFile *config.StackConfigFile) error {
baseDir := filepath.Join(opts.WorkingDir, stackCacheDir)
baseDir := filepath.Join(opts.WorkingDir, stackDir)
if err := os.MkdirAll(baseDir, dirPerm); err != nil {
return errors.New(fmt.Errorf("failed to create base directory: %w", err))
}

for _, unit := range stackFile.Units {
opts.Logger.Infof("Processing unit %s", unit.Name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be in info?


destPath := filepath.Join(baseDir, unit.Path)
dest, err := filepath.Abs(destPath)

Expand Down
12 changes: 10 additions & 2 deletions cli/commands/stack/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
// CommandName stack command name.
CommandName = "stack"
generate = "generate"
run = "run"
)

// NewFlags builds the flags for stack.
Expand All @@ -26,13 +27,20 @@ func NewCommand(opts *options.TerragruntOptions) *cli.Command {
Flags: NewFlags(opts).Sort(),
Subcommands: cli.Commands{
&cli.Command{
Name: "generate",
Usage: "Generate the stack file.",
Name: generate,
Usage: "Generate a stack from a terragrunt.stack.hcl file",
Action: func(ctx *cli.Context) error {
return RunGenerate(ctx.Context, opts.OptionsFromContext(ctx))

},
},
&cli.Command{
Name: run,
Usage: "Run a command on the stack generated from the current directory",
Action: func(ctx *cli.Context) error {
return Run(ctx.Context, opts.OptionsFromContext(ctx))
},
},
},
Action: func(ctx *cli.Context) error {
return cli.ShowCommandHelp(ctx, generate)
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/stacks/basic/units/chick/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

resource "local_file" "file" {
content = "chick"
filename = "${path.module}/test.txt"
}

output "output" {
value = local_file.file.filename
}
4 changes: 4 additions & 0 deletions test/fixtures/stacks/basic/units/chick/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

terraform {
source = "."
}
9 changes: 9 additions & 0 deletions test/fixtures/stacks/basic/units/chicken/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

resource "local_file" "file" {
content = "chicken"
filename = "${path.module}/test.txt"
}

output "output" {
value = local_file.file.filename
}
4 changes: 4 additions & 0 deletions test/fixtures/stacks/basic/units/chicken/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

terraform {
source = "."
}
9 changes: 9 additions & 0 deletions test/fixtures/stacks/basic/units/father/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

resource "local_file" "file" {
content = "father"
filename = "${path.module}/test.txt"
}

output "output" {
value = local_file.file.filename
}
4 changes: 4 additions & 0 deletions test/fixtures/stacks/basic/units/father/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

terraform {
source = "."
}
9 changes: 9 additions & 0 deletions test/fixtures/stacks/basic/units/mother/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

resource "local_file" "file" {
content = "mother"
filename = "${path.module}/test.txt"
}

output "output" {
value = local_file.file.filename
}
4 changes: 4 additions & 0 deletions test/fixtures/stacks/basic/units/mother/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

terraform {
source = "."
}
9 changes: 9 additions & 0 deletions test/fixtures/stacks/inputs/terragrunt.stack.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
unit "unit1" {
source = "units/app"
path = "unit1"
}

unit "unit2" {
source = "units/app"
path = "unit2"
}
17 changes: 17 additions & 0 deletions test/fixtures/stacks/inputs/units/app/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
variable "content" {
type = string
}

variable "filename" {
type = string
default = "file.txt"
}

resource "local_file" "file" {
content = var.content
filename = "${path.module}/${var.filename}"
}

output "output" {
value = local_file.file.filename
}
8 changes: 8 additions & 0 deletions test/fixtures/stacks/inputs/units/app/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

terraform {
source = "."
}

inputs = {
content = "content"
}
119 changes: 119 additions & 0 deletions test/integration_stacks_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package test_test

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/require"

"github.com/gruntwork-io/terragrunt/test/helpers"
Expand All @@ -14,6 +18,7 @@ const (
testFixtureStacksLocals = "fixtures/stacks/locals"
testFixtureStacksLocalsError = "fixtures/stacks/locals-error"
testFixtureStacksRemote = "fixtures/stacks/remote"
testFixtureStacksInputs = "fixtures/stacks/inputs"
)

func TestStacksGenerateBasic(t *testing.T) {
Expand All @@ -24,6 +29,9 @@ func TestStacksGenerateBasic(t *testing.T) {
rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksBasic)

helpers.RunTerragrunt(t, "terragrunt stack generate --experiment stacks --terragrunt-working-dir "+rootPath)

path := util.JoinPath(rootPath, ".terragrunt-stack")
validateStackDir(t, path)
}

func TestStacksGenerateLocals(t *testing.T) {
Expand Down Expand Up @@ -55,4 +63,115 @@ func TestStacksGenerateRemote(t *testing.T) {
rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksRemote)

helpers.RunTerragrunt(t, "terragrunt stack generate --experiment stacks --terragrunt-working-dir "+rootPath)

path := util.JoinPath(rootPath, ".terragrunt-stack")
validateStackDir(t, path)
}

func TestStacksBasic(t *testing.T) {
t.Parallel()

helpers.CleanupTerraformFolder(t, testFixtureStacksBasic)
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)
rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksBasic)

helpers.RunTerragrunt(t, "terragrunt --experiment stacks stack run apply --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)

path := util.JoinPath(rootPath, ".terragrunt-stack")
validateStackDir(t, path)

// check that the stack was applied and .txt files got generated in the stack directory
var txtFiles []string

err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() == "test.txt" {
txtFiles = append(txtFiles, filePath)
}

return nil
})

require.NoError(t, err)
assert.Len(t, txtFiles, 4)
}

func TestStacksInputs(t *testing.T) {
t.Parallel()

helpers.CleanupTerraformFolder(t, testFixtureStacksInputs)
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)
rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksInputs)

helpers.RunTerragrunt(t, "terragrunt stack run plan --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)

path := util.JoinPath(rootPath, ".terragrunt-stack")
validateStackDir(t, path)
}

func TestStacksPlan(t *testing.T) {
t.Parallel()

helpers.CleanupTerraformFolder(t, testFixtureStacksInputs)
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)
rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksInputs)

stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack run plan --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
require.NoError(t, err)

assert.Contains(t, stdout, "Plan: 1 to add, 0 to change, 0 to destroy")
assert.Contains(t, stdout, "local_file.file will be created")
}

func TestStacksApply(t *testing.T) {
t.Parallel()

helpers.CleanupTerraformFolder(t, testFixtureStacksInputs)
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)
rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksInputs)

stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack run apply --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
require.NoError(t, err)

assert.Contains(t, stdout, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed")
assert.Contains(t, stdout, "local_file.file: Creation complete")
}

func TestStacksDestroy(t *testing.T) {
t.Parallel()

helpers.CleanupTerraformFolder(t, testFixtureStacksInputs)
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)
rootPath := util.JoinPath(tmpEnvPath, testFixtureStacksInputs)

helpers.RunTerragrunt(t, "terragrunt stack run apply --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)

stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt stack run destroy --experiment stacks --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
require.NoError(t, err)

assert.Contains(t, stdout, "Plan: 0 to add, 0 to change, 1 to destroy")
assert.Contains(t, stdout, "local_file.file: Destroying...")
}

// check if the stack directory is created and contains files.
func validateStackDir(t *testing.T, path string) {
t.Helper()
assert.DirExists(t, path)

// check that path is not empty directory
entries, err := os.ReadDir(path)
require.NoError(t, err, "Failed to read directory contents")

hasSubdirectories := false
for _, entry := range entries {
if entry.IsDir() {
hasSubdirectories = true

break
}
}

assert.True(t, hasSubdirectories, "The .terragrunt-stack directory should contain at least one subdirectory")
}