Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,52 @@
# 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:

```hcl
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"
```
4 changes: 2 additions & 2 deletions docs/environment_variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
48 changes: 37 additions & 11 deletions opa/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,71 @@ 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 (
policyRoot = "~/.tflint.d/policies"
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) {
// Priority 1: policy_dirs from config
if len(c.PolicyDirs) > 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid unnecessary length checks before loops. You can just declare var dirs []string in the function scope. range c.PolicyDirs, appending in the loop, and then if len(dirs) after that, return.

Same thing for env.

strings.Split(os.Getenv("TFLINT_OPA_POLICY_DIRS"), ",")

Just range over that, if the env is "", you're ranging over an empty slice. And again return dirs if len(dirs) > 0 after that loop too.

var expandedDirs []string
for _, dir := range c.PolicyDirs {
expanded, err := homedir.Expand(dir)
if err != nil {
return nil, err
}
expandedDirs = append(expandedDirs, expanded)
}
return expandedDirs, nil
}

if dir := os.Getenv("TFLINT_OPA_POLICY_DIR"); dir != "" {
return dir, nil
// Priority 2: TFLINT_OPA_POLICY_DIRS environment variable
// Supports multiple directories separated by `,`
if envDirs := os.Getenv("TFLINT_OPA_POLICY_DIRS"); envDirs != "" {
dirs := strings.Split(envDirs, ",")
var expandedDirs []string
for _, dir := range dirs {
dir = strings.TrimSpace(dir)
if dir != "" {
expanded, err := homedir.Expand(dir)
if err != nil {
return nil, err
}
expandedDirs = append(expandedDirs, expanded)
}
}
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) {
Expand Down
64 changes: 52 additions & 12 deletions opa/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")},
},
}

Expand All @@ -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
Expand All @@ -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)
}
}
})
}
Expand Down
4 changes: 2 additions & 2 deletions opa/ruleset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
18 changes: 9 additions & 9 deletions opa/ruleset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}),
},
},
},
Expand All @@ -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{}),
},
},
},
Expand All @@ -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{}),
},
},
},
Expand Down
Loading