sigs.k8s.io/external-dns@v0.14.1/source/crd_test.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 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "strings" 27 "testing" 28 29 "github.com/stretchr/testify/require" 30 "github.com/stretchr/testify/suite" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/labels" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/apimachinery/pkg/runtime/serializer" 36 "k8s.io/client-go/rest" 37 "k8s.io/client-go/rest/fake" 38 39 "sigs.k8s.io/external-dns/endpoint" 40 ) 41 42 type CRDSuite struct { 43 suite.Suite 44 } 45 46 func (suite *CRDSuite) SetupTest() { 47 } 48 49 func defaultHeader() http.Header { 50 header := http.Header{} 51 header.Set("Content-Type", runtime.ContentTypeJSON) 52 return header 53 } 54 55 func objBody(codec runtime.Encoder, obj runtime.Object) io.ReadCloser { 56 return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) 57 } 58 59 func fakeRESTClient(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string, annotations map[string]string, labels map[string]string, t *testing.T) rest.Interface { 60 groupVersion, _ := schema.ParseGroupVersion(apiVersion) 61 scheme := runtime.NewScheme() 62 addKnownTypes(scheme, groupVersion) 63 64 dnsEndpointList := endpoint.DNSEndpointList{} 65 dnsEndpoint := &endpoint.DNSEndpoint{ 66 TypeMeta: metav1.TypeMeta{ 67 APIVersion: apiVersion, 68 Kind: kind, 69 }, 70 ObjectMeta: metav1.ObjectMeta{ 71 Name: name, 72 Namespace: namespace, 73 Annotations: annotations, 74 Labels: labels, 75 Generation: 1, 76 }, 77 Spec: endpoint.DNSEndpointSpec{ 78 Endpoints: endpoints, 79 }, 80 } 81 82 codecFactory := serializer.WithoutConversionCodecFactory{ 83 CodecFactory: serializer.NewCodecFactory(scheme), 84 } 85 86 client := &fake.RESTClient{ 87 GroupVersion: groupVersion, 88 VersionedAPIPath: "/apis/" + apiVersion, 89 NegotiatedSerializer: codecFactory, 90 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 91 codec := codecFactory.LegacyCodec(groupVersion) 92 switch p, m := req.URL.Path, req.Method; { 93 case p == "/apis/"+apiVersion+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet: 94 fallthrough 95 case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet: 96 dnsEndpointList.Items = dnsEndpointList.Items[:0] 97 dnsEndpointList.Items = append(dnsEndpointList.Items, *dnsEndpoint) 98 return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil 99 case strings.HasPrefix(p, "/apis/"+apiVersion+"/namespaces/") && strings.HasSuffix(p, strings.ToLower(kind)+"s") && m == http.MethodGet: 100 return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil 101 case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s/"+name+"/status" && m == http.MethodPut: 102 decoder := json.NewDecoder(req.Body) 103 104 var body endpoint.DNSEndpoint 105 decoder.Decode(&body) 106 dnsEndpoint.Status.ObservedGeneration = body.Status.ObservedGeneration 107 return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, dnsEndpoint)}, nil 108 default: 109 return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req) 110 } 111 }), 112 } 113 114 return client 115 } 116 117 func TestCRDSource(t *testing.T) { 118 suite.Run(t, new(CRDSuite)) 119 t.Run("Interface", testCRDSourceImplementsSource) 120 t.Run("Endpoints", testCRDSourceEndpoints) 121 } 122 123 // testCRDSourceImplementsSource tests that crdSource is a valid Source. 124 func testCRDSourceImplementsSource(t *testing.T) { 125 require.Implements(t, (*Source)(nil), new(crdSource)) 126 } 127 128 // testCRDSourceEndpoints tests various scenarios of using CRD source. 129 func testCRDSourceEndpoints(t *testing.T) { 130 for _, ti := range []struct { 131 title string 132 registeredNamespace string 133 namespace string 134 registeredAPIVersion string 135 apiVersion string 136 registeredKind string 137 kind string 138 endpoints []*endpoint.Endpoint 139 expectEndpoints bool 140 expectError bool 141 annotationFilter string 142 labelFilter string 143 annotations map[string]string 144 labels map[string]string 145 }{ 146 { 147 title: "invalid crd api version", 148 registeredAPIVersion: "test.k8s.io/v1alpha1", 149 apiVersion: "blah.k8s.io/v1alpha1", 150 registeredKind: "DNSEndpoint", 151 kind: "DNSEndpoint", 152 endpoints: []*endpoint.Endpoint{ 153 { 154 DNSName: "abc.example.org", 155 Targets: endpoint.Targets{"1.2.3.4"}, 156 RecordType: endpoint.RecordTypeA, 157 RecordTTL: 180, 158 }, 159 }, 160 expectEndpoints: false, 161 expectError: true, 162 }, 163 { 164 title: "invalid crd kind", 165 registeredAPIVersion: "test.k8s.io/v1alpha1", 166 apiVersion: "test.k8s.io/v1alpha1", 167 registeredKind: "DNSEndpoint", 168 kind: "JustEndpoint", 169 endpoints: []*endpoint.Endpoint{ 170 { 171 DNSName: "abc.example.org", 172 Targets: endpoint.Targets{"1.2.3.4"}, 173 RecordType: endpoint.RecordTypeA, 174 RecordTTL: 180, 175 }, 176 }, 177 expectEndpoints: false, 178 expectError: true, 179 }, 180 { 181 title: "endpoints within a specific namespace", 182 registeredAPIVersion: "test.k8s.io/v1alpha1", 183 apiVersion: "test.k8s.io/v1alpha1", 184 registeredKind: "DNSEndpoint", 185 kind: "DNSEndpoint", 186 namespace: "foo", 187 registeredNamespace: "foo", 188 endpoints: []*endpoint.Endpoint{ 189 { 190 DNSName: "abc.example.org", 191 Targets: endpoint.Targets{"1.2.3.4"}, 192 RecordType: endpoint.RecordTypeA, 193 RecordTTL: 180, 194 }, 195 }, 196 expectEndpoints: true, 197 expectError: false, 198 }, 199 { 200 title: "no endpoints within a specific namespace", 201 registeredAPIVersion: "test.k8s.io/v1alpha1", 202 apiVersion: "test.k8s.io/v1alpha1", 203 registeredKind: "DNSEndpoint", 204 kind: "DNSEndpoint", 205 namespace: "foo", 206 registeredNamespace: "bar", 207 endpoints: []*endpoint.Endpoint{ 208 { 209 DNSName: "abc.example.org", 210 Targets: endpoint.Targets{"1.2.3.4"}, 211 RecordType: endpoint.RecordTypeA, 212 RecordTTL: 180, 213 }, 214 }, 215 expectEndpoints: false, 216 expectError: false, 217 }, 218 { 219 title: "invalid crd with no targets", 220 registeredAPIVersion: "test.k8s.io/v1alpha1", 221 apiVersion: "test.k8s.io/v1alpha1", 222 registeredKind: "DNSEndpoint", 223 kind: "DNSEndpoint", 224 namespace: "foo", 225 registeredNamespace: "foo", 226 endpoints: []*endpoint.Endpoint{ 227 { 228 DNSName: "abc.example.org", 229 Targets: endpoint.Targets{}, 230 RecordType: endpoint.RecordTypeA, 231 RecordTTL: 180, 232 }, 233 }, 234 expectEndpoints: false, 235 expectError: false, 236 }, 237 { 238 title: "valid crd gvk with single endpoint", 239 registeredAPIVersion: "test.k8s.io/v1alpha1", 240 apiVersion: "test.k8s.io/v1alpha1", 241 registeredKind: "DNSEndpoint", 242 kind: "DNSEndpoint", 243 namespace: "foo", 244 registeredNamespace: "foo", 245 endpoints: []*endpoint.Endpoint{ 246 { 247 DNSName: "abc.example.org", 248 Targets: endpoint.Targets{"1.2.3.4"}, 249 RecordType: endpoint.RecordTypeA, 250 RecordTTL: 180, 251 }, 252 }, 253 expectEndpoints: true, 254 expectError: false, 255 }, 256 { 257 title: "valid crd gvk with multiple endpoints", 258 registeredAPIVersion: "test.k8s.io/v1alpha1", 259 apiVersion: "test.k8s.io/v1alpha1", 260 registeredKind: "DNSEndpoint", 261 kind: "DNSEndpoint", 262 namespace: "foo", 263 registeredNamespace: "foo", 264 endpoints: []*endpoint.Endpoint{ 265 { 266 DNSName: "abc.example.org", 267 Targets: endpoint.Targets{"1.2.3.4"}, 268 RecordType: endpoint.RecordTypeA, 269 RecordTTL: 180, 270 }, 271 { 272 DNSName: "xyz.example.org", 273 Targets: endpoint.Targets{"abc.example.org"}, 274 RecordType: endpoint.RecordTypeCNAME, 275 RecordTTL: 180, 276 }, 277 }, 278 expectEndpoints: true, 279 expectError: false, 280 }, 281 { 282 title: "valid crd gvk with annotation and non matching annotation filter", 283 registeredAPIVersion: "test.k8s.io/v1alpha1", 284 apiVersion: "test.k8s.io/v1alpha1", 285 registeredKind: "DNSEndpoint", 286 kind: "DNSEndpoint", 287 namespace: "foo", 288 registeredNamespace: "foo", 289 annotations: map[string]string{"test": "that"}, 290 annotationFilter: "test=filter_something_else", 291 endpoints: []*endpoint.Endpoint{ 292 { 293 DNSName: "abc.example.org", 294 Targets: endpoint.Targets{"1.2.3.4"}, 295 RecordType: endpoint.RecordTypeA, 296 RecordTTL: 180, 297 }, 298 }, 299 expectEndpoints: false, 300 expectError: false, 301 }, 302 { 303 title: "valid crd gvk with annotation and matching annotation filter", 304 registeredAPIVersion: "test.k8s.io/v1alpha1", 305 apiVersion: "test.k8s.io/v1alpha1", 306 registeredKind: "DNSEndpoint", 307 kind: "DNSEndpoint", 308 namespace: "foo", 309 registeredNamespace: "foo", 310 annotations: map[string]string{"test": "that"}, 311 annotationFilter: "test=that", 312 endpoints: []*endpoint.Endpoint{ 313 { 314 DNSName: "abc.example.org", 315 Targets: endpoint.Targets{"1.2.3.4"}, 316 RecordType: endpoint.RecordTypeA, 317 RecordTTL: 180, 318 }, 319 }, 320 expectEndpoints: true, 321 expectError: false, 322 }, 323 { 324 title: "valid crd gvk with label and non matching label filter", 325 registeredAPIVersion: "test.k8s.io/v1alpha1", 326 apiVersion: "test.k8s.io/v1alpha1", 327 registeredKind: "DNSEndpoint", 328 kind: "DNSEndpoint", 329 namespace: "foo", 330 registeredNamespace: "foo", 331 labels: map[string]string{"test": "that"}, 332 labelFilter: "test=filter_something_else", 333 endpoints: []*endpoint.Endpoint{ 334 { 335 DNSName: "abc.example.org", 336 Targets: endpoint.Targets{"1.2.3.4"}, 337 RecordType: endpoint.RecordTypeA, 338 RecordTTL: 180, 339 }, 340 }, 341 expectEndpoints: false, 342 expectError: false, 343 }, 344 { 345 title: "valid crd gvk with label and matching label filter", 346 registeredAPIVersion: "test.k8s.io/v1alpha1", 347 apiVersion: "test.k8s.io/v1alpha1", 348 registeredKind: "DNSEndpoint", 349 kind: "DNSEndpoint", 350 namespace: "foo", 351 registeredNamespace: "foo", 352 labels: map[string]string{"test": "that"}, 353 labelFilter: "test=that", 354 endpoints: []*endpoint.Endpoint{ 355 { 356 DNSName: "abc.example.org", 357 Targets: endpoint.Targets{"1.2.3.4"}, 358 RecordType: endpoint.RecordTypeA, 359 RecordTTL: 180, 360 }, 361 }, 362 expectEndpoints: true, 363 expectError: false, 364 }, 365 { 366 title: "Create NS record", 367 registeredAPIVersion: "test.k8s.io/v1alpha1", 368 apiVersion: "test.k8s.io/v1alpha1", 369 registeredKind: "DNSEndpoint", 370 kind: "DNSEndpoint", 371 namespace: "foo", 372 registeredNamespace: "foo", 373 labels: map[string]string{"test": "that"}, 374 labelFilter: "test=that", 375 endpoints: []*endpoint.Endpoint{ 376 { 377 DNSName: "abc.example.org", 378 Targets: endpoint.Targets{"ns1.k8s.io", "ns2.k8s.io"}, 379 RecordType: endpoint.RecordTypeNS, 380 RecordTTL: 180, 381 }, 382 }, 383 expectEndpoints: true, 384 expectError: false, 385 }, 386 { 387 title: "Create SRV record", 388 registeredAPIVersion: "test.k8s.io/v1alpha1", 389 apiVersion: "test.k8s.io/v1alpha1", 390 registeredKind: "DNSEndpoint", 391 kind: "DNSEndpoint", 392 namespace: "foo", 393 registeredNamespace: "foo", 394 labels: map[string]string{"test": "that"}, 395 labelFilter: "test=that", 396 endpoints: []*endpoint.Endpoint{ 397 { 398 DNSName: "_svc._tcp.example.org", 399 Targets: endpoint.Targets{"0 0 80 abc.example.org", "0 0 80 def.example.org"}, 400 RecordType: endpoint.RecordTypeSRV, 401 RecordTTL: 180, 402 }, 403 }, 404 expectEndpoints: true, 405 expectError: false, 406 }, 407 { 408 title: "Create NAPTR record", 409 registeredAPIVersion: "test.k8s.io/v1alpha1", 410 apiVersion: "test.k8s.io/v1alpha1", 411 registeredKind: "DNSEndpoint", 412 kind: "DNSEndpoint", 413 namespace: "foo", 414 registeredNamespace: "foo", 415 labels: map[string]string{"test": "that"}, 416 labelFilter: "test=that", 417 endpoints: []*endpoint.Endpoint{ 418 { 419 DNSName: "example.org", 420 Targets: endpoint.Targets{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org.`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org.`}, 421 RecordType: endpoint.RecordTypeNAPTR, 422 RecordTTL: 180, 423 }, 424 }, 425 expectEndpoints: true, 426 expectError: false, 427 }, 428 { 429 title: "illegal target CNAME", 430 registeredAPIVersion: "test.k8s.io/v1alpha1", 431 apiVersion: "test.k8s.io/v1alpha1", 432 registeredKind: "DNSEndpoint", 433 kind: "DNSEndpoint", 434 namespace: "foo", 435 registeredNamespace: "foo", 436 labels: map[string]string{"test": "that"}, 437 labelFilter: "test=that", 438 endpoints: []*endpoint.Endpoint{ 439 { 440 DNSName: "example.org", 441 Targets: endpoint.Targets{"foo.example.org."}, 442 RecordType: endpoint.RecordTypeCNAME, 443 RecordTTL: 180, 444 }, 445 }, 446 expectEndpoints: false, 447 expectError: false, 448 }, 449 { 450 title: "illegal target NAPTR", 451 registeredAPIVersion: "test.k8s.io/v1alpha1", 452 apiVersion: "test.k8s.io/v1alpha1", 453 registeredKind: "DNSEndpoint", 454 kind: "DNSEndpoint", 455 namespace: "foo", 456 registeredNamespace: "foo", 457 labels: map[string]string{"test": "that"}, 458 labelFilter: "test=that", 459 endpoints: []*endpoint.Endpoint{ 460 { 461 DNSName: "example.org", 462 Targets: endpoint.Targets{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org`}, 463 RecordType: endpoint.RecordTypeNAPTR, 464 RecordTTL: 180, 465 }, 466 }, 467 expectEndpoints: false, 468 expectError: false, 469 }, 470 } { 471 ti := ti 472 t.Run(ti.title, func(t *testing.T) { 473 t.Parallel() 474 475 restClient := fakeRESTClient(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "test", ti.annotations, ti.labels, t) 476 groupVersion, err := schema.ParseGroupVersion(ti.apiVersion) 477 require.NoError(t, err) 478 479 scheme := runtime.NewScheme() 480 require.NoError(t, addKnownTypes(scheme, groupVersion)) 481 482 labelSelector, err := labels.Parse(ti.labelFilter) 483 require.NoError(t, err) 484 485 // At present, client-go's fake.RESTClient (used by crd_test.go) is known to cause race conditions when used 486 // with informers: https://github.com/kubernetes/kubernetes/issues/95372 487 // So don't start the informer during testing. 488 startInformer := false 489 490 cs, err := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, labelSelector, scheme, startInformer) 491 require.NoError(t, err) 492 493 receivedEndpoints, err := cs.Endpoints(context.Background()) 494 if ti.expectError { 495 require.Errorf(t, err, "Received err %v", err) 496 } else { 497 require.NoErrorf(t, err, "Received err %v", err) 498 } 499 500 if len(receivedEndpoints) == 0 && !ti.expectEndpoints { 501 return 502 } 503 504 if err == nil { 505 validateCRDResource(t, cs, ti.expectError) 506 } 507 508 // Validate received endpoints against expected endpoints. 509 validateEndpoints(t, receivedEndpoints, ti.endpoints) 510 }) 511 } 512 } 513 514 func validateCRDResource(t *testing.T, src Source, expectError bool) { 515 cs := src.(*crdSource) 516 result, err := cs.List(context.Background(), &metav1.ListOptions{}) 517 if expectError { 518 require.Errorf(t, err, "Received err %v", err) 519 } else { 520 require.NoErrorf(t, err, "Received err %v", err) 521 } 522 523 for _, dnsEndpoint := range result.Items { 524 if dnsEndpoint.Status.ObservedGeneration != dnsEndpoint.Generation { 525 require.Errorf(t, err, "Unexpected CRD resource result: ObservedGenerations <%v> is not equal to Generation<%v>", dnsEndpoint.Status.ObservedGeneration, dnsEndpoint.Generation) 526 } 527 } 528 }