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 }