Commit b9b68421 authored by Jonas Michel's avatar Jonas Michel Committed by Martin Linkhorst
Browse files

Add Source implementation for Istio Gateway (#694)

* add Istio Gateway Source

* add documentation for Istio Gateway Source

* make both istio namespace and ingress gateway service configurable

* prefix gateway types, constructors, and flags with 'istio-'

* fix: add missing sources to source flag docs
parent a7ac4f9b
This diff is collapsed.
......@@ -46,7 +46,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"
version = "0.9.0-pre1"
[[constraint]]
name = "github.com/sirupsen/logrus"
......@@ -56,10 +56,6 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
name = "github.com/stretchr/testify"
version = "~1.2.1"
[[constraint]]
name = "k8s.io/client-go"
version = "~8.0.0"
[[override]]
name = "github.com/kubernetes/repo-infra"
revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee"
......@@ -82,4 +78,36 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[constraint]]
name = "github.com/aliyun/alibaba-cloud-sdk-go"
version = "1.27.7"
\ No newline at end of file
version = "1.27.7"
[[constraint]]
name = "istio.io/istio"
version = "1.0.0"
[[override]]
name = "github.com/golang/protobuf"
version = "1.1.0"
[[constraint]]
name = "k8s.io/client-go"
version = "8.0.0"
[[override]]
name = "k8s.io/apimachinery"
version = "kubernetes-1.11.0"
[[override]]
name = "k8s.io/api"
version = "kubernetes-1.11.0"
[[override]]
name = "golang.org/x/sys"
revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835"
[[override]]
name = "github.com/spf13/pflag"
version = "1.0.2"
[[override]]
name = "golang.org/x/net"
revision = "bb807669a61aca6092d8137da1fab2150bb96ad7"
......@@ -21,6 +21,7 @@ All sources live in package `source`.
* `ServiceSource`: collects all Services that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to an annotation set on the Service or is compiled from the Service attributes via the FQDN Go template string.
* `IngressSource`: collects all Ingresses that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to the host rules defined in the Ingress object.
* `IstioGatewaySource`: collects all Istio Gateways and returns them as Endpoint objects. The desired DNS name corresponds to the hosts listed within the servers spec of each Gateway object.
* `FakeSource`: returns a random list of Endpoints for the purpose of testing providers without having access to a Kubernetes cluster.
* `ConnectorSource`: returns a list of Endpoint objects which are served by a tcp server configured through `connector-source-server` flag.
* `CRDSource`: returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to [CRD source](../crd-source.md) documentation.
......
# Configuring ExternalDNS to use the Istio Gateway 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.
**Note:** Using the Istio Gateway source requires Istio >=1.0.0.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service
- --source=ingress
- --source=istio-gateway
- --istio-ingress-gateway=custom-istio-namespace/custom-istio-ingressgateway # omit to use the default (istio-system/istio-ingressgateway)
- --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
- --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
- --registry=txt
- --txt-owner-id=my-identifier
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
- apiGroups: ["networking.istio.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service
- --source=ingress
- --source=istio-gateway
- --istio-ingress-gateway=custom-istio-namespace/custom-istio-ingressgateway # omit to use the default (istio-system/istio-ingressgateway)
- --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
- --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
- --registry=txt
- --txt-owner-id=my-identifier
```
### Verify External DNS works (Gateway example)
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.
The following are relevant snippets from that tutorial.
#### Install a sample service
With automatic sidecar injection:
```bash
$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.0/samples/httpbin/httpbin.yaml
```
Otherwise:
```bash
$ kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.0/samples/httpbin/httpbin.yaml)
```
#### Create an Istio Gateway:
```bash
$ cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: httpbin-gateway
spec:
selector:
istio: ingressgateway # use Istio default gateway implementation
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "httpbin.example.com"
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"
gateways:
- httpbin-gateway
http:
- match:
- uri:
prefix: /status
- uri:
prefix: /delay
route:
- destination:
port:
number: 8000
host: httpbin
EOF
```
#### Access the sample service using `curl`
```bash
$ curl -I http://httpbin.example.com/status/200
HTTP/1.1 200 OK
server: envoy
date: Tue, 28 Aug 2018 15:26:47 GMT
content-type: text/html; charset=utf-8
access-control-allow-origin: *
access-control-allow-credentials: true
content-length: 0
x-envoy-upstream-service-time: 5
```
Accessing any other URL that has not been explicitly exposed should return an HTTP 404 error:
```bash
$ curl -I http://httpbin.example.com/headers
HTTP/1.1 404 Not Found
date: Tue, 28 Aug 2018 15:27:48 GMT
server: envoy
transfer-encoding: chunked
```
**Note:** The `-H` flag in the original Istio tutorial is no longer necessary in the `curl` commands.
......@@ -80,6 +80,7 @@ func main() {
KubeConfig: cfg.KubeConfig,
KubeMaster: cfg.Master,
ServiceTypeFilter: cfg.ServiceTypeFilter,
IstioIngressGateway: cfg.IstioIngressGateway,
}
// Lookup all the selected sources by names and pass them the desired configuration.
......
......@@ -39,6 +39,7 @@ type Config struct {
Master string
KubeConfig string
RequestTimeout time.Duration
IstioIngressGateway string
Sources []string
Namespace string
AnnotationFilter string
......@@ -102,6 +103,7 @@ var defaultConfig = &Config{
Master: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
IstioIngressGateway: "istio-system/istio-ingressgateway",
Sources: nil,
Namespace: "",
AnnotationFilter: "",
......@@ -196,8 +198,11 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)").Default(defaultConfig.KubeConfig).StringVar(&cfg.KubeConfig)
app.Flag("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout").Default(defaultConfig.RequestTimeout.String()).DurationVar(&cfg.RequestTimeout)
// Flags related to Istio
app.Flag("istio-ingress-gateway", "The fully-qualified name of the Istio ingress gateway service (default: istio-system/istio-ingressgateway)").Default(defaultConfig.IstioIngressGateway).StringVar(&cfg.IstioIngressGateway)
// 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, fake, connector)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "fake", "connector", "crd")
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake, connector, istio-gateway, crd").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "istio-gateway", "fake", "connector", "crd")
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)
app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate)
......
......@@ -32,6 +32,7 @@ var (
Master: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
IstioIngressGateway: "istio-system/istio-ingressgateway",
Sources: []string{"service"},
Namespace: "",
FQDNTemplate: "",
......@@ -81,6 +82,7 @@ var (
Master: "http://127.0.0.1:8080",
KubeConfig: "/some/path",
RequestTimeout: time.Second * 77,
IstioIngressGateway: "istio-other/istio-otheringressgateway",
Sources: []string{"service", "ingress", "connector"},
Namespace: "namespace",
FQDNTemplate: "{{.Name}}.service.example.com",
......@@ -153,6 +155,7 @@ func TestParseFlags(t *testing.T) {
"--master=http://127.0.0.1:8080",
"--kubeconfig=/some/path",
"--request-timeout=77s",
"--istio-ingress-gateway=istio-other/istio-otheringressgateway",
"--source=service",
"--source=ingress",
"--source=connector",
......@@ -215,6 +218,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_MASTER": "http://127.0.0.1:8080",
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
"EXTERNAL_DNS_REQUEST_TIMEOUT": "77s",
"EXTERNAL_DNS_ISTIO_INGRESS_GATEWAY": "istio-other/istio-otheringressgateway",
"EXTERNAL_DNS_SOURCE": "service\ningress\nconnector",
"EXTERNAL_DNS_NAMESPACE": "namespace",
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
......
/*
Copyright 2017 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"
"fmt"
"sort"
"strings"
"text/template"
log "github.com/sirupsen/logrus"
istionetworking "istio.io/api/networking/v1alpha3"
istiomodel "istio.io/istio/pilot/pkg/model"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"github.com/kubernetes-incubator/external-dns/endpoint"
"k8s.io/client-go/kubernetes"
)
// gatewaySource is an implementation of Source for Istio Gateway objects.
// The gateway implementation uses the spec.servers.hosts values for the hostnames.
// Use targetAnnotationKey to explicitly set Endpoint.
type gatewaySource struct {
kubeClient kubernetes.Interface
istioClient istiomodel.ConfigStore
istioNamespace string
istioIngressGatewayName string
namespace string
annotationFilter string
fqdnTemplate *template.Template
combineFQDNAnnotation bool
}
// NewIstioGatewaySource creates a new gatewaySource with the given config.
func NewIstioGatewaySource(
kubeClient kubernetes.Interface,
istioClient istiomodel.ConfigStore,
istioIngressGateway string,
namespace string,
annotationFilter string,
fqdnTemplate string,
combineFqdnAnnotation bool,
) (Source, error) {
var (
tmpl *template.Template
err error
)
istioNamespace, istioIngressGatewayName, err := parseIngressGateway(istioIngressGateway)
if err != nil {
return nil, err
}
if fqdnTemplate != "" {
tmpl, err = template.New("endpoint").Funcs(template.FuncMap{
"trimPrefix": strings.TrimPrefix,
}).Parse(fqdnTemplate)
if err != nil {
return nil, err
}
}
return &gatewaySource{
kubeClient: kubeClient,
istioClient: istioClient,
istioNamespace: istioNamespace,
istioIngressGatewayName: istioIngressGatewayName,
namespace: namespace,
annotationFilter: annotationFilter,
fqdnTemplate: tmpl,
combineFQDNAnnotation: combineFqdnAnnotation,
}, nil
}
// Endpoints returns endpoint objects for each host-target combination that should be processed.
// Retrieves all gateway resources in the source's namespace(s).
func (sc *gatewaySource) Endpoints() ([]*endpoint.Endpoint, error) {
configs, err := sc.istioClient.List(istiomodel.Gateway.Type, sc.namespace)
if err != nil {
return nil, err
}
configs, err = sc.filterByAnnotations(configs)
if err != nil {
return nil, err
}
endpoints := []*endpoint.Endpoint{}
for _, config := range configs {
// Check controller annotation to see if we are responsible.
controller, ok := config.Annotations[controllerAnnotationKey]
if ok && controller != controllerAnnotationValue {
log.Debugf("Skipping gateway %s/%s because controller value does not match, found: %s, required: %s",
config.Namespace, config.Name, controller, controllerAnnotationValue)
continue
}
gwEndpoints, err := sc.endpointsFromGatewayConfig(config)
if err != nil {
return nil, err
}
// apply template if host is missing on gateway
if (sc.combineFQDNAnnotation || len(gwEndpoints) == 0) && sc.fqdnTemplate != nil {
iEndpoints, err := sc.endpointsFromTemplate(&config)
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 gateway %s/%s", config.Namespace, config.Name)
continue
}
log.Debugf("Endpoints generated from gateway: %s/%s: %v", config.Namespace, config.Name, gwEndpoints)
sc.setResourceLabel(config, gwEndpoints)
endpoints = append(endpoints, gwEndpoints...)
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
return endpoints, nil
}
func (sc *gatewaySource) endpointsFromTemplate(config *istiomodel.Config) ([]*endpoint.Endpoint, error) {
// Process the whole template string
var buf bytes.Buffer
err := sc.fqdnTemplate.Execute(&buf, config)
if err != nil {
return nil, fmt.Errorf("failed to apply template on istio config %s: %v", config, err)
}
hostnames := buf.String()
ttl, err := getTTLFromAnnotations(config.Annotations)
if err != nil {
log.Warn(err)
}
targets := getTargetsFromTargetAnnotation(config.Annotations)
if len(targets) == 0 {
targets, err = sc.targetsFromIstioIngressStatus()
if err != nil {
return nil, err
}
}
var endpoints []*endpoint.Endpoint
// splits the FQDN template and removes the trailing periods
hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
for _, hostname := range hostnameList {
hostname = strings.TrimSuffix(hostname, ".")
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...)
}
return endpoints, nil
}
// filterByAnnotations filters a list of configs by a given annotation selector.
func (sc *gatewaySource) filterByAnnotations(configs []istiomodel.Config) ([]istiomodel.Config, 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 configs, nil
}
filteredList := []istiomodel.Config{}
for _, config := range configs {
// convert the annotations to an equivalent label selector
annotations := labels.Set(config.Annotations)
// include if the annotations match the selector
if selector.Matches(annotations) {
filteredList = append(filteredList, config)
}
}
return filteredList, nil
}
func (sc *gatewaySource) setResourceLabel(config istiomodel.Config, endpoints []*endpoint.Endpoint) {
for _, ep := range endpoints {
ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("gateway/%s/%s", config.Namespace, config.Name)
}
}
func (sc *gatewaySource) targetsFromIstioIngressStatus() (targets endpoint.Targets, err error) {
if svc, e := sc.kubeClient.CoreV1().Services(sc.istioNamespace).Get(sc.istioIngressGatewayName, metav1.GetOptions{}); e != nil {
err = e
} else {
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
}
return
}
// endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object
func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
ttl, err := getTTLFromAnnotations(config.Annotations)
if err != nil {
log.Warn(err)
}
targets := getTargetsFromTargetAnnotation(config.Annotations)
if len(targets) == 0 {
targets, err = sc.targetsFromIstioIngressStatus()
if err != nil {
return nil, err
}
}
gateway := config.Spec.(*istionetworking.Gateway)
for _, server := range gateway.Servers {
for _, host := range server.Hosts {
if host == "" {
continue
}
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl)...)
}
}
hostnameList := getHostnamesFromAnnotations(config.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...)
}
return endpoints, nil
}
func parseIngressGateway(ingressGateway string) (namespace, name string, err error) {
parts := strings.Split(ingressGateway, "/")
if len(parts) != 2 {
err = fmt.Errorf("invalid ingress gateway service (namespace/name) found '%v'", ingressGateway)
} else {
namespace, name = parts[0], parts[1]
}
return
}
This diff is collapsed.
......@@ -125,24 +125,6 @@ func (sc *ingressSource) Endpoints() ([]*endpoint.Endpoint, error) {
return endpoints, nil
}
// get endpoints from optional "target" annotation
// Returns empty endpoints array if none are found.
func getTargetsFromTargetAnnotation(ing *v1beta1.Ingress) endpoint.Targets {
var targets endpoint.Targets
// Get the desired hostname of the ingress from the annotation.
targetAnnotation, exists := ing.Annotations[targetAnnotationKey]
if exists && targetAnnotation != "" {
// splits the hostname annotation and removes the trailing periods
targetsList := strings.Split(strings.Replace(targetAnnotation, " ", "", -1), ",")
for _, targetHostname := range targetsList {
targetHostname = strings.TrimSuffix(targetHostname, ".")
targets = append(targets, targetHostname)
}
}
return targets
}
func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoint.Endpoint, error) {
// Process the whole template string
var buf bytes.Buffer
......@@ -158,7 +140,7 @@ func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoin
log.Warn(err)
}
targets := getTargetsFromTargetAnnotation(ing)
targets := getTargetsFromTargetAnnotation(ing.Annotations)
if len(targets) == 0 {
targets = targetsFromIngressStatus(ing.Status)
......@@ -220,7 +202,7 @@ func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint {
log.Warn(err)
}
targets := getTargetsFromTargetAnnotation(ing)
targets := getTargetsFromTargetAnnotation(ing.Annotations)
if len(targets) == 0 {
targets = targetsFromIngressStatus(ing.Status)
......@@ -250,46 +232,6 @@ func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint {
return endpoints
}
func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
var aTargets endpoint.Targets
var cnameTargets endpoint.Targets
for _, t := range targets {
switch suitableType(t) {
case endpoint.RecordTypeA:
aTargets = append(aTargets, t)
default:
cnameTargets = append(cnameTargets, t)
}
}
if len(aTargets) > 0 {
epA := &endpoint.Endpoint{
DNSName: strings.TrimSuffix(hostname, "."),
Targets: aTargets,
RecordTTL: ttl,
RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(),
}
endpoints = append(endpoints, epA)
}
if len(cnameTargets) > 0 {
epCNAME := &endpoint.Endpoint{
DNSName: strings.TrimSuffix(hostname, "."),
Targets: cnameTargets,
RecordTTL: ttl,
RecordType: endpoint.RecordTypeCNAME,
Labels: endpoint.NewLabels(),
}
endpoints = append(endpoints, epCNAME)
}
return endpoints
}
func targetsFromIngressStatus(status v1beta1.IngressStatus) endpoint.Targets {
var targets endpoint.Targets
......
......@@ -74,6 +74,24 @@ func getHostnamesFromAnnotations(annotations map[string]string) []string {
return strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")
}
// getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
// Returns empty endpoints array if none are found.
func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets {
var targets endpoint.Targets
// Get the desired hostname of the ingress from the annotation.
targetAnnotation, exists := annotations[targetAnnotationKey]
if exists && targetAnnotation != "" {
// splits the hostname annotation and removes the trailing periods
targetsList := strings.Split(strings.Replace(targetAnnotation, " ", "", -1), ",")
for _, targetHostname := range targetsList {
targetHostname = strings.TrimSuffix(targetHostname, ".")
targets = append(targets, targetHostname)
}
}
return targets
}
// suitableType returns the DNS resource record type suitable for the target.
// In this case type A for IPs and type CNAME for everything else.
func suitableType(target string) string {
......@@ -82,3 +100,44 @@ func suitableType(target string) string {
}
return endpoint.RecordTypeCNAME
}
// endpointsForHostname returns the endpoint objects for each host-target combination.
func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
var aTargets endpoint.Targets
var cnameTargets endpoint.Targets
for _, t := range targets {
switch suitableType(t) {
case endpoint.RecordTypeA:
aTargets = append(aTargets, t)
default:
cnameTargets = append(cnameTargets, t)
}
}
if len(aTargets) > 0 {
epA := &endpoint.Endpoint{
DNSName: strings.TrimSuffix(hostname, "."),
Targets: aTargets,
RecordTTL: ttl,
RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(),
}
endpoints = append(endpoints, epA)
}
if len(cnameTargets) > 0 {
epCNAME := &endpoint.Endpoint{
DNSName: strings.TrimSuffix(hostname, "."),
Targets: cnameTargets,
RecordTTL: ttl,
RecordType: endpoint.RecordTypeCNAME,
Labels: endpoint.NewLabels(),
}
endpoints = append(endpoints, epCNAME)
}
return endpoints
}
......@@ -27,6 +27,8 @@ import (
"github.com/linki/instrumented_http"
log "github.com/sirupsen/logrus"
istiocrd "istio.io/istio/pilot/pkg/config/kube/crd"
istiomodel "istio.io/istio/pilot/pkg/model"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
......@@ -49,11 +51,13 @@ type Config struct {
KubeConfig string
KubeMaster string
ServiceTypeFilter []string
IstioIngressGateway string
}
// ClientGenerator provides clients
type ClientGenerator interface {
KubeClient() (kubernetes.Interface, error)
IstioClient() (istiomodel.ConfigStore, error)
}
// SingletonClientGenerator stores provider clients and guarantees that only one instance of client
......@@ -62,17 +66,28 @@ type SingletonClientGenerator struct {
KubeConfig string
KubeMaster string
RequestTimeout time.Duration
client kubernetes.Interface
sync.Once
kubeClient kubernetes.Interface
istioClient istiomodel.ConfigStore
kubeOnce sync.Once
istioOnce sync.Once
}
// KubeClient generates a kube client if it was not created before
func (p *SingletonClientGenerator) KubeClient() (kubernetes.Interface, error) {
var err error
p.Once.Do(func() {
p.client, err = NewKubeClient(p.KubeConfig, p.KubeMaster, p.RequestTimeout)
p.kubeOnce.Do(func() {
p.kubeClient, err = NewKubeClient(p.KubeConfig, p.KubeMaster, p.RequestTimeout)
})
return p.client, err
return p.kubeClient, err
}
// IstioClient generates an istio client if it was not created before
func (p *SingletonClientGenerator) IstioClient() (istiomodel.ConfigStore, error) {
var err error
p.istioOnce.Do(func() {
p.istioClient, err = NewIstioClient(p.KubeConfig)
})
return p.istioClient, err
}
// ByNames returns multiple Sources given multiple names.
......@@ -104,6 +119,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
return nil, err
}
return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation)
case "istio-gateway":
kubernetesClient, err := p.KubeClient()
if err != nil {
return nil, err
}
istioClient, err := p.IstioClient()
if err != nil {
return nil, err
}
return NewIstioGatewaySource(kubernetesClient, istioClient, cfg.IstioIngressGateway, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation)
case "fake":
return NewFakeSource(cfg.FQDNTemplate)
case "connector":
......@@ -153,7 +178,37 @@ func NewKubeClient(kubeConfig, kubeMaster string, requestTimeout time.Duration)
return nil, err
}
log.Infof("Connected to cluster at %s", config.Host)
log.Infof("Created Kubernetes client %s", config.Host)
return client, nil
}
// NewIstioClient returns a new Istio client object. It uses the configured
// KubeConfig attribute to connect to the cluster. If KubeConfig isn't provided
// it defaults to using the recommended default.
// NB: Istio controls the creation of the underlying Kubernetes client, so we
// have no ability to tack on transport wrappers (e.g., Prometheus request
// wrappers) to the client's config at this level. Furthermore, the Istio client
// constructor does not expose the ability to override the Kubernetes master,
// so the Master config attribute has no effect.
func NewIstioClient(kubeConfig string) (*istiocrd.Client, error) {
if kubeConfig == "" {
if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil {
kubeConfig = clientcmd.RecommendedHomeFile
}
}
client, err := istiocrd.NewClient(
kubeConfig,
"",
istiomodel.ConfigDescriptor{istiomodel.Gateway},
"",
)
if err != nil {
return nil, err
}
log.Info("Created Istio client")
return client, nil
}
......@@ -20,6 +20,7 @@ import (
"errors"
"testing"
istiomodel "istio.io/istio/pilot/pkg/model"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
......@@ -29,14 +30,24 @@ import (
type MockClientGenerator struct {
mock.Mock
client kubernetes.Interface
kubeClient kubernetes.Interface
istioClient istiomodel.ConfigStore
}
func (m *MockClientGenerator) KubeClient() (kubernetes.Interface, error) {
args := m.Called()
if args.Error(1) == nil {
m.client = args.Get(0).(kubernetes.Interface)
return m.client, nil
m.kubeClient = args.Get(0).(kubernetes.Interface)
return m.kubeClient, nil
}
return nil, args.Error(1)
}
func (m *MockClientGenerator) IstioClient() (istiomodel.ConfigStore, error) {
args := m.Called()
if args.Error(1) == nil {
m.istioClient = args.Get(0).(istiomodel.ConfigStore)
return m.istioClient, nil
}
return nil, args.Error(1)
}
......@@ -48,28 +59,29 @@ type ByNamesTestSuite struct {
func (suite *ByNamesTestSuite) TestAllInitialized() {
mockClientGenerator := new(MockClientGenerator)
mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil)
mockClientGenerator.On("IstioClient").Return(NewFakeConfigStore(), nil)
sources, err := ByNames(mockClientGenerator, []string{"service", "ingress", "fake"}, &Config{})
sources, err := ByNames(mockClientGenerator, []string{"service", "ingress", "istio-gateway", "fake"}, minimalConfig)
suite.NoError(err, "should not generate errors")
suite.Len(sources, 3, "should generate all three sources")
suite.Len(sources, 4, "should generate all four sources")
}
func (suite *ByNamesTestSuite) TestOnlyFake() {
mockClientGenerator := new(MockClientGenerator)
mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil)
sources, err := ByNames(mockClientGenerator, []string{"fake"}, &Config{})
sources, err := ByNames(mockClientGenerator, []string{"fake"}, minimalConfig)
suite.NoError(err, "should not generate errors")
suite.Len(sources, 1, "should generate all three sources")
suite.Nil(mockClientGenerator.client, "client should not be created")
suite.Len(sources, 1, "should generate fake source")
suite.Nil(mockClientGenerator.kubeClient, "client should not be created")
}
func (suite *ByNamesTestSuite) TestSourceNotFound() {
mockClientGenerator := new(MockClientGenerator)
mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil)
sources, err := ByNames(mockClientGenerator, []string{"foo"}, &Config{})
suite.Equal(err, ErrSourceNotFound, "should return sourcen not found")
sources, err := ByNames(mockClientGenerator, []string{"foo"}, minimalConfig)
suite.Equal(err, ErrSourceNotFound, "should return source not found")
suite.Len(sources, 0, "should not returns any source")
}
......@@ -77,13 +89,29 @@ func (suite *ByNamesTestSuite) TestKubeClientFails() {
mockClientGenerator := new(MockClientGenerator)
mockClientGenerator.On("KubeClient").Return(nil, errors.New("foo"))
_, err := ByNames(mockClientGenerator, []string{"service"}, &Config{})
suite.Error(err, "should return an error if client cannot be created")
_, err := ByNames(mockClientGenerator, []string{"service"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
_, err = ByNames(mockClientGenerator, []string{"ingress"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
_, err = ByNames(mockClientGenerator, []string{"ingress"}, &Config{})
suite.Error(err, "should return an error if client cannot be created")
_, err = ByNames(mockClientGenerator, []string{"istio-gateway"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
}
func (suite *ByNamesTestSuite) TestIstioClientFails() {
mockClientGenerator := new(MockClientGenerator)
mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil)
mockClientGenerator.On("IstioClient").Return(nil, errors.New("foo"))
_, err := ByNames(mockClientGenerator, []string{"istio-gateway"}, minimalConfig)
suite.Error(err, "should return an error if istio client cannot be created")
}
func TestByNames(t *testing.T) {
suite.Run(t, new(ByNamesTestSuite))
}
var minimalConfig = &Config{
IstioIngressGateway: "istio-system/istio-ingressgateway",
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment