diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d48d9fe0b5..7b92ef6a06a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ To learn more about our roadmap, we recommend reading [this document](ROADMAP.md ### Improvements - **General:** Use more readable timestamps in KEDA Operator logs ([#3066](https://github.com/kedacore/keda/issue/3066)) +- **Selenium Grid Scaler:** Edge active sessions not being properly counted ([#2709](https://github.com/kedacore/keda/issues/2709)) +- **Selenium Grid Scaler:** Max Sessions implementation issue ([#3061](https://github.com/kedacore/keda/issues/3061)) ### Deprecations diff --git a/pkg/scalers/selenium_grid_scaler.go b/pkg/scalers/selenium_grid_scaler.go index c5675c07da1..abffbcb57ab 100644 --- a/pkg/scalers/selenium_grid_scaler.go +++ b/pkg/scalers/selenium_grid_scaler.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io/ioutil" + "math" "net/http" "strconv" "strings" @@ -28,12 +29,13 @@ type seleniumGridScaler struct { } type seleniumGridScalerMetadata struct { - url string - browserName string - targetValue int64 - browserVersion string - unsafeSsl bool - scalerIndex int + url string + browserName string + sessionBrowserName string + targetValue int64 + browserVersion string + unsafeSsl bool + scalerIndex int } type seleniumResponse struct { @@ -47,6 +49,7 @@ type data struct { type grid struct { MaxSession int `json:"maxSession"` + NodeCount int `json:"nodeCount"` } type sessionsInfo struct { @@ -109,6 +112,12 @@ func parseSeleniumGridScalerMetadata(config *ScalerConfig) (*seleniumGridScalerM return nil, fmt.Errorf("no browser name given in metadata") } + if val, ok := config.TriggerMetadata["sessionBrowserName"]; ok { + meta.sessionBrowserName = val + } else { + meta.sessionBrowserName = meta.browserName + } + if val, ok := config.TriggerMetadata["browserVersion"]; ok && val != "" { meta.browserVersion = val } else { @@ -172,7 +181,7 @@ func (s *seleniumGridScaler) IsActive(ctx context.Context) (bool, error) { func (s *seleniumGridScaler) getSessionsCount(ctx context.Context) (int64, error) { body, err := json.Marshal(map[string]string{ - "query": "{ grid { maxSession }, sessionsInfo { sessionQueueRequests, sessions { id, capabilities, nodeId } } }", + "query": "{ grid { maxSession, nodeCount }, sessionsInfo { sessionQueueRequests, sessions { id, capabilities, nodeId } } }", }) if err != nil { @@ -199,14 +208,14 @@ func (s *seleniumGridScaler) getSessionsCount(ctx context.Context) (int64, error if err != nil { return -1, err } - v, err := getCountFromSeleniumResponse(b, s.metadata.browserName, s.metadata.browserVersion) + v, err := getCountFromSeleniumResponse(b, s.metadata.browserName, s.metadata.browserVersion, s.metadata.sessionBrowserName) if err != nil { return -1, err } return v, nil } -func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion string) (int64, error) { +func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion string, sessionBrowserName string) (int64, error) { var count int64 var seleniumResponse = seleniumResponse{} @@ -221,7 +230,7 @@ func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion s if capability.BrowserName == browserName { if strings.HasPrefix(capability.BrowserVersion, browserVersion) { count++ - } else if capability.BrowserVersion == "" && browserVersion == DefaultBrowserVersion { + } else if browserVersion == DefaultBrowserVersion { count++ } } @@ -234,7 +243,7 @@ func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion s for _, session := range sessions { var capability = capability{} if err := json.Unmarshal([]byte(session.Capabilities), &capability); err == nil { - if capability.BrowserName == browserName { + if capability.BrowserName == sessionBrowserName { if strings.HasPrefix(capability.BrowserVersion, browserVersion) { count++ } else if browserVersion == DefaultBrowserVersion { @@ -247,10 +256,12 @@ func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion s } var gridMaxSession = int64(seleniumResponse.Data.Grid.MaxSession) + var gridNodeCount = int64(seleniumResponse.Data.Grid.NodeCount) - if gridMaxSession > 0 { - count = (count + gridMaxSession - 1) / gridMaxSession + if gridMaxSession > 0 && gridNodeCount > 0 { + // Get count, convert count to next highest int64 + var floatCount = float64(count) / (float64(gridMaxSession) / float64(gridNodeCount)) + count = int64(math.Ceil(floatCount)) } - return count, nil } diff --git a/pkg/scalers/selenium_grid_scaler_test.go b/pkg/scalers/selenium_grid_scaler_test.go index 1a6d50dcfe3..d960cc1224d 100644 --- a/pkg/scalers/selenium_grid_scaler_test.go +++ b/pkg/scalers/selenium_grid_scaler_test.go @@ -7,9 +7,10 @@ import ( func Test_getCountFromSeleniumResponse(t *testing.T) { type args struct { - b []byte - browserName string - browserVersion string + b []byte + browserName string + sessionBrowserName string + browserVersion string } tests := []struct { name string @@ -18,7 +19,7 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { wantErr bool }{ { - name: "nil response body should through error", + name: "nil response body should throw error", args: args{ b: []byte(nil), browserName: "", @@ -27,7 +28,7 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { wantErr: true, }, { - name: "empty response body should through error", + name: "empty response body should throw error", args: args{ b: []byte(""), browserName: "", @@ -41,7 +42,8 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { b: []byte(`{ "data": { "grid":{ - "maxSession": 1 + "maxSession": 0, + "nodeCount": 0 }, "sessionsInfo": { "sessionQueueRequests": [], @@ -60,7 +62,8 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { b: []byte(`{ "data": { "grid":{ - "maxSession": 1 + "maxSession": 1, + "nodeCount": 1 }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], @@ -74,8 +77,9 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { } } }`), - browserName: "", - browserVersion: "latest", + browserName: "", + sessionBrowserName: "", + browserVersion: "latest", }, want: 0, wantErr: false, @@ -86,7 +90,8 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { b: []byte(`{ "data": { "grid":{ - "maxSession": 1 + "maxSession": 1, + "nodeCount": 1 }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], @@ -94,65 +99,120 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { } } }`), - browserName: "chrome", - browserVersion: "latest", + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", }, want: 2, wantErr: false, }, { - name: "active sessions with matching browsername and maxSession=2 should return count as 1", + name: "2 active sessions with matching browsername on 2 nodes and maxSession=4 should return count as 3 (rounded up from 2.5)", args: args{ b: []byte(`{ "data": { "grid":{ - "maxSession": 2 + "maxSession": 4, + "nodeCount": 2 + }, + "sessionsInfo": { + "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], + "sessions": [ + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0.4472.114\",\n \"chrome\": {\n \"chromedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"goog:chromeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983c" + }, + { + "id": "0f9c5a941aa4d755a54b84be1f6535b2", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0.4472.114\",\n \"chrome\": {\n \"chromedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"goog:chromeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983d" + } + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + }, + want: 3, + wantErr: false, + }, + { + name: "2 active sessions with matching browsername on 1 node and maxSession=3 should return count as 2 (rounded up from 1.33)", + args: args{ + b: []byte(`{ + "data": { + "grid":{ + "maxSession": 3, + "nodeCount": 1 }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\"\n}"], - "sessions": [] + "sessions": [ + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0.4472.114\",\n \"chrome\": {\n \"chromedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"goog:chromeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983c" + }, + { + "id": "0f9c5a941aa4d755a54b84be1f6535b2", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0.4472.114\",\n \"chrome\": {\n \"chromedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"goog:chromeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983d" + } + ] } } }`), - browserName: "chrome", - browserVersion: "latest", + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", }, - want: 1, + want: 2, wantErr: false, }, { - name: "active sessions with matching browsername should return count as 3", + name: "2 active sessions with matching browsername on 2 nodes should return count as 5", args: args{ b: []byte(`{ "data": { "sessionsInfo": { "grid":{ - "maxSession": 1 + "maxSession": 2, + "nodeCount": 2 }, - "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], + "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], "sessions": [ { "id": "0f9c5a941aa4d755a54b84be1f6535b1", "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0.4472.114\",\n \"chrome\": {\n \"chromedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"goog:chromeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983c" + }, + { + "id": "0f9c5a941aa4d755a54b84be1f6535b2", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0.4472.114\",\n \"chrome\": {\n \"chromedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"goog:chromeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983d" } ] } } }`), - browserName: "chrome", - browserVersion: "latest", + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", }, - want: 3, + want: 5, wantErr: false, }, { - name: "active sessions with matching browsername and version should return count as 2", + name: "1 active session with matching browsername and version should return count as 2", args: args{ b: []byte(`{ "data": { "grid":{ - "maxSession": 1 + "maxSession": 1, + "nodeCount": 1 }, "sessionsInfo": { "sessionQueueRequests": ["{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"91.0\"\n}","{\n \"browserName\": \"chrome\"\n}"], @@ -166,16 +226,101 @@ func Test_getCountFromSeleniumResponse(t *testing.T) { } } }`), - browserName: "chrome", - browserVersion: "91.0", + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", }, want: 2, wantErr: false, }, + { + name: "1 active msedge session with matching browsername/sessionBroswerName should return count as 3", + args: args{ + b: []byte(`{ + "data": { + "grid":{ + "maxSession": 1, + "nodeCount": 1 + }, + "sessionsInfo": { + "sessionQueueRequests": ["{\n \"browserName\": \"MicrosoftEdge\"\n}","{\n \"browserName\": \"MicrosoftEdge\"\n}"], + "sessions": [ + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"msedge\",\n \"browserVersion\": \"91.0.4472.114\",\n \"msedge\": {\n \"msedgedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"ms:edgeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983c" + } + ] + } + } + }`), + browserName: "MicrosoftEdge", + sessionBrowserName: "msedge", + browserVersion: "latest", + }, + want: 3, + wantErr: false, + }, + { + name: "1 active msedge session while asking for 2 chrome sessions should return a count of 2", + args: args{ + b: []byte(`{ + "data": { + "grid":{ + "maxSession": 1, + "nodeCount": 1 + }, + "sessionsInfo": { + "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], + "sessions": [ + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"msedge\",\n \"browserVersion\": \"91.0.4472.114\",\n \"msedge\": {\n \"msedgedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"ms:edgeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983c" + } + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + }, + want: 2, + wantErr: false, + }, + { + name: "1 active msedge session with maxSessions=3 while asking for 3 chrome sessions should return a count of 1", + args: args{ + b: []byte(`{ + "data": { + "grid":{ + "maxSession": 3, + "nodeCount": 1 + }, + "sessionsInfo": { + "sessionQueueRequests": ["{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}","{\n \"browserName\": \"chrome\"\n}"], + "sessions": [ + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\n \"acceptInsecureCerts\": false,\n \"browserName\": \"msedge\",\n \"browserVersion\": \"91.0.4472.114\",\n \"msedge\": {\n \"msedgedriverVersion\": \"91.0.4472.101 (af52a90bf87030dd1523486a1cd3ae25c5d76c9b-refs\\u002fbranch-heads\\u002f4472@{#1462})\",\n \"userDataDir\": \"\\u002ftmp\\u002f.com.google.Chrome.DMqx9m\"\n },\n \"ms:edgeOptions\": {\n \"debuggerAddress\": \"localhost:35839\"\n },\n \"networkConnectionEnabled\": false,\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"proxy\": {\n },\n \"se:cdp\": \"http:\\u002f\\u002flocalhost:35839\",\n \"se:cdpVersion\": \"91.0.4472.114\",\n \"se:vncEnabled\": true,\n \"se:vncLocalAddress\": \"ws:\\u002f\\u002flocalhost:7900\\u002fwebsockify\",\n \"setWindowRect\": true,\n \"strictFileInteractability\": false,\n \"timeouts\": {\n \"implicit\": 0,\n \"pageLoad\": 300000,\n \"script\": 30000\n },\n \"unhandledPromptBehavior\": \"dismiss and notify\",\n \"webauthn:extension:largeBlob\": true,\n \"webauthn:virtualAuthenticators\": true\n}", + "nodeId": "d44dcbc5-0b2c-4d5e-abf4-6f6aa5e0983c" + } + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + }, + want: 1, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getCountFromSeleniumResponse(tt.args.b, tt.args.browserName, tt.args.browserVersion) + got, err := getCountFromSeleniumResponse(tt.args.b, tt.args.browserName, tt.args.browserVersion, tt.args.sessionBrowserName) if (err != nil) != tt.wantErr { t.Errorf("getCountFromSeleniumResponse() error = %v, wantErr %v", err, tt.wantErr) return @@ -229,10 +374,31 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - url: "http://selenium-hub:4444/graphql", - browserName: "chrome", - targetValue: 1, - browserVersion: "latest", + url: "http://selenium-hub:4444/graphql", + browserName: "chrome", + sessionBrowserName: "chrome", + targetValue: 1, + browserVersion: "latest", + }, + }, + { + name: "valid url, browsername, and sessionbrowsername should return metadata", + args: args{ + config: &ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "browserName": "MicrosoftEdge", + "sessionBrowserName": "msedge", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + url: "http://selenium-hub:4444/graphql", + browserName: "MicrosoftEdge", + sessionBrowserName: "msedge", + targetValue: 1, + browserVersion: "latest", }, }, { @@ -249,11 +415,12 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - url: "http://selenium-hub:4444/graphql", - browserName: "chrome", - targetValue: 1, - browserVersion: "91.0", - unsafeSsl: false, + url: "http://selenium-hub:4444/graphql", + browserName: "chrome", + sessionBrowserName: "chrome", + targetValue: 1, + browserVersion: "91.0", + unsafeSsl: false, }, }, { @@ -270,11 +437,12 @@ func Test_parseSeleniumGridScalerMetadata(t *testing.T) { }, wantErr: false, want: &seleniumGridScalerMetadata{ - url: "http://selenium-hub:4444/graphql", - browserName: "chrome", - targetValue: 1, - browserVersion: "91.0", - unsafeSsl: true, + url: "http://selenium-hub:4444/graphql", + browserName: "chrome", + sessionBrowserName: "chrome", + targetValue: 1, + browserVersion: "91.0", + unsafeSsl: true, }, }, } diff --git a/tests/scalers/selenium-grid.test.ts b/tests/scalers/selenium-grid.test.ts index 4e8185a011f..16f602801a1 100644 --- a/tests/scalers/selenium-grid.test.ts +++ b/tests/scalers/selenium-grid.test.ts @@ -39,21 +39,24 @@ test.serial('should have 0 nodes at start', t => { let seleniumChromeNodeReplicaCount = '1'; let seleniumFireFoxReplicaCount = '1'; + let seleniumEdgeReplicaCount = '1'; for (let i = 0; i < 60; i++) { seleniumChromeNodeReplicaCount = sh.exec(`kubectl get deploy/selenium-chrome-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout seleniumFireFoxReplicaCount = sh.exec(`kubectl get deploy/selenium-firefox-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout - if (seleniumChromeNodeReplicaCount == '0' && seleniumFireFoxReplicaCount == '0') { + seleniumEdgeReplicaCount = sh.exec(`kubectl get deploy/selenium-edge-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout + if (seleniumChromeNodeReplicaCount == '0' && seleniumFireFoxReplicaCount == '0' && seleniumEdgeReplicaCount == '0') { break; } - console.log('Waiting for chrome and firefox to scale down to 0 pods') + console.log('Waiting for chrome, firefox, and edge to scale down to 0 pods') sh.exec('sleep 10s') } t.is('0', seleniumChromeNodeReplicaCount, 'Selenium Chrome Node did not scale down to 0 pods') t.is('0', seleniumFireFoxReplicaCount, 'Selenium Firefox Node did not scale down to 0 pods') + t.is('0', seleniumEdgeReplicaCount, 'Selenium Edge Node did not scale down to 0 pods') }); -test.serial('should create one chrome and firefox node', t => { +test.serial('should create one chrome, firefox, and edge node', t => { const seleniumGridTestDeployTmpFile = tmp.fileSync(); fs.writeFileSync( seleniumGridTestDeployTmpFile.name, @@ -67,53 +70,60 @@ test.serial('should create one chrome and firefox node', t => { t.is(0, sh.exec(`kubectl apply --namespace ${seleniumGridNamespace} -f ${seleniumGridTestDeployTmpFile.name}`).code, 'creating a Selenium Grid Tests deployment should work.'); - let seleniumChromeNodeReplicaCount = '0'; + let seleniumChromeNodeReplicaCount = '0'; let seleniumFireFoxReplicaCount = '0'; + let seleniumEdgeReplicaCount = '0'; for (let i = 0; i < 120; i++) { seleniumChromeNodeReplicaCount = seleniumChromeNodeReplicaCount != '1' ? sh.exec(`kubectl get deploy/selenium-chrome-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumChromeNodeReplicaCount; seleniumFireFoxReplicaCount = seleniumFireFoxReplicaCount != '1' ? sh.exec(`kubectl get deploy/selenium-firefox-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumFireFoxReplicaCount; - if (seleniumChromeNodeReplicaCount == '1' && seleniumFireFoxReplicaCount == '1') { + seleniumEdgeReplicaCount = seleniumEdgeReplicaCount != '1' ? sh.exec(`kubectl get deploy/selenium-edge-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumEdgeReplicaCount; + if (seleniumChromeNodeReplicaCount == '1' && seleniumFireFoxReplicaCount == '1' && seleniumEdgeReplicaCount == '1') { break; } - console.log('Waiting for chrome to scale up 1 pod and firefox to 1 pod'); + console.log('Waiting for chrome, firefox, and edge to scale up to 1 pod'); sh.exec('sleep 2s') } t.is('1', seleniumChromeNodeReplicaCount, 'Selenium Chrome Node did not scale up to 1 pod') t.is('1', seleniumFireFoxReplicaCount, 'Selenium Firefox Node did not scale up to 1 pod') + t.is('1', seleniumEdgeReplicaCount, 'Selenium Edge Node did not scale up to 1 pod') // wait for selenium grid tests to complete for (let i = 0; i < 120; i++) { seleniumChromeNodeReplicaCount = seleniumChromeNodeReplicaCount != '0' ? sh.exec(`kubectl get deploy/selenium-chrome-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumChromeNodeReplicaCount; seleniumFireFoxReplicaCount = seleniumFireFoxReplicaCount != '0' ? sh.exec(`kubectl get deploy/selenium-firefox-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumFireFoxReplicaCount; - if (seleniumChromeNodeReplicaCount == '0' && seleniumFireFoxReplicaCount == '0') { + seleniumEdgeReplicaCount = seleniumEdgeReplicaCount != '0' ? sh.exec(`kubectl get deploy/selenium-edge-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumEdgeReplicaCount; + if (seleniumChromeNodeReplicaCount == '0' && seleniumFireFoxReplicaCount == '0' && seleniumEdgeReplicaCount == '0') { break; } - console.log('Waiting for chrome to scale up 0 pod and firefox to 0 pod'); + console.log('Waiting for chrome, firefox, and edge to scale up to 0 pods'); sh.exec('sleep 2s') } sh.exec(`kubectl delete job/${seleniumGridTestName} --namespace ${seleniumGridNamespace}`) }); -test.serial('should scale down chrome and firefox nodes to 0', t => { +test.serial('should scale down chrome, firefox, and edge nodes to 0', t => { let seleniumChromeNodeReplicaCount = '1'; let seleniumFireFoxReplicaCount = '1'; + let seleniumEdgeReplicaCount = '1'; for (let i = 0; i < 120; i++) { seleniumChromeNodeReplicaCount = sh.exec(`kubectl get deploy/selenium-chrome-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout; seleniumFireFoxReplicaCount = sh.exec(`kubectl get deploy/selenium-firefox-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout; - if (seleniumChromeNodeReplicaCount == '0' && seleniumFireFoxReplicaCount == '0') { + seleniumEdgeReplicaCount = sh.exec(`kubectl get deploy/selenium-edge-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout; + if (seleniumChromeNodeReplicaCount == '0' && seleniumFireFoxReplicaCount == '0' && seleniumEdgeReplicaCount == '0') { break; } - console.log('Waiting for chrome and firefox to scale down to 0 pod'); + console.log('Waiting for chrome, firefox,a nd edge to scale down to 0 pods'); sh.exec('sleep 2s') } t.is('0', seleniumChromeNodeReplicaCount, 'Selenium Chrome Node did not scale down to 0 pod') t.is('0', seleniumFireFoxReplicaCount, 'Selenium Firefox Node did not scale down to 0 pod') + t.is('0', seleniumEdgeReplicaCount, 'Selenium Edge Node did not scale down to 0 pod') }); -test.serial('should create two chrome and one firefox nodes', t => { +test.serial('should create two chrome, one firefox, and one edge nodes', t => { const chrome91DeployTmpFile = tmp.fileSync(); fs.writeFileSync(chrome91DeployTmpFile.name, chrome91Yaml.replace(/{{NAMESPACE}}/g, seleniumGridNamespace).replace(/{{SELENIUM_GRID_GRAPHQL_URL}}/g, seleniumGridGraphQLUrl)); @@ -144,15 +154,17 @@ test.serial('should create two chrome and one firefox nodes', t => { let seleniumChromeNodeReplicaCount = '0'; let seleniumFireFoxReplicaCount = '0'; + let seleniumEdgeReplicaCount = '0'; seleniumChrome91NodeReplicaCount = '0'; for (let i = 0; i < 120; i++) { seleniumChromeNodeReplicaCount = seleniumChromeNodeReplicaCount != '1' ? sh.exec(`kubectl get deploy/selenium-chrome-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumChromeNodeReplicaCount; seleniumFireFoxReplicaCount = seleniumFireFoxReplicaCount != '1' ? sh.exec(`kubectl get deploy/selenium-firefox-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumFireFoxReplicaCount; + seleniumEdgeReplicaCount = seleniumEdgeReplicaCount != '1' ? sh.exec(`kubectl get deploy/selenium-edge-node -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumEdgeReplicaCount; seleniumChrome91NodeReplicaCount = seleniumChrome91NodeReplicaCount != '1' ? sh.exec(`kubectl get deploy/selenium-chrome-node-91 -n ${seleniumGridNamespace} -o jsonpath='{.spec.replicas}'`).stdout : seleniumChrome91NodeReplicaCount; - if (seleniumChromeNodeReplicaCount == '1' && seleniumFireFoxReplicaCount == '1' && seleniumChrome91NodeReplicaCount == '1') { + if (seleniumChromeNodeReplicaCount == '1' && seleniumFireFoxReplicaCount == '1' && seleniumEdgeReplicaCount == '1' && seleniumChrome91NodeReplicaCount == '1') { break; } - console.log('Waiting for chrome to scale up 2 pods and firefox to 1 pod'); + console.log('Waiting for chrome to scale up 2 pods, firefox to 1 pod, and edge to 1 pod'); sh.exec('sleep 2s') } @@ -161,16 +173,20 @@ test.serial('should create two chrome and one firefox nodes', t => { t.is('1', seleniumChromeNodeReplicaCount, 'Selenium Chrome Node did not scale up to 1 pod') t.is('1', seleniumChrome91NodeReplicaCount, 'Selenium Chrome 91 Node did not scale up to 1 pod') t.is('1', seleniumFireFoxReplicaCount, 'Selenium Firefox Node did not scale up to 1 pod') + t.is('1', seleniumEdgeReplicaCount, 'Selenium Edge Node did not scale up to 1 pod') }); test.after.always.cb('clean up prometheus deployment', t => { let resources = [ 'scaledobject.keda.sh/selenium-grid-chrome-scaledobject', 'scaledobject.keda.sh/selenium-grid-firefox-scaledobject', + 'scaledobject.keda.sh/selenium-grid-edge-scaledobject', 'service/selenium-chrome-node', 'deployment.apps/selenium-chrome-node', 'service/selenium-firefox-node', 'deployment.apps/selenium-firefox-node', + 'service/selenium-edge-node', + 'deployment.apps/selenium-edge-node', 'service/selenium-hub', 'deployment.apps/selenium-hub', `job/${seleniumGridTestName}`, @@ -249,6 +265,28 @@ spec: port: 6900 targetPort: 5900 --- +apiVersion: v1 +kind: Service +metadata: + name: selenium-edge-node + namespace: {{NAMESPACE}} + labels: + name: selenium-edge-node + app.kubernetes.io/managed-by: helm + app.kubernetes.io/instance: selenium-hpa + app.kubernetes.io/version: 4.0.0-beta-1-prerelease-20210114 + app.kubernetes.io/component: selenium-grid-4.0.0-beta-1-prerelease-20210114 + helm.sh/chart: selenium-grid-0.2.0 +spec: + type: ClusterIP + selector: + app: selenium-edge-node + ports: + - name: tcp-edge + protocol: TCP + port: 6900 + targetPort: 5900 +--- # Source: selenium-grid/templates/hub-service.yaml apiVersion: v1 kind: Service @@ -370,6 +408,51 @@ spec: medium: Memory sizeLimit: 1Gi --- +# Source: selenium-grid/templates/edge-node-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: selenium-edge-node + namespace: {{NAMESPACE}} + labels: &edge_node_labels + app: selenium-edge-node + app.kubernetes.io/name: selenium-edge-node + app.kubernetes.io/managed-by: helm + app.kubernetes.io/instance: selenium-hpa + app.kubernetes.io/version: 4.0.0-beta-1-prerelease-20210114 + app.kubernetes.io/component: selenium-grid-4.0.0-beta-1-prerelease-20210114 + helm.sh/chart: selenium-grid-0.2.0 +spec: + replicas: 0 + selector: + matchLabels: + app: selenium-edge-node + template: + metadata: + labels: *edge_node_labels + annotations: + checksum/event-bus-configmap: 0e5e9d25a669359a37dd0d684c485f4c05729da5a26a841ad9a2743d99460f73 + spec: + containers: + - name: selenium-edge-node + image: selenium/node-edge:4.0.0-rc-1-prerelease-20210618 + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: selenium-event-bus-config + ports: + - containerPort: 5553 + protocol: TCP + volumeMounts: + - name: dshm + mountPath: /dev/shm + resources: + volumes: + - name: dshm + emptyDir: + medium: Memory + sizeLimit: 1Gi +--- # Source: selenium-grid/templates/hub-deployment.yaml apiVersion: apps/v1 kind: Deployment @@ -552,7 +635,27 @@ spec: - type: selenium-grid metadata: url: '{{SELENIUM_GRID_GRAPHQL_URL}}' - browserName: 'firefox'` + browserName: 'firefox' +--- +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: selenium-grid-edge-scaledobject + namespace: {{NAMESPACE}} + labels: + deploymentName: selenium-edge-node +spec: + maxReplicaCount: 1 + pollingInterval: 5 + cooldownPeriod: 5 + scaleTargetRef: + name: selenium-edge-node + triggers: + - type: selenium-grid + metadata: + url: '{{SELENIUM_GRID_GRAPHQL_URL}}' + browserName: 'MicrosoftEdge' + sessionBrowserName: 'msedge'` const seleniumGridTestsYaml = `apiVersion: batch/v1 kind: Job