sigs.k8s.io/external-dns@v0.14.1/source/ambassador_host.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package source 18 19 import ( 20 "context" 21 "fmt" 22 "sort" 23 "strings" 24 25 ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2" 26 "github.com/pkg/errors" 27 log "github.com/sirupsen/logrus" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/labels" 31 "k8s.io/apimachinery/pkg/runtime" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 "k8s.io/client-go/dynamic" 34 "k8s.io/client-go/dynamic/dynamicinformer" 35 "k8s.io/client-go/informers" 36 "k8s.io/client-go/kubernetes" 37 "k8s.io/client-go/kubernetes/scheme" 38 "k8s.io/client-go/tools/cache" 39 40 "sigs.k8s.io/external-dns/endpoint" 41 ) 42 43 // ambHostAnnotation is the annotation in the Host that maps to a Service 44 const ambHostAnnotation = "external-dns.ambassador-service" 45 46 // groupName is the group name for the Ambassador API 47 const groupName = "getambassador.io" 48 49 var schemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v2"} 50 51 var ambHostGVR = schemeGroupVersion.WithResource("hosts") 52 53 // ambassadorHostSource is an implementation of Source for Ambassador Host objects. 54 // The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname. 55 // Use targetAnnotationKey to explicitly set Endpoint. 56 type ambassadorHostSource struct { 57 dynamicKubeClient dynamic.Interface 58 kubeClient kubernetes.Interface 59 namespace string 60 ambassadorHostInformer informers.GenericInformer 61 unstructuredConverter *unstructuredConverter 62 } 63 64 // NewAmbassadorHostSource creates a new ambassadorHostSource with the given config. 65 func NewAmbassadorHostSource( 66 ctx context.Context, 67 dynamicKubeClient dynamic.Interface, 68 kubeClient kubernetes.Interface, 69 namespace string, 70 ) (Source, error) { 71 var err error 72 73 // Use shared informer to listen for add/update/delete of Host in the specified namespace. 74 // Set resync period to 0, to prevent processing when nothing has changed. 75 informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) 76 ambassadorHostInformer := informerFactory.ForResource(ambHostGVR) 77 78 // Add default resource event handlers to properly initialize informer. 79 ambassadorHostInformer.Informer().AddEventHandler( 80 cache.ResourceEventHandlerFuncs{ 81 AddFunc: func(obj interface{}) { 82 }, 83 }, 84 ) 85 86 informerFactory.Start(ctx.Done()) 87 88 if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { 89 return nil, err 90 } 91 92 uc, err := newUnstructuredConverter() 93 if err != nil { 94 return nil, errors.Wrapf(err, "failed to setup Unstructured Converter") 95 } 96 97 return &ambassadorHostSource{ 98 dynamicKubeClient: dynamicKubeClient, 99 kubeClient: kubeClient, 100 namespace: namespace, 101 ambassadorHostInformer: ambassadorHostInformer, 102 unstructuredConverter: uc, 103 }, nil 104 } 105 106 // Endpoints returns endpoint objects for each host-target combination that should be processed. 107 // Retrieves all Hosts in the source's namespace(s). 108 func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { 109 hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything()) 110 if err != nil { 111 return nil, err 112 } 113 114 endpoints := []*endpoint.Endpoint{} 115 for _, hostObj := range hosts { 116 unstructuredHost, ok := hostObj.(*unstructured.Unstructured) 117 if !ok { 118 return nil, errors.New("could not convert") 119 } 120 121 host := &ambassador.Host{} 122 err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, host, nil) 123 if err != nil { 124 return nil, err 125 } 126 127 fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name) 128 129 // look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host` 130 service, found := host.Annotations[ambHostAnnotation] 131 if !found { 132 log.Debugf("Host %s ignored: no annotation %q found", fullname, ambHostAnnotation) 133 continue 134 } 135 136 targets := getTargetsFromTargetAnnotation(host.Annotations) 137 if len(targets) == 0 { 138 targets, err = sc.targetsFromAmbassadorLoadBalancer(ctx, service) 139 if err != nil { 140 log.Warningf("Could not find targets for service %s for Host %s: %v", service, fullname, err) 141 continue 142 } 143 } 144 145 hostEndpoints, err := sc.endpointsFromHost(ctx, host, targets) 146 if err != nil { 147 log.Warningf("Could not get endpoints for Host %s", err) 148 continue 149 } 150 if len(hostEndpoints) == 0 { 151 log.Debugf("No endpoints could be generated from Host %s", fullname) 152 continue 153 } 154 155 log.Debugf("Endpoints generated from Host: %s: %v", fullname, hostEndpoints) 156 endpoints = append(endpoints, hostEndpoints...) 157 } 158 159 for _, ep := range endpoints { 160 sort.Sort(ep.Targets) 161 } 162 163 return endpoints, nil 164 } 165 166 // endpointsFromHost extracts the endpoints from a Host object 167 func (sc *ambassadorHostSource) endpointsFromHost(ctx context.Context, host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { 168 var endpoints []*endpoint.Endpoint 169 annotations := host.Annotations 170 171 resource := fmt.Sprintf("host/%s/%s", host.Namespace, host.Name) 172 providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations) 173 ttl := getTTLFromAnnotations(annotations, resource) 174 175 if host.Spec != nil { 176 hostname := host.Spec.Hostname 177 if hostname != "" { 178 endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) 179 } 180 } 181 182 return endpoints, nil 183 } 184 185 func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Context, service string) (endpoint.Targets, error) { 186 lbNamespace, lbName, err := parseAmbLoadBalancerService(service) 187 if err != nil { 188 return nil, err 189 } 190 191 svc, err := sc.kubeClient.CoreV1().Services(lbNamespace).Get(ctx, lbName, metav1.GetOptions{}) 192 if err != nil { 193 return nil, err 194 } 195 196 var targets = extractLoadBalancerTargets(svc, false) 197 198 return targets, nil 199 } 200 201 // parseAmbLoadBalancerService returns a name/namespace tuple from the annotation in 202 // an Ambassador Host CRD 203 // 204 // This is a thing because Ambassador has historically supported cross-namespace 205 // references using a name.namespace syntax, but here we want to also support 206 // namespace/name. 207 // 208 // Returns namespace, name, error. 209 210 func parseAmbLoadBalancerService(service string) (namespace, name string, err error) { 211 // Start by assuming that we have namespace/name. 212 parts := strings.Split(service, "/") 213 214 if len(parts) == 1 { 215 // No "/" at all, so let's try for name.namespace. To be consistent with the 216 // rest of Ambassador, use SplitN to limit this to one split, so that e.g. 217 // svc.foo.bar uses service "svc" in namespace "foo.bar". 218 parts = strings.SplitN(service, ".", 2) 219 220 if len(parts) == 2 { 221 // We got a namespace, great. 222 name := parts[0] 223 namespace := parts[1] 224 225 return namespace, name, nil 226 } 227 228 // If here, we have no separator, so the whole string is the service, and 229 // we can assume the default namespace. 230 name := service 231 namespace := "default" 232 233 return namespace, name, nil 234 } else if len(parts) == 2 { 235 // This is "namespace/name". Note that the name could be qualified, 236 // which is fine. 237 namespace := parts[0] 238 name := parts[1] 239 240 return namespace, name, nil 241 } 242 243 // If we got here, this string is simply ill-formatted. Return an error. 244 return "", "", errors.New(fmt.Sprintf("invalid external-dns service: %s", service)) 245 } 246 247 func (sc *ambassadorHostSource) AddEventHandler(ctx context.Context, handler func()) { 248 } 249 250 // unstructuredConverter handles conversions between unstructured.Unstructured and Ambassador types 251 type unstructuredConverter struct { 252 // scheme holds an initializer for converting Unstructured to a type 253 scheme *runtime.Scheme 254 } 255 256 // newUnstructuredConverter returns a new unstructuredConverter initialized 257 func newUnstructuredConverter() (*unstructuredConverter, error) { 258 uc := &unstructuredConverter{ 259 scheme: runtime.NewScheme(), 260 } 261 262 // Setup converter to understand custom CRD types 263 ambassador.AddToScheme(uc.scheme) 264 265 // Add the core types we need 266 if err := scheme.AddToScheme(uc.scheme); err != nil { 267 return nil, err 268 } 269 270 return uc, nil 271 }