mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
Merge pull request #1607 from tariq1890/add_vs
add new source for istio virtual services
This commit is contained in:
commit
a11414fa3b
@ -1,4 +1,4 @@
|
||||
# Configuring ExternalDNS to use the Istio Gateway Source
|
||||
# Configuring ExternalDNS to use the Istio Gateway and/or Istio Virtual Service Source
|
||||
This tutorial describes how to configure ExternalDNS to use the Istio Gateway source.
|
||||
It is meant to supplement the other provider-specific setup tutorials.
|
||||
|
||||
@ -32,7 +32,8 @@ spec:
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --source=istio-gateway
|
||||
- --source=istio-gateway # choose one
|
||||
- --source=istio-virtualservice # or both
|
||||
- --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
|
||||
- --provider=aws
|
||||
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
|
||||
@ -63,7 +64,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
- apiGroups: ["networking.istio.io"]
|
||||
resources: ["gateways"]
|
||||
resources: ["gateways", "virtualservices"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
@ -102,6 +103,7 @@ spec:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --source=istio-gateway
|
||||
- --source=istio-virtualservice
|
||||
- --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
|
||||
- --provider=aws
|
||||
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
|
||||
@ -130,7 +132,7 @@ kubectl patch clusterrole external-dns --type='json' \
|
||||
-p='[{"op": "add", "path": "/rules/4", "value": { "apiGroups": [ "networking.istio.io"], "resources": ["gateways"],"verbs": ["get", "watch", "list" ]} }]'
|
||||
```
|
||||
|
||||
### Verify ExternalDNS works (Gateway example)
|
||||
### Verify that Istio Gateway/VirtualService Source works
|
||||
|
||||
Follow the [Istio ingress traffic tutorial](https://istio.io/docs/tasks/traffic-management/ingress/)
|
||||
to deploy a sample service that will be exposed outside of the service mesh.
|
||||
@ -147,7 +149,8 @@ Otherwise:
|
||||
$ kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml)
|
||||
```
|
||||
|
||||
#### Create an Istio Gateway:
|
||||
#### Using a Gateway as a source
|
||||
##### Create an Istio Gateway:
|
||||
```bash
|
||||
$ cat <<EOF | kubectl apply -f -
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
@ -163,11 +166,11 @@ spec:
|
||||
name: http
|
||||
protocol: HTTP
|
||||
hosts:
|
||||
- "httpbin.example.com"
|
||||
- "httpbin.example.com" # this is used by external-dns to extract DNS names
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Configure routes for traffic entering via the Gateway:
|
||||
##### Configure routes for traffic entering via the Gateway:
|
||||
```bash
|
||||
$ cat <<EOF | kubectl apply -f -
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
@ -178,7 +181,56 @@ spec:
|
||||
hosts:
|
||||
- "httpbin.example.com"
|
||||
gateways:
|
||||
- httpbin-gateway
|
||||
- istio-system/httpbin-gateway
|
||||
http:
|
||||
- match:
|
||||
- uri:
|
||||
prefix: /status
|
||||
- uri:
|
||||
prefix: /delay
|
||||
route:
|
||||
- destination:
|
||||
port:
|
||||
number: 8000
|
||||
host: httpbin
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Using a VirtualService as a source
|
||||
|
||||
##### Create an Istio Gateway:
|
||||
```bash
|
||||
$ cat <<EOF | kubectl apply -f -
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: httpbin-gateway
|
||||
namespace: istio-system
|
||||
spec:
|
||||
selector:
|
||||
istio: ingressgateway # use Istio default gateway implementation
|
||||
servers:
|
||||
- port:
|
||||
number: 80
|
||||
name: http
|
||||
protocol: HTTP
|
||||
hosts:
|
||||
- "*"
|
||||
EOF
|
||||
```
|
||||
|
||||
##### Configure routes for traffic entering via the Gateway:
|
||||
```bash
|
||||
$ cat <<EOF | kubectl apply -f -
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
name: httpbin
|
||||
spec:
|
||||
hosts:
|
||||
- "httpbin.example.com" # this is used by external-dns to extract DNS names
|
||||
gateways:
|
||||
- istio-system/httpbin-gateway
|
||||
http:
|
||||
- match:
|
||||
- uri:
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
)
|
||||
|
||||
@ -44,7 +45,7 @@ func (m *MockSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
||||
// AddEventHandler adds an event handler that should be triggered if something in source changes
|
||||
func (m *MockSource) AddEventHandler(ctx context.Context, handler func()) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
|
@ -299,7 +299,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
|
||||
|
||||
// Flags related to processing sources
|
||||
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, cloudfoundry, contour-ingressroute, crd, empty, skipper-routegroup,openshift-route)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "cloudfoundry", "contour-ingressroute", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route")
|
||||
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, crd, empty, skipper-routegroup,openshift-route)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route")
|
||||
|
||||
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
|
||||
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
|
||||
|
@ -180,10 +180,12 @@ func (sc *gatewaySource) Endpoints() ([]*endpoint.Endpoint, error) {
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// TODO(tariq1890): Implement this once we have evaluated and tested GatewayInformers
|
||||
// AddEventHandler adds an event handler that should be triggered if the watched Istio Gateway changes.
|
||||
func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) {
|
||||
}
|
||||
|
||||
// filterByAnnotations2 filters a list of configs by a given annotation selector.
|
||||
// filterByAnnotations filters a list of configs by a given annotation selector.
|
||||
func (sc *gatewaySource) filterByAnnotations(gateways []networkingv1alpha3.Gateway) ([]networkingv1alpha3.Gateway, error) {
|
||||
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
|
||||
if err != nil {
|
||||
@ -220,23 +222,23 @@ func (sc *gatewaySource) setResourceLabel(gateway networkingv1alpha3.Gateway, en
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *gatewaySource) targetsFromGatewayConfig(gateway networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
|
||||
labelSelector, err := metav1.ParseToLabelSelector(labels.Set(gateway.Spec.Selector).String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (sc *gatewaySource) targetsFromGateway(gateway networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
|
||||
targets = getTargetsFromTargetAnnotation(gateway.Annotations)
|
||||
if len(targets) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(selector)
|
||||
services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if !gatewaySelectorMatchesServiceSelector(gateway.Spec.Selector, service.Spec.Selector) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, lb := range service.Status.LoadBalancer.Ingress {
|
||||
if lb.IP != "" {
|
||||
targets = append(targets, lb.IP)
|
||||
@ -262,7 +264,7 @@ func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway networ
|
||||
targets := getTargetsFromTargetAnnotation(annotations)
|
||||
|
||||
if len(targets) == 0 {
|
||||
targets, err = sc.targetsFromGatewayConfig(gateway)
|
||||
targets, err = sc.targetsFromGateway(gateway)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -315,3 +317,12 @@ func (sc *gatewaySource) hostNamesFromTemplate(gateway networkingv1alpha3.Gatewa
|
||||
hostnames := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",")
|
||||
return hostnames, nil
|
||||
}
|
||||
|
||||
func gatewaySelectorMatchesServiceSelector(gwSelector, svcSelector map[string]string) bool {
|
||||
for k, v := range gwSelector {
|
||||
if lbl, ok := svcSelector[k]; !ok || lbl != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -1151,6 +1151,7 @@ type fakeIngressGatewayService struct {
|
||||
hostnames []string
|
||||
namespace string
|
||||
name string
|
||||
selector map[string]string
|
||||
}
|
||||
|
||||
func (ig fakeIngressGatewayService) Service() *v1.Service {
|
||||
@ -1164,6 +1165,9 @@ func (ig fakeIngressGatewayService) Service() *v1.Service {
|
||||
Ingress: []v1.LoadBalancerIngress{},
|
||||
},
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: ig.selector,
|
||||
},
|
||||
}
|
||||
|
||||
for _, ip := range ig.ips {
|
||||
|
@ -17,15 +17,12 @@ limitations under the License.
|
||||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/internal/testutils"
|
||||
)
|
||||
|
||||
func TestGetTTLFromAnnotations(t *testing.T) {
|
||||
@ -108,35 +105,3 @@ func TestSuitableType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSourceEventHandler that AddEventHandler calls provided handler
|
||||
func TestSourceEventHandler(t *testing.T) {
|
||||
source := new(testutils.MockSource)
|
||||
|
||||
handlerCh := make(chan bool)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Define and register a simple handler that sends a message to a channel to show it was called.
|
||||
handler := func() {
|
||||
handlerCh <- true
|
||||
}
|
||||
// Example of preventing handler from being called more than once every 5 seconds.
|
||||
source.AddEventHandler(ctx, handler)
|
||||
|
||||
// Send timeout message after 10 seconds to fail test if handler is not called.
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Wait until we either receive a message from handlerCh or timeoutCh channel after 10 seconds.
|
||||
select {
|
||||
case msg := <-handlerCh:
|
||||
assert.True(t, msg)
|
||||
case <-ctx.Done():
|
||||
assert.Fail(t, "timed out waiting for event handler to be called")
|
||||
}
|
||||
|
||||
close(handlerCh)
|
||||
}
|
||||
|
@ -195,6 +195,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
|
||||
return nil, err
|
||||
}
|
||||
return NewIstioGatewaySource(kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
|
||||
case "istio-virtualservice":
|
||||
kubernetesClient, err := p.KubeClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
istioClient, err := p.IstioClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewIstioVirtualServiceSource(kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation)
|
||||
case "cloudfoundry":
|
||||
cfClient, err := p.CloudFoundryClient(cfg.CFAPIEndpoint, cfg.CFUsername, cfg.CFPassword)
|
||||
if err != nil {
|
||||
|
449
source/virtualservice.go
Normal file
449
source/virtualservice.go
Normal file
@ -0,0 +1,449 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes 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 source
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
istioclient "istio.io/client-go/pkg/clientset/versioned"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
)
|
||||
|
||||
// IstioMeshGateway is the built in gateway for all sidecars
|
||||
const IstioMeshGateway = "mesh"
|
||||
|
||||
// virtualServiceSource is an implementation of Source for Istio VirtualService objects.
|
||||
// The implementation uses the spec.hosts values for the hostnames.
|
||||
// Use targetAnnotationKey to explicitly set Endpoint.
|
||||
type virtualServiceSource struct {
|
||||
kubeClient kubernetes.Interface
|
||||
istioClient istioclient.Interface
|
||||
namespace string
|
||||
annotationFilter string
|
||||
fqdnTemplate *template.Template
|
||||
combineFQDNAnnotation bool
|
||||
ignoreHostnameAnnotation bool
|
||||
serviceInformer coreinformers.ServiceInformer
|
||||
}
|
||||
|
||||
// NewIstioVirtualServiceSource creates a new virtualServiceSource with the given config.
|
||||
func NewIstioVirtualServiceSource(
|
||||
kubeClient kubernetes.Interface,
|
||||
istioClient istioclient.Interface,
|
||||
namespace string,
|
||||
annotationFilter string,
|
||||
fqdnTemplate string,
|
||||
combineFQDNAnnotation bool,
|
||||
ignoreHostnameAnnotation bool,
|
||||
) (Source, error) {
|
||||
var (
|
||||
tmpl *template.Template
|
||||
err error
|
||||
)
|
||||
|
||||
if fqdnTemplate != "" {
|
||||
tmpl, err = template.New("endpoint").Funcs(template.FuncMap{
|
||||
"trimPrefix": strings.TrimPrefix,
|
||||
}).Parse(fqdnTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Use shared informers to listen for add/update/delete of services/pods/nodes in the specified namespace.
|
||||
// Set resync period to 0, to prevent processing when nothing has changed
|
||||
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace))
|
||||
serviceInformer := informerFactory.Core().V1().Services()
|
||||
|
||||
// Add default resource event handlers to properly initialize informer.
|
||||
serviceInformer.Informer().AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
log.Debug("service added")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// TODO informer is not explicitly stopped since controller is not passing in its channel.
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
// wait for the local cache to be populated.
|
||||
err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) {
|
||||
return serviceInformer.Informer().HasSynced(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sync cache: %v", err)
|
||||
}
|
||||
|
||||
return &virtualServiceSource{
|
||||
kubeClient: kubeClient,
|
||||
istioClient: istioClient,
|
||||
namespace: namespace,
|
||||
annotationFilter: annotationFilter,
|
||||
fqdnTemplate: tmpl,
|
||||
combineFQDNAnnotation: combineFQDNAnnotation,
|
||||
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
|
||||
serviceInformer: serviceInformer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Endpoints returns endpoint objects for each host-target combination that should be processed.
|
||||
// Retrieves all VirtualService resources in the source's namespace(s).
|
||||
func (sc *virtualServiceSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
||||
virtualServiceList, err := sc.istioClient.NetworkingV1alpha3().VirtualServices(sc.namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
virtualServices := virtualServiceList.Items
|
||||
virtualServices, err = sc.filterByAnnotations(virtualServices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
for _, virtualService := range virtualServices {
|
||||
// Check controller annotation to see if we are responsible.
|
||||
controller, ok := virtualService.Annotations[controllerAnnotationKey]
|
||||
if ok && controller != controllerAnnotationValue {
|
||||
log.Debugf("Skipping VirtualService %s/%s because controller value does not match, found: %s, required: %s",
|
||||
virtualService.Namespace, virtualService.Name, controller, controllerAnnotationValue)
|
||||
continue
|
||||
}
|
||||
|
||||
gwEndpoints, err := sc.endpointsFromVirtualService(virtualService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// apply template if host is missing on VirtualService
|
||||
if (sc.combineFQDNAnnotation || len(gwEndpoints) == 0) && sc.fqdnTemplate != nil {
|
||||
iEndpoints, err := sc.endpointsFromTemplate(virtualService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sc.combineFQDNAnnotation {
|
||||
gwEndpoints = append(gwEndpoints, iEndpoints...)
|
||||
} else {
|
||||
gwEndpoints = iEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
if len(gwEndpoints) == 0 {
|
||||
log.Debugf("No endpoints could be generated from VirtualService %s/%s", virtualService.Namespace, virtualService.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Endpoints generated from VirtualService: %s/%s: %v", virtualService.Namespace, virtualService.Name, gwEndpoints)
|
||||
sc.setResourceLabel(virtualService, gwEndpoints)
|
||||
endpoints = append(endpoints, gwEndpoints...)
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
sort.Sort(ep.Targets)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// TODO(tariq1890): Implement this once we have evaluated and tested VirtualServiceInformers
|
||||
// AddEventHandler adds an event handler that should be triggered if the watched Istio VirtualService changes.
|
||||
func (sc *virtualServiceSource) AddEventHandler(ctx context.Context, handler func()) {
|
||||
}
|
||||
|
||||
func (sc *virtualServiceSource) getGateway(gatewayStr string, virtualService networkingv1alpha3.VirtualService) *networkingv1alpha3.Gateway {
|
||||
if gatewayStr == "" || gatewayStr == IstioMeshGateway {
|
||||
// This refers to "all sidecars in the mesh"; ignore.
|
||||
return nil
|
||||
}
|
||||
|
||||
namespace, name, err := parseGateway(gatewayStr)
|
||||
if err != nil {
|
||||
log.Debugf("Failed parsing gatewayStr %s of VirtualService %s/%s", gatewayStr, virtualService.Namespace, virtualService.Name)
|
||||
return nil
|
||||
}
|
||||
if namespace == "" {
|
||||
namespace = virtualService.Namespace
|
||||
}
|
||||
|
||||
gateway, err := sc.istioClient.NetworkingV1alpha3().Gateways(namespace).Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("Failed retrieving gateway %s referenced by VirtualService %s/%s: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err)
|
||||
return nil
|
||||
}
|
||||
if gateway == nil {
|
||||
log.Debugf("Gateway %s referenced by VirtualService %s/%s not found: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return gateway
|
||||
}
|
||||
|
||||
func (sc *virtualServiceSource) endpointsFromTemplate(virtualService networkingv1alpha3.VirtualService) ([]*endpoint.Endpoint, error) {
|
||||
// Process the whole template string
|
||||
var buf bytes.Buffer
|
||||
err := sc.fqdnTemplate.Execute(&buf, virtualService)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to apply template on istio config %v: %v", virtualService, err)
|
||||
}
|
||||
|
||||
hostnamesTemplate := buf.String()
|
||||
|
||||
ttl, err := getTTLFromAnnotations(virtualService.Annotations)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
providerSpecific, setIdentifier := getProviderSpecificAnnotations(virtualService.Annotations)
|
||||
|
||||
// splits the FQDN template and removes the trailing periods
|
||||
hostnames := strings.Split(strings.Replace(hostnamesTemplate, " ", "", -1), ",")
|
||||
for _, hostname := range hostnames {
|
||||
hostname = strings.TrimSuffix(hostname, ".")
|
||||
targets, err := sc.targetsFromVirtualService(virtualService, hostname)
|
||||
if err != nil {
|
||||
return endpoints, err
|
||||
}
|
||||
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// filterByAnnotations filters a list of configs by a given annotation selector.
|
||||
func (sc *virtualServiceSource) filterByAnnotations(virtualservices []networkingv1alpha3.VirtualService) ([]networkingv1alpha3.VirtualService, error) {
|
||||
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// empty filter returns original list
|
||||
if selector.Empty() {
|
||||
return virtualservices, nil
|
||||
}
|
||||
|
||||
var filteredList []networkingv1alpha3.VirtualService
|
||||
|
||||
for _, virtualservice := range virtualservices {
|
||||
// convert the annotations to an equivalent label selector
|
||||
annotations := labels.Set(virtualservice.Annotations)
|
||||
|
||||
// include if the annotations match the selector
|
||||
if selector.Matches(annotations) {
|
||||
filteredList = append(filteredList, virtualservice)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredList, nil
|
||||
}
|
||||
|
||||
func (sc *virtualServiceSource) setResourceLabel(virtualservice networkingv1alpha3.VirtualService, endpoints []*endpoint.Endpoint) {
|
||||
for _, ep := range endpoints {
|
||||
ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("virtualservice/%s/%s", virtualservice.Namespace, virtualservice.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *virtualServiceSource) targetsFromVirtualService(virtualService networkingv1alpha3.VirtualService, vsHost string) ([]string, error) {
|
||||
var targets []string
|
||||
// for each host we need to iterate through the gateways because each host might match for only one of the gateways
|
||||
for _, gateway := range virtualService.Spec.Gateways {
|
||||
gateway := sc.getGateway(gateway, virtualService)
|
||||
if gateway == nil {
|
||||
continue
|
||||
}
|
||||
if !virtualServiceBindsToGateway(&virtualService, gateway, vsHost) {
|
||||
continue
|
||||
}
|
||||
tgs, err := sc.targetsFromGateway(gateway)
|
||||
if err != nil {
|
||||
return targets, err
|
||||
}
|
||||
targets = append(targets, tgs...)
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
// endpointsFromVirtualService extracts the endpoints from an Istio VirtualService Config object
|
||||
func (sc *virtualServiceSource) endpointsFromVirtualService(virtualservice networkingv1alpha3.VirtualService) ([]*endpoint.Endpoint, error) {
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
ttl, err := getTTLFromAnnotations(virtualservice.Annotations)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
targetsFromAnnotation := getTargetsFromTargetAnnotation(virtualservice.Annotations)
|
||||
|
||||
providerSpecific, setIdentifier := getProviderSpecificAnnotations(virtualservice.Annotations)
|
||||
|
||||
for _, host := range virtualservice.Spec.Hosts {
|
||||
if host == "" || host == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(host, "/")
|
||||
|
||||
// If the input hostname is of the form my-namespace/foo.bar.com, remove the namespace
|
||||
// before appending it to the list of endpoints to create
|
||||
if len(parts) == 2 {
|
||||
host = parts[1]
|
||||
}
|
||||
|
||||
targets := targetsFromAnnotation
|
||||
if len(targets) == 0 {
|
||||
targets, err = sc.targetsFromVirtualService(virtualservice, host)
|
||||
if err != nil {
|
||||
return endpoints, err
|
||||
}
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...)
|
||||
}
|
||||
|
||||
// Skip endpoints if we do not want entries from annotations
|
||||
if !sc.ignoreHostnameAnnotation {
|
||||
hostnameList := getHostnamesFromAnnotations(virtualservice.Annotations)
|
||||
for _, hostname := range hostnameList {
|
||||
targets := targetsFromAnnotation
|
||||
if len(targets) == 0 {
|
||||
targets, err = sc.targetsFromVirtualService(virtualservice, hostname)
|
||||
if err != nil {
|
||||
return endpoints, err
|
||||
}
|
||||
}
|
||||
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// checks if the given VirtualService should actually bind to the given gateway
|
||||
// see requirements here: https://istio.io/docs/reference/config/networking/gateway/#Server
|
||||
func virtualServiceBindsToGateway(virtualService *networkingv1alpha3.VirtualService, gateway *networkingv1alpha3.Gateway, vsHost string) bool {
|
||||
isValid := false
|
||||
if len(virtualService.Spec.ExportTo) == 0 {
|
||||
isValid = true
|
||||
} else {
|
||||
for _, ns := range virtualService.Spec.ExportTo {
|
||||
if ns == "*" || ns == gateway.Namespace || (ns == "." && gateway.Namespace == virtualService.Namespace) {
|
||||
isValid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isValid {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, server := range gateway.Spec.Servers {
|
||||
for _, host := range server.Hosts {
|
||||
namespace := "*"
|
||||
parts := strings.Split(host, "/")
|
||||
if len(parts) == 2 {
|
||||
namespace = parts[0]
|
||||
host = parts[1]
|
||||
} else if len(parts) != 1 {
|
||||
log.Debugf("Gateway %s/%s has invalid host %s", gateway.Namespace, gateway.Name, host)
|
||||
continue
|
||||
}
|
||||
|
||||
if namespace == "*" || namespace == virtualService.Namespace || (namespace == "." && virtualService.Namespace == gateway.Namespace) {
|
||||
if host == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
suffixMatch := false
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
suffixMatch = true
|
||||
}
|
||||
|
||||
if host == vsHost || (suffixMatch && strings.HasSuffix(vsHost, host[1:])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func parseGateway(gateway string) (namespace, name string, err error) {
|
||||
parts := strings.Split(gateway, "/")
|
||||
if len(parts) == 2 {
|
||||
namespace, name = parts[0], parts[1]
|
||||
} else if len(parts) == 1 {
|
||||
name = parts[0]
|
||||
} else {
|
||||
err = fmt.Errorf("invalid gateway name (name or namespace/name) found '%v'", gateway)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (sc *virtualServiceSource) targetsFromGateway(gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
|
||||
targets = getTargetsFromTargetAnnotation(gateway.Annotations)
|
||||
if len(targets) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if !gatewaySelectorMatchesServiceSelector(gateway.Spec.Selector, service.Spec.Selector) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, lb := range service.Status.LoadBalancer.Ingress {
|
||||
if lb.IP != "" {
|
||||
targets = append(targets, lb.IP)
|
||||
} else if lb.Hostname != "" {
|
||||
targets = append(targets, lb.Hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
1554
source/virtualservice_test.go
Normal file
1554
source/virtualservice_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user