Skip to content

Commit

Permalink
Add --chdir option
Browse files Browse the repository at this point in the history
  • Loading branch information
wata727 committed Dec 3, 2022
1 parent e6a3351 commit 054da12
Show file tree
Hide file tree
Showing 33 changed files with 1,129 additions and 134 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ 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
--force Return zero exit status even if issues found
--color Enable colorized output
--no-color Disable colorized output
Help Options:
-h, --help Show this help message
```

See [User Guide](docs/user-guide) for details.
Expand Down
2 changes: 1 addition & 1 deletion cmd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (cli *CLI) Run(args []string) int {
case opts.Init:
return cli.init(opts)
case opts.Langserver:
return cli.startLanguageServer(opts.Config, opts.toConfig())
return cli.startLanguageServer(opts)
case opts.ActAsBundledPlugin:
return cli.actAsBundledPlugin()
default:
Expand Down
5 changes: 5 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
)

func (cli *CLI) init(opts Options) int {
if opts.Chdir != "" {
fmt.Fprintf(cli.errStream, "Cannot use --chdir with --init\n")
return ExitCodeError
}

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{})
Expand Down
70 changes: 45 additions & 25 deletions cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"os"
"strings"

"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -31,25 +32,10 @@ func (cli *CLI) inspect(opts Options, dir string, filterFiles []string) int {
cfg.Merge(opts.toConfig())
cli.formatter.Format = cfg.Format

// Setup loader
cli.loader, err = terraform.NewLoader(afero.Afero{Fs: afero.NewOsFs()})
if err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to prepare loading; %w", err), map[string][]byte{})
return ExitCodeError
}

// Setup runners
runners, appErr := cli.setupRunners(opts, cfg, dir)
if appErr != nil {
cli.formatter.Print(tflint.Issues{}, appErr, cli.loader.Sources())
return ExitCodeError
}
rootRunner := runners[len(runners)-1]

// Lookup plugins and validation
rulesetPlugin, err := plugin.Discovery(cfg)
if err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to initialize plugins; %w", err), cli.loader.Sources())
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to initialize plugins; %w", err), map[string][]byte{})
return ExitCodeError
}
defer rulesetPlugin.Clean()
Expand All @@ -63,46 +49,80 @@ func (cli *CLI) inspect(opts Options, dir string, filterFiles []string) int {
// VersionConstraints endpoint is available in tflint-plugin-sdk v0.14+.
// Skip verification if not available.
} else {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to get TFLint version constraints to `%s` plugin; %w", name, err), cli.loader.Sources())
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to get TFLint version constraints to `%s` plugin; %w", name, err), map[string][]byte{})
return ExitCodeError
}
}
if !constraints.Check(tflint.Version) {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version), cli.loader.Sources())
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version), map[string][]byte{})
return ExitCodeError
}

if err := ruleset.ApplyGlobalConfig(config); err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to apply global config to `%s` plugin; %w", name, err), cli.loader.Sources())
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to apply global config to `%s` plugin; %w", name, err), map[string][]byte{})
return ExitCodeError
}
configSchema, err := ruleset.ConfigSchema()
if err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to fetch config schema from `%s` plugin; %w", name, err), cli.loader.Sources())
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to fetch config schema from `%s` plugin; %w", name, err), map[string][]byte{})
return ExitCodeError
}
content := &hclext.BodyContent{}
if plugin, exists := cfg.Plugins[name]; exists {
var diags hcl.Diagnostics
content, diags = plugin.Content(configSchema)
if diags.HasErrors() {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to parse `%s` plugin config; %w", name, diags), cli.loader.Sources())
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to parse `%s` plugin config; %w", name, diags), map[string][]byte{})
return ExitCodeError
}
}
err = ruleset.ApplyConfig(content, cfg.Sources())
if err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to apply config to `%s` plugin; %w", name, err), cli.loader.Sources())
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to apply config to `%s` plugin; %w", name, err), map[string][]byte{})
return ExitCodeError
}

rulesets = append(rulesets, ruleset)
}
if err := cfg.ValidateRules(rulesets...); err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to check rule config; %w", err), cli.loader.Sources())
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to check rule config; %w", err), map[string][]byte{})
return ExitCodeError
}

// Switch to a different working directory before setting up loader
originalWd, err := os.Getwd()
if err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to determine current working directory; %w", err), map[string][]byte{})
return ExitCodeError
}
if opts.Chdir != "" {
if dir != "." {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Cannot use --chdir and directory argument at the same time"), map[string][]byte{})
return ExitCodeError
}

err := os.Chdir(opts.Chdir)
if err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to switch to a different working directory; %w", err), map[string][]byte{})
return ExitCodeError
}
}

// Setup loader
cli.loader, err = terraform.NewLoader(afero.Afero{Fs: afero.NewOsFs()}, originalWd)
if err != nil {
cli.formatter.Print(tflint.Issues{}, fmt.Errorf("Failed to prepare loading; %w", err), map[string][]byte{})
return ExitCodeError
}

// Setup runners
runners, appErr := cli.setupRunners(opts, cfg, originalWd, dir)
if appErr != nil {
cli.formatter.Print(tflint.Issues{}, appErr, cli.loader.Sources())
return ExitCodeError
}
rootRunner := runners[len(runners)-1]

// Run inspection
for _, ruleset := range rulesetPlugin.RuleSets {
for _, runner := range runners {
Expand All @@ -129,7 +149,7 @@ func (cli *CLI) inspect(opts Options, dir string, filterFiles []string) int {
return ExitCodeOK
}

func (cli *CLI) setupRunners(opts Options, cfg *tflint.Config, dir string) ([]*tflint.Runner, error) {
func (cli *CLI) setupRunners(opts Options, cfg *tflint.Config, originalWd string, dir string) ([]*tflint.Runner, error) {
configs, diags := cli.loader.LoadConfig(dir, cfg.Module)
if diags.HasErrors() {
return []*tflint.Runner{}, fmt.Errorf("Failed to load configurations; %w", diags)
Expand Down Expand Up @@ -162,7 +182,7 @@ func (cli *CLI) setupRunners(opts Options, cfg *tflint.Config, dir string) ([]*t
}
variables = append(variables, cliVars)

runner, err := tflint.NewRunner(cfg, annotations, configs, variables...)
runner, err := tflint.NewRunner(originalWd, cfg, annotations, configs, variables...)
if err != nil {
return []*tflint.Runner{}, fmt.Errorf("Failed to initialize a runner; %w", err)
}
Expand Down
11 changes: 9 additions & 2 deletions cmd/langserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ package cmd

import (
"context"
"fmt"
"log"
"os"

"github.com/sourcegraph/jsonrpc2"
"github.com/terraform-linters/tflint/langserver"
"github.com/terraform-linters/tflint/tflint"
)

func (cli *CLI) startLanguageServer(configPath string, cliConfig *tflint.Config) int {
func (cli *CLI) startLanguageServer(opts Options) int {
if opts.Chdir != "" {
fmt.Fprintf(cli.errStream, "Cannot use --chdir with --langserver\n")
return ExitCodeError
}
configPath := opts.Config
cliConfig := opts.toConfig()

log.Println("Starting language server...")

handler, plugin, err := langserver.NewHandler(configPath, cliConfig)
Expand Down
1 change: 1 addition & 0 deletions cmd/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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"`
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"`
Expand Down
5 changes: 5 additions & 0 deletions cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (
)

