From 67d088dbe42ecd1810b31c132e24a3aa68dbadbe Mon Sep 17 00:00:00 2001
From: Pedro Felipe Dominguite
Date: Wed, 16 Dec 2020 11:58:59 -0300
Subject: [PATCH] Add new OpenStack Swift scaler
Signed-off-by: Pedro Felipe Dominguite
* Add Swift scaler unit tests
Signed-off-by: Pedro Felipe Dominguite
* Change scaler name to openstack-swift
Signed-off-by: Pedro Felipe Dominguite
* Change scaler name references to openstackSwift
* Add objectPrefix to metricName
Signed-off-by: Pedro Felipe Dominguite
* Use kedautil for handling http
Signed-off-by: Pedro Felipe Dominguite
* Add read stream error handler
Signed-off-by: Pedro Felipe Dominguite
* Update CHANGELOG.md
Signed-off-by: Pedro Felipe Dominguite
---
CHANGELOG.md | 3 +-
.../openstack/keystone_authentication.go | 210 ++++++++++
pkg/scalers/openstack_swift_scaler.go | 382 ++++++++++++++++++
pkg/scalers/openstack_swift_scaler_test.go | 159 ++++++++
pkg/scaling/scale_handler.go | 2 +
5 files changed, 755 insertions(+), 1 deletion(-)
create mode 100644 pkg/scalers/openstack/keystone_authentication.go
create mode 100644 pkg/scalers/openstack_swift_scaler.go
create mode 100644 pkg/scalers/openstack_swift_scaler_test.go
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05b55da0470..b6f9a151c31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,7 +20,8 @@
- Can use Pod Identity with Azure Event Hub scaler ([#994](https://github.com/kedacore/keda/issues/994))
- Introducing InfluxDB scaler ([#1239](https://github.com/kedacore/keda/issues/1239))
- Add Redis cluster support for Redis list and Redis streams scalers ([#1437](https://github.com/kedacore/keda/pull/1437))
-- Global authentication credentials can be managed using ClusterTriggerAuthentication objects ([#1452](https://github.com/kedacore/keda/pull/1452),[#1486](https://github.com/kedacore/keda/pull/1486))
+- Global authentication credentials can be managed using ClusterTriggerAuthentication objects ([#1452](https://github.com/kedacore/keda/pull/1452))
+- Introducing OpenStack Swift scaler ([#1342](https://github.com/kedacore/keda/issues/1342))
### Improvements
- Support add ScaledJob's label to its job ([#1311](https://github.com/kedacore/keda/issues/1311))
diff --git a/pkg/scalers/openstack/keystone_authentication.go b/pkg/scalers/openstack/keystone_authentication.go
new file mode 100644
index 00000000000..dd798916c13
--- /dev/null
+++ b/pkg/scalers/openstack/keystone_authentication.go
@@ -0,0 +1,210 @@
+package openstack
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "path"
+ "time"
+
+ kedautil "github.com/kedacore/keda/v2/pkg/util"
+)
+
+const tokensEndpoint = "/auth/tokens"
+
+// KeystoneAuthMetadata contains all the necessary metadata for Keystone authentication
+type KeystoneAuthMetadata struct {
+ AuthURL string `json:"-"`
+ AuthToken string `json:"-"`
+ HTTPClient *http.Client `json:"-"`
+ Properties *authProps `json:"auth"`
+}
+
+type authProps struct {
+ Identity *identityProps `json:"identity"`
+ Scope *scopeProps `json:"scope,omitempty"`
+}
+
+type identityProps struct {
+ Methods []string `json:"methods"`
+ Password *passwordProps `json:"password,omitempty"`
+ AppCredential *appCredentialProps `json:"application_credential,omitempty"`
+}
+
+type passwordProps struct {
+ User *userProps `json:"user"`
+}
+
+type appCredentialProps struct {
+ ID string `json:"id"`
+ Secret string `json:"secret"`
+}
+
+type scopeProps struct {
+ Project *projectProps `json:"project"`
+}
+
+type userProps struct {
+ ID string `json:"id"`
+ Password string `json:"password"`
+}
+
+type projectProps struct {
+ ID string `json:"id"`
+}
+
+// GetToken retrieves a token from Keystone
+func (authProps *KeystoneAuthMetadata) GetToken() (string, error) {
+ jsonBody, jsonError := json.Marshal(authProps)
+
+ if jsonError != nil {
+ return "", jsonError
+ }
+
+ body := bytes.NewReader(jsonBody)
+
+ tokenURL, err := url.Parse(authProps.AuthURL)
+
+ if err != nil {
+ return "", fmt.Errorf("the authURL is invalid: %s", err.Error())
+ }
+
+ tokenURL.Path = path.Join(tokenURL.Path, tokensEndpoint)
+
+ getTokenRequest, getTokenRequestError := http.NewRequest("POST", tokenURL.String(), body)
+
+ getTokenRequest.Header.Set("Content-Type", "application/json")
+
+ if getTokenRequestError != nil {
+ return "", getTokenRequestError
+ }
+
+ resp, requestError := authProps.HTTPClient.Do(getTokenRequest)
+
+ if requestError != nil {
+ return "", requestError
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 200 && resp.StatusCode < 300 {
+ authProps.AuthToken = resp.Header["X-Subject-Token"][0]
+ return resp.Header["X-Subject-Token"][0], nil
+ }
+
+ errBody, readBodyErr := ioutil.ReadAll(resp.Body)
+
+ if readBodyErr != nil {
+ return "", readBodyErr
+ }
+
+ return "", fmt.Errorf(string(errBody))
+}
+
+// IsTokenValid checks if a authentication token is valid
+func IsTokenValid(authProps KeystoneAuthMetadata) (bool, error) {
+ token := authProps.AuthToken
+
+ tokenURL, err := url.Parse(authProps.AuthURL)
+
+ if err != nil {
+ return false, fmt.Errorf("the authURL is invalid: %s", err.Error())
+ }
+
+ tokenURL.Path = path.Join(tokenURL.Path, tokensEndpoint)
+
+ checkTokenRequest, checkRequestError := http.NewRequest("HEAD", tokenURL.String(), nil)
+ checkTokenRequest.Header.Set("X-Subject-Token", token)
+ checkTokenRequest.Header.Set("X-Auth-Token", token)
+
+ if checkRequestError != nil {
+ return false, checkRequestError
+ }
+
+ checkResp, requestError := authProps.HTTPClient.Do(checkTokenRequest)
+
+ if requestError != nil {
+ return false, requestError
+ }
+
+ defer checkResp.Body.Close()
+
+ if checkResp.StatusCode >= 400 {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+// NewPasswordAuth creates a struct containing metadata for authentication using password method
+func NewPasswordAuth(authURL string, userID string, userPassword string, projectID string, httpTimeout int) (*KeystoneAuthMetadata, error) {
+ var tokenError error
+
+ passAuth := new(KeystoneAuthMetadata)
+
+ passAuth.Properties = new(authProps)
+
+ passAuth.Properties.Scope = new(scopeProps)
+ passAuth.Properties.Scope.Project = new(projectProps)
+
+ passAuth.Properties.Identity = new(identityProps)
+ passAuth.Properties.Identity.Password = new(passwordProps)
+ passAuth.Properties.Identity.Password.User = new(userProps)
+
+ url, err := url.Parse(authURL)
+
+ if err != nil {
+ return nil, fmt.Errorf("authURL is invalid: %s", err.Error())
+ }
+
+ url.Path = path.Join(url.Path, "")
+
+ passAuth.AuthURL = url.String()
+
+ passAuth.HTTPClient = kedautil.CreateHTTPClient(time.Duration(httpTimeout) * time.Second)
+
+ passAuth.Properties.Identity.Methods = []string{"password"}
+ passAuth.Properties.Identity.Password.User.ID = userID
+ passAuth.Properties.Identity.Password.User.Password = userPassword
+
+ passAuth.Properties.Scope.Project.ID = projectID
+
+ passAuth.AuthToken, tokenError = passAuth.GetToken()
+
+ return passAuth, tokenError
+}
+
+// NewAppCredentialsAuth creates a struct containing metadata for authentication using application credentials method
+func NewAppCredentialsAuth(authURL string, id string, secret string, httpTimeout int) (*KeystoneAuthMetadata, error) {
+ var tokenError error
+
+ appAuth := new(KeystoneAuthMetadata)
+
+ appAuth.Properties = new(authProps)
+
+ appAuth.Properties.Identity = new(identityProps)
+
+ url, err := url.Parse(authURL)
+
+ if err != nil {
+ return nil, fmt.Errorf("authURL is invalid: %s", err.Error())
+ }
+
+ url.Path = path.Join(url.Path, "")
+
+ appAuth.AuthURL = url.String()
+
+ appAuth.HTTPClient = kedautil.CreateHTTPClient(time.Duration(httpTimeout) * time.Second)
+
+ appAuth.Properties.Identity.AppCredential = new(appCredentialProps)
+ appAuth.Properties.Identity.Methods = []string{"application_credential"}
+ appAuth.Properties.Identity.AppCredential.ID = id
+ appAuth.Properties.Identity.AppCredential.Secret = secret
+
+ appAuth.AuthToken, tokenError = appAuth.GetToken()
+
+ return appAuth, tokenError
+}
diff --git a/pkg/scalers/openstack_swift_scaler.go b/pkg/scalers/openstack_swift_scaler.go
new file mode 100644
index 00000000000..256838b0f33
--- /dev/null
+++ b/pkg/scalers/openstack_swift_scaler.go
@@ -0,0 +1,382 @@
+package scalers
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "path"
+ "strconv"
+ "strings"
+
+ "github.com/kedacore/keda/v2/pkg/scalers/openstack"
+ kedautil "github.com/kedacore/keda/v2/pkg/util"
+ v2beta2 "k8s.io/api/autoscaling/v2beta2"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/metrics/pkg/apis/external_metrics"
+
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+const (
+ defaultOnlyFiles = false
+ defaultObjectCount = 2
+ defaultObjectLimit = ""
+ defaultObjectPrefix = ""
+ defaultObjectDelimiter = ""
+ defaultHTTPClientTimeout = 30
+)
+
+type openstackSwiftMetadata struct {
+ swiftURL string
+ containerName string
+ objectCount int
+ objectPrefix string
+ objectDelimiter string
+ objectLimit string
+ httpClientTimeout int
+ onlyFiles bool
+}
+
+type openstackSwiftAuthenticationMetadata struct {
+ userID string
+ password string
+ projectID string
+ authURL string
+ appCredentialID string
+ appCredentialSecret string
+}
+
+type openstackSwiftScaler struct {
+ metadata *openstackSwiftMetadata
+ authMetadata *openstack.KeystoneAuthMetadata
+}
+
+var openstackSwiftLog = logf.Log.WithName("openstack_swift_scaler")
+
+func (s *openstackSwiftScaler) getOpenstackSwiftContainerObjectCount() (int, error) {
+ var token string
+ var swiftURL string = s.metadata.swiftURL
+ var containerName string = s.metadata.containerName
+
+ isValid, validationError := openstack.IsTokenValid(*s.authMetadata)
+
+ if validationError != nil {
+ openstackSwiftLog.Error(validationError, "scaler could not validate the token for authentication")
+ return 0, validationError
+ }
+
+ if !isValid {
+ var tokenRequestError error
+ token, tokenRequestError = s.authMetadata.GetToken()
+ s.authMetadata.AuthToken = token
+ if tokenRequestError != nil {
+ openstackSwiftLog.Error(tokenRequestError, "error requesting token for authentication")
+ return 0, tokenRequestError
+ }
+ }
+
+ token = s.authMetadata.AuthToken
+
+ swiftContainerURL, err := url.Parse(swiftURL)
+
+ if err != nil {
+ openstackSwiftLog.Error(err, fmt.Sprintf("the swiftURL is invalid: %s. You might have forgotten to provide the either 'http' or 'https' in the URL. Check our documentation to see if you missed something", swiftURL))
+ return 0, fmt.Errorf("the swiftURL is invalid: %s", err.Error())
+ }
+
+ swiftContainerURL.Path = path.Join(swiftContainerURL.Path, containerName)
+
+ swiftRequest, _ := http.NewRequest("GET", swiftContainerURL.String(), nil)
+
+ swiftRequest.Header.Set("X-Auth-Token", token)
+
+ query := swiftRequest.URL.Query()
+ query.Add("prefix", s.metadata.objectPrefix)
+ query.Add("delimiter", s.metadata.objectDelimiter)
+
+ // If scaler wants to scale based on only files, we first need to query all objects, then filter files and finally limit the result to the specified query limit
+ if !s.metadata.onlyFiles {
+ query.Add("limit", s.metadata.objectLimit)
+ }
+
+ swiftRequest.URL.RawQuery = query.Encode()
+
+ resp, requestError := s.authMetadata.HTTPClient.Do(swiftRequest)
+
+ if requestError != nil {
+ openstackSwiftLog.Error(requestError, fmt.Sprintf("error getting metrics for container '%s'. You probably specified the wrong swift URL or the URL is not reachable", containerName))
+ return 0, requestError
+ }
+
+ defer resp.Body.Close()
+
+ body, readError := ioutil.ReadAll(resp.Body)
+
+ if readError != nil {
+ openstackSwiftLog.Error(readError, "could not read response body from Swift API")
+ return 0, readError
+ }
+ if resp.StatusCode >= 200 && resp.StatusCode < 300 {
+ var objectsList = strings.Split(strings.TrimSpace(string(body)), "\n")
+
+ // If onlyFiles is set to "true", return the total amount of files (excluding empty objects/folders)
+ if s.metadata.onlyFiles {
+ var count int = 0
+ for i := 0; i < len(objectsList); i++ {
+ if !strings.HasSuffix(objectsList[i], "/") {
+ count++
+ }
+ }
+
+ if s.metadata.objectLimit != defaultObjectLimit {
+ objectLimit, conversionError := strconv.Atoi(s.metadata.objectLimit)
+
+ if conversionError != nil {
+ openstackSwiftLog.Error(err, fmt.Sprintf("the objectLimit value provided is invalid: %v", s.metadata.objectLimit))
+ return 0, conversionError
+ }
+
+ if objectLimit <= count && s.metadata.objectLimit != defaultObjectLimit {
+ return objectLimit, nil
+ }
+ }
+
+ return count, nil
+ }
+
+ // Otherwise, if either prefix and/or delimiter are provided, return the total amount of objects
+ if s.metadata.objectPrefix != defaultObjectPrefix || s.metadata.objectDelimiter != defaultObjectDelimiter {
+ return len(objectsList), nil
+ }
+
+ // Finally, if nothing is set, return the standard total amount of objects inside the container
+ objectCount, conversionError := strconv.Atoi(resp.Header["X-Container-Object-Count"][0])
+ return objectCount, conversionError
+ }
+
+ if resp.StatusCode == 401 {
+ openstackSwiftLog.Error(nil, "the retrieved token is not a valid token. Provide the correct auth credentials so the scaler can retrieve a valid access token (Unauthorized)")
+ return 0, fmt.Errorf("the retrieved token is not a valid token. Provide the correct auth credentials so the scaler can retrieve a valid access token (Unauthorized)")
+ }
+
+ if resp.StatusCode == 403 {
+ openstackSwiftLog.Error(nil, "the retrieved token is a valid token, but it does not have sufficient permission to retrieve Swift and/or container metadata (Forbidden)")
+ return 0, fmt.Errorf("the retrieved token is a valid token, but it does not have sufficient permission to retrieve Swift and/or container metadata (Forbidden)")
+ }
+
+ if resp.StatusCode == 404 {
+ openstackSwiftLog.Error(nil, fmt.Sprintf("the container '%s' does not exist (Not Found)", containerName))
+ return 0, fmt.Errorf("the container '%s' does not exist (Not Found)", containerName)
+ }
+
+ return 0, fmt.Errorf(string(body))
+}
+
+// NewOpenstackSwiftScaler creates a new swift scaler
+func NewOpenstackSwiftScaler(config *ScalerConfig) (Scaler, error) {
+ var keystoneAuth *openstack.KeystoneAuthMetadata
+
+ openstackSwiftMetadata, err := parseOpenstackSwiftMetadata(config)
+
+ if err != nil {
+ return nil, fmt.Errorf("error parsing swift metadata: %s", err)
+ }
+
+ authMetadata, err := parseOpenstackSwiftAuthenticationMetadata(config)
+
+ if err != nil {
+ return nil, fmt.Errorf("error parsing swift authentication metadata: %s", err)
+ }
+
+ // User chose the "application_credentials" authentication method
+ if authMetadata.appCredentialID != "" {
+ keystoneAuth, err = openstack.NewAppCredentialsAuth(authMetadata.authURL, authMetadata.appCredentialID, authMetadata.appCredentialSecret, openstackSwiftMetadata.httpClientTimeout)
+ if err != nil {
+ return nil, fmt.Errorf("error getting openstack credentials for application credentials method: %s", err)
+ }
+ } else {
+ // User chose the "password" authentication method
+ if authMetadata.userID != "" {
+ keystoneAuth, err = openstack.NewPasswordAuth(authMetadata.authURL, authMetadata.userID, authMetadata.password, authMetadata.projectID, openstackSwiftMetadata.httpClientTimeout)
+ if err != nil {
+ return nil, fmt.Errorf("error getting openstack credentials for password method: %s", err)
+ }
+ } else {
+ return nil, fmt.Errorf("no authentication method was provided for OpenStack")
+ }
+ }
+
+ return &openstackSwiftScaler{
+ metadata: openstackSwiftMetadata,
+ authMetadata: keystoneAuth,
+ }, nil
+}
+
+func parseOpenstackSwiftMetadata(config *ScalerConfig) (*openstackSwiftMetadata, error) {
+ meta := openstackSwiftMetadata{}
+
+ if val, ok := config.TriggerMetadata["swiftURL"]; ok {
+ meta.swiftURL = val
+ } else {
+ return nil, fmt.Errorf("no swiftURL given")
+ }
+
+ if val, ok := config.TriggerMetadata["containerName"]; ok {
+ meta.containerName = val
+ } else {
+ return nil, fmt.Errorf("no containerName was provided")
+ }
+
+ if val, ok := config.TriggerMetadata["objectCount"]; ok {
+ targetObjectCount, err := strconv.Atoi(val)
+ if err != nil {
+ return nil, fmt.Errorf("objectCount parsing error: %s", err.Error())
+ }
+ meta.objectCount = targetObjectCount
+ } else {
+ meta.objectCount = defaultObjectCount
+ }
+
+ if val, ok := config.TriggerMetadata["objectPrefix"]; ok {
+ meta.objectPrefix = val
+ } else {
+ meta.objectPrefix = defaultObjectPrefix
+ }
+
+ if val, ok := config.TriggerMetadata["objectDelimiter"]; ok {
+ meta.objectDelimiter = val
+ } else {
+ meta.objectDelimiter = defaultObjectDelimiter
+ }
+
+ if val, ok := config.TriggerMetadata["timeout"]; ok {
+ httpClientTimeout, err := strconv.Atoi(val)
+ if err != nil {
+ return nil, fmt.Errorf("httpClientTimeout parsing error: %s", err.Error())
+ }
+ meta.httpClientTimeout = httpClientTimeout
+ } else {
+ meta.httpClientTimeout = defaultHTTPClientTimeout
+ }
+
+ if val, ok := config.TriggerMetadata["onlyFiles"]; ok {
+ isOnlyFiles, conversionError := strconv.ParseBool(val)
+ if conversionError != nil {
+ return nil, fmt.Errorf("onlyFiles parsing error: %s", conversionError.Error())
+ }
+ meta.onlyFiles = isOnlyFiles
+ } else {
+ meta.onlyFiles = defaultOnlyFiles
+ }
+
+ if val, ok := config.TriggerMetadata["objectLimit"]; ok {
+ meta.objectLimit = val
+ } else {
+ meta.objectLimit = defaultObjectLimit
+ }
+
+ return &meta, nil
+}
+
+func parseOpenstackSwiftAuthenticationMetadata(config *ScalerConfig) (*openstackSwiftAuthenticationMetadata, error) {
+ authMeta := openstackSwiftAuthenticationMetadata{}
+
+ if config.AuthParams["authURL"] != "" {
+ authMeta.authURL = config.AuthParams["authURL"]
+ } else {
+ return nil, fmt.Errorf("authURL doesn't exist in the authParams")
+ }
+
+ if config.AuthParams["userID"] != "" {
+ authMeta.userID = config.AuthParams["userID"]
+
+ if config.AuthParams["password"] != "" {
+ authMeta.password = config.AuthParams["password"]
+ } else {
+ return nil, fmt.Errorf("password doesn't exist in the authParams")
+ }
+
+ if config.AuthParams["projectID"] != "" {
+ authMeta.projectID = config.AuthParams["projectID"]
+ } else {
+ return nil, fmt.Errorf("projectID doesn't exist in the authParams")
+ }
+ } else {
+ if config.AuthParams["appCredentialID"] != "" {
+ authMeta.appCredentialID = config.AuthParams["appCredentialID"]
+
+ if config.AuthParams["appCredentialSecret"] != "" {
+ authMeta.appCredentialSecret = config.AuthParams["appCredentialSecret"]
+ } else {
+ return nil, fmt.Errorf("appCredentialSecret doesn't exist in the authParams")
+ }
+ } else {
+ return nil, fmt.Errorf("neither userID or appCredentialID exist in the authParams")
+ }
+ }
+
+ return &authMeta, nil
+}
+
+func (s *openstackSwiftScaler) IsActive(ctx context.Context) (bool, error) {
+ objectCount, err := s.getOpenstackSwiftContainerObjectCount()
+
+ if err != nil {
+ return false, err
+ }
+
+ return objectCount > 0, nil
+}
+
+func (s *openstackSwiftScaler) Close() error {
+ return nil
+}
+
+func (s *openstackSwiftScaler) GetMetrics(ctx context.Context, metricName string, metricSelector labels.Selector) ([]external_metrics.ExternalMetricValue, error) {
+ objectCount, err := s.getOpenstackSwiftContainerObjectCount()
+
+ if err != nil {
+ openstackSwiftLog.Error(err, "error getting objectCount")
+ return []external_metrics.ExternalMetricValue{}, err
+ }
+
+ metric := external_metrics.ExternalMetricValue{
+ MetricName: metricName,
+ Value: *resource.NewQuantity(int64(objectCount), resource.DecimalSI),
+ Timestamp: metav1.Now(),
+ }
+
+ return append([]external_metrics.ExternalMetricValue{}, metric), nil
+}
+
+func (s *openstackSwiftScaler) GetMetricSpecForScaling() []v2beta2.MetricSpec {
+ targetObjectCount := resource.NewQuantity(int64(s.metadata.objectCount), resource.DecimalSI)
+
+ var metricName string
+
+ if s.metadata.objectPrefix != "" {
+ metricName = fmt.Sprintf("%s-%s", s.metadata.containerName, s.metadata.objectPrefix)
+ } else {
+ metricName = s.metadata.containerName
+ }
+
+ externalMetric := &v2beta2.ExternalMetricSource{
+ Metric: v2beta2.MetricIdentifier{
+ Name: kedautil.NormalizeString(fmt.Sprintf("%s-%s", "openstack-swift", metricName)),
+ },
+ Target: v2beta2.MetricTarget{
+ Type: v2beta2.AverageValueMetricType,
+ AverageValue: targetObjectCount,
+ },
+ }
+
+ metricSpec := v2beta2.MetricSpec{
+ External: externalMetric, Type: externalMetricType,
+ }
+
+ return []v2beta2.MetricSpec{metricSpec}
+}
diff --git a/pkg/scalers/openstack_swift_scaler_test.go b/pkg/scalers/openstack_swift_scaler_test.go
new file mode 100644
index 00000000000..2f5b3919bc3
--- /dev/null
+++ b/pkg/scalers/openstack_swift_scaler_test.go
@@ -0,0 +1,159 @@
+package scalers
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type parseOpenstackSwiftMetadataTestData struct {
+ metadata map[string]string
+}
+
+type parseOpenstackSwiftAuthMetadataTestData struct {
+ authMetadata map[string]string
+}
+
+type openstackSwiftMetricIdentifier struct {
+ resolvedEnv map[string]string
+ metadataTestData *parseOpenstackSwiftMetadataTestData
+ authMetadataTestData *parseOpenstackSwiftAuthMetadataTestData
+ name string
+}
+
+var openstackSwiftMetadataTestData = []parseOpenstackSwiftMetadataTestData{
+ // Only required parameters
+ {metadata: map[string]string{"swiftURL": "http://localhost:8080/v1/my-account-id", "containerName": "my-container"}},
+ // Adding objectCount
+ {metadata: map[string]string{"swiftURL": "http://localhost:8080/v1/my-account-id", "containerName": "my-container", "objectCount": "5"}},
+ // Adding objectPrefix
+ {metadata: map[string]string{"swiftURL": "http://localhost:8080/v1/my-account-id", "containerName": "my-container", "objectCount": "5", "objectPrefix": "my-prefix"}},
+ // Adding objectDelimiter
+ {metadata: map[string]string{"swiftURL": "http://localhost:8080/v1/my-account-id", "containerName": "my-container", "objectCount": "5", "objectDelimiter": "/"}},
+ // Adding objectLimit
+ {metadata: map[string]string{"swiftURL": "http://localhost:8080/v1/my-account-id", "containerName": "my-container", "objectCount": "5", "objectLimit": "1000"}},
+ // Adding timeout
+ {metadata: map[string]string{"swiftURL": "http://localhost:8080/v1/my-account-id", "containerName": "my-container", "objectCount": "5", "timeout": "2"}},
+ // Adding onlyFiles
+ {metadata: map[string]string{"swiftURL": "http://localhost:8080/v1/my-account-id", "containerName": "my-container", "onlyFiles": "true"}},
+}
+
+var openstackSwiftAuthMetadataTestData = []parseOpenstackSwiftAuthMetadataTestData{
+ {authMetadata: map[string]string{"userID": "my-id", "password": "my-password", "projectID": "my-project-id", "authURL": "http://localhost:5000/v3/"}},
+ {authMetadata: map[string]string{"appCredentialID": "my-app-credential-id", "appCredentialSecret": "my-app-credential-secret", "authURL": "http://localhost:5000/v3/"}},
+}
+
+var invalidOpenstackSwiftMetadataTestData = []parseOpenstackSwiftMetadataTestData{
+ // Missing swiftURL
+ {metadata: map[string]string{"containerName": "my-container", "objectCount": "5"}},
+ // Missing containerName
+ {metadata: map[string]string{"swiftURL": "http://localhost:8080/v1/my-account-id", "objectCount": "5"}},
+ // objectCount is not an integer value
+ {metadata: map[string]string{"containerName": "my-container", "swiftURL": "http://localhost:8080/v1/my-account-id", "objectCount": "5.5"}},
+ // timeout is not an integer value
+ {metadata: map[string]string{"containerName": "my-container", "swiftURL": "http://localhost:8080/v1/my-account-id", "objectCount": "5", "timeout": "2.5"}},
+ // onlyFiles is not a boolean value
+ {metadata: map[string]string{"containerName": "my-container", "swiftURL": "http://localhost:8080/v1/my-account-id", "objectCount": "5", "onlyFiles": "yes"}},
+}
+
+var invalidOpenstackSwiftAuthMetadataTestData = []parseOpenstackSwiftAuthMetadataTestData{
+ // Using Password method:
+
+ // Missing userID
+ {authMetadata: map[string]string{"password": "my-password", "projectID": "my-project-id", "authURL": "http://localhost:5000/v3/"}},
+ // Missing password
+ {authMetadata: map[string]string{"userID": "my-id", "projectID": "my-project-id", "authURL": "http://localhost:5000/v3/"}},
+ // Missing projectID
+ {authMetadata: map[string]string{"userID": "my-id", "password": "my-password", "authURL": "http://localhost:5000/v3/"}},
+ // Missing authURL
+ {authMetadata: map[string]string{"userID": "my-id", "password": "my-password", "projectID": "my-project-id"}},
+
+ // Using Application Credentials method:
+
+ // Missing appCredentialID
+ {authMetadata: map[string]string{"appCredentialSecret": "my-app-credential-secret", "authURL": "http://localhost:5000/v3/"}},
+ // Missing appCredentialSecret
+ {authMetadata: map[string]string{"appCredentialID": "my-app-credential-id", "authURL": "http://localhost:5000/v3/"}},
+ // Missing authURL
+ {authMetadata: map[string]string{"appCredentialID": "my-app-credential-id", "appCredentialSecret": "my-app-credential-secret"}},
+}
+
+func TestOpenstackSwiftGetMetricSpecForScaling(t *testing.T) {
+ testCases := []openstackSwiftMetricIdentifier{
+ {nil, &openstackSwiftMetadataTestData[0], &openstackSwiftAuthMetadataTestData[0], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[1], &openstackSwiftAuthMetadataTestData[0], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[2], &openstackSwiftAuthMetadataTestData[0], "openstack-swift-my-container-my-prefix"},
+ {nil, &openstackSwiftMetadataTestData[3], &openstackSwiftAuthMetadataTestData[0], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[4], &openstackSwiftAuthMetadataTestData[0], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[5], &openstackSwiftAuthMetadataTestData[0], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[6], &openstackSwiftAuthMetadataTestData[0], "openstack-swift-my-container"},
+
+ {nil, &openstackSwiftMetadataTestData[0], &openstackSwiftAuthMetadataTestData[1], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[1], &openstackSwiftAuthMetadataTestData[1], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[2], &openstackSwiftAuthMetadataTestData[1], "openstack-swift-my-container-my-prefix"},
+ {nil, &openstackSwiftMetadataTestData[3], &openstackSwiftAuthMetadataTestData[1], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[4], &openstackSwiftAuthMetadataTestData[1], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[5], &openstackSwiftAuthMetadataTestData[1], "openstack-swift-my-container"},
+ {nil, &openstackSwiftMetadataTestData[6], &openstackSwiftAuthMetadataTestData[1], "openstack-swift-my-container"},
+ }
+
+ for _, testData := range testCases {
+ testData := testData
+ meta, err := parseOpenstackSwiftMetadata(&ScalerConfig{ResolvedEnv: testData.resolvedEnv, TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.authMetadataTestData.authMetadata})
+ if err != nil {
+ t.Fatal("Could not parse metadata:", err)
+ }
+ _, err = parseOpenstackSwiftAuthenticationMetadata(&ScalerConfig{ResolvedEnv: testData.resolvedEnv, TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.authMetadataTestData.authMetadata})
+ if err != nil {
+ t.Fatal("Could not parse auth metadata:", err)
+ }
+
+ mockSwiftScaler := openstackSwiftScaler{meta, nil}
+
+ metricSpec := mockSwiftScaler.GetMetricSpecForScaling()
+
+ metricName := metricSpec[0].External.Metric.Name
+
+ if metricName != testData.name {
+ t.Error("Wrong External metric source name:", metricName)
+ }
+ }
+}
+
+func TestParseOpenstackSwiftMetadataForInvalidCases(t *testing.T) {
+ testCases := []openstackSwiftMetricIdentifier{
+ {nil, &invalidOpenstackSwiftMetadataTestData[0], &parseOpenstackSwiftAuthMetadataTestData{}, "missing swiftURL"},
+ {nil, &invalidOpenstackSwiftMetadataTestData[1], &parseOpenstackSwiftAuthMetadataTestData{}, "missing containerName"},
+ {nil, &invalidOpenstackSwiftMetadataTestData[2], &parseOpenstackSwiftAuthMetadataTestData{}, "objectCount is not an integer value"},
+ {nil, &invalidOpenstackSwiftMetadataTestData[3], &parseOpenstackSwiftAuthMetadataTestData{}, "onlyFiles is not a boolean value"},
+ {nil, &invalidOpenstackSwiftMetadataTestData[4], &parseOpenstackSwiftAuthMetadataTestData{}, "timeout is not an integer value"},
+ }
+
+ for _, testData := range testCases {
+ testData := testData
+ t.Run(testData.name, func(pt *testing.T) {
+ _, err := parseOpenstackSwiftMetadata(&ScalerConfig{ResolvedEnv: testData.resolvedEnv, TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.authMetadataTestData.authMetadata})
+ assert.NotNil(t, err)
+ })
+ }
+}
+
+func TestParseOpenstackSwiftAuthenticationMetadataForInvalidCases(t *testing.T) {
+ testCases := []openstackSwiftMetricIdentifier{
+ {nil, &parseOpenstackSwiftMetadataTestData{}, &invalidOpenstackSwiftAuthMetadataTestData[0], "missing userID"},
+ {nil, &parseOpenstackSwiftMetadataTestData{}, &invalidOpenstackSwiftAuthMetadataTestData[1], "missing password"},
+ {nil, &parseOpenstackSwiftMetadataTestData{}, &invalidOpenstackSwiftAuthMetadataTestData[2], "missing projectID"},
+ {nil, &parseOpenstackSwiftMetadataTestData{}, &invalidOpenstackSwiftAuthMetadataTestData[3], "missing authURL for password method"},
+ {nil, &parseOpenstackSwiftMetadataTestData{}, &invalidOpenstackSwiftAuthMetadataTestData[4], "missing appCredentialID"},
+ {nil, &parseOpenstackSwiftMetadataTestData{}, &invalidOpenstackSwiftAuthMetadataTestData[5], "missing appCredentialSecret"},
+ {nil, &parseOpenstackSwiftMetadataTestData{}, &invalidOpenstackSwiftAuthMetadataTestData[6], "missing authURL for application credentials method"},
+ }
+
+ for _, testData := range testCases {
+ testData := testData
+ t.Run(testData.name, func(pt *testing.T) {
+ _, err := parseOpenstackSwiftAuthenticationMetadata(&ScalerConfig{ResolvedEnv: testData.resolvedEnv, TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.authMetadataTestData.authMetadata})
+ assert.NotNil(t, err)
+ })
+ }
+}
diff --git a/pkg/scaling/scale_handler.go b/pkg/scaling/scale_handler.go
index 2eea32ce7ff..8801e8e578c 100644
--- a/pkg/scaling/scale_handler.go
+++ b/pkg/scaling/scale_handler.go
@@ -476,6 +476,8 @@ func buildScaler(triggerType string, config *scalers.ScalerConfig) (scalers.Scal
return scalers.NewMetricsAPIScaler(config)
case "mysql":
return scalers.NewMySQLScaler(config)
+ case "openstack-swift":
+ return scalers.NewOpenstackSwiftScaler(config)
case "postgresql":
return scalers.NewPostgreSQLScaler(config)
case "prometheus":