vendor: Update vendor logic

This commit is contained in:
Clayton Coleman 2020-04-08 14:34:43 -04:00
parent c6ac5cbc87
commit 4ca64b85f0
No known key found for this signature in database
GPG key ID: 3D16906B4F1C5CB3
1540 changed files with 265304 additions and 91616 deletions

View file

@ -24,6 +24,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authentication/user"
)
@ -42,12 +43,17 @@ type attributesRecord struct {
// other elements are always accessed in single goroutine.
// But ValidatingAdmissionWebhook add annotations concurrently.
annotations map[string]string
annotations map[string]annotation
annotationsLock sync.RWMutex
reinvocationContext ReinvocationContext
}
type annotation struct {
level auditinternal.Level
value string
}
func NewAttributesRecord(object runtime.Object, oldObject runtime.Object, kind schema.GroupVersionKind, namespace, name string, resource schema.GroupVersionResource, subresource string, operation Operation, operationOptions runtime.Object, dryRun bool, userInfo user.Info) Attributes {
return &attributesRecord{
kind: kind,
@ -111,7 +117,7 @@ func (record *attributesRecord) GetUserInfo() user.Info {
// getAnnotations implements privateAnnotationsGetter.It's a private method used
// by WithAudit decorator.
func (record *attributesRecord) getAnnotations() map[string]string {
func (record *attributesRecord) getAnnotations(maxLevel auditinternal.Level) map[string]string {
record.annotationsLock.RLock()
defer record.annotationsLock.RUnlock()
@ -120,26 +126,36 @@ func (record *attributesRecord) getAnnotations() map[string]string {
}
cp := make(map[string]string, len(record.annotations))
for key, value := range record.annotations {
cp[key] = value
if value.level.Less(maxLevel) || value.level == maxLevel {
cp[key] = value.value
}
}
return cp
}
// AddAnnotation adds an annotation to attributesRecord with Metadata audit level
func (record *attributesRecord) AddAnnotation(key, value string) error {
return record.AddAnnotationWithLevel(key, value, auditinternal.LevelMetadata)
}
func (record *attributesRecord) AddAnnotationWithLevel(key, value string, level auditinternal.Level) error {
if err := checkKeyFormat(key); err != nil {
return err
}
if level.Less(auditinternal.LevelMetadata) {
return fmt.Errorf("admission annotations are not allowed to be set at audit level lower than Metadata, key: %q, level: %s", key, level)
}
record.annotationsLock.Lock()
defer record.annotationsLock.Unlock()
if record.annotations == nil {
record.annotations = make(map[string]string)
record.annotations = make(map[string]annotation)
}
if v, ok := record.annotations[key]; ok && v != value {
return fmt.Errorf("admission annotations are not allowd to be overwritten, key:%q, old value: %q, new value:%q", key, record.annotations[key], value)
annotation := annotation{level: level, value: value}
if v, ok := record.annotations[key]; ok && v != annotation {
return fmt.Errorf("admission annotations are not allowd to be overwritten, key:%q, old value: %v, new value: %v", key, record.annotations[key], annotation)
}
record.annotations[key] = value
record.annotations[key] = annotation
return nil
}

View file

@ -17,6 +17,7 @@ limitations under the License.
package admission
import (
"context"
"fmt"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
@ -44,7 +45,7 @@ func WithAudit(i Interface, ae *auditinternal.Event) Interface {
return &auditHandler{i, ae}
}
func (handler auditHandler) Admit(a Attributes, o ObjectInterfaces) error {
func (handler auditHandler) Admit(ctx context.Context, a Attributes, o ObjectInterfaces) error {
if !handler.Interface.Handles(a.GetOperation()) {
return nil
}
@ -53,13 +54,13 @@ func (handler auditHandler) Admit(a Attributes, o ObjectInterfaces) error {
}
var err error
if mutator, ok := handler.Interface.(MutationInterface); ok {
err = mutator.Admit(a, o)
err = mutator.Admit(ctx, a, o)
handler.logAnnotations(a)
}
return err
}
func (handler auditHandler) Validate(a Attributes, o ObjectInterfaces) error {
func (handler auditHandler) Validate(ctx context.Context, a Attributes, o ObjectInterfaces) error {
if !handler.Interface.Handles(a.GetOperation()) {
return nil
}
@ -68,7 +69,7 @@ func (handler auditHandler) Validate(a Attributes, o ObjectInterfaces) error {
}
var err error
if validator, ok := handler.Interface.(ValidationInterface); ok {
err = validator.Validate(a, o)
err = validator.Validate(ctx, a, o)
handler.logAnnotations(a)
}
return err
@ -84,11 +85,18 @@ func ensureAnnotationGetter(a Attributes) error {
}
func (handler auditHandler) logAnnotations(a Attributes) {
if handler.ae == nil {
return
}
switch a := a.(type) {
case privateAnnotationsGetter:
audit.LogAnnotations(handler.ae, a.getAnnotations())
for key, value := range a.getAnnotations(handler.ae.Level) {
audit.LogAnnotation(handler.ae, key, value)
}
case AnnotationsGetter:
audit.LogAnnotations(handler.ae, a.GetAnnotations())
for key, value := range a.GetAnnotations(handler.ae.Level) {
audit.LogAnnotation(handler.ae, key, value)
}
default:
// this will never happen, because we have already checked it in ensureAnnotationGetter
}

View file

@ -16,6 +16,8 @@ limitations under the License.
package admission
import "context"
// chainAdmissionHandler is an instance of admission.NamedHandler that performs admission control using
// a chain of admission handlers
type chainAdmissionHandler []Interface
@ -26,13 +28,13 @@ func NewChainHandler(handlers ...Interface) chainAdmissionHandler {
}
// Admit performs an admission control check using a chain of handlers, and returns immediately on first error
func (admissionHandler chainAdmissionHandler) Admit(a Attributes, o ObjectInterfaces) error {
func (admissionHandler chainAdmissionHandler) Admit(ctx context.Context, a Attributes, o ObjectInterfaces) error {
for _, handler := range admissionHandler {
if !handler.Handles(a.GetOperation()) {
continue
}
if mutator, ok := handler.(MutationInterface); ok {
err := mutator.Admit(a, o)
err := mutator.Admit(ctx, a, o)
if err != nil {
return err
}
@ -42,13 +44,13 @@ func (admissionHandler chainAdmissionHandler) Admit(a Attributes, o ObjectInterf
}
// Validate performs an admission control check using a chain of handlers, and returns immediately on first error
func (admissionHandler chainAdmissionHandler) Validate(a Attributes, o ObjectInterfaces) error {
func (admissionHandler chainAdmissionHandler) Validate(ctx context.Context, a Attributes, o ObjectInterfaces) error {
for _, handler := range admissionHandler {
if !handler.Handles(a.GetOperation()) {
continue
}
if validator, ok := handler.(ValidationInterface); ok {
err := validator.Validate(a, o)
err := validator.Validate(ctx, a, o)
if err != nil {
return err
}

View file

@ -32,7 +32,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/apis/apiserver"
apiserverv1alpha1 "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
apiserverv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
)
func makeAbs(path, base string) (string, error) {
@ -110,11 +110,11 @@ func ReadAdmissionConfiguration(pluginNames []string, configFilePath string, con
// previously read input from a non-versioned file configuration to the
// current input file.
legacyPluginsWithUnversionedConfig := sets.NewString("ImagePolicyWebhook", "PodNodeSelector")
externalConfig := &apiserverv1alpha1.AdmissionConfiguration{}
externalConfig := &apiserverv1.AdmissionConfiguration{}
for _, pluginName := range pluginNames {
if legacyPluginsWithUnversionedConfig.Has(pluginName) {
externalConfig.Plugins = append(externalConfig.Plugins,
apiserverv1alpha1.AdmissionPluginConfiguration{
apiserverv1.AdmissionPluginConfiguration{
Name: pluginName,
Path: configFilePath})
}

View file

@ -21,13 +21,13 @@ import (
"sort"
"sync/atomic"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1"
"k8s.io/client-go/tools/cache"
)
@ -41,7 +41,7 @@ type mutatingWebhookConfigurationManager struct {
var _ generic.Source = &mutatingWebhookConfigurationManager{}
func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
informer := f.Admissionregistration().V1beta1().MutatingWebhookConfigurations()
informer := f.Admissionregistration().V1().MutatingWebhookConfigurations()
manager := &mutatingWebhookConfigurationManager{
configuration: &atomic.Value{},
lister: informer.Lister(),
@ -79,7 +79,7 @@ func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
m.configuration.Store(mergeMutatingWebhookConfigurations(configurations))
}
func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
func mergeMutatingWebhookConfigurations(configurations []*v1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
// The internal order of webhooks for each configuration is provided by the user
// but configurations themselves can be in any order. As we are going to run these
// webhooks in serial, they are sorted here to have a deterministic order.
@ -93,13 +93,13 @@ func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhoo
n := c.Webhooks[i].Name
uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
names[n]++
accessors = append(accessors, webhook.NewMutatingWebhookAccessor(uid, &c.Webhooks[i]))
accessors = append(accessors, webhook.NewMutatingWebhookAccessor(uid, c.Name, &c.Webhooks[i]))
}
}
return accessors
}
type MutatingWebhookConfigurationSorter []*v1beta1.MutatingWebhookConfiguration
type MutatingWebhookConfigurationSorter []*v1.MutatingWebhookConfiguration
func (a MutatingWebhookConfigurationSorter) ByName(i, j int) bool {
return a[i].Name < a[j].Name

View file

@ -21,13 +21,13 @@ import (
"sort"
"sync/atomic"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1"
"k8s.io/client-go/tools/cache"
)
@ -41,7 +41,7 @@ type validatingWebhookConfigurationManager struct {
var _ generic.Source = &validatingWebhookConfigurationManager{}
func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
informer := f.Admissionregistration().V1beta1().ValidatingWebhookConfigurations()
informer := f.Admissionregistration().V1().ValidatingWebhookConfigurations()
manager := &validatingWebhookConfigurationManager{
configuration: &atomic.Value{},
lister: informer.Lister(),
@ -80,7 +80,7 @@ func (v *validatingWebhookConfigurationManager) updateConfiguration() {
v.configuration.Store(mergeValidatingWebhookConfigurations(configurations))
}
func mergeValidatingWebhookConfigurations(configurations []*v1beta1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
func mergeValidatingWebhookConfigurations(configurations []*v1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
accessors := []webhook.WebhookAccessor{}
for _, c := range configurations {
@ -91,13 +91,13 @@ func mergeValidatingWebhookConfigurations(configurations []*v1beta1.ValidatingWe
n := c.Webhooks[i].Name
uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
names[n]++
accessors = append(accessors, webhook.NewValidatingWebhookAccessor(uid, &c.Webhooks[i]))
accessors = append(accessors, webhook.NewValidatingWebhookAccessor(uid, c.Name, &c.Webhooks[i]))
}
}
return accessors
}
type ValidatingWebhookConfigurationSorter []*v1beta1.ValidatingWebhookConfiguration
type ValidatingWebhookConfigurationSorter []*v1.ValidatingWebhookConfiguration
func (a ValidatingWebhookConfigurationSorter) ByName(i, j int) bool {
return a[i].Name < a[j].Name

View file

@ -21,25 +21,30 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
)
type pluginInitializer struct {
externalClient kubernetes.Interface
externalInformers informers.SharedInformerFactory
authorizer authorizer.Authorizer
featureGates featuregate.FeatureGate
}
// New creates an instance of admission plugins initializer.
// TODO(p0lyn0mial): make the parameters public, this construction seems to be redundant.
// This constructor is public with a long param list so that callers immediately know that new information can be expected
// during compilation when they update a level.
func New(
extClientset kubernetes.Interface,
extInformers informers.SharedInformerFactory,
authz authorizer.Authorizer,
featureGates featuregate.FeatureGate,
) pluginInitializer {
return pluginInitializer{
externalClient: extClientset,
externalInformers: extInformers,
authorizer: authz,
featureGates: featureGates,
}
}
@ -57,6 +62,10 @@ func (i pluginInitializer) Initialize(plugin admission.Interface) {
if wants, ok := plugin.(WantsAuthorizer); ok {
wants.SetAuthorizer(i.authorizer)
}
if wants, ok := plugin.(WantsFeatures); ok {
wants.InspectFeatureGates(i.featureGates)
}
}
var _ admission.PluginInitializer = pluginInitializer{}

View file

@ -21,6 +21,7 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
)
// WantsExternalKubeClientSet defines a function which sets external ClientSet for admission plugins that need it
@ -40,3 +41,14 @@ type WantsAuthorizer interface {
SetAuthorizer(authorizer.Authorizer)
admission.InitializationValidator
}
// WantsFeatureGate defines a function which passes the featureGates for inspection by an admission plugin.
// Admission plugins should not hold a reference to the featureGates. Instead, they should query a particular one
// and assign it to a simple bool in the admission plugin struct.
// func (a *admissionPlugin) InspectFeatureGates(features featuregate.FeatureGate){
// a.myFeatureIsOn = features.Enabled("my-feature")
// }
type WantsFeatures interface {
InspectFeatureGates(featuregate.FeatureGate)
admission.InitializationValidator
}

View file

@ -17,10 +17,12 @@ limitations under the License.
package admission
import (
"context"
"io"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authentication/user"
)
@ -61,8 +63,15 @@ type Attributes interface {
// "podsecuritypolicy" is the name of the plugin, "admission.k8s.io" is the name of the organization, "admit-policy" is the key name.
// An error is returned if the format of key is invalid. When trying to overwrite annotation with a new value, an error is returned.
// Both ValidationInterface and MutationInterface are allowed to add Annotations.
// By default, an annotation gets logged into audit event if the request's audit level is greater or
// equal to Metadata.
AddAnnotation(key, value string) error
// AddAnnotationWithLevel sets annotation according to key-value pair with additional intended audit level.
// An Annotation gets logged into audit event if the request's audit level is greater or equal to the
// intended audit level.
AddAnnotationWithLevel(key, value string, level auditinternal.Level) error
// GetReinvocationContext tracks the admission request information relevant to the re-invocation policy.
GetReinvocationContext() ReinvocationContext
}
@ -85,13 +94,13 @@ type ObjectInterfaces interface {
// privateAnnotationsGetter is a private interface which allows users to get annotations from Attributes.
type privateAnnotationsGetter interface {
getAnnotations() map[string]string
getAnnotations(maxLevel auditinternal.Level) map[string]string
}
// AnnotationsGetter allows users to get annotations from Attributes. An alternate Attribute should implement
// this interface.
type AnnotationsGetter interface {
GetAnnotations() map[string]string
GetAnnotations(maxLevel auditinternal.Level) map[string]string
}
// ReinvocationContext provides access to the admission related state required to implement the re-invocation policy.
@ -120,8 +129,9 @@ type Interface interface {
type MutationInterface interface {
Interface
// Admit makes an admission decision based on the request attributes
Admit(a Attributes, o ObjectInterfaces) (err error)
// Admit makes an admission decision based on the request attributes.
// Context is used only for timeout/deadline/cancellation and tracing information.
Admit(ctx context.Context, a Attributes, o ObjectInterfaces) (err error)
}
// ValidationInterface is an abstract, pluggable interface for Admission Control decisions.
@ -129,7 +139,8 @@ type ValidationInterface interface {
Interface
// Validate makes an admission decision based on the request attributes. It is NOT allowed to mutate
Validate(a Attributes, o ObjectInterfaces) (err error)
// Context is used only for timeout/deadline/cancellation and tracing information.
Validate(ctx context.Context, a Attributes, o ObjectInterfaces) (err error)
}
// Operation is the type of resource operation being checked for admission control

View file

@ -17,18 +17,31 @@ limitations under the License.
package metrics
import (
"context"
"fmt"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apiserver/pkg/admission"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
// WebhookRejectionErrorType defines different error types that happen in a webhook rejection.
type WebhookRejectionErrorType string
const (
namespace = "apiserver"
subsystem = "admission"
// WebhookRejectionCallingWebhookError identifies a calling webhook error which causes
// a webhook admission to reject a request
WebhookRejectionCallingWebhookError WebhookRejectionErrorType = "calling_webhook_error"
// WebhookRejectionAPIServerInternalError identifies an apiserver internal error which
// causes a webhook admission to reject a request
WebhookRejectionAPIServerInternalError WebhookRejectionErrorType = "apiserver_internal_error"
// WebhookRejectionNoError identifies a webhook properly rejected a request
WebhookRejectionNoError WebhookRejectionErrorType = "no_error"
)
var (
@ -75,36 +88,37 @@ type pluginHandlerWithMetrics struct {
}
// Admit performs a mutating admission control check and emit metrics.
func (p pluginHandlerWithMetrics) Admit(a admission.Attributes, o admission.ObjectInterfaces) error {
func (p pluginHandlerWithMetrics) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
mutatingHandler, ok := p.Interface.(admission.MutationInterface)
if !ok {
return nil
}
start := time.Now()
err := mutatingHandler.Admit(a, o)
err := mutatingHandler.Admit(ctx, a, o)
p.observer(time.Since(start), err != nil, a, stepAdmit, p.extraLabels...)
return err
}
// Validate performs a non-mutating admission control check and emits metrics.
func (p pluginHandlerWithMetrics) Validate(a admission.Attributes, o admission.ObjectInterfaces) error {
func (p pluginHandlerWithMetrics) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
validatingHandler, ok := p.Interface.(admission.ValidationInterface)
if !ok {
return nil
}
start := time.Now()
err := validatingHandler.Validate(a, o)
err := validatingHandler.Validate(ctx, a, o)
p.observer(time.Since(start), err != nil, a, stepValidate, p.extraLabels...)
return err
}
// AdmissionMetrics instruments admission with prometheus metrics.
type AdmissionMetrics struct {
step *metricSet
controller *metricSet
webhook *metricSet
step *metricSet
controller *metricSet
webhook *metricSet
webhookRejection *metrics.CounterVec
}
// newAdmissionMetrics create a new AdmissionMetrics, configured with default metric names.
@ -125,10 +139,21 @@ func newAdmissionMetrics() *AdmissionMetrics {
[]string{"name", "type", "operation", "rejected"},
"Admission webhook %s, identified by name and broken out for each operation and API resource and type (validate or admit).", false)
webhookRejection := metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "webhook_rejection_count",
Help: "Admission webhook rejection count, identified by name and broken out for each admission type (validating or admit) and operation. Additional labels specify an error type (calling_webhook_error or apiserver_internal_error if an error occurred; no_error otherwise) and optionally a non-zero rejection code if the webhook rejects the request with an HTTP status code (honored by the apiserver when the code is greater or equal to 400). Codes greater than 600 are truncated to 600, to keep the metrics cardinality bounded.",
StabilityLevel: metrics.ALPHA,
},
[]string{"name", "type", "operation", "error_type", "rejection_code"})
step.mustRegister()
controller.mustRegister()
webhook.mustRegister()
return &AdmissionMetrics{step: step, controller: controller, webhook: webhook}
legacyregistry.MustRegister(webhookRejection)
return &AdmissionMetrics{step: step, controller: controller, webhook: webhook, webhookRejection: webhookRejection}
}
func (m *AdmissionMetrics) reset() {
@ -152,34 +177,46 @@ func (m *AdmissionMetrics) ObserveWebhook(elapsed time.Duration, rejected bool,
m.webhook.observe(elapsed, append(extraLabels, stepType, string(attr.GetOperation()), strconv.FormatBool(rejected))...)
}
// ObserveWebhookRejection records admission related metrics for an admission webhook rejection.
func (m *AdmissionMetrics) ObserveWebhookRejection(name, stepType, operation string, errorType WebhookRejectionErrorType, rejectionCode int) {
// We truncate codes greater than 600 to keep the cardinality bounded.
// This should be rarely done by a malfunctioning webhook server.
if rejectionCode > 600 {
rejectionCode = 600
}
m.webhookRejection.WithLabelValues(name, stepType, operation, string(errorType), strconv.Itoa(rejectionCode)).Inc()
}
type metricSet struct {
latencies *prometheus.HistogramVec
latenciesSummary *prometheus.SummaryVec
latencies *metrics.HistogramVec
latenciesSummary *metrics.SummaryVec
}
func newMetricSet(name string, labels []string, helpTemplate string, hasSummary bool) *metricSet {
var summary *prometheus.SummaryVec
var summary *metrics.SummaryVec
if hasSummary {
summary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: fmt.Sprintf("%s_admission_duration_seconds_summary", name),
Help: fmt.Sprintf(helpTemplate, "latency summary in seconds"),
MaxAge: latencySummaryMaxAge,
summary = metrics.NewSummaryVec(
&metrics.SummaryOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: fmt.Sprintf("%s_admission_duration_seconds_summary", name),
Help: fmt.Sprintf(helpTemplate, "latency summary in seconds"),
MaxAge: latencySummaryMaxAge,
StabilityLevel: metrics.ALPHA,
},
labels,
)
}
return &metricSet{
latencies: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: fmt.Sprintf("%s_admission_duration_seconds", name),
Help: fmt.Sprintf(helpTemplate, "latency histogram in seconds"),
Buckets: latencyBuckets,
latencies: metrics.NewHistogramVec(
&metrics.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: fmt.Sprintf("%s_admission_duration_seconds", name),
Help: fmt.Sprintf(helpTemplate, "latency histogram in seconds"),
Buckets: latencyBuckets,
StabilityLevel: metrics.ALPHA,
},
labels,
),
@ -190,9 +227,9 @@ func newMetricSet(name string, labels []string, helpTemplate string, hasSummary
// MustRegister registers all the prometheus metrics in the metricSet.
func (m *metricSet) mustRegister() {
prometheus.MustRegister(m.latencies)
legacyregistry.MustRegister(m.latencies)
if m.latenciesSummary != nil {
prometheus.MustRegister(m.latenciesSummary)
legacyregistry.MustRegister(m.latenciesSummary)
}
}

View file

@ -17,13 +17,14 @@ limitations under the License.
package lifecycle
import (
"context"
"fmt"
"io"
"time"
"k8s.io/klog"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -73,7 +74,7 @@ var _ = initializer.WantsExternalKubeInformerFactory(&Lifecycle{})
var _ = initializer.WantsExternalKubeClientSet(&Lifecycle{})
// Admit makes an admission decision based on the request attributes
func (l *Lifecycle) Admit(a admission.Attributes, o admission.ObjectInterfaces) error {
func (l *Lifecycle) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
// prevent deletion of immortal namespaces
if a.GetOperation() == admission.Delete && a.GetKind().GroupKind() == v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() && l.immortalNamespaces.Has(a.GetName()) {
return errors.NewForbidden(a.GetResource().GroupResource(), a.GetName(), fmt.Errorf("this namespace may not be deleted"))
@ -169,8 +170,15 @@ func (l *Lifecycle) Admit(a admission.Attributes, o admission.ObjectInterfaces)
return nil
}
// TODO: This should probably not be a 403
return admission.NewForbidden(a, fmt.Errorf("unable to create new content in namespace %s because it is being terminated", a.GetNamespace()))
err := admission.NewForbidden(a, fmt.Errorf("unable to create new content in namespace %s because it is being terminated", a.GetNamespace()))
if apierr, ok := err.(*errors.StatusError); ok {
apierr.ErrStatus.Details.Causes = append(apierr.ErrStatus.Details.Causes, metav1.StatusCause{
Type: v1.NamespaceTerminatingCause,
Message: fmt.Sprintf("namespace %s is being terminated", a.GetNamespace()),
Field: "metadata.namespace",
})
}
return err
}
return nil

View file

@ -17,8 +17,13 @@ limitations under the License.
package webhook
import (
"k8s.io/api/admissionregistration/v1beta1"
"sync"
"k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/rest"
)
// WebhookAccessor provides a common interface to both mutating and validating webhook types.
@ -26,135 +31,267 @@ type WebhookAccessor interface {
// GetUID gets a string that uniquely identifies the webhook.
GetUID() string
// GetConfigurationName gets the name of the webhook configuration that owns this webhook.
GetConfigurationName() string
// GetRESTClient gets the webhook client
GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error)
// GetParsedNamespaceSelector gets the webhook NamespaceSelector field.
GetParsedNamespaceSelector() (labels.Selector, error)
// GetParsedObjectSelector gets the webhook ObjectSelector field.
GetParsedObjectSelector() (labels.Selector, error)
// GetName gets the webhook Name field. Note that the name is scoped to the webhook
// configuration and does not provide a globally unique identity, if a unique identity is
// needed, use GetUID.
GetName() string
// GetClientConfig gets the webhook ClientConfig field.
GetClientConfig() v1beta1.WebhookClientConfig
GetClientConfig() v1.WebhookClientConfig
// GetRules gets the webhook Rules field.
GetRules() []v1beta1.RuleWithOperations
GetRules() []v1.RuleWithOperations
// GetFailurePolicy gets the webhook FailurePolicy field.
GetFailurePolicy() *v1beta1.FailurePolicyType
GetFailurePolicy() *v1.FailurePolicyType
// GetMatchPolicy gets the webhook MatchPolicy field.
GetMatchPolicy() *v1beta1.MatchPolicyType
GetMatchPolicy() *v1.MatchPolicyType
// GetNamespaceSelector gets the webhook NamespaceSelector field.
GetNamespaceSelector() *metav1.LabelSelector
// GetObjectSelector gets the webhook ObjectSelector field.
GetObjectSelector() *metav1.LabelSelector
// GetSideEffects gets the webhook SideEffects field.
GetSideEffects() *v1beta1.SideEffectClass
GetSideEffects() *v1.SideEffectClass
// GetTimeoutSeconds gets the webhook TimeoutSeconds field.
GetTimeoutSeconds() *int32
// GetAdmissionReviewVersions gets the webhook AdmissionReviewVersions field.
GetAdmissionReviewVersions() []string
// GetMutatingWebhook if the accessor contains a MutatingWebhook, returns it and true, else returns false.
GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool)
GetMutatingWebhook() (*v1.MutatingWebhook, bool)
// GetValidatingWebhook if the accessor contains a ValidatingWebhook, returns it and true, else returns false.
GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool)
GetValidatingWebhook() (*v1.ValidatingWebhook, bool)
}
// NewMutatingWebhookAccessor creates an accessor for a MutatingWebhook.
func NewMutatingWebhookAccessor(uid string, h *v1beta1.MutatingWebhook) WebhookAccessor {
return mutatingWebhookAccessor{uid: uid, MutatingWebhook: h}
func NewMutatingWebhookAccessor(uid, configurationName string, h *v1.MutatingWebhook) WebhookAccessor {
return &mutatingWebhookAccessor{uid: uid, configurationName: configurationName, MutatingWebhook: h}
}
type mutatingWebhookAccessor struct {
*v1beta1.MutatingWebhook
uid string
*v1.MutatingWebhook
uid string
configurationName string
initObjectSelector sync.Once
objectSelector labels.Selector
objectSelectorErr error
initNamespaceSelector sync.Once
namespaceSelector labels.Selector
namespaceSelectorErr error
initClient sync.Once
client *rest.RESTClient
clientErr error
}
func (m mutatingWebhookAccessor) GetUID() string {
func (m *mutatingWebhookAccessor) GetUID() string {
return m.uid
}
func (m mutatingWebhookAccessor) GetName() string {
func (m *mutatingWebhookAccessor) GetConfigurationName() string {
return m.configurationName
}
func (m *mutatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error) {
m.initClient.Do(func() {
m.client, m.clientErr = clientManager.HookClient(hookClientConfigForWebhook(m))
})
return m.client, m.clientErr
}
func (m *mutatingWebhookAccessor) GetParsedNamespaceSelector() (labels.Selector, error) {
m.initNamespaceSelector.Do(func() {
m.namespaceSelector, m.namespaceSelectorErr = metav1.LabelSelectorAsSelector(m.NamespaceSelector)
})
return m.namespaceSelector, m.namespaceSelectorErr
}
func (m *mutatingWebhookAccessor) GetParsedObjectSelector() (labels.Selector, error) {
m.initObjectSelector.Do(func() {
m.objectSelector, m.objectSelectorErr = metav1.LabelSelectorAsSelector(m.ObjectSelector)
})
return m.objectSelector, m.objectSelectorErr
}
func (m *mutatingWebhookAccessor) GetName() string {
return m.Name
}
func (m mutatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig {
func (m *mutatingWebhookAccessor) GetClientConfig() v1.WebhookClientConfig {
return m.ClientConfig
}
func (m mutatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations {
func (m *mutatingWebhookAccessor) GetRules() []v1.RuleWithOperations {
return m.Rules
}
func (m mutatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType {
func (m *mutatingWebhookAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return m.FailurePolicy
}
func (m mutatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
func (m *mutatingWebhookAccessor) GetMatchPolicy() *v1.MatchPolicyType {
return m.MatchPolicy
}
func (m mutatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
func (m *mutatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
return m.NamespaceSelector
}
func (m mutatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
func (m *mutatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
return m.ObjectSelector
}
func (m mutatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
func (m *mutatingWebhookAccessor) GetSideEffects() *v1.SideEffectClass {
return m.SideEffects
}
func (m mutatingWebhookAccessor) GetTimeoutSeconds() *int32 {
func (m *mutatingWebhookAccessor) GetTimeoutSeconds() *int32 {
return m.TimeoutSeconds
}
func (m mutatingWebhookAccessor) GetAdmissionReviewVersions() []string {
func (m *mutatingWebhookAccessor) GetAdmissionReviewVersions() []string {
return m.AdmissionReviewVersions
}
func (m mutatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) {
func (m *mutatingWebhookAccessor) GetMutatingWebhook() (*v1.MutatingWebhook, bool) {
return m.MutatingWebhook, true
}
func (m mutatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) {
func (m *mutatingWebhookAccessor) GetValidatingWebhook() (*v1.ValidatingWebhook, bool) {
return nil, false
}
// NewValidatingWebhookAccessor creates an accessor for a ValidatingWebhook.
func NewValidatingWebhookAccessor(uid string, h *v1beta1.ValidatingWebhook) WebhookAccessor {
return validatingWebhookAccessor{uid: uid, ValidatingWebhook: h}
func NewValidatingWebhookAccessor(uid, configurationName string, h *v1.ValidatingWebhook) WebhookAccessor {
return &validatingWebhookAccessor{uid: uid, configurationName: configurationName, ValidatingWebhook: h}
}
type validatingWebhookAccessor struct {
*v1beta1.ValidatingWebhook
uid string
*v1.ValidatingWebhook
uid string
configurationName string
initObjectSelector sync.Once
objectSelector labels.Selector
objectSelectorErr error
initNamespaceSelector sync.Once
namespaceSelector labels.Selector
namespaceSelectorErr error
initClient sync.Once
client *rest.RESTClient
clientErr error
}
func (v validatingWebhookAccessor) GetUID() string {
func (v *validatingWebhookAccessor) GetUID() string {
return v.uid
}
func (v validatingWebhookAccessor) GetName() string {
func (v *validatingWebhookAccessor) GetConfigurationName() string {
return v.configurationName
}
func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error) {
v.initClient.Do(func() {
v.client, v.clientErr = clientManager.HookClient(hookClientConfigForWebhook(v))
})
return v.client, v.clientErr
}
func (v *validatingWebhookAccessor) GetParsedNamespaceSelector() (labels.Selector, error) {
v.initNamespaceSelector.Do(func() {
v.namespaceSelector, v.namespaceSelectorErr = metav1.LabelSelectorAsSelector(v.NamespaceSelector)
})
return v.namespaceSelector, v.namespaceSelectorErr
}
func (v *validatingWebhookAccessor) GetParsedObjectSelector() (labels.Selector, error) {
v.initObjectSelector.Do(func() {
v.objectSelector, v.objectSelectorErr = metav1.LabelSelectorAsSelector(v.ObjectSelector)
})
return v.objectSelector, v.objectSelectorErr
}
func (v *validatingWebhookAccessor) GetName() string {
return v.Name
}
func (v validatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig {
func (v *validatingWebhookAccessor) GetClientConfig() v1.WebhookClientConfig {
return v.ClientConfig
}
func (v validatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations {
func (v *validatingWebhookAccessor) GetRules() []v1.RuleWithOperations {
return v.Rules
}
func (v validatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType {
func (v *validatingWebhookAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return v.FailurePolicy
}
func (v validatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
func (v *validatingWebhookAccessor) GetMatchPolicy() *v1.MatchPolicyType {
return v.MatchPolicy
}
func (v validatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
func (v *validatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
return v.NamespaceSelector
}
func (v validatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
func (v *validatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
return v.ObjectSelector
}
func (v validatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
func (v *validatingWebhookAccessor) GetSideEffects() *v1.SideEffectClass {
return v.SideEffects
}
func (v validatingWebhookAccessor) GetTimeoutSeconds() *int32 {
func (v *validatingWebhookAccessor) GetTimeoutSeconds() *int32 {
return v.TimeoutSeconds
}
func (v validatingWebhookAccessor) GetAdmissionReviewVersions() []string {
func (v *validatingWebhookAccessor) GetAdmissionReviewVersions() []string {
return v.AdmissionReviewVersions
}
func (v validatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) {
func (v *validatingWebhookAccessor) GetMutatingWebhook() (*v1.MutatingWebhook, bool) {
return nil, false
}
func (v validatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) {
func (v *validatingWebhookAccessor) GetValidatingWebhook() (*v1.ValidatingWebhook, bool) {
return v.ValidatingWebhook, true
}
// hookClientConfigForWebhook construct a webhookutil.ClientConfig using a WebhookAccessor to access
// v1beta1.MutatingWebhook and v1beta1.ValidatingWebhook API objects. webhookutil.ClientConfig is used
// to create a HookClient and the purpose of the config struct is to share that with other packages
// that need to create a HookClient.
func hookClientConfigForWebhook(w WebhookAccessor) webhookutil.ClientConfig {
ret := webhookutil.ClientConfig{Name: w.GetName(), CABundle: w.GetClientConfig().CABundle}
if w.GetClientConfig().URL != nil {
ret.URL = *w.GetClientConfig().URL
}
if w.GetClientConfig().Service != nil {
ret.Service = &webhookutil.ClientConfigService{
Name: w.GetClientConfig().Service.Name,
Namespace: w.GetClientConfig().Service.Namespace,
}
if w.GetClientConfig().Service.Port != nil {
ret.Service.Port = *w.GetClientConfig().Service.Port
} else {
ret.Service.Port = 443
}
if w.GetClientConfig().Service.Path != nil {
ret.Service.Path = *w.GetClientConfig().Service.Path
}
}
return ret
}

View file

@ -43,9 +43,11 @@ func Resource(resource string) schema.GroupResource {
}
func addKnownTypes(scheme *runtime.Scheme) error {
// TODO this will get cleaned up with the scheme types are fixed
scheme.AddKnownTypes(SchemeGroupVersion,
&WebhookAdmission{},
)
scheme.AddKnownTypeWithName(SchemeGroupVersion.WithKind("WebhookAdmissionConfiguration"),
&WebhookAdmission{},
)
return nil
}

View file

@ -0,0 +1,23 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:deepcopy-gen=package
// +k8s:conversion-gen=k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission
// +k8s:defaulter-gen=TypeMeta
// +groupName=apiserver.config.k8s.io
// Package v1 is the v1 version of the API.
package v1

View file

@ -0,0 +1,50 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name use in this package
const GroupName = "apiserver.config.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}
var (
// TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api.
// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(addKnownTypes)
}
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypeWithName(SchemeGroupVersion.WithKind("WebhookAdmissionConfiguration"),
&WebhookAdmission{},
)
return nil
}

View file

@ -0,0 +1,29 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// WebhookAdmission provides configuration for the webhook admission controller.
type WebhookAdmission struct {
metav1.TypeMeta `json:",inline"`
// KubeConfigFile is the path to the kubeconfig file.
KubeConfigFile string `json:"kubeConfigFile"`
}

View file

@ -0,0 +1,67 @@
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by conversion-gen. DO NOT EDIT.
package v1
import (
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
webhookadmission "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission"
)
func init() {
localSchemeBuilder.Register(RegisterConversions)
}
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(s *runtime.Scheme) error {
if err := s.AddGeneratedConversionFunc((*WebhookAdmission)(nil), (*webhookadmission.WebhookAdmission)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_WebhookAdmission_To_webhookadmission_WebhookAdmission(a.(*WebhookAdmission), b.(*webhookadmission.WebhookAdmission), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*webhookadmission.WebhookAdmission)(nil), (*WebhookAdmission)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_webhookadmission_WebhookAdmission_To_v1_WebhookAdmission(a.(*webhookadmission.WebhookAdmission), b.(*WebhookAdmission), scope)
}); err != nil {
return err
}
return nil
}
func autoConvert_v1_WebhookAdmission_To_webhookadmission_WebhookAdmission(in *WebhookAdmission, out *webhookadmission.WebhookAdmission, s conversion.Scope) error {
out.KubeConfigFile = in.KubeConfigFile
return nil
}
// Convert_v1_WebhookAdmission_To_webhookadmission_WebhookAdmission is an autogenerated conversion function.
func Convert_v1_WebhookAdmission_To_webhookadmission_WebhookAdmission(in *WebhookAdmission, out *webhookadmission.WebhookAdmission, s conversion.Scope) error {
return autoConvert_v1_WebhookAdmission_To_webhookadmission_WebhookAdmission(in, out, s)
}
func autoConvert_webhookadmission_WebhookAdmission_To_v1_WebhookAdmission(in *webhookadmission.WebhookAdmission, out *WebhookAdmission, s conversion.Scope) error {
out.KubeConfigFile = in.KubeConfigFile
return nil
}
// Convert_webhookadmission_WebhookAdmission_To_v1_WebhookAdmission is an autogenerated conversion function.
func Convert_webhookadmission_WebhookAdmission_To_v1_WebhookAdmission(in *webhookadmission.WebhookAdmission, out *WebhookAdmission, s conversion.Scope) error {
return autoConvert_webhookadmission_WebhookAdmission_To_v1_WebhookAdmission(in, out, s)
}

View file

@ -0,0 +1,50 @@
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookAdmission) DeepCopyInto(out *WebhookAdmission) {
*out = *in
out.TypeMeta = in.TypeMeta
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAdmission.
func (in *WebhookAdmission) DeepCopy() *WebhookAdmission {
if in == nil {
return nil
}
out := new(WebhookAdmission)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *WebhookAdmission) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View file

@ -0,0 +1,32 @@
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by defaulter-gen. DO NOT EDIT.
package v1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}

View file

@ -27,6 +27,7 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1"
)
@ -37,6 +38,7 @@ var (
func init() {
utilruntime.Must(webhookadmission.AddToScheme(scheme))
utilruntime.Must(v1.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))
}

View file

@ -21,8 +21,9 @@ import (
"fmt"
"io"
admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
@ -65,7 +66,14 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
return nil, err
}
cm, err := webhookutil.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme)
cm, err := webhookutil.NewClientManager(
[]schema.GroupVersion{
admissionv1beta1.SchemeGroupVersion,
admissionv1.SchemeGroupVersion,
},
admissionv1beta1.AddToScheme,
admissionv1.AddToScheme,
)
if err != nil {
return nil, err
}
@ -147,7 +155,7 @@ func (a *Webhook) ShouldCallHook(h webhook.WebhookAccessor, attr admission.Attri
break
}
}
if invocation == nil && h.GetMatchPolicy() != nil && *h.GetMatchPolicy() == v1beta1.Equivalent {
if invocation == nil && h.GetMatchPolicy() != nil && *h.GetMatchPolicy() == v1.Equivalent {
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
// honor earlier rules first
@ -203,7 +211,7 @@ type attrWithResourceOverride struct {
func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { return a.resource }
// Dispatch is called by the downstream Validate or Admit methods.
func (a *Webhook) Dispatch(attr admission.Attributes, o admission.ObjectInterfaces) error {
func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
if rules.IsWebhookConfigurationResource(attr) {
return nil
}
@ -211,8 +219,5 @@ func (a *Webhook) Dispatch(attr admission.Attributes, o admission.ObjectInterfac
return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request"))
}
hooks := a.hookSource.Webhooks()
// TODO: Figure out if adding one second timeout make sense here.
ctx := context.TODO()
return a.dispatcher.Dispatch(ctx, attr, o, hooks)
}

View file

@ -27,8 +27,8 @@ import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/klog"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1beta1"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -39,11 +39,24 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook"
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
webhookrequest "k8s.io/apiserver/pkg/admission/plugin/webhook/request"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
utiltrace "k8s.io/utils/trace"
)
const (
// PatchAuditAnnotationPrefix is a prefix for persisting webhook patch in audit annotation.
// Audit handler decides whether annotation with this prefix should be logged based on audit level.
// Since mutating webhook patches the request body, audit level must be greater or equal to Request
// for the annotation to be logged
PatchAuditAnnotationPrefix = "patch.webhook.admission.k8s.io/"
// MutationAuditAnnotationPrefix is a prefix for presisting webhook mutation existence in audit annotation.
MutationAuditAnnotationPrefix = "mutation.webhook.admission.k8s.io/"
)
var encodingjson = json.CaseSensitiveJsonIterator()
type mutatingDispatcher struct {
cm *webhookutil.ClientManager
plugin *Plugin
@ -76,7 +89,7 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
}()
var versionedAttr *generic.VersionedAttributes
for _, hook := range hooks {
for i, hook := range hooks {
attrForCheck := attr
if versionedAttr != nil {
attrForCheck = versionedAttr
@ -90,7 +103,7 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
}
hook, ok := invocation.Webhook.GetMutatingWebhook()
if !ok {
return fmt.Errorf("mutating webhook dispatch requires v1beta1.MutatingWebhook, but got %T", hook)
return fmt.Errorf("mutating webhook dispatch requires v1.MutatingWebhook, but got %T", hook)
}
// This means that during reinvocation, a webhook will not be
// called for the first time. For example, if the webhook is
@ -115,31 +128,61 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
}
t := time.Now()
changed, err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o)
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "admit", hook.Name)
round := 0
if reinvokeCtx.IsReinvoke() {
round = 1
}
changed, err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o, round, i)
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == admissionregistrationv1.Ignore
rejected := false
if err != nil {
switch err := err.(type) {
case *webhookutil.ErrCallingWebhook:
if !ignoreClientCallFailures {
rejected = true
admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "admit", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionCallingWebhookError, 0)
}
case *webhookutil.ErrWebhookRejection:
rejected = true
admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "admit", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionNoError, int(err.Status.ErrStatus.Code))
default:
rejected = true
admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "admit", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionAPIServerInternalError, 0)
}
}
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), rejected, versionedAttr.Attributes, "admit", hook.Name)
if changed {
// Patch had changed the object. Prepare to reinvoke all previous webhooks that are eligible for re-invocation.
webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
}
if hook.ReinvocationPolicy != nil && *hook.ReinvocationPolicy == v1beta1.IfNeededReinvocationPolicy {
if hook.ReinvocationPolicy != nil && *hook.ReinvocationPolicy == admissionregistrationv1.IfNeededReinvocationPolicy {
webhookReinvokeCtx.AddReinvocableWebhookToPreviouslyInvoked(invocation.Webhook.GetUID())
}
if err == nil {
continue
}
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
if ignoreClientCallFailures {
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
continue
select {
case <-ctx.Done():
// parent context is canceled or timed out, no point in continuing
return apierrors.NewTimeoutError("request did not complete within requested timeout", 0)
default:
// individual webhook timed out, but parent context did not, continue
continue
}
}
klog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
return apierrors.NewInternalError(err)
}
if rejectionErr, ok := err.(*webhookutil.ErrWebhookRejection); ok {
return rejectionErr.Status
}
return err
}
@ -153,60 +196,89 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
// note that callAttrMutatingHook updates attr
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) (bool, error) {
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admissionregistrationv1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces, round, idx int) (bool, error) {
configurationName := invocation.Webhook.GetConfigurationName()
annotator := newWebhookAnnotator(attr, round, idx, h.Name, configurationName)
changed := false
defer func() { annotator.addMutationAnnotation(changed) }()
if attr.Attributes.IsDryRun() {
if h.SideEffects == nil {
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
}
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
if !(*h.SideEffects == admissionregistrationv1.SideEffectClassNone || *h.SideEffects == admissionregistrationv1.SideEffectClassNoneOnDryRun) {
return false, webhookerrors.NewDryRunUnsupportedErr(h.Name)
}
}
// Currently dispatcher only supports `v1beta1` AdmissionReview
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")}
}
// Make the webhook request
request := request.CreateAdmissionReview(attr, invocation)
client, err := a.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation)
if err != nil {
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
response := &admissionv1beta1.AdmissionReview{}
r := client.Post().Context(ctx).Body(&request)
if h.TimeoutSeconds != nil {
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
// Make the webhook request
client, err := invocation.Webhook.GetRESTClient(a.cm)
if err != nil {
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
trace := utiltrace.New("Call mutating webhook",
utiltrace.Field{"configuration", configurationName},
utiltrace.Field{"webhook", h.Name},
utiltrace.Field{"resource", attr.GetResource()},
utiltrace.Field{"subresource", attr.GetSubresource()},
utiltrace.Field{"operation", attr.GetOperation()},
utiltrace.Field{"UID", uid})
defer trace.LogIfLong(500 * time.Millisecond)
// if the webhook has a specific timeout, wrap the context to apply it
if h.TimeoutSeconds != nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(*h.TimeoutSeconds)*time.Second)
defer cancel()
}
r := client.Post().Context(ctx).Body(request)
// if the context has a deadline, set it as a parameter to inform the backend
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
// compute the timeout
if timeout := time.Until(deadline); timeout > 0 {
// if it's not an even number of seconds, round up to the nearest second
if truncated := timeout.Truncate(time.Second); truncated != timeout {
timeout = truncated + time.Second
}
// set the timeout
r.Timeout(timeout)
}
}
if err := r.Do().Into(response); err != nil {
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
trace.Step("Request completed")
if response.Response == nil {
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
result, err := webhookrequest.VerifyAdmissionResponse(uid, true, response)
if err != nil {
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
for k, v := range response.Response.AuditAnnotations {
for k, v := range result.AuditAnnotations {
key := h.Name + "/" + k
if err := attr.Attributes.AddAnnotation(key, v); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for mutating webhook %s: %v", key, v, h.Name, err)
}
}
if !response.Response.Allowed {
return false, webhookerrors.ToStatusErr(h.Name, response.Response.Result)
if !result.Allowed {
return false, &webhookutil.ErrWebhookRejection{Status: webhookerrors.ToStatusErr(h.Name, result.Result)}
}
patchJS := response.Response.Patch
if len(patchJS) == 0 {
if len(result.Patch) == 0 {
return false, nil
}
patchObj, err := jsonpatch.DecodePatch(patchJS)
patchObj, err := jsonpatch.DecodePatch(result.Patch)
if err != nil {
return false, apierrors.NewInternalError(err)
}
if len(patchObj) == 0 {
return false, nil
}
@ -216,14 +288,21 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
return false, apierrors.NewInternalError(fmt.Errorf("admission webhook %q attempted to modify the object, which is not supported for this operation", h.Name))
}
var patchedJS []byte
jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false)
objJS, err := runtime.Encode(jsonSerializer, attr.VersionedObject)
if err != nil {
return false, apierrors.NewInternalError(err)
}
patchedJS, err := patchObj.Apply(objJS)
if err != nil {
return false, apierrors.NewInternalError(err)
switch result.PatchType {
// VerifyAdmissionResponse normalizes to v1 patch types, regardless of the AdmissionReview version used
case admissionv1.PatchTypeJSONPatch:
objJS, err := runtime.Encode(jsonSerializer, attr.VersionedObject)
if err != nil {
return false, apierrors.NewInternalError(err)
}
patchedJS, err = patchObj.Apply(objJS)
if err != nil {
return false, apierrors.NewInternalError(err)
}
default:
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("unsupported patch type %q", result.PatchType)}
}
var newVersionedObject runtime.Object
@ -244,10 +323,103 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
return false, apierrors.NewInternalError(err)
}
changed := !apiequality.Semantic.DeepEqual(attr.VersionedObject, newVersionedObject)
changed = !apiequality.Semantic.DeepEqual(attr.VersionedObject, newVersionedObject)
trace.Step("Patch applied")
annotator.addPatchAnnotation(patchObj, result.PatchType)
attr.Dirty = true
attr.VersionedObject = newVersionedObject
o.GetObjectDefaulter().Default(attr.VersionedObject)
return changed, nil
}
type webhookAnnotator struct {
attr *generic.VersionedAttributes
patchAnnotationKey string
mutationAnnotationKey string
webhook string
configuration string
}
func newWebhookAnnotator(attr *generic.VersionedAttributes, round, idx int, webhook, configuration string) *webhookAnnotator {
return &webhookAnnotator{
attr: attr,
patchAnnotationKey: fmt.Sprintf("%sround_%d_index_%d", PatchAuditAnnotationPrefix, round, idx),
mutationAnnotationKey: fmt.Sprintf("%sround_%d_index_%d", MutationAuditAnnotationPrefix, round, idx),
webhook: webhook,
configuration: configuration,
}
}
func (w *webhookAnnotator) addMutationAnnotation(mutated bool) {
if w.attr == nil || w.attr.Attributes == nil {
return
}
value, err := mutationAnnotationValue(w.configuration, w.webhook, mutated)
if err != nil {
klog.Warningf("unexpected error composing mutating webhook annotation: %v", err)
return
}
if err := w.attr.Attributes.AddAnnotation(w.mutationAnnotationKey, value); err != nil {
klog.Warningf("failed to set mutation annotation for mutating webhook key %s to %s: %v", w.mutationAnnotationKey, value, err)
}
}
func (w *webhookAnnotator) addPatchAnnotation(patch interface{}, patchType admissionv1.PatchType) {
if w.attr == nil || w.attr.Attributes == nil {
return
}
var value string
var err error
switch patchType {
case admissionv1.PatchTypeJSONPatch:
value, err = jsonPatchAnnotationValue(w.configuration, w.webhook, patch)
if err != nil {
klog.Warningf("unexpected error composing mutating webhook JSON patch annotation: %v", err)
return
}
default:
klog.Warningf("unsupported patch type for mutating webhook annotation: %v", patchType)
return
}
if err := w.attr.Attributes.AddAnnotationWithLevel(w.patchAnnotationKey, value, auditinternal.LevelRequest); err != nil {
// NOTE: we don't log actual patch in kube-apiserver log to avoid potentially
// leaking information
klog.Warningf("failed to set patch annotation for mutating webhook key %s; confugiration name: %s, webhook name: %s", w.patchAnnotationKey, w.configuration, w.webhook)
}
}
// MutationAuditAnnotation logs if a webhook invocation mutated the request object
type MutationAuditAnnotation struct {
Configuration string `json:"configuration"`
Webhook string `json:"webhook"`
Mutated bool `json:"mutated"`
}
// PatchAuditAnnotation logs a patch from a mutating webhook
type PatchAuditAnnotation struct {
Configuration string `json:"configuration"`
Webhook string `json:"webhook"`
Patch interface{} `json:"patch,omitempty"`
PatchType string `json:"patchType,omitempty"`
}
func mutationAnnotationValue(configuration, webhook string, mutated bool) (string, error) {
m := MutationAuditAnnotation{
Configuration: configuration,
Webhook: webhook,
Mutated: mutated,
}
bytes, err := encodingjson.Marshal(m)
return string(bytes), err
}
func jsonPatchAnnotationValue(configuration, webhook string, patch interface{}) (string, error) {
p := PatchAuditAnnotation{
Configuration: configuration,
Webhook: webhook,
Patch: patch,
PatchType: string(admissionv1.PatchTypeJSONPatch),
}
bytes, err := encodingjson.Marshal(p)
return string(bytes), err
}

View file

@ -17,6 +17,7 @@ limitations under the License.
package mutating
import (
"context"
"io"
"k8s.io/apiserver/pkg/admission"
@ -70,6 +71,6 @@ func (a *Plugin) ValidateInitialization() error {
}
// Admit makes an admission decision based on the request attributes.
func (a *Plugin) Admit(attr admission.Attributes, o admission.ObjectInterfaces) error {
return a.Webhook.Dispatch(attr, o)
func (a *Plugin) Admit(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
return a.Webhook.Dispatch(ctx, attr, o)
}

View file

@ -95,8 +95,7 @@ func (m *Matcher) MatchNamespaceSelector(h webhook.WebhookAccessor, attr admissi
// Also update the comment in types.go
return true, nil
}
// TODO: adding an LRU cache to cache the translation
selector, err := metav1.LabelSelectorAsSelector(h.GetNamespaceSelector())
selector, err := h.GetParsedNamespaceSelector()
if err != nil {
return false, apierrors.NewInternalError(err)
}

View file

@ -19,7 +19,6 @@ package object
import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
@ -47,8 +46,7 @@ func matchObject(obj runtime.Object, selector labels.Selector) bool {
// MatchObjectSelector decideds whether the request matches the ObjectSelector
// of the webhook. Only when they match, the webhook is called.
func (m *Matcher) MatchObjectSelector(h webhook.WebhookAccessor, attr admission.Attributes) (bool, *apierrors.StatusError) {
// TODO: adding an LRU cache to cache the translation
selector, err := metav1.LabelSelectorAsSelector(h.GetObjectSelector())
selector, err := h.GetParsedObjectSelector()
if err != nil {
return false, apierrors.NewInternalError(err)
}

View file

@ -17,16 +17,138 @@ limitations under the License.
package request
import (
"fmt"
admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
// CreateAdmissionReview creates an AdmissionReview for the provided admission.Attributes
func CreateAdmissionReview(versionedAttributes *generic.VersionedAttributes, invocation *generic.WebhookInvocation) admissionv1beta1.AdmissionReview {
// AdmissionResponse contains the fields extracted from an AdmissionReview response
type AdmissionResponse struct {
AuditAnnotations map[string]string
Allowed bool
Patch []byte
PatchType admissionv1.PatchType
Result *metav1.Status
}
// VerifyAdmissionResponse checks the validity of the provided admission review object, and returns the
// audit annotations, whether the response allowed the request, any provided patch/patchType/status,
// or an error if the provided admission review was not valid.
func VerifyAdmissionResponse(uid types.UID, mutating bool, review runtime.Object) (*AdmissionResponse, error) {
switch r := review.(type) {
case *admissionv1.AdmissionReview:
if r.Response == nil {
return nil, fmt.Errorf("webhook response was absent")
}
// Verify UID matches
if r.Response.UID != uid {
return nil, fmt.Errorf("expected response.uid=%q, got %q", uid, r.Response.UID)
}
// Verify GVK
v1GVK := admissionv1.SchemeGroupVersion.WithKind("AdmissionReview")
if r.GroupVersionKind() != v1GVK {
return nil, fmt.Errorf("expected webhook response of %v, got %v", v1GVK.String(), r.GroupVersionKind().String())
}
patch := []byte(nil)
patchType := admissionv1.PatchType("")
if mutating {
// Ensure a mutating webhook provides both patch and patchType together
if len(r.Response.Patch) > 0 && r.Response.PatchType == nil {
return nil, fmt.Errorf("webhook returned response.patch but not response.patchType")
}
if len(r.Response.Patch) == 0 && r.Response.PatchType != nil {
return nil, fmt.Errorf("webhook returned response.patchType but not response.patch")
}
patch = r.Response.Patch
if r.Response.PatchType != nil {
patchType = *r.Response.PatchType
if len(patchType) == 0 {
return nil, fmt.Errorf("webhook returned invalid response.patchType of %q", patchType)
}
}
} else {
// Ensure a validating webhook doesn't return patch or patchType
if len(r.Response.Patch) > 0 {
return nil, fmt.Errorf("validating webhook may not return response.patch")
}
if r.Response.PatchType != nil {
return nil, fmt.Errorf("validating webhook may not return response.patchType")
}
}
return &AdmissionResponse{
AuditAnnotations: r.Response.AuditAnnotations,
Allowed: r.Response.Allowed,
Patch: patch,
PatchType: patchType,
Result: r.Response.Result,
}, nil
case *admissionv1beta1.AdmissionReview:
if r.Response == nil {
return nil, fmt.Errorf("webhook response was absent")
}
// Response GVK and response.uid were not verified in v1beta1 handling, allow any
patch := []byte(nil)
patchType := admissionv1.PatchType("")
if mutating {
patch = r.Response.Patch
if len(r.Response.Patch) > 0 {
// patch type was not verified in v1beta1 admissionreview handling. pin to only supported version if a patch is provided.
patchType = admissionv1.PatchTypeJSONPatch
}
}
return &AdmissionResponse{
AuditAnnotations: r.Response.AuditAnnotations,
Allowed: r.Response.Allowed,
Patch: patch,
PatchType: patchType,
Result: r.Response.Result,
}, nil
default:
return nil, fmt.Errorf("unexpected response type %T", review)
}
}
// CreateAdmissionObjects returns the unique request uid, the AdmissionReview object to send the webhook and to decode the response into,
// or an error if the webhook does not support receiving any of the admission review versions we know to send
func CreateAdmissionObjects(versionedAttributes *generic.VersionedAttributes, invocation *generic.WebhookInvocation) (uid types.UID, request, response runtime.Object, err error) {
for _, version := range invocation.Webhook.GetAdmissionReviewVersions() {
switch version {
case admissionv1.SchemeGroupVersion.Version:
uid := types.UID(uuid.NewUUID())
request := CreateV1AdmissionReview(uid, versionedAttributes, invocation)
response := &admissionv1.AdmissionReview{}
return uid, request, response, nil
case admissionv1beta1.SchemeGroupVersion.Version:
uid := types.UID(uuid.NewUUID())
request := CreateV1beta1AdmissionReview(uid, versionedAttributes, invocation)
response := &admissionv1beta1.AdmissionReview{}
return uid, request, response, nil
}
}
return "", nil, nil, fmt.Errorf("webhook does not accept known AdmissionReview versions (v1, v1beta1)")
}
// CreateV1AdmissionReview creates an AdmissionReview for the provided admission.Attributes
func CreateV1AdmissionReview(uid types.UID, versionedAttributes *generic.VersionedAttributes, invocation *generic.WebhookInvocation) *admissionv1.AdmissionReview {
attr := versionedAttributes.Attributes
gvk := invocation.Kind
gvr := invocation.Resource
@ -48,9 +170,75 @@ func CreateAdmissionReview(versionedAttributes *generic.VersionedAttributes, inv
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
return admissionv1beta1.AdmissionReview{
return &admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
UID: uid,
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
Object: runtime.RawExtension{
Object: versionedAttributes.VersionedObject,
},
OldObject: runtime.RawExtension{
Object: versionedAttributes.VersionedOldObject,
},
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
},
}
}
// CreateV1beta1AdmissionReview creates an AdmissionReview for the provided admission.Attributes
func CreateV1beta1AdmissionReview(uid types.UID, versionedAttributes *generic.VersionedAttributes, invocation *generic.WebhookInvocation) *admissionv1beta1.AdmissionReview {
attr := versionedAttributes.Attributes
gvk := invocation.Kind
gvr := invocation.Resource
subresource := invocation.Subresource
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
userInfo := authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
dryRun := attr.IsDryRun()
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
return &admissionv1beta1.AdmissionReview{
Request: &admissionv1beta1.AdmissionRequest{
UID: uuid.NewUUID(),
UID: uid,
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,

View file

@ -19,7 +19,7 @@ package rules
import (
"strings"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
@ -27,7 +27,7 @@ import (
// Matcher determines if the Attr matches the Rule.
type Matcher struct {
Rule v1beta1.RuleWithOperations
Rule v1.RuleWithOperations
Attr admission.Attributes
}
@ -56,15 +56,15 @@ func exactOrWildcard(items []string, requested string) bool {
var namespaceResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"}
func (r *Matcher) scope() bool {
if r.Rule.Scope == nil || *r.Rule.Scope == v1beta1.AllScopes {
if r.Rule.Scope == nil || *r.Rule.Scope == v1.AllScopes {
return true
}
// attr.GetNamespace() is set to the name of the namespace for requests of the namespace object itself.
switch *r.Rule.Scope {
case v1beta1.NamespacedScope:
case v1.NamespacedScope:
// first make sure that we are not requesting a namespace object (namespace objects are cluster-scoped)
return r.Attr.GetResource() != namespaceResource && r.Attr.GetNamespace() != metav1.NamespaceNone
case v1beta1.ClusterScope:
case v1.ClusterScope:
// also return true if the request is for a namespace object (namespace objects are cluster-scoped)
return r.Attr.GetResource() == namespaceResource || r.Attr.GetNamespace() == metav1.NamespaceNone
default:
@ -83,12 +83,12 @@ func (r *Matcher) version() bool {
func (r *Matcher) operation() bool {
attrOp := r.Attr.GetOperation()
for _, op := range r.Rule.Operations {
if op == v1beta1.OperationAll {
if op == v1.OperationAll {
return true
}
// The constants are the same such that this is a valid cast (and this
// is tested).
if op == v1beta1.OperationType(attrOp) {
if op == v1.OperationType(attrOp) {
return true
}
}

View file

@ -1,58 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"k8s.io/apiserver/pkg/admission/plugin/webhook"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
)
// HookClientConfigForWebhook construct a webhookutil.ClientConfig using a WebhookAccessor to access
// v1beta1.MutatingWebhook and v1beta1.ValidatingWebhook API objects. webhookutil.ClientConfig is used
// to create a HookClient and the purpose of the config struct is to share that with other packages
// that need to create a HookClient.
func HookClientConfigForWebhook(w webhook.WebhookAccessor) webhookutil.ClientConfig {
ret := webhookutil.ClientConfig{Name: w.GetName(), CABundle: w.GetClientConfig().CABundle}
if w.GetClientConfig().URL != nil {
ret.URL = *w.GetClientConfig().URL
}
if w.GetClientConfig().Service != nil {
ret.Service = &webhookutil.ClientConfigService{
Name: w.GetClientConfig().Service.Name,
Namespace: w.GetClientConfig().Service.Namespace,
}
if w.GetClientConfig().Service.Port != nil {
ret.Service.Port = *w.GetClientConfig().Service.Port
} else {
ret.Service.Port = 443
}
if w.GetClientConfig().Service.Path != nil {
ret.Service.Path = *w.GetClientConfig().Service.Path
}
}
return ret
}
// HasAdmissionReviewVersion check whether a version is accepted by a given webhook.
func HasAdmissionReviewVersion(a string, w webhook.WebhookAccessor) bool {
for _, b := range w.GetAdmissionReviewVersions() {
if b == a {
return true
}
}
return false
}

View file

@ -22,8 +22,7 @@ import (
"sync"
"time"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@ -32,10 +31,10 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook"
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
webhookrequest "k8s.io/apiserver/pkg/admission/plugin/webhook/request"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/klog"
utiltrace "k8s.io/utils/trace"
)
type validatingDispatcher struct {
@ -80,6 +79,14 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
return nil
}
// Check if the request has already timed out before spawning remote calls
select {
case <-ctx.Done():
// parent context is canceled or timed out, no point in continuing
return apierrors.NewTimeoutError("request did not complete within requested timeout", 0)
default:
}
wg := sync.WaitGroup{}
errCh := make(chan error, len(relevantHooks))
wg.Add(len(relevantHooks))
@ -88,18 +95,34 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
defer wg.Done()
hook, ok := invocation.Webhook.GetValidatingWebhook()
if !ok {
utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1beta1.ValidatingWebhook, but got %T", hook))
utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1.ValidatingWebhook, but got %T", hook))
return
}
versionedAttr := versionedAttrs[invocation.Kind]
t := time.Now()
err := d.callHook(ctx, hook, invocation, versionedAttr)
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "validating", hook.Name)
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1.Ignore
rejected := false
if err != nil {
switch err := err.(type) {
case *webhookutil.ErrCallingWebhook:
if !ignoreClientCallFailures {
rejected = true
admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionCallingWebhookError, 0)
}
case *webhookutil.ErrWebhookRejection:
rejected = true
admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionNoError, int(err.Status.ErrStatus.Code))
default:
rejected = true
admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionAPIServerInternalError, 0)
}
}
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), rejected, versionedAttr.Attributes, "validating", hook.Name)
if err == nil {
return
}
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
if ignoreClientCallFailures {
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
@ -112,6 +135,9 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
return
}
if rejectionErr, ok := err.(*webhookutil.ErrWebhookRejection); ok {
err = rejectionErr.Status
}
klog.Warningf("rejected by webhook %q: %#v", hook.Name, err)
errCh <- err
}(relevantHooks[i])
@ -135,48 +161,74 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
return errs[0]
}
func (d *validatingDispatcher) callHook(ctx context.Context, h *v1beta1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
if attr.Attributes.IsDryRun() {
if h.SideEffects == nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
}
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
if !(*h.SideEffects == v1.SideEffectClassNone || *h.SideEffects == v1.SideEffectClassNoneOnDryRun) {
return webhookerrors.NewDryRunUnsupportedErr(h.Name)
}
}
// Currently dispatcher only supports `v1beta1` AdmissionReview
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")}
}
// Make the webhook request
request := request.CreateAdmissionReview(attr, invocation)
client, err := d.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation)
if err != nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
response := &admissionv1beta1.AdmissionReview{}
r := client.Post().Context(ctx).Body(&request)
if h.TimeoutSeconds != nil {
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
// Make the webhook request
client, err := invocation.Webhook.GetRESTClient(d.cm)
if err != nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
trace := utiltrace.New("Call validating webhook",
utiltrace.Field{"configuration", invocation.Webhook.GetConfigurationName()},
utiltrace.Field{"webhook", h.Name},
utiltrace.Field{"resource", attr.GetResource()},
utiltrace.Field{"subresource", attr.GetSubresource()},
utiltrace.Field{"operation", attr.GetOperation()},
utiltrace.Field{"UID", uid})
defer trace.LogIfLong(500 * time.Millisecond)
// if the webhook has a specific timeout, wrap the context to apply it
if h.TimeoutSeconds != nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(*h.TimeoutSeconds)*time.Second)
defer cancel()
}
r := client.Post().Context(ctx).Body(request)
// if the context has a deadline, set it as a parameter to inform the backend
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
// compute the timeout
if timeout := time.Until(deadline); timeout > 0 {
// if it's not an even number of seconds, round up to the nearest second
if truncated := timeout.Truncate(time.Second); truncated != timeout {
timeout = truncated + time.Second
}
// set the timeout
r.Timeout(timeout)
}
}
if err := r.Do().Into(response); err != nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
trace.Step("Request completed")
if response.Response == nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
result, err := webhookrequest.VerifyAdmissionResponse(uid, false, response)
if err != nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
for k, v := range response.Response.AuditAnnotations {
for k, v := range result.AuditAnnotations {
key := h.Name + "/" + k
if err := attr.Attributes.AddAnnotation(key, v); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, v, h.Name, err)
}
}
if response.Response.Allowed {
if result.Allowed {
return nil
}
return webhookerrors.ToStatusErr(h.Name, response.Response.Result)
return &webhookutil.ErrWebhookRejection{Status: webhookerrors.ToStatusErr(h.Name, result.Result)}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
package validating
import (
"context"
"io"
"k8s.io/apiserver/pkg/admission"
@ -61,6 +62,6 @@ func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) {
}
// Validate makes an admission decision based on the request attributes.
func (a *Plugin) Validate(attr admission.Attributes, o admission.ObjectInterfaces) error {
return a.Webhook.Dispatch(attr, o)
func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
return a.Webhook.Dispatch(ctx, attr, o)
}

View file

@ -16,6 +16,8 @@ limitations under the License.
package admission
import "context"
// newReinvocationHandler creates a handler that wraps the provided admission chain and reinvokes it
// if needed according to re-invocation policy of the webhooks.
func newReinvocationHandler(admissionChain Interface) Interface {
@ -30,9 +32,9 @@ type reinvoker struct {
// admission chain if needed according to the reinvocation policy. Plugins are expected to check
// the admission attributes' reinvocation context against their reinvocation policy to decide if
// they should re-run, and to update the reinvocation context if they perform any mutations.
func (r *reinvoker) Admit(a Attributes, o ObjectInterfaces) error {
func (r *reinvoker) Admit(ctx context.Context, a Attributes, o ObjectInterfaces) error {
if mutator, ok := r.admissionChain.(MutationInterface); ok {
err := mutator.Admit(a, o)
err := mutator.Admit(ctx, a, o)
if err != nil {
return err
}
@ -42,16 +44,16 @@ func (r *reinvoker) Admit(a Attributes, o ObjectInterfaces) error {
// Calling admit a second time will reinvoke all in-tree plugins
// as well as any webhook plugins that need to be reinvoked based on the
// reinvocation policy.
return mutator.Admit(a, o)
return mutator.Admit(ctx, a, o)
}
}
return nil
}
// Validate performs an admission control check using the wrapped admission chain, and returns immediately on first error.
func (r *reinvoker) Validate(a Attributes, o ObjectInterfaces) error {
func (r *reinvoker) Validate(ctx context.Context, a Attributes, o ObjectInterfaces) error {
if validator, ok := r.admissionChain.(ValidationInterface); ok {
return validator.Validate(a, o)
return validator.Validate(ctx, a, o)
}
return nil
}