Skip to content

Commit

Permalink
Stacks: run (#3762)
Browse files Browse the repository at this point in the history
* Add run cli command

* added functions to validate terragrunt stack directory

* stack run execution

* add generate execution

* stacks run test

* stcks tests

* Improve basic test to generate local files

* terraform source update

* updated text file generation

* collection of text files

* stack inputs

* Add basic apply test

* Add stack destroy flag

* Add integration test for destroy and plan

* Add helper marker

* Lint fixes

* name update

* description update
  • Loading branch information
denis256 authored Jan 16, 2025
1 parent 3385071 commit 2457563
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 4 deletions.
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)
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)

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")
}

0 comments on commit 2457563

Please sign in to comment.