From ab5eca00dd0a1a09105f599b7ea412f5b75b327a Mon Sep 17 00:00:00 2001 From: Tiago Natel Date: Thu, 6 Jul 2023 12:11:57 +0100 Subject: [PATCH 1/4] feat: experimental ensure-stack-id command --- cmd/terramate/cli/cli.go | 53 ++++++++++-- .../e2etests/exp_ensure_stack_id_test.go | 83 +++++++++++++++++++ stack/clone.go | 58 ++++++------- 3 files changed, 158 insertions(+), 36 deletions(-) create mode 100644 cmd/terramate/e2etests/exp_ensure_stack_id_test.go diff --git a/cmd/terramate/cli/cli.go b/cmd/terramate/cli/cli.go index 659fa0ffc..b8f3f6856 100644 --- a/cmd/terramate/cli/cli.go +++ b/cmd/terramate/cli/cli.go @@ -203,6 +203,9 @@ type cliSpec struct { Info struct { } `cmd:"" help:"cloud information status"` } `cmd:"" help:"Terramate Cloud commands"` + + EnsureStackID struct { + } `cmd:"" help:"generate stack.id for all stacks which does not define it"` } `cmd:"" help:"Experimental features (may change or be removed in the future)"` } @@ -548,6 +551,8 @@ func (c *cli) run() { c.getConfigValue() case "experimental cloud info": c.cloudInfo() + case "experimental ensure-stack-id": + c.ensureStackID() default: log.Fatal().Msg("unexpected command sequence") } @@ -914,14 +919,33 @@ func (c *cli) gitSafeguardDefaultBranchIsReachable() { } func (c *cli) listStacks(mgr *stack.Manager, isChanged bool) (*stack.Report, error) { + var ( + err error + report *stack.Report + ) + if isChanged { log.Trace(). Str("action", "listStacks()"). Str("workingDir", c.wd()). - Msg("`Changed` flag was set. List changed stacks.") - return mgr.ListChanged() + Msg("Listing changed stacks") + + report, err = mgr.ListChanged() + } else { + log.Trace(). + Str("action", "listStacks()"). + Str("workingDir", c.wd()). + Msg("Listing all stacks") + + report, err = mgr.List() + } + + if err != nil { + return nil, err } - return mgr.List() + + c.prj.git.repoChecks = report.Checks + return report, nil } func (c *cli) createStack() { @@ -1073,7 +1097,6 @@ func (c *cli) printStacks() { fatal(err, "listing stacks") } - c.prj.git.repoChecks = report.Checks c.gitFileSafeguards(false) for _, entry := range c.filterStacks(report.Stacks) { @@ -1434,6 +1457,27 @@ func (c *cli) checkGenCode() bool { return true } +func (c *cli) ensureStackID() { + mgr := stack.NewManager(c.cfg(), c.prj.baseRef) + report, err := c.listStacks(mgr, false) + if err != nil { + fatal(err, "listing stacks") + } + + for _, entry := range report.Stacks { + if entry.Stack.ID != "" { + continue + } + + id, err := stack.UpdateStackID(entry.Stack.HostDir(c.cfg())) + if err != nil { + fatal(err, "failed to update stack.id of stack %s", entry.Stack.Dir) + } + + c.output.MsgStdOut("Generated ID %s for stack %s", id, entry.Stack.Dir) + } +} + func (c *cli) eval() { ctx := c.setupEvalContext(c.parsedArgs.Experimental.Eval.Global) for _, exprStr := range c.parsedArgs.Experimental.Eval.Exprs { @@ -1759,7 +1803,6 @@ func (c *cli) computeSelectedStacks(ensureCleanRepo bool) (config.List[*config.S return nil, err } - c.prj.git.repoChecks = report.Checks c.gitFileSafeguards(ensureCleanRepo) logger.Trace().Msg("Filter stacks by working directory.") diff --git a/cmd/terramate/e2etests/exp_ensure_stack_id_test.go b/cmd/terramate/e2etests/exp_ensure_stack_id_test.go new file mode 100644 index 000000000..2400b0fcf --- /dev/null +++ b/cmd/terramate/e2etests/exp_ensure_stack_id_test.go @@ -0,0 +1,83 @@ +// Copyright 2023 Terramate GmbH +// SPDX-License-Identifier: MPL-2.0 + +package e2etest + +import ( + "path/filepath" + "testing" + + "github.com/terramate-io/terramate/test/sandbox" +) + +func TestEnsureStackID(t *testing.T) { + type testcase struct { + name string + layout []string + wd string + } + + for _, tc := range []testcase{ + { + name: "single stack at root with id", + layout: []string{ + `s:.:id=test`, + }, + }, + { + name: "single stack at root without id", + layout: []string{ + `s:.`, + }, + }, + { + name: "single stack at root without id but wd not at root", + layout: []string{ + `d:some/deep/dir/for/test`, + `s:.`, + }, + wd: `/some/deep/dir/for/test`, + }, + { + name: "mix of multiple stacks with and without id", + layout: []string{ + `s:s1`, + `s:s1/a1:id=test`, + `s:s2`, + `s:s3/a3:id=test2`, + `s:s3/a1`, + `s:a/b/c/d/e/f/g/h/stack`, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + testEnsureStackID(t, tc.wd, tc.layout) + }) + } +} + +func testEnsureStackID(t *testing.T, wd string, layout []string) { + s := sandbox.New(t) + s.BuildTree(layout) + if wd == "" { + wd = s.RootDir() + } else { + wd = filepath.Join(s.RootDir(), filepath.FromSlash(wd)) + } + tm := newCLI(t, wd) + assertRunResult( + t, + tm.run("experimental", "ensure-stack-id"), + runExpected{ + Status: 0, + IgnoreStdout: true, + }, + ) + + s.ReloadConfig() + for _, stackElem := range s.LoadStacks() { + if stackElem.ID == "" { + t.Fatalf("stack.id not generated for stack %s", stackElem.Dir()) + } + } +} diff --git a/stack/clone.go b/stack/clone.go index e65988f12..8efceb2ef 100644 --- a/stack/clone.go +++ b/stack/clone.go @@ -73,7 +73,7 @@ func Clone(root *config.Root, destdir, srcdir string) error { } logger.Trace().Msg("stack has ID, updating ID of the cloned stack") - err = updateStackID(destdir) + _, err = UpdateStackID(destdir) if err != nil { return err } @@ -85,7 +85,7 @@ func filterDotFiles(_ string, entry os.DirEntry) bool { return !strings.HasPrefix(entry.Name(), ".") } -func updateStackID(stackdir string) error { +func UpdateStackID(stackdir string) (string, error) { logger := log.With(). Str("action", "stack.updateStackID()"). Str("stack", stackdir). @@ -95,78 +95,74 @@ func updateStackID(stackdir string) error { parser, err := hcl.NewTerramateParser(stackdir, stackdir) if err != nil { - return err + return "", err } if err := parser.AddDir(stackdir); err != nil { - return err + return "", err } if err := parser.Parse(); err != nil { - return err + return "", err } logger.Trace().Msg("finding file containing stack definition") stackFilePath := getStackFilepath(parser) if stackFilePath == "" { - return errors.E("cloned stack does not have a stack block") + return "", errors.E("stack does not have a stack block") } // Parsing HCL always delivers an AST that // has no comments on it, so building a new HCL file from the parsed // AST will lose all comments from the original code. - logger.Trace().Msg("reading cloned stack file") + logger.Trace().Msg("reading stack file") stackContents, err := os.ReadFile(stackFilePath) if err != nil { - return errors.E(err, "reading cloned stack definition file") + return "", errors.E(err, "reading stack definition file") } - logger.Trace().Msg("parsing cloned stack file") + logger.Trace().Msg("parsing stack file") parsed, diags := hclwrite.ParseConfig([]byte(stackContents), stackFilePath, hhcl.InitialPos) if diags.HasErrors() { - return errors.E(diags, "parsing cloned stack configuration") + return "", errors.E(diags, "parsing stack configuration") } blocks := parsed.Body().Blocks() logger.Trace().Msg("searching for stack ID attribute") -updateStackID: for _, block := range blocks { if block.Type() != hcl.StackBlockType { continue } - body := block.Body() - attrs := body.Attributes() - for name := range attrs { - if name != "id" { - continue - } + uuid, err := uuid.NewRandom() + if err != nil { + return "", errors.E(err, "creating new ID for stack") + } - id, err := uuid.NewRandom() - if err != nil { - return errors.E(err, "creating new ID for cloned stack") - } + id := uuid.String() - logger.Trace(). - Str("newID", id.String()). - Msg("found stack ID attribute, updating") + body := block.Body() + body.SetAttributeValue("id", cty.StringVal(id)) - body.SetAttributeValue(name, cty.StringVal(id.String())) - break updateStackID + logger.Trace().Msg("saving updated file") + + // TODO(i4k): improve this. + // Since we just created the stack files they have the default + // permissions given by Go on os.Create, 0666. + err = os.WriteFile(stackFilePath, parsed.Bytes(), 0666) + if err != nil { + return "", err } + return id, nil } - logger.Trace().Msg("saving updated file") - - // Since we just created the clones stack files they have the default - // permissions given by Go on os.Create, 0666. - return os.WriteFile(stackFilePath, parsed.Bytes(), 0666) + return "", errors.E("stack block not found") } func getStackFilepath(parser *hcl.TerramateParser) string { From 1d8609b90d6b471b8e609b8af433a789835952e0 Mon Sep 17 00:00:00 2001 From: Tiago Natel Date: Thu, 6 Jul 2023 12:17:14 +0100 Subject: [PATCH 2/4] chore: add docs --- stack/clone.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stack/clone.go b/stack/clone.go index 8efceb2ef..7fb295ac8 100644 --- a/stack/clone.go +++ b/stack/clone.go @@ -85,6 +85,9 @@ func filterDotFiles(_ string, entry os.DirEntry) bool { return !strings.HasPrefix(entry.Name(), ".") } +// UpdateStackID updates the stack.id of the given stack directory. +// The functions updates just the file which defines the stack block. +// The updated file will lose all comments. func UpdateStackID(stackdir string) (string, error) { logger := log.With(). Str("action", "stack.updateStackID()"). From c1a42b4a2e04f88a8aec1cd993ec58bed3fdc53a Mon Sep 17 00:00:00 2001 From: Tiago Natel Date: Thu, 6 Jul 2023 12:27:00 +0100 Subject: [PATCH 3/4] chore: improve file saving --- stack/clone.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/stack/clone.go b/stack/clone.go index 7fb295ac8..3f22ca5a8 100644 --- a/stack/clone.go +++ b/stack/clone.go @@ -116,6 +116,13 @@ func UpdateStackID(stackdir string) (string, error) { return "", errors.E("stack does not have a stack block") } + st, err := os.Lstat(stackFilePath) + if err != nil { + return "", errors.E(err, "stating the stack file") + } + + originalFileMode := st.Mode() + // Parsing HCL always delivers an AST that // has no comments on it, so building a new HCL file from the parsed // AST will lose all comments from the original code. @@ -155,10 +162,7 @@ func UpdateStackID(stackdir string) (string, error) { logger.Trace().Msg("saving updated file") - // TODO(i4k): improve this. - // Since we just created the stack files they have the default - // permissions given by Go on os.Create, 0666. - err = os.WriteFile(stackFilePath, parsed.Bytes(), 0666) + err = os.WriteFile(stackFilePath, parsed.Bytes(), originalFileMode) if err != nil { return "", err } From 05bbd0aa06891c967a7a054a9ac89d49ad0532b9 Mon Sep 17 00:00:00 2001 From: Tiago Natel Date: Thu, 6 Jul 2023 12:33:27 +0100 Subject: [PATCH 4/4] chore: improve cmdline help --- cmd/terramate/cli/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/terramate/cli/cli.go b/cmd/terramate/cli/cli.go index b8f3f6856..d8a2b5ab8 100644 --- a/cmd/terramate/cli/cli.go +++ b/cmd/terramate/cli/cli.go @@ -205,7 +205,7 @@ type cliSpec struct { } `cmd:"" help:"Terramate Cloud commands"` EnsureStackID struct { - } `cmd:"" help:"generate stack.id for all stacks which does not define it"` + } `cmd:"" help:"generate an UUID for the stack.id of all stacks which does not define it"` } `cmd:"" help:"Experimental features (may change or be removed in the future)"` }