diff --git a/cmd/terramate/cli/cli.go b/cmd/terramate/cli/cli.go index 2ee063a67..e73f57cb3 100644 --- a/cmd/terramate/cli/cli.go +++ b/cmd/terramate/cli/cli.go @@ -73,6 +73,14 @@ type cliSpec struct { DisableCheckGitUntracked bool `optional:"true" default:"false" help:"Disable git check for untracked files"` DisableCheckGitUncommitted bool `optional:"true" default:"false" help:"Disable git check for uncommitted files"` + Create struct { + Path string `arg:"" name:"path" help:"Path of the new stack relative to the working dir"` + ID string `help:"ID of the stack, defaults to UUID"` + Name string `help:"Name of the stack, defaults to stack dir base name"` + Description string `help:"Description of the stack, defaults to the stack name"` + Import []string `help:"Add import block for the given path on the stack"` + } `cmd:"" help:"Creates a stack on the project"` + Fmt struct { Check bool `help:"Lists unformatted files, exit with 0 if all is formatted, 1 otherwise"` } `cmd:"" help:"Format all files inside dir recursively"` @@ -330,6 +338,8 @@ func (c *cli) run() { switch c.ctx.Command() { case "fmt": c.format() + case "create ": + c.createStack() case "list": c.printStacks() case "run": @@ -479,6 +489,31 @@ func (c *cli) listStacks(mgr *terramate.Manager, isChanged bool) (*terramate.Sta return mgr.List() } +func (c *cli) createStack() { + logger := log.With(). + Str("workingDir", c.wd()). + Str("action", "cli.createStack()"). + Str("imports", fmt.Sprint(c.parsedArgs.Create.Import)). + Logger() + + logger.Trace().Msg("creating stack") + + stackDir := filepath.Join(c.wd(), c.parsedArgs.Create.Path) + err := stack.Create(c.root(), stack.CreateCfg{ + Dir: stackDir, + ID: c.parsedArgs.Create.ID, + Name: c.parsedArgs.Create.Name, + Description: c.parsedArgs.Create.Description, + Imports: c.parsedArgs.Create.Import, + }) + + if err != nil { + logger.Fatal().Err(err).Msg("creating stack") + } + + c.log("Created stack %s with success", c.parsedArgs.Create.Path) +} + func (c *cli) format() { logger := log.With(). Str("workingDir", c.wd()). diff --git a/cmd/terramate/e2etests/create_test.go b/cmd/terramate/e2etests/create_test.go new file mode 100644 index 000000000..b83ffe417 --- /dev/null +++ b/cmd/terramate/e2etests/create_test.go @@ -0,0 +1,89 @@ +// Copyright 2022 Mineiros GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2etest + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/madlambda/spells/assert" + "github.com/mineiros-io/terramate/test" + "github.com/mineiros-io/terramate/test/sandbox" +) + +func TestCreateStack(t *testing.T) { + s := sandbox.New(t) + cli := newCLI(t, s.RootDir()) + + const ( + stackID = "stack-id" + stackName = "stack name" + stackDescription = "stack description" + stackImport1 = "/core/file1.tm.hcl" + stackImport2 = "/core/file2.tm.hcl" + ) + + createFile := func(path string) { + abspath := filepath.Join(s.RootDir(), path) + test.WriteFile(t, filepath.Dir(abspath), filepath.Base(abspath), "") + } + + createFile(stackImport1) + createFile(stackImport2) + + stackPaths := []string{ + "stack-1", + "/stack-2", + "/stacks/stack-a", + "stacks/stack-b", + } + + for _, stackPath := range stackPaths { + res := cli.run("create", stackPath, + "--id", stackID, + "--name", stackName, + "--description", stackDescription, + "--import", stackImport1, + "--import", stackImport2, + ) + + assertRunResult(t, res, runExpected{ + Stdout: fmt.Sprintf("Created stack %s with success\n", stackPath), + }) + + got := s.LoadStack(stackPath) + + gotID, _ := got.ID() + assert.EqualStrings(t, stackID, gotID) + assert.EqualStrings(t, stackName, got.Name(), "checking stack name") + assert.EqualStrings(t, stackDescription, got.Desc(), "checking stack description") + + test.AssertStackImports(t, s.RootDir(), got, []string{stackImport1, stackImport2}) + } +} + +func TestCreateStackDefaults(t *testing.T) { + s := sandbox.New(t) + cli := newCLI(t, s.RootDir()) + cli.run("create", "stack") + + got := s.LoadStack("stack") + + assert.EqualStrings(t, "stack", got.Name(), "checking stack name") + assert.EqualStrings(t, "stack", got.Desc(), "checking stack description") + + test.AssertStackImports(t, s.RootDir(), got, []string{}) +} diff --git a/stack/create.go b/stack/create.go index ca2c4ce41..00c1a7c5b 100644 --- a/stack/create.go +++ b/stack/create.go @@ -62,7 +62,7 @@ type CreateCfg struct { // // If the stack already exists it will return an error and no changes will be // made to the stack. -func Create(rootdir string, cfg CreateCfg) error { +func Create(rootdir string, cfg CreateCfg) (err error) { const stackFilename = "stack.tm.hcl" logger := log.With(). @@ -90,7 +90,7 @@ func Create(rootdir string, cfg CreateCfg) error { logger.Trace().Msg("validating create configuration") - _, err := os.Stat(filepath.Join(cfg.Dir, stackFilename)) + _, err = os.Stat(filepath.Join(cfg.Dir, stackFilename)) if err == nil { // Even if there is no stack inside the file, we can't overwrite // the user file anyway. @@ -147,10 +147,15 @@ func Create(rootdir string, cfg CreateCfg) error { if err != nil { return errors.E(err, "opening stack file") } + defer func() { - err := stackFile.Close() - if err != nil { - logger.Error().Err(err).Msg("closing stack file") + errClose := stackFile.Close() + if errClose != nil { + if err != nil { + err = errors.L(err, errClose) + } else { + err = errClose + } } }() @@ -158,6 +163,10 @@ func Create(rootdir string, cfg CreateCfg) error { return errors.E(err, "writing stack imports to stack file") } + if len(cfg.Imports) > 0 { + fmt.Fprint(stackFile, "\n") + } + return hcl.PrintImports(stackFile, cfg.Imports) } diff --git a/stack/create_test.go b/stack/create_test.go index 007481842..fa1fec9db 100644 --- a/stack/create_test.go +++ b/stack/create_test.go @@ -236,7 +236,7 @@ func TestStackCreation(t *testing.T) { assert.EqualStrings(t, want.name, got.Name(), "checking stack name") assert.EqualStrings(t, want.desc, got.Desc(), "checking stack description") - assertStackImports(t, s.RootDir(), got, want.imports) + test.AssertStackImports(t, s.RootDir(), got, want.imports) }) } } @@ -250,40 +250,6 @@ func buildImportedFiles(t *testing.T, rootdir string, imports []string) { } } -func assertStackImports(t *testing.T, rootdir string, got stack.S, want []string) { - t.Helper() - - parser, err := hcl.NewTerramateParser(rootdir, got.HostPath()) - assert.NoError(t, err) - - err = parser.AddDir(got.HostPath()) - assert.NoError(t, err) - - err = parser.MinimalParse() - assert.NoError(t, err) - - imports, err := parser.Imports() - assert.NoError(t, err) - - if len(imports) != len(want) { - t.Fatalf("got %d imports, wanted %v", len(imports), want) - } - -checkImports: - for _, wantImport := range want { - for _, gotImportBlock := range imports { - sourceVal, diags := gotImportBlock.Attributes["source"].Expr.Value(nil) - if diags.HasErrors() { - t.Fatalf("error %v evaluating import source attribute", diags) - } - if sourceVal.AsString() == wantImport { - continue checkImports - } - } - t.Errorf("wanted import %s not found", wantImport) - } -} - func TestStackCreationFailsOnRelativePath(t *testing.T) { s := sandbox.New(t) diff --git a/test/stack.go b/test/stack.go new file mode 100644 index 000000000..fde4a822a --- /dev/null +++ b/test/stack.go @@ -0,0 +1,60 @@ +// Copyright 2022 Mineiros GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "testing" + + "github.com/madlambda/spells/assert" + "github.com/mineiros-io/terramate/hcl" + "github.com/mineiros-io/terramate/stack" +) + +// AssertStackImports checks that the given stack has all the wanted import +// definitions. The wanted imports is a slice of the sources that are imported +// on each block. +func AssertStackImports(t *testing.T, rootdir string, got stack.S, want []string) { + t.Helper() + + parser, err := hcl.NewTerramateParser(rootdir, got.HostPath()) + assert.NoError(t, err) + + err = parser.AddDir(got.HostPath()) + assert.NoError(t, err) + + err = parser.MinimalParse() + assert.NoError(t, err) + + imports, err := parser.Imports() + assert.NoError(t, err) + + if len(imports) != len(want) { + t.Fatalf("got %d imports, wanted %v", len(imports), want) + } + +checkImports: + for _, wantImport := range want { + for _, gotImportBlock := range imports { + sourceVal, diags := gotImportBlock.Attributes["source"].Expr.Value(nil) + if diags.HasErrors() { + t.Fatalf("error %v evaluating import source attribute", diags) + } + if sourceVal.AsString() == wantImport { + continue checkImports + } + } + t.Errorf("wanted import %s not found", wantImport) + } +}