// UpdateUrlMap translates the given hostname: endpoint->port mapping into a gce url map. // // HostRule: Conceptually contains all PathRules for a given host. // PathMatcher: Associates a path rule with a host rule. Mostly an optimization. // PathRule: Maps a single path regex to a backend. // // The GCE url map allows multiple hosts to share url->backend mappings without duplication, eg: // Host: foo(PathMatcher1), bar(PathMatcher1,2) // PathMatcher1: // /a -> b1 // /b -> b2 // PathMatcher2: // /c -> b1 // This leads to a lot of complexity in the common case, where all we want is a mapping of // host->{/path: backend}. // // Consider some alternatives: // 1. Using a single backend per PathMatcher: // Host: foo(PathMatcher1,3) bar(PathMatcher1,2,3) // PathMatcher1: // /a -> b1 // PathMatcher2: // /c -> b1 // PathMatcher3: // /b -> b2 // 2. Using a single host per PathMatcher: // Host: foo(PathMatcher1) // PathMatcher1: // /a -> b1 // /b -> b2 // Host: bar(PathMatcher2) // PathMatcher2: // /a -> b1 // /b -> b2 // /c -> b1 // In the context of kubernetes services, 2 makes more sense, because we // rarely want to lookup backends (service:nodeport). When a service is // deleted, we need to find all host PathMatchers that have the backend // and remove the mapping. When a new path is added to a host (happens // more frequently than service deletion) we just need to lookup the 1 // pathmatcher of the host. func (l *L7) UpdateUrlMap(ingressRules utils.GCEURLMap) error { if l.um == nil { return fmt.Errorf("Cannot add url without an urlmap.") } glog.V(3).Infof("Updating urlmap for l7 %v", l.Name) // All UrlMaps must have a default backend. If the Ingress has a default // backend, it applies to all host rules as well as to the urlmap itself. // If it doesn't the urlmap might have a stale default, so replace it with // glbc's default backend. defaultBackend := ingressRules.GetDefaultBackend() if defaultBackend != nil { l.um.DefaultService = defaultBackend.SelfLink } else { l.um.DefaultService = l.glbcDefaultBackend.SelfLink } glog.V(3).Infof("Updating url map %+v", ingressRules) // Every update replaces the entire urlmap. // TODO: when we have multiple loadbalancers point to a single gce url map // this needs modification. For now, there is a 1:1 mapping of urlmaps to // Ingresses, so if the given Ingress doesn't have a host rule we should // delete the path to that backend. l.um.HostRules = []*compute.HostRule{} l.um.PathMatchers = []*compute.PathMatcher{} for hostname, urlToBackend := range ingressRules { // Create a host rule // Create a path matcher // Add all given endpoint:backends to pathRules in path matcher pmName := getNameForPathMatcher(hostname) l.um.HostRules = append(l.um.HostRules, &compute.HostRule{ Hosts: []string{hostname}, PathMatcher: pmName, }) pathMatcher := &compute.PathMatcher{ Name: pmName, DefaultService: l.um.DefaultService, PathRules: []*compute.PathRule{}, } // Longest prefix wins. For equal rules, first hit wins, i.e the second // /foo rule when the first is deleted. for expr, be := range urlToBackend { pathMatcher.PathRules = append( pathMatcher.PathRules, &compute.PathRule{Paths: []string{expr}, Service: be.SelfLink}) } l.um.PathMatchers = append(l.um.PathMatchers, pathMatcher) } um, err := l.cloud.UpdateUrlMap(l.um) if err != nil { return err } l.um = um return nil }
func TestUpdateUrlMap(t *testing.T) { um1 := utils.GCEURLMap{ "bar.example.com": { "/bar2": &compute.BackendService{SelfLink: "bar2svc"}, }, } um2 := utils.GCEURLMap{ "foo.example.com": { "/foo1": &compute.BackendService{SelfLink: "foo1svc"}, "/foo2": &compute.BackendService{SelfLink: "foo2svc"}, }, "bar.example.com": { "/bar1": &compute.BackendService{SelfLink: "bar1svc"}, }, } um2.PutDefaultBackend(&compute.BackendService{SelfLink: "default"}) lbInfo := &L7RuntimeInfo{Name: "test", AllowHTTP: true} f := NewFakeLoadBalancers(lbInfo.Name) pool := newFakeLoadBalancerPool(f, t) pool.Add(lbInfo) l7, err := pool.Get(lbInfo.Name) if err != nil { t.Fatalf("%v", err) } for _, ir := range []utils.GCEURLMap{um1, um2} { if err := l7.UpdateUrlMap(ir); err != nil { t.Fatalf("%v", err) } } // The final map doesn't contain /bar2 expectedMap := map[string]utils.FakeIngressRuleValueMap{ utils.DefaultBackendKey: { utils.DefaultBackendKey: "default", }, "foo.example.com": { "/foo1": "foo1svc", "/foo2": "foo2svc", }, "bar.example.com": { "/bar1": "bar1svc", }, } f.CheckURLMap(t, l7, expectedMap) }
// toUrlMap converts an ingress to a map of subdomain: url-regex: gce backend. func (t *GCETranslator) toUrlMap(ing *extensions.Ingress) (utils.GCEURLMap, error) { hostPathBackend := utils.GCEURLMap{} for _, rule := range ing.Spec.Rules { if rule.HTTP == nil { glog.Errorf("Ignoring non http Ingress rule") continue } pathToBackend := map[string]*compute.BackendService{} for _, p := range rule.HTTP.Paths { backend, err := t.toGCEBackend(&p.Backend, ing.Namespace) if err != nil { // If a service doesn't have a nodeport we can still forward traffic // to all other services under the assumption that the user will // modify nodeport. if _, ok := err.(errorNodePortNotFound); ok { glog.Infof("%v", err) continue } // If a service doesn't have a backend, there's nothing the user // can do to correct this (the admin might've limited quota). // So keep requeuing the l7 till all backends exist. return utils.GCEURLMap{}, err } // The Ingress spec defines empty path as catch-all, so if a user // asks for a single host and multiple empty paths, all traffic is // sent to one of the last backend in the rules list. path := p.Path if path == "" { path = loadbalancers.DefaultPath } pathToBackend[path] = backend } // If multiple hostless rule sets are specified, last one wins host := rule.Host if host == "" { host = loadbalancers.DefaultHost } hostPathBackend[host] = pathToBackend } defaultBackend, _ := t.toGCEBackend(ing.Spec.Backend, ing.Namespace) hostPathBackend.PutDefaultBackend(defaultBackend) return hostPathBackend, nil }
// UpdateUrlMap translates the given hostname: endpoint->port mapping into a gce url map. // // HostRule: Conceptually contains all PathRules for a given host. // PathMatcher: Associates a path rule with a host rule. Mostly an optimization. // PathRule: Maps a single path regex to a backend. // // The GCE url map allows multiple hosts to share url->backend mappings without duplication, eg: // Host: foo(PathMatcher1), bar(PathMatcher1,2) // PathMatcher1: // /a -> b1 // /b -> b2 // PathMatcher2: // /c -> b1 // This leads to a lot of complexity in the common case, where all we want is a mapping of // host->{/path: backend}. // // Consider some alternatives: // 1. Using a single backend per PathMatcher: // Host: foo(PathMatcher1,3) bar(PathMatcher1,2,3) // PathMatcher1: // /a -> b1 // PathMatcher2: // /c -> b1 // PathMatcher3: // /b -> b2 // 2. Using a single host per PathMatcher: // Host: foo(PathMatcher1) // PathMatcher1: // /a -> b1 // /b -> b2 // Host: bar(PathMatcher2) // PathMatcher2: // /a -> b1 // /b -> b2 // /c -> b1 // In the context of kubernetes services, 2 makes more sense, because we // rarely want to lookup backends (service:nodeport). When a service is // deleted, we need to find all host PathMatchers that have the backend // and remove the mapping. When a new path is added to a host (happens // more frequently than service deletion) we just need to lookup the 1 // pathmatcher of the host. func (l *L7) UpdateUrlMap(ingressRules utils.GCEURLMap) error { if l.um == nil { return fmt.Errorf("Cannot add url without an urlmap.") } glog.V(3).Infof("Updating urlmap for l7 %v", l.Name) // All UrlMaps must have a default backend. If the Ingress has a default // backend, it applies to all host rules as well as to the urlmap itself. // If it doesn't the urlmap might have a stale default, so replace it with // glbc's default backend. defaultBackend := ingressRules.GetDefaultBackend() if defaultBackend != nil { l.um.DefaultService = defaultBackend.SelfLink } else { l.um.DefaultService = l.glbcDefaultBackend.SelfLink } glog.V(3).Infof("Updating url map %+v", ingressRules) for hostname, urlToBackend := range ingressRules { // Find the hostrule // Find the path matcher // Add all given endpoint:backends to pathRules in path matcher var hostRule *compute.HostRule pmName := getNameForPathMatcher(hostname) for _, hr := range l.um.HostRules { // TODO: Hostnames must be exact match? if hr.Hosts[0] == hostname { hostRule = hr break } } if hostRule == nil { // This is a new host hostRule = &compute.HostRule{ Hosts: []string{hostname}, PathMatcher: pmName, } // Why not just clobber existing host rules? // Because we can have multiple loadbalancers point to a single // gce url map when we have IngressClaims. l.um.HostRules = append(l.um.HostRules, hostRule) } var pathMatcher *compute.PathMatcher for _, pm := range l.um.PathMatchers { if pm.Name == hostRule.PathMatcher { pathMatcher = pm break } } if pathMatcher == nil { // This is a dangling or new host pathMatcher = &compute.PathMatcher{Name: pmName} l.um.PathMatchers = append(l.um.PathMatchers, pathMatcher) } pathMatcher.DefaultService = l.um.DefaultService // TODO: Every update replaces the entire path map. This will need to // change when we allow joining. Right now we call a single method // to verify current == desired and add new url mappings. pathMatcher.PathRules = []*compute.PathRule{} // Longest prefix wins. For equal rules, first hit wins, i.e the second // /foo rule when the first is deleted. for expr, be := range urlToBackend { pathMatcher.PathRules = append( pathMatcher.PathRules, &compute.PathRule{Paths: []string{expr}, Service: be.SelfLink}) } } um, err := l.cloud.UpdateUrlMap(l.um) if err != nil { return err } l.um = um return nil }