Skip to content

Commit

Permalink
feat: add stack.watch attribute (#448)
Browse files Browse the repository at this point in the history
* feat: add stack.watch attribute

* chore: add tests

* remove log

* add test for watch file outside project

* make sure other not changed files dont change the result

* style
  • Loading branch information
i4ki committed Jul 6, 2022
1 parent a077cef commit ddd45a1
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 11 deletions.
155 changes: 155 additions & 0 deletions cmd/terramate/e2etests/list_watch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// 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 (
"testing"

"github.com/mineiros-io/terramate/stack"
"github.com/mineiros-io/terramate/test/sandbox"
)

func TestListWatchChangedFile(t *testing.T) {
s := sandbox.New(t)

extDir := s.RootEntry().CreateDir("external")
extFile := extDir.CreateFile("file.txt", "anything")
extDir.CreateFile("not-changed.txt", "anything")

s.BuildTree([]string{
`s:stack:watch=["/external/file.txt", "/external/not-changed.txt"]`,
})

stack := s.LoadStack("stack")

cli := newCLI(t, s.RootDir())

git := s.Git()
git.CommitAll("all")
git.Push("main")
git.CheckoutNew("change-the-external")

extFile.Write("changed")
git.CommitAll("external file changed")

want := runExpected{
Stdout: stack.RelPath() + "\n",
}
assertRunResult(t, cli.listChangedStacks(), want)
}

func TestListWatchRelativeChangedFile(t *testing.T) {
s := sandbox.New(t)

extDir := s.RootEntry().CreateDir("external")
extFile := extDir.CreateFile("file.txt", "anything")

s.BuildTree([]string{
`s:stack:watch=["../external/file.txt"]`,
})

stack := s.LoadStack("stack")

cli := newCLI(t, s.RootDir())

git := s.Git()
git.CommitAll("all")
git.Push("main")
git.CheckoutNew("change-the-external")

extFile.Write("changed")
git.CommitAll("external file changed")

want := runExpected{
Stdout: stack.RelPath() + "\n",
}
assertRunResult(t, cli.listChangedStacks(), want)
}

func TestListWatchFileOutsideProject(t *testing.T) {
s := sandbox.New(t)

extDir := s.RootEntry().CreateDir("external")
extFile := extDir.CreateFile("file.txt", "anything")

s.BuildTree([]string{
`s:stack:watch=["../../this-stack-must-never-be-visible/terramate.tm.hcl"]`,
})

s.LoadStack("stack")

cli := newCLI(t, s.RootDir())

git := s.Git()
git.CommitAll("all")
git.Push("main")
git.CheckoutNew("change-the-external")

extFile.Write("changed")
git.CommitAll("external file changed")

want := runExpected{
Status: 1,
StderrRegex: string(stack.ErrInvalidWatch),
}
assertRunResult(t, cli.listChangedStacks(), want)
}

func TestListWatchNonExistentFile(t *testing.T) {
s := sandbox.New(t)

s.BuildTree([]string{
`s:stack:watch=["/external/non-existent.txt"]`,
})

cli := newCLI(t, s.RootDir())

git := s.Git()
git.CommitAll("all")
git.Push("main")
git.CheckoutNew("change-the-external")

s.RootEntry().CreateFile("test.txt", "anything")
git.CommitAll("any change")

assertRun(t, cli.listChangedStacks())
}

func TestListWatchDirectoryFails(t *testing.T) {
s := sandbox.New(t)

extDir := s.RootEntry().CreateDir("external")
extFile := extDir.CreateFile("file.txt", "anything")

s.BuildTree([]string{
`s:stack:watch=["/external"]`,
})

cli := newCLI(t, s.RootDir())

git := s.Git()
git.CommitAll("all")
git.Push("main")
git.CheckoutNew("change-the-external")

extFile.Write("changed")
git.CommitAll("external file changed")

want := runExpected{
Status: 1,
StderrRegex: string(stack.ErrInvalidWatch),
}
assertRunResult(t, cli.listChangedStacks(), want)
}
6 changes: 6 additions & 0 deletions hcl/hcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ type Stack struct {
// Wants is a list of non-duplicated stack entries that must be selected
// whenever the current stack is selected.
Wants []string

// Watch is a list of files to be watched for changes.
Watch []string
}

