From 6a6c9b0963de4382de4e6f2935601d2b77458aec Mon Sep 17 00:00:00 2001 From: Jan Wozniak Date: Tue, 30 Apr 2024 18:06:05 +0200 Subject: [PATCH] parser for auth params Signed-off-by: Jan Wozniak --- .../authentication/authentication_types.go | 84 +++++++ pkg/scalers/prometheus_scaler.go | 62 +++-- pkg/scalers/prometheus_scaler_test.go | 11 +- pkg/scalers/scalersconfig/typed_config.go | 167 +++++++++--- .../scalersconfig/typed_config_test.go | 237 ++++++++++++++++++ 5 files changed, 486 insertions(+), 75 deletions(-) diff --git a/pkg/scalers/authentication/authentication_types.go b/pkg/scalers/authentication/authentication_types.go index 09894572775..3962b2a0d9e 100644 --- a/pkg/scalers/authentication/authentication_types.go +++ b/pkg/scalers/authentication/authentication_types.go @@ -1,6 +1,7 @@ package authentication import ( + "fmt" "net/url" "time" ) @@ -31,6 +32,8 @@ const ( FastHTTP // FastHTTP Fast http client. ) +// AuthMeta is the metadata for the authentication types +// DEPRECATED: Use Config instead type AuthMeta struct { // bearer auth EnableBearerAuth bool @@ -61,6 +64,87 @@ type AuthMeta struct { CustomAuthValue string } +// BasicAuth is a basic authentication type +type BasicAuth struct { + Username string `keda:"name=username, parsingOrder=authParams"` + Password string `keda:"name=password, parsingOrder=authParams, optional=true"` +} + +// CertAuth is a client certificate authentication type +type CertAuth struct { + Cert string `keda:"name=cert, parsingOrder=authParams"` + Key string `keda:"name=key, parsingOrder=authParams"` + CA string `keda:"name=ca, parsingOrder=authParams, optional=true"` +} + +// OAuth is an oAuth2 authentication type +type OAuth struct { + OauthTokenURI string `keda:"name=oauthTokenURI, parsingOrder=authParams"` + Scopes []string `keda:"name=scopes, parsingOrder=authParams"` + ClientID string `keda:"name=clientID, parsingOrder=authParams"` + ClientSecret string `keda:"name=clientSecret, parsingOrder=authParams"` + EndpointParams url.Values `keda:"name=endpointParams, parsingOrder=authParams"` +} + +// CustomAuth is a custom header authentication type +type CustomAuth struct { + CustomAuthHeader string `keda:"name=customAuthHeader, parsingOrder=authParams"` //TODO: check why the code had strings.TrimSuffix(authParams["customAuthHeader"], "\n") + CustomAuthValue string `keda:"name=customAuthValue, parsingOrder=authParams"` //TODO: check why the code had strings.TrimSuffix(authParams["customAuthValue"], "\n") +} + +// Config is the configuration for the authentication types +type Config struct { + Modes []string `keda:"name=authModes, parsingOrder=triggerMetadata, enum=apiKey;basic;tls;bearer;custom;oauth, exclusive=bearer;basic;oauth, optional=true"` + + //TODO: check why the code had strings.TrimSuffix(authParams["bearerToken"], "\n") + BearerToken string `keda:"name=bearerToken, optional=true"` + BasicAuth `keda:"optional=true"` + CertAuth `keda:"optional=true"` + OAuth `keda:"optional=true"` + CustomAuth `keda:"optional=true"` +} + +// Disabled returns true if no auth modes are enabled +func (c *Config) Disabled() bool { + return c == nil || len(c.Modes) == 0 +} + +// Enabled returns true if given auth mode is enabled +func (c *Config) Enabled(mode Type) bool { + for _, m := range c.Modes { + if m == string(mode) { + return true + } + } + return false +} + +// GetBearerToken returns the bearer token with the Bearer prefix +func (c *Config) GetBearerToken() string { + return fmt.Sprintf("Bearer %s", c.BearerToken) +} + +// ToAuthMeta converts the Config to deprecated AuthMeta +func (c *Config) ToAuthMeta() *AuthMeta { + if c.Disabled() { + return nil + } + return &AuthMeta{ + EnableBearerAuth: c.Enabled(BearerAuthType), + BearerToken: c.BearerToken, + EnableBasicAuth: c.Enabled(BasicAuthType), + Username: c.Username, + Password: c.Password, + EnableTLS: c.Enabled(TLSAuthType), + Cert: c.Cert, + Key: c.Key, + CA: c.CA, + EnableOAuth: c.Enabled(OAuthType), + OauthTokenURI: c.OauthTokenURI, + Scopes: c.Scopes, + } +} + type HTTPTransport struct { MaxIdleConnDuration time.Duration ReadTimeout time.Duration diff --git a/pkg/scalers/prometheus_scaler.go b/pkg/scalers/prometheus_scaler.go index 9c7bddb6702..e2498f2114e 100644 --- a/pkg/scalers/prometheus_scaler.go +++ b/pkg/scalers/prometheus_scaler.go @@ -50,19 +50,21 @@ type prometheusScaler struct { // change to false/f if can not accept prometheus return null values // https://github.com/kedacore/keda/issues/3065 type prometheusMetadata struct { - prometheusAuth *authentication.AuthMeta - triggerIndex int - - ServerAddress string `keda:"name=serverAddress, parsingOrder=triggerMetadata"` - Query string `keda:"name=query, parsingOrder=triggerMetadata"` - QueryParameters map[string]string `keda:"name=queryParameters, parsingOrder=triggerMetadata, optional"` - Threshold float64 `keda:"name=threshold, parsingOrder=triggerMetadata"` - ActivationThreshold float64 `keda:"name=activationThreshold, parsingOrder=triggerMetadata, optional"` - Namespace string `keda:"name=namespace, parsingOrder=triggerMetadata, optional"` - CustomHeaders map[string]string `keda:"name=customHeaders, parsingOrder=triggerMetadata, optional"` - IgnoreNullValues bool `keda:"name=ignoreNullValues, parsingOrder=triggerMetadata, optional, default=true"` - UnsafeSSL bool `keda:"name=unsafeSsl, parsingOrder=triggerMetadata, optional"` - CortexOrgID string `keda:"name=cortexOrgID, parsingOrder=triggerMetadata, optional, deprecated=use customHeaders instead"` + triggerIndex int + + PrometheusAuth *authentication.Config `keda:"optional"` + ServerAddress string `keda:"name=serverAddress, parsingOrder=triggerMetadata"` + Query string `keda:"name=query, parsingOrder=triggerMetadata"` + QueryParameters map[string]string `keda:"name=queryParameters, parsingOrder=triggerMetadata, optional"` + Threshold float64 `keda:"name=threshold, parsingOrder=triggerMetadata"` + ActivationThreshold float64 `keda:"name=activationThreshold, parsingOrder=triggerMetadata, optional"` + Namespace string `keda:"name=namespace, parsingOrder=triggerMetadata, optional"` + CustomHeaders map[string]string `keda:"name=customHeaders, parsingOrder=triggerMetadata, optional"` + IgnoreNullValues bool `keda:"name=ignoreNullValues, parsingOrder=triggerMetadata, optional, default=true"` + UnsafeSSL bool `keda:"name=unsafeSsl, parsingOrder=triggerMetadata, optional"` + + // deprecated + CortexOrgID string `keda:"name=cortexOrgID, parsingOrder=triggerMetadata, optional, deprecated=use customHeaders instead"` } type promQueryResult struct { @@ -93,12 +95,12 @@ func NewPrometheusScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, meta.UnsafeSSL) - if meta.prometheusAuth != nil { - if meta.prometheusAuth.CA != "" || meta.prometheusAuth.EnableTLS { + if meta.PrometheusAuth != nil { + if meta.PrometheusAuth.CA != "" || meta.PrometheusAuth.Enabled(authentication.TLSAuthType) { // create http.RoundTripper with auth settings from ScalerConfig transport, err := authentication.CreateHTTPRoundTripper( authentication.NetHTTP, - meta.prometheusAuth, + meta.PrometheusAuth.ToAuthMeta(), ) if err != nil { logger.V(1).Error(err, "init Prometheus client http transport") @@ -156,7 +158,7 @@ func parsePrometheusMetadata(config *scalersconfig.ScalerConfig) (meta *promethe } meta.triggerIndex = config.TriggerIndex - err = parseAuthConfig(config, meta) + err = checkAuthConfigWithPodIdentity(config, meta) if err != nil { return nil, err } @@ -164,18 +166,10 @@ func parsePrometheusMetadata(config *scalersconfig.ScalerConfig) (meta *promethe return meta, nil } -func parseAuthConfig(config *scalersconfig.ScalerConfig, meta *prometheusMetadata) error { - // parse auth configs from ScalerConfig - auth, err := authentication.GetAuthConfigs(config.TriggerMetadata, config.AuthParams) - if err != nil { - return err - } - - if auth != nil && !(config.PodIdentity.Provider == kedav1alpha1.PodIdentityProviderNone || config.PodIdentity.Provider == "") { +func checkAuthConfigWithPodIdentity(config *scalersconfig.ScalerConfig, meta *prometheusMetadata) error { + if meta != nil && !meta.PrometheusAuth.Disabled() && !(config.PodIdentity.Provider == kedav1alpha1.PodIdentityProviderNone || config.PodIdentity.Provider == "") { return fmt.Errorf("pod identity cannot be enabled with other auth types") } - meta.prometheusAuth = auth - return nil } @@ -226,14 +220,14 @@ func (s *prometheusScaler) ExecutePromQuery(ctx context.Context) (float64, error } switch { - case s.metadata.prometheusAuth == nil: + case s.metadata.PrometheusAuth.Disabled(): break - case s.metadata.prometheusAuth.EnableBearerAuth: - req.Header.Set("Authorization", authentication.GetBearerToken(s.metadata.prometheusAuth)) - case s.metadata.prometheusAuth.EnableBasicAuth: - req.SetBasicAuth(s.metadata.prometheusAuth.Username, s.metadata.prometheusAuth.Password) - case s.metadata.prometheusAuth.EnableCustomAuth: - req.Header.Set(s.metadata.prometheusAuth.CustomAuthHeader, s.metadata.prometheusAuth.CustomAuthValue) + case s.metadata.PrometheusAuth.Enabled(authentication.BearerAuthType): + req.Header.Set("Authorization", s.metadata.PrometheusAuth.GetBearerToken()) + case s.metadata.PrometheusAuth.Enabled(authentication.BasicAuthType): + req.SetBasicAuth(s.metadata.PrometheusAuth.Username, s.metadata.PrometheusAuth.Password) + case s.metadata.PrometheusAuth.Enabled(authentication.CustomAuthType): + req.Header.Set(s.metadata.PrometheusAuth.CustomAuthHeader, s.metadata.PrometheusAuth.CustomAuthValue) } r, err := s.httpClient.Do(req) diff --git a/pkg/scalers/prometheus_scaler_test.go b/pkg/scalers/prometheus_scaler_test.go index 04dd524551e..2cacfab3da3 100644 --- a/pkg/scalers/prometheus_scaler_test.go +++ b/pkg/scalers/prometheus_scaler_test.go @@ -18,6 +18,7 @@ import ( "golang.org/x/oauth2" kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + "github.com/kedacore/keda/v2/pkg/scalers/authentication" "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" ) @@ -162,11 +163,11 @@ func TestPrometheusScalerAuthParams(t *testing.T) { } if err == nil { - if meta.prometheusAuth != nil { - if (meta.prometheusAuth.EnableBearerAuth && !strings.Contains(testData.metadata["authModes"], "bearer")) || - (meta.prometheusAuth.EnableBasicAuth && !strings.Contains(testData.metadata["authModes"], "basic")) || - (meta.prometheusAuth.EnableTLS && !strings.Contains(testData.metadata["authModes"], "tls")) || - (meta.prometheusAuth.EnableCustomAuth && !strings.Contains(testData.metadata["authModes"], "custom")) { + if !meta.PrometheusAuth.Disabled() { + if (meta.PrometheusAuth.Enabled(authentication.BearerAuthType) && !strings.Contains(testData.metadata["authModes"], "bearer")) || + (meta.PrometheusAuth.Enabled(authentication.BasicAuthType) && !strings.Contains(testData.metadata["authModes"], "basic")) || + (meta.PrometheusAuth.Enabled(authentication.TLSAuthType) && !strings.Contains(testData.metadata["authModes"], "tls")) || + (meta.PrometheusAuth.Enabled(authentication.CustomAuthType) && !strings.Contains(testData.metadata["authModes"], "custom")) { t.Error("wrong auth mode detected") } } diff --git a/pkg/scalers/scalersconfig/typed_config.go b/pkg/scalers/scalersconfig/typed_config.go index 5068883bc72..18d787eb231 100644 --- a/pkg/scalers/scalersconfig/typed_config.go +++ b/pkg/scalers/scalersconfig/typed_config.go @@ -19,6 +19,7 @@ package scalersconfig import ( "encoding/json" "fmt" + "net/url" "reflect" "strings" @@ -56,15 +57,25 @@ const ( defaultTag = "default" parsingOrderTag = "parsingOrder" nameTag = "name" + enumTag = "enum" + exclusiveTag = "exclusive" ) // Params is a struct that represents the parameter list that can be used in the keda tag type Params struct { + FieldName string Name string Optional bool ParsingOrder []ParsingOrder Default string Deprecated string + Enum []string + Exclusive []string +} + +// IsNested is a function that returns true if the parameter is nested +func (p Params) IsNested() bool { + return p.Name == "" } // IsDeprecated is a function that returns true if the parameter is deprecated @@ -84,6 +95,7 @@ func (p Params) DeprecatedMessage() string { // populating the provided typedConfig where structure fields along with complementary field tags define // declaratively the parsing rules func (sc *ScalerConfig) TypedConfig(typedConfig any) error { + // TODO: recover from reflections panics and instead return an error t := reflect.TypeOf(typedConfig) if t.Kind() != reflect.Pointer { return fmt.Errorf("typedConfig must be a pointer") @@ -95,8 +107,8 @@ func (sc *ScalerConfig) TypedConfig(typedConfig any) error { for i := 0; i < t.NumField(); i++ { fieldType := t.Field(i) fieldValue := v.Field(i) - tag := fieldType.Tag.Get("keda") - if tag == "" { + tag, exists := fieldType.Tag.Lookup("keda") + if !exists { continue } tagParams, err := paramsFromTag(tag, fieldType) @@ -127,12 +139,111 @@ func (sc *ScalerConfig) setValue(field reflect.Value, params Params) error { if !exists && !(params.Optional || params.IsDeprecated()) { return fmt.Errorf("missing required parameter %q in %v", params.Name, params.ParsingOrder) } + if params.Enum != nil { + found := false + for _, e := range params.Enum { + if e == valFromConfig { + found = true + break + } + } + if !found { + return fmt.Errorf("parameter %q value %q must be one of %v", params.Name, valFromConfig, params.Enum) + } + } + if params.Exclusive != nil { + exclusiveMap := make(map[string]bool) + for _, e := range params.Exclusive { + exclusiveMap[e] = true + } + split := strings.Split(valFromConfig, elemSeparator) + exclusiveCount := 0 + for _, s := range split { + s := strings.TrimSpace(s) + if exclusiveMap[s] { + exclusiveCount++ + } + } + if exclusiveCount > 1 { + return fmt.Errorf("parameter %q value %q must contain only one of %v", params.Name, valFromConfig, params.Exclusive) + } + } + if params.IsNested() { + for field.Kind() == reflect.Ptr { + field.Set(reflect.New(field.Type().Elem())) + field = field.Elem() + } + if field.Kind() != reflect.Struct { + return fmt.Errorf("nested parameter %q must be a struct, has kind %q", params.FieldName, field.Kind()) + } + return sc.TypedConfig(field.Addr().Interface()) + } if err := setConfigValueHelper(valFromConfig, field); err != nil { return fmt.Errorf("unable to set param %q value %q: %w", params.Name, valFromConfig, err) } return nil } +// setConfigValueURLParams is a function that sets the value of the url.Values field +func setConfigValueURLParams(valFromConfig string, field reflect.Value) error { + field.Set(reflect.MakeMap(reflect.MapOf(field.Type().Key(), field.Type().Elem()))) + vals, err := url.ParseQuery(valFromConfig) + if err != nil { + return fmt.Errorf("expected url.Values, unable to parse query %q: %w", valFromConfig, err) + } + for k, vs := range vals { + ifcMapKeyElem := reflect.New(field.Type().Key()).Elem() + ifcMapValueElem := reflect.New(field.Type().Elem()).Elem() + if err := setConfigValueHelper(k, ifcMapKeyElem); err != nil { + return fmt.Errorf("map key %q: %w", k, err) + } + for _, v := range vs { + ifcMapValueElem.Set(reflect.Append(ifcMapValueElem, reflect.ValueOf(v))) + } + field.SetMapIndex(ifcMapKeyElem, ifcMapValueElem) + } + return nil +} + +// setConfigValueMap is a function that sets the value of the map field +func setConfigValueMap(valFromConfig string, field reflect.Value) error { + field.Set(reflect.MakeMap(reflect.MapOf(field.Type().Key(), field.Type().Elem()))) + split := strings.Split(valFromConfig, elemSeparator) + for _, s := range split { + s := strings.TrimSpace(s) + kv := strings.Split(s, elemKeyValSeparator) + if len(kv) != 2 { + return fmt.Errorf("expected format key%vvalue, got %q", elemKeyValSeparator, s) + } + key := strings.TrimSpace(kv[0]) + val := strings.TrimSpace(kv[1]) + ifcKeyElem := reflect.New(field.Type().Key()).Elem() + if err := setConfigValueHelper(key, ifcKeyElem); err != nil { + return fmt.Errorf("map key %q: %w", key, err) + } + ifcValueElem := reflect.New(field.Type().Elem()).Elem() + if err := setConfigValueHelper(val, ifcValueElem); err != nil { + return fmt.Errorf("map key %q, value %q: %w", key, val, err) + } + field.SetMapIndex(ifcKeyElem, ifcValueElem) + } + return nil +} + +// setConfigValueSlice is a function that sets the value of the slice field +func setConfigValueSlice(valFromConfig string, field reflect.Value) error { + elemIfc := reflect.New(field.Type().Elem()).Interface() + split := strings.Split(valFromConfig, elemSeparator) + for i, s := range split { + s := strings.TrimSpace(s) + if err := setConfigValueHelper(s, reflect.ValueOf(elemIfc).Elem()); err != nil { + return fmt.Errorf("slice element %d: %w", i, err) + } + field.Set(reflect.Append(field, reflect.ValueOf(elemIfc).Elem())) + } + return nil +} + // setParamValueHelper is a function that sets the value of the parameter func setConfigValueHelper(valFromConfig string, field reflect.Value) error { paramValue := reflect.ValueOf(valFromConfig) @@ -144,40 +255,14 @@ func setConfigValueHelper(valFromConfig string, field reflect.Value) error { field.Set(paramValue.Convert(field.Type())) return nil } + if field.Type() == reflect.TypeOf(url.Values{}) { + return setConfigValueURLParams(valFromConfig, field) + } if field.Kind() == reflect.Map { - field.Set(reflect.MakeMap(reflect.MapOf(field.Type().Key(), field.Type().Elem()))) - split := strings.Split(valFromConfig, elemSeparator) - for _, s := range split { - s := strings.TrimSpace(s) - kv := strings.Split(s, elemKeyValSeparator) - if len(kv) != 2 { - return fmt.Errorf("expected format key%vvalue, got %q", elemKeyValSeparator, s) - } - key := strings.TrimSpace(kv[0]) - val := strings.TrimSpace(kv[1]) - ifcKeyElem := reflect.New(field.Type().Key()).Elem() - if err := setConfigValueHelper(key, ifcKeyElem); err != nil { - return fmt.Errorf("map key %q: %w", key, err) - } - ifcValueElem := reflect.New(field.Type().Elem()).Elem() - if err := setConfigValueHelper(val, ifcValueElem); err != nil { - return fmt.Errorf("map key %q, value %q: %w", key, val, err) - } - field.SetMapIndex(ifcKeyElem, ifcValueElem) - } - return nil + return setConfigValueMap(valFromConfig, field) } if field.Kind() == reflect.Slice { - elemIfc := reflect.New(field.Type().Elem()).Interface() - split := strings.Split(valFromConfig, elemSeparator) - for i, s := range split { - s := strings.TrimSpace(s) - if err := setConfigValueHelper(s, reflect.ValueOf(elemIfc).Elem()); err != nil { - return fmt.Errorf("slice element %d: %w", i, err) - } - field.Set(reflect.Append(field, reflect.ValueOf(elemIfc).Elem())) - } - return nil + return setConfigValueSlice(valFromConfig, field) } if field.CanInterface() { ifc := reflect.New(field.Type()).Interface() @@ -210,12 +295,12 @@ func (sc *ScalerConfig) configParamValue(params Params) (string, bool) { return param, true } } - return "", false + return "", params.IsNested() } // paramsFromTag is a function that returns the Params struct based on the field tag func paramsFromTag(tag string, field reflect.StructField) (Params, error) { - params := Params{Name: field.Name} + params := Params{FieldName: field.Name} tagSplit := strings.Split(tag, tagSeparator) for _, ts := range tagSplit { tsplit := strings.Split(ts, tagKeySeparator) @@ -250,8 +335,18 @@ func paramsFromTag(tag string, field reflect.StructField) (Params, error) { if len(tsplit) > 1 { params.Default = strings.TrimSpace(tsplit[1]) } + case enumTag: + if len(tsplit) > 1 { + params.Enum = strings.Split(tsplit[1], tagValueSeparator) + } + case exclusiveTag: + if len(tsplit) > 1 { + params.Exclusive = strings.Split(tsplit[1], tagValueSeparator) + } + case "": + continue default: - return params, fmt.Errorf("unknown tag %s: %s", tsplit[0], tag) + return params, fmt.Errorf("unknown tag param %s: %s", tsplit[0], tag) } } return params, nil diff --git a/pkg/scalers/scalersconfig/typed_config_test.go b/pkg/scalers/scalersconfig/typed_config_test.go index bdd3071015f..08f58dc97c9 100644 --- a/pkg/scalers/scalersconfig/typed_config_test.go +++ b/pkg/scalers/scalersconfig/typed_config_test.go @@ -17,6 +17,7 @@ limitations under the License. package scalersconfig import ( + "net/url" "testing" . "github.com/onsi/gomega" @@ -229,3 +230,239 @@ func TestSlice(t *testing.T) { Expect(ts.SliceVal[1]).To(Equal(2)) Expect(ts.SliceVal[2]).To(Equal(3)) } + +// TestEnum tests the enum type +func TestEnum(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + TriggerMetadata: map[string]string{ + "enumVal": "value1", + }, + } + + type testStruct struct { + EnumVal string `keda:"name=enumVal, parsingOrder=triggerMetadata, enum=value1;value2"` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.EnumVal).To(Equal("value1")) + + sc2 := &ScalerConfig{ + TriggerMetadata: map[string]string{ + "enumVal": "value3", + }, + } + + ts2 := testStruct{} + err = sc2.TypedConfig(&ts2) + Expect(err).To(MatchError(`parameter "enumVal" value "value3" must be one of [value1 value2]`)) +} + +// TestExclusive tests the exclusive type +func TestExclusive(t *testing.T) { + RegisterTestingT(t) + + type testStruct struct { + IntVal []int `keda:"name=intVal, parsingOrder=triggerMetadata, exclusive=1;4;5"` + } + + sc := &ScalerConfig{ + TriggerMetadata: map[string]string{ + "intVal": "1,2,3", + }, + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + + sc2 := &ScalerConfig{ + TriggerMetadata: map[string]string{ + "intVal": "1,4", + }, + } + + ts2 := testStruct{} + err = sc2.TypedConfig(&ts2) + Expect(err).To(MatchError(`parameter "intVal" value "1,4" must contain only one of [1 4 5]`)) +} + +// TestURLValues tests the url.Values type +func TestURLValues(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + AuthParams: map[string]string{ + "endpointParams": "key1=value1&key2=value2&key1=value3", + }, + } + + type testStruct struct { + EndpointParams url.Values `keda:"name=endpointParams, parsingOrder=authParams"` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.EndpointParams).To(HaveLen(2)) + Expect(ts.EndpointParams).To(HaveKey("key1")) + Expect(ts.EndpointParams).To(HaveKey("key2")) + Expect(ts.EndpointParams["key1"]).To(ConsistOf("value1", "value3")) + Expect(ts.EndpointParams["key2"]).To(ConsistOf("value2")) +} + +// TestGenericMap tests the generic map type that is structurally similar to url.Values +func TestGenericMap(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + AuthParams: map[string]string{ + "endpointParams": "key1=value1,key2=value2,key3=value3", + }, + } + + // structurally similar to url.Values but should behave as generic map + type testStruct struct { + EndpointParams map[string][]string `keda:"name=endpointParams, parsingOrder=authParams"` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.EndpointParams).To(HaveLen(3)) + Expect(ts.EndpointParams).To(HaveKey("key1")) + Expect(ts.EndpointParams).To(HaveKey("key2")) + Expect(ts.EndpointParams).To(HaveKey("key3")) + Expect(ts.EndpointParams["key1"]).To(ConsistOf("value1")) + Expect(ts.EndpointParams["key2"]).To(ConsistOf("value2")) + Expect(ts.EndpointParams["key3"]).To(ConsistOf("value3")) +} + +// TestNestedStruct tests the nested struct type +func TestNestedStruct(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + AuthParams: map[string]string{ + "username": "user", + "password": "pass", + }, + } + + type basicAuth struct { + Username string `keda:"name=username, parsingOrder=authParams"` + Password string `keda:"name=password, parsingOrder=authParams"` + } + + type testStruct struct { + BA basicAuth `keda:""` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.BA.Username).To(Equal("user")) + Expect(ts.BA.Password).To(Equal("pass")) +} + +// TestEmbeddedStruct tests the embedded struct type +func TestEmbeddedStruct(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + AuthParams: map[string]string{ + "username": "user", + "password": "pass", + }, + } + + type testStruct struct { + BasicAuth struct { + Username string `keda:"name=username, parsingOrder=authParams"` + Password string `keda:"name=password, parsingOrder=authParams"` + } `keda:""` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.BasicAuth.Username).To(Equal("user")) + Expect(ts.BasicAuth.Password).To(Equal("pass")) +} + +// TestWrongNestedStruct tests the wrong nested type +func TestWrongNestedStruct(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + AuthParams: map[string]string{ + "username": "user", + "password": "pass", + }, + } + + type testStruct struct { + WrongNesting int `keda:""` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(MatchError(`nested parameter "WrongNesting" must be a struct, has kind "int"`)) +} + +// TestNestedOptional tests the nested optional type +func TestNestedOptional(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + AuthParams: map[string]string{ + "username": "user", + }, + } + + type basicAuth struct { + Username string `keda:"name=username, parsingOrder=authParams"` + Password string `keda:"name=password, parsingOrder=authParams, optional=true"` + } + + type testStruct struct { + BA basicAuth `keda:"optional=true"` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.BA.Username).To(Equal("user")) + Expect(ts.BA.Password).To(Equal("")) +} + +// TestNestedPointer tests the nested pointer type +func TestNestedPointer(t *testing.T) { + RegisterTestingT(t) + + sc := &ScalerConfig{ + AuthParams: map[string]string{ + "username": "user", + "password": "pass", + }, + } + + type basicAuth struct { + Username string `keda:"name=username, parsingOrder=authParams"` + Password string `keda:"name=password, parsingOrder=authParams"` + } + + type testStruct struct { + BA *basicAuth `keda:""` + } + + ts := testStruct{} + err := sc.TypedConfig(&ts) + Expect(err).To(BeNil()) + Expect(ts.BA).ToNot(BeNil()) + Expect(ts.BA.Username).To(Equal("user")) + Expect(ts.BA.Password).To(Equal("pass")) +}