Skip to content

Commit 47ee5a4

Browse files
authored
config: Add plugin_dir config (#1235)
1 parent d4bee82 commit 47ee5a4

File tree

11 files changed

+164
-46
lines changed

11 files changed

+164
-46
lines changed

cmd/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func (cli *CLI) init(opts Options) int {
1717
}
1818

1919
for _, pluginCfg := range cfg.Plugins {
20-
installCfg := tfplugin.NewInstallConfig(pluginCfg)
20+
installCfg := tfplugin.NewInstallConfig(cfg, pluginCfg)
2121

2222
// If version or source is not set, you need to install it manually
2323
if installCfg.ManuallyInstalled() {

docs/user-guide/config.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ The config file is written in [HCL](https://github.com/hashicorp/hcl). An exampl
99

1010
```hcl
1111
config {
12+
plugin_dir = "~/.tflint.d/plugins"
13+
1214
module = true
1315
force = false
1416
disabled_by_default = false
@@ -39,6 +41,10 @@ You can also use another file as a config file with the `--config` option:
3941
$ tflint --config other_config.hcl
4042
```
4143

44+
### `plugin_dir`
45+
46+
Set the plugin directory. The default is `~/.tflint.d/plugins` (or `./.tflint.d/plugins`). See also [Configuring Plugins](plugins.md#advanced-usage)
47+
4248
### `module`
4349

4450
CLI flag: `--module`

docs/user-guide/plugins.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ Plugin developer's PGP public signing key. When this attribute is set, TFLint wi
5454

5555
Plugins under the terraform-linters organization (AWS/GCP/Azure ruleset plugins) can use the built-in signing key, so this attribute can be omitted.
5656

57+
## Plugin directory
58+
59+
Plugins are usually installed under `~/.tflint.d/plugins`. Exceptionally, if you already have `./.tflint.d/plugins` in your working directory, it will be installed there.
60+
61+
The automatically installed plugins are placed as `[plugin dir]/[source]/[version]/tflint-ruleset-[name]`. (`tflint-ruleset-[name].exe` in Windows).
62+
63+
If you want to change the plugin directory, you can change this with the [`plugin_dir`](config.md#plugin_dir) or `TFLINT_PLUGIN_DIR` environment variable.
64+
5765
## Avoiding rate limiting
5866

5967
When you install plugins with `tflint --init`, call the GitHub API to get release metadata. This is typically an unauthenticated request with a rate limit of 60 requests per hour.
@@ -64,7 +72,7 @@ This limitation can be a problem if you need to run `--init` frequently, such as
6472

6573
It's also a good idea to cache the plugin directory, as TFLint will only send requests if plugins aren't installed. See also the [setup-tflint's example](https://github.com/terraform-linters/setup-tflint#usage).
6674

67-
## Advanced Usage
75+
## Manual installation
6876

6977
You can also install the plugin manually. This is mainly useful for plugin development and for plugins that are not published on GitHub. In that case, omit the `source` and `version` attributes.
7078

@@ -74,6 +82,4 @@ plugin "foo" {
7482
}
7583
```
7684

77-
When the plugin is enabled, TFLint invokes the `tflint-ruleset-<NAME>` (`tflint-ruleset-<NAME>.exe` on Windows) binary in the `~/.tflint.d/plugins` (or `./.tflint.d/plugins`) directory. So you should move the binary into the directory in advance.
78-
79-
You can also change the plugin directory with the `TFLINT_PLUGIN_DIR` environment variable.
85+
When the plugin is enabled, TFLint invokes the `tflint-ruleset-[name]` (`tflint-ruleset-[name].exe` on Windows) binary in the plugin directory (For instance, `~/.tflint.d/plugins/tflint-ruleset-[name]`). So you should move the binary into the directory in advance.

plugin/discovery.go

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ func Discovery(config *tflint.Config) (*Plugin, error) {
2424
clients := map[string]*plugin.Client{}
2525
rulesets := map[string]*tfplugin.Client{}
2626

27-
for _, cfg := range config.Plugins {
28-
installCfg := NewInstallConfig(cfg)
27+
for _, pluginCfg := range config.Plugins {
28+
installCfg := NewInstallConfig(config, pluginCfg)
2929
pluginPath, err := FindPluginPath(installCfg)
3030
var cmd *exec.Cmd
3131
if os.IsNotExist(err) {
32-
if cfg.Name == "aws" && installCfg.ManuallyInstalled() {
32+
if pluginCfg.Name == "aws" && installCfg.ManuallyInstalled() {
3333
log.Print("[INFO] Plugin `aws` is not installed, but bundled plugins are available.")
3434
self, err := os.Executable()
3535
if err != nil {
@@ -38,38 +38,38 @@ func Discovery(config *tflint.Config) (*Plugin, error) {
3838
cmd = exec.Command(self, "--act-as-aws-plugin")
3939
} else {
4040
if installCfg.ManuallyInstalled() {
41-
pluginDir, err := getPluginDir()
41+
pluginDir, err := getPluginDir(config)
4242
if err != nil {
4343
return nil, err
4444
}
45-
return nil, fmt.Errorf("Plugin `%s` not found in %s", cfg.Name, pluginDir)
45+
return nil, fmt.Errorf("Plugin `%s` not found in %s", pluginCfg.Name, pluginDir)
4646
}
47-
return nil, fmt.Errorf("Plugin `%s` not found. Did you run `tflint --init`?", cfg.Name)
47+
return nil, fmt.Errorf("Plugin `%s` not found. Did you run `tflint --init`?", pluginCfg.Name)
4848
}
4949
} else {
5050
cmd = exec.Command(pluginPath)
5151
}
5252

53-
if cfg.Enabled {
54-
log.Printf("[INFO] Plugin `%s` found", cfg.Name)
53+
if pluginCfg.Enabled {
54+
log.Printf("[INFO] Plugin `%s` found", pluginCfg.Name)
5555

5656
client := tfplugin.NewClient(&tfplugin.ClientOpts{
5757
Cmd: cmd,
5858
})
5959
rpcClient, err := client.Client()
6060
if err != nil {
61-
return nil, pluginClientError(err, cfg)
61+
return nil, pluginClientError(err, pluginCfg)
6262
}
6363
raw, err := rpcClient.Dispense("ruleset")
6464
if err != nil {
6565
return nil, err
6666
}
6767
ruleset := raw.(*tfplugin.Client)
6868

69-
clients[cfg.Name] = client
70-
rulesets[cfg.Name] = ruleset
69+
clients[pluginCfg.Name] = client
70+
rulesets[pluginCfg.Name] = ruleset
7171
} else {
72-
log.Printf("[INFO] Plugin `%s` found, but the plugin is disabled", cfg.Name)
72+
log.Printf("[INFO] Plugin `%s` found, but the plugin is disabled", pluginCfg.Name)
7373
}
7474
}
7575

@@ -78,7 +78,7 @@ func Discovery(config *tflint.Config) (*Plugin, error) {
7878

7979
// FindPluginPath returns the plugin binary path.
8080
func FindPluginPath(config *InstallConfig) (string, error) {
81-
dir, err := getPluginDir()
81+
dir, err := getPluginDir(config.globalConfig)
8282
if err != nil {
8383
return "", err
8484
}
@@ -94,13 +94,18 @@ func FindPluginPath(config *InstallConfig) (string, error) {
9494
// getPluginDir returns the base plugin directory.
9595
// Adopted with the following priorities:
9696
//
97-
// 1. `TFLINT_PLUGIN_DIR` environment variable
98-
// 2. Current directory (./.tflint.d/plugins)
99-
// 3. Home directory (~/.tflint.d/plugins)
97+
// 1. `plugin_dir` in a global config
98+
// 2. `TFLINT_PLUGIN_DIR` environment variable
99+
// 3. Current directory (./.tflint.d/plugins)
100+
// 4. Home directory (~/.tflint.d/plugins)
100101
//
101102
// If the environment variable is set, other directories will not be considered,
102103
// but if the current directory does not exist, it will fallback to the home directory.
103-
func getPluginDir() (string, error) {
104+
func getPluginDir(cfg *tflint.Config) (string, error) {
105+
if cfg.PluginDir != "" {
106+
return homedir.Expand(cfg.PluginDir)
107+
}
108+
104109
if dir := os.Getenv("TFLINT_PLUGIN_DIR"); dir != "" {
105110
return dir, nil
106111
}

plugin/discovery_test.go

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,38 @@ func Test_Discovery_envVar(t *testing.T) {
119119
}
120120
}
121121

122+
func Test_Discovery_pluginDirConfig(t *testing.T) {
123+
cwd, err := os.Getwd()
124+
if err != nil {
125+
t.Fatal(err)
126+
}
127+
128+
plugin, err := Discovery(&tflint.Config{
129+
PluginDir: filepath.Join(cwd, "test-fixtures", "locals", ".tflint.d", "plugins"),
130+
Plugins: map[string]*tflint.PluginConfig{
131+
"foo": {
132+
Name: "foo",
133+
Enabled: true,
134+
},
135+
"bar": {
136+
Name: "bar",
137+
Enabled: false,
138+
Source: "github.com/terraform-linters/tflint-ruleset-bar",
139+
Version: "0.1.0",
140+
},
141+
},
142+
})
143+
defer plugin.Clean()
144+
145+
if err != nil {
146+
t.Fatalf("Unexpected error occurred %s", err)
147+
}
148+
149+
if len(plugin.RuleSets) != 1 {
150+
t.Fatalf("Only one plugin must be enabled, but %d plugins are enabled", len(plugin.RuleSets))
151+
}
152+
}
153+
122154
func Test_Discovery_notFound(t *testing.T) {
123155
cwd, err := os.Getwd()
124156
if err != nil {
@@ -252,12 +284,12 @@ func Test_FindPluginPath(t *testing.T) {
252284
}{
253285
{
254286
Name: "manually installed",
255-
Input: NewInstallConfig(&tflint.PluginConfig{Name: "foo", Enabled: true}),
287+
Input: NewInstallConfig(tflint.EmptyConfig(), &tflint.PluginConfig{Name: "foo", Enabled: true}),
256288
Expected: filepath.Join(PluginRoot, "tflint-ruleset-foo"+fileExt()),
257289
},
258290
{
259291
Name: "auto installed",
260-
Input: NewInstallConfig(&tflint.PluginConfig{
292+
Input: NewInstallConfig(tflint.EmptyConfig(), &tflint.PluginConfig{
261293
Name: "bar",
262294
Enabled: true,
263295
Source: "github.com/terraform-linters/tflint-ruleset-bar",
@@ -302,12 +334,12 @@ func Test_FindPluginPath_locals(t *testing.T) {
302334
}{
303335
{
304336
Name: "manually installed",
305-
Input: NewInstallConfig(&tflint.PluginConfig{Name: "foo", Enabled: true}),
337+
Input: NewInstallConfig(tflint.EmptyConfig(), &tflint.PluginConfig{Name: "foo", Enabled: true}),
306338
Expected: filepath.Join(localPluginRoot, "tflint-ruleset-foo"+fileExt()),
307339
},
308340
{
309341
Name: "auto installed",
310-
Input: NewInstallConfig(&tflint.PluginConfig{
342+
Input: NewInstallConfig(tflint.EmptyConfig(), &tflint.PluginConfig{
311343
Name: "bar",
312344
Enabled: true,
313345
Source: "github.com/terraform-linters/tflint-ruleset-bar",
@@ -345,12 +377,12 @@ func Test_FindPluginPath_envVar(t *testing.T) {
345377
}{
346378
{
347379
Name: "manually installed",
348-
Input: NewInstallConfig(&tflint.PluginConfig{Name: "foo", Enabled: true}),
380+
Input: NewInstallConfig(tflint.EmptyConfig(), &tflint.PluginConfig{Name: "foo", Enabled: true}),
349381
Expected: filepath.Join(dir, "tflint-ruleset-foo"+fileExt()),
350382
},
351383
{
352384
Name: "auto installed",
353-
Input: NewInstallConfig(&tflint.PluginConfig{
385+
Input: NewInstallConfig(tflint.EmptyConfig(), &tflint.PluginConfig{
354386
Name: "bar",
355387
Enabled: true,
356388
Source: "github.com/terraform-linters/tflint-ruleset-bar",
@@ -371,6 +403,48 @@ func Test_FindPluginPath_envVar(t *testing.T) {
371403
}
372404
}
373405

406+
func Test_FindPluginPath_pluginDirConfig(t *testing.T) {
407+
cwd, err := os.Getwd()
408+
if err != nil {
409+
t.Fatal(err)
410+
}
411+
412+
globalConfig := tflint.EmptyConfig()
413+
globalConfig.PluginDir = filepath.Join(cwd, "test-fixtures", "locals", ".tflint.d", "plugins")
414+
415+
cases := []struct {
416+
Name string
417+
Input *InstallConfig
418+
Expected string
419+
}{
420+
{
421+
Name: "manually installed",
422+
Input: NewInstallConfig(globalConfig, &tflint.PluginConfig{Name: "foo", Enabled: true}),
423+
Expected: filepath.Join(globalConfig.PluginDir, "tflint-ruleset-foo"+fileExt()),
424+
},
425+
{
426+
Name: "auto installed",
427+
Input: NewInstallConfig(globalConfig, &tflint.PluginConfig{
428+
Name: "bar",
429+
Enabled: true,
430+
Source: "github.com/terraform-linters/tflint-ruleset-bar",
431+
Version: "0.1.0",
432+
}),
433+
Expected: filepath.Join(globalConfig.PluginDir, "github.com/terraform-linters/tflint-ruleset-bar", "0.1.0", "tflint-ruleset-bar"+fileExt()),
434+
},
435+
}
436+
437+
for _, tc := range cases {
438+
got, err := FindPluginPath(tc.Input)
439+
if err != nil {
440+
t.Fatalf("Unexpected error occurred %s", err)
441+
}
442+
if got != tc.Expected {
443+
t.Fatalf("Failed `%s`: want=%s got=%s", tc.Name, tc.Expected, got)
444+
}
445+
}
446+
}
447+
374448
func Test_FindPluginPath_withoutExtensionInWindows(t *testing.T) {
375449
cwd, err := os.Getwd()
376450
if err != nil {
@@ -381,7 +455,7 @@ func Test_FindPluginPath_withoutExtensionInWindows(t *testing.T) {
381455
PluginRoot = filepath.Join(cwd, "test-fixtures", "plugins")
382456
defer func() { PluginRoot = original }()
383457

384-
config := NewInstallConfig(&tflint.PluginConfig{Name: "baz", Enabled: true})
458+
config := NewInstallConfig(tflint.EmptyConfig(), &tflint.PluginConfig{Name: "baz", Enabled: true})
385459
expected := filepath.Join(PluginRoot, "tflint-ruleset-baz")
386460

387461
got, err := FindPluginPath(config)

plugin/install.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import (
2020
// InstallConfig is a config for plugin installation.
2121
// This is a wrapper for PluginConfig and manages naming conventions
2222
// and directory names for installation.
23+
// Note that need a global config to manage installation directory.
2324
type InstallConfig struct {
25+
globalConfig *tflint.Config
26+
2427
*tflint.PluginConfig
2528
}
2629

2730
// NewInstallConfig returns a new InstallConfig from passed PluginConfig.
28-
func NewInstallConfig(config *tflint.PluginConfig) *InstallConfig {
29-
return &InstallConfig{PluginConfig: config}
31+
func NewInstallConfig(config *tflint.Config, pluginCfg *tflint.PluginConfig) *InstallConfig {
32+
return &InstallConfig{globalConfig: config, PluginConfig: pluginCfg}
3033
}
3134

3235
// ManuallyInstalled returns whether the plugin should be installed manually.
@@ -71,7 +74,7 @@ func (c *InstallConfig) AssetName() string {
7174
// - The signature file must be binary OpenPGP format
7275
//
7376
func (c *InstallConfig) Install() (string, error) {
74-
dir, err := getPluginDir()
77+
dir, err := getPluginDir(c.globalConfig)
7578
if err != nil {
7679
return "", fmt.Errorf("Failed to get plugin dir: %w", err)
7780
}

plugin/install_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func Test_Install(t *testing.T) {
1212
PluginRoot = t.TempDir()
1313
defer func() { PluginRoot = original }()
1414

15-
config := NewInstallConfig(&tflint.PluginConfig{
15+
config := NewInstallConfig(tflint.EmptyConfig(), &tflint.PluginConfig{
1616
Name: "aws",
1717
Enabled: true,
1818
Version: "0.4.0",

0 commit comments

Comments
 (0)