sigs.k8s.io/external-dns@v0.14.1/source/node_test.go (about)

     1  /*
     2  Copyright 2019 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  	v1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/labels"
    28  	"k8s.io/client-go/kubernetes/fake"
    29  
    30  	"sigs.k8s.io/external-dns/endpoint"
    31  )
    32  
    33  func TestNodeSource(t *testing.T) {
    34  	t.Parallel()
    35  
    36  	t.Run("NewNodeSource", testNodeSourceNewNodeSource)
    37  	t.Run("Endpoints", testNodeSourceEndpoints)
    38  }
    39  
    40  // testNodeSourceNewNodeSource tests that NewNodeService doesn't return an error.
    41  func testNodeSourceNewNodeSource(t *testing.T) {
    42  	t.Parallel()
    43  
    44  	for _, ti := range []struct {
    45  		title            string
    46  		annotationFilter string
    47  		fqdnTemplate     string
    48  		expectError      bool
    49  	}{
    50  		{
    51  			title:        "invalid template",
    52  			expectError:  true,
    53  			fqdnTemplate: "{{.Name",
    54  		},
    55  		{
    56  			title:       "valid empty template",
    57  			expectError: false,
    58  		},
    59  		{
    60  			title:        "valid template",
    61  			expectError:  false,
    62  			fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
    63  		},
    64  		{
    65  			title:            "non-empty annotation filter label",
    66  			expectError:      false,
    67  			annotationFilter: "kubernetes.io/ingress.class=nginx",
    68  		},
    69  	} {
    70  		ti := ti
    71  		t.Run(ti.title, func(t *testing.T) {
    72  			t.Parallel()
    73  
    74  			_, err := NewNodeSource(
    75  				context.TODO(),
    76  				fake.NewSimpleClientset(),
    77  				ti.annotationFilter,
    78  				ti.fqdnTemplate,
    79  				labels.Everything(),
    80  			)
    81  
    82  			if ti.expectError {
    83  				assert.Error(t, err)
    84  			} else {
    85  				assert.NoError(t, err)
    86  			}
    87  		})
    88  	}
    89  }
    90  
    91  // testNodeSourceEndpoints tests that various node generate the correct endpoints.
    92  func testNodeSourceEndpoints(t *testing.T) {
    93  	t.Parallel()
    94  
    95  	for _, tc := range []struct {
    96  		title            string
    97  		annotationFilter string
    98  		labelSelector    string
    99  		fqdnTemplate     string
   100  		nodeName         string
   101  		nodeAddresses    []v1.NodeAddress
   102  		labels           map[string]string
   103  		annotations      map[string]string
   104  		expected         []*endpoint.Endpoint
   105  		expectError      bool
   106  	}{
   107  		{
   108  			title:         "node with short hostname returns one endpoint",
   109  			nodeName:      "node1",
   110  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   111  			expected: []*endpoint.Endpoint{
   112  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
   113  			},
   114  		},
   115  		{
   116  			title:         "node with fqdn returns one endpoint",
   117  			nodeName:      "node1.example.org",
   118  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   119  			expected: []*endpoint.Endpoint{
   120  				{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
   121  			},
   122  		},
   123  		{
   124  			title:         "ipv6 node with fqdn returns one endpoint",
   125  			nodeName:      "node1.example.org",
   126  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
   127  			expected: []*endpoint.Endpoint{
   128  				{RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}},
   129  			},
   130  		},
   131  		{
   132  			title:         "node with fqdn template returns endpoint with expanded hostname",
   133  			fqdnTemplate:  "{{.Name}}.example.org",
   134  			nodeName:      "node1",
   135  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   136  			expected: []*endpoint.Endpoint{
   137  				{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
   138  			},
   139  		},
   140  		{
   141  			title:         "node with fqdn and fqdn template returns one endpoint",
   142  			fqdnTemplate:  "{{.Name}}.example.org",
   143  			nodeName:      "node1.example.org",
   144  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   145  			expected: []*endpoint.Endpoint{
   146  				{RecordType: "A", DNSName: "node1.example.org.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
   147  			},
   148  		},
   149  		{
   150  			title:         "node with fqdn template returns two endpoints with multiple IP addresses and expanded hostname",
   151  			fqdnTemplate:  "{{.Name}}.example.org",
   152  			nodeName:      "node1",
   153  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeExternalIP, Address: "5.6.7.8"}},
   154  			expected: []*endpoint.Endpoint{
   155  				{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4", "5.6.7.8"}},
   156  			},
   157  		},
   158  		{
   159  			title:         "node with fqdn template returns two endpoints with dual-stack IP addresses and expanded hostname",
   160  			fqdnTemplate:  "{{.Name}}.example.org",
   161  			nodeName:      "node1",
   162  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
   163  			expected: []*endpoint.Endpoint{
   164  				{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
   165  				{RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}},
   166  			},
   167  		},
   168  		{
   169  			title:         "node with both external and internal IP returns an endpoint with external IP",
   170  			nodeName:      "node1",
   171  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}},
   172  			expected: []*endpoint.Endpoint{
   173  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
   174  			},
   175  		},
   176  		{
   177  			title:         "node with both external, internal, and IPv6 IP returns endpoints with external IPs",
   178  			nodeName:      "node1",
   179  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
   180  			expected: []*endpoint.Endpoint{
   181  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
   182  				{RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}},
   183  			},
   184  		},
   185  		{
   186  			title:         "node with only internal IP returns an endpoint with internal IP",
   187  			nodeName:      "node1",
   188  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}},
   189  			expected: []*endpoint.Endpoint{
   190  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"2.3.4.5"}},
   191  			},
   192  		},
   193  		{
   194  			title:         "node with only internal IPs returns endpoints with internal IPs",
   195  			nodeName:      "node1",
   196  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
   197  			expected: []*endpoint.Endpoint{
   198  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"2.3.4.5"}},
   199  				{RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}},
   200  			},
   201  		},
   202  		{
   203  			title:         "node with neither external nor internal IP returns no endpoints",
   204  			nodeName:      "node1",
   205  			nodeAddresses: []v1.NodeAddress{},
   206  			expectError:   true,
   207  		},
   208  		{
   209  			title:         "node with target annotation",
   210  			nodeName:      "node1.example.org",
   211  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   212  			annotations: map[string]string{
   213  				"external-dns.alpha.kubernetes.io/target": "203.2.45.7",
   214  			},
   215  			expected: []*endpoint.Endpoint{
   216  				{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"203.2.45.7"}},
   217  			},
   218  		},
   219  		{
   220  			title:         "annotated node without annotation filter returns endpoint",
   221  			nodeName:      "node1",
   222  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   223  			annotations: map[string]string{
   224  				"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
   225  			},
   226  			expected: []*endpoint.Endpoint{
   227  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
   228  			},
   229  		},
   230  		{
   231  			title:            "annotated node with matching annotation filter returns endpoint",
   232  			annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)",
   233  			nodeName:         "node1",
   234  			nodeAddresses:    []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   235  			annotations: map[string]string{
   236  				"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
   237  			},
   238  			expected: []*endpoint.Endpoint{
   239  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
   240  			},
   241  		},
   242  		{
   243  			title:            "annotated node with non-matching annotation filter returns nothing",
   244  			annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)",
   245  			nodeName:         "node1",
   246  			nodeAddresses:    []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   247  			annotations: map[string]string{
   248  				"service.beta.kubernetes.io/external-traffic": "SomethingElse",
   249  			},
   250  			expected: []*endpoint.Endpoint{},
   251  		},
   252  		{
   253  			title:         "labeled node with matching label selector returns endpoint",
   254  			labelSelector: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)",
   255  			nodeName:      "node1",
   256  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   257  			labels: map[string]string{
   258  				"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
   259  			},
   260  			expected: []*endpoint.Endpoint{
   261  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
   262  			},
   263  		},
   264  		{
   265  			title:         "labeled node with non-matching label selector returns nothing",
   266  			labelSelector: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)",
   267  			nodeName:      "node1",
   268  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   269  			labels: map[string]string{
   270  				"service.beta.kubernetes.io/external-traffic": "SomethingElse",
   271  			},
   272  			expected: []*endpoint.Endpoint{},
   273  		},
   274  		{
   275  			title:         "our controller type is dns-controller",
   276  			nodeName:      "node1",
   277  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   278  			annotations: map[string]string{
   279  				controllerAnnotationKey: controllerAnnotationValue,
   280  			},
   281  			expected: []*endpoint.Endpoint{
   282  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
   283  			},
   284  		},
   285  		{
   286  			title:         "different controller types are ignored",
   287  			nodeName:      "node1",
   288  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   289  			annotations: map[string]string{
   290  				controllerAnnotationKey: "not-dns-controller",
   291  			},
   292  			expected: []*endpoint.Endpoint{},
   293  		},
   294  		{
   295  			title:         "ttl not annotated should have RecordTTL.IsConfigured set to false",
   296  			nodeName:      "node1",
   297  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   298  			expected: []*endpoint.Endpoint{
   299  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)},
   300  			},
   301  		},
   302  		{
   303  			title:         "ttl annotated but invalid should have RecordTTL.IsConfigured set to false",
   304  			nodeName:      "node1",
   305  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   306  			annotations: map[string]string{
   307  				ttlAnnotationKey: "foo",
   308  			},
   309  			expected: []*endpoint.Endpoint{
   310  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)},
   311  			},
   312  		},
   313  		{
   314  			title:         "ttl annotated and is valid should set Record.TTL",
   315  			nodeName:      "node1",
   316  			nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
   317  			annotations: map[string]string{
   318  				ttlAnnotationKey: "10",
   319  			},
   320  			expected: []*endpoint.Endpoint{
   321  				{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)},
   322  			},
   323  		},
   324  	} {
   325  		tc := tc
   326  		t.Run(tc.title, func(t *testing.T) {
   327  			t.Parallel()
   328  
   329  			labelSelector := labels.Everything()
   330  			if tc.labelSelector != "" {
   331  				var err error
   332  				labelSelector, err = labels.Parse(tc.labelSelector)
   333  				require.NoError(t, err)
   334  			}
   335  
   336  			// Create a Kubernetes testing client
   337  			kubernetes := fake.NewSimpleClientset()
   338  
   339  			node := &v1.Node{
   340  				ObjectMeta: metav1.ObjectMeta{
   341  					Name:        tc.nodeName,
   342  					Labels:      tc.labels,
   343  					Annotations: tc.annotations,
   344  				},
   345  				Status: v1.NodeStatus{
   346  					Addresses: tc.nodeAddresses,
   347  				},
   348  			}
   349  
   350  			_, err := kubernetes.CoreV1().Nodes().Create(context.Background(), node, metav1.CreateOptions{})
   351  			require.NoError(t, err)
   352  
   353  			// Create our object under test and get the endpoints.
   354  			client, err := NewNodeSource(
   355  				context.TODO(),
   356  				kubernetes,
   357  				tc.annotationFilter,
   358  				tc.fqdnTemplate,
   359  				labelSelector,
   360  			)
   361  			require.NoError(t, err)
   362  
   363  			endpoints, err := client.Endpoints(context.Background())
   364  			if tc.expectError {
   365  				require.Error(t, err)
   366  			} else {
   367  				require.NoError(t, err)
   368  			}
   369  
   370  			// Validate returned endpoints against desired endpoints.
   371  			validateEndpoints(t, endpoints, tc.expected)
   372  		})
   373  	}
   374  }