crossplane

Форк
0
243 строки · 8.5 Кб
1
/*
2
Copyright 2023 The Crossplane Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
// Package composition contains internal logic linked to the validation of the v1.Composition type.
18
package composition
19

20
import (
21
	"context"
22
	"fmt"
23

24
	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
25
	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
26
	kerrors "k8s.io/apimachinery/pkg/api/errors"
27
	"k8s.io/apimachinery/pkg/runtime"
28
	"k8s.io/apimachinery/pkg/runtime/schema"
29
	ctrl "sigs.k8s.io/controller-runtime"
30
	"sigs.k8s.io/controller-runtime/pkg/client"
31
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
32

33
	"github.com/crossplane/crossplane-runtime/pkg/controller"
34
	"github.com/crossplane/crossplane-runtime/pkg/errors"
35

36
	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
37
	"github.com/crossplane/crossplane/internal/features"
38
	"github.com/crossplane/crossplane/pkg/validation/apiextensions/v1/composition"
39
)
40

41
const (
42
	// Key used to index CRDs by "Kind" and "group", to be used when
43
	// indexing and retrieving needed CRDs.
44
	crdsIndexKey = "crd.kind.group"
45
)
46

47
// Error strings.
48
const (
49
	errNotComposition = "supplied object was not a Composition"
50
	errValidationMode = "cannot get validation mode"
51

52
	errFmtTooManyCRDs = "more than one CRD found for %s.%s: %v"
53
	errFmtGetCRDs     = "cannot get the needed CRDs: %v"
54
)
55

56
// SetupWebhookWithManager sets up the webhook with the manager.
57
func SetupWebhookWithManager(mgr ctrl.Manager, options controller.Options) error {
58
	if options.Features.Enabled(features.EnableBetaCompositionWebhookSchemaValidation) {
59
		// Setup an index on CRDs so we can retrieve them by group and kind.
60
		// The index is used by the getCRD function below.
61
		indexer := mgr.GetFieldIndexer()
62
		if err := indexer.IndexField(context.Background(), &extv1.CustomResourceDefinition{}, crdsIndexKey, func(obj client.Object) []string {
63
			return []string{getIndexValueForCRD(obj.(*extv1.CustomResourceDefinition))}
64
		}); err != nil {
65
			return err
66
		}
67
	}
68

69
	v := &validator{reader: mgr.GetClient(), options: options}
70
	return ctrl.NewWebhookManagedBy(mgr).
71
		WithValidator(v).
72
		For(&v1.Composition{}).
73
		Complete()
74
}
75

76
type validator struct {
77
	reader  client.Reader
78
	options controller.Options
79
}
80

81
// ValidateCreate validates a Composition.
82
func (v *validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { //nolint:gocyclo // Currently only at 11
83
	comp, ok := obj.(*v1.Composition)
84
	if !ok {
85
		return nil, errors.New(errNotComposition)
86
	}
87

88
	// Validate the composition itself, we'll disable it on the Validator below.
89
	warns, validationErrs := comp.Validate()
90
	if len(validationErrs) != 0 {
91
		return warns, kerrors.NewInvalid(comp.GroupVersionKind().GroupKind(), comp.GetName(), validationErrs)
92
	}
93

94
	if !v.options.Features.Enabled(features.EnableBetaCompositionWebhookSchemaValidation) {
95
		return warns, nil
96
	}
97

98
	// Get the composition validation mode from annotation
99
	validationMode, err := comp.GetSchemaAwareValidationMode()
100
	if err != nil {
101
		return warns, errors.Wrap(err, errValidationMode)
102
	}
103

104
	// Get all the needed CRDs, Composite Resource, Managed resources ... ?
105
	// Error out if missing in strict mode
106
	gkToCRD, errs := v.getNeededCRDs(ctx, comp)
107
	// If we have errors, and we are in strict mode or any of the errors is not
108
	// a NotFound, return them.
109
	if len(errs) != 0 {
110
		if validationMode == v1.SchemaAwareCompositionValidationModeStrict || containsOtherThanNotFound(errs) {
111
			return warns, errors.Errorf(errFmtGetCRDs, errs)
112
		}
113
		// If we have errors, but we are not in strict mode, and all of the
114
		// errors are not found errors, just move them to warnings and skip any
115
		// further validation.
116

117
		// TODO(phisco): we are playing it safe and skipping validation
118
		// altogether, in the future we might want to also support partially
119
		// available inputs.
120
		for _, err := range errs {
121
			warns = append(warns, err.Error())
122
		}
123
		return warns, nil
124
	}
125

126
	cv, err := composition.NewValidator(
127
		composition.WithCRDGetterFromMap(gkToCRD),
128
		// We disable logical Validation as this has already been done above.
129
		composition.WithoutLogicalValidation(),
130
	)
131
	if err != nil {
132
		return warns, kerrors.NewInternalError(err)
133
	}
134
	schemaWarns, errList := cv.Validate(ctx, comp)
135
	warns = append(warns, schemaWarns...)
136
	if len(errList) != 0 {
137
		if validationMode != v1.SchemaAwareCompositionValidationModeWarn {
138
			return warns, kerrors.NewInvalid(comp.GroupVersionKind().GroupKind(), comp.GetName(), errList)
139
		}
140
		for _, err := range errList {
141
			warns = append(warns, fmt.Sprintf("Composition %q invalid for schema-aware validation: %s", comp.GetName(), err))
142
		}
143
	}
144
	return warns, nil
145
}
146

147
// ValidateUpdate implements the same logic as ValidateCreate.
148
func (v *validator) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
149
	return v.ValidateCreate(ctx, newObj)
150
}
151

152
// ValidateDelete always allows delete requests.
153
func (v *validator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
154
	return nil, nil
155
}
156

157
// containsOtherThanNotFound returns true if the given slice of errors contains
158
// any error other than a not found error.
159
func containsOtherThanNotFound(errs []error) bool {
160
	for _, err := range errs {
161
		if !kerrors.IsNotFound(err) {
162
			return true
163
		}
164
	}
165
	return false
166
}
167

168
func (v *validator) getNeededCRDs(ctx context.Context, comp *v1.Composition) (map[schema.GroupKind]apiextensions.CustomResourceDefinition, []error) {
169
	// TODO(negz): Use https://pkg.go.dev/errors#Join to return a single error?
170
	var resultErrs []error
171
	neededCrds := make(map[schema.GroupKind]apiextensions.CustomResourceDefinition)
172

173
	// Get schema for the Composite Resource Definition defined by
174
	// comp.Spec.CompositeTypeRef.
175
	compositeResGK := schema.FromAPIVersionAndKind(comp.Spec.CompositeTypeRef.APIVersion,
176
		comp.Spec.CompositeTypeRef.Kind).GroupKind()
177

178
	compositeCRD, err := v.getCRD(ctx, &compositeResGK)
179
	if err != nil {
180
		if !kerrors.IsNotFound(err) {
181
			return nil, []error{err}
182
		}
183
		resultErrs = append(resultErrs, err)
184
	}
185
	if compositeCRD != nil {
186
		neededCrds[compositeResGK] = *compositeCRD
187
	}
188

189
	// Get schema for all Managed Resource Definitions defined by
190
	// comp.Spec.Resources.
191
	for _, res := range comp.Spec.Resources {
192
		res := res
193
		gvk, err := composition.GetBaseObjectGVK(&res)
194
		if err != nil {
195
			return nil, []error{err}
196
		}
197
		gk := gvk.GroupKind()
198
		crd, err := v.getCRD(ctx, &gk)
199
		switch {
200
		case kerrors.IsNotFound(err):
201
			resultErrs = append(resultErrs, err)
202
		case err != nil:
203
			return nil, []error{err}
204
		case crd != nil:
205
			neededCrds[gk] = *crd
206
		}
207
	}
208

209
	return neededCrds, resultErrs
210
}
211

212
// getCRD returns the validation schema for the given GVK, by looking up the CRD
213
// by group and kind using the provided client.
214
func (v *validator) getCRD(ctx context.Context, gk *schema.GroupKind) (*apiextensions.CustomResourceDefinition, error) {
215
	crds := extv1.CustomResourceDefinitionList{}
216
	if err := v.reader.List(ctx, &crds, client.MatchingFields{crdsIndexKey: getIndexValueForGroupKind(gk)}); err != nil {
217
		return nil, err
218
	}
219
	switch {
220
	case len(crds.Items) == 0:
221
		return nil, kerrors.NewNotFound(schema.GroupResource{Group: "apiextensions.k8s.io", Resource: "CustomResourceDefinition"}, fmt.Sprintf("%s.%s", gk.Kind, gk.Group))
222
	case len(crds.Items) > 1:
223
		names := []string{}
224
		for _, crd := range crds.Items {
225
			names = append(names, crd.Name)
226
		}
227
		return nil, kerrors.NewInternalError(errors.Errorf(errFmtTooManyCRDs, gk.Kind, gk.Group, names))
228
	}
229
	crd := crds.Items[0]
230
	internal := &apiextensions.CustomResourceDefinition{}
231
	return internal, extv1.Convert_v1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(&crd, internal, nil)
232
}
233

234
// getIndexValueForCRD returns the index value for the given CRD, according to
235
// the resource defined in the spec.
236
func getIndexValueForCRD(crd *extv1.CustomResourceDefinition) string {
237
	return getIndexValueForGroupKind(&schema.GroupKind{Group: crd.Spec.Group, Kind: crd.Spec.Names.Kind})
238
}
239

240
// getIndexValueForGroupKind returns the index value for the given GroupKind.
241
func getIndexValueForGroupKind(gk *schema.GroupKind) string {
242
	return gk.String()
243
}
244

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.