crossplane
243 строки · 8.5 Кб
1/*
2Copyright 2023 The Crossplane Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// Package composition contains internal logic linked to the validation of the v1.Composition type.
18package composition19
20import (21"context"22"fmt"23
24"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"25extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"26kerrors "k8s.io/apimachinery/pkg/api/errors"27"k8s.io/apimachinery/pkg/runtime"28"k8s.io/apimachinery/pkg/runtime/schema"29ctrl "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
36v1 "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
41const (42// Key used to index CRDs by "Kind" and "group", to be used when43// indexing and retrieving needed CRDs.44crdsIndexKey = "crd.kind.group"45)
46
47// Error strings.
48const (49errNotComposition = "supplied object was not a Composition"50errValidationMode = "cannot get validation mode"51
52errFmtTooManyCRDs = "more than one CRD found for %s.%s: %v"53errFmtGetCRDs = "cannot get the needed CRDs: %v"54)
55
56// SetupWebhookWithManager sets up the webhook with the manager.
57func SetupWebhookWithManager(mgr ctrl.Manager, options controller.Options) error {58if 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.61indexer := mgr.GetFieldIndexer()62if err := indexer.IndexField(context.Background(), &extv1.CustomResourceDefinition{}, crdsIndexKey, func(obj client.Object) []string {63return []string{getIndexValueForCRD(obj.(*extv1.CustomResourceDefinition))}64}); err != nil {65return err66}67}68
69v := &validator{reader: mgr.GetClient(), options: options}70return ctrl.NewWebhookManagedBy(mgr).71WithValidator(v).72For(&v1.Composition{}).73Complete()74}
75
76type validator struct {77reader client.Reader78options controller.Options79}
80
81// ValidateCreate validates a Composition.
82func (v *validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { //nolint:gocyclo // Currently only at 1183comp, ok := obj.(*v1.Composition)84if !ok {85return nil, errors.New(errNotComposition)86}87
88// Validate the composition itself, we'll disable it on the Validator below.89warns, validationErrs := comp.Validate()90if len(validationErrs) != 0 {91return warns, kerrors.NewInvalid(comp.GroupVersionKind().GroupKind(), comp.GetName(), validationErrs)92}93
94if !v.options.Features.Enabled(features.EnableBetaCompositionWebhookSchemaValidation) {95return warns, nil96}97
98// Get the composition validation mode from annotation99validationMode, err := comp.GetSchemaAwareValidationMode()100if err != nil {101return warns, errors.Wrap(err, errValidationMode)102}103
104// Get all the needed CRDs, Composite Resource, Managed resources ... ?105// Error out if missing in strict mode106gkToCRD, errs := v.getNeededCRDs(ctx, comp)107// If we have errors, and we are in strict mode or any of the errors is not108// a NotFound, return them.109if len(errs) != 0 {110if validationMode == v1.SchemaAwareCompositionValidationModeStrict || containsOtherThanNotFound(errs) {111return warns, errors.Errorf(errFmtGetCRDs, errs)112}113// If we have errors, but we are not in strict mode, and all of the114// errors are not found errors, just move them to warnings and skip any115// further validation.116
117// TODO(phisco): we are playing it safe and skipping validation118// altogether, in the future we might want to also support partially119// available inputs.120for _, err := range errs {121warns = append(warns, err.Error())122}123return warns, nil124}125
126cv, err := composition.NewValidator(127composition.WithCRDGetterFromMap(gkToCRD),128// We disable logical Validation as this has already been done above.129composition.WithoutLogicalValidation(),130)131if err != nil {132return warns, kerrors.NewInternalError(err)133}134schemaWarns, errList := cv.Validate(ctx, comp)135warns = append(warns, schemaWarns...)136if len(errList) != 0 {137if validationMode != v1.SchemaAwareCompositionValidationModeWarn {138return warns, kerrors.NewInvalid(comp.GroupVersionKind().GroupKind(), comp.GetName(), errList)139}140for _, err := range errList {141warns = append(warns, fmt.Sprintf("Composition %q invalid for schema-aware validation: %s", comp.GetName(), err))142}143}144return warns, nil145}
146
147// ValidateUpdate implements the same logic as ValidateCreate.
148func (v *validator) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) (admission.Warnings, error) {149return v.ValidateCreate(ctx, newObj)150}
151
152// ValidateDelete always allows delete requests.
153func (v *validator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {154return nil, nil155}
156
157// containsOtherThanNotFound returns true if the given slice of errors contains
158// any error other than a not found error.
159func containsOtherThanNotFound(errs []error) bool {160for _, err := range errs {161if !kerrors.IsNotFound(err) {162return true163}164}165return false166}
167
168func (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?170var resultErrs []error171neededCrds := make(map[schema.GroupKind]apiextensions.CustomResourceDefinition)172
173// Get schema for the Composite Resource Definition defined by174// comp.Spec.CompositeTypeRef.175compositeResGK := schema.FromAPIVersionAndKind(comp.Spec.CompositeTypeRef.APIVersion,176comp.Spec.CompositeTypeRef.Kind).GroupKind()177
178compositeCRD, err := v.getCRD(ctx, &compositeResGK)179if err != nil {180if !kerrors.IsNotFound(err) {181return nil, []error{err}182}183resultErrs = append(resultErrs, err)184}185if compositeCRD != nil {186neededCrds[compositeResGK] = *compositeCRD187}188
189// Get schema for all Managed Resource Definitions defined by190// comp.Spec.Resources.191for _, res := range comp.Spec.Resources {192res := res193gvk, err := composition.GetBaseObjectGVK(&res)194if err != nil {195return nil, []error{err}196}197gk := gvk.GroupKind()198crd, err := v.getCRD(ctx, &gk)199switch {200case kerrors.IsNotFound(err):201resultErrs = append(resultErrs, err)202case err != nil:203return nil, []error{err}204case crd != nil:205neededCrds[gk] = *crd206}207}208
209return neededCrds, resultErrs210}
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.
214func (v *validator) getCRD(ctx context.Context, gk *schema.GroupKind) (*apiextensions.CustomResourceDefinition, error) {215crds := extv1.CustomResourceDefinitionList{}216if err := v.reader.List(ctx, &crds, client.MatchingFields{crdsIndexKey: getIndexValueForGroupKind(gk)}); err != nil {217return nil, err218}219switch {220case len(crds.Items) == 0:221return nil, kerrors.NewNotFound(schema.GroupResource{Group: "apiextensions.k8s.io", Resource: "CustomResourceDefinition"}, fmt.Sprintf("%s.%s", gk.Kind, gk.Group))222case len(crds.Items) > 1:223names := []string{}224for _, crd := range crds.Items {225names = append(names, crd.Name)226}227return nil, kerrors.NewInternalError(errors.Errorf(errFmtTooManyCRDs, gk.Kind, gk.Group, names))228}229crd := crds.Items[0]230internal := &apiextensions.CustomResourceDefinition{}231return 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.
236func getIndexValueForCRD(crd *extv1.CustomResourceDefinition) string {237return getIndexValueForGroupKind(&schema.GroupKind{Group: crd.Spec.Group, Kind: crd.Spec.Names.Kind})238}
239
240// getIndexValueForGroupKind returns the index value for the given GroupKind.
241func getIndexValueForGroupKind(gk *schema.GroupKind) string {242return gk.String()243}
244