diff --git a/CHANGELOG.md b/CHANGELOG.md index fa012529..90b00b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v4.3.0 +* Provide support for multiple labels for namespace selection + * Remove flag `--ingress-controller-namespace-selector` + * Add flag `--ingress-controller-namespace-selectors` - which can accept comma separated or repeated inputs + * Add flag `--match-all-namespace-selectors` - for how to match the above provided labels + # v4.2.0 * Add flag `set-real-ip-from-header` to specify the name of the request header for the [real ip module](http://nginx.org/en/docs/http/ngx_http_realip_module.html) to use * The name of the header will be used by the real ip module in the `set_real_ip_from` directive. diff --git a/README.md b/README.md index f5f3a473..454f6182 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,22 @@ They can be overridden by passing the following arguments during startup. A flag `set-real-ip-from-header` can be used to specify the name of the request header for the [real ip module](http://nginx.org/en/docs/http/ngx_http_realip_module.html) to use in the `set_real_ip_from` directive. The default value of this flag would be `X-Forwarded-For` +## Namespace selectors +Namespace selectors can be used for the feed-ingress instance to only process ingress definitions from only those namespaces which have labels matching the ones passed in the input. +The following 2 flags help facilitate this + +1. `ingress-controller-namespace-selectors` - This flag will either be a repeated or comma separated value of namespace labels. + +``` +Examples: + +1. --ingress-controller-namespace-selectors=app=some-app,team=some-team +2. --ingress-controller-namespace-selectors=app=some-app --ingress-controller-namespace-selectors=team=some-team +``` + +2. `match-all-namespace-selectors` - This flag is to determine how the above flags should be used for matching on the namespace labels. This would be false by default which would mean that a namespace matching any of the above labels will be picked. +If this flag is set, the namespace on which the ingress is defined should have all of the passed in labels. + ## Ingress status When using the [ELB](#elb), [NLB](#nlb) or [Merlin](#merlin) updaters, the ingress status will be updated with relevant load balancer information. This can then be used with other controllers such as `external-dns` which can set DNS for any diff --git a/controller/controller.go b/controller/controller.go index 15e6fd3f..55fdbf2d 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -75,9 +75,10 @@ type controller struct { started bool updatesHealth util.SafeError sync.Mutex - name string - includeClasslessIngresses bool - namespaceSelector *k8s.NamespaceSelector + name string + includeClasslessIngresses bool + namespaceSelectors []*k8s.NamespaceSelector + matchAllNamespaceSelectors bool } // Config for creating a new ingress controller. @@ -93,7 +94,8 @@ type Config struct { DefaultProxyBufferBlocks int Name string IncludeClasslessIngresses bool - NamespaceSelector *k8s.NamespaceSelector + NamespaceSelectors []*k8s.NamespaceSelector + MatchAllNamespaceSelectors bool } // New creates an ingress controller. @@ -111,7 +113,8 @@ func New(conf Config, stopCh chan struct{}) Controller { stopCh: stopCh, name: conf.Name, includeClasslessIngresses: conf.IncludeClasslessIngresses, - namespaceSelector: conf.NamespaceSelector, + namespaceSelectors: conf.NamespaceSelectors, + matchAllNamespaceSelectors: conf.MatchAllNamespaceSelectors, } } @@ -185,10 +188,10 @@ func (c *controller) updateIngresses() (err error) { // Get ingresses var ingresses []*v1beta1.Ingress - if c.namespaceSelector == nil { + if c.namespaceSelectors == nil { ingresses, err = c.client.GetAllIngresses() } else { - ingresses, err = c.client.GetIngresses(c.namespaceSelector) + ingresses, err = c.client.GetIngresses(c.namespaceSelectors, c.matchAllNamespaceSelectors) } log.Debugf("Found %d ingresses", len(ingresses)) diff --git a/controller/controller_test.go b/controller/controller_test.go index 0521f02d..426dc023 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -78,7 +78,7 @@ func createDefaultStubs() (*fakeUpdater, *fake.FakeClient) { namespaceWatcher, _ := createFakeWatcher() client.On("GetAllIngresses").Return([]*v1beta1.Ingress{}, nil) - client.On("GetIngresses", mock.Anything).Return([]*v1beta1.Ingress{}, nil) + client.On("GetIngresses", mock.Anything, mock.AnythingOfType("bool")).Return([]*v1beta1.Ingress{}, nil) client.On("GetServices").Return([]*v1.Service{}, nil) client.On("WatchIngresses").Return(ingressWatcher) client.On("WatchServices").Return(serviceWatcher) @@ -1161,7 +1161,7 @@ func TestUpdaterIsUpdatedForIngressClassSetToTestInIngressAndConfig(t *testing.T }) } -func TestNamespaceSelectorIsUsedToGetIngresses(t *testing.T) { +func TestNamespaceSelectorsIsUsedToGetIngresses(t *testing.T) { asserter := assert.New(t) client := new(fake.FakeClient) @@ -1171,7 +1171,8 @@ func TestNamespaceSelectorIsUsedToGetIngresses(t *testing.T) { DefaultAllow: ingressDefaultAllow, DefaultBackendTimeoutSeconds: backendTimeout, Name: defaultIngressClass, - NamespaceSelector: &k8s.NamespaceSelector{LabelName: "team", LabelValue: "theteam"}, + NamespaceSelectors: []*k8s.NamespaceSelector{{LabelName: "team", LabelValue: "theteam"}}, + MatchAllNamespaceSelectors: false, } config.KubernetesClient = client @@ -1183,8 +1184,8 @@ func TestNamespaceSelectorIsUsedToGetIngresses(t *testing.T) { updater.On("Stop").Return(nil) updater.On("Health").Return(nil) - // The purpose of this test is to ensure that the NamespaceSelector is passed to GetIngresses - client.On("GetIngresses", &k8s.NamespaceSelector{LabelName: "team", LabelValue: "theteam"}).Return([]*v1beta1.Ingress{}, nil) + // The purpose of this test is to ensure that the NamespaceSelectors is passed to GetIngresses + client.On("GetIngresses", config.NamespaceSelectors, config.MatchAllNamespaceSelectors).Return([]*v1beta1.Ingress{}, nil) ingressWatcher, ingressCh := createFakeWatcher() serviceWatcher, serviceCh := createFakeWatcher() @@ -1546,7 +1547,7 @@ func TestUpdateFailsWhenK8sClientReturnsNoIngresses(t *testing.T) { func TestUpdateFailsWhenK8sClientReturnsNoNamespaceIngresses(t *testing.T) { - namespaceSelector := &k8s.NamespaceSelector{LabelName: "team", LabelValue: "theteam"} + namespaceSelectors := []*k8s.NamespaceSelector{{LabelName: "team", LabelValue: "theteam"}} test := testSpec{ "ingress without rules definition", @@ -1558,7 +1559,8 @@ func TestUpdateFailsWhenK8sClientReturnsNoNamespaceIngresses(t *testing.T) { DefaultAllow: ingressDefaultAllow, DefaultBackendTimeoutSeconds: backendTimeout, Name: defaultIngressClass, - NamespaceSelector: namespaceSelector, + NamespaceSelectors: namespaceSelectors, + MatchAllNamespaceSelectors: false, }, } @@ -1579,7 +1581,7 @@ func TestUpdateFailsWhenK8sClientReturnsNoNamespaceIngresses(t *testing.T) { updater.On("Health").Return(nil) // This is the call we are testing (by returning an empty array of ingresses) - client.On("GetIngresses", namespaceSelector).Return([]*v1beta1.Ingress{}, nil) + client.On("GetIngresses", namespaceSelectors, false).Return([]*v1beta1.Ingress{}, nil) ingressWatcher, ingressCh := createFakeWatcher() serviceWatcher, serviceCh := createFakeWatcher() diff --git a/feed-ingress/cmd/common.go b/feed-ingress/cmd/common.go index fe083b71..34372790 100644 --- a/feed-ingress/cmd/common.go +++ b/feed-ingress/cmd/common.go @@ -37,10 +37,11 @@ func runCmd(appender appendIngressUpdaters) { log.Fatal("Unable to create ingress updaters: ", err) } - controllerConfig.NamespaceSelector, err = parseNamespaceSelector(namespaceSelector) + controllerConfig.NamespaceSelectors, err = parseNamespaceSelector(namespaceSelectors) if err != nil { - log.Fatalf("invalid format for --%s (%s)", ingressControllerNamespaceSelectorFlag, namespaceSelector) + log.Fatalf("invalid format for --%s (%s)", ingressControllerNamespaceSelectorsFlag, namespaceSelectors) } + controllerConfig.MatchAllNamespaceSelectors = matchAllNamespaceSelectors feedController := controller.New(controllerConfig, stopCh) @@ -91,14 +92,19 @@ func createPortsConfig(ingressPort int, ingressHTTPSPort int) []nginx.Port { return ports } -func parseNamespaceSelector(nameValueStr string) (*k8s.NamespaceSelector, error) { - if len(nameValueStr) == 0 { +func parseNamespaceSelector(nameValueStringSlice []string) ([]*k8s.NamespaceSelector, error) { + if len(nameValueStringSlice) == 0 { return nil, nil } - nameValue := strings.SplitN(nameValueStr, "=", 2) - if len(nameValue) != 2 { - log.Errorf("expecting name=value but was (%s)", nameValueStr) + var namespaceSelectors []*k8s.NamespaceSelector + for _, nameValueStr := range nameValueStringSlice { + nameValue := strings.SplitN(nameValueStr, "=", 2) + namespaceSelectors = append(namespaceSelectors, &k8s.NamespaceSelector{LabelName: nameValue[0], LabelValue: nameValue[1]}) + if len(nameValue) != 2 { + log.Errorf("expecting name=value but was (%s)", nameValueStringSlice) + } } - return &k8s.NamespaceSelector{LabelName: nameValue[0], LabelValue: nameValue[1]}, nil + + return namespaceSelectors, nil } diff --git a/feed-ingress/cmd/root.go b/feed-ingress/cmd/root.go index b1fe3a5c..a33f48b4 100644 --- a/feed-ingress/cmd/root.go +++ b/feed-ingress/cmd/root.go @@ -45,9 +45,10 @@ var ( nginxOpenTracingPluginPath string nginxOpenTracingConfigPath string - ingressClassName string - includeUnnamedIngresses bool - namespaceSelector string + ingressClassName string + includeUnnamedIngresses bool + namespaceSelectors []string + matchAllNamespaceSelectors bool pushgatewayURL string pushgatewayIntervalSeconds int @@ -93,17 +94,16 @@ const ( defaultLargeClientHeaderBufferBlocks = 4 defaultSetRealIPFromHeader = "X-Forwarded-For" - defaultIngressClassName = "" - defaultIncludeUnnamedIngresses = false - defaultIngressControllerNamespaceSelector = "" - + defaultIngressClassName = "" + defaultIncludeUnnamedIngresses = false defaultPushgatewayIntervalSeconds = 60 ) const ( - ingressClassFlag = "ingress-class" - includeClasslessIngressesFlag = "include-classless-ingresses" - ingressControllerNamespaceSelectorFlag = "ingress-controller-namespace-selector" + ingressClassFlag = "ingress-class" + includeClasslessIngressesFlag = "include-classless-ingresses" + ingressControllerNamespaceSelectorsFlag = "ingress-controller-namespace-selectors" + matchAllNamespaceSelectorFlags = "match-all-namespace-selectors" ingressClassAnnotation = "kubernetes.io/ingress.class" ) @@ -148,8 +148,10 @@ func configureGeneralFlags() { fmt.Sprintf("The name of this instance. It will consider only ingress resources with matching %s annotation values.", ingressClassAnnotation)) rootCmd.PersistentFlags().BoolVar(&includeUnnamedIngresses, includeClasslessIngressesFlag, defaultIncludeUnnamedIngresses, fmt.Sprintf("In addition to ingress resources with matching %s annotations, also consider those with no such annotation.", ingressClassAnnotation)) - rootCmd.PersistentFlags().StringVar(&namespaceSelector, ingressControllerNamespaceSelectorFlag, defaultIngressControllerNamespaceSelector, - "Only consider ingresses within namespaces having labels matching this selector (e.g. app=loadtest).") + rootCmd.PersistentFlags().StringSliceVar(&namespaceSelectors, ingressControllerNamespaceSelectorsFlag, []string{}, + "Only consider ingresses within namespaces having labels matching the selectors (e.g. app=loadtest).") + rootCmd.PersistentFlags().BoolVar(&matchAllNamespaceSelectors, matchAllNamespaceSelectorFlags, false, + fmt.Sprintf("Use only those namespaces containing all the labels passed in %s flag. Default is any i.e or match of labels", ingressControllerNamespaceSelectorsFlag)) _ = rootCmd.PersistentFlags().MarkDeprecated(includeClasslessIngressesFlag, fmt.Sprintf("please annotate ingress resources explicitly with %s", ingressClassAnnotation)) diff --git a/k8s/client.go b/k8s/client.go index 1089a60d..70628655 100644 --- a/k8s/client.go +++ b/k8s/client.go @@ -39,7 +39,7 @@ type Client interface { GetAllIngresses() ([]*v1beta1.Ingress, error) // GetIngresses returns ingresses in namespaces with matching labels - GetIngresses(*NamespaceSelector) ([]*v1beta1.Ingress, error) + GetIngresses([]*NamespaceSelector, bool) ([]*v1beta1.Ingress, error) // GetServices returns all the services in the cluster. GetServices() ([]*v1.Service, error) @@ -103,10 +103,10 @@ func New(kubeconfig string, resyncPeriod time.Duration, stopCh chan struct{}) (C } func (c *client) GetAllIngresses() ([]*v1beta1.Ingress, error) { - return c.GetIngresses(nil) + return c.GetIngresses(nil, false) } -func (c *client) GetIngresses(selector *NamespaceSelector) ([]*v1beta1.Ingress, error) { +func (c *client) GetIngresses(namespaceSelectors []*NamespaceSelector, matchAllNamespaceSelectors bool) ([]*v1beta1.Ingress, error) { if !c.namespaceController.HasSynced() { return nil, errors.New("namespaces haven't synced yet") } @@ -119,11 +119,11 @@ func (c *client) GetIngresses(selector *NamespaceSelector) ([]*v1beta1.Ingress, allIngresses = append(allIngresses, obj.(*v1beta1.Ingress)) } - if selector == nil { + if namespaceSelectors == nil { return allIngresses, nil } - supportedNamespaces := supportedNamespaces(selector, toNamespaces(c.namespaceStore.List())) + supportedNamespaces := supportedNamespaces(toNamespaces(c.namespaceStore.List()), namespaceSelectors, matchAllNamespaceSelectors) var filteredIngresses []*v1beta1.Ingress for _, ingress := range allIngresses { @@ -142,19 +142,51 @@ func toNamespaces(interfaces []interface{}) []*v1.Namespace { return namespaces } -func supportedNamespaces(selector *NamespaceSelector, namespaces []*v1.Namespace) []*v1.Namespace { - if selector == nil { +func supportedNamespaces(namespaces []*v1.Namespace, namespaceSelectors []*NamespaceSelector, matchAllNamespaceSelectors bool) []*v1.Namespace { + if namespaceSelectors == nil { return namespaces } + var filteredNamespaces []*v1.Namespace + + if matchAllNamespaceSelectors { + for _, namespace := range namespaces { + filteredNamespaces = safeAppend(filteredNamespaces, filterNamespacesMatchingAllLabels(namespace, namespaceSelectors)) + } + + log.Debugf("Found %d of %d namespaces that match the passed in namespace selectors", len(filteredNamespaces), len(namespaces)) + return filteredNamespaces + } + + for _, namespaceSelector := range namespaceSelectors { + filteredNamespaces = safeAppend(filteredNamespaces, filterNamespacesMatchingAnyLabel(namespaces, namespaceSelector)...) + } + return filteredNamespaces +} + +func filterNamespacesMatchingAllLabels(namespace *v1.Namespace, namespaceSelectors []*NamespaceSelector) *v1.Namespace { + allMatch := true + for _, namespaceSelector := range namespaceSelectors { + _, ok := namespace.Labels[namespaceSelector.LabelName] + allMatch = allMatch && ok + } + + if allMatch { + return namespace + } + return nil +} + +func filterNamespacesMatchingAnyLabel(namespaces []*v1.Namespace, namespaceSelector *NamespaceSelector) []*v1.Namespace { var filteredNamespaces []*v1.Namespace for _, namespace := range namespaces { - if val, ok := namespace.Labels[selector.LabelName]; ok && val == selector.LabelValue { + if val, ok := namespace.Labels[namespaceSelector.LabelName]; ok && val == namespaceSelector.LabelValue { filteredNamespaces = append(filteredNamespaces, namespace) } } + log.Debugf("Found %d of %d namespaces that match the selector %s=%s", - len(filteredNamespaces), len(namespaces), selector.LabelName, selector.LabelValue) + len(filteredNamespaces), len(namespaces), namespaceSelector.LabelName, namespaceSelector.LabelValue) return filteredNamespaces } @@ -291,3 +323,14 @@ func ingressStatusEqual(i1 []v1.LoadBalancerIngress, i2 []v1.LoadBalancerIngress return true } + +func safeAppend(arr []*v1.Namespace, elem ...*v1.Namespace) []*v1.Namespace { + if elem != nil { + for i := 0; i < len(elem); i++ { + if elem[i] != nil { + arr = append(arr, elem[i]) + } + } + } + return arr +} diff --git a/k8s/client_test.go b/k8s/client_test.go index 37c78c3f..f91a0702 100644 --- a/k8s/client_test.go +++ b/k8s/client_test.go @@ -337,6 +337,128 @@ var _ = Describe("Client", func() { }) }) + Describe("GetIngresses", func() { + var ( + fakesIngressStore *cache.FakeCustomStore + fakesNamespaceStore *cache.FakeCustomStore + fakesNamespaceController *fakeController + fakesIngressController *fakeController + clt *client + ) + + BeforeEach(func() { + fakesNamespaceController = &fakeController{} + fakesIngressController = &fakeController{} + fakesIngressStore = &cache.FakeCustomStore{} + fakesNamespaceStore = &cache.FakeCustomStore{} + clt = &client{ + namespaceController: fakesNamespaceController, + ingressController: fakesIngressController, + ingressStore: fakesIngressStore, + namespaceStore: fakesNamespaceStore, + } + fakesNamespaceController.On("HasSynced").Return(true) + fakesIngressController.On("HasSynced").Return(true) + }) + + It("should match all provided labels on ingress namespace", func() { + // given + namespaceSelectors := []*NamespaceSelector{ + { + LabelName: "some-label-name", + LabelValue: "some-value", + }, + { + LabelName: "team", + LabelValue: "some-team-name", + }, + } + fakesIngressStore.ListFunc = func() []interface{} { + ingresses := make([]interface{}, 2) + ingresses[0] = &v1beta1.Ingress{ObjectMeta: metav1.ObjectMeta{ + Namespace: "matching-namespace", + }} + ingresses[1] = &v1beta1.Ingress{ObjectMeta: metav1.ObjectMeta{ + Namespace: "non-matching-namespace", + }} + return ingresses + } + + fakesNamespaceStore.ListFunc = func() []interface{} { + namespaces := make([]interface{}, 2) + namespaces[0] = &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "matching-namespace", + Labels: map[string]string{ + "some-label-name": "some-value", + "team": "some-team-name", + }, + }} + namespaces[1] = &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "non-matching-namespace", + Labels: map[string]string{ + "team": "some-team-name", + }, + }} + return namespaces + } + + // when + ingresses, err := clt.GetIngresses(namespaceSelectors, true) + Expect(err).To(BeNil()) + Expect(len(ingresses)).To(Equal(1)) + Expect(ingresses[0].Namespace).To(Equal("matching-namespace")) + }) + + It("should match any provided labels on ingress namespace", func() { + // given + namespaceSelectors := []*NamespaceSelector{ + { + LabelName: "team", + LabelValue: "some-team-1", + }, + { + LabelName: "team", + LabelValue: "some-team-2", + }, + } + fakesIngressStore.ListFunc = func() []interface{} { + ingresses := make([]interface{}, 2) + ingresses[0] = &v1beta1.Ingress{ObjectMeta: metav1.ObjectMeta{ + Namespace: "matching-namespace-one", + }} + ingresses[1] = &v1beta1.Ingress{ObjectMeta: metav1.ObjectMeta{ + Namespace: "matching-namespace-two", + }} + return ingresses + } + + fakesNamespaceStore.ListFunc = func() []interface{} { + namespaces := make([]interface{}, 2) + namespaces[0] = &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "matching-namespace-one", + Labels: map[string]string{ + "some-label-name": "some-value", + "team": "some-team-1", + }, + }} + namespaces[1] = &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "matching-namespace-two", + Labels: map[string]string{ + "team": "some-team-2", + }, + }} + return namespaces + } + + // when + ingresses, err := clt.GetIngresses(namespaceSelectors, false) + Expect(err).To(BeNil()) + Expect(len(ingresses)).To(Equal(2)) + Expect(ingresses[0].Namespace).To(Equal("matching-namespace-one")) + Expect(ingresses[1].Namespace).To(Equal("matching-namespace-two")) + }) + }) + Describe("GetServices", func() { var ( fakesServiceStore *cache.FakeCustomStore diff --git a/util/test/mocks.go b/util/test/mocks.go index 3d0607e8..ed8a6cb8 100644 --- a/util/test/mocks.go +++ b/util/test/mocks.go @@ -19,8 +19,8 @@ func (c *FakeClient) GetAllIngresses() ([]*v1beta1.Ingress, error) { } // GetIngresses mocks out calls to GetIngresses -func (c *FakeClient) GetIngresses(selector *k8s.NamespaceSelector) ([]*v1beta1.Ingress, error) { - r := c.Called(selector) +func (c *FakeClient) GetIngresses(selectors []*k8s.NamespaceSelector, matchAllSelectors bool) ([]*v1beta1.Ingress, error) { + r := c.Called(selectors, matchAllSelectors) return r.Get(0).([]*v1beta1.Ingress), r.Error(1) }