From 374a7023f671b032b1ce6f11d9598970ee7bb954 Mon Sep 17 00:00:00 2001 From: Abhishek Gupta Date: Mon, 15 Sep 2014 13:38:42 -0700 Subject: [PATCH] Adding Route model and APIs The Route resource maps a frontend with a service to allow a router to route and/or load balance traffic to the service endpoints --- pkg/api/register.go | 1 + pkg/api/v1beta1/register.go | 1 + pkg/client/client.go | 52 +++++ pkg/client/fake.go | 31 +++ pkg/cmd/client/kubecfg.go | 5 + pkg/cmd/client/route/printer.go | 33 +++ pkg/cmd/master/master.go | 5 + pkg/route/api/register.go | 15 ++ pkg/route/api/types.go | 27 +++ pkg/route/api/v1beta1/register.go | 15 ++ pkg/route/api/v1beta1/types.go | 27 +++ pkg/route/api/validation/validation.go | 19 ++ pkg/route/doc.go | 22 ++ pkg/route/registry/etcd/etcd.go | 89 ++++++++ pkg/route/registry/etcd/etcd_test.go | 264 +++++++++++++++++++++++ pkg/route/registry/route/registry.go | 24 +++ pkg/route/registry/route/rest.go | 114 ++++++++++ pkg/route/registry/route/rest_test.go | 279 +++++++++++++++++++++++++ pkg/route/registry/test/route.go | 85 ++++++++ 19 files changed, 1108 insertions(+) create mode 100644 pkg/cmd/client/route/printer.go create mode 100644 pkg/route/api/register.go create mode 100644 pkg/route/api/types.go create mode 100644 pkg/route/api/v1beta1/register.go create mode 100644 pkg/route/api/v1beta1/types.go create mode 100644 pkg/route/api/validation/validation.go create mode 100644 pkg/route/doc.go create mode 100644 pkg/route/registry/etcd/etcd.go create mode 100644 pkg/route/registry/etcd/etcd_test.go create mode 100644 pkg/route/registry/route/registry.go create mode 100644 pkg/route/registry/route/rest.go create mode 100644 pkg/route/registry/route/rest_test.go create mode 100644 pkg/route/registry/test/route.go diff --git a/pkg/api/register.go b/pkg/api/register.go index 5f322a571f81..b6f22f8fecfb 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -9,6 +9,7 @@ import ( _ "github.com/openshift/origin/pkg/deploy/api" _ "github.com/openshift/origin/pkg/image/api" _ "github.com/openshift/origin/pkg/template/api" + _ "github.com/openshift/origin/pkg/route/api" ) // Codec is the identity codec for this package - it can only convert itself diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index b74b1f2580cd..3716b7c1e9ec 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -9,6 +9,7 @@ import ( _ "github.com/openshift/origin/pkg/deploy/api/v1beta1" _ "github.com/openshift/origin/pkg/image/api/v1beta1" _ "github.com/openshift/origin/pkg/template/api/v1beta1" + _ "github.com/openshift/origin/pkg/route/api/v1beta1" ) // Codec encodes internal objects to the v1beta1 scheme diff --git a/pkg/client/client.go b/pkg/client/client.go index 8755b4653663..32f37e607af6 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -12,6 +12,7 @@ import ( buildapi "github.com/openshift/origin/pkg/build/api" deployapi "github.com/openshift/origin/pkg/deploy/api" imageapi "github.com/openshift/origin/pkg/image/api" + routeapi "github.com/openshift/origin/pkg/route/api" ) // Interface exposes methods on OpenShift resources. @@ -23,6 +24,7 @@ type Interface interface { ImageRepositoryMappingInterface DeploymentInterface DeploymentConfigInterface + RouteInterface } // BuildInterface exposes methods on Build resources. @@ -81,6 +83,16 @@ type DeploymentInterface interface { DeleteDeployment(string) error } +// RouteInterface exposes methods on Route resources +type RouteInterface interface { + ListRoutes(selector labels.Selector) (*routeapi.RouteList, error) + GetRoute(routeID string) (*routeapi.Route, error) + CreateRoute(route *routeapi.Route) (*routeapi.Route, error) + UpdateRoute(route *routeapi.Route) (*routeapi.Route, error) + DeleteRoute(routeID string) error + WatchRoutes(label, field labels.Selector, resourceVersion uint64) (watch.Interface, error) +} + // Client is an OpenShift client object type Client struct { *kubeclient.RESTClient @@ -295,3 +307,43 @@ func (c *Client) UpdateDeployment(deployment *deployapi.Deployment) (result *dep func (c *Client) DeleteDeployment(id string) error { return c.Delete().Path("deployments").Path(id).Do().Error() } + +// ListRoutes takes a selector, and returns the list of routes that match that selector +func (c *Client) ListRoutes(selector labels.Selector) (result *routeapi.RouteList, err error) { + err = c.Get().Path("routes").SelectorParam("labels", selector).Do().Into(result) + return +} + +// GetRoute takes the name of the route, and returns the corresponding Route object, and an error if it occurs +func (c *Client) GetRoute(name string) (result *routeapi.Route, err error) { + err = c.Get().Path("routes").Path(name).Do().Into(result) + return +} + +// DeleteRoute takes the name of the route, and returns an error if one occurs +func (c *Client) DeleteRoute(name string) error { + return c.Delete().Path("routes").Path(name).Do().Error() +} + +// CreateRoute takes the representation of a route. Returns the server's representation of the route, and an error, if it occurs +func (c *Client) CreateRoute(route *routeapi.Route) (result *routeapi.Route, err error) { + err = c.Post().Path("routes").Body(route).Do().Into(result) + return +} + +// UpdateRoute takes the representation of a route to update. Returns the server's representation of the route, and an error, if it occurs +func (c *Client) UpdateRoute(route *routeapi.Route) (result *routeapi.Route, err error) { + err = c.Put().Path("routes").Path(route.ID).Body(route).Do().Into(result) + return +} + +// WatchRoutes returns a watch.Interface that watches the requested routes. +func (c *Client) WatchRoutes(label, field labels.Selector, resourceVersion uint64) (watch.Interface, error) { + return c.Get(). + Path("watch"). + Path("routes"). + UintParam("resourceVersion", resourceVersion). + SelectorParam("labels", label). + SelectorParam("fields", field). + Watch() +} diff --git a/pkg/client/fake.go b/pkg/client/fake.go index e4f17fe365d1..01df600f56c6 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -7,6 +7,7 @@ import ( buildapi "github.com/openshift/origin/pkg/build/api" deployapi "github.com/openshift/origin/pkg/deploy/api" imageapi "github.com/openshift/origin/pkg/image/api" + routeapi "github.com/openshift/origin/pkg/route/api" ) type FakeAction struct { @@ -160,3 +161,33 @@ func (c *Fake) DeleteDeployment(id string) error { c.Actions = append(c.Actions, FakeAction{Action: "delete-deployment"}) return nil } + +func (c *Fake) ListRoutes(selector labels.Selector) (*routeapi.RouteList, error) { + c.Actions = append(c.Actions, FakeAction{Action: "list-routes"}) + return &routeapi.RouteList{}, nil +} + +func (c *Fake) GetRoute(id string) (*routeapi.Route, error) { + c.Actions = append(c.Actions, FakeAction{Action: "get-route"}) + return &routeapi.Route{}, nil +} + +func (c *Fake) CreateRoute(route *routeapi.Route) (*routeapi.Route, error) { + c.Actions = append(c.Actions, FakeAction{Action: "create-route"}) + return &routeapi.Route{}, nil +} + +func (c *Fake) UpdateRoute(route *routeapi.Route) (*routeapi.Route, error) { + c.Actions = append(c.Actions, FakeAction{Action: "update-route"}) + return &routeapi.Route{}, nil +} + +func (c *Fake) DeleteRoute(id string) error { + c.Actions = append(c.Actions, FakeAction{Action: "delete-route"}) + return nil +} + +func (c *Fake) WatchRoutes(field, label labels.Selector, resourceVersion uint64) (watch.Interface, error) { + c.Actions = append(c.Actions, FakeAction{Action: "watch-routes"}) + return nil, nil +} diff --git a/pkg/cmd/client/kubecfg.go b/pkg/cmd/client/kubecfg.go index 449a3d13ca1c..11bc350b7741 100644 --- a/pkg/cmd/client/kubecfg.go +++ b/pkg/cmd/client/kubecfg.go @@ -44,11 +44,13 @@ import ( . "github.com/openshift/origin/pkg/cmd/client/api" "github.com/openshift/origin/pkg/cmd/client/build" "github.com/openshift/origin/pkg/cmd/client/image" + "github.com/openshift/origin/pkg/cmd/client/route" "github.com/openshift/origin/pkg/config" configapi "github.com/openshift/origin/pkg/config/api" deployapi "github.com/openshift/origin/pkg/deploy/api" deployclient "github.com/openshift/origin/pkg/deploy/client" imageapi "github.com/openshift/origin/pkg/image/api" + routeapi "github.com/openshift/origin/pkg/route/api" ) type KubeConfig struct { @@ -126,6 +128,7 @@ var parser = kubecfg.NewParser(map[string]runtime.Object{ "config": &configapi.Config{}, "deployments": &deployapi.Deployment{}, "deploymentConfigs": &deployapi.DeploymentConfig{}, + "routes": &routeapi.Route{}, }) func prettyWireStorage() string { @@ -275,6 +278,7 @@ func (c *KubeConfig) Run() { "imageRepositoryMappings": {"ImageRepositoryMapping", client.RESTClient, latest.Codec}, "deployments": {"Deployment", client.RESTClient, latest.Codec}, "deploymentConfigs": {"DeploymentConfig", client.RESTClient, latest.Codec}, + "routes": {"Route", client.RESTClient, latest.Codec}, } matchFound := c.executeConfigRequest(method, clients) || c.executeTemplateRequest(method, client) || c.executeControllerRequest(method, kubeClient) || c.executeAPIRequest(method, clients) @@ -520,6 +524,7 @@ func humanReadablePrinter() *kubecfg.HumanReadablePrinter { build.RegisterPrintHandlers(printer) image.RegisterPrintHandlers(printer) deployclient.RegisterPrintHandlers(printer) + route.RegisterPrintHandlers(printer) return printer } diff --git a/pkg/cmd/client/route/printer.go b/pkg/cmd/client/route/printer.go new file mode 100644 index 000000000000..65219a9621f0 --- /dev/null +++ b/pkg/cmd/client/route/printer.go @@ -0,0 +1,33 @@ +package route + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubecfg" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + + "github.com/openshift/origin/pkg/route/api" +) + +var routeColumns = []string{"ID", "Host/Port", "Path", "Service", "Labels"} + +// RegisterPrintHandlers registers HumanReadablePrinter handlers +func RegisterPrintHandlers(printer *kubecfg.HumanReadablePrinter) { + printer.Handler(routeColumns, printRoute) + printer.Handler(routeColumns, printRouteList) +} + +func printRoute(route *api.Route, w io.Writer) error { + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", route.ID, route.Host, route.Path, route.ServiceName, labels.Set(route.Labels)) + return err +} + +func printRouteList(routeList *api.RouteList, w io.Writer) error { + for _, route := range routeList.Items { + if err := printRoute(&route, w); err != nil { + return err + } + } + return nil +} diff --git a/pkg/cmd/master/master.go b/pkg/cmd/master/master.go index 620b38f335f8..4fcabf1cc052 100644 --- a/pkg/cmd/master/master.go +++ b/pkg/cmd/master/master.go @@ -50,6 +50,8 @@ import ( "github.com/openshift/origin/pkg/image/registry/image" "github.com/openshift/origin/pkg/image/registry/imagerepository" "github.com/openshift/origin/pkg/image/registry/imagerepositorymapping" + routeregistry "github.com/openshift/origin/pkg/route/registry/route" + routeetcd "github.com/openshift/origin/pkg/route/registry/etcd" "github.com/openshift/origin/pkg/template" "github.com/openshift/origin/pkg/version" @@ -57,6 +59,7 @@ import ( _ "github.com/openshift/origin/pkg/config/api/v1beta1" _ "github.com/openshift/origin/pkg/image/api/v1beta1" _ "github.com/openshift/origin/pkg/template/api/v1beta1" + _ "github.com/openshift/origin/pkg/route/api/v1beta1" ) func NewCommandStartAllInOne(name string) *cobra.Command { @@ -214,6 +217,7 @@ func (c *config) runApiserver() { buildRegistry := buildetcd.New(etcdHelper) imageRegistry := imageetcd.New(etcdHelper) deployEtcd := deployetcd.New(etcdHelper) + routeEtcd := routeetcd.New(etcdHelper) // initialize OpenShift API storage := map[string]apiserver.RESTStorage{ @@ -225,6 +229,7 @@ func (c *config) runApiserver() { "deployments": deployregistry.NewREST(deployEtcd), "deploymentConfigs": deployconfigregistry.NewREST(deployEtcd), "templateConfigs": template.NewStorage(), + "routes": routeregistry.NewREST(routeEtcd), } osMux := http.NewServeMux() diff --git a/pkg/route/api/register.go b/pkg/route/api/register.go new file mode 100644 index 000000000000..9e363202bf76 --- /dev/null +++ b/pkg/route/api/register.go @@ -0,0 +1,15 @@ +package api + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +func init() { + api.Scheme.AddKnownTypes("", + &Route{}, + &RouteList{}, + ) +} + +func (*Route) IsAnAPIObject() {} +func (*RouteList) IsAnAPIObject() {} diff --git a/pkg/route/api/types.go b/pkg/route/api/types.go new file mode 100644 index 000000000000..cb9acb5c15c1 --- /dev/null +++ b/pkg/route/api/types.go @@ -0,0 +1,27 @@ +package api + +import ( + kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +// Route encapsulates the inputs needed to connect a DNS/alias to a service proxy. +type Route struct { + kubeapi.JSONBase `json:",inline" yaml:",inline"` + + // Required: Alias/DNS that points to the service + // Can be host or host:port + // host and port are combined to follow the net/url URL struct + Host string `json:"host" yaml:"host"` + // Optional: Path that the router watches for, to route traffic for to the service + Path string `json:"path,omitempty" yaml:"path,omitempty"` + + // the name of the service that this route points to + ServiceName string `json:"serviceName" yaml:"serviceName"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` +} + +// RouteList is a collection of Routes. +type RouteList struct { + kubeapi.JSONBase `json:",inline" yaml:",inline"` + Items []Route `json:"items,omitempty" yaml:"items,omitempty"` +} diff --git a/pkg/route/api/v1beta1/register.go b/pkg/route/api/v1beta1/register.go new file mode 100644 index 000000000000..461c421ccb62 --- /dev/null +++ b/pkg/route/api/v1beta1/register.go @@ -0,0 +1,15 @@ +package v1beta1 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +func init() { + api.Scheme.AddKnownTypes("v1beta1", + &Route{}, + &RouteList{}, + ) +} + +func (*Route) IsAnAPIObject() {} +func (*RouteList) IsAnAPIObject() {} diff --git a/pkg/route/api/v1beta1/types.go b/pkg/route/api/v1beta1/types.go new file mode 100644 index 000000000000..c68d1b6b38db --- /dev/null +++ b/pkg/route/api/v1beta1/types.go @@ -0,0 +1,27 @@ +package v1beta1 + +import ( + v1beta1 "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" +) + +// Route encapsulates the inputs needed to connect a DNS/alias to a service proxy. +type Route struct { + v1beta1.JSONBase `json:",inline" yaml:",inline"` + + // Required: Alias/DNS that points to the service + // Can be host or host:port + // host and port are combined to follow the net/url URL struct + Host string `json:"host" yaml:"host"` + // Optional: Path that the router watches for, to route traffic for to the service + Path string `json:"path,omitempty" yaml:"path,omitempty"` + + // the name of the service that this route points to + ServiceName string `json:"serviceName" yaml:"serviceName"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` +} + +// RouteList is a collection of Routes. +type RouteList struct { + v1beta1.JSONBase `json:",inline" yaml:",inline"` + Items []Route `json:"items,omitempty" yaml:"items,omitempty"` +} diff --git a/pkg/route/api/validation/validation.go b/pkg/route/api/validation/validation.go new file mode 100644 index 000000000000..f5b4ba3b1a5b --- /dev/null +++ b/pkg/route/api/validation/validation.go @@ -0,0 +1,19 @@ +package validation + +import ( + errs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + routeapi "github.com/openshift/origin/pkg/route/api" +) + +// ValidateRoute tests if required fields in the route are set. +func ValidateRoute(route *routeapi.Route) errs.ErrorList { + result := errs.ErrorList{} + + if len(route.Host) == 0 { + result = append(result, errs.NewFieldRequired("host", "")) + } + if len(route.ServiceName) == 0 { + result = append(result, errs.NewFieldRequired("serviceName", "")) + } + return result +} diff --git a/pkg/route/doc.go b/pkg/route/doc.go new file mode 100644 index 000000000000..3fe23b1332a7 --- /dev/null +++ b/pkg/route/doc.go @@ -0,0 +1,22 @@ +/* +Package route provides support for managing and watching routes. +It defines a Route resource type, along with associated storage. + +A Route object allows the user to specify a DNS / alias for a Kubernetes service. +It stores the ID of the Service (ServiceName) and the DNS/alias (Name). +The Route can be used to specify just the DNS/alias or it could also include +port and/or the path. + +The Route model includes the following attributes to specify the frontend URL: + - Host: Alias/DNS that points to the service. Can be host or host:port + - Path: Path allows the router to perform fine-grained routing + +The Route resources can be used by routers and load balancers to route external inbound +traffic. The proxy is expected to have frontend mappings for the Route.Name in its +configuration. For its endpoints, a proxy could either forward the traffic to the +Kubernetes Service port and let it do the load balancing and routing. Alternately, +a more meaningful implementation of a router could take the endpoints for the service +and route/load balance the incoming requests to the corresponding service endpoints. +*/ + +package route diff --git a/pkg/route/registry/etcd/etcd.go b/pkg/route/registry/etcd/etcd.go new file mode 100644 index 000000000000..76451684ffb4 --- /dev/null +++ b/pkg/route/registry/etcd/etcd.go @@ -0,0 +1,89 @@ +package etcd + +import ( + "fmt" + + etcderr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + "github.com/openshift/origin/pkg/route/api" +) + +// Etcd implements route.Registry backed by etcd. +type Etcd struct { + tools.EtcdHelper +} + +// New creates an etcd registry. +func New(helper tools.EtcdHelper) *Etcd { + return &Etcd{ + EtcdHelper: helper, + } +} + +func makeRouteKey(id string) string { + return "/routes/" + id +} + +// ListRoutes obtains a list of Routes. +func (registry *Etcd) ListRoutes(selector labels.Selector) (*api.RouteList, error) { + allRoutes := api.RouteList{} + err := registry.ExtractList("/routes", &allRoutes.Items, &allRoutes.ResourceVersion) + if err != nil { + return nil, err + } + filtered := []api.Route{} + for _, route := range allRoutes.Items { + if selector.Matches(labels.Set(route.Labels)) { + filtered = append(filtered, route) + } + } + allRoutes.Items = filtered + return &allRoutes, nil + +} + +// GetRoute gets a specific Route specified by its ID. +func (registry *Etcd) GetRoute(routeID string) (*api.Route, error) { + route := api.Route{} + err := registry.ExtractObj(makeRouteKey(routeID), &route, false) + if err != nil { + return nil, etcderr.InterpretGetError(err, "route", routeID) + } + return &route, nil +} + +// CreateRoute creates a new Route. +func (registry *Etcd) CreateRoute(route *api.Route) error { + err := registry.CreateObj(makeRouteKey(route.ID), route) + return etcderr.InterpretCreateError(err, "route", route.ID) +} + +// UpdateRoute replaces an existing Route. +func (registry *Etcd) UpdateRoute(route *api.Route) error { + err := registry.SetObj(makeRouteKey(route.ID), route) + return etcderr.InterpretUpdateError(err, "route", route.ID) +} + +// DeleteRoute deletes a Route specified by its ID. +func (registry *Etcd) DeleteRoute(routeID string) error { + key := makeRouteKey(routeID) + err := registry.Delete(key, true) + return etcderr.InterpretDeleteError(err, "route", routeID) +} + +// WatchRoutes begins watching for new, changed, or deleted route configurations. +func (registry *Etcd) WatchRoutes(label, field labels.Selector, resourceVersion uint64) (watch.Interface, error) { + if !label.Empty() { + return nil, fmt.Errorf("label selectors are not supported on routes yet") + } + if value, found := field.RequiresExactMatch("ID"); found { + return registry.Watch(makeRouteKey(value), resourceVersion) + } + if field.Empty() { + return registry.WatchList("/routes", resourceVersion, tools.Everything) + } + return nil, fmt.Errorf("only the 'ID' and default (everything) field selectors are supported") +} diff --git a/pkg/route/registry/etcd/etcd_test.go b/pkg/route/registry/etcd/etcd_test.go new file mode 100644 index 000000000000..7ecc4968599a --- /dev/null +++ b/pkg/route/registry/etcd/etcd_test.go @@ -0,0 +1,264 @@ +package etcd + +import ( + "fmt" + "testing" + + kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/coreos/go-etcd/etcd" + + "github.com/openshift/origin/pkg/api/latest" + "github.com/openshift/origin/pkg/route/api" + _ "github.com/openshift/origin/pkg/route/api/v1beta1" +) + +func NewTestEtcd(client tools.EtcdClient) *Etcd { + return New(tools.EtcdHelper{client, latest.Codec, latest.ResourceVersioner}) +} + +func TestEtcdListEmptyRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + key := "/routes" + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{}, + }, + }, + E: nil, + } + registry := NewTestEtcd(fakeClient) + routes, err := registry.ListRoutes(labels.Everything()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(routes.Items) != 0 { + t.Errorf("Unexpected routes list: %#v", routes) + } +} + +func TestEtcdListErrorRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + key := "/routes" + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: nil, + }, + E: fmt.Errorf("some error"), + } + registry := NewTestEtcd(fakeClient) + routes, err := registry.ListRoutes(labels.Everything()) + if err == nil { + t.Error("unexpected nil error") + } + + if routes != nil { + t.Errorf("Unexpected non-nil routes: %#v", routes) + } +} + +func TestEtcdListEverythingRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + key := "/routes" + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, &api.Route{JSONBase: kubeapi.JSONBase{ID: "foo"}}), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, &api.Route{JSONBase: kubeapi.JSONBase{ID: "bar"}}), + }, + }, + }, + }, + E: nil, + } + registry := NewTestEtcd(fakeClient) + routes, err := registry.ListRoutes(labels.Everything()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(routes.Items) != 2 || routes.Items[0].ID != "foo" || routes.Items[1].ID != "bar" { + t.Errorf("Unexpected routes list: %#v", routes) + } +} + +func TestEtcdListFilteredRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + key := "/routes" + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, &api.Route{ + JSONBase: kubeapi.JSONBase{ID: "foo"}, + Labels: map[string]string{"env": "prod"}, + }), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, &api.Route{ + JSONBase: kubeapi.JSONBase{ID: "bar"}, + Labels: map[string]string{"env": "dev"}, + }), + }, + }, + }, + }, + E: nil, + } + registry := NewTestEtcd(fakeClient) + routes, err := registry.ListRoutes(labels.SelectorFromSet(labels.Set{"env": "dev"})) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(routes.Items) != 1 || routes.Items[0].ID != "bar" { + t.Errorf("Unexpected routes list: %#v", routes) + } +} + +func TestEtcdGetRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.Set("/routes/foo", runtime.EncodeOrDie(latest.Codec, &api.Route{JSONBase: kubeapi.JSONBase{ID: "foo"}}), 0) + registry := NewTestEtcd(fakeClient) + route, err := registry.GetRoute("foo") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if route.ID != "foo" { + t.Errorf("Unexpected route: %#v", route) + } +} + +func TestEtcdGetNotFoundRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.Data["/routes/foo"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: nil, + }, + E: tools.EtcdErrorNotFound, + } + registry := NewTestEtcd(fakeClient) + route, err := registry.GetRoute("foo") + if err == nil { + t.Errorf("Unexpected non-error.") + } + if route != nil { + t.Errorf("Unexpected route: %#v", route) + } +} + +func TestEtcdCreateRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.TestIndex = true + fakeClient.Data["/routes/foo"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: nil, + }, + E: tools.EtcdErrorNotFound, + } + registry := NewTestEtcd(fakeClient) + err := registry.CreateRoute(&api.Route{ + JSONBase: kubeapi.JSONBase{ + ID: "foo", + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + resp, err := fakeClient.Get("/routes/foo", false, false) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + var route api.Route + err = latest.Codec.DecodeInto([]byte(resp.Node.Value), &route) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if route.ID != "foo" { + t.Errorf("Unexpected route: %#v %s", route, resp.Node.Value) + } +} + +func TestEtcdCreateAlreadyExistsRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.Data["/routes/foo"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, &api.Route{JSONBase: kubeapi.JSONBase{ID: "foo"}}), + }, + }, + E: nil, + } + registry := NewTestEtcd(fakeClient) + err := registry.CreateRoute(&api.Route{ + JSONBase: kubeapi.JSONBase{ + ID: "foo", + }, + }) + if err == nil { + t.Error("Unexpected non-error") + } + if !errors.IsAlreadyExists(err) { + t.Errorf("Expected 'already exists' error, got %#v", err) + } +} + +func TestEtcdUpdateOkRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + registry := NewTestEtcd(fakeClient) + err := registry.UpdateRoute(&api.Route{}) + if err != nil { + t.Error("Unexpected error") + } +} + +func TestEtcdDeleteNotFoundRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.Err = tools.EtcdErrorNotFound + registry := NewTestEtcd(fakeClient) + err := registry.DeleteRoute("foo") + if err == nil { + t.Error("Unexpected non-error") + } + if !errors.IsNotFound(err) { + t.Errorf("Expected 'not found' error, got %#v", err) + } +} + +func TestEtcdDeleteErrorRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.Err = fmt.Errorf("Some error") + registry := NewTestEtcd(fakeClient) + err := registry.DeleteRoute("foo") + if err == nil { + t.Error("Unexpected non-error") + } +} + +func TestEtcdDeleteOkRoutes(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + registry := NewTestEtcd(fakeClient) + key := "/routes/foo" + err := registry.DeleteRoute("foo") + if err != nil { + t.Errorf("Unexpected error: %#v", err) + } + if len(fakeClient.DeletedKeys) != 1 { + t.Errorf("Expected 1 delete, found %#v", fakeClient.DeletedKeys) + } else if fakeClient.DeletedKeys[0] != key { + t.Errorf("Unexpected key: %s, expected %s", fakeClient.DeletedKeys[0], key) + } +} diff --git a/pkg/route/registry/route/registry.go b/pkg/route/registry/route/registry.go new file mode 100644 index 000000000000..44c88091eee0 --- /dev/null +++ b/pkg/route/registry/route/registry.go @@ -0,0 +1,24 @@ +package route + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + "github.com/openshift/origin/pkg/route/api" +) + +// Registry is an interface for things that know how to store Routes. +type Registry interface { + // ListRoutes obtains list of routes that match a selector. + ListRoutes(selector labels.Selector) (*api.RouteList, error) + // GetRoute retrieves a specific route. + GetRoute(routeID string) (*api.Route, error) + // CreateRoute creates a new route. + CreateRoute(route *api.Route) error + // UpdateRoute updates a route. + UpdateRoute(route *api.Route) error + // DeleteRoute deletes a route. + DeleteRoute(routeID string) error + // WatchRoutes watches for new/modified/deleted routes. + WatchRoutes(labels, fields labels.Selector, resourceVersion uint64) (watch.Interface, error) +} diff --git a/pkg/route/registry/route/rest.go b/pkg/route/registry/route/rest.go new file mode 100644 index 000000000000..7997ce43f791 --- /dev/null +++ b/pkg/route/registry/route/rest.go @@ -0,0 +1,114 @@ +package route + +import ( + "fmt" + + "code.google.com/p/go-uuid/uuid" + kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + "github.com/openshift/origin/pkg/route/api" + "github.com/openshift/origin/pkg/route/api/validation" +) + +// REST is an implementation of RESTStorage for the api server. +type REST struct { + registry Registry +} + +func NewREST(registry Registry) *REST { + return &REST{ + registry: registry, + } +} + +func (rs *REST) New() runtime.Object { + return &api.Route{} +} + +// List obtains a list of Routes that match selector. +func (rs *REST) List(selector, fields labels.Selector) (runtime.Object, error) { + list, err := rs.registry.ListRoutes(selector) + if err != nil { + return nil, err + } + return list, err +} + +// Get obtains the route specified by its id. +func (rs *REST) Get(id string) (runtime.Object, error) { + route, err := rs.registry.GetRoute(id) + if err != nil { + return nil, err + } + return route, err +} + +// Delete asynchronously deletes the Route specified by its id. +func (rs *REST) Delete(id string) (<-chan runtime.Object, error) { + _, err := rs.registry.GetRoute(id) + if err != nil { + return nil, err + } + return apiserver.MakeAsync(func() (runtime.Object, error) { + return &kubeapi.Status{Status: kubeapi.StatusSuccess}, rs.registry.DeleteRoute(id) + }), nil +} + +// Create registers a given new Route instance to rs.registry. +func (rs *REST) Create(obj runtime.Object) (<-chan runtime.Object, error) { + route, ok := obj.(*api.Route) + if !ok { + return nil, fmt.Errorf("not a route: %#v", obj) + } + + if errs := validation.ValidateRoute(route); len(errs) > 0 { + return nil, errors.NewInvalid("route", route.ID, errs) + } + if len(route.ID) == 0 { + route.ID = uuid.NewUUID().String() + } + + route.CreationTimestamp = util.Now() + + return apiserver.MakeAsync(func() (runtime.Object, error) { + err := rs.registry.CreateRoute(route) + if err != nil { + return nil, err + } + return rs.registry.GetRoute(route.ID) + }), nil +} + +// Update replaces a given Route instance with an existing instance in rs.registry. +func (rs *REST) Update(obj runtime.Object) (<-chan runtime.Object, error) { + route, ok := obj.(*api.Route) + if !ok { + return nil, fmt.Errorf("not a route: %#v", obj) + } + if len(route.ID) == 0 { + return nil, fmt.Errorf("id is unspecified: %#v", route) + } + + if errs := validation.ValidateRoute(route); len(errs) > 0 { + return nil, errors.NewInvalid("route", route.ID, errs) + } + return apiserver.MakeAsync(func() (runtime.Object, error) { + err := rs.registry.UpdateRoute(route) + if err != nil { + return nil, err + } + return rs.registry.GetRoute(route.ID) + }), nil +} + +// Watch returns Routes events via a watch.Interface. +// It implements apiserver.ResourceWatcher. +func (rs *REST) Watch(label, field labels.Selector, resourceVersion uint64) (watch.Interface, error) { + return rs.registry.WatchRoutes(label, field, resourceVersion) +} diff --git a/pkg/route/registry/route/rest_test.go b/pkg/route/registry/route/rest_test.go new file mode 100644 index 000000000000..1bc7acca0572 --- /dev/null +++ b/pkg/route/registry/route/rest_test.go @@ -0,0 +1,279 @@ +package route + +import ( + "strings" + "testing" + "time" + + kubeapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/openshift/origin/pkg/route/api" + "github.com/openshift/origin/pkg/route/registry/test" +) + +func TestListRoutesEmptyList(t *testing.T) { + mockRegistry := test.NewRouteRegistry() + mockRegistry.Routes = &api.RouteList{ + Items: []api.Route{}, + } + + storage := REST{ + registry: mockRegistry, + } + + routes, err := storage.List(labels.Everything(), labels.Everything()) + if err != nil { + t.Errorf("Unexpected non-nil error: %#v", err) + } + + if len(routes.(*api.RouteList).Items) != 0 { + t.Errorf("Unexpected non-zero routes list: %#v", routes) + } +} + +func TestListRoutesPopulatedList(t *testing.T) { + mockRegistry := test.NewRouteRegistry() + mockRegistry.Routes = &api.RouteList{ + Items: []api.Route{ + { + JSONBase: kubeapi.JSONBase{ + ID: "foo", + }, + }, + { + JSONBase: kubeapi.JSONBase{ + ID: "bar", + }, + }, + }, + } + + storage := REST{ + registry: mockRegistry, + } + + list, err := storage.List(labels.Everything(), labels.Everything()) + if err != nil { + t.Errorf("Unexpected non-nil error: %#v", err) + } + + routes := list.(*api.RouteList) + + if e, a := 2, len(routes.Items); e != a { + t.Errorf("Expected %v, got %v", e, a) + } +} + +func TestCreateRouteBadObject(t *testing.T) { + storage := REST{} + + channel, err := storage.Create(&api.RouteList{}) + if channel != nil { + t.Errorf("Expected nil, got %v", channel) + } + if strings.Index(err.Error(), "not a route") == -1 { + t.Errorf("Expected 'not a route' error, got '%v'", err.Error()) + } +} + +func TestCreateRouteOK(t *testing.T) { + mockRegistry := test.NewRouteRegistry() + storage := REST{registry: mockRegistry} + + channel, err := storage.Create(&api.Route{ + JSONBase: kubeapi.JSONBase{ID: "foo"}, + Host: "www.frontend.com", + ServiceName: "myrubyservice", + }) + if channel == nil { + t.Errorf("Expected nil channel, got %v", channel) + } + if err != nil { + t.Errorf("Unexpected non-nil error: %#v", err) + } + + select { + case result := <-channel: + route, ok := result.(*api.Route) + if !ok { + t.Errorf("Expected route type, got: %#v", result) + } + if route.ID != "foo" { + t.Errorf("Unexpected route: %#v", route) + } + case <-time.After(50 * time.Millisecond): + t.Errorf("Timed out waiting for result") + default: + } +} + +func TestGetRouteError(t *testing.T) { + mockRegistry := test.NewRouteRegistry() + storage := REST{registry: mockRegistry} + + route, err := storage.Get("foo") + if route != nil { + t.Errorf("Unexpected non-nil route: %#v", route) + } + expectedError := "Route foo not found" + if err.Error() != expectedError { + t.Errorf("Expected %#v, got %#v", expectedError, err.Error()) + } +} + +func TestGetRouteOK(t *testing.T) { + mockRegistry := test.NewRouteRegistry() + mockRegistry.Routes = &api.RouteList{ + Items: []api.Route{ + { + JSONBase: kubeapi.JSONBase{ID: "foo"}, + }, + }, + } + storage := REST{registry: mockRegistry} + + route, err := storage.Get("foo") + if route == nil { + t.Error("Unexpected nil route") + } + if err != nil { + t.Errorf("Unexpected non-nil error", err) + } + if route.(*api.Route).ID != "foo" { + t.Errorf("Unexpected route: %#v", route) + } +} + +func TestUpdateRouteBadObject(t *testing.T) { + storage := REST{} + + channel, err := storage.Update(&api.RouteList{}) + if channel != nil { + t.Errorf("Expected nil, got %v", channel) + } + if strings.Index(err.Error(), "not a route:") == -1 { + t.Errorf("Expected 'not a route' error, got %v", err) + } +} + +func TestUpdateRouteMissingID(t *testing.T) { + storage := REST{} + + channel, err := storage.Update(&api.Route{}) + if channel != nil { + t.Errorf("Expected nil, got %v", channel) + } + if strings.Index(err.Error(), "id is unspecified:") == -1 { + t.Errorf("Expected 'id is unspecified' error, got %v", err) + } +} + +func TestUpdateRegistryErrorSaving(t *testing.T) { + mockRepositoryRegistry := test.NewRouteRegistry() + storage := REST{registry: mockRepositoryRegistry} + + channel, err := storage.Update(&api.Route{ + JSONBase: kubeapi.JSONBase{ID: "foo"}, + Host: "www.frontend.com", + ServiceName: "rubyservice", + }) + if err != nil { + t.Errorf("Unexpected non-nil error: %#v", err) + } + result := <-channel + status, ok := result.(*kubeapi.Status) + if !ok { + t.Errorf("Expected status, got %#v", result) + } + if status.Status != "failure" || status.Message != "Route foo not found" { + t.Errorf("Expected status=failure, message=Route foo not found, got %#v", status) + } +} + +func TestUpdateRouteOK(t *testing.T) { + mockRepositoryRegistry := test.NewRouteRegistry() + mockRepositoryRegistry.Routes = &api.RouteList{ + Items: []api.Route{ + { + JSONBase: kubeapi.JSONBase{ID: "bar"}, + Host: "www.frontend.com", + ServiceName: "rubyservice", + }, + }, + } + + storage := REST{registry: mockRepositoryRegistry} + + channel, err := storage.Update(&api.Route{ + JSONBase: kubeapi.JSONBase{ID: "bar"}, + Host: "www.newfrontend.com", + ServiceName: "newrubyservice", + }) + + if err != nil { + t.Errorf("Unexpected non-nil error: %#v", err) + } + result := <-channel + route, ok := result.(*api.Route) + if !ok { + t.Errorf("Expected Route, got %#v", result) + } + if route == nil { + t.Errorf("Nil route returned: %#v", route) + t.Errorf("Expected Route, got %#v", result) + } + if route.ID != "bar" { + t.Errorf("Unexpected route returned: %#v", route) + } + if route.Host != "www.newfrontend.com" { + t.Errorf("Updated route not returned: %#v", route) + } + if route.ServiceName != "newrubyservice" { + t.Errorf("Updated route not returned: %#v", route) + } +} + +func TestDeleteRouteError(t *testing.T) { + mockRegistry := test.NewRouteRegistry() + storage := REST{registry: mockRegistry} + _, err := storage.Delete("foo") + if err == nil { + t.Errorf("Unexpected nil error: %#v", err) + } + if err.Error() != "Route foo not found" { + t.Errorf("Expected %#v, got %#v", "Route foo not found", err.Error()) + } +} + +func TestDeleteRouteOk(t *testing.T) { + mockRegistry := test.NewRouteRegistry() + mockRegistry.Routes = &api.RouteList{ + Items: []api.Route{ + { + JSONBase: kubeapi.JSONBase{ID: "foo"}, + }, + }, + } + storage := REST{registry: mockRegistry} + channel, err := storage.Delete("foo") + if channel == nil { + t.Error("Unexpected nil channel") + } + if err != nil { + t.Errorf("Unexpected non-nil error: %#v", err) + } + + select { + case result := <-channel: + status, ok := result.(*kubeapi.Status) + if !ok { + t.Errorf("Expected status type, got: %#v", result) + } + if status.Status != "success" { + t.Errorf("Expected status=success, got: %#v", status) + } + case <-time.After(50 * time.Millisecond): + t.Errorf("Timed out waiting for result") + default: + } +} diff --git a/pkg/route/registry/test/route.go b/pkg/route/registry/test/route.go new file mode 100644 index 000000000000..4458afae5588 --- /dev/null +++ b/pkg/route/registry/test/route.go @@ -0,0 +1,85 @@ +package test + +import ( + "errors" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + routeapi "github.com/openshift/origin/pkg/route/api" +) + +type RouteRegistry struct { + Routes *routeapi.RouteList +} + +func NewRouteRegistry() *RouteRegistry { + return &RouteRegistry{} +} + +func (r *RouteRegistry) ListRoutes(labels labels.Selector) (*routeapi.RouteList, error) { + return r.Routes, nil +} + +func (r *RouteRegistry) GetRoute(id string) (*routeapi.Route, error) { + if r.Routes != nil { + for _, route := range r.Routes.Items { + if route.ID == id { + return &route, nil + } + } + } + return nil, errors.New("Route " + id + " not found") +} + +func (r *RouteRegistry) CreateRoute(route *routeapi.Route) error { + if r.Routes == nil { + r.Routes = &routeapi.RouteList{} + } + newList := []routeapi.Route{} + for _, curRoute := range r.Routes.Items { + newList = append(newList, curRoute) + } + newList = append(newList, *route) + r.Routes.Items = newList + return nil +} + +func (r *RouteRegistry) UpdateRoute(route *routeapi.Route) error { + if r.Routes == nil { + r.Routes = &routeapi.RouteList{} + } + newList := []routeapi.Route{} + found := false + for _, curRoute := range r.Routes.Items { + if curRoute.ID == route.ID { + // route to be updated exists + found = true + } else { + newList = append(newList, curRoute) + } + } + if !found { + return errors.New("Route " + route.ID + " not found") + } + newList = append(newList, *route) + r.Routes.Items = newList + return nil +} + +func (r *RouteRegistry) DeleteRoute(id string) error { + if r.Routes == nil { + r.Routes = &routeapi.RouteList{} + } + newList := []routeapi.Route{} + for _, curRoute := range r.Routes.Items { + if curRoute.ID != id { + newList = append(newList, curRoute) + } + } + r.Routes.Items = newList + return nil +} + +func (r *RouteRegistry) WatchRoutes(labels, fields labels.Selector, resourceVersion uint64) (watch.Interface, error) { + return nil, nil +}