sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/runtime/extensionconfig_webhook.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes 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 runtime
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/url"
    23  	"strings"
    24  
    25  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/util/validation"
    29  	"k8s.io/apimachinery/pkg/util/validation/field"
    30  	"k8s.io/utils/ptr"
    31  	ctrl "sigs.k8s.io/controller-runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    33  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    34  
    35  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    36  	"sigs.k8s.io/cluster-api/feature"
    37  )
    38  
    39  // ExtensionConfig is the webhook for runtimev1.ExtensionConfig.
    40  type ExtensionConfig struct{}
    41  
    42  func (webhook *ExtensionConfig) SetupWebhookWithManager(mgr ctrl.Manager) error {
    43  	return ctrl.NewWebhookManagedBy(mgr).
    44  		For(&runtimev1.ExtensionConfig{}).
    45  		WithDefaulter(webhook).
    46  		WithValidator(webhook).
    47  		Complete()
    48  }
    49  
    50  // +kubebuilder:webhook:verbs=create;update,path=/validate-runtime-cluster-x-k8s-io-v1alpha1-extensionconfig,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=runtime.cluster.x-k8s.io,resources=extensionconfigs,versions=v1alpha1,name=validation.extensionconfig.runtime.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    51  // +kubebuilder:webhook:verbs=create;update,path=/mutate-runtime-cluster-x-k8s-io-v1alpha1-extensionconfig,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=runtime.cluster.x-k8s.io,resources=extensionconfigs,versions=v1alpha1,name=default.extensionconfig.runtime.addons.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    52  
    53  var _ webhook.CustomValidator = &ExtensionConfig{}
    54  var _ webhook.CustomDefaulter = &ExtensionConfig{}
    55  
    56  // Default implements webhook.Defaulter so a webhook will be registered for the type.
    57  func (webhook *ExtensionConfig) Default(_ context.Context, obj runtime.Object) error {
    58  	extensionConfig, ok := obj.(*runtimev1.ExtensionConfig)
    59  	if !ok {
    60  		return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", obj))
    61  	}
    62  	// Default NamespaceSelector to an empty LabelSelector, which matches everything, if not set.
    63  	if extensionConfig.Spec.NamespaceSelector == nil {
    64  		extensionConfig.Spec.NamespaceSelector = &metav1.LabelSelector{}
    65  	}
    66  	if extensionConfig.Spec.ClientConfig.Service != nil {
    67  		if extensionConfig.Spec.ClientConfig.Service.Port == nil {
    68  			extensionConfig.Spec.ClientConfig.Service.Port = ptr.To[int32](443)
    69  		}
    70  	}
    71  	return nil
    72  }
    73  
    74  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
    75  func (webhook *ExtensionConfig) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    76  	extensionConfig, ok := obj.(*runtimev1.ExtensionConfig)
    77  	if !ok {
    78  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", obj))
    79  	}
    80  	return webhook.validate(ctx, nil, extensionConfig)
    81  }
    82  
    83  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
    84  func (webhook *ExtensionConfig) ValidateUpdate(ctx context.Context, old, updated runtime.Object) (admission.Warnings, error) {
    85  	oldExtensionConfig, ok := old.(*runtimev1.ExtensionConfig)
    86  	if !ok {
    87  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", old))
    88  	}
    89  	newExtensionConfig, ok := updated.(*runtimev1.ExtensionConfig)
    90  	if !ok {
    91  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", updated))
    92  	}
    93  	return webhook.validate(ctx, oldExtensionConfig, newExtensionConfig)
    94  }
    95  
    96  // validate validates an ExtensionConfig create or update.
    97  func (webhook *ExtensionConfig) validate(_ context.Context, _, newExtensionConfig *runtimev1.ExtensionConfig) (admission.Warnings, error) {
    98  	// NOTE: ExtensionConfig is behind the RuntimeSDK feature gate flag; the web hook
    99  	// must prevent creating and updating objects in case the feature flag is disabled.
   100  	if !feature.Gates.Enabled(feature.RuntimeSDK) {
   101  		return nil, field.Forbidden(
   102  			field.NewPath("spec"),
   103  			"can be set only if the RuntimeSDK feature flag is enabled",
   104  		)
   105  	}
   106  
   107  	var allErrs field.ErrorList
   108  
   109  	// Name should match Kubernetes naming conventions - validated based on DNS1123 label rules.
   110  	if errStrings := validation.IsDNS1123Label(newExtensionConfig.Name); len(errStrings) > 0 {
   111  		allErrs = append(allErrs, field.Invalid(
   112  			field.NewPath("metadata", "name"),
   113  			newExtensionConfig.Name,
   114  			fmt.Sprintf("ExtensionConfig name should be a valid DNS1123 label name: %s", errStrings)))
   115  	}
   116  	allErrs = append(allErrs, validateExtensionConfigSpec(newExtensionConfig)...)
   117  
   118  	if len(allErrs) > 0 {
   119  		return nil, apierrors.NewInvalid(runtimev1.GroupVersion.WithKind("ExtensionConfig").GroupKind(), newExtensionConfig.Name, allErrs)
   120  	}
   121  	return nil, nil
   122  }
   123  
   124  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
   125  func (webhook *ExtensionConfig) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
   126  	return nil, nil
   127  }
   128  
   129  func validateExtensionConfigSpec(e *runtimev1.ExtensionConfig) field.ErrorList {
   130  	var allErrs field.ErrorList
   131  
   132  	specPath := field.NewPath("spec")
   133  
   134  	if e.Spec.ClientConfig.URL == nil && e.Spec.ClientConfig.Service == nil {
   135  		allErrs = append(allErrs, field.Required(
   136  			specPath.Child("clientConfig"),
   137  			"either url or service must be defined",
   138  		))
   139  	}
   140  	if e.Spec.ClientConfig.URL != nil && e.Spec.ClientConfig.Service != nil {
   141  		allErrs = append(allErrs, field.Forbidden(
   142  			specPath.Child("clientConfig"),
   143  			"only one of url or service can be defined",
   144  		))
   145  	}
   146  
   147  	// Validate URL
   148  	if e.Spec.ClientConfig.URL != nil {
   149  		if uri, err := url.ParseRequestURI(*e.Spec.ClientConfig.URL); err != nil {
   150  			allErrs = append(allErrs, field.Invalid(
   151  				specPath.Child("clientConfig", "url"),
   152  				*e.Spec.ClientConfig.URL,
   153  				fmt.Sprintf("must be a valid URL, e.g. https://example.com: %v", err),
   154  			))
   155  		} else if uri.Scheme != "https" {
   156  			allErrs = append(allErrs, field.Invalid(
   157  				specPath.Child("clientConfig", "url"),
   158  				*e.Spec.ClientConfig.URL,
   159  				"'https' is the only allowed URL scheme, e.g. https://example.com",
   160  			))
   161  		}
   162  	}
   163  
   164  	// Validate Service if defined
   165  	if e.Spec.ClientConfig.Service != nil {
   166  		// Validate that the name is not empty and is a Valid RFC1123 name.
   167  		if e.Spec.ClientConfig.Service.Name == "" {
   168  			allErrs = append(allErrs, field.Required(
   169  				specPath.Child("clientConfig", "service", "name"),
   170  				"must not be empty",
   171  			))
   172  		}
   173  
   174  		for _, msg := range validation.IsDNS1035Label(e.Spec.ClientConfig.Service.Name) {
   175  			allErrs = append(allErrs, field.Invalid(
   176  				specPath.Child("clientConfig", "service", "name"),
   177  				e.Spec.ClientConfig.Service.Name,
   178  				msg,
   179  			))
   180  		}
   181  
   182  		if e.Spec.ClientConfig.Service.Namespace == "" {
   183  			allErrs = append(allErrs, field.Required(
   184  				specPath.Child("clientConfig", "service", "namespace"),
   185  				"must not be empty",
   186  			))
   187  		}
   188  
   189  		for _, msg := range validation.IsDNS1123Label(e.Spec.ClientConfig.Service.Namespace) {
   190  			allErrs = append(allErrs, field.Invalid(
   191  				specPath.Child("clientConfig", "service", "namespace"),
   192  				e.Spec.ClientConfig.Service.Namespace,
   193  				msg,
   194  			))
   195  		}
   196  
   197  		if e.Spec.ClientConfig.Service.Path != nil {
   198  			path := *e.Spec.ClientConfig.Service.Path
   199  			if _, err := url.ParseRequestURI(path); err != nil {
   200  				allErrs = append(allErrs, field.Invalid(
   201  					specPath.Child("clientConfig", "service", "path"),
   202  					path,
   203  					fmt.Sprintf("must be a valid URL path e.g. /path/to/hook: %v", err),
   204  				))
   205  			}
   206  			if !strings.HasPrefix(path, "/") {
   207  				allErrs = append(allErrs, field.Invalid(
   208  					specPath.Child("clientConfig", "service", "path"),
   209  					path,
   210  					"must start with \"/\" to be a valid URL path",
   211  				))
   212  			}
   213  		}
   214  		if e.Spec.ClientConfig.Service.Port != nil {
   215  			for _, msg := range validation.IsValidPortNum(int(*e.Spec.ClientConfig.Service.Port)) {
   216  				allErrs = append(allErrs, field.Invalid(
   217  					specPath.Child("clientConfig", "service", "port"),
   218  					*e.Spec.ClientConfig.Service.Port,
   219  					msg,
   220  				))
   221  			}
   222  		}
   223  	}
   224  	if e.Spec.NamespaceSelector == nil {
   225  		allErrs = append(allErrs, field.Required(
   226  			specPath.Child("namespaceSelector"),
   227  			"must be defined",
   228  		))
   229  	}
   230  
   231  	if _, err := metav1.LabelSelectorAsSelector(e.Spec.NamespaceSelector); err != nil {
   232  		allErrs = append(allErrs, field.Invalid(
   233  			specPath.Child("namespaceSelector"),
   234  			e.Spec.NamespaceSelector,
   235  			err.Error(),
   236  		))
   237  	}
   238  	return allErrs
   239  }