-
Notifications
You must be signed in to change notification settings - Fork 1k
Expand file tree
/
Copy pathvalidate.go
More file actions
230 lines (214 loc) · 7.37 KB
/
validate.go
File metadata and controls
230 lines (214 loc) · 7.37 KB
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
// Copyright IBM Corp. 2017, 2026
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"time"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
// ValidateResourceTypeConfig function
func (s *RawProviderServer) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) {
resp := &tfprotov5.ValidateResourceTypeConfigResponse{}
requiredKeys := []string{"apiVersion", "kind", "metadata"}
forbiddenKeys := []string{"status"}
rt, err := GetResourceType(req.TypeName)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to determine resource type",
Detail: err.Error(),
})
return resp, nil
}
// Decode proposed resource state
config, err := req.Config.Unmarshal(rt)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to unmarshal resource state",
Detail: err.Error(),
})
return resp, nil
}
att := tftypes.NewAttributePath()
att = att.WithAttributeName("manifest")
configVal := make(map[string]tftypes.Value)
err = config.As(&configVal)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to extract resource state from SDK value",
Detail: err.Error(),
})
return resp, nil
}
manifest, ok := configVal["manifest"]
if !ok {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Manifest missing from resource configuration",
Detail: "A manifest attribute containing a valid Kubernetes resource configuration is required.",
Attribute: att,
})
return resp, nil
}
rawManifest := make(map[string]tftypes.Value)
err = manifest.As(&rawManifest)
if err != nil {
if err.Error() == "unmarshaling unknown values is not supported" {
// Likely this validation call came too early and the manifest still contains unknown values.
// Bailing out without error to allow the resource to be completed at a later stage.
return resp, nil
}
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: `Failed to extract "manifest" attribute value from resource configuration`,
Detail: err.Error(),
Attribute: att,
})
return resp, nil
}
for _, key := range requiredKeys {
if _, present := rawManifest[key]; !present {
kp := att.WithAttributeName(key)
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: `Attribute key missing from "manifest" value`,
Detail: fmt.Sprintf("'%s' attribute key is missing from manifest configuration", key),
Attribute: kp,
})
}
}
for _, key := range forbiddenKeys {
if _, present := rawManifest[key]; present {
kp := att.WithAttributeName(key)
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: `Forbidden attribute key in "manifest" value`,
Detail: fmt.Sprintf("'%s' attribute key is not allowed in manifest configuration", key),
Attribute: kp,
})
}
}
// validate timeouts block
timeouts := s.getTimeouts(configVal)
path := tftypes.NewAttributePath().WithAttributeName("timeouts")
for k, v := range timeouts {
_, err := time.ParseDuration(v)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: fmt.Sprintf("Error parsing timeout for %q", k),
Detail: err.Error(),
Attribute: path.WithAttributeName(k),
})
}
}
// validate wait block
if wait, ok := configVal["wait"]; ok && !wait.IsNull() {
var waitBlock []tftypes.Value
wait.As(&waitBlock)
if len(waitBlock) > 0 {
var w map[string]tftypes.Value
waitBlock[0].As(&w)
for k, ww := range w {
if !ww.IsNull() {
if k == "condition" {
var cb []tftypes.Value
ww.As(&cb)
if len(cb) == 0 {
continue
}
}
}
}
}
}
if waitFor, ok := configVal["wait_for"]; ok && !waitFor.IsNull() {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityWarning,
Summary: "Deprecated Attribute",
Detail: `The "wait_for" attribute has been deprecated. Please use the "wait" block instead.`,
Attribute: tftypes.NewAttributePath().WithAttributeName("wait_for"),
})
}
return resp, nil
}
func (s *RawProviderServer) validateResourceOnline(manifest *tftypes.Value) (diags []*tfprotov5.Diagnostic) {
rm, err := s.getRestMapper()
if err != nil {
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to create K8s RESTMapper client",
Detail: err.Error(),
})
return
}
gvk, err := GVKFromTftypesObject(manifest, rm)
if err != nil {
diags = append(diags, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to determine GroupVersionResource for manifest",
Detail: err.Error(),
})
return
}
// Validate if the resource requires a namespace and fail the plan with
// a meaningful error if none is supplied. Ideally this would be done earlier,
// during 'ValidateResourceTypeConfig', but at that point we don't have access to API credentials
// and we need them for calling IsResourceNamespaced (uses the discovery API).
ns, err := IsResourceNamespaced(gvk, rm)
if err != nil {
diags = append(diags,
&tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Detail: err.Error(),
Summary: fmt.Sprintf("Failed to discover scope of resource '%s'", gvk.String()),
})
return
}
nsPath := tftypes.NewAttributePath()
nsPath = nsPath.WithAttributeName("metadata").WithAttributeName("namespace")
nsVal, restPath, err := tftypes.WalkAttributePath(*manifest, nsPath)
if ns {
if err != nil || len(restPath.Steps()) > 0 {
diags = append(diags,
&tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Detail: fmt.Sprintf("Resources of type '%s' require a namespace", gvk.String()),
Summary: "Namespace required",
})
return
}
if nsVal.(tftypes.Value).IsNull() {
diags = append(diags,
&tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Detail: fmt.Sprintf("Namespace for resource '%s' cannot be nil", gvk.String()),
Summary: "Namespace required",
})
}
var nsStr string
err := nsVal.(tftypes.Value).As(&nsStr)
if nsStr == "" && err == nil {
diags = append(diags,
&tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Detail: fmt.Sprintf("Namespace for resource '%s' cannot be empty", gvk.String()),
Summary: "Namespace required",
})
}
} else {
if err == nil && len(restPath.Steps()) == 0 && !nsVal.(tftypes.Value).IsNull() {
diags = append(diags,
&tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Detail: fmt.Sprintf("Resources of type '%s' cannot have a namespace", gvk.String()),
Summary: "Cluster level resource cannot take namespace",
})
}
}
return
}