Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1945: Adding support for regular expression values for the Interface Exclude parameter #1980

1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -23,6 +23,7 @@

# IDE files.
/.idea/
/.vscode/

# Patterns to match files from older releases (prevents lots of
# untracked files from showing up when switching from a maintenance branch.
Expand Down
52 changes: 44 additions & 8 deletions config/config_params.go
Expand Up @@ -33,11 +33,18 @@ import (
)

var (
IfaceListRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,15}(,[a-zA-Z0-9_-]{1,15})*$`)
AuthorityRegexp = regexp.MustCompile(`^[^:/]+:\d+$`)
HostnameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
StringRegexp = regexp.MustCompile(`^.*$`)
IfaceParamRegexp = regexp.MustCompile(`^[a-zA-Z0-9:._+-]{1,15}$`)
// RegexpIfaceElemRegexp matches an individual element in the overall interface list;
// assumes the value represents a regular expression and is marked by '/' at the start
// and end and cannot have spaces
RegexpIfaceElemRegexp = regexp.MustCompile(`^\/[^\s]+\/$`)
// NonRegexpIfaceElemRegexp matches an individual element in the overall interface list;
// assumes the value is between 1-15 chars long and only be alphanumeric or - or _
NonRegexpIfaceElemRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,15}$`)
IfaceListRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,15}(,[a-zA-Z0-9_-]{1,15})*$`)
AuthorityRegexp = regexp.MustCompile(`^[^:/]+:\d+$`)
HostnameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
StringRegexp = regexp.MustCompile(`^.*$`)
IfaceParamRegexp = regexp.MustCompile(`^[a-zA-Z0-9:._+-]{1,15}$`)
)

const (
Expand Down Expand Up @@ -143,7 +150,7 @@ type Config struct {
OpenstackRegion string `config:"region;;die-on-fail"`

InterfacePrefix string `config:"iface-list;cali;non-zero,die-on-fail"`
InterfaceExclude string `config:"iface-list;kube-ipvs0"`
InterfaceExclude string `config:"iface-list-regexp;kube-ipvs0;die-on-fail"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling this out, since it changes the default behaviour.

