diff --git a/README.md b/README.md index d1586d14b..c73961988 100644 --- a/README.md +++ b/README.md @@ -139,9 +139,9 @@ Application Options: --var-file=FILE Terraform variable file name --var='foo=bar' Set a Terraform variable --module Inspect modules - --chdir=DIR Switch to a different working directory before running inspection + --chdir=DIR Switch to a different working directory before executing the command + --recursive Run command in each directory recursively --force Return zero exit status even if issues found - --recursive Inspect directories recursively --color Enable colorized output --no-color Disable colorized output diff --git a/cmd/cli.go b/cmd/cli.go index 467ee9281..952cfebf2 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -178,3 +178,58 @@ func unknownOptionHandler(option string, arg flags.SplitArgument, args []string) } return []string{}, fmt.Errorf("`%s` is unknown option. Please run `tflint --help`", option) } + +func findWorkingDirs(opts Options) ([]string, error) { + if opts.Recursive && opts.Chdir != "" { + return []string{}, errors.New("cannot use --recursive and --chdir at the same time") + } + + workingDirs := []string{} + + if opts.Recursive { + // NOTE: The target directory is always the current directory in recursive mode + err := filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + return nil + } + // hidden directories are skipped + if path != "." && strings.HasPrefix(d.Name(), ".") { + return filepath.SkipDir + } + + workingDirs = append(workingDirs, path) + return nil + }) + if err != nil { + return []string{}, err + } + } else { + if opts.Chdir == "" { + workingDirs = []string{"."} + } else { + workingDirs = []string{opts.Chdir} + } + } + + return workingDirs, nil +} + +func (cli *CLI) withinChangedDir(dir string, proc func() error) (err error) { + if dir != "." { + chErr := os.Chdir(dir) + if chErr != nil { + return fmt.Errorf("Failed to switch to a different working directory; %w", chErr) + } + defer func() { + chErr := os.Chdir(cli.originalWorkingDir) + if chErr != nil { + err = fmt.Errorf("Failed to switch to the original working directory; %s; %w", chErr, err) + } + }() + } + + return proc() +} diff --git a/cmd/init.go b/cmd/init.go index d5c33b67f..dac22a842 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -11,54 +11,73 @@ import ( ) func (cli *CLI) init(opts Options) int { - if opts.Chdir != "" { - fmt.Fprintf(cli.errStream, "Cannot use --chdir with --init\n") + workingDirs, err := findWorkingDirs(opts) + if err != nil { + cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to find workspaces; %w", err), map[string][]byte{}) return ExitCodeError } + if opts.Recursive { - fmt.Fprintf(cli.errStream, "Cannot use --recursive with --init\n") - return ExitCodeError + fmt.Fprint(cli.outStream, "Installing plugins on each working directory...\n\n") } - cfg, err := tflint.LoadConfig(afero.Afero{Fs: afero.NewOsFs()}, opts.Config) - if err != nil { - cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to load TFLint config; %w", err), map[string][]byte{}) - return ExitCodeError - } + for _, wd := range workingDirs { + err := cli.withinChangedDir(wd, func() error { + if opts.Recursive { + fmt.Fprint(cli.outStream, "====================================================\n") + fmt.Fprintf(cli.outStream, "working directory: %s\n\n", wd) + } - for _, pluginCfg := range cfg.Plugins { - installCfg := plugin.NewInstallConfig(cfg, pluginCfg) + cfg, err := tflint.LoadConfig(afero.Afero{Fs: afero.NewOsFs()}, opts.Config) + if err != nil { + return fmt.Errorf("Failed to load TFLint config; %w", err) + } - // If version or source is not set, you need to install it manually - if installCfg.ManuallyInstalled() { - continue - } + found := false + for _, pluginCfg := range cfg.Plugins { + installCfg := plugin.NewInstallConfig(cfg, pluginCfg) - _, err := plugin.FindPluginPath(installCfg) - if os.IsNotExist(err) { - fmt.Fprintf(cli.outStream, "Installing `%s` plugin...\n", pluginCfg.Name) + // If version or source is not set, you need to install it manually + if installCfg.ManuallyInstalled() { + continue + } + found = true - sigchecker := plugin.NewSignatureChecker(installCfg) - if !sigchecker.HasSigningKey() { - _, _ = color.New(color.FgYellow).Fprintln(cli.outStream, "No signing key configured. Set `signing_key` to verify that the release is signed by the plugin developer") - } + _, err := plugin.FindPluginPath(installCfg) + if os.IsNotExist(err) { + fmt.Fprintf(cli.outStream, "Installing `%s` plugin...\n", pluginCfg.Name) - _, err = installCfg.Install() - if err != nil { - cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to install a plugin; %w", err), map[string][]byte{}) - return ExitCodeError + sigchecker := plugin.NewSignatureChecker(installCfg) + if !sigchecker.HasSigningKey() { + _, _ = color.New(color.FgYellow).Fprintln(cli.outStream, "No signing key configured. Set `signing_key` to verify that the release is signed by the plugin developer") + } + + _, err = installCfg.Install() + if err != nil { + return fmt.Errorf("Failed to install a plugin; %w", err) + } + + fmt.Fprintf(cli.outStream, "Installed `%s` (source: %s, version: %s)\n", pluginCfg.Name, pluginCfg.Source, pluginCfg.Version) + continue + } + + if err != nil { + return fmt.Errorf("Failed to find a plugin; %w", err) + } + + fmt.Fprintf(cli.outStream, "Plugin `%s` is already installed\n", pluginCfg.Name) } - fmt.Fprintf(cli.outStream, "Installed `%s` (source: %s, version: %s)\n", pluginCfg.Name, pluginCfg.Source, pluginCfg.Version) - continue - } + if opts.Recursive && !found { + fmt.Fprint(cli.outStream, "No plugins to install\n") + } + return nil + }) if err != nil { - cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to find a plugin; %w", err), map[string][]byte{}) + cli.formatter.Print(tflint.Issues{}, err, map[string][]byte{}) return ExitCodeError } - - fmt.Fprintf(cli.outStream, "Plugin `%s` is already installed\n", pluginCfg.Name) } return ExitCodeOK diff --git a/cmd/inspect.go b/cmd/inspect.go index c63434fef..2db265cb9 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -2,8 +2,6 @@ package cmd import ( "fmt" - "os" - "path/filepath" "strings" "github.com/hashicorp/hcl/v2" @@ -29,35 +27,10 @@ func (cli *CLI) inspect(opts Options, targetDir string, filterFiles []string) in return ExitCodeError } - workingDirs := []string{} - - if opts.Recursive { - // NOTE: The target directory is always the current directory in recursive mode - err := filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - if !d.IsDir() { - return nil - } - // hidden directories are skipped - if path != "." && strings.HasPrefix(d.Name(), ".") { - return filepath.SkipDir - } - - workingDirs = append(workingDirs, path) - return nil - }) - if err != nil { - cli.formatter.Print(tflint.Issues{}, err, map[string][]byte{}) - return ExitCodeError - } - } else { - if opts.Chdir == "" { - workingDirs = []string{"."} - } else { - workingDirs = []string{opts.Chdir} - } + workingDirs, err := findWorkingDirs(opts) + if err != nil { + cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to find workspaces; %w", err), map[string][]byte{}) + return ExitCodeError } issues := tflint.Issues{} @@ -165,23 +138,6 @@ func (cli *CLI) inspectModule(opts Options, dir string, filterFiles []string) (t return issues, nil } -func (cli *CLI) withinChangedDir(dir string, proc func() error) (err error) { - if dir != "." { - chErr := os.Chdir(dir) - if chErr != nil { - return fmt.Errorf("Failed to switch to a different working directory; %w", chErr) - } - defer func() { - chErr := os.Chdir(cli.originalWorkingDir) - if chErr != nil { - err = fmt.Errorf("Failed to switch to the original working directory; %s; %w", chErr, err) - } - }() - } - - return proc() -} - func (cli *CLI) setupRunners(opts Options, dir string) ([]*tflint.Runner, error) { configs, diags := cli.loader.LoadConfig(dir, cli.config.Module) if diags.HasErrors() { diff --git a/cmd/option.go b/cmd/option.go index b28958269..dc3134abf 100644 --- a/cmd/option.go +++ b/cmd/option.go @@ -22,8 +22,8 @@ type Options struct { Varfiles []string `long:"var-file" description:"Terraform variable file name" value-name:"FILE"` Variables []string `long:"var" description:"Set a Terraform variable" value-name:"'foo=bar'"` Module bool `long:"module" description:"Inspect modules"` - Chdir string `long:"chdir" description:"Switch to a different working directory before running inspection" value-name:"DIR"` - Recursive bool `long:"recursive" description:"Inspect directories recursively"` + Chdir string `long:"chdir" description:"Switch to a different working directory before executing the command" value-name:"DIR"` + Recursive bool `long:"recursive" description:"Run command in each directory recursively"` Force bool `long:"force" description:"Return zero exit status even if issues found"` Color bool `long:"color" description:"Enable colorized output"` NoColor bool `long:"no-color" description:"Disable colorized output"` diff --git a/cmd/test-fixtures/arguments/README b/cmd/test-fixtures/arguments/README deleted file mode 100644 index 557db03de..000000000 --- a/cmd/test-fixtures/arguments/README +++ /dev/null @@ -1 +0,0 @@ -Hello World diff --git a/cmd/test-fixtures/arguments/example/test.tf b/cmd/test-fixtures/arguments/example/test.tf deleted file mode 100644 index 29194776a..000000000 --- a/cmd/test-fixtures/arguments/example/test.tf +++ /dev/null @@ -1,8 +0,0 @@ -resource "aws_instance" "backend" { - ami = "ami-b73b63a0" - instance_type = "t1.2xlarge" - - tags { - Name = "HelloWorld" - } -} diff --git a/cmd/test-fixtures/arguments/template.tf b/cmd/test-fixtures/arguments/template.tf deleted file mode 100644 index 29194776a..000000000 --- a/cmd/test-fixtures/arguments/template.tf +++ /dev/null @@ -1,8 +0,0 @@ -resource "aws_instance" "backend" { - ami = "ami-b73b63a0" - instance_type = "t1.2xlarge" - - tags { - Name = "HelloWorld" - } -} diff --git a/cmd/test-fixtures/arguments/test.tf b/cmd/test-fixtures/arguments/test.tf deleted file mode 100644 index 35b84b1e5..000000000 --- a/cmd/test-fixtures/arguments/test.tf +++ /dev/null @@ -1,8 +0,0 @@ -resource "aws_instance" "web" { - ami = "ami-b73b63a0" - instance_type = "t1.2xlarge" - - tags { - Name = "HelloWorld" - } -} diff --git a/cmd/version.go b/cmd/version.go index 9dadfdbcc..6fbe9c07e 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -10,37 +10,60 @@ import ( ) func (cli *CLI) printVersion(opts Options) int { - if opts.Chdir != "" { - fmt.Fprintf(cli.errStream, "Cannot use --chdir with --version\n") + fmt.Fprintf(cli.outStream, "TFLint version %s\n", tflint.Version) + + workingDirs, err := findWorkingDirs(opts) + if err != nil { + cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to find workspaces; %w", err), map[string][]byte{}) return ExitCodeError } + if opts.Recursive { - fmt.Fprintf(cli.errStream, "Cannot use --recursive with --version\n") - return ExitCodeError + fmt.Fprint(cli.outStream, "\n") } - fmt.Fprintf(cli.outStream, "TFLint version %s\n", tflint.Version) + for _, wd := range workingDirs { + err := cli.withinChangedDir(wd, func() error { + if opts.Recursive { + fmt.Fprint(cli.outStream, "====================================================\n") + fmt.Fprintf(cli.outStream, "working directory: %s\n\n", wd) + } + + versions := getPluginVersions(opts) + for _, version := range versions { + fmt.Fprint(cli.outStream, version) + } + if len(versions) == 0 && opts.Recursive { + fmt.Fprint(cli.outStream, "No plugins\n") + } + return nil + }) + if err != nil { + cli.formatter.Print(tflint.Issues{}, err, map[string][]byte{}) + } + } + + return ExitCodeOK +} + +func getPluginVersions(opts Options) []string { // Load configuration files to print plugin versions cfg, err := tflint.LoadConfig(afero.Afero{Fs: afero.NewOsFs()}, opts.Config) if err != nil { log.Printf("[ERROR] Failed to load TFLint config: %s", err) - return ExitCodeOK - } - if len(opts.Only) > 0 { - for _, rule := range cfg.Rules { - rule.Enabled = false - } + return []string{} } cfg.Merge(opts.toConfig()) rulesetPlugin, err := plugin.Discovery(cfg) if err != nil { log.Printf("[ERROR] Failed to initialize plugins: %s", err) - return ExitCodeOK + return []string{} } defer rulesetPlugin.Clean() + versions := []string{} for _, ruleset := range rulesetPlugin.RuleSets { name, err := ruleset.RuleSetName() if err != nil { @@ -53,8 +76,8 @@ func (cli *CLI) printVersion(opts Options) int { continue } - fmt.Fprintf(cli.outStream, "+ ruleset.%s (%s)\n", name, version) + versions = append(versions, fmt.Sprintf("+ ruleset.%s (%s)\n", name, version)) } - return ExitCodeOK + return versions } diff --git a/docs/user-guide/working-directory.md b/docs/user-guide/working-directory.md index 944faf8c9..399a95bcb 100644 --- a/docs/user-guide/working-directory.md +++ b/docs/user-guide/working-directory.md @@ -25,3 +25,11 @@ $ tflint --recursive ``` It takes no arguments in recursive mode. Passing a directory or file name will result in an error. + +These flags are also valid for `--init` and `--version`. Recursive init is required when installing required plugins all at once: + +```console +$ tflint --recursive --init +$ tflint --recursive --version +$ tflint --recursive +``` diff --git a/integrationtest/init/basic/.tflint.hcl b/integrationtest/init/basic/.tflint.hcl index a860498a7..4b9c4e80b 100644 --- a/integrationtest/init/basic/.tflint.hcl +++ b/integrationtest/init/basic/.tflint.hcl @@ -1,6 +1,6 @@ plugin "aws" { enabled = true - version = "0.5.0" + version = "0.21.1" source = "github.com/terraform-linters/tflint-ruleset-aws" } diff --git a/integrationtest/init/init_test.go b/integrationtest/init/init_test.go index 1762e64e9..776a627ef 100644 --- a/integrationtest/init/init_test.go +++ b/integrationtest/init/init_test.go @@ -26,14 +26,14 @@ func TestIntegration(t *testing.T) { t.Fatal(err) } }() - if err := os.Chdir(dir); err != nil { - t.Fatal(err) - } - pluginDir := t.TempDir() os.Setenv("TFLINT_PLUGIN_DIR", pluginDir) defer os.Setenv("TFLINT_PLUGIN_DIR", "") + // Init on the current directory + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } outStream, errStream := new(bytes.Buffer), new(bytes.Buffer) cli, err := cmd.NewCLI(outStream, errStream) if err != nil { @@ -49,7 +49,7 @@ func TestIntegration(t *testing.T) { if !strings.Contains(outStream.String(), "Installing `aws` plugin...") { t.Fatalf("Expected to contain an installation log, but did not: stdout=%s, stderr=%s", outStream, errStream) } - if !strings.Contains(outStream.String(), "Installed `aws` (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.5.0)") { + if !strings.Contains(outStream.String(), "Installed `aws` (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.21.1)") { t.Fatalf("Expected to contain an installed log, but did not: stdout=%s, stderr=%s", outStream, errStream) } @@ -57,4 +57,66 @@ func TestIntegration(t *testing.T) { if !strings.Contains(outStream.String(), "Plugin `aws` is already installed") { t.Fatalf("Expected to contain an already installed log, but did not: stdout=%s, stderr=%s", outStream, errStream) } + + cli.Run([]string{"./tflint", "--version"}) + if !strings.Contains(outStream.String(), "+ ruleset.aws (0.21.1)") { + t.Fatalf("Expected to contain an plugin version log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } + + // Init with --chdir + if err := os.Chdir(current); err != nil { + t.Fatal(err) + } + outStream, errStream = new(bytes.Buffer), new(bytes.Buffer) + cli, err = cmd.NewCLI(outStream, errStream) + if err != nil { + t.Fatal(err) + } + + cli.Run([]string{"./tflint", "--chdir", "basic", "--init"}) + if !strings.Contains(outStream.String(), "Plugin `aws` is already installed") { + t.Fatalf("Expected to contain an already installed log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } + + cli.Run([]string{"./tflint", "--chdir", "basic", "--version"}) + if !strings.Contains(outStream.String(), "+ ruleset.aws (0.21.1)") { + t.Fatalf("Expected to contain an plugin version log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } + + // Init with --recursive + if err := os.Chdir(current); err != nil { + t.Fatal(err) + } + outStream, errStream = new(bytes.Buffer), new(bytes.Buffer) + cli, err = cmd.NewCLI(outStream, errStream) + if err != nil { + t.Fatal(err) + } + + cli.Run([]string{"./tflint", "--recursive", "--init"}) + if !strings.Contains(outStream.String(), "working directory: .") { + t.Fatalf("Expected to contain working dir log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } + if !strings.Contains(outStream.String(), "No plugins to install") { + t.Fatalf("Expected to contain no plugins log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } + if !strings.Contains(outStream.String(), "working directory: basic") { + t.Fatalf("Expected to contain working dir log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } + if !strings.Contains(outStream.String(), "Plugin `aws` is already installed") { + t.Fatalf("Expected to contain an already installed log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } + + outStream, errStream = new(bytes.Buffer), new(bytes.Buffer) + cli, err = cmd.NewCLI(outStream, errStream) + if err != nil { + t.Fatal(err) + } + cli.Run([]string{"./tflint", "--recursive", "--version"}) + if !strings.Contains(outStream.String(), "working directory: basic") { + t.Fatalf("Expected to contain working dir log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } + if !strings.Contains(outStream.String(), "+ ruleset.aws (0.21.1)") { + t.Fatalf("Expected to contain an plugin version log, but did not: stdout=%s, stderr=%s", outStream, errStream) + } }