Skip to content

Commit

Permalink
feat: add terramate create <path> (#449)
Browse files Browse the repository at this point in the history
  • Loading branch information
katcipis committed Jul 6, 2022
1 parent b9fa8be commit a077cef
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 40 deletions.
35 changes: 35 additions & 0 deletions cmd/terramate/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -330,6 +338,8 @@ func (c *cli) run() {
switch c.ctx.Command() {
case "fmt":
c.format()
case "create <path>":
c.createStack()
case "list":
c.printStacks()
case "run":
Expand Down Expand Up @@ -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()).
Expand Down
89 changes: 89 additions & 0 deletions cmd/terramate/e2etests/create_test.go
Original file line number Diff line number Diff line change
@@ -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{})
}
19 changes: 14 additions & 5 deletions stack/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -147,17 +147,26 @@ 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
}
}
}()

if err := hcl.PrintConfig(stackFile, tmCfg); err != nil {
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)
}

Expand Down
36 changes: 1 addition & 35 deletions stack/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Expand All @@ -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)

Expand Down
60 changes: 60 additions & 0 deletions test/stack.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit a077cef

Please sign in to comment.