diff --git a/pilot/pkg/model/push_context.go b/pilot/pkg/model/push_context.go index 643fb6e74de1..075648266063 100644 --- a/pilot/pkg/model/push_context.go +++ b/pilot/pkg/model/push_context.go @@ -69,7 +69,7 @@ type PushContext struct { // ServiceAccounts map[string][]string // Temp: the code in alpha3 should use VirtualService directly VirtualServiceConfigs []Config `json:"-,omitempty"` - + destinationRuleHosts []Hostname destinationRuleByHosts map[Hostname]*combinedDestinationRule @@ -290,6 +290,10 @@ func (ps *PushContext) VirtualServices(gateways map[string]bool) []Config { return out } +func (ps *PushContext) VirtualService(gateways map[string]bool, hostname Hostname) (Config, bool) { + return Config{}, false +} + // InitContext will initialize the data structures used for code generation. // This should be called before starting the push, from the thread creating // the push context. @@ -332,7 +336,8 @@ func (ps *PushContext) initServiceRegistry(env *Environment) error { return err } // Sort the services in order of creation. - ps.Services = sortServicesByCreationTime(services) + sortServicesByCreationTime(services) + ps.Services = services for _, s := range services { ps.ServiceByHostname[s.Hostname] = s } @@ -340,11 +345,10 @@ func (ps *PushContext) initServiceRegistry(env *Environment) error { } // sortServicesByCreationTime sorts the list of services in ascending order by their creation time (if available). -func sortServicesByCreationTime(services []*Service) []*Service { +func sortServicesByCreationTime(services []*Service) { sort.SliceStable(services, func(i, j int) bool { return services[i].CreationTime.Before(services[j].CreationTime) }) - return services } // Caches list of virtual services @@ -353,7 +357,6 @@ func (ps *PushContext) initVirtualServices(env *Environment) error { if err != nil { return err } - sortConfigByCreationTime(vservices) ps.VirtualServiceConfigs = vservices // convert all shortnames in virtual services into FQDNs diff --git a/pilot/pkg/model/radix.go b/pilot/pkg/model/radix.go new file mode 100644 index 000000000000..3e135c994cde --- /dev/null +++ b/pilot/pkg/model/radix.go @@ -0,0 +1,112 @@ +// Copyright 2018 Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +import ( + "github.com/hashicorp/go-immutable-radix" + "strings" +) + +// TODO: if there are conflicts, pick the oldest config + +type hostLookup interface { + Lookup(hostname Hostname) map[Hostname]Config +} + +type radix struct { + radix *iradix.Tree +} + +func newRadix() *radix { + return &radix{ + radix: iradix.New(), + } +} + +// This function returns the set of the most specific matching configs +// for a hostname. It supports wildcards in both the query hostname as well +// as the config hostnames. +// +// To retrieve the most specific matches, we need to define what we consider more or less specific. +// We define the specificity of a match as the amount of the query host that is matched with the +// config host. A match where more of the query host is matched is considered more specific. +// +// Consider the query hostname "abc.def" and config hostnames "abc.def" and "*.def": +// - The query hostname "abc.def" matches both "abc.def" and "*.def" +// - The query hostname "abc.def" has an exact match of all 7 characters of "abc.def", but only 4 +// characters of "*.def". Therefore the match of "abc.def" is considered more specific than +// the match of "*.def". +// +// This definition of specificity becomes important when wildcards are present in both the query +// host and the config host. When the query host contains a wildcard, there can be multiple +// equally specific matches. This is illustrated below with an example: +// +// The query host "*.def" matches "abc.def", "*.def", and "*" +// - the match with "abc.def" and "*.def" have equal specificity: both have an exact match of +// 4 characters with the query host. +// - the match of "*" is less specific than the other two matches, since the exact match is 0 characters. +// thus, the most specific matches are "abc.def" and "*.def" +// +// This function uses a radix to implement the behavior described above. +func (r *radix) Lookup(hostname Hostname) map[Hostname]Config { + configs := make(map[Hostname]Config) + wildcard := strings.Contains(string(hostname), "*") + + // If a wildcard is present in the query hostname there may be multiple equally specific matches, + // so we attempt to walk every config hostname under this prefix. + if wildcard { + r.radix.Root().WalkPrefix(r.toKey(hostname), func(k []byte, v interface{}) bool { + config, _ := v.(Config) + configs[r.fromKey(k)] = config + return false + }) + } + + // If the query hostname has no wildcard, or there were no configs under the prefix, we get the + // longest matching prefix for this query hostname. + if !wildcard || len(configs) == 0 { + k, v, _ := r.radix.Root().LongestPrefix(r.toKey(hostname)) + config, _ := v.(Config) + configs[r.fromKey(k)] = config + } + + return configs +} + +func (r *radix) Insert(hostname Hostname, config Config) { + r.radix, _, _ = r.radix.Insert(r.toKey(hostname), config) +} + +// Strips the wildcard character '*' and stores the hostname in the radix in reversed character order. +func (r *radix) toKey(hostname Hostname) []byte { + s := strings.Replace(string(hostname), "*", "", -1) + data := []byte(s) + reverse(data) + return data +} + +// Unreverses the hostname. +func (r *radix) fromKey(key []byte) Hostname { + data := make([]byte, len(key)) + copy(data, key) + reverse(data) + return Hostname(data) +} + +func reverse(data []byte) { + for i := 0; i < len(data)/2; i++ { + data[i], data[len(data)-i-1] = data[len(data)-i-1], data[i] + } +} diff --git a/pilot/pkg/model/radix_test.go b/pilot/pkg/model/radix_test.go new file mode 100644 index 000000000000..547c8f6804f7 --- /dev/null +++ b/pilot/pkg/model/radix_test.go @@ -0,0 +1,72 @@ +// Copyright 2018 Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package model + +import ( + "testing" +) + +func TestRadix(t *testing.T) { + r := newRadix() + + contents := []struct { + config Config + hostnames Hostnames + }{ + {Config{ConfigMeta: ConfigMeta{Name: "cnn"}}, Hostnames{"www.cnn.com", "*.cnn.com", "*.com"}}, + {Config{ConfigMeta: ConfigMeta{Name: "edition_cnn"}}, Hostnames{"edition.cnn.com"}}, + {Config{ConfigMeta: ConfigMeta{Name: "*.co.uk"}}, Hostnames{"*.co.uk"}}, + {Config{ConfigMeta: ConfigMeta{Name: "*"}}, Hostnames{"*"}}, + {Config{ConfigMeta: ConfigMeta{Name: "io"}}, Hostnames{"*.io"}}, + {Config{ConfigMeta: ConfigMeta{Name: "*.preliminary.io"}}, Hostnames{"*.preliminary.io"}}, + } + + for _, content := range contents { + for _, hostname := range content.hostnames { + r.Insert(hostname, content.config) + } + } + + testCases := []struct { + in Hostname + out Hostnames + }{ + {"www.cnn.com", Hostnames{"www.cnn.com"}}, + {"money.cnn.com", Hostnames{".cnn.com"}}, + {"edition.cnn.com", Hostnames{"edition.cnn.com"}}, + {"bbc.co.uk", Hostnames{".co.uk"}}, + {"www.wikipedia.org", Hostnames{""}}, + {"*.cnn.com", Hostnames{"www.cnn.com", ".cnn.com", "edition.cnn.com"}}, + {"*.com", Hostnames{".com", ".cnn.com", "www.cnn.com", "edition.cnn.com"}}, + {"*.uk", Hostnames{".co.uk"}}, + {"*.istio.io", Hostnames{".io"}}, + {"*.preliminary.io", Hostnames{".preliminary.io"}}, + {"*.io", Hostnames{".io", ".preliminary.io"}}, + {"nothing.nowhere.net", Hostnames{""}}, + // {"*", Hostnames{"www.cnn.com", ".cnn.com", ".com", "edition.cnn.com", "", ".co.uk"}}, // maintenance burden + } + + for _, tt := range testCases { + configs := r.Lookup(tt.in) + if len(tt.out) != len(configs) { + t.Errorf("f(%v) -> wanted len()=%v, got len()=%v", tt.in, len(tt.out), len(configs)) + t.Errorf("%#v", configs) + } + for _, h := range tt.out { + if _, ok := configs[h]; !ok { + t.Errorf("f(%v) -> missing %v", tt.in, h) + } + } + } +} diff --git a/pilot/pkg/networking/core/v1alpha3/listener.go b/pilot/pkg/networking/core/v1alpha3/listener.go index c9c256010284..021f89127338 100644 --- a/pilot/pkg/networking/core/v1alpha3/listener.go +++ b/pilot/pkg/networking/core/v1alpha3/listener.go @@ -383,6 +383,7 @@ func (c outboundListenerConflict) addMetric(push *model.PushContext) { // buildSidecarOutboundListeners generates http and tcp listeners for outbound connections from the service instance // TODO(github.com/istio/pilot/issues/237) // +// FIXME: the doc below is out of date... // Sharing tcp_proxy and http_connection_manager filters on the same port for // different destination services doesn't work with Envoy (yet). When the // tcp_proxy filter's route matching fails for the http service the connection