diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index d51f0376df669..3813a61b41eb0 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -101,6 +101,10 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { j.Spec = api.PodSpec{} c.Fuzz(&j.Spec) }, + func(j *api.Binding, c fuzz.Continue) { + c.Fuzz(&j.ObjectMeta) + j.Target.Name = c.RandString() + }, func(j *api.ReplicationControllerSpec, c fuzz.Continue) { c.FuzzNoCustom(j) // fuzz self without calling this function again j.TemplateRef = nil // this is required for round trip diff --git a/pkg/api/types.go b/pkg/api/types.go index 2b404bb921ff5..0b761327ab23c 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -918,13 +918,14 @@ type NamespaceList struct { Items []Namespace `json:"items"` } -// Binding is written by a scheduler to cause a pod to be bound to a host. +// Binding ties one object to another - for example, a pod is bound to a node by a scheduler. type Binding struct { - TypeMeta `json:",inline"` + TypeMeta `json:",inline"` + // ObjectMeta describes the object that is being bound. ObjectMeta `json:"metadata,omitempty"` - PodID string `json:"podID"` - Host string `json:"host"` + // Target is the object to bind to. + Target ObjectReference `json:"target"` } // Status is a return value for calls that don't return other objects. diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 91feaddcf889b..46ec4a9e8c0c7 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -1324,6 +1324,24 @@ func init() { return nil }, + func(in *Binding, out *newer.Binding, s conversion.Scope) error { + if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil { + return err + } + out.Target = newer.ObjectReference{ + Name: in.Host, + } + out.Name = in.PodID + return nil + }, + func(in *newer.Binding, out *Binding, s conversion.Scope) error { + if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil { + return err + } + out.Host = in.Target.Name + out.PodID = in.Name + return nil + }, ) if err != nil { // If one of the conversion functions is malformed, detect it immediately. diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 1df1a36fbf227..effc0e3e664fc 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -1240,6 +1240,24 @@ func init() { return nil }, + func(in *Binding, out *newer.Binding, s conversion.Scope) error { + if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil { + return err + } + out.Target = newer.ObjectReference{ + Name: in.Host, + } + out.Name = in.PodID + return nil + }, + func(in *newer.Binding, out *Binding, s conversion.Scope) error { + if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil { + return err + } + out.Host = in.Target.Name + out.PodID = in.Name + return nil + }, ) if err != nil { // If one of the conversion functions is malformed, detect it immediately. diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 16f01226d2bae..4bfe077bea23c 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -940,16 +940,14 @@ type NamespaceList struct { Items []Namespace `json:"items" description:"items is the list of Namespace objects in the list"` } -// Binding is written by a scheduler to cause a pod to be bound to a node. Name is not -// required for Bindings. +// Binding ties one object to another - for example, a pod is bound to a node by a scheduler. type Binding struct { - TypeMeta `json:",inline"` + TypeMeta `json:",inline"` + // ObjectMeta describes the object that is being bound. ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - // PodID is a Pod name to be bound to a node. - PodID string `json:"podID" description:"name of the pod to be bound to a node"` - // Host is the name of a node to bind to. - Host string `json:"host" description:"name of the node to bind to"` + // Target is the object to bind to. + Target ObjectReference `json:"target" description:"an object to bind to"` } // Status is a return value for calls that don't return other objects. diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 72d25c2b8e24b..c2e15b95043f3 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -26,16 +26,15 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" - "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/emicklei/go-restful" ) type APIInstaller struct { - group *APIGroupVersion - prefix string // Path prefix where API resources are to be registered. - version string // The API version being installed. + group *APIGroupVersion + info *APIRequestInfoResolver + prefix string // Path prefix where API resources are to be registered. } // Struct capturing information about an action ("GET", "POST", "WATCH", PROXY", etc). @@ -58,15 +57,15 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) { // Initialize the custom handlers. watchHandler := (&WatchHandler{ - storage: a.group.storage, - codec: a.group.codec, - linker: a.group.linker, - info: a.group.info, + storage: a.group.Storage, + codec: a.group.Codec, + linker: a.group.Linker, + info: a.info, }) - redirectHandler := (&RedirectHandler{a.group.storage, a.group.codec, a.group.context, a.group.info}) - proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.storage, a.group.codec, a.group.context, a.group.info}) + redirectHandler := (&RedirectHandler{a.group.Storage, a.group.Codec, a.group.Context, a.info}) + proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info}) - for path, storage := range a.group.storage { + for path, storage := range a.group.Storage { if err := a.registerResourceHandlers(path, storage, ws, watchHandler, redirectHandler, proxyHandler); err != nil { errors = append(errors, err) } @@ -77,18 +76,17 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) { func (a *APIInstaller) newWebService() *restful.WebService { ws := new(restful.WebService) ws.Path(a.prefix) - ws.Doc("API at " + a.prefix + " version " + a.version) + ws.Doc("API at " + a.prefix + " version " + a.group.Version) // TODO: change to restful.MIME_JSON when we set content type in client ws.Consumes("*/*") ws.Produces(restful.MIME_JSON) - ws.ApiVersion(a.version) + ws.ApiVersion(a.group.Version) return ws } func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage, ws *restful.WebService, watchHandler, redirectHandler, proxyHandler http.Handler) error { - codec := a.group.codec - admit := a.group.admit - context := a.group.context + admit := a.group.Admit + context := a.group.Context var resource, subresource string switch parts := strings.Split(path, "/"); len(parts) { @@ -97,19 +95,16 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage case 1: resource = parts[0] default: + // TODO: support deeper paths return fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)") } object := storage.New() - // TODO: add scheme to APIInstaller rather than using api.Scheme - _, kind, err := api.Scheme.ObjectVersionAndKind(object) + _, kind, err := a.group.Typer.ObjectVersionAndKind(object) if err != nil { return err } - versionedPtr, err := api.Scheme.New(a.version, kind) - if conversion.IsNotRegisteredError(err) { - return nil - } + versionedPtr, err := a.group.Creater.New(a.group.Version, kind) if err != nil { return err } @@ -118,15 +113,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage var versionedList interface{} if lister, ok := storage.(RESTLister); ok { list := lister.NewList() - _, listKind, err := api.Scheme.ObjectVersionAndKind(list) - versionedListPtr, err := api.Scheme.New(a.version, listKind) + _, listKind, err := a.group.Typer.ObjectVersionAndKind(list) + versionedListPtr, err := a.group.Creater.New(a.group.Version, listKind) if err != nil { return err } versionedList = indirectArbitraryPointer(versionedListPtr) } - mapping, err := a.group.mapper.RESTMapping(kind, a.version) + mapping, err := a.group.Mapper.RESTMapping(kind, a.group.Version) if err != nil { return err } @@ -156,25 +151,27 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage // Get the list of actions for the given scope. if scope.Name() != meta.RESTScopeNameNamespace { - itemPath := resource + "/{name}" + resourcePath := resource + itemPath := resourcePath + "/{name}" if len(subresource) > 0 { itemPath = itemPath + "/" + subresource + resourcePath = itemPath } nameParams := append(params, nameParam) - namer := rootScopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath)} + namer := rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath)} // Handler for standard REST verbs (GET, PUT, POST and DELETE). - actions = appendIf(actions, action{"LIST", resource, params, namer}, isLister) - actions = appendIf(actions, action{"POST", resource, params, namer}, isCreater) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + resource, params, namer}, allowWatchList) + actions = appendIf(actions, action{"LIST", resourcePath, params, namer}, isLister) + actions = appendIf(actions, action{"POST", resourcePath, params, namer}, isCreater) + actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, params, namer}, allowWatchList) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater) actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, isWatcher) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher) + actions = appendIf(actions, action{"REDIRECT", "redirect/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector) } else { // v1beta3 format with namespace in path @@ -184,29 +181,31 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage namespacedPath := scope.ParamName() + "/{" + scope.ParamName() + "}/" + resource namespaceParams := []*restful.Parameter{namespaceParam} + resourcePath := namespacedPath itemPath := namespacedPath + "/{name}" if len(subresource) > 0 { itemPath = itemPath + "/" + subresource + resourcePath = itemPath } nameParams := append(namespaceParams, nameParam) - namer := scopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath), false} + namer := scopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath), false} - actions = appendIf(actions, action{"LIST", namespacedPath, namespaceParams, namer}, isLister) - actions = appendIf(actions, action{"POST", namespacedPath, namespaceParams, namer}, isCreater) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + namespacedPath, namespaceParams, namer}, allowWatchList) + actions = appendIf(actions, action{"LIST", resourcePath, namespaceParams, namer}, isLister) + actions = appendIf(actions, action{"POST", resourcePath, namespaceParams, namer}, isCreater) + actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, namespaceParams, namer}, allowWatchList) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater) actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, isWatcher) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher) + actions = appendIf(actions, action{"REDIRECT", "redirect/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector) // list across namespace. - namer = scopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath), true} + namer = scopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath), true} actions = appendIf(actions, action{"LIST", resource, params, namer}, isLister) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + resource, params, namer}, allowWatchList) + actions = appendIf(actions, action{"WATCHLIST", "watch/" + resource, params, namer}, allowWatchList) } else { // Handler for standard REST verbs (GET, PUT, POST and DELETE). @@ -214,24 +213,27 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage namespaceParam := ws.QueryParameter(scope.ParamName(), scope.ParamDescription()).DataType("string") namespaceParams := []*restful.Parameter{namespaceParam} - itemPath := resource + "/{name}" + basePath := resource + resourcePath := basePath + itemPath := resourcePath + "/{name}" if len(subresource) > 0 { itemPath = itemPath + "/" + subresource + resourcePath = itemPath } nameParams := append(namespaceParams, nameParam) - namer := legacyScopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath)} + namer := legacyScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath)} - actions = appendIf(actions, action{"LIST", resource, namespaceParams, namer}, isLister) - actions = appendIf(actions, action{"POST", resource, namespaceParams, namer}, isCreater) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + resource, namespaceParams, namer}, allowWatchList) + actions = appendIf(actions, action{"LIST", resourcePath, namespaceParams, namer}, isLister) + actions = appendIf(actions, action{"POST", resourcePath, namespaceParams, namer}, isCreater) + actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, namespaceParams, namer}, allowWatchList) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater) actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, isWatcher) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher) + actions = appendIf(actions, action{"REDIRECT", "redirect/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector) } } @@ -256,7 +258,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage m := monitorFilter(action.Verb, resource) switch action.Verb { case "GET": // Get a resource. - route := ws.GET(action.Path).To(GetResource(getter, ctxFn, action.Namer, codec)). + route := ws.GET(action.Path).To(GetResource(getter, ctxFn, action.Namer, mapping.Codec)). Filter(m). Doc("read the specified " + kind). Operation("read" + kind). @@ -264,7 +266,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage 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, codec, a.group.info)). + route := ws.GET(action.Path).To(ListResource(lister, ctxFn, action.Namer, mapping.Codec, a.group.Version, resource)). Filter(m). Doc("list objects of kind " + kind). Operation("list" + kind). @@ -272,7 +274,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage addParams(route, action.Params) ws.Route(route) case "PUT": // Update a resource. - route := ws.PUT(action.Path).To(UpdateResource(updater, ctxFn, action.Namer, codec, resource, admit)). + route := ws.PUT(action.Path).To(UpdateResource(updater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)). Filter(m). Doc("replace the specified " + kind). Operation("replace" + kind). @@ -280,7 +282,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage addParams(route, action.Params) ws.Route(route) case "POST": // Create a resource. - route := ws.POST(action.Path).To(CreateResource(creater, ctxFn, action.Namer, codec, resource, admit)). + route := ws.POST(action.Path).To(CreateResource(creater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)). Filter(m). Doc("create a " + kind). Operation("create" + kind). @@ -288,7 +290,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage addParams(route, action.Params) ws.Route(route) case "DELETE": // Delete a resource. - route := ws.DELETE(action.Path).To(DeleteResource(deleter, ctxFn, action.Namer, codec, resource, kind, admit)). + route := ws.DELETE(action.Path).To(DeleteResource(deleter, ctxFn, action.Namer, mapping.Codec, resource, kind, admit)). Filter(m). Doc("delete a " + kind). Operation("delete" + kind) diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 5dc1566736003..ee3f78a5bcd5b 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -29,7 +29,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -91,72 +90,38 @@ type Mux interface { HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) } -// defaultAPIServer exposes nested objects for testability. -type defaultAPIServer struct { - http.Handler - group *APIGroupVersion -} - -// Handle returns a Handler function that exposes the provided storage interfaces -// as RESTful resources at prefix, serialized by codec, and also includes the support -// http resources. -// Note: This method is used only in tests. -func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, linker runtime.SelfLinker, admissionControl admission.Interface, contextMapper api.RequestContextMapper, mapper meta.RESTMapper) http.Handler { - prefix := path.Join(root, version) - group := NewAPIGroupVersion(storage, codec, root, prefix, linker, admissionControl, contextMapper, mapper) - container := restful.NewContainer() - container.Router(restful.CurlyRouter{}) - mux := container.ServeMux - group.InstallREST(container, root, version) - ws := new(restful.WebService) - InstallSupport(mux, ws) - container.Add(ws) - return &defaultAPIServer{mux, group} -} - // APIGroupVersion is a helper for exposing RESTStorage objects as http.Handlers via go-restful // It handles URLs of the form: // /${storage_key}[/${object_name}] // Where 'storage_key' points to a RESTStorage object stored in storage. type APIGroupVersion struct { - storage map[string]RESTStorage - codec runtime.Codec - prefix string - linker runtime.SelfLinker - admit admission.Interface - context api.RequestContextMapper - mapper meta.RESTMapper - // TODO: put me into a cleaner interface - info *APIRequestInfoResolver -} + Storage map[string]RESTStorage -// NewAPIGroupVersion returns an object that will serve a set of REST resources and their -// associated operations. The provided codec controls serialization and deserialization. -// This is a helper method for registering multiple sets of REST handlers under different -// prefixes onto a server. -// TODO: add multitype codec serialization -func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, root, prefix string, linker runtime.SelfLinker, admissionControl admission.Interface, contextMapper api.RequestContextMapper, mapper meta.RESTMapper) *APIGroupVersion { - return &APIGroupVersion{ - storage: storage, - codec: codec, - prefix: prefix, - linker: linker, - admit: admissionControl, - context: contextMapper, - mapper: mapper, - info: &APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(root, "/")), latest.RESTMapper}, - } + Root string + Version string + + Mapper meta.RESTMapper + + Codec runtime.Codec + Typer runtime.ObjectTyper + Creater runtime.ObjectCreater + Linker runtime.SelfLinker + + Admit admission.Interface + Context api.RequestContextMapper } // InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container. // It is expected that the provided path root prefix will serve all operations. Root MUST NOT end // in a slash. A restful WebService is created for the group and version. -func (g *APIGroupVersion) InstallREST(container *restful.Container, root string, version string) error { - prefix := path.Join(root, version) +func (g *APIGroupVersion) InstallREST(container *restful.Container) error { + info := &APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(g.Root, "/")), g.Mapper} + + prefix := path.Join(g.Root, g.Version) installer := &APIInstaller{ - group: g, - prefix: prefix, - version: version, + group: g, + info: info, + prefix: prefix, } ws, registrationErrors := installer.Install() container.Add(ws) diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index b94c3753334e9..07d50e3c8b39c 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -41,6 +41,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny" + + "github.com/emicklei/go-restful" ) func convert(obj runtime.Object) (runtime.Object, error) { @@ -115,6 +117,59 @@ func init() { requestContextMapper = api.NewRequestContextMapper() } +// defaultAPIServer exposes nested objects for testability. +type defaultAPIServer struct { + http.Handler + group *APIGroupVersion +} + +// uses the default settings +func handle(storage map[string]RESTStorage) http.Handler { + return handleInternal(storage, admissionControl, mapper, selfLinker) +} + +// tests with a deny admission controller +func handleDeny(storage map[string]RESTStorage) http.Handler { + return handleInternal(storage, deny.NewAlwaysDeny(), mapper, selfLinker) +} + +// tests using the new namespace scope mechanism +func handleNamespaced(storage map[string]RESTStorage) http.Handler { + return handleInternal(storage, admissionControl, namespaceMapper, selfLinker) +} + +// tests using a custom self linker +func handleLinker(storage map[string]RESTStorage, selfLinker runtime.SelfLinker) http.Handler { + return handleInternal(storage, admissionControl, mapper, selfLinker) +} + +func handleInternal(storage map[string]RESTStorage, admissionControl admission.Interface, mapper meta.RESTMapper, selfLinker runtime.SelfLinker) http.Handler { + group := &APIGroupVersion{ + Storage: storage, + + Mapper: mapper, + + Root: "/api", + Version: testVersion, + + Creater: api.Scheme, + Typer: api.Scheme, + Codec: codec, + Linker: selfLinker, + + Admit: admissionControl, + Context: requestContextMapper, + } + container := restful.NewContainer() + container.Router(restful.CurlyRouter{}) + mux := container.ServeMux + group.InstallREST(container) + ws := new(restful.WebService) + InstallSupport(mux, ws) + container.Add(ws) + return &defaultAPIServer{mux, group} +} + type Simple struct { api.TypeMeta `json:",inline"` api.ObjectMeta `json:"metadata"` @@ -285,21 +340,21 @@ func TestNotFound(t *testing.T) { Status int } cases := map[string]T{ - "PATCH method": {"PATCH", "/prefix/version/foo", http.StatusMethodNotAllowed}, - "GET long prefix": {"GET", "/prefix/", http.StatusNotFound}, - "GET missing storage": {"GET", "/prefix/version/blah", http.StatusNotFound}, - "GET with extra segment": {"GET", "/prefix/version/foo/bar/baz", http.StatusNotFound}, - "POST with extra segment": {"POST", "/prefix/version/foo/bar", http.StatusMethodNotAllowed}, - "DELETE without extra segment": {"DELETE", "/prefix/version/foo", http.StatusMethodNotAllowed}, - "DELETE with extra segment": {"DELETE", "/prefix/version/foo/bar/baz", http.StatusNotFound}, - "PUT without extra segment": {"PUT", "/prefix/version/foo", http.StatusMethodNotAllowed}, - "PUT with extra segment": {"PUT", "/prefix/version/foo/bar/baz", http.StatusNotFound}, - "watch missing storage": {"GET", "/prefix/version/watch/", http.StatusNotFound}, - "watch with bad method": {"POST", "/prefix/version/watch/foo/bar", http.StatusMethodNotAllowed}, - } - handler := Handle(map[string]RESTStorage{ + "PATCH method": {"PATCH", "/api/version/foo", http.StatusMethodNotAllowed}, + "GET long prefix": {"GET", "/api/", http.StatusNotFound}, + "GET missing storage": {"GET", "/api/version/blah", http.StatusNotFound}, + "GET with extra segment": {"GET", "/api/version/foo/bar/baz", http.StatusNotFound}, + "POST with extra segment": {"POST", "/api/version/foo/bar", http.StatusMethodNotAllowed}, + "DELETE without extra segment": {"DELETE", "/api/version/foo", http.StatusMethodNotAllowed}, + "DELETE with extra segment": {"DELETE", "/api/version/foo/bar/baz", http.StatusNotFound}, + "PUT without extra segment": {"PUT", "/api/version/foo", http.StatusMethodNotAllowed}, + "PUT with extra segment": {"PUT", "/api/version/foo/bar/baz", http.StatusNotFound}, + "watch missing storage": {"GET", "/api/version/watch/", http.StatusNotFound}, + "watch with bad method": {"POST", "/api/version/watch/foo/bar", http.StatusMethodNotAllowed}, + } + handler := handle(map[string]RESTStorage{ "foo": &SimpleRESTStorage{}, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + }) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -339,19 +394,19 @@ func TestUnimplementedRESTStorage(t *testing.T) { ErrCode int } cases := map[string]T{ - "GET object": {"GET", "/prefix/version/foo/bar", http.StatusNotFound}, - "GET list": {"GET", "/prefix/version/foo", http.StatusNotFound}, - "POST list": {"POST", "/prefix/version/foo", http.StatusNotFound}, - "PUT object": {"PUT", "/prefix/version/foo/bar", http.StatusNotFound}, - "DELETE object": {"DELETE", "/prefix/version/foo/bar", http.StatusNotFound}, - "watch list": {"GET", "/prefix/version/watch/foo", http.StatusNotFound}, - "watch object": {"GET", "/prefix/version/watch/foo/bar", http.StatusNotFound}, - "proxy object": {"GET", "/prefix/version/proxy/foo/bar", http.StatusNotFound}, - "redirect object": {"GET", "/prefix/version/redirect/foo/bar", http.StatusNotFound}, - } - handler := Handle(map[string]RESTStorage{ + "GET object": {"GET", "/api/version/foo/bar", http.StatusNotFound}, + "GET list": {"GET", "/api/version/foo", http.StatusNotFound}, + "POST list": {"POST", "/api/version/foo", http.StatusNotFound}, + "PUT object": {"PUT", "/api/version/foo/bar", http.StatusNotFound}, + "DELETE object": {"DELETE", "/api/version/foo/bar", http.StatusNotFound}, + "watch list": {"GET", "/api/version/watch/foo", http.StatusNotFound}, + "watch object": {"GET", "/api/version/watch/foo/bar", http.StatusNotFound}, + "proxy object": {"GET", "/api/version/proxy/foo/bar", http.StatusNotFound}, + "redirect object": {"GET", "/api/version/redirect/foo/bar", http.StatusNotFound}, + } + handler := handle(map[string]RESTStorage{ "foo": UnimplementedRESTStorage{}, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + }) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -376,7 +431,7 @@ func TestUnimplementedRESTStorage(t *testing.T) { } func TestVersion(t *testing.T) { - handler := Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{}) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -409,14 +464,14 @@ func TestList(t *testing.T) { selfLink string legacy bool }{ - {"/prefix/version/simple", "", "/prefix/version/simple?namespace=", true}, - {"/prefix/version/simple?namespace=other", "other", "/prefix/version/simple?namespace=other", true}, + {"/api/version/simple", "", "/api/version/simple?namespace=", true}, + {"/api/version/simple?namespace=other", "other", "/api/version/simple?namespace=other", true}, // list items across all namespaces - {"/prefix/version/simple?namespace=", "", "/prefix/version/simple?namespace=", true}, - {"/prefix/version/namespaces/default/simple", "default", "/prefix/version/namespaces/default/simple", false}, - {"/prefix/version/namespaces/other/simple", "other", "/prefix/version/namespaces/other/simple", false}, + {"/api/version/simple?namespace=", "", "/api/version/simple?namespace=", true}, + {"/api/version/namespaces/default/simple", "default", "/api/version/namespaces/default/simple", false}, + {"/api/version/namespaces/other/simple", "other", "/api/version/namespaces/other/simple", false}, // list items across all namespaces - {"/prefix/version/simple", "", "/prefix/version/simple", false}, + {"/api/version/simple", "", "/api/version/simple", false}, } for i, testCase := range testCases { storage := map[string]RESTStorage{} @@ -429,9 +484,9 @@ func TestList(t *testing.T) { } var handler http.Handler if testCase.legacy { - handler = Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler = handleLinker(storage, selfLinker) } else { - handler = Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, namespaceMapper) + handler = handleInternal(storage, admissionControl, namespaceMapper, selfLinker) } server := httptest.NewServer(handler) defer server.Close() @@ -462,11 +517,11 @@ func TestErrorList(t *testing.T) { errors: map[string]error{"list": fmt.Errorf("test Error")}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple") + resp, err := http.Get(server.URL + "/api/version/simple") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -487,11 +542,11 @@ func TestNonEmptyList(t *testing.T) { }, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple") + resp, err := http.Get(server.URL + "/api/version/simple") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -515,10 +570,10 @@ func TestNonEmptyList(t *testing.T) { if listOut.Items[0].Other != simpleStorage.list[0].Other { t.Errorf("Unexpected data: %#v, %s", listOut.Items[0], string(body)) } - if listOut.SelfLink != "/prefix/version/simple?namespace=" { + if listOut.SelfLink != "/api/version/simple?namespace=" { t.Errorf("unexpected list self link: %#v", listOut) } - expectedSelfLink := "/prefix/version/simple/something?namespace=other" + expectedSelfLink := "/api/version/simple/something?namespace=other" if listOut.Items[0].ObjectMeta.SelfLink != expectedSelfLink { t.Errorf("Unexpected data: %#v, %s", listOut.Items[0].ObjectMeta.SelfLink, expectedSelfLink) } @@ -535,11 +590,11 @@ func TestSelfLinkSkipsEmptyName(t *testing.T) { }, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple") + resp, err := http.Get(server.URL + "/api/version/simple") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -562,7 +617,7 @@ func TestSelfLinkSkipsEmptyName(t *testing.T) { if listOut.Items[0].Other != simpleStorage.list[0].Other { t.Errorf("Unexpected data: %#v, %s", listOut.Items[0], string(body)) } - if listOut.SelfLink != "/prefix/version/simple?namespace=" { + if listOut.SelfLink != "/api/version/simple?namespace=" { t.Errorf("unexpected list self link: %#v", listOut) } expectedSelfLink := "" @@ -580,16 +635,16 @@ func TestGet(t *testing.T) { } selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/prefix/version/simple/id?namespace=default", + expectedSet: "/api/version/simple/id?namespace=default", name: "id", namespace: "default", } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(storage, selfLinker) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple/id") + resp, err := http.Get(server.URL + "/api/version/simple/id") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -619,16 +674,16 @@ func TestGetAlternateSelfLink(t *testing.T) { } selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/prefix/version/simple/id?namespace=test", + expectedSet: "/api/version/simple/id?namespace=test", name: "id", namespace: "test", } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, legacyNamespaceMapper) + handler := handleLinker(storage, selfLinker) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple/id?namespace=test") + resp, err := http.Get(server.URL + "/api/version/simple/id?namespace=test") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -657,16 +712,16 @@ func TestGetNamespaceSelfLink(t *testing.T) { } selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/prefix/version/namespaces/foo/simple/id", + expectedSet: "/api/version/namespaces/foo/simple/id", name: "id", namespace: "foo", } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, namespaceMapper) + handler := handleInternal(storage, admissionControl, namespaceMapper, selfLinker) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/namespaces/foo/simple/id") + resp, err := http.Get(server.URL + "/api/version/namespaces/foo/simple/id") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -691,11 +746,11 @@ func TestGetMissing(t *testing.T) { errors: map[string]error{"get": apierrs.NewNotFound("simple", "id")}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple/id") + resp, err := http.Get(server.URL + "/api/version/simple/id") if err != nil { t.Errorf("unexpected error: %v", err) } @@ -710,12 +765,12 @@ func TestDelete(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} - request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil) + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, nil) res, err := client.Do(request) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -733,12 +788,12 @@ func TestDeleteInvokesAdmissionControl(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny(), requestContextMapper, mapper) + handler := handleDeny(storage) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} - request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil) + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, nil) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -755,12 +810,12 @@ func TestDeleteMissing(t *testing.T) { errors: map[string]error{"delete": apierrs.NewNotFound("simple", ID)}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} - request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil) + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, nil) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -778,11 +833,11 @@ func TestUpdate(t *testing.T) { storage["simple"] = &simpleStorage selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/prefix/version/simple/" + ID + "?namespace=default", + expectedSet: "/api/version/simple/" + ID + "?namespace=default", name: ID, namespace: api.NamespaceDefault, } - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(storage, selfLinker) server := httptest.NewServer(handler) defer server.Close() @@ -800,7 +855,7 @@ func TestUpdate(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) _, err = client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -819,7 +874,7 @@ func TestUpdateInvokesAdmissionControl(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny(), requestContextMapper, mapper) + handler := handleDeny(storage) server := httptest.NewServer(handler) defer server.Close() @@ -837,7 +892,7 @@ func TestUpdateInvokesAdmissionControl(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -852,7 +907,7 @@ func TestUpdateRequiresMatchingName(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny(), requestContextMapper, mapper) + handler := handleDeny(storage) server := httptest.NewServer(handler) defer server.Close() @@ -866,7 +921,7 @@ func TestUpdateRequiresMatchingName(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -881,7 +936,7 @@ func TestUpdateAllowsMissingNamespace(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() @@ -898,7 +953,7 @@ func TestUpdateAllowsMissingNamespace(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -918,7 +973,7 @@ func TestUpdateAllowsMismatchedNamespaceOnError(t *testing.T) { t: t, err: fmt.Errorf("test error"), } - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(storage, selfLinker) server := httptest.NewServer(handler) defer server.Close() @@ -936,7 +991,7 @@ func TestUpdateAllowsMismatchedNamespaceOnError(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) _, err = client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -955,7 +1010,7 @@ func TestUpdatePreventsMismatchedNamespace(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() @@ -973,7 +1028,7 @@ func TestUpdatePreventsMismatchedNamespace(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -990,7 +1045,7 @@ func TestUpdateMissing(t *testing.T) { errors: map[string]error{"update": apierrs.NewNotFound("simple", ID)}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() @@ -1007,7 +1062,7 @@ func TestUpdateMissing(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -1018,20 +1073,20 @@ func TestUpdateMissing(t *testing.T) { } func TestCreateNotFound(t *testing.T) { - handler := Handle(map[string]RESTStorage{ + handler := handle(map[string]RESTStorage{ "simple": &SimpleRESTStorage{ // storage.Create can fail with not found error in theory. // See https://github.com/GoogleCloudPlatform/kubernetes/pull/486#discussion_r15037092. errors: map[string]error{"create": apierrs.NewNotFound("simple", "id")}, }, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + }) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} simple := &Simple{Other: "foo"} data, _ := codec.Encode(simple) - request, err := http.NewRequest("POST", server.URL+"/prefix/version/simple", bytes.NewBuffer(data)) + request, err := http.NewRequest("POST", server.URL+"/api/version/simple", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1046,6 +1101,54 @@ func TestCreateNotFound(t *testing.T) { } } +func TestCreateChecksDecode(t *testing.T) { + handler := handle(map[string]RESTStorage{"simple": &SimpleRESTStorage{}}) + server := httptest.NewServer(handler) + defer server.Close() + client := http.Client{} + + simple := &api.Pod{} + data, _ := codec.Encode(simple) + request, err := http.NewRequest("POST", server.URL+"/api/version/simple", bytes.NewBuffer(data)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusBadRequest { + t.Errorf("Unexpected response %#v", response) + } + if b, _ := ioutil.ReadAll(response.Body); !strings.Contains(string(b), "must be of type Simple") { + t.Errorf("unexpected response: %s", string(b)) + } +} + +func TestUpdateChecksDecode(t *testing.T) { + handler := handle(map[string]RESTStorage{"simple": &SimpleRESTStorage{}}) + server := httptest.NewServer(handler) + defer server.Close() + client := http.Client{} + + simple := &api.Pod{} + data, _ := codec.Encode(simple) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/bar", bytes.NewBuffer(data)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusBadRequest { + t.Errorf("Unexpected response %#v", response) + } + if b, _ := ioutil.ReadAll(response.Body); !strings.Contains(string(b), "must be of type Simple") { + t.Errorf("unexpected response: %s", string(b)) + } +} + func TestParseTimeout(t *testing.T) { if d := parseTimeout(""); d != 30*time.Second { t.Errorf("blank timeout produces %v", d) @@ -1089,11 +1192,9 @@ func TestCreate(t *testing.T) { t: t, name: "bar", namespace: "default", - expectedSet: "/prefix/version/foo/bar?namespace=default", + expectedSet: "/api/version/foo/bar?namespace=default", } - handler := Handle(map[string]RESTStorage{ - "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(map[string]RESTStorage{"foo": &storage}, selfLinker) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -1102,7 +1203,7 @@ func TestCreate(t *testing.T) { Other: "bar", } data, _ := codec.Encode(simple) - request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data)) + request, err := http.NewRequest("POST", server.URL+"/api/version/foo", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1147,11 +1248,9 @@ func TestCreateInNamespace(t *testing.T) { t: t, name: "bar", namespace: "other", - expectedSet: "/prefix/version/foo/bar?namespace=other", + expectedSet: "/api/version/foo/bar?namespace=other", } - handler := Handle(map[string]RESTStorage{ - "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(map[string]RESTStorage{"foo": &storage}, selfLinker) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -1160,7 +1259,7 @@ func TestCreateInNamespace(t *testing.T) { Other: "bar", } data, _ := codec.Encode(simple) - request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo?namespace=other", bytes.NewBuffer(data)) + request, err := http.NewRequest("POST", server.URL+"/api/version/foo?namespace=other", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1205,11 +1304,9 @@ func TestCreateInvokesAdmissionControl(t *testing.T) { t: t, name: "bar", namespace: "other", - expectedSet: "/prefix/version/foo/bar?namespace=other", + expectedSet: "/api/version/foo/bar?namespace=other", } - handler := Handle(map[string]RESTStorage{ - "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny(), requestContextMapper, mapper) + handler := handleInternal(map[string]RESTStorage{"foo": &storage}, deny.NewAlwaysDeny(), mapper, selfLinker) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -1218,7 +1315,7 @@ func TestCreateInvokesAdmissionControl(t *testing.T) { Other: "bar", } data, _ := codec.Encode(simple) - request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo?namespace=other", bytes.NewBuffer(data)) + request, err := http.NewRequest("POST", server.URL+"/api/version/foo?namespace=other", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1269,11 +1366,11 @@ func TestDelayReturnsError(t *testing.T) { return nil, apierrs.NewAlreadyExists("foo", "bar") }, } - handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": &storage}) server := httptest.NewServer(handler) defer server.Close() - status := expectApiStatus(t, "DELETE", fmt.Sprintf("%s/prefix/version/foo/bar", server.URL), nil, http.StatusConflict) + status := expectApiStatus(t, "DELETE", fmt.Sprintf("%s/api/version/foo/bar", server.URL), nil, http.StatusConflict) if status.Status != api.StatusFailure || status.Message == "" || status.Details == nil || status.Reason != api.StatusReasonAlreadyExists { t.Errorf("Unexpected status %#v", status) } @@ -1333,15 +1430,15 @@ func TestCreateTimeout(t *testing.T) { return obj, nil }, } - handler := Handle(map[string]RESTStorage{ + handler := handle(map[string]RESTStorage{ "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + }) server := httptest.NewServer(handler) defer server.Close() simple := &Simple{Other: "foo"} data, _ := codec.Encode(simple) - itemOut := expectApiStatus(t, "POST", server.URL+"/prefix/version/foo?timeout=4ms", data, apierrs.StatusServerTimeout) + itemOut := expectApiStatus(t, "POST", server.URL+"/api/version/foo?timeout=4ms", data, apierrs.StatusServerTimeout) if itemOut.Status != api.StatusFailure || itemOut.Reason != api.StatusReasonTimeout { t.Errorf("Unexpected status %#v", itemOut) } @@ -1367,7 +1464,7 @@ func TestCORSAllowedOrigins(t *testing.T) { } handler := CORS( - Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper), + handle(map[string]RESTStorage{}), allowedOriginRegexps, nil, nil, "true", ) server := httptest.NewServer(handler) diff --git a/pkg/apiserver/proxy_test.go b/pkg/apiserver/proxy_test.go index 90b56ec9b3380..797728ffc14ec 100644 --- a/pkg/apiserver/proxy_test.go +++ b/pkg/apiserver/proxy_test.go @@ -280,14 +280,10 @@ func TestProxy(t *testing.T) { expectedResourceNamespace: item.reqNamespace, } - namespaceHandler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper) + namespaceHandler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage}) namespaceServer := httptest.NewServer(namespaceHandler) defer namespaceServer.Close() - legacyNamespaceHandler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, legacyNamespaceMapper) + legacyNamespaceHandler := handle(map[string]RESTStorage{"foo": simpleStorage}) legacyNamespaceServer := httptest.NewServer(legacyNamespaceHandler) defer legacyNamespaceServer.Close() @@ -296,8 +292,8 @@ func TestProxy(t *testing.T) { server *httptest.Server proxyTestPattern string }{ - {namespaceServer, "/prefix/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path}, - {legacyNamespaceServer, "/prefix/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace}, + {namespaceServer, "/api/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path}, + {legacyNamespaceServer, "/api/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace}, } for _, serverPattern := range serverPatterns { @@ -344,14 +340,12 @@ func TestProxyUpgrade(t *testing.T) { expectedResourceNamespace: "myns", } - namespaceHandler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper) + namespaceHandler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(namespaceHandler) defer server.Close() - ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/prefix/version/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/") + ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/api/version/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/") if err != nil { t.Fatalf("websocket dial err: %s", err) } diff --git a/pkg/apiserver/redirect_test.go b/pkg/apiserver/redirect_test.go index cc3edaa28760b..c74c99994c88e 100644 --- a/pkg/apiserver/redirect_test.go +++ b/pkg/apiserver/redirect_test.go @@ -29,9 +29,7 @@ func TestRedirect(t *testing.T) { errors: map[string]error{}, expectedResourceNamespace: "default", } - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() @@ -54,7 +52,7 @@ func TestRedirect(t *testing.T) { for _, item := range table { simpleStorage.errors["resourceLocation"] = item.err simpleStorage.resourceLocation = item.id - resp, err := client.Get(server.URL + "/prefix/version/redirect/foo/" + item.id) + resp, err := client.Get(server.URL + "/api/version/redirect/foo/" + item.id) if resp == nil { t.Fatalf("Unexpected nil resp") } @@ -82,9 +80,7 @@ func TestRedirectWithNamespaces(t *testing.T) { errors: map[string]error{}, expectedResourceNamespace: "other", } - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper) + handler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() @@ -107,7 +103,7 @@ func TestRedirectWithNamespaces(t *testing.T) { for _, item := range table { simpleStorage.errors["resourceLocation"] = item.err simpleStorage.resourceLocation = item.id - resp, err := client.Get(server.URL + "/prefix/version/redirect/namespaces/other/foo/" + item.id) + resp, err := client.Get(server.URL + "/api/version/redirect/namespaces/other/foo/" + item.id) if resp == nil { t.Fatalf("Unexpected nil resp") } diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index dd295d0385914..d5230c6ee5871 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -17,6 +17,7 @@ limitations under the License. package apiserver import ( + "fmt" "net/http" "net/url" gpath "path" @@ -97,7 +98,7 @@ func parseSelectorQueryParams(query url.Values, version, apiResource string) (la } // ListResource returns a function that handles retrieving a list of resources from a RESTStorage object. -func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, requestInfoResolver *APIRequestInfoResolver) restful.RouteFunction { +func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, version, apiResource string) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter @@ -109,13 +110,7 @@ func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runti ctx := ctxFn(req) ctx = api.WithNamespace(ctx, namespace) - requestInfo, err := requestInfoResolver.GetAPIRequestInfo(req.Request) - if err != nil { - errorJSON(err, codec, w) - return - } - - label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), requestInfo.APIVersion, requestInfo.Resource) + label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), version, apiResource) if err != nil { errorJSON(err, codec, w) return @@ -135,7 +130,7 @@ func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runti } // CreateResource returns a function that will handle a resource creation. -func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource string, admit admission.Interface) restful.RouteFunction { +func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter @@ -158,6 +153,7 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec ru obj := r.New() if err := codec.DecodeInto(body, obj); err != nil { + err = transformDecodeError(typer, err, obj, body) errorJSON(err, codec, w) return } @@ -190,7 +186,7 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec ru } // UpdateResource returns a function that will handle a resource update -func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource string, admit admission.Interface) restful.RouteFunction { +func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter @@ -213,6 +209,7 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec ru obj := r.New() if err := codec.DecodeInto(body, obj); err != nil { + err = transformDecodeError(typer, err, obj, body) errorJSON(err, codec, w) return } @@ -346,6 +343,18 @@ func finishRequest(timeout time.Duration, fn resultFunc) (result runtime.Object, } } +// transformDecodeError adds additional information when a decode fails. +func transformDecodeError(typer runtime.ObjectTyper, baseErr error, into runtime.Object, body []byte) error { + _, kind, err := typer.ObjectVersionAndKind(into) + if err != nil { + return err + } + if version, dataKind, err := typer.DataVersionAndKind(body); err == nil && len(dataKind) > 0 { + return errors.NewBadRequest(fmt.Sprintf("%s in version %s cannot be handled as a %s: %v", dataKind, version, kind, baseErr)) + } + return errors.NewBadRequest(fmt.Sprintf("the object provided is unrecognized (must be of type %s): %v", kind, baseErr)) +} + // setSelfLink sets the self link of an object (or the child items in a list) to the base URL of the request // plus the path and query generated by the provided linkFunc func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error { diff --git a/pkg/apiserver/watch_test.go b/pkg/apiserver/watch_test.go index d7b44be63070d..95e4ef1c3636e 100644 --- a/pkg/apiserver/watch_test.go +++ b/pkg/apiserver/watch_test.go @@ -49,9 +49,7 @@ var watchTestTable = []struct { func TestWatchWebsocket(t *testing.T) { simpleStorage := &SimpleRESTStorage{} _ = ResourceWatcher(simpleStorage) // Give compile error if this doesn't work. - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() @@ -103,9 +101,7 @@ func TestWatchWebsocket(t *testing.T) { func TestWatchHTTP(t *testing.T) { simpleStorage := &SimpleRESTStorage{} - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -170,9 +166,7 @@ func TestWatchParamParsing(t *testing.T) { return label, value, nil }) simpleStorage := &SimpleRESTStorage{} - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/api", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() @@ -242,9 +236,7 @@ func TestWatchParamParsing(t *testing.T) { func TestWatchProtocolSelection(t *testing.T) { simpleStorage := &SimpleRESTStorage{} - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() defer server.CloseClientConnections() diff --git a/pkg/client/fake_pods.go b/pkg/client/fake_pods.go index 27e9e7b55512f..a5e5e97924dda 100644 --- a/pkg/client/fake_pods.go +++ b/pkg/client/fake_pods.go @@ -52,3 +52,8 @@ func (c *FakePods) Update(pod *api.Pod) (*api.Pod, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-pod", Value: pod.Name}) return &api.Pod{}, nil } + +func (c *FakePods) Bind(bind *api.Binding) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "bind-pod", Value: bind.Name}) + return nil +} diff --git a/pkg/client/pods.go b/pkg/client/pods.go index 7782b948a8627..30366081550e4 100644 --- a/pkg/client/pods.go +++ b/pkg/client/pods.go @@ -36,6 +36,8 @@ type PodInterface interface { Delete(name string) error Create(pod *api.Pod) (*api.Pod, error) Update(pod *api.Pod) (*api.Pod, error) + + Bind(binding *api.Binding) error } // pods implements PodsNamespacer interface @@ -92,3 +94,8 @@ func (c *pods) Update(pod *api.Pod) (result *api.Pod, err error) { err = c.r.Put().Namespace(c.ns).Resource("pods").Name(pod.Name).Body(pod).Do().Into(result) return } + +// Bind applies the provided binding to the named pod in the current namespace (binding.Namespace is ignored). +func (c *pods) Bind(binding *api.Binding) error { + return c.r.Post().Namespace(c.ns).Resource("pods").Name(binding.Name).SubResource("binding").Body(binding).Do().Error() +} diff --git a/pkg/client/request.go b/pkg/client/request.go index bfece8e3b504a..3519a76d684bc 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -105,6 +105,7 @@ type Request struct { namespaceSet bool resource string resourceName string + subresource string selector labels.Selector timeout time.Duration @@ -166,6 +167,21 @@ func (r *Request) Resource(resource string) *Request { return r } +// SubResource sets a sub-resource path which can be multiple segments segment after the resource +// name but before the suffix. +func (r *Request) SubResource(subresources ...string) *Request { + if r.err != nil { + return r + } + subresource := path.Join(subresources...) + if len(r.subresource) != 0 { + r.err = fmt.Errorf("subresource already set to %q, cannot change to %q", r.resource, subresource) + return r + } + r.subresource = subresource + return r +} + // Name sets the name of a resource to access (/[ns//]) func (r *Request) Name(resourceName string) *Request { if r.err != nil { @@ -360,8 +376,8 @@ func (r *Request) finalURL() string { p = path.Join(p, resource) } // Join trims trailing slashes, so preserve r.path's trailing slash for backwards compat if nothing was changed - if len(r.resourceName) != 0 || len(r.subpath) != 0 { - p = path.Join(p, r.resourceName, r.subpath) + if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 { + p = path.Join(p, r.resourceName, r.subresource, r.subpath) } finalURL := *r.baseURL diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index 4c1bfe642a85b..ed38611da856c 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -129,6 +129,16 @@ func TestRequestOrdersNamespaceInPath(t *testing.T) { } } +func TestRequestOrdersSubResource(t *testing.T) { + r := (&Request{ + baseURL: &url.URL{}, + path: "/test/", + }).Name("bar").Resource("baz").Namespace("foo").Suffix("test").SubResource("a", "b") + if s := r.finalURL(); s != "/test/namespaces/foo/baz/bar/a/b/test" { + t.Errorf("namespace should be in order in path: %s", s) + } +} + func TestRequestSetTwiceError(t *testing.T) { if (&Request{}).Name("bar").Name("baz").err == nil { t.Errorf("setting name twice should result in error") @@ -139,6 +149,9 @@ func TestRequestSetTwiceError(t *testing.T) { if (&Request{}).Resource("bar").Resource("baz").err == nil { t.Errorf("setting resource twice should result in error") } + if (&Request{}).SubResource("bar").SubResource("baz").err == nil { + t.Errorf("setting subresource twice should result in error") + } } func TestRequestParseSelectorParam(t *testing.T) { diff --git a/pkg/master/master.go b/pkg/master/master.go index 6dc999ccb6d01..f84fd3d4aa061 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -30,7 +30,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" @@ -55,7 +54,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/ui" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -406,9 +404,10 @@ func (m *Master) init(c *Config) { // TODO: Factor out the core API registration m.storage = map[string]apiserver.RESTStorage{ - "pods": podStorage, - "pods/status": podStatusStorage, - "bindings": bindingStorage, + "pods": podStorage, + "pods/status": podStatusStorage, + "pods/binding": bindingStorage, + "bindings": bindingStorage, "replicationControllers": controller.NewREST(registry, podRegistry), "services": service.NewREST(m.serviceRegistry, c.Cloud, m.nodeRegistry, m.portalNet, c.ClusterName), @@ -425,14 +424,14 @@ func (m *Master) init(c *Config) { } apiVersions := []string{"v1beta1", "v1beta2"} - if err := apiserver.NewAPIGroupVersion(m.api_v1beta1()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta1"); err != nil { + if err := m.api_v1beta1().InstallREST(m.handlerContainer); err != nil { glog.Fatalf("Unable to setup API v1beta1: %v", err) } - if err := apiserver.NewAPIGroupVersion(m.api_v1beta2()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta2"); err != nil { + if err := m.api_v1beta2().InstallREST(m.handlerContainer); err != nil { glog.Fatalf("Unable to setup API v1beta2: %v", err) } if c.EnableV1Beta3 { - if err := apiserver.NewAPIGroupVersion(m.api_v1beta3()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta3"); err != nil { + if err := m.api_v1beta3().InstallREST(m.handlerContainer); err != nil { glog.Fatalf("Unable to setup API v1beta3: %v", err) } apiVersions = []string{"v1beta1", "v1beta2", "v1beta3"} @@ -569,26 +568,49 @@ func (m *Master) getServersToValidate(c *Config) map[string]apiserver.Server { return serversToValidate } +func (m *Master) defaultAPIGroupVersion() *apiserver.APIGroupVersion { + return &apiserver.APIGroupVersion{ + Root: m.apiPrefix, + + Mapper: latest.RESTMapper, + + Creater: api.Scheme, + Typer: api.Scheme, + Linker: latest.SelfLinker, + + Admit: m.admissionControl, + Context: m.requestContextMapper, + } +} + // api_v1beta1 returns the resources and codec for API version v1beta1. -func (m *Master) api_v1beta1() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, api.RequestContextMapper, meta.RESTMapper) { +func (m *Master) api_v1beta1() *apiserver.APIGroupVersion { storage := make(map[string]apiserver.RESTStorage) for k, v := range m.storage { storage[k] = v } - return storage, v1beta1.Codec, "api", "/api/v1beta1", latest.SelfLinker, m.admissionControl, m.requestContextMapper, latest.RESTMapper + version := m.defaultAPIGroupVersion() + version.Storage = storage + version.Version = "v1beta1" + version.Codec = v1beta1.Codec + return version } // api_v1beta2 returns the resources and codec for API version v1beta2. -func (m *Master) api_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, api.RequestContextMapper, meta.RESTMapper) { +func (m *Master) api_v1beta2() *apiserver.APIGroupVersion { storage := make(map[string]apiserver.RESTStorage) for k, v := range m.storage { storage[k] = v } - return storage, v1beta2.Codec, "api", "/api/v1beta2", latest.SelfLinker, m.admissionControl, m.requestContextMapper, latest.RESTMapper + version := m.defaultAPIGroupVersion() + version.Storage = storage + version.Version = "v1beta2" + version.Codec = v1beta2.Codec + return version } // api_v1beta3 returns the resources and codec for API version v1beta3. -func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, api.RequestContextMapper, meta.RESTMapper) { +func (m *Master) api_v1beta3() *apiserver.APIGroupVersion { storage := make(map[string]apiserver.RESTStorage) for k, v := range m.storage { if k == "minions" { @@ -596,5 +618,9 @@ func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, } storage[strings.ToLower(k)] = v } - return storage, v1beta3.Codec, "api", "/api/v1beta3", latest.SelfLinker, m.admissionControl, m.requestContextMapper, latest.RESTMapper + version := m.defaultAPIGroupVersion() + version.Storage = storage + version.Version = "v1beta3" + version.Codec = v1beta3.Codec + return version } diff --git a/pkg/registry/binding/doc.go b/pkg/registry/binding/doc.go deleted file mode 100644 index 2fdfa915dba5f..0000000000000 --- a/pkg/registry/binding/doc.go +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -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 binding contains the middle layer logic for bindings. -// Bindings are objects containing instructions for how a pod ought to -// be bound to a host. This allows a registry object which supports this -// action (ApplyBinding) to be served through an apiserver. -package binding diff --git a/pkg/registry/binding/mock.go b/pkg/registry/binding/mock.go deleted file mode 100644 index f49429cd1668b..0000000000000 --- a/pkg/registry/binding/mock.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -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 binding - -import ( - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -// MockRegistry can be used for testing. -type MockRegistry struct { - OnApplyBinding func(binding *api.Binding) error -} - -func (mr MockRegistry) ApplyBinding(ctx api.Context, binding *api.Binding) error { - return mr.OnApplyBinding(binding) -} diff --git a/pkg/registry/binding/registry.go b/pkg/registry/binding/registry.go deleted file mode 100644 index 8a61559bc93c9..0000000000000 --- a/pkg/registry/binding/registry.go +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -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 binding - -import ( - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -// Registry contains the functions needed to support a BindingStorage. -type Registry interface { - // ApplyBinding should apply the binding. That is, it should actually - // assign or place pod binding.PodID on machine binding.Host. - ApplyBinding(ctx api.Context, binding *api.Binding) error -} diff --git a/pkg/registry/binding/rest.go b/pkg/registry/binding/rest.go deleted file mode 100644 index 42d7970870efb..0000000000000 --- a/pkg/registry/binding/rest.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -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 binding - -import ( - "fmt" - "net/http" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" -) - -// REST implements the RESTStorage interface for bindings. When bindings are written, it -// changes the location of the affected pods. This information is eventually reflected -// in the pod's CurrentState.Host field. -type REST struct { - registry Registry -} - -// NewREST creates a new REST backed by the given bindingRegistry. -func NewREST(bindingRegistry Registry) *REST { - return &REST{ - registry: bindingRegistry, - } -} - -// New returns a new binding object fit for having data unmarshalled into it. -func (*REST) New() runtime.Object { - return &api.Binding{} -} - -// Create attempts to make the assignment indicated by the binding it recieves. -func (b *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { - binding, ok := obj.(*api.Binding) - if !ok { - return nil, fmt.Errorf("incorrect type: %#v", obj) - } - if err := b.registry.ApplyBinding(ctx, binding); err != nil { - return nil, err - } - return &api.Status{Status: api.StatusSuccess, Code: http.StatusCreated}, nil -} diff --git a/pkg/registry/binding/rest_test.go b/pkg/registry/binding/rest_test.go deleted file mode 100644 index 5ddac234105cb..0000000000000 --- a/pkg/registry/binding/rest_test.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -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 binding - -import ( - "errors" - "net/http" - "reflect" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" -) - -func TestNewREST(t *testing.T) { - mockRegistry := MockRegistry{ - OnApplyBinding: func(b *api.Binding) error { return nil }, - } - b := NewREST(mockRegistry) - - binding := &api.Binding{ - PodID: "foo", - Host: "bar", - } - body, err := latest.Codec.Encode(binding) - if err != nil { - t.Fatalf("Unexpected encode error %v", err) - } - obj := b.New() - err = latest.Codec.DecodeInto(body, obj) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - if e, a := binding, obj; !reflect.DeepEqual(e, a) { - t.Errorf("Expected %#v, but got %#v", e, a) - } -} - -func TestRESTPost(t *testing.T) { - table := []struct { - b *api.Binding - err error - }{ - {b: &api.Binding{PodID: "foo", Host: "bar"}, err: errors.New("no host bar")}, - {b: &api.Binding{PodID: "baz", Host: "qux"}, err: nil}, - {b: &api.Binding{PodID: "dvorak", Host: "qwerty"}, err: nil}, - } - - for i, item := range table { - mockRegistry := MockRegistry{ - OnApplyBinding: func(b *api.Binding) error { - if !reflect.DeepEqual(item.b, b) { - t.Errorf("%v: expected %#v, but got %#v", i, item, b) - } - return item.err - }, - } - ctx := api.NewContext() - b := NewREST(mockRegistry) - result, err := b.Create(ctx, item.b) - if err != nil && item.err == nil { - t.Errorf("Unexpected error %v", err) - continue - } - if err == nil && item.err != nil { - t.Errorf("Unexpected error %v", err) - continue - } - var expect interface{} - if item.err == nil { - expect = &api.Status{Status: api.StatusSuccess, Code: http.StatusCreated} - } - if e, a := expect, result; !reflect.DeepEqual(e, a) { - t.Errorf("%v: expected %#v, got %#v", i, e, a) - } - } -} diff --git a/pkg/registry/pod/etcd/etcd.go b/pkg/registry/pod/etcd/etcd.go index 2ca4e738446f2..d0f1c43857e4c 100644 --- a/pkg/registry/pod/etcd/etcd.go +++ b/pkg/registry/pod/etcd/etcd.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" etcderr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "github.com/GoogleCloudPlatform/kubernetes/pkg/constraint" @@ -146,7 +147,14 @@ func (r *BindingREST) New() runtime.Object { // Create ensures a pod is bound to a specific host. func (r *BindingREST) Create(ctx api.Context, obj runtime.Object) (out runtime.Object, err error) { binding := obj.(*api.Binding) - err = r.assignPod(ctx, binding.PodID, binding.Host) + // TODO: move me to a binding strategy + if len(binding.Target.Kind) != 0 && (binding.Target.Kind != "Node" && binding.Target.Kind != "Minion") { + return nil, errors.NewInvalid("binding", binding.Name, errors.ValidationErrorList{errors.NewFieldInvalid("to.kind", binding.Target.Kind, "must be empty, 'Node', or 'Minion'")}) + } + if len(binding.Target.Name) == 0 { + return nil, errors.NewInvalid("binding", binding.Name, errors.ValidationErrorList{errors.NewFieldRequired("to.name", binding.Target.Name)}) + } + err = r.assignPod(ctx, binding.Name, binding.Target.Name) err = etcderr.InterpretCreateError(err, "binding", "") out = &api.Status{Status: api.StatusSuccess} return diff --git a/pkg/registry/pod/etcd/etcd_test.go b/pkg/registry/pod/etcd/etcd_test.go index 3b6781e5c3ee6..cd7d20c92057a 100644 --- a/pkg/registry/pod/etcd/etcd_test.go +++ b/pkg/registry/pod/etcd/etcd_test.go @@ -817,7 +817,10 @@ func TestEtcdCreate(t *testing.T) { } // Suddenly, a wild scheduler appears: - _, err = bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine", ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault}}) + _, err = bindingRegistry.Create(ctx, &api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine"}, + }) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -865,7 +868,10 @@ func TestEtcdCreateBindingNoPod(t *testing.T) { // - Create (apiserver) // - Schedule (scheduler) // - Delete (apiserver) - _, err := bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine", ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault}}) + _, err := bindingRegistry.Create(ctx, &api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine"}, + }) if err == nil { t.Fatalf("Expected not-found-error but got nothing") } @@ -935,7 +941,10 @@ func TestEtcdCreateWithContainersError(t *testing.T) { } // Suddenly, a wild scheduler appears: - _, err = bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine"}) + _, err = bindingRegistry.Create(ctx, &api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine"}, + }) if !errors.IsAlreadyExists(err) { t.Fatalf("Unexpected error returned: %#v", err) } @@ -973,7 +982,10 @@ func TestEtcdCreateWithContainersNotFound(t *testing.T) { } // Suddenly, a wild scheduler appears: - _, err = bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine"}) + _, err = bindingRegistry.Create(ctx, &api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine"}, + }) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1025,7 +1037,10 @@ func TestEtcdCreateWithExistingContainers(t *testing.T) { } // Suddenly, a wild scheduler appears: - _, err = bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine"}) + _, err = bindingRegistry.Create(ctx, &api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine"}, + }) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1055,6 +1070,70 @@ func TestEtcdCreateWithExistingContainers(t *testing.T) { } } +func TestEtcdCreateBinding(t *testing.T) { + registry, bindingRegistry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + fakeClient.TestIndex = true + + testCases := map[string]struct { + binding api.Binding + errOK func(error) bool + }{ + "noName": { + binding: api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{}, + }, + errOK: func(err error) bool { return errors.IsInvalid(err) }, + }, + "badKind": { + binding: api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine", Kind: "unknown"}, + }, + errOK: func(err error) bool { return errors.IsInvalid(err) }, + }, + "emptyKind": { + binding: api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine"}, + }, + errOK: func(err error) bool { return err == nil }, + }, + "kindNode": { + binding: api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine", Kind: "Node"}, + }, + errOK: func(err error) bool { return err == nil }, + }, + "kindMinion": { + binding: api.Binding{ + ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"}, + Target: api.ObjectReference{Name: "machine", Kind: "Minion"}, + }, + errOK: func(err error) bool { return err == nil }, + }, + } + for k, test := range testCases { + key, _ := registry.store.KeyFunc(ctx, "foo") + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: nil, + }, + E: tools.EtcdErrorNotFound, + } + fakeClient.Set("/registry/nodes/machine/boundpods", runtime.EncodeOrDie(latest.Codec, &api.BoundPods{}), 0) + if _, err := registry.Create(ctx, validNewPod()); err != nil { + t.Fatalf("%s: unexpected error: %v", k, err) + } + fakeClient.Set("/registry/nodes/machine/boundpods", runtime.EncodeOrDie(latest.Codec, &api.BoundPods{}), 0) + if _, err := bindingRegistry.Create(ctx, &test.binding); !test.errOK(err) { + t.Errorf("%s: unexpected error: %v", k, err) + } + } +} + func TestEtcdUpdateNotFound(t *testing.T) { registry, _, _, fakeClient, _ := newStorage(t) ctx := api.NewDefaultContext() diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index 4bfbf087e8c77..11735027b9e75 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -45,6 +45,11 @@ type ObjectTyper interface { ObjectVersionAndKind(Object) (version, kind string, err error) } +// ObjectCreater contains methods for instantiating an object by kind and version. +type ObjectCreater interface { + New(version, kind string) (out Object, err error) +} + // ResourceVersioner provides methods for setting and retrieving // the resource version from an API object. type ResourceVersioner interface { diff --git a/plugin/pkg/scheduler/factory/factory.go b/plugin/pkg/scheduler/factory/factory.go index 2611eaa30dbe6..c201a14797245 100644 --- a/plugin/pkg/scheduler/factory/factory.go +++ b/plugin/pkg/scheduler/factory/factory.go @@ -287,9 +287,11 @@ type binder struct { // Bind just does a POST binding RPC. func (b *binder) Bind(binding *api.Binding) error { - glog.V(2).Infof("Attempting to bind %v to %v", binding.PodID, binding.Host) + glog.V(2).Infof("Attempting to bind %v to %v", binding.Name, binding.Target.Name) ctx := api.WithNamespace(api.NewContext(), binding.Namespace) return b.Post().Namespace(api.NamespaceValue(ctx)).Resource("bindings").Body(binding).Do().Error() + // TODO: use Pods interface for binding once clusters are upgraded + // return b.Pods(binding.Namespace).Bind(binding) } type clock interface { diff --git a/plugin/pkg/scheduler/factory/factory_test.go b/plugin/pkg/scheduler/factory/factory_test.go index 9d3eff0a57190..70b50b44c567a 100644 --- a/plugin/pkg/scheduler/factory/factory_test.go +++ b/plugin/pkg/scheduler/factory/factory_test.go @@ -366,9 +366,11 @@ func TestBind(t *testing.T) { {binding: &api.Binding{ ObjectMeta: api.ObjectMeta{ Namespace: api.NamespaceDefault, + Name: "foo", + }, + Target: api.ObjectReference{ + Name: "foohost.kubernetes.mydomain.com", }, - PodID: "foo", - Host: "foohost.kubernetes.mydomain.com", }}, } diff --git a/plugin/pkg/scheduler/scheduler.go b/plugin/pkg/scheduler/scheduler.go index bf6d3e8b7df24..a23205f18a748 100644 --- a/plugin/pkg/scheduler/scheduler.go +++ b/plugin/pkg/scheduler/scheduler.go @@ -80,9 +80,11 @@ func (s *Scheduler) scheduleOne() { return } b := &api.Binding{ - ObjectMeta: api.ObjectMeta{Namespace: pod.Namespace}, - PodID: pod.Name, - Host: dest, + ObjectMeta: api.ObjectMeta{Namespace: pod.Namespace, Name: pod.Name}, + Target: api.ObjectReference{ + Kind: "Node", + Name: dest, + }, } if err := s.config.Binder.Bind(b); err != nil { glog.V(1).Infof("Failed to bind pod: %v", err) diff --git a/plugin/pkg/scheduler/scheduler_test.go b/plugin/pkg/scheduler/scheduler_test.go index d47f3a61a80b2..5374b929cf900 100644 --- a/plugin/pkg/scheduler/scheduler_test.go +++ b/plugin/pkg/scheduler/scheduler_test.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" "github.com/GoogleCloudPlatform/kubernetes/pkg/scheduler" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) type fakeBinder struct { @@ -63,7 +64,7 @@ func TestScheduler(t *testing.T) { { sendPod: podWithID("foo"), algo: mockScheduler{"machine1", nil}, - expectBind: &api.Binding{PodID: "foo", Host: "machine1"}, + expectBind: &api.Binding{ObjectMeta: api.ObjectMeta{Name: "foo"}, Target: api.ObjectReference{Kind: "Node", Name: "machine1"}}, eventReason: "scheduled", }, { sendPod: podWithID("foo"), @@ -74,7 +75,7 @@ func TestScheduler(t *testing.T) { }, { sendPod: podWithID("foo"), algo: mockScheduler{"machine1", nil}, - expectBind: &api.Binding{PodID: "foo", Host: "machine1"}, + expectBind: &api.Binding{ObjectMeta: api.ObjectMeta{Name: "foo"}, Target: api.ObjectReference{Kind: "Node", Name: "machine1"}}, injectBindError: errB, expectError: errB, expectErrorPod: podWithID("foo"), @@ -120,7 +121,7 @@ func TestScheduler(t *testing.T) { t.Errorf("%v: error: wanted %v, got %v", i, e, a) } if e, a := item.expectBind, gotBinding; !reflect.DeepEqual(e, a) { - t.Errorf("%v: error: wanted %v, got %v", i, e, a) + t.Errorf("%v: error: %s", i, util.ObjectDiff(e, a)) } <-called events.Stop()