sigs.k8s.io/external-dns@v0.14.1/source/openshift_route_test.go (about) 1 /* 2 Copyright 2017 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 "testing" 22 23 "github.com/stretchr/testify/assert" 24 "github.com/stretchr/testify/require" 25 "github.com/stretchr/testify/suite" 26 "k8s.io/apimachinery/pkg/labels" 27 28 routev1 "github.com/openshift/api/route/v1" 29 fake "github.com/openshift/client-go/route/clientset/versioned/fake" 30 corev1 "k8s.io/api/core/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 33 "sigs.k8s.io/external-dns/endpoint" 34 ) 35 36 type OCPRouteSuite struct { 37 suite.Suite 38 sc Source 39 routeWithTargets *routev1.Route 40 } 41 42 func (suite *OCPRouteSuite) SetupTest() { 43 fakeClient := fake.NewSimpleClientset() 44 var err error 45 46 suite.sc, err = NewOcpRouteSource( 47 context.TODO(), 48 fakeClient, 49 "", 50 "", 51 "{{.Name}}", 52 false, 53 false, 54 labels.Everything(), 55 "", 56 ) 57 58 suite.routeWithTargets = &routev1.Route{ 59 Spec: routev1.RouteSpec{ 60 Host: "my-domain.com", 61 }, 62 ObjectMeta: metav1.ObjectMeta{ 63 Namespace: "default", 64 Name: "route-with-targets", 65 Annotations: map[string]string{}, 66 }, 67 Status: routev1.RouteStatus{ 68 Ingress: []routev1.RouteIngress{ 69 { 70 RouterCanonicalHostname: "apps.my-domain.com", 71 }, 72 }, 73 }, 74 } 75 76 suite.NoError(err, "should initialize route source") 77 78 _, err = fakeClient.RouteV1().Routes(suite.routeWithTargets.Namespace).Create(context.Background(), suite.routeWithTargets, metav1.CreateOptions{}) 79 suite.NoError(err, "should successfully create route") 80 } 81 82 func (suite *OCPRouteSuite) TestResourceLabelIsSet() { 83 endpoints, _ := suite.sc.Endpoints(context.Background()) 84 for _, ep := range endpoints { 85 suite.Equal("route/default/route-with-targets", ep.Labels[endpoint.ResourceLabelKey], "should set correct resource label") 86 } 87 } 88 89 func TestOcpRouteSource(t *testing.T) { 90 t.Parallel() 91 92 suite.Run(t, new(OCPRouteSuite)) 93 t.Run("Interface", testOcpRouteSourceImplementsSource) 94 t.Run("NewOcpRouteSource", testOcpRouteSourceNewOcpRouteSource) 95 t.Run("Endpoints", testOcpRouteSourceEndpoints) 96 } 97 98 // testOcpRouteSourceImplementsSource tests that ocpRouteSource is a valid Source. 99 func testOcpRouteSourceImplementsSource(t *testing.T) { 100 assert.Implements(t, (*Source)(nil), new(ocpRouteSource)) 101 } 102 103 // testOcpRouteSourceNewOcpRouteSource tests that NewOcpRouteSource doesn't return an error. 104 func testOcpRouteSourceNewOcpRouteSource(t *testing.T) { 105 t.Parallel() 106 107 for _, ti := range []struct { 108 title string 109 annotationFilter string 110 fqdnTemplate string 111 expectError bool 112 labelFilter string 113 }{ 114 { 115 title: "invalid template", 116 expectError: true, 117 fqdnTemplate: "{{.Name", 118 }, 119 { 120 title: "valid empty template", 121 expectError: false, 122 }, 123 { 124 title: "valid template", 125 expectError: false, 126 fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com", 127 }, 128 { 129 title: "non-empty annotation filter label", 130 expectError: false, 131 annotationFilter: "kubernetes.io/ingress.class=nginx", 132 }, 133 { 134 title: "valid label selector", 135 expectError: false, 136 labelFilter: "app=web-external", 137 }, 138 } { 139 ti := ti 140 labelSelector, err := labels.Parse(ti.labelFilter) 141 require.NoError(t, err) 142 t.Run(ti.title, func(t *testing.T) { 143 t.Parallel() 144 145 _, err := NewOcpRouteSource( 146 context.TODO(), 147 fake.NewSimpleClientset(), 148 "", 149 ti.annotationFilter, 150 ti.fqdnTemplate, 151 false, 152 false, 153 labelSelector, 154 "", 155 ) 156 157 if ti.expectError { 158 assert.Error(t, err) 159 } else { 160 assert.NoError(t, err) 161 } 162 }) 163 } 164 } 165 166 // testOcpRouteSourceEndpoints tests that various OCP routes generate the correct endpoints. 167 func testOcpRouteSourceEndpoints(t *testing.T) { 168 for _, tc := range []struct { 169 title string 170 ocpRoute *routev1.Route 171 expected []*endpoint.Endpoint 172 expectError bool 173 labelFilter string 174 ocpRouterName string 175 }{ 176 { 177 title: "route with basic hostname and route status target", 178 ocpRoute: &routev1.Route{ 179 ObjectMeta: metav1.ObjectMeta{ 180 Namespace: "default", 181 Name: "route-with-target", 182 }, 183 Status: routev1.RouteStatus{ 184 Ingress: []routev1.RouteIngress{ 185 { 186 Host: "my-domain.com", 187 RouterCanonicalHostname: "apps.my-domain.com", 188 Conditions: []routev1.RouteIngressCondition{ 189 { 190 Type: routev1.RouteAdmitted, 191 Status: corev1.ConditionTrue, 192 }, 193 }, 194 }, 195 }, 196 }, 197 }, 198 expected: []*endpoint.Endpoint{ 199 { 200 DNSName: "my-domain.com", 201 RecordType: endpoint.RecordTypeCNAME, 202 Targets: []string{ 203 "apps.my-domain.com", 204 }, 205 }, 206 }, 207 }, 208 { 209 title: "route with basic hostname, route status target and ocpRouterName defined", 210 ocpRoute: &routev1.Route{ 211 ObjectMeta: metav1.ObjectMeta{ 212 Namespace: "default", 213 Name: "route-with-target", 214 }, 215 Status: routev1.RouteStatus{ 216 Ingress: []routev1.RouteIngress{ 217 { 218 Host: "my-domain.com", 219 RouterName: "default", 220 RouterCanonicalHostname: "router-default.my-domain.com", 221 Conditions: []routev1.RouteIngressCondition{ 222 { 223 Type: routev1.RouteAdmitted, 224 Status: corev1.ConditionTrue, 225 }, 226 }, 227 }, 228 }, 229 }, 230 }, 231 ocpRouterName: "default", 232 expected: []*endpoint.Endpoint{ 233 { 234 DNSName: "my-domain.com", 235 RecordType: endpoint.RecordTypeCNAME, 236 Targets: []string{ 237 "router-default.my-domain.com", 238 }, 239 }, 240 }, 241 }, 242 { 243 title: "route with basic hostname, route status target, one ocpRouterName and two router canonical names", 244 ocpRoute: &routev1.Route{ 245 ObjectMeta: metav1.ObjectMeta{ 246 Namespace: "default", 247 Name: "route-with-target", 248 }, 249 Status: routev1.RouteStatus{ 250 Ingress: []routev1.RouteIngress{ 251 { 252 Host: "my-domain.com", 253 RouterName: "default", 254 RouterCanonicalHostname: "router-default.my-domain.com", 255 Conditions: []routev1.RouteIngressCondition{ 256 { 257 Type: routev1.RouteAdmitted, 258 Status: corev1.ConditionTrue, 259 }, 260 }, 261 }, 262 { 263 Host: "my-domain.com", 264 RouterName: "test", 265 RouterCanonicalHostname: "router-test.my-domain.com", 266 Conditions: []routev1.RouteIngressCondition{ 267 { 268 Type: routev1.RouteAdmitted, 269 Status: corev1.ConditionTrue, 270 }, 271 }, 272 }, 273 }, 274 }, 275 }, 276 ocpRouterName: "default", 277 expected: []*endpoint.Endpoint{ 278 { 279 DNSName: "my-domain.com", 280 RecordType: endpoint.RecordTypeCNAME, 281 Targets: []string{ 282 "router-default.my-domain.com", 283 }, 284 }, 285 }, 286 }, 287 { 288 title: "route not admitted by the given router", 289 ocpRoute: &routev1.Route{ 290 ObjectMeta: metav1.ObjectMeta{ 291 Namespace: "default", 292 Name: "route-with-target", 293 }, 294 Status: routev1.RouteStatus{ 295 Ingress: []routev1.RouteIngress{ 296 { 297 Host: "my-domain.com", 298 RouterName: "default", 299 RouterCanonicalHostname: "router-default.my-domain.com", 300 Conditions: []routev1.RouteIngressCondition{ 301 { 302 Type: routev1.RouteAdmitted, 303 Status: corev1.ConditionTrue, 304 }, 305 }, 306 }, 307 { 308 Host: "my-domain.com", 309 RouterName: "test", 310 RouterCanonicalHostname: "router-test.my-domain.com", 311 Conditions: []routev1.RouteIngressCondition{ 312 { 313 Type: routev1.RouteAdmitted, 314 Status: corev1.ConditionFalse, 315 }, 316 }, 317 }, 318 }, 319 }, 320 }, 321 ocpRouterName: "test", 322 expected: []*endpoint.Endpoint{}, 323 }, 324 { 325 title: "route not admitted by any router", 326 ocpRoute: &routev1.Route{ 327 Spec: routev1.RouteSpec{ 328 Host: "my-domain.com", 329 }, 330 ObjectMeta: metav1.ObjectMeta{ 331 Namespace: "default", 332 Name: "route-with-target", 333 }, 334 Status: routev1.RouteStatus{ 335 Ingress: []routev1.RouteIngress{ 336 { 337 Host: "my-domain.com", 338 RouterName: "default", 339 RouterCanonicalHostname: "router-default.my-domain.com", 340 Conditions: []routev1.RouteIngressCondition{ 341 { 342 Type: routev1.RouteAdmitted, 343 Status: corev1.ConditionFalse, 344 }, 345 }, 346 }, 347 { 348 Host: "my-domain.com", 349 RouterName: "test", 350 RouterCanonicalHostname: "router-test.my-domain.com", 351 Conditions: []routev1.RouteIngressCondition{ 352 { 353 Type: routev1.RouteAdmitted, 354 Status: corev1.ConditionFalse, 355 }, 356 }, 357 }, 358 }, 359 }, 360 }, 361 expected: []*endpoint.Endpoint{}, 362 }, 363 { 364 title: "route admitted by first appropriate router", 365 ocpRoute: &routev1.Route{ 366 ObjectMeta: metav1.ObjectMeta{ 367 Namespace: "default", 368 Name: "route-with-target", 369 }, 370 Status: routev1.RouteStatus{ 371 Ingress: []routev1.RouteIngress{ 372 { 373 Host: "my-domain.com", 374 RouterName: "default", 375 RouterCanonicalHostname: "router-default.my-domain.com", 376 Conditions: []routev1.RouteIngressCondition{ 377 { 378 Type: routev1.RouteAdmitted, 379 Status: corev1.ConditionFalse, 380 }, 381 }, 382 }, 383 { 384 Host: "my-domain.com", 385 RouterName: "test", 386 RouterCanonicalHostname: "router-test.my-domain.com", 387 Conditions: []routev1.RouteIngressCondition{ 388 { 389 Type: routev1.RouteAdmitted, 390 Status: corev1.ConditionTrue, 391 }, 392 }, 393 }, 394 }, 395 }, 396 }, 397 expected: []*endpoint.Endpoint{ 398 { 399 DNSName: "my-domain.com", 400 RecordType: endpoint.RecordTypeCNAME, 401 Targets: []string{ 402 "router-test.my-domain.com", 403 }, 404 }, 405 }, 406 }, 407 { 408 title: "route with incorrect externalDNS controller annotation", 409 ocpRoute: &routev1.Route{ 410 ObjectMeta: metav1.ObjectMeta{ 411 Namespace: "default", 412 Name: "route-with-ignore-annotation", 413 Annotations: map[string]string{ 414 "external-dns.alpha.kubernetes.io/controller": "foo", 415 }, 416 }, 417 }, 418 expected: []*endpoint.Endpoint{}, 419 }, 420 { 421 title: "route with basic hostname and annotation target", 422 ocpRoute: &routev1.Route{ 423 ObjectMeta: metav1.ObjectMeta{ 424 Namespace: "default", 425 Name: "route-with-annotation-target", 426 Annotations: map[string]string{ 427 "external-dns.alpha.kubernetes.io/target": "my.site.foo.com", 428 }, 429 }, 430 Status: routev1.RouteStatus{ 431 Ingress: []routev1.RouteIngress{ 432 { 433 Host: "my-annotation-domain.com", 434 RouterName: "default", 435 RouterCanonicalHostname: "router-default.my-domain.com", 436 Conditions: []routev1.RouteIngressCondition{ 437 { 438 Type: routev1.RouteAdmitted, 439 Status: corev1.ConditionTrue, 440 }, 441 }, 442 }, 443 }, 444 }, 445 }, 446 expected: []*endpoint.Endpoint{ 447 { 448 DNSName: "my-annotation-domain.com", 449 RecordType: endpoint.RecordTypeCNAME, 450 Targets: []string{ 451 "my.site.foo.com", 452 }, 453 }, 454 }, 455 }, 456 { 457 title: "route with matching labels", 458 labelFilter: "app=web-external", 459 ocpRoute: &routev1.Route{ 460 ObjectMeta: metav1.ObjectMeta{ 461 Namespace: "default", 462 Name: "route-with-matching-labels", 463 Annotations: map[string]string{ 464 "external-dns.alpha.kubernetes.io/target": "my.site.foo.com", 465 }, 466 Labels: map[string]string{ 467 "app": "web-external", 468 "name": "service-frontend", 469 }, 470 }, 471 Status: routev1.RouteStatus{ 472 Ingress: []routev1.RouteIngress{ 473 { 474 Host: "my-annotation-domain.com", 475 RouterName: "default", 476 RouterCanonicalHostname: "router-default.my-domain.com", 477 Conditions: []routev1.RouteIngressCondition{ 478 { 479 Type: routev1.RouteAdmitted, 480 Status: corev1.ConditionTrue, 481 }, 482 }, 483 }, 484 }, 485 }, 486 }, 487 expected: []*endpoint.Endpoint{ 488 { 489 DNSName: "my-annotation-domain.com", 490 RecordType: endpoint.RecordTypeCNAME, 491 Targets: []string{ 492 "my.site.foo.com", 493 }, 494 }, 495 }, 496 }, 497 { 498 title: "route without matching labels", 499 labelFilter: "app=web-external", 500 ocpRoute: &routev1.Route{ 501 Spec: routev1.RouteSpec{ 502 Host: "my-annotation-domain.com", 503 }, 504 ObjectMeta: metav1.ObjectMeta{ 505 Namespace: "default", 506 Name: "route-without-matching-labels", 507 Annotations: map[string]string{ 508 "external-dns.alpha.kubernetes.io/target": "my.site.foo.com", 509 }, 510 Labels: map[string]string{ 511 "app": "web-internal", 512 "name": "service-frontend", 513 }, 514 }, 515 }, 516 expected: []*endpoint.Endpoint{}, 517 }, 518 } { 519 tc := tc 520 t.Run(tc.title, func(t *testing.T) { 521 t.Parallel() 522 // Create a Kubernetes testing client 523 fakeClient := fake.NewSimpleClientset() 524 _, err := fakeClient.RouteV1().Routes(tc.ocpRoute.Namespace).Create(context.Background(), tc.ocpRoute, metav1.CreateOptions{}) 525 require.NoError(t, err) 526 527 labelSelector, err := labels.Parse(tc.labelFilter) 528 require.NoError(t, err) 529 530 source, err := NewOcpRouteSource( 531 context.TODO(), 532 fakeClient, 533 "", 534 "", 535 "{{.Name}}", 536 false, 537 false, 538 labelSelector, 539 tc.ocpRouterName, 540 ) 541 require.NoError(t, err) 542 543 res, err := source.Endpoints(context.Background()) 544 if tc.expectError { 545 require.Error(t, err) 546 } else { 547 require.NoError(t, err) 548 } 549 550 // Validate returned endpoints against desired endpoints. 551 validateEndpoints(t, res, tc.expected) 552 }) 553 } 554 }