github.com/cilium/controller-tools@v0.3.1-0.20230329170030-f2b7ff866fde/pkg/webhook/parser.go (about)

     1  /*
     2  Copyright 2018 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 webhook contains libraries for generating webhookconfig manifests
    18  // from markers in Go source files.
    19  //
    20  // The markers take the form:
    21  //
    22  //  +kubebuilder:webhook:webhookVersions=<[]string>,failurePolicy=<string>,matchPolicy=<string>,groups=<[]string>,resources=<[]string>,verbs=<[]string>,versions=<[]string>,name=<string>,path=<string>,mutating=<bool>,sideEffects=<string>,admissionReviewVersions=<[]string>
    23  package webhook
    24  
    25  import (
    26  	"fmt"
    27  	"strings"
    28  
    29  	admissionregv1 "k8s.io/api/admissionregistration/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  
    34  	"sigs.k8s.io/controller-tools/pkg/genall"
    35  	"sigs.k8s.io/controller-tools/pkg/markers"
    36  )
    37  
    38  // The default {Mutating,Validating}WebhookConfiguration version to generate.
    39  const (
    40  	defaultWebhookVersion = "v1"
    41  )
    42  
    43  var (
    44  	// ConfigDefinition s a marker for defining Webhook manifests.
    45  	// Call ToWebhook on the value to get a Kubernetes Webhook.
    46  	ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{}))
    47  )
    48  
    49  // supportedWebhookVersions returns currently supported API version of {Mutating,Validating}WebhookConfiguration.
    50  func supportedWebhookVersions() []string {
    51  	return []string{defaultWebhookVersion, "v1beta1"}
    52  }
    53  
    54  // +controllertools:marker:generateHelp:category=Webhook
    55  
    56  // Config specifies how a webhook should be served.
    57  //
    58  // It specifies only the details that are intrinsic to the application serving
    59  // it (e.g. the resources it can handle, or the path it serves on).
    60  type Config struct {
    61  	// Mutating marks this as a mutating webhook (it's validating only if false)
    62  	//
    63  	// Mutating webhooks are allowed to change the object in their response,
    64  	// and are called *before* all validating webhooks.  Mutating webhooks may
    65  	// choose to reject an object, similarly to a validating webhook.
    66  	Mutating bool
    67  	// FailurePolicy specifies what should happen if the API server cannot reach the webhook.
    68  	//
    69  	// It may be either "ignore" (to skip the webhook and continue on) or "fail" (to reject
    70  	// the object in question).
    71  	FailurePolicy string
    72  	// MatchPolicy defines how the "rules" list is used to match incoming requests.
    73  	// Allowed values are "Exact" (match only if it exactly matches the specified rule)
    74  	// or "Equivalent" (match a request if it modifies a resource listed in rules, even via another API group or version).
    75  	MatchPolicy string `marker:",optional"`
    76  	// SideEffects specify whether calling the webhook will have side effects.
    77  	// This has an impact on dry runs and `kubectl diff`: if the sideEffect is "Unknown" (the default) or "Some", then
    78  	// the API server will not call the webhook on a dry-run request and fails instead.
    79  	// If the value is "None", then the webhook has no side effects and the API server will call it on dry-run.
    80  	// If the value is "NoneOnDryRun", then the webhook is responsible for inspecting the "dryRun" property of the
    81  	// AdmissionReview sent in the request, and avoiding side effects if that value is "true."
    82  	SideEffects string `marker:",optional"`
    83  
    84  	// Groups specifies the API groups that this webhook receives requests for.
    85  	Groups []string
    86  	// Resources specifies the API resources that this webhook receives requests for.
    87  	Resources []string
    88  	// Verbs specifies the Kubernetes API verbs that this webhook receives requests for.
    89  	//
    90  	// Only modification-like verbs may be specified.
    91  	// May be "create", "update", "delete", "connect", or "*" (for all).
    92  	Verbs []string
    93  	// Versions specifies the API versions that this webhook receives requests for.
    94  	Versions []string
    95  
    96  	// Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots
    97  	Name string
    98  
    99  	// Path specifies that path that the API server should connect to this webhook on. Must be
   100  	// prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by
   101  	// $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group
   102  	// are substituted for hyphens. For example, a validating webhook path for type
   103  	// batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be
   104  	// /validate-batch-tutorial-kubebuilder-io-v1-cronjob
   105  	Path string
   106  
   107  	// WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects
   108  	// itself to generate.  Defaults to v1.
   109  	WebhookVersions []string `marker:"webhookVersions,optional"`
   110  
   111  	// AdmissionReviewVersions is an ordered list of preferred `AdmissionReview`
   112  	// versions the Webhook expects.
   113  	// For generating v1 {Mutating,Validating}WebhookConfiguration, this is mandatory.
   114  	// For generating v1beta1 {Mutating,Validating}WebhookConfiguration, this is optional, and default to v1beta1.
   115  	AdmissionReviewVersions []string `marker:"admissionReviewVersions,optional"`
   116  }
   117  
   118  // verbToAPIVariant converts a marker's verb to the proper value for the API.
   119  // Unrecognized verbs are passed through.
   120  func verbToAPIVariant(verbRaw string) admissionregv1.OperationType {
   121  	switch strings.ToLower(verbRaw) {
   122  	case strings.ToLower(string(admissionregv1.Create)):
   123  		return admissionregv1.Create
   124  	case strings.ToLower(string(admissionregv1.Update)):
   125  		return admissionregv1.Update
   126  	case strings.ToLower(string(admissionregv1.Delete)):
   127  		return admissionregv1.Delete
   128  	case strings.ToLower(string(admissionregv1.Connect)):
   129  		return admissionregv1.Connect
   130  	case strings.ToLower(string(admissionregv1.OperationAll)):
   131  		return admissionregv1.OperationAll
   132  	default:
   133  		return admissionregv1.OperationType(verbRaw)
   134  	}
   135  }
   136  
   137  // ToMutatingWebhook converts this rule to its Kubernetes API form.
   138  func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) {
   139  	if !c.Mutating {
   140  		return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name)
   141  	}
   142  
   143  	matchPolicy, err := c.matchPolicy()
   144  	if err != nil {
   145  		return admissionregv1.MutatingWebhook{}, err
   146  	}
   147  
   148  	return admissionregv1.MutatingWebhook{
   149  		Name:                    c.Name,
   150  		Rules:                   c.rules(),
   151  		FailurePolicy:           c.failurePolicy(),
   152  		MatchPolicy:             matchPolicy,
   153  		ClientConfig:            c.clientConfig(),
   154  		SideEffects:             c.sideEffects(),
   155  		AdmissionReviewVersions: c.AdmissionReviewVersions,
   156  	}, nil
   157  }
   158  
   159  // ToValidatingWebhook converts this rule to its Kubernetes API form.
   160  func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) {
   161  	if c.Mutating {
   162  		return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name)
   163  	}
   164  
   165  	matchPolicy, err := c.matchPolicy()
   166  	if err != nil {
   167  		return admissionregv1.ValidatingWebhook{}, err
   168  	}
   169  
   170  	return admissionregv1.ValidatingWebhook{
   171  		Name:                    c.Name,
   172  		Rules:                   c.rules(),
   173  		FailurePolicy:           c.failurePolicy(),
   174  		MatchPolicy:             matchPolicy,
   175  		ClientConfig:            c.clientConfig(),
   176  		SideEffects:             c.sideEffects(),
   177  		AdmissionReviewVersions: c.AdmissionReviewVersions,
   178  	}, nil
   179  }
   180  
   181  // rules returns the configuration of what operations on what
   182  // resources/subresources a webhook should care about.
   183  func (c Config) rules() []admissionregv1.RuleWithOperations {
   184  	whConfig := admissionregv1.RuleWithOperations{
   185  		Rule: admissionregv1.Rule{
   186  			APIGroups:   c.Groups,
   187  			APIVersions: c.Versions,
   188  			Resources:   c.Resources,
   189  		},
   190  		Operations: make([]admissionregv1.OperationType, len(c.Verbs)),
   191  	}
   192  
   193  	for i, verbRaw := range c.Verbs {
   194  		whConfig.Operations[i] = verbToAPIVariant(verbRaw)
   195  	}
   196  
   197  	// fix the group names, since letting people type "core" is nice
   198  	for i, group := range whConfig.APIGroups {
   199  		if group == "core" {
   200  			whConfig.APIGroups[i] = ""
   201  		}
   202  	}
   203  
   204  	return []admissionregv1.RuleWithOperations{whConfig}
   205  }
   206  
   207  // failurePolicy converts the string value to the proper value for the API.
   208  // Unrecognized values are passed through.
   209  func (c Config) failurePolicy() *admissionregv1.FailurePolicyType {
   210  	var failurePolicy admissionregv1.FailurePolicyType
   211  	switch strings.ToLower(c.FailurePolicy) {
   212  	case strings.ToLower(string(admissionregv1.Ignore)):
   213  		failurePolicy = admissionregv1.Ignore
   214  	case strings.ToLower(string(admissionregv1.Fail)):
   215  		failurePolicy = admissionregv1.Fail
   216  	default:
   217  		failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy)
   218  	}
   219  	return &failurePolicy
   220  }
   221  
   222  // matchPolicy converts the string value to the proper value for the API.
   223  func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) {
   224  	var matchPolicy admissionregv1.MatchPolicyType
   225  	switch strings.ToLower(c.MatchPolicy) {
   226  	case strings.ToLower(string(admissionregv1.Exact)):
   227  		matchPolicy = admissionregv1.Exact
   228  	case strings.ToLower(string(admissionregv1.Equivalent)):
   229  		matchPolicy = admissionregv1.Equivalent
   230  	case "":
   231  		return nil, nil
   232  	default:
   233  		return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy)
   234  	}
   235  	return &matchPolicy, nil
   236  }
   237  
   238  // clientConfig returns the client config for a webhook.
   239  func (c Config) clientConfig() admissionregv1.WebhookClientConfig {
   240  	path := c.Path
   241  	return admissionregv1.WebhookClientConfig{
   242  		Service: &admissionregv1.ServiceReference{
   243  			Name:      "webhook-service",
   244  			Namespace: "system",
   245  			Path:      &path,
   246  		},
   247  		// OpenAPI marks the field as required before 1.13 because of a bug that got fixed in
   248  		// https://github.com/kubernetes/api/commit/e7d9121e9ffd63cea0288b36a82bcc87b073bd1b
   249  		// Put "\n" as an placeholder as a workaround til 1.13+ is almost everywhere.
   250  		CABundle: []byte("\n"),
   251  	}
   252  }
   253  
   254  // sideEffects returns the sideEffects config for a webhook.
   255  func (c Config) sideEffects() *admissionregv1.SideEffectClass {
   256  	var sideEffects admissionregv1.SideEffectClass
   257  	switch strings.ToLower(c.SideEffects) {
   258  	case strings.ToLower(string(admissionregv1.SideEffectClassNone)):
   259  		sideEffects = admissionregv1.SideEffectClassNone
   260  	case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)):
   261  		sideEffects = admissionregv1.SideEffectClassNoneOnDryRun
   262  	case strings.ToLower(string(admissionregv1.SideEffectClassSome)):
   263  		sideEffects = admissionregv1.SideEffectClassSome
   264  	case "":
   265  		return nil
   266  	default:
   267  		return nil
   268  	}
   269  	return &sideEffects
   270  }
   271  
   272  // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook.
   273  func (c Config) webhookVersions() ([]string, error) {
   274  	// If WebhookVersions is not specified, we default it to `v1`.
   275  	if len(c.WebhookVersions) == 0 {
   276  		return []string{defaultWebhookVersion}, nil
   277  	}
   278  	supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...)
   279  	for _, version := range c.WebhookVersions {
   280  		if !supportedWebhookVersions.Has(version) {
   281  			return nil, fmt.Errorf("unsupported webhook version: %s", version)
   282  		}
   283  	}
   284  	return sets.NewString(c.WebhookVersions...).UnsortedList(), nil
   285  }
   286  
   287  // +controllertools:marker:generateHelp
   288  
   289  // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects.
   290  type Generator struct{}
   291  
   292  func (Generator) RegisterMarkers(into *markers.Registry) error {
   293  	if err := into.Register(ConfigDefinition); err != nil {
   294  		return err
   295  	}
   296  	into.AddHelp(ConfigDefinition, Config{}.Help())
   297  	return nil
   298  }
   299  
   300  func (Generator) Generate(ctx *genall.GenerationContext) error {
   301  	supportedWebhookVersions := supportedWebhookVersions()
   302  	mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions))
   303  	validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions))
   304  	for _, root := range ctx.Roots {
   305  		markerSet, err := markers.PackageMarkers(ctx.Collector, root)
   306  		if err != nil {
   307  			root.AddError(err)
   308  		}
   309  
   310  		for _, cfg := range markerSet[ConfigDefinition.Name] {
   311  			cfg := cfg.(Config)
   312  			webhookVersions, err := cfg.webhookVersions()
   313  			if err != nil {
   314  				return err
   315  			}
   316  			if cfg.Mutating {
   317  				w, err := cfg.ToMutatingWebhook()
   318  				if err != nil {
   319  					return err
   320  				}
   321  				for _, webhookVersion := range webhookVersions {
   322  					mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w)
   323  				}
   324  			} else {
   325  				w, err := cfg.ToValidatingWebhook()
   326  				if err != nil {
   327  					return err
   328  				}
   329  				for _, webhookVersion := range webhookVersions {
   330  					validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w)
   331  				}
   332  			}
   333  		}
   334  	}
   335  
   336  	versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions))
   337  	for _, version := range supportedWebhookVersions {
   338  		if cfgs, ok := mutatingCfgs[version]; ok {
   339  			objRaw := &admissionregv1.MutatingWebhookConfiguration{
   340  				TypeMeta: metav1.TypeMeta{
   341  					Kind:       "MutatingWebhookConfiguration",
   342  					APIVersion: admissionregv1.SchemeGroupVersion.String(),
   343  				},
   344  				ObjectMeta: metav1.ObjectMeta{
   345  					Name: "mutating-webhook-configuration",
   346  				},
   347  				Webhooks: cfgs,
   348  			}
   349  			if version == defaultWebhookVersion {
   350  				for i := range objRaw.Webhooks {
   351  					// SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
   352  					// we return an error
   353  					if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
   354  						return err
   355  					}
   356  					// AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
   357  					// we return an error
   358  					if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
   359  						return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
   360  					}
   361  				}
   362  				versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
   363  			} else {
   364  				conv, err := MutatingWebhookConfigurationAsVersion(objRaw, schema.GroupVersion{Group: admissionregv1.SchemeGroupVersion.Group, Version: version})
   365  				if err != nil {
   366  					return err
   367  				}
   368  				versionedWebhooks[version] = append(versionedWebhooks[version], conv)
   369  			}
   370  		}
   371  
   372  		if cfgs, ok := validatingCfgs[version]; ok {
   373  			objRaw := &admissionregv1.ValidatingWebhookConfiguration{
   374  				TypeMeta: metav1.TypeMeta{
   375  					Kind:       "ValidatingWebhookConfiguration",
   376  					APIVersion: admissionregv1.SchemeGroupVersion.String(),
   377  				},
   378  				ObjectMeta: metav1.ObjectMeta{
   379  					Name: "validating-webhook-configuration",
   380  				},
   381  				Webhooks: cfgs,
   382  			}
   383  			if version == defaultWebhookVersion {
   384  				for i := range objRaw.Webhooks {
   385  					// SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
   386  					// we return an error
   387  					if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
   388  						return err
   389  					}
   390  					// AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
   391  					// we return an error
   392  					if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
   393  						return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
   394  					}
   395  				}
   396  				versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
   397  			} else {
   398  				conv, err := ValidatingWebhookConfigurationAsVersion(objRaw, schema.GroupVersion{Group: admissionregv1.SchemeGroupVersion.Group, Version: version})
   399  				if err != nil {
   400  					return err
   401  				}
   402  				versionedWebhooks[version] = append(versionedWebhooks[version], conv)
   403  			}
   404  		}
   405  	}
   406  
   407  	for k, v := range versionedWebhooks {
   408  		var fileName string
   409  		if k == defaultWebhookVersion {
   410  			fileName = fmt.Sprintf("manifests.yaml")
   411  		} else {
   412  			fileName = fmt.Sprintf("manifests.%s.yaml", k)
   413  		}
   414  		if err := ctx.WriteYAML(fileName, v...); err != nil {
   415  			return err
   416  		}
   417  	}
   418  	return nil
   419  }
   420  
   421  func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error {
   422  	if sideEffects == nil {
   423  		return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration")
   424  	}
   425  	if *sideEffects == admissionregv1.SideEffectClassUnknown ||
   426  		*sideEffects == admissionregv1.SideEffectClassSome {
   427  		return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration")
   428  	}
   429  	return nil
   430  }