From dba0a8793bc510407e616960896a3665b15d391d Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Thu, 13 Apr 2023 13:36:57 +0200 Subject: [PATCH] feat!: Version schema for rule sets (#436) --- .codecov.yml | 2 +- .mockery.yaml | 2 + cmd/serve/decision.go | 5 +- cmd/serve/proxy.go | 5 +- cmd/validate/config.go | 33 +- cmd/validate/config_test.go | 92 +++ cmd/validate/errors.go | 5 + .../configuration/rules/configuration.adoc | 17 +- .../docs/configuration/rules/providers.adoc | 12 +- example_rules.yaml | 3 + go.mod | 2 +- go.sum | 106 +--- internal/handler/decision/handler.go | 12 +- internal/handler/decision/handler_test.go | 100 ++- .../handler/envoyextauth/grpcv3/handler.go | 10 +- .../envoyextauth/grpcv3/handler_test.go | 57 +- .../handler/envoyextauth/grpcv3/module.go | 4 +- .../handler/envoyextauth/grpcv3/service.go | 4 +- internal/handler/proxy/handler.go | 14 +- internal/handler/proxy/handler_test.go | 71 +-- .../rules/composite_error_handler_test.go | 31 +- .../rules/composite_subject_creator_test.go | 44 +- .../rules/composite_subject_handler_test.go | 56 +- internal/rules/{rule => config}/decoder.go | 4 +- .../{rule => config}/mapstructure_decoder.go | 4 +- .../mapstructure_decoder_test.go | 4 +- internal/rules/{rule => config}/matcher.go | 6 +- .../rules/{rule => config}/matcher_test.go | 2 +- .../rulesetparser => config}/parser.go | 31 +- internal/rules/config/parser_test.go | 181 ++++++ .../{rule/configuration.go => config/rule.go} | 14 +- internal/rules/config/rule_set.go | 39 ++ internal/rules/config/rule_set_test.go | 39 ++ .../rule_test.go} | 8 +- internal/rules/config/version.go | 3 + internal/rules/error_handler.go | 2 + internal/rules/event/event.go | 23 +- internal/rules/event/queue.go | 2 +- .../authenticators/authenticator.go | 2 + .../body_parameter_extract_strategy.go | 2 +- .../authenticators/jwt_authenticator.go | 4 +- .../authenticators/mocks/authenticator.go | 189 ++++++ .../mechanisms/authorizers/authorizer.go | 2 + .../authorizers/mocks/authorizer.go | 178 ++++++ internal/rules/mechanisms/cellib/types.go | 2 +- .../contextualizers/contextualizer.go | 2 + .../contextualizers/mocks/contextualizer.go | 178 ++++++ .../mechanisms/errorhandlers/error_handler.go | 2 + .../errorhandlers/mocks/error_handler.go | 145 +++++ internal/rules/mechanisms/factory.go | 132 +--- internal/rules/mechanisms/factory_impl.go | 130 ++++ internal/rules/mechanisms/factory_test.go | 91 ++- .../rules/mechanisms/mocks/authenticator.go | 55 -- internal/rules/mechanisms/mocks/authorizer.go | 48 -- .../rules/mechanisms/mocks/contextualizer.go | 48 -- .../rules/mechanisms/mocks/error_handler.go | 45 -- internal/rules/mechanisms/mocks/factory.go | 326 ++++++++++ internal/rules/mechanisms/mocks/unifier.go | 48 -- internal/rules/mechanisms/oauth2/claims.go | 6 +- .../rules/mechanisms/oauth2/expectations.go | 8 +- .../mechanisms/unifiers/mocks/unifier.go | 178 ++++++ internal/rules/mechanisms/unifiers/unifier.go | 2 + internal/rules/mocks/error_handler.go | 100 ++- internal/rules/mocks/mechanism_factory.go | 97 --- internal/rules/mocks/repository.go | 40 -- internal/rules/mocks/rule_factory.go | 53 -- internal/rules/mocks/subject_creator.go | 144 ++++- internal/rules/mocks/subject_handler.go | 132 +++- internal/rules/module.go | 64 +- internal/rules/patternmatcher/glob_matcher.go | 9 +- .../rules/patternmatcher/pattern_matcher.go | 4 +- .../rules/patternmatcher/regex_matcher.go | 8 + internal/rules/provider/cloudblob/module.go | 14 +- internal/rules/provider/cloudblob/provider.go | 171 ++--- .../provider/cloudblob/provider_registrar.go | 62 -- .../cloudblob/provider_registrar_test.go | 115 ---- .../rules/provider/cloudblob/provider_test.go | 249 +++++--- .../provider/cloudblob/ruleset_endpoint.go | 57 +- .../cloudblob/ruleset_endpoint_test.go | 101 +-- .../provider/cloudblob/ruleset_fetcher.go | 4 +- internal/rules/provider/filesystem/module.go | 14 +- .../rules/provider/filesystem/provider.go | 313 +++++----- .../provider/filesystem/provider_registrar.go | 62 -- .../filesystem/provider_registrar_test.go | 141 ----- .../provider/filesystem/provider_test.go | 422 ++++++++----- .../rules/provider/httpendpoint/module.go | 14 +- .../rules/provider/httpendpoint/provider.go | 190 +++--- .../httpendpoint/provider_registrar.go | 64 -- .../httpendpoint/provider_registrar_test.go | 118 ---- .../provider/httpendpoint/provider_test.go | 466 ++++++++++---- .../provider/httpendpoint/ruleset_endpoint.go | 34 +- .../httpendpoint/ruleset_endpoint_test.go | 78 ++- .../provider/httpendpoint/ruleset_fetcher.go | 4 +- .../provider/kubernetes/api/v1alpha1/types.go | 6 +- .../api/v1alpha1/zz_generated.deepcopy.go | 4 +- internal/rules/provider/kubernetes/module.go | 12 +- .../rules/provider/kubernetes/provider.go | 177 ++++-- .../provider/kubernetes/provider_registrar.go | 72 --- .../kubernetes/provider_registrar_test.go | 136 ---- .../provider/kubernetes/provider_test.go | 584 +++++++++++++++--- .../rules/provider/pathprefix/path_prefix.go | 47 -- .../provider/pathprefix/path_prefix_test.go | 52 -- .../provider/rulesetparser/parser_test.go | 110 ---- .../provider/rulesetparser/yaml_parser.go | 46 -- .../rulesetparser/yaml_parser_test.go | 75 --- internal/rules/repository.go | 201 ------ internal/rules/repository_impl.go | 261 ++++++++ internal/rules/repository_impl_test.go | 359 +++++++++++ internal/rules/repository_test.go | 468 -------------- .../cloudblob/rule_set.go => rule/factory.go} | 16 +- internal/rules/rule/mocks/factory.go | 137 ++++ internal/rules/rule/mocks/repository.go | 92 +++ internal/rules/rule/mocks/rule.go | 258 ++++++++ internal/rules/rule/mocks/rule_mock.go | 44 -- internal/rules/rule/mocks/set_processor.go | 162 +++++ .../rule_set.go => rule/repository.go} | 13 +- internal/rules/rule/rule.go | 2 + internal/rules/rule/ruleset_processor.go | 11 + .../{rule_factory.go => rule_factory_impl.go} | 95 ++- ...tory_test.go => rule_factory_impl_test.go} | 271 ++++---- internal/rules/rule_impl.go | 1 + internal/rules/rule_impl_test.go | 122 ++-- internal/rules/rule_set_definition_loader.go | 24 - internal/rules/ruleset_processor_impl.go | 114 ++++ internal/rules/ruleset_processor_test.go | 233 +++++++ internal/rules/subject_creator.go | 2 + internal/rules/subject_handler.go | 2 + internal/x/errorchain/error_chain.go | 2 +- internal/x/slicex/filter.go | 13 + .../x/testsupport/mock/argument_captor.go | 37 ++ .../x/testsupport/mock/arguments_captor.go | 31 + internal/x/testsupport/patched_os_exit.go | 38 ++ 132 files changed, 6368 insertions(+), 4178 deletions(-) create mode 100644 .mockery.yaml create mode 100644 cmd/validate/config_test.go create mode 100644 cmd/validate/errors.go rename internal/rules/{rule => config}/decoder.go (96%) rename internal/rules/{rule => config}/mapstructure_decoder.go (95%) rename internal/rules/{rule => config}/mapstructure_decoder_test.go (98%) rename internal/rules/{rule => config}/matcher.go (95%) rename internal/rules/{rule => config}/matcher_test.go (99%) rename internal/rules/{provider/rulesetparser => config}/parser.go (69%) create mode 100644 internal/rules/config/parser_test.go rename internal/rules/{rule/configuration.go => config/rule.go} (87%) create mode 100644 internal/rules/config/rule_set.go create mode 100644 internal/rules/config/rule_set_test.go rename internal/rules/{rule/configuration_test.go => config/rule_test.go} (96%) create mode 100644 internal/rules/config/version.go create mode 100644 internal/rules/mechanisms/authenticators/mocks/authenticator.go create mode 100644 internal/rules/mechanisms/authorizers/mocks/authorizer.go create mode 100644 internal/rules/mechanisms/contextualizers/mocks/contextualizer.go create mode 100644 internal/rules/mechanisms/errorhandlers/mocks/error_handler.go create mode 100644 internal/rules/mechanisms/factory_impl.go delete mode 100644 internal/rules/mechanisms/mocks/authenticator.go delete mode 100644 internal/rules/mechanisms/mocks/authorizer.go delete mode 100644 internal/rules/mechanisms/mocks/contextualizer.go delete mode 100644 internal/rules/mechanisms/mocks/error_handler.go create mode 100644 internal/rules/mechanisms/mocks/factory.go delete mode 100644 internal/rules/mechanisms/mocks/unifier.go create mode 100644 internal/rules/mechanisms/unifiers/mocks/unifier.go delete mode 100644 internal/rules/mocks/mechanism_factory.go delete mode 100644 internal/rules/mocks/repository.go delete mode 100644 internal/rules/mocks/rule_factory.go delete mode 100644 internal/rules/provider/cloudblob/provider_registrar.go delete mode 100644 internal/rules/provider/cloudblob/provider_registrar_test.go delete mode 100644 internal/rules/provider/filesystem/provider_registrar.go delete mode 100644 internal/rules/provider/filesystem/provider_registrar_test.go delete mode 100644 internal/rules/provider/httpendpoint/provider_registrar.go delete mode 100644 internal/rules/provider/httpendpoint/provider_registrar_test.go delete mode 100644 internal/rules/provider/kubernetes/provider_registrar.go delete mode 100644 internal/rules/provider/kubernetes/provider_registrar_test.go delete mode 100644 internal/rules/provider/pathprefix/path_prefix.go delete mode 100644 internal/rules/provider/pathprefix/path_prefix_test.go delete mode 100644 internal/rules/provider/rulesetparser/parser_test.go delete mode 100644 internal/rules/provider/rulesetparser/yaml_parser.go delete mode 100644 internal/rules/provider/rulesetparser/yaml_parser_test.go delete mode 100644 internal/rules/repository.go create mode 100644 internal/rules/repository_impl.go create mode 100644 internal/rules/repository_impl_test.go delete mode 100644 internal/rules/repository_test.go rename internal/rules/{provider/cloudblob/rule_set.go => rule/factory.go} (72%) create mode 100644 internal/rules/rule/mocks/factory.go create mode 100644 internal/rules/rule/mocks/repository.go create mode 100644 internal/rules/rule/mocks/rule.go delete mode 100644 internal/rules/rule/mocks/rule_mock.go create mode 100644 internal/rules/rule/mocks/set_processor.go rename internal/rules/{provider/httpendpoint/rule_set.go => rule/repository.go} (79%) create mode 100644 internal/rules/rule/ruleset_processor.go rename internal/rules/{rule_factory.go => rule_factory_impl.go} (77%) rename internal/rules/{rule_factory_test.go => rule_factory_impl_test.go} (71%) delete mode 100644 internal/rules/rule_set_definition_loader.go create mode 100644 internal/rules/ruleset_processor_impl.go create mode 100644 internal/rules/ruleset_processor_test.go create mode 100644 internal/x/slicex/filter.go create mode 100644 internal/x/testsupport/mock/argument_captor.go create mode 100644 internal/x/testsupport/mock/arguments_captor.go create mode 100644 internal/x/testsupport/patched_os_exit.go diff --git a/.codecov.yml b/.codecov.yml index c77f7182a..d25ab39e5 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -2,7 +2,7 @@ ignore: - "**/*generated.go" - "**/mocks" - "**/*_test.go" -- "**/testsupport/*.go" +- "**/testsupport/**" - "**/zz_generated.*" coverage: status: diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 000000000..1b933dad8 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,2 @@ +case: underscore +with-expecter: true \ No newline at end of file diff --git a/cmd/serve/decision.go b/cmd/serve/decision.go index ca96acd0f..e6159c89d 100644 --- a/cmd/serve/decision.go +++ b/cmd/serve/decision.go @@ -17,6 +17,8 @@ package serve import ( + "os" + "github.com/spf13/cobra" "go.uber.org/fx" @@ -36,7 +38,8 @@ func NewDecisionCommand() *cobra.Command { app, err := createDecisionApp(cmd) if err != nil { cmd.PrintErrf("Failed to initialize decision service: %v", err) - panic(err) + + os.Exit(1) } app.Run() diff --git a/cmd/serve/proxy.go b/cmd/serve/proxy.go index 5681f5196..338309696 100644 --- a/cmd/serve/proxy.go +++ b/cmd/serve/proxy.go @@ -17,6 +17,8 @@ package serve import ( + "os" + "github.com/spf13/cobra" "go.uber.org/fx" @@ -35,7 +37,8 @@ func NewProxyCommand() *cobra.Command { app, err := createProxyApp(cmd) if err != nil { cmd.PrintErrf("Failed to initialize proxy service: %v", err) - panic(err) + + os.Exit(1) } app.Run() diff --git a/cmd/validate/config.go b/cmd/validate/config.go index 5b1865396..13e0d96ef 100644 --- a/cmd/validate/config.go +++ b/cmd/validate/config.go @@ -17,33 +17,36 @@ package validate import ( - "errors" + "os" "github.com/spf13/cobra" "github.com/dadrus/heimdall/internal/config" ) -var ErrNoConfigFile = errors.New("no config file provided") - // NewValidateConfigCommand represents the "validate config" command. func NewValidateConfigCommand() *cobra.Command { return &cobra.Command{ - Use: "config", - Short: "Validates heimdall's configuration", - RunE: func(cmd *cobra.Command, args []string) error { - configPath, _ := cmd.Flags().GetString("config") - if len(configPath) == 0 { - return ErrNoConfigFile - } - - if err := config.ValidateConfig(configPath); err != nil { + Use: "config", + Short: "Validates heimdall's configuration", + Example: "heimdall validate config -c myconfig.yaml", + Run: func(cmd *cobra.Command, args []string) { + if err := validateConfig(cmd); err != nil { cmd.PrintErrf("%v\n", err) - } else { - cmd.Printf("Configuration is valid\n") + + os.Exit(1) } - return nil + cmd.Println("Configuration is valid") }, } } + +func validateConfig(cmd *cobra.Command) error { + configPath, _ := cmd.Flags().GetString("config") + if len(configPath) == 0 { + return ErrNoConfigFile + } + + return config.ValidateConfig(configPath) +} diff --git a/cmd/validate/config_test.go b/cmd/validate/config_test.go new file mode 100644 index 000000000..fe574e6f1 --- /dev/null +++ b/cmd/validate/config_test.go @@ -0,0 +1,92 @@ +package validate + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dadrus/heimdall/internal/x/testsupport" +) + +func TestValidateConfig(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + confFile string + expError error + }{ + {uc: "no config provided", expError: ErrNoConfigFile}, + {uc: "invalid config", confFile: "doesnotexist.yaml", expError: os.ErrNotExist}, + {uc: "valid config", confFile: "../../internal/config/test_data/test_config.yaml"}, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + cmd := NewValidateConfigCommand() + cmd.Flags().StringP("config", "c", "", "Path to heimdall's configuration file.") + + if len(tc.confFile) != 0 { + err := cmd.ParseFlags([]string{"--config", tc.confFile}) + require.NoError(t, err) + } + + // WHEN + err := validateConfig(cmd) + + // THEN + if tc.expError != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tc.expError) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestRunValidateCommand(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + confFile string + expError string + }{ + {uc: "invalid config", confFile: "doesnotexist.yaml", expError: "no such file or dir"}, + {uc: "valid config", confFile: "../../internal/config/test_data/test_config.yaml"}, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + exit, err := testsupport.PatchOSExit(t, func(int) {}) + require.NoError(t, err) + + cmd := NewValidateConfigCommand() + + buf := bytes.NewBuffer([]byte{}) + cmd.SetOut(buf) + cmd.SetErr(buf) + + cmd.Flags().StringP("config", "c", "", "Path to heimdall's configuration file.") + + if len(tc.confFile) != 0 { + err := cmd.ParseFlags([]string{"--config", tc.confFile}) + require.NoError(t, err) + } + + // WHEN + cmd.Run(cmd, []string{}) + + log := buf.String() + if len(tc.expError) != 0 { + assert.Contains(t, log, tc.expError) + assert.True(t, exit.Called) + assert.Equal(t, 1, exit.Code) + } else { + assert.Contains(t, log, "Configuration is valid") + } + }) + } +} diff --git a/cmd/validate/errors.go b/cmd/validate/errors.go new file mode 100644 index 000000000..da1a8771e --- /dev/null +++ b/cmd/validate/errors.go @@ -0,0 +1,5 @@ +package validate + +import "errors" + +var ErrNoConfigFile = errors.New("no config file provided") diff --git a/docs/content/docs/configuration/rules/configuration.adoc b/docs/content/docs/configuration/rules/configuration.adoc index 8af168421..653624b63 100644 --- a/docs/content/docs/configuration/rules/configuration.adoc +++ b/docs/content/docs/configuration/rules/configuration.adoc @@ -168,12 +168,27 @@ This example uses two error handlers, named `foo` and `bar`. `bar` will only be == Rule Set -A rule set is just a list of rules, typically defined in a format specified by a particular link:{{< relref "providers.adoc" >}}[provider]. In its simplest case a rule set does not require further configuration options and can look as shown below: +In principle, a rule set is just a list of rules with some additional meta information. Each `RuleSet` definition has the following attributes if not stated otherwise by a particular link:{{< relref "providers.adoc" >}}[provider]: + +* *`version`*: _string_ (mandatory) ++ +The version schema of the `RuleSet`. The current version of heimdall supports only the version `1`. + +* *`name`*: _string_ (optional) ++ +The name of a rule set. Used only for logging purposes. + +* *`rules`*: _link:{{< relref "configuration.adoc#_rule_configuration" >}}[Rule Configuration] array_ (mandatory) ++ +List of the actual rules. .Rule set with two rules ==== [source, yaml] ---- +version: "1" +name: my-rule-set +rules: - id: rule:1 match: url: https://my-service1.local/<**> diff --git a/docs/content/docs/configuration/rules/providers.adoc b/docs/content/docs/configuration/rules/providers.adoc index 03cb8ac64..2e83664b4 100644 --- a/docs/content/docs/configuration/rules/providers.adoc +++ b/docs/content/docs/configuration/rules/providers.adoc @@ -9,13 +9,13 @@ menu: parent: "Rules" --- -Providers define the sources to load the link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets] from. These make Heimdall's behavior dynamic. All providers, you want to enable for a Heimdall instance must be configured within the `providers` section of heimdall's `rules` configuration. +Providers define the sources to load the link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets] from. These make heimdall's behavior dynamic. All providers, you want to enable for a heimdall instance must be configured within the `providers` section of heimdall's `rules` configuration. Supported providers, including the corresponding configuration options are described below == Filesystem -The filesystem provider allows loading of rule sets from a file system. The configuration of this provider goes into the `file_system` property. This provider is handy for e.g. starting playing around with heimdall, e.g. locally, or using Docker, as well as if your deployment strategy considers deploying a heimdall instance as a Side-Car for each of your services. +The filesystem provider allows loading of rule sets in a format defined in link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets] from a file system. The configuration of this provider goes into the `file_system` property. This provider is handy for e.g. starting playing around with heimdall, e.g. locally, or using Docker, as well as if your deployment strategy considers deploying a heimdall instance as a Side-Car for each of your services. Following configuration options are supported: @@ -27,8 +27,6 @@ Can either be a single file, containing a rule set, or a directory with files, e + Whether the configured `src` should be watched for updates. Defaults to `false`. If the `src` has been configured to a single file, the provider will watch for changes in that file. Otherwise, if the `src` has been configured to a directory, the provider will watch for files appearing and disappearing in this directory, as well as for changes in each particular file in this directory. Recursive lookup is not supported. That is, if the configured directory contains further directories, these, as well as their contents are ignored. -This provider doesn't need any additional configuration for a rule set. So the contents of files can be just a list of rules as described in link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets]. - .Load rule sets from the files residing in the `/path/to/rules/dir` directory and watch for changes. ==== [source, yaml] @@ -50,7 +48,7 @@ file_system: == HTTP Endpoint -This provider allows loading of link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets] from any remote endpoint accessible via HTTP(s) and supports rule sets in YAML, as well as in JSON format. The differentiation happens based on the `Content-Type` set in the response from the endpoint, which must be either `application/yaml` or `application/json`, otherwise an error is logged and the response from the endpoint is ignored. +This provider allows loading of rule sets in a format defined in link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets] from any remote endpoint accessible via HTTP(s) and supports rule sets in YAML, as well as in JSON format. The differentiation happens based on the `Content-Type` set in the response from the endpoint, which must be either `application/yaml` or `application/json`, otherwise an error is logged and the response from the endpoint is ignored. The loading and removal of rules happens as follows: @@ -76,8 +74,6 @@ This property can be used to create kind of a namespace for the rule sets retrie NOTE: HTTP caching according to https://www.rfc-editor.org/rfc/rfc7234[RFC 7234] is enabled by default. It can be disabled by setting `enable_http_cache` to `false`. -This provider doesn't need any additional configuration for a rule set. So the contents of files can be just a list of rules as described in link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets]. - .Minimal possible configuration ==== Here the provider is configured to load a rule set from one endpoint without polling it for changes. @@ -122,7 +118,7 @@ http_endpoint: == Cloud Blob -This provider allows loading of link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets] from cloud blobs, like AWS S3 buckets, Google Cloud Storage, Azure Blobs, or other API compatible implementations and supports rule sets in YAML, as well as in JSON format. The differentiation happens based on the `Content-Type` set in the metadata of the loaded blob, which must be either `application/yaml` or `application/json`, otherwise an error is logged and the blob is ignored. +This provider allows loading of rule sets in a format defined in link:{{< relref "configuration.adoc#_rule_set" >}}[Rule Sets] from cloud blobs, like AWS S3 buckets, Google Cloud Storage, Azure Blobs, or other API compatible implementations and supports rule sets in YAML, as well as in JSON format. The differentiation happens based on the `Content-Type` set in the metadata of the loaded blob, which must be either `application/yaml` or `application/json`, otherwise an error is logged and the blob is ignored. The loading and removal of rules happens as follows: diff --git a/example_rules.yaml b/example_rules.yaml index e94c797ed..5d043406b 100644 --- a/example_rules.yaml +++ b/example_rules.yaml @@ -1,3 +1,6 @@ +version: v1alpha1 +name: test-rule-set +rules: - id: rule:foo match: url: http://foo.bar/<**> diff --git a/go.mod b/go.mod index 60e952717..1fe65e05a 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/stretchr/testify v1.8.2 github.com/tidwall/gjson v1.14.4 github.com/tonglil/opentelemetry-go-datadog-propagator v0.1.0 + github.com/undefinedlabs/go-mpatch v1.0.6 github.com/valyala/fasthttp v1.45.0 github.com/ybbus/httpretry v1.0.2 github.com/yl2chen/cidranger v1.0.2 @@ -85,7 +86,6 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect - github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect github.com/aws/aws-sdk-go v1.44.200 // indirect github.com/aws/aws-sdk-go-v2 v1.17.4 // indirect diff --git a/go.sum b/go.sum index 1ccc1ec7b..2e8ce3355 100644 --- a/go.sum +++ b/go.sum @@ -38,7 +38,6 @@ cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34h cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go v0.109.0 h1:38CZoKGlCnPZjGdyj0ZfpoGae0/wgNfy5F0byyxg0Gk= cloud.google.com/go v0.109.0/go.mod h1:2sYycXt75t/CSB5R9M2wPU1tJmire7AQZTPtITcGBVE= cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= @@ -124,7 +123,6 @@ cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= @@ -219,10 +217,7 @@ cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3Q cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI= cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM= -cloud.google.com/go/iam v0.12.0 h1:DRtTY29b75ciH6Ov1PHb4/iat2CLCvrOm40Q0a6DFpE= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= @@ -244,8 +239,8 @@ cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6 cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.4.0 h1:v+X4EwhHl6xE+TG1XgXj4T1XpKKs7ZevcAJ3FOu0YmY= cloud.google.com/go/longrunning v0.4.0/go.mod h1:eF3Qsw58iX/bkKtVjMTYpH0LRjQ2goDkjkNQTlzq/ZM= +cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= @@ -536,13 +531,9 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= -github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -830,8 +821,6 @@ github.com/dgryski/go-sip13 v0.0.0-20200911182023-62edffca9245/go.mod h1:vAd38F8 github.com/digitalocean/godo v1.78.0/go.mod h1:GBmu8MkjZmNARE7IXRPmkbbnocNN8+uBm0xbEVw2LCs= github.com/digitalocean/godo v1.95.0/go.mod h1:NRpFznZFvhHjBoqZAaOD3khVzsJ3EibzKqFL4R60dmA= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= -github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= @@ -867,8 +856,6 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/elnormous/contenttype v1.0.3 h1:5DrD4LGO3ohab+jPplwE/LlY9JqmkYdssz4Zu7xl8Cs= -github.com/elnormous/contenttype v1.0.3/go.mod h1:ngVcyGGU8pnn4QJ5sL4StrNgc/wmXZXy5IQSBuHOFPg= github.com/elnormous/contenttype v1.0.4 h1:FjmVNkvQOGqSX70yvocph7keC8DtmJaLzTTq6ZOQCI8= github.com/elnormous/contenttype v1.0.4/go.mod h1:5KTOW8m1kdX1dLMiUJeN9szzR2xkngiv2K+RVZwWBbI= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -887,7 +874,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3 h1:xdCVXxEe0Y3FQith+0cj2irwZudqGYvecuLB1HtdexY= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.0 h1:jtLewhRR2vMRNnq2ZZUoCjUlgut+Y0+sDDWPOfwOi1o= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= @@ -933,12 +919,6 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= -github.com/go-co-op/gocron v1.18.1 h1:erHHbIIav46xAV54lnyKKjrKLP+2RgjuDsbwGamBEvI= -github.com/go-co-op/gocron v1.18.1/go.mod h1:UqVyvM90I1q/R1qGEX6cBORI6WArLuEgYlbncLMvzRM= -github.com/go-co-op/gocron v1.19.0 h1:XlPLqNnxnKblmCRLdfcWV1UgbukQaU54QdNeR1jtgak= -github.com/go-co-op/gocron v1.19.0/go.mod h1:UqVyvM90I1q/R1qGEX6cBORI6WArLuEgYlbncLMvzRM= -github.com/go-co-op/gocron v1.20.0 h1:v7CglUeL9kBzwOUVA+5R+2DDPD6BNyujUwlL0zIj/oU= -github.com/go-co-op/gocron v1.20.0/go.mod h1:UqVyvM90I1q/R1qGEX6cBORI6WArLuEgYlbncLMvzRM= github.com/go-co-op/gocron v1.20.1 h1:wCGabII3xf/NrrYeOzJ4voLBBtA5k7Rb99+7l/iiu+g= github.com/go-co-op/gocron v1.20.1/go.mod h1:UqVyvM90I1q/R1qGEX6cBORI6WArLuEgYlbncLMvzRM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -977,15 +957,15 @@ github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuA github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= github.com/go-openapi/runtime v0.23.1/go.mod h1:AKurw9fNre+h3ELZfk6ILsfvPN+bvvlaU/M9q/r9hpk= @@ -1051,10 +1031,6 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= -github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.1 h1:lEs5Ob+oOG/Ze199njvzHbhn6p9T+h64F5hRj69iTTo= -github.com/goccy/go-json v0.10.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= @@ -1064,12 +1040,8 @@ github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6 github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofiber/adaptor/v2 v2.1.32 h1:94cL79U4ekq78TmqfXPrulMWkpfPxqzHimUc/B+jmkY= -github.com/gofiber/adaptor/v2 v2.1.32/go.mod h1:aX4qfSo+1AJYIWnLL1Mx3EQ6znC6WW46MqFQruUQE6c= github.com/gofiber/adaptor/v2 v2.2.0 h1:MGz/LW8l+avBER56v87dzcH+mqi+90CX00k8Lv8QQz8= github.com/gofiber/adaptor/v2 v2.2.0/go.mod h1:A51dt83PyWNUZp/9Op4FBI2qxDUceg15hWtf8Vk9ZOU= -github.com/gofiber/fiber/v2 v2.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8= -github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc= github.com/gofiber/fiber/v2 v2.43.0 h1:yit3E4kHf178B60p5CQBa/3v+WVuziWMa/G2ZNyLJB0= github.com/gofiber/fiber/v2 v2.43.0/go.mod h1:mpS1ZNE5jU+u+BA4FbM+KKnUzJ4wzTK+FT2tG3tU+6I= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -1130,7 +1102,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -1141,8 +1112,6 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/cel-go v0.13.0 h1:z+8OBOcmh7IeKyqwT/6IlnMvy621fYUqnTVPEdegGlU= -github.com/google/cel-go v0.13.0/go.mod h1:K2hpQgEjDp18J76a2DKFRlPBPpgRZgi6EbnpDgIhJ8s= github.com/google/cel-go v0.14.0 h1:LFobwuUDslWUHdQ48SXVXvQgPH2X1XVhsgOGNioAEZ4= github.com/google/cel-go v0.14.0/go.mod h1:YzWEoI07MC/a/wj9in8GeVatqfypkldgBlwXh9bCwqY= github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= @@ -1226,7 +1195,6 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A= github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= @@ -1256,7 +1224,6 @@ github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWf github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= @@ -1348,13 +1315,9 @@ github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/instana/go-otel-exporter v0.0.0-20220908102301-52c5d8dbfd86 h1:5akgRofYR1vxat/Vc3r6gWfMMGhZzhpaT4piqSDVXOU= -github.com/instana/go-otel-exporter v0.0.0-20220908102301-52c5d8dbfd86/go.mod h1:chO0kaNOIV+bhh+eYRBiSShhuOHMV6HHQYgVo/7xxAs= github.com/instana/go-otel-exporter v1.0.0 h1:s7PPvvB8xcSRNaXpgjYpBQWnFZRAqGGJZPkQ/j6RNjU= github.com/instana/go-otel-exporter v1.0.0/go.mod h1:chO0kaNOIV+bhh+eYRBiSShhuOHMV6HHQYgVo/7xxAs= github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= @@ -1416,8 +1379,6 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= -github.com/johannesboyne/gofakes3 v0.0.0-20230129080941-f6a8a9ae6fd3 h1:aTscQmvmU/1AS3PqVaNtUtJUwyMexxqVErkhwsWoEpw= -github.com/johannesboyne/gofakes3 v0.0.0-20230129080941-f6a8a9ae6fd3/go.mod h1:Cnosl0cRZIfKjTMuH49sQog2LeNsU5Hf4WnPIDWIDV0= github.com/johannesboyne/gofakes3 v0.0.0-20230310080033-c0edf658332b h1:dRMf9/2xfp4tky4wnvFxsMQz78n92VeqDIxR27uass4= github.com/johannesboyne/gofakes3 v0.0.0-20230310080033-c0edf658332b/go.mod h1:Cnosl0cRZIfKjTMuH49sQog2LeNsU5Hf4WnPIDWIDV0= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= @@ -1451,9 +1412,6 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= -github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= @@ -1468,8 +1426,6 @@ github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= -github.com/knadh/koanf/v2 v2.0.0 h1:XPQ5ilNnwnNaHrfQ1YpTVhUAjcGHnEKA+lRpipQv02Y= -github.com/knadh/koanf/v2 v2.0.0/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g= github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= @@ -1714,7 +1670,6 @@ github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= @@ -1811,8 +1766,8 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -1829,14 +1784,11 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5P github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= -github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 h1:WCcC4vZDS1tYNxjWlwRJZQy28r8CMoggKnxNzxsVDMQ= -github.com/santhosh-tekuri/jsonschema/v5 v5.2.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo= github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= -github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo= github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= @@ -1887,8 +1839,6 @@ github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKv github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= @@ -1942,7 +1892,6 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= @@ -1956,14 +1905,14 @@ github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/undefinedlabs/go-mpatch v1.0.6 h1:h8q5ORH/GaOE1Se1DMhrOyljXZEhRcROO7agMqWXCOY= +github.com/undefinedlabs/go-mpatch v1.0.6/go.mod h1:TyJZDQ/5AgyN7FSLiBJ8RO9u2c6wbtRvK827b6AVqY4= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q= -github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA= github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= @@ -2195,7 +2144,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -2204,7 +2152,6 @@ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= @@ -2223,8 +2170,6 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230124195608-d38c7dcee874/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= @@ -2337,7 +2282,6 @@ golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220805013720-a33c5aa5df48/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220921155015-db77216a4ee9/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= @@ -2349,8 +2293,6 @@ golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmL golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2383,7 +2325,6 @@ golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= @@ -2555,7 +2496,6 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2568,7 +2508,6 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= @@ -2585,7 +2524,6 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -2692,10 +2630,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2773,7 +2708,6 @@ google.golang.org/api v0.104.0/go.mod h1:JCspTXJbBxa5ySXw4UgUqVer7DfVxbvc/CTUFqA google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU= google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= @@ -2920,14 +2854,7 @@ google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio= google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488 h1:QQF+HdiI4iocoxUjjpLgvTYDHKm99C/VtTBFnfiCJos= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683 h1:khxVcsk/FhnzxMKOyD+TDGwjbEOpcPuIpmafPGFmhMA= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd h1:sLpv7bNL1AsX3fdnWh9WVh7ejIzXdOc1RRHGeAmeStU= google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -2973,7 +2900,6 @@ google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.52.1/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= @@ -2992,12 +2918,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= -google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= @@ -3066,10 +2987,6 @@ k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= k8s.io/api v0.26.1/go.mod h1:xd/GBNgR0f707+ATNyPmQ1oyKSgndzXij81FzWGsejg= -k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ= -k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= -k8s.io/api v0.26.3 h1:emf74GIQMTik01Aum9dPP0gAypL8JTLl/lHa4V9RFSU= -k8s.io/api v0.26.3/go.mod h1:PXsqwPMXBSBcL1lJ9CYDKy7kIReUydukS5JiRlxC3qE= k8s.io/api v0.27.0 h1:2owttiA8Oa+J3idFeq8TSnNpm4y6AOGPI3PDbIpp2cE= k8s.io/api v0.27.0/go.mod h1:Wl+QRvQlh+T8SK5f4F6YBhhyH6hrFO08nl74xZb1MUE= k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= @@ -3079,10 +2996,6 @@ k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= -k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ= -k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= -k8s.io/apimachinery v0.26.3 h1:dQx6PNETJ7nODU3XPtrwkfuubs6w7sX0M8n61zHIV/k= -k8s.io/apimachinery v0.26.3/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= k8s.io/apimachinery v0.27.0 h1:vEyy/PVMbPMCPutrssCVHCf0JNZ0Px+YqPi82K2ALlk= k8s.io/apimachinery v0.27.0/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM= k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= @@ -3095,10 +3008,6 @@ k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/client-go v0.26.1/go.mod h1:IWNSglg+rQ3OcvDkhY6+QLeasV4OYHDjdqeWkDQZwGE= -k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI= -k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU= -k8s.io/client-go v0.26.3 h1:k1UY+KXfkxV2ScEL3gilKcF7761xkYsSD6BC9szIu8s= -k8s.io/client-go v0.26.3/go.mod h1:ZPNu9lm8/dbRIPAgteN30RSXea6vrCpFvq+MateTUuQ= k8s.io/client-go v0.27.0 h1:DyZS1fJkv73tEy7rWv4VF6NwGeJ7SKvNaLRXZBYLA+4= k8s.io/client-go v0.27.0/go.mod h1:XVEmpNnM+4JYO3EENoFV/ZDv3KxKVJUnzGo70avk+C4= k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= @@ -3131,8 +3040,8 @@ k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2R k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= -k8s.io/kube-openapi v0.0.0-20221207184640-f3cff1453715 h1:tBEbstoM+K0FiBV5KGAKQ0kuvf54v/hwpldiJt69w1s= k8s.io/kube-openapi v0.0.0-20221207184640-f3cff1453715/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= @@ -3140,7 +3049,6 @@ k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/internal/handler/decision/handler.go b/internal/handler/decision/handler.go index 5474d67f4..2eb958bb0 100644 --- a/internal/handler/decision/handler.go +++ b/internal/handler/decision/handler.go @@ -25,13 +25,13 @@ import ( fiberxforwarded "github.com/dadrus/heimdall/internal/fiber/middleware/xfmphu" "github.com/dadrus/heimdall/internal/handler/requestcontext" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x" "github.com/dadrus/heimdall/internal/x/errorchain" ) type Handler struct { - r rules.Repository + r rule.Repository s heimdall.JWTSigner code int } @@ -40,7 +40,7 @@ type handlerArgs struct { fx.In App *fiber.App `name:"decision"` - RulesRepository rules.Repository + RulesRepository rule.Repository Config *config.Configuration Signer heimdall.JWTSigner Logger zerolog.Logger @@ -73,19 +73,19 @@ func (h *Handler) decisions(c *fiber.Ctx) error { reqURL := fiberxforwarded.RequestURL(c.UserContext()) method := fiberxforwarded.RequestMethod(c.UserContext()) - rule, err := h.r.FindRule(reqURL) + rul, err := h.r.FindRule(reqURL) if err != nil { return err } - if !rule.MatchesMethod(method) { + if !rul.MatchesMethod(method) { return errorchain.NewWithMessagef(heimdall.ErrMethodNotAllowed, "rule doesn't match %s method", method) } reqCtx := requestcontext.New(c, method, reqURL, h.s) - _, err = rule.Execute(reqCtx) + _, err = rul.Execute(reqCtx) if err != nil { return err } diff --git a/internal/handler/decision/handler_test.go b/internal/handler/decision/handler_test.go index 51af6da14..6321d654b 100644 --- a/internal/handler/decision/handler_test.go +++ b/internal/handler/decision/handler_test.go @@ -32,7 +32,6 @@ import ( "github.com/dadrus/heimdall/internal/cache/mocks" "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - mocks2 "github.com/dadrus/heimdall/internal/rules/mocks" mocks4 "github.com/dadrus/heimdall/internal/rules/rule/mocks" ) @@ -44,7 +43,7 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { uc string serviceConf config.ServiceConfig createRequest func(t *testing.T) *http.Request - configureMocks func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) + configureMocks func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) assertResponse func(t *testing.T, err error, response *http.Response) }{ { @@ -54,10 +53,10 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodGet, "/", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - repository.On("FindRule", mock.Anything).Return(nil, heimdall.ErrNoRuleFound) + repository.EXPECT().FindRule(mock.Anything).Return(nil, heimdall.ErrNoRuleFound) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -77,12 +76,11 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodPost, "/", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(false) - - repository.On("FindRule", mock.Anything).Return(rule, nil) + rule.EXPECT().MatchesMethod(http.MethodPost).Return(false) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -102,13 +100,13 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodPost, "/", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything).Return(nil, heimdall.ErrAuthentication) + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything).Return(nil, heimdall.ErrAuthentication) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -128,17 +126,17 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodPost, "/", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.SetPipelineError(heimdall.ErrAuthorization) return true })).Return(nil, nil) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -169,18 +167,18 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") return true })).Return(&url.URL{Scheme: "http", Host: "heimdall.test.local", Path: "/foobar"}, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.Scheme == "http" && reqURL.Host == "heimdall.test.local" && reqURL.Path == "/foobar" })).Return(rule, nil) }, @@ -221,18 +219,18 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") return true })).Return(&url.URL{Scheme: "https", Host: "test.com", Path: "/bar"}, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.Scheme == "http" && reqURL.Host == "heimdall.test.local" && reqURL.Path == "/foobar" })).Return(rule, nil) }, @@ -274,18 +272,18 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") return true })).Return(&url.URL{Scheme: "http", Host: "heimdall.test.local", Path: "/foobar"}, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.Scheme == "http" && reqURL.Host == "heimdall.test.local" && reqURL.Path == "/foobar" })).Return(rule, nil) }, @@ -324,14 +322,14 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodGet).Return(true) - rule.On("Execute", mock.Anything). + rule.EXPECT().MatchesMethod(http.MethodGet).Return(true) + rule.EXPECT().Execute(mock.Anything). Return(&url.URL{Scheme: "http", Host: "heimdall.test.local", Path: "/foobar"}, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.Scheme == "http" && reqURL.Host == "heimdall.test.local" && reqURL.Path == "/foobar" })).Return(rule, nil) }, @@ -358,14 +356,14 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything). + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything). Return(&url.URL{Scheme: "http", Host: "test.com", Path: "/foobar"}, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.Scheme == "http" && reqURL.Host == "test.com" && reqURL.Path == "/foobar" })).Return(rule, nil) }, @@ -392,14 +390,14 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything). + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything). Return(&url.URL{Scheme: "http", Host: "heimdall.test.local", Path: "/bar"}, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.Scheme == "http" && reqURL.Host == "heimdall.test.local" && reqURL.Path == "bar" })).Return(rule, nil) }, @@ -426,14 +424,14 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything). + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything). Return(&url.URL{Scheme: "https", Host: "heimdall.test.local", Path: "/foobar"}, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.Scheme == "https" && reqURL.Host == "heimdall.test.local" && reqURL.Path == "/foobar" })).Return(rule, nil) }, @@ -463,14 +461,14 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPatch).Return(true) - rule.On("Execute", mock.Anything). + rule.EXPECT().MatchesMethod(http.MethodPatch).Return(true) + rule.EXPECT().Execute(mock.Anything). Return(&url.URL{Scheme: "https", Host: "test.com", Path: "/bar"}, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.Scheme == "https" && reqURL.Host == "test.com" && reqURL.Path == "bar" })).Return(rule, nil) }, @@ -486,8 +484,8 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { // GIVEN conf := &config.Configuration{Serve: config.ServeConfig{Decision: tc.serviceConf}} cch := &mocks.MockCache{} - repo := &mocks2.MockRepository{} - rule := &mocks4.MockRule{} + repo := mocks4.NewRepositoryMock(t) + rule := mocks4.NewRuleMock(t) logger := log.Logger tc.configureMocks(t, repo, rule) @@ -518,8 +516,6 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { } tc.assertResponse(t, err, resp) - repo.AssertExpectations(t) - rule.AssertExpectations(t) }) } } diff --git a/internal/handler/envoyextauth/grpcv3/handler.go b/internal/handler/envoyextauth/grpcv3/handler.go index 90764f4a5..d4f19e519 100644 --- a/internal/handler/envoyextauth/grpcv3/handler.go +++ b/internal/handler/envoyextauth/grpcv3/handler.go @@ -23,12 +23,12 @@ import ( "github.com/rs/zerolog" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x/errorchain" ) type Handler struct { - r rules.Repository + r rule.Repository s heimdall.JWTSigner } @@ -38,17 +38,17 @@ func (h *Handler) Check(ctx context.Context, req *envoy_auth.CheckRequest) (*env reqCtx := NewRequestContext(ctx, req, h.s) - rule, err := h.r.FindRule(reqCtx.RequestURL()) + rul, err := h.r.FindRule(reqCtx.RequestURL()) if err != nil { return nil, err } - if !rule.MatchesMethod(reqCtx.RequestMethod()) { + if !rul.MatchesMethod(reqCtx.RequestMethod()) { return nil, errorchain.NewWithMessagef(heimdall.ErrMethodNotAllowed, "rule doesn't match %s method", reqCtx.RequestMethod()) } - _, err = rule.Execute(reqCtx) + _, err = rul.Execute(reqCtx) if err != nil { return nil, err } diff --git a/internal/handler/envoyextauth/grpcv3/handler_test.go b/internal/handler/envoyextauth/grpcv3/handler_test.go index 8372e7997..3a98350db 100644 --- a/internal/handler/envoyextauth/grpcv3/handler_test.go +++ b/internal/handler/envoyextauth/grpcv3/handler_test.go @@ -21,22 +21,21 @@ import ( "github.com/dadrus/heimdall/internal/cache/mocks" "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - mocks2 "github.com/dadrus/heimdall/internal/rules/mocks" - mocks4 "github.com/dadrus/heimdall/internal/rules/rule/mocks" + mocks2 "github.com/dadrus/heimdall/internal/rules/rule/mocks" ) func TestHandleDecisionEndpointRequest(t *testing.T) { for _, tc := range []struct { uc string - configureMocks func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) + configureMocks func(t *testing.T, repository *mocks2.RepositoryMock, rule *mocks2.RuleMock) assertResponse func(t *testing.T, err error, response *envoy_auth.CheckResponse) }{ { uc: "no rules configured", - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks2.RepositoryMock, rule *mocks2.RuleMock) { t.Helper() - repository.On("FindRule", mock.Anything).Return(nil, heimdall.ErrNoRuleFound) + repository.EXPECT().FindRule(mock.Anything).Return(nil, heimdall.ErrNoRuleFound) }, assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { t.Helper() @@ -53,12 +52,12 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { }, { uc: "rule doesn't match method", - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks2.RepositoryMock, rule *mocks2.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(false) + rule.EXPECT().MatchesMethod(http.MethodPost).Return(false) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { t.Helper() @@ -75,13 +74,13 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { }, { uc: "rule execution fails with authentication error", - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks2.RepositoryMock, rule *mocks2.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything).Return(nil, heimdall.ErrAuthentication) + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything).Return(nil, heimdall.ErrAuthentication) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { t.Helper() @@ -98,17 +97,17 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { }, { uc: "rule execution fails with authorization error", - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks2.RepositoryMock, rule *mocks2.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx heimdall.Context) bool { + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx heimdall.Context) bool { ctx.SetPipelineError(heimdall.ErrAuthorization) return true })).Return(nil, nil) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { t.Helper() @@ -125,17 +124,17 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { }, { uc: "rule execution fails with a redirect", - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks2.RepositoryMock, rule *mocks2.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything).Return(nil, &heimdall.RedirectError{ + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything).Return(nil, &heimdall.RedirectError{ Message: "test redirect", Code: http.StatusFound, RedirectTo: "http://foo.bar", }) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { t.Helper() @@ -154,13 +153,13 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { }, { uc: "rule execution succeeds", - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks2.RepositoryMock, rule *mocks2.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything).Return(nil, nil) + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything).Return(nil, nil) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { t.Helper() @@ -175,8 +174,10 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { }, { uc: "server panics and error does not contain traces", - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks2.RepositoryMock, rule *mocks2.RuleMock) { t.Helper() + + repository.EXPECT().FindRule(mock.Anything).Panic("wuff") }, assertResponse: func(t *testing.T, err error, response *envoy_auth.CheckResponse) { t.Helper() @@ -195,8 +196,8 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { require.NoError(t, err) conf := &config.Configuration{Metrics: config.MetricsConfig{Enabled: true}} cch := &mocks.MockCache{} - repo := &mocks2.MockRepository{} - rule := &mocks4.MockRule{} + repo := mocks2.NewRepositoryMock(t) + rule := mocks2.NewRuleMock(t) tc.configureMocks(t, repo, rule) @@ -226,8 +227,6 @@ func TestHandleDecisionEndpointRequest(t *testing.T) { // THEN tc.assertResponse(t, err, resp) - repo.AssertExpectations(t) - rule.AssertExpectations(t) }) } } diff --git a/internal/handler/envoyextauth/grpcv3/module.go b/internal/handler/envoyextauth/grpcv3/module.go index fe381a8ea..ef67268a7 100644 --- a/internal/handler/envoyextauth/grpcv3/module.go +++ b/internal/handler/envoyextauth/grpcv3/module.go @@ -27,7 +27,7 @@ import ( "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/handler/listener" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/rules/rule" ) var Module = fx.Options( // nolint: gochecknoglobals @@ -40,7 +40,7 @@ type hooksArgs struct { Lifecycle fx.Lifecycle Config *config.Configuration Logger zerolog.Logger - Repository rules.Repository + Repository rule.Repository Signer heimdall.JWTSigner Registerer prometheus.Registerer Cache cache.Cache diff --git a/internal/handler/envoyextauth/grpcv3/service.go b/internal/handler/envoyextauth/grpcv3/service.go index 61276df84..850279dca 100644 --- a/internal/handler/envoyextauth/grpcv3/service.go +++ b/internal/handler/envoyextauth/grpcv3/service.go @@ -35,7 +35,7 @@ import ( loggermiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/logger" prometheusmiddleware "github.com/dadrus/heimdall/internal/handler/envoyextauth/grpcv3/middleware/prometheus" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/rules/rule" ) func newService( @@ -43,7 +43,7 @@ func newService( registerer prometheus.Registerer, cch cache.Cache, logger zerolog.Logger, - repository rules.Repository, + repository rule.Repository, signer heimdall.JWTSigner, ) *grpc.Server { service := conf.Serve.Decision diff --git a/internal/handler/proxy/handler.go b/internal/handler/proxy/handler.go index 49931bd34..cdd365866 100644 --- a/internal/handler/proxy/handler.go +++ b/internal/handler/proxy/handler.go @@ -27,12 +27,12 @@ import ( fiberxforwarded "github.com/dadrus/heimdall/internal/fiber/middleware/xfmphu" "github.com/dadrus/heimdall/internal/handler/requestcontext" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules" + "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x/errorchain" ) type Handler struct { - r rules.Repository + r rule.Repository s heimdall.JWTSigner t time.Duration } @@ -41,7 +41,7 @@ type handlerArgs struct { fx.In App *fiber.App `name:"proxy"` - RulesRepository rules.Repository + RulesRepository rule.Repository Config *config.Configuration Signer heimdall.JWTSigner Logger zerolog.Logger @@ -72,19 +72,19 @@ func (h *Handler) proxy(c *fiber.Ctx) error { reqURL := fiberxforwarded.RequestURL(c.UserContext()) method := fiberxforwarded.RequestMethod(c.UserContext()) - rule, err := h.r.FindRule(reqURL) + rul, err := h.r.FindRule(reqURL) if err != nil { return err } - if !rule.MatchesMethod(method) { + if !rul.MatchesMethod(method) { return errorchain.NewWithMessagef(heimdall.ErrMethodNotAllowed, - "rule (id=%s, src=%s) doesn't match %s method", rule.ID(), rule.SrcID(), method) + "rule (id=%s, src=%s) doesn't match %s method", rul.ID(), rul.SrcID(), method) } reqCtx := requestcontext.New(c, method, reqURL, h.s) - upstreamURL, err := rule.Execute(reqCtx) + upstreamURL, err := rul.Execute(reqCtx) if err != nil { return err } diff --git a/internal/handler/proxy/handler_test.go b/internal/handler/proxy/handler_test.go index b82299c2a..7083d1797 100644 --- a/internal/handler/proxy/handler_test.go +++ b/internal/handler/proxy/handler_test.go @@ -36,7 +36,6 @@ import ( "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/handler/requestcontext" "github.com/dadrus/heimdall/internal/heimdall" - mocks2 "github.com/dadrus/heimdall/internal/rules/mocks" mocks4 "github.com/dadrus/heimdall/internal/rules/rule/mocks" "github.com/dadrus/heimdall/internal/x" ) @@ -77,7 +76,7 @@ func TestHandleProxyEndpointRequest(t *testing.T) { uc string serviceConf config.ServiceConfig createRequest func(t *testing.T) *http.Request - configureMocks func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) + configureMocks func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) instructUpstream func(t *testing.T) assertResponse func(t *testing.T, err error, response *http.Response) }{ @@ -88,10 +87,10 @@ func TestHandleProxyEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodGet, "http://heimdall.test.local/foobar", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - repository.On("FindRule", mock.Anything).Return(nil, heimdall.ErrNoRuleFound) + repository.EXPECT().FindRule(mock.Anything).Return(nil, heimdall.ErrNoRuleFound) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -113,14 +112,14 @@ func TestHandleProxyEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodPost, "http://heimdall.test.local/foobar", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(false) - rule.On("ID").Return("test") - rule.On("SrcID").Return("test") + rule.EXPECT().MatchesMethod(http.MethodPost).Return(false) + rule.EXPECT().ID().Return("test") + rule.EXPECT().SrcID().Return("test") - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -142,13 +141,13 @@ func TestHandleProxyEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodPost, "http://heimdall.test.local/foobar", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything, mock.Anything).Return(nil, nil) + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything).Return(nil, nil) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -170,13 +169,13 @@ func TestHandleProxyEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodPost, "http://heimdall.test.local/foobar", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.Anything).Return(nil, heimdall.ErrAuthentication) + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.Anything).Return(nil, heimdall.ErrAuthentication) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -198,17 +197,17 @@ func TestHandleProxyEndpointRequest(t *testing.T) { return httptest.NewRequest(http.MethodPost, "http://heimdall.test.local/foobar", nil) }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", "POST").Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { ctx.SetPipelineError(heimdall.ErrAuthorization) return true })).Return(upstreamURL, nil) - repository.On("FindRule", mock.Anything).Return(rule, nil) + repository.EXPECT().FindRule(mock.Anything).Return(rule, nil) }, assertResponse: func(t *testing.T, err error, response *http.Response) { t.Helper() @@ -241,18 +240,18 @@ func TestHandleProxyEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") return true })).Return(upstreamURL, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.String() == "http://heimdall.test.local/foobar" })).Return(rule, nil) }, @@ -315,18 +314,18 @@ func TestHandleProxyEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodGet).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { + rule.EXPECT().MatchesMethod(http.MethodGet).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") return true })).Return(upstreamURL, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.String() == "http://heimdall.test.local/foobar" })).Return(rule, nil) }, @@ -388,18 +387,18 @@ func TestHandleProxyEndpointRequest(t *testing.T) { return req }, - configureMocks: func(t *testing.T, repository *mocks2.MockRepository, rule *mocks4.MockRule) { + configureMocks: func(t *testing.T, repository *mocks4.RepositoryMock, rule *mocks4.RuleMock) { t.Helper() - rule.On("MatchesMethod", http.MethodPost).Return(true) - rule.On("Execute", mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { + rule.EXPECT().MatchesMethod(http.MethodPost).Return(true) + rule.EXPECT().Execute(mock.MatchedBy(func(ctx *requestcontext.RequestContext) bool { ctx.AddHeaderForUpstream("X-Foo-Bar", "baz") ctx.AddCookieForUpstream("X-Bar-Foo", "zab") return true })).Return(upstreamURL, nil) - repository.On("FindRule", mock.MatchedBy(func(reqURL *url.URL) bool { + repository.EXPECT().FindRule(mock.MatchedBy(func(reqURL *url.URL) bool { return reqURL.String() == "http://heimdall.test.local/barfoo" })).Return(rule, nil) }, @@ -455,8 +454,8 @@ func TestHandleProxyEndpointRequest(t *testing.T) { conf := &config.Configuration{Serve: config.ServeConfig{Proxy: tc.serviceConf}} cch := &mocks.MockCache{} - repo := &mocks2.MockRepository{} - rule := &mocks4.MockRule{} + repo := mocks4.NewRepositoryMock(t) + rule := mocks4.NewRuleMock(t) logger := log.Logger tc.configureMocks(t, repo, rule) @@ -488,8 +487,6 @@ func TestHandleProxyEndpointRequest(t *testing.T) { } tc.assertResponse(t, err, resp) - repo.AssertExpectations(t) - rule.AssertExpectations(t) }) } } diff --git a/internal/rules/composite_error_handler_test.go b/internal/rules/composite_error_handler_test.go index d4f751701..4f2e6f78f 100644 --- a/internal/rules/composite_error_handler_test.go +++ b/internal/rules/composite_error_handler_test.go @@ -34,11 +34,11 @@ func TestCompositeErrorHandlerExecutionWithFallback(t *testing.T) { ctx := &mocks.MockContext{} ctx.On("AppContext").Return(context.Background()) - eh1 := &rulemocks.MockErrorHandler{} - eh1.On("Execute", ctx, testsupport.ErrTestPurpose).Return(false, nil) + eh1 := rulemocks.NewErrorHandlerMock(t) + eh1.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(false, nil) - eh2 := &rulemocks.MockErrorHandler{} - eh2.On("Execute", ctx, testsupport.ErrTestPurpose).Return(true, nil) + eh2 := rulemocks.NewErrorHandlerMock(t) + eh2.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(true, nil) eh := compositeErrorHandler{eh1, eh2} @@ -48,9 +48,6 @@ func TestCompositeErrorHandlerExecutionWithFallback(t *testing.T) { // THEN assert.NoError(t, err) assert.True(t, ok) - - eh1.AssertExpectations(t) - eh2.AssertExpectations(t) } func TestCompositeErrorHandlerExecutionWithoutFallback(t *testing.T) { @@ -60,10 +57,10 @@ func TestCompositeErrorHandlerExecutionWithoutFallback(t *testing.T) { ctx := &mocks.MockContext{} ctx.On("AppContext").Return(context.Background()) - eh1 := &rulemocks.MockErrorHandler{} - eh1.On("Execute", ctx, testsupport.ErrTestPurpose).Return(true, nil) + eh1 := rulemocks.NewErrorHandlerMock(t) + eh1.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(true, nil) - eh2 := &rulemocks.MockErrorHandler{} + eh2 := rulemocks.NewErrorHandlerMock(t) eh := compositeErrorHandler{eh1, eh2} @@ -73,9 +70,6 @@ func TestCompositeErrorHandlerExecutionWithoutFallback(t *testing.T) { // THEN assert.NoError(t, err) assert.True(t, ok) - - eh1.AssertExpectations(t) - eh2.AssertExpectations(t) } func TestCompositeErrorHandlerExecutionWithNoApplicableErrorHandler(t *testing.T) { @@ -85,11 +79,11 @@ func TestCompositeErrorHandlerExecutionWithNoApplicableErrorHandler(t *testing.T ctx := &mocks.MockContext{} ctx.On("AppContext").Return(context.Background()) - eh1 := &rulemocks.MockErrorHandler{} - eh1.On("Execute", ctx, testsupport.ErrTestPurpose).Return(false, nil) + eh1 := rulemocks.NewErrorHandlerMock(t) + eh1.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(false, nil) - eh2 := &rulemocks.MockErrorHandler{} - eh2.On("Execute", ctx, testsupport.ErrTestPurpose).Return(false, nil) + eh2 := rulemocks.NewErrorHandlerMock(t) + eh2.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(false, nil) eh := compositeErrorHandler{eh1, eh2} @@ -99,7 +93,4 @@ func TestCompositeErrorHandlerExecutionWithNoApplicableErrorHandler(t *testing.T // THEN assert.Error(t, err) assert.False(t, ok) - - eh1.AssertExpectations(t) - eh2.AssertExpectations(t) } diff --git a/internal/rules/composite_subject_creator_test.go b/internal/rules/composite_subject_creator_test.go index 430f62e26..7fe2ca96d 100644 --- a/internal/rules/composite_subject_creator_test.go +++ b/internal/rules/composite_subject_creator_test.go @@ -34,19 +34,19 @@ func TestCompositeSubjectCreatorExecution(t *testing.T) { for _, tc := range []struct { uc string - configureMocks func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectCreator, - second *rulemocks.MockSubjectCreator, sub *subject.Subject) + configureMocks func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectCreatorMock, + second *rulemocks.SubjectCreatorMock, sub *subject.Subject) assert func(t *testing.T, err error) }{ { uc: "with fallback due to argument error on first authenticator", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectCreator, - second *rulemocks.MockSubjectCreator, sub *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectCreatorMock, + second *rulemocks.SubjectCreatorMock, sub *subject.Subject, ) { t.Helper() - first.On("Execute", ctx).Return(nil, heimdall.ErrArgument) - second.On("Execute", ctx).Return(sub, nil) + first.EXPECT().Execute(ctx).Return(nil, heimdall.ErrArgument) + second.EXPECT().Execute(ctx).Return(sub, nil) }, assert: func(t *testing.T, err error) { t.Helper() @@ -56,13 +56,13 @@ func TestCompositeSubjectCreatorExecution(t *testing.T) { }, { uc: "with fallback and both authenticators returning argument error", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectCreator, - second *rulemocks.MockSubjectCreator, _ *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectCreatorMock, + second *rulemocks.SubjectCreatorMock, _ *subject.Subject, ) { t.Helper() - first.On("Execute", ctx).Return(nil, heimdall.ErrArgument) - second.On("Execute", ctx).Return(nil, heimdall.ErrArgument) + first.EXPECT().Execute(ctx).Return(nil, heimdall.ErrArgument) + second.EXPECT().Execute(ctx).Return(nil, heimdall.ErrArgument) }, assert: func(t *testing.T, err error) { t.Helper() @@ -73,13 +73,13 @@ func TestCompositeSubjectCreatorExecution(t *testing.T) { }, { uc: "without fallback as first authenticator returns an error not equal to argument error", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectCreator, - second *rulemocks.MockSubjectCreator, _ *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectCreatorMock, + second *rulemocks.SubjectCreatorMock, _ *subject.Subject, ) { t.Helper() - first.On("Execute", ctx).Return(nil, testsupport.ErrTestPurpose) - first.On("IsFallbackOnErrorAllowed").Return(false) + first.EXPECT().Execute(ctx).Return(nil, testsupport.ErrTestPurpose) + first.EXPECT().IsFallbackOnErrorAllowed().Return(false) }, assert: func(t *testing.T, err error) { t.Helper() @@ -90,14 +90,14 @@ func TestCompositeSubjectCreatorExecution(t *testing.T) { }, { uc: "with fallback on error since first authenticator allows fallback on any error", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectCreator, - second *rulemocks.MockSubjectCreator, sub *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectCreatorMock, + second *rulemocks.SubjectCreatorMock, sub *subject.Subject, ) { t.Helper() - first.On("Execute", ctx).Return(nil, testsupport.ErrTestPurpose) - first.On("IsFallbackOnErrorAllowed").Return(true) - second.On("Execute", ctx).Return(sub, nil) + first.EXPECT().Execute(ctx).Return(nil, testsupport.ErrTestPurpose) + first.EXPECT().IsFallbackOnErrorAllowed().Return(true) + second.EXPECT().Execute(ctx).Return(sub, nil) }, assert: func(t *testing.T, err error) { t.Helper() @@ -113,8 +113,8 @@ func TestCompositeSubjectCreatorExecution(t *testing.T) { ctx := &mocks.MockContext{} ctx.On("AppContext").Return(context.Background()) - auth1 := &rulemocks.MockSubjectCreator{} - auth2 := &rulemocks.MockSubjectCreator{} + auth1 := rulemocks.NewSubjectCreatorMock(t) + auth2 := rulemocks.NewSubjectCreatorMock(t) tc.configureMocks(t, ctx, auth1, auth2, sub) auth := compositeSubjectCreator{auth1, auth2} @@ -129,8 +129,6 @@ func TestCompositeSubjectCreatorExecution(t *testing.T) { assert.Equal(t, sub, rSub) } - auth1.AssertExpectations(t) - auth2.AssertExpectations(t) ctx.AssertExpectations(t) }) } diff --git a/internal/rules/composite_subject_handler_test.go b/internal/rules/composite_subject_handler_test.go index 2276cad9b..cc5bcad22 100644 --- a/internal/rules/composite_subject_handler_test.go +++ b/internal/rules/composite_subject_handler_test.go @@ -19,19 +19,19 @@ func TestCompositeSubjectHandlerExecution(t *testing.T) { for _, tc := range []struct { uc string - configureMocks func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectHandler, - second *rulemocks.MockSubjectHandler, sub *subject.Subject) + configureMocks func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectHandlerMock, + second *rulemocks.SubjectHandlerMock, sub *subject.Subject) assert func(t *testing.T, err error) }{ { uc: "All succeeded", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectHandler, - second *rulemocks.MockSubjectHandler, sub *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectHandlerMock, + second *rulemocks.SubjectHandlerMock, sub *subject.Subject, ) { t.Helper() - first.On("Execute", ctx, sub).Return(nil) - second.On("Execute", ctx, sub).Return(nil) + first.EXPECT().Execute(ctx, sub).Return(nil) + second.EXPECT().Execute(ctx, sub).Return(nil) }, assert: func(t *testing.T, err error) { t.Helper() @@ -41,13 +41,13 @@ func TestCompositeSubjectHandlerExecution(t *testing.T) { }, { uc: "First fails without pipeline continuation", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectHandler, - second *rulemocks.MockSubjectHandler, sub *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectHandlerMock, + second *rulemocks.SubjectHandlerMock, sub *subject.Subject, ) { t.Helper() - first.On("Execute", ctx, sub).Return(errors.New("first fails")) // nolint: goerr113 - first.On("ContinueOnError").Return(false) + first.EXPECT().Execute(ctx, sub).Return(errors.New("first fails")) // nolint: goerr113 + first.EXPECT().ContinueOnError().Return(false) }, assert: func(t *testing.T, err error) { t.Helper() @@ -58,14 +58,14 @@ func TestCompositeSubjectHandlerExecution(t *testing.T) { }, { uc: "First fails with pipeline continuation, second succeeds", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectHandler, - second *rulemocks.MockSubjectHandler, sub *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectHandlerMock, + second *rulemocks.SubjectHandlerMock, sub *subject.Subject, ) { t.Helper() - first.On("Execute", ctx, sub).Return(errors.New("first fails")) // nolint: goerr113 - first.On("ContinueOnError").Return(true) - second.On("Execute", ctx, sub).Return(nil) + first.EXPECT().Execute(ctx, sub).Return(errors.New("first fails")) // nolint: goerr113 + first.EXPECT().ContinueOnError().Return(true) + second.EXPECT().Execute(ctx, sub).Return(nil) }, assert: func(t *testing.T, err error) { t.Helper() @@ -75,14 +75,14 @@ func TestCompositeSubjectHandlerExecution(t *testing.T) { }, { uc: "Second fails without pipeline continuation", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectHandler, - second *rulemocks.MockSubjectHandler, sub *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectHandlerMock, + second *rulemocks.SubjectHandlerMock, sub *subject.Subject, ) { t.Helper() - first.On("Execute", ctx, sub).Return(nil) - second.On("Execute", ctx, sub).Return(errors.New("second fails")) // nolint: goerr113 - second.On("ContinueOnError").Return(false) + first.EXPECT().Execute(ctx, sub).Return(nil) + second.EXPECT().Execute(ctx, sub).Return(errors.New("second fails")) // nolint: goerr113 + second.EXPECT().ContinueOnError().Return(false) }, assert: func(t *testing.T, err error) { t.Helper() @@ -93,14 +93,14 @@ func TestCompositeSubjectHandlerExecution(t *testing.T) { }, { uc: "Second fails with pipeline continuation", - configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.MockSubjectHandler, - second *rulemocks.MockSubjectHandler, sub *subject.Subject, + configureMocks: func(t *testing.T, ctx heimdall.Context, first *rulemocks.SubjectHandlerMock, + second *rulemocks.SubjectHandlerMock, sub *subject.Subject, ) { t.Helper() - first.On("Execute", ctx, sub).Return(nil) - second.On("Execute", ctx, sub).Return(errors.New("second fails")) // nolint: goerr113 - second.On("ContinueOnError").Return(true) + first.EXPECT().Execute(ctx, sub).Return(nil) + second.EXPECT().Execute(ctx, sub).Return(errors.New("second fails")) // nolint: goerr113 + second.EXPECT().ContinueOnError().Return(true) }, assert: func(t *testing.T, err error) { t.Helper() @@ -115,8 +115,8 @@ func TestCompositeSubjectHandlerExecution(t *testing.T) { ctx := &mocks.MockContext{} ctx.On("AppContext").Return(context.Background()) - handler1 := &rulemocks.MockSubjectHandler{} - handler2 := &rulemocks.MockSubjectHandler{} + handler1 := rulemocks.NewSubjectHandlerMock(t) + handler2 := rulemocks.NewSubjectHandlerMock(t) tc.configureMocks(t, ctx, handler1, handler2, sub) handler := compositeSubjectHandler{handler1, handler2} @@ -127,8 +127,6 @@ func TestCompositeSubjectHandlerExecution(t *testing.T) { // THEN tc.assert(t, err) - handler1.AssertExpectations(t) - handler2.AssertExpectations(t) ctx.AssertExpectations(t) }) } diff --git a/internal/rules/rule/decoder.go b/internal/rules/config/decoder.go similarity index 96% rename from internal/rules/rule/decoder.go rename to internal/rules/config/decoder.go index 42883e619..ce0d01815 100644 --- a/internal/rules/rule/decoder.go +++ b/internal/rules/config/decoder.go @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package rule +package config import ( "github.com/mitchellh/mapstructure" @@ -24,7 +24,7 @@ func DecodeConfig(input any, output any) error { dec, err := mapstructure.NewDecoder( &mapstructure.DecoderConfig{ DecodeHook: mapstructure.ComposeDecodeHookFunc( - decodeMatcher, + matcherDecodeHookFunc, ), Result: output, ErrorUnused: true, diff --git a/internal/rules/rule/mapstructure_decoder.go b/internal/rules/config/mapstructure_decoder.go similarity index 95% rename from internal/rules/rule/mapstructure_decoder.go rename to internal/rules/config/mapstructure_decoder.go index fbd821649..a8ff0bd02 100644 --- a/internal/rules/rule/mapstructure_decoder.go +++ b/internal/rules/config/mapstructure_decoder.go @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package rule +package config import ( "errors" @@ -31,7 +31,7 @@ var ( ErrUnsupportedStrategy = errors.New("unsupported strategy") ) -func decodeMatcher(from reflect.Type, to reflect.Type, data any) (any, error) { +func matcherDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error) { if to != reflect.TypeOf(Matcher{}) { return data, nil } diff --git a/internal/rules/rule/mapstructure_decoder_test.go b/internal/rules/config/mapstructure_decoder_test.go similarity index 98% rename from internal/rules/rule/mapstructure_decoder_test.go rename to internal/rules/config/mapstructure_decoder_test.go index 3b141bfe3..4284d9f16 100644 --- a/internal/rules/rule/mapstructure_decoder_test.go +++ b/internal/rules/config/mapstructure_decoder_test.go @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package rule +package config import ( "testing" @@ -25,7 +25,7 @@ import ( "github.com/dadrus/heimdall/internal/x/testsupport" ) -func TestMapstructureDecodeMatcher(t *testing.T) { +func TestMatcherDecodeHookFunc(t *testing.T) { t.Parallel() type Typ struct { diff --git a/internal/rules/rule/matcher.go b/internal/rules/config/matcher.go similarity index 95% rename from internal/rules/rule/matcher.go rename to internal/rules/config/matcher.go index df8bf6f62..acc83d7ef 100644 --- a/internal/rules/rule/matcher.go +++ b/internal/rules/config/matcher.go @@ -14,9 +14,11 @@ // // SPDX-License-Identifier: Apache-2.0 -package rule +package config -import "github.com/goccy/go-json" +import ( + "github.com/goccy/go-json" +) type Matcher struct { URL string `json:"url" yaml:"url"` diff --git a/internal/rules/rule/matcher_test.go b/internal/rules/config/matcher_test.go similarity index 99% rename from internal/rules/rule/matcher_test.go rename to internal/rules/config/matcher_test.go index 137c05a55..477cdc45c 100644 --- a/internal/rules/rule/matcher_test.go +++ b/internal/rules/config/matcher_test.go @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package rule +package config import ( "testing" diff --git a/internal/rules/provider/rulesetparser/parser.go b/internal/rules/config/parser.go similarity index 69% rename from internal/rules/provider/rulesetparser/parser.go rename to internal/rules/config/parser.go index c23f949de..40adf2625 100644 --- a/internal/rules/provider/rulesetparser/parser.go +++ b/internal/rules/config/parser.go @@ -14,18 +14,21 @@ // // SPDX-License-Identifier: Apache-2.0 -package rulesetparser +package config import ( "errors" "io" + "gopkg.in/yaml.v3" + "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x/errorchain" ) -func ParseRules(contentType string, reader io.Reader) ([]rule.Configuration, error) { +var ErrEmptyRuleSet = errors.New("empty rule set") + +func ParseRules(contentType string, reader io.Reader) (*RuleSet, error) { switch contentType { case "application/json": fallthrough @@ -35,7 +38,7 @@ func ParseRules(contentType string, reader io.Reader) ([]rule.Configuration, err // check if the contents are empty. in that case nothing needs to be decoded anyway b := make([]byte, 1) if _, err := reader.Read(b); err != nil && errors.Is(err, io.EOF) { - return []rule.Configuration{}, nil + return nil, ErrEmptyRuleSet } // otherwise @@ -43,3 +46,23 @@ func ParseRules(contentType string, reader io.Reader) ([]rule.Configuration, err "unsupported '%s' content type", contentType) } } + +func parseYAML(reader io.Reader) (*RuleSet, error) { + var ( + rawConfig map[string]any + ruleSet RuleSet + ) + + dec := yaml.NewDecoder(reader) + if err := dec.Decode(&rawConfig); err != nil { + if errors.Is(err, io.EOF) { + return nil, ErrEmptyRuleSet + } + + return nil, err + } + + err := DecodeConfig(rawConfig, &ruleSet) + + return &ruleSet, err +} diff --git a/internal/rules/config/parser_test.go b/internal/rules/config/parser_test.go new file mode 100644 index 000000000..867757cf2 --- /dev/null +++ b/internal/rules/config/parser_test.go @@ -0,0 +1,181 @@ +// Copyright 2022 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dadrus/heimdall/internal/heimdall" +) + +func TestParseRules(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + contentType string + content []byte + assert func(t *testing.T, err error, ruleSet *RuleSet) + }{ + { + uc: "unsupported content type and not empty contents", + contentType: "foobar", + content: []byte(`foo: bar`), + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, heimdall.ErrInternal) + assert.Contains(t, err.Error(), "unsupported 'foobar'") + }, + }, + { + uc: "unsupported content type and empty contents", + contentType: "foobar", + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.ErrorIs(t, err, ErrEmptyRuleSet) + require.Nil(t, ruleSet) + }, + }, + { + uc: "JSON content type and not empty contents", + contentType: "application/json", + content: []byte(`{ +"version": "1", +"name": "foo", +"rules": [{"id": "bar"}] +}`), + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.NoError(t, err) + require.NotNil(t, ruleSet) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "foo", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + }, + }, + { + uc: "JSON content type and empty contents", + contentType: "application/json", + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.ErrorIs(t, err, ErrEmptyRuleSet) + require.Nil(t, ruleSet) + }, + }, + { + uc: "YAML content type and not empty contents", + contentType: "application/yaml", + content: []byte(` +version: "1" +name: foo +rules: +- id: bar +`), + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.NoError(t, err) + require.NotNil(t, ruleSet) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "foo", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + }, + }, + { + uc: "YAML content and empty contents", + contentType: "application/yaml", + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.ErrorIs(t, err, ErrEmptyRuleSet) + require.Nil(t, ruleSet) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // WHEN + rules, err := ParseRules(tc.contentType, bytes.NewBuffer(tc.content)) + + // THEN + tc.assert(t, err, rules) + }) + } +} + +func TestParseYAML(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + conf []byte + assert func(t *testing.T, err error, ruleSet *RuleSet) + }{ + { + uc: "empty rule set spec", + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.ErrorIs(t, err, ErrEmptyRuleSet) + }, + }, + { + uc: "invalid rule set spec", + conf: []byte(`foo: bar`), + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.Error(t, err) + }, + }, + { + uc: "valid rule set spec", + conf: []byte(` +version: "1" +name: foo +rules: +- id: bar +`), + assert: func(t *testing.T, err error, ruleSet *RuleSet) { + t.Helper() + + require.NoError(t, err) + require.NotNil(t, ruleSet) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "foo", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, ruleSet.Rules[0].ID, "bar") + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // WHEN + ruleSet, err := parseYAML(bytes.NewBuffer(tc.conf)) + + // THEN + tc.assert(t, err, ruleSet) + }) + } +} diff --git a/internal/rules/rule/configuration.go b/internal/rules/config/rule.go similarity index 87% rename from internal/rules/rule/configuration.go rename to internal/rules/config/rule.go index 12d5a5ae6..a01cc0563 100644 --- a/internal/rules/rule/configuration.go +++ b/internal/rules/config/rule.go @@ -14,11 +14,13 @@ // // SPDX-License-Identifier: Apache-2.0 -package rule +package config -import "github.com/dadrus/heimdall/internal/config" +import ( + "github.com/dadrus/heimdall/internal/config" +) -type Configuration struct { +type Rule struct { ID string `json:"id" yaml:"id"` RuleMatcher Matcher `json:"match" yaml:"match"` Upstream string `json:"upstream" yaml:"upstream"` @@ -27,7 +29,7 @@ type Configuration struct { ErrorHandler []config.MechanismConfig `json:"on_error" yaml:"on_error"` } -func (in *Configuration) DeepCopyInto(out *Configuration) { +func (in *Rule) DeepCopyInto(out *Rule) { *out = *in out.RuleMatcher = in.RuleMatcher @@ -57,12 +59,12 @@ func (in *Configuration) DeepCopyInto(out *Configuration) { } } -func (in *Configuration) DeepCopy() *Configuration { +func (in *Rule) DeepCopy() *Rule { if in == nil { return nil } - out := new(Configuration) + out := new(Rule) in.DeepCopyInto(out) return out diff --git a/internal/rules/config/rule_set.go b/internal/rules/config/rule_set.go new file mode 100644 index 000000000..e6505ecd2 --- /dev/null +++ b/internal/rules/config/rule_set.go @@ -0,0 +1,39 @@ +package config + +import ( + "strings" + "time" + + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/x/errorchain" +) + +type MetaData struct { + Hash []byte `json:"-" yaml:"-"` + Source string `json:"-" yaml:"-"` + ModTime time.Time `json:"-" yaml:"-"` +} + +type RuleSet struct { + MetaData + + Version string `json:"version" yaml:"version"` + Name string `json:"name" yaml:"name"` + Rules []Rule `json:"rules" yaml:"rules"` +} + +func (rs RuleSet) VerifyPathPrefix(prefix string) error { + for _, rule := range rs.Rules { + if strings.HasPrefix(rule.RuleMatcher.URL, "/") && + // only path is specified + !strings.HasPrefix(rule.RuleMatcher.URL, prefix) || + // patterns are specified before the path + // There should be a better way to check it + !strings.Contains(rule.RuleMatcher.URL, prefix) { + return errorchain.NewWithMessage(heimdall.ErrConfiguration, + "path prefix validation failed for rule ID=%s") + } + } + + return nil +} diff --git a/internal/rules/config/rule_set_test.go b/internal/rules/config/rule_set_test.go new file mode 100644 index 000000000..a527f772a --- /dev/null +++ b/internal/rules/config/rule_set_test.go @@ -0,0 +1,39 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRuleSetConfigurationVerifyPathPrefixPathPrefixVerify(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + prefix string + url string + fail bool + }{ + {uc: "path only and without required prefix", prefix: "/foo/bar", url: "/bar/foo/moo", fail: true}, + {uc: "path only with required prefix", prefix: "/foo/bar", url: "/foo/bar/moo", fail: false}, + {uc: "full url and without required prefix", prefix: "/foo/bar", url: "https://<**>/bar/foo/moo", fail: true}, + {uc: "full url with required prefix", prefix: "/foo/bar", url: "https://<**>/foo/bar/moo", fail: false}, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + rs := RuleSet{ + Rules: []Rule{{RuleMatcher: Matcher{URL: tc.url}}}, + } + + // WHEN + err := rs.VerifyPathPrefix(tc.prefix) + + if tc.fail { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/rules/rule/configuration_test.go b/internal/rules/config/rule_test.go similarity index 96% rename from internal/rules/rule/configuration_test.go rename to internal/rules/config/rule_test.go index 0253517c9..4c7fd8a17 100644 --- a/internal/rules/rule/configuration_test.go +++ b/internal/rules/config/rule_test.go @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package rule +package config import ( "testing" @@ -29,9 +29,9 @@ func TestRuleConfigDeepCopyInto(t *testing.T) { t.Parallel() // GIVEN - var out Configuration + var out Rule - in := Configuration{ + in := Rule{ ID: "foo", RuleMatcher: Matcher{ URL: "bar", @@ -60,7 +60,7 @@ func TestRuleConfigDeepCopy(t *testing.T) { t.Parallel() // GIVEN - in := Configuration{ + in := Rule{ ID: "foo", RuleMatcher: Matcher{ URL: "bar", diff --git a/internal/rules/config/version.go b/internal/rules/config/version.go new file mode 100644 index 000000000..29d212ae5 --- /dev/null +++ b/internal/rules/config/version.go @@ -0,0 +1,3 @@ +package config + +const CurrentRuleSetVersion = "1" diff --git a/internal/rules/error_handler.go b/internal/rules/error_handler.go index 7f598ee4c..d66f6e26c 100644 --- a/internal/rules/error_handler.go +++ b/internal/rules/error_handler.go @@ -18,6 +18,8 @@ package rules import "github.com/dadrus/heimdall/internal/heimdall" +//go:generate mockery --name errorHandler --structname ErrorHandlerMock + type errorHandler interface { Execute(ctx heimdall.Context, err error) (bool, error) } diff --git a/internal/rules/event/event.go b/internal/rules/event/event.go index 73b6bdc2c..8b84813f3 100644 --- a/internal/rules/event/event.go +++ b/internal/rules/event/event.go @@ -16,7 +16,9 @@ package event -import "github.com/dadrus/heimdall/internal/rules/rule" +import ( + "github.com/dadrus/heimdall/internal/rules/rule" +) type ChangeType uint32 @@ -24,18 +26,25 @@ type ChangeType uint32 const ( Create ChangeType = 1 << iota Remove + Update ) func (t ChangeType) String() string { - if t == Create { + switch t { + case Create: return "Create" + case Remove: + return "Remove" + case Update: + return "Update" + default: + return "Unknown" } - - return "Remove" } -type RuleSetChangedEvent struct { - Src string - RuleSet []rule.Configuration +type RuleSetChanged struct { + Source string + Name string + Rules []rule.Rule ChangeType ChangeType } diff --git a/internal/rules/event/queue.go b/internal/rules/event/queue.go index 72f46b8f7..c03d20352 100644 --- a/internal/rules/event/queue.go +++ b/internal/rules/event/queue.go @@ -16,4 +16,4 @@ package event -type RuleSetChangedEventQueue chan RuleSetChangedEvent +type RuleSetChangedEventQueue chan RuleSetChanged diff --git a/internal/rules/mechanisms/authenticators/authenticator.go b/internal/rules/mechanisms/authenticators/authenticator.go index a2a565738..38500c758 100644 --- a/internal/rules/mechanisms/authenticators/authenticator.go +++ b/internal/rules/mechanisms/authenticators/authenticator.go @@ -21,6 +21,8 @@ import ( "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) +//go:generate mockery --name Authenticator --structname AuthenticatorMock + type Authenticator interface { Execute(heimdall.Context) (*subject.Subject, error) WithConfig(config map[string]any) (Authenticator, error) diff --git a/internal/rules/mechanisms/authenticators/extractors/body_parameter_extract_strategy.go b/internal/rules/mechanisms/authenticators/extractors/body_parameter_extract_strategy.go index 573ef9ab9..07773fa64 100644 --- a/internal/rules/mechanisms/authenticators/extractors/body_parameter_extract_strategy.go +++ b/internal/rules/mechanisms/authenticators/extractors/body_parameter_extract_strategy.go @@ -86,7 +86,7 @@ type bodyParameterAuthData struct { value string } -func (c *bodyParameterAuthData) ApplyTo(req *http.Request) { +func (c *bodyParameterAuthData) ApplyTo(_ *http.Request) { panic("application of extracted body parameters to a request is not yet supported") } diff --git a/internal/rules/mechanisms/authenticators/jwt_authenticator.go b/internal/rules/mechanisms/authenticators/jwt_authenticator.go index 97e99a514..2ce62f9f3 100644 --- a/internal/rules/mechanisms/authenticators/jwt_authenticator.go +++ b/internal/rules/mechanisms/authenticators/jwt_authenticator.go @@ -317,9 +317,9 @@ func (a *jwtAuthenticator) verifyTokenWithoutKID(ctx heimdall.Context, token *jw rawClaims, err = a.verifyTokenWithKey(token, &sigKey) if err == nil { break - } else { - logger.Info().Err(err).Str("_key_id", sigKey.KeyID).Msg("Failed to verify JWT") } + + logger.Info().Err(err).Str("_key_id", sigKey.KeyID).Msg("Failed to verify JWT") } if len(rawClaims) == 0 { diff --git a/internal/rules/mechanisms/authenticators/mocks/authenticator.go b/internal/rules/mechanisms/authenticators/mocks/authenticator.go new file mode 100644 index 000000000..5a6f1cd73 --- /dev/null +++ b/internal/rules/mechanisms/authenticators/mocks/authenticator.go @@ -0,0 +1,189 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + heimdall "github.com/dadrus/heimdall/internal/heimdall" + authenticators "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators" + + mock "github.com/stretchr/testify/mock" + + subject "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" +) + +// AuthenticatorMock is an autogenerated mock type for the Authenticator type +type AuthenticatorMock struct { + mock.Mock +} + +type AuthenticatorMock_Expecter struct { + mock *mock.Mock +} + +func (_m *AuthenticatorMock) EXPECT() *AuthenticatorMock_Expecter { + return &AuthenticatorMock_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: _a0 +func (_m *AuthenticatorMock) Execute(_a0 heimdall.Context) (*subject.Subject, error) { + ret := _m.Called(_a0) + + var r0 *subject.Subject + var r1 error + if rf, ok := ret.Get(0).(func(heimdall.Context) (*subject.Subject, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(heimdall.Context) *subject.Subject); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*subject.Subject) + } + } + + if rf, ok := ret.Get(1).(func(heimdall.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AuthenticatorMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type AuthenticatorMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 heimdall.Context +func (_e *AuthenticatorMock_Expecter) Execute(_a0 interface{}) *AuthenticatorMock_Execute_Call { + return &AuthenticatorMock_Execute_Call{Call: _e.mock.On("Execute", _a0)} +} + +func (_c *AuthenticatorMock_Execute_Call) Run(run func(_a0 heimdall.Context)) *AuthenticatorMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context)) + }) + return _c +} + +func (_c *AuthenticatorMock_Execute_Call) Return(_a0 *subject.Subject, _a1 error) *AuthenticatorMock_Execute_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AuthenticatorMock_Execute_Call) RunAndReturn(run func(heimdall.Context) (*subject.Subject, error)) *AuthenticatorMock_Execute_Call { + _c.Call.Return(run) + return _c +} + +// IsFallbackOnErrorAllowed provides a mock function with given fields: +func (_m *AuthenticatorMock) IsFallbackOnErrorAllowed() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// AuthenticatorMock_IsFallbackOnErrorAllowed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsFallbackOnErrorAllowed' +type AuthenticatorMock_IsFallbackOnErrorAllowed_Call struct { + *mock.Call +} + +// IsFallbackOnErrorAllowed is a helper method to define mock.On call +func (_e *AuthenticatorMock_Expecter) IsFallbackOnErrorAllowed() *AuthenticatorMock_IsFallbackOnErrorAllowed_Call { + return &AuthenticatorMock_IsFallbackOnErrorAllowed_Call{Call: _e.mock.On("IsFallbackOnErrorAllowed")} +} + +func (_c *AuthenticatorMock_IsFallbackOnErrorAllowed_Call) Run(run func()) *AuthenticatorMock_IsFallbackOnErrorAllowed_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *AuthenticatorMock_IsFallbackOnErrorAllowed_Call) Return(_a0 bool) *AuthenticatorMock_IsFallbackOnErrorAllowed_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *AuthenticatorMock_IsFallbackOnErrorAllowed_Call) RunAndReturn(run func() bool) *AuthenticatorMock_IsFallbackOnErrorAllowed_Call { + _c.Call.Return(run) + return _c +} + +// WithConfig provides a mock function with given fields: config +func (_m *AuthenticatorMock) WithConfig(config map[string]interface{}) (authenticators.Authenticator, error) { + ret := _m.Called(config) + + var r0 authenticators.Authenticator + var r1 error + if rf, ok := ret.Get(0).(func(map[string]interface{}) (authenticators.Authenticator, error)); ok { + return rf(config) + } + if rf, ok := ret.Get(0).(func(map[string]interface{}) authenticators.Authenticator); ok { + r0 = rf(config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authenticators.Authenticator) + } + } + + if rf, ok := ret.Get(1).(func(map[string]interface{}) error); ok { + r1 = rf(config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AuthenticatorMock_WithConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithConfig' +type AuthenticatorMock_WithConfig_Call struct { + *mock.Call +} + +// WithConfig is a helper method to define mock.On call +// - config map[string]interface{} +func (_e *AuthenticatorMock_Expecter) WithConfig(config interface{}) *AuthenticatorMock_WithConfig_Call { + return &AuthenticatorMock_WithConfig_Call{Call: _e.mock.On("WithConfig", config)} +} + +func (_c *AuthenticatorMock_WithConfig_Call) Run(run func(config map[string]interface{})) *AuthenticatorMock_WithConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]interface{})) + }) + return _c +} + +func (_c *AuthenticatorMock_WithConfig_Call) Return(_a0 authenticators.Authenticator, _a1 error) *AuthenticatorMock_WithConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AuthenticatorMock_WithConfig_Call) RunAndReturn(run func(map[string]interface{}) (authenticators.Authenticator, error)) *AuthenticatorMock_WithConfig_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewAuthenticatorMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewAuthenticatorMock creates a new instance of AuthenticatorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAuthenticatorMock(t mockConstructorTestingTNewAuthenticatorMock) *AuthenticatorMock { + mock := &AuthenticatorMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/mechanisms/authorizers/authorizer.go b/internal/rules/mechanisms/authorizers/authorizer.go index 909ecfcc8..45eb50202 100644 --- a/internal/rules/mechanisms/authorizers/authorizer.go +++ b/internal/rules/mechanisms/authorizers/authorizer.go @@ -21,6 +21,8 @@ import ( "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) +//go:generate mockery --name Authorizer --structname AuthorizerMock + type Authorizer interface { Execute(heimdall.Context, *subject.Subject) error WithConfig(config map[string]any) (Authorizer, error) diff --git a/internal/rules/mechanisms/authorizers/mocks/authorizer.go b/internal/rules/mechanisms/authorizers/mocks/authorizer.go new file mode 100644 index 000000000..b804872e5 --- /dev/null +++ b/internal/rules/mechanisms/authorizers/mocks/authorizer.go @@ -0,0 +1,178 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + heimdall "github.com/dadrus/heimdall/internal/heimdall" + authorizers "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers" + + mock "github.com/stretchr/testify/mock" + + subject "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" +) + +// AuthorizerMock is an autogenerated mock type for the Authorizer type +type AuthorizerMock struct { + mock.Mock +} + +type AuthorizerMock_Expecter struct { + mock *mock.Mock +} + +func (_m *AuthorizerMock) EXPECT() *AuthorizerMock_Expecter { + return &AuthorizerMock_Expecter{mock: &_m.Mock} +} + +// ContinueOnError provides a mock function with given fields: +func (_m *AuthorizerMock) ContinueOnError() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// AuthorizerMock_ContinueOnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContinueOnError' +type AuthorizerMock_ContinueOnError_Call struct { + *mock.Call +} + +// ContinueOnError is a helper method to define mock.On call +func (_e *AuthorizerMock_Expecter) ContinueOnError() *AuthorizerMock_ContinueOnError_Call { + return &AuthorizerMock_ContinueOnError_Call{Call: _e.mock.On("ContinueOnError")} +} + +func (_c *AuthorizerMock_ContinueOnError_Call) Run(run func()) *AuthorizerMock_ContinueOnError_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *AuthorizerMock_ContinueOnError_Call) Return(_a0 bool) *AuthorizerMock_ContinueOnError_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *AuthorizerMock_ContinueOnError_Call) RunAndReturn(run func() bool) *AuthorizerMock_ContinueOnError_Call { + _c.Call.Return(run) + return _c +} + +// Execute provides a mock function with given fields: _a0, _a1 +func (_m *AuthorizerMock) Execute(_a0 heimdall.Context, _a1 *subject.Subject) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(heimdall.Context, *subject.Subject) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AuthorizerMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type AuthorizerMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 heimdall.Context +// - _a1 *subject.Subject +func (_e *AuthorizerMock_Expecter) Execute(_a0 interface{}, _a1 interface{}) *AuthorizerMock_Execute_Call { + return &AuthorizerMock_Execute_Call{Call: _e.mock.On("Execute", _a0, _a1)} +} + +func (_c *AuthorizerMock_Execute_Call) Run(run func(_a0 heimdall.Context, _a1 *subject.Subject)) *AuthorizerMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context), args[1].(*subject.Subject)) + }) + return _c +} + +func (_c *AuthorizerMock_Execute_Call) Return(_a0 error) *AuthorizerMock_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *AuthorizerMock_Execute_Call) RunAndReturn(run func(heimdall.Context, *subject.Subject) error) *AuthorizerMock_Execute_Call { + _c.Call.Return(run) + return _c +} + +// WithConfig provides a mock function with given fields: config +func (_m *AuthorizerMock) WithConfig(config map[string]interface{}) (authorizers.Authorizer, error) { + ret := _m.Called(config) + + var r0 authorizers.Authorizer + var r1 error + if rf, ok := ret.Get(0).(func(map[string]interface{}) (authorizers.Authorizer, error)); ok { + return rf(config) + } + if rf, ok := ret.Get(0).(func(map[string]interface{}) authorizers.Authorizer); ok { + r0 = rf(config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authorizers.Authorizer) + } + } + + if rf, ok := ret.Get(1).(func(map[string]interface{}) error); ok { + r1 = rf(config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AuthorizerMock_WithConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithConfig' +type AuthorizerMock_WithConfig_Call struct { + *mock.Call +} + +// WithConfig is a helper method to define mock.On call +// - config map[string]interface{} +func (_e *AuthorizerMock_Expecter) WithConfig(config interface{}) *AuthorizerMock_WithConfig_Call { + return &AuthorizerMock_WithConfig_Call{Call: _e.mock.On("WithConfig", config)} +} + +func (_c *AuthorizerMock_WithConfig_Call) Run(run func(config map[string]interface{})) *AuthorizerMock_WithConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]interface{})) + }) + return _c +} + +func (_c *AuthorizerMock_WithConfig_Call) Return(_a0 authorizers.Authorizer, _a1 error) *AuthorizerMock_WithConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AuthorizerMock_WithConfig_Call) RunAndReturn(run func(map[string]interface{}) (authorizers.Authorizer, error)) *AuthorizerMock_WithConfig_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewAuthorizerMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewAuthorizerMock creates a new instance of AuthorizerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAuthorizerMock(t mockConstructorTestingTNewAuthorizerMock) *AuthorizerMock { + mock := &AuthorizerMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/mechanisms/cellib/types.go b/internal/rules/mechanisms/cellib/types.go index ba1b62898..392347216 100644 --- a/internal/rules/mechanisms/cellib/types.go +++ b/internal/rules/mechanisms/cellib/types.go @@ -40,7 +40,7 @@ type URL struct { url.URL } -func (u *URL) Receive(function string, _ string, args []ref.Val) ref.Val { +func (u *URL) Receive(function string, _ string, _ []ref.Val) ref.Val { switch function { case "String": return types.String(u.String()) diff --git a/internal/rules/mechanisms/contextualizers/contextualizer.go b/internal/rules/mechanisms/contextualizers/contextualizer.go index bb9cb6fbd..5c4430bc5 100644 --- a/internal/rules/mechanisms/contextualizers/contextualizer.go +++ b/internal/rules/mechanisms/contextualizers/contextualizer.go @@ -21,6 +21,8 @@ import ( "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) +//go:generate mockery --name Contextualizer --structname ContextualizerMock + type Contextualizer interface { Execute(heimdall.Context, *subject.Subject) error WithConfig(config map[string]any) (Contextualizer, error) diff --git a/internal/rules/mechanisms/contextualizers/mocks/contextualizer.go b/internal/rules/mechanisms/contextualizers/mocks/contextualizer.go new file mode 100644 index 000000000..35f6d1e35 --- /dev/null +++ b/internal/rules/mechanisms/contextualizers/mocks/contextualizer.go @@ -0,0 +1,178 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + heimdall "github.com/dadrus/heimdall/internal/heimdall" + contextualizers "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers" + + mock "github.com/stretchr/testify/mock" + + subject "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" +) + +// ContextualizerMock is an autogenerated mock type for the Contextualizer type +type ContextualizerMock struct { + mock.Mock +} + +type ContextualizerMock_Expecter struct { + mock *mock.Mock +} + +func (_m *ContextualizerMock) EXPECT() *ContextualizerMock_Expecter { + return &ContextualizerMock_Expecter{mock: &_m.Mock} +} + +// ContinueOnError provides a mock function with given fields: +func (_m *ContextualizerMock) ContinueOnError() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ContextualizerMock_ContinueOnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContinueOnError' +type ContextualizerMock_ContinueOnError_Call struct { + *mock.Call +} + +// ContinueOnError is a helper method to define mock.On call +func (_e *ContextualizerMock_Expecter) ContinueOnError() *ContextualizerMock_ContinueOnError_Call { + return &ContextualizerMock_ContinueOnError_Call{Call: _e.mock.On("ContinueOnError")} +} + +func (_c *ContextualizerMock_ContinueOnError_Call) Run(run func()) *ContextualizerMock_ContinueOnError_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ContextualizerMock_ContinueOnError_Call) Return(_a0 bool) *ContextualizerMock_ContinueOnError_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ContextualizerMock_ContinueOnError_Call) RunAndReturn(run func() bool) *ContextualizerMock_ContinueOnError_Call { + _c.Call.Return(run) + return _c +} + +// Execute provides a mock function with given fields: _a0, _a1 +func (_m *ContextualizerMock) Execute(_a0 heimdall.Context, _a1 *subject.Subject) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(heimdall.Context, *subject.Subject) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ContextualizerMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type ContextualizerMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 heimdall.Context +// - _a1 *subject.Subject +func (_e *ContextualizerMock_Expecter) Execute(_a0 interface{}, _a1 interface{}) *ContextualizerMock_Execute_Call { + return &ContextualizerMock_Execute_Call{Call: _e.mock.On("Execute", _a0, _a1)} +} + +func (_c *ContextualizerMock_Execute_Call) Run(run func(_a0 heimdall.Context, _a1 *subject.Subject)) *ContextualizerMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context), args[1].(*subject.Subject)) + }) + return _c +} + +func (_c *ContextualizerMock_Execute_Call) Return(_a0 error) *ContextualizerMock_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ContextualizerMock_Execute_Call) RunAndReturn(run func(heimdall.Context, *subject.Subject) error) *ContextualizerMock_Execute_Call { + _c.Call.Return(run) + return _c +} + +// WithConfig provides a mock function with given fields: config +func (_m *ContextualizerMock) WithConfig(config map[string]interface{}) (contextualizers.Contextualizer, error) { + ret := _m.Called(config) + + var r0 contextualizers.Contextualizer + var r1 error + if rf, ok := ret.Get(0).(func(map[string]interface{}) (contextualizers.Contextualizer, error)); ok { + return rf(config) + } + if rf, ok := ret.Get(0).(func(map[string]interface{}) contextualizers.Contextualizer); ok { + r0 = rf(config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(contextualizers.Contextualizer) + } + } + + if rf, ok := ret.Get(1).(func(map[string]interface{}) error); ok { + r1 = rf(config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ContextualizerMock_WithConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithConfig' +type ContextualizerMock_WithConfig_Call struct { + *mock.Call +} + +// WithConfig is a helper method to define mock.On call +// - config map[string]interface{} +func (_e *ContextualizerMock_Expecter) WithConfig(config interface{}) *ContextualizerMock_WithConfig_Call { + return &ContextualizerMock_WithConfig_Call{Call: _e.mock.On("WithConfig", config)} +} + +func (_c *ContextualizerMock_WithConfig_Call) Run(run func(config map[string]interface{})) *ContextualizerMock_WithConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]interface{})) + }) + return _c +} + +func (_c *ContextualizerMock_WithConfig_Call) Return(_a0 contextualizers.Contextualizer, _a1 error) *ContextualizerMock_WithConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ContextualizerMock_WithConfig_Call) RunAndReturn(run func(map[string]interface{}) (contextualizers.Contextualizer, error)) *ContextualizerMock_WithConfig_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewContextualizerMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewContextualizerMock creates a new instance of ContextualizerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewContextualizerMock(t mockConstructorTestingTNewContextualizerMock) *ContextualizerMock { + mock := &ContextualizerMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/mechanisms/errorhandlers/error_handler.go b/internal/rules/mechanisms/errorhandlers/error_handler.go index 073ab9330..268ecfaa5 100644 --- a/internal/rules/mechanisms/errorhandlers/error_handler.go +++ b/internal/rules/mechanisms/errorhandlers/error_handler.go @@ -20,6 +20,8 @@ import ( "github.com/dadrus/heimdall/internal/heimdall" ) +//go:generate mockery --name ErrorHandler --structname ErrorHandlerMock + type ErrorHandler interface { Execute(ctx heimdall.Context, err error) (bool, error) WithConfig(config map[string]any) (ErrorHandler, error) diff --git a/internal/rules/mechanisms/errorhandlers/mocks/error_handler.go b/internal/rules/mechanisms/errorhandlers/mocks/error_handler.go new file mode 100644 index 000000000..0878d0285 --- /dev/null +++ b/internal/rules/mechanisms/errorhandlers/mocks/error_handler.go @@ -0,0 +1,145 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + heimdall "github.com/dadrus/heimdall/internal/heimdall" + errorhandlers "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers" + + mock "github.com/stretchr/testify/mock" +) + +// ErrorHandlerMock is an autogenerated mock type for the ErrorHandler type +type ErrorHandlerMock struct { + mock.Mock +} + +type ErrorHandlerMock_Expecter struct { + mock *mock.Mock +} + +func (_m *ErrorHandlerMock) EXPECT() *ErrorHandlerMock_Expecter { + return &ErrorHandlerMock_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: ctx, err +func (_m *ErrorHandlerMock) Execute(ctx heimdall.Context, err error) (bool, error) { + ret := _m.Called(ctx, err) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(heimdall.Context, error) (bool, error)); ok { + return rf(ctx, err) + } + if rf, ok := ret.Get(0).(func(heimdall.Context, error) bool); ok { + r0 = rf(ctx, err) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(heimdall.Context, error) error); ok { + r1 = rf(ctx, err) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ErrorHandlerMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type ErrorHandlerMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ctx heimdall.Context +// - err error +func (_e *ErrorHandlerMock_Expecter) Execute(ctx interface{}, err interface{}) *ErrorHandlerMock_Execute_Call { + return &ErrorHandlerMock_Execute_Call{Call: _e.mock.On("Execute", ctx, err)} +} + +func (_c *ErrorHandlerMock_Execute_Call) Run(run func(ctx heimdall.Context, err error)) *ErrorHandlerMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context), args[1].(error)) + }) + return _c +} + +func (_c *ErrorHandlerMock_Execute_Call) Return(_a0 bool, _a1 error) *ErrorHandlerMock_Execute_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ErrorHandlerMock_Execute_Call) RunAndReturn(run func(heimdall.Context, error) (bool, error)) *ErrorHandlerMock_Execute_Call { + _c.Call.Return(run) + return _c +} + +// WithConfig provides a mock function with given fields: config +func (_m *ErrorHandlerMock) WithConfig(config map[string]interface{}) (errorhandlers.ErrorHandler, error) { + ret := _m.Called(config) + + var r0 errorhandlers.ErrorHandler + var r1 error + if rf, ok := ret.Get(0).(func(map[string]interface{}) (errorhandlers.ErrorHandler, error)); ok { + return rf(config) + } + if rf, ok := ret.Get(0).(func(map[string]interface{}) errorhandlers.ErrorHandler); ok { + r0 = rf(config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(errorhandlers.ErrorHandler) + } + } + + if rf, ok := ret.Get(1).(func(map[string]interface{}) error); ok { + r1 = rf(config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ErrorHandlerMock_WithConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithConfig' +type ErrorHandlerMock_WithConfig_Call struct { + *mock.Call +} + +// WithConfig is a helper method to define mock.On call +// - config map[string]interface{} +func (_e *ErrorHandlerMock_Expecter) WithConfig(config interface{}) *ErrorHandlerMock_WithConfig_Call { + return &ErrorHandlerMock_WithConfig_Call{Call: _e.mock.On("WithConfig", config)} +} + +func (_c *ErrorHandlerMock_WithConfig_Call) Run(run func(config map[string]interface{})) *ErrorHandlerMock_WithConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]interface{})) + }) + return _c +} + +func (_c *ErrorHandlerMock_WithConfig_Call) Return(_a0 errorhandlers.ErrorHandler, _a1 error) *ErrorHandlerMock_WithConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ErrorHandlerMock_WithConfig_Call) RunAndReturn(run func(map[string]interface{}) (errorhandlers.ErrorHandler, error)) *ErrorHandlerMock_WithConfig_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewErrorHandlerMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewErrorHandlerMock creates a new instance of ErrorHandlerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewErrorHandlerMock(t mockConstructorTestingTNewErrorHandlerMock) *ErrorHandlerMock { + mock := &ErrorHandlerMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/mechanisms/factory.go b/internal/rules/mechanisms/factory.go index 19495d732..6754526b9 100644 --- a/internal/rules/mechanisms/factory.go +++ b/internal/rules/mechanisms/factory.go @@ -19,15 +19,12 @@ package mechanisms import ( "errors" - "github.com/rs/zerolog" - "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators" "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers" "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers" "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers" "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers" - "github.com/dadrus/heimdall/internal/x/errorchain" ) var ( @@ -38,127 +35,12 @@ var ( ErrErrorHandlerCreation = errors.New("failed to create error handler") ) -type Factory interface { - CreateAuthenticator(id string, conf config.MechanismConfig) (authenticators.Authenticator, error) - CreateAuthorizer(id string, conf config.MechanismConfig) (authorizers.Authorizer, error) - CreateContextualizer(id string, conf config.MechanismConfig) (contextualizers.Contextualizer, error) - CreateUnifier(id string, conf config.MechanismConfig) (unifiers.Unifier, error) - CreateErrorHandler(id string, conf config.MechanismConfig) (errorhandlers.ErrorHandler, error) -} - -func NewFactory(conf *config.Configuration, logger zerolog.Logger) (Factory, error) { - logger.Info().Msg("Loading pipeline definitions") - - repository, err := newPrototypeRepository(conf, logger) - if err != nil { - logger.Error().Err(err).Msg("Failed loading pipeline definitions") - - return nil, err - } - - return &mechanismsFactory{r: repository}, nil -} - -type mechanismsFactory struct { - r *prototypeRepository -} - -func (hf *mechanismsFactory) CreateAuthenticator(id string, conf config.MechanismConfig) ( - authenticators.Authenticator, error, -) { - prototype, err := hf.r.Authenticator(id) - if err != nil { - return nil, errorchain.New(ErrAuthenticatorCreation).CausedBy(err) - } - - if conf != nil { - authenticator, err := prototype.WithConfig(conf) - if err != nil { - return nil, errorchain.New(ErrAuthenticatorCreation).CausedBy(err) - } - - return authenticator, nil - } - - return prototype, nil -} - -func (hf *mechanismsFactory) CreateAuthorizer(id string, conf config.MechanismConfig) ( - authorizers.Authorizer, error, -) { - prototype, err := hf.r.Authorizer(id) - if err != nil { - return nil, errorchain.New(ErrAuthorizerCreation).CausedBy(err) - } - - if conf != nil { - authorizer, err := prototype.WithConfig(conf) - if err != nil { - return nil, errorchain.New(ErrAuthorizerCreation).CausedBy(err) - } +//go:generate mockery --name Factory --structname FactoryMock - return authorizer, nil - } - - return prototype, nil -} - -func (hf *mechanismsFactory) CreateContextualizer(id string, conf config.MechanismConfig) ( - contextualizers.Contextualizer, error, -) { - prototype, err := hf.r.Contextualizer(id) - if err != nil { - return nil, errorchain.New(ErrContextualizerCreation).CausedBy(err) - } - - if conf != nil { - contextualizer, err := prototype.WithConfig(conf) - if err != nil { - return nil, errorchain.New(ErrContextualizerCreation).CausedBy(err) - } - - return contextualizer, nil - } - - return prototype, nil -} - -func (hf *mechanismsFactory) CreateUnifier(id string, conf config.MechanismConfig) ( - unifiers.Unifier, error, -) { - prototype, err := hf.r.Unifier(id) - if err != nil { - return nil, errorchain.New(ErrUnifierCreation).CausedBy(err) - } - - if conf != nil { - unifier, err := prototype.WithConfig(conf) - if err != nil { - return nil, errorchain.New(ErrUnifierCreation).CausedBy(err) - } - - return unifier, nil - } - - return prototype, nil -} - -func (hf *mechanismsFactory) CreateErrorHandler(id string, conf config.MechanismConfig) ( - errorhandlers.ErrorHandler, error, -) { - prototype, err := hf.r.ErrorHandler(id) - if err != nil { - return nil, errorchain.New(ErrErrorHandlerCreation).CausedBy(err) - } - - if conf != nil { - errorHandler, err := prototype.WithConfig(conf) - if err != nil { - return nil, errorchain.New(ErrErrorHandlerCreation).CausedBy(err) - } - - return errorHandler, nil - } - - return prototype, nil +type Factory interface { + CreateAuthenticator(version, id string, conf config.MechanismConfig) (authenticators.Authenticator, error) + CreateAuthorizer(version, id string, conf config.MechanismConfig) (authorizers.Authorizer, error) + CreateContextualizer(version, id string, conf config.MechanismConfig) (contextualizers.Contextualizer, error) + CreateUnifier(version, id string, conf config.MechanismConfig) (unifiers.Unifier, error) + CreateErrorHandler(version, id string, conf config.MechanismConfig) (errorhandlers.ErrorHandler, error) } diff --git a/internal/rules/mechanisms/factory_impl.go b/internal/rules/mechanisms/factory_impl.go new file mode 100644 index 000000000..e87195a04 --- /dev/null +++ b/internal/rules/mechanisms/factory_impl.go @@ -0,0 +1,130 @@ +package mechanisms + +import ( + "github.com/rs/zerolog" + + "github.com/dadrus/heimdall/internal/config" + "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators" + "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers" + "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers" + "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers" + "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers" + "github.com/dadrus/heimdall/internal/x/errorchain" +) + +func NewFactory(conf *config.Configuration, logger zerolog.Logger) (Factory, error) { + logger.Info().Msg("Loading pipeline definitions") + + repository, err := newPrototypeRepository(conf, logger) + if err != nil { + logger.Error().Err(err).Msg("Failed loading pipeline definitions") + + return nil, err + } + + return &mechanismsFactory{r: repository}, nil +} + +type mechanismsFactory struct { + r *prototypeRepository +} + +func (hf *mechanismsFactory) CreateAuthenticator(_, id string, conf config.MechanismConfig) ( + authenticators.Authenticator, error, +) { + prototype, err := hf.r.Authenticator(id) + if err != nil { + return nil, errorchain.New(ErrAuthenticatorCreation).CausedBy(err) + } + + if conf != nil { + authenticator, err := prototype.WithConfig(conf) + if err != nil { + return nil, errorchain.New(ErrAuthenticatorCreation).CausedBy(err) + } + + return authenticator, nil + } + + return prototype, nil +} + +func (hf *mechanismsFactory) CreateAuthorizer(_, id string, conf config.MechanismConfig) ( + authorizers.Authorizer, error, +) { + prototype, err := hf.r.Authorizer(id) + if err != nil { + return nil, errorchain.New(ErrAuthorizerCreation).CausedBy(err) + } + + if conf != nil { + authorizer, err := prototype.WithConfig(conf) + if err != nil { + return nil, errorchain.New(ErrAuthorizerCreation).CausedBy(err) + } + + return authorizer, nil + } + + return prototype, nil +} + +func (hf *mechanismsFactory) CreateContextualizer(_, id string, conf config.MechanismConfig) ( + contextualizers.Contextualizer, error, +) { + prototype, err := hf.r.Contextualizer(id) + if err != nil { + return nil, errorchain.New(ErrContextualizerCreation).CausedBy(err) + } + + if conf != nil { + contextualizer, err := prototype.WithConfig(conf) + if err != nil { + return nil, errorchain.New(ErrContextualizerCreation).CausedBy(err) + } + + return contextualizer, nil + } + + return prototype, nil +} + +func (hf *mechanismsFactory) CreateUnifier(_, id string, conf config.MechanismConfig) ( + unifiers.Unifier, error, +) { + prototype, err := hf.r.Unifier(id) + if err != nil { + return nil, errorchain.New(ErrUnifierCreation).CausedBy(err) + } + + if conf != nil { + unifier, err := prototype.WithConfig(conf) + if err != nil { + return nil, errorchain.New(ErrUnifierCreation).CausedBy(err) + } + + return unifier, nil + } + + return prototype, nil +} + +func (hf *mechanismsFactory) CreateErrorHandler(_, id string, conf config.MechanismConfig) ( + errorhandlers.ErrorHandler, error, +) { + prototype, err := hf.r.ErrorHandler(id) + if err != nil { + return nil, errorchain.New(ErrErrorHandlerCreation).CausedBy(err) + } + + if conf != nil { + errorHandler, err := prototype.WithConfig(conf) + if err != nil { + return nil, errorchain.New(ErrErrorHandlerCreation).CausedBy(err) + } + + return errorHandler, nil + } + + return prototype, nil +} diff --git a/internal/rules/mechanisms/factory_test.go b/internal/rules/mechanisms/factory_test.go index 5ddae69b7..1de86ed21 100644 --- a/internal/rules/mechanisms/factory_test.go +++ b/internal/rules/mechanisms/factory_test.go @@ -27,11 +27,15 @@ import ( "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators" + "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators/mocks" "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers" + mocks3 "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers/mocks" "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers" + mocks4 "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers/mocks" "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers" - mocks2 "github.com/dadrus/heimdall/internal/rules/mechanisms/mocks" + mocks5 "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers/mocks" "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers" + mocks6 "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers/mocks" "github.com/dadrus/heimdall/internal/x" ) @@ -44,7 +48,7 @@ func TestHandlerFactoryCreateAuthenticator(t *testing.T) { uc string id string conf map[string]any - configureMock func(t *testing.T, mAuth *mocks2.MockAuthenticator) + configureMock func(t *testing.T, mAuth *mocks.AuthenticatorMock) assert func(t *testing.T, err error, auth authenticators.Authenticator) }{ { @@ -61,10 +65,10 @@ func TestHandlerFactoryCreateAuthenticator(t *testing.T) { { uc: "with failing creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mAuth *mocks2.MockAuthenticator) { + configureMock: func(t *testing.T, mAuth *mocks.AuthenticatorMock) { t.Helper() - mAuth.On("WithConfig", mock.Anything).Return(nil, heimdall.ErrArgument) + mAuth.EXPECT().WithConfig(mock.Anything).Return(nil, heimdall.ErrArgument) }, assert: func(t *testing.T, err error, auth authenticators.Authenticator) { t.Helper() @@ -77,10 +81,10 @@ func TestHandlerFactoryCreateAuthenticator(t *testing.T) { { uc: "successful creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mAuth *mocks2.MockAuthenticator) { + configureMock: func(t *testing.T, mAuth *mocks.AuthenticatorMock) { t.Helper() - mAuth.On("WithConfig", mock.Anything).Return(mAuth, nil) + mAuth.EXPECT().WithConfig(mock.Anything).Return(mAuth, nil) }, assert: func(t *testing.T, err error, auth authenticators.Authenticator) { t.Helper() @@ -103,9 +107,9 @@ func TestHandlerFactoryCreateAuthenticator(t *testing.T) { // GIVEN configureMock := x.IfThenElse(tc.configureMock != nil, tc.configureMock, - func(t *testing.T, mAuth *mocks2.MockAuthenticator) { t.Helper() }) + func(t *testing.T, mAuth *mocks.AuthenticatorMock) { t.Helper() }) - mAuth := &mocks2.MockAuthenticator{} + mAuth := mocks.NewAuthenticatorMock(t) configureMock(t, mAuth) factory := &mechanismsFactory{ @@ -119,11 +123,10 @@ func TestHandlerFactoryCreateAuthenticator(t *testing.T) { id := x.IfThenElse(len(tc.id) != 0, tc.id, ID) // WHEN - auth, err := factory.CreateAuthenticator(id, tc.conf) + auth, err := factory.CreateAuthenticator("test", id, tc.conf) // THEN tc.assert(t, err, auth) - mAuth.AssertExpectations(t) }) } } @@ -137,7 +140,7 @@ func TestHandlerFactoryCreateAuthorizer(t *testing.T) { uc string id string conf map[string]any - configureMock func(t *testing.T, mAuth *mocks2.MockAuthorizer) + configureMock func(t *testing.T, mAuth *mocks3.AuthorizerMock) assert func(t *testing.T, err error, auth authorizers.Authorizer) }{ { @@ -154,10 +157,10 @@ func TestHandlerFactoryCreateAuthorizer(t *testing.T) { { uc: "with failing creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mAuth *mocks2.MockAuthorizer) { + configureMock: func(t *testing.T, mAuth *mocks3.AuthorizerMock) { t.Helper() - mAuth.On("WithConfig", mock.Anything).Return(nil, heimdall.ErrArgument) + mAuth.EXPECT().WithConfig(mock.Anything).Return(nil, heimdall.ErrArgument) }, assert: func(t *testing.T, err error, auth authorizers.Authorizer) { t.Helper() @@ -170,10 +173,10 @@ func TestHandlerFactoryCreateAuthorizer(t *testing.T) { { uc: "successful creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mAuth *mocks2.MockAuthorizer) { + configureMock: func(t *testing.T, mAuth *mocks3.AuthorizerMock) { t.Helper() - mAuth.On("WithConfig", mock.Anything).Return(mAuth, nil) + mAuth.EXPECT().WithConfig(mock.Anything).Return(mAuth, nil) }, assert: func(t *testing.T, err error, auth authorizers.Authorizer) { t.Helper() @@ -196,9 +199,9 @@ func TestHandlerFactoryCreateAuthorizer(t *testing.T) { // GIVEN configureMock := x.IfThenElse(tc.configureMock != nil, tc.configureMock, - func(t *testing.T, mAuth *mocks2.MockAuthorizer) { t.Helper() }) + func(t *testing.T, mAuth *mocks3.AuthorizerMock) { t.Helper() }) - mAuth := &mocks2.MockAuthorizer{} + mAuth := mocks3.NewAuthorizerMock(t) configureMock(t, mAuth) factory := &mechanismsFactory{ @@ -212,11 +215,10 @@ func TestHandlerFactoryCreateAuthorizer(t *testing.T) { id := x.IfThenElse(len(tc.id) != 0, tc.id, ID) // WHEN - auth, err := factory.CreateAuthorizer(id, tc.conf) + auth, err := factory.CreateAuthorizer("test", id, tc.conf) // THEN tc.assert(t, err, auth) - mAuth.AssertExpectations(t) }) } } @@ -230,7 +232,7 @@ func TestHandlerFactoryCreateContextualizer(t *testing.T) { uc string id string conf map[string]any - configureMock func(t *testing.T, mContextualizer *mocks2.MockContextualizer) + configureMock func(t *testing.T, mContextualizer *mocks4.ContextualizerMock) assert func(t *testing.T, err error, contextualizer contextualizers.Contextualizer) }{ { @@ -247,10 +249,10 @@ func TestHandlerFactoryCreateContextualizer(t *testing.T) { { uc: "with failing creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mContextualizer *mocks2.MockContextualizer) { + configureMock: func(t *testing.T, mContextualizer *mocks4.ContextualizerMock) { t.Helper() - mContextualizer.On("WithConfig", mock.Anything). + mContextualizer.EXPECT().WithConfig(mock.Anything). Return(nil, heimdall.ErrArgument) }, assert: func(t *testing.T, err error, contextualizer contextualizers.Contextualizer) { @@ -264,10 +266,10 @@ func TestHandlerFactoryCreateContextualizer(t *testing.T) { { uc: "successful creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mContextualizer *mocks2.MockContextualizer) { + configureMock: func(t *testing.T, mContextualizer *mocks4.ContextualizerMock) { t.Helper() - mContextualizer.On("WithConfig", mock.Anything).Return(mContextualizer, nil) + mContextualizer.EXPECT().WithConfig(mock.Anything).Return(mContextualizer, nil) }, assert: func(t *testing.T, err error, contextualizer contextualizers.Contextualizer) { t.Helper() @@ -290,9 +292,9 @@ func TestHandlerFactoryCreateContextualizer(t *testing.T) { // GIVEN configureMock := x.IfThenElse(tc.configureMock != nil, tc.configureMock, - func(t *testing.T, mHydr *mocks2.MockContextualizer) { t.Helper() }) + func(t *testing.T, mHydr *mocks4.ContextualizerMock) { t.Helper() }) - mContextualizer := &mocks2.MockContextualizer{} + mContextualizer := mocks4.NewContextualizerMock(t) configureMock(t, mContextualizer) factory := &mechanismsFactory{ @@ -306,11 +308,10 @@ func TestHandlerFactoryCreateContextualizer(t *testing.T) { id := x.IfThenElse(len(tc.id) != 0, tc.id, ID) // WHEN - contextualizer, err := factory.CreateContextualizer(id, tc.conf) + contextualizer, err := factory.CreateContextualizer("test", id, tc.conf) // THEN tc.assert(t, err, contextualizer) - mContextualizer.AssertExpectations(t) }) } } @@ -324,7 +325,7 @@ func TestHandlerFactoryCreateUnifier(t *testing.T) { uc string id string conf map[string]any - configureMock func(t *testing.T, mUn *mocks2.MockUnifier) + configureMock func(t *testing.T, mUn *mocks6.UnifierMock) assert func(t *testing.T, err error, unifier unifiers.Unifier) }{ { @@ -341,10 +342,10 @@ func TestHandlerFactoryCreateUnifier(t *testing.T) { { uc: "with failing creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mUn *mocks2.MockUnifier) { + configureMock: func(t *testing.T, mUn *mocks6.UnifierMock) { t.Helper() - mUn.On("WithConfig", mock.Anything).Return(nil, heimdall.ErrArgument) + mUn.EXPECT().WithConfig(mock.Anything).Return(nil, heimdall.ErrArgument) }, assert: func(t *testing.T, err error, unifier unifiers.Unifier) { t.Helper() @@ -357,10 +358,10 @@ func TestHandlerFactoryCreateUnifier(t *testing.T) { { uc: "successful creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mUn *mocks2.MockUnifier) { + configureMock: func(t *testing.T, mUn *mocks6.UnifierMock) { t.Helper() - mUn.On("WithConfig", mock.Anything).Return(mUn, nil) + mUn.EXPECT().WithConfig(mock.Anything).Return(mUn, nil) }, assert: func(t *testing.T, err error, unifier unifiers.Unifier) { t.Helper() @@ -383,9 +384,9 @@ func TestHandlerFactoryCreateUnifier(t *testing.T) { // GIVEN configureMock := x.IfThenElse(tc.configureMock != nil, tc.configureMock, - func(t *testing.T, mUn *mocks2.MockUnifier) { t.Helper() }) + func(t *testing.T, mUn *mocks6.UnifierMock) { t.Helper() }) - mUn := &mocks2.MockUnifier{} + mUn := mocks6.NewUnifierMock(t) configureMock(t, mUn) factory := &mechanismsFactory{ @@ -399,11 +400,10 @@ func TestHandlerFactoryCreateUnifier(t *testing.T) { id := x.IfThenElse(len(tc.id) != 0, tc.id, ID) // WHEN - unifier, err := factory.CreateUnifier(id, tc.conf) + unifier, err := factory.CreateUnifier("test", id, tc.conf) // THEN tc.assert(t, err, unifier) - mUn.AssertExpectations(t) }) } } @@ -417,7 +417,7 @@ func TestHandlerFactoryCreateErrorHandler(t *testing.T) { uc string id string conf map[string]any - configureMock func(t *testing.T, mEH *mocks2.MockErrorHandler) + configureMock func(t *testing.T, mEH *mocks5.ErrorHandlerMock) assert func(t *testing.T, err error, errorHandler errorhandlers.ErrorHandler) }{ { @@ -434,10 +434,10 @@ func TestHandlerFactoryCreateErrorHandler(t *testing.T) { { uc: "with failing creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mEH *mocks2.MockErrorHandler) { + configureMock: func(t *testing.T, mEH *mocks5.ErrorHandlerMock) { t.Helper() - mEH.On("WithConfig", mock.Anything).Return(nil, heimdall.ErrArgument) + mEH.EXPECT().WithConfig(mock.Anything).Return(nil, heimdall.ErrArgument) }, assert: func(t *testing.T, err error, errorHandler errorhandlers.ErrorHandler) { t.Helper() @@ -450,10 +450,10 @@ func TestHandlerFactoryCreateErrorHandler(t *testing.T) { { uc: "successful creation from prototype", conf: map[string]any{"foo": "bar"}, - configureMock: func(t *testing.T, mEH *mocks2.MockErrorHandler) { + configureMock: func(t *testing.T, mEH *mocks5.ErrorHandlerMock) { t.Helper() - mEH.On("WithConfig", mock.Anything).Return(mEH, nil) + mEH.EXPECT().WithConfig(mock.Anything).Return(mEH, nil) }, assert: func(t *testing.T, err error, errorHandler errorhandlers.ErrorHandler) { t.Helper() @@ -476,9 +476,9 @@ func TestHandlerFactoryCreateErrorHandler(t *testing.T) { // GIVEN configureMock := x.IfThenElse(tc.configureMock != nil, tc.configureMock, - func(t *testing.T, mEH *mocks2.MockErrorHandler) { t.Helper() }) + func(t *testing.T, mEH *mocks5.ErrorHandlerMock) { t.Helper() }) - mEH := &mocks2.MockErrorHandler{} + mEH := mocks5.NewErrorHandlerMock(t) configureMock(t, mEH) factory := &mechanismsFactory{ @@ -492,11 +492,10 @@ func TestHandlerFactoryCreateErrorHandler(t *testing.T) { id := x.IfThenElse(len(tc.id) != 0, tc.id, ID) // WHEN - errorHandler, err := factory.CreateErrorHandler(id, tc.conf) + errorHandler, err := factory.CreateErrorHandler("test", id, tc.conf) // THEN tc.assert(t, err, errorHandler) - mEH.AssertExpectations(t) }) } } diff --git a/internal/rules/mechanisms/mocks/authenticator.go b/internal/rules/mechanisms/mocks/authenticator.go deleted file mode 100644 index 39a8c76d1..000000000 --- a/internal/rules/mechanisms/mocks/authenticator.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators" - "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" -) - -type MockAuthenticator struct { - mock.Mock -} - -func (m *MockAuthenticator) Execute(ctx heimdall.Context) (*subject.Subject, error) { - args := m.Called(ctx) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(*subject.Subject), nil - } - - return nil, args.Error(1) -} - -func (m *MockAuthenticator) WithConfig(config map[string]any) (authenticators.Authenticator, error) { - args := m.Called(config) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(authenticators.Authenticator), nil - } - - return nil, args.Error(1) -} - -func (m *MockAuthenticator) IsFallbackOnErrorAllowed() bool { - return m.Called().Bool(0) -} diff --git a/internal/rules/mechanisms/mocks/authorizer.go b/internal/rules/mechanisms/mocks/authorizer.go deleted file mode 100644 index 94c367eb7..000000000 --- a/internal/rules/mechanisms/mocks/authorizer.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers" - "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" -) - -type MockAuthorizer struct { - mock.Mock -} - -func (m *MockAuthorizer) Execute(ctx heimdall.Context, sub *subject.Subject) error { - return m.Called(ctx, sub).Error(0) -} - -func (m *MockAuthorizer) WithConfig(config map[string]any) (authorizers.Authorizer, error) { - args := m.Called(config) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(authorizers.Authorizer), nil - } - - return nil, args.Error(1) -} - -func (m *MockAuthorizer) ContinueOnError() bool { - return m.Called().Bool(0) -} diff --git a/internal/rules/mechanisms/mocks/contextualizer.go b/internal/rules/mechanisms/mocks/contextualizer.go deleted file mode 100644 index 9b11f35de..000000000 --- a/internal/rules/mechanisms/mocks/contextualizer.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers" - "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" -) - -type MockContextualizer struct { - mock.Mock -} - -func (m *MockContextualizer) Execute(ctx heimdall.Context, sub *subject.Subject) error { - return m.Called(ctx, sub).Error(0) -} - -func (m *MockContextualizer) WithConfig(config map[string]any) (contextualizers.Contextualizer, error) { - args := m.Called(config) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(contextualizers.Contextualizer), nil - } - - return nil, args.Error(1) -} - -func (m *MockContextualizer) ContinueOnError() bool { - return m.Called().Bool(0) -} diff --git a/internal/rules/mechanisms/mocks/error_handler.go b/internal/rules/mechanisms/mocks/error_handler.go deleted file mode 100644 index 8a3a1d962..000000000 --- a/internal/rules/mechanisms/mocks/error_handler.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers" -) - -type MockErrorHandler struct { - mock.Mock -} - -func (m *MockErrorHandler) Execute(ctx heimdall.Context, err error) (bool, error) { - args := m.Called(ctx, err) - - return args.Bool(0), args.Error(0) -} - -func (m *MockErrorHandler) WithConfig(config map[string]any) (errorhandlers.ErrorHandler, error) { - args := m.Called(config) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(errorhandlers.ErrorHandler), nil - } - - return nil, args.Error(1) -} diff --git a/internal/rules/mechanisms/mocks/factory.go b/internal/rules/mechanisms/mocks/factory.go new file mode 100644 index 000000000..695889ad4 --- /dev/null +++ b/internal/rules/mechanisms/mocks/factory.go @@ -0,0 +1,326 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + authenticators "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators" + authorizers "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers" + + config "github.com/dadrus/heimdall/internal/config" + + contextualizers "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers" + + errorhandlers "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers" + + mock "github.com/stretchr/testify/mock" + + unifiers "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers" +) + +// FactoryMock is an autogenerated mock type for the Factory type +type FactoryMock struct { + mock.Mock +} + +type FactoryMock_Expecter struct { + mock *mock.Mock +} + +func (_m *FactoryMock) EXPECT() *FactoryMock_Expecter { + return &FactoryMock_Expecter{mock: &_m.Mock} +} + +// CreateAuthenticator provides a mock function with given fields: version, id, conf +func (_m *FactoryMock) CreateAuthenticator(version string, id string, conf config.MechanismConfig) (authenticators.Authenticator, error) { + ret := _m.Called(version, id, conf) + + var r0 authenticators.Authenticator + var r1 error + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) (authenticators.Authenticator, error)); ok { + return rf(version, id, conf) + } + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) authenticators.Authenticator); ok { + r0 = rf(version, id, conf) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authenticators.Authenticator) + } + } + + if rf, ok := ret.Get(1).(func(string, string, config.MechanismConfig) error); ok { + r1 = rf(version, id, conf) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FactoryMock_CreateAuthenticator_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAuthenticator' +type FactoryMock_CreateAuthenticator_Call struct { + *mock.Call +} + +// CreateAuthenticator is a helper method to define mock.On call +// - version string +// - id string +// - conf config.MechanismConfig +func (_e *FactoryMock_Expecter) CreateAuthenticator(version interface{}, id interface{}, conf interface{}) *FactoryMock_CreateAuthenticator_Call { + return &FactoryMock_CreateAuthenticator_Call{Call: _e.mock.On("CreateAuthenticator", version, id, conf)} +} + +func (_c *FactoryMock_CreateAuthenticator_Call) Run(run func(version string, id string, conf config.MechanismConfig)) *FactoryMock_CreateAuthenticator_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(config.MechanismConfig)) + }) + return _c +} + +func (_c *FactoryMock_CreateAuthenticator_Call) Return(_a0 authenticators.Authenticator, _a1 error) *FactoryMock_CreateAuthenticator_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FactoryMock_CreateAuthenticator_Call) RunAndReturn(run func(string, string, config.MechanismConfig) (authenticators.Authenticator, error)) *FactoryMock_CreateAuthenticator_Call { + _c.Call.Return(run) + return _c +} + +// CreateAuthorizer provides a mock function with given fields: version, id, conf +func (_m *FactoryMock) CreateAuthorizer(version string, id string, conf config.MechanismConfig) (authorizers.Authorizer, error) { + ret := _m.Called(version, id, conf) + + var r0 authorizers.Authorizer + var r1 error + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) (authorizers.Authorizer, error)); ok { + return rf(version, id, conf) + } + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) authorizers.Authorizer); ok { + r0 = rf(version, id, conf) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authorizers.Authorizer) + } + } + + if rf, ok := ret.Get(1).(func(string, string, config.MechanismConfig) error); ok { + r1 = rf(version, id, conf) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FactoryMock_CreateAuthorizer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAuthorizer' +type FactoryMock_CreateAuthorizer_Call struct { + *mock.Call +} + +// CreateAuthorizer is a helper method to define mock.On call +// - version string +// - id string +// - conf config.MechanismConfig +func (_e *FactoryMock_Expecter) CreateAuthorizer(version interface{}, id interface{}, conf interface{}) *FactoryMock_CreateAuthorizer_Call { + return &FactoryMock_CreateAuthorizer_Call{Call: _e.mock.On("CreateAuthorizer", version, id, conf)} +} + +func (_c *FactoryMock_CreateAuthorizer_Call) Run(run func(version string, id string, conf config.MechanismConfig)) *FactoryMock_CreateAuthorizer_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(config.MechanismConfig)) + }) + return _c +} + +func (_c *FactoryMock_CreateAuthorizer_Call) Return(_a0 authorizers.Authorizer, _a1 error) *FactoryMock_CreateAuthorizer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FactoryMock_CreateAuthorizer_Call) RunAndReturn(run func(string, string, config.MechanismConfig) (authorizers.Authorizer, error)) *FactoryMock_CreateAuthorizer_Call { + _c.Call.Return(run) + return _c +} + +// CreateContextualizer provides a mock function with given fields: version, id, conf +func (_m *FactoryMock) CreateContextualizer(version string, id string, conf config.MechanismConfig) (contextualizers.Contextualizer, error) { + ret := _m.Called(version, id, conf) + + var r0 contextualizers.Contextualizer + var r1 error + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) (contextualizers.Contextualizer, error)); ok { + return rf(version, id, conf) + } + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) contextualizers.Contextualizer); ok { + r0 = rf(version, id, conf) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(contextualizers.Contextualizer) + } + } + + if rf, ok := ret.Get(1).(func(string, string, config.MechanismConfig) error); ok { + r1 = rf(version, id, conf) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FactoryMock_CreateContextualizer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateContextualizer' +type FactoryMock_CreateContextualizer_Call struct { + *mock.Call +} + +// CreateContextualizer is a helper method to define mock.On call +// - version string +// - id string +// - conf config.MechanismConfig +func (_e *FactoryMock_Expecter) CreateContextualizer(version interface{}, id interface{}, conf interface{}) *FactoryMock_CreateContextualizer_Call { + return &FactoryMock_CreateContextualizer_Call{Call: _e.mock.On("CreateContextualizer", version, id, conf)} +} + +func (_c *FactoryMock_CreateContextualizer_Call) Run(run func(version string, id string, conf config.MechanismConfig)) *FactoryMock_CreateContextualizer_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(config.MechanismConfig)) + }) + return _c +} + +func (_c *FactoryMock_CreateContextualizer_Call) Return(_a0 contextualizers.Contextualizer, _a1 error) *FactoryMock_CreateContextualizer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FactoryMock_CreateContextualizer_Call) RunAndReturn(run func(string, string, config.MechanismConfig) (contextualizers.Contextualizer, error)) *FactoryMock_CreateContextualizer_Call { + _c.Call.Return(run) + return _c +} + +// CreateErrorHandler provides a mock function with given fields: version, id, conf +func (_m *FactoryMock) CreateErrorHandler(version string, id string, conf config.MechanismConfig) (errorhandlers.ErrorHandler, error) { + ret := _m.Called(version, id, conf) + + var r0 errorhandlers.ErrorHandler + var r1 error + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) (errorhandlers.ErrorHandler, error)); ok { + return rf(version, id, conf) + } + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) errorhandlers.ErrorHandler); ok { + r0 = rf(version, id, conf) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(errorhandlers.ErrorHandler) + } + } + + if rf, ok := ret.Get(1).(func(string, string, config.MechanismConfig) error); ok { + r1 = rf(version, id, conf) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FactoryMock_CreateErrorHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateErrorHandler' +type FactoryMock_CreateErrorHandler_Call struct { + *mock.Call +} + +// CreateErrorHandler is a helper method to define mock.On call +// - version string +// - id string +// - conf config.MechanismConfig +func (_e *FactoryMock_Expecter) CreateErrorHandler(version interface{}, id interface{}, conf interface{}) *FactoryMock_CreateErrorHandler_Call { + return &FactoryMock_CreateErrorHandler_Call{Call: _e.mock.On("CreateErrorHandler", version, id, conf)} +} + +func (_c *FactoryMock_CreateErrorHandler_Call) Run(run func(version string, id string, conf config.MechanismConfig)) *FactoryMock_CreateErrorHandler_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(config.MechanismConfig)) + }) + return _c +} + +func (_c *FactoryMock_CreateErrorHandler_Call) Return(_a0 errorhandlers.ErrorHandler, _a1 error) *FactoryMock_CreateErrorHandler_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FactoryMock_CreateErrorHandler_Call) RunAndReturn(run func(string, string, config.MechanismConfig) (errorhandlers.ErrorHandler, error)) *FactoryMock_CreateErrorHandler_Call { + _c.Call.Return(run) + return _c +} + +// CreateUnifier provides a mock function with given fields: version, id, conf +func (_m *FactoryMock) CreateUnifier(version string, id string, conf config.MechanismConfig) (unifiers.Unifier, error) { + ret := _m.Called(version, id, conf) + + var r0 unifiers.Unifier + var r1 error + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) (unifiers.Unifier, error)); ok { + return rf(version, id, conf) + } + if rf, ok := ret.Get(0).(func(string, string, config.MechanismConfig) unifiers.Unifier); ok { + r0 = rf(version, id, conf) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(unifiers.Unifier) + } + } + + if rf, ok := ret.Get(1).(func(string, string, config.MechanismConfig) error); ok { + r1 = rf(version, id, conf) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FactoryMock_CreateUnifier_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateUnifier' +type FactoryMock_CreateUnifier_Call struct { + *mock.Call +} + +// CreateUnifier is a helper method to define mock.On call +// - version string +// - id string +// - conf config.MechanismConfig +func (_e *FactoryMock_Expecter) CreateUnifier(version interface{}, id interface{}, conf interface{}) *FactoryMock_CreateUnifier_Call { + return &FactoryMock_CreateUnifier_Call{Call: _e.mock.On("CreateUnifier", version, id, conf)} +} + +func (_c *FactoryMock_CreateUnifier_Call) Run(run func(version string, id string, conf config.MechanismConfig)) *FactoryMock_CreateUnifier_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(config.MechanismConfig)) + }) + return _c +} + +func (_c *FactoryMock_CreateUnifier_Call) Return(_a0 unifiers.Unifier, _a1 error) *FactoryMock_CreateUnifier_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FactoryMock_CreateUnifier_Call) RunAndReturn(run func(string, string, config.MechanismConfig) (unifiers.Unifier, error)) *FactoryMock_CreateUnifier_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewFactoryMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewFactoryMock creates a new instance of FactoryMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFactoryMock(t mockConstructorTestingTNewFactoryMock) *FactoryMock { + mock := &FactoryMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/mechanisms/mocks/unifier.go b/internal/rules/mechanisms/mocks/unifier.go deleted file mode 100644 index 37ed2f231..000000000 --- a/internal/rules/mechanisms/mocks/unifier.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" - "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers" -) - -type MockUnifier struct { - mock.Mock -} - -func (m *MockUnifier) Execute(ctx heimdall.Context, sub *subject.Subject) error { - return m.Called(ctx, sub).Error(0) -} - -func (m *MockUnifier) WithConfig(config map[string]any) (unifiers.Unifier, error) { - args := m.Called(config) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(unifiers.Unifier), nil - } - - return nil, args.Error(1) -} - -func (m *MockUnifier) ContinueOnError() bool { - return m.Called().Bool(0) -} diff --git a/internal/rules/mechanisms/oauth2/claims.go b/internal/rules/mechanisms/oauth2/claims.go index cb0f2d3a8..b97110b75 100644 --- a/internal/rules/mechanisms/oauth2/claims.go +++ b/internal/rules/mechanisms/oauth2/claims.go @@ -50,9 +50,5 @@ func (c Claims) Validate(exp Expectation) error { return err } - if err := exp.AssertScopes(x.IfThenElse(len(c.Scp) != 0, c.Scp, c.Scope)); err != nil { - return err - } - - return nil + return exp.AssertScopes(x.IfThenElse(len(c.Scp) != 0, c.Scp, c.Scope)) } diff --git a/internal/rules/mechanisms/oauth2/expectations.go b/internal/rules/mechanisms/oauth2/expectations.go index e7827b682..c7d0f480c 100644 --- a/internal/rules/mechanisms/oauth2/expectations.go +++ b/internal/rules/mechanisms/oauth2/expectations.go @@ -107,10 +107,4 @@ func (e *Expectation) AssertIssuanceTime(issuedAt time.Time) error { return nil } -func (e *Expectation) AssertScopes(scopes []string) error { - if err := e.ScopesMatcher.Match(scopes); err != nil { - return err - } - - return nil -} +func (e *Expectation) AssertScopes(scopes []string) error { return e.ScopesMatcher.Match(scopes) } diff --git a/internal/rules/mechanisms/unifiers/mocks/unifier.go b/internal/rules/mechanisms/unifiers/mocks/unifier.go new file mode 100644 index 000000000..de165a8fb --- /dev/null +++ b/internal/rules/mechanisms/unifiers/mocks/unifier.go @@ -0,0 +1,178 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + heimdall "github.com/dadrus/heimdall/internal/heimdall" + mock "github.com/stretchr/testify/mock" + + subject "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" + + unifiers "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers" +) + +// UnifierMock is an autogenerated mock type for the Unifier type +type UnifierMock struct { + mock.Mock +} + +type UnifierMock_Expecter struct { + mock *mock.Mock +} + +func (_m *UnifierMock) EXPECT() *UnifierMock_Expecter { + return &UnifierMock_Expecter{mock: &_m.Mock} +} + +// ContinueOnError provides a mock function with given fields: +func (_m *UnifierMock) ContinueOnError() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// UnifierMock_ContinueOnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContinueOnError' +type UnifierMock_ContinueOnError_Call struct { + *mock.Call +} + +// ContinueOnError is a helper method to define mock.On call +func (_e *UnifierMock_Expecter) ContinueOnError() *UnifierMock_ContinueOnError_Call { + return &UnifierMock_ContinueOnError_Call{Call: _e.mock.On("ContinueOnError")} +} + +func (_c *UnifierMock_ContinueOnError_Call) Run(run func()) *UnifierMock_ContinueOnError_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UnifierMock_ContinueOnError_Call) Return(_a0 bool) *UnifierMock_ContinueOnError_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UnifierMock_ContinueOnError_Call) RunAndReturn(run func() bool) *UnifierMock_ContinueOnError_Call { + _c.Call.Return(run) + return _c +} + +// Execute provides a mock function with given fields: ctx, sub +func (_m *UnifierMock) Execute(ctx heimdall.Context, sub *subject.Subject) error { + ret := _m.Called(ctx, sub) + + var r0 error + if rf, ok := ret.Get(0).(func(heimdall.Context, *subject.Subject) error); ok { + r0 = rf(ctx, sub) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnifierMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type UnifierMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ctx heimdall.Context +// - sub *subject.Subject +func (_e *UnifierMock_Expecter) Execute(ctx interface{}, sub interface{}) *UnifierMock_Execute_Call { + return &UnifierMock_Execute_Call{Call: _e.mock.On("Execute", ctx, sub)} +} + +func (_c *UnifierMock_Execute_Call) Run(run func(ctx heimdall.Context, sub *subject.Subject)) *UnifierMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context), args[1].(*subject.Subject)) + }) + return _c +} + +func (_c *UnifierMock_Execute_Call) Return(_a0 error) *UnifierMock_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UnifierMock_Execute_Call) RunAndReturn(run func(heimdall.Context, *subject.Subject) error) *UnifierMock_Execute_Call { + _c.Call.Return(run) + return _c +} + +// WithConfig provides a mock function with given fields: config +func (_m *UnifierMock) WithConfig(config map[string]interface{}) (unifiers.Unifier, error) { + ret := _m.Called(config) + + var r0 unifiers.Unifier + var r1 error + if rf, ok := ret.Get(0).(func(map[string]interface{}) (unifiers.Unifier, error)); ok { + return rf(config) + } + if rf, ok := ret.Get(0).(func(map[string]interface{}) unifiers.Unifier); ok { + r0 = rf(config) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(unifiers.Unifier) + } + } + + if rf, ok := ret.Get(1).(func(map[string]interface{}) error); ok { + r1 = rf(config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnifierMock_WithConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithConfig' +type UnifierMock_WithConfig_Call struct { + *mock.Call +} + +// WithConfig is a helper method to define mock.On call +// - config map[string]interface{} +func (_e *UnifierMock_Expecter) WithConfig(config interface{}) *UnifierMock_WithConfig_Call { + return &UnifierMock_WithConfig_Call{Call: _e.mock.On("WithConfig", config)} +} + +func (_c *UnifierMock_WithConfig_Call) Run(run func(config map[string]interface{})) *UnifierMock_WithConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]interface{})) + }) + return _c +} + +func (_c *UnifierMock_WithConfig_Call) Return(_a0 unifiers.Unifier, _a1 error) *UnifierMock_WithConfig_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UnifierMock_WithConfig_Call) RunAndReturn(run func(map[string]interface{}) (unifiers.Unifier, error)) *UnifierMock_WithConfig_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewUnifierMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewUnifierMock creates a new instance of UnifierMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewUnifierMock(t mockConstructorTestingTNewUnifierMock) *UnifierMock { + mock := &UnifierMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/mechanisms/unifiers/unifier.go b/internal/rules/mechanisms/unifiers/unifier.go index 65a509f1d..16018c705 100644 --- a/internal/rules/mechanisms/unifiers/unifier.go +++ b/internal/rules/mechanisms/unifiers/unifier.go @@ -21,6 +21,8 @@ import ( "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) +//go:generate mockery --name Unifier --structname UnifierMock + type Unifier interface { Execute(ctx heimdall.Context, sub *subject.Subject) error WithConfig(config map[string]any) (Unifier, error) diff --git a/internal/rules/mocks/error_handler.go b/internal/rules/mocks/error_handler.go index 19371409d..82551cefa 100644 --- a/internal/rules/mocks/error_handler.go +++ b/internal/rules/mocks/error_handler.go @@ -1,33 +1,89 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks import ( - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/heimdall" + heimdall "github.com/dadrus/heimdall/internal/heimdall" + mock "github.com/stretchr/testify/mock" ) -type MockErrorHandler struct { +// ErrorHandlerMock is an autogenerated mock type for the errorHandler type +type ErrorHandlerMock struct { mock.Mock } -func (m *MockErrorHandler) Execute(ctx heimdall.Context, e error) (bool, error) { - args := m.Called(ctx, e) +type ErrorHandlerMock_Expecter struct { + mock *mock.Mock +} + +func (_m *ErrorHandlerMock) EXPECT() *ErrorHandlerMock_Expecter { + return &ErrorHandlerMock_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: ctx, err +func (_m *ErrorHandlerMock) Execute(ctx heimdall.Context, err error) (bool, error) { + ret := _m.Called(ctx, err) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(heimdall.Context, error) (bool, error)); ok { + return rf(ctx, err) + } + if rf, ok := ret.Get(0).(func(heimdall.Context, error) bool); ok { + r0 = rf(ctx, err) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(heimdall.Context, error) error); ok { + r1 = rf(ctx, err) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ErrorHandlerMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type ErrorHandlerMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ctx heimdall.Context +// - err error +func (_e *ErrorHandlerMock_Expecter) Execute(ctx interface{}, err interface{}) *ErrorHandlerMock_Execute_Call { + return &ErrorHandlerMock_Execute_Call{Call: _e.mock.On("Execute", ctx, err)} +} + +func (_c *ErrorHandlerMock_Execute_Call) Run(run func(ctx heimdall.Context, err error)) *ErrorHandlerMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context), args[1].(error)) + }) + return _c +} + +func (_c *ErrorHandlerMock_Execute_Call) Return(_a0 bool, _a1 error) *ErrorHandlerMock_Execute_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ErrorHandlerMock_Execute_Call) RunAndReturn(run func(heimdall.Context, error) (bool, error)) *ErrorHandlerMock_Execute_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewErrorHandlerMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewErrorHandlerMock creates a new instance of ErrorHandlerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewErrorHandlerMock(t mockConstructorTestingTNewErrorHandlerMock) *ErrorHandlerMock { + mock := &ErrorHandlerMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) - return args.Bool(0), args.Error(1) + return mock } diff --git a/internal/rules/mocks/mechanism_factory.go b/internal/rules/mocks/mechanism_factory.go deleted file mode 100644 index b35bf9c02..000000000 --- a/internal/rules/mocks/mechanism_factory.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators" - "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers" - "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers" - "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers" - "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers" -) - -type MockFactory struct { - mock.Mock -} - -func (m *MockFactory) CreateAuthenticator(id string, conf config.MechanismConfig) ( - authenticators.Authenticator, error, -) { - args := m.Called(id, conf) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(authenticators.Authenticator), nil - } - - return nil, args.Error(1) -} - -func (m *MockFactory) CreateAuthorizer(id string, conf config.MechanismConfig) ( - authorizers.Authorizer, error, -) { - args := m.Called(id, conf) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(authorizers.Authorizer), nil - } - - return nil, args.Error(1) -} - -func (m *MockFactory) CreateContextualizer(id string, conf config.MechanismConfig) ( - contextualizers.Contextualizer, error, -) { - args := m.Called(id, conf) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(contextualizers.Contextualizer), nil - } - - return nil, args.Error(1) -} - -func (m *MockFactory) CreateUnifier(id string, conf config.MechanismConfig) ( - unifiers.Unifier, error, -) { - args := m.Called(id, conf) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(unifiers.Unifier), nil - } - - return nil, args.Error(1) -} - -func (m *MockFactory) CreateErrorHandler(id string, conf config.MechanismConfig) ( - errorhandlers.ErrorHandler, error, -) { - args := m.Called(id, conf) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(errorhandlers.ErrorHandler), nil - } - - return nil, args.Error(1) -} diff --git a/internal/rules/mocks/repository.go b/internal/rules/mocks/repository.go deleted file mode 100644 index 80a8c87f8..000000000 --- a/internal/rules/mocks/repository.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "net/url" - - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/rules/rule" -) - -type MockRepository struct { - mock.Mock -} - -func (m *MockRepository) FindRule(reqURL *url.URL) (rule.Rule, error) { - args := m.Called(reqURL) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(rule.Rule), nil - } - - return nil, args.Error(1) -} diff --git a/internal/rules/mocks/rule_factory.go b/internal/rules/mocks/rule_factory.go deleted file mode 100644 index 56101e3cb..000000000 --- a/internal/rules/mocks/rule_factory.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/rules/rule" -) - -type MockRuleFactory struct { - mock.Mock -} - -func (m *MockRuleFactory) CreateRule(srcID string, ruleConfig rule.Configuration) (rule.Rule, error) { - args := m.Called(srcID, ruleConfig) - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(rule.Rule), nil - } - - return nil, args.Error(1) -} - -func (m *MockRuleFactory) HasDefaultRule() bool { - return m.Called().Bool(0) -} - -func (m *MockRuleFactory) DefaultRule() rule.Rule { - args := m.Called() - - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(rule.Rule) - } - - return nil -} diff --git a/internal/rules/mocks/subject_creator.go b/internal/rules/mocks/subject_creator.go index 356ea8f51..9441d072e 100644 --- a/internal/rules/mocks/subject_creator.go +++ b/internal/rules/mocks/subject_creator.go @@ -1,43 +1,133 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks import ( - "github.com/stretchr/testify/mock" + heimdall "github.com/dadrus/heimdall/internal/heimdall" + mock "github.com/stretchr/testify/mock" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" + subject "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) -type MockSubjectCreator struct { +// SubjectCreatorMock is an autogenerated mock type for the subjectCreator type +type SubjectCreatorMock struct { mock.Mock } -func (a *MockSubjectCreator) Execute(ctx heimdall.Context) (*subject.Subject, error) { - args := a.Called(ctx) +type SubjectCreatorMock_Expecter struct { + mock *mock.Mock +} + +func (_m *SubjectCreatorMock) EXPECT() *SubjectCreatorMock_Expecter { + return &SubjectCreatorMock_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: _a0 +func (_m *SubjectCreatorMock) Execute(_a0 heimdall.Context) (*subject.Subject, error) { + ret := _m.Called(_a0) + + var r0 *subject.Subject + var r1 error + if rf, ok := ret.Get(0).(func(heimdall.Context) (*subject.Subject, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(heimdall.Context) *subject.Subject); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*subject.Subject) + } + } + + if rf, ok := ret.Get(1).(func(heimdall.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubjectCreatorMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type SubjectCreatorMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 heimdall.Context +func (_e *SubjectCreatorMock_Expecter) Execute(_a0 interface{}) *SubjectCreatorMock_Execute_Call { + return &SubjectCreatorMock_Execute_Call{Call: _e.mock.On("Execute", _a0)} +} + +func (_c *SubjectCreatorMock_Execute_Call) Run(run func(_a0 heimdall.Context)) *SubjectCreatorMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context)) + }) + return _c +} + +func (_c *SubjectCreatorMock_Execute_Call) Return(_a0 *subject.Subject, _a1 error) *SubjectCreatorMock_Execute_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *SubjectCreatorMock_Execute_Call) RunAndReturn(run func(heimdall.Context) (*subject.Subject, error)) *SubjectCreatorMock_Execute_Call { + _c.Call.Return(run) + return _c +} - if val := args.Get(0); val != nil { - // nolint: forcetypeassert - return val.(*subject.Subject), nil +// IsFallbackOnErrorAllowed provides a mock function with given fields: +func (_m *SubjectCreatorMock) IsFallbackOnErrorAllowed() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) } - return nil, args.Error(1) + return r0 +} + +// SubjectCreatorMock_IsFallbackOnErrorAllowed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsFallbackOnErrorAllowed' +type SubjectCreatorMock_IsFallbackOnErrorAllowed_Call struct { + *mock.Call +} + +// IsFallbackOnErrorAllowed is a helper method to define mock.On call +func (_e *SubjectCreatorMock_Expecter) IsFallbackOnErrorAllowed() *SubjectCreatorMock_IsFallbackOnErrorAllowed_Call { + return &SubjectCreatorMock_IsFallbackOnErrorAllowed_Call{Call: _e.mock.On("IsFallbackOnErrorAllowed")} +} + +func (_c *SubjectCreatorMock_IsFallbackOnErrorAllowed_Call) Run(run func()) *SubjectCreatorMock_IsFallbackOnErrorAllowed_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SubjectCreatorMock_IsFallbackOnErrorAllowed_Call) Return(_a0 bool) *SubjectCreatorMock_IsFallbackOnErrorAllowed_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SubjectCreatorMock_IsFallbackOnErrorAllowed_Call) RunAndReturn(run func() bool) *SubjectCreatorMock_IsFallbackOnErrorAllowed_Call { + _c.Call.Return(run) + return _c } -func (a *MockSubjectCreator) IsFallbackOnErrorAllowed() bool { - return a.Called().Bool(0) +type mockConstructorTestingTNewSubjectCreatorMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewSubjectCreatorMock creates a new instance of SubjectCreatorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewSubjectCreatorMock(t mockConstructorTestingTNewSubjectCreatorMock) *SubjectCreatorMock { + mock := &SubjectCreatorMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/internal/rules/mocks/subject_handler.go b/internal/rules/mocks/subject_handler.go index 0bd5e8987..30581bccf 100644 --- a/internal/rules/mocks/subject_handler.go +++ b/internal/rules/mocks/subject_handler.go @@ -1,36 +1,122 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks import ( - "github.com/stretchr/testify/mock" + heimdall "github.com/dadrus/heimdall/internal/heimdall" + mock "github.com/stretchr/testify/mock" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" + subject "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) -type MockSubjectHandler struct { +// SubjectHandlerMock is an autogenerated mock type for the subjectHandler type +type SubjectHandlerMock struct { mock.Mock } -func (m *MockSubjectHandler) Execute(ctx heimdall.Context, sub *subject.Subject) error { - return m.Called(ctx, sub).Error(0) +type SubjectHandlerMock_Expecter struct { + mock *mock.Mock } -func (m *MockSubjectHandler) ContinueOnError() bool { - return m.Called().Bool(0) +func (_m *SubjectHandlerMock) EXPECT() *SubjectHandlerMock_Expecter { + return &SubjectHandlerMock_Expecter{mock: &_m.Mock} +} + +// ContinueOnError provides a mock function with given fields: +func (_m *SubjectHandlerMock) ContinueOnError() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// SubjectHandlerMock_ContinueOnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContinueOnError' +type SubjectHandlerMock_ContinueOnError_Call struct { + *mock.Call +} + +// ContinueOnError is a helper method to define mock.On call +func (_e *SubjectHandlerMock_Expecter) ContinueOnError() *SubjectHandlerMock_ContinueOnError_Call { + return &SubjectHandlerMock_ContinueOnError_Call{Call: _e.mock.On("ContinueOnError")} +} + +func (_c *SubjectHandlerMock_ContinueOnError_Call) Run(run func()) *SubjectHandlerMock_ContinueOnError_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *SubjectHandlerMock_ContinueOnError_Call) Return(_a0 bool) *SubjectHandlerMock_ContinueOnError_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SubjectHandlerMock_ContinueOnError_Call) RunAndReturn(run func() bool) *SubjectHandlerMock_ContinueOnError_Call { + _c.Call.Return(run) + return _c +} + +// Execute provides a mock function with given fields: _a0, _a1 +func (_m *SubjectHandlerMock) Execute(_a0 heimdall.Context, _a1 *subject.Subject) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(heimdall.Context, *subject.Subject) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubjectHandlerMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type SubjectHandlerMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 heimdall.Context +// - _a1 *subject.Subject +func (_e *SubjectHandlerMock_Expecter) Execute(_a0 interface{}, _a1 interface{}) *SubjectHandlerMock_Execute_Call { + return &SubjectHandlerMock_Execute_Call{Call: _e.mock.On("Execute", _a0, _a1)} +} + +func (_c *SubjectHandlerMock_Execute_Call) Run(run func(_a0 heimdall.Context, _a1 *subject.Subject)) *SubjectHandlerMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context), args[1].(*subject.Subject)) + }) + return _c +} + +func (_c *SubjectHandlerMock_Execute_Call) Return(_a0 error) *SubjectHandlerMock_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SubjectHandlerMock_Execute_Call) RunAndReturn(run func(heimdall.Context, *subject.Subject) error) *SubjectHandlerMock_Execute_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewSubjectHandlerMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewSubjectHandlerMock creates a new instance of SubjectHandlerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewSubjectHandlerMock(t mockConstructorTestingTNewSubjectHandlerMock) *SubjectHandlerMock { + mock := &SubjectHandlerMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/internal/rules/module.go b/internal/rules/module.go index 0f2acfb0a..20f88a761 100644 --- a/internal/rules/module.go +++ b/internal/rules/module.go @@ -24,6 +24,7 @@ import ( "github.com/dadrus/heimdall/internal/rules/event" "github.com/dadrus/heimdall/internal/rules/provider" + "github.com/dadrus/heimdall/internal/rules/rule" ) const defaultQueueSize = 20 @@ -31,44 +32,29 @@ const defaultQueueSize = 20 // Module is invoked on app bootstrapping. // nolint: gochecknoglobals var Module = fx.Options( - fx.Provide(func(logger zerolog.Logger) event.RuleSetChangedEventQueue { - logger.Debug().Msg("Creating rule set event queue.") - - return make(event.RuleSetChangedEventQueue, defaultQueueSize) - }), - fx.Provide(NewRepository, NewRuleFactory), - fx.Invoke(registerRuleDefinitionHandler, registerRuleSetChangedEventQueueCloser), + fx.Provide( + fx.Annotate( + func(logger zerolog.Logger) event.RuleSetChangedEventQueue { + logger.Debug().Msg("Creating rule set event queue.") + + return make(event.RuleSetChangedEventQueue, defaultQueueSize) + }, + fx.OnStop( + func(queue event.RuleSetChangedEventQueue, logger zerolog.Logger) { + logger.Debug().Msg("Closing rule set event queue") + + close(queue) + }, + ), + ), + NewRuleFactory, + fx.Annotate( + newRepository, + fx.OnStart(func(ctx context.Context, o *repository) error { return o.Start(ctx) }), + fx.OnStop(func(ctx context.Context, o *repository) error { return o.Stop(ctx) }), + ), + func(r *repository) rule.Repository { return r }, + NewRuleSetProcessor, + ), provider.Module, ) - -func registerRuleSetChangedEventQueueCloser( - lifecycle fx.Lifecycle, - queue event.RuleSetChangedEventQueue, - logger zerolog.Logger, -) { - lifecycle.Append(fx.Hook{ - OnStop: func(ctx context.Context) error { - logger.Debug().Msg("Closing rule set event queue") - - close(queue) - - return nil - }, - }) -} - -func registerRuleDefinitionHandler(lifecycle fx.Lifecycle, logger zerolog.Logger, r Repository) { - rdf, ok := r.(ruleSetDefinitionLoader) - if !ok { - logger.Fatal().Msg("No rule set definition loader available") - - return - } - - lifecycle.Append( - fx.Hook{ - OnStart: func(ctx context.Context) error { return rdf.Start(ctx) }, - OnStop: func(ctx context.Context) error { return rdf.Stop(ctx) }, - }, - ) -} diff --git a/internal/rules/patternmatcher/glob_matcher.go b/internal/rules/patternmatcher/glob_matcher.go index 08422d284..b944b8994 100644 --- a/internal/rules/patternmatcher/glob_matcher.go +++ b/internal/rules/patternmatcher/glob_matcher.go @@ -23,7 +23,10 @@ import ( "github.com/gobwas/glob" ) -var ErrUnbalancedPattern = errors.New("unbalanced pattern") +var ( + ErrUnbalancedPattern = errors.New("unbalanced pattern") + ErrNoGlobPatternDefined = errors.New("no glob pattern defined") +) type globMatcher struct { compiled glob.Glob @@ -34,6 +37,10 @@ func (m *globMatcher) Match(value string) bool { } func newGlobMatcher(pattern string) (*globMatcher, error) { + if len(pattern) == 0 { + return nil, ErrNoGlobPatternDefined + } + compiled, err := compileGlob(pattern, '<', '>') if err != nil { return nil, err diff --git a/internal/rules/patternmatcher/pattern_matcher.go b/internal/rules/patternmatcher/pattern_matcher.go index 03b51eccc..4a0ae68d4 100644 --- a/internal/rules/patternmatcher/pattern_matcher.go +++ b/internal/rules/patternmatcher/pattern_matcher.go @@ -16,7 +16,9 @@ package patternmatcher -import "errors" +import ( + "errors" +) var ErrUnsupportedPatternMatcher = errors.New("unsupported pattern matcher") diff --git a/internal/rules/patternmatcher/regex_matcher.go b/internal/rules/patternmatcher/regex_matcher.go index 911f381e4..7b223b3d6 100644 --- a/internal/rules/patternmatcher/regex_matcher.go +++ b/internal/rules/patternmatcher/regex_matcher.go @@ -17,15 +17,23 @@ package patternmatcher import ( + "errors" + "github.com/dlclark/regexp2" "github.com/ory/ladon/compiler" ) +var ErrNoRegexPatternDefined = errors.New("no glob pattern defined") + type regexpMatcher struct { compiled *regexp2.Regexp } func newRegexMatcher(pattern string) (*regexpMatcher, error) { + if len(pattern) == 0 { + return nil, ErrNoRegexPatternDefined + } + compiled, err := compiler.CompileRegex(pattern, '<', '>') if err != nil { return nil, err diff --git a/internal/rules/provider/cloudblob/module.go b/internal/rules/provider/cloudblob/module.go index 83a88d3a5..f46a29426 100644 --- a/internal/rules/provider/cloudblob/module.go +++ b/internal/rules/provider/cloudblob/module.go @@ -16,10 +16,20 @@ package cloudblob -import "go.uber.org/fx" +import ( + "context" + + "go.uber.org/fx" +) // Module is used on app bootstrap. // nolint: gochecknoglobals var Module = fx.Options( - fx.Invoke(registerProvider), + fx.Invoke( + fx.Annotate( + newProvider, + fx.OnStart(func(ctx context.Context, p *provider) error { return p.Start(ctx) }), + fx.OnStop(func(ctx context.Context, p *provider) error { return p.Stop(ctx) }), + ), + ), ) diff --git a/internal/rules/provider/cloudblob/provider.go b/internal/rules/provider/cloudblob/provider.go index 9e4e44792..d672ad69b 100644 --- a/internal/rules/provider/cloudblob/provider.go +++ b/internal/rules/provider/cloudblob/provider.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "errors" + "fmt" "sync" "time" @@ -28,8 +29,10 @@ import ( "golang.org/x/exp/maps" "golang.org/x/exp/slices" + "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" + rule_config "github.com/dadrus/heimdall/internal/rules/config" + "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x" "github.com/dadrus/heimdall/internal/x/errorchain" "github.com/dadrus/heimdall/internal/x/slicex" @@ -38,62 +41,66 @@ import ( type BucketState map[string][]byte type provider struct { - q event.RuleSetChangedEventQueue - l zerolog.Logger - s *gocron.Scheduler - cancel context.CancelFunc - - mu sync.Mutex - states map[string]BucketState + p rule.SetProcessor + l zerolog.Logger + s *gocron.Scheduler + cancel context.CancelFunc + states sync.Map + configured bool } func newProvider( - rawConf map[string]any, - queue event.RuleSetChangedEventQueue, + conf *config.Configuration, + processor rule.SetProcessor, logger zerolog.Logger, ) (*provider, error) { + rawConf := conf.Rules.Providers.CloudBlob + + if rawConf == nil { + return &provider{}, nil + } + type Config struct { Buckets []*ruleSetEndpoint `mapstructure:"buckets"` WatchInterval *time.Duration `mapstructure:"watch_interval"` } - var conf Config - if err := decodeConfig(rawConf, &conf); err != nil { + var providerConf Config + if err := decodeConfig(rawConf, &providerConf); err != nil { return nil, errorchain. NewWithMessage(heimdall.ErrConfiguration, "failed to decode cloud_blob rule provider config"). CausedBy(err) } - if len(conf.Buckets) == 0 { + if len(providerConf.Buckets) == 0 { return nil, errorchain.NewWithMessage(heimdall.ErrConfiguration, "no buckets configured for cloud_blob rule provider") } + logger = logger.With().Str("_provider_type", "cloud_blob").Logger() + ctx, cancel := context.WithCancel(context.Background()) - ctx = logger.With(). - Str("_rule_provider_type", "cloud_blob"). - Logger(). - WithContext(ctx) + ctx = logger.With().Logger().WithContext(ctx) scheduler := gocron.NewScheduler(time.UTC) scheduler.SingletonModeAll() prov := &provider{ - q: queue, - l: logger, - s: scheduler, - cancel: cancel, - states: make(map[string]BucketState), + p: processor, + l: logger, + s: scheduler, + cancel: cancel, + configured: true, } - for idx, bucket := range conf.Buckets { + for idx, bucket := range providerConf.Buckets { if bucket.URL == nil { return nil, errorchain.NewWithMessagef(heimdall.ErrConfiguration, "missing url for #%d bucket in cloud_blob rule provider configuration", idx) } - if _, err := x.IfThenElseExec(conf.WatchInterval != nil && *conf.WatchInterval > 0, - func() *gocron.Scheduler { return prov.s.Every(*conf.WatchInterval) }, + if _, err := x.IfThenElseExec(providerConf.WatchInterval != nil && *providerConf.WatchInterval > 0, + func() *gocron.Scheduler { return prov.s.Every(*providerConf.WatchInterval) }, func() *gocron.Scheduler { return prov.s.Every(1 * time.Second).LimitRunsTo(1) }). Do(prov.watchChanges, ctx, bucket); err != nil { return nil, errorchain.NewWithMessagef(heimdall.ErrInternal, @@ -102,13 +109,17 @@ func newProvider( } } + logger.Info().Msg("Rule provider configured.") + return prov, nil } func (p *provider) Start(_ context.Context) error { - p.l.Info(). - Str("_rule_provider_type", "cloud_blob"). - Msg("Starting rule definitions provider") + if !p.configured { + return nil + } + + p.l.Info().Msg("Starting rule definitions provider") p.s.StartAsync() //nolint:contextcheck @@ -116,26 +127,31 @@ func (p *provider) Start(_ context.Context) error { } func (p *provider) Stop(_ context.Context) error { - p.l.Info(). - Str("_rule_provider_type", "cloud_blob"). - Msg("Tearing down rule provider.") + if !p.configured { + return nil + } + + p.l.Info().Msg("Tearing down rule provider.") - p.cancel() p.s.Stop() + p.cancel() return nil } func (p *provider) watchChanges(ctx context.Context, rsf RuleSetFetcher) error { - p.l.Debug(). - Str("_rule_provider_type", "cloud_blob"). - Msg("Retrieving rule set") + p.l.Debug().Msg("Retrieving rule set") ruleSets, err := rsf.FetchRuleSets(ctx) if err != nil { + if errors.Is(err, context.Canceled) { + p.l.Debug().Msg("Watcher closed") + + return nil + } + p.l.Warn(). Err(err). - Str("_rule_provider_type", "cloud_blob"). Str("_endpoint", rsf.ID()). Msg("Failed to fetch rule set") @@ -148,20 +164,19 @@ func (p *provider) watchChanges(ctx context.Context, rsf RuleSetFetcher) error { // if no rule sets are available and no rule sets were known from the past if len(ruleSets) == 0 && len(state) == 0 { - p.l.Debug(). - Str("_rule_provider_type", "cloud_blob"). - Str("_endpoint", rsf.ID()). - Msg("No updates received") + p.l.Debug().Str("_endpoint", rsf.ID()).Msg("No updates received") return nil } - p.ruleSetsUpdated(ruleSets, state, rsf.ID()) + if err = p.ruleSetsUpdated(ruleSets, state, rsf.ID()); err != nil { + p.l.Warn().Err(err).Str("_endpoint", rsf.ID()).Msg("Failed to apply rule set changes") + } return nil } -func (p *provider) ruleSetsUpdated(ruleSets []RuleSet, state BucketState, buketID string) { +func (p *provider) ruleSetsUpdated(ruleSets []*rule_config.RuleSet, state BucketState, buketID string) error { // check which were present in the past and are not present now // and which are new currentIDs := toRuleSetIDs(ruleSets) @@ -171,74 +186,64 @@ func (p *provider) ruleSetsUpdated(ruleSets []RuleSet, state BucketState, buketI newIDs := slicex.Subtract(currentIDs, oldIDs) for _, ID := range removedIDs { - delete(state, ID) + conf := &rule_config.RuleSet{ + MetaData: rule_config.MetaData{ + Source: fmt.Sprintf("blob:%s", ID), + ModTime: time.Now(), + }, + } - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: "blob:" + ID, - ChangeType: event.Remove, - }) + if err := p.p.OnDeleted(conf); err != nil { + return err + } + + delete(state, ID) } // check which rule sets are new and which are modified for _, ruleSet := range ruleSets { - isNew := slices.Contains(newIDs, ruleSet.Key) - hasChanged := !isNew && !bytes.Equal(state[ruleSet.Key], ruleSet.Hash) - - state[ruleSet.Key] = ruleSet.Hash + isNew := slices.Contains(newIDs, ruleSet.Source) + hasChanged := !isNew && !bytes.Equal(state[ruleSet.Source], ruleSet.Hash) if !isNew && !hasChanged { p.l.Debug(). - Str("_rule_provider_type", "cloud_blob"). Str("_bucket", buketID). - Str("_rule_set", ruleSet.Key). + Str("_rule_set", ruleSet.Source). Msg("No updates received") continue } - if hasChanged { - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: "blob:" + ruleSet.Key, - ChangeType: event.Remove, - }) + var err error + + if isNew { + err = p.p.OnCreated(ruleSet) + } else if hasChanged { + err = p.p.OnUpdated(ruleSet) + } + + if err != nil { + return err } - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: "blob:" + ruleSet.Key, - ChangeType: event.Create, - RuleSet: ruleSet.Rules, - }) + state[ruleSet.Source] = ruleSet.Hash } + + return nil } func (p *provider) getBucketState(key string) BucketState { - p.mu.Lock() - state, present := p.states[key] + value, _ := p.states.LoadOrStore(key, make(BucketState)) - if !present { - state = make(BucketState) - p.states[key] = state - } - p.mu.Unlock() - - return state + return value.(BucketState) // nolint: forcetypeassert } -func toRuleSetIDs(ruleSets []RuleSet) []string { +func toRuleSetIDs(ruleSets []*rule_config.RuleSet) []string { currentIDs := make([]string, len(ruleSets)) for idx, ruleSet := range ruleSets { - currentIDs[idx] = ruleSet.Key + currentIDs[idx] = ruleSet.Source } return currentIDs } - -func (p *provider) ruleSetChanged(evt event.RuleSetChangedEvent) { - p.l.Info(). - Str("_rule_provider_type", "cloud_blob"). - Str("_src", evt.Src). - Str("_type", evt.ChangeType.String()). - Msg("Rule set changed") - p.q <- evt -} diff --git a/internal/rules/provider/cloudblob/provider_registrar.go b/internal/rules/provider/cloudblob/provider_registrar.go deleted file mode 100644 index d8fcd4b6e..000000000 --- a/internal/rules/provider/cloudblob/provider_registrar.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package cloudblob - -import ( - "context" - - "github.com/rs/zerolog" - "go.uber.org/fx" - - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/x/errorchain" -) - -type registrationArguments struct { - fx.In - - Lifecycle fx.Lifecycle - Config *config.Configuration - Queue event.RuleSetChangedEventQueue -} - -func registerProvider(args registrationArguments, logger zerolog.Logger) error { - if args.Config.Rules.Providers.CloudBlob == nil { - return nil - } - - provider, err := newProvider(args.Config.Rules.Providers.CloudBlob, args.Queue, logger) - if err != nil { - return errorchain.NewWithMessage(heimdall.ErrInternal, "failed to create cloud_blob provider"). - CausedBy(err) - } - - logger.Info(). - Str("_rule_provider_type", "cloud_blob"). - Msg("Rule provider configured.") - - args.Lifecycle.Append( - fx.Hook{ - OnStart: func(ctx context.Context) error { return provider.Start(ctx) }, - OnStop: func(ctx context.Context) error { return provider.Stop(ctx) }, - }, - ) - - return nil -} diff --git a/internal/rules/provider/cloudblob/provider_registrar_test.go b/internal/rules/provider/cloudblob/provider_registrar_test.go deleted file mode 100644 index 4a739ae47..000000000 --- a/internal/rules/provider/cloudblob/provider_registrar_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package cloudblob - -import ( - "testing" - - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.uber.org/fx" - - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/x" - "github.com/dadrus/heimdall/internal/x/testsupport" -) - -type mockLifecycle struct{ mock.Mock } - -func (m *mockLifecycle) Append(hook fx.Hook) { m.Called(hook) } - -func TestRegisterProvider(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - conf []byte - setupMocks func(t *testing.T, mockLC *mockLifecycle) - assert func(t *testing.T, err error) - }{ - { - uc: "without it being configured", - assert: func(t *testing.T, err error) { - t.Helper() - - assert.NoError(t, err) - }, - }, - { - uc: "with invalid configuration, unknown filed", - conf: []byte(`foo: bar`), - assert: func(t *testing.T, err error) { - t.Helper() - - require.Error(t, err) - assert.ErrorIs(t, err, heimdall.ErrConfiguration) - assert.Contains(t, err.Error(), "failed to decode") - }, - }, - { - uc: "with valid configuration", - conf: []byte(` -buckets: - - url: s3://foo.bar -`), - setupMocks: func(t *testing.T, mockLC *mockLifecycle) { - t.Helper() - - mockLC.On("Append", mock.AnythingOfType("fx.Hook")) - }, - assert: func(t *testing.T, err error) { - t.Helper() - - require.NoError(t, err) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - providerConf, err := testsupport.DecodeTestConfig(tc.conf) - require.NoError(t, err) - - conf := &config.Configuration{ - Rules: config.Rules{ - Providers: config.RuleProviders{CloudBlob: providerConf}, - }, - } - queue := make(event.RuleSetChangedEventQueue, 10) - mlc := &mockLifecycle{} - - setupMocks := x.IfThenElse(tc.setupMocks != nil, - tc.setupMocks, - func(t *testing.T, mockLC *mockLifecycle) { t.Helper() }) - - setupMocks(t, mlc) - - args := registrationArguments{Lifecycle: mlc, Config: conf, Queue: queue} - - // WHEN - err = registerProvider(args, log.Logger) - - // THEN - tc.assert(t, err) - - mlc.AssertExpectations(t) - }) - } -} diff --git a/internal/rules/provider/cloudblob/provider_test.go b/internal/rules/provider/cloudblob/provider_test.go index 708cd3102..17e41999a 100644 --- a/internal/rules/provider/cloudblob/provider_test.go +++ b/internal/rules/provider/cloudblob/provider_test.go @@ -29,12 +29,16 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" + config2 "github.com/dadrus/heimdall/internal/rules/config" + "github.com/dadrus/heimdall/internal/rules/rule/mocks" "github.com/dadrus/heimdall/internal/x" "github.com/dadrus/heimdall/internal/x/testsupport" + mock2 "github.com/dadrus/heimdall/internal/x/testsupport/mock" ) func TestNewProvider(t *testing.T) { @@ -115,7 +119,7 @@ buckets: require.NotNil(t, prov) assert.NotNil(t, prov.s) - assert.NotNil(t, prov.q) + assert.NotNil(t, prov.p) assert.NotNil(t, prov.cancel) assert.False(t, prov.s.IsRunning()) assert.Len(t, prov.s.Jobs(), 2) @@ -129,10 +133,14 @@ buckets: providerConf, err := testsupport.DecodeTestConfig(tc.conf) require.NoError(t, err) - queue := make(event.RuleSetChangedEventQueue, 10) + conf := &config.Configuration{ + Rules: config.Rules{ + Providers: config.RuleProviders{CloudBlob: providerConf}, + }, + } // WHEN - prov, err := newProvider(providerConf, queue, log.Logger) + prov, err := newProvider(conf, mocks.NewRuleSetProcessorMock(t), log.Logger) // THEN tc.assert(t, err, prov) @@ -169,10 +177,11 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx } type testCase struct { - uc string - conf []byte - setupBucket func(t *testing.T) - assert func(t *testing.T, tc testCase, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) + uc string + conf []byte + setupBucket func(t *testing.T) + setupProcessor func(t *testing.T, processor *mocks.RuleSetProcessorMock) + assert func(t *testing.T, tc testCase, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) } for _, tc := range []testCase{ @@ -182,7 +191,7 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx buckets: - url: s3://does-not-exist-for-heimdall?endpoint=does-not-exist.local®ion=eu-central-1 `), - assert: func(t *testing.T, tc testCase, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + assert: func(t *testing.T, tc testCase, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(1 * time.Second) @@ -200,7 +209,7 @@ buckets: buckets: - url: s3://` + bucketName + `?endpoint=` + srv.URL + `&disableSSL=true&s3ForcePathStyle=true®ion=eu-central-1 `), - assert: func(t *testing.T, tc testCase, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + assert: func(t *testing.T, tc testCase, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) @@ -224,7 +233,7 @@ buckets: strings.NewReader(``), 0) require.NoError(t, err) }, - assert: func(t *testing.T, tc testCase, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + assert: func(t *testing.T, tc testCase, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) @@ -243,27 +252,38 @@ buckets: setupBucket: func(t *testing.T) { t.Helper() - data := "- id: foo" + data := ` +version: "1" +name: test +rules: +- id: foo +` _, err := backend.PutObject(bucketName, "test-rule", map[string]string{"Content-Type": "application/yaml"}, strings.NewReader(data), int64(len(data))) require.NoError(t, err) }, - assert: func(t *testing.T, tc testCase, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, tc testCase, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(600 * time.Millisecond) assert.NotContains(t, logs.String(), "No updates received") - require.Len(t, queue, 1) - - evt := <-queue - assert.Contains(t, evt.Src, "blob:test-rule@s3") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "test-rule@s3://"+bucketName) + assert.Equal(t, "1", ruleSet.Version) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "foo", ruleSet.Rules[0].ID) + assert.Contains(t, ruleSet.Source, "test-rule@s3") }, }, { @@ -276,27 +296,38 @@ buckets: setupBucket: func(t *testing.T) { t.Helper() - data := "- id: foo" + data := ` +version: "1" +name: test +rules: +- id: foo +` _, err := backend.PutObject(bucketName, "test-rule", map[string]string{"Content-Type": "application/yaml"}, strings.NewReader(data), int64(len(data))) require.NoError(t, err) }, - assert: func(t *testing.T, tc testCase, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, tc testCase, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(600 * time.Millisecond) assert.Contains(t, logs.String(), "No updates received") - require.Len(t, queue, 1) - - evt := <-queue - assert.Contains(t, evt.Src, "blob:test-rule@s3") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "test-rule@s3://"+bucketName) + assert.Equal(t, "1", ruleSet.Version) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "foo", ruleSet.Rules[0].ID) + assert.Contains(t, ruleSet.Source, "test-rule@s3") }, }, { @@ -314,7 +345,12 @@ buckets: switch callIdx { case 1: - data := "- id: foo" + data := ` +version: "1" +name: test +rules: +- id: foo +` _, err := backend.PutObject(bucketName, "test-rule1", map[string]string{"Content-Type": "application/yaml"}, @@ -323,7 +359,12 @@ buckets: case 2: clearBucket(t) default: - data := "- id: bar" + data := ` +version: "1" +name: test +rules: +- id: bar +` _, err := backend.PutObject(bucketName, "test-rule2", map[string]string{"Content-Type": "application/yaml"}, @@ -334,7 +375,18 @@ buckets: callIdx++ } }(), - assert: func(t *testing.T, tc testCase, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Twice() + + processor.EXPECT().OnDeleted(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor2").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, tc testCase, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(150 * time.Millisecond) @@ -349,24 +401,19 @@ buckets: assert.Contains(t, logs.String(), "No updates received") - require.Len(t, queue, 3) - - evt := <-queue - assert.Contains(t, evt.Src, "blob:test-rule1@s3") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "blob:test-rule1@s3") - assert.Len(t, evt.RuleSet, 0) - assert.Equal(t, event.Remove, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "blob:test-rule2@s3") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "bar", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSets := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Values() + require.Len(t, ruleSets, 2) + assert.Equal(t, "foo", ruleSets[0].Rules[0].ID) + assert.Contains(t, ruleSets[0].Source, "test-rule1@s3") + assert.Equal(t, "1", ruleSets[0].Version) + assert.Len(t, ruleSets[0].Rules, 1) + assert.Contains(t, ruleSets[1].Source, "test-rule2@s3") + assert.Equal(t, "1", ruleSets[1].Version) + assert.Len(t, ruleSets[1].Rules, 1) + assert.Equal(t, "bar", ruleSets[1].Rules[0].ID) + + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor2").Value() + assert.Contains(t, ruleSet.Source, "test-rule1@s3") }, }, { @@ -384,21 +431,36 @@ buckets: switch callIdx { case 1: - data := "- id: foo" + data := ` +version: "1" +name: test +rules: +- id: foo +` _, err := backend.PutObject(bucketName, "test-rule", map[string]string{"Content-Type": "application/yaml"}, strings.NewReader(data), int64(len(data))) require.NoError(t, err) case 2: - data := "- id: bar" + data := ` +version: "1" +name: test +rules: +- id: bar +` _, err := backend.PutObject(bucketName, "test-rule", map[string]string{"Content-Type": "application/yaml"}, strings.NewReader(data), int64(len(data))) require.NoError(t, err) default: - data := "- id: baz" + data := ` +version: "1" +name: test +rules: +- id: baz +` _, err := backend.PutObject(bucketName, "test-rule", map[string]string{"Content-Type": "application/yaml"}, @@ -409,7 +471,17 @@ buckets: callIdx++ } }(), - assert: func(t *testing.T, tc testCase, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + processor.EXPECT().OnUpdated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor2").Capture). + Return(nil).Twice() + }, + assert: func(t *testing.T, tc testCase, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(150 * time.Millisecond) @@ -424,35 +496,21 @@ buckets: assert.Contains(t, logs.String(), "No updates received") - require.Len(t, queue, 5) - - evt := <-queue - assert.Contains(t, evt.Src, "blob:test-rule@s3") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "blob:test-rule@s3") - assert.Len(t, evt.RuleSet, 0) - assert.Equal(t, event.Remove, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "blob:test-rule@s3") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "bar", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "blob:test-rule@s3") - assert.Len(t, evt.RuleSet, 0) - assert.Equal(t, event.Remove, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "blob:test-rule@s3") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "baz", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Equal(t, "foo", ruleSet.Rules[0].ID) + assert.Contains(t, ruleSet.Source, "test-rule@s3") + assert.Len(t, ruleSet.Rules, 1) + + ruleSets := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor2").Values() + assert.Len(t, ruleSets, 2) + assert.Contains(t, ruleSets[0].Source, "test-rule@s3") + assert.Equal(t, "1", ruleSets[0].Version) + assert.Len(t, ruleSets[0].Rules, 1) + assert.Equal(t, "bar", ruleSets[0].Rules[0].ID) + assert.Contains(t, ruleSets[1].Source, "test-rule@s3") + assert.Equal(t, "1", ruleSets[1].Version) + assert.Len(t, ruleSets[1].Rules, 1) + assert.Equal(t, "baz", ruleSets[1].Rules[0].ID) }, }, } { @@ -460,16 +518,31 @@ buckets: // GIVEN clearBucket(t) - setupBucket := x.IfThenElse(tc.setupBucket != nil, tc.setupBucket, func(t *testing.T) { t.Helper() }) + setupBucket := x.IfThenElse( + tc.setupBucket != nil, + tc.setupBucket, + func(t *testing.T) { t.Helper() }, + ) + setupProcessor := x.IfThenElse( + tc.setupProcessor != nil, + tc.setupProcessor, + func(t *testing.T, _ *mocks.RuleSetProcessorMock) { t.Helper() }, + ) providerConf, err := testsupport.DecodeTestConfig(tc.conf) require.NoError(t, err) - queue := make(event.RuleSetChangedEventQueue, 10) - defer close(queue) + mock := mocks.NewRuleSetProcessorMock(t) + setupProcessor(t, mock) + + conf := &config.Configuration{ + Rules: config.Rules{ + Providers: config.RuleProviders{CloudBlob: providerConf}, + }, + } logs := &strings.Builder{} - prov, err := newProvider(providerConf, queue, zerolog.New(logs)) + prov, err := newProvider(conf, mock, zerolog.New(logs)) require.NoError(t, err) ctx := context.Background() @@ -483,7 +556,7 @@ buckets: // THEN require.NoError(t, err) - tc.assert(t, tc, logs, queue) + tc.assert(t, tc, logs, mock) }) } } diff --git a/internal/rules/provider/cloudblob/ruleset_endpoint.go b/internal/rules/provider/cloudblob/ruleset_endpoint.go index 0ea4ca536..ba0be56f4 100644 --- a/internal/rules/provider/cloudblob/ruleset_endpoint.go +++ b/internal/rules/provider/cloudblob/ruleset_endpoint.go @@ -30,24 +30,21 @@ import ( "gocloud.dev/gcerrors" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/provider/pathprefix" - "github.com/dadrus/heimdall/internal/rules/provider/rulesetparser" + "github.com/dadrus/heimdall/internal/rules/config" "github.com/dadrus/heimdall/internal/x/errorchain" ) -var errEmptyRuleSet = errors.New("empty rule set") - type ruleSetEndpoint struct { - URL *url.URL `mapstructure:"url"` - Prefix string `mapstructure:"prefix"` - RulesPathPrefix pathprefix.PathPrefix `mapstructure:"rule_path_match_prefix"` + URL *url.URL `mapstructure:"url"` + Prefix string `mapstructure:"prefix"` + RulesPathPrefix string `mapstructure:"rule_path_match_prefix"` } func (e *ruleSetEndpoint) ID() string { return fmt.Sprintf("%s/%s", e.URL, e.Prefix) } -func (e *ruleSetEndpoint) FetchRuleSets(ctx context.Context) ([]RuleSet, error) { +func (e *ruleSetEndpoint) FetchRuleSets(ctx context.Context) ([]*config.RuleSet, error) { bucket, err := blob.OpenBucket(ctx, e.URL.String()) if err != nil { return nil, errorchain.NewWithMessage(heimdall.ErrInternal, "failed to open bucket"). @@ -63,8 +60,8 @@ func (e *ruleSetEndpoint) FetchRuleSets(ctx context.Context) ([]RuleSet, error) return e.readAllBlobs(ctx, bucket) } -func (e *ruleSetEndpoint) readAllBlobs(ctx context.Context, bucket *blob.Bucket) ([]RuleSet, error) { - var ruleSets []RuleSet +func (e *ruleSetEndpoint) readAllBlobs(ctx context.Context, bucket *blob.Bucket) ([]*config.RuleSet, error) { + var ruleSets []*config.RuleSet it := bucket.List(&blob.ListOptions{Prefix: e.Prefix}) @@ -80,7 +77,7 @@ func (e *ruleSetEndpoint) readAllBlobs(ctx context.Context, bucket *blob.Bucket) ruleSet, err := e.readRuleSet(ctx, bucket, obj.Key) if err != nil { - if errors.Is(err, errEmptyRuleSet) { + if errors.Is(err, config.ErrEmptyRuleSet) { continue } @@ -93,52 +90,50 @@ func (e *ruleSetEndpoint) readAllBlobs(ctx context.Context, bucket *blob.Bucket) return ruleSets, nil } -func (e *ruleSetEndpoint) readSingleBlob(ctx context.Context, bucket *blob.Bucket) ([]RuleSet, error) { +func (e *ruleSetEndpoint) readSingleBlob(ctx context.Context, bucket *blob.Bucket) ([]*config.RuleSet, error) { ruleSet, err := e.readRuleSet(ctx, bucket, e.URL.Path) if err != nil { - if errors.Is(err, errEmptyRuleSet) { - return []RuleSet{}, nil + if errors.Is(err, config.ErrEmptyRuleSet) { + return []*config.RuleSet{}, nil } return nil, err } - return []RuleSet{ruleSet}, nil + return []*config.RuleSet{ruleSet}, nil } -func (e *ruleSetEndpoint) readRuleSet(ctx context.Context, bucket *blob.Bucket, key string) (RuleSet, error) { +func (e *ruleSetEndpoint) readRuleSet(ctx context.Context, bucket *blob.Bucket, key string) ( + *config.RuleSet, error, +) { attrs, err := bucket.Attributes(ctx, key) if err != nil { - return RuleSet{}, mapError(err, "failed to get blob attributes") + return nil, mapError(err, "failed to get blob attributes") } reader, err := bucket.NewReader(ctx, key, nil) if err != nil { - return RuleSet{}, mapError(err, "failed reading blob contents") + return nil, mapError(err, "failed reading blob contents") } defer reader.Close() - contents, err := rulesetparser.ParseRules(attrs.ContentType, reader) + contents, err := config.ParseRules(attrs.ContentType, reader) if err != nil { - return RuleSet{}, errorchain.NewWithMessage(heimdall.ErrInternal, "failed to decode received rule set"). + return nil, errorchain. + NewWithMessage(heimdall.ErrInternal, "failed to decode received rule set"). CausedBy(err) } - if len(contents) == 0 { - return RuleSet{}, errEmptyRuleSet + if err = contents.VerifyPathPrefix(e.RulesPathPrefix); err != nil { + return nil, err } - if err = e.RulesPathPrefix.Verify(contents); err != nil { - return RuleSet{}, err - } + contents.Hash = attrs.MD5 + contents.Source = fmt.Sprintf("%s@%s", key, e.ID()) + contents.ModTime = attrs.ModTime - return RuleSet{ - Rules: contents, - Hash: attrs.MD5, - Key: fmt.Sprintf("%s@%s", key, e.ID()), - ModTime: attrs.ModTime, - }, nil + return contents, nil } func mapError(err error, message string) error { diff --git a/internal/rules/provider/cloudblob/ruleset_endpoint_test.go b/internal/rules/provider/cloudblob/ruleset_endpoint_test.go index 356f0089f..596dba4a8 100644 --- a/internal/rules/provider/cloudblob/ruleset_endpoint_test.go +++ b/internal/rules/provider/cloudblob/ruleset_endpoint_test.go @@ -30,6 +30,7 @@ import ( "github.com/stretchr/testify/require" "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules/config" "github.com/dadrus/heimdall/internal/x" ) @@ -65,7 +66,7 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx uc string endpoint ruleSetEndpoint setup func(t *testing.T) - assert func(t *testing.T, err error, ruleSets []RuleSet) + assert func(t *testing.T, err error, ruleSets []*config.RuleSet) }{ { uc: "failed to open bucket", @@ -76,7 +77,7 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx RawQuery: "endpoint=does-not-exist.local&foo=bar®ion=eu-central-1", }, }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.Error(t, err) @@ -93,7 +94,7 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx RawQuery: fmt.Sprintf("endpoint=%s&disableSSL=true&s3ForcePathStyle=true®ion=eu-central-1", srv.URL), }, }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.Error(t, err) @@ -120,7 +121,7 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx strings.NewReader(data), int64(len(data))) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.Error(t, err) @@ -137,7 +138,7 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx RawQuery: fmt.Sprintf("endpoint=%s&disableSSL=true&s3ForcePathStyle=true®ion=eu-central-1", srv.URL), }, }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.NoError(t, err) @@ -161,7 +162,7 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx strings.NewReader(""), 0) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.NoError(t, err) @@ -181,21 +182,26 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx setup: func(t *testing.T) { t.Helper() - data := `[{ - "id": "foobar", - "match": "http://<**>/bar/foo/api", - "methods": ["GET", "POST"], - "execute": [ - { "authenticator": "foobar" } - ] - }]` + data := ` +{ + "version": "1", + "name": "test", + "rules": [{ + "id": "foobar", + "match": "http://<**>/bar/foo/api", + "methods": ["GET", "POST"], + "execute": [ + { "authenticator": "foobar" } + ] + }] +}` _, err := backend.PutObject(bucketName, "test-rule", map[string]string{"Content-Type": "application/json"}, strings.NewReader(data), int64(len(data))) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.Error(t, err) @@ -216,17 +222,24 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx setup: func(t *testing.T) { t.Helper() - ruleSet1 := `[ + ruleSet1 := ` { - "id": "foobar", - "match": "http://<**>/foo/bar/api1", - "methods": ["GET", "POST"], - "execute": [ - { "authenticator": "foobar" } - ] -}]` + "version": "1", + "name": "test", + "rules": [{ + "id": "foobar", + "match": "http://<**>/foo/bar/api1", + "methods": ["GET", "POST"], + "execute": [ + { "authenticator": "foobar" } + ] + }] +}` ruleSet2 := ` +version: "1" +name: test2 +rules: - id: barfoo match: http://<**>/foo/bar/api2 methods: @@ -245,19 +258,19 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx strings.NewReader(ruleSet2), int64(len(ruleSet2))) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.NoError(t, err) require.Len(t, ruleSets, 2) - assert.Contains(t, ruleSets[0].Key, "test-rule1") + assert.Contains(t, ruleSets[0].Source, "test-rule1") assert.NotEmpty(t, ruleSets[0].Hash) assert.Len(t, ruleSets[0].Rules, 1) assert.Equal(t, "foobar", ruleSets[0].Rules[0].ID) - assert.Contains(t, ruleSets[1].Key, "test-rule2") + assert.Contains(t, ruleSets[1].Source, "test-rule2") assert.NotEmpty(t, ruleSets[1].Hash) assert.Len(t, ruleSets[1].Rules, 1) assert.Equal(t, "barfoo", ruleSets[1].Rules[0].ID) @@ -276,25 +289,29 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx setup: func(t *testing.T) { t.Helper() - ruleSet1 := `[ - { + ruleSet1 := `{ + "version": "1", + "name": "test1", + "rules": [{ "id": "foobar", "match": "http://<**>/foo/bar/api1", "methods": ["GET", "POST"], "execute": [ { "authenticator": "foobar" } ] - }]` + }]}` - ruleSet2 := `[ - { + ruleSet2 := `{ + "version": "1", + "name": "test2", + "rules": [{ "id": "barfoo", "url": "http://<**>/foo/bar/api2", "methods": ["GET", "POST"], "execute": [ { "authenticator": "barfoo" } ] - }]` + }]}` _, err := backend.PutObject(bucketName, "api-rule", map[string]string{"Content-Type": "application/json"}, @@ -306,14 +323,14 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx strings.NewReader(ruleSet2), int64(len(ruleSet2))) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.NoError(t, err) require.Len(t, ruleSets, 1) - assert.Contains(t, ruleSets[0].Key, "api-rule") + assert.Contains(t, ruleSets[0].Source, "api-rule") assert.NotEmpty(t, ruleSets[0].Hash) assert.Len(t, ruleSets[0].Rules, 1) assert.Equal(t, "foobar", ruleSets[0].Rules[0].ID) @@ -330,7 +347,7 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx }, Prefix: "api", }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.Error(t, err) @@ -357,7 +374,7 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx strings.NewReader(""), 0) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.NoError(t, err) @@ -378,29 +395,31 @@ func TestFetchRuleSets(t *testing.T) { //nolint:maintidx setup: func(t *testing.T) { t.Helper() - ruleSet1 := `[ - { + ruleSet1 := `{ + "version": "1", + "name": "test", + "rules": [{ "id": "foobar", "match": "http://<**>/foo/bar/api1", "methods": ["GET", "POST"], "execute": [ { "authenticator": "foobar" } ] - }]` + }]}` _, err := backend.PutObject(bucketName, "ruleset", map[string]string{"Content-Type": "application/json"}, strings.NewReader(ruleSet1), int64(len(ruleSet1))) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSets []RuleSet) { + assert: func(t *testing.T, err error, ruleSets []*config.RuleSet) { t.Helper() require.NoError(t, err) require.Len(t, ruleSets, 1) - assert.Contains(t, ruleSets[0].Key, "ruleset") + assert.Contains(t, ruleSets[0].Source, "ruleset") assert.NotEmpty(t, ruleSets[0].Hash) assert.Len(t, ruleSets[0].Rules, 1) assert.Equal(t, "foobar", ruleSets[0].Rules[0].ID) diff --git a/internal/rules/provider/cloudblob/ruleset_fetcher.go b/internal/rules/provider/cloudblob/ruleset_fetcher.go index 3d19c46ee..36d6362b9 100644 --- a/internal/rules/provider/cloudblob/ruleset_fetcher.go +++ b/internal/rules/provider/cloudblob/ruleset_fetcher.go @@ -18,9 +18,11 @@ package cloudblob import ( "context" + + "github.com/dadrus/heimdall/internal/rules/config" ) type RuleSetFetcher interface { - FetchRuleSets(ctx context.Context) ([]RuleSet, error) + FetchRuleSets(ctx context.Context) ([]*config.RuleSet, error) ID() string } diff --git a/internal/rules/provider/filesystem/module.go b/internal/rules/provider/filesystem/module.go index a7a416f7c..48dbcc5cb 100644 --- a/internal/rules/provider/filesystem/module.go +++ b/internal/rules/provider/filesystem/module.go @@ -16,10 +16,20 @@ package filesystem -import "go.uber.org/fx" +import ( + "context" + + "go.uber.org/fx" +) // Module is used on app bootstrap. // nolint: gochecknoglobals var Module = fx.Options( - fx.Invoke(registerProvider), + fx.Invoke( + fx.Annotate( + NewProvider, + fx.OnStart(func(ctx context.Context, p *Provider) error { return p.Start(ctx) }), + fx.OnStop(func(ctx context.Context, p *Provider) error { return p.Stop(ctx) }), + ), + ), ) diff --git a/internal/rules/provider/filesystem/provider.go b/internal/rules/provider/filesystem/provider.go index d0e86bbc6..f21eb8b19 100644 --- a/internal/rules/provider/filesystem/provider.go +++ b/internal/rules/provider/filesystem/provider.go @@ -19,48 +19,59 @@ package filesystem import ( "bytes" "context" + "crypto/sha256" + "errors" + "fmt" + "io" "os" "path/filepath" + "sync" + "time" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog" + "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/rules/provider/rulesetparser" + config2 "github.com/dadrus/heimdall/internal/rules/config" + "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x/errorchain" ) -type provider struct { - src string - w *fsnotify.Watcher - q event.RuleSetChangedEventQueue - l zerolog.Logger +type Provider struct { + src string + w *fsnotify.Watcher + p rule.SetProcessor + l zerolog.Logger + states sync.Map + configured bool } -func newProvider( - rawConf map[string]any, - queue event.RuleSetChangedEventQueue, - logger zerolog.Logger, -) (*provider, error) { +func NewProvider(conf *config.Configuration, processor rule.SetProcessor, logger zerolog.Logger) (*Provider, error) { + rawConf := conf.Rules.Providers.FileSystem + + if conf.Rules.Providers.FileSystem == nil { + return &Provider{}, nil + } + type Config struct { Src string `koanf:"src"` Watch bool `koanf:"watch"` } - var conf Config - if err := decodeConfig(rawConf, &conf); err != nil { + var providerConf Config + if err := decodeConfig(rawConf, &providerConf); err != nil { return nil, errorchain. NewWithMessage(heimdall.ErrConfiguration, "failed to decode file_system rule provider config"). CausedBy(err) } - if len(conf.Src) == 0 { + if len(providerConf.Src) == 0 { return nil, errorchain. NewWithMessage(heimdall.ErrConfiguration, "no src configured for file_system rule provider") } - absPath, err := filepath.Abs(conf.Src) + absPath, err := filepath.Abs(providerConf.Src) if err != nil { return nil, errorchain. NewWithMessage(heimdall.ErrInternal, "failed to get the absolute path for the configured src"). @@ -76,7 +87,7 @@ func newProvider( } var watcher *fsnotify.Watcher - if conf.Watch { + if providerConf.Watch { watcher, err = fsnotify.NewWatcher() if err != nil { return nil, errorchain. @@ -85,39 +96,40 @@ func newProvider( } } - return &provider{ - src: absPath, - w: watcher, - q: queue, - l: logger, + logger = logger.With().Str("_provider_type", "file_system").Logger() + logger.Info().Msg("Rule provider configured.") + + return &Provider{ + src: absPath, + w: watcher, + p: processor, + l: logger, + configured: true, }, nil } -func (p *provider) Start(_ context.Context) error { - p.l.Info(). - Str("_rule_provider_type", "file_system"). - Msg("Starting rule definitions provider") +func (p *Provider) Start(_ context.Context) error { + if !p.configured { + return nil + } + + p.l.Info().Msg("Starting rule definitions provider") if err := p.loadInitialRuleSet(); err != nil { - p.l.Error().Err(err). - Str("_rule_provider_type", "file_system"). - Msg("Failed loading initial rule sets") + p.l.Error().Err(err).Msg("Failed loading initial rule sets") return err } if p.w == nil { p.l.Warn(). - Str("_rule_provider_type", "file_system"). Msg("Watcher for file_system provider is not configured. Updates to rules will have no effects.") return nil } if err := p.w.Add(p.src); err != nil { - p.l.Error().Err(err). - Str("_rule_provider_type", "file_system"). - Msg("Failed to start rule definitions provider") + p.l.Error().Err(err).Msg("Failed to start rule definitions provider") return err } @@ -127,10 +139,12 @@ func (p *provider) Start(_ context.Context) error { return nil } -func (p *provider) Stop(_ context.Context) error { - p.l.Info(). - Str("_rule_provider_type", "file_system"). - Msg("Tearing down rule provider") +func (p *Provider) Stop(_ context.Context) error { + if !p.configured { + return nil + } + + p.l.Info().Msg("Tearing down rule provider") if p.w != nil { return p.w.Close() @@ -139,126 +153,167 @@ func (p *provider) Stop(_ context.Context) error { return nil } -func (p *provider) watchFiles() { - p.l.Debug(). - Str("_rule_provider_type", "file_system"). - Msg("Watching rule files for changes") +func (p *Provider) watchFiles() { + p.l.Debug().Msg("Watching rule files for changes") for { select { case evt, ok := <-p.w.Events: if !ok { - p.l.Debug(). - Str("_rule_provider_type", "file_system"). - Msg("Watcher events channel closed") + p.l.Debug().Msg("Watcher closed") return } - p.l.Debug(). - Str("_rule_provider_type", "file_system"). - Str("_event", evt.String()). - Str("_src", evt.Name). - Msg("Rule update event received") - - switch { - case evt.Op&fsnotify.Create == fsnotify.Create: - p.notifyRuleSetCreated(evt) - case evt.Op&fsnotify.Remove == fsnotify.Remove: - p.notifyRuleSetDeleted(evt) - case evt.Op&fsnotify.Write == fsnotify.Write: - p.notifyRuleSetDeleted(evt) - p.notifyRuleSetCreated(evt) + if err := p.ruleSetsChanged(evt); err != nil { + p.l.Warn().Err(err).Str("_src", evt.Name).Msg("Failed to apply rule set changes") } case err, ok := <-p.w.Errors: if !ok { - p.l.Debug(). - Str("_rule_provider_type", "file_system"). - Msg("Watcher error channel closed") + p.l.Debug().Msg("Watcher error channel closed") return } - p.l.Warn().Err(err). - Str("_rule_provider_type", "file_system"). - Msg("Watcher error received") + p.l.Warn().Err(err).Msg("Watcher error received") } } } -func (p *provider) notifyRuleSetDeleted(evt fsnotify.Event) { - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: "file_system:" + evt.Name, - ChangeType: event.Remove, - }) +func (p *Provider) ruleSetsChanged(evt fsnotify.Event) error { + p.l.Debug(). + Str("_event", evt.String()). + Str("_src", evt.Name). + Msg("Rule update event received") + + var err error + + switch { + case evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) || evt.Has(fsnotify.Chmod): + err = p.ruleSetCreatedOrUpdated(evt.Name) + case evt.Has(fsnotify.Remove): + err = p.ruleSetDeleted(evt.Name) + } + + return err } -func (p *provider) notifyRuleSetCreated(evt fsnotify.Event) { - file := evt.Name +func (p *Provider) ruleSetCreatedOrUpdated(fileName string) error { + ruleSet, err := p.loadRuleSet(fileName) + if err != nil { + if errors.Is(err, config2.ErrEmptyRuleSet) || errors.Is(err, os.ErrNotExist) { + return p.ruleSetDeleted(fileName) + } + + return err + } + + var hash []byte + + value, ok := p.states.Load(fileName) + if ok { + hash = value.([]byte) // nolint: forcetypeassert + } + + switch { + case len(hash) == 0: + err = p.p.OnCreated(ruleSet) + case !bytes.Equal(hash, ruleSet.Hash): + err = p.p.OnUpdated(ruleSet) + default: + return nil + } - data, err := os.ReadFile(file) if err != nil { - p.l.Error().Err(err). - Str("_rule_provider_type", "file_system"). - Str("_file", file). - Msg("Failed reading") + return err + } + + p.states.Store(fileName, ruleSet.Hash) + + return nil +} - return +func (p *Provider) ruleSetDeleted(fileName string) error { + if _, ok := p.states.Load(fileName); !ok { + return nil } - if len(data) == 0 { - p.l.Warn(). - Str("_rule_provider_type", "file_system"). - Str("_file", file). - Msg("File is empty") + conf := &config2.RuleSet{ + MetaData: config2.MetaData{ + Source: fmt.Sprintf("file_system:%s", fileName), + ModTime: time.Now(), + }, + } - return + if err := p.p.OnDeleted(conf); err != nil { + return err } - ruleSet, err := rulesetparser.ParseRules("application/yaml", bytes.NewBuffer(data)) + p.states.Delete(fileName) + + return nil +} + +func (p *Provider) loadRuleSet(fileName string) (*config2.RuleSet, error) { + file, err := os.Open(fileName) if err != nil { - p.l.Warn(). - Err(err). - Str("_rule_provider_type", "file_system"). - Str("_file", file). - Msg("Failed to parse rule set definition") + return nil, errorchain.NewWithMessagef(heimdall.ErrInternal, + "failed opening file %s", fileName).CausedBy(err) + } + + md := sha256.New() - return + ruleSet, err := config2.ParseRules("application/yaml", io.TeeReader(file, md)) + if err != nil { + return nil, errorchain.NewWithMessage(heimdall.ErrInternal, "failed to parse received rule set"). + CausedBy(err) } - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: "file_system:" + file, - RuleSet: ruleSet, - ChangeType: event.Create, - }) + stat, _ := os.Stat(fileName) + + ruleSet.Hash = md.Sum(nil) + ruleSet.Source = fmt.Sprintf("file_system:%s", fileName) + ruleSet.ModTime = stat.ModTime() + + return ruleSet, nil } -func (p *provider) loadInitialRuleSet() error { - p.l.Info(). - Str("_rule_provider_type", "file_system"). - Msg("Loading initial rule set") +func (p *Provider) loadInitialRuleSet() error { + p.l.Info().Msg("Loading initial rule set") + + sources, err := p.sources() + if err != nil { + return err + } + for _, src := range sources { + if err = p.ruleSetCreatedOrUpdated(src); err != nil { + return err + } + } + + return nil +} + +func (p *Provider) sources() ([]string, error) { var sources []string fInfo, err := os.Stat(p.src) if err != nil { - return err + return nil, err } if fInfo.IsDir() { dirEntries, err := os.ReadDir(p.src) if err != nil { - return err + return nil, err } for _, entry := range dirEntries { path := filepath.Join(p.src, entry.Name()) if entry.IsDir() { - p.l.Warn(). - Str("_rule_provider_type", "file_system"). - Str("_path", path). - Msg("Ignoring directory") + p.l.Warn().Str("_path", path).Msg("Ignoring directory") continue } @@ -269,51 +324,5 @@ func (p *provider) loadInitialRuleSet() error { sources = append(sources, p.src) } - for _, src := range sources { - data, err := os.ReadFile(src) - if err != nil { - p.l.Error().Err(err). - Str("_rule_provider_type", "file_system"). - Msg("Failed loading initial rule set") - - return err - } - - if len(data) == 0 { - p.l.Warn(). - Str("_rule_provider_type", "file_system"). - Str("_file", src). - Msg("File is empty") - - return err - } - - ruleSet, err := rulesetparser.ParseRules("application/yaml", bytes.NewBuffer(data)) - if err != nil { - p.l.Warn(). - Err(err). - Str("_rule_provider_type", "file_system"). - Str("_file", src). - Msg("Failed to parse rule set definition") - - return err - } - - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: "file_system:" + src, - RuleSet: ruleSet, - ChangeType: event.Create, - }) - } - - return nil -} - -func (p *provider) ruleSetChanged(evt event.RuleSetChangedEvent) { - p.l.Info(). - Str("_rule_provider_type", "file_system"). - Str("_src", evt.Src). - Str("_type", evt.ChangeType.String()). - Msg("Rule set changed") - p.q <- evt + return sources, nil } diff --git a/internal/rules/provider/filesystem/provider_registrar.go b/internal/rules/provider/filesystem/provider_registrar.go deleted file mode 100644 index bc2ab5715..000000000 --- a/internal/rules/provider/filesystem/provider_registrar.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package filesystem - -import ( - "context" - - "github.com/rs/zerolog" - "go.uber.org/fx" - - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/x/errorchain" -) - -type registrationArguments struct { - fx.In - - Lifecycle fx.Lifecycle - Config *config.Configuration - Queue event.RuleSetChangedEventQueue -} - -func registerProvider(args registrationArguments, logger zerolog.Logger) error { - if args.Config.Rules.Providers.FileSystem == nil { - return nil - } - - provider, err := newProvider(args.Config.Rules.Providers.FileSystem, args.Queue, logger) - if err != nil { - return errorchain.NewWithMessage(heimdall.ErrInternal, "failed to create file_system provider"). - CausedBy(err) - } - - logger.Info(). - Str("_rule_provider_type", "file_system"). - Msg("Rule provider configured.") - - args.Lifecycle.Append( - fx.Hook{ - OnStart: func(ctx context.Context) error { return provider.Start(ctx) }, - OnStop: func(ctx context.Context) error { return provider.Stop(ctx) }, - }, - ) - - return nil -} diff --git a/internal/rules/provider/filesystem/provider_registrar_test.go b/internal/rules/provider/filesystem/provider_registrar_test.go deleted file mode 100644 index 3dc5a4a68..000000000 --- a/internal/rules/provider/filesystem/provider_registrar_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package filesystem - -import ( - "os" - "testing" - - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.uber.org/fx" - - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/x" - "github.com/dadrus/heimdall/internal/x/testsupport" -) - -type mockLifecycle struct{ mock.Mock } - -func (m *mockLifecycle) Append(hook fx.Hook) { m.Called(hook) } - -func TestRegisterProvider(t *testing.T) { - t.Parallel() - - tmpFile, err := os.CreateTemp(os.TempDir(), "test-rule-") - require.NoError(t, err) - - defer os.Remove(tmpFile.Name()) - - for _, tc := range []struct { - uc string - conf []byte - setupMocks func(t *testing.T, mockLC *mockLifecycle) - assert func(t *testing.T, err error) - }{ - { - uc: "without it being configured", - assert: func(t *testing.T, err error) { - t.Helper() - - assert.NoError(t, err) - }, - }, - { - uc: "without provided rules file/directory", - conf: []byte(`watch: true`), - assert: func(t *testing.T, err error) { - t.Helper() - - require.Error(t, err) - assert.ErrorIs(t, err, heimdall.ErrConfiguration) - }, - }, - { - uc: "with not existing referenced file", - conf: []byte(`src: foo.bar`), - assert: func(t *testing.T, err error) { - t.Helper() - - require.Error(t, err) - assert.Contains(t, err.Error(), "no such file") - }, - }, - { - uc: "with existing rules file", - conf: []byte(`src: ` + tmpFile.Name()), - setupMocks: func(t *testing.T, mockLC *mockLifecycle) { - t.Helper() - - mockLC.On("Append", mock.AnythingOfType("fx.Hook")) - }, - assert: func(t *testing.T, err error) { - t.Helper() - - require.NoError(t, err) - }, - }, - { - uc: "with existing rules file and enabled watcher", - conf: []byte(` -watch: true -src: ` + tmpFile.Name()), - setupMocks: func(t *testing.T, mockLC *mockLifecycle) { - t.Helper() - - mockLC.On("Append", mock.AnythingOfType("fx.Hook")) - }, - assert: func(t *testing.T, err error) { - t.Helper() - - require.NoError(t, err) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - providerConf, err := testsupport.DecodeTestConfig(tc.conf) - require.NoError(t, err) - - conf := &config.Configuration{ - Rules: config.Rules{ - Providers: config.RuleProviders{FileSystem: providerConf}, - }, - } - - mlc := &mockLifecycle{} - queue := make(event.RuleSetChangedEventQueue, 10) - setupMocks := x.IfThenElse(tc.setupMocks != nil, - tc.setupMocks, - func(t *testing.T, mockLC *mockLifecycle) { t.Helper() }) - - setupMocks(t, mlc) - - // WHEN - err = registerProvider(registrationArguments{Lifecycle: mlc, Config: conf, Queue: queue}, log.Logger) - - // THEN - tc.assert(t, err) - - mlc.AssertExpectations(t) - }) - } -} diff --git a/internal/rules/provider/filesystem/provider_test.go b/internal/rules/provider/filesystem/provider_test.go index 50b3d72cb..0509400a0 100644 --- a/internal/rules/provider/filesystem/provider_test.go +++ b/internal/rules/provider/filesystem/provider_test.go @@ -22,43 +22,132 @@ import ( "testing" "time" + "github.com/fsnotify/fsnotify" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/dadrus/heimdall/internal/rules/event" + "github.com/dadrus/heimdall/internal/config" + "github.com/dadrus/heimdall/internal/heimdall" + config2 "github.com/dadrus/heimdall/internal/rules/config" + "github.com/dadrus/heimdall/internal/rules/rule/mocks" "github.com/dadrus/heimdall/internal/x" + mock2 "github.com/dadrus/heimdall/internal/x/testsupport/mock" ) -// nolint: maintidx -func TestStartProvider(t *testing.T) { +func TestNewProvider(t *testing.T) { t.Parallel() - var tearDownFuncs []func() + tmpFile, err := os.CreateTemp(os.TempDir(), "test-dir-") + require.NoError(t, err) + + defer os.Remove(tmpFile.Name()) + + for _, tc := range []struct { + uc string + conf map[string]any + assert func(t *testing.T, err error, prov *Provider) + }{ + { + uc: "not configured provider", + assert: func(t *testing.T, err error, prov *Provider) { + t.Helper() + + require.NoError(t, err) + require.NotNil(t, prov) + assert.False(t, prov.configured) + }, + }, + { + uc: "bad configuration", + conf: map[string]any{"foo": "bar"}, + assert: func(t *testing.T, err error, prov *Provider) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, heimdall.ErrConfiguration) + assert.Contains(t, err.Error(), "failed to decode") + }, + }, + { + uc: "no src configured", + conf: map[string]any{"watch": true}, + assert: func(t *testing.T, err error, prov *Provider) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, heimdall.ErrConfiguration) + assert.Contains(t, err.Error(), "no src") + }, + }, + { + uc: "configured src does not exist", + conf: map[string]any{"src": "foo.bar"}, + assert: func(t *testing.T, err error, prov *Provider) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, heimdall.ErrInternal) + assert.Contains(t, err.Error(), "failed to get info") + }, + }, + { + uc: "successfully created provider without watcher", + conf: map[string]any{"src": tmpFile.Name()}, + assert: func(t *testing.T, err error, prov *Provider) { + t.Helper() - defer func() { - for _, f := range tearDownFuncs { - f() - } - }() + require.NoError(t, err) + require.NotNil(t, prov) + assert.True(t, prov.configured) + assert.Equal(t, tmpFile.Name(), prov.src) + assert.Nil(t, prov.w) + }, + }, + { + uc: "successfully created provider with watcher", + conf: map[string]any{"src": tmpFile.Name(), "watch": true}, + assert: func(t *testing.T, err error, prov *Provider) { + t.Helper() + require.NoError(t, err) + require.NotNil(t, prov) + assert.True(t, prov.configured) + assert.Equal(t, tmpFile.Name(), prov.src) + assert.NotNil(t, prov.w) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEN + conf := &config.Configuration{Rules: config.Rules{Providers: config.RuleProviders{FileSystem: tc.conf}}} + + prov, err := NewProvider(conf, nil, log.Logger) + + tc.assert(t, err, prov) + }) + } +} + +// nolint: maintidx +func TestProviderLifecycle(t *testing.T) { for _, tc := range []struct { uc string - createProvider func(t *testing.T, file *os.File, dir string) *provider + watch bool + setupContents func(t *testing.T, file *os.File, dir string) string + setupProcessor func(t *testing.T, processor *mocks.RuleSetProcessorMock) writeContents func(t *testing.T, file *os.File, dir string) - assert func(t *testing.T, err error, provider *provider) + assert func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) }{ { uc: "start provider using not existing file", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { + setupContents: func(t *testing.T, file *os.File, dir string) string { t.Helper() - return &provider{ - src: "foo.bar", - l: log.Logger, - } + return "foo.bar" }, - assert: func(t *testing.T, err error, provider *provider) { + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { t.Helper() require.Error(t, err) @@ -67,17 +156,14 @@ func TestStartProvider(t *testing.T) { }, { uc: "start provider using file without read permissions", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { + setupContents: func(t *testing.T, file *os.File, dir string) string { t.Helper() require.NoError(t, file.Chmod(0o200)) - return &provider{ - src: file.Name(), - l: log.Logger, - } + return file.Name() }, - assert: func(t *testing.T, err error, provider *provider) { + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { t.Helper() require.Error(t, err) @@ -86,106 +172,97 @@ func TestStartProvider(t *testing.T) { }, { uc: "successfully start provider without watcher using empty file", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { - t.Helper() - - return &provider{ - src: file.Name(), - l: log.Logger, - q: make(event.RuleSetChangedEventQueue, 10), - } - }, - assert: func(t *testing.T, err error, provider *provider) { + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { t.Helper() require.NoError(t, err) - - assert.Len(t, provider.q, 0) }, }, { uc: "successfully start provider without watcher using not empty file", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { + setupContents: func(t *testing.T, file *os.File, dir string) string { t.Helper() - _, err := file.Write([]byte(`- id: foo`)) + _, err := file.Write([]byte(` +version: "1" +rules: +- id: foo +`)) require.NoError(t, err) - return &provider{ - src: file.Name(), - l: log.Logger, - q: make(event.RuleSetChangedEventQueue, 10), - } + return file.Name() }, - assert: func(t *testing.T, err error, provider *provider) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { t.Helper() - require.NoError(t, err) - - assert.Len(t, provider.q, 1) + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { + t.Helper() - evt := <-provider.q + require.NoError(t, err) - assert.Contains(t, evt.Src, "file_system:") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "file_system:") + assert.Equal(t, "1", ruleSet.Version) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "foo", ruleSet.Rules[0].ID) }, }, { uc: "successfully start provider without watcher using empty dir", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { + setupContents: func(t *testing.T, file *os.File, dir string) string { t.Helper() - return &provider{ - src: dir, - l: log.Logger, - q: make(event.RuleSetChangedEventQueue, 10), - } + return dir }, - assert: func(t *testing.T, err error, provider *provider) { + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { t.Helper() require.NoError(t, err) - - assert.Len(t, provider.q, 0) }, }, { uc: "successfully start provider without watcher using dir with not empty file", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { + setupContents: func(t *testing.T, file *os.File, dir string) string { t.Helper() tmpFile, err := os.CreateTemp(dir, "test-rule-") require.NoError(t, err) - _, err = tmpFile.Write([]byte(`- id: foo`)) + _, err = tmpFile.Write([]byte(` +version: "2" +rules: +- id: foo +`)) require.NoError(t, err) - return &provider{ - src: dir, - l: log.Logger, - q: make(event.RuleSetChangedEventQueue, 10), - } + return dir }, - assert: func(t *testing.T, err error, provider *provider) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { t.Helper() - require.NoError(t, err) - - assert.Len(t, provider.q, 1) + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { + t.Helper() - evt := <-provider.q + require.NoError(t, err) - assert.Contains(t, evt.Src, "file_system:") - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "file_system:") + assert.Equal(t, "2", ruleSet.Version) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "foo", ruleSet.Rules[0].ID) }, }, { uc: "successfully start provider without watcher using dir with other directory with rule file", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { + setupContents: func(t *testing.T, file *os.File, dir string) string { t.Helper() tmpDir, err := os.MkdirTemp(dir, "test-dir-") @@ -194,36 +271,29 @@ func TestStartProvider(t *testing.T) { tmpFile, err := os.CreateTemp(tmpDir, "test-rule-") require.NoError(t, err) - _, err = tmpFile.Write([]byte(`- id: foo`)) + _, err = tmpFile.Write([]byte(` +version: "1" +rules: +- id: foo +`)) require.NoError(t, err) - return &provider{ - src: dir, - l: log.Logger, - q: make(event.RuleSetChangedEventQueue, 10), - } + return dir }, - assert: func(t *testing.T, err error, provider *provider) { + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { t.Helper() require.NoError(t, err) - - assert.Len(t, provider.q, 0) }, }, { uc: "successfully start provider with watcher using initially empty dir and adding rule " + "file and deleting it then", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { + watch: true, + setupContents: func(t *testing.T, file *os.File, dir string) string { t.Helper() - provider, err := newProvider( - map[string]any{"src": dir, "watch": true}, - make(event.RuleSetChangedEventQueue, 10), - log.Logger) - require.NoError(t, err) - - return provider + return dir }, writeContents: func(t *testing.T, file *os.File, dir string) { t.Helper() @@ -233,7 +303,11 @@ func TestStartProvider(t *testing.T) { time.Sleep(200 * time.Millisecond) - _, err = tmpFile.Write([]byte(`- id: foo`)) + _, err = tmpFile.Write([]byte(` +version: "1" +rules: +- id: foo +`)) require.NoError(t, err) time.Sleep(200 * time.Millisecond) @@ -243,48 +317,68 @@ func TestStartProvider(t *testing.T) { time.Sleep(200 * time.Millisecond) }, - assert: func(t *testing.T, err error, provider *provider) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { t.Helper() - require.NoError(t, err) + call1 := processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() - require.Len(t, provider.q, 3) + processor.EXPECT().OnDeleted(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor2").Capture). + Return(nil).Once().NotBefore(call1) + }, + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { + t.Helper() - evt := <-provider.q - assert.Contains(t, evt.Src, "file_system:"+provider.src) - assert.Empty(t, evt.RuleSet) - assert.Equal(t, event.Remove, evt.ChangeType) + require.NoError(t, err) - evt = <-provider.q - assert.Contains(t, evt.Src, "file_system:"+provider.src) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "file_system:") + assert.Equal(t, "1", ruleSet.Version) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "foo", ruleSet.Rules[0].ID) - evt = <-provider.q - assert.Contains(t, evt.Src, "file_system:"+provider.src) - assert.Empty(t, evt.RuleSet) - assert.Equal(t, event.Remove, evt.ChangeType) + ruleSet = mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor2").Value() + assert.Contains(t, ruleSet.Source, "file_system:") }, }, { uc: "successfully start provider with watcher using initially empty file, " + - "updating it afterwards and deleting it then", - createProvider: func(t *testing.T, file *os.File, dir string) *provider { + "updating it with same content, then with different content and deleting it then", + watch: true, + writeContents: func(t *testing.T, file *os.File, dir string) { t.Helper() - provider, err := newProvider( - map[string]any{"src": file.Name(), "watch": true}, - make(event.RuleSetChangedEventQueue, 10), - log.Logger) + _, err := file.Write([]byte(` +version: "1" +rules: +- id: foo +`)) require.NoError(t, err) - return provider - }, - writeContents: func(t *testing.T, file *os.File, dir string) { - t.Helper() + time.Sleep(200 * time.Millisecond) - _, err := file.Write([]byte(`- id: foo`)) + _, err = file.Seek(0, 0) + require.NoError(t, err) + + _, err = file.Write([]byte(` +version: "1" +rules: +- id: foo +`)) + require.NoError(t, err) + + time.Sleep(200 * time.Millisecond) + + _, err = file.Seek(0, 0) + require.NoError(t, err) + + _, err = file.Write([]byte(` +version: "2" +rules: +- id: bar +`)) require.NoError(t, err) time.Sleep(200 * time.Millisecond) @@ -294,23 +388,40 @@ func TestStartProvider(t *testing.T) { time.Sleep(200 * time.Millisecond) }, - assert: func(t *testing.T, err error, provider *provider) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + call1 := processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + + call2 := processor.EXPECT().OnUpdated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor2").Capture). + Return(nil).Once().NotBefore(call1) + + processor.EXPECT().OnDeleted(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor3").Capture). + Return(nil).Once().NotBefore(call2) + }, + assert: func(t *testing.T, err error, provider *Provider, processor *mocks.RuleSetProcessorMock) { t.Helper() require.NoError(t, err) - require.Len(t, provider.q, 2) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "file_system:") + assert.Equal(t, "1", ruleSet.Version) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "foo", ruleSet.Rules[0].ID) - evt := <-provider.q - assert.Contains(t, evt.Src, "file_system:"+provider.src) - assert.Empty(t, evt.RuleSet) - assert.Equal(t, event.Remove, evt.ChangeType) + ruleSet = mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor2").Value() + assert.Contains(t, ruleSet.Source, "file_system:") + assert.Equal(t, "2", ruleSet.Version) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "bar", ruleSet.Rules[0].ID) - evt = <-provider.q - assert.Contains(t, evt.Src, "file_system:"+provider.src) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet = mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor3").Value() + assert.Contains(t, ruleSet.Source, "file_system:") }, }, } { @@ -319,30 +430,59 @@ func TestStartProvider(t *testing.T) { tmpFile, err := os.CreateTemp(os.TempDir(), "test-dir-") require.NoError(t, err) - tearDownFuncs = append(tearDownFuncs, func() { os.Remove(tmpFile.Name()) }) + defer os.Remove(tmpFile.Name()) tmpDir, err := os.MkdirTemp(os.TempDir(), "test-rule-") require.NoError(t, err) - tearDownFuncs = append(tearDownFuncs, func() { os.Remove(tmpDir) }) + defer os.Remove(tmpDir) writeContents := x.IfThenElse(tc.writeContents != nil, tc.writeContents, func(t *testing.T, file *os.File, dir string) { t.Helper() }, ) + setupContents := x.IfThenElse(tc.setupContents != nil, + tc.setupContents, + func(t *testing.T, file *os.File, dir string) string { + t.Helper() + + return file.Name() + }, + ) + + setupProcessor := x.IfThenElse(tc.setupProcessor != nil, + tc.setupProcessor, + func(t *testing.T, processor *mocks.RuleSetProcessorMock) { t.Helper() }) + + processor := mocks.NewRuleSetProcessorMock(t) + setupProcessor(t, processor) + + var watcher *fsnotify.Watcher + + if tc.watch { + watcher, err = fsnotify.NewWatcher() + require.NoError(t, err) + } + // GIVEN - provider := tc.createProvider(t, tmpFile, tmpDir) + prov := &Provider{ + src: setupContents(t, tmpFile, tmpDir), + p: processor, + l: log.Logger, + w: watcher, + configured: true, + } // WHEN - err = provider.Start(ctx) - writeContents(t, tmpFile, tmpDir) + err = prov.Start(ctx) + + defer prov.Stop(ctx) // nolint: errcheck - // nolint: errcheck - tearDownFuncs = append(tearDownFuncs, func() { provider.Stop(ctx) }) + writeContents(t, tmpFile, tmpDir) // THEN - tc.assert(t, err, provider) + tc.assert(t, err, prov, processor) }) } } diff --git a/internal/rules/provider/httpendpoint/module.go b/internal/rules/provider/httpendpoint/module.go index 8d4e9e20a..d0f04d7b3 100644 --- a/internal/rules/provider/httpendpoint/module.go +++ b/internal/rules/provider/httpendpoint/module.go @@ -16,10 +16,20 @@ package httpendpoint -import "go.uber.org/fx" +import ( + "context" + + "go.uber.org/fx" +) // Module is used on app bootstrap. // nolint: gochecknoglobals var Module = fx.Options( - fx.Invoke(registerProvider), + fx.Invoke( + fx.Annotate( + newProvider, + fx.OnStart(func(ctx context.Context, p *provider) error { return p.Start(ctx) }), + fx.OnStop(func(ctx context.Context, p *provider) error { return p.Stop(ctx) }), + ), + ), ) diff --git a/internal/rules/provider/httpendpoint/provider.go b/internal/rules/provider/httpendpoint/provider.go index fc21a069f..cd52d5d20 100644 --- a/internal/rules/provider/httpendpoint/provider.go +++ b/internal/rules/provider/httpendpoint/provider.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "errors" + "fmt" "sync" "time" @@ -27,47 +28,54 @@ import ( "github.com/rs/zerolog" "github.com/dadrus/heimdall/internal/cache" + "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" + config2 "github.com/dadrus/heimdall/internal/rules/config" + "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x" "github.com/dadrus/heimdall/internal/x/errorchain" ) type provider struct { - q event.RuleSetChangedEventQueue - l zerolog.Logger - s *gocron.Scheduler - cancel context.CancelFunc - - mu sync.Mutex - state map[string][]byte + p rule.SetProcessor + l zerolog.Logger + s *gocron.Scheduler + cancel context.CancelFunc + states sync.Map + configured bool } func newProvider( - rawConf map[string]any, + conf *config.Configuration, cch cache.Cache, - queue event.RuleSetChangedEventQueue, + processor rule.SetProcessor, logger zerolog.Logger, ) (*provider, error) { + rawConf := conf.Rules.Providers.HTTPEndpoint + + if rawConf == nil { + return &provider{}, nil + } + type Config struct { Endpoints []*ruleSetEndpoint `mapstructure:"endpoints"` WatchInterval *time.Duration `mapstructure:"watch_interval"` } - var conf Config - if err := decodeConfig(rawConf, &conf); err != nil { + var providerConf Config + if err := decodeConfig(rawConf, &providerConf); err != nil { return nil, errorchain. NewWithMessage(heimdall.ErrConfiguration, "failed to decode http_endpoint rule provider config"). CausedBy(err) } - if len(conf.Endpoints) == 0 { + if len(providerConf.Endpoints) == 0 { return nil, errorchain. NewWithMessage(heimdall.ErrConfiguration, "no endpoints configured for http_endpoint rule provider") } - for idx, ep := range conf.Endpoints { + for idx, ep := range providerConf.Endpoints { if err := ep.init(); err != nil { return nil, errorchain. NewWithMessagef(heimdall.ErrConfiguration, @@ -76,41 +84,43 @@ func newProvider( } } + logger = logger.With().Str("_provider_type", "http_endpoint").Logger() ctx, cancel := context.WithCancel(context.Background()) - ctx = logger.With(). - Str("_rule_provider_type", "http_endpoint"). - Logger(). - WithContext(cache.WithContext(ctx, cch)) + ctx = logger.WithContext(cache.WithContext(ctx, cch)) scheduler := gocron.NewScheduler(time.UTC) scheduler.SingletonModeAll() prov := &provider{ - q: queue, - l: logger, - s: scheduler, - cancel: cancel, - state: make(map[string][]byte), + p: processor, + l: logger, + s: scheduler, + cancel: cancel, + configured: true, } - for idx, ep := range conf.Endpoints { - if _, err := x.IfThenElseExec(conf.WatchInterval != nil && *conf.WatchInterval > 0, - func() *gocron.Scheduler { return prov.s.Every(*conf.WatchInterval) }, - func() *gocron.Scheduler { return prov.s.Every(1 * time.Second).LimitRunsTo(1) }). - Do(prov.watchChanges, ctx, ep); err != nil { + for idx, ep := range providerConf.Endpoints { + if _, err := x.IfThenElseExec(providerConf.WatchInterval != nil && *providerConf.WatchInterval > 0, + func() *gocron.Scheduler { return prov.s.Every(*providerConf.WatchInterval) }, + func() *gocron.Scheduler { return prov.s.Every(1 * time.Second).LimitRunsTo(1) }, + ).Do(prov.watchChanges, ctx, ep); err != nil { return nil, errorchain.NewWithMessagef(heimdall.ErrInternal, "failed to create a rule provider worker to fetch rules sets from #%d http_endpoint", idx). CausedBy(err) } } + logger.Info().Msg("Rule provider configured.") + return prov, nil } func (p *provider) Start(_ context.Context) error { - p.l.Info(). - Str("_rule_provider_type", "http_endpoint"). - Msg("Starting rule definitions provider") + if !p.configured { + return nil + } + + p.l.Info().Msg("Starting rule definitions provider") p.s.StartAsync() //nolint:contextcheck @@ -118,97 +128,97 @@ func (p *provider) Start(_ context.Context) error { } func (p *provider) Stop(_ context.Context) error { - p.l.Info(). - Str("_rule_provider_type", "http_endpoint"). - Msg("Tearing down rule provider.") + if !p.configured { + return nil + } + + p.l.Info().Msg("Tearing down rule provider.") - p.cancel() p.s.Stop() + p.cancel() return nil } func (p *provider) watchChanges(ctx context.Context, rsf RuleSetFetcher) error { p.l.Debug(). - Str("_rule_provider_type", "http_endpoint"). Str("_endpoint", rsf.ID()). Msg("Retrieving rule set") ruleSet, err := rsf.FetchRuleSet(ctx) if err != nil { - p.l.Warn(). - Err(err). - Str("_rule_provider_type", "http_endpoint"). + if errors.Is(err, context.Canceled) { + p.l.Debug().Msg("Watcher closed") + + return nil + } + + p.l.Warn().Err(err). Str("_endpoint", rsf.ID()). Msg("Failed to fetch rule set") - if errors.Is(err, heimdall.ErrInternal) || errors.Is(err, heimdall.ErrConfiguration) { + if !errors.Is(err, config2.ErrEmptyRuleSet) && + (errors.Is(err, heimdall.ErrInternal) || errors.Is(err, heimdall.ErrConfiguration)) { return err } - } - changeType := x.IfThenElse(len(ruleSet.Rules) == 0, event.Remove, event.Create) - - stateUpdated, removeOld := p.checkAndUpdateState(changeType, rsf.ID(), ruleSet.Hash) - if !stateUpdated { - p.l.Debug(). - Str("_rule_provider_type", "http_endpoint"). - Str("_endpoint", rsf.ID()). - Msg("No updates received") - - return nil + ruleSet = &config2.RuleSet{ + MetaData: config2.MetaData{ + Source: fmt.Sprintf("http_endpoint:%s", rsf.ID()), + ModTime: time.Now(), + }, + } } - if removeOld { - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: "http_endpoint:" + rsf.ID(), - ChangeType: event.Remove, - }) + if err = p.ruleSetsUpdated(ruleSet, rsf.ID()); err != nil { + p.l.Warn().Err(err). + Str("_src", rsf.ID()). + Msg("Failed to apply rule set changes") } - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: "http_endpoint:" + rsf.ID(), - ChangeType: changeType, - RuleSet: ruleSet.Rules, - }) - return nil } -func (p *provider) checkAndUpdateState(changeType event.ChangeType, stateID string, newValue []byte) (bool, bool) { - p.mu.Lock() - defer p.mu.Unlock() +func (p *provider) ruleSetsUpdated(ruleSet *config2.RuleSet, stateID string) error { + var hash []byte - removeOld := false - oldValue, known := p.state[stateID] + if value, ok := p.states.Load(stateID); ok { //nolint:nestif + hash = value.([]byte) // nolint: forcetypeassert - switch changeType { - case event.Remove: - if !known { - // nothing needs to be done, this rule set is not known - return false, false - } + // rule set was known + if len(ruleSet.Rules) == 0 { + // rule set removed + if err := p.p.OnDeleted(ruleSet); err != nil { + return err + } + + p.states.Delete(stateID) + + return nil + } else if !bytes.Equal(hash, ruleSet.Hash) { + // rule set updated + if err := p.p.OnUpdated(ruleSet); err != nil { + return err + } - delete(p.state, stateID) - case event.Create: - if known && bytes.Equal(oldValue, newValue) { - // nothing needs to be done, this rule set is already known - return false, false - } else if known { - removeOld = true + p.states.Store(stateID, ruleSet.Hash) + + return nil + } + } else if len(ruleSet.Rules) != 0 { + // previously unknown rule set + if err := p.p.OnCreated(ruleSet); err != nil { + return err } - p.state[stateID] = newValue + p.states.Store(stateID, ruleSet.Hash) + + return nil } - return true, removeOld -} + p.l.Debug(). + Str("_endpoint", stateID). + Msg("No updates received") -func (p *provider) ruleSetChanged(evt event.RuleSetChangedEvent) { - p.l.Info(). - Str("_rule_provider_type", "http_endpoint"). - Str("_src", evt.Src). - Str("_type", evt.ChangeType.String()). - Msg("Rule set changed") - p.q <- evt + return nil } diff --git a/internal/rules/provider/httpendpoint/provider_registrar.go b/internal/rules/provider/httpendpoint/provider_registrar.go deleted file mode 100644 index f4a80939c..000000000 --- a/internal/rules/provider/httpendpoint/provider_registrar.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package httpendpoint - -import ( - "context" - - "github.com/rs/zerolog" - "go.uber.org/fx" - - "github.com/dadrus/heimdall/internal/cache" - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/x/errorchain" -) - -type registrationArguments struct { - fx.In - - Lifecycle fx.Lifecycle - Config *config.Configuration - Queue event.RuleSetChangedEventQueue - Cache cache.Cache -} - -func registerProvider(args registrationArguments, logger zerolog.Logger) error { - if args.Config.Rules.Providers.HTTPEndpoint == nil { - return nil - } - - provider, err := newProvider(args.Config.Rules.Providers.HTTPEndpoint, args.Cache, args.Queue, logger) - if err != nil { - return errorchain.NewWithMessage(heimdall.ErrInternal, "failed to create http_endpoint provider"). - CausedBy(err) - } - - logger.Info(). - Str("_rule_provider_type", "http_endpoint"). - Msg("Rule provider configured.") - - args.Lifecycle.Append( - fx.Hook{ - OnStart: func(ctx context.Context) error { return provider.Start(ctx) }, - OnStop: func(ctx context.Context) error { return provider.Stop(ctx) }, - }, - ) - - return nil -} diff --git a/internal/rules/provider/httpendpoint/provider_registrar_test.go b/internal/rules/provider/httpendpoint/provider_registrar_test.go deleted file mode 100644 index 1e87e269b..000000000 --- a/internal/rules/provider/httpendpoint/provider_registrar_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package httpendpoint - -import ( - "testing" - - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.uber.org/fx" - - "github.com/dadrus/heimdall/internal/cache/mocks" - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/x" - "github.com/dadrus/heimdall/internal/x/testsupport" -) - -type mockLifecycle struct{ mock.Mock } - -func (m *mockLifecycle) Append(hook fx.Hook) { m.Called(hook) } - -func TestRegisterProvider(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - conf []byte - setupMocks func(t *testing.T, mockLC *mockLifecycle) - assert func(t *testing.T, err error) - }{ - { - uc: "without it being configured", - assert: func(t *testing.T, err error) { - t.Helper() - - assert.NoError(t, err) - }, - }, - { - uc: "with invalid configuration, unknown filed", - conf: []byte(`foo: bar`), - assert: func(t *testing.T, err error) { - t.Helper() - - require.Error(t, err) - assert.ErrorIs(t, err, heimdall.ErrConfiguration) - assert.Contains(t, err.Error(), "failed to decode") - }, - }, - { - uc: "with valid configuration", - conf: []byte(` -endpoints: - - url: https://foo.bar -watch_interval: 5m -`), - setupMocks: func(t *testing.T, mockLC *mockLifecycle) { - t.Helper() - - mockLC.On("Append", mock.AnythingOfType("fx.Hook")) - }, - assert: func(t *testing.T, err error) { - t.Helper() - - require.NoError(t, err) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - providerConf, err := testsupport.DecodeTestConfig(tc.conf) - require.NoError(t, err) - - conf := &config.Configuration{ - Rules: config.Rules{ - Providers: config.RuleProviders{HTTPEndpoint: providerConf}, - }, - } - queue := make(event.RuleSetChangedEventQueue, 10) - mlc := &mockLifecycle{} - cch := &mocks.MockCache{} - - setupMocks := x.IfThenElse(tc.setupMocks != nil, - tc.setupMocks, - func(t *testing.T, mockLC *mockLifecycle) { t.Helper() }) - - setupMocks(t, mlc) - - args := registrationArguments{Lifecycle: mlc, Config: conf, Queue: queue, Cache: cch} - - // WHEN - err = registerProvider(args, log.Logger) - - // THEN - tc.assert(t, err) - - mlc.AssertExpectations(t) - }) - } -} diff --git a/internal/rules/provider/httpendpoint/provider_test.go b/internal/rules/provider/httpendpoint/provider_test.go index da9e91647..d80949f76 100644 --- a/internal/rules/provider/httpendpoint/provider_test.go +++ b/internal/rules/provider/httpendpoint/provider_test.go @@ -29,13 +29,17 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/dadrus/heimdall/internal/cache/memory" + "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" + config2 "github.com/dadrus/heimdall/internal/rules/config" + "github.com/dadrus/heimdall/internal/rules/rule/mocks" "github.com/dadrus/heimdall/internal/x" "github.com/dadrus/heimdall/internal/x/testsupport" + mock2 "github.com/dadrus/heimdall/internal/x/testsupport/mock" ) func TestNewProvider(t *testing.T) { @@ -109,7 +113,7 @@ endpoints: require.NoError(t, err) require.NotNil(t, prov) assert.NotNil(t, prov.s) - assert.NotNil(t, prov.q) + assert.NotNil(t, prov.p) assert.NotNil(t, prov.cancel) assert.False(t, prov.s.IsRunning()) assert.Len(t, prov.s.Jobs(), 1) @@ -131,7 +135,7 @@ endpoints: require.NoError(t, err) require.NotNil(t, prov) assert.NotNil(t, prov.s) - assert.NotNil(t, prov.q) + assert.NotNil(t, prov.p) assert.NotNil(t, prov.cancel) assert.False(t, prov.s.IsRunning()) assert.Len(t, prov.s.Jobs(), 2) @@ -145,10 +149,14 @@ endpoints: providerConf, err := testsupport.DecodeTestConfig(tc.conf) require.NoError(t, err) - queue := make(event.RuleSetChangedEventQueue, 10) + conf := &config.Configuration{ + Rules: config.Rules{ + Providers: config.RuleProviders{HTTPEndpoint: providerConf}, + }, + } // WHEN - prov, err := newProvider(providerConf, memory.New(), queue, log.Logger) + prov, err := newProvider(conf, memory.New(), mocks.NewRuleSetProcessorMock(t), log.Logger) // THEN tc.assert(t, err, prov) @@ -178,10 +186,11 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx defer srv.Close() for _, tc := range []struct { - uc string - conf []byte - writeResponse ResponseWriter - assert func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) + uc string + conf []byte + setupProcessor func(t *testing.T, processor *mocks.RuleSetProcessorMock) + writeResponse ResponseWriter + assert func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) }{ { uc: "with rule set loading error due to DNS error", @@ -189,7 +198,7 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx endpoints: - url: https://foo.bar.local/rules.yaml `), - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) @@ -197,8 +206,6 @@ endpoints: messages := logs.String() assert.Contains(t, messages, "name resolution") assert.Contains(t, messages, "No updates received") - - require.Len(t, queue, 0) }, }, { @@ -212,7 +219,7 @@ endpoints: w.WriteHeader(http.StatusBadRequest) }, - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) @@ -220,8 +227,6 @@ endpoints: messages := logs.String() assert.Contains(t, messages, "response code: 400") assert.Contains(t, messages, "No updates received") - - require.Len(t, queue, 0) }, }, { @@ -235,15 +240,13 @@ endpoints: w.WriteHeader(http.StatusOK) }, - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) assert.Equal(t, 1, requestCount) assert.Contains(t, logs.String(), "No updates received") - - require.Len(t, queue, 0) }, }, { @@ -256,10 +259,22 @@ endpoints: t.Helper() w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: foo")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: foo +`)) require.NoError(t, err) }, - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(600 * time.Millisecond) @@ -267,13 +282,12 @@ endpoints: assert.Equal(t, 1, requestCount) assert.NotContains(t, logs.String(), "No updates received") - require.Len(t, queue, 1) - - evt := <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "foo", ruleSet.Rules[0].ID) }, }, { @@ -287,10 +301,22 @@ endpoints: t.Helper() w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: bar")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: bar +`)) require.NoError(t, err) }, - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(600 * time.Millisecond) @@ -298,13 +324,12 @@ endpoints: assert.Equal(t, 3, requestCount) assert.Contains(t, logs.String(), "No updates received") - require.Len(t, queue, 1) - - evt := <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "bar", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "bar", ruleSet.Rules[0].ID) }, }, { @@ -323,20 +348,41 @@ endpoints: switch callIdx { case 1: w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: bar")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: foo +`)) require.NoError(t, err) case 2: - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusNotFound) default: w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: bar")) + _, err := w.Write([]byte(` +version: "2" +name: test +rules: +- id: bar +`)) require.NoError(t, err) } callIdx++ } }(), - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Twice() + + processor.EXPECT().OnDeleted(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor2").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(1000 * time.Millisecond) @@ -344,24 +390,22 @@ endpoints: assert.True(t, requestCount >= 4) assert.Contains(t, logs.String(), "No updates received") - require.Len(t, queue, 3) - - evt := <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "bar", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 0) - assert.Equal(t, event.Remove, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "bar", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSets := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Values() + assert.Contains(t, ruleSets[0].Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSets[0].Version) + assert.Equal(t, "test", ruleSets[0].Name) + assert.Len(t, ruleSets[0].Rules, 1) + assert.Equal(t, "foo", ruleSets[0].Rules[0].ID) + + assert.Contains(t, ruleSets[1].Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "2", ruleSets[1].Version) + assert.Equal(t, "test", ruleSets[1].Name) + assert.Len(t, ruleSets[1].Rules, 1) + assert.Equal(t, "bar", ruleSets[1].Rules[0].ID) + + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor2").Value() + assert.Contains(t, ruleSet.Source, "http_endpoint:"+srv.URL) + assert.Empty(t, ruleSet.Rules) }, }, { @@ -380,26 +424,57 @@ endpoints: switch callIdx { case 1: w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: bar")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: bar +`)) require.NoError(t, err) case 2: w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: baz")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: baz +`)) require.NoError(t, err) case 3: w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: foo")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: foo +`)) require.NoError(t, err) default: w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: foz")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: foz +`)) require.NoError(t, err) } callIdx++ } }(), - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + + processor.EXPECT().OnUpdated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor2").Capture). + Return(nil).Times(3) + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(2 * time.Second) @@ -407,46 +482,31 @@ endpoints: assert.True(t, requestCount >= 4) assert.Contains(t, logs.String(), "No updates received") - require.Len(t, queue, 7) - - evt := <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "bar", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 0) - assert.Equal(t, event.Remove, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "baz", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 0) - assert.Equal(t, event.Remove, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foo", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 0) - assert.Equal(t, event.Remove, evt.ChangeType) - - evt = <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "foz", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "bar", ruleSet.Rules[0].ID) + + ruleSets := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor2").Values() + assert.Contains(t, ruleSets[0].Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSets[0].Version) + assert.Equal(t, "test", ruleSets[0].Name) + assert.Len(t, ruleSets[0].Rules, 1) + assert.Equal(t, "baz", ruleSets[0].Rules[0].ID) + + assert.Contains(t, ruleSets[1].Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSets[1].Version) + assert.Equal(t, "test", ruleSets[1].Name) + assert.Len(t, ruleSets[1].Rules, 1) + assert.Equal(t, "foo", ruleSets[1].Rules[0].ID) + + assert.Contains(t, ruleSets[2].Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSets[2].Version) + assert.Equal(t, "test", ruleSets[2].Name) + assert.Len(t, ruleSets[2].Rules, 1) + assert.Equal(t, "foz", ruleSets[2].Rules[0].ID) }, }, { @@ -461,23 +521,35 @@ endpoints: w.Header().Set("Expires", time.Now().Add(20*time.Second).UTC().Format(http.TimeFormat)) w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: bar")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: bar +`)) require.NoError(t, err) }, - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(1 * time.Second) assert.Equal(t, 1, requestCount) assert.GreaterOrEqual(t, strings.Count(logs.String(), "No updates received"), 3) - require.Len(t, queue, 1) - evt := <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "bar", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "bar", ruleSet.Rules[0].ID) }, }, { @@ -493,10 +565,22 @@ endpoints: w.Header().Set("Expires", time.Now().Add(20*time.Second).UTC().Format(http.TimeFormat)) w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: bar")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: bar +`)) require.NoError(t, err) }, - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(1 * time.Second) @@ -506,13 +590,139 @@ endpoints: noUpdatesCount := strings.Count(logs.String(), "No updates received") assert.GreaterOrEqual(t, noUpdatesCount, 3) - require.Len(t, queue, 1) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "http_endpoint:"+srv.URL) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + assert.Equal(t, "bar", ruleSet.Rules[0].ID) + }, + }, + { + uc: "previously unknown rule set with error on creation", + conf: []byte(` +endpoints: +- url: ` + srv.URL + ` +`), + writeResponse: func(t *testing.T, w http.ResponseWriter) { + t.Helper() + + w.Header().Set("Content-Type", "application/yaml") + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: foo +`)) + require.NoError(t, err) + }, + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything).Return(testsupport.ErrTestPurpose).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + time.Sleep(200 * time.Millisecond) + + assert.Equal(t, 1, requestCount) + assert.Contains(t, logs.String(), "Failed to apply rule set changes") + }, + }, + { + uc: "updated rule set with error on update", + conf: []byte(` +watch_interval: 200ms +endpoints: +- url: ` + srv.URL + ` +`), + writeResponse: func() ResponseWriter { + callIdx := 1 + + return func(t *testing.T, w http.ResponseWriter) { + t.Helper() + + if callIdx == 1 { + w.Header().Set("Content-Type", "application/yaml") + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: bar +`)) + require.NoError(t, err) + } else { + w.Header().Set("Content-Type", "application/yaml") + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: baz +`)) + require.NoError(t, err) + } + + callIdx++ + } + }(), + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything).Return(nil).Once() + processor.EXPECT().OnUpdated(mock.Anything).Return(testsupport.ErrTestPurpose) + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + time.Sleep(600 * time.Millisecond) + + assert.GreaterOrEqual(t, requestCount, 2) + assert.Contains(t, logs.String(), "Failed to apply rule set changes") + }, + }, + { + uc: "deleted rule set with error on delete", + conf: []byte(` +watch_interval: 200ms +endpoints: +- url: ` + srv.URL + ` +`), + writeResponse: func() ResponseWriter { + callIdx := 1 + + return func(t *testing.T, w http.ResponseWriter) { + t.Helper() - evt := <-queue - assert.Contains(t, evt.Src, "http_endpoint:"+srv.URL) - assert.Len(t, evt.RuleSet, 1) - assert.Equal(t, "bar", evt.RuleSet[0].ID) - assert.Equal(t, event.Create, evt.ChangeType) + if callIdx == 1 { + w.Header().Set("Content-Type", "application/yaml") + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: bar +`)) + require.NoError(t, err) + } else { + w.WriteHeader(http.StatusNotFound) + } + + callIdx++ + } + }(), + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + call := processor.EXPECT().OnCreated(mock.Anything).Return(nil).Once() + processor.EXPECT().OnDeleted(mock.Anything).Return(testsupport.ErrTestPurpose).NotBefore(call) + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + time.Sleep(600 * time.Millisecond) + + assert.GreaterOrEqual(t, requestCount, 2) + assert.Contains(t, logs.String(), "Failed to apply rule set changes") }, }, } { @@ -520,14 +730,24 @@ endpoints: // GIVEN requestCount = 0 + setupProcessor := x.IfThenElse(tc.setupProcessor != nil, + tc.setupProcessor, + func(t *testing.T, processor *mocks.RuleSetProcessorMock) { t.Helper() }) + providerConf, err := testsupport.DecodeTestConfig(tc.conf) require.NoError(t, err) - queue := make(event.RuleSetChangedEventQueue, 10) - defer close(queue) + conf := &config.Configuration{ + Rules: config.Rules{ + Providers: config.RuleProviders{HTTPEndpoint: providerConf}, + }, + } + + processor := mocks.NewRuleSetProcessorMock(t) + setupProcessor(t, processor) logs := &strings.Builder{} - prov, err := newProvider(providerConf, memory.New(), queue, zerolog.New(logs)) + prov, err := newProvider(conf, memory.New(), processor, zerolog.New(logs)) require.NoError(t, err) ctx := context.Background() @@ -547,7 +767,7 @@ endpoints: // THEN require.NoError(t, err) - tc.assert(t, logs, queue) + tc.assert(t, logs, processor) }) } } diff --git a/internal/rules/provider/httpendpoint/ruleset_endpoint.go b/internal/rules/provider/httpendpoint/ruleset_endpoint.go index 52936560c..23e54f381 100644 --- a/internal/rules/provider/httpendpoint/ruleset_endpoint.go +++ b/internal/rules/provider/httpendpoint/ruleset_endpoint.go @@ -20,29 +20,30 @@ import ( "context" "crypto/sha256" "errors" + "fmt" "io" "net/http" "net/url" + "time" "github.com/dadrus/heimdall/internal/endpoint" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/provider/pathprefix" - "github.com/dadrus/heimdall/internal/rules/provider/rulesetparser" + "github.com/dadrus/heimdall/internal/rules/config" "github.com/dadrus/heimdall/internal/x/errorchain" ) type ruleSetEndpoint struct { endpoint.Endpoint `mapstructure:",squash"` - RulesPathPrefix pathprefix.PathPrefix `mapstructure:"rule_path_match_prefix"` + RulesPathPrefix string `mapstructure:"rule_path_match_prefix"` } func (e *ruleSetEndpoint) ID() string { return e.URL } -func (e *ruleSetEndpoint) FetchRuleSet(ctx context.Context) (RuleSet, error) { +func (e *ruleSetEndpoint) FetchRuleSet(ctx context.Context) (*config.RuleSet, error) { req, err := e.CreateRequest(ctx, nil, nil) if err != nil { - return RuleSet{}, errorchain. + return nil, errorchain. NewWithMessage(heimdall.ErrInternal, "failed creating request"). CausedBy(err) } @@ -53,12 +54,12 @@ func (e *ruleSetEndpoint) FetchRuleSet(ctx context.Context) (RuleSet, error) { if err != nil { var clientErr *url.Error if errors.As(err, &clientErr) && clientErr.Timeout() { - return RuleSet{}, errorchain. + return nil, errorchain. NewWithMessage(heimdall.ErrCommunicationTimeout, "request to rule set endpoint timed out"). CausedBy(err) } - return RuleSet{}, errorchain. + return nil, errorchain. NewWithMessage(heimdall.ErrCommunication, "request to rule set endpoint failed"). CausedBy(err) } @@ -66,26 +67,27 @@ func (e *ruleSetEndpoint) FetchRuleSet(ctx context.Context) (RuleSet, error) { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return RuleSet{}, errorchain.NewWithMessagef(heimdall.ErrCommunication, + return nil, errorchain.NewWithMessagef(heimdall.ErrCommunication, "unexpected response code: %v", resp.StatusCode) } md := sha256.New() - contents, err := rulesetparser.ParseRules(resp.Header.Get("Content-Type"), io.TeeReader(resp.Body, md)) + ruleSet, err := config.ParseRules(resp.Header.Get("Content-Type"), io.TeeReader(resp.Body, md)) if err != nil { - return RuleSet{}, errorchain.NewWithMessage(heimdall.ErrInternal, "failed to decode received rule set"). + return nil, errorchain.NewWithMessage(heimdall.ErrInternal, "failed to parse received rule set"). CausedBy(err) } - if err = e.RulesPathPrefix.Verify(contents); err != nil { - return RuleSet{}, err + if err = ruleSet.VerifyPathPrefix(e.RulesPathPrefix); err != nil { + return nil, err } - return RuleSet{ - Rules: contents, - Hash: md.Sum(nil), - }, nil + ruleSet.Hash = md.Sum(nil) + ruleSet.Source = fmt.Sprintf("http_endpoint:%s", e.ID()) + ruleSet.ModTime = time.Now() + + return ruleSet, nil } func (e *ruleSetEndpoint) init() error { diff --git a/internal/rules/provider/httpendpoint/ruleset_endpoint_test.go b/internal/rules/provider/httpendpoint/ruleset_endpoint_test.go index ffb901ce7..5a7e8fc4a 100644 --- a/internal/rules/provider/httpendpoint/ruleset_endpoint_test.go +++ b/internal/rules/provider/httpendpoint/ruleset_endpoint_test.go @@ -32,6 +32,7 @@ import ( "github.com/dadrus/heimdall/internal/cache/mocks" "github.com/dadrus/heimdall/internal/endpoint" "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules/config" "github.com/dadrus/heimdall/internal/x" otelmock "github.com/dadrus/heimdall/internal/x/opentelemetry/mocks" ) @@ -105,7 +106,7 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx uc string ep *ruleSetEndpoint writeResponse ResponseWriter - assert func(t *testing.T, err error, ruleSet *RuleSet) + assert func(t *testing.T, err error, ruleSet *config.RuleSet) }{ { uc: "rule set loading error due to DNS error", @@ -115,7 +116,7 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx Method: http.MethodGet, }, }, - assert: func(t *testing.T, err error, _ *RuleSet) { + assert: func(t *testing.T, err error, _ *config.RuleSet) { t.Helper() require.Error(t, err) @@ -136,7 +137,7 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx w.WriteHeader(http.StatusBadRequest) }, - assert: func(t *testing.T, err error, _ *RuleSet) { + assert: func(t *testing.T, err error, _ *config.RuleSet) { t.Helper() require.Error(t, err) @@ -155,10 +156,15 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx writeResponse: func(t *testing.T, w http.ResponseWriter) { t.Helper() - _, err := w.Write([]byte("foobar")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: bar +`)) require.NoError(t, err) }, - assert: func(t *testing.T, err error, _ *RuleSet) { + assert: func(t *testing.T, err error, _ *config.RuleSet) { t.Helper() require.Error(t, err) @@ -179,14 +185,11 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx w.WriteHeader(http.StatusOK) }, - assert: func(t *testing.T, err error, ruleSet *RuleSet) { + assert: func(t *testing.T, err error, ruleSet *config.RuleSet) { t.Helper() - require.NoError(t, err) - - require.NotNil(t, ruleSet) - require.Empty(t, ruleSet.Rules) - require.NotEmpty(t, ruleSet.Hash) + require.Error(t, err) + assert.ErrorIs(t, err, config.ErrEmptyRuleSet) }, }, { @@ -201,10 +204,15 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx t.Helper() w.Header().Set("Content-Type", "application/yaml") - _, err := w.Write([]byte("- id: foo")) + _, err := w.Write([]byte(` +version: "1" +name: test +rules: +- id: foo +`)) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSet *RuleSet) { + assert: func(t *testing.T, err error, ruleSet *config.RuleSet) { t.Helper() require.NoError(t, err) @@ -227,10 +235,16 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx t.Helper() w.Header().Set("Content-Type", "application/json") - _, err := w.Write([]byte(`[{"id":"foo"}]`)) + _, err := w.Write([]byte(`{ + "version": "1", + "name": "test", + "rules": [ + { "id": "foo" } + ] +}`)) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSet *RuleSet) { + assert: func(t *testing.T, err error, ruleSet *config.RuleSet) { t.Helper() require.NoError(t, err) @@ -254,10 +268,16 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx t.Helper() w.Header().Set("Content-Type", "application/json") - _, err := w.Write([]byte(`[{"id":"foo", "match":"/bar/foo/<**>"}]`)) + _, err := w.Write([]byte(`{ + "version": "1", + "name": "test", + "rules": [ + { "id": "foo", "match":"/bar/foo/<**>" } + ] +}`)) require.NoError(t, err) }, - assert: func(t *testing.T, err error, _ *RuleSet) { + assert: func(t *testing.T, err error, _ *config.RuleSet) { t.Helper() require.Error(t, err) @@ -278,10 +298,16 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx t.Helper() w.Header().Set("Content-Type", "application/json") - _, err := w.Write([]byte(`[{"id":"foo", "match":"<**>://moobar.local:9090/bar/foo/<**>"}]`)) + _, err := w.Write([]byte(`{ + "version": "1", + "name": "test", + "rules": [ + { "id": "foo", "match":"<**>://moobar.local:9090/bar/foo/<**>" } + ] +}`)) require.NoError(t, err) }, - assert: func(t *testing.T, err error, _ *RuleSet) { + assert: func(t *testing.T, err error, _ *config.RuleSet) { t.Helper() require.Error(t, err) @@ -302,10 +328,16 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx t.Helper() w.Header().Set("Content-Type", "application/json") - _, err := w.Write([]byte(`[{"id":"foo", "match":"<**>://moobar.local:9090/foo/bar/<**>"}]`)) + _, err := w.Write([]byte(`{ + "version": "1", + "name": "test", + "rules": [ + { "id": "foo", "match":"<**>://moobar.local:9090/foo/bar/<**>" } + ] +}`)) require.NoError(t, err) }, - assert: func(t *testing.T, err error, ruleSet *RuleSet) { + assert: func(t *testing.T, err error, ruleSet *config.RuleSet) { t.Helper() require.NoError(t, err) @@ -321,7 +353,7 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx // GIVEN cch := &mocks.MockCache{} ctx := log.Logger.With(). - Str("_rule_provider_type", "http_endpoint"). + Str("_provider_type", "http_endpoint"). Logger(). WithContext(cache.WithContext(context.Background(), cch)) @@ -337,7 +369,7 @@ func TestRuleSetEndpointFetchRuleSet(t *testing.T) { //nolint:maintidx ruleSet, err := tc.ep.FetchRuleSet(ctx) // THEN - tc.assert(t, err, &ruleSet) + tc.assert(t, err, ruleSet) }) } } diff --git a/internal/rules/provider/httpendpoint/ruleset_fetcher.go b/internal/rules/provider/httpendpoint/ruleset_fetcher.go index 355de7de4..016b5252f 100644 --- a/internal/rules/provider/httpendpoint/ruleset_fetcher.go +++ b/internal/rules/provider/httpendpoint/ruleset_fetcher.go @@ -18,9 +18,11 @@ package httpendpoint import ( "context" + + "github.com/dadrus/heimdall/internal/rules/config" ) type RuleSetFetcher interface { - FetchRuleSet(ctx context.Context) (RuleSet, error) + FetchRuleSet(ctx context.Context) (*config.RuleSet, error) ID() string } diff --git a/internal/rules/provider/kubernetes/api/v1alpha1/types.go b/internal/rules/provider/kubernetes/api/v1alpha1/types.go index fbe2822dc..f186a887b 100644 --- a/internal/rules/provider/kubernetes/api/v1alpha1/types.go +++ b/internal/rules/provider/kubernetes/api/v1alpha1/types.go @@ -22,13 +22,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "github.com/dadrus/heimdall/internal/rules/rule" + "github.com/dadrus/heimdall/internal/rules/config" ) // +kubebuilder:object:generate=true type RuleSetSpec struct { - AuthClassName string `json:"authClassName"` //nolint:tagliatelle - Rules []rule.Configuration `json:"rules"` + AuthClassName string `json:"authClassName"` //nolint:tagliatelle + Rules []config.Rule `json:"rules"` } // +kubebuilder:object:generate=true diff --git a/internal/rules/provider/kubernetes/api/v1alpha1/zz_generated.deepcopy.go b/internal/rules/provider/kubernetes/api/v1alpha1/zz_generated.deepcopy.go index cb9b418ba..048dafd80 100644 --- a/internal/rules/provider/kubernetes/api/v1alpha1/zz_generated.deepcopy.go +++ b/internal/rules/provider/kubernetes/api/v1alpha1/zz_generated.deepcopy.go @@ -6,7 +6,7 @@ package v1alpha1 import ( - "github.com/dadrus/heimdall/internal/rules/rule" + "github.com/dadrus/heimdall/internal/rules/config" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -56,7 +56,7 @@ func (in *RuleSetSpec) DeepCopyInto(out *RuleSetSpec) { *out = *in if in.Rules != nil { in, out := &in.Rules, &out.Rules - *out = make([]rule.Configuration, len(*in)) + *out = make([]config.Rule, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/internal/rules/provider/kubernetes/module.go b/internal/rules/provider/kubernetes/module.go index a02f12c42..18baf78c1 100644 --- a/internal/rules/provider/kubernetes/module.go +++ b/internal/rules/provider/kubernetes/module.go @@ -17,6 +17,8 @@ package kubernetes import ( + "context" + "go.uber.org/fx" "k8s.io/client-go/rest" ) @@ -24,6 +26,12 @@ import ( // Module is used on app bootstrap. // nolint: gochecknoglobals var Module = fx.Options( - fx.Provide(func() ConfigFactory { return rest.InClusterConfig }), - fx.Invoke(registerProvider), + fx.Provide(func() ConfigFactory { return rest.InClusterConfig }, fx.Private), + fx.Invoke( + fx.Annotate( + newProvider, + fx.OnStart(func(ctx context.Context, p *provider) error { return p.Start(ctx) }), + fx.OnStop(func(ctx context.Context, p *provider) error { return p.Stop(ctx) }), + ), + ), ) diff --git a/internal/rules/provider/kubernetes/provider.go b/internal/rules/provider/kubernetes/provider.go index 33983312c..059e53b6a 100644 --- a/internal/rules/provider/kubernetes/provider.go +++ b/internal/rules/provider/kubernetes/provider.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "sync" + "time" "github.com/go-logr/zerologr" "github.com/rs/zerolog" @@ -31,34 +32,48 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" + "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" + config2 "github.com/dadrus/heimdall/internal/rules/config" "github.com/dadrus/heimdall/internal/rules/provider/kubernetes/api/v1alpha1" + "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x" "github.com/dadrus/heimdall/internal/x/errorchain" ) -var ( - ErrNilRuleSet = errors.New("nil RuleSet") - ErrNotARuleSet = errors.New("not a RuleSet") - ErrBadAuthClass = errors.New("bad authClass in a RuleSet") -) +var ErrBadAuthClass = errors.New("bad authClass in a RuleSet") + +type ConfigFactory func() (*rest.Config, error) type provider struct { - q event.RuleSetChangedEventQueue - l zerolog.Logger - cl v1alpha1.Client - cancel context.CancelFunc - wg sync.WaitGroup - ac string + p rule.SetProcessor + l zerolog.Logger + cl v1alpha1.Client + cancel context.CancelFunc + configured bool + wg sync.WaitGroup + ac string } func newProvider( - rawConf map[string]any, - k8sConf *rest.Config, - queue event.RuleSetChangedEventQueue, + conf *config.Configuration, + k8sCF ConfigFactory, + processor rule.SetProcessor, logger zerolog.Logger, ) (*provider, error) { + rawConf := conf.Rules.Providers.Kubernetes + + if rawConf == nil { + return &provider{}, nil + } + + k8sConf, err := k8sCF() + if err != nil { + return nil, errorchain.NewWithMessage(heimdall.ErrInternal, + "failed to create kubernetes provider"). + CausedBy(err) + } + type Config struct { AuthClass string `mapstructure:"auth_class"` } @@ -70,21 +85,24 @@ func newProvider( CausedBy(err) } - var conf Config - if err = decodeConfig(rawConf, &conf); err != nil { + var providerConf Config + if err = decodeConfig(rawConf, &providerConf); err != nil { return nil, errorchain. NewWithMessage(heimdall.ErrConfiguration, "failed to decode kubernetes rule provider config"). CausedBy(err) } - prov := &provider{ - q: queue, - l: logger, - cl: client, - ac: x.IfThenElse(len(conf.AuthClass) != 0, conf.AuthClass, DefaultClass), - } + logger = logger.With().Str("_provider_type", ProviderType).Logger() + + logger.Info().Msg("Rule provider configured.") - return prov, nil + return &provider{ + p: processor, + l: logger, + cl: client, + ac: x.IfThenElse(len(providerConf.AuthClass) != 0, providerConf.AuthClass, DefaultClass), + configured: true, + }, nil } func (p *provider) newController(ctx context.Context, namespace string) cache.Controller { @@ -109,7 +127,6 @@ func (p *provider) filterAuthClass(input any) (any, error) { if rs.Spec.AuthClassName != p.ac { p.l.Info(). - Str("_rule_provider_type", ProviderType). Msgf("Ignoring ruleset due to authClassName mismatch (namespace=%s, name=%s, uid=%s)", rs.Namespace, rs.Name, rs.UID) @@ -120,17 +137,16 @@ func (p *provider) filterAuthClass(input any) (any, error) { } func (p *provider) Start(_ context.Context) error { + if !p.configured { + return nil + } + klog.SetLogger(zerologr.New(&p.l)) - p.l.Info(). - Str("_rule_provider_type", ProviderType). - Msg("Starting rule definitions provider") + p.l.Info().Msg("Starting rule definitions provider") ctx, cancel := context.WithCancel(context.Background()) - ctx = p.l.With(). - Str("_rule_provider_type", ProviderType). - Logger(). - WithContext(ctx) + ctx = p.l.With().Logger().WithContext(ctx) p.cancel = cancel @@ -150,9 +166,11 @@ func (p *provider) Start(_ context.Context) error { } func (p *provider) Stop(ctx context.Context) error { - p.l.Info(). - Str("_rule_provider_type", ProviderType). - Msg("Tearing down rule provider.") + if !p.configured { + return nil + } + + p.l.Info().Msg("Tearing down rule provider.") p.cancel() @@ -167,49 +185,90 @@ func (p *provider) Stop(ctx context.Context) error { case <-done: return nil case <-ctx.Done(): - p.l.Warn(). - Str("_rule_provider_type", ProviderType). - Msg("Graceful tearing down aborted (timed out).") + p.l.Warn().Msg("Graceful tearing down aborted (timed out).") return nil } } -func (p *provider) updateRuleSet(oldObj, newObj any) { +func (p *provider) updateRuleSet(_, newObj any) { // should never be of a different type. ok if panics - oldRs := oldObj.(*v1alpha1.RuleSet) // nolint: forcetypeassert - newRs := newObj.(*v1alpha1.RuleSet) // nolint: forcetypeassert + rs := newObj.(*v1alpha1.RuleSet) // nolint: forcetypeassert - p.deleteRuleSet(oldRs) - p.addRuleSet(newRs) + conf := &config2.RuleSet{ + MetaData: config2.MetaData{ + Source: fmt.Sprintf("%s:%s:%s", ProviderType, rs.Namespace, rs.UID), + ModTime: rs.CreationTimestamp.Time, + }, + Version: p.mapVersion(rs.APIVersion), + Name: rs.Name, + Rules: rs.Spec.Rules, + } + + p.l.Info().Msg("Rule set update received") + + p.l.Debug().Str("_src", conf.Source). + Msgf("Rule set resource version mapped from '%s' to '%s'", rs.APIVersion, conf.Version) + + if err := p.p.OnUpdated(conf); err != nil { + p.l.Warn().Err(err).Str("_src", conf.Source).Msg("Failed to apply rule set updates") + } else { + p.l.Info().Str("_src", conf.Source).Msg("Rule set updated") + } } func (p *provider) addRuleSet(obj any) { // should never be of a different type. ok if panics rs := obj.(*v1alpha1.RuleSet) // nolint: forcetypeassert - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: fmt.Sprintf("%s:%s:%s:%s", ProviderType, rs.Namespace, rs.Name, rs.UID), - ChangeType: event.Create, - RuleSet: rs.Spec.Rules, - }) + conf := &config2.RuleSet{ + MetaData: config2.MetaData{ + Source: fmt.Sprintf("%s:%s:%s", ProviderType, rs.Namespace, rs.UID), + ModTime: rs.CreationTimestamp.Time, + }, + Version: p.mapVersion(rs.APIVersion), + Name: rs.Name, + Rules: rs.Spec.Rules, + } + + p.l.Info().Msg("New rule set received") + + p.l.Debug().Str("_src", conf.Source). + Msgf("Rule set resource version mapped from '%s' to '%s'", rs.APIVersion, conf.Version) + + if err := p.p.OnCreated(conf); err != nil { + p.l.Warn().Err(err).Str("_src", conf.Source).Msg("Failed creating rule set") + } else { + p.l.Info().Str("_src", conf.Source).Msg("Rule set created") + } } func (p *provider) deleteRuleSet(obj any) { // should never be of a different type. ok if panics rs := obj.(*v1alpha1.RuleSet) // nolint: forcetypeassert - p.ruleSetChanged(event.RuleSetChangedEvent{ - Src: fmt.Sprintf("%s:%s:%s:%s", ProviderType, rs.Namespace, rs.Name, rs.UID), - ChangeType: event.Remove, - }) + conf := &config2.RuleSet{ + MetaData: config2.MetaData{ + Source: fmt.Sprintf("%s:%s:%s", ProviderType, rs.Namespace, rs.UID), + ModTime: time.Now(), + }, + Version: p.mapVersion(rs.APIVersion), + Name: rs.Name, + } + + p.l.Info().Msg("Rule set deletion received") + + p.l.Debug().Str("_src", conf.Source). + Msgf("Rule set resource version mapped from '%s' to '%s'", rs.APIVersion, conf.Version) + + if err := p.p.OnDeleted(conf); err != nil { + p.l.Warn().Err(err).Str("_src", conf.Source).Msg("Failed deleting rule set") + } else { + p.l.Info().Str("_src", conf.Source).Msg("Rule set deleted") + } } -func (p *provider) ruleSetChanged(evt event.RuleSetChangedEvent) { - p.l.Info(). - Str("_rule_provider_type", ProviderType). - Str("_src", evt.Src). - Str("_type", evt.ChangeType.String()). - Msg("Rule set changed") - p.q <- evt +func (p *provider) mapVersion(_ string) string { + // currently the only possible version is v1alpha1, which is mapped to the version "1" used internally + return "1" } diff --git a/internal/rules/provider/kubernetes/provider_registrar.go b/internal/rules/provider/kubernetes/provider_registrar.go deleted file mode 100644 index dba783242..000000000 --- a/internal/rules/provider/kubernetes/provider_registrar.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package kubernetes - -import ( - "context" - - "github.com/rs/zerolog" - "go.uber.org/fx" - "k8s.io/client-go/rest" - - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/x/errorchain" -) - -type ConfigFactory func() (*rest.Config, error) - -type registrationArguments struct { - fx.In - - Lifecycle fx.Lifecycle - Config *config.Configuration - K8sConfig ConfigFactory - Queue event.RuleSetChangedEventQueue -} - -func registerProvider(args registrationArguments, logger zerolog.Logger) error { - if args.Config.Rules.Providers.Kubernetes == nil { - return nil - } - - k8sConf, err := args.K8sConfig() - if err != nil { - return errorchain.NewWithMessage(heimdall.ErrInternal, "failed to create kubernetes provider"). - CausedBy(err) - } - - provider, err := newProvider(args.Config.Rules.Providers.Kubernetes, k8sConf, args.Queue, logger) - if err != nil { - return errorchain.NewWithMessage(heimdall.ErrInternal, "failed to create kubernetes provider"). - CausedBy(err) - } - - logger.Info(). - Str("_rule_provider_type", ProviderType). - Msg("Rule provider configured.") - - args.Lifecycle.Append( - fx.Hook{ - OnStart: func(ctx context.Context) error { return provider.Start(ctx) }, - OnStop: func(ctx context.Context) error { return provider.Stop(ctx) }, - }, - ) - - return nil -} diff --git a/internal/rules/provider/kubernetes/provider_registrar_test.go b/internal/rules/provider/kubernetes/provider_registrar_test.go deleted file mode 100644 index dde31a5ac..000000000 --- a/internal/rules/provider/kubernetes/provider_registrar_test.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package kubernetes - -import ( - "testing" - - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.uber.org/fx" - "k8s.io/client-go/rest" - - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/x" - "github.com/dadrus/heimdall/internal/x/testsupport" -) - -type mockLifecycle struct{ mock.Mock } - -func (m *mockLifecycle) Append(hook fx.Hook) { m.Called(hook) } - -func TestRegisterProvider(t *testing.T) { - // provider creates a client which registers its scheme - // the corresponding k8s api is not threat safe. - // So, to avoid concurrent map writes, this test is not configured - // to run parallel - for _, tc := range []struct { - uc string - conf []byte - setupMocks func(t *testing.T, mockLC *mockLifecycle) - assert func(t *testing.T, err error) - }{ - { - uc: "without it being configured", - assert: func(t *testing.T, err error) { - t.Helper() - - assert.NoError(t, err) - }, - }, - { - uc: "with invalid configuration, unknown field", - conf: []byte(`foo: bar`), - assert: func(t *testing.T, err error) { - t.Helper() - - require.Error(t, err) - assert.ErrorIs(t, err, heimdall.ErrConfiguration) - assert.Contains(t, err.Error(), "failed to decode") - }, - }, - { - uc: "with valid configuration", - conf: []byte(`auth_class: foo`), - setupMocks: func(t *testing.T, mockLC *mockLifecycle) { - t.Helper() - - mockLC.On("Append", mock.AnythingOfType("fx.Hook")) - }, - assert: func(t *testing.T, err error) { - t.Helper() - - require.NoError(t, err) - }, - }, - { - uc: "with valid empty configuration", - conf: []byte(`{}}`), - setupMocks: func(t *testing.T, mockLC *mockLifecycle) { - t.Helper() - - mockLC.On("Append", mock.AnythingOfType("fx.Hook")) - }, - assert: func(t *testing.T, err error) { - t.Helper() - - require.NoError(t, err) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - providerConf, err := testsupport.DecodeTestConfig(tc.conf) - require.NoError(t, err) - - conf := &config.Configuration{ - Rules: config.Rules{ - Providers: config.RuleProviders{Kubernetes: providerConf}, - }, - } - queue := make(event.RuleSetChangedEventQueue, 10) - mlc := &mockLifecycle{} - - setupMocks := x.IfThenElse(tc.setupMocks != nil, - tc.setupMocks, - func(t *testing.T, mockLC *mockLifecycle) { t.Helper() }) - - setupMocks(t, mlc) - - args := registrationArguments{ - Lifecycle: mlc, - Config: conf, - Queue: queue, - K8sConfig: func() (*rest.Config, error) { - return &rest.Config{Host: "http://localhost:80001"}, nil - }, - } - - // WHEN - err = registerProvider(args, log.Logger) - - // THEN - tc.assert(t, err) - - mlc.AssertExpectations(t) - }) - } -} diff --git a/internal/rules/provider/kubernetes/provider_test.go b/internal/rules/provider/kubernetes/provider_test.go index b15df620d..f13cd0935 100644 --- a/internal/rules/provider/kubernetes/provider_test.go +++ b/internal/rules/provider/kubernetes/provider_test.go @@ -29,6 +29,7 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/watch" @@ -36,10 +37,12 @@ import ( "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" + config2 "github.com/dadrus/heimdall/internal/rules/config" "github.com/dadrus/heimdall/internal/rules/provider/kubernetes/api/v1alpha1" - event2 "github.com/dadrus/heimdall/internal/rules/rule" + "github.com/dadrus/heimdall/internal/rules/rule/mocks" + "github.com/dadrus/heimdall/internal/x" "github.com/dadrus/heimdall/internal/x/testsupport" + mock2 "github.com/dadrus/heimdall/internal/x/testsupport/mock" ) func TestNewProvider(t *testing.T) { @@ -95,10 +98,15 @@ func TestNewProvider(t *testing.T) { providerConf, err := testsupport.DecodeTestConfig(tc.conf) require.NoError(t, err) - queue := make(event.RuleSetChangedEventQueue, 10) + conf := &config.Configuration{ + Rules: config.Rules{ + Providers: config.RuleProviders{Kubernetes: providerConf}, + }, + } + k8sCF := func() (*rest.Config, error) { return &rest.Config{Host: "http://localhost:80001"}, nil } // WHEN - prov, err := newProvider(providerConf, &rest.Config{Host: "http://localhost:80001"}, queue, log.Logger) + prov, err := newProvider(conf, k8sCF, mocks.NewRuleSetProcessorMock(t), log.Logger) // THEN tc.assert(t, err, prov) @@ -118,10 +126,11 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop defer srv.Close() for _, tc := range []struct { - uc string - conf []byte - writeResponse ResponseWriter - assert func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) + uc string + conf []byte + writeResponse ResponseWriter + setupProcessor func(t *testing.T, processor *mocks.RuleSetProcessorMock) + assert func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) }{ { uc: "rule set filtered due to wrong auth class", @@ -156,10 +165,10 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop }, Spec: v1alpha1.RuleSetSpec{ AuthClassName: "bar", - Rules: []event2.Configuration{ + Rules: []config2.Rule{ { ID: "test", - RuleMatcher: event2.Matcher{ + RuleMatcher: config2.Matcher{ URL: "http://foo.bar", Strategy: "glob", }, @@ -217,7 +226,7 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop } } }(), - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) @@ -225,8 +234,6 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop messages := logs.String() assert.Contains(t, messages, "Ignoring ruleset") assert.NotContains(t, messages, "Rule set changed") - - require.Empty(t, queue) }, }, { @@ -262,10 +269,10 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop }, Spec: v1alpha1.RuleSetSpec{ AuthClassName: "bar", - Rules: []event2.Configuration{ + Rules: []config2.Rule{ { ID: "test", - RuleMatcher: event2.Matcher{ + RuleMatcher: config2.Matcher{ URL: "http://foo.bar", Strategy: "glob", }, @@ -323,22 +330,28 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop } } }(), - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) messages := logs.String() - assert.Contains(t, messages, "Rule set changed") - - require.Len(t, queue, 1) + assert.Contains(t, messages, "Rule set created") - evt := <-queue - assert.Equal(t, "kubernetes:foo:test-rule:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", evt.Src) - assert.Equal(t, event.Create, evt.ChangeType) + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Contains(t, ruleSet.Source, "kubernetes:foo:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86") + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test-rule", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) - require.Len(t, evt.RuleSet, 1) - rule := evt.RuleSet[0] + rule := ruleSet.Rules[0] assert.Equal(t, "test", rule.ID) assert.Equal(t, "http://foo.bar", rule.RuleMatcher.URL) assert.Equal(t, "http://bar", rule.Upstream) @@ -351,6 +364,113 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop assert.Equal(t, "authz", rule.Execute[1]["authorizer"]) }, }, + { + uc: "adding rule set fails", + conf: []byte("auth_class: bar"), + writeResponse: func() ResponseWriter { + callIdx := 0 + + return func(t *testing.T, watchRequest bool, w http.ResponseWriter) { + t.Helper() + + rls := v1alpha1.RuleSetList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.GroupName, v1alpha1.GroupVersion), + Kind: "RuleSetList", + }, + ListMeta: metav1.ListMeta{ + ResourceVersion: "735820", + }, + Items: []v1alpha1.RuleSet{ + { + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.GroupName, v1alpha1.GroupVersion), + Kind: "RuleSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rule", + Namespace: "foo", + ResourceVersion: "702666", + UID: "dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", + Generation: 1, + CreationTimestamp: metav1.NewTime(time.Now()), + }, + Spec: v1alpha1.RuleSetSpec{ + AuthClassName: "bar", + Rules: []config2.Rule{ + { + ID: "test", + RuleMatcher: config2.Matcher{ + URL: "http://foo.bar", + Strategy: "glob", + }, + Upstream: "http://bar", + Methods: []string{http.MethodGet}, + Execute: []config.MechanismConfig{ + {"authenticator": "authn"}, + {"authorizer": "authz"}, + }, + }, + }, + }, + }, + }, + } + + rawRls, err := json.Marshal(rls) + require.NoError(t, err) + + var evt metav1.WatchEvent + + err = metav1.Convert_watch_Event_To_v1_WatchEvent( + &watch.Event{ + Type: watch.Bookmark, + Object: &v1alpha1.RuleSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.GroupName, v1alpha1.GroupVersion), + Kind: "RuleSet", + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: "715382", + }, + }, + }, + &evt, nil) + require.NoError(t, err) + + rawEvt, err := json.Marshal(evt) + require.NoError(t, err) + + w.Header().Set("Content-Type", "application/json") + if watchRequest { + if callIdx == 0 { + _, err := w.Write(rawEvt) + require.NoError(t, err) + } else { + time.Sleep(1 * time.Second) + w.WriteHeader(http.StatusInternalServerError) + } + + callIdx++ + } else { + _, err := w.Write(rawRls) + require.NoError(t, err) + } + } + }(), + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything).Return(testsupport.ErrTestPurpose).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + time.Sleep(250 * time.Millisecond) + + assert.Contains(t, logs.String(), "Failed creating rule set") + }, + }, { uc: "a ruleset is added and then removed", conf: []byte("auth_class: bar"), @@ -381,10 +501,10 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop }, Spec: v1alpha1.RuleSetSpec{ AuthClassName: "bar", - Rules: []event2.Configuration{ + Rules: []config2.Rule{ { ID: "test", - RuleMatcher: event2.Matcher{ + RuleMatcher: config2.Matcher{ URL: "http://foo.bar", Strategy: "glob", }, @@ -451,36 +571,164 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop } } }(), - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + + processor.EXPECT().OnDeleted(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor2").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) messages := logs.String() - assert.Equal(t, 2, strings.Count(messages, "Rule set changed")) + assert.Contains(t, messages, "Rule set created") + assert.Contains(t, messages, "Rule set deleted") + + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Equal(t, ruleSet.Source, "kubernetes:foo:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86") + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test-rule", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + + createdRule := ruleSet.Rules[0] + assert.Equal(t, "test", createdRule.ID) + assert.Equal(t, "http://foo.bar", createdRule.RuleMatcher.URL) + assert.Equal(t, "http://bar", createdRule.Upstream) + assert.Equal(t, "glob", createdRule.RuleMatcher.Strategy) + assert.Len(t, createdRule.Methods, 1) + assert.Contains(t, createdRule.Methods, http.MethodGet) + assert.Empty(t, createdRule.ErrorHandler) + assert.Len(t, createdRule.Execute, 2) + assert.Equal(t, "authn", createdRule.Execute[0]["authenticator"]) + assert.Equal(t, "authz", createdRule.Execute[1]["authorizer"]) + + ruleSet = mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor2").Value() + assert.Equal(t, "kubernetes:foo:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", ruleSet.Source) + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test-rule", ruleSet.Name) + }, + }, + { + uc: "removing rule set fails", + conf: []byte("auth_class: bar"), + writeResponse: func() ResponseWriter { + callIdx := 0 - require.Len(t, queue, 2) + return func(t *testing.T, watchRequest bool, w http.ResponseWriter) { + t.Helper() - evt := <-queue - assert.Equal(t, "kubernetes:foo:test-rule:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", evt.Src) - assert.Equal(t, event.Create, evt.ChangeType) + w.Header().Set("Content-Type", "application/json") + if watchRequest { + callIdx++ - require.Len(t, evt.RuleSet, 1) - rule := evt.RuleSet[0] - assert.Equal(t, "test", rule.ID) - assert.Equal(t, "http://foo.bar", rule.RuleMatcher.URL) - assert.Equal(t, "http://bar", rule.Upstream) - assert.Equal(t, "glob", rule.RuleMatcher.Strategy) - assert.Len(t, rule.Methods, 1) - assert.Contains(t, rule.Methods, http.MethodGet) - assert.Empty(t, rule.ErrorHandler) - assert.Len(t, rule.Execute, 2) - assert.Equal(t, "authn", rule.Execute[0]["authenticator"]) - assert.Equal(t, "authz", rule.Execute[1]["authorizer"]) + evt := watch.Event{ + Type: watch.Added, + Object: &v1alpha1.RuleSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.GroupName, v1alpha1.GroupVersion), + Kind: "RuleSet", + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: "715382", + Name: "test-rule", + Namespace: "foo", + UID: "dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", + Generation: 1, + CreationTimestamp: metav1.NewTime(time.Now()), + }, + Spec: v1alpha1.RuleSetSpec{ + AuthClassName: "bar", + Rules: []config2.Rule{ + { + ID: "test", + RuleMatcher: config2.Matcher{ + URL: "http://foo.bar", + Strategy: "glob", + }, + Upstream: "http://bar", + Methods: []string{http.MethodGet}, + Execute: []config.MechanismConfig{ + {"authenticator": "authn"}, + {"authorizer": "authz"}, + }, + }, + }, + }, + }, + } + + switch callIdx { + case 1: + // add a rule set + var watchEvt metav1.WatchEvent + + err := metav1.Convert_watch_Event_To_v1_WatchEvent(&evt, &watchEvt, nil) + require.NoError(t, err) + + rawEvt, err := json.Marshal(watchEvt) + require.NoError(t, err) + + _, err = w.Write(rawEvt) + require.NoError(t, err) + case 2: + // remove it + var watchEvt metav1.WatchEvent + + evt.Type = watch.Deleted + err := metav1.Convert_watch_Event_To_v1_WatchEvent(&evt, &watchEvt, nil) + require.NoError(t, err) + + rawEvt, err := json.Marshal(watchEvt) + require.NoError(t, err) + + _, err = w.Write(rawEvt) + require.NoError(t, err) + default: + // no changes + time.Sleep(1 * time.Second) + w.WriteHeader(http.StatusInternalServerError) + } + } else { + // empty rule set initially + rls := v1alpha1.RuleSetList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.GroupName, v1alpha1.GroupVersion), + Kind: "RuleSetList", + }, + ListMeta: metav1.ListMeta{ + ResourceVersion: "735820", + }, + } + + rawRls, err := json.Marshal(rls) + require.NoError(t, err) + + _, err = w.Write(rawRls) + require.NoError(t, err) + } + } + }(), + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() - evt = <-queue - assert.Equal(t, "kubernetes:foo:test-rule:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", evt.Src) - assert.Equal(t, event.Remove, evt.ChangeType) + processor.EXPECT().OnCreated(mock.Anything).Return(nil).Once() + processor.EXPECT().OnDeleted(mock.Anything).Return(testsupport.ErrTestPurpose).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + time.Sleep(250 * time.Millisecond) + + messages := logs.String() + assert.Contains(t, messages, "Rule set created") + assert.Contains(t, messages, "Failed deleting rule set") }, }, { @@ -513,10 +761,10 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop }, Spec: v1alpha1.RuleSetSpec{ AuthClassName: "bar", - Rules: []event2.Configuration{ + Rules: []config2.Rule{ { ID: "test", - RuleMatcher: event2.Matcher{ + RuleMatcher: config2.Matcher{ URL: "http://foo.bar", Strategy: "glob", }, @@ -553,10 +801,10 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop ruleSet := evt.Object.(*v1alpha1.RuleSet) // nolint:forcetypeassert ruleSet.Spec = v1alpha1.RuleSetSpec{ AuthClassName: "bar", - Rules: []event2.Configuration{ + Rules: []config2.Rule{ { ID: "test", - RuleMatcher: event2.Matcher{ + RuleMatcher: config2.Matcher{ URL: "http://foo.bar", Strategy: "glob", }, @@ -602,53 +850,196 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop } } }(), - assert: func(t *testing.T, logs fmt.Stringer, queue event.RuleSetChangedEventQueue) { + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor1").Capture). + Return(nil).Once() + + processor.EXPECT().OnUpdated(mock.Anything). + Run(mock2.NewArgumentCaptor[*config2.RuleSet](&processor.Mock, "captor2").Capture). + Return(nil).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { t.Helper() time.Sleep(250 * time.Millisecond) messages := logs.String() - assert.Equal(t, 3, strings.Count(messages, "Rule set changed")) + assert.Contains(t, messages, "Rule set created") + assert.Contains(t, messages, "Rule set updated") + + ruleSet := mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor1").Value() + assert.Equal(t, ruleSet.Source, "kubernetes:foo:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86") + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test-rule", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + + createdRule := ruleSet.Rules[0] + assert.Equal(t, "test", createdRule.ID) + assert.Equal(t, "http://foo.bar", createdRule.RuleMatcher.URL) + assert.Equal(t, "http://bar", createdRule.Upstream) + assert.Equal(t, "glob", createdRule.RuleMatcher.Strategy) + assert.Len(t, createdRule.Methods, 1) + assert.Contains(t, createdRule.Methods, http.MethodGet) + assert.Empty(t, createdRule.ErrorHandler) + assert.Len(t, createdRule.Execute, 2) + assert.Equal(t, "authn", createdRule.Execute[0]["authenticator"]) + assert.Equal(t, "authz", createdRule.Execute[1]["authorizer"]) + + ruleSet = mock2.ArgumentCaptorFrom[*config2.RuleSet](&processor.Mock, "captor2").Value() + assert.Equal(t, ruleSet.Source, "kubernetes:foo:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86") + assert.Equal(t, "1", ruleSet.Version) + assert.Equal(t, "test-rule", ruleSet.Name) + assert.Len(t, ruleSet.Rules, 1) + + updatedRule := ruleSet.Rules[0] + assert.Equal(t, "test", updatedRule.ID) + assert.Equal(t, "http://foo.bar", updatedRule.RuleMatcher.URL) + assert.Equal(t, "http://bar", updatedRule.Upstream) + assert.Equal(t, "glob", updatedRule.RuleMatcher.Strategy) + assert.Len(t, updatedRule.Methods, 1) + assert.Contains(t, updatedRule.Methods, http.MethodGet) + assert.Empty(t, updatedRule.ErrorHandler) + assert.Len(t, updatedRule.Execute, 2) + assert.Equal(t, "test_authn", updatedRule.Execute[0]["authenticator"]) + assert.Equal(t, "test_authz", updatedRule.Execute[1]["authorizer"]) + }, + }, + { + uc: "failed updating rule set", + conf: []byte("auth_class: bar"), + writeResponse: func() ResponseWriter { + callIdx := 0 - require.Len(t, queue, 3) + return func(t *testing.T, watchRequest bool, w http.ResponseWriter) { + t.Helper() - evt := <-queue - assert.Equal(t, "kubernetes:foo:test-rule:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", evt.Src) - assert.Equal(t, event.Create, evt.ChangeType) + w.Header().Set("Content-Type", "application/json") + if watchRequest { + callIdx++ - require.Len(t, evt.RuleSet, 1) - rule := evt.RuleSet[0] - assert.Equal(t, "test", rule.ID) - assert.Equal(t, "http://foo.bar", rule.RuleMatcher.URL) - assert.Equal(t, "http://bar", rule.Upstream) - assert.Equal(t, "glob", rule.RuleMatcher.Strategy) - assert.Len(t, rule.Methods, 1) - assert.Contains(t, rule.Methods, http.MethodGet) - assert.Empty(t, rule.ErrorHandler) - assert.Len(t, rule.Execute, 2) - assert.Equal(t, "authn", rule.Execute[0]["authenticator"]) - assert.Equal(t, "authz", rule.Execute[1]["authorizer"]) + evt := watch.Event{ + Type: watch.Added, + Object: &v1alpha1.RuleSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.GroupName, v1alpha1.GroupVersion), + Kind: "RuleSet", + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: "715382", + Name: "test-rule", + Namespace: "foo", + UID: "dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", + Generation: 1, + CreationTimestamp: metav1.NewTime(time.Now()), + }, + Spec: v1alpha1.RuleSetSpec{ + AuthClassName: "bar", + Rules: []config2.Rule{ + { + ID: "test", + RuleMatcher: config2.Matcher{ + URL: "http://foo.bar", + Strategy: "glob", + }, + Upstream: "http://bar", + Methods: []string{http.MethodGet}, + Execute: []config.MechanismConfig{ + {"authenticator": "authn"}, + {"authorizer": "authz"}, + }, + }, + }, + }, + }, + } - evt = <-queue - assert.Equal(t, "kubernetes:foo:test-rule:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", evt.Src) - assert.Equal(t, event.Remove, evt.ChangeType) + switch callIdx { + case 1: + // add a rule set + var watchEvt metav1.WatchEvent - evt = <-queue - assert.Equal(t, "kubernetes:foo:test-rule:dfb2a2f1-1ad2-4d8c-8456-516fc94abb86", evt.Src) - assert.Equal(t, event.Create, evt.ChangeType) + err := metav1.Convert_watch_Event_To_v1_WatchEvent(&evt, &watchEvt, nil) + require.NoError(t, err) - require.Len(t, evt.RuleSet, 1) - rule = evt.RuleSet[0] - assert.Equal(t, "test", rule.ID) - assert.Equal(t, "http://foo.bar", rule.RuleMatcher.URL) - assert.Equal(t, "http://bar", rule.Upstream) - assert.Equal(t, "glob", rule.RuleMatcher.Strategy) - assert.Len(t, rule.Methods, 1) - assert.Contains(t, rule.Methods, http.MethodGet) - assert.Empty(t, rule.ErrorHandler) - assert.Len(t, rule.Execute, 2) - assert.Equal(t, "test_authn", rule.Execute[0]["authenticator"]) - assert.Equal(t, "test_authz", rule.Execute[1]["authorizer"]) + rawEvt, err := json.Marshal(watchEvt) + require.NoError(t, err) + + _, err = w.Write(rawEvt) + require.NoError(t, err) + case 2: + // update it + var watchEvt metav1.WatchEvent + + evt.Type = watch.Modified + ruleSet := evt.Object.(*v1alpha1.RuleSet) // nolint:forcetypeassert + ruleSet.Spec = v1alpha1.RuleSetSpec{ + AuthClassName: "bar", + Rules: []config2.Rule{ + { + ID: "test", + RuleMatcher: config2.Matcher{ + URL: "http://foo.bar", + Strategy: "glob", + }, + Upstream: "http://bar", + Methods: []string{http.MethodGet}, + Execute: []config.MechanismConfig{ + {"authenticator": "test_authn"}, + {"authorizer": "test_authz"}, + }, + }, + }, + } + err := metav1.Convert_watch_Event_To_v1_WatchEvent(&evt, &watchEvt, nil) + require.NoError(t, err) + + rawEvt, err := json.Marshal(watchEvt) + require.NoError(t, err) + + _, err = w.Write(rawEvt) + require.NoError(t, err) + default: + // no changes + time.Sleep(1 * time.Second) + w.WriteHeader(http.StatusInternalServerError) + } + } else { + // empty rule set initially + rls := v1alpha1.RuleSetList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", v1alpha1.GroupName, v1alpha1.GroupVersion), + Kind: "RuleSetList", + }, + ListMeta: metav1.ListMeta{ + ResourceVersion: "735820", + }, + } + + rawRls, err := json.Marshal(rls) + require.NoError(t, err) + + _, err = w.Write(rawRls) + require.NoError(t, err) + } + } + }(), + setupProcessor: func(t *testing.T, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + processor.EXPECT().OnCreated(mock.Anything).Return(nil).Once() + processor.EXPECT().OnUpdated(mock.Anything).Return(testsupport.ErrTestPurpose).Once() + }, + assert: func(t *testing.T, logs fmt.Stringer, processor *mocks.RuleSetProcessorMock) { + t.Helper() + + time.Sleep(250 * time.Millisecond) + + messages := logs.String() + assert.Contains(t, messages, "Rule set created") + assert.Contains(t, messages, "Failed to apply rule set updates") }, }, } { @@ -657,11 +1048,22 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop providerConf, err := testsupport.DecodeTestConfig(tc.conf) require.NoError(t, err) - queue := make(event.RuleSetChangedEventQueue, 10) - defer close(queue) + conf := &config.Configuration{ + Rules: config.Rules{ + Providers: config.RuleProviders{Kubernetes: providerConf}, + }, + } + k8sCF := func() (*rest.Config, error) { return &rest.Config{Host: srv.URL}, nil } + + setupProcessor := x.IfThenElse(tc.setupProcessor != nil, + tc.setupProcessor, + func(t *testing.T, _ *mocks.RuleSetProcessorMock) { t.Helper() }) + + processor := mocks.NewRuleSetProcessorMock(t) + setupProcessor(t, processor) logs := &strings.Builder{} - prov, err := newProvider(providerConf, &rest.Config{Host: srv.URL}, queue, zerolog.New(logs)) + prov, err := newProvider(conf, k8sCF, processor, zerolog.New(logs)) require.NoError(t, err) ctx := context.Background() @@ -674,7 +1076,7 @@ func TestProviderLifecycle(t *testing.T) { //nolint:maintidx,gocognit, cyclop // THEN require.NoError(t, err) - tc.assert(t, logs, queue) + tc.assert(t, logs, processor) }) } } diff --git a/internal/rules/provider/pathprefix/path_prefix.go b/internal/rules/provider/pathprefix/path_prefix.go deleted file mode 100644 index 28f266d22..000000000 --- a/internal/rules/provider/pathprefix/path_prefix.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package pathprefix - -import ( - "strings" - - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/rule" - "github.com/dadrus/heimdall/internal/x/errorchain" -) - -type PathPrefix string - -func (p PathPrefix) Verify(rules []rule.Configuration) error { - if len(p) == 0 { - return nil - } - - for _, rule := range rules { - if strings.HasPrefix(rule.RuleMatcher.URL, "/") && - // only path is specified - !strings.HasPrefix(rule.RuleMatcher.URL, string(p)) || - // patterns are specified before the path - // There should be a better way to check it - !strings.Contains(rule.RuleMatcher.URL, string(p)) { - return errorchain.NewWithMessage(heimdall.ErrConfiguration, - "path prefix validation failed for rule ID=%s") - } - } - - return nil -} diff --git a/internal/rules/provider/pathprefix/path_prefix_test.go b/internal/rules/provider/pathprefix/path_prefix_test.go deleted file mode 100644 index dc5889066..000000000 --- a/internal/rules/provider/pathprefix/path_prefix_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package pathprefix - -import ( - "testing" - - "github.com/stretchr/testify/require" - - event2 "github.com/dadrus/heimdall/internal/rules/rule" -) - -func TestPathPrefixVerify(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - prefix PathPrefix - url string - fail bool - }{ - {uc: "path only and without required prefix", prefix: "/foo/bar", url: "/bar/foo/moo", fail: true}, - {uc: "path only with required prefix", prefix: "/foo/bar", url: "/foo/bar/moo", fail: false}, - {uc: "full url and without required prefix", prefix: "/foo/bar", url: "https://<**>/bar/foo/moo", fail: true}, - {uc: "full url with required prefix", prefix: "/foo/bar", url: "https://<**>/foo/bar/moo", fail: false}, - } { - t.Run(tc.uc, func(t *testing.T) { - // WHEN - err := tc.prefix.Verify([]event2.Configuration{{RuleMatcher: event2.Matcher{URL: tc.url}}}) - - if tc.fail { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/internal/rules/provider/rulesetparser/parser_test.go b/internal/rules/provider/rulesetparser/parser_test.go deleted file mode 100644 index 93cbc30e3..000000000 --- a/internal/rules/provider/rulesetparser/parser_test.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package rulesetparser - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/rule" -) - -func TestParseRules(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - contentType string - content []byte - assert func(t *testing.T, err error, rules []rule.Configuration) - }{ - { - uc: "unsupported content type and not empty contents", - contentType: "foobar", - content: []byte(`foo: bar`), - assert: func(t *testing.T, err error, rules []rule.Configuration) { - t.Helper() - - require.Error(t, err) - assert.ErrorIs(t, err, heimdall.ErrInternal) - assert.Contains(t, err.Error(), "unsupported 'foobar'") - }, - }, - { - uc: "unsupported content type and empty contents", - contentType: "foobar", - assert: func(t *testing.T, err error, rules []rule.Configuration) { - t.Helper() - - require.NoError(t, err) - require.Empty(t, rules) - }, - }, - { - uc: "JSON content and not empty contents", - contentType: "application/json", - content: []byte(`[{"id": "bar"}]`), - assert: func(t *testing.T, err error, rules []rule.Configuration) { - t.Helper() - - require.NoError(t, err) - }, - }, - { - uc: "JSON content and empty contents", - contentType: "application/json", - assert: func(t *testing.T, err error, rules []rule.Configuration) { - t.Helper() - - require.NoError(t, err) - require.Empty(t, rules) - }, - }, - { - uc: "YAML content and not empty contents", - contentType: "application/yaml", - content: []byte(`- id: bar`), - assert: func(t *testing.T, err error, rules []rule.Configuration) { - t.Helper() - - require.NoError(t, err) - }, - }, - { - uc: "YAML content and empty contents", - contentType: "application/yaml", - assert: func(t *testing.T, err error, rules []rule.Configuration) { - t.Helper() - - require.NoError(t, err) - require.Empty(t, rules) - }, - }, - } { - t.Run(tc.uc, func(t *testing.T) { - // WHEN - rules, err := ParseRules(tc.contentType, bytes.NewBuffer(tc.content)) - - // THEN - tc.assert(t, err, rules) - }) - } -} diff --git a/internal/rules/provider/rulesetparser/yaml_parser.go b/internal/rules/provider/rulesetparser/yaml_parser.go deleted file mode 100644 index 4d49565a7..000000000 --- a/internal/rules/provider/rulesetparser/yaml_parser.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package rulesetparser - -import ( - "errors" - "io" - - "gopkg.in/yaml.v3" - - "github.com/dadrus/heimdall/internal/rules/rule" -) - -func parseYAML(reader io.Reader) ([]rule.Configuration, error) { - var ( - rawConfig []map[string]any - rcs []rule.Configuration - ) - - dec := yaml.NewDecoder(reader) - if err := dec.Decode(&rawConfig); err != nil { - if errors.Is(err, io.EOF) { - return rcs, nil - } - - return nil, err - } - - err := rule.DecodeConfig(rawConfig, &rcs) - - return rcs, err -} diff --git a/internal/rules/provider/rulesetparser/yaml_parser_test.go b/internal/rules/provider/rulesetparser/yaml_parser_test.go deleted file mode 100644 index c5e22cb4c..000000000 --- a/internal/rules/provider/rulesetparser/yaml_parser_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package rulesetparser - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/dadrus/heimdall/internal/rules/rule" -) - -func TestParseYAML(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - conf []byte - assert func(t *testing.T, err error, ruleSet []rule.Configuration) - }{ - { - uc: "empty rule set spec", - assert: func(t *testing.T, err error, ruleSet []rule.Configuration) { - t.Helper() - - require.NoError(t, err) - require.Empty(t, ruleSet) - }, - }, - { - uc: "invalid rule set spec", - conf: []byte(`- foo: bar`), - assert: func(t *testing.T, err error, ruleSet []rule.Configuration) { - t.Helper() - - require.Error(t, err) - }, - }, - { - uc: "valid rule set spec", - conf: []byte(`- id: bar`), - assert: func(t *testing.T, err error, ruleSet []rule.Configuration) { - t.Helper() - - require.NoError(t, err) - require.Len(t, ruleSet, 1) - assert.Equal(t, "bar", ruleSet[0].ID) - }, - }, - } { - t.Run(tc.uc, func(t *testing.T) { - // WHEN - ruleSet, err := parseYAML(bytes.NewBuffer(tc.conf)) - - // THEN - tc.assert(t, err, ruleSet) - }) - } -} diff --git a/internal/rules/repository.go b/internal/rules/repository.go deleted file mode 100644 index b1d1b0c7c..000000000 --- a/internal/rules/repository.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package rules - -import ( - "context" - "net/url" - "sync" - - "github.com/rs/zerolog" - - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/rules/rule" - "github.com/dadrus/heimdall/internal/x/errorchain" -) - -const defaultRuleListSize = 0 - -type Repository interface { - FindRule(*url.URL) (rule.Rule, error) -} - -func NewRepository( - queue event.RuleSetChangedEventQueue, - ruleFactory RuleFactory, - logger zerolog.Logger, -) (Repository, error) { - return &repository{ - rf: ruleFactory, - logger: logger, - rules: make([]rule.Rule, defaultRuleListSize), - queue: queue, - quit: make(chan bool), - }, nil -} - -type repository struct { - rf RuleFactory - logger zerolog.Logger - - rules []rule.Rule - mutex sync.RWMutex - - queue event.RuleSetChangedEventQueue - quit chan bool -} - -func (r *repository) FindRule(requestURL *url.URL) (rule.Rule, error) { - r.mutex.RLock() - defer r.mutex.RUnlock() - - for _, rul := range r.rules { - if rul.MatchesURL(requestURL) { - return rul, nil - } - } - - if r.rf.HasDefaultRule() { - return r.rf.DefaultRule(), nil - } - - return nil, errorchain.NewWithMessagef(heimdall.ErrNoRuleFound, - "no applicable rule found for %s", requestURL.String()) -} - -func (r *repository) Start(_ context.Context) error { - r.logger.Info().Msg("Starting rule definition loader") - - go r.watchRuleSetChanges() - - return nil -} - -func (r *repository) Stop(_ context.Context) error { - r.logger.Info().Msg("Tearing down rule definition loader") - - r.quit <- true - - close(r.quit) - - return nil -} - -func (r *repository) watchRuleSetChanges() { - for { - select { - case evt, ok := <-r.queue: - if !ok { - r.logger.Debug().Msg("Rule set definition queue closed") - } - - if evt.ChangeType == event.Create { - r.onRuleSetCreated(evt.Src, evt.RuleSet) - } else if evt.ChangeType == event.Remove { - r.onRuleSetDeleted(evt.Src) - } - case <-r.quit: - r.logger.Info().Msg("Rule definition loader stopped") - - return - } - } -} - -func (r *repository) loadRules(srcID string, ruleSet []rule.Configuration) ([]rule.Rule, error) { - rules := make([]rule.Rule, len(ruleSet)) - - for idx, rc := range ruleSet { - rul, err := r.rf.CreateRule(srcID, rc) - if err != nil { - return nil, errorchain.NewWithMessage(heimdall.ErrInternal, "failed loading rule").CausedBy(err) - } - - rules[idx] = rul - } - - return rules, nil -} - -func (r *repository) addRule(rule rule.Rule) { - r.mutex.Lock() - defer r.mutex.Unlock() - - r.rules = append(r.rules, rule) - - r.logger.Debug().Str("_src", rule.SrcID()).Str("_id", rule.ID()).Msg("Rule added") -} - -func (r *repository) removeRules(srcID string) { - r.logger.Info().Str("_src", srcID).Msg("Removing rules") - - r.mutex.Lock() - defer r.mutex.Unlock() - - // find all indexes for affected rules - var idxs []int - - for idx, rul := range r.rules { - if rul.SrcID() == srcID { - idxs = append(idxs, idx) - - r.logger.Debug().Str("_id", rul.ID()).Msg("Removing rule") - } - } - - // if all rules should be dropped, just create a new slice - if len(idxs) == len(r.rules) { - r.rules = make([]rule.Rule, defaultRuleListSize) - - return - } - - // move the elements from the end of the rules slice to the found positions - // and set the corresponding "emptied" values to nil - for i, idx := range idxs { - tailIdx := len(r.rules) - (1 + i) - - r.rules[idx] = r.rules[tailIdx] - - // the below re-slice preserves the capacity of the slice. - // this is required to avoid memory leaks - r.rules[tailIdx] = nil - } - - // re-slice - r.rules = r.rules[:len(r.rules)-len(idxs)] -} - -func (r *repository) onRuleSetCreated(srcID string, ruleSet []rule.Configuration) { - // create rules - r.logger.Info().Str("_src", srcID).Msg("Loading rule set") - - rules, err := r.loadRules(srcID, ruleSet) - if err != nil { - r.logger.Error().Err(err).Str("_src", srcID).Msg("Failed loading rule set") - } - - // add them - for _, rul := range rules { - r.addRule(rul) - } -} - -func (r *repository) onRuleSetDeleted(src string) { - r.removeRules(src) -} diff --git a/internal/rules/repository_impl.go b/internal/rules/repository_impl.go new file mode 100644 index 000000000..88c5b0f90 --- /dev/null +++ b/internal/rules/repository_impl.go @@ -0,0 +1,261 @@ +package rules + +import ( + "bytes" + "context" + "net/url" + "sync" + + "github.com/rs/zerolog" + + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules/event" + "github.com/dadrus/heimdall/internal/rules/rule" + "github.com/dadrus/heimdall/internal/x/errorchain" + "github.com/dadrus/heimdall/internal/x/slicex" +) + +func newRepository( + queue event.RuleSetChangedEventQueue, + ruleFactory rule.Factory, + logger zerolog.Logger, +) *repository { + return &repository{ + dr: ruleFactory.DefaultRule(), + logger: logger, + queue: queue, + quit: make(chan bool), + } +} + +type repository struct { + dr rule.Rule + logger zerolog.Logger + + rules []rule.Rule + mutex sync.RWMutex + + queue event.RuleSetChangedEventQueue + quit chan bool +} + +func (r *repository) FindRule(requestURL *url.URL) (rule.Rule, error) { + r.mutex.RLock() + defer r.mutex.RUnlock() + + for _, rul := range r.rules { + if rul.MatchesURL(requestURL) { + return rul, nil + } + } + + if r.dr != nil { + return r.dr, nil + } + + return nil, errorchain.NewWithMessagef(heimdall.ErrNoRuleFound, + "no applicable rule found for %s", requestURL.String()) +} + +func (r *repository) Start(_ context.Context) error { + r.logger.Info().Msg("Starting rule definition loader") + + go r.watchRuleSetChanges() + + return nil +} + +func (r *repository) Stop(_ context.Context) error { + r.logger.Info().Msg("Tearing down rule definition loader") + + r.quit <- true + + close(r.quit) + + return nil +} + +func (r *repository) watchRuleSetChanges() { + for { + select { + case evt, ok := <-r.queue: + if !ok { + r.logger.Debug().Msg("Rule set definition queue closed") + } + + switch evt.ChangeType { + case event.Create: + r.addRuleSet(evt.Source, evt.Rules) + case event.Update: + r.updateRuleSet(evt.Source, evt.Rules) + case event.Remove: + r.deleteRuleSet(evt.Source) + } + case <-r.quit: + r.logger.Info().Msg("Rule definition loader stopped") + + return + } + } +} + +func (r *repository) addRuleSet(srcID string, rules []rule.Rule) { + // create rules + r.logger.Info().Str("_src", srcID).Msg("Adding rule set") + + r.mutex.Lock() + defer r.mutex.Unlock() + + // add them + r.addRules(rules) +} + +func (r *repository) updateRuleSet(srcID string, rules []rule.Rule) { + // create rules + r.logger.Info().Str("_src", srcID).Msg("Updating rule set") + + // find all rules for the given src id + applicable := func() []rule.Rule { + r.mutex.Lock() + defer r.mutex.Unlock() + + return slicex.Filter(r.rules, func(r rule.Rule) bool { return r.SrcID() == srcID }) + }() + + // find new rules + newRules := slicex.Filter(rules, func(r rule.Rule) bool { + var known bool + + for _, existing := range applicable { + if existing.ID() == r.ID() { + known = true + + break + } + } + + return !known + }) + + // find update rules + updatedRules := slicex.Filter(rules, func(r rule.Rule) bool { + loaded := r.(*ruleImpl) // nolint: forcetypeassert + var updated bool + + for _, existing := range applicable { + known := existing.(*ruleImpl) // nolint: forcetypeassert + + if known.id == loaded.id && !bytes.Equal(known.hash, loaded.hash) { + updated = true + + break + } + } + + return updated + }) + + // find deleted rules + deletedRules := slicex.Filter(applicable, func(r rule.Rule) bool { + var present bool + + for _, loaded := range rules { + if loaded.ID() == r.ID() { + present = true + + break + } + } + + return !present + }) + + func() { + r.mutex.Lock() + defer r.mutex.Unlock() + + // remove deleted rules + r.removeRules(deletedRules) + + // replace updated rules + r.replaceRules(updatedRules) + + // add new rules + r.addRules(newRules) + }() +} + +func (r *repository) deleteRuleSet(srcID string) { + r.logger.Info().Str("_src", srcID).Msg("Deleting rule set") + + r.mutex.Lock() + defer r.mutex.Unlock() + + // find all rules for the given src id + applicable := slicex.Filter(r.rules, func(r rule.Rule) bool { return r.SrcID() == srcID }) + + // remove them + r.removeRules(applicable) +} + +func (r *repository) addRules(rules []rule.Rule) { + for _, rul := range rules { + r.rules = append(r.rules, rul) + + r.logger.Debug().Str("_src", rul.SrcID()).Str("_id", rul.ID()).Msg("Rule added") + } +} + +func (r *repository) removeRules(rules []rule.Rule) { + // find all indexes for affected rules + var idxs []int + + for idx, rul := range r.rules { + for _, tbd := range rules { + if rul.ID() == tbd.ID() { + idxs = append(idxs, idx) + + r.logger.Debug().Str("_src", rul.SrcID()).Str("_id", rul.ID()).Msg("Rule removed") + } + } + } + + // if all rules should be dropped, just create a new slice + if len(idxs) == len(r.rules) { + r.rules = nil + + return + } + + // move the elements from the end of the rules slice to the found positions + // and set the corresponding "emptied" values to nil + for i, idx := range idxs { + tailIdx := len(r.rules) - (1 + i) + + r.rules[idx] = r.rules[tailIdx] + + // the below re-slice preserves the capacity of the slice. + // this is required to avoid memory leaks + r.rules[tailIdx] = nil + } + + // re-slice + r.rules = r.rules[:len(r.rules)-len(idxs)] +} + +func (r *repository) replaceRules(rules []rule.Rule) { + for _, updated := range rules { + for idx, existing := range r.rules { + if existing.ID() == updated.ID() { + r.rules[idx] = updated + + r.logger.Debug(). + Str("_src", existing.SrcID()). + Str("_id", existing.ID()). + Msg("Rule updated") + + break + } + } + } +} diff --git a/internal/rules/repository_impl_test.go b/internal/rules/repository_impl_test.go new file mode 100644 index 000000000..a87d95333 --- /dev/null +++ b/internal/rules/repository_impl_test.go @@ -0,0 +1,359 @@ +// Copyright 2022 Dimitrij Drus +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package rules + +import ( + "context" + "net/url" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules/event" + "github.com/dadrus/heimdall/internal/rules/patternmatcher" + "github.com/dadrus/heimdall/internal/rules/rule" + "github.com/dadrus/heimdall/internal/rules/rule/mocks" + "github.com/dadrus/heimdall/internal/x" +) + +func TestRepositoryAddAndRemoveRulesFromSameRuleSet(t *testing.T) { + t.Parallel() + + // GIVEN + repo := newRepository(nil, &ruleFactory{}, *zerolog.Ctx(context.Background())) + + // WHEN + repo.addRuleSet("bar", []rule.Rule{ + &ruleImpl{id: "1", srcID: "bar"}, + &ruleImpl{id: "2", srcID: "bar"}, + &ruleImpl{id: "3", srcID: "bar"}, + &ruleImpl{id: "4", srcID: "bar"}, + }) + + // THEN + assert.Len(t, repo.rules, 4) + + // WHEN + repo.deleteRuleSet("bar") + + // THEN + assert.Len(t, repo.rules, 0) +} + +func TestRepositoryFindRule(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + requestURL *url.URL + addRules func(t *testing.T, repo *repository) + configureFactory func(t *testing.T, factory *mocks.FactoryMock) + assert func(t *testing.T, err error, rul rule.Rule) + }{ + { + uc: "no matching rule without default rule", + requestURL: &url.URL{Scheme: "http", Host: "foo.bar", Path: "baz"}, + assert: func(t *testing.T, err error, rul rule.Rule) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, heimdall.ErrNoRuleFound) + }, + }, + { + uc: "no matching rule with default rule", + requestURL: &url.URL{Scheme: "http", Host: "foo.bar", Path: "baz"}, + configureFactory: func(t *testing.T, factory *mocks.FactoryMock) { + t.Helper() + + factory.EXPECT().DefaultRule().Return(&ruleImpl{id: "test", isDefault: true}) + }, + assert: func(t *testing.T, err error, rul rule.Rule) { + t.Helper() + + require.NoError(t, err) + require.Equal(t, &ruleImpl{id: "test", isDefault: true}, rul) + }, + }, + { + uc: "matching rule", + requestURL: &url.URL{Scheme: "http", Host: "foo.bar", Path: "baz"}, + addRules: func(t *testing.T, repo *repository) { + t.Helper() + + repo.rules = append(repo.rules, + &ruleImpl{ + id: "test1", + srcID: "bar", + urlMatcher: func() patternmatcher.PatternMatcher { + matcher, _ := patternmatcher.NewPatternMatcher("glob", + "http://heimdall.test.local/baz") + + return matcher + }(), + }, + &ruleImpl{ + id: "test2", + srcID: "baz", + urlMatcher: func() patternmatcher.PatternMatcher { + matcher, _ := patternmatcher.NewPatternMatcher("glob", + "http://foo.bar/baz") + + return matcher + }(), + }, + ) + }, + assert: func(t *testing.T, err error, rul rule.Rule) { + t.Helper() + + require.NoError(t, err) + + impl, ok := rul.(*ruleImpl) + require.True(t, ok) + + require.Equal(t, "test2", impl.id) + require.Equal(t, "baz", impl.srcID) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + configureMocks := x.IfThenElse(tc.configureFactory != nil, + tc.configureFactory, + func(t *testing.T, factory *mocks.FactoryMock) { + t.Helper() + + factory.EXPECT().DefaultRule().Return(nil) + }) + + addRules := x.IfThenElse(tc.addRules != nil, + tc.addRules, + func(t *testing.T, _ *repository) { t.Helper() }) + + factory := mocks.NewFactoryMock(t) + configureMocks(t, factory) + + repo := newRepository(nil, factory, *zerolog.Ctx(context.Background())) + + addRules(t, repo) + + // WHEN + rul, err := repo.FindRule(tc.requestURL) + + // THEN + tc.assert(t, err, rul) + factory.AssertExpectations(t) + }) + } +} + +func TestRepositoryAddAndRemoveRulesFromDifferentRuleSets(t *testing.T) { + t.Parallel() + + // GIVEN + repo := newRepository(nil, &ruleFactory{}, *zerolog.Ctx(context.Background())) + + // WHEN + repo.addRules([]rule.Rule{ + &ruleImpl{id: "1", srcID: "bar"}, + &ruleImpl{id: "2", srcID: "baz"}, + &ruleImpl{id: "3", srcID: "bar"}, + &ruleImpl{id: "4", srcID: "foo"}, + }) + + // THEN + assert.Len(t, repo.rules, 4) + + // WHEN + repo.deleteRuleSet("baz") + + // THEN + assert.Len(t, repo.rules, 3) + assert.ElementsMatch(t, repo.rules, []rule.Rule{ + &ruleImpl{id: "1", srcID: "bar"}, + &ruleImpl{id: "3", srcID: "bar"}, + &ruleImpl{id: "4", srcID: "foo"}, + }) + + // WHEN + repo.deleteRuleSet("foo") + + // THEN + assert.Len(t, repo.rules, 2) + assert.ElementsMatch(t, repo.rules, []rule.Rule{ + &ruleImpl{id: "1", srcID: "bar"}, + &ruleImpl{id: "3", srcID: "bar"}, + }) + + // WHEN + repo.deleteRuleSet("bar") + + // THEN + assert.Len(t, repo.rules, 0) +} + +func TestRepositoryRuleSetLifecycleManagement(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + events []event.RuleSetChanged + assert func(t *testing.T, repo *repository) + }{ + { + uc: "empty rule set definition", + events: []event.RuleSetChanged{{Source: "test", ChangeType: event.Create}}, + assert: func(t *testing.T, repo *repository) { + t.Helper() + + assert.Len(t, repo.rules, 0) + }, + }, + { + uc: "rule set with one rule", + events: []event.RuleSetChanged{ + { + Source: "test", + ChangeType: event.Create, + Rules: []rule.Rule{&ruleImpl{id: "rule:foo", srcID: "test"}}, + }, + }, + assert: func(t *testing.T, repo *repository) { + t.Helper() + + assert.Len(t, repo.rules, 1) + assert.Equal(t, &ruleImpl{id: "rule:foo", srcID: "test"}, repo.rules[0]) + }, + }, + { + uc: "multiple rule sets", + events: []event.RuleSetChanged{ + { + Source: "test1", + ChangeType: event.Create, + Rules: []rule.Rule{&ruleImpl{id: "rule:bar", srcID: "test1"}}, + }, + { + Source: "test2", + ChangeType: event.Create, + Rules: []rule.Rule{&ruleImpl{id: "rule:foo", srcID: "test2"}}, + }, + }, + assert: func(t *testing.T, repo *repository) { + t.Helper() + + assert.Len(t, repo.rules, 2) + assert.Equal(t, &ruleImpl{id: "rule:bar", srcID: "test1"}, repo.rules[0]) + assert.Equal(t, &ruleImpl{id: "rule:foo", srcID: "test2"}, repo.rules[1]) + }, + }, + { + uc: "multiple rule sets created and one of these deleted", + events: []event.RuleSetChanged{ + { + Source: "test1", + ChangeType: event.Create, + Rules: []rule.Rule{&ruleImpl{id: "rule:bar", srcID: "test1"}}, + }, + { + Source: "test2", + ChangeType: event.Create, + Rules: []rule.Rule{&ruleImpl{id: "rule:foo", srcID: "test2"}}, + }, + { + Source: "test2", + ChangeType: event.Remove, + }, + }, + assert: func(t *testing.T, repo *repository) { + t.Helper() + + assert.Len(t, repo.rules, 1) + assert.Equal(t, &ruleImpl{id: "rule:bar", srcID: "test1"}, repo.rules[0]) + }, + }, + { + uc: "multiple rule sets created and one updated", + events: []event.RuleSetChanged{ + { + Source: "test1", + ChangeType: event.Create, + Rules: []rule.Rule{&ruleImpl{id: "rule:bar", srcID: "test1"}}, + }, + { + Source: "test2", + ChangeType: event.Create, + Rules: []rule.Rule{ + &ruleImpl{id: "rule:foo1", srcID: "test2", hash: []byte{1}}, + &ruleImpl{id: "rule:foo2", srcID: "test2", hash: []byte{2}}, + &ruleImpl{id: "rule:foo3", srcID: "test2", hash: []byte{3}}, + &ruleImpl{id: "rule:foo4", srcID: "test2", hash: []byte{4}}, + }, + }, + { + Source: "test2", + ChangeType: event.Update, + Rules: []rule.Rule{ + &ruleImpl{id: "rule:foo1", srcID: "test2", hash: []byte{5}}, // updated + &ruleImpl{id: "rule:foo2", srcID: "test2", hash: []byte{2}}, // as before + // &ruleImpl{id: "rule:foo3", srcID: "test2", hash: []byte{3}}, // deleted + &ruleImpl{id: "rule:foo4", srcID: "test2", hash: []byte{4}}, // as before + }, + }, + }, + assert: func(t *testing.T, repo *repository) { + t.Helper() + + require.Len(t, repo.rules, 4) + assert.Equal(t, &ruleImpl{id: "rule:bar", srcID: "test1"}, repo.rules[0]) + assert.Equal(t, &ruleImpl{id: "rule:foo1", srcID: "test2", hash: []byte{5}}, repo.rules[1]) + assert.Equal(t, &ruleImpl{id: "rule:foo2", srcID: "test2", hash: []byte{2}}, repo.rules[2]) + assert.Equal(t, &ruleImpl{id: "rule:foo4", srcID: "test2", hash: []byte{4}}, repo.rules[3]) + }, + }, + } { + t.Run("case="+tc.uc, func(t *testing.T) { + // GIVEN + ctx := context.Background() + + queue := make(event.RuleSetChangedEventQueue, 10) + defer close(queue) + + repo := newRepository(queue, &ruleFactory{}, log.Logger) + require.NoError(t, repo.Start(ctx)) + + // nolint: errcheck + defer repo.Stop(ctx) + + // WHEN + for _, evt := range tc.events { + queue <- evt + } + + time.Sleep(100 * time.Millisecond) + + // THEN + tc.assert(t, repo) + }) + } +} diff --git a/internal/rules/repository_test.go b/internal/rules/repository_test.go deleted file mode 100644 index 641820e76..000000000 --- a/internal/rules/repository_test.go +++ /dev/null @@ -1,468 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package rules - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/dadrus/heimdall/internal/config" - "github.com/dadrus/heimdall/internal/heimdall" - "github.com/dadrus/heimdall/internal/rules/event" - "github.com/dadrus/heimdall/internal/rules/mocks" - "github.com/dadrus/heimdall/internal/rules/patternmatcher" - "github.com/dadrus/heimdall/internal/rules/rule" - "github.com/dadrus/heimdall/internal/x" - "github.com/dadrus/heimdall/internal/x/testsupport" -) - -func TestRepositoryAddAndRemoveRulesFromSameRuleSet(t *testing.T) { - t.Parallel() - - // GIVEN - r, err := NewRepository(nil, nil, *zerolog.Ctx(context.Background())) - require.NoError(t, err) - - repo, ok := r.(*repository) - require.True(t, ok) - - // WHEN - repo.addRule(&ruleImpl{id: "1", srcID: "bar"}) - repo.addRule(&ruleImpl{id: "2", srcID: "bar"}) - repo.addRule(&ruleImpl{id: "3", srcID: "bar"}) - repo.addRule(&ruleImpl{id: "4", srcID: "bar"}) - - // THEN - assert.Len(t, repo.rules, 4) - - // WHEN - repo.removeRules("bar") - - // THEN - assert.Len(t, repo.rules, 0) -} - -func TestRepositoryFindRule(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - uc string - requestURL *url.URL - addRules func(t *testing.T, repo *repository) - configureMocks func(t *testing.T, factory *mocks.MockRuleFactory) - assert func(t *testing.T, err error, rul rule.Rule) - }{ - { - uc: "no matching rule without default rule", - requestURL: &url.URL{Scheme: "http", Host: "foo.bar", Path: "baz"}, - configureMocks: func(t *testing.T, factory *mocks.MockRuleFactory) { - t.Helper() - - factory.On("HasDefaultRule").Return(false) - }, - assert: func(t *testing.T, err error, rul rule.Rule) { - t.Helper() - - require.Error(t, err) - assert.ErrorIs(t, err, heimdall.ErrNoRuleFound) - }, - }, - { - uc: "no matching rule with default rule", - requestURL: &url.URL{Scheme: "http", Host: "foo.bar", Path: "baz"}, - configureMocks: func(t *testing.T, factory *mocks.MockRuleFactory) { - t.Helper() - - factory.On("HasDefaultRule").Return(true) - factory.On("DefaultRule").Return(&ruleImpl{id: "test", srcID: "baz"}) - }, - assert: func(t *testing.T, err error, rul rule.Rule) { - t.Helper() - - require.NoError(t, err) - require.Equal(t, &ruleImpl{id: "test", srcID: "baz"}, rul) - }, - }, - { - uc: "matching rule", - requestURL: &url.URL{Scheme: "http", Host: "foo.bar", Path: "baz"}, - addRules: func(t *testing.T, repo *repository) { - t.Helper() - - repo.rules = append(repo.rules, - &ruleImpl{ - id: "test1", - srcID: "bar", - urlMatcher: func() patternmatcher.PatternMatcher { - matcher, _ := patternmatcher.NewPatternMatcher("glob", - "http://heimdall.test.local/baz") - - return matcher - }(), - }, - &ruleImpl{ - id: "test2", - srcID: "baz", - urlMatcher: func() patternmatcher.PatternMatcher { - matcher, _ := patternmatcher.NewPatternMatcher("glob", - "http://foo.bar/baz") - - return matcher - }(), - }, - ) - }, - assert: func(t *testing.T, err error, rul rule.Rule) { - t.Helper() - - require.NoError(t, err) - - impl, ok := rul.(*ruleImpl) - require.True(t, ok) - - require.Equal(t, "test2", impl.id) - require.Equal(t, "baz", impl.srcID) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - configureMocks := x.IfThenElse(tc.configureMocks != nil, - tc.configureMocks, - func(t *testing.T, _ *mocks.MockRuleFactory) { t.Helper() }) - - addRules := x.IfThenElse(tc.addRules != nil, - tc.addRules, - func(t *testing.T, _ *repository) { t.Helper() }) - - factory := &mocks.MockRuleFactory{} - configureMocks(t, factory) - - r, err := NewRepository(nil, factory, *zerolog.Ctx(context.Background())) - require.NoError(t, err) - - repo, ok := r.(*repository) - require.True(t, ok) - - addRules(t, repo) - - // WHEN - rul, err := repo.FindRule(tc.requestURL) - - // THEN - tc.assert(t, err, rul) - factory.AssertExpectations(t) - }) - } -} - -func TestRepositoryAddAndRemoveRulesFromDifferentRuleSets(t *testing.T) { - t.Parallel() - - // GIVEN - r, err := NewRepository(nil, nil, *zerolog.Ctx(context.Background())) - require.NoError(t, err) - - repo, ok := r.(*repository) - require.True(t, ok) - - // WHEN - repo.addRule(&ruleImpl{id: "1", srcID: "bar"}) - repo.addRule(&ruleImpl{id: "2", srcID: "baz"}) - repo.addRule(&ruleImpl{id: "3", srcID: "bar"}) - repo.addRule(&ruleImpl{id: "4", srcID: "foo"}) - - // THEN - assert.Len(t, repo.rules, 4) - - // WHEN - repo.removeRules("baz") - - // THEN - assert.Len(t, repo.rules, 3) - assert.ElementsMatch(t, repo.rules, []rule.Rule{ - &ruleImpl{id: "1", srcID: "bar"}, - &ruleImpl{id: "3", srcID: "bar"}, - &ruleImpl{id: "4", srcID: "foo"}, - }) - - // WHEN - repo.removeRules("foo") - - // THEN - assert.Len(t, repo.rules, 2) - assert.ElementsMatch(t, repo.rules, []rule.Rule{ - &ruleImpl{id: "1", srcID: "bar"}, - &ruleImpl{id: "3", srcID: "bar"}, - }) - - // WHEN - repo.removeRules("bar") - - // THEN - assert.Len(t, repo.rules, 0) -} - -func TestRepositoryRuleSetLifecycleManagement(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - queue := make(event.RuleSetChangedEventQueue, 10) - defer close(queue) - - repo, err := NewRepository(queue, nil, log.Logger) - require.NoError(t, err) - - impl, ok := repo.(*repository) - require.True(t, ok) - - require.NoError(t, impl.Start(ctx)) - - // nolint: errcheck - defer impl.Stop(ctx) - - for _, tc := range []struct { - uc string - events []event.RuleSetChangedEvent - configureMocks func(t *testing.T, factory *mocks.MockRuleFactory) - assert func(t *testing.T, repo *repository) - }{ - { - uc: "empty rule set definition", - events: []event.RuleSetChangedEvent{{Src: "test", ChangeType: event.Create}}, - assert: func(t *testing.T, repo *repository) { - t.Helper() - - assert.Len(t, repo.rules, 0) - }, - }, - { - uc: "rule set with one rule", - events: []event.RuleSetChangedEvent{ - { - Src: "test", - ChangeType: event.Create, - RuleSet: []rule.Configuration{ - { - ID: "rule:foo", - RuleMatcher: rule.Matcher{ - URL: "http://foo.bar/<**>", - Strategy: "regex", - }, - Methods: []string{"PATCH"}, - Execute: []config.MechanismConfig{ - {"authenticator": "unauthorized_authenticator"}, - {"contextualizer": "subscription_contextualizer"}, - {"authorizer": "allow_all_authorizer"}, - {"unifier": "jwt"}, - }, - ErrorHandler: []config.MechanismConfig{ - {"error_handler": "default"}, - }, - }, - }, - }, - }, - configureMocks: func(t *testing.T, factory *mocks.MockRuleFactory) { - t.Helper() - - factory.On("CreateRule", "test", mock.MatchedBy( - func(conf rule.Configuration) bool { - assert.Equal(t, "rule:foo", conf.ID) - assert.Equal(t, "http://foo.bar/<**>", conf.RuleMatcher.URL) - assert.Equal(t, "regex", conf.RuleMatcher.Strategy) - assert.ElementsMatch(t, conf.Methods, []string{"PATCH"}) - require.Len(t, conf.Execute, 4) - require.Len(t, conf.ErrorHandler, 1) - - assert.Len(t, conf.Execute[0], 1) - assert.Equal(t, "unauthorized_authenticator", conf.Execute[0]["authenticator"]) - - assert.Len(t, conf.Execute[1], 1) - assert.Equal(t, "subscription_contextualizer", conf.Execute[1]["contextualizer"]) - - assert.Len(t, conf.Execute[2], 1) - assert.Equal(t, "allow_all_authorizer", conf.Execute[2]["authorizer"]) - - assert.Len(t, conf.Execute[3], 1) - assert.Equal(t, "jwt", conf.Execute[3]["unifier"]) - - assert.Len(t, conf.ErrorHandler[0], 1) - assert.Equal(t, "default", conf.ErrorHandler[0]["error_handler"]) - - return true - })).Return(&ruleImpl{id: "test", srcID: "test"}, nil) - }, - assert: func(t *testing.T, repo *repository) { - t.Helper() - - assert.Len(t, repo.rules, 1) - assert.Equal(t, &ruleImpl{id: "test", srcID: "test"}, repo.rules[0]) - }, - }, - { - uc: "multiple rule sets", - events: []event.RuleSetChangedEvent{ - { - Src: "test1", - ChangeType: event.Create, - RuleSet: []rule.Configuration{ - { - ID: "rule:bar", - RuleMatcher: rule.Matcher{URL: "http://bar.foo/<**>"}, - Methods: []string{"GET"}, - }, - }, - }, - { - Src: "test2", - ChangeType: event.Create, - RuleSet: []rule.Configuration{ - { - ID: "rule:foo", - RuleMatcher: rule.Matcher{URL: "http://foo.bar/<**>"}, - Methods: []string{"POST"}, - }, - }, - }, - }, - configureMocks: func(t *testing.T, factory *mocks.MockRuleFactory) { - t.Helper() - - factory.On("CreateRule", "test1", mock.Anything). - Return(&ruleImpl{id: "rule:bar", srcID: "test1"}, nil) - - factory.On("CreateRule", "test2", mock.Anything). - Return(&ruleImpl{id: "rule:foo", srcID: "test2"}, nil) - }, - assert: func(t *testing.T, repo *repository) { - t.Helper() - - assert.Len(t, repo.rules, 2) - assert.Equal(t, &ruleImpl{id: "rule:bar", srcID: "test1"}, repo.rules[0]) - assert.Equal(t, &ruleImpl{id: "rule:foo", srcID: "test2"}, repo.rules[1]) - }, - }, - { - uc: "multiple rule sets created and one of these deleted", - events: []event.RuleSetChangedEvent{ - { - Src: "test1", - ChangeType: event.Create, - RuleSet: []rule.Configuration{ - { - ID: "rule:bar", - RuleMatcher: rule.Matcher{URL: "http://bar.foo/<**>"}, - Methods: []string{"GET"}, - }, - }, - }, - { - Src: "test2", - ChangeType: event.Create, - RuleSet: []rule.Configuration{ - { - ID: "rule:foo", - RuleMatcher: rule.Matcher{URL: "http://foo.bar/<**>"}, - Methods: []string{"POST"}, - }, - }, - }, - { - Src: "test2", - ChangeType: event.Remove, - }, - }, - configureMocks: func(t *testing.T, factory *mocks.MockRuleFactory) { - t.Helper() - - factory.On("CreateRule", "test1", mock.Anything). - Return(&ruleImpl{id: "rule:bar", srcID: "test1"}, nil) - - factory.On("CreateRule", "test2", mock.Anything). - Return(&ruleImpl{id: "rule:foo", srcID: "test2"}, nil) - }, - assert: func(t *testing.T, repo *repository) { - t.Helper() - - assert.Len(t, repo.rules, 1) - assert.Equal(t, &ruleImpl{id: "rule:bar", srcID: "test1"}, repo.rules[0]) - }, - }, - { - uc: "error while creating rule", - events: []event.RuleSetChangedEvent{ - { - Src: "test", - ChangeType: event.Create, - RuleSet: []rule.Configuration{ - { - ID: "rule:bar", - RuleMatcher: rule.Matcher{URL: "http://bar.foo/<**>"}, - Methods: []string{"GET"}, - }, - }, - }, - }, - configureMocks: func(t *testing.T, factory *mocks.MockRuleFactory) { - t.Helper() - - factory.On("CreateRule", "test", mock.Anything). - Return(nil, testsupport.ErrTestPurpose) - }, - assert: func(t *testing.T, repo *repository) { - t.Helper() - - assert.Len(t, repo.rules, 0) - }, - }, - } { - t.Run("case="+tc.uc, func(t *testing.T) { - // GIVEN - impl.rules = make([]rule.Rule, defaultRuleListSize) - - configureMocks := x.IfThenElse(tc.configureMocks != nil, - tc.configureMocks, - func(t *testing.T, factory *mocks.MockRuleFactory) { t.Helper() }) - - factory := &mocks.MockRuleFactory{} - configureMocks(t, factory) - - impl.rf = factory - - // WHEN - for _, evt := range tc.events { - queue <- evt - } - - time.Sleep(100 * time.Millisecond) - - // THEN - tc.assert(t, impl) - factory.AssertExpectations(t) - }) - } -} diff --git a/internal/rules/provider/cloudblob/rule_set.go b/internal/rules/rule/factory.go similarity index 72% rename from internal/rules/provider/cloudblob/rule_set.go rename to internal/rules/rule/factory.go index c8dadb6b4..8c172ef46 100644 --- a/internal/rules/provider/cloudblob/rule_set.go +++ b/internal/rules/rule/factory.go @@ -14,17 +14,13 @@ // // SPDX-License-Identifier: Apache-2.0 -package cloudblob +package rule -import ( - "time" +import "github.com/dadrus/heimdall/internal/rules/config" - "github.com/dadrus/heimdall/internal/rules/rule" -) +//go:generate mockery --name Factory --structname FactoryMock -type RuleSet struct { - Rules []rule.Configuration - Hash []byte - Key string - ModTime time.Time +type Factory interface { + CreateRule(version, srcID string, ruleConfig config.Rule) (Rule, error) + DefaultRule() Rule } diff --git a/internal/rules/rule/mocks/factory.go b/internal/rules/rule/mocks/factory.go new file mode 100644 index 000000000..fea028cf8 --- /dev/null +++ b/internal/rules/rule/mocks/factory.go @@ -0,0 +1,137 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + config "github.com/dadrus/heimdall/internal/rules/config" + mock "github.com/stretchr/testify/mock" + + rule "github.com/dadrus/heimdall/internal/rules/rule" +) + +// FactoryMock is an autogenerated mock type for the Factory type +type FactoryMock struct { + mock.Mock +} + +type FactoryMock_Expecter struct { + mock *mock.Mock +} + +func (_m *FactoryMock) EXPECT() *FactoryMock_Expecter { + return &FactoryMock_Expecter{mock: &_m.Mock} +} + +// CreateRule provides a mock function with given fields: version, srcID, ruleConfig +func (_m *FactoryMock) CreateRule(version string, srcID string, ruleConfig config.Rule) (rule.Rule, error) { + ret := _m.Called(version, srcID, ruleConfig) + + var r0 rule.Rule + var r1 error + if rf, ok := ret.Get(0).(func(string, string, config.Rule) (rule.Rule, error)); ok { + return rf(version, srcID, ruleConfig) + } + if rf, ok := ret.Get(0).(func(string, string, config.Rule) rule.Rule); ok { + r0 = rf(version, srcID, ruleConfig) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rule.Rule) + } + } + + if rf, ok := ret.Get(1).(func(string, string, config.Rule) error); ok { + r1 = rf(version, srcID, ruleConfig) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FactoryMock_CreateRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRule' +type FactoryMock_CreateRule_Call struct { + *mock.Call +} + +// CreateRule is a helper method to define mock.On call +// - version string +// - srcID string +// - ruleConfig config.Rule +func (_e *FactoryMock_Expecter) CreateRule(version interface{}, srcID interface{}, ruleConfig interface{}) *FactoryMock_CreateRule_Call { + return &FactoryMock_CreateRule_Call{Call: _e.mock.On("CreateRule", version, srcID, ruleConfig)} +} + +func (_c *FactoryMock_CreateRule_Call) Run(run func(version string, srcID string, ruleConfig config.Rule)) *FactoryMock_CreateRule_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(config.Rule)) + }) + return _c +} + +func (_c *FactoryMock_CreateRule_Call) Return(_a0 rule.Rule, _a1 error) *FactoryMock_CreateRule_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FactoryMock_CreateRule_Call) RunAndReturn(run func(string, string, config.Rule) (rule.Rule, error)) *FactoryMock_CreateRule_Call { + _c.Call.Return(run) + return _c +} + +// DefaultRule provides a mock function with given fields: +func (_m *FactoryMock) DefaultRule() rule.Rule { + ret := _m.Called() + + var r0 rule.Rule + if rf, ok := ret.Get(0).(func() rule.Rule); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rule.Rule) + } + } + + return r0 +} + +// FactoryMock_DefaultRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DefaultRule' +type FactoryMock_DefaultRule_Call struct { + *mock.Call +} + +// DefaultRule is a helper method to define mock.On call +func (_e *FactoryMock_Expecter) DefaultRule() *FactoryMock_DefaultRule_Call { + return &FactoryMock_DefaultRule_Call{Call: _e.mock.On("DefaultRule")} +} + +func (_c *FactoryMock_DefaultRule_Call) Run(run func()) *FactoryMock_DefaultRule_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *FactoryMock_DefaultRule_Call) Return(_a0 rule.Rule) *FactoryMock_DefaultRule_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *FactoryMock_DefaultRule_Call) RunAndReturn(run func() rule.Rule) *FactoryMock_DefaultRule_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewFactoryMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewFactoryMock creates a new instance of FactoryMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFactoryMock(t mockConstructorTestingTNewFactoryMock) *FactoryMock { + mock := &FactoryMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/rule/mocks/repository.go b/internal/rules/rule/mocks/repository.go new file mode 100644 index 000000000..d59c4edea --- /dev/null +++ b/internal/rules/rule/mocks/repository.go @@ -0,0 +1,92 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + url "net/url" + + rule "github.com/dadrus/heimdall/internal/rules/rule" + mock "github.com/stretchr/testify/mock" +) + +// RepositoryMock is an autogenerated mock type for the Repository type +type RepositoryMock struct { + mock.Mock +} + +type RepositoryMock_Expecter struct { + mock *mock.Mock +} + +func (_m *RepositoryMock) EXPECT() *RepositoryMock_Expecter { + return &RepositoryMock_Expecter{mock: &_m.Mock} +} + +// FindRule provides a mock function with given fields: _a0 +func (_m *RepositoryMock) FindRule(_a0 *url.URL) (rule.Rule, error) { + ret := _m.Called(_a0) + + var r0 rule.Rule + var r1 error + if rf, ok := ret.Get(0).(func(*url.URL) (rule.Rule, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*url.URL) rule.Rule); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rule.Rule) + } + } + + if rf, ok := ret.Get(1).(func(*url.URL) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RepositoryMock_FindRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRule' +type RepositoryMock_FindRule_Call struct { + *mock.Call +} + +// FindRule is a helper method to define mock.On call +// - _a0 *url.URL +func (_e *RepositoryMock_Expecter) FindRule(_a0 interface{}) *RepositoryMock_FindRule_Call { + return &RepositoryMock_FindRule_Call{Call: _e.mock.On("FindRule", _a0)} +} + +func (_c *RepositoryMock_FindRule_Call) Run(run func(_a0 *url.URL)) *RepositoryMock_FindRule_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*url.URL)) + }) + return _c +} + +func (_c *RepositoryMock_FindRule_Call) Return(_a0 rule.Rule, _a1 error) *RepositoryMock_FindRule_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RepositoryMock_FindRule_Call) RunAndReturn(run func(*url.URL) (rule.Rule, error)) *RepositoryMock_FindRule_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewRepositoryMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewRepositoryMock creates a new instance of RepositoryMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRepositoryMock(t mockConstructorTestingTNewRepositoryMock) *RepositoryMock { + mock := &RepositoryMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/rule/mocks/rule.go b/internal/rules/rule/mocks/rule.go new file mode 100644 index 000000000..2763701b7 --- /dev/null +++ b/internal/rules/rule/mocks/rule.go @@ -0,0 +1,258 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + heimdall "github.com/dadrus/heimdall/internal/heimdall" + mock "github.com/stretchr/testify/mock" + + url "net/url" +) + +// RuleMock is an autogenerated mock type for the Rule type +type RuleMock struct { + mock.Mock +} + +type RuleMock_Expecter struct { + mock *mock.Mock +} + +func (_m *RuleMock) EXPECT() *RuleMock_Expecter { + return &RuleMock_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: _a0 +func (_m *RuleMock) Execute(_a0 heimdall.Context) (*url.URL, error) { + ret := _m.Called(_a0) + + var r0 *url.URL + var r1 error + if rf, ok := ret.Get(0).(func(heimdall.Context) (*url.URL, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(heimdall.Context) *url.URL); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*url.URL) + } + } + + if rf, ok := ret.Get(1).(func(heimdall.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RuleMock_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type RuleMock_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 heimdall.Context +func (_e *RuleMock_Expecter) Execute(_a0 interface{}) *RuleMock_Execute_Call { + return &RuleMock_Execute_Call{Call: _e.mock.On("Execute", _a0)} +} + +func (_c *RuleMock_Execute_Call) Run(run func(_a0 heimdall.Context)) *RuleMock_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(heimdall.Context)) + }) + return _c +} + +func (_c *RuleMock_Execute_Call) Return(_a0 *url.URL, _a1 error) *RuleMock_Execute_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RuleMock_Execute_Call) RunAndReturn(run func(heimdall.Context) (*url.URL, error)) *RuleMock_Execute_Call { + _c.Call.Return(run) + return _c +} + +// ID provides a mock function with given fields: +func (_m *RuleMock) ID() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// RuleMock_ID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ID' +type RuleMock_ID_Call struct { + *mock.Call +} + +// ID is a helper method to define mock.On call +func (_e *RuleMock_Expecter) ID() *RuleMock_ID_Call { + return &RuleMock_ID_Call{Call: _e.mock.On("ID")} +} + +func (_c *RuleMock_ID_Call) Run(run func()) *RuleMock_ID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *RuleMock_ID_Call) Return(_a0 string) *RuleMock_ID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RuleMock_ID_Call) RunAndReturn(run func() string) *RuleMock_ID_Call { + _c.Call.Return(run) + return _c +} + +// MatchesMethod provides a mock function with given fields: _a0 +func (_m *RuleMock) MatchesMethod(_a0 string) bool { + ret := _m.Called(_a0) + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// RuleMock_MatchesMethod_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MatchesMethod' +type RuleMock_MatchesMethod_Call struct { + *mock.Call +} + +// MatchesMethod is a helper method to define mock.On call +// - _a0 string +func (_e *RuleMock_Expecter) MatchesMethod(_a0 interface{}) *RuleMock_MatchesMethod_Call { + return &RuleMock_MatchesMethod_Call{Call: _e.mock.On("MatchesMethod", _a0)} +} + +func (_c *RuleMock_MatchesMethod_Call) Run(run func(_a0 string)) *RuleMock_MatchesMethod_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *RuleMock_MatchesMethod_Call) Return(_a0 bool) *RuleMock_MatchesMethod_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RuleMock_MatchesMethod_Call) RunAndReturn(run func(string) bool) *RuleMock_MatchesMethod_Call { + _c.Call.Return(run) + return _c +} + +// MatchesURL provides a mock function with given fields: _a0 +func (_m *RuleMock) MatchesURL(_a0 *url.URL) bool { + ret := _m.Called(_a0) + + var r0 bool + if rf, ok := ret.Get(0).(func(*url.URL) bool); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// RuleMock_MatchesURL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MatchesURL' +type RuleMock_MatchesURL_Call struct { + *mock.Call +} + +// MatchesURL is a helper method to define mock.On call +// - _a0 *url.URL +func (_e *RuleMock_Expecter) MatchesURL(_a0 interface{}) *RuleMock_MatchesURL_Call { + return &RuleMock_MatchesURL_Call{Call: _e.mock.On("MatchesURL", _a0)} +} + +func (_c *RuleMock_MatchesURL_Call) Run(run func(_a0 *url.URL)) *RuleMock_MatchesURL_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*url.URL)) + }) + return _c +} + +func (_c *RuleMock_MatchesURL_Call) Return(_a0 bool) *RuleMock_MatchesURL_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RuleMock_MatchesURL_Call) RunAndReturn(run func(*url.URL) bool) *RuleMock_MatchesURL_Call { + _c.Call.Return(run) + return _c +} + +// SrcID provides a mock function with given fields: +func (_m *RuleMock) SrcID() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// RuleMock_SrcID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SrcID' +type RuleMock_SrcID_Call struct { + *mock.Call +} + +// SrcID is a helper method to define mock.On call +func (_e *RuleMock_Expecter) SrcID() *RuleMock_SrcID_Call { + return &RuleMock_SrcID_Call{Call: _e.mock.On("SrcID")} +} + +func (_c *RuleMock_SrcID_Call) Run(run func()) *RuleMock_SrcID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *RuleMock_SrcID_Call) Return(_a0 string) *RuleMock_SrcID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RuleMock_SrcID_Call) RunAndReturn(run func() string) *RuleMock_SrcID_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewRuleMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewRuleMock creates a new instance of RuleMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRuleMock(t mockConstructorTestingTNewRuleMock) *RuleMock { + mock := &RuleMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/rule/mocks/rule_mock.go b/internal/rules/rule/mocks/rule_mock.go deleted file mode 100644 index 57f585fed..000000000 --- a/internal/rules/rule/mocks/rule_mock.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package mocks - -import ( - "net/url" - - "github.com/stretchr/testify/mock" - - "github.com/dadrus/heimdall/internal/heimdall" -) - -type MockRule struct { - mock.Mock -} - -func (m *MockRule) ID() string { return m.Called().String(0) } -func (m *MockRule) SrcID() string { return m.Called().String(0) } -func (m *MockRule) MatchesMethod(method string) bool { return m.Called(method).Bool(0) } -func (m *MockRule) MatchesURL(reqURL *url.URL) bool { return m.Called(reqURL).Bool(0) } - -func (m *MockRule) Execute(ctx heimdall.Context) (*url.URL, error) { - args := m.Called(ctx) - - if val := args.Get(0); val != nil { - return val.(*url.URL), nil // nolint: forcetypeassert - } - - return nil, args.Error(1) -} diff --git a/internal/rules/rule/mocks/set_processor.go b/internal/rules/rule/mocks/set_processor.go new file mode 100644 index 000000000..f8b4b25c3 --- /dev/null +++ b/internal/rules/rule/mocks/set_processor.go @@ -0,0 +1,162 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + config "github.com/dadrus/heimdall/internal/rules/config" + mock "github.com/stretchr/testify/mock" +) + +// RuleSetProcessorMock is an autogenerated mock type for the SetProcessor type +type RuleSetProcessorMock struct { + mock.Mock +} + +type RuleSetProcessorMock_Expecter struct { + mock *mock.Mock +} + +func (_m *RuleSetProcessorMock) EXPECT() *RuleSetProcessorMock_Expecter { + return &RuleSetProcessorMock_Expecter{mock: &_m.Mock} +} + +// OnCreated provides a mock function with given fields: ruleSet +func (_m *RuleSetProcessorMock) OnCreated(ruleSet *config.RuleSet) error { + ret := _m.Called(ruleSet) + + var r0 error + if rf, ok := ret.Get(0).(func(*config.RuleSet) error); ok { + r0 = rf(ruleSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RuleSetProcessorMock_OnCreated_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnCreated' +type RuleSetProcessorMock_OnCreated_Call struct { + *mock.Call +} + +// OnCreated is a helper method to define mock.On call +// - ruleSet *config.RuleSet +func (_e *RuleSetProcessorMock_Expecter) OnCreated(ruleSet interface{}) *RuleSetProcessorMock_OnCreated_Call { + return &RuleSetProcessorMock_OnCreated_Call{Call: _e.mock.On("OnCreated", ruleSet)} +} + +func (_c *RuleSetProcessorMock_OnCreated_Call) Run(run func(ruleSet *config.RuleSet)) *RuleSetProcessorMock_OnCreated_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*config.RuleSet)) + }) + return _c +} + +func (_c *RuleSetProcessorMock_OnCreated_Call) Return(_a0 error) *RuleSetProcessorMock_OnCreated_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RuleSetProcessorMock_OnCreated_Call) RunAndReturn(run func(*config.RuleSet) error) *RuleSetProcessorMock_OnCreated_Call { + _c.Call.Return(run) + return _c +} + +// OnDeleted provides a mock function with given fields: ruleSet +func (_m *RuleSetProcessorMock) OnDeleted(ruleSet *config.RuleSet) error { + ret := _m.Called(ruleSet) + + var r0 error + if rf, ok := ret.Get(0).(func(*config.RuleSet) error); ok { + r0 = rf(ruleSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RuleSetProcessorMock_OnDeleted_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnDeleted' +type RuleSetProcessorMock_OnDeleted_Call struct { + *mock.Call +} + +// OnDeleted is a helper method to define mock.On call +// - ruleSet *config.RuleSet +func (_e *RuleSetProcessorMock_Expecter) OnDeleted(ruleSet interface{}) *RuleSetProcessorMock_OnDeleted_Call { + return &RuleSetProcessorMock_OnDeleted_Call{Call: _e.mock.On("OnDeleted", ruleSet)} +} + +func (_c *RuleSetProcessorMock_OnDeleted_Call) Run(run func(ruleSet *config.RuleSet)) *RuleSetProcessorMock_OnDeleted_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*config.RuleSet)) + }) + return _c +} + +func (_c *RuleSetProcessorMock_OnDeleted_Call) Return(_a0 error) *RuleSetProcessorMock_OnDeleted_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RuleSetProcessorMock_OnDeleted_Call) RunAndReturn(run func(*config.RuleSet) error) *RuleSetProcessorMock_OnDeleted_Call { + _c.Call.Return(run) + return _c +} + +// OnUpdated provides a mock function with given fields: ruleSet +func (_m *RuleSetProcessorMock) OnUpdated(ruleSet *config.RuleSet) error { + ret := _m.Called(ruleSet) + + var r0 error + if rf, ok := ret.Get(0).(func(*config.RuleSet) error); ok { + r0 = rf(ruleSet) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RuleSetProcessorMock_OnUpdated_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnUpdated' +type RuleSetProcessorMock_OnUpdated_Call struct { + *mock.Call +} + +// OnUpdated is a helper method to define mock.On call +// - ruleSet *config.RuleSet +func (_e *RuleSetProcessorMock_Expecter) OnUpdated(ruleSet interface{}) *RuleSetProcessorMock_OnUpdated_Call { + return &RuleSetProcessorMock_OnUpdated_Call{Call: _e.mock.On("OnUpdated", ruleSet)} +} + +func (_c *RuleSetProcessorMock_OnUpdated_Call) Run(run func(ruleSet *config.RuleSet)) *RuleSetProcessorMock_OnUpdated_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*config.RuleSet)) + }) + return _c +} + +func (_c *RuleSetProcessorMock_OnUpdated_Call) Return(_a0 error) *RuleSetProcessorMock_OnUpdated_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RuleSetProcessorMock_OnUpdated_Call) RunAndReturn(run func(*config.RuleSet) error) *RuleSetProcessorMock_OnUpdated_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewRuleSetProcessorMock interface { + mock.TestingT + Cleanup(func()) +} + +// NewRuleSetProcessorMock creates a new instance of RuleSetProcessorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRuleSetProcessorMock(t mockConstructorTestingTNewRuleSetProcessorMock) *RuleSetProcessorMock { + mock := &RuleSetProcessorMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/rules/provider/httpendpoint/rule_set.go b/internal/rules/rule/repository.go similarity index 79% rename from internal/rules/provider/httpendpoint/rule_set.go rename to internal/rules/rule/repository.go index b2772765e..6d5cdacdf 100644 --- a/internal/rules/provider/httpendpoint/rule_set.go +++ b/internal/rules/rule/repository.go @@ -14,11 +14,14 @@ // // SPDX-License-Identifier: Apache-2.0 -package httpendpoint +package rule -import "github.com/dadrus/heimdall/internal/rules/rule" +import ( + "net/url" +) -type RuleSet struct { - Rules []rule.Configuration - Hash []byte +//go:generate mockery --name Repository --structname RepositoryMock + +type Repository interface { + FindRule(*url.URL) (Rule, error) } diff --git a/internal/rules/rule/rule.go b/internal/rules/rule/rule.go index 361c8135f..7fe8d783d 100644 --- a/internal/rules/rule/rule.go +++ b/internal/rules/rule/rule.go @@ -22,6 +22,8 @@ import ( "github.com/dadrus/heimdall/internal/heimdall" ) +//go:generate mockery --name Rule --structname RuleMock + type Rule interface { ID() string SrcID() string diff --git a/internal/rules/rule/ruleset_processor.go b/internal/rules/rule/ruleset_processor.go new file mode 100644 index 000000000..8d28a7df1 --- /dev/null +++ b/internal/rules/rule/ruleset_processor.go @@ -0,0 +1,11 @@ +package rule + +import "github.com/dadrus/heimdall/internal/rules/config" + +//go:generate mockery --name SetProcessor --structname RuleSetProcessorMock + +type SetProcessor interface { + OnCreated(ruleSet *config.RuleSet) error + OnUpdated(ruleSet *config.RuleSet) error + OnDeleted(ruleSet *config.RuleSet) error +} diff --git a/internal/rules/rule_factory.go b/internal/rules/rule_factory_impl.go similarity index 77% rename from internal/rules/rule_factory.go rename to internal/rules/rule_factory_impl.go index 1cbb1f79d..2366a1287 100644 --- a/internal/rules/rule_factory.go +++ b/internal/rules/rule_factory_impl.go @@ -1,29 +1,16 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - package rules import ( + "crypto" "fmt" "net/url" + "github.com/goccy/go-json" "github.com/rs/zerolog" "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" + config2 "github.com/dadrus/heimdall/internal/rules/config" "github.com/dadrus/heimdall/internal/rules/mechanisms" "github.com/dadrus/heimdall/internal/rules/patternmatcher" "github.com/dadrus/heimdall/internal/rules/rule" @@ -31,15 +18,7 @@ import ( "github.com/dadrus/heimdall/internal/x/errorchain" ) -type RuleFactory interface { - CreateRule(srcID string, ruleConfig rule.Configuration) (rule.Rule, error) - HasDefaultRule() bool - DefaultRule() rule.Rule -} - -func NewRuleFactory(hf mechanisms.Factory, conf *config.Configuration, logger zerolog.Logger) ( - RuleFactory, error, -) { +func NewRuleFactory(hf mechanisms.Factory, conf *config.Configuration, logger zerolog.Logger) (rule.Factory, error) { logger.Debug().Msg("Creating rule factory") rf := &ruleFactory{hf: hf, hasDefaultRule: false, logger: logger} @@ -62,6 +41,7 @@ type ruleFactory struct { // nolint: gocognit, cyclop func (f *ruleFactory) createExecutePipeline( + version string, pipeline []config.MechanismConfig, ) (compositeSubjectCreator, compositeSubjectHandler, compositeSubjectHandler, error) { var ( @@ -78,7 +58,7 @@ func (f *ruleFactory) createExecutePipeline( "an authenticator is defined after some other non authenticator type") } - authenticator, err := f.hf.CreateAuthenticator(id.(string), f.getConfig(pipelineStep["config"])) + authenticator, err := f.hf.CreateAuthenticator(version, id.(string), f.getConfig(pipelineStep["config"])) if err != nil { return nil, nil, nil, err } @@ -95,7 +75,7 @@ func (f *ruleFactory) createExecutePipeline( "at least one unifier is defined before an authorizer") } - authorizer, err := f.hf.CreateAuthorizer(id.(string), f.getConfig(pipelineStep["config"])) + authorizer, err := f.hf.CreateAuthorizer(version, id.(string), f.getConfig(pipelineStep["config"])) if err != nil { return nil, nil, nil, err } @@ -112,7 +92,7 @@ func (f *ruleFactory) createExecutePipeline( "at least one unifier is defined before a contextualizer") } - contextualizer, err := f.hf.CreateContextualizer(id.(string), f.getConfig(pipelineStep["config"])) + contextualizer, err := f.hf.CreateContextualizer(version, id.(string), f.getConfig(pipelineStep["config"])) if err != nil { return nil, nil, nil, err } @@ -124,7 +104,7 @@ func (f *ruleFactory) createExecutePipeline( id, found = pipelineStep["unifier"] if found { - unifier, err := f.hf.CreateUnifier(id.(string), f.getConfig(pipelineStep["config"])) + unifier, err := f.hf.CreateUnifier(version, id.(string), f.getConfig(pipelineStep["config"])) if err != nil { return nil, nil, nil, err } @@ -159,11 +139,7 @@ func (f *ruleFactory) DefaultRule() rule.Rule { return f.defaultRule } -func (f *ruleFactory) HasDefaultRule() bool { - return f.hasDefaultRule -} - -func (f *ruleFactory) CreateRule(srcID string, ruleConfig rule.Configuration) ( // nolint: cyclop +func (f *ruleFactory) CreateRule(version, srcID string, ruleConfig config2.Rule) ( // nolint: cyclop rule.Rule, error, ) { if len(ruleConfig.ID) == 0 { @@ -171,19 +147,13 @@ func (f *ruleFactory) CreateRule(srcID string, ruleConfig rule.Configuration) ( "no ID defined for rule ID=%s from %s", ruleConfig.ID, srcID) } - if len(ruleConfig.RuleMatcher.URL) == 0 { - return nil, errorchain.NewWithMessagef(heimdall.ErrConfiguration, - "no URL defined for rule ID=%s from %s", ruleConfig.ID, srcID) - } - matcher, err := patternmatcher.NewPatternMatcher( ruleConfig.RuleMatcher.Strategy, ruleConfig.RuleMatcher.URL, ) if err != nil { return nil, errorchain.NewWithMessagef(heimdall.ErrConfiguration, "bad URL pattern for %s strategy defined for rule ID=%s from %s", - ruleConfig.RuleMatcher.Strategy, ruleConfig.ID, srcID). - CausedBy(err) + ruleConfig.RuleMatcher.Strategy, ruleConfig.ID, srcID).CausedBy(err) } var upstreamURL *url.URL @@ -192,17 +162,16 @@ func (f *ruleFactory) CreateRule(srcID string, ruleConfig rule.Configuration) ( upstreamURL, err = url.Parse(ruleConfig.Upstream) if err != nil { return nil, errorchain.NewWithMessagef(heimdall.ErrConfiguration, - "bad upstream URL defined for rule ID=%s from %s", ruleConfig.ID, srcID). - CausedBy(err) + "bad upstream URL defined for rule ID=%s from %s", ruleConfig.ID, srcID).CausedBy(err) } } - authenticators, subHandlers, unifiers, err := f.createExecutePipeline(ruleConfig.Execute) + authenticators, subHandlers, unifiers, err := f.createExecutePipeline(version, ruleConfig.Execute) if err != nil { return nil, err } - errorHandlers, err := f.createOnErrorPipeline(ruleConfig.ErrorHandler) + errorHandlers, err := f.createOnErrorPipeline(version, ruleConfig.ErrorHandler) if err != nil { return nil, err } @@ -232,6 +201,12 @@ func (f *ruleFactory) CreateRule(srcID string, ruleConfig rule.Configuration) ( "no methods defined for rule ID=%s from %s", ruleConfig.ID, srcID) } + hash, err := f.createHash(ruleConfig) + if err != nil { + return nil, errorchain.NewWithMessagef(heimdall.ErrConfiguration, + "failed to create hash for rule ID=%s from %s", ruleConfig.ID, srcID) + } + return &ruleImpl{ id: ruleConfig.ID, urlMatcher: matcher, @@ -239,6 +214,7 @@ func (f *ruleFactory) CreateRule(srcID string, ruleConfig rule.Configuration) ( methods: methods, srcID: srcID, isDefault: false, + hash: hash, sc: authenticators, sh: subHandlers, un: unifiers, @@ -246,13 +222,28 @@ func (f *ruleFactory) CreateRule(srcID string, ruleConfig rule.Configuration) ( }, nil } -func (f *ruleFactory) createOnErrorPipeline(ehConfigs []config.MechanismConfig) (compositeErrorHandler, error) { +func (f *ruleFactory) createHash(ruleConfig config2.Rule) ([]byte, error) { + rawRuleConfig, err := json.Marshal(ruleConfig) + if err != nil { + return nil, err + } + + md := crypto.SHA256.New() + md.Write(rawRuleConfig) + + return md.Sum(nil), nil +} + +func (f *ruleFactory) createOnErrorPipeline( + version string, + ehConfigs []config.MechanismConfig, +) (compositeErrorHandler, error) { var errorHandlers compositeErrorHandler for _, ehStep := range ehConfigs { id, found := ehStep["error_handler"] if found { - eh, err := f.hf.CreateErrorHandler(id.(string), f.getConfig(ehStep["config"])) + eh, err := f.hf.CreateErrorHandler(version, id.(string), f.getConfig(ehStep["config"])) if err != nil { return nil, err } @@ -278,12 +269,18 @@ func (f *ruleFactory) initWithDefaultRule(ruleConfig *config.DefaultRule, logger logger.Debug().Msg("Loading default rule") - authenticators, subHandlers, unifiers, err := f.createExecutePipeline(ruleConfig.Execute) + authenticators, subHandlers, unifiers, err := f.createExecutePipeline( + config2.CurrentRuleSetVersion, + ruleConfig.Execute, + ) if err != nil { return err } - errorHandlers, err := f.createOnErrorPipeline(ruleConfig.ErrorHandler) + errorHandlers, err := f.createOnErrorPipeline( + config2.CurrentRuleSetVersion, + ruleConfig.ErrorHandler, + ) if err != nil { return err } diff --git a/internal/rules/rule_factory_test.go b/internal/rules/rule_factory_impl_test.go similarity index 71% rename from internal/rules/rule_factory_test.go rename to internal/rules/rule_factory_impl_test.go index 2b52c4e5a..73e9a9db8 100644 --- a/internal/rules/rule_factory_test.go +++ b/internal/rules/rule_factory_impl_test.go @@ -26,9 +26,14 @@ import ( "github.com/dadrus/heimdall/internal/config" "github.com/dadrus/heimdall/internal/heimdall" + config2 "github.com/dadrus/heimdall/internal/rules/config" + mocks2 "github.com/dadrus/heimdall/internal/rules/mechanisms/authenticators/mocks" + mocks4 "github.com/dadrus/heimdall/internal/rules/mechanisms/authorizers/mocks" + mocks5 "github.com/dadrus/heimdall/internal/rules/mechanisms/contextualizers/mocks" + mocks6 "github.com/dadrus/heimdall/internal/rules/mechanisms/errorhandlers/mocks" mocks3 "github.com/dadrus/heimdall/internal/rules/mechanisms/mocks" + mocks7 "github.com/dadrus/heimdall/internal/rules/mechanisms/unifiers/mocks" "github.com/dadrus/heimdall/internal/rules/mocks" - event2 "github.com/dadrus/heimdall/internal/rules/rule" "github.com/dadrus/heimdall/internal/x" "github.com/dadrus/heimdall/internal/x/testsupport" ) @@ -40,7 +45,7 @@ func TestRuleFactoryNew(t *testing.T) { for _, tc := range []struct { uc string config *config.Configuration - configureMocks func(t *testing.T, mhf *mocks.MockFactory) + configureMocks func(t *testing.T, mhf *mocks3.FactoryMock) assert func(t *testing.T, err error, ruleFactory *ruleFactory) }{ { @@ -52,7 +57,6 @@ func TestRuleFactoryNew(t *testing.T) { require.NoError(t, err) require.NotNil(t, ruleFactory) - assert.False(t, ruleFactory.HasDefaultRule()) assert.Nil(t, ruleFactory.DefaultRule()) }, }, @@ -100,10 +104,10 @@ func TestRuleFactoryNew(t *testing.T) { }, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateContextualizer", "bar", mock.Anything). + mhf.EXPECT().CreateContextualizer(mock.Anything, "bar", mock.Anything). Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { @@ -124,11 +128,10 @@ func TestRuleFactoryNew(t *testing.T) { }, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateUnifier", "bar", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateUnifier(mock.Anything, "bar", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -145,11 +148,10 @@ func TestRuleFactoryNew(t *testing.T) { Execute: []config.MechanismConfig{{"authenticator": "foo"}}, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "foo", mock.Anything). - Return(nil, testsupport.ErrTestPurpose) + mhf.EXPECT().CreateAuthenticator(mock.Anything, "foo", mock.Anything).Return(nil, testsupport.ErrTestPurpose) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -168,11 +170,10 @@ func TestRuleFactoryNew(t *testing.T) { }, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateUnifier", "bar", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateUnifier(mock.Anything, "bar", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -189,11 +190,10 @@ func TestRuleFactoryNew(t *testing.T) { Execute: []config.MechanismConfig{{"authorizer": "foo"}}, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthorizer", "foo", mock.Anything). - Return(nil, testsupport.ErrTestPurpose) + mhf.EXPECT().CreateAuthorizer(mock.Anything, "foo", mock.Anything).Return(nil, testsupport.ErrTestPurpose) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -212,11 +212,10 @@ func TestRuleFactoryNew(t *testing.T) { }, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateUnifier", "bar", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateUnifier(mock.Anything, "bar", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -233,10 +232,10 @@ func TestRuleFactoryNew(t *testing.T) { Execute: []config.MechanismConfig{{"contextualizer": "foo"}}, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateContextualizer", "foo", mock.Anything). + mhf.EXPECT().CreateContextualizer(mock.Anything, "foo", mock.Anything). Return(nil, testsupport.ErrTestPurpose) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { @@ -253,11 +252,10 @@ func TestRuleFactoryNew(t *testing.T) { Execute: []config.MechanismConfig{{"unifier": "foo"}}, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateUnifier", "foo", mock.Anything). - Return(nil, testsupport.ErrTestPurpose) + mhf.EXPECT().CreateUnifier(mock.Anything, "foo", mock.Anything).Return(nil, testsupport.ErrTestPurpose) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -273,11 +271,10 @@ func TestRuleFactoryNew(t *testing.T) { ErrorHandler: []config.MechanismConfig{{"error_handler": "foo"}}, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateErrorHandler", "foo", mock.Anything). - Return(nil, testsupport.ErrTestPurpose) + mhf.EXPECT().CreateErrorHandler(mock.Anything, "foo", mock.Anything).Return(nil, testsupport.ErrTestPurpose) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -310,11 +307,10 @@ func TestRuleFactoryNew(t *testing.T) { }, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "bar", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateAuthenticator(mock.Anything, "bar", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -334,13 +330,11 @@ func TestRuleFactoryNew(t *testing.T) { }, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "bar", mock.Anything). - Return(nil, nil) - mhf.On("CreateContextualizer", "baz", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateAuthenticator(mock.Anything, "bar", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateContextualizer(mock.Anything, "baz", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -361,15 +355,12 @@ func TestRuleFactoryNew(t *testing.T) { }, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "bar", mock.Anything). - Return(nil, nil) - mhf.On("CreateContextualizer", "baz", mock.Anything). - Return(nil, nil) - mhf.On("CreateAuthorizer", "zab", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateAuthenticator(mock.Anything, "bar", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateContextualizer(mock.Anything, "baz", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateAuthorizer(mock.Anything, "zab", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -389,13 +380,11 @@ func TestRuleFactoryNew(t *testing.T) { }, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "bar", mock.Anything). - Return(nil, nil) - mhf.On("CreateUnifier", "baz", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateAuthenticator(mock.Anything, "bar", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateUnifier(mock.Anything, "baz", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() @@ -416,20 +405,17 @@ func TestRuleFactoryNew(t *testing.T) { Methods: []string{"FOO"}, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "bar", mock.Anything). - Return(nil, nil) - mhf.On("CreateUnifier", "baz", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateAuthenticator(mock.Anything, "bar", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateUnifier(mock.Anything, "baz", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() require.NoError(t, err) require.NotNil(t, ruleFactory) - assert.True(t, ruleFactory.HasDefaultRule()) assert.NotNil(t, ruleFactory.DefaultRule()) assert.Equal(t, ruleFactory.defaultRule, ruleFactory.DefaultRule()) @@ -461,28 +447,21 @@ func TestRuleFactoryNew(t *testing.T) { Methods: []string{"FOO", "BAR"}, }, }}, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "bar", mock.Anything). - Return(nil, nil) - mhf.On("CreateUnifier", "baz", mock.Anything). - Return(nil, nil) - mhf.On("CreateAuthorizer", "zab", mock.Anything). - Return(nil, nil) - mhf.On("CreateContextualizer", "foo", mock.Anything). - Return(nil, nil) - mhf.On("CreateErrorHandler", "foobar", mock.Anything). - Return(nil, nil) - mhf.On("CreateErrorHandler", "barfoo", mock.Anything). - Return(nil, nil) + mhf.EXPECT().CreateAuthenticator(mock.Anything, "bar", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateUnifier(mock.Anything, "baz", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateAuthorizer(mock.Anything, "zab", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateContextualizer(mock.Anything, "foo", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateErrorHandler(mock.Anything, "foobar", mock.Anything).Return(nil, nil) + mhf.EXPECT().CreateErrorHandler(mock.Anything, "barfoo", mock.Anything).Return(nil, nil) }, assert: func(t *testing.T, err error, ruleFactory *ruleFactory) { t.Helper() require.NoError(t, err) require.NotNil(t, ruleFactory) - assert.True(t, ruleFactory.HasDefaultRule()) assert.NotNil(t, ruleFactory.DefaultRule()) assert.Equal(t, ruleFactory.defaultRule, ruleFactory.DefaultRule()) @@ -502,9 +481,9 @@ func TestRuleFactoryNew(t *testing.T) { // GIVEN configureMocks := x.IfThenElse(tc.configureMocks != nil, tc.configureMocks, - func(t *testing.T, mhf *mocks.MockFactory) { t.Helper() }) + func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() }) - handlerFactory := &mocks.MockFactory{} + handlerFactory := mocks3.NewFactoryMock(t) configureMocks(t, handlerFactory) // WHEN @@ -523,7 +502,6 @@ func TestRuleFactoryNew(t *testing.T) { // THEN tc.assert(t, err, impl) - handlerFactory.AssertExpectations(t) }) } } @@ -534,14 +512,14 @@ func TestRuleFactoryCreateRule(t *testing.T) { for _, tc := range []struct { uc string - config event2.Configuration + config config2.Rule defaultRule *ruleImpl - configureMocks func(t *testing.T, mhf *mocks.MockFactory) + configureMocks func(t *testing.T, mhf *mocks3.FactoryMock) assert func(t *testing.T, err error, rul *ruleImpl) }{ { uc: "without default rule and with missing id", - config: event2.Configuration{}, + config: config2.Rule{}, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -552,18 +530,18 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "without default rule, with id, but without url", - config: event2.Configuration{ID: "foobar"}, + config: config2.Rule{ID: "foobar"}, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() require.Error(t, err) assert.ErrorIs(t, err, heimdall.ErrConfiguration) - assert.Contains(t, err.Error(), "no URL defined") + assert.Contains(t, err.Error(), "bad URL pattern") }, }, { uc: "without default rule, with id, but bad url pattern", - config: event2.Configuration{ID: "foobar", RuleMatcher: event2.Matcher{URL: "?>?<*??"}}, + config: config2.Rule{ID: "foobar", RuleMatcher: config2.Matcher{URL: "?>?<*??"}}, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -574,9 +552,9 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "without default rule and error in upstream url", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, Upstream: "http://[::1]:namedport", }, assert: func(t *testing.T, err error, rul *ruleImpl) { @@ -589,16 +567,15 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "with error while creating execute pipeline", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "regex"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "regex"}, Execute: []config.MechanismConfig{{"authenticator": "foo"}}, }, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "foo", mock.Anything). - Return(nil, testsupport.ErrTestPurpose) + mhf.EXPECT().CreateAuthenticator("test", "foo", mock.Anything).Return(nil, testsupport.ErrTestPurpose) }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -609,16 +586,15 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "with error while creating on_error pipeline", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, ErrorHandler: []config.MechanismConfig{{"error_handler": "foo"}}, }, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateErrorHandler", "foo", mock.Anything). - Return(nil, testsupport.ErrTestPurpose) + mhf.EXPECT().CreateErrorHandler("test", "foo", mock.Anything).Return(nil, testsupport.ErrTestPurpose) }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -629,9 +605,9 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "without default rule and without any execute configuration", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "regex"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "regex"}, }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -643,16 +619,15 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "without default rule and with only authenticator configured", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, Execute: []config.MechanismConfig{{"authenticator": "foo"}}, }, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "foo", mock.Anything). - Return(&mocks3.MockAuthenticator{}, nil) + mhf.EXPECT().CreateAuthenticator("test", "foo", mock.Anything).Return(&mocks2.AuthenticatorMock{}, nil) }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -664,21 +639,19 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "without default rule and with only authenticator and contextualizer configured", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, Execute: []config.MechanismConfig{ {"authenticator": "foo"}, {"contextualizer": "bar"}, }, }, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "foo", mock.Anything). - Return(&mocks3.MockAuthenticator{}, nil) - mhf.On("CreateContextualizer", "bar", mock.Anything). - Return(&mocks3.MockContextualizer{}, nil) + mhf.EXPECT().CreateAuthenticator("test", "foo", mock.Anything).Return(&mocks2.AuthenticatorMock{}, nil) + mhf.EXPECT().CreateContextualizer("test", "bar", mock.Anything).Return(&mocks5.ContextualizerMock{}, nil) }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -690,24 +663,21 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "without default rule and with only authenticator, contextualizer and authorizer configured", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "regex"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "regex"}, Execute: []config.MechanismConfig{ {"authenticator": "foo"}, {"contextualizer": "bar"}, {"authorizer": "baz"}, }, }, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "foo", mock.Anything). - Return(&mocks3.MockAuthenticator{}, nil) - mhf.On("CreateContextualizer", "bar", mock.Anything). - Return(&mocks3.MockContextualizer{}, nil) - mhf.On("CreateAuthorizer", "baz", mock.Anything). - Return(&mocks3.MockAuthorizer{}, nil) + mhf.EXPECT().CreateAuthenticator("test", "foo", mock.Anything).Return(&mocks2.AuthenticatorMock{}, nil) + mhf.EXPECT().CreateContextualizer("test", "bar", mock.Anything).Return(&mocks5.ContextualizerMock{}, nil) + mhf.EXPECT().CreateAuthorizer("test", "baz", mock.Anything).Return(&mocks4.AuthorizerMock{}, nil) }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -719,21 +689,19 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "without default rule and with authenticator and unifier configured, but without methods", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, Execute: []config.MechanismConfig{ {"authenticator": "foo"}, {"unifier": "bar"}, }, }, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "foo", mock.Anything). - Return(&mocks3.MockAuthenticator{}, nil) - mhf.On("CreateUnifier", "bar", mock.Anything). - Return(&mocks3.MockUnifier{}, nil) + mhf.EXPECT().CreateAuthenticator("test", "foo", mock.Anything).Return(&mocks2.AuthenticatorMock{}, nil) + mhf.EXPECT().CreateUnifier("test", "bar", mock.Anything).Return(&mocks7.UnifierMock{}, nil) }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -745,22 +713,20 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "without default rule but with minimum required configuration", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, Execute: []config.MechanismConfig{ {"authenticator": "foo"}, {"unifier": "bar"}, }, Methods: []string{"FOO", "BAR"}, }, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "foo", mock.Anything). - Return(&mocks3.MockAuthenticator{}, nil) - mhf.On("CreateUnifier", "bar", mock.Anything). - Return(&mocks3.MockUnifier{}, nil) + mhf.EXPECT().CreateAuthenticator("test", "foo", mock.Anything).Return(&mocks2.AuthenticatorMock{}, nil) + mhf.EXPECT().CreateUnifier("test", "bar", mock.Anything).Return(&mocks7.UnifierMock{}, nil) }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -781,16 +747,16 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "with default rule and with id and url only", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, }, defaultRule: &ruleImpl{ methods: []string{"FOO"}, - sc: compositeSubjectCreator{&mocks.MockSubjectCreator{}}, - sh: compositeSubjectHandler{&mocks.MockSubjectHandler{}}, - un: compositeSubjectHandler{&mocks.MockSubjectHandler{}}, - eh: compositeErrorHandler{&mocks.MockErrorHandler{}}, + sc: compositeSubjectCreator{&mocks.SubjectCreatorMock{}}, + sh: compositeSubjectHandler{&mocks.SubjectHandlerMock{}}, + un: compositeSubjectHandler{&mocks.SubjectHandlerMock{}}, + eh: compositeErrorHandler{&mocks.ErrorHandlerMock{}}, }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -811,9 +777,9 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, { uc: "with default rule and with all attributes defined by the rule itself", - config: event2.Configuration{ + config: config2.Rule{ ID: "foobar", - RuleMatcher: event2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, + RuleMatcher: config2.Matcher{URL: "http://foo.bar", Strategy: "glob"}, Upstream: "http://bar.foo", Execute: []config.MechanismConfig{ {"authenticator": "foo"}, @@ -828,24 +794,24 @@ func TestRuleFactoryCreateRule(t *testing.T) { }, defaultRule: &ruleImpl{ methods: []string{"FOO"}, - sc: compositeSubjectCreator{&mocks.MockSubjectCreator{}}, - sh: compositeSubjectHandler{&mocks.MockSubjectHandler{}}, - un: compositeSubjectHandler{&mocks.MockSubjectHandler{}}, - eh: compositeErrorHandler{&mocks.MockErrorHandler{}}, + sc: compositeSubjectCreator{&mocks.SubjectCreatorMock{}}, + sh: compositeSubjectHandler{&mocks.SubjectHandlerMock{}}, + un: compositeSubjectHandler{&mocks.SubjectHandlerMock{}}, + eh: compositeErrorHandler{&mocks.ErrorHandlerMock{}}, }, - configureMocks: func(t *testing.T, mhf *mocks.MockFactory) { + configureMocks: func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() - mhf.On("CreateAuthenticator", "foo", mock.Anything). - Return(&mocks3.MockAuthenticator{}, nil) - mhf.On("CreateContextualizer", "bar", mock.Anything). - Return(&mocks3.MockContextualizer{}, nil) - mhf.On("CreateAuthorizer", "zab", mock.Anything). - Return(&mocks3.MockAuthorizer{}, nil) - mhf.On("CreateUnifier", "baz", mock.Anything). - Return(&mocks3.MockUnifier{}, nil) - mhf.On("CreateErrorHandler", "foo", mock.Anything). - Return(&mocks3.MockErrorHandler{}, nil) + mhf.EXPECT().CreateAuthenticator("test", "foo", mock.Anything). + Return(&mocks2.AuthenticatorMock{}, nil) + mhf.EXPECT().CreateContextualizer("test", "bar", mock.Anything). + Return(&mocks5.ContextualizerMock{}, nil) + mhf.EXPECT().CreateAuthorizer("test", "zab", mock.Anything). + Return(&mocks4.AuthorizerMock{}, nil) + mhf.EXPECT().CreateUnifier("test", "baz", mock.Anything). + Return(&mocks7.UnifierMock{}, nil) + mhf.EXPECT().CreateErrorHandler("test", "foo", mock.Anything). + Return(&mocks6.ErrorHandlerMock{}, nil) }, assert: func(t *testing.T, err error, rul *ruleImpl) { t.Helper() @@ -878,9 +844,9 @@ func TestRuleFactoryCreateRule(t *testing.T) { // GIVEN configureMocks := x.IfThenElse(tc.configureMocks != nil, tc.configureMocks, - func(t *testing.T, mhf *mocks.MockFactory) { t.Helper() }) + func(t *testing.T, mhf *mocks3.FactoryMock) { t.Helper() }) - handlerFactory := &mocks.MockFactory{} + handlerFactory := mocks3.NewFactoryMock(t) configureMocks(t, handlerFactory) factory := &ruleFactory{ @@ -891,7 +857,7 @@ func TestRuleFactoryCreateRule(t *testing.T) { } // WHEN - rul, err := factory.CreateRule("test", tc.config) + rul, err := factory.CreateRule("test", "test", tc.config) // THEN var ( @@ -906,7 +872,6 @@ func TestRuleFactoryCreateRule(t *testing.T) { // THEN tc.assert(t, err, impl) - handlerFactory.AssertExpectations(t) }) } } diff --git a/internal/rules/rule_impl.go b/internal/rules/rule_impl.go index 63bd21054..ba94dbe4e 100644 --- a/internal/rules/rule_impl.go +++ b/internal/rules/rule_impl.go @@ -33,6 +33,7 @@ type ruleImpl struct { methods []string srcID string isDefault bool + hash []byte sc compositeSubjectCreator sh compositeSubjectHandler un compositeSubjectHandler diff --git a/internal/rules/rule_impl_test.go b/internal/rules/rule_impl_test.go index eee0d2a50..cc8415384 100644 --- a/internal/rules/rule_impl_test.go +++ b/internal/rules/rule_impl_test.go @@ -140,26 +140,25 @@ func TestRuleExecute(t *testing.T) { configureMocks func( t *testing.T, ctx *heimdallmocks.MockContext, - authenticator *mocks.MockSubjectCreator, - authorizer *mocks.MockSubjectHandler, - unifier *mocks.MockSubjectHandler, - errHandler *mocks.MockErrorHandler, + authenticator *mocks.SubjectCreatorMock, + authorizer *mocks.SubjectHandlerMock, + unifier *mocks.SubjectHandlerMock, + errHandler *mocks.ErrorHandlerMock, ) assert func(t *testing.T, err error, upstreamURL *url.URL) }{ { uc: "authenticator fails, but error handler succeeds", upstreamURL: &url.URL{Scheme: "http", Host: "test.local", Path: "foo"}, - configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.MockSubjectCreator, - authorizer *mocks.MockSubjectHandler, unifier *mocks.MockSubjectHandler, - errHandler *mocks.MockErrorHandler, + configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.SubjectCreatorMock, + authorizer *mocks.SubjectHandlerMock, unifier *mocks.SubjectHandlerMock, + errHandler *mocks.ErrorHandlerMock, ) { t.Helper() - authenticator.On("Execute", ctx).Return(nil, testsupport.ErrTestPurpose) - authenticator.On("IsFallbackOnErrorAllowed").Return(false) - errHandler.On("Execute", ctx, testsupport.ErrTestPurpose). - Return(true, nil) + authenticator.EXPECT().Execute(ctx).Return(nil, testsupport.ErrTestPurpose) + authenticator.EXPECT().IsFallbackOnErrorAllowed().Return(false) + errHandler.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(true, nil) }, assert: func(t *testing.T, err error, upstreamURL *url.URL) { t.Helper() @@ -171,16 +170,15 @@ func TestRuleExecute(t *testing.T) { { uc: "authenticator fails, and error handler fails", upstreamURL: &url.URL{Scheme: "http", Host: "test.local", Path: "foo"}, - configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.MockSubjectCreator, - authorizer *mocks.MockSubjectHandler, unifier *mocks.MockSubjectHandler, - errHandler *mocks.MockErrorHandler, + configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.SubjectCreatorMock, + authorizer *mocks.SubjectHandlerMock, unifier *mocks.SubjectHandlerMock, + errHandler *mocks.ErrorHandlerMock, ) { t.Helper() - authenticator.On("Execute", ctx).Return(nil, testsupport.ErrTestPurpose) - authenticator.On("IsFallbackOnErrorAllowed").Return(false) - errHandler.On("Execute", ctx, testsupport.ErrTestPurpose). - Return(true, testsupport.ErrTestPurpose2) + authenticator.EXPECT().Execute(ctx).Return(nil, testsupport.ErrTestPurpose) + authenticator.EXPECT().IsFallbackOnErrorAllowed().Return(false) + errHandler.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(true, testsupport.ErrTestPurpose2) }, assert: func(t *testing.T, err error, upstreamURL *url.URL) { t.Helper() @@ -193,19 +191,18 @@ func TestRuleExecute(t *testing.T) { { uc: "authenticator succeeds, authorizer fails, but error handler succeeds", upstreamURL: &url.URL{Scheme: "http", Host: "test.local", Path: "foo"}, - configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.MockSubjectCreator, - authorizer *mocks.MockSubjectHandler, unifier *mocks.MockSubjectHandler, - errHandler *mocks.MockErrorHandler, + configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.SubjectCreatorMock, + authorizer *mocks.SubjectHandlerMock, unifier *mocks.SubjectHandlerMock, + errHandler *mocks.ErrorHandlerMock, ) { t.Helper() sub := &subject.Subject{ID: "Foo"} - authenticator.On("Execute", ctx).Return(sub, nil) - authorizer.On("Execute", ctx, sub).Return(testsupport.ErrTestPurpose) - authorizer.On("ContinueOnError").Return(false) - errHandler.On("Execute", ctx, testsupport.ErrTestPurpose). - Return(true, nil) + authenticator.EXPECT().Execute(ctx).Return(sub, nil) + authorizer.EXPECT().Execute(ctx, sub).Return(testsupport.ErrTestPurpose) + authorizer.EXPECT().ContinueOnError().Return(false) + errHandler.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(true, nil) }, assert: func(t *testing.T, err error, upstreamURL *url.URL) { t.Helper() @@ -217,19 +214,18 @@ func TestRuleExecute(t *testing.T) { { uc: "authenticator succeeds, authorizer fails and error handler fails", upstreamURL: &url.URL{Scheme: "http", Host: "test.local", Path: "foo"}, - configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.MockSubjectCreator, - authorizer *mocks.MockSubjectHandler, unifier *mocks.MockSubjectHandler, - errHandler *mocks.MockErrorHandler, + configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.SubjectCreatorMock, + authorizer *mocks.SubjectHandlerMock, unifier *mocks.SubjectHandlerMock, + errHandler *mocks.ErrorHandlerMock, ) { t.Helper() sub := &subject.Subject{ID: "Foo"} - authenticator.On("Execute", ctx).Return(sub, nil) - authorizer.On("Execute", ctx, sub).Return(testsupport.ErrTestPurpose) - authorizer.On("ContinueOnError").Return(false) - errHandler.On("Execute", ctx, testsupport.ErrTestPurpose). - Return(true, testsupport.ErrTestPurpose2) + authenticator.EXPECT().Execute(ctx).Return(sub, nil) + authorizer.EXPECT().Execute(ctx, sub).Return(testsupport.ErrTestPurpose) + authorizer.EXPECT().ContinueOnError().Return(false) + errHandler.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(true, testsupport.ErrTestPurpose2) }, assert: func(t *testing.T, err error, upstreamURL *url.URL) { t.Helper() @@ -242,20 +238,19 @@ func TestRuleExecute(t *testing.T) { { uc: "authenticator succeeds, authorizer succeeds, unifier fails, but error handler succeeds", upstreamURL: &url.URL{Scheme: "http", Host: "test.local", Path: "foo"}, - configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.MockSubjectCreator, - authorizer *mocks.MockSubjectHandler, unifier *mocks.MockSubjectHandler, - errHandler *mocks.MockErrorHandler, + configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.SubjectCreatorMock, + authorizer *mocks.SubjectHandlerMock, unifier *mocks.SubjectHandlerMock, + errHandler *mocks.ErrorHandlerMock, ) { t.Helper() sub := &subject.Subject{ID: "Foo"} - authenticator.On("Execute", ctx).Return(sub, nil) - authorizer.On("Execute", ctx, sub).Return(nil) - unifier.On("Execute", ctx, sub).Return(testsupport.ErrTestPurpose) - unifier.On("ContinueOnError").Return(false) - errHandler.On("Execute", ctx, testsupport.ErrTestPurpose). - Return(true, nil) + authenticator.EXPECT().Execute(ctx).Return(sub, nil) + authorizer.EXPECT().Execute(ctx, sub).Return(nil) + unifier.EXPECT().Execute(ctx, sub).Return(testsupport.ErrTestPurpose) + unifier.EXPECT().ContinueOnError().Return(false) + errHandler.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(true, nil) }, assert: func(t *testing.T, err error, upstreamURL *url.URL) { t.Helper() @@ -267,20 +262,19 @@ func TestRuleExecute(t *testing.T) { { uc: "authenticator succeeds, authorizer succeeds, unifier fails and error handler fails", upstreamURL: &url.URL{Scheme: "http", Host: "test.local", Path: "foo"}, - configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.MockSubjectCreator, - authorizer *mocks.MockSubjectHandler, unifier *mocks.MockSubjectHandler, - errHandler *mocks.MockErrorHandler, + configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.SubjectCreatorMock, + authorizer *mocks.SubjectHandlerMock, unifier *mocks.SubjectHandlerMock, + errHandler *mocks.ErrorHandlerMock, ) { t.Helper() sub := &subject.Subject{ID: "Foo"} - authenticator.On("Execute", ctx).Return(sub, nil) - authorizer.On("Execute", ctx, sub).Return(nil) - unifier.On("Execute", ctx, sub).Return(testsupport.ErrTestPurpose) - unifier.On("ContinueOnError").Return(false) - errHandler.On("Execute", ctx, testsupport.ErrTestPurpose). - Return(true, testsupport.ErrTestPurpose2) + authenticator.EXPECT().Execute(ctx).Return(sub, nil) + authorizer.EXPECT().Execute(ctx, sub).Return(nil) + unifier.EXPECT().Execute(ctx, sub).Return(testsupport.ErrTestPurpose) + unifier.EXPECT().ContinueOnError().Return(false) + errHandler.EXPECT().Execute(ctx, testsupport.ErrTestPurpose).Return(true, testsupport.ErrTestPurpose2) }, assert: func(t *testing.T, err error, upstreamURL *url.URL) { t.Helper() @@ -293,17 +287,17 @@ func TestRuleExecute(t *testing.T) { { uc: "all handler succeed", upstreamURL: &url.URL{Scheme: "http", Host: "test.local", Path: "foo"}, - configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.MockSubjectCreator, - authorizer *mocks.MockSubjectHandler, unifier *mocks.MockSubjectHandler, - errHandler *mocks.MockErrorHandler, + configureMocks: func(t *testing.T, ctx *heimdallmocks.MockContext, authenticator *mocks.SubjectCreatorMock, + authorizer *mocks.SubjectHandlerMock, unifier *mocks.SubjectHandlerMock, + errHandler *mocks.ErrorHandlerMock, ) { t.Helper() sub := &subject.Subject{ID: "Foo"} - authenticator.On("Execute", ctx).Return(sub, nil) - authorizer.On("Execute", ctx, sub).Return(nil) - unifier.On("Execute", ctx, sub).Return(nil) + authenticator.EXPECT().Execute(ctx).Return(sub, nil) + authorizer.EXPECT().Execute(ctx, sub).Return(nil) + unifier.EXPECT().Execute(ctx, sub).Return(nil) }, assert: func(t *testing.T, err error, upstreamURL *url.URL) { t.Helper() @@ -318,10 +312,10 @@ func TestRuleExecute(t *testing.T) { ctx := &heimdallmocks.MockContext{} ctx.On("AppContext").Return(context.Background()) - authenticator := &mocks.MockSubjectCreator{} - authorizer := &mocks.MockSubjectHandler{} - unifier := &mocks.MockSubjectHandler{} - errHandler := &mocks.MockErrorHandler{} + authenticator := mocks.NewSubjectCreatorMock(t) + authorizer := mocks.NewSubjectHandlerMock(t) + unifier := mocks.NewSubjectHandlerMock(t) + errHandler := mocks.NewErrorHandlerMock(t) rul := &ruleImpl{ upstreamURL: tc.upstreamURL, @@ -338,10 +332,6 @@ func TestRuleExecute(t *testing.T) { // THEN tc.assert(t, err, upstreamURL) - authenticator.AssertExpectations(t) - authorizer.AssertExpectations(t) - unifier.AssertExpectations(t) - errHandler.AssertExpectations(t) }) } } diff --git a/internal/rules/rule_set_definition_loader.go b/internal/rules/rule_set_definition_loader.go deleted file mode 100644 index c31d1018d..000000000 --- a/internal/rules/rule_set_definition_loader.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2022 Dimitrij Drus -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package rules - -import "context" - -type ruleSetDefinitionLoader interface { - Start(ctx context.Context) error - Stop(ctx context.Context) error -} diff --git a/internal/rules/ruleset_processor_impl.go b/internal/rules/ruleset_processor_impl.go new file mode 100644 index 000000000..4b4d1134f --- /dev/null +++ b/internal/rules/ruleset_processor_impl.go @@ -0,0 +1,114 @@ +package rules + +import ( + "errors" + + "github.com/rs/zerolog" + + "github.com/dadrus/heimdall/internal/heimdall" + "github.com/dadrus/heimdall/internal/rules/config" + "github.com/dadrus/heimdall/internal/rules/event" + "github.com/dadrus/heimdall/internal/rules/rule" + "github.com/dadrus/heimdall/internal/x/errorchain" +) + +var ErrUnsupportedRuleSetVersion = errors.New("unsupported rule set version") + +type ruleSetProcessor struct { + q event.RuleSetChangedEventQueue + f rule.Factory + l zerolog.Logger +} + +func NewRuleSetProcessor( + queue event.RuleSetChangedEventQueue, factory rule.Factory, logger zerolog.Logger, +) rule.SetProcessor { + return &ruleSetProcessor{ + q: queue, + f: factory, + l: logger, + } +} + +func (p *ruleSetProcessor) isVersionSupported(version string) bool { + return version == config.CurrentRuleSetVersion +} + +func (p *ruleSetProcessor) loadRules(ruleSet *config.RuleSet) ([]rule.Rule, error) { + rules := make([]rule.Rule, len(ruleSet.Rules)) + + for idx, rc := range ruleSet.Rules { + rul, err := p.f.CreateRule(ruleSet.Version, ruleSet.Source, rc) + if err != nil { + return nil, errorchain.NewWithMessage(heimdall.ErrInternal, "failed loading rule").CausedBy(err) + } + + rules[idx] = rul + } + + return rules, nil +} + +func (p *ruleSetProcessor) OnCreated(ruleSet *config.RuleSet) error { + if !p.isVersionSupported(ruleSet.Version) { + return errorchain.NewWithMessage(ErrUnsupportedRuleSetVersion, ruleSet.Version) + } + + rules, err := p.loadRules(ruleSet) + if err != nil { + return err + } + + evt := event.RuleSetChanged{ + Source: ruleSet.Source, + Name: ruleSet.Name, + Rules: rules, + ChangeType: event.Create, + } + + p.sendEvent(evt) + + return nil +} + +func (p *ruleSetProcessor) OnUpdated(ruleSet *config.RuleSet) error { + if !p.isVersionSupported(ruleSet.Version) { + return errorchain.NewWithMessage(ErrUnsupportedRuleSetVersion, ruleSet.Version) + } + + rules, err := p.loadRules(ruleSet) + if err != nil { + return err + } + + evt := event.RuleSetChanged{ + Source: ruleSet.Source, + Name: ruleSet.Name, + Rules: rules, + ChangeType: event.Update, + } + + p.sendEvent(evt) + + return nil +} + +func (p *ruleSetProcessor) OnDeleted(ruleSet *config.RuleSet) error { + evt := event.RuleSetChanged{ + Source: ruleSet.Source, + Name: ruleSet.Name, + ChangeType: event.Remove, + } + + p.sendEvent(evt) + + return nil +} + +func (p *ruleSetProcessor) sendEvent(evt event.RuleSetChanged) { + p.l.Info(). + Str("_src", evt.Source). + Str("_type", evt.ChangeType.String()). + Msg("Rule set changed") + p.q <- evt +} diff --git a/internal/rules/ruleset_processor_test.go b/internal/rules/ruleset_processor_test.go new file mode 100644 index 000000000..d257627cb --- /dev/null +++ b/internal/rules/ruleset_processor_test.go @@ -0,0 +1,233 @@ +package rules + +import ( + "testing" + + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/dadrus/heimdall/internal/rules/config" + "github.com/dadrus/heimdall/internal/rules/event" + "github.com/dadrus/heimdall/internal/rules/rule/mocks" + "github.com/dadrus/heimdall/internal/x" + "github.com/dadrus/heimdall/internal/x/testsupport" +) + +func TestRuleSetProcessorOnCreated(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + ruleset *config.RuleSet + configureFactory func(t *testing.T, mhf *mocks.FactoryMock) + assert func(t *testing.T, err error, queue event.RuleSetChangedEventQueue) + }{ + { + uc: "unsupported version", + ruleset: &config.RuleSet{Version: "foo"}, + assert: func(t *testing.T, err error, _ event.RuleSetChangedEventQueue) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, ErrUnsupportedRuleSetVersion) + }, + }, + { + uc: "error while loading rule set", + ruleset: &config.RuleSet{Version: config.CurrentRuleSetVersion, Rules: []config.Rule{{ID: "foo"}}}, + configureFactory: func(t *testing.T, mhf *mocks.FactoryMock) { + t.Helper() + + mhf.EXPECT().CreateRule(mock.Anything, mock.Anything, mock.Anything). + Return(nil, testsupport.ErrTestPurpose) + }, + assert: func(t *testing.T, err error, queue event.RuleSetChangedEventQueue) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, testsupport.ErrTestPurpose) + assert.Contains(t, err.Error(), "failed loading") + }, + }, + { + uc: "successful", + ruleset: &config.RuleSet{ + MetaData: config.MetaData{Source: "test"}, + Version: config.CurrentRuleSetVersion, + Name: "foobar", + Rules: []config.Rule{{ID: "foo"}}, + }, + configureFactory: func(t *testing.T, mhf *mocks.FactoryMock) { + t.Helper() + + mhf.EXPECT().CreateRule(config.CurrentRuleSetVersion, mock.Anything, mock.Anything).Return(&mocks.RuleMock{}, nil) + }, + assert: func(t *testing.T, err error, queue event.RuleSetChangedEventQueue) { + t.Helper() + + require.NoError(t, err) + require.Len(t, queue, 1) + + evt := <-queue + require.Len(t, evt.Rules, 1) + assert.Equal(t, event.Create, evt.ChangeType) + assert.Equal(t, "test", evt.Source) + assert.Equal(t, "foobar", evt.Name) + + assert.Equal(t, &mocks.RuleMock{}, evt.Rules[0]) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEM + configureFactory := x.IfThenElse(tc.configureFactory != nil, + tc.configureFactory, + func(t *testing.T, mhf *mocks.FactoryMock) { t.Helper() }) + + queue := make(event.RuleSetChangedEventQueue, 10) + + factory := mocks.NewFactoryMock(t) + configureFactory(t, factory) + + processor := NewRuleSetProcessor(queue, factory, log.Logger) + + // WHEN + err := processor.OnCreated(tc.ruleset) + + // THEN + tc.assert(t, err, queue) + }) + } +} + +func TestRuleSetProcessorOnUpdated(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + ruleset *config.RuleSet + configureFactory func(t *testing.T, mhf *mocks.FactoryMock) + assert func(t *testing.T, err error, queue event.RuleSetChangedEventQueue) + }{ + { + uc: "unsupported version", + ruleset: &config.RuleSet{Version: "foo"}, + assert: func(t *testing.T, err error, _ event.RuleSetChangedEventQueue) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, ErrUnsupportedRuleSetVersion) + }, + }, + { + uc: "error while loading rule set", + ruleset: &config.RuleSet{Version: config.CurrentRuleSetVersion, Rules: []config.Rule{{ID: "foo"}}}, + configureFactory: func(t *testing.T, mhf *mocks.FactoryMock) { + t.Helper() + + mhf.EXPECT().CreateRule(mock.Anything, mock.Anything, mock.Anything). + Return(nil, testsupport.ErrTestPurpose) + }, + assert: func(t *testing.T, err error, queue event.RuleSetChangedEventQueue) { + t.Helper() + + require.Error(t, err) + assert.ErrorIs(t, err, testsupport.ErrTestPurpose) + assert.Contains(t, err.Error(), "failed loading") + }, + }, + { + uc: "successful", + ruleset: &config.RuleSet{ + MetaData: config.MetaData{Source: "test"}, + Version: config.CurrentRuleSetVersion, + Name: "foobar", + Rules: []config.Rule{{ID: "foo"}}, + }, + configureFactory: func(t *testing.T, mhf *mocks.FactoryMock) { + t.Helper() + + mhf.EXPECT().CreateRule(config.CurrentRuleSetVersion, mock.Anything, mock.Anything). + Return(&mocks.RuleMock{}, nil) + }, + assert: func(t *testing.T, err error, queue event.RuleSetChangedEventQueue) { + t.Helper() + + require.NoError(t, err) + require.Len(t, queue, 1) + + evt := <-queue + require.Len(t, evt.Rules, 1) + assert.Equal(t, event.Update, evt.ChangeType) + assert.Equal(t, "test", evt.Source) + assert.Equal(t, "foobar", evt.Name) + + assert.Equal(t, &mocks.RuleMock{}, evt.Rules[0]) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEM + configureFactory := x.IfThenElse(tc.configureFactory != nil, + tc.configureFactory, + func(t *testing.T, mhf *mocks.FactoryMock) { t.Helper() }) + + queue := make(event.RuleSetChangedEventQueue, 10) + + factory := mocks.NewFactoryMock(t) + configureFactory(t, factory) + + processor := NewRuleSetProcessor(queue, factory, log.Logger) + + // WHEN + err := processor.OnUpdated(tc.ruleset) + + // THEN + tc.assert(t, err, queue) + }) + } +} + +func TestRuleSetProcessorOnDeleted(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + uc string + ruleset *config.RuleSet + assert func(t *testing.T, err error, queue event.RuleSetChangedEventQueue) + }{ + { + uc: "successful", + ruleset: &config.RuleSet{ + MetaData: config.MetaData{Source: "test"}, + Version: config.CurrentRuleSetVersion, + Name: "foobar", + }, + assert: func(t *testing.T, err error, queue event.RuleSetChangedEventQueue) { + t.Helper() + + require.NoError(t, err) + require.Len(t, queue, 1) + + evt := <-queue + assert.Equal(t, event.Remove, evt.ChangeType) + assert.Equal(t, "test", evt.Source) + assert.Equal(t, "foobar", evt.Name) + }, + }, + } { + t.Run(tc.uc, func(t *testing.T) { + // GIVEM + queue := make(event.RuleSetChangedEventQueue, 10) + processor := NewRuleSetProcessor(queue, mocks.NewFactoryMock(t), log.Logger) + + // WHEN + err := processor.OnDeleted(tc.ruleset) + + // THEN + tc.assert(t, err, queue) + }) + } +} diff --git a/internal/rules/subject_creator.go b/internal/rules/subject_creator.go index 72854f243..a7eed9194 100644 --- a/internal/rules/subject_creator.go +++ b/internal/rules/subject_creator.go @@ -21,6 +21,8 @@ import ( "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) +//go:generate mockery --name subjectCreator --structname SubjectCreatorMock + type subjectCreator interface { Execute(heimdall.Context) (*subject.Subject, error) IsFallbackOnErrorAllowed() bool diff --git a/internal/rules/subject_handler.go b/internal/rules/subject_handler.go index 7765a1951..6e6971c96 100644 --- a/internal/rules/subject_handler.go +++ b/internal/rules/subject_handler.go @@ -21,6 +21,8 @@ import ( "github.com/dadrus/heimdall/internal/rules/mechanisms/subject" ) +//go:generate mockery --name subjectHandler --structname SubjectHandlerMock + type subjectHandler interface { Execute(heimdall.Context, *subject.Subject) error ContinueOnError() bool diff --git a/internal/x/errorchain/error_chain.go b/internal/x/errorchain/error_chain.go index 2e165a9a8..67e9c35f7 100644 --- a/internal/x/errorchain/error_chain.go +++ b/internal/x/errorchain/error_chain.go @@ -168,7 +168,7 @@ func (ec *ErrorChain) MarshalJSON() ([]byte, error) { }) } -func (ec *ErrorChain) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error { +func (ec *ErrorChain) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { return encoder.Encode( message{ XMLName: xml.Name{Local: "error"}, diff --git a/internal/x/slicex/filter.go b/internal/x/slicex/filter.go new file mode 100644 index 000000000..feeae224f --- /dev/null +++ b/internal/x/slicex/filter.go @@ -0,0 +1,13 @@ +package slicex + +func Filter[T any](src []T, apply func(T) bool) []T { + var dst []T + + for _, n := range src { + if apply(n) { + dst = append(dst, n) + } + } + + return dst +} diff --git a/internal/x/testsupport/mock/argument_captor.go b/internal/x/testsupport/mock/argument_captor.go new file mode 100644 index 000000000..90a3f62f2 --- /dev/null +++ b/internal/x/testsupport/mock/argument_captor.go @@ -0,0 +1,37 @@ +package mock + +import "github.com/stretchr/testify/mock" + +type ArgumentCaptor[T any] struct { + capturedArgs []T +} + +func NewArgumentCaptor[T any](m *mock.Mock, name string) *ArgumentCaptor[T] { + captor := &ArgumentCaptor[T]{} + + m.TestData().Set(name, captor) + + return captor +} + +func (c *ArgumentCaptor[T]) Capture(val T) { + c.capturedArgs = append(c.capturedArgs, val) +} + +func (c *ArgumentCaptor[T]) Values() []T { + return c.capturedArgs +} + +func (c *ArgumentCaptor[T]) Value() T { + var def T + + if len(c.capturedArgs)-1 >= 0 { + return c.capturedArgs[0] + } + + return def +} + +func ArgumentCaptorFrom[T any](m *mock.Mock, name string) *ArgumentCaptor[T] { + return m.TestData().Get(name).Data().(*ArgumentCaptor[T]) // nolint: forcetypeassert +} diff --git a/internal/x/testsupport/mock/arguments_captor.go b/internal/x/testsupport/mock/arguments_captor.go new file mode 100644 index 000000000..c8c76a087 --- /dev/null +++ b/internal/x/testsupport/mock/arguments_captor.go @@ -0,0 +1,31 @@ +package mock + +import "github.com/stretchr/testify/mock" + +type ArgumentsCaptor struct { + capturedArgs []mock.Arguments +} + +func NewArgumentsCaptor(m *mock.Mock, name string) *ArgumentsCaptor { + captor := &ArgumentsCaptor{} + + m.TestData().Set(name, captor) + + return captor +} + +func (c *ArgumentsCaptor) Capture(args mock.Arguments) { + c.capturedArgs = append(c.capturedArgs, args) +} + +func (c *ArgumentsCaptor) Values(call int) mock.Arguments { + if len(c.capturedArgs)-1 >= call { + return c.capturedArgs[call] + } + + return nil +} + +func ArgumentsCaptorFrom(m *mock.Mock, name string) *ArgumentsCaptor { + return m.TestData().Get(name).Data().(*ArgumentsCaptor) // nolint: forcetypeassert +} diff --git a/internal/x/testsupport/patched_os_exit.go b/internal/x/testsupport/patched_os_exit.go new file mode 100644 index 000000000..2db4010f4 --- /dev/null +++ b/internal/x/testsupport/patched_os_exit.go @@ -0,0 +1,38 @@ +package testsupport + +import ( + "os" + "testing" + + "github.com/undefinedlabs/go-mpatch" +) + +type PatchedOSExit struct { + Called bool + Code int + + patchFunc *mpatch.Patch +} + +func PatchOSExit(t *testing.T, mockOSExitImpl func(int)) (*PatchedOSExit, error) { + t.Helper() + + patchedExit := &PatchedOSExit{Called: false} + + var err error + + patchedExit.patchFunc, err = mpatch.PatchMethod(os.Exit, func(code int) { + patchedExit.Called = true + patchedExit.Code = code + + mockOSExitImpl(code) + }) + + t.Cleanup(func() { + if patchedExit.patchFunc != nil { + _ = patchedExit.patchFunc.Unpatch() + } + }) + + return patchedExit, err +}