Commit 7b72d953 authored by Brian Grant's avatar Brian Grant

Merge pull request #5054 from smarterclayton/switch_to_sub_bindings

Make Pod binding a subresource (and be generic to other types)
parents 9e253170 dfc19185
......@@ -101,6 +101,10 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer {
j.Spec = api.PodSpec{}
c.Fuzz(&j.Spec)
},
func(j *api.Binding, c fuzz.Continue) {
c.Fuzz(&j.ObjectMeta)
j.Target.Name = c.RandString()
},
func(j *api.ReplicationControllerSpec, c fuzz.Continue) {
c.FuzzNoCustom(j) // fuzz self without calling this function again
j.TemplateRef = nil // this is required for round trip
......
......@@ -918,13 +918,14 @@ type NamespaceList struct {
Items []Namespace `json:"items"`
}
// Binding is written by a scheduler to cause a pod to be bound to a host.
// Binding ties one object to another - for example, a pod is bound to a node by a scheduler.
type Binding struct {
TypeMeta `json:",inline"`
TypeMeta `json:",inline"`
// ObjectMeta describes the object that is being bound.
ObjectMeta `json:"metadata,omitempty"`
PodID string `json:"podID"`
Host string `json:"host"`
// Target is the object to bind to.
Target ObjectReference `json:"target"`
}
// Status is a return value for calls that don't return other objects.
......
......@@ -1324,6 +1324,24 @@ func init() {
return nil
},
func(in *Binding, out *newer.Binding, s conversion.Scope) error {
if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil {
return err
}
out.Target = newer.ObjectReference{
Name: in.Host,
}
out.Name = in.PodID
return nil
},
func(in *newer.Binding, out *Binding, s conversion.Scope) error {
if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil {
return err
}
out.Host = in.Target.Name
out.PodID = in.Name
return nil
},
)
if err != nil {
// If one of the conversion functions is malformed, detect it immediately.
......
......@@ -1240,6 +1240,24 @@ func init() {
return nil
},
func(in *Binding, out *newer.Binding, s conversion.Scope) error {
if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil {
return err
}
out.Target = newer.ObjectReference{
Name: in.Host,
}
out.Name = in.PodID
return nil
},
func(in *newer.Binding, out *Binding, s conversion.Scope) error {
if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil {
return err
}
out.Host = in.Target.Name
out.PodID = in.Name
return nil
},
)
if err != nil {
// If one of the conversion functions is malformed, detect it immediately.
......
......@@ -940,16 +940,14 @@ type NamespaceList struct {
Items []Namespace `json:"items" description:"items is the list of Namespace objects in the list"`
}
// Binding is written by a scheduler to cause a pod to be bound to a node. Name is not
// required for Bindings.
// Binding ties one object to another - for example, a pod is bound to a node by a scheduler.
type Binding struct {
TypeMeta `json:",inline"`
TypeMeta `json:",inline"`
// ObjectMeta describes the object that is being bound.
ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"`
// PodID is a Pod name to be bound to a node.
PodID string `json:"podID" description:"name of the pod to be bound to a node"`
// Host is the name of a node to bind to.
Host string `json:"host" description:"name of the node to bind to"`
// Target is the object to bind to.
Target ObjectReference `json:"target" description:"an object to bind to"`
}
// Status is a return value for calls that don't return other objects.
......
......@@ -29,7 +29,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
......@@ -91,72 +90,38 @@ type Mux interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}
// defaultAPIServer exposes nested objects for testability.
type defaultAPIServer struct {
http.Handler
group *APIGroupVersion
}
// Handle returns a Handler function that exposes the provided storage interfaces
// as RESTful resources at prefix, serialized by codec, and also includes the support
// http resources.
// Note: This method is used only in tests.
func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, linker runtime.SelfLinker, admissionControl admission.Interface, contextMapper api.RequestContextMapper, mapper meta.RESTMapper) http.Handler {
prefix := path.Join(root, version)
group := NewAPIGroupVersion(storage, codec, root, prefix, linker, admissionControl, contextMapper, mapper)
container := restful.NewContainer()
container.Router(restful.CurlyRouter{})
mux := container.ServeMux
group.InstallREST(container, root, version)
ws := new(restful.WebService)
InstallSupport(mux, ws)
container.Add(ws)
return &defaultAPIServer{mux, group}
}
// APIGroupVersion is a helper for exposing RESTStorage objects as http.Handlers via go-restful
// It handles URLs of the form:
// /${storage_key}[/${object_name}]
// Where 'storage_key' points to a RESTStorage object stored in storage.
type APIGroupVersion struct {
storage map[string]RESTStorage
codec runtime.Codec
prefix string
linker runtime.SelfLinker
admit admission.Interface
context api.RequestContextMapper
mapper meta.RESTMapper
// TODO: put me into a cleaner interface
info *APIRequestInfoResolver
}
Storage map[string]RESTStorage
// NewAPIGroupVersion returns an object that will serve a set of REST resources and their
// associated operations. The provided codec controls serialization and deserialization.
// This is a helper method for registering multiple sets of REST handlers under different
// prefixes onto a server.
// TODO: add multitype codec serialization
func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, root, prefix string, linker runtime.SelfLinker, admissionControl admission.Interface, contextMapper api.RequestContextMapper, mapper meta.RESTMapper) *APIGroupVersion {
return &APIGroupVersion{
storage: storage,
codec: codec,
prefix: prefix,
linker: linker,
admit: admissionControl,
context: contextMapper,
mapper: mapper,
info: &APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(root, "/")), latest.RESTMapper},
}
Root string
Version string
Mapper meta.RESTMapper
Codec runtime.Codec
Typer runtime.ObjectTyper
Creater runtime.ObjectCreater
Linker runtime.SelfLinker
Admit admission.Interface
Context api.RequestContextMapper
}
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
// in a slash. A restful WebService is created for the group and version.
func (g *APIGroupVersion) InstallREST(container *restful.Container, root string, version string) error {
prefix := path.Join(root, version)
func (g *APIGroupVersion) InstallREST(container *restful.Container) error {
info := &APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(g.Root, "/")), g.Mapper}
prefix := path.Join(g.Root, g.Version)
installer := &APIInstaller{
group: g,
prefix: prefix,
version: version,
group: g,
info: info,
prefix: prefix,
}
ws, registrationErrors := installer.Install()
container.Add(ws)
......
......@@ -280,14 +280,10 @@ func TestProxy(t *testing.T) {
expectedResourceNamespace: item.reqNamespace,
}
namespaceHandler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper)
namespaceHandler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage})
namespaceServer := httptest.NewServer(namespaceHandler)
defer namespaceServer.Close()
legacyNamespaceHandler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, legacyNamespaceMapper)
legacyNamespaceHandler := handle(map[string]RESTStorage{"foo": simpleStorage})
legacyNamespaceServer := httptest.NewServer(legacyNamespaceHandler)
defer legacyNamespaceServer.Close()
......@@ -296,8 +292,8 @@ func TestProxy(t *testing.T) {
server *httptest.Server
proxyTestPattern string
}{
{namespaceServer, "/prefix/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path},
{legacyNamespaceServer, "/prefix/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace},
{namespaceServer, "/api/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path},
{legacyNamespaceServer, "/api/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace},
}
for _, serverPattern := range serverPatterns {
......@@ -344,14 +340,12 @@ func TestProxyUpgrade(t *testing.T) {
expectedResourceNamespace: "myns",
}
namespaceHandler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper)
namespaceHandler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage})
server := httptest.NewServer(namespaceHandler)
defer server.Close()
ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/prefix/version/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/")
ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/api/version/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/")
if err != nil {
t.Fatalf("websocket dial err: %s", err)
}
......
......@@ -29,9 +29,7 @@ func TestRedirect(t *testing.T) {
errors: map[string]error{},
expectedResourceNamespace: "default",
}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, mapper)
handler := handle(map[string]RESTStorage{"foo": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
......@@ -54,7 +52,7 @@ func TestRedirect(t *testing.T) {
for _, item := range table {
simpleStorage.errors["resourceLocation"] = item.err
simpleStorage.resourceLocation = item.id
resp, err := client.Get(server.URL + "/prefix/version/redirect/foo/" + item.id)
resp, err := client.Get(server.URL + "/api/version/redirect/foo/" + item.id)
if resp == nil {
t.Fatalf("Unexpected nil resp")
}
......@@ -82,9 +80,7 @@ func TestRedirectWithNamespaces(t *testing.T) {
errors: map[string]error{},
expectedResourceNamespace: "other",
}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper)
handler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
......@@ -107,7 +103,7 @@ func TestRedirectWithNamespaces(t *testing.T) {
for _, item := range table {
simpleStorage.errors["resourceLocation"] = item.err
simpleStorage.resourceLocation = item.id
resp, err := client.Get(server.URL + "/prefix/version/redirect/namespaces/other/foo/" + item.id)
resp, err := client.Get(server.URL + "/api/version/redirect/namespaces/other/foo/" + item.id)
if resp == nil {
t.Fatalf("Unexpected nil resp")
}
......
......@@ -17,6 +17,7 @@ limitations under the License.
package apiserver
import (
"fmt"
"net/http"
"net/url"
gpath "path"
......@@ -97,7 +98,7 @@ func parseSelectorQueryParams(query url.Values, version, apiResource string) (la
}
// ListResource returns a function that handles retrieving a list of resources from a RESTStorage object.
func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, requestInfoResolver *APIRequestInfoResolver) restful.RouteFunction {
func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, version, apiResource string) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
......@@ -109,13 +110,7 @@ func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runti
ctx := ctxFn(req)
ctx = api.WithNamespace(ctx, namespace)
requestInfo, err := requestInfoResolver.GetAPIRequestInfo(req.Request)
if err != nil {
errorJSON(err, codec, w)
return
}
label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), requestInfo.APIVersion, requestInfo.Resource)
label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), version, apiResource)
if err != nil {
errorJSON(err, codec, w)
return
......@@ -135,7 +130,7 @@ func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runti
}
// CreateResource returns a function that will handle a resource creation.
func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource string, admit admission.Interface) restful.RouteFunction {
func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
......@@ -158,6 +153,7 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec ru
obj := r.New()
if err := codec.DecodeInto(body, obj); err != nil {
err = transformDecodeError(typer, err, obj, body)
errorJSON(err, codec, w)
return
}
......@@ -190,7 +186,7 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec ru
}
// UpdateResource returns a function that will handle a resource update
func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource string, admit admission.Interface) restful.RouteFunction {
func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
......@@ -213,6 +209,7 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec ru
obj := r.New()
if err := codec.DecodeInto(body, obj); err != nil {
err = transformDecodeError(typer, err, obj, body)
errorJSON(err, codec, w)
return
}
......@@ -346,6 +343,18 @@ func finishRequest(timeout time.Duration, fn resultFunc) (result runtime.Object,
}
}
// transformDecodeError adds additional information when a decode fails.
func transformDecodeError(typer runtime.ObjectTyper, baseErr error, into runtime.Object, body []byte) error {
_, kind, err := typer.ObjectVersionAndKind(into)
if err != nil {
return err
}
if version, dataKind, err := typer.DataVersionAndKind(body); err == nil && len(dataKind) > 0 {
return errors.NewBadRequest(fmt.Sprintf("%s in version %s cannot be handled as a %s: %v", dataKind, version, kind, baseErr))
}
return errors.NewBadRequest(fmt.Sprintf("the object provided is unrecognized (must be of type %s): %v", kind, baseErr))
}
// setSelfLink sets the self link of an object (or the child items in a list) to the base URL of the request
// plus the path and query generated by the provided linkFunc
func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error {
......
......@@ -49,9 +49,7 @@ var watchTestTable = []struct {
func TestWatchWebsocket(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
_ = ResourceWatcher(simpleStorage) // Give compile error if this doesn't work.
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper)
handler := handle(map[string]RESTStorage{"foo": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
......@@ -103,9 +101,7 @@ func TestWatchWebsocket(t *testing.T) {
func TestWatchHTTP(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper)
handler := handle(map[string]RESTStorage{"foo": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
......@@ -170,9 +166,7 @@ func TestWatchParamParsing(t *testing.T) {
return label, value, nil
})
simpleStorage := &SimpleRESTStorage{}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/api", testVersion, selfLinker, admissionControl, requestContextMapper, mapper)
handler := handle(map[string]RESTStorage{"foo": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
......@@ -242,9 +236,7 @@ func TestWatchParamParsing(t *testing.T) {
func TestWatchProtocolSelection(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper)
handler := handle(map[string]RESTStorage{"foo": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
defer server.CloseClientConnections()
......
......@@ -52,3 +52,8 @@ func (c *FakePods) Update(pod *api.Pod) (*api.Pod, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-pod", Value: pod.Name})
return &api.Pod{}, nil
}
func (c *FakePods) Bind(bind *api.Binding) error {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "bind-pod", Value: bind.Name})
return nil
}
......@@ -36,6 +36,8 @@ type PodInterface interface {
Delete(name string) error
Create(pod *api.Pod) (*api.Pod, error)
Update(pod *api.Pod) (*api.Pod, error)
Bind(binding *api.Binding) error
}
// pods implements PodsNamespacer interface
......@@ -92,3 +94,8 @@ func (c *pods) Update(pod *api.Pod) (result *api.Pod, err error) {
err = c.r.Put().Namespace(c.ns).Resource("pods").Name(pod.Name).Body(pod).Do().Into(result)
return
}
// Bind applies the provided binding to the named pod in the current namespace (binding.Namespace is ignored).
func (c *pods) Bind(binding *api.Binding) error {
return c.r.Post().Namespace(c.ns).Resource("pods").Name(binding.Name).SubResource("binding").Body(binding).Do().Error()
}
......@@ -105,6 +105,7 @@ type Request struct {
namespaceSet bool
resource string
resourceName string
subresource string
selector labels.Selector
timeout time.Duration
......@@ -166,6 +167,21 @@ func (r *Request) Resource(resource string) *Request {
return r
}
// SubResource sets a sub-resource path which can be multiple segments segment after the resource
// name but before the suffix.
func (r *Request) SubResource(subresources ...string) *Request {
if r.err != nil {
return r
}
subresource := path.Join(subresources...)
if len(r.subresource) != 0 {
r.err = fmt.Errorf("subresource already set to %q, cannot change to %q", r.resource, subresource)
return r
}
r.subresource = subresource
return r
}
// Name sets the name of a resource to access (<resource>/[ns/<namespace>/]<name>)
func (r *Request) Name(resourceName string) *Request {
if r.err != nil {
......@@ -360,8 +376,8 @@ func (r *Request) finalURL() string {
p = path.Join(p, resource)
}
// Join trims trailing slashes, so preserve r.path's trailing slash for backwards compat if nothing was changed
if len(r.resourceName) != 0 || len(r.subpath) != 0 {
p = path.Join(p, r.resourceName, r.subpath)
if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 {
p = path.Join(p, r.resourceName, r.subresource, r.subpath)
}
finalURL := *r.baseURL
......
......@@ -129,6 +129,16 @@ func TestRequestOrdersNamespaceInPath(t *testing.T) {
}
}
func TestRequestOrdersSubResource(t *testing.T) {
r := (&Request{
baseURL: &url.URL{},
path: "/test/",
}).Name("bar").Resource("baz").Namespace("foo").Suffix("test").SubResource("a", "b")
if s := r.finalURL(); s != "/test/namespaces/foo/baz/bar/a/b/test" {
t.Errorf("namespace should be in order in path: %s", s)
}
}
func TestRequestSetTwiceError(t *testing.T) {
if (&Request{}).Name("bar").Name("baz").err == nil {
t.Errorf("setting name twice should result in error")
......@@ -139,6 +149,9 @@ func TestRequestSetTwiceError(t *testing.T) {
if (&Request{}).Resource("bar").Resource("baz").err == nil {
t.Errorf("setting resource twice should result in error")
}
if (&Request{}).SubResource("bar").SubResource("baz").err == nil {
t.Errorf("setting subresource twice should result in error")
}
}
func TestRequestParseSelectorParam(t *testing.T) {
......
......@@ -30,7 +30,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3"
......@@ -55,7 +54,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/ui"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
......@@ -406,9 +404,10 @@ func (m *Master) init(c *Config) {
// TODO: Factor out the core API registration
m.storage = map[string]apiserver.RESTStorage{
"pods": podStorage,
"pods/status": podStatusStorage,
"bindings": bindingStorage,
"pods": podStorage,
"pods/status": podStatusStorage,
"pods/binding": bindingStorage,
"bindings": bindingStorage,
"replicationControllers": controller.NewREST(registry, podRegistry),
"services": service.NewREST(m.serviceRegistry, c.Cloud, m.nodeRegistry, m.portalNet, c.ClusterName),
......@@ -425,14 +424,14 @@ func (m *Master) init(c *Config) {
}
apiVersions := []string{"v1beta1", "v1beta2"}
if err := apiserver.NewAPIGroupVersion(m.api_v1beta1()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta1"); err != nil {
if err := m.api_v1beta1().InstallREST(m.handlerContainer); err != nil {
glog.Fatalf("Unable to setup API v1beta1: %v", err)
}
if err := apiserver.NewAPIGroupVersion(m.api_v1beta2()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta2"); err != nil {
if err := m.api_v1beta2().InstallREST(m.handlerContainer); err != nil {
glog.Fatalf("Unable to setup API v1beta2: %v", err)
}
if c.EnableV1Beta3 {
if err := apiserver.NewAPIGroupVersion(m.api_v1beta3()).InstallREST(m.handlerContainer, c.APIPrefix, "v1beta3"); err != nil {
if err := m.api_v1beta3().InstallREST(m.handlerContainer); err != nil {
glog.Fatalf("Unable to setup API v1beta3: %v", err)
}
apiVersions = []string{"v1beta1", "v1beta2", "v1beta3"}
......@@ -569,26 +568,49 @@ func (m *Master) getServersToValidate(c *Config) map[string]apiserver.Server {
return serversToValidate
}
func (m *Master) defaultAPIGroupVersion() *apiserver.APIGroupVersion {
return &apiserver.APIGroupVersion{
Root: m.apiPrefix,
Mapper: latest.RESTMapper,
Creater: api.Scheme,
Typer: api.Scheme,
Linker: latest.SelfLinker,
Admit: m.admissionControl,
Context: m.requestContextMapper,
}
}
// api_v1beta1 returns the resources and codec for API version v1beta1.
func (m *Master) api_v1beta1() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, api.RequestContextMapper, meta.RESTMapper) {
func (m *Master) api_v1beta1() *apiserver.APIGroupVersion {
storage := make(map[string]apiserver.RESTStorage)
for k, v := range m.storage {
storage[k] = v
}
return storage, v1beta1.Codec, "api", "/api/v1beta1", latest.SelfLinker, m.admissionControl, m.requestContextMapper, latest.RESTMapper
version := m.defaultAPIGroupVersion()
version.Storage = storage
version.Version = "v1beta1"
version.Codec = v1beta1.Codec
return version
}
// api_v1beta2 returns the resources and codec for API version v1beta2.
func (m *Master) api_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, api.RequestContextMapper, meta.RESTMapper) {
func (m *Master) api_v1beta2() *apiserver.APIGroupVersion {
storage := make(map[string]apiserver.RESTStorage)
for k, v := range m.storage {
storage[k] = v
}
return storage, v1beta2.Codec, "api", "/api/v1beta2", latest.SelfLinker, m.admissionControl, m.requestContextMapper, latest.RESTMapper
version := m.defaultAPIGroupVersion()
version.Storage = storage
version.Version = "v1beta2"
version.Codec = v1beta2.Codec
return version
}
// api_v1beta3 returns the resources and codec for API version v1beta3.
func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, api.RequestContextMapper, meta.RESTMapper) {
func (m *Master) api_v1beta3() *apiserver.APIGroupVersion {
storage := make(map[string]apiserver.RESTStorage)
for k, v := range m.storage {
if k == "minions" {
......@@ -596,5 +618,9 @@ func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec,
}
storage[strings.ToLower(k)] = v
}
return storage, v1beta3.Codec, "api", "/api/v1beta3", latest.SelfLinker, m.admissionControl, m.requestContextMapper, latest.RESTMapper
version := m.defaultAPIGroupVersion()
version.Storage = storage
version.Version = "v1beta3"
version.Codec = v1beta3.Codec
return version
}
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package binding contains the middle layer logic for bindings.
// Bindings are objects containing instructions for how a pod ought to
// be bound to a host. This allows a registry object which supports this
// action (ApplyBinding) to be served through an apiserver.
package binding
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package binding
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// MockRegistry can be used for testing.
type MockRegistry struct {
OnApplyBinding func(binding *api.Binding) error
}
func (mr MockRegistry) ApplyBinding(ctx api.Context, binding *api.Binding) error {
return mr.OnApplyBinding(binding)
}
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package binding
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// Registry contains the functions needed to support a BindingStorage.
type Registry interface {
// ApplyBinding should apply the binding. That is, it should actually
// assign or place pod binding.PodID on machine binding.Host.
ApplyBinding(ctx api.Context, binding *api.Binding) error
}
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package binding
import (
"fmt"
"net/http"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
// REST implements the RESTStorage interface for bindings. When bindings are written, it
// changes the location of the affected pods. This information is eventually reflected
// in the pod's CurrentState.Host field.
type REST struct {
registry Registry
}
// NewREST creates a new REST backed by the given bindingRegistry.
func NewREST(bindingRegistry Registry) *REST {
return &REST{
registry: bindingRegistry,
}
}
// New returns a new binding object fit for having data unmarshalled into it.
func (*REST) New() runtime.Object {
return &api.Binding{}
}
// Create attempts to make the assignment indicated by the binding it recieves.
func (b *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) {
binding, ok := obj.(*api.Binding)
if !ok {
return nil, fmt.Errorf("incorrect type: %#v", obj)
}
if err := b.registry.ApplyBinding(ctx, binding); err != nil {
return nil, err
}
return &api.Status{Status: api.StatusSuccess, Code: http.StatusCreated}, nil
}
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package binding
import (
"errors"
"net/http"
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
)
func TestNewREST(t *testing.T) {
mockRegistry := MockRegistry{
OnApplyBinding: func(b *api.Binding) error { return nil },
}
b := NewREST(mockRegistry)
binding := &api.Binding{
PodID: "foo",
Host: "bar",
}
body, err := latest.Codec.Encode(binding)
if err != nil {
t.Fatalf("Unexpected encode error %v", err)
}
obj := b.New()
err = latest.Codec.DecodeInto(body, obj)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if e, a := binding, obj; !reflect.DeepEqual(e, a) {
t.Errorf("Expected %#v, but got %#v", e, a)
}
}
func TestRESTPost(t *testing.T) {
table := []struct {
b *api.Binding
err error
}{
{b: &api.Binding{PodID: "foo", Host: "bar"}, err: errors.New("no host bar")},
{b: &api.Binding{PodID: "baz", Host: "qux"}, err: nil},
{b: &api.Binding{PodID: "dvorak", Host: "qwerty"}, err: nil},
}
for i, item := range table {
mockRegistry := MockRegistry{
OnApplyBinding: func(b *api.Binding) error {
if !reflect.DeepEqual(item.b, b) {
t.Errorf("%v: expected %#v, but got %#v", i, item, b)
}
return item.err
},
}
ctx := api.NewContext()
b := NewREST(mockRegistry)
result, err := b.Create(ctx, item.b)
if err != nil && item.err == nil {
t.Errorf("Unexpected error %v", err)
continue
}
if err == nil && item.err != nil {
t.Errorf("Unexpected error %v", err)
continue
}
var expect interface{}
if item.err == nil {
expect = &api.Status{Status: api.StatusSuccess, Code: http.StatusCreated}
}
if e, a := expect, result; !reflect.DeepEqual(e, a) {
t.Errorf("%v: expected %#v, got %#v", i, e, a)
}
}
}
......@@ -20,6 +20,7 @@ import (
"fmt"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
etcderr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/constraint"
......@@ -146,7 +147,14 @@ func (r *BindingREST) New() runtime.Object {
// Create ensures a pod is bound to a specific host.
func (r *BindingREST) Create(ctx api.Context, obj runtime.Object) (out runtime.Object, err error) {
binding := obj.(*api.Binding)
err = r.assignPod(ctx, binding.PodID, binding.Host)
// TODO: move me to a binding strategy
if len(binding.Target.Kind) != 0 && (binding.Target.Kind != "Node" && binding.Target.Kind != "Minion") {
return nil, errors.NewInvalid("binding", binding.Name, errors.ValidationErrorList{errors.NewFieldInvalid("to.kind", binding.Target.Kind, "must be empty, 'Node', or 'Minion'")})
}
if len(binding.Target.Name) == 0 {
return nil, errors.NewInvalid("binding", binding.Name, errors.ValidationErrorList{errors.NewFieldRequired("to.name", binding.Target.Name)})
}
err = r.assignPod(ctx, binding.Name, binding.Target.Name)
err = etcderr.InterpretCreateError(err, "binding", "")
out = &api.Status{Status: api.StatusSuccess}
return
......
......@@ -817,7 +817,10 @@ func TestEtcdCreate(t *testing.T) {
}
// Suddenly, a wild scheduler appears:
_, err = bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine", ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault}})
_, err = bindingRegistry.Create(ctx, &api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
......@@ -865,7 +868,10 @@ func TestEtcdCreateBindingNoPod(t *testing.T) {
// - Create (apiserver)
// - Schedule (scheduler)
// - Delete (apiserver)
_, err := bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine", ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault}})
_, err := bindingRegistry.Create(ctx, &api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine"},
})
if err == nil {
t.Fatalf("Expected not-found-error but got nothing")
}
......@@ -935,7 +941,10 @@ func TestEtcdCreateWithContainersError(t *testing.T) {
}
// Suddenly, a wild scheduler appears:
_, err = bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine"})
_, err = bindingRegistry.Create(ctx, &api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine"},
})
if !errors.IsAlreadyExists(err) {
t.Fatalf("Unexpected error returned: %#v", err)
}
......@@ -973,7 +982,10 @@ func TestEtcdCreateWithContainersNotFound(t *testing.T) {
}
// Suddenly, a wild scheduler appears:
_, err = bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine"})
_, err = bindingRegistry.Create(ctx, &api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
......@@ -1025,7 +1037,10 @@ func TestEtcdCreateWithExistingContainers(t *testing.T) {
}
// Suddenly, a wild scheduler appears:
_, err = bindingRegistry.Create(ctx, &api.Binding{PodID: "foo", Host: "machine"})
_, err = bindingRegistry.Create(ctx, &api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
......@@ -1055,6 +1070,70 @@ func TestEtcdCreateWithExistingContainers(t *testing.T) {
}
}
func TestEtcdCreateBinding(t *testing.T) {
registry, bindingRegistry, _, fakeClient, _ := newStorage(t)
ctx := api.NewDefaultContext()
fakeClient.TestIndex = true
testCases := map[string]struct {
binding api.Binding
errOK func(error) bool
}{
"noName": {
binding: api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{},
},
errOK: func(err error) bool { return errors.IsInvalid(err) },
},
"badKind": {
binding: api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine", Kind: "unknown"},
},
errOK: func(err error) bool { return errors.IsInvalid(err) },
},
"emptyKind": {
binding: api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine"},
},
errOK: func(err error) bool { return err == nil },
},
"kindNode": {
binding: api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine", Kind: "Node"},
},
errOK: func(err error) bool { return err == nil },
},
"kindMinion": {
binding: api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault, Name: "foo"},
Target: api.ObjectReference{Name: "machine", Kind: "Minion"},
},
errOK: func(err error) bool { return err == nil },
},
}
for k, test := range testCases {
key, _ := registry.store.KeyFunc(ctx, "foo")
fakeClient.Data[key] = tools.EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: tools.EtcdErrorNotFound,
}
fakeClient.Set("/registry/nodes/machine/boundpods", runtime.EncodeOrDie(latest.Codec, &api.BoundPods{}), 0)
if _, err := registry.Create(ctx, validNewPod()); err != nil {
t.Fatalf("%s: unexpected error: %v", k, err)
}
fakeClient.Set("/registry/nodes/machine/boundpods", runtime.EncodeOrDie(latest.Codec, &api.BoundPods{}), 0)
if _, err := bindingRegistry.Create(ctx, &test.binding); !test.errOK(err) {
t.Errorf("%s: unexpected error: %v", k, err)
}
}
}
func TestEtcdUpdateNotFound(t *testing.T) {
registry, _, _, fakeClient, _ := newStorage(t)
ctx := api.NewDefaultContext()
......
......@@ -45,6 +45,11 @@ type ObjectTyper interface {
ObjectVersionAndKind(Object) (version, kind string, err error)
}
// ObjectCreater contains methods for instantiating an object by kind and version.
type ObjectCreater interface {
New(version, kind string) (out Object, err error)
}
// ResourceVersioner provides methods for setting and retrieving
// the resource version from an API object.
type ResourceVersioner interface {
......
......@@ -287,9 +287,11 @@ type binder struct {
// Bind just does a POST binding RPC.
func (b *binder) Bind(binding *api.Binding) error {
glog.V(2).Infof("Attempting to bind %v to %v", binding.PodID, binding.Host)
glog.V(2).Infof("Attempting to bind %v to %v", binding.Name, binding.Target.Name)
ctx := api.WithNamespace(api.NewContext(), binding.Namespace)
return b.Post().Namespace(api.NamespaceValue(ctx)).Resource("bindings").Body(binding).Do().Error()
// TODO: use Pods interface for binding once clusters are upgraded
// return b.Pods(binding.Namespace).Bind(binding)
}
type clock interface {
......
......@@ -366,9 +366,11 @@ func TestBind(t *testing.T) {
{binding: &api.Binding{
ObjectMeta: api.ObjectMeta{
Namespace: api.NamespaceDefault,
Name: "foo",
},
Target: api.ObjectReference{
Name: "foohost.kubernetes.mydomain.com",
},
PodID: "foo",
Host: "foohost.kubernetes.mydomain.com",
}},
}
......
......@@ -80,9 +80,11 @@ func (s *Scheduler) scheduleOne() {
return
}
b := &api.Binding{
ObjectMeta: api.ObjectMeta{Namespace: pod.Namespace},
PodID: pod.Name,
Host: dest,
ObjectMeta: api.ObjectMeta{Namespace: pod.Namespace, Name: pod.Name},
Target: api.ObjectReference{
Kind: "Node",
Name: dest,
},
}
if err := s.config.Binder.Bind(b); err != nil {
glog.V(1).Infof("Failed to bind pod: %v", err)
......
......@@ -25,6 +25,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/record"
"github.com/GoogleCloudPlatform/kubernetes/pkg/scheduler"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
type fakeBinder struct {
......@@ -63,7 +64,7 @@ func TestScheduler(t *testing.T) {
{
sendPod: podWithID("foo"),
algo: mockScheduler{"machine1", nil},
expectBind: &api.Binding{PodID: "foo", Host: "machine1"},
expectBind: &api.Binding{ObjectMeta: api.ObjectMeta{Name: "foo"}, Target: api.ObjectReference{Kind: "Node", Name: "machine1"}},
eventReason: "scheduled",
}, {
sendPod: podWithID("foo"),
......@@ -74,7 +75,7 @@ func TestScheduler(t *testing.T) {
}, {
sendPod: podWithID("foo"),
algo: mockScheduler{"machine1", nil},
expectBind: &api.Binding{PodID: "foo", Host: "machine1"},
expectBind: &api.Binding{ObjectMeta: api.ObjectMeta{Name: "foo"}, Target: api.ObjectReference{Kind: "Node", Name: "machine1"}},
injectBindError: errB,
expectError: errB,
expectErrorPod: podWithID("foo"),
......@@ -120,7 +121,7 @@ func TestScheduler(t *testing.T) {
t.Errorf("%v: error: wanted %v, got %v", i, e, a)
}
if e, a := item.expectBind, gotBinding; !reflect.DeepEqual(e, a) {
t.Errorf("%v: error: wanted %v, got %v", i, e, a)
t.Errorf("%v: error: %s", i, util.ObjectDiff(e, a))
}
<-called
events.Stop()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment