github.com/alex123012/deckhouse-controller-tools@v0.0.0-20230510090815-d594daf1af8c/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>,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  	"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  	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  
    85  	// Groups specifies the API groups that this webhook receives requests for.
    86  	Groups []string
    87  	// Resources specifies the API resources that this webhook receives requests for.
    88  	Resources []string
    89  	// Verbs specifies the Kubernetes API verbs that this webhook receives requests for.
    90  	//
    91  	// Only modification-like verbs may be specified.
    92  	// May be "create", "update", "delete", "connect", or "*" (for all).
    93  	Verbs []string
    94  	// Versions specifies the API versions that this webhook receives requests for.
    95  	Versions []string
    96  
    97  	// Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots
    98  	Name string
    99  
   100  	// Path specifies that path that the API server should connect to this webhook on. Must be
   101  	// prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by
   102  	// $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group
   103  	// are substituted for hyphens. For example, a validating webhook path for type
   104  	// batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be
   105  	// /validate-batch-tutorial-kubebuilder-io-v1-cronjob
   106  	Path string
   107  
   108  	// WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects
   109  	// itself to generate. The only supported value is v1. Defaults to v1.
   110  	WebhookVersions []string `marker:"webhookVersions,optional"`
   111  
   112  	// AdmissionReviewVersions is an ordered list of preferred `AdmissionReview`
   113  	// versions the Webhook expects.
   114  	AdmissionReviewVersions []string `marker:"admissionReviewVersions"`
   115  
   116  	// ReinvocationPolicy allows mutating webhooks to request reinvocation after other mutations
   117  	//
   118  	// To allow mutating admission plugins to observe changes made by other plugins,
   119  	// built-in mutating admission plugins are re-run if a mutating webhook modifies
   120  	// an object, and mutating webhooks can specify a reinvocationPolicy to control
   121  	// whether they are reinvoked as well.
   122  	ReinvocationPolicy string `marker:"reinvocationPolicy,optional"`
   123  }
   124  
   125  // verbToAPIVariant converts a marker's verb to the proper value for the API.
   126  // Unrecognized verbs are passed through.
   127  func verbToAPIVariant(verbRaw string) admissionregv1.OperationType {
   128  	switch strings.ToLower(verbRaw) {
   129  	case strings.ToLower(string(admissionregv1.Create)):
   130  		return admissionregv1.Create
   131  	case strings.ToLower(string(admissionregv1.Update)):
   132  		return admissionregv1.Update
   133  	case strings.ToLower(string(admissionregv1.Delete)):
   134  		return admissionregv1.Delete
   135  	case strings.ToLower(string(admissionregv1.Connect)):
   136  		return admissionregv1.Connect
   137  	case strings.ToLower(string(admissionregv1.OperationAll)):
   138  		return admissionregv1.OperationAll
   139  	default:
   140  		return admissionregv1.OperationType(verbRaw)
   141  	}
   142  }
   143  
   144  // ToMutatingWebhook converts this rule to its Kubernetes API form.
   145  func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) {
   146  	if !c.Mutating {
   147  		return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name)
   148  	}
   149  
   150  	matchPolicy, err := c.matchPolicy()
   151  	if err != nil {
   152  		return admissionregv1.MutatingWebhook{}, err
   153  	}
   154  
   155  	return admissionregv1.MutatingWebhook{
   156  		Name:                    c.Name,
   157  		Rules:                   c.rules(),
   158  		FailurePolicy:           c.failurePolicy(),
   159  		MatchPolicy:             matchPolicy,
   160  		ClientConfig:            c.clientConfig(),
   161  		SideEffects:             c.sideEffects(),
   162  		AdmissionReviewVersions: c.AdmissionReviewVersions,
   163  		ReinvocationPolicy:      c.reinvocationPolicy(),
   164  	}, nil
   165  }
   166  
   167  // ToValidatingWebhook converts this rule to its Kubernetes API form.
   168  func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) {
   169  	if c.Mutating {
   170  		return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name)
   171  	}
   172  
   173  	matchPolicy, err := c.matchPolicy()
   174  	if err != nil {
   175  		return admissionregv1.ValidatingWebhook{}, err
   176  	}
   177  
   178  	return admissionregv1.ValidatingWebhook{
   179  		Name:                    c.Name,
   180  		Rules:                   c.rules(),
   181  		FailurePolicy:           c.failurePolicy(),
   182  		MatchPolicy:             matchPolicy,
   183  		ClientConfig:            c.clientConfig(),
   184  		SideEffects:             c.sideEffects(),
   185  		AdmissionReviewVersions: c.AdmissionReviewVersions,
   186  	}, nil
   187  }
   188  
   189  // rules returns the configuration of what operations on what
   190  // resources/subresources a webhook should care about.
   191  func (c Config) rules() []admissionregv1.RuleWithOperations {
   192  	whConfig := admissionregv1.RuleWithOperations{
   193  		Rule: admissionregv1.Rule{
   194  			APIGroups:   c.Groups,
   195  			APIVersions: c.Versions,
   196  			Resources:   c.Resources,
   197  		},
   198  		Operations: make([]admissionregv1.OperationType, len(c.Verbs)),
   199  	}
   200  
   201  	for i, verbRaw := range c.Verbs {
   202  		whConfig.Operations[i] = verbToAPIVariant(verbRaw)
   203  	}
   204  
   205  	// fix the group names, since letting people type "core" is nice
   206  	for i, group := range whConfig.APIGroups {
   207  		if group == "core" {
   208  			whConfig.APIGroups[i] = ""
   209  		}
   210  	}
   211  
   212  	return []admissionregv1.RuleWithOperations{whConfig}
   213  }
   214  
   215  // failurePolicy converts the string value to the proper value for the API.
   216  // Unrecognized values are passed through.
   217  func (c Config) failurePolicy() *admissionregv1.FailurePolicyType {
   218  	var failurePolicy admissionregv1.FailurePolicyType
   219  	switch strings.ToLower(c.FailurePolicy) {
   220  	case strings.ToLower(string(admissionregv1.Ignore)):
   221  		failurePolicy = admissionregv1.Ignore
   222  	case strings.ToLower(string(admissionregv1.Fail)):
   223  		failurePolicy = admissionregv1.Fail
   224  	default:
   225  		failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy)
   226  	}
   227  	return &failurePolicy
   228  }
   229  
   230  // matchPolicy converts the string value to the proper value for the API.
   231  func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) {
   232  	var matchPolicy admissionregv1.MatchPolicyType
   233  	switch strings.ToLower(c.MatchPolicy) {
   234  	case strings.ToLower(string(admissionregv1.Exact)):
   235  		matchPolicy = admissionregv1.Exact
   236  	case strings.ToLower(string(admissionregv1.Equivalent)):
   237  		matchPolicy = admissionregv1.Equivalent
   238  	case "":
   239  		return nil, nil
   240  	default:
   241  		return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy)
   242  	}
   243  	return &matchPolicy, nil
   244  }
   245  
   246  // clientConfig returns the client config for a webhook.
   247  func (c Config) clientConfig() admissionregv1.WebhookClientConfig {
   248  	path := c.Path
   249  	return admissionregv1.WebhookClientConfig{
   250  		Service: &admissionregv1.ServiceReference{
   251  			Name:      "webhook-service",
   252  			Namespace: "system",
   253  			Path:      &path,
   254  		},
   255  	}
   256  }
   257  
   258  // sideEffects returns the sideEffects config for a webhook.
   259  func (c Config) sideEffects() *admissionregv1.SideEffectClass {
   260  	var sideEffects admissionregv1.SideEffectClass
   261  	switch strings.ToLower(c.SideEffects) {
   262  	case strings.ToLower(string(admissionregv1.SideEffectClassNone)):
   263  		sideEffects = admissionregv1.SideEffectClassNone
   264  	case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)):
   265  		sideEffects = admissionregv1.SideEffectClassNoneOnDryRun
   266  	case strings.ToLower(string(admissionregv1.SideEffectClassSome)):
   267  		sideEffects = admissionregv1.SideEffectClassSome
   268  	case "":
   269  		return nil
   270  	default:
   271  		return nil
   272  	}
   273  	return &sideEffects
   274  }
   275  
   276  // reinvocationPolicy returns the reinvocationPolicy config for a mutating webhook.
   277  func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType {
   278  	var reinvocationPolicy admissionregv1.ReinvocationPolicyType
   279  	switch strings.ToLower(c.ReinvocationPolicy) {
   280  	case strings.ToLower(string(admissionregv1.NeverReinvocationPolicy)):
   281  		reinvocationPolicy = admissionregv1.NeverReinvocationPolicy
   282  	case strings.ToLower(string(admissionregv1.IfNeededReinvocationPolicy)):
   283  		reinvocationPolicy = admissionregv1.IfNeededReinvocationPolicy
   284  	default:
   285  		return nil
   286  	}
   287  	return &reinvocationPolicy
   288  }
   289  
   290  // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook.
   291  func (c Config) webhookVersions() ([]string, error) {
   292  	// If WebhookVersions is not specified, we default it to `v1`.
   293  	if len(c.WebhookVersions) == 0 {
   294  		return []string{defaultWebhookVersion}, nil
   295  	}
   296  	supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...)
   297  	for _, version := range c.WebhookVersions {
   298  		if !supportedWebhookVersions.Has(version) {
   299  			return nil, fmt.Errorf("unsupported webhook version: %s", version)
   300  		}
   301  	}
   302  	return sets.NewString(c.WebhookVersions...).UnsortedList(), nil
   303  }
   304  
   305  // +controllertools:marker:generateHelp
   306  
   307  // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects.
   308  type Generator struct {
   309  	// HeaderFile specifies the header text (e.g. license) to prepend to generated files.
   310  	HeaderFile string `marker:",optional"`
   311  
   312  	// Year specifies the year to substitute for " YEAR" in the header file.
   313  	Year string `marker:",optional"`
   314  }
   315  
   316  func (Generator) RegisterMarkers(into *markers.Registry) error {
   317  	if err := into.Register(ConfigDefinition); err != nil {
   318  		return err
   319  	}
   320  	into.AddHelp(ConfigDefinition, Config{}.Help())
   321  	return nil
   322  }
   323  
   324  func (g Generator) Generate(ctx *genall.GenerationContext) error {
   325  	supportedWebhookVersions := supportedWebhookVersions()
   326  	mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions))
   327  	validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions))
   328  	for _, root := range ctx.Roots {
   329  		markerSet, err := markers.PackageMarkers(ctx.Collector, root)
   330  		if err != nil {
   331  			root.AddError(err)
   332  		}
   333  
   334  		cfgs := markerSet[ConfigDefinition.Name]
   335  		sort.SliceStable(cfgs, func(i, j int) bool {
   336  			return cfgs[i].(Config).Name < cfgs[j].(Config).Name
   337  		})
   338  
   339  		for _, cfg := range cfgs {
   340  			cfg := cfg.(Config)
   341  			webhookVersions, err := cfg.webhookVersions()
   342  			if err != nil {
   343  				return err
   344  			}
   345  			if cfg.Mutating {
   346  				w, err := cfg.ToMutatingWebhook()
   347  				if err != nil {
   348  					return err
   349  				}
   350  				for _, webhookVersion := range webhookVersions {
   351  					mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w)
   352  				}
   353  			} else {
   354  				w, err := cfg.ToValidatingWebhook()
   355  				if err != nil {
   356  					return err
   357  				}
   358  				for _, webhookVersion := range webhookVersions {
   359  					validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w)
   360  				}
   361  			}
   362  		}
   363  	}
   364  
   365  	versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions))
   366  	for _, version := range supportedWebhookVersions {
   367  		if cfgs, ok := mutatingCfgs[version]; ok {
   368  			// The only possible version in supportedWebhookVersions is v1,
   369  			// so use it for all versioned types in this context.
   370  			objRaw := &admissionregv1.MutatingWebhookConfiguration{}
   371  			objRaw.SetGroupVersionKind(schema.GroupVersionKind{
   372  				Group:   admissionregv1.SchemeGroupVersion.Group,
   373  				Version: version,
   374  				Kind:    "MutatingWebhookConfiguration",
   375  			})
   376  			objRaw.SetName("mutating-webhook-configuration")
   377  			objRaw.Webhooks = cfgs
   378  			for i := range objRaw.Webhooks {
   379  				// SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
   380  				// return an error
   381  				if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
   382  					return err
   383  				}
   384  				// AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
   385  				// return an error
   386  				if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
   387  					return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
   388  				}
   389  			}
   390  			versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
   391  		}
   392  
   393  		if cfgs, ok := validatingCfgs[version]; ok {
   394  			// The only possible version in supportedWebhookVersions is v1,
   395  			// so use it for all versioned types in this context.
   396  			objRaw := &admissionregv1.ValidatingWebhookConfiguration{}
   397  			objRaw.SetGroupVersionKind(schema.GroupVersionKind{
   398  				Group:   admissionregv1.SchemeGroupVersion.Group,
   399  				Version: version,
   400  				Kind:    "ValidatingWebhookConfiguration",
   401  			})
   402  			objRaw.SetName("validating-webhook-configuration")
   403  			objRaw.Webhooks = cfgs
   404  			for i := range objRaw.Webhooks {
   405  				// SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
   406  				// return an error
   407  				if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
   408  					return err
   409  				}
   410  				// AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
   411  				// return an error
   412  				if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
   413  					return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
   414  				}
   415  			}
   416  			versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
   417  		}
   418  	}
   419  
   420  	var headerText string
   421  	if g.HeaderFile != "" {
   422  		headerBytes, err := ctx.ReadFile(g.HeaderFile)
   423  		if err != nil {
   424  			return err
   425  		}
   426  		headerText = string(headerBytes)
   427  	}
   428  	headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year)
   429  
   430  	for k, v := range versionedWebhooks {
   431  		var fileName string
   432  		if k == defaultWebhookVersion {
   433  			fileName = fmt.Sprintf("manifests.yaml")
   434  		} else {
   435  			fileName = fmt.Sprintf("manifests.%s.yaml", k)
   436  		}
   437  		if err := ctx.WriteYAML(fileName, headerText, v, genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil {
   438  			return err
   439  		}
   440  	}
   441  	return nil
   442  }
   443  
   444  func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error {
   445  	if sideEffects == nil {
   446  		return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration")
   447  	}
   448  	if *sideEffects == admissionregv1.SideEffectClassUnknown ||
   449  		*sideEffects == admissionregv1.SideEffectClassSome {
   450  		return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration")
   451  	}
   452  	return nil
   453  }