-
Notifications
You must be signed in to change notification settings - Fork 887
/
validating_handler.go
337 lines (299 loc) · 12.8 KB
/
validating_handler.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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package applicationconfiguration
import (
"context"
"fmt"
"net/http"
"strings"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
controller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
const (
errFmtWorkloadNameNotEmpty = "versioning-enabled component's workload name MUST NOT be assigned, expect workload name %q to be empty"
errFmtRevisionName = "componentName %q and revisionName %q are mutually exclusive, you can only specify one of them"
errFmtUnappliableTrait = "the trait %q cannot apply to workload %q of component %q (appliable: %q)"
errFmtTraitConflict = "conflict(rule: %q) between traits (%q and %q) of component %q is detected"
errFmtTraitConflictWithAll = "trait %q of component %q conflicts with all other traits"
errFmtInvalidLabelSelector = "labelSelector in conflict rule (%q) is invalid for %w"
// WorkloadNamePath indicates field path of workload name
WorkloadNamePath = "metadata.name"
)
var appConfigResource = v1alpha2.SchemeGroupVersion.WithResource("applicationconfigurations")
// AppConfigValidator provides functions to validate ApplicationConfiguration
type AppConfigValidator interface {
Validate(context.Context, ValidatingAppConfig) []error
}
// AppConfigValidateFunc implements function to validate ApplicationConfiguration
type AppConfigValidateFunc func(context.Context, ValidatingAppConfig) []error
// Validate validates ApplicationConfiguration
func (fn AppConfigValidateFunc) Validate(ctx context.Context, v ValidatingAppConfig) []error {
return fn(ctx, v)
}
// ValidatingHandler handles CloneSet
type ValidatingHandler struct {
Client client.Client
Mapper discoverymapper.DiscoveryMapper
// Decoder decodes objects
Decoder *admission.Decoder
Validators []AppConfigValidator
}
var _ admission.Handler = &ValidatingHandler{}
// Handle validate ApplicationConfiguration Spec here
func (h *ValidatingHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
app := &v1alpha2.ApplicationConfiguration{}
if req.Resource.String() != appConfigResource.String() {
return admission.Errored(http.StatusBadRequest, fmt.Errorf("expect resource to be %s", appConfigResource))
}
err := h.Decoder.Decode(req, app)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if !app.ObjectMeta.DeletionTimestamp.IsZero() {
// TODO: validate finalizer too
// skip validating the AppConfig being deleted
klog.Info("skip validating applicationConfiguration being deleted", " name: ", app.Name,
" deletiongTimestamp: ", app.GetDeletionTimestamp())
return admission.ValidationResponse(true, "")
}
switch req.Operation {
case admissionv1.Delete:
if len(req.OldObject.Raw) != 0 {
if err := h.Decoder.DecodeRaw(req.OldObject, app); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
} else {
// TODO(wonderflow): we can audit delete or something else here.
klog.Info("deleting Application Configuration", req.Name)
}
case admissionv1.Update:
oldApp := &v1alpha2.ApplicationConfiguration{}
if err := h.Decoder.DecodeRaw(req.AdmissionRequest.OldObject, oldApp); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if allErrs := h.ValidateUpdate(ctx, app, oldApp); len(allErrs) > 0 {
return admission.Errored(http.StatusUnprocessableEntity, allErrs.ToAggregate())
}
case admissionv1.Create:
if allErrs := h.ValidateCreate(ctx, app); len(allErrs) > 0 {
return admission.Errored(http.StatusUnprocessableEntity, allErrs.ToAggregate())
}
default:
// Do nothing for CONNECT
}
return admission.ValidationResponse(true, "")
}
// ValidateCreate validates the Application on creation
func (h *ValidatingHandler) ValidateCreate(ctx context.Context, obj *v1alpha2.ApplicationConfiguration) field.ErrorList {
var componentErrs field.ErrorList
vAppConfig := &ValidatingAppConfig{}
ctx = util.SetNamespaceInCtx(ctx, obj.Namespace)
if err := vAppConfig.PrepareForValidation(ctx, h.Client, h.Mapper, obj); err != nil {
klog.InfoS("failed to prepare information before validation ", " name: ", obj.Name, " errMsg: ", err.Error())
componentErrs = append(componentErrs, field.Invalid(field.NewPath("spec"), obj.Spec,
fmt.Sprintf("failed to prepare information before validation, err = %s", err.Error())))
return componentErrs
}
for _, validator := range h.Validators {
if allErrs := validator.Validate(ctx, *vAppConfig); len(allErrs) != 0 {
// utilerrors.NewAggregate can remove nil from allErrs
klog.InfoS("validation failed", " name: ", obj.Name, " errMsgi: ",
utilerrors.NewAggregate(allErrs).Error())
for _, err := range allErrs {
componentErrs = append(componentErrs, field.Invalid(field.NewPath("spec"), obj.Spec,
fmt.Sprintf("validation failed, err = %s", err.Error())))
}
}
}
return componentErrs
}
// ValidateUpdate validates the Application on update
func (h *ValidatingHandler) ValidateUpdate(ctx context.Context, newApp, oldApp *v1alpha2.ApplicationConfiguration) field.ErrorList {
// check if the newApp is valid
componentErrs := h.ValidateCreate(ctx, newApp)
// TODO: add more oam.AnnotationAppRollout
return componentErrs
}
// ValidateRevisionNameFn validates revisionName and componentName are assigned both.
func ValidateRevisionNameFn(_ context.Context, v ValidatingAppConfig) []error {
klog.Info("validate revisionName in applicationConfiguration", "name", v.appConfig.Name)
var allErrs []error
for _, c := range v.validatingComps {
if c.appConfigComponent.ComponentName != "" && c.appConfigComponent.RevisionName != "" {
allErrs = append(allErrs, fmt.Errorf(errFmtRevisionName,
c.appConfigComponent.ComponentName, c.appConfigComponent.RevisionName))
}
}
return allErrs
}
// ValidateWorkloadNameForVersioningFn validates workload name for version-enabled component
func ValidateWorkloadNameForVersioningFn(_ context.Context, v ValidatingAppConfig) []error {
var allErrs []error
for _, c := range v.validatingComps {
isVersionEnabled := false
for _, t := range c.validatingTraits {
if t.traitDefinition.Spec.RevisionEnabled {
isVersionEnabled = true
break
}
}
if isVersionEnabled {
if ok, workloadName := checkParams(c.component.Spec.Parameters, c.appConfigComponent.ParameterValues); !ok {
allErrs = append(allErrs, fmt.Errorf(errFmtWorkloadNameNotEmpty, workloadName))
}
if workloadName := c.workloadContent.GetName(); workloadName != "" {
allErrs = append(allErrs, fmt.Errorf(errFmtWorkloadNameNotEmpty, workloadName))
}
}
}
return allErrs
}
// ValidateTraitAppliableToWorkloadFn validates whether a trait is allowed to apply to the workload.
func ValidateTraitAppliableToWorkloadFn(_ context.Context, v ValidatingAppConfig) []error {
klog.Info("validate trait is appliable to workload", "name", v.appConfig.Name)
var allErrs []error
for _, c := range v.validatingComps {
// TODO(roywang) consider a CRD group could have multiple versions
// and maybe we need to specify the minimum version here in the future
workloadType := c.workloadDefinition.Spec.Reference.Name
workloadTypeGroup := schema.ParseGroupResource(workloadType).Group
klog.InfoS("validate trait is appliable to workload: ",
"workloadType", workloadType, "workloadTypeGroup", workloadTypeGroup)
ValidateApplyTo:
for _, t := range c.validatingTraits {
if len(t.traitDefinition.Spec.AppliesToWorkloads) == 0 {
// AppliesToWorkloads is empty, the trait can be applied to ANY workload
continue
}
for _, applyTo := range t.traitDefinition.Spec.AppliesToWorkloads {
if applyTo == "*" {
// "*" means the trait can be applied to ANY workload
continue ValidateApplyTo
}
if strings.HasPrefix(applyTo, "*.") && workloadTypeGroup == applyTo[2:] {
continue ValidateApplyTo
}
if workloadType == applyTo {
continue ValidateApplyTo
}
}
allErrs = append(allErrs, fmt.Errorf(errFmtUnappliableTrait,
t.traitDefinition.GetName(),
c.workloadDefinition.GetName(),
c.compName, t.traitDefinition.Spec.AppliesToWorkloads))
}
}
return allErrs
}
// ValidateTraitConflictFn validates whether conflicting traits are applied to the same workload.
// NOTE(roywang) It returns immediately if one conflict is detected
// instead of returning after collecting ALL conflicts
func ValidateTraitConflictFn(_ context.Context, v ValidatingAppConfig) []error {
klog.Info("validate trait conflicts ", "appconfig name:", v.appConfig.Name)
allErrs := make([]error, 0)
for _, comp := range v.validatingComps {
allConflictRules := map[string][]string{}
// collect conflicts rules of all traits applied to this workload
for _, trait := range comp.validatingTraits {
allConflictRules[trait.traitDefinition.Name] = trait.traitDefinition.Spec.ConflictsWith
}
for rulesOwner, rules := range allConflictRules {
if len(rules) == 0 {
// empty rules means this trait can work with any other ones
continue
}
for _, rule := range rules {
if rule == "*" && len(comp.validatingTraits) != 1 {
// '*' means this trait conflicts with all other ones
// validation fails unless there's only one trait
allErrs = append(allErrs, fmt.Errorf(errFmtTraitConflictWithAll, rulesOwner, comp.compName))
return allErrs
}
}
// validate each rule on each trait
for _, rule := range rules {
var ruleLabelSelector labels.Selector
var err error
if strings.HasPrefix(rule, "labelSelector:") {
ruleLabelSelector, err = labels.Parse(rule[len("labelSelector:"):])
if err != nil {
validationErr := fmt.Errorf(errFmtInvalidLabelSelector, rule, err)
allErrs = append(allErrs, validationErr)
return allErrs
}
}
for _, trait := range comp.validatingTraits {
traitDefName := trait.traitDefinition.Name
if traitDefName == rulesOwner {
// skip self-check
continue
}
// TODO(roywang) consider a CRD group could have multiple versions
// and maybe we need to specify the minimum version here in the future
// according to OAM convention, Spec.Reference.Name in traitDefinition is CRD name
traitCRDName := trait.traitDefinition.Spec.Reference.Name
traitGroup := schema.ParseGroupResource(traitCRDName).Group
traitLabelSet := labels.Set(trait.traitDefinition.Labels)
if (strings.HasPrefix(rule, "*.") && traitGroup == rule[2:]) || // API group conflict
traitCRDName == rule || // CRD name conflict
traitDefName == rule || // trait definition name conflict
(ruleLabelSelector != nil && ruleLabelSelector.Matches(traitLabelSet)) { // labels conflict
err := fmt.Errorf(errFmtTraitConflict, rule, rulesOwner, traitDefName, comp.compName)
allErrs = append(allErrs, err)
return allErrs
}
}
}
}
}
return allErrs
}
var _ inject.Client = &ValidatingHandler{}
// InjectClient injects the client into the ValidatingHandler
func (h *ValidatingHandler) InjectClient(c client.Client) error {
h.Client = c
return nil
}
var _ admission.DecoderInjector = &ValidatingHandler{}
// InjectDecoder injects the decoder into the ValidatingHandler
func (h *ValidatingHandler) InjectDecoder(d *admission.Decoder) error {
h.Decoder = d
return nil
}
// RegisterValidatingHandler will register application configuration validation to webhook
func RegisterValidatingHandler(mgr manager.Manager, args controller.Args) {
server := mgr.GetWebhookServer()
server.Register("/validating-core-oam-dev-v1alpha2-applicationconfigurations", &webhook.Admission{Handler: &ValidatingHandler{
Mapper: args.DiscoveryMapper,
Validators: []AppConfigValidator{
AppConfigValidateFunc(ValidateRevisionNameFn),
AppConfigValidateFunc(ValidateWorkloadNameForVersioningFn),
AppConfigValidateFunc(ValidateTraitAppliableToWorkloadFn),
AppConfigValidateFunc(ValidateTraitConflictFn),
// TODO(wonderflow): Add more validation logic here.
},
}})
}