package controller
import (
"fmt"
"strings"
"github.com/golang/glog"
kapi "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/util/sets"
"k8s.io/kubernetes/pkg/watch"
routeapi "github.com/openshift/origin/pkg/route/api"
"github.com/openshift/origin/pkg/route/api/validation"
"github.com/openshift/origin/pkg/router"
)
// RouteHostFunc returns a host for a route. It may return an empty string.
type RouteHostFunc func(*routeapi.Route) string
// HostForRoute returns the host set on the route.
func HostForRoute(route *routeapi.Route) string {
return route.Spec.Host
}
type HostToRouteMap map[string][]*routeapi.Route
type RouteToHostMap map[string]string
// RejectionRecorder is an object capable of recording why a route was rejected
type RejectionRecorder interface {
RecordRouteRejection(route *routeapi.Route, reason, message string)
}
var LogRejections = logRecorder{}
type logRecorder struct{}
func (_ logRecorder) RecordRouteRejection(route *routeapi.Route, reason, message string) {
glog.V(4).Infof("Rejected route %s: %s: %s", route.Name, reason, message)
}
// UniqueHost implements the router.Plugin interface to provide
// a template based, backend-agnostic router.
type UniqueHost struct {
plugin router.Plugin
hostForRoute RouteHostFunc
recorder RejectionRecorder
hostToRoute HostToRouteMap
routeToHost RouteToHostMap
// nil means different than empty
allowedNamespaces sets.String
}
// NewUniqueHost creates a plugin wrapper that ensures only unique routes are passed into
// the underlying plugin. Recorder is an interface for indicating why a route was
// rejected.
func NewUniqueHost(plugin router.Plugin, fn RouteHostFunc, recorder RejectionRecorder) *UniqueHost {
return &UniqueHost{
plugin: plugin,
hostForRoute: fn,
recorder: recorder,
hostToRoute: make(HostToRouteMap),
routeToHost: make(RouteToHostMap),
}
}
// RoutesForHost is a helper that allows routes to be retrieved.
func (p *UniqueHost) RoutesForHost(host string) ([]*routeapi.Route, bool) {
routes, ok := p.hostToRoute[host]
return routes, ok
}
// HostLen returns the number of hosts currently tracked by this plugin.
func (p *UniqueHost) HostLen() int {
return len(p.hostToRoute)
}
// HandleEndpoints processes watch events on the Endpoints resource.
func (p *UniqueHost) HandleEndpoints(eventType watch.EventType, endpoints *kapi.Endpoints) error {
if p.allowedNamespaces != nil && !p.allowedNamespaces.Has(endpoints.Namespace) {
return nil
}
return p.plugin.HandleEndpoints(eventType, endpoints)
}
// HandleNode processes watch events on the Node resource and calls the router
func (p *UniqueHost) HandleNode(eventType watch.EventType, node *kapi.Node) error {
return p.plugin.HandleNode(eventType, node)
}
// HandleRoute processes watch events on the Route resource.
// TODO: this function can probably be collapsed with the router itself, as a function that
// determines which component needs to be recalculated (which template) and then does so
// on demand.
func (p *UniqueHost) HandleRoute(eventType watch.EventType, route *routeapi.Route) error {
if p.allowedNamespaces != nil && !p.allowedNamespaces.Has(route.Namespace) {
return nil
}
routeName := routeNameKey(route)
host := p.hostForRoute(route)
if len(host) == 0 {
glog.V(4).Infof("Route %s has no host value", routeName)
p.recorder.RecordRouteRejection(route, "NoHostValue", "no host value was defined for the route")
return nil
}
route.Spec.Host = host
// Run time check to defend against older routes. Validate that the
// route host name conforms to DNS requirements.
if errs := validation.ValidateHostName(route); len(errs) > 0 {
glog.V(4).Infof("Route %s - invalid host name %s", routeName, host)
errMessages := make([]string, len(errs))
for i := 0; i < len(errs); i++ {
errMessages[i] = errs[i].Error()
}
err := fmt.Errorf("host name validation errors: %s", strings.Join(errMessages, ", "))
p.recorder.RecordRouteRejection(route, "InvalidHost", err.Error())
return err
}
// ensure hosts can only be claimed by one namespace at a time
// TODO: this could be abstracted above this layer?
if old, ok := p.hostToRoute[host]; ok {
oldest := old[0]
// multiple paths can be added from the namespace of the oldest route
if oldest.Namespace == route.Namespace {
added := false
for i := range old {
if old[i].Spec.Path == route.Spec.Path {
if routeapi.RouteLessThan(old[i], route) {
glog.V(4).Infof("Route %s cannot take %s from %s", routeName, host, routeNameKey(oldest))
err := fmt.Errorf("route %s already exposes %s and is older", oldest.Name, host)
p.recorder.RecordRouteRejection(route, "HostAlreadyClaimed", err.Error())
return err
}
added = true
if old[i].Namespace == route.Namespace && old[i].Name == route.Name {
old[i] = route
break
}
glog.V(4).Infof("route %s will replace path %s from %s because it is older", routeName, route.Spec.Path, old[i].Name)
p.recorder.RecordRouteRejection(old[i], "HostAlreadyClaimed", fmt.Sprintf("replaced by older route %s", route.Name))
p.plugin.HandleRoute(watch.Deleted, old[i])
old[i] = route
}
}
if !added {
// Clean out any old form of this route
next := []*routeapi.Route{}
for i := range old {
if routeNameKey(old[i]) != routeNameKey(route) {
next = append(next, old[i])
}
}
old = next
// We need to reset the oldest in case we removed it, but if it was the only
// item, we'll just use ourselves since we'll become the oldest, and for
// the append below, it doesn't matter
if len(next) > 0 {
oldest = old[0]
} else {
oldest = route
}
if routeapi.RouteLessThan(route, oldest) {
p.hostToRoute[host] = append([]*routeapi.Route{route}, old...)
} else {
p.hostToRoute[host] = append(old, route)
}
}
} else {
if routeapi.RouteLessThan(oldest, route) {
glog.V(4).Infof("Route %s cannot take %s from %s", routeName, host, routeNameKey(oldest))
err := fmt.Errorf("a route in another namespace holds %s and is older than %s", host, route.Name)
p.recorder.RecordRouteRejection(route, "HostAlreadyClaimed", err.Error())
return err
}
glog.V(4).Infof("Route %s is reclaiming %s from namespace %s", routeName, host, oldest.Namespace)
for i := range old {
p.recorder.RecordRouteRejection(old[i], "HostAlreadyClaimed", fmt.Sprintf("namespace %s owns hostname %s", oldest.Namespace, host))
p.plugin.HandleRoute(watch.Deleted, old[i])
}
p.hostToRoute[host] = []*routeapi.Route{route}
}
} else {
glog.V(4).Infof("Route %s claims %s", routeName, host)
p.hostToRoute[host] = []*routeapi.Route{route}
}
switch eventType {
case watch.Added, watch.Modified:
if old, ok := p.routeToHost[routeName]; ok {
if old != host {
glog.V(4).Infof("Route %s changed from serving host %s to host %s", routeName, old, host)
delete(p.hostToRoute, old)
}
}
p.routeToHost[routeName] = host
return p.plugin.HandleRoute(eventType, route)
case watch.Deleted:
glog.V(4).Infof("Deleting routes for %s", routeName)
if old, ok := p.hostToRoute[host]; ok {
switch len(old) {
case 1, 0:
delete(p.hostToRoute, host)
default:
next := []*routeapi.Route{}
for i := range old {
if old[i].Name != route.Name {
next = append(next, old[i])
}
}
if len(next) > 0 {
p.hostToRoute[host] = next
} else {
delete(p.hostToRoute, host)
}
}
}
delete(p.routeToHost, routeName)
return p.plugin.HandleRoute(eventType, route)
}
return nil
}
// HandleAllowedNamespaces limits the scope of valid routes to only those that match
// the provided namespace list.
func (p *UniqueHost) HandleNamespaces(namespaces sets.String) error {
p.allowedNamespaces = namespaces
changed := false
for k, v := range p.hostToRoute {
if namespaces.Has(v[0].Namespace) {
continue
}
delete(p.hostToRoute, k)
for i := range v {
delete(p.routeToHost, routeNameKey(v[i]))
}
changed = true
}
if !changed && len(namespaces) > 0 {
return nil
}
return p.plugin.HandleNamespaces(namespaces)
}
func (p *UniqueHost) Commit() error {
return p.plugin.Commit()
}
// routeKeys returns the internal router key to use for the given Route.
func routeKeys(route *routeapi.Route) []string {
keys := make([]string, 1+len(route.Spec.AlternateBackends))
keys[0] = fmt.Sprintf("%s/%s", route.Namespace, route.Spec.To.Name)
for i, svc := range route.Spec.AlternateBackends {
keys[i] = fmt.Sprintf("%s/%s", route.Namespace, svc.Name)
}
return keys
}
// routeNameKey returns a unique name for a given route
func routeNameKey(route *routeapi.Route) string {
return fmt.Sprintf("%s/%s", route.Namespace, route.Name)
}