From 38cdd387ba88050c4b315533042a6ed83d2006d5 Mon Sep 17 00:00:00 2001 From: ramr Date: Wed, 11 Mar 2015 16:39:30 -0700 Subject: [PATCH 1/3] Add simple shard allocator plugin to autogenerate host names for routes based on service and namespace and hook it into the route processing [GOFM]. --- pkg/cmd/server/origin/master.go | 17 +- pkg/route/api/types.go | 12 ++ pkg/route/api/v1beta1/types.go | 14 ++ pkg/route/controller/allocation/controller.go | 46 +++++ .../controller/allocation/controller_test.go | 87 +++++++++ pkg/route/controller/allocation/doc.go | 2 + pkg/route/controller/allocation/factory.go | 23 +++ .../controller/allocation/test/controller.go | 30 +++ pkg/route/controller/doc.go | 2 + pkg/route/interfaces.go | 12 ++ pkg/route/registry/route/rest.go | 18 +- pkg/route/registry/route/rest_test.go | 57 +++++- plugins/route/allocation/doc.go | 2 + plugins/route/allocation/simple/doc.go | 2 + plugins/route/allocation/simple/plugin.go | 72 +++++++ .../route/allocation/simple/plugin_test.go | 177 ++++++++++++++++++ plugins/route/doc.go | 2 + 17 files changed, 561 insertions(+), 14 deletions(-) create mode 100644 pkg/route/controller/allocation/controller.go create mode 100644 pkg/route/controller/allocation/controller_test.go create mode 100644 pkg/route/controller/allocation/doc.go create mode 100644 pkg/route/controller/allocation/factory.go create mode 100644 pkg/route/controller/allocation/test/controller.go create mode 100644 pkg/route/controller/doc.go create mode 100644 pkg/route/interfaces.go create mode 100644 plugins/route/allocation/doc.go create mode 100644 plugins/route/allocation/simple/doc.go create mode 100644 plugins/route/allocation/simple/plugin.go create mode 100644 plugins/route/allocation/simple/plugin_test.go create mode 100644 plugins/route/doc.go diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 1be38095501a..105ffa6a09e4 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -65,6 +65,7 @@ import ( clientauthorizationregistry "github.com/openshift/origin/pkg/oauth/registry/clientauthorization" oauthetcd "github.com/openshift/origin/pkg/oauth/registry/etcd" projectregistry "github.com/openshift/origin/pkg/project/registry/project" + routeallocationcontroller "github.com/openshift/origin/pkg/route/controller/allocation" routeetcd "github.com/openshift/origin/pkg/route/registry/etcd" routeregistry "github.com/openshift/origin/pkg/route/registry/route" "github.com/openshift/origin/pkg/service" @@ -85,6 +86,7 @@ import ( rolebindingregistry "github.com/openshift/origin/pkg/authorization/registry/rolebinding" subjectaccessreviewregistry "github.com/openshift/origin/pkg/authorization/registry/subjectaccessreview" "github.com/openshift/origin/pkg/cmd/server/bootstrappolicy" + routeplugin "github.com/openshift/origin/plugins/route/allocation/simple" ) const ( @@ -197,6 +199,7 @@ func (c *MasterConfig) InstallProtectedAPI(container *restful.Container) []strin imageRepositoryMappingStorage := imagerepositorymapping.NewREST(imageRegistry, imageRepositoryRegistry) imageRepositoryTagStorage := imagerepositorytag.NewREST(imageRegistry, imageRepositoryRegistry) imageStreamImageStorage := imagestreamimage.NewREST(imageRegistry, imageRepositoryRegistry) + routeAllocator := c.RouteAllocator() // TODO: with sharding, this needs to be changed deployConfigGenerator := &deployconfiggenerator.DeploymentConfigGenerator{ @@ -238,7 +241,7 @@ func (c *MasterConfig) InstallProtectedAPI(container *restful.Container) []strin "templateConfigs": templateregistry.NewREST(), "templates": templateetcd.NewREST(c.EtcdHelper), - "routes": routeregistry.NewREST(routeEtcd), + "routes": routeregistry.NewREST(routeEtcd, *routeAllocator), "projects": projectregistry.NewREST(kclient.Namespaces(), c.ProjectAuthorizationCache), @@ -739,6 +742,18 @@ func (c *MasterConfig) RunDeploymentImageChangeTriggerController() { controller.Run() } +// RouteAllocator returns a route allocation controller. +func (c *MasterConfig) RouteAllocator() *routeallocationcontroller.RouteAllocationController { + factory := routeallocationcontroller.RouteAllocationControllerFactory{ + OSClient: c.OSClient, + KubeClient: c.KubeClient(), + } + + // TODO(ramr): Get plugin name + params from config. + plugin, _ := routeplugin.NewSimpleAllocationPlugin("") + return factory.Create(plugin) +} + // ensureCORSAllowedOrigins takes a string list of origins and attempts to covert them to CORS origin // regexes, or exits if it cannot. func (c *MasterConfig) ensureCORSAllowedOrigins() []*regexp.Regexp { diff --git a/pkg/route/api/types.go b/pkg/route/api/types.go index 94ec611a9e96..f36cd2acc477 100644 --- a/pkg/route/api/types.go +++ b/pkg/route/api/types.go @@ -30,6 +30,18 @@ type RouteList struct { Items []Route `json:"items"` } +// RouterShard has information of a routing shard and is used to +// generate host names and routing table entries when a routing shard is +// allocated for a specific route. +type RouterShard struct { + // Shard name uniquely identifies a router shard in the "set" of + // routers used for routing traffic to the services. + ShardName string + + // The DNS suffix for the shard ala: shard-1.v3.openshift.com + DNSSuffix string +} + // TLSConfig defines config used to secure a route and provide termination type TLSConfig struct { // Termination indicates termination type. If termination type is not set, any termination config will be ignored diff --git a/pkg/route/api/v1beta1/types.go b/pkg/route/api/v1beta1/types.go index 1e1a1082f160..9cdce6a0d85f 100644 --- a/pkg/route/api/v1beta1/types.go +++ b/pkg/route/api/v1beta1/types.go @@ -30,6 +30,20 @@ type RouteList struct { Items []Route `json:"items"` } +// RouterShard has information of a routing shard and is used to +// generate host names and routing table entries when a routing shard is +// allocated for a specific route. +// Caveat: This is WIP and will likely undergo modifications when sharding +// support is added. +type RouterShard struct { + // Shard name uniquely identifies a router shard in the "set" of + // routers used for routing traffic to the services. + ShardName string + + // The DNS suffix for the shard ala: shard-1.v3.openshift.com + DNSSuffix string +} + // TLSConfig defines config used to secure a route and provide termination type TLSConfig struct { // Termination indicates termination type. If termination type is not set, any termination config will be ignored diff --git a/pkg/route/controller/allocation/controller.go b/pkg/route/controller/allocation/controller.go new file mode 100644 index 000000000000..4ced1a6c6b69 --- /dev/null +++ b/pkg/route/controller/allocation/controller.go @@ -0,0 +1,46 @@ +package allocation + +import ( + "github.com/golang/glog" + + "github.com/openshift/origin/pkg/route" + routeapi "github.com/openshift/origin/pkg/route/api" +) + +// RouteAllocationController abstracts the details of how routes are +// allocated to router shards. +type RouteAllocationController struct { + Plugin route.AllocationPlugin +} + +// Allocate a router shard for the given route. +func (c *RouteAllocationController) Allocate(route *routeapi.Route) (*routeapi.RouterShard, error) { + + glog.V(4).Infof("RoutingAllocationController: Allocating shard for Route: %s [alias=%s]", + route.ServiceName, route.Host) + + shard, err := c.Plugin.Allocate(route) + + if err != nil { + glog.Errorf("RoutingAllocationController: Unable to allocate router shard: %v", err) + return shard, err + } + + glog.V(4).Infof("RoutingAllocationController: Route %s allocated to shard %s [suffix=%s]", + route.ServiceName, shard.ShardName, shard.DNSSuffix) + + return shard, err +} + +// Generate a host name for the given route and router shard combination. +func (c *RouteAllocationController) GenerateHostname(route *routeapi.Route, shard *routeapi.RouterShard) string { + glog.V(4).Infof("Generating host name for Route: %s", + route.ServiceName) + + s := c.Plugin.GenerateHostname(route, shard) + + glog.V(4).Infof("Route: %s, generated host name/alias=%s", + route.ServiceName, s) + + return s +} diff --git a/pkg/route/controller/allocation/controller_test.go b/pkg/route/controller/allocation/controller_test.go new file mode 100644 index 000000000000..c6bb24a1e9ac --- /dev/null +++ b/pkg/route/controller/allocation/controller_test.go @@ -0,0 +1,87 @@ +package allocation + +import ( + "fmt" + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + routeapi "github.com/openshift/origin/pkg/route/api" +) + +type TestAllocationPlugin struct { + Name string +} + +func (p *TestAllocationPlugin) Allocate(route *routeapi.Route) (*routeapi.RouterShard, error) { + + return &routeapi.RouterShard{ShardName: "test", DNSSuffix: "openshift.test"}, nil +} + +func (p *TestAllocationPlugin) GenerateHostname(route *routeapi.Route, shard *routeapi.RouterShard) string { + if len(route.ServiceName) > 0 && len(route.Namespace) > 0 { + return fmt.Sprintf("%s-%s.%s", route.ServiceName, route.Namespace, shard.DNSSuffix) + } + + return "test-test-test.openshift.test" +} + +func TestRouteAllocationController(t *testing.T) { + tests := []struct { + name string + route *routeapi.Route + }{ + { + name: "No Name", + route: &routeapi.Route{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: "namespace", + }, + ServiceName: "service", + }, + }, + { + name: "No namespace", + route: &routeapi.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + }, + ServiceName: "nonamespace", + }, + }, + { + name: "No service name", + route: &routeapi.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + Namespace: "foo", + }, + }, + }, + { + name: "Valid route", + route: &routeapi.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + Namespace: "foo", + }, + Host: "www.example.org", + ServiceName: "serviceName", + }, + }, + } + + plugin := &TestAllocationPlugin{Name: "test allocation plugin"} + fac := &RouteAllocationControllerFactory{nil, nil} + allocator := fac.Create(plugin) + for _, tc := range tests { + shard, err := allocator.Allocate(tc.route) + if err != nil { + t.Errorf("Test case %s got an error %s", tc.name, err) + continue + } + name := allocator.GenerateHostname(tc.route, shard) + if len(name) <= 0 { + t.Errorf("Test case %s got %d length name", tc.name, len(name)) + } + } +} diff --git a/pkg/route/controller/allocation/doc.go b/pkg/route/controller/allocation/doc.go new file mode 100644 index 000000000000..c4246665e5b1 --- /dev/null +++ b/pkg/route/controller/allocation/doc.go @@ -0,0 +1,2 @@ +// Package allocation contains all the route allocation controllers. +package allocation diff --git a/pkg/route/controller/allocation/factory.go b/pkg/route/controller/allocation/factory.go new file mode 100644 index 000000000000..779747a0d241 --- /dev/null +++ b/pkg/route/controller/allocation/factory.go @@ -0,0 +1,23 @@ +package allocation + +import ( + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + + osclient "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/route" +) + +// RouteAllocationControllerFactory creates a RouteAllocationController +// that allocates router shards to specific routes. +type RouteAllocationControllerFactory struct { + // Client is is an OpenShift client. + OSClient osclient.Interface + + // KubeClient is a Kubernetes client. + KubeClient kclient.Interface +} + +// Create a RouteAllocationController instance. +func (factory *RouteAllocationControllerFactory) Create(plugin route.AllocationPlugin) *RouteAllocationController { + return &RouteAllocationController{Plugin: plugin} +} diff --git a/pkg/route/controller/allocation/test/controller.go b/pkg/route/controller/allocation/test/controller.go new file mode 100644 index 000000000000..2914436995c4 --- /dev/null +++ b/pkg/route/controller/allocation/test/controller.go @@ -0,0 +1,30 @@ +package test + +import ( + "fmt" + + routeapi "github.com/openshift/origin/pkg/route/api" + "github.com/openshift/origin/pkg/route/controller/allocation" +) + +type TestAllocationPlugin struct { + Name string +} + +func (p *TestAllocationPlugin) Allocate(route *routeapi.Route) (*routeapi.RouterShard, error) { + + return &routeapi.RouterShard{ShardName: "test", DNSSuffix: "openshift.test"}, nil +} + +func (p *TestAllocationPlugin) GenerateHostname(route *routeapi.Route, shard *routeapi.RouterShard) string { + if len(route.ServiceName) > 0 && len(route.Namespace) > 0 { + return fmt.Sprintf("%s-%s.%s", route.ServiceName, route.Namespace, shard.DNSSuffix) + } + + return "test-test-test.openshift.test" +} + +func NewTestRouteAllocationController() *allocation.RouteAllocationController { + plugin := &TestAllocationPlugin{"test route allocation plugin"} + return &allocation.RouteAllocationController{Plugin: plugin} +} diff --git a/pkg/route/controller/doc.go b/pkg/route/controller/doc.go new file mode 100644 index 000000000000..beca750b75d8 --- /dev/null +++ b/pkg/route/controller/doc.go @@ -0,0 +1,2 @@ +// Package controller contains all the route handling controllers. +package controller diff --git a/pkg/route/interfaces.go b/pkg/route/interfaces.go new file mode 100644 index 000000000000..e30384ed9334 --- /dev/null +++ b/pkg/route/interfaces.go @@ -0,0 +1,12 @@ +package route + +import ( + api "github.com/openshift/origin/pkg/route/api" +) + +// AllocationPlugin is the interface the route controller dispatches +// requests for RouterShard allocation and name generation. +type AllocationPlugin interface { + Allocate(*api.Route) (*api.RouterShard, error) + GenerateHostname(*api.Route, *api.RouterShard) string +} diff --git a/pkg/route/registry/route/rest.go b/pkg/route/registry/route/rest.go index 4a25b09268f7..417852f38dba 100644 --- a/pkg/route/registry/route/rest.go +++ b/pkg/route/registry/route/rest.go @@ -13,16 +13,19 @@ import ( "github.com/openshift/origin/pkg/route/api" "github.com/openshift/origin/pkg/route/api/validation" + rac "github.com/openshift/origin/pkg/route/controller/allocation" ) // REST is an implementation of RESTStorage for the api server. type REST struct { - registry Registry + registry Registry + allocator rac.RouteAllocationController } -func NewREST(registry Registry) *REST { +func NewREST(registry Registry, allocator rac.RouteAllocationController) *REST { return &REST{ - registry: registry, + registry: registry, + allocator: allocator, } } @@ -71,6 +74,15 @@ func (rs *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, er return nil, errors.NewConflict("route", route.Namespace, fmt.Errorf("Route.Namespace does not match the provided context")) } + shard, allocError := rs.allocator.Allocate(route) + if allocError != nil { + return nil, fmt.Errorf("allocation error: %s for route: %#v", allocError, obj) + } + + if len(route.Host) == 0 { + route.Host = rs.allocator.GenerateHostname(route, shard) + } + if errs := validation.ValidateRoute(route); len(errs) > 0 { return nil, errors.NewInvalid("route", route.Name, errs) } diff --git a/pkg/route/registry/route/rest_test.go b/pkg/route/registry/route/rest_test.go index cfbba8100501..65ffd7956d82 100644 --- a/pkg/route/registry/route/rest_test.go +++ b/pkg/route/registry/route/rest_test.go @@ -10,17 +10,20 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/openshift/origin/pkg/route/api" + ractest "github.com/openshift/origin/pkg/route/controller/allocation/test" "github.com/openshift/origin/pkg/route/registry/test" ) func TestListRoutesEmptyList(t *testing.T) { mockRegistry := test.NewRouteRegistry() + mockAllocator := ractest.NewTestRouteAllocationController() mockRegistry.Routes = &api.RouteList{ Items: []api.Route{}, } storage := REST{ - registry: mockRegistry, + registry: mockRegistry, + allocator: *mockAllocator, } routes, err := storage.List(kapi.NewDefaultContext(), labels.Everything(), labels.Everything()) @@ -35,6 +38,7 @@ func TestListRoutesEmptyList(t *testing.T) { func TestListRoutesPopulatedList(t *testing.T) { mockRegistry := test.NewRouteRegistry() + mockAllocator := ractest.NewTestRouteAllocationController() mockRegistry.Routes = &api.RouteList{ Items: []api.Route{ { @@ -51,7 +55,8 @@ func TestListRoutesPopulatedList(t *testing.T) { } storage := REST{ - registry: mockRegistry, + registry: mockRegistry, + allocator: *mockAllocator, } list, err := storage.List(kapi.NewDefaultContext(), labels.Everything(), labels.Everything()) @@ -80,7 +85,11 @@ func TestCreateRouteBadObject(t *testing.T) { func TestCreateRouteOK(t *testing.T) { mockRegistry := test.NewRouteRegistry() - storage := REST{registry: mockRegistry} + mockAllocator := ractest.NewTestRouteAllocationController() + storage := REST{ + registry: mockRegistry, + allocator: *mockAllocator, + } obj, err := storage.Create(kapi.NewDefaultContext(), &api.Route{ ObjectMeta: kapi.ObjectMeta{Name: "foo"}, @@ -105,7 +114,11 @@ func TestCreateRouteOK(t *testing.T) { func TestGetRouteError(t *testing.T) { mockRegistry := test.NewRouteRegistry() - storage := REST{registry: mockRegistry} + mockAllocator := ractest.NewTestRouteAllocationController() + storage := REST{ + registry: mockRegistry, + allocator: *mockAllocator, + } route, err := storage.Get(kapi.NewDefaultContext(), "foo") if route != nil { @@ -119,6 +132,7 @@ func TestGetRouteError(t *testing.T) { func TestGetRouteOK(t *testing.T) { mockRegistry := test.NewRouteRegistry() + mockAllocator := ractest.NewTestRouteAllocationController() mockRegistry.Routes = &api.RouteList{ Items: []api.Route{ { @@ -126,7 +140,10 @@ func TestGetRouteOK(t *testing.T) { }, }, } - storage := REST{registry: mockRegistry} + storage := REST{ + registry: mockRegistry, + allocator: *mockAllocator, + } route, err := storage.Get(kapi.NewDefaultContext(), "foo") if route == nil { @@ -166,7 +183,11 @@ func TestUpdateRouteMissingID(t *testing.T) { func TestUpdateRegistryErrorSaving(t *testing.T) { mockRepositoryRegistry := test.NewRouteRegistry() - storage := REST{registry: mockRepositoryRegistry} + mockAllocator := ractest.NewTestRouteAllocationController() + storage := REST{ + registry: mockRepositoryRegistry, + allocator: *mockAllocator, + } _, _, err := storage.Update(kapi.NewDefaultContext(), &api.Route{ ObjectMeta: kapi.ObjectMeta{Name: "foo"}, @@ -180,6 +201,7 @@ func TestUpdateRegistryErrorSaving(t *testing.T) { func TestUpdateRouteOK(t *testing.T) { mockRepositoryRegistry := test.NewRouteRegistry() + mockAllocator := ractest.NewTestRouteAllocationController() mockRepositoryRegistry.Routes = &api.RouteList{ Items: []api.Route{ { @@ -190,7 +212,10 @@ func TestUpdateRouteOK(t *testing.T) { }, } - storage := REST{registry: mockRepositoryRegistry} + storage := REST{ + registry: mockRepositoryRegistry, + allocator: *mockAllocator, + } obj, created, err := storage.Update(kapi.NewDefaultContext(), &api.Route{ ObjectMeta: kapi.ObjectMeta{Name: "bar"}, @@ -222,7 +247,11 @@ func TestUpdateRouteOK(t *testing.T) { func TestDeleteRouteError(t *testing.T) { mockRegistry := test.NewRouteRegistry() - storage := REST{registry: mockRegistry} + mockAllocator := ractest.NewTestRouteAllocationController() + storage := REST{ + registry: mockRegistry, + allocator: *mockAllocator, + } _, err := storage.Delete(kapi.NewDefaultContext(), "foo") if err == nil { t.Errorf("Unexpected nil error: %#v", err) @@ -234,6 +263,7 @@ func TestDeleteRouteError(t *testing.T) { func TestDeleteRouteOk(t *testing.T) { mockRegistry := test.NewRouteRegistry() + mockAllocator := ractest.NewTestRouteAllocationController() mockRegistry.Routes = &api.RouteList{ Items: []api.Route{ { @@ -241,7 +271,10 @@ func TestDeleteRouteOk(t *testing.T) { }, }, } - storage := REST{registry: mockRegistry} + storage := REST{ + registry: mockRegistry, + allocator: *mockAllocator, + } obj, err := storage.Delete(kapi.NewDefaultContext(), "foo") if obj == nil { t.Error("Unexpected nil obj") @@ -275,7 +308,11 @@ func TestCreateRouteConflictingNamespace(t *testing.T) { func TestUpdateRouteConflictingNamespace(t *testing.T) { mockRepositoryRegistry := test.NewRouteRegistry() - storage := REST{registry: mockRepositoryRegistry} + mockAllocator := ractest.NewTestRouteAllocationController() + storage := REST{ + registry: mockRepositoryRegistry, + allocator: *mockAllocator, + } obj, created, err := storage.Update(kapi.WithNamespace(kapi.NewContext(), "legal-name"), &api.Route{ ObjectMeta: kapi.ObjectMeta{Name: "bar", Namespace: "some-value"}, diff --git a/plugins/route/allocation/doc.go b/plugins/route/allocation/doc.go new file mode 100644 index 000000000000..51f5c17971bb --- /dev/null +++ b/plugins/route/allocation/doc.go @@ -0,0 +1,2 @@ +// Package allocation contains all the route allocation plugins. +package allocation diff --git a/plugins/route/allocation/simple/doc.go b/plugins/route/allocation/simple/doc.go new file mode 100644 index 000000000000..4b723d6a049a --- /dev/null +++ b/plugins/route/allocation/simple/doc.go @@ -0,0 +1,2 @@ +// Package simple contains the SimpleAllocation route plugin. +package simple diff --git a/plugins/route/allocation/simple/plugin.go b/plugins/route/allocation/simple/plugin.go new file mode 100644 index 000000000000..910ec339928a --- /dev/null +++ b/plugins/route/allocation/simple/plugin.go @@ -0,0 +1,72 @@ +package simple + +import ( + "errors" + "fmt" + + "code.google.com/p/go-uuid/uuid" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" + + routeapi "github.com/openshift/origin/pkg/route/api" +) + +// Default DNS suffix to use if no configuration is passed to this plugin. +// Would be better if we could use "v3.openshift.app", someone bought that! +const defaultDNSSuffix = "v3.openshift.com" + +// SimpleAllocationPlugin implements the route.AllocationPlugin interface +// to provide a simple unsharded (or single sharded) allocation plugin. +type SimpleAllocationPlugin struct { + DNSSuffix string +} + +// Creates a new SimpleAllocationPlugin. +func NewSimpleAllocationPlugin(suffix string) (*SimpleAllocationPlugin, error) { + if len(suffix) == 0 { + suffix = defaultDNSSuffix + } + + glog.V(4).Infof("NewSimpleAllocationPlugin: suffix=%s", suffix) + + // Check that the DNS suffix is valid. + if !util.IsDNSSubdomain(suffix) { + errmsg := fmt.Sprintf("invalid DNS suffix: %s", suffix) + glog.Errorf("NewSimpleAllocationPlugin: %s", errmsg) + return nil, errors.New(errmsg) + } + + return &SimpleAllocationPlugin{DNSSuffix: suffix}, nil +} + +// Allocate a router shard for the given route. This plugin always returns +// the "global" router shard. +func (p *SimpleAllocationPlugin) Allocate(route *routeapi.Route) (*routeapi.RouterShard, error) { + + glog.V(4).Infof("SimpleAllocationPlugin: Allocating global shard *.%s to Route: %s", + p.DNSSuffix, route.ServiceName) + + return &routeapi.RouterShard{ShardName: "global", DNSSuffix: p.DNSSuffix}, nil +} + +// Generate a host name for a route - using the service name, +// namespace (if provided) and the router shard dns suffix. +func (p *SimpleAllocationPlugin) GenerateHostname(route *routeapi.Route, shard *routeapi.RouterShard) string { + + name := route.ServiceName + if len(name) == 0 { + name = uuid.NewUUID().String() + glog.V(4).Infof("SimpleAllocationPlugin: No service name passed, using generated name: %s", name) + } + + s := "" + if len(route.Namespace) <= 0 { + s = fmt.Sprintf("%s.%s", name, shard.DNSSuffix) + } else { + s = fmt.Sprintf("%s-%s.%s", name, route.Namespace, shard.DNSSuffix) + } + + glog.V(4).Infof("SimpleAllocationPlugin: Generated hostname=%s for Route: %s", s, route.ServiceName) + + return s +} diff --git a/plugins/route/allocation/simple/plugin_test.go b/plugins/route/allocation/simple/plugin_test.go new file mode 100644 index 000000000000..df497273f829 --- /dev/null +++ b/plugins/route/allocation/simple/plugin_test.go @@ -0,0 +1,177 @@ +package simple + +import ( + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + "github.com/openshift/origin/pkg/route/api" + rac "github.com/openshift/origin/pkg/route/controller/allocation" +) + +func TestNewSimpleAllocationPlugin(t *testing.T) { + tests := []struct { + Name string + ErrorExpectation bool + }{ + { + Name: "www.example.org", + ErrorExpectation: false, + }, + { + Name: "www^acme^org", + ErrorExpectation: true, + }, + { + Name: "bad wolf.whoswho", + ErrorExpectation: true, + }, + { + Name: "tardis#1.watch", + ErrorExpectation: true, + }, + { + Name: "こんにちはopenshift.com", + ErrorExpectation: true, + }, + { + Name: "yo!yo!@#$%%$%^&*(0){[]}:;',<>?/1.test", + ErrorExpectation: true, + }, + } + + for _, tc := range tests { + _, err := NewSimpleAllocationPlugin(tc.Name) + if err != nil && !tc.ErrorExpectation { + t.Errorf("Test case for %s got an error where none was expected", tc.Name) + } + } +} + +func TestSimpleAllocationPlugin(t *testing.T) { + tests := []struct { + name string + route *api.Route + }{ + { + name: "No Name", + route: &api.Route{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: "namespace", + }, + ServiceName: "service", + }, + }, + { + name: "No namespace", + route: &api.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + }, + ServiceName: "nonamespace", + }, + }, + { + name: "No service name", + route: &api.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + Namespace: "foo", + }, + }, + }, + { + name: "Valid route", + route: &api.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + Namespace: "foo", + }, + Host: "www.example.com", + ServiceName: "myservice", + }, + }, + } + + plugin, err := NewSimpleAllocationPlugin("www.example.org") + if err != nil { + t.Errorf("Error creating SimpleAllocationPlugin got %s", err) + return + } + + for _, tc := range tests { + shard, _ := plugin.Allocate(tc.route) + name := plugin.GenerateHostname(tc.route, shard) + if len(name) <= 0 { + t.Errorf("Test case %s got %d length name.", tc.name, len(name)) + } + if !util.IsDNSSubdomain(name) { + t.Errorf("Test case %s got %s - invalid DNS name.", tc.name, name) + } + } +} + +func TestSimpleAllocationPluginViaController(t *testing.T) { + tests := []struct { + name string + route *api.Route + }{ + { + name: "No Name", + route: &api.Route{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: "namespace", + }, + ServiceName: "service", + }, + }, + { + name: "No namespace", + route: &api.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + }, + ServiceName: "nonamespace", + }, + }, + { + name: "No service name", + route: &api.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + Namespace: "foo", + }, + }, + }, + { + name: "Valid route", + route: &api.Route{ + ObjectMeta: kapi.ObjectMeta{ + Name: "name", + Namespace: "foo", + }, + Host: "www.example.com", + ServiceName: "s3", + }, + }, + } + + plugin, _ := NewSimpleAllocationPlugin("www.example.org") + fac := &rac.RouteAllocationControllerFactory{nil, nil} + sac := fac.Create(plugin) + + for _, tc := range tests { + shard, err := sac.Allocate(tc.route) + if err != nil { + t.Errorf("Test case %s got an error %s", tc.name, err) + } + name := sac.GenerateHostname(tc.route, shard) + if len(name) <= 0 { + t.Errorf("Test case %s got %d length name", tc.name, len(name)) + } + if !util.IsDNSSubdomain(name) { + t.Errorf("Test case %s got %s - invalid DNS name.", tc.name, name) + } + } +} diff --git a/plugins/route/doc.go b/plugins/route/doc.go new file mode 100644 index 000000000000..31e86d8a2e9a --- /dev/null +++ b/plugins/route/doc.go @@ -0,0 +1,2 @@ +// Package route contains all the route plugins. +package route From b985b82e29b4046890596a66dd8a3ae062fabd56 Mon Sep 17 00:00:00 2001 From: ramr Date: Thu, 12 Mar 2015 11:11:18 -0700 Subject: [PATCH 2/3] Fixes to route allocator plugin PR as per @smarterclayton comments. --- pkg/cmd/server/origin/master.go | 15 +++++++++++--- pkg/route/api/v1beta1/types.go | 4 ++-- pkg/route/controller/allocation/controller.go | 8 ++++---- .../controller/allocation/controller_test.go | 2 +- pkg/route/interfaces.go | 7 +++++++ pkg/route/registry/route/rest.go | 14 ++++++------- pkg/route/registry/route/rest_test.go | 20 +++++++++---------- plugins/route/allocation/simple/plugin.go | 13 +++++------- .../route/allocation/simple/plugin_test.go | 2 +- 9 files changed, 49 insertions(+), 36 deletions(-) diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 105ffa6a09e4..d509dfc54784 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -93,6 +93,7 @@ const ( OpenShiftAPIPrefix = "/osapi" OpenShiftAPIV1Beta1 = "v1beta1" OpenShiftAPIPrefixV1Beta1 = OpenShiftAPIPrefix + "/" + OpenShiftAPIV1Beta1 + OpenShiftRouteSubdomain = "openshift.local" swaggerAPIPrefix = "/swaggerapi/" ) @@ -241,7 +242,7 @@ func (c *MasterConfig) InstallProtectedAPI(container *restful.Container) []strin "templateConfigs": templateregistry.NewREST(), "templates": templateetcd.NewREST(c.EtcdHelper), - "routes": routeregistry.NewREST(routeEtcd, *routeAllocator), + "routes": routeregistry.NewREST(routeEtcd, routeAllocator), "projects": projectregistry.NewREST(kclient.Namespaces(), c.ProjectAuthorizationCache), @@ -749,8 +750,16 @@ func (c *MasterConfig) RouteAllocator() *routeallocationcontroller.RouteAllocati KubeClient: c.KubeClient(), } - // TODO(ramr): Get plugin name + params from config. - plugin, _ := routeplugin.NewSimpleAllocationPlugin("") + subdomain := os.Getenv("OPENSHIFT_ROUTE_SUBDOMAIN") + if len(subdomain) == 0 { + subdomain = OpenShiftRouteSubdomain + } + + plugin, err := routeplugin.NewSimpleAllocationPlugin(subdomain) + if err != nil { + glog.Fatalf("Route plugin initialization failed: %v", err) + } + return factory.Create(plugin) } diff --git a/pkg/route/api/v1beta1/types.go b/pkg/route/api/v1beta1/types.go index 9cdce6a0d85f..dad96b9cd7a0 100644 --- a/pkg/route/api/v1beta1/types.go +++ b/pkg/route/api/v1beta1/types.go @@ -38,10 +38,10 @@ type RouteList struct { type RouterShard struct { // Shard name uniquely identifies a router shard in the "set" of // routers used for routing traffic to the services. - ShardName string + ShardName string `json:"shardName"` // The DNS suffix for the shard ala: shard-1.v3.openshift.com - DNSSuffix string + DNSSuffix string `json:"dnsSuffix"` } // TLSConfig defines config used to secure a route and provide termination diff --git a/pkg/route/controller/allocation/controller.go b/pkg/route/controller/allocation/controller.go index 4ced1a6c6b69..c0d358f69b80 100644 --- a/pkg/route/controller/allocation/controller.go +++ b/pkg/route/controller/allocation/controller.go @@ -14,19 +14,19 @@ type RouteAllocationController struct { } // Allocate a router shard for the given route. -func (c *RouteAllocationController) Allocate(route *routeapi.Route) (*routeapi.RouterShard, error) { +func (c *RouteAllocationController) AllocateRouterShard(route *routeapi.Route) (*routeapi.RouterShard, error) { - glog.V(4).Infof("RoutingAllocationController: Allocating shard for Route: %s [alias=%s]", + glog.V(4).Infof("Allocating router shard for Route: %s [alias=%s]", route.ServiceName, route.Host) shard, err := c.Plugin.Allocate(route) if err != nil { - glog.Errorf("RoutingAllocationController: Unable to allocate router shard: %v", err) + glog.Errorf("unable to allocate router shard: %v", err) return shard, err } - glog.V(4).Infof("RoutingAllocationController: Route %s allocated to shard %s [suffix=%s]", + glog.V(4).Infof("Route %s allocated to shard %s [suffix=%s]", route.ServiceName, shard.ShardName, shard.DNSSuffix) return shard, err diff --git a/pkg/route/controller/allocation/controller_test.go b/pkg/route/controller/allocation/controller_test.go index c6bb24a1e9ac..d98b155b395d 100644 --- a/pkg/route/controller/allocation/controller_test.go +++ b/pkg/route/controller/allocation/controller_test.go @@ -74,7 +74,7 @@ func TestRouteAllocationController(t *testing.T) { fac := &RouteAllocationControllerFactory{nil, nil} allocator := fac.Create(plugin) for _, tc := range tests { - shard, err := allocator.Allocate(tc.route) + shard, err := allocator.AllocateRouterShard(tc.route) if err != nil { t.Errorf("Test case %s got an error %s", tc.name, err) continue diff --git a/pkg/route/interfaces.go b/pkg/route/interfaces.go index e30384ed9334..c4e66782f6e9 100644 --- a/pkg/route/interfaces.go +++ b/pkg/route/interfaces.go @@ -10,3 +10,10 @@ type AllocationPlugin interface { Allocate(*api.Route) (*api.RouterShard, error) GenerateHostname(*api.Route, *api.RouterShard) string } + +// RouteAllocator is the interface for the route allocation controller +// which handles requests for RouterShard allocation and name generation. +type RouteAllocator interface { + AllocateRouterShard(*api.Route) (*api.RouterShard, error) + GenerateHostname(*api.Route, *api.RouterShard) string +} diff --git a/pkg/route/registry/route/rest.go b/pkg/route/registry/route/rest.go index 417852f38dba..7f92a44a5d5d 100644 --- a/pkg/route/registry/route/rest.go +++ b/pkg/route/registry/route/rest.go @@ -11,18 +11,18 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + "github.com/openshift/origin/pkg/route" "github.com/openshift/origin/pkg/route/api" "github.com/openshift/origin/pkg/route/api/validation" - rac "github.com/openshift/origin/pkg/route/controller/allocation" ) // REST is an implementation of RESTStorage for the api server. type REST struct { registry Registry - allocator rac.RouteAllocationController + allocator route.RouteAllocator } -func NewREST(registry Registry, allocator rac.RouteAllocationController) *REST { +func NewREST(registry Registry, allocator route.RouteAllocator) *REST { return &REST{ registry: registry, allocator: allocator, @@ -74,9 +74,9 @@ func (rs *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, er return nil, errors.NewConflict("route", route.Namespace, fmt.Errorf("Route.Namespace does not match the provided context")) } - shard, allocError := rs.allocator.Allocate(route) - if allocError != nil { - return nil, fmt.Errorf("allocation error: %s for route: %#v", allocError, obj) + shard, err := rs.allocator.AllocateRouterShard(route) + if err != nil { + return nil, fmt.Errorf("allocation error: %s for route: %#v", err, obj) } if len(route.Host) == 0 { @@ -94,7 +94,7 @@ func (rs *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, er escapeNewLines(route.TLS) - err := rs.registry.CreateRoute(ctx, route) + err = rs.registry.CreateRoute(ctx, route) if err != nil { return nil, err } diff --git a/pkg/route/registry/route/rest_test.go b/pkg/route/registry/route/rest_test.go index 65ffd7956d82..df6889906139 100644 --- a/pkg/route/registry/route/rest_test.go +++ b/pkg/route/registry/route/rest_test.go @@ -23,7 +23,7 @@ func TestListRoutesEmptyList(t *testing.T) { storage := REST{ registry: mockRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } routes, err := storage.List(kapi.NewDefaultContext(), labels.Everything(), labels.Everything()) @@ -56,7 +56,7 @@ func TestListRoutesPopulatedList(t *testing.T) { storage := REST{ registry: mockRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } list, err := storage.List(kapi.NewDefaultContext(), labels.Everything(), labels.Everything()) @@ -88,7 +88,7 @@ func TestCreateRouteOK(t *testing.T) { mockAllocator := ractest.NewTestRouteAllocationController() storage := REST{ registry: mockRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } obj, err := storage.Create(kapi.NewDefaultContext(), &api.Route{ @@ -117,7 +117,7 @@ func TestGetRouteError(t *testing.T) { mockAllocator := ractest.NewTestRouteAllocationController() storage := REST{ registry: mockRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } route, err := storage.Get(kapi.NewDefaultContext(), "foo") @@ -142,7 +142,7 @@ func TestGetRouteOK(t *testing.T) { } storage := REST{ registry: mockRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } route, err := storage.Get(kapi.NewDefaultContext(), "foo") @@ -186,7 +186,7 @@ func TestUpdateRegistryErrorSaving(t *testing.T) { mockAllocator := ractest.NewTestRouteAllocationController() storage := REST{ registry: mockRepositoryRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } _, _, err := storage.Update(kapi.NewDefaultContext(), &api.Route{ @@ -214,7 +214,7 @@ func TestUpdateRouteOK(t *testing.T) { storage := REST{ registry: mockRepositoryRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } obj, created, err := storage.Update(kapi.NewDefaultContext(), &api.Route{ @@ -250,7 +250,7 @@ func TestDeleteRouteError(t *testing.T) { mockAllocator := ractest.NewTestRouteAllocationController() storage := REST{ registry: mockRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } _, err := storage.Delete(kapi.NewDefaultContext(), "foo") if err == nil { @@ -273,7 +273,7 @@ func TestDeleteRouteOk(t *testing.T) { } storage := REST{ registry: mockRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } obj, err := storage.Delete(kapi.NewDefaultContext(), "foo") if obj == nil { @@ -311,7 +311,7 @@ func TestUpdateRouteConflictingNamespace(t *testing.T) { mockAllocator := ractest.NewTestRouteAllocationController() storage := REST{ registry: mockRepositoryRegistry, - allocator: *mockAllocator, + allocator: mockAllocator, } obj, created, err := storage.Update(kapi.WithNamespace(kapi.NewContext(), "legal-name"), &api.Route{ diff --git a/plugins/route/allocation/simple/plugin.go b/plugins/route/allocation/simple/plugin.go index 910ec339928a..17df7b03c292 100644 --- a/plugins/route/allocation/simple/plugin.go +++ b/plugins/route/allocation/simple/plugin.go @@ -1,7 +1,6 @@ package simple import ( - "errors" "fmt" "code.google.com/p/go-uuid/uuid" @@ -27,13 +26,11 @@ func NewSimpleAllocationPlugin(suffix string) (*SimpleAllocationPlugin, error) { suffix = defaultDNSSuffix } - glog.V(4).Infof("NewSimpleAllocationPlugin: suffix=%s", suffix) + glog.V(4).Infof("Route plugin initialized with suffix=%s", suffix) // Check that the DNS suffix is valid. if !util.IsDNSSubdomain(suffix) { - errmsg := fmt.Sprintf("invalid DNS suffix: %s", suffix) - glog.Errorf("NewSimpleAllocationPlugin: %s", errmsg) - return nil, errors.New(errmsg) + return nil, fmt.Errorf("invalid DNS suffix: %s", suffix) } return &SimpleAllocationPlugin{DNSSuffix: suffix}, nil @@ -43,7 +40,7 @@ func NewSimpleAllocationPlugin(suffix string) (*SimpleAllocationPlugin, error) { // the "global" router shard. func (p *SimpleAllocationPlugin) Allocate(route *routeapi.Route) (*routeapi.RouterShard, error) { - glog.V(4).Infof("SimpleAllocationPlugin: Allocating global shard *.%s to Route: %s", + glog.V(4).Infof("Allocating global shard *.%s to Route: %s", p.DNSSuffix, route.ServiceName) return &routeapi.RouterShard{ShardName: "global", DNSSuffix: p.DNSSuffix}, nil @@ -56,7 +53,7 @@ func (p *SimpleAllocationPlugin) GenerateHostname(route *routeapi.Route, shard * name := route.ServiceName if len(name) == 0 { name = uuid.NewUUID().String() - glog.V(4).Infof("SimpleAllocationPlugin: No service name passed, using generated name: %s", name) + glog.V(4).Infof("No service name passed, using generated name: %s", name) } s := "" @@ -66,7 +63,7 @@ func (p *SimpleAllocationPlugin) GenerateHostname(route *routeapi.Route, shard * s = fmt.Sprintf("%s-%s.%s", name, route.Namespace, shard.DNSSuffix) } - glog.V(4).Infof("SimpleAllocationPlugin: Generated hostname=%s for Route: %s", s, route.ServiceName) + glog.V(4).Infof("Generated hostname=%s for Route: %s", s, route.ServiceName) return s } diff --git a/plugins/route/allocation/simple/plugin_test.go b/plugins/route/allocation/simple/plugin_test.go index df497273f829..38cc67b51bb1 100644 --- a/plugins/route/allocation/simple/plugin_test.go +++ b/plugins/route/allocation/simple/plugin_test.go @@ -162,7 +162,7 @@ func TestSimpleAllocationPluginViaController(t *testing.T) { sac := fac.Create(plugin) for _, tc := range tests { - shard, err := sac.Allocate(tc.route) + shard, err := sac.AllocateRouterShard(tc.route) if err != nil { t.Errorf("Test case %s got an error %s", tc.name, err) } From d0d6febf882e31a9188a598506267f5d65a73a95 Mon Sep 17 00:00:00 2001 From: ramr Date: Thu, 12 Mar 2015 13:23:34 -0700 Subject: [PATCH 3/3] Switch default route dns suffix to "router.default.local" --- pkg/cmd/server/origin/master.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index d509dfc54784..ba9dad8b32cd 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -93,7 +93,7 @@ const ( OpenShiftAPIPrefix = "/osapi" OpenShiftAPIV1Beta1 = "v1beta1" OpenShiftAPIPrefixV1Beta1 = OpenShiftAPIPrefix + "/" + OpenShiftAPIV1Beta1 - OpenShiftRouteSubdomain = "openshift.local" + OpenShiftRouteSubdomain = "router.default.local" swaggerAPIPrefix = "/swaggerapi/" )