From d45e5f110edb9c6021032ccfb3df84feab84d198 Mon Sep 17 00:00:00 2001 From: Noe Brown Date: Fri, 8 Jul 2022 12:56:03 -0400 Subject: [PATCH] Intentions module input configuration added --- config/intentions_service.go | 130 +++++++ config/module_input.go | 5 + config/module_input_intentions.go | 93 +++++ config/module_input_intentions_test.go | 277 ++++++++++++++ config/module_input_test.go | 134 ++++++- config/monitor_intentions.go | 152 ++++++++ config/monitor_intentions_test.go | 493 +++++++++++++++++++++++++ 7 files changed, 1280 insertions(+), 4 deletions(-) create mode 100644 config/intentions_service.go create mode 100644 config/module_input_intentions.go create mode 100644 config/module_input_intentions_test.go create mode 100644 config/monitor_intentions.go create mode 100644 config/monitor_intentions_test.go diff --git a/config/intentions_service.go b/config/intentions_service.go new file mode 100644 index 000000000..a6ce159e9 --- /dev/null +++ b/config/intentions_service.go @@ -0,0 +1,130 @@ +package config + +import ( + "fmt" + "regexp" +) + +// IntentionsServicesConfig configures regexp/list of names of services. +type IntentionsServicesConfig struct { + // Regexp configures the source services to monitor by matching on the service name. + // Either Regexp or Names must be configured, not both. + Regexp *string `mapstructure:"regexp"` + + // Names configures the services to monitor by listing the service name. + // Either Regexp or Names must be configured, not both. + Names []string `mapstructure:"names"` +} + +// Copy returns a deep copy of IntentionsServicesConfig +func (c *IntentionsServicesConfig) Copy() *IntentionsServicesConfig { + if c == nil { + return nil + } + + var o IntentionsServicesConfig + o.Regexp = StringCopy(c.Regexp) + + if c.Names != nil { + o.Names = make([]string, 0, len(c.Names)) + o.Names = append(o.Names, c.Names...) + } + return &o +} + +// Merge combines all values in this IntentionsServicesConfig with the values in the other +// configuration, with values in the other configuration taking precedence. +// Maps and slices are merged, most other values are overwritten. Complex +// structs define their own merge functionality. +func (c *IntentionsServicesConfig) Merge(o *IntentionsServicesConfig) *IntentionsServicesConfig { + if c == nil { + if o == nil { + return nil + } + return o.Copy() + } + + if o == nil { + return c.Copy() + } + + r := c.Copy() + + if o.Regexp != nil { + r.Regexp = StringCopy(o.Regexp) + } + + r.Names = mergeSlices(c.Names, o.Names) + + return r +} + +// Finalize ensures there no nil pointers. +// Exception: `Regexp` is never finalized and can potentially be nil. +func (c *IntentionsServicesConfig) Finalize() { + if c == nil { // config not required, return early + return + } + + if c.Names == nil { + c.Names = []string{} + } +} + +func (c *IntentionsServicesConfig) Validate() error { + if c == nil { // config not required, return early + return nil + } + + // Check that either regex or names is configured but not both + namesConfigured := c.Names != nil && len(c.Names) > 0 + regexConfigured := c.Regexp != nil + + if namesConfigured && regexConfigured { + return fmt.Errorf("regexp and names fields cannot both be " + + "configured. If both are needed, consider including the list of " + + "names as part of the regex or creating separate tasks") + } + if !namesConfigured && !regexConfigured { + return fmt.Errorf("either the regexp or names field must be configured") + } + + // Validate regex + if regexConfigured { + if _, err := regexp.Compile(StringVal(c.Regexp)); err != nil { + return fmt.Errorf("unable to compile intentions service regexp: %s", err) + } + } + + // Check that names does not contain empty strings + if namesConfigured { + for _, name := range c.Names { + if name == "" { + return fmt.Errorf("names field includes empty string(s). " + + "intentions service names cannot be empty") + } + } + } + + return nil +} + +func (c *IntentionsServicesConfig) GoString() string { + if c == nil { + return ": (*IntentionsServicesConfig)(nil)" + } + + if len(c.Names) > 0 { + return fmt.Sprintf( + "Names:%s", + c.Names, + ) + + } else { + return fmt.Sprintf( + "Regexp:%s", + StringVal(c.Regexp), + ) + + } +} diff --git a/config/module_input.go b/config/module_input.go index 48d77a617..c8da1ac9c 100644 --- a/config/module_input.go +++ b/config/module_input.go @@ -64,6 +64,11 @@ func moduleInputToTypeFunc() mapstructure.DecodeHookFunc { return decodeModuleInputToType(c, &config) } + if c, ok := moduleInputs[intentionsType]; ok { + var config IntentionsModuleInputConfig + return decodeModuleInputToType(c, &config) + } + return nil, fmt.Errorf("unsupported module_input type: %v", data) } } diff --git a/config/module_input_intentions.go b/config/module_input_intentions.go new file mode 100644 index 000000000..b78c544d3 --- /dev/null +++ b/config/module_input_intentions.go @@ -0,0 +1,93 @@ +package config + +import ( + "fmt" +) + +var _ ModuleInputConfig = (*IntentionsModuleInputConfig)(nil) + +// IntentionsModuleInputConfig configures a module_input configuration block of +// type 'intentions'. Data about the intentions monitored will be used as input +// for the module variables. +type IntentionsModuleInputConfig struct { + IntentionsMonitorConfig `mapstructure:",squash"` +} + +// Copy returns a deep copy of this configuration. +func (c *IntentionsModuleInputConfig) Copy() MonitorConfig { + if c == nil { + return nil + } + + conf, ok := c.IntentionsMonitorConfig.Copy().(*IntentionsMonitorConfig) + if !ok { + return nil + } + return &IntentionsModuleInputConfig{ + IntentionsMonitorConfig: *conf, + } +} + +// Merge combines all values in this configuration `c` with the values in the other +// configuration `o`, with values in the other configuration taking precedence. +// Maps and slices are merged, most other values are overwritten. Complex +// structs define their own merge functionality. +func (c *IntentionsModuleInputConfig) Merge(o MonitorConfig) MonitorConfig { + if c == nil { + if isModuleInputNil(o) { // o is interface, use isConditionNil() + return nil + } + return o.Copy() + } + + if isModuleInputNil(o) { + return c.Copy() + } + + o2, ok := o.(*IntentionsModuleInputConfig) + if !ok { + return nil + } + + merged, ok := c.IntentionsMonitorConfig.Merge(&o2.IntentionsMonitorConfig).(*IntentionsMonitorConfig) + if !ok { + return nil + } + + return &IntentionsModuleInputConfig{ + IntentionsMonitorConfig: *merged, + } +} + +// Finalize ensures there are no nil pointers. +func (c *IntentionsModuleInputConfig) Finalize() { + if c == nil { // config not required, return early + return + } + c.IntentionsMonitorConfig.Finalize() +} + +// Validate validates the values and required options. This method is recommended +// to run after Finalize() to ensure the configuration is safe to proceed. +func (c *IntentionsModuleInputConfig) Validate() error { + if c == nil { // config not required, return early + return nil + } + if err := c.IntentionsMonitorConfig.Validate(); err != nil { + return fmt.Errorf("error validating `module_input \"intentions\"`: %s", err) + } + return nil +} + +// GoString defines the printable version of this struct. +func (c *IntentionsModuleInputConfig) GoString() string { + if c == nil { + return "(*IntentionsModuleInputConfig)(nil)" + } + + return fmt.Sprintf("&IntentionsModuleInputConfig{"+ + "%s"+ + "}", + c.IntentionsMonitorConfig.GoString(), + ) +} diff --git a/config/module_input_intentions_test.go b/config/module_input_intentions_test.go new file mode 100644 index 000000000..8cba0fc0f --- /dev/null +++ b/config/module_input_intentions_test.go @@ -0,0 +1,277 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIntentionsModuleInputConfig_Copy(t *testing.T) { + t.Parallel() + + finalizedConf := &IntentionsModuleInputConfig{} + finalizedConf.Finalize() + + cases := []struct { + name string + a *IntentionsModuleInputConfig + }{ + { + "nil", + nil, + }, + { + "empty", + &IntentionsModuleInputConfig{}, + }, + { + "finalized", + finalizedConf, + }, + { + "happy_path", + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r := tc.a.Copy() + if tc.a == nil { + // returned nil interface has nil type, which is unequal to tc.a + assert.Nil(t, r) + } else { + assert.Equal(t, tc.a, r) + } + }) + } +} + +func TestIntentionsModuleInputConfig_Merge(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + a *IntentionsModuleInputConfig + b *IntentionsModuleInputConfig + r *IntentionsModuleInputConfig + }{ + { + "nil_a", + nil, + &IntentionsModuleInputConfig{}, + &IntentionsModuleInputConfig{}, + }, + { + "nil_b", + &IntentionsModuleInputConfig{}, + nil, + &IntentionsModuleInputConfig{}, + }, + { + "nil_both", + nil, + nil, + nil, + }, + { + "empty", + &IntentionsModuleInputConfig{}, + &IntentionsModuleInputConfig{}, + &IntentionsModuleInputConfig{}, + }, + { + "happy_path", + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String("datacenter_overidden"), + Namespace: nil, + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + }, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String("datacenter"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: nil, + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: nil, + }, + }, + }, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String("datacenter"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r := tc.a.Merge(tc.b) + if tc.r == nil { + // returned nil interface has nil type, which is unequal to tc.r + assert.Nil(t, r) + } else { + assert.Equal(t, tc.r, r) + } + }) + } +} + +func TestIntentionsModuleInputConfig_Finalize(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + i *IntentionsModuleInputConfig + r *IntentionsModuleInputConfig + }{ + { + "nil", + nil, + nil, + }, + { + "happy_path", + &IntentionsModuleInputConfig{}, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String(""), + Namespace: String(""), + SourceServices: (*IntentionsServicesConfig)(nil), + DestinationServices: (*IntentionsServicesConfig)(nil), + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.i.Finalize() + assert.Equal(t, tc.r, tc.i) + }) + } +} + +func TestIntentionsModuleInputConfig_Validate(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + expectErr bool + c *IntentionsModuleInputConfig + }{ + { + "valid", + false, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{ + Regexp: String(".*"), + }, + }, + }, + }, + { + "nil", + false, + nil, + }, + { + "invalid", + true, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{ + Regexp: String("*"), + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.c.Validate() + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestIntentionsModuleInputConfig_TestGoString(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + ssv *IntentionsModuleInputConfig + expected string + }{ + { + "configured services module_input", + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + }, + "&IntentionsModuleInputConfig{" + + "&IntentionsMonitorConfig{" + + "Datacenter:dc, " + + "Namespace:namespace, " + + "Source Services Regexp:^web.*, " + + "Destination Services Regexp:^api.*" + + "}" + + "}", + }, + { + "nil intentions module_input", + nil, + "(*IntentionsModuleInputConfig)(nil)", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actual := tc.ssv.GoString() + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/config/module_input_test.go b/config/module_input_test.go index 6eebabc7c..aecb03d04 100644 --- a/config/module_input_test.go +++ b/config/module_input_test.go @@ -42,6 +42,26 @@ task { recurse = true } }` + + testModuleInputIntentionsSuccess = ` +task { + name = "module_input_task" + module = "..." + condition "schedule" { + cron = "* * * * * * *" + } + module_input "intentions" { + datacenter = "dc2" + namespace = "ns2" + source_services { + regexp = "^web.*" + } + destination_services { + regexp = "^api.*" + } + } +}` + testModuleInputsSuccess = ` task { name = "module_input_task" @@ -55,6 +75,14 @@ task { module_input "consul-kv" { path = "my/path" } + module_input "intentions" { + source_services { + regexp = "^web.*" + } + destination_services { + regexp = "^api.*" + } + } }` // Errors @@ -84,6 +112,23 @@ task { recurse = true } }` + testModuleInputIntentionsUnsupportedFieldError = ` +task { + name = "module_input_task" + module = "..." + condition "schedule" { + cron = "* * * * * * *" + } + module_input "intentions" { + nonexistent_field = true + source_services { + regexp = "^web.*" + } + destination_services { + regexp = "^api.*" + } + } +}` testFileName = "config.hcl" ) @@ -125,6 +170,26 @@ func TestModuleInput_DecodeConfig_Success(t *testing.T) { }, config: testModuleInputConsulKVSuccess, }, + { + name: "intentions", + expected: &ModuleInputConfigs{ + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String("dc2"), + Namespace: String("ns2"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + Names: []string{}, + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + Names: []string{}, + }, + }, + }, + }, + config: testModuleInputIntentionsSuccess, + }, { name: "multiple unique module_inputs", expected: &ModuleInputConfigs{ @@ -145,6 +210,20 @@ func TestModuleInput_DecodeConfig_Success(t *testing.T) { Namespace: String(""), }, }, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String(""), + Namespace: String(""), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + Names: []string{}, + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + Names: []string{}, + }, + }, + }, }, config: testModuleInputsSuccess, }, @@ -184,6 +263,11 @@ func TestModuleInput_DecodeConfig_Error(t *testing.T) { expected: nil, config: testModuleInputConsulKVUnsupportedFieldError, }, + { + name: "intentions unsupported field", + expected: nil, + config: testModuleInputConsulKVUnsupportedFieldError, + }, } for _, tc := range cases { @@ -213,8 +297,9 @@ func TestModuleInputConfigs_Len(t *testing.T) { &ModuleInputConfigs{ &ServicesModuleInputConfig{}, &ConsulKVModuleInputConfig{}, + &IntentionsModuleInputConfig{}, }, - 2, + 3, }, } @@ -266,6 +351,18 @@ func TestModuleInputConfigs_Copy(t *testing.T) { Namespace: String("ns2"), }, }, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String("datacenter"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + }, }, &ModuleInputConfigs{ &ServicesModuleInputConfig{ @@ -287,6 +384,18 @@ func TestModuleInputConfigs_Copy(t *testing.T) { Namespace: String("ns2"), }, }, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig{ + Datacenter: String("datacenter"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + }, }, }, } @@ -335,10 +444,10 @@ func TestModuleInputConfigs_Merge(t *testing.T) { { "happy_path_different_type", &ModuleInputConfigs{&ServicesModuleInputConfig{}}, - &ModuleInputConfigs{&ConsulKVModuleInputConfig{}}, + &ModuleInputConfigs{&IntentionsModuleInputConfig{}}, &ModuleInputConfigs{ &ServicesModuleInputConfig{}, - &ConsulKVModuleInputConfig{}, + &IntentionsModuleInputConfig{}, }, }, { @@ -432,6 +541,7 @@ func TestModuleInputConfigs_Validate(t *testing.T) { moduleInputs: &ModuleInputConfigs{ &ServicesModuleInputConfig{}, &ConsulKVModuleInputConfig{}, + &IntentionsModuleInputConfig{}, }, valid: true, }, @@ -441,6 +551,7 @@ func TestModuleInputConfigs_Validate(t *testing.T) { moduleInputs: &ModuleInputConfigs{ &ServicesModuleInputConfig{}, &ConsulKVModuleInputConfig{}, + &IntentionsModuleInputConfig{}, }, valid: true, }, @@ -517,11 +628,26 @@ func TestModuleInputConfigs_GoString(t *testing.T) { Path: String("my/path"), }, }, + &IntentionsModuleInputConfig{ + IntentionsMonitorConfig: IntentionsMonitorConfig{ + Datacenter: String("datacenter"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + }, }, "{&ServicesModuleInputConfig{&ServicesMonitorConfig{Regexp:^api$, Names:[], " + "Datacenter:, Namespace:, Filter:, CTSUserDefinedMeta:map[]}}, " + "&ConsulKVModuleInputConfig{&ConsulKVMonitorConfig{Path:my/path, " + - "Recurse:false, Datacenter:, Namespace:, }}}", + "Recurse:false, Datacenter:, Namespace:, }}, " + + "&IntentionsModuleInputConfig{&IntentionsMonitorConfig" + + "{Datacenter:datacenter, Namespace:namespace, " + + "Source Services Regexp:^web.*, Destination Services Regexp:^api.*}}}", }, } diff --git a/config/monitor_intentions.go b/config/monitor_intentions.go new file mode 100644 index 000000000..4a39f6df7 --- /dev/null +++ b/config/monitor_intentions.go @@ -0,0 +1,152 @@ +package config + +import ( + "fmt" +) + +const intentionsType = "intentions" + +var _ MonitorConfig = (*IntentionsMonitorConfig)(nil) + +// IntentionsMonitorConfig configures a configuration block adhering to the +// monitor interface of type 'intentions'. An intention monitor watches for changes +// that occur to intentions. +type IntentionsMonitorConfig struct { + // Datacenter is the datacenter the intention is in. + Datacenter *string `mapstructure:"datacenter"` + + // Namespace is the namespace of the intention (Consul Enterprise only). If + // not provided, the namespace will be inferred from the CTS ACL token, or + // default to the `default` namespace. + Namespace *string `mapstructure:"namespace"` + + // SourceServices configures regexp/list of names of source services. + SourceServices *IntentionsServicesConfig `mapstructure:"source_services"` + + // DestinationServices configures regexp/list of names of destination services. + DestinationServices *IntentionsServicesConfig `mapstructure:"destination_services"` +} + +func (c *IntentionsMonitorConfig) VariableType() string { + return intentionsType +} + +// Copy returns a deep copy of this configuration. +func (c *IntentionsMonitorConfig) Copy() MonitorConfig { + if c == nil { + return nil + } + + var o IntentionsMonitorConfig + + o.Datacenter = StringCopy(c.Datacenter) + o.Namespace = StringCopy(c.Namespace) + o.SourceServices = c.SourceServices.Copy() + o.DestinationServices = c.DestinationServices.Copy() + + return &o +} + +// Merge combines all values in this configuration with the values in the other +// configuration, with values in the other configuration taking precedence. +// Maps and slices are merged, most other values are overwritten. Complex +// structs define their own merge functionality. +func (c *IntentionsMonitorConfig) Merge(o MonitorConfig) MonitorConfig { + if c == nil { + if isConditionNil(o) { // o is interface, use isConditionNil() + return nil + } + return o.Copy() + } + + if isConditionNil(o) { + return c.Copy() + } + + r := c.Copy() + o2, ok := o.(*IntentionsMonitorConfig) + if !ok { + return r + } + + r2 := r.(*IntentionsMonitorConfig) + + if o2.Datacenter != nil { + r2.Datacenter = StringCopy(o2.Datacenter) + } + if o2.Namespace != nil { + r2.Namespace = StringCopy(o2.Namespace) + } + + if o2.DestinationServices != nil { + r2.DestinationServices = r2.DestinationServices.Merge(o2.DestinationServices) + } + + if o2.SourceServices != nil { + r2.SourceServices = r2.SourceServices.Merge(o2.SourceServices) + } + + return r2 +} + +// Finalize ensures there no nil pointers. +func (c *IntentionsMonitorConfig) Finalize() { + if c == nil { // config not required, return early + return + } + + if c.Datacenter == nil { + c.Datacenter = String("") + } + if c.Namespace == nil { + c.Namespace = String("") + } + + c.SourceServices.Finalize() + c.DestinationServices.Finalize() +} + +// Validate validates the values and required options. This method is recommended +// to run after Finalize() to ensure the configuration is safe to proceed. +// Note, it handles the possibility of nil Regexp value even after Finalize(). +func (c *IntentionsMonitorConfig) Validate() error { + if c == nil { // config not required, return early + return nil + } + + if c.SourceServices.Validate() != nil && c.DestinationServices.Validate() == nil { + return fmt.Errorf("both source services and destination services must be configured") + } + + if c.SourceServices.Validate() == nil && c.DestinationServices.Validate() != nil { + return fmt.Errorf("both source services and destination services must be configured") + } + + if err := c.SourceServices.Validate(); err != nil { + return err + } + + if err := c.DestinationServices.Validate(); err != nil { + return err + } + + return nil +} + +// GoString defines the printable version of this struct. +func (c *IntentionsMonitorConfig) GoString() string { + if c == nil { + return "(*IntentionsMonitorConfig)(nil)" + } + return fmt.Sprintf("&IntentionsMonitorConfig{"+ + "Datacenter:%s, "+ + "Namespace:%s, "+ + "Source Services %s, "+ + "Destination Services %s"+ + "}", + StringVal(c.Datacenter), + StringVal(c.Namespace), + c.SourceServices.GoString(), + c.DestinationServices.GoString(), + ) +} diff --git a/config/monitor_intentions_test.go b/config/monitor_intentions_test.go new file mode 100644 index 000000000..5cb1b98f8 --- /dev/null +++ b/config/monitor_intentions_test.go @@ -0,0 +1,493 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIntentionsMonitorConfig_Copy(t *testing.T) { + t.Parallel() + + finalizedConf := &IntentionsMonitorConfig{} + finalizedConf.Finalize() + + cases := []struct { + name string + a *IntentionsMonitorConfig + }{ + { + "nil", + nil, + }, + { + "empty", + &IntentionsMonitorConfig{}, + }, + { + "finalized", + finalizedConf, + }, + { + "regexp_fully_configured", + &IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + }, + { + "names_fully_configured", + &IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Names: []string{"web", "api"}, + }, + DestinationServices: &IntentionsServicesConfig{ + Names: []string{"web", "api"}, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r := tc.a.Copy() + if tc.a == nil { + // returned nil interface has nil type, which is unequal to tc.a + assert.Nil(t, r) + } else { + assert.Equal(t, tc.a, r) + } + }) + } +} + +func TestIntentionsMonitorConfig_Merge(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + a *IntentionsMonitorConfig + b *IntentionsMonitorConfig + r *IntentionsMonitorConfig + }{ + { + "nil_a", + nil, + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{}, + }, + { + "nil_b", + &IntentionsMonitorConfig{}, + nil, + &IntentionsMonitorConfig{}, + }, + { + "nil_both", + nil, + nil, + nil, + }, + { + "empty", + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{}, + }, + { + "datacenter_overrides", + &IntentionsMonitorConfig{Datacenter: String("datacenter")}, + &IntentionsMonitorConfig{Datacenter: String("dc")}, + &IntentionsMonitorConfig{Datacenter: String("dc")}, + }, + { + "datacenter_empty_one", + &IntentionsMonitorConfig{Datacenter: String("datacenter")}, + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{Datacenter: String("datacenter")}, + }, + { + "datacenter_empty_two", + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{Datacenter: String("datacenter")}, + &IntentionsMonitorConfig{Datacenter: String("datacenter")}, + }, + { + "datacenter_same", + &IntentionsMonitorConfig{Datacenter: String("datacenter")}, + &IntentionsMonitorConfig{Datacenter: String("datacenter")}, + &IntentionsMonitorConfig{Datacenter: String("datacenter")}, + }, + { + "namespace_overrides", + &IntentionsMonitorConfig{Namespace: String("namespace")}, + &IntentionsMonitorConfig{Namespace: String("ns")}, + &IntentionsMonitorConfig{Namespace: String("ns")}, + }, + { + "namespace_empty_one", + &IntentionsMonitorConfig{Namespace: String("namespace")}, + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{Namespace: String("namespace")}, + }, + { + "namespace_empty_two", + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{Namespace: String("namespace")}, + &IntentionsMonitorConfig{Namespace: String("namespace")}, + }, + { + "namespace_same", + &IntentionsMonitorConfig{Namespace: String("namespace")}, + &IntentionsMonitorConfig{Namespace: String("namespace")}, + &IntentionsMonitorConfig{Namespace: String("namespace")}, + }, + { + "regexp_overrides", + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("same")}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("different")}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("different")}}, + }, + { + "regexp_empty_one", + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("same")}}, + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("same")}}, + }, + { + "regexp_empty_two", + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("same")}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("same")}}, + }, + { + "regexp_empty_same", + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("same")}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("same")}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Regexp: String("same")}}, + }, + { + "names_merges", + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"a"}}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"b"}}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"a", "b"}}}, + }, + { + "names_same_merges", + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"a"}}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"a"}}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"a"}}}, + }, + { + "names_empty_one", + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"service"}}}, + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"service"}}}, + }, + { + "names_empty_two", + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"service"}}}, + &IntentionsMonitorConfig{SourceServices: &IntentionsServicesConfig{Names: []string{"service"}}}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r := tc.a.Merge(tc.b) + if tc.r == nil { + // returned nil interface has nil type, which is unequal to tc.r + assert.Nil(t, r) + } else { + assert.Equal(t, tc.r, r) + } + }) + } +} + +func TestIntentionsMonitorConfig_Finalize(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + i *IntentionsMonitorConfig + r *IntentionsMonitorConfig + }{ + { + "nil", + nil, + nil, + }, + { + "empty", + &IntentionsMonitorConfig{}, + &IntentionsMonitorConfig{ + Datacenter: String(""), + Namespace: String(""), + SourceServices: nil, + DestinationServices: nil, + }, + }, + { + "regexp_fully_configured", + &IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + &IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + Names: []string{}, + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + Names: []string{}, + }, + }, + }, + { + "names_fully_configured", + &IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Names: []string{"service"}, + }, + DestinationServices: &IntentionsServicesConfig{ + Names: []string{"service"}, + }, + }, + &IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Names: []string{"service"}, + }, + DestinationServices: &IntentionsServicesConfig{ + Names: []string{"service"}, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.i.Finalize() + assert.Equal(t, tc.r, tc.i) + }) + } +} + +func TestIntentionsMonitorConfig_Validate(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + expectErr bool + c *IntentionsMonitorConfig + }{ + { + "nil", + false, + nil, + }, + { + "valid_with_regexp", + false, + &IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{ + Regexp: String(".*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String(".*"), + }, + }, + }, + { + "valid_with_names", + false, + &IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{ + Names: []string{"api"}, + }, + DestinationServices: &IntentionsServicesConfig{ + Names: []string{"web"}, + }, + }, + }, + { + "valid_regexp_empty_string", + false, + &IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{ + Regexp: String(""), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String(""), + }, + }, + }, + { + "invalid_source_service_not_configured", + true, + &IntentionsMonitorConfig{ + DestinationServices: &IntentionsServicesConfig{ + Regexp: String(".*"), + }, + SourceServices: &IntentionsServicesConfig{}, + }, + }, + { + "invalid_regexp", + true, + &IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{ + Regexp: String("*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String(".*"), + }, + }, + }, + { + "invalid_empty_string_names", + true, + &IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{ + Names: []string{"api", ""}, + }, + DestinationServices: &IntentionsServicesConfig{ + Names: []string{"", "web"}, + }, + }, + }, + { + "invalid_both_regexp_and_names_configured", + true, + &IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{ + Regexp: String(".*"), + Names: []string{"api"}, + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String(".*"), + Names: []string{"web"}, + }, + }, + }, + { + "invalid_no_regexp_no_names_configured", + true, + &IntentionsMonitorConfig{ + SourceServices: &IntentionsServicesConfig{}, + DestinationServices: &IntentionsServicesConfig{}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.c.Validate() + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestIntentionsMonitorConfig_GoString(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + i *IntentionsMonitorConfig + expected string + }{ + { + "nil", + nil, + "(*IntentionsMonitorConfig)(nil)", + }, + { + "regexp_fully_configured", + &IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Regexp: String("^web.*"), + }, + DestinationServices: &IntentionsServicesConfig{ + Regexp: String("^api.*"), + }, + }, + "&IntentionsMonitorConfig{Datacenter:dc, Namespace:namespace, " + + "Source Services Regexp:^web.*, Destination Services Regexp:^api.*}", + }, + { + "names_fully_configured", + &IntentionsMonitorConfig{ + Datacenter: String("dc"), + Namespace: String("namespace"), + SourceServices: &IntentionsServicesConfig{ + Names: []string{"servicea"}, + }, + DestinationServices: &IntentionsServicesConfig{ + Names: []string{"serviceb"}, + }, + }, + "&IntentionsMonitorConfig{Datacenter:dc, Namespace:namespace, " + + "Source Services Names:[servicea], Destination Services Names:[serviceb]}", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actual := tc.i.GoString() + require.Equal(t, tc.expected, actual) + }) + } +} + +// TestIntentionsMonitorConfig_RegexpNil tests the exception that when `Regexp` is +// unset, it retains nil value after Finalize() and Validate(). Tests it is +// idempotent +func TestIntentionsMonitorConfig_RegexpNil(t *testing.T) { + t.Parallel() + + conf := + &IntentionsServicesConfig{ + Names: []string{"api"}, + // Regexp unset + } + + // Confirm `Regexp` nil + conf.Finalize() + err := conf.Validate() + assert.NoError(t, err) + assert.Nil(t, conf.Regexp) + + // Confirm idempotent - Validate() doesn't error and `Regexp` still nil + conf.Finalize() + err = conf.Validate() + assert.NoError(t, err) + assert.Nil(t, conf.Regexp) +}