From 2af6166d263ab5d74be4178aa882a00f4aa9acf1 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Thu, 19 Sep 2024 22:32:27 +0700 Subject: [PATCH] Experimental: Selenium Grid scaler in K8s implementation preview (#2400) --- .github/workflows/deploy.yml | 4 +- .keda/README.md | 62 + .keda/scalers/selenium-grid-scaler.md | 207 ++ .keda/scalers/selenium_grid_scaler.go | 379 ++++ .keda/scalers/selenium_grid_scaler_test.go | 1808 +++++++++++++++++ Makefile | 51 +- charts/selenium-grid/CONFIGURATION.md | 7 +- charts/selenium-grid/README.md | 2 + charts/selenium-grid/values.yaml | 22 +- generate_chart_changelog.sh | 4 + .../ci/DeploymentAutoscaling-values.yaml | 2 +- tests/charts/ci/base-auth-ingress-values.yaml | 4 + tests/charts/make/chart_cluster_setup.sh | 1 - tests/charts/make/chart_test.sh | 18 +- update_tag_in_docs_and_files.sh | 5 + 15 files changed, 2555 insertions(+), 21 deletions(-) create mode 100644 .keda/README.md create mode 100644 .keda/scalers/selenium-grid-scaler.md create mode 100644 .keda/scalers/selenium_grid_scaler.go create mode 100644 .keda/scalers/selenium_grid_scaler_test.go diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3bf8147fc..8ecb7fe70 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -152,7 +152,9 @@ jobs: continue_on_error: true command: VERSION="${GRID_VERSION}" BUILD_DATE=${BUILD_DATE} make release_latest - name: Update package versions - run: make generate_latest_sbom + run: | + make generate_latest_sbom + make fetch_grid_scaler_resources - name: Tag browser images if: github.event.inputs.skip-build-push-image != 'true' uses: nick-invision/retry@master diff --git a/.keda/README.md b/.keda/README.md new file mode 100644 index 000000000..7b8c08281 --- /dev/null +++ b/.keda/README.md @@ -0,0 +1,62 @@ +# Introduction + +Selenium Grid Scaler is a built-in scaler is maintained in upstream KEDA [repository](https://github.com/kedacore/keda). The scaler implementation could be found [here](https://github.com/kedacore/keda/blob/main/pkg/scalers/selenium_grid_scaler.go). The official docs of the scaler could be seen [here](https://keda.sh/docs/latest/scalers/selenium-grid-scaler/). + +Now, [SeleniumHQ/docker-selenium](https://github.com/SeleniumHQ/docker-selenium) involves as the maintainer for the scaler. + +In order to deliver and get feedback continuously on any new bug fixes, improvement, or features for the Selenium Grid scaler. We select the latest stable version of KEDA core, patch the scaler implementation then build and deploy KEDA container images following our image tag convention. + +The stable implementation will be merged to the upstream KEDA repository frequently and will be available in the next KEDA core release. + +# How to use the patched scaler + +Replace the image registry and tag of these KEDA components with the patched image tag: + +```bash +docker pull selenium/keda:2.15.1-selenium-grid-20240907 +docker pull selenium/keda-metrics-apiserver:2.15.1-selenium-grid-20240907 +docker pull selenium/keda-admission-webhooks:2.15.1-selenium-grid-20240907 +``` + +Besides that, you also can use image tag `latest` or `nightly`. + +If you are deploying KEDA core using their official Helm chart, you can overwrite the image registry and tag by providing the following values in the `values.yaml` file. For example: + +```yaml + image: + keda: + registry: selenium + repository: keda + tag: "2.15.1-selenium-grid-20240907" + metricsApiServer: + registry: selenium + repository: keda-metrics-apiserver + tag: "2.15.1-selenium-grid-20240907" + webhooks: + registry: selenium + repository: keda-admission-webhooks + tag: "2.15.1-selenium-grid-20240907" +``` + +If you are deployment Selenium Grid chart with `autoscaling.enabled` is `true` (implies installing KEDA sub-chart), KEDA images registry and tag already set in the `values.yaml`. Refer to list [configuration](../charts/selenium-grid/CONFIGURATION.md). + +# Pull requests under testing + +Here is list of pull requests that are under testing and will be merged to the upstream KEDA repository. +You can involve to review and discuss the pull requests to help us early detect and fix any issues. + +[kedacore/keda](https://github.com/kedacore/keda) + +- https://github.com/kedacore/keda/pull/6169 + +[kedacore/keda-docs](https://github.com/kedacore/keda-docs) + +- https://github.com/kedacore/keda-docs/pull/1468 + +# Resources + +You can inspect the implementation of current Selenium Grid scaler: + +- [selenium_grid_scaler.go](./scalers/selenium_grid_scaler.go) +- [selenium_grid_scaler_test.go](./scalers/selenium_grid_scaler_test.go) +- [selenium-grid-scaler.md](./scalers/selenium-grid-scaler.md) diff --git a/.keda/scalers/selenium-grid-scaler.md b/.keda/scalers/selenium-grid-scaler.md new file mode 100644 index 000000000..1305ff477 --- /dev/null +++ b/.keda/scalers/selenium-grid-scaler.md @@ -0,0 +1,207 @@ ++++ +title = "Selenium Grid Scaler" +availability = "v2.4+" +maintainer = "Volvo Cars, SeleniumHQ" +category = "Testing" +description = "Scales Selenium browser nodes based on number of requests waiting in session queue" +go_file = "selenium_grid_scaler" ++++ + +### Trigger Specification + +This specification describes the `selenium-grid` trigger that scales browser nodes based on number of requests in session queue and the max sessions per grid. + +The scaler creates one browser node per pending request in session queue, divided by the max amount of sessions that can run in parallel. You will have to create one trigger per browser capability that you would like to support in your Selenium Grid. + +The below is an example trigger configuration for chrome node. + +```yaml +triggers: + - type: selenium-grid + metadata: + url: 'http://selenium-hub:4444/graphql' # Required. Can be ommitted if specified via TriggerAuthentication/ClusterTriggerAuthentication. + browserName: 'chrome' # Required + browserVersion: '91.0' # Optional. Only required when supporting multiple versions of browser in your Selenium Grid. + unsafeSsl : 'true' # Optional + activationThreshold: 5 # Optional + platformName: 'Linux' # Optional +``` + +**Parameter list:** + +- `url` - Graphql url of your Selenium Grid. Refer to the Selenium Grid's documentation [here](https://www.selenium.dev/documentation/en/grid/grid_4/graphql_support/) to for more info. +- `username` - Username for basic authentication in GraphQL endpoint instead of embedding in the URL. (Optional) +- `password` - Password for basic authentication in GraphQL endpoint instead of embedding in the URL. (Optional) +- `browserName` - Name of browser that usually gets passed in the browser capability. Refer to the [Selenium Grid's](https://www.selenium.dev/documentation/en/getting_started_with_webdriver/browsers/) and [WebdriverIO's](https://webdriver.io/docs/options/#capabilities) documentation for more info. +- `sessionBrowserName` - Name of the browser when it is an active session, only set if `BrowserName` changes between the queue and the active session. See the Edge example below for further detail. (Optional) +- `browserVersion` - Version of browser that usually gets passed in the browser capability. Refer to the [Selenium Grid's](https://www.selenium.dev/documentation/en/getting_started_with_webdriver/browsers/) and [WebdriverIO's](https://webdriver.io/docs/options/#capabilities) documentation for more info. (Optional) +- `unsafeSsl` - Skip certificate validation when connecting over HTTPS. (Values: `true`, `false`, Default: `false`, Optional) +- `activationThreshold` - Target value for activating the scaler. Learn more about activation [here](./../concepts/scaling-deployments.md#activating-and-scaling-thresholds). (Default: `0`, Optional) +- `platformName` - Name of the browser platform. Refer to the [Selenium Grid's](https://www.selenium.dev/documentation/en/getting_started_with_webdriver/browsers/) and [WebdriverIO's](https://webdriver.io/docs/options/#capabilities) documentation for more info. (Default: `Linux`, Optional) +- `nodeMaxSessions` - Number of maximum sessions that can run in parallel on a Node. (Default: `1`, Optional). Update this parameter align with node config `--max-sessions` (`SE_NODE_MAX_SESSIONS`) to have the correct scaling behavior. + +### Example + +Here is a full example of scaled object definition using Selenium Grid trigger: + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: selenium-grid-chrome-scaledobject + namespace: keda + labels: + deploymentName: selenium-chrome-node +spec: + maxReplicaCount: 8 + scaleTargetRef: + name: selenium-chrome-node + triggers: + - type: selenium-grid + metadata: + url: 'http://selenium-hub:4444/graphql' + browserName: 'chrome' +``` + +The above example will create Chrome browser nodes equal to the requests pending in session queue for Chrome browser. + +Similarly for Firefox + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: selenium-grid-firefox-scaledobject + namespace: keda + labels: + deploymentName: selenium-firefox-node +spec: + maxReplicaCount: 8 + scaleTargetRef: + name: selenium-firefox-node + triggers: + - type: selenium-grid + metadata: + url: 'http://selenium-hub:4444/graphql' + browserName: 'firefox' +``` + +Similarly for Edge. Note that for Edge you must set the `sessionBrowserName` to `msedge` inorder for scaling to work properly. + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: selenium-grid-edge-scaledobject + namespace: keda + labels: + deploymentName: selenium-edge-node +spec: + maxReplicaCount: 8 + scaleTargetRef: + name: selenium-edge-node + triggers: + - type: selenium-grid + metadata: + url: 'http://selenium-hub:4444/graphql' + browserName: 'MicrosoftEdge' + sessionBrowserName: 'msedge' +``` + +If you are supporting multiple versions of browser capability in your Selenium Grid, You should create one scaler for every browser version and pass the `browserVersion` in the metadata. + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: selenium-grid-chrome-91-scaledobject + namespace: keda + labels: + deploymentName: selenium-chrome-node-91 +spec: + maxReplicaCount: 8 + scaleTargetRef: + name: selenium-chrome-node-91 + triggers: + - type: selenium-grid + metadata: + url: 'http://selenium-hub:4444/graphql' + browserName: 'chrome' + browserVersion: '91.0' +``` + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: selenium-grid-chrome-90-scaledobject + namespace: keda + labels: + deploymentName: selenium-chrome-node-90 +spec: + maxReplicaCount: 8 + scaleTargetRef: + name: selenium-chrome-node-90 + triggers: + - type: selenium-grid + metadata: + url: 'http://selenium-hub:4444/graphql' + browserName: 'chrome' + browserVersion: '90.0' +``` + +### Authentication Parameters + +It is possible to specify the Graphql url of your Selenium Grid using authentication parameters. This useful if you have enabled Selenium Grid's Basic HTTP Authentication and would like to keep your credentials secure. + +- `url` - Graphql url of your Selenium Grid. Refer to the Selenium Grid's documentation [here](https://www.selenium.dev/documentation/en/grid/grid_4/graphql_support/) for more info. +- `username` - Username for basic authentication in GraphQL endpoint instead of embedding in the URL. (Optional) +- `password` - Password for basic authentication in GraphQL endpoint instead of embedding in the URL. (Optional) + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: selenium-grid-secret + namespace: keda +type: Opaque +data: + graphql-url: base64 encoded value of GraphQL URL + graphql-username: base64 encoded value of GraphQL Username + graphql-password: base64 encoded value of GraphQL Password +--- +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: keda-trigger-auth-selenium-grid-secret + namespace: keda +spec: + secretTargetRef: + - parameter: url + name: selenium-grid-secret + key: graphql-url + - parameter: username + name: selenium-grid-secret + key: graphql-username + - parameter: password + name: selenium-grid-secret + key: graphql-password +--- +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: selenium-grid-chrome-scaledobject + namespace: keda + labels: + deploymentName: selenium-chrome-node +spec: + maxReplicaCount: 8 + scaleTargetRef: + name: selenium-chrome-node + triggers: + - type: selenium-grid + metadata: + browserName: 'chrome' + authenticationRef: + name: keda-trigger-auth-selenium-grid-secret +``` diff --git a/.keda/scalers/selenium_grid_scaler.go b/.keda/scalers/selenium_grid_scaler.go new file mode 100644 index 000000000..842f0f9ca --- /dev/null +++ b/.keda/scalers/selenium_grid_scaler.go @@ -0,0 +1,379 @@ +package scalers + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/go-logr/logr" + v2 "k8s.io/api/autoscaling/v2" + "k8s.io/metrics/pkg/apis/external_metrics" + + "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" + kedautil "github.com/kedacore/keda/v2/pkg/util" +) + +type seleniumGridScaler struct { + metricType v2.MetricTargetType + metadata *seleniumGridScalerMetadata + httpClient *http.Client + logger logr.Logger +} + +type seleniumGridScalerMetadata struct { + triggerIndex int + + URL string `keda:"name=url, order=triggerMetadata;authParams"` + Username string `keda:"name=username, order=triggerMetadata;authParams, optional"` + Password string `keda:"name=password, order=triggerMetadata;authParams, optional"` + BrowserName string `keda:"name=browserName, order=triggerMetadata"` + SessionBrowserName string `keda:"name=sessionBrowserName, order=triggerMetadata, optional"` + ActivationThreshold int64 `keda:"name=activationThreshold, order=triggerMetadata, optional"` + BrowserVersion string `keda:"name=browserVersion, order=triggerMetadata, optional, default=latest"` + UnsafeSsl bool `keda:"name=unsafeSsl, order=triggerMetadata, optional, default=false"` + PlatformName string `keda:"name=platformName, order=triggerMetadata, optional, default=linux"` + NodeMaxSessions int `keda:"name=nodeMaxSessions, order=triggerMetadata, optional, default=1"` + + TargetValue int64 +} + +type SeleniumResponse struct { + Data Data `json:"data"` +} + +type Data struct { + Grid Grid `json:"grid"` + NodesInfo NodesInfo `json:"nodesInfo"` + SessionsInfo SessionsInfo `json:"sessionsInfo"` +} + +type Grid struct { + SessionCount int `json:"sessionCount"` + MaxSession int `json:"maxSession"` + TotalSlots int `json:"totalSlots"` +} + +type NodesInfo struct { + Nodes Nodes `json:"nodes"` +} + +type SessionsInfo struct { + SessionQueueRequests []string `json:"sessionQueueRequests"` +} + +type Nodes []struct { + ID string `json:"id"` + Status string `json:"status"` + SessionCount int `json:"sessionCount"` + MaxSession int `json:"maxSession"` + SlotCount int `json:"slotCount"` + Stereotypes string `json:"stereotypes"` + Sessions Sessions `json:"sessions"` +} + +type ReservedNodes struct { + ID string `json:"id"` + MaxSession int `json:"maxSession"` + SlotCount int `json:"slotCount"` +} + +type Sessions []struct { + ID string `json:"id"` + Capabilities string `json:"capabilities"` + Slot Slot `json:"slot"` +} + +type Slot struct { + ID string `json:"id"` + Stereotype string `json:"stereotype"` +} + +type Capability struct { + BrowserName string `json:"browserName"` + BrowserVersion string `json:"browserVersion"` + PlatformName string `json:"platformName"` +} + +type Stereotypes []struct { + Slots int `json:"slots"` + Stereotype Capability `json:"stereotype"` +} + +const ( + DefaultBrowserVersion string = "latest" +) + +func NewSeleniumGridScaler(config *scalersconfig.ScalerConfig) (Scaler, error) { + metricType, err := GetMetricTargetType(config) + if err != nil { + return nil, fmt.Errorf("error getting scaler metric type: %w", err) + } + + logger := InitializeLogger(config, "selenium_grid_scaler") + + meta, err := parseSeleniumGridScalerMetadata(config) + + if err != nil { + return nil, fmt.Errorf("error parsing selenium grid metadata: %w", err) + } + + httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, meta.UnsafeSsl) + + return &seleniumGridScaler{ + metricType: metricType, + metadata: meta, + httpClient: httpClient, + logger: logger, + }, nil +} + +func parseSeleniumGridScalerMetadata(config *scalersconfig.ScalerConfig) (*seleniumGridScalerMetadata, error) { + meta := &seleniumGridScalerMetadata{ + TargetValue: 1, + } + + if err := config.TypedConfig(meta); err != nil { + return nil, fmt.Errorf("error parsing prometheus metadata: %w", err) + } + + meta.triggerIndex = config.TriggerIndex + + if meta.SessionBrowserName == "" { + meta.SessionBrowserName = meta.BrowserName + } + return meta, nil +} + +// No cleanup required for Selenium Grid scaler +func (s *seleniumGridScaler) Close(context.Context) error { + if s.httpClient != nil { + s.httpClient.CloseIdleConnections() + } + return nil +} + +func (s *seleniumGridScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) { + sessions, err := s.getSessionsCount(ctx, s.logger) + if err != nil { + return []external_metrics.ExternalMetricValue{}, false, fmt.Errorf("error requesting selenium grid endpoint: %w", err) + } + + metric := GenerateMetricInMili(metricName, float64(sessions)) + + return []external_metrics.ExternalMetricValue{metric}, sessions > s.metadata.ActivationThreshold, nil +} + +func (s *seleniumGridScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { + metricName := kedautil.NormalizeString(fmt.Sprintf("seleniumgrid-%s", s.metadata.BrowserName)) + externalMetric := &v2.ExternalMetricSource{ + Metric: v2.MetricIdentifier{ + Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, metricName), + }, + Target: GetMetricTarget(s.metricType, s.metadata.TargetValue), + } + metricSpec := v2.MetricSpec{ + External: externalMetric, Type: externalMetricType, + } + return []v2.MetricSpec{metricSpec} +} + +func (s *seleniumGridScaler) getSessionsCount(ctx context.Context, logger logr.Logger) (int64, error) { + body, err := json.Marshal(map[string]string{ + "query": "{ grid { sessionCount, maxSession, totalSlots }, nodesInfo { nodes { id, status, sessionCount, maxSession, slotCount, stereotypes, sessions { id, capabilities, slot { id, stereotype } } } }, sessionsInfo { sessionQueueRequests } }", + }) + + if err != nil { + return -1, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", s.metadata.URL, bytes.NewBuffer(body)) + if err != nil { + return -1, err + } + + if s.metadata.Username != "" && s.metadata.Password != "" { + req.SetBasicAuth(s.metadata.Username, s.metadata.Password) + } + + res, err := s.httpClient.Do(req) + if err != nil { + return -1, err + } + + if res.StatusCode != http.StatusOK { + msg := fmt.Sprintf("selenium grid returned %d", res.StatusCode) + return -1, errors.New(msg) + } + + defer res.Body.Close() + b, err := io.ReadAll(res.Body) + if err != nil { + return -1, err + } + v, err := getCountFromSeleniumResponse(b, s.metadata.BrowserName, s.metadata.BrowserVersion, s.metadata.SessionBrowserName, s.metadata.PlatformName, s.metadata.NodeMaxSessions, logger) + if err != nil { + return -1, err + } + return v, nil +} + +func countMatchingSlotsStereotypes(stereotypes Stereotypes, request Capability, browserName string, browserVersion string, sessionBrowserName string, platformName string) int { + var matchingSlots int + for _, stereotype := range stereotypes { + if checkCapabilitiesMatch(stereotype.Stereotype, request, browserName, browserVersion, sessionBrowserName, platformName) { + matchingSlots += stereotype.Slots + } + } + return matchingSlots +} + +func countMatchingSessions(sessions Sessions, request Capability, browserName string, browserVersion string, sessionBrowserName string, platformName string, logger logr.Logger) int { + var matchingSessions int + for _, session := range sessions { + var capability = Capability{} + if err := json.Unmarshal([]byte(session.Capabilities), &capability); err == nil { + if checkCapabilitiesMatch(capability, request, browserName, browserVersion, sessionBrowserName, platformName) { + matchingSessions++ + } + } else { + logger.Error(err, fmt.Sprintf("Error when unmarshaling session capabilities: %s", err)) + } + } + return matchingSessions +} + +func checkCapabilitiesMatch(capability Capability, requestCapability Capability, browserName string, browserVersion string, sessionBrowserName string, platformName string) bool { + // Ensure the logic should be aligned with DefaultSlotMatcher in Selenium Grid - SeleniumHQ/selenium/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java + // A browserName matches when one of the following conditions is met: + // 1. `browserName` in capability matches with `browserName` or `sessionBrowserName` in scaler metadata + // 2. `browserName` in request capability is empty or not provided + var browserNameMatches = strings.EqualFold(capability.BrowserName, browserName) || strings.EqualFold(capability.BrowserName, sessionBrowserName) || + requestCapability.BrowserName == "" + // A browserVersion matches when one of the following conditions is met: + // 1. `browserVersion` in request capability is empty or not provided or `stable` + // 2. `browserVersion` in capability matches with prefix of the scaler metadata `browserVersion` + // 3. `browserVersion` in scaler metadata is `latest` + var browserVersionMatches = requestCapability.BrowserVersion == "" || requestCapability.BrowserVersion == "stable" || + strings.HasPrefix(capability.BrowserVersion, browserVersion) || browserVersion == DefaultBrowserVersion + // A platformName matches when one of the following conditions is met: + // 1. `platformName` in request capability is empty or not provided + // 2. `platformName` in capability is empty or not provided + // 3. `platformName` in capability matches with the scaler metadata `platformName` + // 4. `platformName` in scaler metadata is empty or not provided + var platformNameMatches = requestCapability.PlatformName == "" || capability.PlatformName == "" || + strings.EqualFold(capability.PlatformName, platformName) || platformName == "" + return browserNameMatches && browserVersionMatches && platformNameMatches +} + +func checkNodeReservedSlots(reservedNodes []ReservedNodes, nodeID string, availableSlots int) int { + for _, reservedNode := range reservedNodes { + if strings.EqualFold(reservedNode.ID, nodeID) { + return reservedNode.SlotCount + } + } + return availableSlots +} + +func updateOrAddReservedNode(reservedNodes []ReservedNodes, nodeID string, slotCount int, maxSession int) []ReservedNodes { + for i, reservedNode := range reservedNodes { + if strings.EqualFold(reservedNode.ID, nodeID) { + // Update remaining available slots for the reserved node + reservedNodes[i].SlotCount = slotCount + return reservedNodes + } + } + // Add new reserved node if not found + return append(reservedNodes, ReservedNodes{ID: nodeID, SlotCount: slotCount, MaxSession: maxSession}) +} + +func getCountFromSeleniumResponse(b []byte, browserName string, browserVersion string, sessionBrowserName string, platformName string, nodeMaxSessions int, logger logr.Logger) (int64, error) { + // The returned count of the number of new Nodes will be scaled up + var count int64 + // Track number of available slots of existing Nodes in the Grid can be reserved for the matched requests + var availableSlots int + // Track number of matched requests in the sessions queue will be served by this scaler + var queueSlots int + + var seleniumResponse = SeleniumResponse{} + if err := json.Unmarshal(b, &seleniumResponse); err != nil { + return 0, err + } + + var sessionQueueRequests = seleniumResponse.Data.SessionsInfo.SessionQueueRequests + var nodes = seleniumResponse.Data.NodesInfo.Nodes + // Track list of existing Nodes that have available slots for the matched requests + var reservedNodes []ReservedNodes + // Track list of new Nodes will be scaled up with number of available slots following scaler parameter `nodeMaxSessions` + var newRequestNodes []ReservedNodes + for requestIndex, sessionQueueRequest := range sessionQueueRequests { + var isRequestMatched bool + var requestCapability = Capability{} + if err := json.Unmarshal([]byte(sessionQueueRequest), &requestCapability); err == nil { + if checkCapabilitiesMatch(requestCapability, requestCapability, browserName, browserVersion, sessionBrowserName, platformName) { + queueSlots++ + isRequestMatched = true + } + } else { + logger.Error(err, fmt.Sprintf("Error when unmarshaling sessionQueueRequest capability: %s", err)) + } + + // Skip the request if the capability does not match the scaler parameters + if !isRequestMatched { + continue + } + + var isRequestReserved bool + // Check if the matched request can be assigned to available slots of existing Nodes in the Grid + for _, node := range nodes { + // Check if node is UP and has available slots (maxSession > sessionCount) + if strings.EqualFold(node.Status, "UP") && checkNodeReservedSlots(reservedNodes, node.ID, node.MaxSession-node.SessionCount) > 0 { + var stereotypes = Stereotypes{} + var availableSlotsMatch int + if err := json.Unmarshal([]byte(node.Stereotypes), &stereotypes); err == nil { + // Count available slots that match the request capability and scaler metadata + availableSlotsMatch += countMatchingSlotsStereotypes(stereotypes, requestCapability, browserName, browserVersion, sessionBrowserName, platformName) + } else { + logger.Error(err, fmt.Sprintf("Error when unmarshaling node stereotypes: %s", err)) + } + // Count ongoing sessions that match the request capability and scaler metadata + var currentSessionsMatch = countMatchingSessions(node.Sessions, requestCapability, browserName, browserVersion, sessionBrowserName, platformName, logger) + // Count remaining available slots can be reserved for this request + var availableSlotsCanBeReserved = checkNodeReservedSlots(reservedNodes, node.ID, node.MaxSession-node.SessionCount) + // Reserve one available slot for the request if available slots match is greater than current sessions match + if availableSlotsMatch > currentSessionsMatch { + availableSlots++ + reservedNodes = updateOrAddReservedNode(reservedNodes, node.ID, availableSlotsCanBeReserved-1, node.MaxSession) + isRequestReserved = true + break + } + } + } + // Check if the matched request can be assigned to available slots of new Nodes will be scaled up, since the scaler parameter `nodeMaxSessions` can be greater than 1 + if !isRequestReserved { + for _, newRequestNode := range newRequestNodes { + if newRequestNode.SlotCount > 0 { + newRequestNodes = updateOrAddReservedNode(newRequestNodes, newRequestNode.ID, newRequestNode.SlotCount-1, nodeMaxSessions) + isRequestReserved = true + break + } + } + } + // Check if a new Node should be scaled up to reserve for the matched request + if !isRequestReserved { + newRequestNodes = updateOrAddReservedNode(newRequestNodes, string(rune(requestIndex)), nodeMaxSessions-1, nodeMaxSessions) + } + } + + if queueSlots > availableSlots { + count = int64(len(newRequestNodes)) + } else { + count = 0 + } + + return count, nil +} diff --git a/.keda/scalers/selenium_grid_scaler_test.go b/.keda/scalers/selenium_grid_scaler_test.go new file mode 100644 index 000000000..c31c151be --- /dev/null +++ b/.keda/scalers/selenium_grid_scaler_test.go @@ -0,0 +1,1808 @@ +package scalers + +import ( + "reflect" + "testing" + + "github.com/go-logr/logr" + + "github.com/kedacore/keda/v2/pkg/scalers/scalersconfig" +) + +func Test_getCountFromSeleniumResponse(t *testing.T) { + type args struct { + b []byte + browserName string + sessionBrowserName string + browserVersion string + platformName string + nodeMaxSessions int + } + tests := []struct { + name string + args args + want int64 + wantErr bool + }{ + { + name: "nil response body should throw error", + args: args{ + b: []byte(nil), + browserName: "", + }, + wantErr: true, + }, + { + name: "empty response body should throw error", + args: args{ + b: []byte(""), + browserName: "", + }, + wantErr: true, + }, + { + name: "no sessionQueueRequests should return count as 0", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 0, + "totalSlots": 0 + }, + "nodesInfo": { + "nodes": [] + }, + "sessionsInfo": { + "sessionQueueRequests": [] + } + } + } + `), + browserName: "", + }, + want: 0, + wantErr: false, + }, + { + name: "12 sessionQueueRequests with 4 requests matching browserName chrome should return count as 4", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 0, + "totalSlots": 0 + }, + "nodesInfo": { + "nodes": [] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\n \"browserName\": \"chrome\",\n \"goog:chromeOptions\": {\n \"extensions\": [\n ],\n \"args\": [\n \"disable-features=DownloadBubble,DownloadBubbleV2\"\n ]\n },\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_download_file (ChromeTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_with_frames (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_download_file (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_title_and_maximize_window (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"browserName\": \"chrome\",\n \"goog:chromeOptions\": {\n \"extensions\": [\n ],\n \"args\": [\n \"disable-features=DownloadBubble,DownloadBubbleV2\"\n ]\n },\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_play_video (ChromeTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"browserName\": \"chrome\",\n \"goog:chromeOptions\": {\n \"extensions\": [\n ],\n \"args\": [\n \"disable-features=DownloadBubble,DownloadBubbleV2\"\n ]\n },\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_select_from_a_dropdown (ChromeTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_visit_basic_auth_secured_page (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_select_from_a_dropdown (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"browserName\": \"chrome\",\n \"goog:chromeOptions\": {\n \"extensions\": [\n ],\n \"args\": [\n \"disable-features=DownloadBubble,DownloadBubbleV2\"\n ]\n },\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_title (ChromeTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_title (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_accept_languages (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_play_video (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}" + ] + } + } + } + `), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 4, + wantErr: false, + }, + { + name: "2 sessionQueueRequests and 1 available nodeStereotypes with matching browserName firefox should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 7, + "totalSlots": 7 + }, + "nodesInfo": { + "nodes": [ + { + "id": "82ee33bd-390e-4dd6-aee2-06b17ecee18e", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-m5n8z-4br6x\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "reserved", + "capabilities": "{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-m5n8z-4br6x\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}", + "slot": { + "id": "83c9d9f5-f79d-4dea-bc9b-ce61bf2bc01c", + "stereotype": "{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-m5n8z-4br6x\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}" + } + } + ] + }, + { + "id": "b4d3d31a-3239-4c09-a5f5-3650d4fcef48", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-s2gq6-82lwb\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "reserved", + "capabilities": "{\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-s2gq6-82lwb\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}", + "slot": { + "id": "b03b80c0-95f8-4b9c-ba06-bebd2568ce3d", + "stereotype": "{\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-s2gq6-82lwb\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}" + } + } + ] + }, + { + "id": "f3e67bf7-3c40-42d4-ab10-666b49c88925", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-xh95p-9c2cl\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [] + }, + { + "id": "f1e315fe-5f32-4a73-bb31-b73ed9a728e5", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-j2xbn-lq76c\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "reserved", + "capabilities": "{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-j2xbn-lq76c\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}", + "slot": { + "id": "9d91cd87-b443-4a0c-93e7-eea8c4661207", + "stereotype": "{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-j2xbn-lq76c\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}" + } + } + ] + }, + { + "id": "0ae48415-a230-4bc4-a26c-4fc4ffc3abc1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-xk6mm-2m6jh\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "reserved", + "capabilities": "{\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-xk6mm-2m6jh\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}", + "slot": { + "id": "2c1fc5c4-881a-48fd-9b9e-b4d3ecbc1bd8", + "stereotype": "{\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-xk6mm-2m6jh\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}" + } + } + ] + }, + { + "id": "284fa982-5be0-44a6-b64e-e2e76fe52d1f", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-bvq59-6dh6q\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "reserved", + "capabilities": "{\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-bvq59-6dh6q\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}", + "slot": { + "id": "5f8f9ba0-0f61-473e-b367-b68d9368dc24", + "stereotype": "{\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-bvq59-6dh6q\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}" + } + } + ] + }, + { + "id": "451442d0-3649-4b21-a5a5-32bc847f1765", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-42xbf-zpdd4\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [] + }, + { + "id": "a4d26330-e5be-4630-b4da-9078f2495ece", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-qt9z2-6xx86\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "reserved", + "capabilities": "{\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-qt9z2-6xx86\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}", + "slot": { + "id": "38bd0b09-ffe0-46e9-8983-bd208270c8da", + "stereotype": "{\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-qt9z2-6xx86\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}" + } + } + ] + }, + { + "id": "e81f0038-fc72-4045-9de1-b98143053eae", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-v7nrv-xsfkb\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "reserved", + "capabilities": "{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-v7nrv-xsfkb\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}", + "slot": { + "id": "43b992cc-39bb-4b0f-92b6-99603a543459", + "stereotype": "{\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-v7nrv-xsfkb\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n}" + } + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_accept_languages (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_play_video (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}" + ] + } + } + } + `), + browserName: "firefox", + sessionBrowserName: "firefox", + browserVersion: "latest", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, + { + name: "1 sessionQueueRequests and 1 available nodeStereotypes with matching browserName chrome should return count as 0", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 0, + "totalSlots": 0 + }, + "nodesInfo": { + "nodes": [ + { + "id": "f3e67bf7-3c40-42d4-ab10-666b49c88925", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-chrome-name-xh95p-9c2cl\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [] + }, + { + "id": "451442d0-3649-4b21-a5a5-32bc847f1765", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[\n {\n \"slots\": 1,\n \"stereotype\": {\n \"browserName\": \"firefox\",\n \"browserVersion\": \"130.0\",\n \"moz:firefoxOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002ffirefox\"\n },\n \"platformName\": \"linux\",\n \"se:containerName\": \"my-firefox-name-42xbf-zpdd4\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\n \"acceptInsecureCerts\": true,\n \"browserName\": \"firefox\",\n \"moz:debuggerAddress\": true,\n \"moz:firefoxOptions\": {\n \"prefs\": {\n \"remote.active-protocols\": 3\n },\n \"profile\": \"profile\"\n },\n \"pageLoadStrategy\": \"normal\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_accept_languages (FirefoxTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}", + "{\n \"browserName\": \"chrome\",\n \"goog:chromeOptions\": {\n \"extensions\": [\n ],\n \"args\": [\n \"disable-features=DownloadBubble,DownloadBubbleV2\"\n ]\n },\n \"pageLoadStrategy\": \"normal\",\n \"platformName\": \"linux\",\n \"se:downloadsEnabled\": true,\n \"se:name\": \"test_visit_basic_auth_secured_page (ChromeTests)\",\n \"se:recordVideo\": true,\n \"se:screenResolution\": \"1920x1080\"\n}" + ] + } + } + } + `), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 0, + wantErr: false, + }, + { + name: "1 sessionQueueRequests Linux and 1 available nodeStereotypes Windows with matching browserName chrome should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 2, + "totalSlots": 2 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Windows 11\"}}]", + "sessions": [] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"firefox\", \"browserVersion\": \"130.0\", \"platformName\": \"Windows 11\"}}]", + "sessions": [] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"firefox\", \"browserVersion\": \"130.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}" + ] + } + } + } + `), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, + { + name: "scaler browserVersion is latest, 2 sessionQueueRequests wihtout browserVersion, 2 available nodeStereotypes with different versions and platforms, should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 0, + "totalSlots": 0 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"92.0\", \"platformName\": \"Windows 11\"}}]", + "sessions": [] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, + { + name: "scaler browserVersion is latest, 5 sessionQueueRequests wihtout browserVersion also 1 different platformName, 1 available nodeStereotypes with 3 slots Linux and 1 node Windows, should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 6, + "totalSlots": 6 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 0, + "maxSession": 3, + "slotCount": 3, + "stereotypes": "[{\"slots\": 3, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 0, + "maxSession": 3, + "slotCount": 3, + "stereotypes": "[{\"slots\": 3, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"92.0\", \"platformName\": \"Windows 11\"}}]", + "sessions": [] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"Windows 11\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, + { + name: "queue request with browserName browserVersion and browserVersion but no available nodes should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 1, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"firefox\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"firefox\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, + { + name: "1 queue request with browserName browserVersion and browserVersion but 2 nodes without available slots should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 2, + "maxSession": 2, + "totalSlots": 2 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-2", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, + { + name: "2 session queue with matching browsername and browserversion of 2 available slots should return count as 0", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 2, + "totalSlots": 2 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "linux", + }, + want: 0, + wantErr: false, + }, + { + name: "2 queue requests with browserName browserVersion and platformName matching 2 available slots on 2 different nodes should return count as 0", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 2, + "maxSession": 4, + "totalSlots": 4 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 2, + "slotCount": 2, + "stereotypes": "[{\"slots\": 2, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 1, + "maxSession": 2, + "slotCount": 2, + "stereotypes": "[{\"slots\": 2, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-2", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "linux", + }, + want: 0, + wantErr: false, + }, + { + name: "1 queue request with browserName browserVersion and platformName matching 1 available slot on node has 3 max sessions should return count as 0", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 2, + "maxSession": 3, + "totalSlots": 3 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 2, + "maxSession": 3, + "slotCount": 3, + "stereotypes": "[{\"slots\": 3, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + }, + { + "id": "session-2", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "linux", + }, + want: 0, + wantErr: false, + }, + { + name: "3 queue requests with browserName browserVersion and platformName but 2 running nodes are busy should return count as 3", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 2, + "maxSession": 2, + "totalSlots": 2 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-2", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "linux", + }, + want: 3, + wantErr: false, + }, + { + name: "3 queue requests with browserName browserVersion and platformName but 2 running nodes are busy with different versions should return count as 3", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 2, + "maxSession": 2, + "totalSlots": 2 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-2", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"90.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"92.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"93.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 3, + wantErr: false, + }, + { + name: "3 queue requests with browserName and platformName but 2 running nodes are busy with different versions should return count as 3", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 2, + "maxSession": 2, + "totalSlots": 2 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + }, + { + "id": "node-2", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-2", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 3, + wantErr: false, + }, + { + name: "1 active session with matching browsername and version should return count as 2", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 1, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "linux", + }, + want: 2, + wantErr: false, + }, + { + name: "1 request without browserName and browserVersion stable can be match any available node should return count as 0", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"v128.0\", \"platformName\": \"linux\"}}]", + "sessions": [] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserVersion\": \"stable\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 0, + wantErr: false, + }, + { + name: "1 request without browserName and browserVersion stable should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 1, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"v128.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"v128.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserVersion\": \"stable\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, + { + name: "2 queue requests with browserName in string match node stereotype and scaler metadata browserVersion should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 1, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"msedge\", \"browserVersion\": \"dev\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"msedge\", \"browserVersion\": \"dev\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"MicrosoftEdge\", \"browserVersion\": \"beta\", \"platformName\": \"linux\"}", + "{\"browserName\": \"MicrosoftEdge\", \"browserVersion\": \"dev\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "MicrosoftEdge", + sessionBrowserName: "msedge", + browserVersion: "dev", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, + { + name: "2 queue requests with matching browsername/sessionBrowserName but 1 node is busy should return count as 2", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 1, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"msedge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"msedge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"MicrosoftEdge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"MicrosoftEdge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "MicrosoftEdge", + sessionBrowserName: "msedge", + browserVersion: "91.0", + platformName: "linux", + }, + want: 2, + wantErr: false, + }, + { + name: "2 queue requests with matching browsername/sessionBrowserName and 1 node is is available should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"msedge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"MicrosoftEdge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}", + "{\"browserName\": \"MicrosoftEdge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "MicrosoftEdge", + sessionBrowserName: "msedge", + browserVersion: "91.0", + platformName: "linux", + }, + want: 1, + wantErr: false, + }, { + name: "2 queue requests with platformName and without platformName and node with 1 slot available should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 1, + "maxSession": 2, + "totalSlots": 2 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 2, + "slotCount": 2, + "stereotypes": "[{\"slots\": 2, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"Windows 11\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"Windows 11\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"Windows 11\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "Windows 11", + }, + want: 1, + 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": { + "sessionCount": 1, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"msedge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"msedge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 2, + wantErr: false, + }, + { + name: "3 queue requests browserName chrome platformName linux but 1 node has maxSessions=3 with browserName msedge should return a count of 3", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 1, + "maxSession": 3, + "totalSlots": 3 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 1, + "maxSession": 3, + "slotCount": 3, + "stereotypes": "[{\"slots\": 3, \"stereotype\": {\"browserName\": \"msedge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}}]", + "sessions": [ + { + "id": "session-1", + "capabilities": "{\"browserName\": \"msedge\", \"browserVersion\": \"91.0\", \"platformName\": \"linux\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + }, + want: 3, + wantErr: false, + }, + { + name: "session request with matching browsername and no specific platformName should return count as 2", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "maxSession": 0, + "nodeCount": 0, + "totalSlots": 0 + }, + "nodesInfo": { + "nodes": [] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"Windows 11\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "", + }, + want: 2, + wantErr: false, + }, + { + name: "2 queue requests with 1 matching browsername and platformName and 1 existing slot is available should return count as 0", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 1, + "totalSlots": 1 + }, + "nodesInfo": { + "nodes": [ + { + "id": "node-1", + "status": "UP", + "sessionCount": 0, + "maxSession": 1, + "slotCount": 1, + "stereotypes": "[{\"slots\": 1, \"stereotype\": {\"browserName\": \"chrome\", \"browserVersion\": \"91.0\", \"platformName\": \"Windows 11\"}}]", + "sessions": [] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"platformName\": \"Windows 11\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "Windows 11", + }, + want: 0, + wantErr: false, + }, + { + name: "2 queue requests with 1 request matching browserName and platformName but 1 existing node is busy should return count as 1", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 2, + "maxSession": 2, + "totalSlots": 2 + }, + "nodesInfo": { + "nodes": [ + { + "id": "82ee33bd-390e-4dd6-aee2-06b17ecee18e", + "status": "UP", + "sessionCount": 2, + "maxSession": 2, + "slotCount": 2, + "stereotypes": "[\n {\n \"slots\": 2,\n \"stereotype\": {\n \"browserName\": \"chrome\",\n \"browserVersion\": \"128.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"se:containerName\": \"my-chrome-name-m5n8z-4br6x\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\"browserName\": \"chrome\", \"platformName\": \"Windows 11\", \"browserVersion\": \"91.0\"}" + }, + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\"browserName\": \"chrome\", \"platformName\": \"linux\", \"browserVersion\": \"91.0\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"Windows 11\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "Windows 11", + }, + want: 1, + wantErr: false, + }, + { + name: "5 queue requests with scaler parameter nodeMaxSessions is 2 should return count as 3", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 0, + "totalSlots": 0 + }, + "nodesInfo": { + "nodes": [] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}" + ] + } + } + } + `), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + nodeMaxSessions: 2, + }, + want: 3, + wantErr: false, + }, + { + name: "5 queue requests with scaler parameter nodeMaxSessions is 3 should return count as 2", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 0, + "maxSession": 0, + "totalSlots": 0 + }, + "nodesInfo": { + "nodes": [] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}", + "{\"browserName\": \"chrome\", \"browserVersion\": \"128.0\", \"platformName\": \"Linux\"}" + ] + } + } + } + `), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "latest", + platformName: "linux", + nodeMaxSessions: 3, + }, + want: 2, + wantErr: false, + }, + { + name: "5 queue requests with request matching browserName and platformName and scaler param nodeMaxSessions is 3 and existing node with 1 available slot should return count as 2", + args: args{ + b: []byte(`{ + "data": { + "grid": { + "sessionCount": 2, + "maxSession": 3, + "totalSlots": 3 + }, + "nodesInfo": { + "nodes": [ + { + "id": "82ee33bd-390e-4dd6-aee2-06b17ecee18e", + "status": "UP", + "sessionCount": 2, + "maxSession": 3, + "slotCount": 3, + "stereotypes": "[\n {\n \"slots\": 3,\n \"stereotype\": {\n \"browserName\": \"chrome\",\n \"platformName\": \"linux\",\n \"browserVersion\": \"91.0\",\n \"goog:chromeOptions\": {\n \"binary\": \"\\u002fusr\\u002fbin\\u002fchromium\"\n },\n \"se:containerName\": \"my-chrome-name-m5n8z-4br6x\",\n \"se:downloadsEnabled\": true,\n \"se:noVncPort\": 7900,\n \"se:vncEnabled\": true\n }\n }\n]", + "sessions": [ + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\"browserName\": \"chrome\", \"platformName\": \"Linux\", \"browserVersion\": \"91.0\"}" + }, + { + "id": "0f9c5a941aa4d755a54b84be1f6535b1", + "capabilities": "{\"browserName\": \"chrome\", \"platformName\": \"linux\", \"browserVersion\": \"91.0\"}" + } + ] + } + ] + }, + "sessionsInfo": { + "sessionQueueRequests": [ + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}", + "{\"browserName\": \"chrome\", \"platformName\": \"linux\"}" + ] + } + } + }`), + browserName: "chrome", + sessionBrowserName: "chrome", + browserVersion: "91.0", + platformName: "linux", + nodeMaxSessions: 3, + }, + want: 2, + 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, tt.args.sessionBrowserName, tt.args.platformName, tt.args.nodeMaxSessions, logr.Discard()) + if (err != nil) != tt.wantErr { + t.Errorf("getCountFromSeleniumResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getCountFromSeleniumResponse() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseSeleniumGridScalerMetadata(t *testing.T) { + type args struct { + config *scalersconfig.ScalerConfig + } + tests := []struct { + name string + args args + want *seleniumGridScalerMetadata + wantErr bool + }{ + { + name: "invalid url string should throw error", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{}, + }, + }, + wantErr: true, + }, + { + name: "invalid browsername string should throw error", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "", + }, + }, + }, + wantErr: true, + }, + { + name: "valid url and browsername should return metadata", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "browserName": "chrome", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + BrowserVersion: "latest", + PlatformName: "linux", + NodeMaxSessions: 1, + }, + }, + { + name: "valid url, browsername, and sessionbrowsername should return metadata", + args: args{ + config: &scalersconfig.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", + PlatformName: "linux", + NodeMaxSessions: 1, + }, + }, + { + name: "valid url in AuthParams, browsername, and sessionbrowsername should return metadata", + args: args{ + config: &scalersconfig.ScalerConfig{ + AuthParams: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "username": "user", + "password": "password", + }, + TriggerMetadata: map[string]string{ + "browserName": "MicrosoftEdge", + "sessionBrowserName": "msedge", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + URL: "http://selenium-hub:4444/graphql", + Username: "user", + Password: "password", + BrowserName: "MicrosoftEdge", + SessionBrowserName: "msedge", + TargetValue: 1, + BrowserVersion: "latest", + PlatformName: "linux", + NodeMaxSessions: 1, + }, + }, + { + name: "valid url and browsername should return metadata", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "browserName": "chrome", + "browserVersion": "91.0", + "unsafeSsl": "false", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + BrowserVersion: "91.0", + UnsafeSsl: false, + PlatformName: "linux", + NodeMaxSessions: 1, + }, + }, + { + name: "valid url, browsername, unsafeSsl and activationThreshold should return metadata", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "browserName": "chrome", + "browserVersion": "91.0", + "unsafeSsl": "true", + "activationThreshold": "10", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + ActivationThreshold: 10, + BrowserVersion: "91.0", + UnsafeSsl: true, + PlatformName: "linux", + NodeMaxSessions: 1, + }, + }, + { + name: "valid url, browsername and unsafeSsl but invalid activationThreshold should throw an error", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "browserName": "chrome", + "browserVersion": "91.0", + "unsafeSsl": "true", + "activationThreshold": "AA", + }, + }, + }, + wantErr: true, + }, + { + name: "valid url, browsername, unsafeSsl and activationThreshold with default platformName should return metadata", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "browserName": "chrome", + "browserVersion": "91.0", + "unsafeSsl": "true", + "activationThreshold": "10", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + ActivationThreshold: 10, + BrowserVersion: "91.0", + UnsafeSsl: true, + PlatformName: "linux", + NodeMaxSessions: 1, + }, + }, + { + name: "valid url, browsername, unsafeSsl, activationThreshold and platformName should return metadata", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "browserName": "chrome", + "browserVersion": "91.0", + "unsafeSsl": "true", + "activationThreshold": "10", + "platformName": "Windows 11", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + URL: "http://selenium-hub:4444/graphql", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + ActivationThreshold: 10, + BrowserVersion: "91.0", + UnsafeSsl: true, + PlatformName: "Windows 11", + NodeMaxSessions: 1, + }, + }, + { + name: "valid url, browsername, unsafeSsl, activationThreshold, nodeMaxSessions and platformName should return metadata", + args: args{ + config: &scalersconfig.ScalerConfig{ + TriggerMetadata: map[string]string{ + "url": "http://selenium-hub:4444/graphql", + "username": "user", + "password": "password", + "browserName": "chrome", + "browserVersion": "91.0", + "unsafeSsl": "true", + "activationThreshold": "10", + "platformName": "Windows 11", + "nodeMaxSessions": "3", + }, + }, + }, + wantErr: false, + want: &seleniumGridScalerMetadata{ + URL: "http://selenium-hub:4444/graphql", + Username: "user", + Password: "password", + BrowserName: "chrome", + SessionBrowserName: "chrome", + TargetValue: 1, + ActivationThreshold: 10, + BrowserVersion: "91.0", + UnsafeSsl: true, + PlatformName: "Windows 11", + NodeMaxSessions: 3, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseSeleniumGridScalerMetadata(tt.args.config) + if (err != nil) != tt.wantErr { + t.Errorf("parseSeleniumGridScalerMetadata() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseSeleniumGridScalerMetadata() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/Makefile b/Makefile index 9e91fe9b1..811875590 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,10 @@ PLATFORMS := $(or $(PLATFORMS),$(shell echo $$PLATFORMS),$(CURRENT_PLATFORM)) SEL_PASSWD := $(or $(SEL_PASSWD),$(SEL_PASSWD),secret) CHROMIUM_VERSION := $(or $(CHROMIUM_VERSION),$(CHROMIUM_VERSION),latest) SBOM_OUTPUT := $(or $(SBOM_OUTPUT),$(SBOM_OUTPUT),package_versions.txt) +KEDA_TAG_PREV_VERSION := $(or $(KEDA_TAG_PREV_VERSION),$(KEDA_TAG_PREV_VERSION),2.15.1-selenium-grid) +KEDA_TAG_VERSION := $(or $(KEDA_TAG_VERSION),$(KEDA_TAG_VERSION),2.15.1-selenium-grid) +KEDA_BASED_NAME := $(or $(KEDA_BASED_NAME),$(KEDA_BASED_NAME),ndviet) +KEDA_BASED_TAG := $(or $(KEDA_BASED_TAG),$(KEDA_BASED_TAG),2.15.1-selenium-grid) all: hub \ distributor \ @@ -242,6 +246,33 @@ standalone_edge_beta: edge_beta video: cd ./Video && SEL_PASSWD=$(SEL_PASSWD) docker buildx build --platform $(PLATFORMS) $(BUILD_ARGS) --build-arg NAMESPACE=$(FFMPEG_BASED_NAME) --build-arg BASED_TAG=$(FFMPEG_BASED_TAG) --secret id=SEL_PASSWD --sbom=true --attest type=provenance,mode=max -t $(NAME)/video:$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) . +fetch_grid_scaler_resources: + mkdir -p ./.keda/scalers \ + && cd ./.keda/scalers \ + && curl -L https://raw.githubusercontent.com/$(KEDA_BASED_NAME)/keda/v$(KEDA_BASED_TAG)/pkg/scalers/selenium_grid_scaler.go -o selenium_grid_scaler.go \ + && curl -L https://raw.githubusercontent.com/$(KEDA_BASED_NAME)/keda/v$(KEDA_BASED_TAG)/pkg/scalers/selenium_grid_scaler_test.go -o selenium_grid_scaler_test.go \ + && curl -L https://raw.githubusercontent.com/$(KEDA_BASED_NAME)/keda-docs/main/content/docs/2.16/scalers/selenium-grid-scaler.md -o selenium-grid-scaler.md + +fetch_grid_scaler_images: + docker pull --platform linux/amd64 --platform linux/arm64 $(KEDA_BASED_NAME)/keda:$(KEDA_BASED_TAG) + docker pull --platform linux/amd64 --platform linux/arm64 $(KEDA_BASED_NAME)/keda-metrics-apiserver:$(KEDA_BASED_TAG) + docker pull --platform linux/amd64 --platform linux/arm64 $(KEDA_BASED_NAME)/keda-admission-webhooks:$(KEDA_BASED_TAG) + +release_grid_scaler: fetch_grid_scaler_images + docker buildx imagetools create -t $(NAME)/keda:$(KEDA_TAG_VERSION)-$(BUILD_DATE) $(KEDA_BASED_NAME)/keda:$(KEDA_BASED_TAG) + docker buildx imagetools create -t $(NAME)/keda-metrics-apiserver:$(KEDA_TAG_VERSION)-$(BUILD_DATE) $(KEDA_BASED_NAME)/keda-metrics-apiserver:$(KEDA_BASED_TAG) + docker buildx imagetools create -t $(NAME)/keda-admission-webhooks:$(KEDA_TAG_VERSION)-$(BUILD_DATE) $(KEDA_BASED_NAME)/keda-admission-webhooks:$(KEDA_BASED_TAG) + +release_grid_scaler_latest: fetch_grid_scaler_images + docker buildx imagetools create -t $(NAME)/keda:latest $(KEDA_BASED_NAME)/keda:$(KEDA_BASED_TAG) + docker buildx imagetools create -t $(NAME)/keda-metrics-apiserver:latest $(KEDA_BASED_NAME)/keda-metrics-apiserver:$(KEDA_BASED_TAG) + docker buildx imagetools create -t $(NAME)/keda-admission-webhooks:latest $(KEDA_BASED_NAME)/keda-admission-webhooks:$(KEDA_BASED_TAG) + +release_grid_scaler_nightly: fetch_grid_scaler_images + docker buildx imagetools create -t $(NAME)/keda:nightly $(KEDA_BASED_NAME)/keda:$(KEDA_BASED_TAG) + docker buildx imagetools create -t $(NAME)/keda-metrics-apiserver:nightly $(KEDA_BASED_NAME)/keda-metrics-apiserver:$(KEDA_BASED_TAG) + docker buildx imagetools create -t $(NAME)/keda-admission-webhooks:nightly $(KEDA_BASED_NAME)/keda-admission-webhooks:$(KEDA_BASED_TAG) + count_image_layers: docker history $(NAME)/base:$(TAG_VERSION) -q | wc -l docker history $(NAME)/hub:$(TAG_VERSION) -q | wc -l @@ -321,7 +352,7 @@ tag_latest: docker tag $(NAME)/standalone-docker:$(TAG_VERSION) $(NAME)/standalone-docker:latest docker tag $(NAME)/video:$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) $(NAME)/video:latest -release_latest: +release_latest: release_grid_scaler_latest docker push $(NAME)/base:latest docker push $(NAME)/hub:latest docker push $(NAME)/distributor:latest @@ -366,7 +397,7 @@ tag_nightly: docker tag $(NAME)/standalone-docker:$(TAG_VERSION) $(NAME)/standalone-docker:nightly docker tag $(NAME)/video:$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) $(NAME)/video:nightly -release_nightly: +release_nightly: release_grid_scaler_nightly docker push $(NAME)/base:nightly docker push $(NAME)/hub:nightly docker push $(NAME)/distributor:nightly @@ -446,7 +477,7 @@ tag_major_minor: docker tag $(NAME)/standalone-firefox:$(TAG_VERSION) $(NAME)/standalone-firefox:$(MAJOR_MINOR_PATCH) docker tag $(NAME)/standalone-docker:$(TAG_VERSION) $(NAME)/standalone-docker:$(MAJOR_MINOR_PATCH) -release: tag_major_minor +release: tag_major_minor release_grid_scaler @if ! docker images $(NAME)/base | awk '{ print $$2 }' | grep -q -F $(TAG_VERSION); then echo "$(NAME)/base version $(TAG_VERSION) is not yet built. Please run 'make build'"; false; fi @if ! docker images $(NAME)/hub | awk '{ print $$2 }' | grep -q -F $(TAG_VERSION); then echo "$(NAME)/hub version $(TAG_VERSION) is not yet built. Please run 'make build'"; false; fi @if ! docker images $(NAME)/distributor | awk '{ print $$2 }' | grep -q -F $(TAG_VERSION); then echo "$(NAME)/distributor version $(TAG_VERSION) is not yet built. Please run 'make build'"; false; fi @@ -875,7 +906,7 @@ chart_render_template: chart_test_autoscaling_disabled: PLATFORMS=$(PLATFORMS) TEST_CHROMIUM=true RELEASE_NAME=selenium SELENIUM_GRID_AUTOSCALING=false CHART_ENABLE_TRACING=true \ SECURE_INGRESS_ONLY_GENERATE=true SELENIUM_GRID_PROTOCOL=https SELENIUM_GRID_HOST=$$(hostname -i) SELENIUM_GRID_PORT=443 EXTERNAL_UPLOADER_CONFIG=true \ - VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ + VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ TEMPLATE_OUTPUT_FILENAME="k8s_nodeChromium_enableTracing_secureIngress_generateCerts_ingressPublicIP_subPath.yaml" \ ./tests/charts/make/chart_test.sh NoAutoscaling @@ -883,7 +914,7 @@ chart_test_autoscaling_deployment_https: PLATFORMS=$(PLATFORMS) CHART_FULL_DISTRIBUTED_MODE=true CHART_ENABLE_BASIC_AUTH=true \ SECURE_INGRESS_ONLY_DEFAULT=true INGRESS_DISABLE_USE_HTTP2=true SELENIUM_GRID_PROTOCOL=https CHART_ENABLE_INGRESS_HOSTNAME=true SELENIUM_GRID_PORT=443 \ SELENIUM_GRID_AUTOSCALING_MIN_REPLICA=1 \ - VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ + VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ TEMPLATE_OUTPUT_FILENAME="k8s_fullDistributed_basicAuth_secureIngress_defaultCerts_ingressHostName_disableHttp2_autoScaling_scaledObject_subPath.yaml" \ ./tests/charts/make/chart_test.sh DeploymentAutoscaling @@ -891,28 +922,28 @@ chart_test_autoscaling_deployment: PLATFORMS=$(PLATFORMS) TEST_EXISTING_KEDA=true RELEASE_NAME=selenium CHART_ENABLE_TRACING=true \ SECURE_CONNECTION_SERVER=true SECURE_USE_EXTERNAL_CERT=true SERVICE_TYPE_NODEPORT=true SELENIUM_GRID_PROTOCOL=https SELENIUM_GRID_HOST=$$(hostname -i) SELENIUM_GRID_PORT=31444 \ SELENIUM_GRID_AUTOSCALING_MIN_REPLICA=1 \ - VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ + VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ TEMPLATE_OUTPUT_FILENAME="k8s_prefixSelenium_enableTracing_secureServer_externalCerts_nodePort_autoScaling_scaledObject_existingKEDA_subPath.yaml" \ ./tests/charts/make/chart_test.sh DeploymentAutoscaling chart_test_autoscaling_job_https: PLATFORMS=$(PLATFORMS) TEST_EXISTING_KEDA=true RELEASE_NAME=selenium CHART_ENABLE_BASIC_AUTH=true \ SECURE_CONNECTION_SERVER=true SELENIUM_GRID_PROTOCOL=https SELENIUM_GRID_PORT=443 SUB_PATH=/ \ - VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) EXTERNAL_UPLOADER_CONFIG=true \ + VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) EXTERNAL_UPLOADER_CONFIG=true \ TEMPLATE_OUTPUT_FILENAME="k8s_prefixSelenium_basicAuth_secureServer_autoScaling_scaledJob_existingKEDA.yaml" \ ./tests/charts/make/chart_test.sh JobAutoscaling chart_test_autoscaling_job_hostname: PLATFORMS=$(PLATFORMS) CHART_ENABLE_TRACING=true CHART_ENABLE_BASIC_AUTH=true \ SECURE_INGRESS_ONLY_DEFAULT=true SECURE_USE_EXTERNAL_CERT=true SELENIUM_GRID_PROTOCOL=https SELENIUM_GRID_HOST=$$(hostname -i) SELENIUM_GRID_PORT=443 \ - VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ + VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ TEMPLATE_OUTPUT_FILENAME="k8s_enableTracing_basicAuth_secureIngress_externalCerts_ingressPublicIP_autoScaling_scaledJob_subPath.yaml" \ ./tests/charts/make/chart_test.sh JobAutoscaling chart_test_autoscaling_job: PLATFORMS=$(PLATFORMS) TEST_EXISTING_KEDA=true TEST_CHROMIUM=true RELEASE_NAME=selenium CHART_ENABLE_TRACING=true CHART_FULL_DISTRIBUTED_MODE=true \ SECURE_INGRESS_ONLY_CONFIG_INLINE=true SECURE_USE_EXTERNAL_CERT=true CHART_ENABLE_INGRESS_HOSTNAME=true SELENIUM_GRID_PROTOCOL=https SELENIUM_GRID_HOST=selenium-grid.prod SUB_PATH=/ SELENIUM_GRID_PORT=443 \ - VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ + VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ TEMPLATE_OUTPUT_FILENAME="k8s_fullDistributed_secureIngress_externalCerts_ingressHostName_ingressTLSInline_autoScaling_scaledJob_existingKEDA_prefixSelenium_nodeChromium_enableTracing.yaml" \ ./tests/charts/make/chart_test.sh JobAutoscaling @@ -920,7 +951,7 @@ chart_test_language_bindings: PLATFORMS=$(PLATFORMS) \ SELENIUM_GRID_HOST=$$(hostname -i) \ SELENIUM_GRID_AUTOSCALING_MIN_REPLICA=1 \ - VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ + VERSION=$(TAG_VERSION) VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) KEDA_BASED_NAME=$(KEDA_BASED_NAME) KEDA_BASED_TAG=$(KEDA_BASED_TAG) NAMESPACE=$(NAMESPACE) BINDING_VERSION=$(BINDING_VERSION) \ ./tests/charts/make/chart_test.sh DeploymentAutoscaling .PHONY: \ diff --git a/charts/selenium-grid/CONFIGURATION.md b/charts/selenium-grid/CONFIGURATION.md index eddfa2e15..d98a1902e 100644 --- a/charts/selenium-grid/CONFIGURATION.md +++ b/charts/selenium-grid/CONFIGURATION.md @@ -311,7 +311,7 @@ A Helm chart for creating a Selenium Grid Server in Kubernetes | autoscaling.scaledOptions.minReplicaCount | int | `0` | Minimum number of replicas | | autoscaling.scaledOptions.maxReplicaCount | int | `8` | Maximum number of replicas | | autoscaling.scaledOptions.pollingInterval | int | `10` | Polling interval in seconds | -| autoscaling.scaledJobOptions.scalingStrategy.strategy | string | `"accurate"` | Scaling strategy for KEDA ScaledJob | +| autoscaling.scaledJobOptions.scalingStrategy.strategy | string | `"default"` | Scaling strategy for KEDA ScaledJob | | autoscaling.scaledJobOptions.successfulJobsHistoryLimit | int | `0` | Number of Completed jobs should be kept | | autoscaling.scaledJobOptions.failedJobsHistoryLimit | int | `0` | Number of Failed jobs should be kept (for troubleshooting purposes) | | autoscaling.scaledJobOptions.jobTargetRef | object | `{"backoffLimit":0,"completions":1,"parallelism":1}` | Specify job target ref for KEDA ScaledJob | @@ -495,7 +495,10 @@ A Helm chart for creating a Selenium Grid Server in Kubernetes | videoRecorder.extraVolumes | list | `[]` | Extra volumes for video recorder pod | | videoRecorder.s3 | object | `{"args":[],"command":[],"extraEnvironmentVariables":null,"imageName":"bitnami/aws-cli","imagePullPolicy":"IfNotPresent","imageRegistry":"public.ecr.aws","imageTag":"latest","securityContext":{"runAsUser":0}}` | Container spec for the uploader if above it is defined as "uploader.name: s3" | | customLabels | object | `{}` | Custom labels for k8s resources | -| keda | object | `{"additionalAnnotations":null,"http":{"timeout":60000},"webhooks":{"enabled":false}}` | Configuration for dependency chart keda | +| keda.image | object | `{"keda":{"registry":"selenium","repository":"keda","tag":"2.15.1-selenium-grid-20240907"},"metricsApiServer":{"registry":"selenium","repository":"keda-metrics-apiserver","tag":"2.15.1-selenium-grid-20240907"},"webhooks":{"registry":"selenium","repository":"keda-admission-webhooks","tag":"2.15.1-selenium-grid-20240907"}}` | Specify image for KEDA components | +| keda.additionalAnnotations | string | `nil` | Annotations for KEDA resources | +| keda.http.timeout | int | `60000` | | +| keda.webhooks | object | `{"enabled":false}` | Enable KEDA admission webhooks component | | ingress-nginx | object | `{"controller":{"admissionWebhooks":{"enabled":false}}}` | Configuration for dependency chart ingress-nginx | | kube-prometheus-stack | object | `{"cleanPrometheusOperatorObjectNames":true}` | Configuration for dependency chart kube-prometheus-stack | | jaeger | object | `{"agent":{"enabled":false},"allInOne":{"enabled":true,"extraEnv":[{"name":"QUERY_BASE_PATH","value":"/jaeger"}]},"collector":{"enabled":false},"provisionDataStore":{"cassandra":false},"query":{"enabled":false},"storage":{"type":"badger"}}` | Configuration for dependency chart jaeger | diff --git a/charts/selenium-grid/README.md b/charts/selenium-grid/README.md index 3f07dd8a1..70222a736 100644 --- a/charts/selenium-grid/README.md +++ b/charts/selenium-grid/README.md @@ -127,6 +127,8 @@ or [jobs](https://keda.sh/docs/latest/concepts/scaling-jobs/) and the charts sup chart support both modes. It is controlled with `autoscaling.scalingType` that can be set to either job (default) or deployment. +Preview new changes in Selenium Grid scaler implementation. Refer to [README](../../.keda/README.md) + ### Settings common for both `job` and `deployment` scalingType There are few settings that are common for both scaling types. These are grouped under `autoscaling.scaledOptions`. diff --git a/charts/selenium-grid/values.yaml b/charts/selenium-grid/values.yaml index 9e9ae5fab..e0bc1dcdb 100644 --- a/charts/selenium-grid/values.yaml +++ b/charts/selenium-grid/values.yaml @@ -819,9 +819,9 @@ autoscaling: # Options for KEDA ScaledJobs (only used when scalingType is set to "job"). See https://keda.sh/docs/latest/concepts/scaling-jobs/#scaledjob-spec scaledJobOptions: scalingStrategy: - # Change this to "accurate" when the calculation problem is fixed + # Offer the strategy default with scaler calculation updated in https://github.com/SeleniumHQ/docker-selenium/tree/trunk/.keda/README.md # -- Scaling strategy for KEDA ScaledJob - strategy: accurate + strategy: default # -- Number of Completed jobs should be kept successfulJobsHistoryLimit: 0 # -- Number of Failed jobs should be kept (for troubleshooting purposes) @@ -1517,12 +1517,28 @@ videoRecorder: # -- Custom labels for k8s resources customLabels: {} -# -- Configuration for dependency chart keda +# Configuration for dependency chart keda keda: # enabled: false + # -- Specify image for KEDA components + image: + keda: + registry: selenium + repository: keda + tag: "2.15.1-selenium-grid-20240907" + metricsApiServer: + registry: selenium + repository: keda-metrics-apiserver + tag: "2.15.1-selenium-grid-20240907" + webhooks: + registry: selenium + repository: keda-admission-webhooks + tag: "2.15.1-selenium-grid-20240907" + # -- Annotations for KEDA resources additionalAnnotations: http: timeout: 60000 + # -- Enable KEDA admission webhooks component webhooks: enabled: false diff --git a/generate_chart_changelog.sh b/generate_chart_changelog.sh index 1e960fbd0..1a4ab27dc 100755 --- a/generate_chart_changelog.sh +++ b/generate_chart_changelog.sh @@ -82,6 +82,10 @@ generate_changelog() { echo "" >>"$temp_file" fi + echo "### Experimental" >>"$temp_file" + echo "- Selenium Grid Scaler implementation preview. [README](https://github.com/seleniumhq/docker-selenium/tree/trunk/.keda/README.md)" >>"$temp_file" + echo "" >>"$temp_file" + # Create chart_release_notes.md release_notes_file="$CHART_DIR/RELEASE_NOTES.md" chart_description=$(find . \( -type d -name .git -prune \) -o -type f -wholename '*/selenium-grid/Chart.yaml' -print0 | xargs -0 cat | grep ^description | cut -d ':' -f 2) diff --git a/tests/charts/ci/DeploymentAutoscaling-values.yaml b/tests/charts/ci/DeploymentAutoscaling-values.yaml index c92299350..d947de48f 100644 --- a/tests/charts/ci/DeploymentAutoscaling-values.yaml +++ b/tests/charts/ci/DeploymentAutoscaling-values.yaml @@ -2,7 +2,7 @@ autoscaling: scalingType: deployment scaledOptions: minReplicaCount: 0 - maxReplicaCount: 3 + maxReplicaCount: 4 pollingInterval: 10 scaledObjectOptions: cooldownPeriod: 30 diff --git a/tests/charts/ci/base-auth-ingress-values.yaml b/tests/charts/ci/base-auth-ingress-values.yaml index b5a310ba7..2e1c64344 100644 --- a/tests/charts/ci/base-auth-ingress-values.yaml +++ b/tests/charts/ci/base-auth-ingress-values.yaml @@ -33,6 +33,10 @@ hub: components: extraEnvironmentVariables: *extraEnvironmentVariables +keda: + webhooks: + enabled: true + ingress-nginx: controller: hostPort: diff --git a/tests/charts/make/chart_cluster_setup.sh b/tests/charts/make/chart_cluster_setup.sh index ded500a19..1d0ffe29e 100755 --- a/tests/charts/make/chart_cluster_setup.sh +++ b/tests/charts/make/chart_cluster_setup.sh @@ -18,7 +18,6 @@ SKIP_CLEANUP=${SKIP_CLEANUP:-"false"} # For debugging purposes, retain the clust KUBERNETES_VERSION=${KUBERNETES_VERSION:-$(curl -L -s https://dl.k8s.io/release/stable.txt)} CNI=${CNI:-"calico"} # auto, calico, cilium CONTAINER_RUNTIME=${CONTAINER_RUNTIME:-"docker"} # docker, containerd, cri-o -TEST_EXISTING_KEDA=${TEST_EXISTING_KEDA:-"true"} # Function to clean up for retry step on workflow cleanup() { diff --git a/tests/charts/make/chart_test.sh b/tests/charts/make/chart_test.sh index 88093e032..d33a50db3 100755 --- a/tests/charts/make/chart_test.sh +++ b/tests/charts/make/chart_test.sh @@ -26,6 +26,8 @@ SKIP_CLEANUP=${SKIP_CLEANUP:-"true"} # For debugging purposes, retain the cluste CHART_CERT_PATH=${CHART_CERT_PATH:-"${CHART_PATH}/certs/tls.crt"} SSL_CERT_DIR=${SSL_CERT_DIR:-"/etc/ssl/certs"} VIDEO_TAG=${VIDEO_TAG:-"latest"} +KEDA_BASED_NAME=${KEDA_BASED_NAME:-"selenium"} +KEDA_BASED_TAG=${KEDA_BASED_TAG:-"latest"} CHART_ENABLE_TRACING=${CHART_ENABLE_TRACING:-"false"} CHART_FULL_DISTRIBUTED_MODE=${CHART_FULL_DISTRIBUTED_MODE:-"false"} HOSTNAME_ADDRESS=${HOSTNAME_ADDRESS:-${SELENIUM_GRID_HOST}} @@ -34,7 +36,7 @@ CHART_ENABLE_BASIC_AUTH=${CHART_ENABLE_BASIC_AUTH:-"false"} BASIC_AUTH_USERNAME=${BASIC_AUTH_USERNAME:-"sysAdminUser"} BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD:-"myStrongPassword"} LOG_LEVEL=${LOG_LEVEL:-"INFO"} -TEST_EXISTING_KEDA=${TEST_EXISTING_KEDA:-"true"} +TEST_EXISTING_KEDA=${TEST_EXISTING_KEDA:-"false"} TEST_UPGRADE_CHART=${TEST_UPGRADE_CHART:-"false"} RENDER_HELM_TEMPLATE_ONLY=${RENDER_HELM_TEMPLATE_ONLY:-"false"} TEST_PV_CLAIM_NAME=${TEST_PV_CLAIM_NAME:-"selenium-grid-pvc-local"} @@ -328,8 +330,18 @@ fi if [ "${TEST_EXISTING_KEDA}" = "true" ] && [ "${TEST_UPGRADE_CHART}" != "true" ]; then helm repo add kedacore https://kedacore.github.io/charts - echo "Install KEDA core on kind kubernetes cluster" - helm upgrade -i ${KEDA_NAMESPACE} -n ${KEDA_NAMESPACE} --create-namespace --set webhooks.enabled=false kedacore/keda + echo "Install KEDA core on Kubernetes cluster" + helm upgrade -i ${KEDA_NAMESPACE} -n ${KEDA_NAMESPACE} --create-namespace --set webhooks.enabled=true \ + --set image.keda.registry=${KEDA_BASED_NAME} --set image.keda.repository=keda --set image.keda.tag=${KEDA_BASED_TAG} \ + --set image.metricsApiServer.registry=${KEDA_BASED_NAME} --set image.metricsApiServer.repository=keda-metrics-apiserver --set image.metricsApiServer.tag=${KEDA_BASED_TAG} \ + --set image.webhooks.registry=${KEDA_BASED_NAME} --set image.webhooks.repository=keda-admission-webhooks --set image.webhooks.tag=${KEDA_BASED_TAG} \ + kedacore/keda +elif [ "${TEST_EXISTING_KEDA}" != "true" ] && [ "${TEST_UPGRADE_CHART}" != "true" ]; then + HELM_COMMAND_SET_IMAGES="${HELM_COMMAND_SET_IMAGES} \ + --set keda.image.keda.registry=${KEDA_BASED_NAME} --set keda.image.keda.repository=keda --set keda.image.keda.tag=${KEDA_BASED_TAG} \ + --set keda.image.metricsApiServer.registry=${KEDA_BASED_NAME} --set keda.image.metricsApiServer.repository=keda-metrics-apiserver --set keda.image.metricsApiServer.tag=${KEDA_BASED_TAG} \ + --set keda.image.webhooks.registry=${KEDA_BASED_NAME} --set keda.image.webhooks.repository=keda-admission-webhooks --set keda.image.webhooks.tag=${KEDA_BASED_TAG} \ + " fi if [ "${TEST_EXISTING_KEDA}" = "true" ] && [ "${TEST_UPGRADE_CHART}" != "true" ]; then diff --git a/update_tag_in_docs_and_files.sh b/update_tag_in_docs_and_files.sh index c122ec237..3e218c763 100755 --- a/update_tag_in_docs_and_files.sh +++ b/update_tag_in_docs_and_files.sh @@ -8,6 +8,8 @@ latest_chart_app_version=$(find . \( -type d -name .git -prune \) -o -type f -wh FFMPEG_TAG_PREV_VERSION=$(grep FFMPEG_TAG_PREV_VERSION Makefile | sed 's/.*,\([^)]*\))/\1/p' | head -n 1) FFMPEG_TAG_VERSION=$(grep FFMPEG_TAG_VERSION Makefile | sed 's/.*,\([^)]*\))/\1/p' | head -n 1) RCLONE_TAG_VERSION=$(grep RCLONE_TAG_VERSION Makefile | sed 's/.*,\([^)]*\))/\1/p' | head -n 1) +KEDA_TAG_PREV_VERSION=$(grep KEDA_TAG_PREV_VERSION Makefile | sed 's/.*,\([^)]*\))/\1/p' | head -n 1) +KEDA_TAG_VERSION=$(grep KEDA_TAG_VERSION Makefile | sed 's/.*,\([^)]*\))/\1/p' | head -n 1) echo -e "\033[0;32m Updating tag displayed in docs and files...\033[0m" echo -e "\033[0;32m LATEST_TAG -> ${LATEST_TAG}\033[0m" @@ -16,6 +18,9 @@ echo -e "\033[0;32m NEXT_TAG -> ${NEXT_TAG}\033[0m" # If you want to test this locally and you are using macOS, do `brew install gnu-sed` and change `sed` for `gsed`. find . \( -type d -name .git -prune \) -o -type f ! -name 'CHANGELOG.md' -print0 | xargs -0 sed -i "s/${FFMPEG_TAG_PREV_VERSION}/${FFMPEG_TAG_VERSION}/g" +# If you want to test this locally and you are using macOS, do `brew install gnu-sed` and change `sed` for `gsed`. +find . \( -type d -name .git -prune \) -o -type f ! -name 'CHANGELOG.md' -print0 | xargs -0 sed -i "s/${KEDA_TAG_PREV_VERSION}/${KEDA_TAG_VERSION}/g" + # If you want to test this locally and you are using macOS, do `brew install gnu-sed` and change `sed` for `gsed`. find . \( -type d -name .git -prune \) -o -type f ! -name 'CHANGELOG.md' -print0 | xargs -0 sed -i "s/${LATEST_TAG}/${NEXT_TAG}/g"