diff --git a/docs/configuration.md b/docs/configuration.md index 14931cd..dad354d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration -This plugin can take advantage of additional features by configure the plugin block. Currently, this configuration is only available for customizing a policy directory. +This plugin can take advantage of additional features by configure the plugin block. Currently, this configuration is only available for customizing the directories to load policies. Here's an example: @@ -8,19 +8,45 @@ Here's an example: plugin "opa" { // Plugin common attributes - policy_dir = "./policies" + policy_dirs = ["./policies", "./other-policies"] } ``` -## `policy_dir` +## `policy_dirs` Default: `./.tflint.d/policies`, `~/.tflint.d/policies` -Change the directory from which policies are loaded. The priority is as follows: +Change the directories from which policies are loaded. You can specify multiple directories to load policies from different locations. The priority is as follows: -1. `policy_dir` in the config -2. `TFLINT_OPA_POLICY_DIR` environment variable +1. `policy_dirs` in the config +2. `TFLINT_OPA_POLICY_DIRS` environment variable (supports multiple directories separated `,`) 3. `./.tflint.d/policies` 4. `~/.tflint.d/policies` A relative path is resolved from the current directory. + +### Examples + +Single directory: +```hcl +plugin "opa" { + policy_dirs = ["./policies"] +} +``` + +Multiple directories: +```hcl +plugin "opa" { + policy_dirs = ["./policies", "./team-policies", "~/shared-policies"] +} +``` + +Using environment variable with a single directory: +```bash +export TFLINT_OPA_POLICY_DIRS="./policies" +``` + +Using environment variable with multiple directories: +```bash +export TFLINT_OPA_POLICY_DIRS="./policies,./team-policies,~/shared-policies" +``` diff --git a/docs/environment_variables.md b/docs/environment_variables.md index 2c6477c..f54a712 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -2,8 +2,8 @@ Below is a list of environment variables that have meaning in the OPA ruleset: -- `TFLINT_OPA_POLICY_DIR` - - Directory where policy files are placed. See [Configuration](./configuration.md). +- `TFLINT_OPA_POLICY_DIRS` + - Directories where policy files are placed. Supports multiple directories separated by `,`. See [Configuration](./configuration.md). - `TFLINT_OPA_TRACE` - Enable tracing. See [Debugging](./debug.md). - `TFLINT_OPA_TEST` diff --git a/integration/checks/.tflint.hcl b/integration/checks/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/checks/.tflint.hcl +++ b/integration/checks/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/data_sources/.tflint.hcl b/integration/data_sources/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/data_sources/.tflint.hcl +++ b/integration/data_sources/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/ephemerals/.tflint.hcl b/integration/ephemerals/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/ephemerals/.tflint.hcl +++ b/integration/ephemerals/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/expr_without_eval/.tflint.hcl b/integration/expr_without_eval/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/expr_without_eval/.tflint.hcl +++ b/integration/expr_without_eval/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/imports/.tflint.hcl b/integration/imports/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/imports/.tflint.hcl +++ b/integration/imports/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/instance_type/.tflint.hcl b/integration/instance_type/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/instance_type/.tflint.hcl +++ b/integration/instance_type/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/locals/.tflint.hcl b/integration/locals/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/locals/.tflint.hcl +++ b/integration/locals/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/module_calls/.tflint.hcl b/integration/module_calls/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/module_calls/.tflint.hcl +++ b/integration/module_calls/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/moved/.tflint.hcl b/integration/moved/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/moved/.tflint.hcl +++ b/integration/moved/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/naming_convention/.tflint.hcl b/integration/naming_convention/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/naming_convention/.tflint.hcl +++ b/integration/naming_convention/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/outputs/.tflint.hcl b/integration/outputs/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/outputs/.tflint.hcl +++ b/integration/outputs/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/providers/.tflint.hcl b/integration/providers/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/providers/.tflint.hcl +++ b/integration/providers/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/removed/.tflint.hcl b/integration/removed/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/removed/.tflint.hcl +++ b/integration/removed/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/resources/.tflint.hcl b/integration/resources/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/resources/.tflint.hcl +++ b/integration/resources/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/settings/.tflint.hcl b/integration/settings/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/settings/.tflint.hcl +++ b/integration/settings/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/tagged/.tflint.hcl b/integration/tagged/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/tagged/.tflint.hcl +++ b/integration/tagged/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/variables/.tflint.hcl b/integration/variables/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/variables/.tflint.hcl +++ b/integration/variables/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/volume_size/.tflint.hcl b/integration/volume_size/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/volume_size/.tflint.hcl +++ b/integration/volume_size/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/integration/volume_type/.tflint.hcl b/integration/volume_type/.tflint.hcl index 3d0b8cc..e8ff1dc 100644 --- a/integration/volume_type/.tflint.hcl +++ b/integration/volume_type/.tflint.hcl @@ -5,5 +5,5 @@ plugin "terraform" { plugin "opa" { enabled = true - policy_dir = "policies" + policy_dirs = ["policies"] } diff --git a/opa/config.go b/opa/config.go index 9ac91df..d136d27 100644 --- a/opa/config.go +++ b/opa/config.go @@ -2,13 +2,14 @@ package opa import ( "os" + "strings" "github.com/mitchellh/go-homedir" ) // Config is the configuration for the ruleset. type Config struct { - PolicyDir string `hclext:"policy_dir,optional"` + PolicyDirs []string `hclext:"policy_dirs,optional"` } var ( @@ -16,31 +17,57 @@ var ( localPolicyRoot = "./.tflint.d/policies" ) -// policyDir returns the base policy directory. +// policyDirs returns the policy directories to load. // Adopted with the following priorities: // -// 1. `policy_dir` in a config file -// 2. `TFLINT_OPA_POLICY_DIR` environment variable +// 1. `policy_dirs` in a config file +// 2. `TFLINT_OPA_POLICY_DIRS` environment variable (supports multiple directories separated by `,`) // 3. Current directory (./.tflint.d/policies) // 4. Home directory (~/.tflint.d/policies) // // If the environment variable is set, other directories will not be considered, // but if the current directory does not exist, it will fallback to the home directory. -func (c *Config) policyDir() (string, error) { - if c.PolicyDir != "" { - return homedir.Expand(c.PolicyDir) +func (c *Config) policyDirs() ([]string, error) { + var expandedDirs []string + + // Priority 1: policy_dirs from config + for _, dir := range c.PolicyDirs { + expanded, err := homedir.Expand(dir) + if err != nil { + return nil, err + } + expandedDirs = append(expandedDirs, expanded) + } + + if len(expandedDirs) > 0 { + return expandedDirs, nil + } + + // Priority 2: TFLINT_OPA_POLICY_DIRS environment variable + // Supports multiple directories separated by `,` + for dir := range strings.SplitSeq(os.Getenv("TFLINT_OPA_POLICY_DIRS"), ",") { + dir = strings.TrimSpace(dir) + if dir != "" { + expanded, err := homedir.Expand(dir) + if err != nil { + return nil, err + } + expandedDirs = append(expandedDirs, expanded) + } } - if dir := os.Getenv("TFLINT_OPA_POLICY_DIR"); dir != "" { - return dir, nil + if len(expandedDirs) > 0 { + return expandedDirs, nil } + // Priority 3 & 4: Check local directory, fallback to home directory _, err := os.Stat(localPolicyRoot) if os.IsNotExist(err) { - return policyRootDir() + dir, err := policyRootDir() + return []string{dir}, err } - return localPolicyRoot, err + return []string{localPolicyRoot}, err } func policyRootDir() (string, error) { diff --git a/opa/config_test.go b/opa/config_test.go index 86ac2b1..5668a2c 100644 --- a/opa/config_test.go +++ b/opa/config_test.go @@ -19,40 +19,74 @@ func TestPolicyDir(t *testing.T) { root string currentDir string env map[string]string - want string + want []string err error }{ { name: "default (not exists)", config: &Config{}, root: filepath.Join(cwd, "test-fixtures", "config", "root-not-exists", ".tflint.d", "policies"), - want: filepath.Join(cwd, "test-fixtures", "config", "root-not-exists", ".tflint.d", "policies"), + want: []string{filepath.Join(cwd, "test-fixtures", "config", "root-not-exists", ".tflint.d", "policies")}, err: os.ErrNotExist, }, { name: "default (exists)", config: &Config{}, root: filepath.Join(cwd, "test-fixtures", "config", "root-exists", ".tflint.d", "policies"), - want: filepath.Join(cwd, "test-fixtures", "config", "root-exists", ".tflint.d", "policies"), + want: []string{filepath.Join(cwd, "test-fixtures", "config", "root-exists", ".tflint.d", "policies")}, }, { name: "local", config: &Config{}, currentDir: filepath.Join(cwd, "test-fixtures", "config", "local"), - want: "./.tflint.d/policies", + want: []string{"./.tflint.d/policies"}, }, { name: "env", config: &Config{}, env: map[string]string{ - "TFLINT_OPA_POLICY_DIR": "policies", + "TFLINT_OPA_POLICY_DIRS": "policies", }, - want: "policies", + want: []string{"policies"}, }, { - name: "config", - config: &Config{PolicyDir: "config/policies"}, - want: "config/policies", + name: "env multiple directories", + config: &Config{}, + env: map[string]string{ + "TFLINT_OPA_POLICY_DIRS": "policies,other/policies", + }, + want: []string{"policies", "other/policies"}, + }, + { + name: "env multiple directories with spaces", + config: &Config{}, + env: map[string]string{ + "TFLINT_OPA_POLICY_DIRS": " policies , other/policies ", + }, + want: []string{"policies", "other/policies"}, + }, + { + name: "env with tilde expansion", + config: &Config{}, + env: map[string]string{ + "TFLINT_OPA_POLICY_DIRS": "~/policies", + }, + want: []string{filepath.Join(os.Getenv("HOME"), "policies")}, + }, + { + name: "config single directory", + config: &Config{PolicyDirs: []string{"config/policies"}}, + want: []string{"config/policies"}, + }, + { + name: "config multiple directories", + config: &Config{PolicyDirs: []string{"config/policies", "other/policies"}}, + want: []string{"config/policies", "other/policies"}, + }, + { + name: "config with tilde expansion", + config: &Config{PolicyDirs: []string{"~/policies"}}, + want: []string{filepath.Join(os.Getenv("HOME"), "policies")}, }, } @@ -71,7 +105,7 @@ func TestPolicyDir(t *testing.T) { t.Setenv(k, v) } - got, err := test.config.policyDir() + got, err := test.config.policyDirs() if err != nil { if errors.Is(err, test.err) { return @@ -82,8 +116,14 @@ func TestPolicyDir(t *testing.T) { t.Fatal("should return an error, but it does not") } - if got != test.want { - t.Fatalf("want: %s, got: %s", test.want, got) + if len(got) != len(test.want) { + t.Fatalf("want: %v, got: %v", test.want, got) + } + + for i := range got { + if got[i] != test.want[i] { + t.Fatalf("want: %v, got: %v", test.want, got) + } } }) } diff --git a/opa/ruleset.go b/opa/ruleset.go index 3718b5e..461c39f 100644 --- a/opa/ruleset.go +++ b/opa/ruleset.go @@ -40,7 +40,7 @@ func (r *RuleSet) ApplyConfig(body *hclext.BodyContent) error { return diags } - policyDir, err := r.config.policyDir() + policyDirs, err := r.config.policyDirs() if err != nil { // If you declare the directory in config or environment variables, // os.ErrNotExist will not be returned, resulting in load errors @@ -51,7 +51,7 @@ func (r *RuleSet) ApplyConfig(body *hclext.BodyContent) error { return err } - ret, err := loader.NewFileLoader().Filtered([]string{policyDir}, nil) + ret, err := loader.NewFileLoader().Filtered(policyDirs, nil) if err != nil { return fmt.Errorf("failed to load policies; %w", err) } diff --git a/opa/ruleset_test.go b/opa/ruleset_test.go index 390f6ff..4e28190 100644 --- a/opa/ruleset_test.go +++ b/opa/ruleset_test.go @@ -30,9 +30,9 @@ func TestApplyConfig(t *testing.T) { name: "rules exists", config: &hclext.BodyContent{ Attributes: hclext.Attributes{ - "policy_dir": &hclext.Attribute{ - Name: "policy_dir", - Expr: hcl.StaticExpr(cty.StringVal(filepath.Join(cwd, "test-fixtures", "config", "root-exists", ".tflint.d", "policies")), hcl.Range{}), + "policy_dirs": &hclext.Attribute{ + Name: "policy_dirs", + Expr: hcl.StaticExpr(cty.TupleVal([]cty.Value{cty.StringVal(filepath.Join(cwd, "test-fixtures", "config", "root-exists", ".tflint.d", "policies"))}), hcl.Range{}), }, }, }, @@ -42,9 +42,9 @@ func TestApplyConfig(t *testing.T) { name: "tests exists", config: &hclext.BodyContent{ Attributes: hclext.Attributes{ - "policy_dir": &hclext.Attribute{ - Name: "policy_dir", - Expr: hcl.StaticExpr(cty.StringVal(filepath.Join(cwd, "test-fixtures", "config", "root-exists", ".tflint.d", "policies")), hcl.Range{}), + "policy_dirs": &hclext.Attribute{ + Name: "policy_dirs", + Expr: hcl.StaticExpr(cty.TupleVal([]cty.Value{cty.StringVal(filepath.Join(cwd, "test-fixtures", "config", "root-exists", ".tflint.d", "policies"))}), hcl.Range{}), }, }, }, @@ -62,9 +62,9 @@ func TestApplyConfig(t *testing.T) { name: "policy dir does not exists", config: &hclext.BodyContent{ Attributes: hclext.Attributes{ - "policy_dir": &hclext.Attribute{ - Name: "policy_dir", - Expr: hcl.StaticExpr(cty.StringVal(filepath.Join(cwd, "test-fixtures", "config", "root-not-exists", ".tflint.d", "policies")), hcl.Range{}), + "policy_dirs": &hclext.Attribute{ + Name: "policy_dirs", + Expr: hcl.StaticExpr(cty.TupleVal([]cty.Value{cty.StringVal(filepath.Join(cwd, "test-fixtures", "config", "root-not-exists", ".tflint.d", "policies"))}), hcl.Range{}), }, }, }, diff --git a/tflint-ruleset-opa b/tflint-ruleset-opa new file mode 100755 index 0000000..2d42e32 Binary files /dev/null and b/tflint-ruleset-opa differ