diff --git a/cmd/integration/integration.go b/cmd/integration/integration.go index e1def7c8547..5591b5e7de3 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -19,6 +19,7 @@ limitations under the License. package main import ( + "fmt" "io/ioutil" "net" "net/http" @@ -278,18 +279,19 @@ func runReplicationControllerTest(c *client.Client) { } glog.Infof("Creating replication controllers") - if _, err := c.ReplicationControllers(api.NamespaceDefault).Create(&controller); err != nil { + updated, err := c.ReplicationControllers("test").Create(&controller) + if err != nil { glog.Fatalf("Unexpected error: %v", err) } glog.Infof("Done creating replication controllers") // Give the controllers some time to actually create the pods - if err := wait.Poll(time.Second, time.Second*30, client.ControllerHasDesiredReplicas(c, &controller)); err != nil { + if err := wait.Poll(time.Second, time.Second*30, client.ControllerHasDesiredReplicas(c, updated)); err != nil { glog.Fatalf("FAILED: pods never created %v", err) } // wait for minions to indicate they have info about the desired pods - pods, err := c.Pods(api.NamespaceDefault).List(labels.Set(controller.Spec.Selector).AsSelector()) + pods, err := c.Pods("test").List(labels.Set(updated.Spec.Selector).AsSelector()) if err != nil { glog.Fatalf("FAILED: unable to get pods to list: %v", err) } @@ -311,12 +313,15 @@ func runAPIVersionsTest(c *client.Client) { glog.Infof("Version test passed") } -func runSelfLinkTest(c *client.Client) { +func runSelfLinkTestOnNamespace(c *client.Client, namespace string) { var svc api.Service - err := c.Post().Resource("services").Body( + err := c.Post(). + NamespaceIfScoped(namespace, len(namespace) > 0). + Resource("services").Body( &api.Service{ ObjectMeta: api.ObjectMeta{ - Name: "selflinktest", + Name: "selflinktest", + Namespace: namespace, Labels: map[string]string{ "name": "selflinktest", }, @@ -335,18 +340,19 @@ func runSelfLinkTest(c *client.Client) { if err != nil { glog.Fatalf("Failed creating selflinktest service: %v", err) } - err = c.Get().AbsPath(svc.SelfLink).Do().Into(&svc) + // TODO: this is not namespace aware + err = c.Get().RequestURI(svc.SelfLink).Do().Into(&svc) if err != nil { glog.Fatalf("Failed listing service with supplied self link '%v': %v", svc.SelfLink, err) } var svcList api.ServiceList - err = c.Get().Resource("services").Do().Into(&svcList) + err = c.Get().NamespaceIfScoped(namespace, len(namespace) > 0).Resource("services").Do().Into(&svcList) if err != nil { glog.Fatalf("Failed listing services: %v", err) } - err = c.Get().AbsPath(svcList.SelfLink).Do().Into(&svcList) + err = c.Get().RequestURI(svcList.SelfLink).Do().Into(&svcList) if err != nil { glog.Fatalf("Failed listing services with supplied self link '%v': %v", svcList.SelfLink, err) } @@ -358,16 +364,16 @@ func runSelfLinkTest(c *client.Client) { continue } found = true - err = c.Get().AbsPath(item.SelfLink).Do().Into(&svc) + err = c.Get().RequestURI(item.SelfLink).Do().Into(&svc) if err != nil { glog.Fatalf("Failed listing service with supplied self link '%v': %v", item.SelfLink, err) } break } if !found { - glog.Fatalf("never found selflinktest service") + glog.Fatalf("never found selflinktest service in namespace %s", namespace) } - glog.Infof("Self link test passed") + glog.Infof("Self link test passed in namespace %s", namespace) // TODO: Should test PUT at some point, too. } @@ -518,7 +524,7 @@ func runMasterServiceTest(client *client.Client) { } func runServiceTest(client *client.Client) { - pod := api.Pod{ + pod := &api.Pod{ ObjectMeta: api.ObjectMeta{ Name: "foo", Labels: map[string]string{ @@ -543,14 +549,14 @@ func runServiceTest(client *client.Client) { PodIP: "1.2.3.4", }, } - _, err := client.Pods(api.NamespaceDefault).Create(&pod) + pod, err := client.Pods(api.NamespaceDefault).Create(pod) if err != nil { glog.Fatalf("Failed to create pod: %v, %v", pod, err) } if err := wait.Poll(time.Second, time.Second*20, podExists(client, pod.Namespace, pod.Name)); err != nil { glog.Fatalf("FAILED: pod never started running %v", err) } - svc1 := api.Service{ + svc1 := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "service1"}, Spec: api.ServiceSpec{ Selector: map[string]string{ @@ -561,15 +567,33 @@ func runServiceTest(client *client.Client) { SessionAffinity: "None", }, } - _, err = client.Services(api.NamespaceDefault).Create(&svc1) + svc1, err = client.Services(api.NamespaceDefault).Create(svc1) if err != nil { glog.Fatalf("Failed to create service: %v, %v", svc1, err) } + + // create an identical service in the default namespace + svc3 := &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "service1"}, + Spec: api.ServiceSpec{ + Selector: map[string]string{ + "name": "thisisalonglabel", + }, + Port: 8080, + Protocol: "TCP", + SessionAffinity: "None", + }, + } + svc3, err = client.Services("other").Create(svc3) + if err != nil { + glog.Fatalf("Failed to create service: %v, %v", svc3, err) + } + if err := wait.Poll(time.Second, time.Second*20, endpointsSet(client, svc1.Namespace, svc1.Name, 1)); err != nil { glog.Fatalf("FAILED: unexpected endpoints: %v", err) } // A second service with the same port. - svc2 := api.Service{ + svc2 := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "service2"}, Spec: api.ServiceSpec{ Selector: map[string]string{ @@ -580,13 +604,30 @@ func runServiceTest(client *client.Client) { SessionAffinity: "None", }, } - _, err = client.Services(api.NamespaceDefault).Create(&svc2) + svc2, err = client.Services(api.NamespaceDefault).Create(svc2) if err != nil { glog.Fatalf("Failed to create service: %v, %v", svc2, err) } if err := wait.Poll(time.Second, time.Second*20, endpointsSet(client, svc2.Namespace, svc2.Name, 1)); err != nil { glog.Fatalf("FAILED: unexpected endpoints: %v", err) } + + if ok, err := endpointsSet(client, svc3.Namespace, svc3.Name, 0)(); !ok || err != nil { + glog.Fatalf("FAILED: service in other namespace should have no endpoints: %v %v", ok, err) + } + + svcList, err := client.Services(api.NamespaceAll).List(labels.Everything()) + if err != nil { + glog.Fatalf("Failed to list services across namespaces: %v", err) + } + names := util.NewStringSet() + for _, svc := range svcList.Items { + names.Insert(fmt.Sprintf("%s/%s", svc.Namespace, svc.Name)) + } + if !names.HasAll("default/kubernetes", "default/kubernetes-ro", "default/service1", "default/service2", "other/service1") { + glog.Fatalf("Unexpected service list: %#v", names) + } + glog.Info("Service test passed.") } @@ -623,7 +664,10 @@ func main() { runServiceTest, runAPIVersionsTest, runMasterServiceTest, - runSelfLinkTest, + func(c *client.Client) { + runSelfLinkTestOnNamespace(c, "") + runSelfLinkTestOnNamespace(c, "other") + }, } var wg sync.WaitGroup wg.Add(len(testFuncs)) diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index feca4af310f..12e9c4ac89a 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -156,6 +156,11 @@ for version in "${kube_api_versions[@]}"; do howmanypods="$(kubectl get pods -o template -t "{{ len .items }}" "${kube_flags[@]}")" [ "$howmanypods" -eq 0 ] + # make calls in another namespace + kubectl create --namespace=other -f examples/guestbook/redis-master.json "${kube_flags[@]}" + kubectl get pod --namespace=other redis-master + kubectl delete pod --namespace=other redis-master + kube::log::status "Testing kubectl(${version}:services)" kubectl get services "${kube_flags[@]}" kubectl create -f examples/guestbook/frontend-service.json "${kube_flags[@]}" diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 03a771e136b..95dd50d5a57 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -42,6 +42,7 @@ type action struct { Verb string // Verb identifying the action ("GET", "POST", "WATCH", PROXY", etc). Path string // The path of the action Params []*restful.Parameter // List of parameters associated with the action. + Namer ScopeNamer } // errEmptyName is returned when API requests do not fill the name section of the path. @@ -87,7 +88,6 @@ func (a *APIInstaller) newWebService() *restful.WebService { func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage, ws *restful.WebService, watchHandler http.Handler, redirectHandler http.Handler, proxyHandler http.Handler) error { codec := a.group.codec admit := a.group.admit - linker := a.group.linker context := a.group.context resource := path @@ -149,13 +149,6 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage } var ctxFn ContextFunc - var namespaceFn ResourceNamespaceFunc - var nameFn ResourceNameFunc - var generateLinkFn linkFunc - var objNameFn ObjectNameFunc - linkFn := func(req *restful.Request, obj runtime.Object) error { - return setSelfLink(obj, req.Request, a.group.linker, generateLinkFn) - } ctxFn = func(req *restful.Request) api.Context { if ctx, ok := context.Get(req.Request); ok { return ctx @@ -168,146 +161,76 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage nameParam := ws.PathParameter("name", "name of the "+kind).DataType("string") params := []*restful.Parameter{} actions := []action{} + // Get the list of actions for the given scope. if scope.Name() != meta.RESTScopeNameNamespace { - objNameFn = func(obj runtime.Object) (namespace, name string, err error) { - name, err = linker.Name(obj) - if len(name) == 0 { - err = errEmptyName - } - return - } - - // Handler for standard REST verbs (GET, PUT, POST and DELETE). - actions = appendIf(actions, action{"LIST", path, params}, storageVerbs["RESTLister"]) - actions = appendIf(actions, action{"POST", path, params}, storageVerbs["RESTCreater"]) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params}, allowWatchList) - itemPath := path + "/{name}" nameParams := append(params, nameParam) - namespaceFn = func(req *restful.Request) (namespace string, err error) { - return - } - nameFn = func(req *restful.Request) (namespace, name string, err error) { - name = req.PathParameter("name") - if len(name) == 0 { - err = errEmptyName - } - return - } - generateLinkFn = func(namespace, name string) (path string, query string) { - path = strings.Replace(itemPath, "{name}", name, 1) - path = gpath.Join(a.prefix, path) - return - } + 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", path, params, namer}, storageVerbs["RESTLister"]) + actions = appendIf(actions, action{"POST", path, params, namer}, storageVerbs["RESTCreater"]) + actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params, namer}, allowWatchList) + + actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, storageVerbs["RESTGetter"]) + actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, storageVerbs["RESTUpdater"]) + actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, storageVerbs["RESTDeleter"]) + actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, storageVerbs["ResourceWatcher"]) + actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, storageVerbs["Redirector"]) + actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, storageVerbs["Redirector"]) + actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, storageVerbs["Redirector"]) - actions = appendIf(actions, action{"GET", itemPath, nameParams}, storageVerbs["RESTGetter"]) - actions = appendIf(actions, action{"PUT", itemPath, nameParams}, storageVerbs["RESTUpdater"]) - actions = appendIf(actions, action{"DELETE", itemPath, nameParams}, storageVerbs["RESTDeleter"]) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams}, storageVerbs["ResourceWatcher"]) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams}, storageVerbs["Redirector"]) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams}, storageVerbs["Redirector"]) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams}, storageVerbs["Redirector"]) } else { - objNameFn = func(obj runtime.Object) (namespace, name string, err error) { - if name, err = linker.Name(obj); err != nil { - return - } - namespace, err = linker.Namespace(obj) - return - } - // v1beta3 format with namespace in path if scope.ParamPath() { // Handler for standard REST verbs (GET, PUT, POST and DELETE). namespaceParam := ws.PathParameter(scope.ParamName(), scope.ParamDescription()).DataType("string") namespacedPath := scope.ParamName() + "/{" + scope.ParamName() + "}/" + path namespaceParams := []*restful.Parameter{namespaceParam} - namespaceFn = func(req *restful.Request) (namespace string, err error) { - namespace = req.PathParameter(scope.ParamName()) - if len(namespace) == 0 { - namespace = api.NamespaceDefault - } - return - } - - actions = appendIf(actions, action{"LIST", namespacedPath, namespaceParams}, storageVerbs["RESTLister"]) - actions = appendIf(actions, action{"POST", namespacedPath, namespaceParams}, storageVerbs["RESTCreater"]) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + namespacedPath, namespaceParams}, allowWatchList) itemPath := namespacedPath + "/{name}" nameParams := append(namespaceParams, nameParam) - nameFn = func(req *restful.Request) (namespace, name string, err error) { - namespace, _ = namespaceFn(req) - name = req.PathParameter("name") - if len(name) == 0 { - err = errEmptyName - } - return - } - generateLinkFn = func(namespace, name string) (path string, query string) { - path = strings.Replace(itemPath, "{name}", name, 1) - path = strings.Replace(path, "{"+scope.ParamName()+"}", namespace, 1) - path = gpath.Join(a.prefix, path) - return - } + namer := scopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath), false} - actions = appendIf(actions, action{"GET", itemPath, nameParams}, storageVerbs["RESTGetter"]) - actions = appendIf(actions, action{"PUT", itemPath, nameParams}, storageVerbs["RESTUpdater"]) - actions = appendIf(actions, action{"DELETE", itemPath, nameParams}, storageVerbs["RESTDeleter"]) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams}, storageVerbs["ResourceWatcher"]) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams}, storageVerbs["Redirector"]) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams}, storageVerbs["Redirector"]) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams}, storageVerbs["Redirector"]) + actions = appendIf(actions, action{"LIST", namespacedPath, namespaceParams, namer}, storageVerbs["RESTLister"]) + actions = appendIf(actions, action{"POST", namespacedPath, namespaceParams, namer}, storageVerbs["RESTCreater"]) + actions = appendIf(actions, action{"WATCHLIST", "/watch/" + namespacedPath, namespaceParams, namer}, allowWatchList) + + actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, storageVerbs["RESTGetter"]) + actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, storageVerbs["RESTUpdater"]) + actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, storageVerbs["RESTDeleter"]) + actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, storageVerbs["ResourceWatcher"]) + actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, storageVerbs["Redirector"]) + actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, storageVerbs["Redirector"]) + actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, storageVerbs["Redirector"]) // list across namespace. - actions = appendIf(actions, action{"LIST", path, params}, storageVerbs["RESTLister"]) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params}, allowWatchList) + namer = scopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath), true} + actions = appendIf(actions, action{"LIST", path, params, namer}, storageVerbs["RESTLister"]) + actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params, namer}, allowWatchList) + } else { // Handler for standard REST verbs (GET, PUT, POST and DELETE). // v1beta1/v1beta2 format where namespace was a query parameter namespaceParam := ws.QueryParameter(scope.ParamName(), scope.ParamDescription()).DataType("string") namespaceParams := []*restful.Parameter{namespaceParam} - namespaceFn = func(req *restful.Request) (namespace string, err error) { - namespace = req.QueryParameter(scope.ParamName()) - if len(namespace) == 0 { - namespace = api.NamespaceDefault - } - return - } - - actions = appendIf(actions, action{"LIST", path, namespaceParams}, storageVerbs["RESTLister"]) - actions = appendIf(actions, action{"POST", path, namespaceParams}, storageVerbs["RESTCreater"]) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, namespaceParams}, allowWatchList) itemPath := path + "/{name}" nameParams := append(namespaceParams, nameParam) - nameFn = func(req *restful.Request) (namespace, name string, err error) { - namespace, _ = namespaceFn(req) - name = req.PathParameter("name") - if len(name) == 0 { - err = errEmptyName - } - return - } - generateLinkFn = func(namespace, name string) (path string, query string) { - path = strings.Replace(itemPath, "{name}", name, -1) - path = gpath.Join(a.prefix, path) - if len(namespace) > 0 { - values := make(url.Values) - values.Set(scope.ParamName(), namespace) - query = values.Encode() - } - return - } + namer := legacyScopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath)} - actions = appendIf(actions, action{"GET", itemPath, nameParams}, storageVerbs["RESTGetter"]) - actions = appendIf(actions, action{"PUT", itemPath, nameParams}, storageVerbs["RESTUpdater"]) - actions = appendIf(actions, action{"DELETE", itemPath, nameParams}, storageVerbs["RESTDeleter"]) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams}, storageVerbs["ResourceWatcher"]) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams}, storageVerbs["Redirector"]) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams}, storageVerbs["Redirector"]) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams}, storageVerbs["Redirector"]) + actions = appendIf(actions, action{"LIST", path, namespaceParams, namer}, storageVerbs["RESTLister"]) + actions = appendIf(actions, action{"POST", path, namespaceParams, namer}, storageVerbs["RESTCreater"]) + actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, namespaceParams, namer}, allowWatchList) + + actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, storageVerbs["RESTGetter"]) + actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, storageVerbs["RESTUpdater"]) + actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, storageVerbs["RESTDeleter"]) + actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, storageVerbs["ResourceWatcher"]) + actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, storageVerbs["Redirector"]) + actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, storageVerbs["Redirector"]) + actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, storageVerbs["Redirector"]) } } @@ -332,7 +255,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, nameFn, linkFn, codec)). + route := ws.GET(action.Path).To(GetResource(getter, ctxFn, action.Namer, codec)). Filter(m). Doc("read the specified " + kind). Operation("read" + kind). @@ -340,7 +263,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, namespaceFn, linkFn, codec)). + route := ws.GET(action.Path).To(ListResource(lister, ctxFn, action.Namer, codec)). Filter(m). Doc("list objects of kind " + kind). Operation("list" + kind). @@ -348,7 +271,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, nameFn, objNameFn, linkFn, codec, resource, admit)). + route := ws.PUT(action.Path).To(UpdateResource(updater, ctxFn, action.Namer, codec, resource, admit)). Filter(m). Doc("update the specified " + kind). Operation("update" + kind). @@ -356,7 +279,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, namespaceFn, linkFn, codec, resource, admit)). + route := ws.POST(action.Path).To(CreateResource(creater, ctxFn, action.Namer, codec, resource, admit)). Filter(m). Doc("create a " + kind). Operation("create" + kind). @@ -364,7 +287,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, nameFn, linkFn, codec, resource, kind, admit)). + route := ws.DELETE(action.Path).To(DeleteResource(deleter, ctxFn, action.Namer, codec, resource, kind, admit)). Filter(m). Doc("delete a " + kind). Operation("delete" + kind) @@ -409,6 +332,224 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage return nil } +// rootScopeNaming reads only names from a request and ignores namespaces. It implements ScopeNamer +// for root scoped resources. +type rootScopeNaming struct { + scope meta.RESTScope + runtime.SelfLinker + itemPath string +} + +// rootScopeNaming implements ScopeNamer +var _ ScopeNamer = rootScopeNaming{} + +// Namespace returns an empty string because root scoped objects have no namespace. +func (n rootScopeNaming) Namespace(req *restful.Request) (namespace string, err error) { + return "", nil +} + +// Name returns the name from the path and an empty string for namespace, or an error if the +// name is empty. +func (n rootScopeNaming) Name(req *restful.Request) (namespace, name string, err error) { + name = req.PathParameter("name") + if len(name) == 0 { + return "", "", errEmptyName + } + return "", name, nil +} + +// GenerateLink returns the appropriate path and query to locate an object by its canonical path. +func (n rootScopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) { + _, name, err := n.ObjectName(obj) + if err != nil { + return "", "", err + } + if len(name) == 0 { + _, name, err = n.Name(req) + if err != nil { + return "", "", err + } + } + path = strings.Replace(n.itemPath, "{name}", name, 1) + return path, "", nil +} + +// GenerateListLink returns the appropriate path and query to locate a list by its canonical path. +func (n rootScopeNaming) GenerateListLink(req *restful.Request) (path, query string, err error) { + path = req.Request.URL.Path + return path, "", nil +} + +// ObjectName returns the name set on the object, or an error if the +// name cannot be returned. Namespace is empty +// TODO: distinguish between objects with name/namespace and without via a specific error. +func (n rootScopeNaming) ObjectName(obj runtime.Object) (namespace, name string, err error) { + name, err = n.SelfLinker.Name(obj) + if err != nil { + return "", "", err + } + if len(name) == 0 { + return "", "", errEmptyName + } + return "", name, nil +} + +// scopeNaming returns naming information from a request. It implements ScopeNamer for +// namespace scoped resources. +type scopeNaming struct { + scope meta.RESTScope + runtime.SelfLinker + itemPath string + allNamespaces bool +} + +// scopeNaming implements ScopeNamer +var _ ScopeNamer = scopeNaming{} + +// Namespace returns the namespace from the path or the default. +func (n scopeNaming) Namespace(req *restful.Request) (namespace string, err error) { + if n.allNamespaces { + return "", nil + } + namespace = req.PathParameter(n.scope.ParamName()) + if len(namespace) == 0 { + // a URL was constructed without the namespace, or this method was invoked + // on an object without a namespace path parameter. + return "", fmt.Errorf("no namespace parameter found on request") + } + return namespace, nil +} + +// Name returns the name from the path, the namespace (or default), or an error if the +// name is empty. +func (n scopeNaming) Name(req *restful.Request) (namespace, name string, err error) { + namespace, _ = n.Namespace(req) + name = req.PathParameter("name") + if len(name) == 0 { + return "", "", errEmptyName + } + return +} + +// GenerateLink returns the appropriate path and query to locate an object by its canonical path. +func (n scopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) { + namespace, name, err := n.ObjectName(obj) + if err != nil { + return "", "", err + } + if len(namespace) == 0 && len(name) == 0 { + namespace, name, err = n.Name(req) + if err != nil { + return "", "", err + } + } + path = strings.Replace(n.itemPath, "{name}", name, 1) + if !n.allNamespaces { + path = strings.Replace(path, "{"+n.scope.ParamName()+"}", namespace, 1) + } + return path, "", nil +} + +// GenerateListLink returns the appropriate path and query to locate a list by its canonical path. +func (n scopeNaming) GenerateListLink(req *restful.Request) (path, query string, err error) { + path = req.Request.URL.Path + return path, "", nil +} + +// ObjectName returns the name and namespace set on the object, or an error if the +// name cannot be returned. +// TODO: distinguish between objects with name/namespace and without via a specific error. +func (n scopeNaming) ObjectName(obj runtime.Object) (namespace, name string, err error) { + name, err = n.SelfLinker.Name(obj) + if err != nil { + return "", "", err + } + namespace, err = n.SelfLinker.Namespace(obj) + if err != nil { + return "", "", err + } + return namespace, name, err +} + +// legacyScopeNaming modifies a scopeNaming to read namespace from the query. It implements +// ScopeNamer for older query based namespace parameters. +type legacyScopeNaming struct { + scope meta.RESTScope + runtime.SelfLinker + itemPath string +} + +// legacyScopeNaming implements ScopeNamer +var _ ScopeNamer = legacyScopeNaming{} + +// Namespace returns the namespace from the query or the default. +func (n legacyScopeNaming) Namespace(req *restful.Request) (namespace string, err error) { + values, ok := req.Request.URL.Query()[n.scope.ParamName()] + if !ok || len(values) == 0 { + // legacy behavior + if req.Request.Method == "POST" || len(req.PathParameter("name")) > 0 { + return api.NamespaceDefault, nil + } + return api.NamespaceAll, nil + } + return values[0], nil +} + +// Name returns the name from the path, the namespace (or default), or an error if the +// name is empty. +func (n legacyScopeNaming) Name(req *restful.Request) (namespace, name string, err error) { + namespace, _ = n.Namespace(req) + name = req.PathParameter("name") + if len(name) == 0 { + return "", "", errEmptyName + } + return namespace, name, nil +} + +// GenerateLink returns the appropriate path and query to locate an object by its canonical path. +func (n legacyScopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) { + namespace, name, err := n.ObjectName(obj) + if err != nil { + return "", "", err + } + if len(name) == 0 { + return "", "", errEmptyName + } + path = strings.Replace(n.itemPath, "{name}", name, -1) + values := make(url.Values) + values.Set(n.scope.ParamName(), namespace) + query = values.Encode() + return path, query, nil +} + +// GenerateListLink returns the appropriate path and query to locate a list by its canonical path. +func (n legacyScopeNaming) GenerateListLink(req *restful.Request) (path, query string, err error) { + namespace, err := n.Namespace(req) + if err != nil { + return "", "", err + } + path = req.Request.URL.Path + values := make(url.Values) + values.Set(n.scope.ParamName(), namespace) + query = values.Encode() + return path, query, nil +} + +// ObjectName returns the name and namespace set on the object, or an error if the +// name cannot be returned. +// TODO: distinguish between objects with name/namespace and without via a specific error. +func (n legacyScopeNaming) ObjectName(obj runtime.Object) (namespace, name string, err error) { + name, err = n.SelfLinker.Name(obj) + if err != nil { + return "", "", err + } + namespace, err = n.SelfLinker.Namespace(obj) + if err != nil { + return "", "", err + } + return namespace, name, err +} + // This magic incantation returns *ptrToObject for an arbitrary pointer func indirectArbitraryPointer(ptrToObject interface{}) interface{} { return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface() diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index f4260c22514..b94c3753334 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -154,6 +154,9 @@ type SimpleRESTStorage struct { updated *Simple created *Simple + actualNamespace string + namespacePresent bool + // These are set when Watch is called fakeWatch *watch.FakeWatcher requestedLabelSelector labels.Selector @@ -172,6 +175,7 @@ type SimpleRESTStorage struct { } func (storage *SimpleRESTStorage) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) { + storage.checkContext(ctx) result := &SimpleList{ Items: storage.list, } @@ -179,10 +183,16 @@ func (storage *SimpleRESTStorage) List(ctx api.Context, label, field labels.Sele } func (storage *SimpleRESTStorage) Get(ctx api.Context, id string) (runtime.Object, error) { + storage.checkContext(ctx) return api.Scheme.CopyOrDie(&storage.item), storage.errors["get"] } +func (storage *SimpleRESTStorage) checkContext(ctx api.Context) { + storage.actualNamespace, storage.namespacePresent = api.NamespaceFrom(ctx) +} + func (storage *SimpleRESTStorage) Delete(ctx api.Context, id string) (runtime.Object, error) { + storage.checkContext(ctx) storage.deleted = id if err := storage.errors["delete"]; err != nil { return nil, err @@ -204,6 +214,7 @@ func (storage *SimpleRESTStorage) NewList() runtime.Object { } func (storage *SimpleRESTStorage) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { + storage.checkContext(ctx) storage.created = obj.(*Simple) if err := storage.errors["create"]; err != nil { return nil, err @@ -216,6 +227,7 @@ func (storage *SimpleRESTStorage) Create(ctx api.Context, obj runtime.Object) (r } func (storage *SimpleRESTStorage) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + storage.checkContext(ctx) storage.updated = obj.(*Simple) if err := storage.errors["update"]; err != nil { return nil, false, err @@ -229,6 +241,7 @@ func (storage *SimpleRESTStorage) Update(ctx api.Context, obj runtime.Object) (r // Implement ResourceWatcher. func (storage *SimpleRESTStorage) Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) { + storage.checkContext(ctx) storage.requestedLabelSelector = label storage.requestedFieldSelector = field storage.requestedResourceVersion = resourceVersion @@ -242,6 +255,7 @@ func (storage *SimpleRESTStorage) Watch(ctx api.Context, label, field labels.Sel // Implement Redirector. func (storage *SimpleRESTStorage) ResourceLocation(ctx api.Context, id string) (string, error) { + storage.checkContext(ctx) // validate that the namespace context on the request matches the expected input storage.requestedResourceNamespace = api.NamespaceValue(ctx) if storage.expectedResourceNamespace != storage.requestedResourceNamespace { @@ -388,29 +402,57 @@ func TestVersion(t *testing.T) { } } -func TestSimpleList(t *testing.T) { - storage := map[string]RESTStorage{} - simpleStorage := SimpleRESTStorage{} - storage["simple"] = &simpleStorage - selfLinker := &setTestSelfLinker{ - t: t, - namespace: "other", - expectedSet: "/prefix/version/simple?namespace=other", +func TestList(t *testing.T) { + testCases := []struct { + url string + namespace string + selfLink string + legacy bool + }{ + {"/prefix/version/simple", "", "/prefix/version/simple?namespace=", true}, + {"/prefix/version/simple?namespace=other", "other", "/prefix/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}, + // list items across all namespaces + {"/prefix/version/simple", "", "/prefix/version/simple", false}, } - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) - server := httptest.NewServer(handler) - defer server.Close() + for i, testCase := range testCases { + storage := map[string]RESTStorage{} + simpleStorage := SimpleRESTStorage{expectedResourceNamespace: testCase.namespace} + storage["simple"] = &simpleStorage + selfLinker := &setTestSelfLinker{ + t: t, + namespace: testCase.namespace, + expectedSet: testCase.selfLink, + } + var handler http.Handler + if testCase.legacy { + handler = Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + } else { + handler = Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, namespaceMapper) + } + server := httptest.NewServer(handler) + defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple") - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - if resp.StatusCode != http.StatusOK { - t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, http.StatusOK, resp) - } - if !selfLinker.called { - t.Errorf("Never set self link") + resp, err := http.Get(server.URL + testCase.url) + if err != nil { + t.Errorf("%d: unexpected error: %v", i, err) + continue + } + if resp.StatusCode != http.StatusOK { + t.Errorf("%d: unexpected status: %d, Expected: %d, %#v", i, resp.StatusCode, http.StatusOK, resp) + } + // TODO: future, restore get links + if !selfLinker.called { + t.Errorf("%d: never set self link", i) + } + if !simpleStorage.namespacePresent { + t.Errorf("%d: namespace not set", i) + } else if simpleStorage.actualNamespace != testCase.namespace { + t.Errorf("%d: unexpected resource namespace: %s", i, simpleStorage.actualNamespace) + } } } @@ -426,7 +468,7 @@ func TestErrorList(t *testing.T) { resp, err := http.Get(server.URL + "/prefix/version/simple") if err != nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } if resp.StatusCode != http.StatusInternalServerError { @@ -439,7 +481,54 @@ func TestNonEmptyList(t *testing.T) { simpleStorage := SimpleRESTStorage{ list: []Simple{ { - TypeMeta: api.TypeMeta{Kind: "Simple"}, + ObjectMeta: api.ObjectMeta{Name: "something", Namespace: "other"}, + Other: "foo", + }, + }, + } + storage["simple"] = &simpleStorage + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + server := httptest.NewServer(handler) + defer server.Close() + + resp, err := http.Get(server.URL + "/prefix/version/simple") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, http.StatusOK, resp) + body, _ := ioutil.ReadAll(resp.Body) + t.Logf("Data: %s", string(body)) + } + + var listOut SimpleList + body, err := extractBody(resp, &listOut) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(listOut.Items) != 1 { + t.Errorf("Unexpected response: %#v", listOut) + return + } + 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=" { + t.Errorf("unexpected list self link: %#v", listOut) + } + expectedSelfLink := "/prefix/version/simple/something?namespace=other" + if listOut.Items[0].ObjectMeta.SelfLink != expectedSelfLink { + t.Errorf("Unexpected data: %#v, %s", listOut.Items[0].ObjectMeta.SelfLink, expectedSelfLink) + } +} + +func TestSelfLinkSkipsEmptyName(t *testing.T) { + storage := map[string]RESTStorage{} + simpleStorage := SimpleRESTStorage{ + list: []Simple{ + { ObjectMeta: api.ObjectMeta{Namespace: "other"}, Other: "foo", }, @@ -452,7 +541,7 @@ func TestNonEmptyList(t *testing.T) { resp, err := http.Get(server.URL + "/prefix/version/simple") if err != nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } if resp.StatusCode != http.StatusOK { @@ -460,11 +549,10 @@ func TestNonEmptyList(t *testing.T) { body, _ := ioutil.ReadAll(resp.Body) t.Logf("Data: %s", string(body)) } - var listOut SimpleList body, err := extractBody(resp, &listOut) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } if len(listOut.Items) != 1 { @@ -474,7 +562,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)) } - expectedSelfLink := "/prefix/version/simple?namespace=other" + if listOut.SelfLink != "/prefix/version/simple?namespace=" { + t.Errorf("unexpected list self link: %#v", listOut) + } + expectedSelfLink := "" if listOut.Items[0].ObjectMeta.SelfLink != expectedSelfLink { t.Errorf("Unexpected data: %#v, %s", listOut.Items[0].ObjectMeta.SelfLink, expectedSelfLink) } @@ -817,6 +908,48 @@ func TestUpdateAllowsMissingNamespace(t *testing.T) { } } +// when the object name and namespace can't be retrieved, skip name checking +func TestUpdateAllowsMismatchedNamespaceOnError(t *testing.T) { + storage := map[string]RESTStorage{} + simpleStorage := SimpleRESTStorage{} + ID := "id" + storage["simple"] = &simpleStorage + selfLinker := &setTestSelfLinker{ + t: t, + err: fmt.Errorf("test error"), + } + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + server := httptest.NewServer(handler) + defer server.Close() + + item := &Simple{ + ObjectMeta: api.ObjectMeta{ + Name: ID, + Namespace: "other", // does not match request + }, + Other: "bar", + } + body, err := codec.Encode(item) + if err != nil { + // The following cases will fail, so die now + t.Fatalf("unexpected error: %v", err) + } + + client := http.Client{} + request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + _, err = client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if simpleStorage.updated == nil || simpleStorage.updated.Name != item.Name { + t.Errorf("Unexpected update value %#v, expected %#v.", simpleStorage.updated, item) + } + if selfLinker.called { + t.Errorf("self link ignored") + } +} + func TestUpdatePreventsMismatchedNamespace(t *testing.T) { storage := map[string]RESTStorage{} simpleStorage := SimpleRESTStorage{} @@ -931,20 +1064,79 @@ type setTestSelfLinker struct { name string namespace string called bool + err error } -func (s *setTestSelfLinker) Namespace(runtime.Object) (string, error) { return s.namespace, nil } -func (s *setTestSelfLinker) Name(runtime.Object) (string, error) { return s.name, nil } -func (*setTestSelfLinker) SelfLink(runtime.Object) (string, error) { return "", nil } +func (s *setTestSelfLinker) Namespace(runtime.Object) (string, error) { return s.namespace, s.err } +func (s *setTestSelfLinker) Name(runtime.Object) (string, error) { return s.name, s.err } +func (s *setTestSelfLinker) SelfLink(runtime.Object) (string, error) { return "", s.err } func (s *setTestSelfLinker) SetSelfLink(obj runtime.Object, selfLink string) error { if e, a := s.expectedSet, selfLink; e != a { s.t.Errorf("expected '%v', got '%v'", e, a) } s.called = true - return nil + return s.err } func TestCreate(t *testing.T) { + storage := SimpleRESTStorage{ + injectedFunction: func(obj runtime.Object) (runtime.Object, error) { + time.Sleep(5 * time.Millisecond) + return obj, nil + }, + } + selfLinker := &setTestSelfLinker{ + t: t, + name: "bar", + namespace: "default", + expectedSet: "/prefix/version/foo/bar?namespace=default", + } + handler := Handle(map[string]RESTStorage{ + "foo": &storage, + }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + server := httptest.NewServer(handler) + defer server.Close() + client := http.Client{} + + simple := &Simple{ + Other: "bar", + } + data, _ := codec.Encode(simple) + request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + wg := sync.WaitGroup{} + wg.Add(1) + var response *http.Response + go func() { + response, err = client.Do(request) + wg.Done() + }() + wg.Wait() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + var itemOut Simple + body, err := extractBody(response, &itemOut) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(&itemOut, simple) { + t.Errorf("Unexpected data: %#v, expected %#v (%s)", itemOut, simple, string(body)) + } + if response.StatusCode != http.StatusCreated { + t.Errorf("Unexpected status: %d, Expected: %d, %#v", response.StatusCode, http.StatusOK, response) + } + if !selfLinker.called { + t.Errorf("Never set self link") + } +} + +func TestCreateInNamespace(t *testing.T) { storage := SimpleRESTStorage{ injectedFunction: func(obj runtime.Object) (runtime.Object, error) { time.Sleep(5 * time.Millisecond) diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 7e052811b51..496e6a253ee 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -18,6 +18,7 @@ package apiserver import ( "net/http" + gpath "path" "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" @@ -27,66 +28,70 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/emicklei/go-restful" + "github.com/golang/glog" ) // ContextFunc returns a Context given a request - a context must be returned type ContextFunc func(req *restful.Request) api.Context -// ResourceNameFunc returns a name (and optional namespace) given a request - if no name is present -// an error must be returned. -type ResourceNameFunc func(req *restful.Request) (namespace, name string, err error) - -// ObjectNameFunc returns the name (and optional namespace) of an object -type ObjectNameFunc func(obj runtime.Object) (namespace, name string, err error) - -// ResourceNamespaceFunc returns the namespace associated with the given request - if no namespace -// is present an error must be returned. -type ResourceNamespaceFunc func(req *restful.Request) (namespace string, err error) - -// LinkResourceFunc updates the provided object with a SelfLink that is appropriate for the current -// request. -type LinkResourceFunc func(req *restful.Request, obj runtime.Object) error +// ScopeNamer handles accessing names from requests and objects +type ScopeNamer interface { + // Namespace returns the appropriate namespace value from the request (may be empty) or an + // error. + Namespace(req *restful.Request) (namespace string, err error) + // Name returns the name from the request, and an optional namespace value if this is a namespace + // scoped call. An error is returned if the name is not available. + Name(req *restful.Request) (namespace, name string, err error) + // ObjectName returns the namespace and name from an object if they exist, or an error if the object + // does not support names. + ObjectName(obj runtime.Object) (namespace, name string, err error) + // SetSelfLink sets the provided URL onto the object. The method should return nil if the object + // does not support selfLinks. + SetSelfLink(obj runtime.Object, url string) error + // GenerateLink creates a path and query for a given runtime object that represents the canonical path. + GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) + // GenerateLink creates a path and query for a list that represents the canonical path. + GenerateListLink(req *restful.Request) (path, query string, err error) +} // GetResource returns a function that handles retrieving a single resource from a RESTStorage object. -func GetResource(r RESTGetter, ctxFn ContextFunc, nameFn ResourceNameFunc, linkFn LinkResourceFunc, codec runtime.Codec) restful.RouteFunction { +func GetResource(r RESTGetter, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter - namespace, name, err := nameFn(req) + namespace, name, err := namer.Name(req) if err != nil { notFound(w, req.Request) return } ctx := ctxFn(req) - if len(namespace) > 0 { - ctx = api.WithNamespace(ctx, namespace) - } - item, err := r.Get(ctx, name) + ctx = api.WithNamespace(ctx, namespace) + + result, err := r.Get(ctx, name) if err != nil { errorJSON(err, codec, w) return } - if err := linkFn(req, item); err != nil { + if err := setSelfLink(result, req, namer); err != nil { errorJSON(err, codec, w) return } - writeJSON(http.StatusOK, codec, item, w) + writeJSON(http.StatusOK, codec, result, w) } } // ListResource returns a function that handles retrieving a list of resources from a RESTStorage object. -func ListResource(r RESTLister, ctxFn ContextFunc, namespaceFn ResourceNamespaceFunc, linkFn LinkResourceFunc, codec runtime.Codec) restful.RouteFunction { +func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter - namespace, err := namespaceFn(req) + namespace, err := namer.Namespace(req) if err != nil { notFound(w, req.Request) return } ctx := ctxFn(req) - if len(namespace) > 0 { - ctx = api.WithNamespace(ctx, namespace) - } + ctx = api.WithNamespace(ctx, namespace) + label, err := labels.ParseSelector(req.Request.URL.Query().Get("labels")) if err != nil { errorJSON(err, codec, w) @@ -98,36 +103,34 @@ func ListResource(r RESTLister, ctxFn ContextFunc, namespaceFn ResourceNamespace return } - item, err := r.List(ctx, label, field) + result, err := r.List(ctx, label, field) if err != nil { errorJSON(err, codec, w) return } - if err := linkFn(req, item); err != nil { + if err := setListSelfLink(result, req, namer); err != nil { errorJSON(err, codec, w) return } - writeJSON(http.StatusOK, codec, item, w) + writeJSON(http.StatusOK, codec, result, w) } } // CreateResource returns a function that will handle a resource creation. -func CreateResource(r RESTCreater, ctxFn ContextFunc, namespaceFn ResourceNamespaceFunc, linkFn LinkResourceFunc, codec runtime.Codec, resource string, admit admission.Interface) restful.RouteFunction { +func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource string, 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 := namespaceFn(req) + namespace, err := namer.Namespace(req) if err != nil { notFound(w, req.Request) return } ctx := ctxFn(req) - if len(namespace) > 0 { - ctx = api.WithNamespace(ctx, namespace) - } + ctx = api.WithNamespace(ctx, namespace) body, err := readBody(req.Request) if err != nil { @@ -159,7 +162,7 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namespaceFn ResourceNamesp return } - if err := linkFn(req, result); err != nil { + if err := setSelfLink(result, req, namer); err != nil { errorJSON(err, codec, w) return } @@ -169,22 +172,20 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namespaceFn ResourceNamesp } // UpdateResource returns a function that will handle a resource update -func UpdateResource(r RESTUpdater, ctxFn ContextFunc, nameFn ResourceNameFunc, objNameFunc ObjectNameFunc, linkFn LinkResourceFunc, codec runtime.Codec, resource string, admit admission.Interface) restful.RouteFunction { +func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource string, 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 := nameFn(req) + namespace, name, err := namer.Name(req) if err != nil { notFound(w, req.Request) return } ctx := ctxFn(req) - if len(namespace) > 0 { - ctx = api.WithNamespace(ctx, namespace) - } + ctx = api.WithNamespace(ctx, namespace) body, err := readBody(req.Request) if err != nil { @@ -198,20 +199,18 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, nameFn ResourceNameFunc, o return } - objNamespace, objName, err := objNameFunc(obj) - if err != nil { - errorJSON(err, codec, w) - return - } - if objName != name { - errorJSON(errors.NewBadRequest("the name of the object does not match the name on the URL"), codec, w) - return - } - if len(namespace) > 0 { - if len(objNamespace) > 0 && objNamespace != namespace { - errorJSON(errors.NewBadRequest("the namespace of the object does not match the namespace on the request"), codec, w) + // check the provided name against the request + if objNamespace, objName, err := namer.ObjectName(obj); err == nil { + if objName != name { + errorJSON(errors.NewBadRequest("the name of the object does not match the name on the URL"), codec, w) return } + if len(namespace) > 0 { + if len(objNamespace) > 0 && objNamespace != namespace { + errorJSON(errors.NewBadRequest("the namespace of the object does not match the namespace on the request"), codec, w) + return + } + } } err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE")) @@ -231,7 +230,7 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, nameFn ResourceNameFunc, o return } - if err := linkFn(req, result); err != nil { + if err := setSelfLink(result, req, namer); err != nil { errorJSON(err, codec, w) return } @@ -245,14 +244,14 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, nameFn ResourceNameFunc, o } // DeleteResource returns a function that will handle a resource deletion -func DeleteResource(r RESTDeleter, ctxFn ContextFunc, nameFn ResourceNameFunc, linkFn LinkResourceFunc, codec runtime.Codec, resource, kind string, admit admission.Interface) restful.RouteFunction { +func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource, kind string, 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 := nameFn(req) + namespace, name, err := namer.Name(req) if err != nil { notFound(w, req.Request) return @@ -290,7 +289,7 @@ func DeleteResource(r RESTDeleter, ctxFn ContextFunc, nameFn ResourceNameFunc, l } else { // when a non-status response is returned, set the self link if _, ok := result.(*api.Status); !ok { - if err := linkFn(req, result); err != nil { + if err := setSelfLink(result, req, namer); err != nil { errorJSON(err, codec, w) return } @@ -329,42 +328,58 @@ func finishRequest(timeout time.Duration, fn resultFunc) (result runtime.Object, } } -type linkFunc func(namespace, name string) (path string, query string) - // 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 *http.Request, linker runtime.SelfLinker, fn linkFunc) error { - namespace, err := linker.Namespace(obj) +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 } - name, err := linker.Name(obj) - if err != nil { - return err - } - path, query := fn(namespace, name) - newURL := *req.URL - newURL.Path = path + newURL := *req.Request.URL + // use only canonical paths + newURL.Path = gpath.Clean(path) newURL.RawQuery = query newURL.Fragment = "" - if err := linker.SetSelfLink(obj, newURL.String()); err != nil { - return err - } + return namer.SetSelfLink(obj, newURL.String()) +} + +// setListSelfLink sets the self link of a list to the base URL, then sets the self links +// on all child objects returned. +func setListSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error { if !runtime.IsListType(obj) { return nil } + // TODO: List SelfLink generation should return a full URL? + path, query, err := namer.GenerateListLink(req) + if err != nil { + return err + } + newURL := *req.Request.URL + newURL.Path = path + newURL.RawQuery = query + // use the path that got us here + newURL.Fragment = "" + if err := namer.SetSelfLink(obj, newURL.String()); err != nil { + glog.V(4).Infof("Unable to set self link on object: %v", err) + } + // Set self-link of objects in the list. items, err := runtime.ExtractList(obj) if err != nil { return err } for i := range items { - if err := setSelfLink(items[i], req, linker, fn); err != nil { + if err := setSelfLink(items[i], req, namer); err != nil { return err } } return runtime.SetList(obj, items) + } diff --git a/pkg/client/cache/listwatch_test.go b/pkg/client/cache/listwatch_test.go index f6395ad3065..c61412069f8 100644 --- a/pkg/client/cache/listwatch_test.go +++ b/pkg/client/cache/listwatch_test.go @@ -42,7 +42,7 @@ func buildResourcePath(prefix, namespace, resource string) string { base := path.Join("/api", testapi.Version(), prefix) if len(namespace) > 0 { if !(testapi.Version() == "v1beta1" || testapi.Version() == "v1beta2") { - base = path.Join(base, "ns", namespace) + base = path.Join(base, "namespaces", namespace) } } return path.Join(base, resource) @@ -58,10 +58,8 @@ func buildQueryValues(namespace string, query url.Values) url.Values { } } } - if len(namespace) > 0 { - if testapi.Version() == "v1beta1" || testapi.Version() == "v1beta2" { - v.Set("namespace", namespace) - } + if testapi.Version() == "v1beta1" || testapi.Version() == "v1beta2" { + v.Set("namespace", namespace) } return v } diff --git a/pkg/client/events.go b/pkg/client/events.go index 9f4a9665454..04c7a5ea03c 100644 --- a/pkg/client/events.go +++ b/pkg/client/events.go @@ -66,7 +66,7 @@ func (e *events) Create(event *api.Event) (*api.Event, error) { } result := &api.Event{} err := e.client.Post(). - Namespace(event.Namespace). + NamespaceIfScoped(event.Namespace, len(event.Namespace) > 0). Resource("events"). Body(event). Do(). @@ -85,7 +85,7 @@ func (e *events) Update(event *api.Event) (*api.Event, error) { } result := &api.Event{} err := e.client.Put(). - Namespace(event.Namespace). + NamespaceIfScoped(event.Namespace, len(event.Namespace) > 0). Resource("events"). Name(event.Name). Body(event). @@ -98,7 +98,7 @@ func (e *events) Update(event *api.Event) (*api.Event, error) { func (e *events) List(label, field labels.Selector) (*api.EventList, error) { result := &api.EventList{} err := e.client.Get(). - Namespace(e.namespace). + NamespaceIfScoped(e.namespace, len(e.namespace) > 0). Resource("events"). SelectorParam("labels", label). SelectorParam("fields", field). @@ -115,7 +115,7 @@ func (e *events) Get(name string) (*api.Event, error) { result := &api.Event{} err := e.client.Get(). - Namespace(e.namespace). + NamespaceIfScoped(e.namespace, len(e.namespace) > 0). Resource("events"). Name(name). Do(). @@ -127,7 +127,7 @@ func (e *events) Get(name string) (*api.Event, error) { func (e *events) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { return e.client.Get(). Prefix("watch"). - Namespace(e.namespace). + NamespaceIfScoped(e.namespace, len(e.namespace) > 0). Resource("events"). Param("resourceVersion", resourceVersion). SelectorParam("labels", label). diff --git a/pkg/client/events_test.go b/pkg/client/events_test.go index 171131a2620..41920a22656 100644 --- a/pkg/client/events_test.go +++ b/pkg/client/events_test.go @@ -62,7 +62,9 @@ func TestEventCreate(t *testing.T) { } timeStamp := util.Now() event := &api.Event{ - //namespace: namespace{"default"}, + ObjectMeta: api.ObjectMeta{ + Namespace: api.NamespaceDefault, + }, InvolvedObject: *objReference, FirstTimestamp: timeStamp, LastTimestamp: timeStamp, @@ -80,7 +82,7 @@ func TestEventCreate(t *testing.T) { response, err := c.Setup().Events("").Create(event) if err != nil { - t.Errorf("%#v should be nil.", err) + t.Fatalf("%v should be nil.", err) } if e, a := *objReference, response.InvolvedObject; !reflect.DeepEqual(e, a) { @@ -99,6 +101,9 @@ func TestEventGet(t *testing.T) { } timeStamp := util.Now() event := &api.Event{ + ObjectMeta: api.ObjectMeta{ + Namespace: "other", + }, InvolvedObject: *objReference, FirstTimestamp: timeStamp, LastTimestamp: timeStamp, @@ -116,7 +121,7 @@ func TestEventGet(t *testing.T) { response, err := c.Setup().Events("").Get("1") if err != nil { - t.Errorf("%#v should be nil.", err) + t.Fatalf("%v should be nil.", err) } if e, r := event.InvolvedObject, response.InvolvedObject; !reflect.DeepEqual(e, r) { diff --git a/pkg/client/request.go b/pkg/client/request.go index 3ea84e11f15..051f94cb06e 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -90,7 +90,7 @@ type Request struct { // generic components accessible via method setters path string subpath string - params map[string]string + params url.Values // structural elements of the request that are part of the Kubernetes API conventions namespace string @@ -181,6 +181,14 @@ func (r *Request) Namespace(namespace string) *Request { return r } +// NamespaceIfScoped is a convenience function to set a namespace if scoped is true +func (r *Request) NamespaceIfScoped(namespace string, scoped bool) *Request { + if scoped { + return r.Namespace(namespace) + } + return r +} + // AbsPath overwrites an existing path with the segments provided. Trailing slashes are preserved // when a single segment is passed. func (r *Request) AbsPath(segments ...string) *Request { @@ -196,6 +204,29 @@ func (r *Request) AbsPath(segments ...string) *Request { return r } +// RequestURI overwrites existing path and parameters with the value of the provided server relative +// URI. Some parameters (those in specialParameters) cannot be overwritten. +func (r *Request) RequestURI(uri string) *Request { + if r.err != nil { + return r + } + locator, err := url.Parse(uri) + if err != nil { + r.err = err + return r + } + r.path = locator.Path + if len(locator.Query()) > 0 { + if r.params == nil { + r.params = make(url.Values) + } + for k, v := range locator.Query() { + r.params[k] = v + } + } + return r +} + // ParseSelectorParam parses the given string as a resource label selector. // This is a convenience function so you don't have to first check that it's a // validly formatted selector. @@ -244,9 +275,9 @@ func (r *Request) setParam(paramName, value string) *Request { return r } if r.params == nil { - r.params = make(map[string]string) + r.params = make(url.Values) } - r.params[paramName] = value + r.params[paramName] = []string{value} return r } @@ -317,16 +348,16 @@ func (r *Request) finalURL() string { query := url.Values{} for key, value := range r.params { - query.Add(key, value) + query[key] = value } - if r.namespaceSet && r.namespaceInQuery && len(r.namespace) > 0 { - query.Add("namespace", r.namespace) + if r.namespaceSet && r.namespaceInQuery { + query.Set("namespace", r.namespace) } // timeout is handled specially here. if r.timeout != 0 { - query.Add("timeout", r.timeout.String()) + query.Set("timeout", r.timeout.String()) } finalURL.RawQuery = query.Encode() return finalURL.String() @@ -427,6 +458,14 @@ func (r *Request) Do() Result { return Result{err: &RequestConstructionError{r.err}} } + // TODO: added to catch programmer errors (invoking operations with an object with an empty namespace) + if (r.verb == "GET" || r.verb == "PUT" || r.verb == "DELETE") && r.namespaceSet && len(r.resourceName) > 0 && len(r.namespace) == 0 { + return Result{err: &RequestConstructionError{fmt.Errorf("an empty namespace may not be set when a resource name is provided")}} + } + if (r.verb == "POST") && r.namespaceSet && len(r.namespace) == 0 { + return Result{err: &RequestConstructionError{fmt.Errorf("an empty namespace may not be set during creation")}} + } + req, err := http.NewRequest(r.verb, r.finalURL(), r.body) if err != nil { return Result{err: &RequestConstructionError{err}} diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index f4bdafe9538..f23cbef97a7 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -148,7 +148,19 @@ func TestRequestParseSelectorParam(t *testing.T) { func TestRequestParam(t *testing.T) { r := (&Request{}).Param("foo", "a") - if !api.Semantic.DeepDerivative(r.params, map[string]string{"foo": "a"}) { + if !api.Semantic.DeepDerivative(r.params, url.Values{"foo": []string{"a"}}) { + t.Errorf("should have set a param: %#v", r) + } +} + +func TestRequestURI(t *testing.T) { + r := (&Request{}).Param("foo", "a") + r.Prefix("other") + r.RequestURI("/test?foo=b&a=b") + if r.path != "/test" { + t.Errorf("path is wrong: %#v", r) + } + if !api.Semantic.DeepDerivative(r.params, url.Values{"a": []string{"b"}, "foo": []string{"b"}}) { t.Errorf("should have set a param: %#v", r) } } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 40614acb295..dc925da6e58 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -56,17 +56,15 @@ func TestCreateObjects(t *testing.T) { items := []runtime.Object{} items = append(items, &api.Pod{ - TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Pod"}, - ObjectMeta: api.ObjectMeta{Name: "test-pod"}, + ObjectMeta: api.ObjectMeta{Name: "test-pod", Namespace: "default"}, }) items = append(items, &api.Service{ - TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Service"}, - ObjectMeta: api.ObjectMeta{Name: "test-service"}, + ObjectMeta: api.ObjectMeta{Name: "test-service", Namespace: "default"}, }) typer, mapper := getTyperAndMapper() - client, s := getFakeClient(t, []string{"/api/v1beta1/pods", "/api/v1beta1/services"}) + client, s := getFakeClient(t, []string{"/api/v1beta1/pods?namespace=default", "/api/v1beta1/services?namespace=default"}) errs := CreateObjects(typer, mapper, client, items) s.Close() diff --git a/pkg/kubectl/resource/helper.go b/pkg/kubectl/resource/helper.go index 0fe6ad5cc40..cbee4587dbc 100644 --- a/pkg/kubectl/resource/helper.go +++ b/pkg/kubectl/resource/helper.go @@ -35,6 +35,8 @@ type Helper struct { // An interface for reading or writing the resource version of this // type. Versioner runtime.ResourceVersioner + // True if the resource type is scoped to namespaces + NamespaceScoped bool } // NewHelper creates a Helper from a ResourceMapping @@ -44,12 +46,14 @@ func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper { Resource: mapping.Resource, Codec: mapping.Codec, Versioner: mapping.MetadataAccessor, + + NamespaceScoped: mapping.Scope.Name() == meta.RESTScopeNameNamespace, } } func (m *Helper) Get(namespace, name string) (runtime.Object, error) { return m.RESTClient.Get(). - Namespace(namespace). + NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). Name(name). Do(). @@ -58,7 +62,7 @@ func (m *Helper) Get(namespace, name string) (runtime.Object, error) { func (m *Helper) List(namespace string, selector labels.Selector) (runtime.Object, error) { return m.RESTClient.Get(). - Namespace(namespace). + NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). SelectorParam("labels", selector). Do(). @@ -68,7 +72,7 @@ func (m *Helper) List(namespace string, selector labels.Selector) (runtime.Objec func (m *Helper) Watch(namespace, resourceVersion string, labelSelector, fieldSelector labels.Selector) (watch.Interface, error) { return m.RESTClient.Get(). Prefix("watch"). - Namespace(namespace). + NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). Param("resourceVersion", resourceVersion). SelectorParam("labels", labelSelector). @@ -79,7 +83,7 @@ func (m *Helper) Watch(namespace, resourceVersion string, labelSelector, fieldSe func (m *Helper) WatchSingle(namespace, name, resourceVersion string) (watch.Interface, error) { return m.RESTClient.Get(). Prefix("watch"). - Namespace(namespace). + NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). Name(name). Param("resourceVersion", resourceVersion). @@ -88,7 +92,7 @@ func (m *Helper) WatchSingle(namespace, name, resourceVersion string) (watch.Int func (m *Helper) Delete(namespace, name string) error { return m.RESTClient.Delete(). - Namespace(namespace). + NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). Name(name). Do(). @@ -100,14 +104,14 @@ func (m *Helper) Create(namespace string, modify bool, data []byte) (runtime.Obj obj, err := m.Codec.Decode(data) if err != nil { // We don't know how to check a version on this object, but create it anyway - return createResource(m.RESTClient, m.Resource, namespace, data) + return m.createResource(m.RESTClient, m.Resource, namespace, data) } // Attempt to version the object based on client logic. version, err := m.Versioner.ResourceVersion(obj) if err != nil { // We don't know how to clear the version on this object, so send it to the server as is - return createResource(m.RESTClient, m.Resource, namespace, data) + return m.createResource(m.RESTClient, m.Resource, namespace, data) } if version != "" { if err := m.Versioner.SetResourceVersion(obj, ""); err != nil { @@ -121,11 +125,11 @@ func (m *Helper) Create(namespace string, modify bool, data []byte) (runtime.Obj } } - return createResource(m.RESTClient, m.Resource, namespace, data) + return m.createResource(m.RESTClient, m.Resource, namespace, data) } -func createResource(c RESTClient, resource, namespace string, data []byte) (runtime.Object, error) { - return c.Post().Namespace(namespace).Resource(resource).Body(data).Do().Get() +func (m *Helper) createResource(c RESTClient, resource, namespace string, data []byte) (runtime.Object, error) { + return c.Post().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(resource).Body(data).Do().Get() } func (m *Helper) Update(namespace, name string, overwrite bool, data []byte) (runtime.Object, error) { @@ -134,21 +138,21 @@ func (m *Helper) Update(namespace, name string, overwrite bool, data []byte) (ru obj, err := m.Codec.Decode(data) if err != nil { // We don't know how to handle this object, but update it anyway - return updateResource(c, m.Resource, namespace, name, data) + return m.updateResource(c, m.Resource, namespace, name, data) } // Attempt to version the object based on client logic. version, err := m.Versioner.ResourceVersion(obj) if err != nil { // We don't know how to version this object, so send it to the server as is - return updateResource(c, m.Resource, namespace, name, data) + return m.updateResource(c, m.Resource, namespace, name, data) } if version == "" && overwrite { // Retrieve the current version of the object to overwrite the server object serverObj, err := c.Get().Namespace(namespace).Resource(m.Resource).Name(name).Do().Get() if err != nil { // The object does not exist, but we want it to be created - return updateResource(c, m.Resource, namespace, name, data) + return m.updateResource(c, m.Resource, namespace, name, data) } serverVersion, err := m.Versioner.ResourceVersion(serverObj) if err != nil { @@ -164,9 +168,9 @@ func (m *Helper) Update(namespace, name string, overwrite bool, data []byte) (ru data = newData } - return updateResource(c, m.Resource, namespace, name, data) + return m.updateResource(c, m.Resource, namespace, name, data) } -func updateResource(c RESTClient, resource, namespace, name string, data []byte) (runtime.Object, error) { - return c.Put().Namespace(namespace).Resource(resource).Name(name).Body(data).Do().Get() +func (m *Helper) updateResource(c RESTClient, resource, namespace, name string, data []byte) (runtime.Object, error) { + return c.Put().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(resource).Name(name).Body(data).Do().Get() } diff --git a/pkg/kubectl/resource/helper_test.go b/pkg/kubectl/resource/helper_test.go index e133efae98c..b3891104af6 100644 --- a/pkg/kubectl/resource/helper_test.go +++ b/pkg/kubectl/resource/helper_test.go @@ -75,6 +75,10 @@ func TestHelperDelete(t *testing.T) { return false } parts := splitPath(req.URL.Path) + if len(parts) < 3 { + t.Errorf("expected URL path to have 3 parts: %s", req.URL.Path) + return false + } if parts[1] != "bar" { t.Errorf("url doesn't contain namespace: %#v", req) return false @@ -94,7 +98,8 @@ func TestHelperDelete(t *testing.T) { Err: test.HttpErr, } modifier := &Helper{ - RESTClient: client, + RESTClient: client, + NamespaceScoped: true, } err := modifier.Delete("bar", "foo") if (err != nil) != test.Err { @@ -185,9 +190,10 @@ func TestHelperCreate(t *testing.T) { client.Client = test.RespFunc } modifier := &Helper{ - RESTClient: client, - Codec: testapi.Codec(), - Versioner: testapi.MetadataAccessor(), + RESTClient: client, + Codec: testapi.Codec(), + Versioner: testapi.MetadataAccessor(), + NamespaceScoped: true, } data := []byte{} if test.Object != nil { @@ -267,7 +273,8 @@ func TestHelperGet(t *testing.T) { Err: test.HttpErr, } modifier := &Helper{ - RESTClient: client, + RESTClient: client, + NamespaceScoped: true, } obj, err := modifier.Get("bar", "foo") if (err != nil) != test.Err { @@ -337,7 +344,8 @@ func TestHelperList(t *testing.T) { Err: test.HttpErr, } modifier := &Helper{ - RESTClient: client, + RESTClient: client, + NamespaceScoped: true, } obj, err := modifier.List("bar", labels.SelectorFromSet(labels.Set{"foo": "baz"})) if (err != nil) != test.Err { @@ -440,9 +448,10 @@ func TestHelperUpdate(t *testing.T) { client.Client = test.RespFunc } modifier := &Helper{ - RESTClient: client, - Codec: testapi.Codec(), - Versioner: testapi.MetadataAccessor(), + RESTClient: client, + Codec: testapi.Codec(), + Versioner: testapi.MetadataAccessor(), + NamespaceScoped: true, } data := []byte{} if test.Object != nil { diff --git a/pkg/service/endpoints_controller_test.go b/pkg/service/endpoints_controller_test.go index 1755680bf88..3fcf227f602 100644 --- a/pkg/service/endpoints_controller_test.go +++ b/pkg/service/endpoints_controller_test.go @@ -260,7 +260,7 @@ func TestSyncEndpointsItemsEmptySelectorSelectsAll(t *testing.T) { serviceList := api.ServiceList{ Items: []api.Service{ { - ObjectMeta: api.ObjectMeta{Name: "foo"}, + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, Spec: api.ServiceSpec{ Selector: map[string]string{}, }, @@ -290,14 +290,14 @@ func TestSyncEndpointsItemsEmptySelectorSelectsAll(t *testing.T) { }, Endpoints: []string{"1.2.3.4:8080"}, }) - endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints/foo", "PUT", &data) + endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints/foo?namespace=other", "PUT", &data) } func TestSyncEndpointsItemsPreexisting(t *testing.T) { serviceList := api.ServiceList{ Items: []api.Service{ { - ObjectMeta: api.ObjectMeta{Name: "foo"}, + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"}, Spec: api.ServiceSpec{ Selector: map[string]string{ "foo": "bar", @@ -329,14 +329,14 @@ func TestSyncEndpointsItemsPreexisting(t *testing.T) { }, Endpoints: []string{"1.2.3.4:8080"}, }) - endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints/foo", "PUT", &data) + endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints/foo?namespace=bar", "PUT", &data) } func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) { serviceList := api.ServiceList{ Items: []api.Service{ { - ObjectMeta: api.ObjectMeta{Name: "foo"}, + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ Selector: map[string]string{ "foo": "bar", @@ -360,14 +360,14 @@ func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) { if err := endpoints.SyncServiceEndpoints(); err != nil { t.Errorf("unexpected error: %v", err) } - endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints/foo", "GET", nil) + endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints/foo?namespace=default", "GET", nil) } func TestSyncEndpointsItems(t *testing.T) { serviceList := api.ServiceList{ Items: []api.Service{ { - ObjectMeta: api.ObjectMeta{Name: "foo"}, + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, Spec: api.ServiceSpec{ Selector: map[string]string{ "foo": "bar", @@ -392,7 +392,7 @@ func TestSyncEndpointsItems(t *testing.T) { }, Endpoints: []string{"1.2.3.4:8080"}, }) - endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints", "POST", &data) + endpointsHandler.ValidateRequest(t, "/api/"+testapi.Version()+"/endpoints?namespace=other", "POST", &data) } func TestSyncEndpointsPodError(t *testing.T) { diff --git a/plugin/pkg/scheduler/factory/factory_test.go b/plugin/pkg/scheduler/factory/factory_test.go index 455c2822038..a63a4c096a2 100644 --- a/plugin/pkg/scheduler/factory/factory_test.go +++ b/plugin/pkg/scheduler/factory/factory_test.go @@ -277,7 +277,13 @@ func TestBind(t *testing.T) { table := []struct { binding *api.Binding }{ - {binding: &api.Binding{PodID: "foo", Host: "foohost.kubernetes.mydomain.com"}}, + {binding: &api.Binding{ + ObjectMeta: api.ObjectMeta{ + Namespace: api.NamespaceDefault, + }, + PodID: "foo", + Host: "foohost.kubernetes.mydomain.com", + }}, } for _, item := range table { @@ -296,7 +302,7 @@ func TestBind(t *testing.T) { continue } expectedBody := runtime.EncodeOrDie(testapi.Codec(), item.binding) - handler.ValidateRequest(t, "/api/"+testapi.Version()+"/bindings", "POST", &expectedBody) + handler.ValidateRequest(t, "/api/"+testapi.Version()+"/bindings?namespace=default", "POST", &expectedBody) } }