diff --git a/staging/src/k8s.io/apiserver/pkg/admission/interfaces.go b/staging/src/k8s.io/apiserver/pkg/admission/interfaces.go index a8e671db514..5aa37710230 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/interfaces.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/interfaces.go @@ -61,6 +61,16 @@ type Interface interface { Handles(operation Operation) bool } +// ValidationInterface is an abstract, pluggable interface for Admission Control decisions. +type ValidationInterface interface { + // Validate makes an admission decision based on the request attributes. It is NOT allowed to mutate + Validate(a Attributes) (err error) + + // Handles returns true if this admission controller can handle the given operation + // where operation can be one of CREATE, UPDATE, DELETE, or CONNECT + Handles(operation Operation) bool +} + // Operation is the type of resource operation being checked for admission control type Operation string diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index 96c2da4b8bd..ae2e0bf42dc 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -34,7 +34,7 @@ import ( utiltrace "k8s.io/apiserver/pkg/util/trace" ) -func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface, includeName bool) http.HandlerFunc { +func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.ObjectTyper, mutatingAdmission admission.Interface, validatingAdmission admission.ValidationInterface, includeName bool) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { // For performance tracking purposes. trace := utiltrace.New("Create " + req.URL.Path) @@ -93,10 +93,10 @@ func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.Object ae := request.AuditEventFrom(ctx) audit.LogRequestObject(ae, obj, scope.Resource, scope.Subresource, scope.Serializer) - if admit != nil && admit.Handles(admission.Create) { - userInfo, _ := request.UserFrom(ctx) - - err = admit.Admit(admission.NewAttributesRecord(obj, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, userInfo)) + userInfo, _ := request.UserFrom(ctx) + admissionAttributes := admission.NewAttributesRecord(obj, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, userInfo) + if mutatingAdmission != nil && mutatingAdmission.Handles(admission.Create) { + err = mutatingAdmission.Admit(admissionAttributes) if err != nil { scope.err(err, w, req) return @@ -108,7 +108,13 @@ func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.Object trace.Step("About to store object in database") result, err := finishRequest(timeout, func() (runtime.Object, error) { - return r.Create(ctx, name, obj, includeUninitialized) + return r.Create( + ctx, + name, + obj, + rest.AdmissionToValidateObjectFunc(validatingAdmission, admissionAttributes), + includeUninitialized, + ) }) if err != nil { scope.err(err, w, req) @@ -144,19 +150,19 @@ func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.Object } // CreateNamedResource returns a function that will handle a resource creation with name. -func CreateNamedResource(r rest.NamedCreater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) http.HandlerFunc { - return createHandler(r, scope, typer, admit, true) +func CreateNamedResource(r rest.NamedCreater, scope RequestScope, typer runtime.ObjectTyper, mutatingAdmission admission.Interface, validatingAdmission admission.ValidationInterface) http.HandlerFunc { + return createHandler(r, scope, typer, mutatingAdmission, validatingAdmission, true) } // CreateResource returns a function that will handle a resource creation. -func CreateResource(r rest.Creater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) http.HandlerFunc { - return createHandler(&namedCreaterAdapter{r}, scope, typer, admit, false) +func CreateResource(r rest.Creater, scope RequestScope, typer runtime.ObjectTyper, mutatingAdmission admission.Interface, validatingAdmission admission.ValidationInterface) http.HandlerFunc { + return createHandler(&namedCreaterAdapter{r}, scope, typer, mutatingAdmission, validatingAdmission, false) } type namedCreaterAdapter struct { rest.Creater } -func (c *namedCreaterAdapter) Create(ctx request.Context, name string, obj runtime.Object, includeUninitialized bool) (runtime.Object, error) { - return c.Creater.Create(ctx, obj, includeUninitialized) +func (c *namedCreaterAdapter) Create(ctx request.Context, name string, obj runtime.Object, createValidatingAdmission rest.ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) { + return c.Creater.Create(ctx, obj, createValidatingAdmission, includeUninitialized) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/delete.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/delete.go index 81fe41be198..2eeea0889e5 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/delete.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/delete.go @@ -34,7 +34,8 @@ import ( ) // DeleteResource returns a function that will handle a resource deletion -func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestScope, admit admission.Interface) http.HandlerFunc { +// TODO admission here becomes solely validating admission +func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestScope, mutatingAdmission admission.Interface, validatingAdmission admission.ValidationInterface) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { // For performance tracking purposes. trace := utiltrace.New("Delete " + req.URL.Path) @@ -93,10 +94,19 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco } trace.Step("About to check admission control") - if admit != nil && admit.Handles(admission.Delete) { + if mutatingAdmission != nil && mutatingAdmission.Handles(admission.Delete) { userInfo, _ := request.UserFrom(ctx) - err = admit.Admit(admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Delete, userInfo)) + err = mutatingAdmission.Admit(admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Delete, userInfo)) + if err != nil { + scope.err(err, w, req) + return + } + } + if validatingAdmission != nil && validatingAdmission.Handles(admission.Delete) { + userInfo, _ := request.UserFrom(ctx) + + err = validatingAdmission.Validate(admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Delete, userInfo)) if err != nil { scope.err(err, w, req) return @@ -157,7 +167,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco } // DeleteCollection returns a function that will handle a collection deletion -func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestScope, admit admission.Interface) http.HandlerFunc { +func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestScope, mutatingAdmission admission.Interface, validatingAdmission admission.ValidationInterface) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { // TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer) timeout := parseTimeout(req.URL.Query().Get("timeout")) @@ -171,10 +181,19 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestSco ctx := scope.ContextFunc(req) ctx = request.WithNamespace(ctx, namespace) - if admit != nil && admit.Handles(admission.Delete) { + if mutatingAdmission != nil && mutatingAdmission.Handles(admission.Delete) { userInfo, _ := request.UserFrom(ctx) - err = admit.Admit(admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, "", scope.Resource, scope.Subresource, admission.Delete, userInfo)) + err = mutatingAdmission.Admit(admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, "", scope.Resource, scope.Subresource, admission.Delete, userInfo)) + if err != nil { + scope.err(err, w, req) + return + } + } + if validatingAdmission != nil && validatingAdmission.Handles(admission.Delete) { + userInfo, _ := request.UserFrom(ctx) + + err = validatingAdmission.Validate(admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, "", scope.Resource, scope.Subresource, admission.Delete, userInfo)) if err != nil { scope.err(err, w, req) return diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index 9743f4c7a6b..0404c4a0cf5 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -33,7 +33,7 @@ import ( ) // UpdateResource returns a function that will handle a resource update -func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) http.HandlerFunc { +func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectTyper, mutatingAdmission admission.Interface, validatingAdmission admission.ValidationInterface) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { // For performance tracking purposes. trace := utiltrace.New("Update " + req.URL.Path) @@ -86,18 +86,25 @@ func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectType return } + userInfo, _ := request.UserFrom(ctx) + staticAdmissionAttributes := admission.NewAttributesRecord(nil, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Update, userInfo) var transformers []rest.TransformFunc - if admit != nil && admit.Handles(admission.Update) { + if mutatingAdmission != nil && mutatingAdmission.Handles(admission.Update) { transformers = append(transformers, func(ctx request.Context, newObj, oldObj runtime.Object) (runtime.Object, error) { - userInfo, _ := request.UserFrom(ctx) - return newObj, admit.Admit(admission.NewAttributesRecord(newObj, oldObj, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Update, userInfo)) + return newObj, mutatingAdmission.Admit(admission.NewAttributesRecord(newObj, oldObj, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Update, userInfo)) }) } trace.Step("About to store object in database") wasCreated := false result, err := finishRequest(timeout, func() (runtime.Object, error) { - obj, created, err := r.Update(ctx, name, rest.DefaultUpdatedObjectInfo(obj, transformers...)) + obj, created, err := r.Update( + ctx, + name, + rest.DefaultUpdatedObjectInfo(obj, transformers...), + rest.AdmissionToValidateObjectFunc(validatingAdmission, staticAdmissionAttributes), + rest.AdmissionToValidateObjectUpdateFunc(validatingAdmission, staticAdmissionAttributes), + ) wasCreated = created return obj, err }) diff --git a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go index c8a9fd6c857..96f3c91e985 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go @@ -52,6 +52,16 @@ import ( // object. type ObjectFunc func(obj runtime.Object) error +// ValidateObjectFunc is a function to act on a given object. An error may be returned +// if the hook cannot be completed. An ObjectFunc may NOT transform the provided +// object. +type ValidateObjectFunc func(obj runtime.Object) error + +// ValidateObjectUpdateFunc is a function to act on a given object and its predecessor. +// An error may be returned if the hook cannot be completed. An UpdateObjectFunc +// may NOT transform the provided object. +type ValidateObjectUpdateFunc func(obj, old runtime.Object) error + // GenericStore interface can be used for type assertions when we need to access the underlying strategies. type GenericStore interface { GetCreateStrategy() rest.RESTCreateStrategy @@ -148,11 +158,13 @@ type Store struct { // AfterCreate implements a further operation to run after a resource is // created and before it is decorated, optional. AfterCreate ObjectFunc + // UpdateStrategy implements resource-specific behavior during updates. UpdateStrategy rest.RESTUpdateStrategy // AfterUpdate implements a further operation to run after a resource is // updated and before it is decorated, optional. AfterUpdate ObjectFunc + // DeleteStrategy implements resource-specific behavior during deletion. DeleteStrategy rest.RESTDeleteStrategy // AfterDelete implements a further operation to run after a resource is @@ -303,10 +315,18 @@ func (e *Store) ListPredicate(ctx genericapirequest.Context, p storage.Selection } // Create inserts a new item according to the unique key from the object. -func (e *Store) Create(ctx genericapirequest.Context, obj runtime.Object, includeUninitialized bool) (runtime.Object, error) { +func (e *Store) Create(ctx genericapirequest.Context, obj runtime.Object, createValidation ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) { if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil { return nil, err } + // at this point we have a fully formed object. It is time to call the validators that the apiserver + // handling chain wants to enforce. + if createValidation != nil { + if err := createValidation(obj.DeepCopyObject()); err != nil { + return nil, err + } + } + name, err := e.ObjectNameFunc(obj) if err != nil { return nil, err @@ -494,7 +514,7 @@ func (e *Store) deleteWithoutFinalizers(ctx genericapirequest.Context, name, key // Update performs an atomic update and set of the object. Returns the result of the update // or an error. If the registry allows create-on-update, the create flow will be executed. // A bool is returned along with the object and any errors, to indicate object creation. -func (e *Store) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) { +func (e *Store) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation ValidateObjectFunc, updateValidation ValidateObjectUpdateFunc) (runtime.Object, bool, error) { key, err := e.KeyFunc(ctx, name) if err != nil { return nil, false, err @@ -544,10 +564,18 @@ func (e *Store) Update(ctx genericapirequest.Context, name string, objInfo rest. if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil { return nil, nil, err } + // at this point we have a fully formed object. It is time to call the validators that the apiserver + // handling chain wants to enforce. + if createValidation != nil { + if err := createValidation(obj.DeepCopyObject()); err != nil { + return nil, nil, err + } + } ttl, err := e.calculateTTL(obj, 0, false) if err != nil { return nil, nil, err } + return obj, &ttl, nil } @@ -582,6 +610,13 @@ func (e *Store) Update(ctx genericapirequest.Context, name string, objInfo rest. if err := rest.BeforeUpdate(e.UpdateStrategy, ctx, obj, existing); err != nil { return nil, nil, err } + // at this point we have a fully formed object. It is time to call the validators that the apiserver + // handling chain wants to enforce. + if updateValidation != nil { + if err := updateValidation(obj.DeepCopyObject(), existing.DeepCopyObject()); err != nil { + return nil, nil, err + } + } if e.shouldDeleteDuringUpdate(ctx, key, obj, existing) { deleteObj = obj return nil, nil, errEmptiedFinalizers diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go index 55b628f0308..450d2b3f46d 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/storage/names" @@ -151,3 +152,24 @@ type NamespaceScopedStrategy interface { // NamespaceScoped returns if the object must be in a namespace. NamespaceScoped() bool } + +// AdmissionToValidateObjectFunc converts validating admission to a rest validate object func +func AdmissionToValidateObjectFunc(validatingAdmission admission.ValidationInterface, staticAttributes admission.Attributes) ValidateObjectFunc { + return func(obj runtime.Object) error { + finalAttributes := admission.NewAttributesRecord( + obj, + staticAttributes.GetOldObject(), + staticAttributes.GetKind(), + staticAttributes.GetNamespace(), + staticAttributes.GetName(), + staticAttributes.GetResource(), + staticAttributes.GetSubresource(), + staticAttributes.GetOperation(), + staticAttributes.GetUserInfo(), + ) + if !validatingAdmission.Handles(finalAttributes.GetOperation()) { + return nil + } + return validatingAdmission.Validate(finalAttributes) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go index 21673110fd5..5a147b2a69c 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go @@ -192,7 +192,7 @@ type Creater interface { // Create creates a new version of a resource. If includeUninitialized is set, the object may be returned // without completing initialization. - Create(ctx genericapirequest.Context, obj runtime.Object, includeUninitialized bool) (runtime.Object, error) + Create(ctx genericapirequest.Context, obj runtime.Object, createValidation ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) } // NamedCreater is an object that can create an instance of a RESTful object using a name parameter. @@ -205,7 +205,7 @@ type NamedCreater interface { // This is needed for create operations on subresources which include the name of the parent // resource in the path. If includeUninitialized is set, the object may be returned without // completing initialization. - Create(ctx genericapirequest.Context, name string, obj runtime.Object, includeUninitialized bool) (runtime.Object, error) + Create(ctx genericapirequest.Context, name string, obj runtime.Object, createValidation ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) } // UpdatedObjectInfo provides information about an updated object to an Updater. @@ -221,6 +221,16 @@ type UpdatedObjectInfo interface { UpdatedObject(ctx genericapirequest.Context, oldObj runtime.Object) (newObj runtime.Object, err error) } +// ValidateObjectFunc is a function to act on a given object. An error may be returned +// if the hook cannot be completed. An ObjectFunc may NOT transform the provided +// object. +type ValidateObjectFunc func(obj runtime.Object) error + +// ValidateObjectUpdateFunc is a function to act on a given object and its predecessor. +// An error may be returned if the hook cannot be completed. An UpdateObjectFunc +// may NOT transform the provided object. +type ValidateObjectUpdateFunc func(obj, old runtime.Object) error + // Updater is an object that can update an instance of a RESTful object. type Updater interface { // New returns an empty object that can be used with Update after request data has been put into it. @@ -230,14 +240,14 @@ type Updater interface { // Update finds a resource in the storage and updates it. Some implementations // may allow updates creates the object - they should set the created boolean // to true. - Update(ctx genericapirequest.Context, name string, objInfo UpdatedObjectInfo) (runtime.Object, bool, error) + Update(ctx genericapirequest.Context, name string, objInfo UpdatedObjectInfo, createValidation ValidateObjectFunc, updateValidation ValidateObjectUpdateFunc) (runtime.Object, bool, error) } // CreaterUpdater is a storage object that must support both create and update. // Go prevents embedded interfaces that implement the same method. type CreaterUpdater interface { Creater - Update(ctx genericapirequest.Context, name string, objInfo UpdatedObjectInfo) (runtime.Object, bool, error) + Update(ctx genericapirequest.Context, name string, objInfo UpdatedObjectInfo, createValidation ValidateObjectFunc, updateValidation ValidateObjectUpdateFunc) (runtime.Object, bool, error) } // CreaterUpdater must satisfy the Updater interface. diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go index 02b27544746..eeff4e727a4 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -229,3 +230,24 @@ func (i *wrappedUpdatedObjectInfo) UpdatedObject(ctx genericapirequest.Context, return newObj, nil } + +// AdmissionToValidateObjectUpdateFunc converts validating admission to a rest validate object update func +func AdmissionToValidateObjectUpdateFunc(validatingAdmission admission.ValidationInterface, staticAttributes admission.Attributes) ValidateObjectUpdateFunc { + return func(obj, old runtime.Object) error { + finalAttributes := admission.NewAttributesRecord( + obj, + old, + staticAttributes.GetKind(), + staticAttributes.GetNamespace(), + staticAttributes.GetName(), + staticAttributes.GetResource(), + staticAttributes.GetSubresource(), + staticAttributes.GetOperation(), + staticAttributes.GetUserInfo(), + ) + if !validatingAdmission.Handles(finalAttributes.GetOperation()) { + return nil + } + return validatingAdmission.Validate(finalAttributes) + } +}