crossplane
185 строк · 6.4 Кб
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 xrd contains internal logic linked to the validation of the v1.CompositeResourceDefinition type.
18package xrd
19
20import (
21"context"
22"errors"
23"fmt"
24
25apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
26kerrors "k8s.io/apimachinery/pkg/api/errors"
27"k8s.io/apimachinery/pkg/runtime"
28ctrl "sigs.k8s.io/controller-runtime"
29"sigs.k8s.io/controller-runtime/pkg/client"
30"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
31
32"github.com/crossplane/crossplane-runtime/pkg/controller"
33xperrors "github.com/crossplane/crossplane-runtime/pkg/errors"
34
35v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
36"github.com/crossplane/crossplane/internal/xcrd"
37)
38
39// Error strings.
40const (
41errNotCompositeResourceDefinition = "supplied object was not a CompositeResourceDefinition"
42
43errUnexpectedType = "unexpected type"
44)
45
46// SetupWebhookWithManager sets up the webhook with the manager.
47func SetupWebhookWithManager(mgr ctrl.Manager, _ controller.Options) error {
48v := &validator{client: mgr.GetClient()}
49return ctrl.NewWebhookManagedBy(mgr).
50WithValidator(v).
51For(&v1.CompositeResourceDefinition{}).
52Complete()
53}
54
55type validator struct {
56client client.Client
57}
58
59func getAllCRDsForXRD(in *v1.CompositeResourceDefinition) (out []*apiextv1.CustomResourceDefinition, err error) {
60crd, err := xcrd.ForCompositeResource(in)
61if err != nil {
62return out, xperrors.Wrap(err, "cannot get CRD for Composite Resource")
63}
64out = append(out, crd)
65// if claim enabled, validate claim CRD
66if in.Spec.ClaimNames == nil {
67return out, nil
68}
69crdClaim, err := xcrd.ForCompositeResourceClaim(in)
70if err != nil {
71return out, xperrors.Wrap(err, "cannot get Claim CRD for Composite Claim")
72}
73out = append(out, crdClaim)
74return out, nil
75}
76
77// ValidateCreate validates a Composition.
78func (v *validator) ValidateCreate(ctx context.Context, obj runtime.Object) (warns admission.Warnings, err error) {
79in, ok := obj.(*v1.CompositeResourceDefinition)
80if !ok {
81return nil, errors.New(errNotCompositeResourceDefinition)
82}
83validationWarns, validationErr := in.Validate()
84warns = append(warns, validationWarns...)
85if validationErr != nil {
86return validationWarns, validationErr.ToAggregate()
87}
88crds, err := getAllCRDsForXRD(in)
89if err != nil {
90return warns, xperrors.Wrap(err, "cannot get CRDs for CompositeResourceDefinition")
91}
92for _, crd := range crds {
93// Can't use validation.ValidateCustomResourceDefinition because it leads to dependency errors,
94// see https://github.com/kubernetes/apiextensions-apiserver/issues/59
95// if errs := validation.ValidateCustomResourceDefinition(ctx, crd); len(errs) != 0 {
96// return warns, errors.Wrap(errs.ToAggregate(), "invalid CRD generated for CompositeResourceDefinition")
97//}
98if err := v.client.Create(ctx, crd, client.DryRunAll); err != nil {
99return warns, v.rewriteError(err, in, crd)
100}
101}
102
103return warns, nil
104}
105
106// ValidateUpdate implements the same logic as ValidateCreate.
107func (v *validator) ValidateUpdate(ctx context.Context, old, new runtime.Object) (warns admission.Warnings, err error) {
108// Validate the update
109oldObj, ok := old.(*v1.CompositeResourceDefinition)
110if !ok {
111return nil, errors.New(errUnexpectedType)
112}
113newObj, ok := new.(*v1.CompositeResourceDefinition)
114if !ok {
115return nil, errors.New(errUnexpectedType)
116}
117// Validate the update
118validationWarns, validationErr := newObj.ValidateUpdate(oldObj)
119warns = append(warns, validationWarns...)
120if validationErr != nil {
121return validationWarns, validationErr.ToAggregate()
122}
123crds, err := getAllCRDsForXRD(newObj)
124if err != nil {
125return warns, xperrors.Wrap(err, "cannot get CRDs for CompositeResourceDefinition")
126}
127for _, crd := range crds {
128// Can't use validation.ValidateCustomResourceDefinition because it leads to dependency errors,
129// see https://github.com/kubernetes/apiextensions-apiserver/issues/59
130// if errs := validation.ValidateCustomResourceDefinition(ctx, crd); len(errs) != 0 {
131// return warns, errors.Wrap(errs.ToAggregate(), "invalid CRD generated for CompositeResourceDefinition")
132//}
133//
134// We need to be able to handle both cases:
135// 1. both CRDs exists already, which should be most of the time
136// 2. Claim's CRD does not exist yet, e.g. the user updated the XRD spec
137// which previously did not specify a claim.
138err := v.dryRunUpdateOrCreateIfNotFound(ctx, crd)
139if err != nil {
140return warns, v.rewriteError(err, newObj, crd)
141}
142}
143
144return warns, nil
145}
146
147func (v *validator) dryRunUpdateOrCreateIfNotFound(ctx context.Context, crd *apiextv1.CustomResourceDefinition) error {
148got := crd.DeepCopy()
149err := v.client.Get(ctx, client.ObjectKey{Name: crd.Name}, got)
150if err == nil {
151got.Spec = crd.Spec
152return v.client.Update(ctx, got, client.DryRunAll)
153}
154if kerrors.IsNotFound(err) {
155return v.client.Create(ctx, crd, client.DryRunAll)
156}
157return err
158}
159
160// ValidateDelete always allows delete requests.
161func (v *validator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
162return nil, nil
163}
164
165func (v *validator) rewriteError(err error, in *v1.CompositeResourceDefinition, crd *apiextv1.CustomResourceDefinition) error {
166// the handler is just discarding wrapping errors unfortunately, so
167// we need to unwrap it here, modify its content and return that
168// instead
169if err == nil {
170return nil
171}
172var apiErr *kerrors.StatusError
173if errors.As(err, &apiErr) {
174apiErr.ErrStatus.Message = "invalid CRD generated for CompositeResourceDefinition: " + apiErr.ErrStatus.Message
175apiErr.ErrStatus.Details.Kind = v1.CompositeResourceDefinitionKind
176apiErr.ErrStatus.Details.Group = v1.Group
177apiErr.ErrStatus.Details.Name = in.GetName()
178for i, cause := range apiErr.ErrStatus.Details.Causes {
179cause.Field = fmt.Sprintf("<generated_CRD_%q>.%s", crd.GetName(), cause.Field)
180apiErr.ErrStatus.Details.Causes[i] = cause
181}
182return apiErr
183}
184return err
185}
186