Added the die-on-fail option, because it seemed sensible for it to halt on failed validation (it's going to panic anyways during the Config.InterfaceExcludes() if any of the individual values do not compile).

But there might be a very good reason why die-on-fail was not there in the first place! Please let me know if that is the case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'm fully understanding here, but it feels wrong for this detail of behaviour to change. I think it's straightforward to avoid "it's going to panic anyways ... if any of the individual values do not compile" by using regexp.Compile in that code instead of regexp.MustCompile.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(die-on-fail will cause Felix to go into a restart loop, so we generally specify die-on-fail only for essential config parameters)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's straightforward to avoid "it's going to panic anyways ... if any of the individual values do not compile" by using regexp.Compile in that code instead of regexp.MustCompile.

Right.

When I originally considered this I came to a conclusion that using regexp.Compile to avoid a panic would require knowing what we want to do when one of the user provided values fails to compile to regexp. What should we do in that case?

We can log an error message as a bare minimum. But if we gracefully continue onwards, it means we don't setup that particular interface exclude value. I just wasn't sure from a user experience standpoint what our acceptable behaviour was. E.g. if I wanted to exclude all interfaces with a name starting with kube but I somehow mess up the syntax such that it doesn't compile. Ideally I would want felix to catch my mistake and halt during startup to tell me so that I can correct myself.

So that was my original intention.

(die-on-fail will cause Felix to go into a restart loop, so we generally specify die-on-fail only for essential config parameters)

Fair point. I will remove the die-on-fail and switch the compiling of values from regexp.MustCompile to regexp.Compile. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually looking at the logic for handling die-on-fail (DieOnParseFailure flag) after parsing a parameter it looks like it wipes the error and then sets the value to default, which for InterfaceExclude is kube-ipvs0. That seems undesirable (especially now that we could have regex values), no?

https://github.com/projectcalico/felix/blob/master/config/config_params.go#L335

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the behaviour for a parse error for a parameter without die-on-fail is to log a warning and then use the default value. I think that's what we should do here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sounds good. I thought it over again and you're right, there's no reason to change this behaviour from what it's always done. That's what users would expect since it's established.

Changing it back.


ChainInsertMode string `config:"oneof(insert,append);insert;non-zero,die-on-fail"`
DefaultEndpointToHostAction string `config:"oneof(DROP,RETURN,ACCEPT);DROP;non-zero,die-on-fail"`
Expand Down Expand Up @@ -244,8 +251,30 @@ func (c *Config) InterfacePrefixes() []string {
return strings.Split(c.InterfacePrefix, ",")
}

func (c *Config) InterfaceExcludes() []string {
return strings.Split(c.InterfaceExclude, ",")
// InterfaceExcludes returns a list of Regexp objects, each representing either an
// exact interface value match or a class of interface matches. This function will
// panic if any individual value cannot be parsed / compiled into a Regexp.
func (config *Config) InterfaceExcludes() []*regexp.Regexp {
log.Debugf("Compiling regexp values for InterfaceExclude %s \n", config.InterfaceExclude)
strValues := strings.Split(config.InterfaceExclude, ",")
regexpValues := []*regexp.Regexp{}

for _, strValue := range strValues {
// Ignore empty values
if len(strValue) == 0 {
continue
}
// Any value not in regexp format will be converted into equivalent regexp
// that tests for exact match
if !RegexpIfaceElemRegexp.Match([]byte(strValue)) {
convertedValue := fmt.Sprintf("^%s$", regexp.QuoteMeta(strValue))
regexpValues = append(regexpValues, regexp.MustCompile(convertedValue))
} else {
parsedValue := strValue[1 : len(strValue)-1]
regexpValues = append(regexpValues, regexp.MustCompile(parsedValue))
}
}
return regexpValues
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Steve, I'm afraid this still isn't how I was expecting - so I guess I've done a poor job of communicating that. I've posted a commit - which is a delta on top of your changes - at nelljerram@77a59b0, which I hope will show more clearly what I've been trying to express.

Could you take a look at that and let me know what you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @neiljerram I just took a look at your changes.

I see two main changes:

  • Making the parse do more than just validation (so it returns the slice of Regexp objects)
  • This makes InterfaceExcludes() redundant, so it's been removed

I don't think you did a poor job of communicating this as you had a comment about having the Parse return an array of the values instead of the raw (granted this version now returns a slice of Regexp, not just slice of strings). I remember making a comment that I wasn't 100% sure on how that change would affect other components that use felix (e.g. FelixConfig), specifically this change. I recall you said it doesn't affect them and I see what you mean now. The scope of changing that field from string to []*Regexp is still only internal to felix.

If my understanding is correct, your changes look fine to me, shall I just apply your delta on my branch?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so, and then I believe we can call this done!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied and pushed! Was just waiting for CI to pass before I added a comment here confirming.

Thanks very much for all the feedback @neiljerram! Much appreciated.

}

func (config *Config) OpenstackActive() bool {
Expand Down Expand Up @@ -510,6 +539,13 @@ func loadParams() {
case "iface-list":
param = &RegexpParam{Regexp: IfaceListRegexp,
Msg: "invalid Linux interface name"}
case "iface-list-regexp":
param = &RegexpPatternListParam{
NonRegexpElemRegexp: NonRegexpIfaceElemRegexp,
RegexpElemRegexp: RegexpIfaceElemRegexp,
Delimiter: ",",
Msg: "list contains invalid Linux interface name or regex pattern",
}
stevegaossou marked this conversation as resolved.
Show resolved Hide resolved
case "iface-param":
param = &RegexpParam{Regexp: IfaceParamRegexp,
Msg: "invalid Linux interface parameter"}
Expand Down
45 changes: 42 additions & 3 deletions config/config_params_test.go
Expand Up @@ -15,6 +15,8 @@
package config_test

import (
"regexp"

. "github.com/projectcalico/felix/config"

"net"
Expand All @@ -29,7 +31,7 @@ import (

log "github.com/sirupsen/logrus"

"github.com/projectcalico/libcalico-go/lib/apis/v3"
v3 "github.com/projectcalico/libcalico-go/lib/apis/v3"
"github.com/projectcalico/libcalico-go/lib/numorstring"
)

Expand Down Expand Up @@ -158,8 +160,15 @@ var _ = DescribeTable("Config parsing",

Entry("InterfacePrefix", "InterfacePrefix", "tap", "tap"),
Entry("InterfacePrefix list", "InterfacePrefix", "tap,cali", "tap,cali"),
Entry("InterfaceExclude", "InterfaceExclude", "kube-ipvs0", "kube-ipvs0"),
Entry("InterfaceExclude list", "InterfaceExclude", "kube-ipvs0,dummy", "kube-ipvs0,dummy"),

Entry("InterfaceExclude one value no regexp", "InterfaceExclude", "kube-ipvs0", "kube-ipvs0"),
Entry("InterfaceExclude list no regexp", "InterfaceExclude", "kube-ipvs0,dummy", "kube-ipvs0,dummy"),
Entry("InterfaceExclude one value regexp", "InterfaceExclude", "/kube-ipvs/", "/kube-ipvs/"),
Entry("InterfaceExclude list regexp", "InterfaceExclude", "kube-ipvs0,dummy,/^veth*$/", "kube-ipvs0,dummy,/^veth*$/"),
Entry("InterfaceExclude no regexp", "InterfaceExclude", "/^kube.*/,/veth/", "/^kube.*/,/veth/"),
Entry("InterfaceExclude list regexp empty", "InterfaceExclude", "kube,//", "kube-ipvs0", true), // empty regexp value
Entry("InterfaceExclude list regexp invalid comma", "InterfaceExclude", "/kube,/,dummy", "kube-ipvs0", true), // bad comma use
Entry("InterfaceExclude list regexp invalid symbol", "InterfaceExclude", `/^kube\K/`, "kube-ipvs0", true), // Invalid regexp

Entry("ChainInsertMode append", "ChainInsertMode", "append", "append"),
Entry("ChainInsertMode append", "ChainInsertMode", "Append", "append"),
Expand Down Expand Up @@ -457,3 +466,33 @@ var _ = DescribeTable("Config validation",
"OpenstackRegion": "my-region-has-a-very-long-and-extremely-interesting-name",
}, false),
)

var _ = DescribeTable("Config InterfaceExcludes",
func(excludeList string, expected []*regexp.Regexp) {
cfg := New()
cfg.InterfaceExclude = excludeList
regexps := cfg.InterfaceExcludes()
Expect(regexps).To(Equal(expected))
},

Entry("empty exclude list", "", []*regexp.Regexp{}),
Entry("non-regexp single value", "kube-ipvs0", []*regexp.Regexp{
regexp.MustCompile("^kube-ipvs0$"),
}),
Entry("non-regexp multiple values", "kube-ipvs0,veth1", []*regexp.Regexp{
regexp.MustCompile("^kube-ipvs0$"),
regexp.MustCompile("^veth1$"),
}),
Entry("regexp single value", "/^veth.*/", []*regexp.Regexp{
regexp.MustCompile("^veth.*"),
}),
Entry("regexp multiple values", "/veth/,/^kube.*/", []*regexp.Regexp{
regexp.MustCompile("veth"),
regexp.MustCompile("^kube.*"),
}),
Entry("both non-regexp and regexp values", "kube-ipvs0,/veth/,/^kube.*/", []*regexp.Regexp{
regexp.MustCompile("^kube-ipvs0$"),
regexp.MustCompile("veth"),
regexp.MustCompile("^kube.*"),
}),
)
31 changes: 31 additions & 0 deletions config/param_types.go
Expand Up @@ -174,6 +174,37 @@ func (p *RegexpParam) Parse(raw string) (result interface{}, err error) {
return
}

// RegexpPatternListParam differs from RegexpParam (above) in that it validates
// string values that are (themselves) regular expressions.
type RegexpPatternListParam struct {
Metadata
RegexpElemRegexp *regexp.Regexp
NonRegexpElemRegexp *regexp.Regexp
Delimiter string
Msg string
}

// Parse validates whether the given raw string contains a list of valid values.
// Validation is dictated by two regexp patterns: one for valid regular expression
// values, another for non-regular expressions.
func (p *RegexpPatternListParam) Parse(raw string) (interface{}, error) {
// Split into individual elements and validate each one
tokens := strings.Split(raw, p.Delimiter)
for _, t := range tokens {
if p.RegexpElemRegexp.Match([]byte(t)) {
// Need to remove the start and end symbols that wrap the actual regexp
regexpValue := t[1 : len(t)-1]
_, compileErr := regexp.Compile(regexpValue)
if compileErr != nil {
return nil, p.parseFailed(raw, p.Msg)
}
} else if !p.NonRegexpElemRegexp.Match([]byte(t)) {
return nil, p.parseFailed(raw, p.Msg)
}
}
return raw, nil
}
nelljerram marked this conversation as resolved.
Show resolved Hide resolved

type FileParam struct {
Metadata
MustExist bool
Expand Down
1 change: 1 addition & 0 deletions dataplane/linux/int_dataplane_test.go
Expand Up @@ -35,6 +35,7 @@ var _ = Describe("Constructor test", func() {

JustBeforeEach(func() {
configParams = config.New()
configParams.InterfaceExclude = "/^kube.*/,/veth/,eth2"
dpConfig = intdataplane.Config{
IfaceMonitorConfig: ifacemonitor.Config{
InterfaceExcludes: configParams.InterfaceExcludes(),
Expand Down
8 changes: 4 additions & 4 deletions ifacemonitor/iface_monitor.go
Expand Up @@ -15,6 +15,7 @@
package ifacemonitor

import (
"regexp"
"syscall"
"time"

Expand Down Expand Up @@ -45,7 +46,7 @@ type AddrStateCallback func(ifaceName string, addrs set.Set)

type Config struct {
// List of interface names that dataplane receives no callbacks from them.
InterfaceExcludes []string
InterfaceExcludes []*regexp.Regexp
}
type InterfaceMonitor struct {
Config
Expand Down Expand Up @@ -136,12 +137,11 @@ readLoop:
}

func (m *InterfaceMonitor) isExcludedInterface(ifName string) bool {
for _, name := range m.InterfaceExcludes {
if ifName == name {
for _, nameExp := range m.InterfaceExcludes {
if nameExp.Match([]byte(ifName)) {
nelljerram marked this conversation as resolved.
Show resolved Hide resolved
return true
}
}

return false
}

Expand Down
81 changes: 51 additions & 30 deletions ifacemonitor/ifacemonitor_test.go
Expand Up @@ -15,6 +15,8 @@
package ifacemonitor_test

import (
"fmt"
"regexp"
"strings"
"sync"
"syscall"
Expand Down Expand Up @@ -347,7 +349,12 @@ var _ = Describe("ifacemonitor", func() {
}
resyncC = make(chan time.Time)
config := ifacemonitor.Config{
InterfaceExcludes: []string{"kube-ipvs0"},
// Test the regexp ability of interface excludes
InterfaceExcludes: []*regexp.Regexp{
regexp.MustCompile("^kube-ipvs.*"),
regexp.MustCompile("^veth1$"),
regexp.MustCompile("dummy"),
},
}
im = ifacemonitor.NewWithStubs(config, nl, resyncC)

Expand All @@ -373,36 +380,50 @@ var _ = Describe("ifacemonitor", func() {
})

It("should skip netlink address updates for ipvs", func() {
// Should not receives any address callbacks.
nl.addLink("kube-ipvs0")
resyncC <- time.Time{}
dp.notExpectAddrStateCb()
dp.notExpectLinkStateCb()
nl.addAddr("kube-ipvs0", "10.100.0.1/32")
dp.notExpectAddrStateCb()

nl.changeLinkState("kube-ipvs0", "up")
dp.expectLinkStateCb("kube-ipvs0", ifacemonitor.StateUp)
nl.changeLinkState("kube-ipvs0", "down")
dp.expectLinkStateCb("kube-ipvs0", ifacemonitor.StateDown)

// Should notify down from up on deletion.
nl.changeLinkState("kube-ipvs0", "up")
dp.expectLinkStateCb("kube-ipvs0", ifacemonitor.StateUp)
nl.delLink("kube-ipvs0")
dp.notExpectAddrStateCb()
dp.expectLinkStateCb("kube-ipvs0", ifacemonitor.StateDown)

// Check it can be added again.
nl.addLink("kube-ipvs0")
resyncC <- time.Time{}
dp.notExpectAddrStateCb()
dp.notExpectLinkStateCb()
var netlinkUpdates = func(iface string) {
// Should not receive any address callbacks.
nl.addLink(iface)
resyncC <- time.Time{}
dp.notExpectAddrStateCb()
dp.notExpectLinkStateCb()
nl.addAddr(iface, "10.100.0.1/32")
dp.notExpectAddrStateCb()

nl.changeLinkState(iface, "up")
dp.expectLinkStateCb(iface, ifacemonitor.StateUp)
nl.changeLinkState(iface, "down")
dp.expectLinkStateCb(iface, ifacemonitor.StateDown)

// Should notify down from up on deletion.
nl.changeLinkState(iface, "up")
dp.expectLinkStateCb(iface, ifacemonitor.StateUp)
nl.delLink(iface)
dp.notExpectAddrStateCb()
dp.expectLinkStateCb(iface, ifacemonitor.StateDown)

// Check it can be added again.
nl.addLink(iface)
resyncC <- time.Time{}
dp.notExpectAddrStateCb()
dp.notExpectLinkStateCb()

// Clean it.
nl.delLink(iface)
dp.notExpectAddrStateCb()
dp.notExpectLinkStateCb()
}

// Repeat for 3 different interfaces (to test regexp of interface excludes)
for index := 0; index < 3; index++ {
interfaceName := fmt.Sprintf("kube-ipvs%d", index)
netlinkUpdates(interfaceName)
}

// Repeat test for second interface exclude entry
netlinkUpdates("veth1")

// Clean it.
nl.delLink("kube-ipvs0")
dp.notExpectAddrStateCb()
dp.notExpectLinkStateCb()
// Repeat test for third interface exclude entry
netlinkUpdates("0dummy1")
nelljerram marked this conversation as resolved.
Show resolved Hide resolved
})

It("should handle mainline netlink updates", func() {
Expand Down