From d476efb02c3f27d892f952459967bf06fb834f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Tue, 31 Mar 2020 13:38:42 +0000 Subject: [PATCH 1/9] Add OpenShift route as possible source --- go.mod | 2 + go.sum | 3 + source/ocproute.go | 278 +++++++++++++++++++++++++++++++++++++++++++++ source/store.go | 73 ++++++++++-- 4 files changed, 345 insertions(+), 11 deletions(-) create mode 100644 source/ocproute.go diff --git a/go.mod b/go.mod index e481347a73..1e9f44d2df 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,8 @@ require ( github.com/miekg/dns v1.0.14 github.com/nesv/go-dynect v0.6.0 github.com/nic-at/rc0go v1.1.0 + github.com/openshift/api v0.0.0-20190322043348-8741ff068a47 + github.com/openshift/client-go v3.9.0+incompatible github.com/oracle/oci-go-sdk v1.8.0 github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014 github.com/pkg/errors v0.8.1 diff --git a/go.sum b/go.sum index 48bcf53bb2..975c13abef 100644 --- a/go.sum +++ b/go.sum @@ -438,7 +438,10 @@ github.com/open-policy-agent/opa v0.8.2/go.mod h1:rlfeSeHuZmMEpmrcGla42AjkOUjP4r github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/openshift/api v0.0.0-20190322043348-8741ff068a47 h1:PAlaAXvwmPxgh8gm0/eVmNMGLeJ1bURwyKvJVLnsr6s= github.com/openshift/api v0.0.0-20190322043348-8741ff068a47/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= +github.com/openshift/client-go v3.9.0+incompatible h1:13k3Ok0B7TA2hA3bQW2aFqn6y04JaJWdk7ITTyg+Ek0= +github.com/openshift/client-go v3.9.0+incompatible/go.mod h1:6rzn+JTr7+WYS2E1TExP4gByoABxMznR6y2SnUIkmxk= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/operator-framework/operator-sdk v0.7.0/go.mod h1:iVyukRkam5JZa8AnjYf+/G3rk7JI1+M6GsU0sq0B9NA= diff --git a/source/ocproute.go b/source/ocproute.go new file mode 100644 index 0000000000..1c0cf0354f --- /dev/null +++ b/source/ocproute.go @@ -0,0 +1,278 @@ +/* +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" + "time" + + log "github.com/sirupsen/logrus" + routeapi "github.com/openshift/api/route/v1" + versioned "github.com/openshift/client-go/route/clientset/versioned" + extInformers "github.com/openshift/client-go/route/informers/externalversions" + routeInformer "github.com/openshift/client-go/route/informers/externalversions/route/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/cache" + + "sigs.k8s.io/external-dns/endpoint" +) + +// ocpRouteSource is an implementation of Source for OpenShift Route objects. +// Route implementation will use the spec.host value for the hostname +// Use targetAnnotationKey to explicitly set Endpoint. (useful if the router +// does not update, or to override with alternative endpoint) +type ocpRouteSource struct { + client versioned.Interface + namespace string + annotationFilter string + fqdnTemplate *template.Template + combineFQDNAnnotation bool + ignoreHostnameAnnotation bool + routeInformer routeInformer.RouteInformer +} + +// NewOcpRouteSource creates a new ocpRouteSource with the given config. +func NewOcpRouteSource( + ocpClient versioned.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 informer to listen for add/update/delete of Routes in the specified namespace. + // Set resync period to 0, to prevent processing when nothing has changed. + informerFactory := extInformers.NewFilteredSharedInformerFactory(ocpClient, 0, namespace, nil) + routeInformer := informerFactory.Route().V1().Routes() + + // Add default resource event handlers to properly initialize informer. + routeInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + }, + }, + ) + + // 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 routeInformer.Informer().HasSynced() == true, nil + }) + if err != nil { + return nil, fmt.Errorf("failed to sync cache: ${err}") + } + + return &ocpRouteSource{ + client: ocpClient, + namespace: namespace, + annotationFilter: annotationFilter, + fqdnTemplate: tmpl, + combineFQDNAnnotation: combineFQDNAnnotation, + ignoreHostnameAnnotation: ignoreHostnameAnnotation, + routeInformer: routeInformer, + }, nil +} + +// Endpoints returns endpoint objects for each host-target combination that should be processed. +// Retrieves all OpenShift Route resources on all namespaces +func (ors *ocpRouteSource) Endpoints() ([]*endpoint.Endpoint, error) { + ocpRoutes, err := ors.routeInformer.Lister().Routes(ors.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + ocpRoutes, err = ors.filterByAnnotations(ocpRoutes) + if err != nil { + return nil, err + } + + endpoints := []*endpoint.Endpoint{} + + for _, ocpRoute := range ocpRoutes { + // Check controller annotation to see if we are responsible. + controller, ok := ocpRoute.Annotations[controllerAnnotationKey] + if ok && controller != controllerAnnotationValue { + log.Debugf("Skipping OpenShift Route %s/%s because controller value does not match, found: %s, required: %s", + ocpRoute.Namespace, ocpRoute.Name, controller, controllerAnnotationValue) + continue + } + + orEndpoints := endpointsFromOcpRoute(ocpRoute, ors.ignoreHostnameAnnotation) + + // apply template if host is missing on OpenShift Route + if (ors.combineFQDNAnnotation || len(orEndpoints) ==0) && ors.fqdnTemplate != nil { + oEndpoints, err := ors.endpointsFromTemplate(ocpRoute) + if err != nil { + return nil, err + } + + if ors.combineFQDNAnnotation { + orEndpoints = append(orEndpoints, oEndpoints...) + } else { + orEndpoints = oEndpoints + } + } + + if len(orEndpoints) == 0 { + log.Debugf("No endpoints could be generated from OpenShift Route %s/%s", ocpRoute.Namespace, ocpRoute.Name) + continue + } + + log.Debugf("Endpoints generated from OpenShift Route: %s/%s: %v", ocpRoute.Namespace, ocpRoute.Name, orEndpoints) + ors.setResourceLabel(ocpRoute, orEndpoints) + endpoints = append(endpoints, orEndpoints...) + } + + for _, ep := range endpoints { + sort.Sort(ep.Targets) + } + + return endpoints, nil +} + +func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routeapi.Route) ([]*endpoint.Endpoint, error) { + // Process the whole template string + var buf bytes.Buffer + err := ors.fqdnTemplate.Execute(&buf, ocpRoute) + if err != nil { + return nil, fmt.Errorf("failed to apply template on OpenShift Route #{route.String()}: #{err}") + } + + hostnames := buf.String() + + ttl, err := getTTLFromAnnotations(ocpRoute.Annotations) + if err != nil { + log.Warn(err) + } + + targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations) + + if len(targets) == 0 { + targets = targetsFromOcpRouteStatus(ocpRoute.Status) + } + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations) + + 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, providerSpecific, setIdentifier)...) + } + return endpoints, nil +} + +func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routeapi.Route) ([]*routeapi.Route, error) { + labelSelector, err := metav1.ParseToLabelSelector(ors.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 ocpRoutes, nil + } + + filteredList := []*routeapi.Route{} + + for _, ocpRoute := range ocpRoutes { + // convert the Route's annotations to an equivalent label selector + annotations := labels.Set(ocpRoute.Annotations) + + // include ocpRoute if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, ocpRoute) + } + } + + return filteredList, nil +} + +func (ors *ocpRouteSource) setResourceLabel(ocpRoute *routeapi.Route, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("route/${ocpRoute.Namespace}/${ocpRoute.Name}") + } +} + +// endpointsFromOcpRoute extracts the endpoints from a OpenShift Route object +func endpointsFromOcpRoute(ocpRoute *routeapi.Route, ignoreHostnameAnnotation bool) []*endpoint.Endpoint { + var endpoints []*endpoint.Endpoint + + ttl, err := getTTLFromAnnotations(ocpRoute.Annotations) + if err != nil { + log.Warn(err) + } + + targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations) + + if len(targets) == 0 { + targets = targetsFromOcpRouteStatus(ocpRoute.Status) + } + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations) + + if host := ocpRoute.Spec.Host; host != "" { + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) + } + + // Skip endpoints if we do not want entries from annotations + if !ignoreHostnameAnnotation { + hostnameList := getHostnamesFromAnnotations(ocpRoute.Annotations) + for _, hostname := range hostnameList { + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) + } + } + return endpoints +} + +func targetsFromOcpRouteStatus(status routeapi.RouteStatus) endpoint.Targets { + var targets endpoint.Targets + + for _, ing := range status.Ingress { + if ing.Host != "" { + targets = append(targets, ing.Host) + } + } + + return targets +} \ No newline at end of file diff --git a/source/store.go b/source/store.go index a90e1ded83..d14b692749 100644 --- a/source/store.go +++ b/source/store.go @@ -26,6 +26,7 @@ import ( "github.com/cloudfoundry-community/go-cfclient" contour "github.com/heptio/contour/apis/generated/clientset/versioned" + openshift "github.com/openshift/client-go/route/clientset/versioned" "github.com/linki/instrumented_http" log "github.com/sirupsen/logrus" istiocontroller "istio.io/istio/pilot/pkg/config/kube/crd/controller" @@ -70,22 +71,25 @@ type ClientGenerator interface { IstioClient() (istiomodel.ConfigStore, error) CloudFoundryClient(cfAPPEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) ContourClient() (contour.Interface, error) + OpenShiftClient() (openshift.Interface, error) } // SingletonClientGenerator stores provider clients and guarantees that only one instance of client // will be generated type SingletonClientGenerator struct { - KubeConfig string - KubeMaster string - RequestTimeout time.Duration - kubeClient kubernetes.Interface - istioClient istiomodel.ConfigStore - cfClient *cfclient.Client - contourClient contour.Interface - kubeOnce sync.Once - istioOnce sync.Once - cfOnce sync.Once - contourOnce sync.Once + KubeConfig string + KubeMaster string + RequestTimeout time.Duration + kubeClient kubernetes.Interface + istioClient istiomodel.ConfigStore + cfClient *cfclient.Client + contourClient contour.Interface + openshiftClient openshift.Interface + kubeOnce sync.Once + istioOnce sync.Once + cfOnce sync.Once + contourOnce sync.Once + openshiftOnce sync.Once } // KubeClient generates a kube client if it was not created before @@ -139,6 +143,14 @@ func (p *SingletonClientGenerator) ContourClient() (contour.Interface, error) { return p.contourClient, err } +func (p *SingletonClientGenerator) OpenShiftClient() (openshift.Interface, error) { + var err error + p.openshiftOnce.Do(func() { + p.openshiftClient, err = NewOpenShiftClient(p.KubeConfig, p.KubeMaster, p.RequestTimeout) + }) + return p.openshiftClient, err +} + // ByNames returns multiple Sources given multiple names. func ByNames(p ClientGenerator, names []string, cfg *Config) ([]Source, error) { sources := []Source{} @@ -200,6 +212,12 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err return nil, err } return NewContourIngressRouteSource(kubernetesClient, contourClient, cfg.ContourLoadBalancerService, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + case "openshift-route": + ocpClient, err := p.OpenShiftClient() + if err != nil { + return nil, err + } + return NewOcpRouteSource(ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) case "fake": return NewFakeSource(cfg.FQDNTemplate) case "connector": @@ -354,3 +372,36 @@ func NewContourClient(kubeConfig, kubeMaster string, requestTimeout time.Duratio return client, nil } + +func NewOpenShiftClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) (*openshift.Clientset, error) { + if kubeConfig == "" { + if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { + kubeConfig = clientcmd.RecommendedHomeFile + } + } + + config, err := clientcmd.BuildConfigFromFlags(kubeMaster, kubeConfig) + if err != nil { + return nil, err + } + + config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{ + PathProcessor: func(path string) string { + parts := strings.Split(path, "/") + return parts[len(parts)-1] + }, + }) + } + + config.Timeout = requestTimeout + + client, err := openshift.NewForConfig(config) + if err != nil { + return nil, err + } + + log.Infof("Created OpenShift client %s", config.Host) + + return client, nil +} \ No newline at end of file From 228be31b3e7d7d58c0400299dc2bd035404fb466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Wed, 1 Apr 2020 06:43:53 +0000 Subject: [PATCH 2/9] Added empty eventhandler --- source/ocproute.go | 3 +++ source/store_test.go | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/source/ocproute.go b/source/ocproute.go index 1c0cf0354f..8e5107a1f4 100644 --- a/source/ocproute.go +++ b/source/ocproute.go @@ -108,6 +108,9 @@ func NewOcpRouteSource( }, nil } +func (ors *ocpRouteSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { +} + // Endpoints returns endpoint objects for each host-target combination that should be processed. // Retrieves all OpenShift Route resources on all namespaces func (ors *ocpRouteSource) Endpoints() ([]*endpoint.Endpoint, error) { diff --git a/source/store_test.go b/source/store_test.go index 5377fce4e7..8980efd711 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -22,6 +22,7 @@ import ( cfclient "github.com/cloudfoundry-community/go-cfclient" contour "github.com/heptio/contour/apis/generated/clientset/versioned" + openshift "github.com/openshift/client-go/route/clientset/versioned" fakeContour "github.com/heptio/contour/apis/generated/clientset/versioned/fake" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" @@ -36,6 +37,7 @@ type MockClientGenerator struct { istioClient istiomodel.ConfigStore cloudFoundryClient *cfclient.Client contourClient contour.Interface + openshiftClient openshift.Interface } func (m *MockClientGenerator) KubeClient() (kubernetes.Interface, error) { @@ -74,6 +76,15 @@ func (m *MockClientGenerator) ContourClient() (contour.Interface, error) { return nil, args.Error(1) } +func (m *MockClientGenerator) OpenShiftClient() (openshift.Interface, error) { + args := m.Called() + if args.Error(1) == nil { + m.openshiftClient = args.Get(0).(openshift.Interface) + return m.openshiftClient, nil + } + return nil, args.Error(1) +} + type ByNamesTestSuite struct { suite.Suite } From b1d6d3b688a5e09412e25f4252ae7c22c01b8bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Wed, 1 Apr 2020 07:14:37 +0000 Subject: [PATCH 3/9] source-type openshift-route was missing --- pkg/apis/externaldns/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index c567a68ead..12d7e4551c 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -295,7 +295,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)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "cloudfoundry", "contour-ingressroute", "fake", "connector", "crd", "empty", "skipper-routegroup") + 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("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) From 76851b809f12db225c6ac76ebd90d880f33841a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Wed, 1 Apr 2020 08:10:10 +0000 Subject: [PATCH 4/9] fixed sync cache --- source/ocproute.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/ocproute.go b/source/ocproute.go index 8e5107a1f4..f668eb8ecd 100644 --- a/source/ocproute.go +++ b/source/ocproute.go @@ -91,10 +91,10 @@ func NewOcpRouteSource( // wait for the local cache to be populated. err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) { - return routeInformer.Informer().HasSynced() == true, nil + return routeInformer.Informer().HasSynced(), nil }) if err != nil { - return nil, fmt.Errorf("failed to sync cache: ${err}") + return nil, fmt.Errorf("failed to sync cache: %v", err) } return &ocpRouteSource{ From acd32c237a5d612019875b99c9b8c5b673672d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Wed, 1 Apr 2020 13:36:01 +0000 Subject: [PATCH 5/9] Fixed fmt logoutput --- source/ocproute.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/ocproute.go b/source/ocproute.go index f668eb8ecd..444de920c1 100644 --- a/source/ocproute.go +++ b/source/ocproute.go @@ -173,7 +173,7 @@ func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routeapi.Route) ([]*e var buf bytes.Buffer err := ors.fqdnTemplate.Execute(&buf, ocpRoute) if err != nil { - return nil, fmt.Errorf("failed to apply template on OpenShift Route #{route.String()}: #{err}") + return nil, fmt.Errorf("failed to apply template on OpenShift Route %s: %s", ocpRoute.Name, err) } hostnames := buf.String() @@ -233,7 +233,7 @@ func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routeapi.Route) ([]* func (ors *ocpRouteSource) setResourceLabel(ocpRoute *routeapi.Route, endpoints []*endpoint.Endpoint) { for _, ep := range endpoints { - ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("route/${ocpRoute.Namespace}/${ocpRoute.Name}") + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name) } } From 7b89849bf4b224c125076c1f45ce3406c3bb7309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Tue, 7 Apr 2020 06:49:10 +0000 Subject: [PATCH 6/9] Some comments --- source/ocproute.go | 3 ++- source/store.go | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/source/ocproute.go b/source/ocproute.go index 444de920c1..9357ea7cca 100644 --- a/source/ocproute.go +++ b/source/ocproute.go @@ -108,6 +108,7 @@ func NewOcpRouteSource( }, nil } +// TODO add a meaningful EventHandler func (ors *ocpRouteSource) AddEventHandler(handler func() error, stopChan <-chan struct{}, minInterval time.Duration) { } @@ -138,7 +139,7 @@ func (ors *ocpRouteSource) Endpoints() ([]*endpoint.Endpoint, error) { orEndpoints := endpointsFromOcpRoute(ocpRoute, ors.ignoreHostnameAnnotation) // apply template if host is missing on OpenShift Route - if (ors.combineFQDNAnnotation || len(orEndpoints) ==0) && ors.fqdnTemplate != nil { + if (ors.combineFQDNAnnotation || len(orEndpoints) == 0) && ors.fqdnTemplate != nil { oEndpoints, err := ors.endpointsFromTemplate(ocpRoute) if err != nil { return nil, err diff --git a/source/store.go b/source/store.go index d14b692749..c949121d4b 100644 --- a/source/store.go +++ b/source/store.go @@ -143,6 +143,7 @@ func (p *SingletonClientGenerator) ContourClient() (contour.Interface, error) { return p.contourClient, err } +// OpenShiftClient generates an openshift client if it was not created before func (p *SingletonClientGenerator) OpenShiftClient() (openshift.Interface, error) { var err error p.openshiftOnce.Do(func() { @@ -373,6 +374,9 @@ func NewContourClient(kubeConfig, kubeMaster string, requestTimeout time.Duratio return client, nil } +// NewOpenShiftClient returns a new Openshift client object. It takes a Config and +// uses KubeMaster and KubeConfig attributes to connect to the cluster. If +// KubeConfig isn't provided it defaults to using the recommended default. func NewOpenShiftClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) (*openshift.Clientset, error) { if kubeConfig == "" { if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { From d3cb632659eef4f630b831abc248e0537ba49e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Tue, 7 Apr 2020 07:57:54 +0000 Subject: [PATCH 7/9] gofmt to remediate goimports'ed linting errors --- source/ocproute.go | 8 ++++---- source/store.go | 6 +++--- source/store_test.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/source/ocproute.go b/source/ocproute.go index 9357ea7cca..265acd9c6c 100644 --- a/source/ocproute.go +++ b/source/ocproute.go @@ -24,11 +24,11 @@ import ( "text/template" "time" - log "github.com/sirupsen/logrus" routeapi "github.com/openshift/api/route/v1" versioned "github.com/openshift/client-go/route/clientset/versioned" extInformers "github.com/openshift/client-go/route/informers/externalversions" routeInformer "github.com/openshift/client-go/route/informers/externalversions/route/v1" + log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/wait" @@ -43,7 +43,7 @@ import ( // does not update, or to override with alternative endpoint) type ocpRouteSource struct { client versioned.Interface - namespace string + namespace string annotationFilter string fqdnTemplate *template.Template combineFQDNAnnotation bool @@ -81,7 +81,7 @@ func NewOcpRouteSource( // Add default resource event handlers to properly initialize informer. routeInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { + AddFunc: func(obj interface{}) { }, }, ) @@ -279,4 +279,4 @@ func targetsFromOcpRouteStatus(status routeapi.RouteStatus) endpoint.Targets { } return targets -} \ No newline at end of file +} diff --git a/source/store.go b/source/store.go index c949121d4b..bb474fab60 100644 --- a/source/store.go +++ b/source/store.go @@ -26,8 +26,8 @@ import ( "github.com/cloudfoundry-community/go-cfclient" contour "github.com/heptio/contour/apis/generated/clientset/versioned" - openshift "github.com/openshift/client-go/route/clientset/versioned" "github.com/linki/instrumented_http" + openshift "github.com/openshift/client-go/route/clientset/versioned" log "github.com/sirupsen/logrus" istiocontroller "istio.io/istio/pilot/pkg/config/kube/crd/controller" istiomodel "istio.io/istio/pilot/pkg/model" @@ -391,7 +391,7 @@ func NewOpenShiftClient(kubeConfig, kubeMaster string, requestTimeout time.Durat config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{ - PathProcessor: func(path string) string { + PathProcessor: func(path string) string { parts := strings.Split(path, "/") return parts[len(parts)-1] }, @@ -408,4 +408,4 @@ func NewOpenShiftClient(kubeConfig, kubeMaster string, requestTimeout time.Durat log.Infof("Created OpenShift client %s", config.Host) return client, nil -} \ No newline at end of file +} diff --git a/source/store_test.go b/source/store_test.go index 8980efd711..f9acffa5d6 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -22,8 +22,8 @@ import ( cfclient "github.com/cloudfoundry-community/go-cfclient" contour "github.com/heptio/contour/apis/generated/clientset/versioned" - openshift "github.com/openshift/client-go/route/clientset/versioned" fakeContour "github.com/heptio/contour/apis/generated/clientset/versioned/fake" + openshift "github.com/openshift/client-go/route/clientset/versioned" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" istiomodel "istio.io/istio/pilot/pkg/model" From cdfd7f2da5c5e664f82929619de339213d71c54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Thu, 9 Apr 2020 13:23:21 +0000 Subject: [PATCH 8/9] Use RouterCanonicalHostname to avoid CNAME-loops --- source/ocproute.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/ocproute.go b/source/ocproute.go index 265acd9c6c..528cb8fcee 100644 --- a/source/ocproute.go +++ b/source/ocproute.go @@ -273,8 +273,8 @@ func targetsFromOcpRouteStatus(status routeapi.RouteStatus) endpoint.Targets { var targets endpoint.Targets for _, ing := range status.Ingress { - if ing.Host != "" { - targets = append(targets, ing.Host) + if ing.RouterCanonicalHostname != "" { + targets = append(targets, ing.RouterCanonicalHostname) } } From 3331392d74f5f5822150d4d9541db40900a99116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Grumb=C3=B6ck?= Date: Fri, 10 Apr 2020 09:44:05 +0000 Subject: [PATCH 9/9] added tutorial --- docs/tutorials/openshift.md | 172 ++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 docs/tutorials/openshift.md diff --git a/docs/tutorials/openshift.md b/docs/tutorials/openshift.md new file mode 100644 index 0000000000..6a2cec3740 --- /dev/null +++ b/docs/tutorials/openshift.md @@ -0,0 +1,172 @@ +# Configuring ExternalDNS to use the OpenShift Route Source +This tutorial describes how to configure ExternalDNS to use the OpenShift Route source. +It is meant to supplement the other provider-specific setup tutorials. + +### Prepare ROUTER_CANONICAL_HOSTNAME in default/router deployment +Read and go through [Finding the Host Name of the Router](https://docs.openshift.com/container-platform/3.11/install_config/router/default_haproxy_router.html#finding-router-hostname). +If no ROUTER_CANONICAL_HOSTNAME is set, you must annotate each route with external-dns.alpha.kubernetes.io/target! + +### Manifest (for clusters without RBAC enabled) +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + args: + - --source=openshift-route + - --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","endpoints","pods"] + verbs: ["get","watch","list"] +- apiGroups: ["extensions"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list"] +- apiGroups: ["route.openshift.io"] + resources: ["routes"] + 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: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + 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=openshift-route + - --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 (OpenShift Route example) +The following instructions are based on the +[Hello Openshift](https://github.com/openshift/origin/tree/master/examples/hello-openshift). + +#### Install a sample service and expose it +```bash +$ oc apply -f - <