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  }