func (cli *CLI) printVersion(opts Options) int {
if opts.Chdir != "" {
fmt.Fprintf(cli.errStream, "Cannot use --chdir with --version\n")
return ExitCodeError
}

fmt.Fprintf(cli.outStream, "TFLint version %s\n", tflint.Version)

// Load configuration files to print plugin versions
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ This guide describes the various features of TFLint for end users.
- [Module Inspection](module-inspection.md)
- [Annotations](annotations.md)
- [Compatibility with Terraform](compatibility.md)
- [Switching working directory with --chdir](working_directory.md)
- [Editor Integration](editor-integration.md)
14 changes: 14 additions & 0 deletions docs/user-guide/working_directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Switching working directory with --chdir

The `--chdir` option is available in TFLint like Terraform:

```console
$ tflint --chdir=environments/production
```

Its behavior is the same as [Terraform's behavior](https://developer.hashicorp.com/terraform/cli/commands#switching-working-directory-with-chdir), but there are some TFLint-specific considerations:

- Config file (`.tflint.hcl`) is processed before acting on the `--chdir` option.
- Files specified with relative paths like `--var-file` and `varfile` on config files are resolved against the original working directory.

TFLint also accepts a directory as an argument, but `--chdir` is recommended in most cases. The directory argument is deprecated and may be removed in a future version.
14 changes: 14 additions & 0 deletions integrationtest/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ func TestIntegration(t *testing.T) {
status: cmd.ExitCodeError,
stderr: fmt.Sprintf("Failed to load `%s`: Multiple files in different directories are not allowed", filepath.Join("subdir", "main.tf")),
},
{
name: "--chdir",
command: "./tflint --chdir subdir",
dir: "multiple_files",
status: cmd.ExitCodeIssuesFound,
stdout: fmt.Sprintf("%s (aws_instance_example_type)", color.New(color.Bold).Sprint("instance type is m5.2xlarge")),
},
{
name: "--chdir and directory argument",
command: "./tflint --chdir subdir ../",
dir: "multiple_files",
status: cmd.ExitCodeError,
stderr: "Cannot use --chdir and directory argument at the same time",
},
}

dir, _ := os.Getwd()
Expand Down
4 changes: 0 additions & 4 deletions integrationtest/inspection/bad-config/.tflint.hcl
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
plugin "testing" {
enabled = true
}

plugin "aws" {
enabled = false
}
11 changes: 11 additions & 0 deletions integrationtest/inspection/chdir/.tflint.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugin "testing" {
enabled = true
}

plugin "terraform" {
enabled = false
}

config {
varfile = ["dir/from_config.tfvars"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"aws_instance","Source":"./module","Dir":"module"}]}
1 change: 1 addition & 0 deletions integrationtest/inspection/chdir/dir/dir.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file_in_dir
1 change: 1 addition & 0 deletions integrationtest/inspection/chdir/dir/from_cli.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from_cli="from_cli"
1 change: 1 addition & 0 deletions integrationtest/inspection/chdir/dir/from_config.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from_config="from_config"
21 changes: 21 additions & 0 deletions integrationtest/inspection/chdir/dir/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
variable "from_config" {
default = "default"
}

