191 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			191 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| Copyright 2017 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 handlers
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"k8s.io/apimachinery/pkg/api/errors"
 | |
| 	"k8s.io/apimachinery/pkg/runtime"
 | |
| 	"k8s.io/apimachinery/pkg/runtime/schema"
 | |
| 	"k8s.io/apiserver/pkg/admission"
 | |
| 	"k8s.io/apiserver/pkg/audit"
 | |
| 	"k8s.io/apiserver/pkg/authorization/authorizer"
 | |
| 	"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
 | |
| 	"k8s.io/apiserver/pkg/endpoints/request"
 | |
| 	"k8s.io/apiserver/pkg/registry/rest"
 | |
| 	utiltrace "k8s.io/apiserver/pkg/util/trace"
 | |
| )
 | |
| 
 | |
| // UpdateResource returns a function that will handle a resource update
 | |
| func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interface) http.HandlerFunc {
 | |
| 	return func(w http.ResponseWriter, req *http.Request) {
 | |
| 		// For performance tracking purposes.
 | |
| 		trace := utiltrace.New("Update " + req.URL.Path)
 | |
| 		defer trace.LogIfLong(500 * time.Millisecond)
 | |
| 
 | |
| 		if isDryRun(req.URL) {
 | |
| 			scope.err(errors.NewBadRequest("dryRun is not supported yet"), w, req)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// 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"))
 | |
| 
 | |
| 		namespace, name, err := scope.Namer.Name(req)
 | |
| 		if err != nil {
 | |
| 			scope.err(err, w, req)
 | |
| 			return
 | |
| 		}
 | |
| 		ctx := req.Context()
 | |
| 		ctx = request.WithNamespace(ctx, namespace)
 | |
| 
 | |
| 		body, err := readBody(req)
 | |
| 		if err != nil {
 | |
| 			scope.err(err, w, req)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		s, err := negotiation.NegotiateInputSerializer(req, false, scope.Serializer)
 | |
| 		if err != nil {
 | |
| 			scope.err(err, w, req)
 | |
| 			return
 | |
| 		}
 | |
| 		defaultGVK := scope.Kind
 | |
| 		original := r.New()
 | |
| 		trace.Step("About to convert to expected version")
 | |
| 		decoder := scope.Serializer.DecoderToVersion(s.Serializer, schema.GroupVersion{Group: defaultGVK.Group, Version: runtime.APIVersionInternal})
 | |
| 		obj, gvk, err := decoder.Decode(body, &defaultGVK, original)
 | |
| 		if err != nil {
 | |
| 			err = transformDecodeError(scope.Typer, err, original, gvk, body)
 | |
| 			scope.err(err, w, req)
 | |
| 			return
 | |
| 		}
 | |
| 		if gvk.GroupVersion() != defaultGVK.GroupVersion() {
 | |
| 			err = errors.NewBadRequest(fmt.Sprintf("the API version in the data (%s) does not match the expected API version (%s)", gvk.GroupVersion(), defaultGVK.GroupVersion()))
 | |
| 			scope.err(err, w, req)
 | |
| 			return
 | |
| 		}
 | |
| 		trace.Step("Conversion done")
 | |
| 
 | |
| 		ae := request.AuditEventFrom(ctx)
 | |
| 		audit.LogRequestObject(ae, obj, scope.Resource, scope.Subresource, scope.Serializer)
 | |
| 		admit = admission.WithAudit(admit, ae)
 | |
| 
 | |
| 		if err := checkName(obj, name, namespace, scope.Namer); err != nil {
 | |
| 			scope.err(err, w, req)
 | |
| 			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 mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Update) {
 | |
| 			transformers = append(transformers, func(ctx context.Context, newObj, oldObj runtime.Object) (runtime.Object, error) {
 | |
| 				return newObj, mutatingAdmission.Admit(admission.NewAttributesRecord(newObj, oldObj, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Update, userInfo))
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		createAuthorizerAttributes := authorizer.AttributesRecord{
 | |
| 			User:            userInfo,
 | |
| 			ResourceRequest: true,
 | |
| 			Path:            req.URL.Path,
 | |
| 			Verb:            "create",
 | |
| 			APIGroup:        scope.Resource.Group,
 | |
| 			APIVersion:      scope.Resource.Version,
 | |
| 			Resource:        scope.Resource.Resource,
 | |
| 			Subresource:     scope.Subresource,
 | |
| 			Namespace:       namespace,
 | |
| 			Name:            name,
 | |
| 		}
 | |
| 
 | |
| 		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...),
 | |
| 				withAuthorization(rest.AdmissionToValidateObjectFunc(admit, staticAdmissionAttributes), scope.Authorizer, createAuthorizerAttributes),
 | |
| 				rest.AdmissionToValidateObjectUpdateFunc(admit, staticAdmissionAttributes),
 | |
| 				false,
 | |
| 			)
 | |
| 			wasCreated = created
 | |
| 			return obj, err
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			scope.err(err, w, req)
 | |
| 			return
 | |
| 		}
 | |
| 		trace.Step("Object stored in database")
 | |
| 
 | |
| 		requestInfo, ok := request.RequestInfoFrom(ctx)
 | |
| 		if !ok {
 | |
| 			scope.err(fmt.Errorf("missing requestInfo"), w, req)
 | |
| 			return
 | |
| 		}
 | |
| 		if err := setSelfLink(result, requestInfo, scope.Namer); err != nil {
 | |
| 			scope.err(err, w, req)
 | |
| 			return
 | |
| 		}
 | |
| 		trace.Step("Self-link added")
 | |
| 
 | |
| 		status := http.StatusOK
 | |
| 		if wasCreated {
 | |
| 			status = http.StatusCreated
 | |
| 		}
 | |
| 
 | |
| 		transformResponseObject(ctx, scope, req, w, status, result)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func withAuthorization(validate rest.ValidateObjectFunc, a authorizer.Authorizer, attributes authorizer.Attributes) rest.ValidateObjectFunc {
 | |
| 	var once sync.Once
 | |
| 	var authorizerDecision authorizer.Decision
 | |
| 	var authorizerReason string
 | |
| 	var authorizerErr error
 | |
| 	return func(obj runtime.Object) error {
 | |
| 		if a == nil {
 | |
| 			return errors.NewInternalError(fmt.Errorf("no authorizer provided, unable to authorize a create on update"))
 | |
| 		}
 | |
| 		once.Do(func() {
 | |
| 			authorizerDecision, authorizerReason, authorizerErr = a.Authorize(attributes)
 | |
| 		})
 | |
| 		// an authorizer like RBAC could encounter evaluation errors and still allow the request, so authorizer decision is checked before error here.
 | |
| 		if authorizerDecision == authorizer.DecisionAllow {
 | |
| 			// Continue to validating admission
 | |
| 			return validate(obj)
 | |
| 		}
 | |
| 		if authorizerErr != nil {
 | |
| 			return errors.NewInternalError(authorizerErr)
 | |
| 		}
 | |
| 
 | |
| 		// The user is not authorized to perform this action, so we need to build the error response
 | |
| 		gr := schema.GroupResource{
 | |
| 			Group:    attributes.GetAPIGroup(),
 | |
| 			Resource: attributes.GetResource(),
 | |
| 		}
 | |
| 		name := attributes.GetName()
 | |
| 		err := fmt.Errorf("%v", authorizerReason)
 | |
| 		return errors.NewForbidden(gr, name, err)
 | |
| 	}
 | |
| }
 |