-
Notifications
You must be signed in to change notification settings - Fork 132
/
Copy pathvalidator.go
274 lines (230 loc) · 8.15 KB
/
validator.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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// This is the main package to import to embed kubeconform in your software
package validator
import (
"context"
"errors"
"fmt"
"io"
jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/yannh/kubeconform/pkg/cache"
"github.com/yannh/kubeconform/pkg/registry"
"github.com/yannh/kubeconform/pkg/resource"
"sigs.k8s.io/yaml"
)
// Different types of validation results
type Status int
const (
_ Status = iota
Error // an error occurred processing the file / resource
Skipped // resource has been skipped, for example if its Kind was part of the kinds to skip
Valid // resource is valid
Invalid // resource is invalid
Empty // resource is empty. Note: is triggered for files starting with a --- separator.
)
type ValidationError struct {
Path string `json:"path"`
Msg string `json:"msg"`
}
func (ve *ValidationError) Error() string {
return ve.Msg
}
// Result contains the details of the result of a resource validation
type Result struct {
Resource resource.Resource
Err error
Status Status
ValidationErrors []ValidationError
}
// Validator exposes multiple methods to validate your Kubernetes resources.
type Validator interface {
ValidateResource(res resource.Resource) Result
Validate(filename string, r io.ReadCloser) []Result
ValidateWithContext(ctx context.Context, filename string, r io.ReadCloser) []Result
}
// Opts contains a set of options for the validator.
type Opts struct {
Cache string // Cache schemas downloaded via HTTP to this folder
Debug bool // Debug infos will be print here
SkipTLS bool // skip TLS validation when downloading from an HTTP Schema Registry
SkipKinds map[string]struct{} // List of resource Kinds to ignore
RejectKinds map[string]struct{} // List of resource Kinds to reject
KubernetesVersion string // Kubernetes Version - has to match one in https://github.com/instrumenta/kubernetes-json-schema
Strict bool // thros an error if resources contain undocumented fields
IgnoreMissingSchemas bool // skip a resource if no schema for that resource can be found
}
// New returns a new Validator
func New(schemaLocations []string, opts Opts) (Validator, error) {
// Default to our kubernetes-json-schema fork
// raw.githubusercontent.com is frontend by Fastly and very fast
if len(schemaLocations) == 0 {
schemaLocations = []string{"https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json"}
}
registries := []registry.Registry{}
for _, schemaLocation := range schemaLocations {
reg, err := registry.New(schemaLocation, opts.Cache, opts.Strict, opts.SkipTLS, opts.Debug)
if err != nil {
return nil, err
}
registries = append(registries, reg)
}
if opts.KubernetesVersion == "" {
opts.KubernetesVersion = "master"
}
if opts.SkipKinds == nil {
opts.SkipKinds = map[string]struct{}{}
}
if opts.RejectKinds == nil {
opts.RejectKinds = map[string]struct{}{}
}
return &v{
opts: opts,
schemaDownload: downloadSchema,
schemaCache: cache.NewInMemoryCache(),
regs: registries,
}, nil
}
type v struct {
opts Opts
schemaCache cache.Cache
schemaDownload func(registries []registry.Registry, kind, version, k8sVersion string) (*jsonschema.Schema, error)
regs []registry.Registry
}
// ValidateResource validates a single resource. This allows to validate
// large resource streams using multiple Go Routines.
func (val *v) ValidateResource(res resource.Resource) Result {
// For backward compatibility reasons when determining whether
// a resource should be skipped or rejected we use both
// the GVK encoding of the resource signatures (the recommended method
// for skipping/rejecting resources) and the raw Kind.
skip := func(signature resource.Signature) bool {
if _, ok := val.opts.SkipKinds[signature.GroupVersionKind()]; ok {
return ok
}
_, ok := val.opts.SkipKinds[signature.Kind]
return ok
}
reject := func(signature resource.Signature) bool {
if _, ok := val.opts.RejectKinds[signature.GroupVersionKind()]; ok {
return ok
}
_, ok := val.opts.RejectKinds[signature.Kind]
return ok
}
if len(res.Bytes) == 0 {
return Result{Resource: res, Err: nil, Status: Empty}
}
var r map[string]interface{}
unmarshaller := yaml.Unmarshal
if val.opts.Strict {
unmarshaller = yaml.UnmarshalStrict
}
if err := unmarshaller(res.Bytes, &r); err != nil {
return Result{Resource: res, Status: Error, Err: fmt.Errorf("error unmarshalling resource: %s", err)}
}
if r == nil { // Resource is empty
return Result{Resource: res, Err: nil, Status: Empty}
}
sig, err := res.SignatureFromMap(r)
if err != nil {
return Result{Resource: res, Err: fmt.Errorf("error while parsing: %s", err), Status: Error}
}
if skip(*sig) {
return Result{Resource: res, Err: nil, Status: Skipped}
}
if reject(*sig) {
return Result{Resource: res, Err: fmt.Errorf("prohibited resource kind %s", sig.Kind), Status: Error}
}
cached := false
var schema *jsonschema.Schema
if val.schemaCache != nil {
s, err := val.schemaCache.Get(sig.Kind, sig.Version, val.opts.KubernetesVersion)
if err == nil {
cached = true
schema = s.(*jsonschema.Schema)
}
}
if !cached {
if schema, err = val.schemaDownload(val.regs, sig.Kind, sig.Version, val.opts.KubernetesVersion); err != nil {
return Result{Resource: res, Err: err, Status: Error}
}
if val.schemaCache != nil {
val.schemaCache.Set(sig.Kind, sig.Version, val.opts.KubernetesVersion, schema)
}
}
if schema == nil {
if val.opts.IgnoreMissingSchemas {
return Result{Resource: res, Err: nil, Status: Skipped}
}
return Result{Resource: res, Err: fmt.Errorf("could not find schema for %s", sig.Kind), Status: Error}
}
err = schema.Validate(r)
if err != nil {
validationErrors := []ValidationError{}
var e *jsonschema.ValidationError
if errors.As(err, &e) {
for _, ve := range e.Causes {
validationErrors = append(validationErrors, ValidationError{
Path: ve.InstanceLocation,
Msg: ve.Message,
})
}
}
return Result{
Resource: res,
Status: Invalid,
Err: fmt.Errorf("problem validating schema. Check JSON formatting: %s", err),
ValidationErrors: validationErrors,
}
}
return Result{Resource: res, Status: Valid}
}
// ValidateWithContext validates resources found in r
// filename should be a name for the stream, such as a filename or stdin
func (val *v) ValidateWithContext(ctx context.Context, filename string, r io.ReadCloser) []Result {
validationResults := []Result{}
resourcesChan, _ := resource.FromStream(ctx, filename, r)
for {
select {
case res, ok := <-resourcesChan:
validationResults = append(validationResults, val.ValidateResource(res))
if !ok {
resourcesChan = nil
}
case <-ctx.Done():
break
}
if resourcesChan == nil {
break
}
}
r.Close()
return validationResults
}
// Validate validates resources found in r
// filename should be a name for the stream, such as a filename or stdin
func (val *v) Validate(filename string, r io.ReadCloser) []Result {
return val.ValidateWithContext(context.Background(), filename, r)
}
func downloadSchema(registries []registry.Registry, kind, version, k8sVersion string) (*jsonschema.Schema, error) {
var err error
var schemaBytes []byte
var path string
for _, reg := range registries {
path, schemaBytes, err = reg.DownloadSchema(kind, version, k8sVersion)
if err == nil {
schema, err := jsonschema.CompileString(path, string(schemaBytes))
// If we got a non-parseable response, we try the next registry
if err != nil {
continue
}
return schema, err
}
// If we get a 404, we try the next registry, but we exit if we get a real failure
if _, notfound := err.(*registry.NotFoundError); notfound {
continue
}
return nil, err
}
return nil, nil // No schema found - we don't consider it an error, resource will be skipped
}