mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-09-20 15:41:01 +02:00
Add OpenShift route as possible source
This commit is contained in:
parent
2767488552
commit
d476efb02c
2
go.mod
2
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
|
||||
|
3
go.sum
3
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=
|
||||
|
278
source/ocproute.go
Normal file
278
source/ocproute.go
Normal file
@ -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
|
||||
}
|
@ -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,6 +71,7 @@ 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
|
||||
@ -82,10 +84,12 @@ type SingletonClientGenerator struct {
|
||||
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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user