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

WIP: TG stacks #3627

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4cb4199
Unit struct update
denis256 Dec 5, 2024
d575b6e
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 6, 2024
b7530ca
Add stack parsing
denis256 Dec 6, 2024
47cd721
Add cty serialization
denis256 Dec 6, 2024
4641420
stock go
denis256 Dec 9, 2024
558f3aa
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 10, 2024
97ec9e5
Stack config path
denis256 Dec 10, 2024
1a966ab
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 11, 2024
78a67b1
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 12, 2024
0a1be85
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 13, 2024
01ecf5d
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 16, 2024
7c9f8bd
unit struct clone
denis256 Dec 16, 2024
8928af0
stack cli
denis256 Dec 16, 2024
9d088fb
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 16, 2024
82dde7a
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 18, 2024
bd61393
stack cli update
denis256 Dec 18, 2024
fd83fb4
Stack cli commands
denis256 Dec 18, 2024
14d2088
stack update
denis256 Dec 19, 2024
472aeb9
action update
denis256 Dec 19, 2024
def9b27
Add cli flags
denis256 Dec 19, 2024
f32fb86
stack command
denis256 Dec 19, 2024
46f57c2
tg generate
denis256 Dec 19, 2024
7e3fd2a
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 20, 2024
65dc02d
Config parse update
denis256 Dec 20, 2024
bf26ab2
stacks parsing
denis256 Dec 20, 2024
231303f
stack config file parsing
denis256 Dec 20, 2024
a532787
update terragrunt path
denis256 Dec 20, 2024
ed16d24
action config update
denis256 Dec 20, 2024
b050d2d
symbol links
denis256 Dec 20, 2024
f76ae52
Units parsing
denis256 Dec 20, 2024
8542090
Symbol link issues
denis256 Dec 20, 2024
a96e5cb
Merge remote-tracking branch 'origin/main' into tg-432-stack-support
denis256 Dec 31, 2024
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
3 changes: 3 additions & 0 deletions cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
206 changes: 206 additions & 0 deletions cli/commands/stack/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package stack

import (
"context"
"fmt"
"io"
"net/url"
"os"
"path/filepath"

"github.com/gruntwork-io/terragrunt/config"
getter "github.com/hashicorp/go-getter"

"github.com/gruntwork-io/terragrunt/internal/errors"
"github.com/gruntwork-io/terragrunt/options"
)

const (
generate = "generate"
stackCacheDir = ".terragrunt-stack"
)

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 {
//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, opts, stackFile); err != nil {
return err
}

return 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(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))
}

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)

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

return nil
}
27 changes: 27 additions & 0 deletions cli/commands/stack/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +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 cli.Flags{}
}

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 {
command := ctx.Args().Get(0)
return Run(ctx.Context, opts.OptionsFromContext(ctx), command)
},
}
}
7 changes: 4 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,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"

Expand Down
35 changes: 35 additions & 0 deletions config/stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +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
}
51 changes: 51 additions & 0 deletions config/unit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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,
}
}
2 changes: 2 additions & 0 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions test/fixtures/stacks/basic/terragrunt.stack.hcl
Original file line number Diff line number Diff line change
@@ -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"
}

Empty file.
Empty file.
Empty file.
Empty file.