variable "from_cli" {
default = "default"
}

variable "from_auto" {
default = "default"
}

variable "from_auto_default" {
default = "default"
}

module "aws_instance" {
source = "./module"

instance_type = "${var.from_config}-${var.from_cli}-${var.from_auto}-${var.from_auto_default}-${file("dir.txt")}-${file("${path.cwd}/root.txt")}"
}
5 changes: 5 additions & 0 deletions integrationtest/inspection/chdir/dir/module/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
variable "instance_type" {}

resource "aws_instance" "main" {
instance_type = var.instance_type
}
1 change: 1 addition & 0 deletions integrationtest/inspection/chdir/dir/subdir.auto.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from_auto="from_auto"
1 change: 1 addition & 0 deletions integrationtest/inspection/chdir/dir/terraform.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from_auto_default="from_auto_default"
48 changes: 48 additions & 0 deletions integrationtest/inspection/chdir/result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"issues": [
{
"rule": {
"name": "aws_instance_example_type",
"severity": "error",
"link": ""
},
"message": "instance type is from_config-from_cli-from_auto-from_auto_default-file_in_dir-file_in_root",
"range": {
"filename": "dir/main.tf",
"start": {
"line": 20,
"column": 19
},
"end": {
"line": 20,
"column": 148
}
},
"callers": [
{
"filename": "dir/main.tf",
"start": {
"line": 20,
"column": 19
},
"end": {
"line": 20,
"column": 148
}
},
{
"filename": "dir/module/main.tf",
"start": {
"line": 4,
"column": 19
},
"end": {
"line": 4,
"column": 36
}
}
]
}
],
"errors": []
}
1 change: 1 addition & 0 deletions integrationtest/inspection/chdir/root.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file_in_root
5 changes: 5 additions & 0 deletions integrationtest/inspection/inspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ func TestIntegration(t *testing.T) {
Command: "tflint --module --format json",
Dir: "expand",
},
{
Name: "chdir",
Command: fmt.Sprintf("tflint --chdir dir --module --var-file %s --format json", filepath.Join("dir", "from_cli.tfvars")),
Dir: "chdir",
},
}

// Disable the bundled plugin because the `os.Executable()` is go(1) in the tests
Expand Down
Loading

0 comments on commit 054da12

Please sign in to comment.