sigs.k8s.io/external-dns@v0.14.1/source/crd.go (about) 1 /* 2 Copyright 2018 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 "os" 23 "strings" 24 25 "k8s.io/apimachinery/pkg/util/wait" 26 "k8s.io/apimachinery/pkg/watch" 27 "k8s.io/client-go/tools/cache" 28 29 log "github.com/sirupsen/logrus" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/labels" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/apimachinery/pkg/runtime/schema" 34 "k8s.io/apimachinery/pkg/runtime/serializer" 35 "k8s.io/client-go/kubernetes" 36 "k8s.io/client-go/rest" 37 "k8s.io/client-go/tools/clientcmd" 38 39 "sigs.k8s.io/external-dns/endpoint" 40 ) 41 42 // crdSource is an implementation of Source that provides endpoints by listing 43 // specified CRD and fetching Endpoints embedded in Spec. 44 type crdSource struct { 45 crdClient rest.Interface 46 namespace string 47 crdResource string 48 codec runtime.ParameterCodec 49 annotationFilter string 50 labelSelector labels.Selector 51 informer *cache.SharedInformer 52 } 53 54 func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error { 55 scheme.AddKnownTypes(groupVersion, 56 &endpoint.DNSEndpoint{}, 57 &endpoint.DNSEndpointList{}, 58 ) 59 metav1.AddToGroupVersion(scheme, groupVersion) 60 return nil 61 } 62 63 // NewCRDClientForAPIVersionKind return rest client for the given apiVersion and kind of the CRD 64 func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiServerURL, apiVersion, kind string) (*rest.RESTClient, *runtime.Scheme, error) { 65 if kubeConfig == "" { 66 if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { 67 kubeConfig = clientcmd.RecommendedHomeFile 68 } 69 } 70 71 config, err := clientcmd.BuildConfigFromFlags(apiServerURL, kubeConfig) 72 if err != nil { 73 return nil, nil, err 74 } 75 76 groupVersion, err := schema.ParseGroupVersion(apiVersion) 77 if err != nil { 78 return nil, nil, err 79 } 80 apiResourceList, err := client.Discovery().ServerResourcesForGroupVersion(groupVersion.String()) 81 if err != nil { 82 return nil, nil, fmt.Errorf("error listing resources in GroupVersion %q: %w", groupVersion.String(), err) 83 } 84 85 var crdAPIResource *metav1.APIResource 86 for _, apiResource := range apiResourceList.APIResources { 87 if apiResource.Kind == kind { 88 crdAPIResource = &apiResource 89 break 90 } 91 } 92 if crdAPIResource == nil { 93 return nil, nil, fmt.Errorf("unable to find Resource Kind %q in GroupVersion %q", kind, apiVersion) 94 } 95 96 scheme := runtime.NewScheme() 97 addKnownTypes(scheme, groupVersion) 98 99 config.ContentConfig.GroupVersion = &groupVersion 100 config.APIPath = "/apis" 101 config.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)} 102 103 crdClient, err := rest.UnversionedRESTClientFor(config) 104 if err != nil { 105 return nil, nil, err 106 } 107 return crdClient, scheme, nil 108 } 109 110 // NewCRDSource creates a new crdSource with the given config. 111 func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFilter string, labelSelector labels.Selector, scheme *runtime.Scheme, startInformer bool) (Source, error) { 112 sourceCrd := crdSource{ 113 crdResource: strings.ToLower(kind) + "s", 114 namespace: namespace, 115 annotationFilter: annotationFilter, 116 labelSelector: labelSelector, 117 crdClient: crdClient, 118 codec: runtime.NewParameterCodec(scheme), 119 } 120 if startInformer { 121 // external-dns already runs its sync-handler periodically (controlled by `--interval` flag) to ensure any 122 // missed or dropped events are handled. specify a resync period 0 to avoid unnecessary sync handler invocations. 123 informer := cache.NewSharedInformer( 124 &cache.ListWatch{ 125 ListFunc: func(lo metav1.ListOptions) (result runtime.Object, err error) { 126 return sourceCrd.List(context.TODO(), &lo) 127 }, 128 WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { 129 return sourceCrd.watch(context.TODO(), &lo) 130 }, 131 }, 132 &endpoint.DNSEndpoint{}, 133 0) 134 sourceCrd.informer = &informer 135 go informer.Run(wait.NeverStop) 136 } 137 return &sourceCrd, nil 138 } 139 140 func (cs *crdSource) AddEventHandler(ctx context.Context, handler func()) { 141 if cs.informer != nil { 142 log.Debug("Adding event handler for CRD") 143 // Right now there is no way to remove event handler from informer, see: 144 // https://github.com/kubernetes/kubernetes/issues/79610 145 informer := *cs.informer 146 informer.AddEventHandler( 147 cache.ResourceEventHandlerFuncs{ 148 AddFunc: func(obj interface{}) { 149 handler() 150 }, 151 UpdateFunc: func(old interface{}, new interface{}) { 152 handler() 153 }, 154 DeleteFunc: func(obj interface{}) { 155 handler() 156 }, 157 }, 158 ) 159 } 160 } 161 162 // Endpoints returns endpoint objects. 163 func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { 164 endpoints := []*endpoint.Endpoint{} 165 166 var ( 167 result *endpoint.DNSEndpointList 168 err error 169 ) 170 171 result, err = cs.List(ctx, &metav1.ListOptions{LabelSelector: cs.labelSelector.String()}) 172 if err != nil { 173 return nil, err 174 } 175 176 result, err = cs.filterByAnnotations(result) 177 178 if err != nil { 179 return nil, err 180 } 181 182 for _, dnsEndpoint := range result.Items { 183 // Make sure that all endpoints have targets for A or CNAME type 184 crdEndpoints := []*endpoint.Endpoint{} 185 for _, ep := range dnsEndpoint.Spec.Endpoints { 186 if (ep.RecordType == "CNAME" || ep.RecordType == "A" || ep.RecordType == "AAAA") && len(ep.Targets) < 1 { 187 log.Warnf("Endpoint %s with DNSName %s has an empty list of targets", dnsEndpoint.ObjectMeta.Name, ep.DNSName) 188 continue 189 } 190 191 illegalTarget := false 192 for _, target := range ep.Targets { 193 if ep.RecordType != "NAPTR" && strings.HasSuffix(target, ".") { 194 illegalTarget = true 195 break 196 } 197 if ep.RecordType == "NAPTR" && !strings.HasSuffix(target, ".") { 198 illegalTarget = true 199 break 200 } 201 } 202 if illegalTarget { 203 log.Warnf("Endpoint %s with DNSName %s has an illegal target. The subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com')", dnsEndpoint.ObjectMeta.Name, ep.DNSName) 204 continue 205 } 206 207 if ep.Labels == nil { 208 ep.Labels = endpoint.NewLabels() 209 } 210 211 crdEndpoints = append(crdEndpoints, ep) 212 } 213 214 cs.setResourceLabel(&dnsEndpoint, crdEndpoints) 215 endpoints = append(endpoints, crdEndpoints...) 216 217 if dnsEndpoint.Status.ObservedGeneration == dnsEndpoint.Generation { 218 continue 219 } 220 221 dnsEndpoint.Status.ObservedGeneration = dnsEndpoint.Generation 222 // Update the ObservedGeneration 223 _, err = cs.UpdateStatus(ctx, &dnsEndpoint) 224 if err != nil { 225 log.Warnf("Could not update ObservedGeneration of the CRD: %v", err) 226 } 227 } 228 229 return endpoints, nil 230 } 231 232 func (cs *crdSource) setResourceLabel(crd *endpoint.DNSEndpoint, endpoints []*endpoint.Endpoint) { 233 for _, ep := range endpoints { 234 ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("crd/%s/%s", crd.ObjectMeta.Namespace, crd.ObjectMeta.Name) 235 } 236 } 237 238 func (cs *crdSource) watch(ctx context.Context, opts *metav1.ListOptions) (watch.Interface, error) { 239 opts.Watch = true 240 return cs.crdClient.Get(). 241 Namespace(cs.namespace). 242 Resource(cs.crdResource). 243 VersionedParams(opts, cs.codec). 244 Watch(ctx) 245 } 246 247 func (cs *crdSource) List(ctx context.Context, opts *metav1.ListOptions) (result *endpoint.DNSEndpointList, err error) { 248 result = &endpoint.DNSEndpointList{} 249 err = cs.crdClient.Get(). 250 Namespace(cs.namespace). 251 Resource(cs.crdResource). 252 VersionedParams(opts, cs.codec). 253 Do(ctx). 254 Into(result) 255 return 256 } 257 258 func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *endpoint.DNSEndpoint) (result *endpoint.DNSEndpoint, err error) { 259 result = &endpoint.DNSEndpoint{} 260 err = cs.crdClient.Put(). 261 Namespace(dnsEndpoint.Namespace). 262 Resource(cs.crdResource). 263 Name(dnsEndpoint.Name). 264 SubResource("status"). 265 Body(dnsEndpoint). 266 Do(ctx). 267 Into(result) 268 return 269 } 270 271 // filterByAnnotations filters a list of dnsendpoints by a given annotation selector. 272 func (cs *crdSource) filterByAnnotations(dnsendpoints *endpoint.DNSEndpointList) (*endpoint.DNSEndpointList, error) { 273 labelSelector, err := metav1.ParseToLabelSelector(cs.annotationFilter) 274 if err != nil { 275 return nil, err 276 } 277 selector, err := metav1.LabelSelectorAsSelector(labelSelector) 278 if err != nil { 279 return nil, err 280 } 281 282 // empty filter returns original list 283 if selector.Empty() { 284 return dnsendpoints, nil 285 } 286 287 filteredList := endpoint.DNSEndpointList{} 288 289 for _, dnsendpoint := range dnsendpoints.Items { 290 // convert the dnsendpoint' annotations to an equivalent label selector 291 annotations := labels.Set(dnsendpoint.Annotations) 292 293 // include dnsendpoint if its annotations match the selector 294 if selector.Matches(annotations) { 295 filteredList.Items = append(filteredList.Items, dnsendpoint) 296 } 297 } 298 299 return &filteredList, nil 300 }