diff --git a/pkg/api/rest/rest.go b/pkg/api/rest/rest.go index 3698bf2948d..14e434c7749 100644 --- a/pkg/api/rest/rest.go +++ b/pkg/api/rest/rest.go @@ -17,6 +17,7 @@ limitations under the License. package rest import ( + "io" "net/http" "net/url" @@ -148,3 +149,21 @@ type Redirector interface { // ResourceLocation should return the remote location of the given resource, and an optional transport to use to request it, or an error. ResourceLocation(ctx api.Context, id string) (remoteLocation *url.URL, transport http.RoundTripper, err error) } + +// ResourceStreamer is an interface implemented by objects that prefer to be streamed from the server +// instead of decoded directly. +type ResourceStreamer interface { + // InputStream should return an io.Reader if the provided object supports streaming. The desired + // api version and a accept header (may be empty) are passed to the call. If no error occurs, + // the caller may return a content type string with the reader that indicates the type of the + // stream. + InputStream(apiVersion, acceptHeader string) (io.ReadCloser, string, error) +} + +// StorageMetadata is an optional interface that callers can implement to provide additional +// information about their Storage objects. +type StorageMetadata interface { + // ProducesMIMETypes returns a list of the MIME types the specified HTTP verb (GET, POST, DELETE, + // PATCH) can respond with. + ProducesMIMETypes(verb string) []string +} diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 6f6dc8e9c20..4dda0319b80 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -147,6 +147,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag patcher, isPatcher := storage.(rest.Patcher) _, isWatcher := storage.(rest.Watcher) _, isRedirector := storage.(rest.Redirector) + storageMeta, isMetadata := storage.(rest.StorageMetadata) + if !isMetadata { + storageMeta = defaultStorageMetadata{} + } var versionedDeleterObject runtime.Object switch { @@ -283,56 +287,70 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag // // test/integration/auth_test.go is currently the most comprehensive status code test + reqScope := RequestScope{ + ContextFunc: ctxFn, + Codec: mapping.Codec, + APIVersion: a.group.Version, + Resource: resource, + Kind: kind, + } for _, action := range actions { + reqScope.Namer = action.Namer m := monitorFilter(action.Verb, resource) switch action.Verb { case "GET": // Get a resource. - route := ws.GET(action.Path).To(GetResource(getter, ctxFn, action.Namer, mapping.Codec)). + route := ws.GET(action.Path).To(GetResource(getter, reqScope)). Filter(m). Doc("read the specified " + kind). Operation("read" + kind). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Writes(versionedObject) addParams(route, action.Params) ws.Route(route) case "LIST": // List all resources of a kind. - route := ws.GET(action.Path).To(ListResource(lister, ctxFn, action.Namer, mapping.Codec, a.group.Version, resource)). + route := ws.GET(action.Path).To(ListResource(lister, reqScope)). Filter(m). Doc("list objects of kind " + kind). Operation("list" + kind). + Produces("application/json"). Writes(versionedList) addParams(route, action.Params) ws.Route(route) case "PUT": // Update a resource. - route := ws.PUT(action.Path).To(UpdateResource(updater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)). + route := ws.PUT(action.Path).To(UpdateResource(updater, reqScope, a.group.Typer, admit)). Filter(m). Doc("replace the specified " + kind). Operation("replace" + kind). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Reads(versionedObject) addParams(route, action.Params) ws.Route(route) case "PATCH": // Partially update a resource - route := ws.PATCH(action.Path).To(PatchResource(patcher, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)). + route := ws.PATCH(action.Path).To(PatchResource(patcher, reqScope, a.group.Typer, admit)). Filter(m). Doc("partially update the specified " + kind). // TODO: toggle patch strategy by content type // Consumes("application/merge-patch+json", "application/json-patch+json"). Operation("patch" + kind). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Reads(versionedObject) addParams(route, action.Params) ws.Route(route) case "POST": // Create a resource. - route := ws.POST(action.Path).To(CreateResource(creater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)). + route := ws.POST(action.Path).To(CreateResource(creater, reqScope, a.group.Typer, admit)). Filter(m). Doc("create a " + kind). Operation("create" + kind). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Reads(versionedObject) addParams(route, action.Params) ws.Route(route) case "DELETE": // Delete a resource. - route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, ctxFn, action.Namer, mapping.Codec, resource, kind, admit)). + route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, reqScope, admit)). Filter(m). Doc("delete a " + kind). - Operation("delete" + kind) + Operation("delete" + kind). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...) if isGracefulDeleter { route.Reads(versionedDeleterObject) } @@ -343,6 +361,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Filter(m). Doc("watch a particular " + kind). Operation("watch" + kind). + Produces("application/json"). Writes(versionedObject) addParams(route, action.Params) ws.Route(route) @@ -351,6 +370,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Filter(m). Doc("watch a list of " + kind). Operation("watch" + kind + "list"). + Produces("application/json"). Writes(versionedList) addParams(route, action.Params) ws.Route(route) @@ -630,3 +650,13 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) { route.Param(param) } } + +// defaultStorageMetadata provides default answers to rest.StorageMetadata. +type defaultStorageMetadata struct{} + +// defaultStorageMetadata implements rest.StorageMetadata +var _ rest.StorageMetadata = defaultStorageMetadata{} + +func (defaultStorageMetadata) ProducesMIMETypes(verb string) []string { + return nil +} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f7ac60d8b80..a16b582fcc4 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -20,6 +20,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "path" @@ -196,6 +197,26 @@ func APIVersionHandler(versions ...string) restful.RouteFunction { } } +// write renders a returned runtime.Object to the response as a stream or an encoded object. +func write(statusCode int, apiVersion string, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, req *http.Request) { + if stream, ok := object.(rest.ResourceStreamer); ok { + out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept")) + if err != nil { + errorJSONFatal(err, codec, w) + return + } + defer out.Close() + if len(contentType) == 0 { + contentType = "application/octet-stream" + } + w.Header().Set("Content-Type", contentType) + w.WriteHeader(statusCode) + io.Copy(w, out) + return + } + writeJSON(statusCode, codec, object, w) +} + // writeJSON renders an object as JSON to the response. func writeJSON(statusCode int, codec runtime.Codec, object runtime.Object, w http.ResponseWriter) { output, err := codec.Encode(object) diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 13d439e35e6..b753ec462fa 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "net/http" "net/http/httptest" @@ -122,7 +123,8 @@ func init() { // defaultAPIServer exposes nested objects for testability. type defaultAPIServer struct { http.Handler - group *APIGroupVersion + group *APIGroupVersion + container *restful.Container } // uses the default settings @@ -169,7 +171,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission. ws := new(restful.WebService) InstallSupport(mux, ws) container.Add(ws) - return &defaultAPIServer{mux, group} + return &defaultAPIServer{mux, group, container} } type Simple struct { @@ -212,6 +214,8 @@ type SimpleRESTStorage struct { updated *Simple created *Simple + stream *SimpleStream + deleted string deleteOptions *api.DeleteOptions @@ -243,8 +247,34 @@ func (storage *SimpleRESTStorage) List(ctx api.Context, label labels.Selector, f return result, storage.errors["list"] } +type SimpleStream struct { + version string + accept string + contentType string + err error + + io.Reader + closed bool +} + +func (s *SimpleStream) Close() error { + s.closed = true + return nil +} + +func (s *SimpleStream) IsAnAPIObject() {} + +func (s *SimpleStream) InputStream(version, accept string) (io.ReadCloser, string, error) { + s.version = version + s.accept = accept + return s, s.contentType, s.err +} + func (storage *SimpleRESTStorage) Get(ctx api.Context, id string) (runtime.Object, error) { storage.checkContext(ctx) + if id == "binary" { + return storage.stream, storage.errors["get"] + } return api.Scheme.CopyOrDie(&storage.item), storage.errors["get"] } @@ -343,6 +373,15 @@ func (storage LegacyRESTStorage) Delete(ctx api.Context, id string) (runtime.Obj return storage.SimpleRESTStorage.Delete(ctx, id, nil) } +type MetadataRESTStorage struct { + *SimpleRESTStorage + types []string +} + +func (m *MetadataRESTStorage) ProducesMIMETypes(method string) []string { + return m.types +} + func extractBody(response *http.Response, object runtime.Object) (string, error) { defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) @@ -646,6 +685,26 @@ func TestSelfLinkSkipsEmptyName(t *testing.T) { } } +func TestMetadata(t *testing.T) { + simpleStorage := &MetadataRESTStorage{&SimpleRESTStorage{}, []string{"text/plain"}} + h := handle(map[string]rest.Storage{"simple": simpleStorage}) + ws := h.(*defaultAPIServer).container.RegisteredWebServices() + if len(ws) == 0 { + t.Fatal("no web services registered") + } + matches := map[string]int{} + for _, w := range ws { + for _, r := range w.Routes() { + s := strings.Join(r.Produces, ",") + i := matches[s] + matches[s] = i + 1 + } + } + if matches["text/plain,application/json"] == 0 || matches["application/json"] == 0 || matches["*/*"] == 0 || len(matches) != 3 { + t.Errorf("unexpected mime types: %v", matches) + } +} + func TestGet(t *testing.T) { storage := map[string]rest.Storage{} simpleStorage := SimpleRESTStorage{ @@ -685,6 +744,36 @@ func TestGet(t *testing.T) { } } +func TestGetBinary(t *testing.T) { + simpleStorage := SimpleRESTStorage{ + stream: &SimpleStream{ + contentType: "text/plain", + Reader: bytes.NewBufferString("response data"), + }, + } + stream := simpleStorage.stream + server := httptest.NewServer(handle(map[string]rest.Storage{"simple": &simpleStorage})) + defer server.Close() + + req, _ := http.NewRequest("GET", server.URL+"/api/version/simple/binary", nil) + req.Header.Add("Accept", "text/other, */*") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected response: %#v", resp) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !stream.closed || stream.version != "version" || stream.accept != "text/other, */*" || + resp.Header.Get("Content-Type") != stream.contentType || string(body) != "response data" { + t.Errorf("unexpected stream: %#v", stream) + } +} + func TestGetAlternateSelfLink(t *testing.T) { storage := map[string]rest.Storage{} simpleStorage := SimpleRESTStorage{ diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index c8b25c5f1f7..df363a6fba3 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -59,28 +59,38 @@ type ScopeNamer interface { GenerateListLink(req *restful.Request) (path, query string, err error) } +// RequestScope encapsulates common fields across all RESTful handler methods. +type RequestScope struct { + Namer ScopeNamer + ContextFunc + runtime.Codec + Resource string + Kind string + APIVersion string +} + // GetResource returns a function that handles retrieving a single resource from a rest.Storage object. -func GetResource(r rest.Getter, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec) restful.RouteFunction { +func GetResource(r rest.Getter, scope RequestScope) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter - namespace, name, err := namer.Name(req) + namespace, name, err := scope.Namer.Name(req) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - ctx := ctxFn(req) + ctx := scope.ContextFunc(req) ctx = api.WithNamespace(ctx, namespace) result, err := r.Get(ctx, name) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - if err := setSelfLink(result, req, namer); err != nil { - errorJSON(err, codec, w) + if err := setSelfLink(result, req, scope.Namer); err != nil { + errorJSON(err, scope.Codec, w) return } - writeJSON(http.StatusOK, codec, result, w) + write(http.StatusOK, scope.APIVersion, scope.Codec, result, w, req.Request) } } @@ -103,69 +113,69 @@ func parseSelectorQueryParams(query url.Values, version, apiResource string) (la } // ListResource returns a function that handles retrieving a list of resources from a rest.Storage object. -func ListResource(r rest.Lister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, version, apiResource string) restful.RouteFunction { +func ListResource(r rest.Lister, scope RequestScope) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter - namespace, err := namer.Namespace(req) + namespace, err := scope.Namer.Namespace(req) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - ctx := ctxFn(req) + ctx := scope.ContextFunc(req) ctx = api.WithNamespace(ctx, namespace) - label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), version, apiResource) + label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), scope.APIVersion, scope.Resource) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } result, err := r.List(ctx, label, field) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - if err := setListSelfLink(result, req, namer); err != nil { - errorJSON(err, codec, w) + if err := setListSelfLink(result, req, scope.Namer); err != nil { + errorJSON(err, scope.Codec, w) return } - writeJSON(http.StatusOK, codec, result, w) + write(http.StatusOK, scope.APIVersion, scope.Codec, result, w, req.Request) } } // CreateResource returns a function that will handle a resource creation. -func CreateResource(r rest.Creater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction { +func CreateResource(r rest.Creater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter // 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.Request.URL.Query().Get("timeout")) - namespace, err := namer.Namespace(req) + namespace, err := scope.Namer.Namespace(req) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - ctx := ctxFn(req) + ctx := scope.ContextFunc(req) ctx = api.WithNamespace(ctx, namespace) body, err := readBody(req.Request) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } obj := r.New() - if err := codec.DecodeInto(body, obj); err != nil { + if err := scope.Codec.DecodeInto(body, obj); err != nil { err = transformDecodeError(typer, err, obj, body) - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "CREATE")) + err = admit.Admit(admission.NewAttributesRecord(obj, namespace, scope.Resource, "CREATE")) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } @@ -177,73 +187,73 @@ func CreateResource(r rest.Creater, ctxFn ContextFunc, namer ScopeNamer, codec r return out, err }) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - if err := setSelfLink(result, req, namer); err != nil { - errorJSON(err, codec, w) + if err := setSelfLink(result, req, scope.Namer); err != nil { + errorJSON(err, scope.Codec, w) return } - writeJSON(http.StatusCreated, codec, result, w) + write(http.StatusCreated, scope.APIVersion, scope.Codec, result, w, req.Request) } } // PatchResource returns a function that will handle a resource patch // TODO: Eventually PatchResource should just use AtomicUpdate and this routine should be a bit cleaner -func PatchResource(r rest.Patcher, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction { +func PatchResource(r rest.Patcher, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter // 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.Request.URL.Query().Get("timeout")) - namespace, name, err := namer.Name(req) + namespace, name, err := scope.Namer.Name(req) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } obj := r.New() // PATCH requires same permission as UPDATE - err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE")) + err = admit.Admit(admission.NewAttributesRecord(obj, namespace, scope.Resource, "UPDATE")) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - ctx := ctxFn(req) + ctx := scope.ContextFunc(req) ctx = api.WithNamespace(ctx, namespace) original, err := r.Get(ctx, name) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - originalObjJs, err := codec.Encode(original) + originalObjJs, err := scope.Codec.Encode(original) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } patchJs, err := readBody(req.Request) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } patchedObjJs, err := jsonpatch.MergePatch(originalObjJs, patchJs) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - if err := codec.DecodeInto(patchedObjJs, obj); err != nil { - errorJSON(err, codec, w) + if err := scope.Codec.DecodeInto(patchedObjJs, obj); err != nil { + errorJSON(err, scope.Codec, w) return } - if err := checkName(obj, name, namespace, namer); err != nil { - errorJSON(err, codec, w) + if err := checkName(obj, name, namespace, scope.Namer); err != nil { + errorJSON(err, scope.Codec, w) return } @@ -253,56 +263,56 @@ func PatchResource(r rest.Patcher, ctxFn ContextFunc, namer ScopeNamer, codec ru return obj, err }) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - if err := setSelfLink(result, req, namer); err != nil { - errorJSON(err, codec, w) + if err := setSelfLink(result, req, scope.Namer); err != nil { + errorJSON(err, scope.Codec, w) return } - writeJSON(http.StatusOK, codec, result, w) + write(http.StatusOK, scope.APIVersion, scope.Codec, result, w, req.Request) } } // UpdateResource returns a function that will handle a resource update -func UpdateResource(r rest.Updater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction { +func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectTyper, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter // 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.Request.URL.Query().Get("timeout")) - namespace, name, err := namer.Name(req) + namespace, name, err := scope.Namer.Name(req) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - ctx := ctxFn(req) + ctx := scope.ContextFunc(req) ctx = api.WithNamespace(ctx, namespace) body, err := readBody(req.Request) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } obj := r.New() - if err := codec.DecodeInto(body, obj); err != nil { + if err := scope.Codec.DecodeInto(body, obj); err != nil { err = transformDecodeError(typer, err, obj, body) - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - if err := checkName(obj, name, namespace, namer); err != nil { - errorJSON(err, codec, w) + if err := checkName(obj, name, namespace, scope.Namer); err != nil { + errorJSON(err, scope.Codec, w) return } - err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE")) + err = admit.Admit(admission.NewAttributesRecord(obj, namespace, scope.Resource, "UPDATE")) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } @@ -313,12 +323,12 @@ func UpdateResource(r rest.Updater, ctxFn ContextFunc, namer ScopeNamer, codec r return obj, err }) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - if err := setSelfLink(result, req, namer); err != nil { - errorJSON(err, codec, w) + if err := setSelfLink(result, req, scope.Namer); err != nil { + errorJSON(err, scope.Codec, w) return } @@ -326,46 +336,44 @@ func UpdateResource(r rest.Updater, ctxFn ContextFunc, namer ScopeNamer, codec r if wasCreated { status = http.StatusCreated } - writeJSON(status, codec, result, w) + writeJSON(status, scope.Codec, result, w) } } // DeleteResource returns a function that will handle a resource deletion -func DeleteResource(r rest.GracefulDeleter, checkBody bool, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource, kind string, admit admission.Interface) restful.RouteFunction { +func DeleteResource(r rest.GracefulDeleter, checkBody bool, scope RequestScope, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter // 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.Request.URL.Query().Get("timeout")) - namespace, name, err := namer.Name(req) + namespace, name, err := scope.Namer.Name(req) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } - ctx := ctxFn(req) - if len(namespace) > 0 { - ctx = api.WithNamespace(ctx, namespace) - } + ctx := scope.ContextFunc(req) + ctx = api.WithNamespace(ctx, namespace) options := &api.DeleteOptions{} if checkBody { body, err := readBody(req.Request) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } if len(body) > 0 { - if err := codec.DecodeInto(body, options); err != nil { - errorJSON(err, codec, w) + if err := scope.Codec.DecodeInto(body, options); err != nil { + errorJSON(err, scope.Codec, w) return } } } - err = admit.Admit(admission.NewAttributesRecord(nil, namespace, resource, "DELETE")) + err = admit.Admit(admission.NewAttributesRecord(nil, namespace, scope.Resource, "DELETE")) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } @@ -373,7 +381,7 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, ctxFn ContextFunc, n return r.Delete(ctx, name, options) }) if err != nil { - errorJSON(err, codec, w) + errorJSON(err, scope.Codec, w) return } @@ -385,19 +393,19 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, ctxFn ContextFunc, n Code: http.StatusOK, Details: &api.StatusDetails{ ID: name, - Kind: kind, + Kind: scope.Kind, }, } } else { // when a non-status response is returned, set the self link if _, ok := result.(*api.Status); !ok { - if err := setSelfLink(result, req, namer); err != nil { - errorJSON(err, codec, w) + if err := setSelfLink(result, req, scope.Namer); err != nil { + errorJSON(err, scope.Codec, w) return } } } - writeJSON(http.StatusOK, codec, result, w) + write(http.StatusOK, scope.APIVersion, scope.Codec, result, w, req.Request) } } @@ -449,11 +457,8 @@ func transformDecodeError(typer runtime.ObjectTyper, baseErr error, into runtime func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error { // TODO: SelfLink generation should return a full URL? path, query, err := namer.GenerateLink(req, obj) - if err == errEmptyName { - return nil - } if err != nil { - return err + return nil } newURL := *req.Request.URL