Skip to content

Commit 2d4dc1b

Browse files
authored
Add google_disabled_api rule (#75)
* Enable deep checking * Add google_disabled_api rule * Add documentation * Fix rule name
1 parent c3bb8c9 commit 2d4dc1b

17 files changed

+637
-18
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ plugin "google" {
2020
}
2121
```
2222

23+
For more configuration about the plugin, see [Plugin Configuration](docs/configuration.md).
24+
2325
## Rules
2426

25-
100+ rules are available. See the [documentation](docs/README.md).
27+
100+ rules are available. See the [documentation](docs/rules/README.md).
2628

2729
## Building the plugin
2830

docs/configuration.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Configuration
2+
3+
This plugin can take advantage of additional features by configure the `plugin` block. Currently, this configuration is only available for [Deep Checking](deep_checking.md).
4+
5+
Here's an example:
6+
7+
```hcl
8+
plugin "google" {
9+
enabled = true
10+
11+
deep_check = false
12+
}
13+
```
14+
15+
## `deep_check`
16+
17+
Default: false
18+
19+
Enable [Deep Checking](deep_checking.md).

docs/deep_checking.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Deep Checking
2+
3+
Deep Checking uses your provider's credentials to perform a more strict inspection.
4+
5+
For example, you can confirm that the API you are trying to use in the project is enabled. See also the [google_disabled_api](rules/google_disabled_api.md) rule.
6+
7+
```console
8+
$ tflint
9+
1 issue(s) found:
10+
11+
Error: Compute Engine API has not been used in foo-bar-baz before or it is disabled. (google_disabled_api)
12+
13+
on template.tf line 25:
14+
25: resource "google_compute_network" "vpc_network" {
15+
16+
```
17+
18+
You can enable Deep Checking by changing the plugin configuration.
19+
20+
```hcl
21+
plugin "google" {
22+
enabled = true
23+
24+
deep_check = true
25+
}
26+
```
27+
28+
## Credentials
29+
30+
Currently, credentials, regions, etc. declared inside the "google" provider block are not considered except for the `project` attribute. You need to pass the credentials to TFLint using environment variables and so on.

docs/README.md renamed to docs/rules/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ These rules warn you if a machine type not listed at https://cloud.google.com/co
2020

2121
## Magic Modules Rules
2222

23-
These are the rules that warn against invalid values generated from [Magic Modules](https://github.com/terraform-linters/magic-modules). These rules are defined under the [`rules/magicmodules`](../rules/magicmodules) directory.
23+
These are the rules that warn against invalid values generated from [Magic Modules](https://github.com/terraform-linters/magic-modules). These rules are defined under the [`rules/magicmodules`](../../rules/magicmodules) directory.
2424

25-
See the [`tools`](../tools) directory for how to generate these rules.
25+
See the [`tools`](../../tools) directory for how to generate these rules.
26+
27+
## Deep Checking Rules
28+
29+
By enabling [Deep Checking](../deep_checking.md), you can enable rules that invoke API to perform more strict checking.
30+
31+
|Name|Severity|Enabled|
32+
| --- | --- | --- |
33+
|google_disabled_api|ERROR||

docs/rules/google_disabled_api.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# google_disabled_api
2+
3+
Disallow to declare resources with disabled API.
4+
5+
## Example
6+
7+
```hcl
8+
resource "google_compute_network" "vpc_network" {
9+
name = "terraform-network"
10+
auto_create_subnetworks = "true"
11+
}
12+
```
13+
14+
```
15+
$ tflint
16+
1 issue(s) found:
17+
18+
Error: Compute Engine API has not been used in foo-bar-baz before or it is disabled. (google_disabled_api)
19+
20+
on template.tf line 1:
21+
1: resource "google_compute_network" "vpc_network" {
22+
23+
```
24+
25+
[Service Usage API](https://cloud.google.com/service-usage/docs/reference/rest) must be enabled in order to use this rule.
26+
27+
## Why
28+
29+
`terraform apply` will fail when the resources refer to disabled APIs.
30+
31+
```
32+
$ terraform apply
33+
34+
...
35+
36+
Error: Error creating Network: googleapi: Error 403: Compute Engine API has not been used in project 1234567890 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/compute.googleapis.com/overview?project=1234567890 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry., accessNotConfigured
37+
```
38+
39+
Even more unfortunately, not all errors are returned. For example, if you have resources that depend on multiple disabled APIs, you will not see an error for the other APIs. This rule allows you to detect all disabled API errors in advance.
40+
41+
## How to Fix
42+
43+
Enable the API from the Cloud Console in your project or remove the resource.
44+
45+
https://cloud.google.com/endpoints/docs/openapi/enable-api
46+

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ require (
77
github.com/hashicorp/hcl/v2 v2.9.0
88
github.com/hashicorp/terraform-plugin-sdk/v2 v2.4.4
99
github.com/terraform-linters/tflint-plugin-sdk v0.8.1
10+
google.golang.org/api v0.40.0
1011
)

go.sum

Lines changed: 75 additions & 11 deletions
Large diffs are not rendered by default.

google/client.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package google
2+
3+
import (
4+
"context"
5+
6+
"google.golang.org/api/serviceusage/v1"
7+
)
8+
9+
// Client is a wrapper of the Google API client
10+
type Client struct {
11+
ServiceUsage *serviceusage.Service
12+
}
13+
14+
// NewClient returns a new Client
15+
func NewClient() (*Client, error) {
16+
serviceusageClient, err := serviceusage.NewService(context.TODO())
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
return &Client{
22+
ServiceUsage: serviceusageClient,
23+
}, nil
24+
}

google/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package google
2+
3+
import "github.com/hashicorp/hcl/v2"
4+
5+
// Config is the configuration for the ruleset.
6+
type Config struct {
7+
DeepCheck bool `hcl:"deep_check,optional"`
8+
9+
Remain hcl.Body `hcl:",remain"`
10+
}

google/provider.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package google
2+
3+
import (
4+
"os"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/terraform-linters/tflint-plugin-sdk/terraform/configs"
8+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
9+
)
10+
11+
// GoogleProviderBlockSchema is a schema of `google` provider block
12+
var GoogleProviderBlockSchema = &hcl.BodySchema{
13+
Attributes: []hcl.AttributeSchema{
14+
{Name: "project"},
15+
},
16+
}
17+
18+
// ProviderData represents a provider block with an eval context (runner)
19+
type ProviderData struct {
20+
provider *configs.Provider
21+
runner tflint.Runner
22+
attributes hcl.Attributes
23+
blocks hcl.Blocks
24+
}
25+
26+
// GetProject retrieves project_id from the "provider" block in the Terraform configuration and environment variables
27+
func GetProject(runner tflint.Runner) (string, error) {
28+
provider, err := runner.RootProvider("google")
29+
if err != nil {
30+
return "", err
31+
}
32+
if provider == nil {
33+
return getProjectFromEnv(), nil
34+
}
35+
36+
d, err := newProviderData(provider, runner)
37+
if err != nil {
38+
return "", err
39+
}
40+
41+
project, exists, err := d.Get("project")
42+
if exists {
43+
return project, err
44+
}
45+
46+
return getProjectFromEnv(), nil
47+
}
48+
49+
func getProjectFromEnv() string {
50+
envs := []string{"GOOGLE_PROJECT", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT", "CLOUDSDK_CORE_PROJECT"}
51+
for _, env := range envs {
52+
if project := os.Getenv(env); project != "" {
53+
return project
54+
}
55+
}
56+
return ""
57+
}
58+
59+
func newProviderData(provider *configs.Provider, runner tflint.Runner) (*ProviderData, error) {
60+
providerData := &ProviderData{
61+
provider: provider,
62+
runner: runner,
63+
attributes: map[string]*hcl.Attribute{},
64+
blocks: []*hcl.Block{},
65+
}
66+
67+
if provider != nil {
68+
content, _, diags := provider.Config.PartialContent(GoogleProviderBlockSchema)
69+
if diags.HasErrors() {
70+
return nil, diags
71+
}
72+
73+
providerData.attributes = content.Attributes
74+
providerData.blocks = content.Blocks
75+
}
76+
77+
return providerData, nil
78+
}
79+
80+
// Get returns a value corresponding to the given key
81+
// It should be noted that the value is evaluated if it is evaluable
82+
// The second return value is a flag that determines whether a value exists
83+
// We assume the provider has only simple attributes, so it just returns string
84+
func (d *ProviderData) Get(key string) (string, bool, error) {
85+
attribute, exists := d.attributes[key]
86+
if !exists {
87+
return "", false, nil
88+
}
89+
90+
var val string
91+
err := d.runner.EvaluateExprOnRootCtx(attribute.Expr, &val, nil)
92+
93+
err = d.runner.EnsureNoError(err, func() error { return nil })
94+
if err != nil {
95+
return "", true, err
96+
}
97+
return val, true, nil
98+
}

google/ruleset.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/hcl/v2/gohcl"
7+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
8+
)
9+
10+
// RuleSet is the custom ruleset for the Google provider plugin.
11+
type RuleSet struct {
12+
tflint.BuiltinRuleSet
13+
APIRules []tflint.Rule
14+
config *Config
15+
}
16+
17+
// RuleNames is a list of rule names provided by the plugin.
18+
func (r *RuleSet) RuleNames() []string {
19+
names := []string{}
20+
for _, rule := range r.Rules {
21+
names = append(names, rule.Name())
22+
}
23+
for _, rule := range r.APIRules {
24+
names = append(names, rule.Name())
25+
}
26+
return names
27+
}
28+
29+
// ApplyConfig reflects the plugin configuration to the ruleset.
30+
func (r *RuleSet) ApplyConfig(config *tflint.Config) error {
31+
r.ApplyCommonConfig(config)
32+
33+
// Apply "plugin" block config
34+
cfg := Config{}
35+
diags := gohcl.DecodeBody(config.Body, nil, &cfg)
36+
if diags.HasErrors() {
37+
return diags
38+
}
39+
r.config = &cfg
40+
41+
// Apply config for API rules
42+
for _, rule := range r.APIRules {
43+
enabled := rule.Enabled()
44+
if cfg := config.Rules[rule.Name()]; cfg != nil {
45+
enabled = cfg.Enabled
46+
} else if config.DisabledByDefault {
47+
enabled = false
48+
}
49+
50+
if cfg.DeepCheck && enabled {
51+
r.EnabledRules = append(r.EnabledRules, rule)
52+
}
53+
}
54+
55+
return nil
56+
}
57+
58+
// Check runs inspections for each rule with the custom Google runner.
59+
func (r *RuleSet) Check(rr tflint.Runner) error {
60+
runner, err := NewRunner(rr, r.config)
61+
if err != nil {
62+
return err
63+
}
64+
65+
for _, rule := range r.EnabledRules {
66+
if err := rule.Check(runner); err != nil {
67+
return fmt.Errorf("Failed to check `%s` rule: %s", rule.Name(), err)
68+
}
69+
}
70+
return nil
71+
}

google/runner.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
7+
)
8+
9+
// Runner is a wrapper of RPC client for inserting custom actions for Google provider.
10+
type Runner struct {
11+
tflint.Runner
12+
Client *Client
13+
Project string
14+
}
15+
16+
// NewRunner returns a custom Google runner.
17+
func NewRunner(runner tflint.Runner, config *Config) (*Runner, error) {
18+
var client *Client
19+
var project string
20+
var err error
21+
if config.DeepCheck {
22+
client, err = NewClient()
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
project, err = GetProject(runner)
28+
if err != nil {
29+
return nil, err
30+
}
31+
}
32+
33+
return &Runner{
34+
Runner: runner,
35+
Client: client,
36+
Project: project,
37+
}, nil
38+
}
39+
40+
// ParentProject returns a part of the API path about the parent project
41+
func (r *Runner) ParentProject() string {
42+
return fmt.Sprintf("projects/%s", r.Project)
43+
}

0 commit comments

Comments
 (0)