diff --git a/cli/generate.go b/cli/generate.go index 7f34ec2..58724d0 100644 --- a/cli/generate.go +++ b/cli/generate.go @@ -9,9 +9,9 @@ import ( type GenerateCommand struct { fs *flag.FlagSet - file string - glob string - check bool + file string + recursive bool + check bool } // NewGenerateCommand sub-command to generate files @@ -20,8 +20,8 @@ func NewGenerateCommand() *GenerateCommand { fs: flag.NewFlagSet("generate", flag.ContinueOnError), } - c.fs.StringVar(&c.file, "file", "", "path to generator file") - c.fs.StringVar(&c.glob, "glob", "", "glob used to find generator files") + c.fs.StringVar(&c.file, "file", "tf-generator.hcl", "path to generator file") + c.fs.BoolVar(&c.recursive, "recursive", false, "search recursively for all files match generator file name") c.fs.BoolVar(&c.check, "check", false, "only check if file is up-to-date, do not update it") return c @@ -43,15 +43,9 @@ func (c *GenerateCommand) Init(args []string) error { } func (c *GenerateCommand) Run() error { - if c.glob != "" && c.file != "" { - return fmt.Errorf("cannot specify --file and --glob") - } - if c.glob != "" { - return generate.RunGlob(c.glob, c.check) + if c.recursive { + return generate.RunRecursive(c.file, c.check) } else { - if c.file == "" { - c.file = "tf-generator.hcl" - } return generate.Run(c.file, c.check) } } diff --git a/default.nix b/default.nix index 6cb97b4..c24afa6 100644 --- a/default.nix +++ b/default.nix @@ -5,7 +5,7 @@ final: prev: src = ./.; - vendorHash = "sha256-TIDg83EHqgzL9QOOsgGPm6f50qvnFbZPHwVo6pBaPQQ="; + vendorHash = "sha256-InRf1tpAVo2x6IFUmqERfRcT9P0qAd3lEvZ1u5Nfbn4="; meta = { description = "Simple generator for Terraform/OpenTofu"; diff --git a/fixtures/invalid/nested-folder-with-error/a/b/c/locals.tfvars b/fixtures/invalid/nested-folder-with-error/a/b/c/locals.tfvars new file mode 100644 index 0000000..c91e43a --- /dev/null +++ b/fixtures/invalid/nested-folder-with-error/a/b/c/locals.tfvars @@ -0,0 +1 @@ +invalid { \ No newline at end of file diff --git a/fixtures/invalid/nested-folder-with-error/a/b/c/tf-generator.hcl b/fixtures/invalid/nested-folder-with-error/a/b/c/tf-generator.hcl new file mode 100644 index 0000000..295314c --- /dev/null +++ b/fixtures/invalid/nested-folder-with-error/a/b/c/tf-generator.hcl @@ -0,0 +1,6 @@ +generate { + content = merge-tfvars([ + load("locals.tfvars"), + ]) + output = "tf-generator.tfvars" +} diff --git a/generate/run.go b/generate/run.go index 1d2589f..ebaddc3 100644 --- a/generate/run.go +++ b/generate/run.go @@ -2,8 +2,7 @@ package generate import ( "fmt" - - "github.com/yargevad/filepathx" + "os" ) // Run main entry point for the `generate` command @@ -37,8 +36,13 @@ func Run(filePath string, check bool) error { return nil } -func RunGlob(glob string, check bool) error { - files, err := filepathx.Glob(glob) +func RunRecursive(filename string, check bool) error { + if hasDirs(filename) { + return fmt.Errorf("file name %s cannot contain folders", filename) + } + + cwd, _ := os.Getwd() + files, err := findFilesByName(cwd, filename) if err != nil { return err } diff --git a/generate/util.go b/generate/util.go new file mode 100644 index 0000000..2afcae3 --- /dev/null +++ b/generate/util.go @@ -0,0 +1,35 @@ +package generate + +import ( + "os" + "path/filepath" +) + +// findFilesByName finds all files with a specific name in a directory and its subdirectories +func findFilesByName(directory, filename string) ([]string, error) { + var matches []string + + err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if info.Name() == filename { + matches = append(matches, path) + } + return nil + }) + + if err != nil { + return nil, err + } + return matches, nil +} + +// hasDirs check if the file path specifies any directories +func hasDirs(filePath string) bool { + dir, _ := filepath.Split(filePath) + return dir != "" +} diff --git a/generate/util_test.go b/generate/util_test.go new file mode 100644 index 0000000..4baa8aa --- /dev/null +++ b/generate/util_test.go @@ -0,0 +1,37 @@ +package generate + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type HasDirsFixture struct { + filePath string + hasDirs bool +} + +func TestHasDirsFixtures(t *testing.T) { + for _, fixture := range []HasDirsFixture{ + { + filePath: "test.txt", + hasDirs: false, + }, + { + filePath: "abc", + hasDirs: false, + }, + { + filePath: "./test.txt", + hasDirs: true, + }, + { + filePath: "test/test.txt", + hasDirs: true, + }, + } { + t.Run(fixture.filePath, func(t *testing.T) { + assert.Equal(t, fixture.hasDirs, hasDirs(fixture.filePath)) + }) + } +} diff --git a/go.mod b/go.mod index d446e51..29ec0b9 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/google/go-cmp v0.5.8 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/yargevad/filepathx v1.0.0 // indirect golang.org/x/text v0.11.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 00790ea..23c9ece 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NF github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= -github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/zclconf/go-cty v1.14.2 h1:kTG7lqmBou0Zkx35r6HJHUQTvaRPr5bIAf3AoHS0izI= github.com/zclconf/go-cty v1.14.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= diff --git a/main_test.go b/main_test.go index 73f9d7a..d75ca0b 100644 --- a/main_test.go +++ b/main_test.go @@ -11,56 +11,91 @@ import ( ) type ValidFixture struct { + cwd string args []string } type InvalidFixture struct { + cwd string args []string expectedMessageContains string isDiag bool } +func withCwd(t *testing.T, newDir string, fn func()) { + // Save current working directory + oldDir, err := os.Getwd() + if err != nil { + t.Fatalf("error getting current working directory: %v", err) + } + + // Change cwd temporarily + if err := os.Chdir(newDir); err != nil { + t.Fatalf("error changing directory to %s: %v", newDir, err) + } + + fn() + + // Restore original cwd + if err := os.Chdir(oldDir); err != nil { + t.Fatalf("error restoring original directory: %v", err) + } +} + func TestValidFixtures(t *testing.T) { for _, fixture := range []ValidFixture{ { + cwd: ".", args: []string{"--file", "examples/basics-01-load-and-combine/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "examples/basics-02-locals/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "examples/basics-03-merge-tfvars/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "examples/basics-04-remove-tfvar-keys/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "examples/basics-05-combine-with-inject/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "fixtures/valid/empty/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "fixtures/valid/empty-tfvars/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "fixtures/valid/duplicate-includes/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "fixtures/valid/include-file-comments/tf-generator.hcl"}, }, { + cwd: ".", args: []string{"--file", "fixtures/valid/locals-referencing-locals/tf-generator.hcl"}, }, { - args: []string{"--glob", "fixtures/valid/*/tf-generator.hcl"}, + cwd: "fixtures/valid/", + args: []string{"--recursive"}, }, } { t.Run(strings.Join(fixture.args, " "), func(t *testing.T) { - args := append([]string{"generate", "--check"}, fixture.args...) - if err := run(args); err != nil { - t.Fatal(err) - } + withCwd(t, fixture.cwd, func() { + args := append([]string{"generate", "--check"}, fixture.args...) + if err := run(args); err != nil { + t.Fatal(err) + } + }) }) } } @@ -68,74 +103,94 @@ func TestValidFixtures(t *testing.T) { func TestInvalidFixtures(t *testing.T) { for _, fixture := range []InvalidFixture{ { + cwd: ".", args: []string{"--file", "fixtures/invalid/unknown-block/tf-generator.hcl"}, expectedMessageContains: "Unsupported block type; Blocks of type \"unknown\" are not expected here.", isDiag: true, }, { + cwd: ".", args: []string{"--file", "fixtures/invalid/syntax-error/tf-generator.hcl"}, expectedMessageContains: "Unclosed configuration block; There is no closing brace for this block before the end of the file.", isDiag: true, }, { + cwd: ".", args: []string{"--file", "fixtures/invalid/unknown/tf-generator.hcl"}, expectedMessageContains: ": Configuration file not found; The configuration file fixtures/invalid/unknown/tf-generator.hcl does not exist.", isDiag: true, }, { + cwd: ".", args: []string{"--file", "fixtures/invalid/tfvars-does-not-exist/tf-generator.hcl"}, expectedMessageContains: "no such file or directory.", isDiag: true, }, { + cwd: ".", args: []string{"--file", "fixtures/invalid/empty-generated-tfvars/tf-generator.hcl"}, expectedMessageContains: "the new tfvars file does not match the existing file.", isDiag: false, }, { + cwd: ".", args: []string{"--file", "fixtures/invalid/invalid-tfvars/tf-generator.hcl"}, expectedMessageContains: "parse config: [locals.tfvars:1,9-10: Unclosed configuration block; There is no closing brace for this block", isDiag: true, }, { + cwd: ".", args: []string{"--file", "fixtures/invalid/injected-tfvar-does-not-exist/tf-generator.hcl"}, expectedMessageContains: "Could not find tfvar for ref `injectvar.does-not-exist`", isDiag: true, }, { + cwd: ".", args: []string{"--file", "fixtures/invalid/duplicate-local/tf-generator.hcl"}, expectedMessageContains: `tf-generator.hcl:6,3-8: local "a" already defined`, isDiag: true, }, { - args: []string{"--glob", "fixtures/invalid/*/tf-generator.hcl"}, + cwd: ".", + args: []string{"--file", "a/tf-generator.hcl", "--recursive"}, + expectedMessageContains: `file name a/tf-generator.hcl cannot contain folders`, + isDiag: false, + }, + { + cwd: "fixtures/invalid/", + args: []string{"--recursive"}, expectedMessageContains: `local "a" already defined`, isDiag: true, }, { - args: []string{"--glob", "fixtures/**/tf-generator.hcl"}, - expectedMessageContains: `local "a" already defined`, + cwd: "fixtures/invalid/nested-folder-with-error", + args: []string{"--recursive"}, + expectedMessageContains: "parse config: [locals.tfvars:1,9-10: Unclosed configuration block; There is no closing brace for this block", isDiag: true, }, { - args: []string{"--file", "test", "--glob", "test"}, - expectedMessageContains: "cannot specify --file and --glob", - isDiag: false, + cwd: "fixtures/", + args: []string{"--recursive"}, + expectedMessageContains: `local "a" already defined`, + isDiag: true, }, { + cwd: ".", args: []string{}, expectedMessageContains: "The configuration file tf-generator.hcl does not exist.", isDiag: true, }, } { t.Run(strings.Join(fixture.args, " "), func(t *testing.T) { - args := append([]string{"generate", "--check"}, fixture.args...) - err := run(args) - assert.NotNilf(t, err, "expected error") - errMsg := err.Error() - assert.Contains(t, errMsg, fixture.expectedMessageContains) - _, isDiag := err.(hcl.Diagnostics) - assert.Equal(t, fixture.isDiag, isDiag) + withCwd(t, fixture.cwd, func() { + args := append([]string{"generate", "--check"}, fixture.args...) + err := run(args) + assert.NotNilf(t, err, "expected error") + errMsg := err.Error() + assert.Contains(t, errMsg, fixture.expectedMessageContains) + _, isDiag := err.(hcl.Diagnostics) + assert.Equal(t, fixture.isDiag, isDiag) + }) }) } } diff --git a/nix-test.sh b/nix-test.sh index f4d59bf..831369c 100644 --- a/nix-test.sh +++ b/nix-test.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash set -e -nix-build -E 'let tf-generator-overlay = import ./.; pkgs = import { overlays = [ tf-generator-overlay ]; }; in pkgs.mkShellNoCC { buildInputs = with pkgs; [ tf-generator ]; }' --dry-run +mkdir -p dist +nix-build -E 'let tf-generator-overlay = import ./.; pkgs = import { overlays = [ tf-generator-overlay ]; }; in pkgs.mkShellNoCC { buildInputs = with pkgs; [ tf-generator ]; }' -o dist/release +rm -rf dist