sigs.k8s.io/external-dns@v0.14.1/source/ambassador_host_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  	"fmt"
    21  	"testing"
    22  
    23  	ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2"
    24  	"github.com/stretchr/testify/assert"
    25  	"github.com/stretchr/testify/require"
    26  	"github.com/stretchr/testify/suite"
    27  	"golang.org/x/net/context"
    28  	v1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	fakeDynamic "k8s.io/client-go/dynamic/fake"
    33  	fakeKube "k8s.io/client-go/kubernetes/fake"
    34  	"sigs.k8s.io/external-dns/endpoint"
    35  )
    36  
    37  const defaultAmbassadorNamespace = "ambassador"
    38  const defaultAmbassadorServiceName = "ambassador"
    39  
    40  type AmbassadorSuite struct {
    41  	suite.Suite
    42  }
    43  
    44  func TestAmbassadorSource(t *testing.T) {
    45  	suite.Run(t, new(AmbassadorSuite))
    46  	t.Run("Interface", testAmbassadorSourceImplementsSource)
    47  }
    48  
    49  // testAmbassadorSourceImplementsSource tests that ambassadorHostSource is a valid Source.
    50  func testAmbassadorSourceImplementsSource(t *testing.T) {
    51  	require.Implements(t, (*Source)(nil), new(ambassadorHostSource))
    52  }
    53  
    54  func TestAmbassadorHostSource(t *testing.T) {
    55  	t.Parallel()
    56  
    57  	hostAnnotation := fmt.Sprintf("%s/%s", defaultAmbassadorNamespace, defaultAmbassadorServiceName)
    58  
    59  	for _, ti := range []struct {
    60  		title    string
    61  		host     ambassador.Host
    62  		service  v1.Service
    63  		expected []*endpoint.Endpoint
    64  	}{
    65  		{
    66  			title: "Simple host",
    67  			host: ambassador.Host{
    68  				ObjectMeta: metav1.ObjectMeta{
    69  					Name: "basic-host",
    70  					Annotations: map[string]string{
    71  						ambHostAnnotation: hostAnnotation,
    72  					},
    73  				},
    74  				Spec: &ambassador.HostSpec{
    75  					Hostname: "www.example.org",
    76  				},
    77  			},
    78  			service: v1.Service{
    79  				ObjectMeta: metav1.ObjectMeta{
    80  					Name: defaultAmbassadorServiceName,
    81  				},
    82  				Status: v1.ServiceStatus{
    83  					LoadBalancer: v1.LoadBalancerStatus{
    84  						Ingress: []v1.LoadBalancerIngress{{
    85  							IP: "1.1.1.1",
    86  						}},
    87  					},
    88  				},
    89  			},
    90  			expected: []*endpoint.Endpoint{
    91  				{
    92  					DNSName:    "www.example.org",
    93  					RecordType: endpoint.RecordTypeA,
    94  					Targets:    endpoint.Targets{"1.1.1.1"},
    95  				},
    96  			},
    97  		}, {
    98  			title: "Service with load balancer hostname",
    99  			host: ambassador.Host{
   100  				ObjectMeta: metav1.ObjectMeta{
   101  					Name: "basic-host",
   102  					Annotations: map[string]string{
   103  						ambHostAnnotation: hostAnnotation,
   104  					},
   105  				},
   106  				Spec: &ambassador.HostSpec{
   107  					Hostname: "www.example.org",
   108  				},
   109  			},
   110  			service: v1.Service{
   111  				ObjectMeta: metav1.ObjectMeta{
   112  					Name: defaultAmbassadorServiceName,
   113  				},
   114  				Status: v1.ServiceStatus{
   115  					LoadBalancer: v1.LoadBalancerStatus{
   116  						Ingress: []v1.LoadBalancerIngress{{
   117  							Hostname: "dns.google",
   118  						}},
   119  					},
   120  				},
   121  			},
   122  			expected: []*endpoint.Endpoint{
   123  				{
   124  					DNSName:    "www.example.org",
   125  					RecordType: endpoint.RecordTypeCNAME,
   126  					Targets:    endpoint.Targets{"dns.google"},
   127  				},
   128  			},
   129  		}, {
   130  			title: "Service with external IP",
   131  			host: ambassador.Host{
   132  				ObjectMeta: metav1.ObjectMeta{
   133  					Name: "service-external-ip",
   134  					Annotations: map[string]string{
   135  						ambHostAnnotation: hostAnnotation,
   136  					},
   137  				},
   138  				Spec: &ambassador.HostSpec{
   139  					Hostname: "www.example.org",
   140  				},
   141  			},
   142  			service: v1.Service{
   143  				ObjectMeta: metav1.ObjectMeta{
   144  					Name: defaultAmbassadorServiceName,
   145  				},
   146  				Spec: v1.ServiceSpec{
   147  					ExternalIPs: []string{"2.2.2.2"},
   148  				},
   149  				Status: v1.ServiceStatus{
   150  					LoadBalancer: v1.LoadBalancerStatus{
   151  						Ingress: []v1.LoadBalancerIngress{{
   152  							IP: "1.1.1.1",
   153  						}},
   154  					},
   155  				},
   156  			},
   157  			expected: []*endpoint.Endpoint{
   158  				{
   159  					DNSName:    "www.example.org",
   160  					RecordType: endpoint.RecordTypeA,
   161  					Targets:    endpoint.Targets{"2.2.2.2"},
   162  				},
   163  			},
   164  		}, {
   165  			title: "Host with target annotation",
   166  			host: ambassador.Host{
   167  				ObjectMeta: metav1.ObjectMeta{
   168  					Name: "basic-host",
   169  					Annotations: map[string]string{
   170  						ambHostAnnotation:   hostAnnotation,
   171  						targetAnnotationKey: "3.3.3.3",
   172  					},
   173  				},
   174  				Spec: &ambassador.HostSpec{
   175  					Hostname: "www.example.org",
   176  				},
   177  			},
   178  			service: v1.Service{
   179  				ObjectMeta: metav1.ObjectMeta{
   180  					Name: defaultAmbassadorServiceName,
   181  				},
   182  				Status: v1.ServiceStatus{
   183  					LoadBalancer: v1.LoadBalancerStatus{
   184  						Ingress: []v1.LoadBalancerIngress{{
   185  							IP: "1.1.1.1",
   186  						}},
   187  					},
   188  				},
   189  			},
   190  			expected: []*endpoint.Endpoint{
   191  				{
   192  					DNSName:    "www.example.org",
   193  					RecordType: endpoint.RecordTypeA,
   194  					Targets:    endpoint.Targets{"3.3.3.3"},
   195  				},
   196  			},
   197  		}, {
   198  			title: "Host with TTL annotation",
   199  			host: ambassador.Host{
   200  				ObjectMeta: metav1.ObjectMeta{
   201  					Name: "basic-host",
   202  					Annotations: map[string]string{
   203  						ambHostAnnotation: hostAnnotation,
   204  						ttlAnnotationKey:  "180",
   205  					},
   206  				},
   207  				Spec: &ambassador.HostSpec{
   208  					Hostname: "www.example.org",
   209  				},
   210  			},
   211  			service: v1.Service{
   212  				ObjectMeta: metav1.ObjectMeta{
   213  					Name: defaultAmbassadorServiceName,
   214  				},
   215  				Status: v1.ServiceStatus{
   216  					LoadBalancer: v1.LoadBalancerStatus{
   217  						Ingress: []v1.LoadBalancerIngress{{
   218  							IP: "1.1.1.1",
   219  						}},
   220  					},
   221  				},
   222  			},
   223  			expected: []*endpoint.Endpoint{
   224  				{
   225  					DNSName:    "www.example.org",
   226  					RecordType: endpoint.RecordTypeA,
   227  					Targets:    endpoint.Targets{"1.1.1.1"},
   228  					RecordTTL:  180,
   229  				},
   230  			},
   231  		}, {
   232  			title: "Host with provider specific annotation",
   233  			host: ambassador.Host{
   234  				ObjectMeta: metav1.ObjectMeta{
   235  					Name: "basic-host",
   236  					Annotations: map[string]string{
   237  						ambHostAnnotation:    hostAnnotation,
   238  						CloudflareProxiedKey: "true",
   239  					},
   240  				},
   241  				Spec: &ambassador.HostSpec{
   242  					Hostname: "www.example.org",
   243  				},
   244  			},
   245  			service: v1.Service{
   246  				ObjectMeta: metav1.ObjectMeta{
   247  					Name: defaultAmbassadorServiceName,
   248  				},
   249  				Status: v1.ServiceStatus{
   250  					LoadBalancer: v1.LoadBalancerStatus{
   251  						Ingress: []v1.LoadBalancerIngress{{
   252  							IP: "1.1.1.1",
   253  						}},
   254  					},
   255  				},
   256  			},
   257  			expected: []*endpoint.Endpoint{
   258  				{
   259  					DNSName:    "www.example.org",
   260  					RecordType: endpoint.RecordTypeA,
   261  					Targets:    endpoint.Targets{"1.1.1.1"},
   262  					ProviderSpecific: endpoint.ProviderSpecific{{
   263  						Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   264  						Value: "true",
   265  					}},
   266  				},
   267  			},
   268  		}, {
   269  			title: "Host with missing Ambassador annotation",
   270  			host: ambassador.Host{
   271  				ObjectMeta: metav1.ObjectMeta{
   272  					Name: "basic-host",
   273  				},
   274  				Spec: &ambassador.HostSpec{
   275  					Hostname: "www.example.org",
   276  				},
   277  			},
   278  			service: v1.Service{
   279  				ObjectMeta: metav1.ObjectMeta{
   280  					Name: defaultAmbassadorServiceName,
   281  				},
   282  				Status: v1.ServiceStatus{
   283  					LoadBalancer: v1.LoadBalancerStatus{
   284  						Ingress: []v1.LoadBalancerIngress{{
   285  							IP: "1.1.1.1",
   286  						}},
   287  					},
   288  				},
   289  			},
   290  			expected: []*endpoint.Endpoint{},
   291  		},
   292  	} {
   293  		ti := ti
   294  		t.Run(ti.title, func(t *testing.T) {
   295  			t.Parallel()
   296  
   297  			fakeKubernetesClient := fakeKube.NewSimpleClientset()
   298  			ambassadorScheme := runtime.NewScheme()
   299  			ambassador.AddToScheme(ambassadorScheme)
   300  			fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(ambassadorScheme)
   301  
   302  			namespace := "default"
   303  
   304  			// Create Ambassador service
   305  			_, err := fakeKubernetesClient.CoreV1().Services(defaultAmbassadorNamespace).Create(context.Background(), &ti.service, metav1.CreateOptions{})
   306  			assert.NoError(t, err)
   307  
   308  			// Create host resource
   309  			host, err := createAmbassadorHost(&ti.host)
   310  			assert.NoError(t, err)
   311  
   312  			_, err = fakeDynamicClient.Resource(ambHostGVR).Namespace(namespace).Create(context.Background(), host, metav1.CreateOptions{})
   313  			assert.NoError(t, err)
   314  
   315  			source, err := NewAmbassadorHostSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, namespace)
   316  			assert.NoError(t, err)
   317  			assert.NotNil(t, source)
   318  
   319  			endpoints, err := source.Endpoints(context.Background())
   320  			assert.NoError(t, err)
   321  			// Validate returned endpoints against expected endpoints.
   322  			validateEndpoints(t, endpoints, ti.expected)
   323  		})
   324  	}
   325  }
   326  
   327  func createAmbassadorHost(host *ambassador.Host) (*unstructured.Unstructured, error) {
   328  	obj := &unstructured.Unstructured{}
   329  	uc, _ := newUnstructuredConverter()
   330  	err := uc.scheme.Convert(host, obj, nil)
   331  
   332  	return obj, err
   333  }
   334  
   335  // TestParseAmbLoadBalancerService tests our parsing of Ambassador service info.
   336  func TestParseAmbLoadBalancerService(t *testing.T) {
   337  	vectors := []struct {
   338  		input  string
   339  		ns     string
   340  		svc    string
   341  		errstr string
   342  	}{
   343  		{"svc", "default", "svc", ""},
   344  		{"ns/svc", "ns", "svc", ""},
   345  		{"svc.ns", "ns", "svc", ""},
   346  		{"svc.ns.foo.bar", "ns.foo.bar", "svc", ""},
   347  		{"ns/svc/foo/bar", "", "", "invalid external-dns service: ns/svc/foo/bar"},
   348  		{"ns/svc/foo.bar", "", "", "invalid external-dns service: ns/svc/foo.bar"},
   349  		{"ns.foo/svc/bar", "", "", "invalid external-dns service: ns.foo/svc/bar"},
   350  	}
   351  
   352  	for _, v := range vectors {
   353  		ns, svc, err := parseAmbLoadBalancerService(v.input)
   354  
   355  		errstr := ""
   356  
   357  		if err != nil {
   358  			errstr = err.Error()
   359  		}
   360  		if v.ns != ns {
   361  			t.Errorf("%s: got ns \"%s\", wanted \"%s\"", v.input, ns, v.ns)
   362  		}
   363  
   364  		if v.svc != svc {
   365  			t.Errorf("%s: got svc \"%s\", wanted \"%s\"", v.input, svc, v.svc)
   366  		}
   367  
   368  		if v.errstr != errstr {
   369  			t.Errorf("%s: got err \"%s\", wanted \"%s\"", v.input, errstr, v.errstr)
   370  		}
   371  	}
   372  }