1 package kates
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "path"
8 "sync"
9
10 apiextVInternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
11 apiextV1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
12 apiextV1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
13 "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
14 "k8s.io/kube-openapi/pkg/validation/validate"
15
16 "github.com/datawire/dlib/derror"
17 )
18
19
20
21 type Validator struct {
22 client *Client
23 static map[TypeMeta]*apiextVInternal.CustomResourceDefinition
24
25 mutex sync.Mutex
26 validators map[TypeMeta]*validate.SchemaValidator
27 }
28
29
30
31
32
33 func NewValidator(client *Client, staticCRDs []Object) (*Validator, error) {
34 if client == nil && len(staticCRDs) == 0 {
35 return nil, errors.New("at least 1 client or static CRD must be provided")
36 }
37
38 static := make(map[TypeMeta]*apiextVInternal.CustomResourceDefinition, len(staticCRDs))
39 for i, untypedCRD := range staticCRDs {
40 var crd apiextVInternal.CustomResourceDefinition
41 switch untypedCRD.GetObjectKind().GroupVersionKind() {
42 case apiextV1beta1.SchemeGroupVersion.WithKind("CustomResourceDefinition"):
43 var crdV1beta1 apiextV1beta1.CustomResourceDefinition
44 if err := convert(untypedCRD, &crdV1beta1); err != nil {
45 return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
46 }
47 apiextV1beta1.SetDefaults_CustomResourceDefinition(&crdV1beta1)
48 if err := apiextV1beta1.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crdV1beta1, &crd, nil); err != nil {
49 return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
50 }
51 case apiextV1.SchemeGroupVersion.WithKind("CustomResourceDefinition"):
52 var crdV1 apiextV1.CustomResourceDefinition
53 if err := convert(untypedCRD, &crdV1); err != nil {
54 return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
55 }
56 apiextV1.SetDefaults_CustomResourceDefinition(&crdV1)
57 if err := apiextV1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crdV1, &crd, nil); err != nil {
58 return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
59 }
60 default:
61 err := fmt.Errorf("unrecognized CRD GroupVersionKind: %v", untypedCRD.GetObjectKind().GroupVersionKind())
62 return nil, fmt.Errorf("staticCRDs[%d]: %w", i, err)
63 }
64 for _, version := range crd.Spec.Versions {
65 static[TypeMeta{
66 APIVersion: crd.Spec.Group + "/" + version.Name,
67 Kind: crd.Spec.Names.Kind,
68 }] = &crd
69 }
70 }
71
72 return &Validator{
73 client: client,
74 static: static,
75
76 validators: make(map[TypeMeta]*validate.SchemaValidator),
77 }, nil
78 }
79
80 func (v *Validator) getCRD(ctx context.Context, tm TypeMeta) (*apiextVInternal.CustomResourceDefinition, error) {
81 if crd, ok := v.static[tm]; ok {
82 return crd, nil
83 }
84 if v.client != nil {
85 mapping, err := v.client.mappingFor(tm.GroupVersionKind().GroupKind().String())
86 if err != nil {
87 return nil, err
88 }
89 crd := mapping.Resource.GroupResource().String()
90
91 obj := &apiextV1.CustomResourceDefinition{
92 TypeMeta: TypeMeta{
93 Kind: "CustomResourceDefinition",
94 },
95 ObjectMeta: ObjectMeta{
96 Name: crd,
97 },
98 }
99 err = v.client.Get(ctx, obj, obj)
100 if err != nil {
101 if IsNotFound(err) {
102 return nil, nil
103 }
104
105 return nil, err
106 }
107
108 var ret apiextVInternal.CustomResourceDefinition
109 err = apiextV1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(obj, &ret, nil)
110 if err != nil {
111 return nil, err
112 }
113 return &ret, nil
114 }
115 return nil, nil
116 }
117
118 func (v *Validator) getValidator(ctx context.Context, tm TypeMeta) (*validate.SchemaValidator, error) {
119 v.mutex.Lock()
120 defer v.mutex.Unlock()
121
122 validator, ok := v.validators[tm]
123 if !ok {
124 crd, err := v.getCRD(ctx, tm)
125 if err != nil {
126 return nil, err
127 }
128
129 if crd != nil {
130 if crd.Spec.Validation != nil {
131 validator, _, err = validation.NewSchemaValidator(crd.Spec.Validation)
132 if err != nil {
133 return nil, err
134 }
135 } else {
136 tmVersion := path.Base(tm.APIVersion)
137 for _, version := range crd.Spec.Versions {
138 if version.Name == tmVersion {
139 validator, _, err = validation.NewSchemaValidator(version.Schema)
140 if err != nil {
141 return nil, err
142 }
143 break
144 }
145 }
146 }
147 }
148
149 v.validators[tm] = validator
150 }
151 return validator, nil
152 }
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169 func (v *Validator) Validate(ctx context.Context, resource interface{}) error {
170 var tm TypeMeta
171 err := convert(resource, &tm)
172 if err != nil {
173 return err
174 }
175
176 validator, err := v.getValidator(ctx, tm)
177 if err != nil {
178 return err
179 }
180
181 result := validator.Validate(resource)
182
183 var errs derror.MultiError
184 for _, e := range result.Errors {
185 errs = append(errs, e)
186 }
187
188 for _, w := range result.Warnings {
189 errs = append(errs, w)
190 }
191
192 if len(errs) > 0 {
193 return errs
194 }
195
196 return nil
197 }
198
View as plain text