// GenHCLBlock represents a parsed generate_hcl block.
Expand Down Expand Up @@ -973,6 +976,9 @@ func parseStack(stack *Stack, stackblock *ast.Block) error {
case "wants":
errs.Append(assignSet(attr.Name, &stack.Wants, attrVal))

case "watch":
errs.Append(assignSet(attr.Name, &stack.Watch, attrVal))

case "description":
logger.Trace().Msg("parsing stack description.")
if attrVal.Type() != cty.String {
Expand Down
2 changes: 2 additions & 0 deletions hcl/hcl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ func TestHCLParserTerramateBlocksMerging(t *testing.T) {
after = ["after"]
before = ["before"]
wants = ["wants"]
watch = ["watch"]
}
`,
},
Expand All @@ -616,6 +617,7 @@ func TestHCLParserTerramateBlocksMerging(t *testing.T) {
After: []string{"after"},
Before: []string{"before"},
Wants: []string{"wants"},
Watch: []string{"watch"},
},
},
},
Expand Down
16 changes: 10 additions & 6 deletions hcl/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ func PrintConfig(w io.Writer, cfg Config) error {
stackBlock := rootBody.AppendNewBlock("stack", nil)
stackBody := stackBlock.Body()

if stack.Name != "" {
stackBody.SetAttributeValue("name", cty.StringVal(stack.Name))
}

if stack.Description != "" {
stackBody.SetAttributeValue("description", cty.StringVal(stack.Description))
}

if len(stack.After) > 0 {
stackBody.SetAttributeValue("after", cty.SetVal(listToValue(stack.After)))
}
Expand All @@ -90,12 +98,8 @@ func PrintConfig(w io.Writer, cfg Config) error {
stackBody.SetAttributeValue("wants", cty.SetVal(listToValue(stack.Wants)))
}

if stack.Name != "" {
stackBody.SetAttributeValue("name", cty.StringVal(stack.Name))
}

if stack.Description != "" {
stackBody.SetAttributeValue("description", cty.StringVal(stack.Description))
if len(stack.Watch) > 0 {
stackBody.SetAttributeValue("watch", cty.SetVal(listToValue(stack.Watch)))
}

if id, ok := stack.ID.Value(); ok {
Expand Down
37 changes: 35 additions & 2 deletions manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func (m *Manager) ListChanged() (*StacksReport, error) {

logger.Debug().Msg("List changed files.")

files, err := listChangedFiles(m.root, m.gitBaseRef)
changedFiles, err := listChangedFiles(m.root, m.gitBaseRef)
if err != nil {
return nil, errors.E(errListChanged, err)
}
Expand All @@ -157,7 +157,7 @@ func (m *Manager) ListChanged() (*StacksReport, error) {

logger.Trace().
Msg("Range over files.")
for _, path := range files {
for _, path := range changedFiles {
if strings.HasPrefix(path, ".") {
continue
}
Expand Down Expand Up @@ -207,12 +207,34 @@ func (m *Manager) ListChanged() (*StacksReport, error) {

logger.Trace().Msg("Range over all stacks.")

rangeStacks:
for _, stackEntry := range allstacks {
stack := stackEntry.Stack
if _, ok := stackSet[stack.Path()]; ok {
continue
}

logger.Debug().
Stringer("stack", stack).
Msg("Check for changed watch files.")

if changed, ok := hasChangedWatchedFiles(stack, changedFiles); ok {
logger.Debug().
Stringer("stack", stack).
Str("watchfile", changed).
Msg("changed.")

stack.SetChanged(true)
stackSet[stack.Path()] = Entry{
Stack: stack,
Reason: fmt.Sprintf(
"stack changed because watched file %q changed",
changed,
),
}
continue rangeStacks
}

logger.Debug().
Stringer("stack", stack).
Msg("Apply function to stack.")
Expand Down Expand Up @@ -550,6 +572,17 @@ func listChangedFiles(dir string, gitBaseRef string) ([]string, error) {
return g.DiffNames(baseRef, headRef)
}

func hasChangedWatchedFiles(stack stack.S, changedFiles []string) (string, bool) {
for _, watchFile := range stack.Watch() {
for _, file := range changedFiles {
if file == watchFile[1:] { // project paths
return watchFile, true
}
}
}
return "", false
}

func checkRepoIsClean(g *git.Git) (RepoChecks, error) {
logger := log.With().
Str("action", "checkRepoIsClean()").
Expand Down

0 comments on commit ddd45a1

Please sign in to comment.