diff --git a/cmd/terramate/e2etests/run_test.go b/cmd/terramate/e2etests/run_test.go index fc0b0e5e8..59c5a3b8c 100644 --- a/cmd/terramate/e2etests/run_test.go +++ b/cmd/terramate/e2etests/run_test.go @@ -18,6 +18,7 @@ import ( "fmt" "testing" + "github.com/madlambda/spells/assert" "github.com/mineiros-io/terramate" "github.com/mineiros-io/terramate/cmd/terramate/cli" @@ -463,7 +464,8 @@ func TestRunOrderNotChangedStackIgnored(t *testing.T) { stack := s.CreateStack("stack") stackMainTf := stack.CreateFile(mainTfFileName, "# some code") - stackConfig := hcl.NewConfig(terramate.DefaultVersionConstraint()) + stackConfig, err := hcl.NewConfig(stack.Path(), terramate.DefaultVersionConstraint()) + assert.NoError(t, err) stackConfig.Stack = &hcl.Stack{ After: []string{project.RelPath(s.RootDir(), stack2.Path())}, } @@ -526,7 +528,9 @@ func TestRunOrderAllChangedStacksExecuted(t *testing.T) { stack := s.CreateStack("stack") stackMainTf := stack.CreateFile(mainTfFileName, "# some code") - stackConfig := hcl.NewConfig(terramate.DefaultVersionConstraint()) + stackConfig, err := hcl.NewConfig(stack.Path(), terramate.DefaultVersionConstraint()) + assert.NoError(t, err) + stackConfig.Stack = &hcl.Stack{ After: []string{project.RelPath(s.RootDir(), stack2.Path())}, } diff --git a/hcl/hcl.go b/hcl/hcl.go index d229ebb30..a0c816d59 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "sort" "github.com/hashicorp/hcl/v2/hclparse" @@ -34,6 +35,8 @@ type Module struct { } type Config struct { + // absdir is the absolute path to the configuration directory. + absdir string Terramate *Terramate Stack *Stack } @@ -85,11 +88,58 @@ const ( ErrStackInvalidRunOrder errutil.Error = "invalid stack execution order definition" ) -func NewConfig(reqversion string) Config { +// NewConfig creates a new HCL config with dir as config directory path. +// If reqversion is provided then it also creates a terramate block with +// RequiredVersion set to this value. +func NewConfig(dir string, reqversion string) (Config, error) { + st, err := os.Stat(dir) + if err != nil { + return Config{}, fmt.Errorf("initializing config: %w", err) + } + + if !st.IsDir() { + return Config{}, fmt.Errorf("config constructor requires a directory path") + } + + var tmblock *Terramate + if reqversion != "" { + tmblock = NewTerramate(reqversion) + } + return Config{ - Terramate: &Terramate{ - RequiredVersion: reqversion, - }, + absdir: dir, + Terramate: tmblock, + }, nil +} + +// AbsDir returns the absolute path of the configuration directory. +func (c Config) AbsDir() string { return c.absdir } + +// Save the configuration file using filename inside config directory. +func (c Config) Save(filename string) (err error) { + cfgpath := filepath.Join(c.absdir, filename) + f, err := os.Create(cfgpath) + if err != nil { + return fmt.Errorf("saving configuration file %q: %w", cfgpath, err) + } + + defer func() { + err2 := f.Close() + + if err != nil { + return + } + + err = err2 + }() + + return PrintConfig(f, c) +} + +// NewTerramate creates a new TerramateBlock with reqversion. +func NewTerramate(reqversion string) *Terramate { + return &Terramate{ + RequiredVersion: reqversion, } } @@ -214,7 +264,11 @@ func Parse(fname string, data []byte) (Config, error) { ) } - var tmconfig Config + tmconfig, err := NewConfig(filepath.Dir(fname), "") + if err != nil { + return Config{}, fmt.Errorf("parsing HCL file %q: %w", fname, err) + } + var tmblock, stackblock *hclsyntax.Block var foundtm, foundstack bool @@ -365,7 +419,7 @@ func Parse(fname string, data []byte) (Config, error) { logger.Debug(). Msg("Parse stack.") tmconfig.Stack = &Stack{} - err := parseStack(tmconfig.Stack, stackblock) + err = parseStack(tmconfig.Stack, stackblock) if err != nil { return Config{}, err } diff --git a/init.go b/init.go index 9722bc6c9..0b3750dbf 100644 --- a/init.go +++ b/init.go @@ -79,14 +79,19 @@ func Init(root, dir string, force bool) error { dir, parentStack.Dir) } - logger.Trace(). - Msg("Get stack file.") + logger.Trace().Msg("Get stack file.") + stackfile := filepath.Join(dir, config.Filename) isInitialized := false - logger.Trace(). + logger = log.With(). + Str("action", "Init()"). + Str("stack", dir). Str("configFile", stackfile). - Msg("Get stack file info.") + Logger() + + logger.Trace().Msg("Get stack file info.") + st, err := os.Stat(stackfile) if err != nil { if !os.IsNotExist(err) { @@ -101,22 +106,18 @@ func Init(root, dir string, force bool) error { } if isInitialized && !force { - logger.Trace(). - Str("configFile", stackfile). - Msg("Stack is initialized annd not forced.") + logger.Trace().Msg("Stack is initialized and not forced.") + + logger.Trace().Msg("Parse version.") - logger.Trace(). - Str("configFile", stackfile). - Msg("Parse version.") vconstraint, err := parseVersion(stackfile) if err != nil { return fmt.Errorf("stack already initialized: error fetching "+ "version: %w", err) } - logger.Trace(). - Str("configFile", stackfile). - Msg("Create new constraint from version.") + logger.Trace().Msg("Create new constraint from version.") + constraint, err := hclversion.NewConstraint(vconstraint) if err != nil { return fmt.Errorf("unable to check stack constraint: %w", err) @@ -128,28 +129,20 @@ func Init(root, dir string, force bool) error { } } - logger.Debug(). - Str("configFile", stackfile). - Msg("Create stack file.") - f, err := os.Create(stackfile) + logger.Debug().Msg("Create new configuration.") + + cfg, err := hcl.NewConfig(dir, DefaultVersionConstraint()) if err != nil { - return err + return fmt.Errorf("failed to create new stack config: %w", err) } - defer f.Close() - - logger.Debug(). - Str("configFile", stackfile). - Msg("Create new configuration.") - cfg := hcl.NewConfig(DefaultVersionConstraint()) cfg.Stack = &hcl.Stack{ Name: filepath.Base(dir), } - logger.Debug(). - Str("configFile", stackfile). - Msg("Print configuration.") - err = hcl.PrintConfig(f, cfg) + logger.Debug().Msg("Save configuration.") + + err = cfg.Save(config.Filename) if err != nil { return fmt.Errorf("failed to write %q: %w", stackfile, err) } diff --git a/stack/loader.go b/stack/loader.go index 1f586a9ab..80564c522 100644 --- a/stack/loader.go +++ b/stack/loader.go @@ -72,6 +72,7 @@ func (l Loader) TryLoad(dir string) (stack S, found bool, err error) { logger.Trace(). Msg("Get relative stack path to root directory.") + stackpath := project.RelPath(l.root, dir) if s, ok := l.stacks[stackpath]; ok { return s, true, nil @@ -80,13 +81,14 @@ func (l Loader) TryLoad(dir string) (stack S, found bool, err error) { if ok := config.Exists(dir); !ok { return S{}, false, err } + fname := filepath.Join(dir, config.Filename) logger.Debug(). Str("configFile", fname). Msg("Parse config file.") - cfg, err := hcl.ParseFile(fname) + cfg, err := hcl.ParseFile(fname) if err != nil { return S{}, false, err } @@ -104,10 +106,11 @@ func (l Loader) TryLoad(dir string) (stack S, found bool, err error) { return S{}, false, fmt.Errorf("stack %q is not a leaf stack", dir) } - logger.Debug(). - Msg("Set stack path and stack config.") - l.set(stackpath, cfg.Stack) - return l.stacks[stackpath], true, nil + logger.Debug().Msg("Create a new stack") + + stack = New(l.root, cfg) + l.stacks[stack.Dir] = stack + return stack, true, nil } // TryLoadChanged is like TryLoad but sets the stack as changed if loaded @@ -128,29 +131,6 @@ func (l Loader) TryLoadChanged(root, dir string) (stack S, found bool, err error return s, ok, err } -func (l Loader) set(path string, block *hcl.Stack) { - var name string - log.Debug(). - Str("action", "set()"). - Str("stack", path). - Msg("Set stack name.") - if block.Name != "" { - name = block.Name - } else { - name = filepath.Base(path) - } - - log.Trace(). - Str("action", "set()"). - Str("stack", path). - Msg("Set stack information.") - l.stacks[path] = S{ - name: name, - Dir: path, - block: block, - } -} - // Set stacks in the loader's cache. The dir directory must be relative to // project's root. func (l Loader) Set(dir string, s S) { diff --git a/stack/stack.go b/stack/stack.go index 527f7bb29..5900c31c4 100644 --- a/stack/stack.go +++ b/stack/stack.go @@ -18,6 +18,7 @@ import ( "path/filepath" "github.com/mineiros-io/terramate/hcl" + "github.com/mineiros-io/terramate/project" "github.com/zclconf/go-cty/cty" ) @@ -27,9 +28,20 @@ type ( // Dir is the stack dir path relative to the project root Dir string - name string + // name of the stack. + name string + + // description of the stack. + description string + + // after is a list of stack paths that must run before this stack. + after []string + + // before is a list of stack paths that must run after this stack. + before []string + + // changed tells if this is a changed stack. changed bool - block *hcl.Stack } // Metadata has all metadata loaded per stack @@ -40,26 +52,47 @@ type ( } ) -func (s S) Name() string { - if s.block.Name != "" { - return s.block.Name +// New creates a new stack from configuration cfg. +func New(root string, cfg hcl.Config) S { + name := cfg.Stack.Name + if name == "" { + name = filepath.Base(cfg.AbsDir()) + } + + return S{ + name: name, + Dir: project.RelPath(root, cfg.AbsDir()), + description: cfg.Stack.Description, + after: cfg.Stack.After, + before: cfg.Stack.Before, } - return filepath.Base(s.Dir) } -func (s S) Description() string { - return s.block.Description +// Name of the stack. +func (s S) Name() string { + if s.name != "" { + return s.name + } + return s.Dir } -func (s S) After() []string { return s.block.After } -func (s S) Before() []string { return s.block.Before } +// Description of stack. +func (s S) Description() string { return s.description } + +// After specifies the list of stacks that must run before this stack. +func (s S) After() []string { return s.after } -func (s S) IsChanged() bool { return s.changed } +// Before specifies the list of stacks that must run after this stack. +func (s S) Before() []string { return s.before } + +// IsChanged tells if the stack is marked as changed. +func (s S) IsChanged() bool { return s.changed } + +// SetChanged sets the changed flag of the stack. func (s *S) SetChanged(b bool) { s.changed = b } -func (s S) String() string { - return s.Name() -} +// String representation of the stack. +func (s S) String() string { return s.Name() } // Meta returns the stack metadata. func (s S) Meta() Metadata { diff --git a/test/sandbox/sandbox.go b/test/sandbox/sandbox.go index 75cdd44b1..0ab513136 100644 --- a/test/sandbox/sandbox.go +++ b/test/sandbox/sandbox.go @@ -133,38 +133,34 @@ func (s S) BuildTree(layout []string) { gentmfile := func(relpath, data string) { attrs := strings.Split(data, ";") - tm := hcl.Config{ - Terramate: &hcl.Terramate{}, - Stack: &hcl.Stack{}, - } + cfgdir := filepath.Join(s.RootDir(), relpath) + test.MkdirAll(t, cfgdir) + cfg, err := hcl.NewConfig(cfgdir, "") + assert.NoError(t, err) + + cfg.Stack = &hcl.Stack{} + cfg.Terramate = &hcl.Terramate{} + for _, attr := range attrs { parts := strings.Split(attr, "=") name := parts[0] value := parts[1] switch name { case "version": - tm.Terramate.RequiredVersion = value + cfg.Terramate.RequiredVersion = value case "after": - tm.Stack.After = specList(t, name, value) + cfg.Stack.After = specList(t, name, value) case "before": - tm.Stack.Before = specList(t, name, value) + cfg.Stack.Before = specList(t, name, value) case "description": - tm.Stack.Description = value + cfg.Stack.Description = value default: t.Fatalf("attribute " + parts[0] + " not supported.") } } - path := filepath.Join(s.RootDir(), filepath.Join(relpath, config.Filename)) - test.MkdirAll(t, filepath.Dir(path)) - - f, err := os.Create(path) - assert.NoError(t, err, "BuildTree() failed to create file") - - defer f.Close() - - err = hcl.PrintConfig(f, tm) - assert.NoError(t, err, "BuildTree() failed to generate tm file") + assert.NoError(t, cfg.Save(config.Filename), + "BuildTree() failed to generate config file.") } for _, spec := range layout {