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