github.com/TheSpiritXIII/controller-tools@v0.14.1/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>,timeoutSeconds=<int>,admissionReviewVersions=<[]string>,reinvocationPolicy=<string>
    23  package webhook
    24  
    25  import (
    26  	"fmt"
    27  	"sort"
    28  	"strings"
    29  
    30  	admissionregv1 "k8s.io/api/admissionregistration/v1"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  
    34  	"github.com/TheSpiritXIII/controller-tools/pkg/genall"
    35  	"github.com/TheSpiritXIII/controller-tools/pkg/markers"
    36  )
    37  
    38  // The default {Mutating,Validating}WebhookConfiguration version to generate.
    39  const (
    40  	v1                    = "v1"
    41  	defaultWebhookVersion = v1
    42  )
    43  
    44  var (
    45  	// ConfigDefinition s a marker for defining Webhook manifests.
    46  	// Call ToWebhook on the value to get a Kubernetes Webhook.
    47  	ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{}))
    48  )
    49  
    50  // supportedWebhookVersions returns currently supported API version of {Mutating,Validating}WebhookConfiguration.
    51  func supportedWebhookVersions() []string {
    52  	return []string{defaultWebhookVersion}
    53  }
    54  
    55  // +controllertools:marker:generateHelp:category=Webhook
    56  
    57  // Config specifies how a webhook should be served.
    58  //
    59  // It specifies only the details that are intrinsic to the application serving
    60  // it (e.g. the resources it can handle, or the path it serves on).
    61  type Config struct {
    62  	// Mutating marks this as a mutating webhook (it's validating only if false)
    63  	//
    64  	// Mutating webhooks are allowed to change the object in their response,
    65  	// and are called *before* all validating webhooks.  Mutating webhooks may
    66  	// choose to reject an object, similarly to a validating webhook.
    67  	Mutating bool
    68  	// FailurePolicy specifies what should happen if the API server cannot reach the webhook.
    69  	//
    70  	// It may be either "ignore" (to skip the webhook and continue on) or "fail" (to reject
    71  	// the object in question).
    72  	FailurePolicy string
    73  	// MatchPolicy defines how the "rules" list is used to match incoming requests.
    74  	// Allowed values are "Exact" (match only if it exactly matches the specified rule)
    75  	// or "Equivalent" (match a request if it modifies a resource listed in rules, even via another API group or version).
    76  	MatchPolicy string `marker:",optional"`
    77  	// SideEffects specify whether calling the webhook will have side effects.
    78  	// This has an impact on dry runs and `kubectl diff`: if the sideEffect is "Unknown" (the default) or "Some", then
    79  	// the API server will not call the webhook on a dry-run request and fails instead.
    80  	// If the value is "None", then the webhook has no side effects and the API server will call it on dry-run.
    81  	// If the value is "NoneOnDryRun", then the webhook is responsible for inspecting the "dryRun" property of the
    82  	// AdmissionReview sent in the request, and avoiding side effects if that value is "true."
    83  	SideEffects string `marker:",optional"`
    84  	// TimeoutSeconds allows configuring how long the API server should wait for a webhook to respond before treating the call as a failure.
    85  	// If the timeout expires before the webhook responds, the webhook call will be ignored or the API call will be rejected based on the failure policy.
    86  	// The timeout value must be between 1 and 30 seconds.
    87  	// The timeout for an admission webhook defaults to 10 seconds.
    88  	TimeoutSeconds int `marker:",optional"`
    89  
    90  	// Groups specifies the API groups that this webhook receives requests for.
    91  	Groups []string
    92  	// Resources specifies the API resources that this webhook receives requests for.
    93  	Resources []string
    94  	// Verbs specifies the Kubernetes API verbs that this webhook receives requests for.
    95  	//
    96  	// Only modification-like verbs may be specified.
    97  	// May be "create", "update", "delete", "connect", or "*" (for all).
    98  	Verbs []string
    99  	// Versions specifies the API versions that this webhook receives requests for.
   100  	Versions []string
   101  
   102  	// Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots
   103  	Name string
   104  
   105  	// Path specifies that path that the API server should connect to this webhook on. Must be
   106  	// prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by
   107  	// $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group
   108  	// are substituted for hyphens. For example, a validating webhook path for type
   109  	// batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be
   110  	// /validate-batch-tutorial-kubebuilder-io-v1-cronjob
   111  	Path string `marker:"path,optional"`
   112  
   113  	// WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects
   114  	// itself to generate. The only supported value is v1. Defaults to v1.
   115  	WebhookVersions []string `marker:"webhookVersions,optional"`
   116  
   117  	// AdmissionReviewVersions is an ordered list of preferred `AdmissionReview`
   118  	// versions the Webhook expects.
   119  	AdmissionReviewVersions []string `marker:"admissionReviewVersions"`
   120  
   121  	// ReinvocationPolicy allows mutating webhooks to request reinvocation after other mutations
   122  	//
   123  	// To allow mutating admission plugins to observe changes made by other plugins,
   124  	// built-in mutating admission plugins are re-run if a mutating webhook modifies
   125  	// an object, and mutating webhooks can specify a reinvocationPolicy to control
   126  	// whether they are reinvoked as well.
   127  	ReinvocationPolicy string `marker:"reinvocationPolicy,optional"`
   128  
   129  	// URL allows mutating webhooks configuration to specify an external URL when generating
   130  	// the manifests, instead of using the internal service communication. Should be in format of
   131  	// https://address:port/path
   132  	// When this option is specified, the serviceConfig.Service is removed from webhook the manifest.
   133  	// The URL configuration should be between quotes.
   134  	// `url` cannot be specified when `path` is specified.
   135  	URL string `marker:"url,optional"`
   136  }
   137  
   138  // verbToAPIVariant converts a marker's verb to the proper value for the API.
   139  // Unrecognized verbs are passed through.
   140  func verbToAPIVariant(verbRaw string) admissionregv1.OperationType {
   141  	switch strings.ToLower(verbRaw) {
   142  	case strings.ToLower(string(admissionregv1.Create)):
   143  		return admissionregv1.Create
   144  	case strings.ToLower(string(admissionregv1.Update)):
   145  		return admissionregv1.Update
   146  	case strings.ToLower(string(admissionregv1.Delete)):
   147  		return admissionregv1.Delete
   148  	case strings.ToLower(string(admissionregv1.Connect)):
   149  		return admissionregv1.Connect
   150  	case strings.ToLower(string(admissionregv1.OperationAll)):
   151  		return admissionregv1.OperationAll
   152  	default:
   153  		return admissionregv1.OperationType(verbRaw)
   154  	}
   155  }
   156  
   157  // ToMutatingWebhook converts this rule to its Kubernetes API form.
   158  func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) {
   159  	if !c.Mutating {
   160  		return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name)
   161  	}
   162  
   163  	matchPolicy, err := c.matchPolicy()
   164  	if err != nil {
   165  		return admissionregv1.MutatingWebhook{}, err
   166  	}
   167  
   168  	clientConfig, err := c.clientConfig()
   169  	if err != nil {
   170  		return admissionregv1.MutatingWebhook{}, err
   171  	}
   172  
   173  	return admissionregv1.MutatingWebhook{
   174  		Name:                    c.Name,
   175  		Rules:                   c.rules(),
   176  		FailurePolicy:           c.failurePolicy(),
   177  		MatchPolicy:             matchPolicy,
   178  		ClientConfig:            clientConfig,
   179  		SideEffects:             c.sideEffects(),
   180  		TimeoutSeconds:          c.timeoutSeconds(),
   181  		AdmissionReviewVersions: c.AdmissionReviewVersions,
   182  		ReinvocationPolicy:      c.reinvocationPolicy(),
   183  	}, nil
   184  }
   185  
   186  // ToValidatingWebhook converts this rule to its Kubernetes API form.
   187  func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) {
   188  	if c.Mutating {
   189  		return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name)
   190  	}
   191  
   192  	matchPolicy, err := c.matchPolicy()
   193  	if err != nil {
   194  		return admissionregv1.ValidatingWebhook{}, err
   195  	}
   196  
   197  	clientConfig, err := c.clientConfig()
   198  	if err != nil {
   199  		return admissionregv1.ValidatingWebhook{}, err
   200  	}
   201  
   202  	return admissionregv1.ValidatingWebhook{
   203  		Name:                    c.Name,
   204  		Rules:                   c.rules(),
   205  		FailurePolicy:           c.failurePolicy(),
   206  		MatchPolicy:             matchPolicy,
   207  		ClientConfig:            clientConfig,
   208  		SideEffects:             c.sideEffects(),
   209  		TimeoutSeconds:          c.timeoutSeconds(),
   210  		AdmissionReviewVersions: c.AdmissionReviewVersions,
   211  	}, nil
   212  }
   213  
   214  // rules returns the configuration of what operations on what
   215  // resources/subresources a webhook should care about.
   216  func (c Config) rules() []admissionregv1.RuleWithOperations {
   217  	whConfig := admissionregv1.RuleWithOperations{
   218  		Rule: admissionregv1.Rule{
   219  			APIGroups:   c.Groups,
   220  			APIVersions: c.Versions,
   221  			Resources:   c.Resources,
   222  		},
   223  		Operations: make([]admissionregv1.OperationType, len(c.Verbs)),
   224  	}
   225  
   226  	for i, verbRaw := range c.Verbs {
   227  		whConfig.Operations[i] = verbToAPIVariant(verbRaw)
   228  	}
   229  
   230  	// fix the group names, since letting people type "core" is nice
   231  	for i, group := range whConfig.APIGroups {
   232  		if group == "core" {
   233  			whConfig.APIGroups[i] = ""
   234  		}
   235  	}
   236  
   237  	return []admissionregv1.RuleWithOperations{whConfig}
   238  }
   239  
   240  // failurePolicy converts the string value to the proper value for the API.
   241  // Unrecognized values are passed through.
   242  func (c Config) failurePolicy() *admissionregv1.FailurePolicyType {
   243  	var failurePolicy admissionregv1.FailurePolicyType
   244  	switch strings.ToLower(c.FailurePolicy) {
   245  	case strings.ToLower(string(admissionregv1.Ignore)):
   246  		failurePolicy = admissionregv1.Ignore
   247  	case strings.ToLower(string(admissionregv1.Fail)):
   248  		failurePolicy = admissionregv1.Fail
   249  	default:
   250  		failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy)
   251  	}
   252  	return &failurePolicy
   253  }
   254  
   255  // matchPolicy converts the string value to the proper value for the API.
   256  func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) {
   257  	var matchPolicy admissionregv1.MatchPolicyType
   258  	switch strings.ToLower(c.MatchPolicy) {
   259  	case strings.ToLower(string(admissionregv1.Exact)):
   260  		matchPolicy = admissionregv1.Exact
   261  	case strings.ToLower(string(admissionregv1.Equivalent)):
   262  		matchPolicy = admissionregv1.Equivalent
   263  	case "":
   264  		return nil, nil
   265  	default:
   266  		return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy)
   267  	}
   268  	return &matchPolicy, nil
   269  }
   270  
   271  // clientConfig returns the client config for a webhook.
   272  func (c Config) clientConfig() (admissionregv1.WebhookClientConfig, error) {
   273  	if (c.Path != "" && c.URL != "") || (c.Path == "" && c.URL == "") {
   274  		return admissionregv1.WebhookClientConfig{}, fmt.Errorf("`url` or `path` markers are required and mutually exclusive")
   275  	}
   276  
   277  	path := c.Path
   278  	if path != "" {
   279  		return admissionregv1.WebhookClientConfig{
   280  			Service: &admissionregv1.ServiceReference{
   281  				Name:      "webhook-service",
   282  				Namespace: "system",
   283  				Path:      &path,
   284  			},
   285  		}, nil
   286  	}
   287  
   288  	url := c.URL
   289  	return admissionregv1.WebhookClientConfig{
   290  		URL: &url,
   291  	}, nil
   292  
   293  }
   294  
   295  // sideEffects returns the sideEffects config for a webhook.
   296  func (c Config) sideEffects() *admissionregv1.SideEffectClass {
   297  	var sideEffects admissionregv1.SideEffectClass
   298  	switch strings.ToLower(c.SideEffects) {
   299  	case strings.ToLower(string(admissionregv1.SideEffectClassNone)):
   300  		sideEffects = admissionregv1.SideEffectClassNone
   301  	case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)):
   302  		sideEffects = admissionregv1.SideEffectClassNoneOnDryRun
   303  	case strings.ToLower(string(admissionregv1.SideEffectClassSome)):
   304  		sideEffects = admissionregv1.SideEffectClassSome
   305  	case "":
   306  		return nil
   307  	default:
   308  		return nil
   309  	}
   310  	return &sideEffects
   311  }
   312  
   313  // timeoutSeconds returns the timeoutSeconds config for a webhook.
   314  func (c Config) timeoutSeconds() *int32 {
   315  	if c.TimeoutSeconds != 0 {
   316  		timeoutSeconds := int32(c.TimeoutSeconds)
   317  		return &timeoutSeconds
   318  	}
   319  	return nil
   320  }
   321  
   322  // reinvocationPolicy returns the reinvocationPolicy config for a mutating webhook.
   323  func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType {
   324  	var reinvocationPolicy admissionregv1.ReinvocationPolicyType
   325  	switch strings.ToLower(c.ReinvocationPolicy) {
   326  	case strings.ToLower(string(admissionregv1.NeverReinvocationPolicy)):
   327  		reinvocationPolicy = admissionregv1.NeverReinvocationPolicy
   328  	case strings.ToLower(string(admissionregv1.IfNeededReinvocationPolicy)):
   329  		reinvocationPolicy = admissionregv1.IfNeededReinvocationPolicy
   330  	default:
   331  		return nil
   332  	}
   333  	return &reinvocationPolicy
   334  }
   335  
   336  // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook.
   337  func (c Config) webhookVersions() ([]string, error) {
   338  	// If WebhookVersions is not specified, we default it to `v1`.
   339  	if len(c.WebhookVersions) == 0 {
   340  		return []string{defaultWebhookVersion}, nil
   341  	}
   342  	supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...)
   343  	for _, version := range c.WebhookVersions {
   344  		if !supportedWebhookVersions.Has(version) {
   345  			return nil, fmt.Errorf("unsupported webhook version: %s", version)
   346  		}
   347  	}
   348  	return sets.NewString(c.WebhookVersions...).UnsortedList(), nil
   349  }
   350  
   351  // +controllertools:marker:generateHelp
   352  
   353  // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects.
   354  type Generator struct {
   355  	// HeaderFile specifies the header text (e.g. license) to prepend to generated files.
   356  	HeaderFile string `marker:",optional"`
   357  
   358  	// Year specifies the year to substitute for " YEAR" in the header file.
   359  	Year string `marker:",optional"`
   360  }
   361  
   362  func (Generator) RegisterMarkers(into *markers.Registry) error {
   363  	if err := into.Register(ConfigDefinition); err != nil {
   364  		return err
   365  	}
   366  	into.AddHelp(ConfigDefinition, Config{}.Help())
   367  	return nil
   368  }
   369  
   370  func (g Generator) Generate(ctx *genall.GenerationContext) error {
   371  	supportedWebhookVersions := supportedWebhookVersions()
   372  	mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions))
   373  	validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions))
   374  	for _, root := range ctx.Roots {
   375  		markerSet, err := markers.PackageMarkers(ctx.Collector, root)
   376  		if err != nil {
   377  			root.AddError(err)
   378  		}
   379  
   380  		cfgs := markerSet[ConfigDefinition.Name]
   381  		sort.SliceStable(cfgs, func(i, j int) bool {
   382  			return cfgs[i].(Config).Name < cfgs[j].(Config).Name
   383  		})
   384  
   385  		for _, cfg := range cfgs {
   386  			cfg := cfg.(Config)
   387  			webhookVersions, err := cfg.webhookVersions()
   388  			if err != nil {
   389  				return err
   390  			}
   391  			if cfg.Mutating {
   392  				w, err := cfg.ToMutatingWebhook()
   393  				if err != nil {
   394  					return err
   395  				}
   396  				for _, webhookVersion := range webhookVersions {
   397  					mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w)
   398  				}
   399  			} else {
   400  				w, err := cfg.ToValidatingWebhook()
   401  				if err != nil {
   402  					return err
   403  				}
   404  				for _, webhookVersion := range webhookVersions {
   405  					validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w)
   406  				}
   407  			}
   408  		}
   409  	}
   410  
   411  	versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions))
   412  	for _, version := range supportedWebhookVersions {
   413  		if cfgs, ok := mutatingCfgs[version]; ok {
   414  			// The only possible version in supportedWebhookVersions is v1,
   415  			// so use it for all versioned types in this context.
   416  			objRaw := &admissionregv1.MutatingWebhookConfiguration{}
   417  			objRaw.SetGroupVersionKind(schema.GroupVersionKind{
   418  				Group:   admissionregv1.SchemeGroupVersion.Group,
   419  				Version: version,
   420  				Kind:    "MutatingWebhookConfiguration",
   421  			})
   422  			objRaw.SetName("mutating-webhook-configuration")
   423  			objRaw.Webhooks = cfgs
   424  			for i := range objRaw.Webhooks {
   425  				// SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
   426  				// return an error
   427  				if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
   428  					return err
   429  				}
   430  				// TimeoutSeconds must be nil or between 1 and 30 seconds, otherwise,
   431  				// return an error
   432  				if err := checkTimeoutSeconds(objRaw.Webhooks[i].TimeoutSeconds); err != nil {
   433  					return err
   434  				}
   435  				// AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
   436  				// return an error
   437  				if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
   438  					return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
   439  				}
   440  			}
   441  			versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
   442  		}
   443  
   444  		if cfgs, ok := validatingCfgs[version]; ok {
   445  			// The only possible version in supportedWebhookVersions is v1,
   446  			// so use it for all versioned types in this context.
   447  			objRaw := &admissionregv1.ValidatingWebhookConfiguration{}
   448  			objRaw.SetGroupVersionKind(schema.GroupVersionKind{
   449  				Group:   admissionregv1.SchemeGroupVersion.Group,
   450  				Version: version,
   451  				Kind:    "ValidatingWebhookConfiguration",
   452  			})
   453  			objRaw.SetName("validating-webhook-configuration")
   454  			objRaw.Webhooks = cfgs
   455  			for i := range objRaw.Webhooks {
   456  				// SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
   457  				// return an error
   458  				if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
   459  					return err
   460  				}
   461  				// TimeoutSeconds must be nil or between 1 and 30 seconds, otherwise,
   462  				// return an error
   463  				if err := checkTimeoutSeconds(objRaw.Webhooks[i].TimeoutSeconds); err != nil {
   464  					return err
   465  				}
   466  				// AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
   467  				// return an error
   468  				if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
   469  					return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
   470  				}
   471  			}
   472  			versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
   473  		}
   474  	}
   475  
   476  	var headerText string
   477  	if g.HeaderFile != "" {
   478  		headerBytes, err := ctx.ReadFile(g.HeaderFile)
   479  		if err != nil {
   480  			return err
   481  		}
   482  		headerText = string(headerBytes)
   483  	}
   484  	headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year)
   485  
   486  	for k, v := range versionedWebhooks {
   487  		var fileName string
   488  		if k == defaultWebhookVersion {
   489  			fileName = fmt.Sprintf("manifests.yaml")
   490  		} else {
   491  			fileName = fmt.Sprintf("manifests.%s.yaml", k)
   492  		}
   493  		if err := ctx.WriteYAML(fileName, headerText, v, genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil {
   494  			return err
   495  		}
   496  	}
   497  	return nil
   498  }
   499  
   500  func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error {
   501  	if sideEffects == nil {
   502  		return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration")
   503  	}
   504  	if *sideEffects == admissionregv1.SideEffectClassUnknown ||
   505  		*sideEffects == admissionregv1.SideEffectClassSome {
   506  		return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration")
   507  	}
   508  	return nil
   509  }
   510  
   511  func checkTimeoutSeconds(timeoutSeconds *int32) error {
   512  	if timeoutSeconds != nil && (*timeoutSeconds < 1 || *timeoutSeconds > 30) {
   513  		return fmt.Errorf("TimeoutSeconds must be between 1 and 30 seconds")
   514  	}
   515  	return nil
   516  }