/
server.go
230 lines (200 loc) · 7.35 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
package plugin
import (
"errors"
"fmt"
"log"
"github.com/hashicorp/go-version"
hcl "github.com/hashicorp/hcl/v2"
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/plugin/plugin2host"
sdk "github.com/terraform-linters/tflint-plugin-sdk/tflint"
"github.com/terraform-linters/tflint/terraform"
"github.com/terraform-linters/tflint/tflint"
"github.com/zclconf/go-cty/cty"
)
// GRPCServer is a gRPC server for responding to requests from plugins.
type GRPCServer struct {
runner *tflint.Runner
rootRunner *tflint.Runner
files map[string]*hcl.File
clientSDKVersion *version.Version
}
var _ plugin2host.Server = (*GRPCServer)(nil)
// NewGRPCServer initializes a gRPC server for plugins.
func NewGRPCServer(runner *tflint.Runner, rootRunner *tflint.Runner, files map[string]*hcl.File, sdkVersion *version.Version) *GRPCServer {
return &GRPCServer{runner: runner, rootRunner: rootRunner, files: files, clientSDKVersion: sdkVersion}
}
// GetOriginalwd returns the original working directory.
func (s *GRPCServer) GetOriginalwd() string {
return s.runner.Ctx.Meta.OriginalWorkingDir
}
// GetModulePath returns the current module path.
func (s *GRPCServer) GetModulePath() []string {
return s.runner.TFConfig.Path
}
// GetModuleContent returns module content based on the passed schema and options.
func (s *GRPCServer) GetModuleContent(bodyS *hclext.BodySchema, opts sdk.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
var module *terraform.Module
var ctx *terraform.Evaluator
switch opts.ModuleCtx {
case sdk.SelfModuleCtxType:
module = s.runner.TFConfig.Module
ctx = s.runner.Ctx
case sdk.RootModuleCtxType:
module = s.rootRunner.TFConfig.Module
ctx = s.rootRunner.Ctx
default:
panic(fmt.Sprintf("unknown module ctx: %s", opts.ModuleCtx))
}
// For performance, determine in advance whether the target resource exists.
if opts.Hint.ResourceType != "" {
if _, exists := module.Resources[opts.Hint.ResourceType]; !exists {
return &hclext.BodyContent{}, nil
}
}
if opts.ExpandMode == sdk.ExpandModeNone {
ctx = nil
}
return module.PartialContent(bodyS, ctx)
}
// GetFile returns the hcl.File based on passed the file name.
func (s *GRPCServer) GetFile(name string) (*hcl.File, error) {
// Considering that autofix has been applied, prioritize returning the value of runner.Files().
if file, exists := s.runner.Files()[name]; exists {
return file, nil
}
// If the file is not found in the current module, it may be in other modules (e.g. root module).
log.Printf(`[DEBUG] The file "%s" is not found in the current module. Fall back to global caches.`, name)
return s.files[name], nil
}
// GetFiles returns all hcl.File in the module.
func (s *GRPCServer) GetFiles(ty sdk.ModuleCtxType) map[string][]byte {
switch ty {
case sdk.SelfModuleCtxType:
return s.runner.Sources()
case sdk.RootModuleCtxType:
return s.rootRunner.Sources()
default:
panic(fmt.Sprintf("invalid ModuleCtxType: %s", ty))
}
}
// GetRuleConfigContent extracts the rule config based on the schema.
// It returns an extracted body content and sources.
// The reason for returning sources is to encode the expression, and there is room for improvement here.
func (s *GRPCServer) GetRuleConfigContent(name string, bodyS *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) {
config := s.runner.RuleConfig(name)
if config == nil {
return &hclext.BodyContent{}, s.runner.ConfigSources(), nil
}
enabledByCLI := false
configBody := config.Body
// If you enable the rule through the CLI instead of the file, its hcl.Body will be nil.
if config.Body == nil {
enabledByCLI = true
configBody = hcl.EmptyBody()
}
body, diags := hclext.Content(configBody, bodyS)
if diags.HasErrors() {
if enabledByCLI {
return nil, s.runner.ConfigSources(), errors.New("This rule cannot be enabled with the --enable-rule option because it lacks the required configuration")
}
return body, s.runner.ConfigSources(), diags
}
return body, s.runner.ConfigSources(), nil
}
// EvaluateExpr returns the value of the passed expression.
func (s *GRPCServer) EvaluateExpr(expr hcl.Expression, opts sdk.EvaluateExprOption) (cty.Value, error) {
var runner *tflint.Runner
switch opts.ModuleCtx {
case sdk.SelfModuleCtxType:
runner = s.runner
case sdk.RootModuleCtxType:
runner = s.rootRunner
}
val, diags := runner.Ctx.EvaluateExpr(expr, *opts.WantType)
if diags.HasErrors() {
return val, diags
}
// SDK v0.16+ introduces client-side handling of unknown/NULL/sensitive values.
if s.clientSDKVersion != nil && s.clientSDKVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.16.0"))) {
return val, nil
}
if val.ContainsMarked() {
err := fmt.Errorf(
"sensitive value found in %s:%d%w",
expr.Range().Filename,
expr.Range().Start.Line,
sdk.ErrSensitive,
)
log.Printf("[INFO] %s. TFLint ignores expressions with sensitive values.", err)
return cty.NullVal(cty.NilType), err
}
if *opts.WantType == cty.DynamicPseudoType {
return val, nil
}
err := cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) {
if !v.IsKnown() {
err := fmt.Errorf(
"unknown value found in %s:%d%w",
expr.Range().Filename,
expr.Range().Start.Line,
sdk.ErrUnknownValue,
)
log.Printf("[INFO] %s. TFLint can only evaluate provided variables and skips dynamic values.", err)
return false, err
}
if v.IsNull() {
err := fmt.Errorf(
"null value found in %s:%d%w",
expr.Range().Filename,
expr.Range().Start.Line,
sdk.ErrNullValue,
)
log.Printf("[INFO] %s. TFLint ignores expressions with null values.", err)
return false, err
}
return true, nil
})
if err != nil {
return cty.NullVal(cty.NilType), err
}
return val, nil
}
// EmitIssue stores an issue in the server based on passed rule, message, and location.
// It attempts to detect whether the issue range represents an expression and emits it based on that context.
// However, some ranges may be syntactically valid but not actually represent an expression.
// In these cases, the "expression" is still provided as context and the client should ignore any errors when attempting to evaluate it.
func (s *GRPCServer) EmitIssue(rule sdk.Rule, message string, location hcl.Range, fixable bool) (bool, error) {
// If the issue range represents an expression, it is emitted based on that context.
// This is required to emit issues in called modules.
expr, err := s.getExprFromRange(location)
if err != nil {
// If the range does not represent an expression, just emit it without context.
return s.runner.EmitIssue(rule, message, location, fixable), nil
}
var applied bool
err = s.runner.WithExpressionContext(expr, func() error {
applied = s.runner.EmitIssue(rule, message, location, fixable)
return nil
})
return applied, err
}
func (s *GRPCServer) getExprFromRange(location hcl.Range) (hcl.Expression, error) {
file := s.runner.File(location.Filename)
if file == nil {
return nil, errors.New("file not found")
}
expr, diags := hclext.ParseExpression(location.SliceBytes(file.Bytes), location.Filename, location.Start)
if diags.HasErrors() {
return nil, diags
}
return expr, nil
}
// ApplyChanges applies the autofix changes to the runner.
func (s *GRPCServer) ApplyChanges(changes map[string][]byte) error {
diags := s.runner.ApplyChanges(changes)
if diags.HasErrors() {
return diags
}
return nil
}