Merge pull request #4395 from smarterclayton/split_naming

Fix cross-namespace LIST and WATCH
This commit is contained in:
Brian Grant 2015-02-17 09:40:09 -08:00
commit 876d651737
15 changed files with 777 additions and 309 deletions

View File

@ -19,6 +19,7 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
@ -278,18 +279,19 @@ func runReplicationControllerTest(c *client.Client) {
} }
glog.Infof("Creating replication controllers") 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.Fatalf("Unexpected error: %v", err)
} }
glog.Infof("Done creating replication controllers") glog.Infof("Done creating replication controllers")
// Give the controllers some time to actually create the pods // 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) glog.Fatalf("FAILED: pods never created %v", err)
} }
// wait for minions to indicate they have info about the desired pods // 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 { if err != nil {
glog.Fatalf("FAILED: unable to get pods to list: %v", err) 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") glog.Infof("Version test passed")
} }
func runSelfLinkTest(c *client.Client) { func runSelfLinkTestOnNamespace(c *client.Client, namespace string) {
var svc api.Service var svc api.Service
err := c.Post().Resource("services").Body( err := c.Post().
NamespaceIfScoped(namespace, len(namespace) > 0).
Resource("services").Body(
&api.Service{ &api.Service{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Name: "selflinktest", Name: "selflinktest",
Namespace: namespace,
Labels: map[string]string{ Labels: map[string]string{
"name": "selflinktest", "name": "selflinktest",
}, },
@ -335,18 +340,19 @@ func runSelfLinkTest(c *client.Client) {
if err != nil { if err != nil {
glog.Fatalf("Failed creating selflinktest service: %v", err) 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 { if err != nil {
glog.Fatalf("Failed listing service with supplied self link '%v': %v", svc.SelfLink, err) glog.Fatalf("Failed listing service with supplied self link '%v': %v", svc.SelfLink, err)
} }
var svcList api.ServiceList 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 { if err != nil {
glog.Fatalf("Failed listing services: %v", err) 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 { if err != nil {
glog.Fatalf("Failed listing services with supplied self link '%v': %v", svcList.SelfLink, err) glog.Fatalf("Failed listing services with supplied self link '%v': %v", svcList.SelfLink, err)
} }
@ -358,16 +364,16 @@ func runSelfLinkTest(c *client.Client) {
continue continue
} }
found = true found = true
err = c.Get().AbsPath(item.SelfLink).Do().Into(&svc) err = c.Get().RequestURI(item.SelfLink).Do().Into(&svc)
if err != nil { if err != nil {
glog.Fatalf("Failed listing service with supplied self link '%v': %v", item.SelfLink, err) glog.Fatalf("Failed listing service with supplied self link '%v': %v", item.SelfLink, err)
} }
break break
} }
if !found { 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. // TODO: Should test PUT at some point, too.
} }
@ -518,7 +524,7 @@ func runMasterServiceTest(client *client.Client) {
} }
func runServiceTest(client *client.Client) { func runServiceTest(client *client.Client) {
pod := api.Pod{ pod := &api.Pod{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Name: "foo", Name: "foo",
Labels: map[string]string{ Labels: map[string]string{
@ -543,14 +549,14 @@ func runServiceTest(client *client.Client) {
PodIP: "1.2.3.4", PodIP: "1.2.3.4",
}, },
} }
_, err := client.Pods(api.NamespaceDefault).Create(&pod) pod, err := client.Pods(api.NamespaceDefault).Create(pod)
if err != nil { if err != nil {
glog.Fatalf("Failed to create pod: %v, %v", pod, err) 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 { 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) glog.Fatalf("FAILED: pod never started running %v", err)
} }
svc1 := api.Service{ svc1 := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "service1"}, ObjectMeta: api.ObjectMeta{Name: "service1"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{
@ -561,15 +567,33 @@ func runServiceTest(client *client.Client) {
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }
_, err = client.Services(api.NamespaceDefault).Create(&svc1) svc1, err = client.Services(api.NamespaceDefault).Create(svc1)
if err != nil { if err != nil {
glog.Fatalf("Failed to create service: %v, %v", svc1, err) 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 { 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) glog.Fatalf("FAILED: unexpected endpoints: %v", err)
} }
// A second service with the same port. // A second service with the same port.
svc2 := api.Service{ svc2 := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "service2"}, ObjectMeta: api.ObjectMeta{Name: "service2"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{
@ -580,13 +604,30 @@ func runServiceTest(client *client.Client) {
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }
_, err = client.Services(api.NamespaceDefault).Create(&svc2) svc2, err = client.Services(api.NamespaceDefault).Create(svc2)
if err != nil { if err != nil {
glog.Fatalf("Failed to create service: %v, %v", svc2, err) 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 { 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) 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.") glog.Info("Service test passed.")
} }
@ -623,7 +664,10 @@ func main() {
runServiceTest, runServiceTest,
runAPIVersionsTest, runAPIVersionsTest,
runMasterServiceTest, runMasterServiceTest,
runSelfLinkTest, func(c *client.Client) {
runSelfLinkTestOnNamespace(c, "")
runSelfLinkTestOnNamespace(c, "other")
},
} }
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(len(testFuncs)) wg.Add(len(testFuncs))

View File

@ -156,6 +156,11 @@ for version in "${kube_api_versions[@]}"; do
howmanypods="$(kubectl get pods -o template -t "{{ len .items }}" "${kube_flags[@]}")" howmanypods="$(kubectl get pods -o template -t "{{ len .items }}" "${kube_flags[@]}")"
[ "$howmanypods" -eq 0 ] [ "$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)" kube::log::status "Testing kubectl(${version}:services)"
kubectl get services "${kube_flags[@]}" kubectl get services "${kube_flags[@]}"
kubectl create -f examples/guestbook/frontend-service.json "${kube_flags[@]}" kubectl create -f examples/guestbook/frontend-service.json "${kube_flags[@]}"

View File

@ -42,6 +42,7 @@ type action struct {
Verb string // Verb identifying the action ("GET", "POST", "WATCH", PROXY", etc). Verb string // Verb identifying the action ("GET", "POST", "WATCH", PROXY", etc).
Path string // The path of the action Path string // The path of the action
Params []*restful.Parameter // List of parameters associated with 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. // 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 { 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 codec := a.group.codec
admit := a.group.admit admit := a.group.admit
linker := a.group.linker
context := a.group.context context := a.group.context
resource := path resource := path
@ -149,13 +149,6 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
} }
var ctxFn ContextFunc 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 { ctxFn = func(req *restful.Request) api.Context {
if ctx, ok := context.Get(req.Request); ok { if ctx, ok := context.Get(req.Request); ok {
return ctx return ctx
@ -168,146 +161,76 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
nameParam := ws.PathParameter("name", "name of the "+kind).DataType("string") nameParam := ws.PathParameter("name", "name of the "+kind).DataType("string")
params := []*restful.Parameter{} params := []*restful.Parameter{}
actions := []action{} actions := []action{}
// Get the list of actions for the given scope. // Get the list of actions for the given scope.
if scope.Name() != meta.RESTScopeNameNamespace { 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}" itemPath := path + "/{name}"
nameParams := append(params, nameParam) nameParams := append(params, nameParam)
namespaceFn = func(req *restful.Request) (namespace string, err error) { namer := rootScopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath)}
return
} // Handler for standard REST verbs (GET, PUT, POST and DELETE).
nameFn = func(req *restful.Request) (namespace, name string, err error) { actions = appendIf(actions, action{"LIST", path, params, namer}, storageVerbs["RESTLister"])
name = req.PathParameter("name") actions = appendIf(actions, action{"POST", path, params, namer}, storageVerbs["RESTCreater"])
if len(name) == 0 { actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params, namer}, allowWatchList)
err = errEmptyName
} actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, storageVerbs["RESTGetter"])
return actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, storageVerbs["RESTUpdater"])
} actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, storageVerbs["RESTDeleter"])
generateLinkFn = func(namespace, name string) (path string, query string) { actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, storageVerbs["ResourceWatcher"])
path = strings.Replace(itemPath, "{name}", name, 1) actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, storageVerbs["Redirector"])
path = gpath.Join(a.prefix, path) actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, storageVerbs["Redirector"])
return 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 { } 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 // v1beta3 format with namespace in path
if scope.ParamPath() { if scope.ParamPath() {
// Handler for standard REST verbs (GET, PUT, POST and DELETE). // Handler for standard REST verbs (GET, PUT, POST and DELETE).
namespaceParam := ws.PathParameter(scope.ParamName(), scope.ParamDescription()).DataType("string") namespaceParam := ws.PathParameter(scope.ParamName(), scope.ParamDescription()).DataType("string")
namespacedPath := scope.ParamName() + "/{" + scope.ParamName() + "}/" + path namespacedPath := scope.ParamName() + "/{" + scope.ParamName() + "}/" + path
namespaceParams := []*restful.Parameter{namespaceParam} 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}" itemPath := namespacedPath + "/{name}"
nameParams := append(namespaceParams, nameParam) nameParams := append(namespaceParams, nameParam)
nameFn = func(req *restful.Request) (namespace, name string, err error) { namer := scopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath), false}
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
}
actions = appendIf(actions, action{"GET", itemPath, nameParams}, storageVerbs["RESTGetter"]) actions = appendIf(actions, action{"LIST", namespacedPath, namespaceParams, namer}, storageVerbs["RESTLister"])
actions = appendIf(actions, action{"PUT", itemPath, nameParams}, storageVerbs["RESTUpdater"]) actions = appendIf(actions, action{"POST", namespacedPath, namespaceParams, namer}, storageVerbs["RESTCreater"])
actions = appendIf(actions, action{"DELETE", itemPath, nameParams}, storageVerbs["RESTDeleter"]) actions = appendIf(actions, action{"WATCHLIST", "/watch/" + namespacedPath, namespaceParams, namer}, allowWatchList)
actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams}, storageVerbs["ResourceWatcher"])
actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams}, storageVerbs["Redirector"]) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, storageVerbs["RESTGetter"])
actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams}, storageVerbs["Redirector"]) actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, storageVerbs["RESTUpdater"])
actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams}, storageVerbs["Redirector"]) 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. // list across namespace.
actions = appendIf(actions, action{"LIST", path, params}, storageVerbs["RESTLister"]) namer = scopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath), true}
actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params}, allowWatchList) actions = appendIf(actions, action{"LIST", path, params, namer}, storageVerbs["RESTLister"])
actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params, namer}, allowWatchList)
} else { } else {
// Handler for standard REST verbs (GET, PUT, POST and DELETE). // Handler for standard REST verbs (GET, PUT, POST and DELETE).
// v1beta1/v1beta2 format where namespace was a query parameter // v1beta1/v1beta2 format where namespace was a query parameter
namespaceParam := ws.QueryParameter(scope.ParamName(), scope.ParamDescription()).DataType("string") namespaceParam := ws.QueryParameter(scope.ParamName(), scope.ParamDescription()).DataType("string")
namespaceParams := []*restful.Parameter{namespaceParam} 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}" itemPath := path + "/{name}"
nameParams := append(namespaceParams, nameParam) nameParams := append(namespaceParams, nameParam)
nameFn = func(req *restful.Request) (namespace, name string, err error) { namer := legacyScopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath)}
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
}
actions = appendIf(actions, action{"GET", itemPath, nameParams}, storageVerbs["RESTGetter"]) actions = appendIf(actions, action{"LIST", path, namespaceParams, namer}, storageVerbs["RESTLister"])
actions = appendIf(actions, action{"PUT", itemPath, nameParams}, storageVerbs["RESTUpdater"]) actions = appendIf(actions, action{"POST", path, namespaceParams, namer}, storageVerbs["RESTCreater"])
actions = appendIf(actions, action{"DELETE", itemPath, nameParams}, storageVerbs["RESTDeleter"]) actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, namespaceParams, namer}, allowWatchList)
actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams}, storageVerbs["ResourceWatcher"])
actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams}, storageVerbs["Redirector"]) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, storageVerbs["RESTGetter"])
actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams}, storageVerbs["Redirector"]) actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, storageVerbs["RESTUpdater"])
actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams}, storageVerbs["Redirector"]) 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) m := monitorFilter(action.Verb, resource)
switch action.Verb { switch action.Verb {
case "GET": // Get a resource. 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). Filter(m).
Doc("read the specified " + kind). Doc("read the specified " + kind).
Operation("read" + kind). Operation("read" + kind).
@ -340,7 +263,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
addParams(route, action.Params) addParams(route, action.Params)
ws.Route(route) ws.Route(route)
case "LIST": // List all resources of a kind. 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). Filter(m).
Doc("list objects of kind " + kind). Doc("list objects of kind " + kind).
Operation("list" + kind). Operation("list" + kind).
@ -348,7 +271,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
addParams(route, action.Params) addParams(route, action.Params)
ws.Route(route) ws.Route(route)
case "PUT": // Update a resource. 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). Filter(m).
Doc("update the specified " + kind). Doc("update the specified " + kind).
Operation("update" + kind). Operation("update" + kind).
@ -356,7 +279,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
addParams(route, action.Params) addParams(route, action.Params)
ws.Route(route) ws.Route(route)
case "POST": // Create a resource. 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). Filter(m).
Doc("create a " + kind). Doc("create a " + kind).
Operation("create" + kind). Operation("create" + kind).
@ -364,7 +287,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
addParams(route, action.Params) addParams(route, action.Params)
ws.Route(route) ws.Route(route)
case "DELETE": // Delete a resource. 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). Filter(m).
Doc("delete a " + kind). Doc("delete a " + kind).
Operation("delete" + kind) Operation("delete" + kind)
@ -409,6 +332,224 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
return nil 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 // This magic incantation returns *ptrToObject for an arbitrary pointer
func indirectArbitraryPointer(ptrToObject interface{}) interface{} { func indirectArbitraryPointer(ptrToObject interface{}) interface{} {
return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface() return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface()

View File

@ -154,6 +154,9 @@ type SimpleRESTStorage struct {
updated *Simple updated *Simple
created *Simple created *Simple
actualNamespace string
namespacePresent bool
// These are set when Watch is called // These are set when Watch is called
fakeWatch *watch.FakeWatcher fakeWatch *watch.FakeWatcher
requestedLabelSelector labels.Selector requestedLabelSelector labels.Selector
@ -172,6 +175,7 @@ type SimpleRESTStorage struct {
} }
func (storage *SimpleRESTStorage) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) { func (storage *SimpleRESTStorage) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) {
storage.checkContext(ctx)
result := &SimpleList{ result := &SimpleList{
Items: storage.list, 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) { func (storage *SimpleRESTStorage) Get(ctx api.Context, id string) (runtime.Object, error) {
storage.checkContext(ctx)
return api.Scheme.CopyOrDie(&storage.item), storage.errors["get"] 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) { func (storage *SimpleRESTStorage) Delete(ctx api.Context, id string) (runtime.Object, error) {
storage.checkContext(ctx)
storage.deleted = id storage.deleted = id
if err := storage.errors["delete"]; err != nil { if err := storage.errors["delete"]; err != nil {
return nil, err 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) { func (storage *SimpleRESTStorage) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) {
storage.checkContext(ctx)
storage.created = obj.(*Simple) storage.created = obj.(*Simple)
if err := storage.errors["create"]; err != nil { if err := storage.errors["create"]; err != nil {
return nil, err 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) { func (storage *SimpleRESTStorage) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) {
storage.checkContext(ctx)
storage.updated = obj.(*Simple) storage.updated = obj.(*Simple)
if err := storage.errors["update"]; err != nil { if err := storage.errors["update"]; err != nil {
return nil, false, err return nil, false, err
@ -229,6 +241,7 @@ func (storage *SimpleRESTStorage) Update(ctx api.Context, obj runtime.Object) (r
// Implement ResourceWatcher. // Implement ResourceWatcher.
func (storage *SimpleRESTStorage) Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) { func (storage *SimpleRESTStorage) Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) {
storage.checkContext(ctx)
storage.requestedLabelSelector = label storage.requestedLabelSelector = label
storage.requestedFieldSelector = field storage.requestedFieldSelector = field
storage.requestedResourceVersion = resourceVersion storage.requestedResourceVersion = resourceVersion
@ -242,6 +255,7 @@ func (storage *SimpleRESTStorage) Watch(ctx api.Context, label, field labels.Sel
// Implement Redirector. // Implement Redirector.
func (storage *SimpleRESTStorage) ResourceLocation(ctx api.Context, id string) (string, error) { 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 // validate that the namespace context on the request matches the expected input
storage.requestedResourceNamespace = api.NamespaceValue(ctx) storage.requestedResourceNamespace = api.NamespaceValue(ctx)
if storage.expectedResourceNamespace != storage.requestedResourceNamespace { if storage.expectedResourceNamespace != storage.requestedResourceNamespace {
@ -388,29 +402,57 @@ func TestVersion(t *testing.T) {
} }
} }
func TestSimpleList(t *testing.T) { 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},
}
for i, testCase := range testCases {
storage := map[string]RESTStorage{} storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{} simpleStorage := SimpleRESTStorage{expectedResourceNamespace: testCase.namespace}
storage["simple"] = &simpleStorage storage["simple"] = &simpleStorage
selfLinker := &setTestSelfLinker{ selfLinker := &setTestSelfLinker{
t: t, t: t,
namespace: "other", namespace: testCase.namespace,
expectedSet: "/prefix/version/simple?namespace=other", 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)
} }
handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper)
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
resp, err := http.Get(server.URL + "/prefix/version/simple") resp, err := http.Get(server.URL + testCase.url)
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("%d: unexpected error: %v", i, err)
continue
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, http.StatusOK, resp) t.Errorf("%d: unexpected status: %d, Expected: %d, %#v", i, resp.StatusCode, http.StatusOK, resp)
} }
// TODO: future, restore get links
if !selfLinker.called { if !selfLinker.called {
t.Errorf("Never set self link") 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") resp, err := http.Get(server.URL + "/prefix/version/simple")
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if resp.StatusCode != http.StatusInternalServerError { if resp.StatusCode != http.StatusInternalServerError {
@ -439,7 +481,54 @@ func TestNonEmptyList(t *testing.T) {
simpleStorage := SimpleRESTStorage{ simpleStorage := SimpleRESTStorage{
list: []Simple{ 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"}, ObjectMeta: api.ObjectMeta{Namespace: "other"},
Other: "foo", Other: "foo",
}, },
@ -452,7 +541,7 @@ func TestNonEmptyList(t *testing.T) {
resp, err := http.Get(server.URL + "/prefix/version/simple") resp, err := http.Get(server.URL + "/prefix/version/simple")
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
@ -460,11 +549,10 @@ func TestNonEmptyList(t *testing.T) {
body, _ := ioutil.ReadAll(resp.Body) body, _ := ioutil.ReadAll(resp.Body)
t.Logf("Data: %s", string(body)) t.Logf("Data: %s", string(body))
} }
var listOut SimpleList var listOut SimpleList
body, err := extractBody(resp, &listOut) body, err := extractBody(resp, &listOut)
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if len(listOut.Items) != 1 { if len(listOut.Items) != 1 {
@ -474,7 +562,10 @@ func TestNonEmptyList(t *testing.T) {
if listOut.Items[0].Other != simpleStorage.list[0].Other { if listOut.Items[0].Other != simpleStorage.list[0].Other {
t.Errorf("Unexpected data: %#v, %s", listOut.Items[0], string(body)) 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 { if listOut.Items[0].ObjectMeta.SelfLink != expectedSelfLink {
t.Errorf("Unexpected data: %#v, %s", 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) { func TestUpdatePreventsMismatchedNamespace(t *testing.T) {
storage := map[string]RESTStorage{} storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{} simpleStorage := SimpleRESTStorage{}
@ -931,20 +1064,79 @@ type setTestSelfLinker struct {
name string name string
namespace string namespace string
called bool called bool
err error
} }
func (s *setTestSelfLinker) Namespace(runtime.Object) (string, error) { return s.namespace, 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, nil } func (s *setTestSelfLinker) Name(runtime.Object) (string, error) { return s.name, s.err }
func (*setTestSelfLinker) SelfLink(runtime.Object) (string, error) { return "", nil } func (s *setTestSelfLinker) SelfLink(runtime.Object) (string, error) { return "", s.err }
func (s *setTestSelfLinker) SetSelfLink(obj runtime.Object, selfLink string) error { func (s *setTestSelfLinker) SetSelfLink(obj runtime.Object, selfLink string) error {
if e, a := s.expectedSet, selfLink; e != a { if e, a := s.expectedSet, selfLink; e != a {
s.t.Errorf("expected '%v', got '%v'", e, a) s.t.Errorf("expected '%v', got '%v'", e, a)
} }
s.called = true s.called = true
return nil return s.err
} }
func TestCreate(t *testing.T) { 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{ storage := SimpleRESTStorage{
injectedFunction: func(obj runtime.Object) (runtime.Object, error) { injectedFunction: func(obj runtime.Object) (runtime.Object, error) {
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)

View File

@ -18,6 +18,7 @@ package apiserver
import ( import (
"net/http" "net/http"
gpath "path"
"time" "time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
@ -27,66 +28,70 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
"github.com/golang/glog"
) )
// ContextFunc returns a Context given a request - a context must be returned // ContextFunc returns a Context given a request - a context must be returned
type ContextFunc func(req *restful.Request) api.Context type ContextFunc func(req *restful.Request) api.Context
// ResourceNameFunc returns a name (and optional namespace) given a request - if no name is present // ScopeNamer handles accessing names from requests and objects
// an error must be returned. type ScopeNamer interface {
type ResourceNameFunc func(req *restful.Request) (namespace, name string, err error) // Namespace returns the appropriate namespace value from the request (may be empty) or an
// error.
// ObjectNameFunc returns the name (and optional namespace) of an object Namespace(req *restful.Request) (namespace string, err error)
type ObjectNameFunc func(obj runtime.Object) (namespace, name 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.
// ResourceNamespaceFunc returns the namespace associated with the given request - if no namespace Name(req *restful.Request) (namespace, name string, err error)
// is present an error must be returned. // ObjectName returns the namespace and name from an object if they exist, or an error if the object
type ResourceNamespaceFunc func(req *restful.Request) (namespace string, err error) // does not support names.
ObjectName(obj runtime.Object) (namespace, name string, err error)
// LinkResourceFunc updates the provided object with a SelfLink that is appropriate for the current // SetSelfLink sets the provided URL onto the object. The method should return nil if the object
// request. // does not support selfLinks.
type LinkResourceFunc func(req *restful.Request, obj runtime.Object) error 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. // 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) { return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter w := res.ResponseWriter
namespace, name, err := nameFn(req) namespace, name, err := namer.Name(req)
if err != nil { if err != nil {
notFound(w, req.Request) notFound(w, req.Request)
return return
} }
ctx := ctxFn(req) ctx := ctxFn(req)
if len(namespace) > 0 {
ctx = api.WithNamespace(ctx, namespace) ctx = api.WithNamespace(ctx, namespace)
}
item, err := r.Get(ctx, name) result, err := r.Get(ctx, name)
if err != nil { if err != nil {
errorJSON(err, codec, w) errorJSON(err, codec, w)
return return
} }
if err := linkFn(req, item); err != nil { if err := setSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w) errorJSON(err, codec, w)
return 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. // 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) { return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter w := res.ResponseWriter
namespace, err := namespaceFn(req) namespace, err := namer.Namespace(req)
if err != nil { if err != nil {
notFound(w, req.Request) notFound(w, req.Request)
return return
} }
ctx := ctxFn(req) 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")) label, err := labels.ParseSelector(req.Request.URL.Query().Get("labels"))
if err != nil { if err != nil {
errorJSON(err, codec, w) errorJSON(err, codec, w)
@ -98,36 +103,34 @@ func ListResource(r RESTLister, ctxFn ContextFunc, namespaceFn ResourceNamespace
return return
} }
item, err := r.List(ctx, label, field) result, err := r.List(ctx, label, field)
if err != nil { if err != nil {
errorJSON(err, codec, w) errorJSON(err, codec, w)
return return
} }
if err := linkFn(req, item); err != nil { if err := setListSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w) errorJSON(err, codec, w)
return return
} }
writeJSON(http.StatusOK, codec, item, w) writeJSON(http.StatusOK, codec, result, w)
} }
} }
// CreateResource returns a function that will handle a resource creation. // 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) { return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter 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) // 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")) timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
namespace, err := namespaceFn(req) namespace, err := namer.Namespace(req)
if err != nil { if err != nil {
notFound(w, req.Request) notFound(w, req.Request)
return return
} }
ctx := ctxFn(req) ctx := ctxFn(req)
if len(namespace) > 0 {
ctx = api.WithNamespace(ctx, namespace) ctx = api.WithNamespace(ctx, namespace)
}
body, err := readBody(req.Request) body, err := readBody(req.Request)
if err != nil { if err != nil {
@ -159,7 +162,7 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namespaceFn ResourceNamesp
return return
} }
if err := linkFn(req, result); err != nil { if err := setSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w) errorJSON(err, codec, w)
return return
} }
@ -169,22 +172,20 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namespaceFn ResourceNamesp
} }
// UpdateResource returns a function that will handle a resource update // 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) { return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter 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) // 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")) timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
namespace, name, err := nameFn(req) namespace, name, err := namer.Name(req)
if err != nil { if err != nil {
notFound(w, req.Request) notFound(w, req.Request)
return return
} }
ctx := ctxFn(req) ctx := ctxFn(req)
if len(namespace) > 0 {
ctx = api.WithNamespace(ctx, namespace) ctx = api.WithNamespace(ctx, namespace)
}
body, err := readBody(req.Request) body, err := readBody(req.Request)
if err != nil { if err != nil {
@ -198,11 +199,8 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, nameFn ResourceNameFunc, o
return return
} }
objNamespace, objName, err := objNameFunc(obj) // check the provided name against the request
if err != nil { if objNamespace, objName, err := namer.ObjectName(obj); err == nil {
errorJSON(err, codec, w)
return
}
if objName != name { if objName != name {
errorJSON(errors.NewBadRequest("the name of the object does not match the name on the URL"), codec, w) errorJSON(errors.NewBadRequest("the name of the object does not match the name on the URL"), codec, w)
return return
@ -213,6 +211,7 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, nameFn ResourceNameFunc, o
return return
} }
} }
}
err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE")) err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE"))
if err != nil { if err != nil {
@ -231,7 +230,7 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, nameFn ResourceNameFunc, o
return return
} }
if err := linkFn(req, result); err != nil { if err := setSelfLink(result, req, namer); err != nil {
errorJSON(err, codec, w) errorJSON(err, codec, w)
return return
} }
@ -245,14 +244,14 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, nameFn ResourceNameFunc, o
} }
// DeleteResource returns a function that will handle a resource deletion // 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) { return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter 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) // 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")) timeout := parseTimeout(req.Request.URL.Query().Get("timeout"))
namespace, name, err := nameFn(req) namespace, name, err := namer.Name(req)
if err != nil { if err != nil {
notFound(w, req.Request) notFound(w, req.Request)
return return
@ -290,7 +289,7 @@ func DeleteResource(r RESTDeleter, ctxFn ContextFunc, nameFn ResourceNameFunc, l
} else { } else {
// when a non-status response is returned, set the self link // when a non-status response is returned, set the self link
if _, ok := result.(*api.Status); !ok { 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) errorJSON(err, codec, w)
return 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 // 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 // plus the path and query generated by the provided linkFunc
func setSelfLink(obj runtime.Object, req *http.Request, linker runtime.SelfLinker, fn linkFunc) error { func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error {
namespace, err := linker.Namespace(obj) // TODO: SelfLink generation should return a full URL?
path, query, err := namer.GenerateLink(req, obj)
if err == errEmptyName {
return nil
}
if err != nil { if err != nil {
return err return err
} }
name, err := linker.Name(obj)
if err != nil {
return err
}
path, query := fn(namespace, name)
newURL := *req.URL newURL := *req.Request.URL
newURL.Path = path // use only canonical paths
newURL.Path = gpath.Clean(path)
newURL.RawQuery = query newURL.RawQuery = query
newURL.Fragment = "" newURL.Fragment = ""
if err := linker.SetSelfLink(obj, newURL.String()); err != nil { return namer.SetSelfLink(obj, newURL.String())
return err
} }
// 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) { if !runtime.IsListType(obj) {
return nil 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. // Set self-link of objects in the list.
items, err := runtime.ExtractList(obj) items, err := runtime.ExtractList(obj)
if err != nil { if err != nil {
return err return err
} }
for i := range items { 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 err
} }
} }
return runtime.SetList(obj, items) return runtime.SetList(obj, items)
} }

View File

@ -42,7 +42,7 @@ func buildResourcePath(prefix, namespace, resource string) string {
base := path.Join("/api", testapi.Version(), prefix) base := path.Join("/api", testapi.Version(), prefix)
if len(namespace) > 0 { if len(namespace) > 0 {
if !(testapi.Version() == "v1beta1" || testapi.Version() == "v1beta2") { if !(testapi.Version() == "v1beta1" || testapi.Version() == "v1beta2") {
base = path.Join(base, "ns", namespace) base = path.Join(base, "namespaces", namespace)
} }
} }
return path.Join(base, resource) return path.Join(base, resource)
@ -58,11 +58,9 @@ func buildQueryValues(namespace string, query url.Values) url.Values {
} }
} }
} }
if len(namespace) > 0 {
if testapi.Version() == "v1beta1" || testapi.Version() == "v1beta2" { if testapi.Version() == "v1beta1" || testapi.Version() == "v1beta2" {
v.Set("namespace", namespace) v.Set("namespace", namespace)
} }
}
return v return v
} }

View File

@ -66,7 +66,7 @@ func (e *events) Create(event *api.Event) (*api.Event, error) {
} }
result := &api.Event{} result := &api.Event{}
err := e.client.Post(). err := e.client.Post().
Namespace(event.Namespace). NamespaceIfScoped(event.Namespace, len(event.Namespace) > 0).
Resource("events"). Resource("events").
Body(event). Body(event).
Do(). Do().
@ -85,7 +85,7 @@ func (e *events) Update(event *api.Event) (*api.Event, error) {
} }
result := &api.Event{} result := &api.Event{}
err := e.client.Put(). err := e.client.Put().
Namespace(event.Namespace). NamespaceIfScoped(event.Namespace, len(event.Namespace) > 0).
Resource("events"). Resource("events").
Name(event.Name). Name(event.Name).
Body(event). 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) { func (e *events) List(label, field labels.Selector) (*api.EventList, error) {
result := &api.EventList{} result := &api.EventList{}
err := e.client.Get(). err := e.client.Get().
Namespace(e.namespace). NamespaceIfScoped(e.namespace, len(e.namespace) > 0).
Resource("events"). Resource("events").
SelectorParam("labels", label). SelectorParam("labels", label).
SelectorParam("fields", field). SelectorParam("fields", field).
@ -115,7 +115,7 @@ func (e *events) Get(name string) (*api.Event, error) {
result := &api.Event{} result := &api.Event{}
err := e.client.Get(). err := e.client.Get().
Namespace(e.namespace). NamespaceIfScoped(e.namespace, len(e.namespace) > 0).
Resource("events"). Resource("events").
Name(name). Name(name).
Do(). 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) { func (e *events) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) {
return e.client.Get(). return e.client.Get().
Prefix("watch"). Prefix("watch").
Namespace(e.namespace). NamespaceIfScoped(e.namespace, len(e.namespace) > 0).
Resource("events"). Resource("events").
Param("resourceVersion", resourceVersion). Param("resourceVersion", resourceVersion).
SelectorParam("labels", label). SelectorParam("labels", label).

View File

@ -62,7 +62,9 @@ func TestEventCreate(t *testing.T) {
} }
timeStamp := util.Now() timeStamp := util.Now()
event := &api.Event{ event := &api.Event{
//namespace: namespace{"default"}, ObjectMeta: api.ObjectMeta{
Namespace: api.NamespaceDefault,
},
InvolvedObject: *objReference, InvolvedObject: *objReference,
FirstTimestamp: timeStamp, FirstTimestamp: timeStamp,
LastTimestamp: timeStamp, LastTimestamp: timeStamp,
@ -80,7 +82,7 @@ func TestEventCreate(t *testing.T) {
response, err := c.Setup().Events("").Create(event) response, err := c.Setup().Events("").Create(event)
if err != nil { 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) { if e, a := *objReference, response.InvolvedObject; !reflect.DeepEqual(e, a) {
@ -99,6 +101,9 @@ func TestEventGet(t *testing.T) {
} }
timeStamp := util.Now() timeStamp := util.Now()
event := &api.Event{ event := &api.Event{
ObjectMeta: api.ObjectMeta{
Namespace: "other",
},
InvolvedObject: *objReference, InvolvedObject: *objReference,
FirstTimestamp: timeStamp, FirstTimestamp: timeStamp,
LastTimestamp: timeStamp, LastTimestamp: timeStamp,
@ -116,7 +121,7 @@ func TestEventGet(t *testing.T) {
response, err := c.Setup().Events("").Get("1") response, err := c.Setup().Events("").Get("1")
if err != nil { 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) { if e, r := event.InvolvedObject, response.InvolvedObject; !reflect.DeepEqual(e, r) {

View File

@ -90,7 +90,7 @@ type Request struct {
// generic components accessible via method setters // generic components accessible via method setters
path string path string
subpath string subpath string
params map[string]string params url.Values
// structural elements of the request that are part of the Kubernetes API conventions // structural elements of the request that are part of the Kubernetes API conventions
namespace string namespace string
@ -181,6 +181,14 @@ func (r *Request) Namespace(namespace string) *Request {
return r 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 // AbsPath overwrites an existing path with the segments provided. Trailing slashes are preserved
// when a single segment is passed. // when a single segment is passed.
func (r *Request) AbsPath(segments ...string) *Request { func (r *Request) AbsPath(segments ...string) *Request {
@ -196,6 +204,29 @@ func (r *Request) AbsPath(segments ...string) *Request {
return r 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. // 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 // This is a convenience function so you don't have to first check that it's a
// validly formatted selector. // validly formatted selector.
@ -244,9 +275,9 @@ func (r *Request) setParam(paramName, value string) *Request {
return r return r
} }
if r.params == nil { 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 return r
} }
@ -317,16 +348,16 @@ func (r *Request) finalURL() string {
query := url.Values{} query := url.Values{}
for key, value := range r.params { for key, value := range r.params {
query.Add(key, value) query[key] = value
} }
if r.namespaceSet && r.namespaceInQuery && len(r.namespace) > 0 { if r.namespaceSet && r.namespaceInQuery {
query.Add("namespace", r.namespace) query.Set("namespace", r.namespace)
} }
// timeout is handled specially here. // timeout is handled specially here.
if r.timeout != 0 { if r.timeout != 0 {
query.Add("timeout", r.timeout.String()) query.Set("timeout", r.timeout.String())
} }
finalURL.RawQuery = query.Encode() finalURL.RawQuery = query.Encode()
return finalURL.String() return finalURL.String()
@ -427,6 +458,14 @@ func (r *Request) Do() Result {
return Result{err: &RequestConstructionError{r.err}} 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) req, err := http.NewRequest(r.verb, r.finalURL(), r.body)
if err != nil { if err != nil {
return Result{err: &RequestConstructionError{err}} return Result{err: &RequestConstructionError{err}}

View File

@ -148,7 +148,19 @@ func TestRequestParseSelectorParam(t *testing.T) {
func TestRequestParam(t *testing.T) { func TestRequestParam(t *testing.T) {
r := (&Request{}).Param("foo", "a") 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) t.Errorf("should have set a param: %#v", r)
} }
} }

View File

@ -56,17 +56,15 @@ func TestCreateObjects(t *testing.T) {
items := []runtime.Object{} items := []runtime.Object{}
items = append(items, &api.Pod{ items = append(items, &api.Pod{
TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Pod"}, ObjectMeta: api.ObjectMeta{Name: "test-pod", Namespace: "default"},
ObjectMeta: api.ObjectMeta{Name: "test-pod"},
}) })
items = append(items, &api.Service{ items = append(items, &api.Service{
TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Service"}, ObjectMeta: api.ObjectMeta{Name: "test-service", Namespace: "default"},
ObjectMeta: api.ObjectMeta{Name: "test-service"},
}) })
typer, mapper := getTyperAndMapper() 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) errs := CreateObjects(typer, mapper, client, items)
s.Close() s.Close()

View File

@ -35,6 +35,8 @@ type Helper struct {
// An interface for reading or writing the resource version of this // An interface for reading or writing the resource version of this
// type. // type.
Versioner runtime.ResourceVersioner Versioner runtime.ResourceVersioner
// True if the resource type is scoped to namespaces
NamespaceScoped bool
} }
// NewHelper creates a Helper from a ResourceMapping // NewHelper creates a Helper from a ResourceMapping
@ -44,12 +46,14 @@ func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper {
Resource: mapping.Resource, Resource: mapping.Resource,
Codec: mapping.Codec, Codec: mapping.Codec,
Versioner: mapping.MetadataAccessor, Versioner: mapping.MetadataAccessor,
NamespaceScoped: mapping.Scope.Name() == meta.RESTScopeNameNamespace,
} }
} }
func (m *Helper) Get(namespace, name string) (runtime.Object, error) { func (m *Helper) Get(namespace, name string) (runtime.Object, error) {
return m.RESTClient.Get(). return m.RESTClient.Get().
Namespace(namespace). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
Name(name). Name(name).
Do(). 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) { func (m *Helper) List(namespace string, selector labels.Selector) (runtime.Object, error) {
return m.RESTClient.Get(). return m.RESTClient.Get().
Namespace(namespace). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
SelectorParam("labels", selector). SelectorParam("labels", selector).
Do(). 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) { func (m *Helper) Watch(namespace, resourceVersion string, labelSelector, fieldSelector labels.Selector) (watch.Interface, error) {
return m.RESTClient.Get(). return m.RESTClient.Get().
Prefix("watch"). Prefix("watch").
Namespace(namespace). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
Param("resourceVersion", resourceVersion). Param("resourceVersion", resourceVersion).
SelectorParam("labels", labelSelector). 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) { func (m *Helper) WatchSingle(namespace, name, resourceVersion string) (watch.Interface, error) {
return m.RESTClient.Get(). return m.RESTClient.Get().
Prefix("watch"). Prefix("watch").
Namespace(namespace). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
Name(name). Name(name).
Param("resourceVersion", resourceVersion). 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 { func (m *Helper) Delete(namespace, name string) error {
return m.RESTClient.Delete(). return m.RESTClient.Delete().
Namespace(namespace). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
Name(name). Name(name).
Do(). Do().
@ -100,14 +104,14 @@ func (m *Helper) Create(namespace string, modify bool, data []byte) (runtime.Obj
obj, err := m.Codec.Decode(data) obj, err := m.Codec.Decode(data)
if err != nil { if err != nil {
// We don't know how to check a version on this object, but create it anyway // 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. // Attempt to version the object based on client logic.
version, err := m.Versioner.ResourceVersion(obj) version, err := m.Versioner.ResourceVersion(obj)
if err != nil { if err != nil {
// We don't know how to clear the version on this object, so send it to the server as is // 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 version != "" {
if err := m.Versioner.SetResourceVersion(obj, ""); err != nil { 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) { func (m *Helper) createResource(c RESTClient, resource, namespace string, data []byte) (runtime.Object, error) {
return c.Post().Namespace(namespace).Resource(resource).Body(data).Do().Get() 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) { 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) obj, err := m.Codec.Decode(data)
if err != nil { if err != nil {
// We don't know how to handle this object, but update it anyway // 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. // Attempt to version the object based on client logic.
version, err := m.Versioner.ResourceVersion(obj) version, err := m.Versioner.ResourceVersion(obj)
if err != nil { if err != nil {
// We don't know how to version this object, so send it to the server as is // 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 { if version == "" && overwrite {
// Retrieve the current version of the object to overwrite the server object // 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() serverObj, err := c.Get().Namespace(namespace).Resource(m.Resource).Name(name).Do().Get()
if err != nil { if err != nil {
// The object does not exist, but we want it to be created // 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) serverVersion, err := m.Versioner.ResourceVersion(serverObj)
if err != nil { if err != nil {
@ -164,9 +168,9 @@ func (m *Helper) Update(namespace, name string, overwrite bool, data []byte) (ru
data = newData 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) { func (m *Helper) 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() return c.Put().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(resource).Name(name).Body(data).Do().Get()
} }

View File

@ -75,6 +75,10 @@ func TestHelperDelete(t *testing.T) {
return false return false
} }
parts := splitPath(req.URL.Path) 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" { if parts[1] != "bar" {
t.Errorf("url doesn't contain namespace: %#v", req) t.Errorf("url doesn't contain namespace: %#v", req)
return false return false
@ -95,6 +99,7 @@ func TestHelperDelete(t *testing.T) {
} }
modifier := &Helper{ modifier := &Helper{
RESTClient: client, RESTClient: client,
NamespaceScoped: true,
} }
err := modifier.Delete("bar", "foo") err := modifier.Delete("bar", "foo")
if (err != nil) != test.Err { if (err != nil) != test.Err {
@ -188,6 +193,7 @@ func TestHelperCreate(t *testing.T) {
RESTClient: client, RESTClient: client,
Codec: testapi.Codec(), Codec: testapi.Codec(),
Versioner: testapi.MetadataAccessor(), Versioner: testapi.MetadataAccessor(),
NamespaceScoped: true,
} }
data := []byte{} data := []byte{}
if test.Object != nil { if test.Object != nil {
@ -268,6 +274,7 @@ func TestHelperGet(t *testing.T) {
} }
modifier := &Helper{ modifier := &Helper{
RESTClient: client, RESTClient: client,
NamespaceScoped: true,
} }
obj, err := modifier.Get("bar", "foo") obj, err := modifier.Get("bar", "foo")
if (err != nil) != test.Err { if (err != nil) != test.Err {
@ -338,6 +345,7 @@ func TestHelperList(t *testing.T) {
} }
modifier := &Helper{ modifier := &Helper{
RESTClient: client, RESTClient: client,
NamespaceScoped: true,
} }
obj, err := modifier.List("bar", labels.SelectorFromSet(labels.Set{"foo": "baz"})) obj, err := modifier.List("bar", labels.SelectorFromSet(labels.Set{"foo": "baz"}))
if (err != nil) != test.Err { if (err != nil) != test.Err {
@ -443,6 +451,7 @@ func TestHelperUpdate(t *testing.T) {
RESTClient: client, RESTClient: client,
Codec: testapi.Codec(), Codec: testapi.Codec(),
Versioner: testapi.MetadataAccessor(), Versioner: testapi.MetadataAccessor(),
NamespaceScoped: true,
} }
data := []byte{} data := []byte{}
if test.Object != nil { if test.Object != nil {

View File

@ -260,7 +260,7 @@ func TestSyncEndpointsItemsEmptySelectorSelectsAll(t *testing.T) {
serviceList := api.ServiceList{ serviceList := api.ServiceList{
Items: []api.Service{ Items: []api.Service{
{ {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{}, Selector: map[string]string{},
}, },
@ -290,14 +290,14 @@ func TestSyncEndpointsItemsEmptySelectorSelectsAll(t *testing.T) {
}, },
Endpoints: []string{"1.2.3.4:8080"}, 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) { func TestSyncEndpointsItemsPreexisting(t *testing.T) {
serviceList := api.ServiceList{ serviceList := api.ServiceList{
Items: []api.Service{ Items: []api.Service{
{ {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{
"foo": "bar", "foo": "bar",
@ -329,14 +329,14 @@ func TestSyncEndpointsItemsPreexisting(t *testing.T) {
}, },
Endpoints: []string{"1.2.3.4:8080"}, 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) { func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) {
serviceList := api.ServiceList{ serviceList := api.ServiceList{
Items: []api.Service{ Items: []api.Service{
{ {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{
"foo": "bar", "foo": "bar",
@ -360,14 +360,14 @@ func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) {
if err := endpoints.SyncServiceEndpoints(); err != nil { if err := endpoints.SyncServiceEndpoints(); err != nil {
t.Errorf("unexpected error: %v", err) 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) { func TestSyncEndpointsItems(t *testing.T) {
serviceList := api.ServiceList{ serviceList := api.ServiceList{
Items: []api.Service{ Items: []api.Service{
{ {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{
"foo": "bar", "foo": "bar",
@ -392,7 +392,7 @@ func TestSyncEndpointsItems(t *testing.T) {
}, },
Endpoints: []string{"1.2.3.4:8080"}, 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) { func TestSyncEndpointsPodError(t *testing.T) {

View File

@ -277,7 +277,13 @@ func TestBind(t *testing.T) {
table := []struct { table := []struct {
binding *api.Binding 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 { for _, item := range table {
@ -296,7 +302,7 @@ func TestBind(t *testing.T) {
continue continue
} }
expectedBody := runtime.EncodeOrDie(testapi.Codec(), item.binding) 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)
} }
} }