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

     1  /*
     2  Copyright 2018 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  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/stretchr/testify/require"
    30  	"github.com/stretchr/testify/suite"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/labels"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/apimachinery/pkg/runtime/schema"
    35  	"k8s.io/apimachinery/pkg/runtime/serializer"
    36  	"k8s.io/client-go/rest"
    37  	"k8s.io/client-go/rest/fake"
    38  
    39  	"sigs.k8s.io/external-dns/endpoint"
    40  )
    41  
    42  type CRDSuite struct {
    43  	suite.Suite
    44  }
    45  
    46  func (suite *CRDSuite) SetupTest() {
    47  }
    48  
    49  func defaultHeader() http.Header {
    50  	header := http.Header{}
    51  	header.Set("Content-Type", runtime.ContentTypeJSON)
    52  	return header
    53  }
    54  
    55  func objBody(codec runtime.Encoder, obj runtime.Object) io.ReadCloser {
    56  	return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
    57  }
    58  
    59  func fakeRESTClient(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string, annotations map[string]string, labels map[string]string, t *testing.T) rest.Interface {
    60  	groupVersion, _ := schema.ParseGroupVersion(apiVersion)
    61  	scheme := runtime.NewScheme()
    62  	addKnownTypes(scheme, groupVersion)
    63  
    64  	dnsEndpointList := endpoint.DNSEndpointList{}
    65  	dnsEndpoint := &endpoint.DNSEndpoint{
    66  		TypeMeta: metav1.TypeMeta{
    67  			APIVersion: apiVersion,
    68  			Kind:       kind,
    69  		},
    70  		ObjectMeta: metav1.ObjectMeta{
    71  			Name:        name,
    72  			Namespace:   namespace,
    73  			Annotations: annotations,
    74  			Labels:      labels,
    75  			Generation:  1,
    76  		},
    77  		Spec: endpoint.DNSEndpointSpec{
    78  			Endpoints: endpoints,
    79  		},
    80  	}
    81  
    82  	codecFactory := serializer.WithoutConversionCodecFactory{
    83  		CodecFactory: serializer.NewCodecFactory(scheme),
    84  	}
    85  
    86  	client := &fake.RESTClient{
    87  		GroupVersion:         groupVersion,
    88  		VersionedAPIPath:     "/apis/" + apiVersion,
    89  		NegotiatedSerializer: codecFactory,
    90  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
    91  			codec := codecFactory.LegacyCodec(groupVersion)
    92  			switch p, m := req.URL.Path, req.Method; {
    93  			case p == "/apis/"+apiVersion+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet:
    94  				fallthrough
    95  			case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet:
    96  				dnsEndpointList.Items = dnsEndpointList.Items[:0]
    97  				dnsEndpointList.Items = append(dnsEndpointList.Items, *dnsEndpoint)
    98  				return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil
    99  			case strings.HasPrefix(p, "/apis/"+apiVersion+"/namespaces/") && strings.HasSuffix(p, strings.ToLower(kind)+"s") && m == http.MethodGet:
   100  				return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil
   101  			case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s/"+name+"/status" && m == http.MethodPut:
   102  				decoder := json.NewDecoder(req.Body)
   103  
   104  				var body endpoint.DNSEndpoint
   105  				decoder.Decode(&body)
   106  				dnsEndpoint.Status.ObservedGeneration = body.Status.ObservedGeneration
   107  				return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, dnsEndpoint)}, nil
   108  			default:
   109  				return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req)
   110  			}
   111  		}),
   112  	}
   113  
   114  	return client
   115  }
   116  
   117  func TestCRDSource(t *testing.T) {
   118  	suite.Run(t, new(CRDSuite))
   119  	t.Run("Interface", testCRDSourceImplementsSource)
   120  	t.Run("Endpoints", testCRDSourceEndpoints)
   121  }
   122  
   123  // testCRDSourceImplementsSource tests that crdSource is a valid Source.
   124  func testCRDSourceImplementsSource(t *testing.T) {
   125  	require.Implements(t, (*Source)(nil), new(crdSource))
   126  }
   127  
   128  // testCRDSourceEndpoints tests various scenarios of using CRD source.
   129  func testCRDSourceEndpoints(t *testing.T) {
   130  	for _, ti := range []struct {
   131  		title                string
   132  		registeredNamespace  string
   133  		namespace            string
   134  		registeredAPIVersion string
   135  		apiVersion           string
   136  		registeredKind       string
   137  		kind                 string
   138  		endpoints            []*endpoint.Endpoint
   139  		expectEndpoints      bool
   140  		expectError          bool
   141  		annotationFilter     string
   142  		labelFilter          string
   143  		annotations          map[string]string
   144  		labels               map[string]string
   145  	}{
   146  		{
   147  			title:                "invalid crd api version",
   148  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   149  			apiVersion:           "blah.k8s.io/v1alpha1",
   150  			registeredKind:       "DNSEndpoint",
   151  			kind:                 "DNSEndpoint",
   152  			endpoints: []*endpoint.Endpoint{
   153  				{
   154  					DNSName:    "abc.example.org",
   155  					Targets:    endpoint.Targets{"1.2.3.4"},
   156  					RecordType: endpoint.RecordTypeA,
   157  					RecordTTL:  180,
   158  				},
   159  			},
   160  			expectEndpoints: false,
   161  			expectError:     true,
   162  		},
   163  		{
   164  			title:                "invalid crd kind",
   165  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   166  			apiVersion:           "test.k8s.io/v1alpha1",
   167  			registeredKind:       "DNSEndpoint",
   168  			kind:                 "JustEndpoint",
   169  			endpoints: []*endpoint.Endpoint{
   170  				{
   171  					DNSName:    "abc.example.org",
   172  					Targets:    endpoint.Targets{"1.2.3.4"},
   173  					RecordType: endpoint.RecordTypeA,
   174  					RecordTTL:  180,
   175  				},
   176  			},
   177  			expectEndpoints: false,
   178  			expectError:     true,
   179  		},
   180  		{
   181  			title:                "endpoints within a specific namespace",
   182  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   183  			apiVersion:           "test.k8s.io/v1alpha1",
   184  			registeredKind:       "DNSEndpoint",
   185  			kind:                 "DNSEndpoint",
   186  			namespace:            "foo",
   187  			registeredNamespace:  "foo",
   188  			endpoints: []*endpoint.Endpoint{
   189  				{
   190  					DNSName:    "abc.example.org",
   191  					Targets:    endpoint.Targets{"1.2.3.4"},
   192  					RecordType: endpoint.RecordTypeA,
   193  					RecordTTL:  180,
   194  				},
   195  			},
   196  			expectEndpoints: true,
   197  			expectError:     false,
   198  		},
   199  		{
   200  			title:                "no endpoints within a specific namespace",
   201  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   202  			apiVersion:           "test.k8s.io/v1alpha1",
   203  			registeredKind:       "DNSEndpoint",
   204  			kind:                 "DNSEndpoint",
   205  			namespace:            "foo",
   206  			registeredNamespace:  "bar",
   207  			endpoints: []*endpoint.Endpoint{
   208  				{
   209  					DNSName:    "abc.example.org",
   210  					Targets:    endpoint.Targets{"1.2.3.4"},
   211  					RecordType: endpoint.RecordTypeA,
   212  					RecordTTL:  180,
   213  				},
   214  			},
   215  			expectEndpoints: false,
   216  			expectError:     false,
   217  		},
   218  		{
   219  			title:                "invalid crd with no targets",
   220  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   221  			apiVersion:           "test.k8s.io/v1alpha1",
   222  			registeredKind:       "DNSEndpoint",
   223  			kind:                 "DNSEndpoint",
   224  			namespace:            "foo",
   225  			registeredNamespace:  "foo",
   226  			endpoints: []*endpoint.Endpoint{
   227  				{
   228  					DNSName:    "abc.example.org",
   229  					Targets:    endpoint.Targets{},
   230  					RecordType: endpoint.RecordTypeA,
   231  					RecordTTL:  180,
   232  				},
   233  			},
   234  			expectEndpoints: false,
   235  			expectError:     false,
   236  		},
   237  		{
   238  			title:                "valid crd gvk with single endpoint",
   239  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   240  			apiVersion:           "test.k8s.io/v1alpha1",
   241  			registeredKind:       "DNSEndpoint",
   242  			kind:                 "DNSEndpoint",
   243  			namespace:            "foo",
   244  			registeredNamespace:  "foo",
   245  			endpoints: []*endpoint.Endpoint{
   246  				{
   247  					DNSName:    "abc.example.org",
   248  					Targets:    endpoint.Targets{"1.2.3.4"},
   249  					RecordType: endpoint.RecordTypeA,
   250  					RecordTTL:  180,
   251  				},
   252  			},
   253  			expectEndpoints: true,
   254  			expectError:     false,
   255  		},
   256  		{
   257  			title:                "valid crd gvk with multiple endpoints",
   258  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   259  			apiVersion:           "test.k8s.io/v1alpha1",
   260  			registeredKind:       "DNSEndpoint",
   261  			kind:                 "DNSEndpoint",
   262  			namespace:            "foo",
   263  			registeredNamespace:  "foo",
   264  			endpoints: []*endpoint.Endpoint{
   265  				{
   266  					DNSName:    "abc.example.org",
   267  					Targets:    endpoint.Targets{"1.2.3.4"},
   268  					RecordType: endpoint.RecordTypeA,
   269  					RecordTTL:  180,
   270  				},
   271  				{
   272  					DNSName:    "xyz.example.org",
   273  					Targets:    endpoint.Targets{"abc.example.org"},
   274  					RecordType: endpoint.RecordTypeCNAME,
   275  					RecordTTL:  180,
   276  				},
   277  			},
   278  			expectEndpoints: true,
   279  			expectError:     false,
   280  		},
   281  		{
   282  			title:                "valid crd gvk with annotation and non matching annotation filter",
   283  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   284  			apiVersion:           "test.k8s.io/v1alpha1",
   285  			registeredKind:       "DNSEndpoint",
   286  			kind:                 "DNSEndpoint",
   287  			namespace:            "foo",
   288  			registeredNamespace:  "foo",
   289  			annotations:          map[string]string{"test": "that"},
   290  			annotationFilter:     "test=filter_something_else",
   291  			endpoints: []*endpoint.Endpoint{
   292  				{
   293  					DNSName:    "abc.example.org",
   294  					Targets:    endpoint.Targets{"1.2.3.4"},
   295  					RecordType: endpoint.RecordTypeA,
   296  					RecordTTL:  180,
   297  				},
   298  			},
   299  			expectEndpoints: false,
   300  			expectError:     false,
   301  		},
   302  		{
   303  			title:                "valid crd gvk with annotation and matching annotation filter",
   304  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   305  			apiVersion:           "test.k8s.io/v1alpha1",
   306  			registeredKind:       "DNSEndpoint",
   307  			kind:                 "DNSEndpoint",
   308  			namespace:            "foo",
   309  			registeredNamespace:  "foo",
   310  			annotations:          map[string]string{"test": "that"},
   311  			annotationFilter:     "test=that",
   312  			endpoints: []*endpoint.Endpoint{
   313  				{
   314  					DNSName:    "abc.example.org",
   315  					Targets:    endpoint.Targets{"1.2.3.4"},
   316  					RecordType: endpoint.RecordTypeA,
   317  					RecordTTL:  180,
   318  				},
   319  			},
   320  			expectEndpoints: true,
   321  			expectError:     false,
   322  		},
   323  		{
   324  			title:                "valid crd gvk with label and non matching label filter",
   325  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   326  			apiVersion:           "test.k8s.io/v1alpha1",
   327  			registeredKind:       "DNSEndpoint",
   328  			kind:                 "DNSEndpoint",
   329  			namespace:            "foo",
   330  			registeredNamespace:  "foo",
   331  			labels:               map[string]string{"test": "that"},
   332  			labelFilter:          "test=filter_something_else",
   333  			endpoints: []*endpoint.Endpoint{
   334  				{
   335  					DNSName:    "abc.example.org",
   336  					Targets:    endpoint.Targets{"1.2.3.4"},
   337  					RecordType: endpoint.RecordTypeA,
   338  					RecordTTL:  180,
   339  				},
   340  			},
   341  			expectEndpoints: false,
   342  			expectError:     false,
   343  		},
   344  		{
   345  			title:                "valid crd gvk with label and matching label filter",
   346  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   347  			apiVersion:           "test.k8s.io/v1alpha1",
   348  			registeredKind:       "DNSEndpoint",
   349  			kind:                 "DNSEndpoint",
   350  			namespace:            "foo",
   351  			registeredNamespace:  "foo",
   352  			labels:               map[string]string{"test": "that"},
   353  			labelFilter:          "test=that",
   354  			endpoints: []*endpoint.Endpoint{
   355  				{
   356  					DNSName:    "abc.example.org",
   357  					Targets:    endpoint.Targets{"1.2.3.4"},
   358  					RecordType: endpoint.RecordTypeA,
   359  					RecordTTL:  180,
   360  				},
   361  			},
   362  			expectEndpoints: true,
   363  			expectError:     false,
   364  		},
   365  		{
   366  			title:                "Create NS record",
   367  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   368  			apiVersion:           "test.k8s.io/v1alpha1",
   369  			registeredKind:       "DNSEndpoint",
   370  			kind:                 "DNSEndpoint",
   371  			namespace:            "foo",
   372  			registeredNamespace:  "foo",
   373  			labels:               map[string]string{"test": "that"},
   374  			labelFilter:          "test=that",
   375  			endpoints: []*endpoint.Endpoint{
   376  				{
   377  					DNSName:    "abc.example.org",
   378  					Targets:    endpoint.Targets{"ns1.k8s.io", "ns2.k8s.io"},
   379  					RecordType: endpoint.RecordTypeNS,
   380  					RecordTTL:  180,
   381  				},
   382  			},
   383  			expectEndpoints: true,
   384  			expectError:     false,
   385  		},
   386  		{
   387  			title:                "Create SRV record",
   388  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   389  			apiVersion:           "test.k8s.io/v1alpha1",
   390  			registeredKind:       "DNSEndpoint",
   391  			kind:                 "DNSEndpoint",
   392  			namespace:            "foo",
   393  			registeredNamespace:  "foo",
   394  			labels:               map[string]string{"test": "that"},
   395  			labelFilter:          "test=that",
   396  			endpoints: []*endpoint.Endpoint{
   397  				{
   398  					DNSName:    "_svc._tcp.example.org",
   399  					Targets:    endpoint.Targets{"0 0 80 abc.example.org", "0 0 80 def.example.org"},
   400  					RecordType: endpoint.RecordTypeSRV,
   401  					RecordTTL:  180,
   402  				},
   403  			},
   404  			expectEndpoints: true,
   405  			expectError:     false,
   406  		},
   407  		{
   408  			title:                "Create NAPTR record",
   409  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   410  			apiVersion:           "test.k8s.io/v1alpha1",
   411  			registeredKind:       "DNSEndpoint",
   412  			kind:                 "DNSEndpoint",
   413  			namespace:            "foo",
   414  			registeredNamespace:  "foo",
   415  			labels:               map[string]string{"test": "that"},
   416  			labelFilter:          "test=that",
   417  			endpoints: []*endpoint.Endpoint{
   418  				{
   419  					DNSName:    "example.org",
   420  					Targets:    endpoint.Targets{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org.`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org.`},
   421  					RecordType: endpoint.RecordTypeNAPTR,
   422  					RecordTTL:  180,
   423  				},
   424  			},
   425  			expectEndpoints: true,
   426  			expectError:     false,
   427  		},
   428  		{
   429  			title:                "illegal target CNAME",
   430  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   431  			apiVersion:           "test.k8s.io/v1alpha1",
   432  			registeredKind:       "DNSEndpoint",
   433  			kind:                 "DNSEndpoint",
   434  			namespace:            "foo",
   435  			registeredNamespace:  "foo",
   436  			labels:               map[string]string{"test": "that"},
   437  			labelFilter:          "test=that",
   438  			endpoints: []*endpoint.Endpoint{
   439  				{
   440  					DNSName:    "example.org",
   441  					Targets:    endpoint.Targets{"foo.example.org."},
   442  					RecordType: endpoint.RecordTypeCNAME,
   443  					RecordTTL:  180,
   444  				},
   445  			},
   446  			expectEndpoints: false,
   447  			expectError:     false,
   448  		},
   449  		{
   450  			title:                "illegal target NAPTR",
   451  			registeredAPIVersion: "test.k8s.io/v1alpha1",
   452  			apiVersion:           "test.k8s.io/v1alpha1",
   453  			registeredKind:       "DNSEndpoint",
   454  			kind:                 "DNSEndpoint",
   455  			namespace:            "foo",
   456  			registeredNamespace:  "foo",
   457  			labels:               map[string]string{"test": "that"},
   458  			labelFilter:          "test=that",
   459  			endpoints: []*endpoint.Endpoint{
   460  				{
   461  					DNSName:    "example.org",
   462  					Targets:    endpoint.Targets{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org`},
   463  					RecordType: endpoint.RecordTypeNAPTR,
   464  					RecordTTL:  180,
   465  				},
   466  			},
   467  			expectEndpoints: false,
   468  			expectError:     false,
   469  		},
   470  	} {
   471  		ti := ti
   472  		t.Run(ti.title, func(t *testing.T) {
   473  			t.Parallel()
   474  
   475  			restClient := fakeRESTClient(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "test", ti.annotations, ti.labels, t)
   476  			groupVersion, err := schema.ParseGroupVersion(ti.apiVersion)
   477  			require.NoError(t, err)
   478  
   479  			scheme := runtime.NewScheme()
   480  			require.NoError(t, addKnownTypes(scheme, groupVersion))
   481  
   482  			labelSelector, err := labels.Parse(ti.labelFilter)
   483  			require.NoError(t, err)
   484  
   485  			// At present, client-go's fake.RESTClient (used by crd_test.go) is known to cause race conditions when used
   486  			// with informers: https://github.com/kubernetes/kubernetes/issues/95372
   487  			// So don't start the informer during testing.
   488  			startInformer := false
   489  
   490  			cs, err := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, labelSelector, scheme, startInformer)
   491  			require.NoError(t, err)
   492  
   493  			receivedEndpoints, err := cs.Endpoints(context.Background())
   494  			if ti.expectError {
   495  				require.Errorf(t, err, "Received err %v", err)
   496  			} else {
   497  				require.NoErrorf(t, err, "Received err %v", err)
   498  			}
   499  
   500  			if len(receivedEndpoints) == 0 && !ti.expectEndpoints {
   501  				return
   502  			}
   503  
   504  			if err == nil {
   505  				validateCRDResource(t, cs, ti.expectError)
   506  			}
   507  
   508  			// Validate received endpoints against expected endpoints.
   509  			validateEndpoints(t, receivedEndpoints, ti.endpoints)
   510  		})
   511  	}
   512  }
   513  
   514  func validateCRDResource(t *testing.T, src Source, expectError bool) {
   515  	cs := src.(*crdSource)
   516  	result, err := cs.List(context.Background(), &metav1.ListOptions{})
   517  	if expectError {
   518  		require.Errorf(t, err, "Received err %v", err)
   519  	} else {
   520  		require.NoErrorf(t, err, "Received err %v", err)
   521  	}
   522  
   523  	for _, dnsEndpoint := range result.Items {
   524  		if dnsEndpoint.Status.ObservedGeneration != dnsEndpoint.Generation {
   525  			require.Errorf(t, err, "Unexpected CRD resource result: ObservedGenerations <%v> is not equal to Generation<%v>", dnsEndpoint.Status.ObservedGeneration, dnsEndpoint.Generation)
   526  		}
   527  	}
   528  }