Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenStack Swift scaler #1462

Merged
merged 1 commit into from
Jan 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
210 changes: 210 additions & 0 deletions pkg/scalers/openstack/keystone_authentication.go
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +146 to +155
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems to me that you don't need to make passAuth, passAuth.Properties, passAuth.Properties.Identity, passAuth.Properties.Identity.Password, passAuth.Properties.Identity.Password.User, or passAuth.Properties.Scope references, and then you'll get a default object for all of them just by doing

passAuth := KeystoneAuthMetadata{}

right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know (and tried), since KeystoneAuthMetadata is a struct that has other structs inside that are pointers, they need to be initialized somehow first. Thus,

passAuth := KeystoneAuthMetadata{}

is not sufficient to get a proper KeystoneAuthMetadata default object. Assigning values to it without manually making those references will result in:

runtime error: invalid memory address or nil pointer dereference

Maybe there is a more elegant way to do or another way to initialize these "inner structs", but my limited experience in Go does not allow me to find it out 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess what I meant is maybe non of these pointers need to be a pointer since you never expect them to actually be nil, right? So KeystoreAuthMetadata would be

type KeystoneAuthMetadata struct {
	AuthURL    string       `json:"-"`
	AuthToken  string       `json:"-"`
	HTTPClient *http.Client `json:"-"`
-	Properties *authProps   `json:"auth"`
+	Properties authProps   `json:"auth"`
}

and in turn authProps would be

type authProps struct {
-	Identity *identityProps `json:"identity"`
+	Identity identityProps `json:"identity"`
-	Scope    *scopeProps    `json:"scope,omitempty"`
+	Scope    scopeProps    `json:"scope,omitempty"`
}

and so on.

I think the only difference is in the json.Marshal() behavior where a nil is an empty object but any non-nil object is not empty, but I don't think it makes a difference really. Just thought it would make the code a bit easier to read since you're already initializing all the pointers with new() anyway. No worries if it gives you trouble.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! They do not need to be pointers. The only reason we did that way was because of json.Marshal() as you noticed. Since we use these structs to build a JSON payload for our token request, some of them need to be omitted (and thus be pointers, and then be nil) in order to make it work properly.

Also, we plan to submit new scalers for OpenStack projects soon. As they come, we can study better ways to do it and, eventually, updated the way we originally structured this.

That is nice feedback, anyway @ahmelsayed. Thanks! We'll keep an eye on it 😄


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
}
Loading