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

     1  /*
     2  Copyright 2020 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  	fakeDynamic "k8s.io/client-go/dynamic/fake"
    24  
    25  	"github.com/pkg/errors"
    26  	projectcontour "github.com/projectcontour/contour/apis/projectcontour/v1"
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  	"github.com/stretchr/testify/suite"
    30  	v1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"sigs.k8s.io/external-dns/endpoint"
    35  )
    36  
    37  // This is a compile-time validation that httpProxySource is a Source.
    38  var _ Source = &httpProxySource{}
    39  
    40  type HTTPProxySuite struct {
    41  	suite.Suite
    42  	source    Source
    43  	httpProxy *projectcontour.HTTPProxy
    44  }
    45  
    46  func newDynamicKubernetesClient() (*fakeDynamic.FakeDynamicClient, *runtime.Scheme) {
    47  	s := runtime.NewScheme()
    48  	_ = projectcontour.AddToScheme(s)
    49  	return fakeDynamic.NewSimpleDynamicClient(s), s
    50  }
    51  
    52  type fakeLoadBalancerService struct {
    53  	ips       []string
    54  	hostnames []string
    55  	namespace string
    56  	name      string
    57  }
    58  
    59  func (ig fakeLoadBalancerService) Service() *v1.Service {
    60  	svc := &v1.Service{
    61  		ObjectMeta: metav1.ObjectMeta{
    62  			Namespace: ig.namespace,
    63  			Name:      ig.name,
    64  		},
    65  		Status: v1.ServiceStatus{
    66  			LoadBalancer: v1.LoadBalancerStatus{
    67  				Ingress: []v1.LoadBalancerIngress{},
    68  			},
    69  		},
    70  	}
    71  
    72  	for _, ip := range ig.ips {
    73  		svc.Status.LoadBalancer.Ingress = append(svc.Status.LoadBalancer.Ingress, v1.LoadBalancerIngress{
    74  			IP: ip,
    75  		})
    76  	}
    77  	for _, hostname := range ig.hostnames {
    78  		svc.Status.LoadBalancer.Ingress = append(svc.Status.LoadBalancer.Ingress, v1.LoadBalancerIngress{
    79  			Hostname: hostname,
    80  		})
    81  	}
    82  
    83  	return svc
    84  }
    85  
    86  func (suite *HTTPProxySuite) SetupTest() {
    87  	fakeDynamicClient, s := newDynamicKubernetesClient()
    88  	var err error
    89  
    90  	suite.source, err = NewContourHTTPProxySource(
    91  		context.TODO(),
    92  		fakeDynamicClient,
    93  		"default",
    94  		"",
    95  		"{{.Name}}",
    96  		false,
    97  		false,
    98  	)
    99  	suite.NoError(err, "should initialize httpproxy source")
   100  
   101  	suite.httpProxy = (fakeHTTPProxy{
   102  		name:      "foo-httpproxy-with-targets",
   103  		namespace: "default",
   104  		host:      "example.com",
   105  	}).HTTPProxy()
   106  
   107  	// Convert to unstructured
   108  	unstructuredHTTPProxy, err := convertHTTPProxyToUnstructured(suite.httpProxy, s)
   109  	if err != nil {
   110  		suite.Error(err)
   111  	}
   112  
   113  	_, err = fakeDynamicClient.Resource(projectcontour.HTTPProxyGVR).Namespace(suite.httpProxy.Namespace).Create(context.Background(), unstructuredHTTPProxy, metav1.CreateOptions{})
   114  	suite.NoError(err, "should succeed")
   115  }
   116  
   117  func (suite *HTTPProxySuite) TestResourceLabelIsSet() {
   118  	endpoints, _ := suite.source.Endpoints(context.Background())
   119  	for _, ep := range endpoints {
   120  		suite.Equal("httpproxy/default/foo-httpproxy-with-targets", ep.Labels[endpoint.ResourceLabelKey], "should set correct resource label")
   121  	}
   122  }
   123  
   124  func convertHTTPProxyToUnstructured(hp *projectcontour.HTTPProxy, s *runtime.Scheme) (*unstructured.Unstructured, error) {
   125  	unstructuredHTTPProxy := &unstructured.Unstructured{}
   126  	if err := s.Convert(hp, unstructuredHTTPProxy, context.Background()); err != nil {
   127  		return nil, err
   128  	}
   129  	return unstructuredHTTPProxy, nil
   130  }
   131  
   132  func TestHTTPProxy(t *testing.T) {
   133  	t.Parallel()
   134  
   135  	suite.Run(t, new(HTTPProxySuite))
   136  	t.Run("endpointsFromHTTPProxy", testEndpointsFromHTTPProxy)
   137  	t.Run("Endpoints", testHTTPProxyEndpoints)
   138  }
   139  
   140  func TestNewContourHTTPProxySource(t *testing.T) {
   141  	t.Parallel()
   142  
   143  	for _, ti := range []struct {
   144  		title                    string
   145  		annotationFilter         string
   146  		fqdnTemplate             string
   147  		combineFQDNAndAnnotation bool
   148  		expectError              bool
   149  	}{
   150  		{
   151  			title:        "invalid template",
   152  			expectError:  true,
   153  			fqdnTemplate: "{{.Name",
   154  		},
   155  		{
   156  			title:       "valid empty template",
   157  			expectError: false,
   158  		},
   159  		{
   160  			title:        "valid template",
   161  			expectError:  false,
   162  			fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
   163  		},
   164  		{
   165  			title:        "valid template",
   166  			expectError:  false,
   167  			fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
   168  		},
   169  		{
   170  			title:                    "valid template",
   171  			expectError:              false,
   172  			fqdnTemplate:             "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
   173  			combineFQDNAndAnnotation: true,
   174  		},
   175  		{
   176  			title:            "non-empty annotation filter label",
   177  			expectError:      false,
   178  			annotationFilter: "contour.heptio.com/ingress.class=contour",
   179  		},
   180  	} {
   181  		ti := ti
   182  		t.Run(ti.title, func(t *testing.T) {
   183  			t.Parallel()
   184  
   185  			fakeDynamicClient, _ := newDynamicKubernetesClient()
   186  
   187  			_, err := NewContourHTTPProxySource(
   188  				context.TODO(),
   189  				fakeDynamicClient,
   190  				"",
   191  				ti.annotationFilter,
   192  				ti.fqdnTemplate,
   193  				ti.combineFQDNAndAnnotation,
   194  				false,
   195  			)
   196  			if ti.expectError {
   197  				assert.Error(t, err)
   198  			} else {
   199  				assert.NoError(t, err)
   200  			}
   201  		})
   202  	}
   203  }
   204  
   205  func testEndpointsFromHTTPProxy(t *testing.T) {
   206  	t.Parallel()
   207  
   208  	for _, ti := range []struct {
   209  		title     string
   210  		httpProxy fakeHTTPProxy
   211  		expected  []*endpoint.Endpoint
   212  	}{
   213  		{
   214  			title: "one rule.host one lb.hostname",
   215  			httpProxy: fakeHTTPProxy{
   216  				host: "foo.bar", // Kubernetes requires removal of trailing dot
   217  				loadBalancer: fakeLoadBalancerService{
   218  					hostnames: []string{"lb.com"}, // Kubernetes omits the trailing dot
   219  				},
   220  			},
   221  			expected: []*endpoint.Endpoint{
   222  				{
   223  					DNSName:    "foo.bar",
   224  					RecordType: endpoint.RecordTypeCNAME,
   225  					Targets:    endpoint.Targets{"lb.com"},
   226  				},
   227  			},
   228  		},
   229  		{
   230  			title: "one rule.host one lb.IP",
   231  			httpProxy: fakeHTTPProxy{
   232  				host: "foo.bar",
   233  				loadBalancer: fakeLoadBalancerService{
   234  					ips: []string{"8.8.8.8"},
   235  				},
   236  			},
   237  			expected: []*endpoint.Endpoint{
   238  				{
   239  					DNSName:    "foo.bar",
   240  					RecordType: endpoint.RecordTypeA,
   241  					Targets:    endpoint.Targets{"8.8.8.8"},
   242  				},
   243  			},
   244  		},
   245  		{
   246  			title: "one rule.host two lb.IP and two lb.Hostname",
   247  			httpProxy: fakeHTTPProxy{
   248  				host: "foo.bar",
   249  				loadBalancer: fakeLoadBalancerService{
   250  					ips:       []string{"8.8.8.8", "127.0.0.1"},
   251  					hostnames: []string{"elb.com", "alb.com"},
   252  				},
   253  			},
   254  			expected: []*endpoint.Endpoint{
   255  				{
   256  					DNSName:    "foo.bar",
   257  					RecordType: endpoint.RecordTypeA,
   258  					Targets:    endpoint.Targets{"8.8.8.8", "127.0.0.1"},
   259  				},
   260  				{
   261  					DNSName:    "foo.bar",
   262  					RecordType: endpoint.RecordTypeCNAME,
   263  					Targets:    endpoint.Targets{"elb.com", "alb.com"},
   264  				},
   265  			},
   266  		},
   267  		{
   268  			title:     "no rule.host",
   269  			httpProxy: fakeHTTPProxy{},
   270  			expected:  []*endpoint.Endpoint{},
   271  		},
   272  		{
   273  			title:     "no targets",
   274  			httpProxy: fakeHTTPProxy{},
   275  			expected:  []*endpoint.Endpoint{},
   276  		},
   277  		{
   278  			title: "delegate httpproxy",
   279  			httpProxy: fakeHTTPProxy{
   280  				delegate: true,
   281  			},
   282  			expected: []*endpoint.Endpoint{},
   283  		},
   284  	} {
   285  		ti := ti
   286  		t.Run(ti.title, func(t *testing.T) {
   287  			t.Parallel()
   288  
   289  			if source, err := newTestHTTPProxySource(); err != nil {
   290  				require.NoError(t, err)
   291  			} else if endpoints, err := source.endpointsFromHTTPProxy(ti.httpProxy.HTTPProxy()); err != nil {
   292  				require.NoError(t, err)
   293  			} else {
   294  				validateEndpoints(t, endpoints, ti.expected)
   295  			}
   296  		})
   297  	}
   298  }
   299  
   300  func testHTTPProxyEndpoints(t *testing.T) {
   301  	t.Parallel()
   302  
   303  	namespace := "testing"
   304  	for _, ti := range []struct {
   305  		title                    string
   306  		targetNamespace          string
   307  		annotationFilter         string
   308  		loadBalancer             fakeLoadBalancerService
   309  		httpProxyItems           []fakeHTTPProxy
   310  		expected                 []*endpoint.Endpoint
   311  		expectError              bool
   312  		fqdnTemplate             string
   313  		combineFQDNAndAnnotation bool
   314  		ignoreHostnameAnnotation bool
   315  	}{
   316  		{
   317  			title:           "no httpproxy",
   318  			targetNamespace: "",
   319  		},
   320  		{
   321  			title:           "two simple httpproxys",
   322  			targetNamespace: "",
   323  			loadBalancer: fakeLoadBalancerService{
   324  				ips:       []string{"8.8.8.8"},
   325  				hostnames: []string{"lb.com"},
   326  			},
   327  			httpProxyItems: []fakeHTTPProxy{
   328  				{
   329  					name:      "fake1",
   330  					namespace: namespace,
   331  					host:      "example.org",
   332  				},
   333  				{
   334  					name:      "fake2",
   335  					namespace: namespace,
   336  					host:      "new.org",
   337  				},
   338  			},
   339  			expected: []*endpoint.Endpoint{
   340  				{
   341  					DNSName:    "example.org",
   342  					RecordType: endpoint.RecordTypeA,
   343  					Targets:    endpoint.Targets{"8.8.8.8"},
   344  				},
   345  				{
   346  					DNSName:    "example.org",
   347  					RecordType: endpoint.RecordTypeCNAME,
   348  					Targets:    endpoint.Targets{"lb.com"},
   349  				},
   350  				{
   351  					DNSName:    "new.org",
   352  					RecordType: endpoint.RecordTypeA,
   353  					Targets:    endpoint.Targets{"8.8.8.8"},
   354  				},
   355  				{
   356  					DNSName:    "new.org",
   357  					RecordType: endpoint.RecordTypeCNAME,
   358  					Targets:    endpoint.Targets{"lb.com"},
   359  				},
   360  			},
   361  		},
   362  		{
   363  			title:           "two simple httpproxys on different namespaces",
   364  			targetNamespace: "",
   365  			loadBalancer: fakeLoadBalancerService{
   366  				ips:       []string{"8.8.8.8"},
   367  				hostnames: []string{"lb.com"},
   368  			},
   369  			httpProxyItems: []fakeHTTPProxy{
   370  				{
   371  					name:      "fake1",
   372  					namespace: "testing1",
   373  					host:      "example.org",
   374  				},
   375  				{
   376  					name:      "fake2",
   377  					namespace: "testing2",
   378  					host:      "new.org",
   379  				},
   380  			},
   381  			expected: []*endpoint.Endpoint{
   382  				{
   383  					DNSName:    "example.org",
   384  					RecordType: endpoint.RecordTypeA,
   385  					Targets:    endpoint.Targets{"8.8.8.8"},
   386  				},
   387  				{
   388  					DNSName:    "example.org",
   389  					RecordType: endpoint.RecordTypeCNAME,
   390  					Targets:    endpoint.Targets{"lb.com"},
   391  				},
   392  				{
   393  					DNSName:    "new.org",
   394  					RecordType: endpoint.RecordTypeA,
   395  					Targets:    endpoint.Targets{"8.8.8.8"},
   396  				},
   397  				{
   398  					DNSName:    "new.org",
   399  					RecordType: endpoint.RecordTypeCNAME,
   400  					Targets:    endpoint.Targets{"lb.com"},
   401  				},
   402  			},
   403  		},
   404  		{
   405  			title:           "two simple httpproxys on different namespaces and a target namespace",
   406  			targetNamespace: "testing1",
   407  			loadBalancer: fakeLoadBalancerService{
   408  				ips:       []string{"8.8.8.8"},
   409  				hostnames: []string{"lb.com"},
   410  			},
   411  			httpProxyItems: []fakeHTTPProxy{
   412  				{
   413  					name:      "fake1",
   414  					namespace: "testing1",
   415  					host:      "example.org",
   416  				},
   417  				{
   418  					name:      "fake2",
   419  					namespace: "testing2",
   420  					host:      "new.org",
   421  				},
   422  			},
   423  			expected: []*endpoint.Endpoint{
   424  				{
   425  					DNSName:    "example.org",
   426  					RecordType: endpoint.RecordTypeA,
   427  					Targets:    endpoint.Targets{"8.8.8.8"},
   428  				},
   429  				{
   430  					DNSName:    "example.org",
   431  					RecordType: endpoint.RecordTypeCNAME,
   432  					Targets:    endpoint.Targets{"lb.com"},
   433  				},
   434  			},
   435  		},
   436  		{
   437  			title:            "valid matching annotation filter expression",
   438  			targetNamespace:  "",
   439  			annotationFilter: "contour.heptio.com/ingress.class in (alb, contour)",
   440  			loadBalancer: fakeLoadBalancerService{
   441  				ips: []string{"8.8.8.8"},
   442  			},
   443  			httpProxyItems: []fakeHTTPProxy{
   444  				{
   445  					name:      "fake1",
   446  					namespace: namespace,
   447  					annotations: map[string]string{
   448  						"contour.heptio.com/ingress.class": "contour",
   449  					},
   450  					host: "example.org",
   451  				},
   452  			},
   453  			expected: []*endpoint.Endpoint{
   454  				{
   455  					DNSName:    "example.org",
   456  					RecordType: endpoint.RecordTypeA,
   457  					Targets:    endpoint.Targets{"8.8.8.8"},
   458  				},
   459  			},
   460  		},
   461  		{
   462  			title:            "valid non-matching annotation filter expression",
   463  			targetNamespace:  "",
   464  			annotationFilter: "contour.heptio.com/ingress.class in (alb, contour)",
   465  			loadBalancer: fakeLoadBalancerService{
   466  				ips: []string{"8.8.8.8"},
   467  			},
   468  			httpProxyItems: []fakeHTTPProxy{
   469  				{
   470  					name:      "fake1",
   471  					namespace: namespace,
   472  					annotations: map[string]string{
   473  						"contour.heptio.com/ingress.class": "tectonic",
   474  					},
   475  					host: "example.org",
   476  				},
   477  			},
   478  			expected: []*endpoint.Endpoint{},
   479  		},
   480  		{
   481  			title:            "invalid annotation filter expression",
   482  			targetNamespace:  "",
   483  			annotationFilter: "contour.heptio.com/ingress.name in (a b)",
   484  			loadBalancer: fakeLoadBalancerService{
   485  				ips: []string{"8.8.8.8"},
   486  			},
   487  			httpProxyItems: []fakeHTTPProxy{
   488  				{
   489  					name:      "fake1",
   490  					namespace: namespace,
   491  					annotations: map[string]string{
   492  						"contour.heptio.com/ingress.class": "alb",
   493  					},
   494  					host: "example.org",
   495  				},
   496  			},
   497  			expected:    []*endpoint.Endpoint{},
   498  			expectError: true,
   499  		},
   500  		{
   501  			title:            "valid matching annotation filter label",
   502  			targetNamespace:  "",
   503  			annotationFilter: "contour.heptio.com/ingress.class=contour",
   504  			loadBalancer: fakeLoadBalancerService{
   505  				ips: []string{"8.8.8.8"},
   506  			},
   507  			httpProxyItems: []fakeHTTPProxy{
   508  				{
   509  					name:      "fake1",
   510  					namespace: namespace,
   511  					annotations: map[string]string{
   512  						"contour.heptio.com/ingress.class": "contour",
   513  					},
   514  					host: "example.org",
   515  				},
   516  			},
   517  			expected: []*endpoint.Endpoint{
   518  				{
   519  					DNSName:    "example.org",
   520  					RecordType: endpoint.RecordTypeA,
   521  					Targets:    endpoint.Targets{"8.8.8.8"},
   522  				},
   523  			},
   524  		},
   525  		{
   526  			title:            "valid non-matching annotation filter label",
   527  			targetNamespace:  "",
   528  			annotationFilter: "contour.heptio.com/ingress.class=contour",
   529  			loadBalancer: fakeLoadBalancerService{
   530  				ips: []string{"8.8.8.8"},
   531  			},
   532  			httpProxyItems: []fakeHTTPProxy{
   533  				{
   534  					name:      "fake1",
   535  					namespace: namespace,
   536  					annotations: map[string]string{
   537  						"contour.heptio.com/ingress.class": "alb",
   538  					},
   539  					host: "example.org",
   540  				},
   541  			},
   542  			expected: []*endpoint.Endpoint{},
   543  		},
   544  		{
   545  			title:           "our controller type is dns-controller",
   546  			targetNamespace: "",
   547  			loadBalancer: fakeLoadBalancerService{
   548  				ips: []string{"8.8.8.8"},
   549  			},
   550  			httpProxyItems: []fakeHTTPProxy{
   551  				{
   552  					name:      "fake1",
   553  					namespace: namespace,
   554  					annotations: map[string]string{
   555  						controllerAnnotationKey: controllerAnnotationValue,
   556  					},
   557  					host: "example.org",
   558  				},
   559  			},
   560  			expected: []*endpoint.Endpoint{
   561  				{
   562  					DNSName:    "example.org",
   563  					RecordType: endpoint.RecordTypeA,
   564  					Targets:    endpoint.Targets{"8.8.8.8"},
   565  				},
   566  			},
   567  		},
   568  		{
   569  			title:           "different controller types are ignored",
   570  			targetNamespace: "",
   571  			loadBalancer: fakeLoadBalancerService{
   572  				ips: []string{"8.8.8.8"},
   573  			},
   574  			httpProxyItems: []fakeHTTPProxy{
   575  				{
   576  					name:      "fake1",
   577  					namespace: namespace,
   578  					annotations: map[string]string{
   579  						controllerAnnotationKey: "some-other-tool",
   580  					},
   581  					host: "example.org",
   582  				},
   583  			},
   584  			expected: []*endpoint.Endpoint{},
   585  		},
   586  		{
   587  			title:           "template for httpproxy if host is missing",
   588  			targetNamespace: "",
   589  			loadBalancer: fakeLoadBalancerService{
   590  				ips:       []string{"8.8.8.8"},
   591  				hostnames: []string{"elb.com"},
   592  			},
   593  			httpProxyItems: []fakeHTTPProxy{
   594  				{
   595  					name:      "fake1",
   596  					namespace: namespace,
   597  					annotations: map[string]string{
   598  						controllerAnnotationKey: controllerAnnotationValue,
   599  					},
   600  					host: "",
   601  				},
   602  			},
   603  			expected: []*endpoint.Endpoint{
   604  				{
   605  					DNSName:    "fake1.ext-dns.test.com",
   606  					RecordType: endpoint.RecordTypeA,
   607  					Targets:    endpoint.Targets{"8.8.8.8"},
   608  				},
   609  				{
   610  					DNSName:    "fake1.ext-dns.test.com",
   611  					RecordType: endpoint.RecordTypeCNAME,
   612  					Targets:    endpoint.Targets{"elb.com"},
   613  				},
   614  			},
   615  			fqdnTemplate: "{{.Name}}.ext-dns.test.com",
   616  		},
   617  		{
   618  			title:           "another controller annotation skipped even with template",
   619  			targetNamespace: "",
   620  			loadBalancer: fakeLoadBalancerService{
   621  				ips: []string{"8.8.8.8"},
   622  			},
   623  			httpProxyItems: []fakeHTTPProxy{
   624  				{
   625  					name:      "fake1",
   626  					namespace: namespace,
   627  					annotations: map[string]string{
   628  						controllerAnnotationKey: "other-controller",
   629  					},
   630  					host: "",
   631  				},
   632  			},
   633  			expected:     []*endpoint.Endpoint{},
   634  			fqdnTemplate: "{{.Name}}.ext-dns.test.com",
   635  		},
   636  		{
   637  			title:           "multiple FQDN template hostnames",
   638  			targetNamespace: "",
   639  			loadBalancer: fakeLoadBalancerService{
   640  				ips: []string{"8.8.8.8"},
   641  			},
   642  			httpProxyItems: []fakeHTTPProxy{
   643  				{
   644  					name:        "fake1",
   645  					namespace:   namespace,
   646  					annotations: map[string]string{},
   647  					host:        "",
   648  				},
   649  			},
   650  			expected: []*endpoint.Endpoint{
   651  				{
   652  					DNSName:    "fake1.ext-dns.test.com",
   653  					Targets:    endpoint.Targets{"8.8.8.8"},
   654  					RecordType: endpoint.RecordTypeA,
   655  				},
   656  				{
   657  					DNSName:    "fake1.ext-dna.test.com",
   658  					Targets:    endpoint.Targets{"8.8.8.8"},
   659  					RecordType: endpoint.RecordTypeA,
   660  				},
   661  			},
   662  			fqdnTemplate: "{{.Name}}.ext-dns.test.com, {{.Name}}.ext-dna.test.com",
   663  		},
   664  		{
   665  			title:           "multiple FQDN template hostnames",
   666  			targetNamespace: "",
   667  			loadBalancer: fakeLoadBalancerService{
   668  				ips: []string{"8.8.8.8"},
   669  			},
   670  			httpProxyItems: []fakeHTTPProxy{
   671  				{
   672  					name:        "fake1",
   673  					namespace:   namespace,
   674  					annotations: map[string]string{},
   675  					host:        "",
   676  				},
   677  				{
   678  					name:      "fake2",
   679  					namespace: namespace,
   680  					annotations: map[string]string{
   681  						targetAnnotationKey: "httpproxy-target.com",
   682  					},
   683  					host: "example.org",
   684  				},
   685  			},
   686  			expected: []*endpoint.Endpoint{
   687  				{
   688  					DNSName:    "fake1.ext-dns.test.com",
   689  					Targets:    endpoint.Targets{"8.8.8.8"},
   690  					RecordType: endpoint.RecordTypeA,
   691  				},
   692  				{
   693  					DNSName:    "fake1.ext-dna.test.com",
   694  					Targets:    endpoint.Targets{"8.8.8.8"},
   695  					RecordType: endpoint.RecordTypeA,
   696  				},
   697  				{
   698  					DNSName:    "example.org",
   699  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   700  					RecordType: endpoint.RecordTypeCNAME,
   701  				},
   702  				{
   703  					DNSName:    "fake2.ext-dns.test.com",
   704  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   705  					RecordType: endpoint.RecordTypeCNAME,
   706  				},
   707  				{
   708  					DNSName:    "fake2.ext-dna.test.com",
   709  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   710  					RecordType: endpoint.RecordTypeCNAME,
   711  				},
   712  			},
   713  			fqdnTemplate:             "{{.Name}}.ext-dns.test.com, {{.Name}}.ext-dna.test.com",
   714  			combineFQDNAndAnnotation: true,
   715  		},
   716  		{
   717  			title:           "httpproxy rules with annotation",
   718  			targetNamespace: "",
   719  			loadBalancer: fakeLoadBalancerService{
   720  				ips: []string{"8.8.8.8"},
   721  			},
   722  			httpProxyItems: []fakeHTTPProxy{
   723  				{
   724  					name:      "fake1",
   725  					namespace: namespace,
   726  					annotations: map[string]string{
   727  						targetAnnotationKey: "httpproxy-target.com",
   728  					},
   729  					host: "example.org",
   730  				},
   731  				{
   732  					name:      "fake2",
   733  					namespace: namespace,
   734  					annotations: map[string]string{
   735  						targetAnnotationKey: "httpproxy-target.com",
   736  					},
   737  					host: "example2.org",
   738  				},
   739  				{
   740  					name:      "fake3",
   741  					namespace: namespace,
   742  					annotations: map[string]string{
   743  						targetAnnotationKey: "1.2.3.4",
   744  					},
   745  					host: "example3.org",
   746  				},
   747  			},
   748  			expected: []*endpoint.Endpoint{
   749  				{
   750  					DNSName:    "example.org",
   751  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   752  					RecordType: endpoint.RecordTypeCNAME,
   753  				},
   754  				{
   755  					DNSName:    "example2.org",
   756  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   757  					RecordType: endpoint.RecordTypeCNAME,
   758  				},
   759  				{
   760  					DNSName:    "example3.org",
   761  					Targets:    endpoint.Targets{"1.2.3.4"},
   762  					RecordType: endpoint.RecordTypeA,
   763  				},
   764  			},
   765  		},
   766  		{
   767  			title:           "httpproxy rules with hostname annotation",
   768  			targetNamespace: "",
   769  			loadBalancer: fakeLoadBalancerService{
   770  				ips: []string{"1.2.3.4"},
   771  			},
   772  			httpProxyItems: []fakeHTTPProxy{
   773  				{
   774  					name:      "fake1",
   775  					namespace: namespace,
   776  					annotations: map[string]string{
   777  						hostnameAnnotationKey: "dns-through-hostname.com",
   778  					},
   779  					host: "example.org",
   780  				},
   781  			},
   782  			expected: []*endpoint.Endpoint{
   783  				{
   784  					DNSName:    "example.org",
   785  					Targets:    endpoint.Targets{"1.2.3.4"},
   786  					RecordType: endpoint.RecordTypeA,
   787  				},
   788  				{
   789  					DNSName:    "dns-through-hostname.com",
   790  					Targets:    endpoint.Targets{"1.2.3.4"},
   791  					RecordType: endpoint.RecordTypeA,
   792  				},
   793  			},
   794  		},
   795  		{
   796  			title:           "httpproxy rules with hostname annotation having multiple hostnames",
   797  			targetNamespace: "",
   798  			loadBalancer: fakeLoadBalancerService{
   799  				ips: []string{"1.2.3.4"},
   800  			},
   801  			httpProxyItems: []fakeHTTPProxy{
   802  				{
   803  					name:      "fake1",
   804  					namespace: namespace,
   805  					annotations: map[string]string{
   806  						hostnameAnnotationKey: "dns-through-hostname.com, another-dns-through-hostname.com",
   807  					},
   808  					host: "example.org",
   809  				},
   810  			},
   811  			expected: []*endpoint.Endpoint{
   812  				{
   813  					DNSName:    "example.org",
   814  					Targets:    endpoint.Targets{"1.2.3.4"},
   815  					RecordType: endpoint.RecordTypeA,
   816  				},
   817  				{
   818  					DNSName:    "dns-through-hostname.com",
   819  					Targets:    endpoint.Targets{"1.2.3.4"},
   820  					RecordType: endpoint.RecordTypeA,
   821  				},
   822  				{
   823  					DNSName:    "another-dns-through-hostname.com",
   824  					Targets:    endpoint.Targets{"1.2.3.4"},
   825  					RecordType: endpoint.RecordTypeA,
   826  				},
   827  			},
   828  		},
   829  		{
   830  			title:           "httpproxy rules with hostname and target annotation",
   831  			targetNamespace: "",
   832  			loadBalancer: fakeLoadBalancerService{
   833  				ips: []string{},
   834  			},
   835  			httpProxyItems: []fakeHTTPProxy{
   836  				{
   837  					name:      "fake1",
   838  					namespace: namespace,
   839  					annotations: map[string]string{
   840  						hostnameAnnotationKey: "dns-through-hostname.com",
   841  						targetAnnotationKey:   "httpproxy-target.com",
   842  					},
   843  					host: "example.org",
   844  				},
   845  			},
   846  			expected: []*endpoint.Endpoint{
   847  				{
   848  					DNSName:    "example.org",
   849  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   850  					RecordType: endpoint.RecordTypeCNAME,
   851  				},
   852  				{
   853  					DNSName:    "dns-through-hostname.com",
   854  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   855  					RecordType: endpoint.RecordTypeCNAME,
   856  				},
   857  			},
   858  		},
   859  		{
   860  			title:           "httpproxy rules with annotation and custom TTL",
   861  			targetNamespace: "",
   862  			loadBalancer: fakeLoadBalancerService{
   863  				ips: []string{"8.8.8.8"},
   864  			},
   865  			httpProxyItems: []fakeHTTPProxy{
   866  				{
   867  					name:      "fake1",
   868  					namespace: namespace,
   869  					annotations: map[string]string{
   870  						targetAnnotationKey: "httpproxy-target.com",
   871  						ttlAnnotationKey:    "6",
   872  					},
   873  					host: "example.org",
   874  				},
   875  				{
   876  					name:      "fake2",
   877  					namespace: namespace,
   878  					annotations: map[string]string{
   879  						targetAnnotationKey: "httpproxy-target.com",
   880  						ttlAnnotationKey:    "1",
   881  					},
   882  					host: "example2.org",
   883  				},
   884  				{
   885  					name:      "fake3",
   886  					namespace: namespace,
   887  					annotations: map[string]string{
   888  						targetAnnotationKey: "httpproxy-target.com",
   889  						ttlAnnotationKey:    "10s",
   890  					},
   891  					host: "example3.org",
   892  				},
   893  			},
   894  			expected: []*endpoint.Endpoint{
   895  				{
   896  					DNSName:    "example.org",
   897  					RecordType: endpoint.RecordTypeCNAME,
   898  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   899  					RecordTTL:  endpoint.TTL(6),
   900  				},
   901  				{
   902  					DNSName:    "example2.org",
   903  					RecordType: endpoint.RecordTypeCNAME,
   904  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   905  					RecordTTL:  endpoint.TTL(1),
   906  				},
   907  				{
   908  					DNSName:    "example3.org",
   909  					RecordType: endpoint.RecordTypeCNAME,
   910  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   911  					RecordTTL:  endpoint.TTL(10),
   912  				},
   913  			},
   914  		},
   915  		{
   916  			title:           "template for httpproxy with annotation",
   917  			targetNamespace: "",
   918  			loadBalancer: fakeLoadBalancerService{
   919  				ips:       []string{},
   920  				hostnames: []string{},
   921  			},
   922  			httpProxyItems: []fakeHTTPProxy{
   923  				{
   924  					name:      "fake1",
   925  					namespace: namespace,
   926  					annotations: map[string]string{
   927  						targetAnnotationKey: "httpproxy-target.com",
   928  					},
   929  					host: "",
   930  				},
   931  				{
   932  					name:      "fake2",
   933  					namespace: namespace,
   934  					annotations: map[string]string{
   935  						targetAnnotationKey: "httpproxy-target.com",
   936  					},
   937  					host: "",
   938  				},
   939  				{
   940  					name:      "fake3",
   941  					namespace: namespace,
   942  					annotations: map[string]string{
   943  						targetAnnotationKey: "1.2.3.4",
   944  					},
   945  					host: "",
   946  				},
   947  			},
   948  			expected: []*endpoint.Endpoint{
   949  				{
   950  					DNSName:    "fake1.ext-dns.test.com",
   951  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   952  					RecordType: endpoint.RecordTypeCNAME,
   953  				},
   954  				{
   955  					DNSName:    "fake2.ext-dns.test.com",
   956  					Targets:    endpoint.Targets{"httpproxy-target.com"},
   957  					RecordType: endpoint.RecordTypeCNAME,
   958  				},
   959  				{
   960  					DNSName:    "fake3.ext-dns.test.com",
   961  					Targets:    endpoint.Targets{"1.2.3.4"},
   962  					RecordType: endpoint.RecordTypeA,
   963  				},
   964  			},
   965  			fqdnTemplate: "{{.Name}}.ext-dns.test.com",
   966  		},
   967  		{
   968  			title:           "httpproxy with empty annotation",
   969  			targetNamespace: "",
   970  			loadBalancer: fakeLoadBalancerService{
   971  				ips:       []string{},
   972  				hostnames: []string{},
   973  			},
   974  			httpProxyItems: []fakeHTTPProxy{
   975  				{
   976  					name:      "fake1",
   977  					namespace: namespace,
   978  					annotations: map[string]string{
   979  						targetAnnotationKey: "",
   980  					},
   981  					host: "",
   982  				},
   983  			},
   984  			expected:     []*endpoint.Endpoint{},
   985  			fqdnTemplate: "{{.Name}}.ext-dns.test.com",
   986  		},
   987  		{
   988  			title:           "ignore hostname annotations",
   989  			targetNamespace: "",
   990  			loadBalancer: fakeLoadBalancerService{
   991  				ips:       []string{"8.8.8.8"},
   992  				hostnames: []string{"lb.com"},
   993  			},
   994  			httpProxyItems: []fakeHTTPProxy{
   995  				{
   996  					name:      "fake1",
   997  					namespace: namespace,
   998  					annotations: map[string]string{
   999  						hostnameAnnotationKey: "ignore.me",
  1000  					},
  1001  					host: "example.org",
  1002  				},
  1003  				{
  1004  					name:      "fake2",
  1005  					namespace: namespace,
  1006  					annotations: map[string]string{
  1007  						hostnameAnnotationKey: "ignore.me.too",
  1008  					},
  1009  					host: "new.org",
  1010  				},
  1011  			},
  1012  			expected: []*endpoint.Endpoint{
  1013  				{
  1014  					DNSName:    "example.org",
  1015  					RecordType: endpoint.RecordTypeA,
  1016  					Targets:    endpoint.Targets{"8.8.8.8"},
  1017  				},
  1018  				{
  1019  					DNSName:    "example.org",
  1020  					RecordType: endpoint.RecordTypeCNAME,
  1021  					Targets:    endpoint.Targets{"lb.com"},
  1022  				},
  1023  				{
  1024  					DNSName:    "new.org",
  1025  					RecordType: endpoint.RecordTypeA,
  1026  					Targets:    endpoint.Targets{"8.8.8.8"},
  1027  				},
  1028  				{
  1029  					DNSName:    "new.org",
  1030  					RecordType: endpoint.RecordTypeCNAME,
  1031  					Targets:    endpoint.Targets{"lb.com"},
  1032  				},
  1033  			},
  1034  			ignoreHostnameAnnotation: true,
  1035  		},
  1036  	} {
  1037  		ti := ti
  1038  		t.Run(ti.title, func(t *testing.T) {
  1039  			t.Parallel()
  1040  
  1041  			httpProxies := make([]*projectcontour.HTTPProxy, 0)
  1042  			for _, item := range ti.httpProxyItems {
  1043  				item.loadBalancer = ti.loadBalancer
  1044  				httpProxies = append(httpProxies, item.HTTPProxy())
  1045  			}
  1046  
  1047  			fakeDynamicClient, scheme := newDynamicKubernetesClient()
  1048  			for _, httpProxy := range httpProxies {
  1049  				converted, err := convertHTTPProxyToUnstructured(httpProxy, scheme)
  1050  				require.NoError(t, err)
  1051  				_, err = fakeDynamicClient.Resource(projectcontour.HTTPProxyGVR).Namespace(httpProxy.Namespace).Create(context.Background(), converted, metav1.CreateOptions{})
  1052  				require.NoError(t, err)
  1053  			}
  1054  
  1055  			httpProxySource, err := NewContourHTTPProxySource(
  1056  				context.TODO(),
  1057  				fakeDynamicClient,
  1058  				ti.targetNamespace,
  1059  				ti.annotationFilter,
  1060  				ti.fqdnTemplate,
  1061  				ti.combineFQDNAndAnnotation,
  1062  				ti.ignoreHostnameAnnotation,
  1063  			)
  1064  			require.NoError(t, err)
  1065  
  1066  			res, err := httpProxySource.Endpoints(context.Background())
  1067  			if ti.expectError {
  1068  				assert.Error(t, err)
  1069  			} else {
  1070  				assert.NoError(t, err)
  1071  			}
  1072  
  1073  			validateEndpoints(t, res, ti.expected)
  1074  		})
  1075  	}
  1076  }
  1077  
  1078  // httpproxy specific helper functions
  1079  func newTestHTTPProxySource() (*httpProxySource, error) {
  1080  	fakeDynamicClient, _ := newDynamicKubernetesClient()
  1081  
  1082  	src, err := NewContourHTTPProxySource(
  1083  		context.TODO(),
  1084  		fakeDynamicClient,
  1085  		"default",
  1086  		"",
  1087  		"{{.Name}}",
  1088  		false,
  1089  		false,
  1090  	)
  1091  	if err != nil {
  1092  		return nil, err
  1093  	}
  1094  
  1095  	irsrc, ok := src.(*httpProxySource)
  1096  	if !ok {
  1097  		return nil, errors.New("underlying source type was not httpproxy")
  1098  	}
  1099  
  1100  	return irsrc, nil
  1101  }
  1102  
  1103  type fakeHTTPProxy struct {
  1104  	namespace   string
  1105  	name        string
  1106  	annotations map[string]string
  1107  
  1108  	host         string
  1109  	delegate     bool
  1110  	loadBalancer fakeLoadBalancerService
  1111  }
  1112  
  1113  func (ir fakeHTTPProxy) HTTPProxy() *projectcontour.HTTPProxy {
  1114  	var spec projectcontour.HTTPProxySpec
  1115  	if ir.delegate {
  1116  		spec = projectcontour.HTTPProxySpec{}
  1117  	} else {
  1118  		spec = projectcontour.HTTPProxySpec{
  1119  			VirtualHost: &projectcontour.VirtualHost{
  1120  				Fqdn: ir.host,
  1121  			},
  1122  		}
  1123  	}
  1124  
  1125  	lb := v1.LoadBalancerStatus{
  1126  		Ingress: []v1.LoadBalancerIngress{},
  1127  	}
  1128  
  1129  	for _, ip := range ir.loadBalancer.ips {
  1130  		lb.Ingress = append(lb.Ingress, v1.LoadBalancerIngress{
  1131  			IP: ip,
  1132  		})
  1133  	}
  1134  	for _, hostname := range ir.loadBalancer.hostnames {
  1135  		lb.Ingress = append(lb.Ingress, v1.LoadBalancerIngress{
  1136  			Hostname: hostname,
  1137  		})
  1138  	}
  1139  
  1140  	httpProxy := &projectcontour.HTTPProxy{
  1141  		ObjectMeta: metav1.ObjectMeta{
  1142  			Namespace:   ir.namespace,
  1143  			Name:        ir.name,
  1144  			Annotations: ir.annotations,
  1145  		},
  1146  		Spec: spec,
  1147  		Status: projectcontour.HTTPProxyStatus{
  1148  			LoadBalancer:  lb,
  1149  		},
  1150  	}
  1151  
  1152  	return httpProxy
  1153  }