Skip to content

Commit

Permalink
Add support for multiple namespace selectors (#232)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
supreethrao authored Mar 12, 2021
1 parent 1a8aed9 commit d8dad57
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 46 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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))
Expand Down
18 changes: 10 additions & 8 deletions controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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",
Expand All @@ -1558,7 +1559,8 @@ func TestUpdateFailsWhenK8sClientReturnsNoNamespaceIngresses(t *testing.T) {
DefaultAllow: ingressDefaultAllow,
DefaultBackendTimeoutSeconds: backendTimeout,
Name: defaultIngressClass,
NamespaceSelector: namespaceSelector,
NamespaceSelectors: namespaceSelectors,
MatchAllNamespaceSelectors: false,
},
}

Expand All @@ -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()
Expand Down
22 changes: 14 additions & 8 deletions feed-ingress/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
}
26 changes: 14 additions & 12 deletions feed-ingress/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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))
Expand Down
61 changes: 52 additions & 9 deletions k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit d8dad57

Please sign in to comment.