diff --git a/go.mod b/go.mod index e481347a7..1e9f44d2d 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 48bcf53bb..975c13abe 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 000000000..1c0cf0354 --- /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 a90e1ded8..d14b69274 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