diff --git a/CHANGELOG.md b/CHANGELOG.md index e144e353289..2a38c13a881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio - **Azure Pod Identity**: Introduce validation to prevent usage of empty identity ID for Azure identity providers ([#4528](https://github.com/kedacore/keda/issues/4528)) - **Prometheus Scaler**: Remove trailing whitespaces in customAuthHeader and customAuthValue ([#4960](https://github.com/kedacore/keda/issues/4960)) - **Pulsar Scaler**: Add support for OAuth extensions ([#4700](https://github.com/kedacore/keda/issues/4700)) +- **Redis Scalers**: Add TLS authentication support for Redis and Redis stream scalers ([#4917](https://github.com/kedacore/keda/issues/4917)) ### Fixes - **RabbitMQ Scaler**: Allow subpaths along with vhost in connection string ([#2634](https://github.com/kedacore/keda/issues/2634)) diff --git a/pkg/scalers/redis_scaler.go b/pkg/scalers/redis_scaler.go index b549c22672f..81aa3797004 100644 --- a/pkg/scalers/redis_scaler.go +++ b/pkg/scalers/redis_scaler.go @@ -55,6 +55,10 @@ type redisConnectionInfo struct { ports []string enableTLS bool unsafeSsl bool + cert string + key string + keyPassword string + ca string } type redisMetadata struct { @@ -188,31 +192,74 @@ func createRedisScalerWithClient(client *redis.Client, meta *redisMetadata, scri } } -func parseRedisMetadata(config *ScalerConfig, parserFn redisAddressParser) (*redisMetadata, error) { - connInfo, err := parserFn(config.TriggerMetadata, config.ResolvedEnv, config.AuthParams) - if err != nil { - return nil, err - } - meta := redisMetadata{ - connectionInfo: connInfo, - } - - meta.connectionInfo.enableTLS = defaultEnableTLS +func parseTLSConfigIntoConnectionInfo(config *ScalerConfig, connInfo *redisConnectionInfo) error { + enableTLS := defaultEnableTLS if val, ok := config.TriggerMetadata["enableTLS"]; ok { tls, err := strconv.ParseBool(val) if err != nil { - return nil, fmt.Errorf("enableTLS parsing error %w", err) + return fmt.Errorf("enableTLS parsing error %w", err) } - meta.connectionInfo.enableTLS = tls + enableTLS = tls } - meta.connectionInfo.unsafeSsl = false + connInfo.unsafeSsl = false if val, ok := config.TriggerMetadata["unsafeSsl"]; ok { parsedVal, err := strconv.ParseBool(val) if err != nil { - return nil, fmt.Errorf("error parsing unsafeSsl: %w", err) + return fmt.Errorf("error parsing unsafeSsl: %w", err) } - meta.connectionInfo.unsafeSsl = parsedVal + connInfo.unsafeSsl = parsedVal + } + + // parse tls config defined in auth params + if val, ok := config.AuthParams["tls"]; ok { + val = strings.TrimSpace(val) + if enableTLS { + return errors.New("unable to set `tls` in both ScaledObject and TriggerAuthentication together") + } + switch val { + case stringEnable: + enableTLS = true + case stringDisable: + enableTLS = false + default: + return fmt.Errorf("error incorrect TLS value given, got %s", val) + } + } + if enableTLS { + certGiven := config.AuthParams["cert"] != "" + keyGiven := config.AuthParams["key"] != "" + if certGiven && !keyGiven { + return errors.New("key must be provided with cert") + } + if keyGiven && !certGiven { + return errors.New("cert must be provided with key") + } + connInfo.ca = config.AuthParams["ca"] + connInfo.cert = config.AuthParams["cert"] + connInfo.key = config.AuthParams["key"] + if value, found := config.AuthParams["keyPassword"]; found { + connInfo.keyPassword = value + } else { + connInfo.keyPassword = "" + } + } + connInfo.enableTLS = enableTLS + return nil +} + +func parseRedisMetadata(config *ScalerConfig, parserFn redisAddressParser) (*redisMetadata, error) { + connInfo, err := parserFn(config.TriggerMetadata, config.ResolvedEnv, config.AuthParams) + if err != nil { + return nil, err + } + meta := redisMetadata{ + connectionInfo: connInfo, + } + + err = parseTLSConfigIntoConnectionInfo(config, &meta.connectionInfo) + if err != nil { + return nil, err } meta.listLength = defaultListLength @@ -463,7 +510,11 @@ func getRedisClusterClient(ctx context.Context, info redisConnectionInfo) (*redi Password: info.password, } if info.enableTLS { - options.TLSConfig = util.CreateTLSClientConfig(info.unsafeSsl) + tlsConfig, err := util.NewTLSConfigWithPassword(info.cert, info.key, info.keyPassword, info.ca, info.unsafeSsl) + if err != nil { + return nil, err + } + options.TLSConfig = tlsConfig } // confirm if connected @@ -485,7 +536,11 @@ func getRedisSentinelClient(ctx context.Context, info redisConnectionInfo, dbInd MasterName: info.sentinelMaster, } if info.enableTLS { - options.TLSConfig = util.CreateTLSClientConfig(info.unsafeSsl) + tlsConfig, err := util.NewTLSConfigWithPassword(info.cert, info.key, info.keyPassword, info.ca, info.unsafeSsl) + if err != nil { + return nil, err + } + options.TLSConfig = tlsConfig } // confirm if connected @@ -504,7 +559,11 @@ func getRedisClient(ctx context.Context, info redisConnectionInfo, dbIndex int) DB: dbIndex, } if info.enableTLS { - options.TLSConfig = util.CreateTLSClientConfig(info.unsafeSsl) + tlsConfig, err := util.NewTLSConfigWithPassword(info.cert, info.key, info.keyPassword, info.ca, info.unsafeSsl) + if err != nil { + return nil, err + } + options.TLSConfig = tlsConfig } // confirm if connected diff --git a/pkg/scalers/redis_scaler_test.go b/pkg/scalers/redis_scaler_test.go index c822725f13f..e89d245c86b 100644 --- a/pkg/scalers/redis_scaler_test.go +++ b/pkg/scalers/redis_scaler_test.go @@ -23,6 +23,7 @@ type parseRedisMetadataTestData struct { metadata map[string]string isError bool authParams map[string]string + enableTLS bool } type redisMetricIdentifier struct { @@ -33,31 +34,43 @@ type redisMetricIdentifier struct { var testRedisMetadata = []parseRedisMetadataTestData{ // nothing passed - {map[string]string{}, true, map[string]string{}}, + {map[string]string{}, true, map[string]string{}, false}, // properly formed listName - {map[string]string{"listName": "mylist", "listLength": "10", "addressFromEnv": "REDIS_HOST", "passwordFromEnv": "REDIS_PASSWORD"}, false, map[string]string{}}, + {map[string]string{"listName": "mylist", "listLength": "10", "addressFromEnv": "REDIS_HOST", "passwordFromEnv": "REDIS_PASSWORD"}, false, map[string]string{}, false}, // properly formed hostPort - {map[string]string{"listName": "mylist", "listLength": "10", "hostFromEnv": "REDIS_HOST", "portFromEnv": "REDIS_PORT", "passwordFromEnv": "REDIS_PASSWORD"}, false, map[string]string{}}, + {map[string]string{"listName": "mylist", "listLength": "10", "hostFromEnv": "REDIS_HOST", "portFromEnv": "REDIS_PORT", "passwordFromEnv": "REDIS_PASSWORD"}, false, map[string]string{}, false}, // properly formed hostPort - {map[string]string{"listName": "mylist", "listLength": "10", "addressFromEnv": "REDIS_HOST", "host": "REDIS_HOST", "port": "REDIS_PORT", "passwordFromEnv": "REDIS_PASSWORD"}, false, map[string]string{}}, + {map[string]string{"listName": "mylist", "listLength": "10", "addressFromEnv": "REDIS_HOST", "host": "REDIS_HOST", "port": "REDIS_PORT", "passwordFromEnv": "REDIS_PASSWORD"}, false, map[string]string{}, false}, // improperly formed hostPort - {map[string]string{"listName": "mylist", "listLength": "10", "hostFromEnv": "REDIS_HOST", "passwordFromEnv": "REDIS_PASSWORD"}, true, map[string]string{}}, + {map[string]string{"listName": "mylist", "listLength": "10", "hostFromEnv": "REDIS_HOST", "passwordFromEnv": "REDIS_PASSWORD"}, true, map[string]string{}, false}, // properly formed listName, empty address - {map[string]string{"listName": "mylist", "listLength": "10", "address": "", "password": ""}, true, map[string]string{}}, + {map[string]string{"listName": "mylist", "listLength": "10", "address": "", "password": ""}, true, map[string]string{}, false}, // improperly formed listLength - {map[string]string{"listName": "mylist", "listLength": "AA", "addressFromEnv": "REDIS_HOST", "password": ""}, true, map[string]string{}}, + {map[string]string{"listName": "mylist", "listLength": "AA", "addressFromEnv": "REDIS_HOST", "password": ""}, true, map[string]string{}, false}, // improperly formed activationListLength - {map[string]string{"listName": "mylist", "listLength": "1", "activationListLength": "AA", "addressFromEnv": "REDIS_HOST", "password": ""}, true, map[string]string{}}, + {map[string]string{"listName": "mylist", "listLength": "1", "activationListLength": "AA", "addressFromEnv": "REDIS_HOST", "password": ""}, true, map[string]string{}, false}, // address does not resolve - {map[string]string{"listName": "mylist", "listLength": "0", "addressFromEnv": "REDIS_WRONG", "password": ""}, true, map[string]string{}}, + {map[string]string{"listName": "mylist", "listLength": "0", "addressFromEnv": "REDIS_WRONG", "password": ""}, true, map[string]string{}, false}, // password is defined in the authParams - {map[string]string{"listName": "mylist", "listLength": "0", "addressFromEnv": "REDIS_WRONG"}, true, map[string]string{"password": ""}}, + {map[string]string{"listName": "mylist", "listLength": "0", "addressFromEnv": "REDIS_WRONG"}, true, map[string]string{"password": ""}, false}, // address is defined in the authParams - {map[string]string{"listName": "mylist", "listLength": "0"}, false, map[string]string{"address": "localhost:6379"}}, + {map[string]string{"listName": "mylist", "listLength": "0"}, false, map[string]string{"address": "localhost:6379"}, false}, // host and port is defined in the authParams - {map[string]string{"listName": "mylist", "listLength": "0"}, false, map[string]string{"host": "localhost", "port": "6379"}}, + {map[string]string{"listName": "mylist", "listLength": "0"}, false, map[string]string{"host": "localhost", "port": "6379"}, false}, + // enableTLS, TLS defined in the authParams only + {map[string]string{"listName": "mylist", "listLength": "0"}, false, map[string]string{"address": "localhost:6379", "tls": "enable", "ca": "caaa", "cert": "ceert", "key": "keey"}, true}, + // enableTLS, TLS cert/key and assumed public CA + {map[string]string{"listName": "mylist", "listLength": "0"}, false, map[string]string{"address": "localhost:6379", "tls": "enable", "cert": "ceert", "key": "keey"}, true}, + // enableTLS, TLS cert/key + key password and assumed public CA + {map[string]string{"listName": "mylist", "listLength": "0"}, false, map[string]string{"address": "localhost:6379", "tls": "enable", "cert": "ceert", "key": "keey", "keyPassword": "keeyPassword"}, true}, + // enableTLS, TLS CA only + {map[string]string{"listName": "mylist", "listLength": "0"}, false, map[string]string{"address": "localhost:6379", "tls": "enable", "ca": "caaa"}, true}, + // enableTLS is enabled by metadata + {map[string]string{"listName": "mylist", "listLength": "0", "enableTLS": "true"}, false, map[string]string{"address": "localhost:6379"}, true}, + // enableTLS is defined both in authParams and metadata + {map[string]string{"listName": "mylist", "listLength": "0", "enableTLS": "true"}, true, map[string]string{"address": "localhost:6379", "tls": "disable"}, true}, // host only is defined in the authParams - {map[string]string{"listName": "mylist", "listLength": "0"}, true, map[string]string{"host": "localhost"}}} + {map[string]string{"listName": "mylist", "listLength": "0"}, true, map[string]string{"host": "localhost"}, false}} var redisMetricIdentifiers = []redisMetricIdentifier{ {&testRedisMetadata[1], 0, "s0-redis-mylist"}, @@ -65,16 +78,36 @@ var redisMetricIdentifiers = []redisMetricIdentifier{ } func TestRedisParseMetadata(t *testing.T) { - testCaseNum := 1 + testCaseNum := 0 for _, testData := range testRedisMetadata { - _, err := parseRedisMetadata(&ScalerConfig{TriggerMetadata: testData.metadata, ResolvedEnv: testRedisResolvedEnv, AuthParams: testData.authParams}, parseRedisAddress) + testCaseNum++ + meta, err := parseRedisMetadata(&ScalerConfig{TriggerMetadata: testData.metadata, ResolvedEnv: testRedisResolvedEnv, AuthParams: testData.authParams}, parseRedisAddress) if err != nil && !testData.isError { t.Errorf("Expected success but got error for unit test # %v", testCaseNum) } if testData.isError && err == nil { t.Errorf("Expected error but got success for unit test #%v", testCaseNum) } - testCaseNum++ + if testData.isError { + continue + } + if meta.connectionInfo.enableTLS != testData.enableTLS { + t.Errorf("Expected enableTLS to be set to %v but got %v for unit test #%v\n", testData.enableTLS, meta.connectionInfo.enableTLS, testCaseNum) + } + if meta.connectionInfo.enableTLS { + if meta.connectionInfo.ca != testData.authParams["ca"] { + t.Errorf("Expected ca to be set to %v but got %v for unit test #%v\n", testData.authParams["ca"], meta.connectionInfo.enableTLS, testCaseNum) + } + if meta.connectionInfo.cert != testData.authParams["cert"] { + t.Errorf("Expected cert to be set to %v but got %v for unit test #%v\n", testData.authParams["cert"], meta.connectionInfo.cert, testCaseNum) + } + if meta.connectionInfo.key != testData.authParams["key"] { + t.Errorf("Expected key to be set to %v but got %v for unit test #%v\n", testData.authParams["key"], meta.connectionInfo.key, testCaseNum) + } + if meta.connectionInfo.keyPassword != testData.authParams["keyPassword"] { + t.Errorf("Expected key to be set to %v but got %v for unit test #%v\n", testData.authParams["keyPassword"], meta.connectionInfo.key, testCaseNum) + } + } } } diff --git a/pkg/scalers/redis_streams_scaler.go b/pkg/scalers/redis_streams_scaler.go index 106c3c0106a..644936950af 100644 --- a/pkg/scalers/redis_streams_scaler.go +++ b/pkg/scalers/redis_streams_scaler.go @@ -265,22 +265,9 @@ func parseRedisStreamsMetadata(config *ScalerConfig, parseFn redisAddressParser) connectionInfo: connInfo, } - meta.connectionInfo.enableTLS = defaultEnableTLS - if val, ok := config.TriggerMetadata["enableTLS"]; ok { - tls, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf("enableTLS parsing error %w", err) - } - meta.connectionInfo.enableTLS = tls - } - - meta.connectionInfo.unsafeSsl = false - if val, ok := config.TriggerMetadata["unsafeSsl"]; ok { - parsedVal, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf("error parsing unsafeSsl: %w", err) - } - meta.connectionInfo.unsafeSsl = parsedVal + err = parseTLSConfigIntoConnectionInfo(config, &meta.connectionInfo) + if err != nil { + return nil, err } if val, ok := config.TriggerMetadata[streamNameMetadata]; ok { diff --git a/pkg/scalers/redis_streams_scaler_test.go b/pkg/scalers/redis_streams_scaler_test.go index 26e67797b4b..36e0a02b842 100644 --- a/pkg/scalers/redis_streams_scaler_test.go +++ b/pkg/scalers/redis_streams_scaler_test.go @@ -737,6 +737,43 @@ func TestParseRedisClusterStreamsMetadata(t *testing.T) { }, wantErr: nil, }, + { + name: "tls in auth param", + metadata: map[string]string{ + "hosts": "a, b, c", + "ports": "1, 2, 3", + "stream": "my-stream", + "pendingEntriesCount": "5", + "consumerGroup": "consumer1", + }, + authParams: map[string]string{ + "password": "password", + "tls": "enable", + "ca": "caaa", + "cert": "ceert", + "key": "keey", + "keyPassword": "keeyPassword", + }, + wantMeta: &redisStreamsMetadata{ + streamName: "my-stream", + targetPendingEntriesCount: 5, + activationLagCount: 0, + consumerGroupName: "consumer1", + connectionInfo: redisConnectionInfo{ + addresses: []string{"a:1", "b:2", "c:3"}, + hosts: []string{"a", "b", "c"}, + ports: []string{"1", "2", "3"}, + password: "password", + enableTLS: true, + ca: "caaa", + cert: "ceert", + key: "keey", + keyPassword: "keeyPassword", + }, + scaleFactor: xPendingFactor, + }, + wantErr: nil, + }, { name: "stream is provided", metadata: map[string]string{