diff --git a/ENV_VARIABLES.md b/ENV_VARIABLES.md index bdd746a219..8c9958e280 100644 --- a/ENV_VARIABLES.md +++ b/ENV_VARIABLES.md @@ -53,7 +53,7 @@ | SE_HTTPS_PRIVATE_KEY | /opt/selenium/secrets/tls.key | | | | SE_ENABLE_TRACING | true | | | | SE_OTEL_EXPORTER_ENDPOINT | | | | -| SE_OTEL_SERVICE_NAME | selenium-standalone-docker | | | +| SE_OTEL_SERVICE_NAME | selenium-router | | | | SE_OTEL_JVM_ARGS | | | | | SE_OTEL_TRACES_EXPORTER | otlp | | | | SE_OTEL_JAVA_GLOBAL_AUTOCONFIGURE_ENABLED | true | | | @@ -121,3 +121,24 @@ | SE_SUPERVISORD_START_RETRIES | 5 | | | | SE_RECORD_AUDIO | false | Flag to enable recording the audio source (default is Pulse Audio input) | | | SE_AUDIO_SOURCE | -f pulse -ac 2 -i default | FFmpeg arguments to record the audio source | | +| SE_BROWSER_BINARY_LOCATION | | | | +| SE_NODE_BROWSER_NAME | | | | +| SE_NODE_CONTAINER_NAME | | | | +| SE_NODE_HOST | | | | +| SE_NODE_MAX_CONCURRENCY | | When node is handled both browser and relay, SE_NODE_MAX_CONCURRENCY is used to configure max concurrency based on sum of them | | +| SE_NODE_RELAY_BROWSER_NAME | | | | +| SE_NODE_RELAY_MAX_SESSIONS | | | | +| SE_NODE_RELAY_PLATFORM_NAME | | | | +| SE_NODE_RELAY_PLATFORM_VERSION | | | | +| SE_NODE_RELAY_PROTOCOL_VERSION | | | | +| SE_NODE_RELAY_STATUS_ENDPOINT | | | | +| SE_NODE_RELAY_URL | | | | +| SE_NODE_STEREOTYPE | | Capabilities in JSON string to overwrite the default Node stereotype | | +| SE_NODE_STEREOTYPE_EXTRA | | Extra capabilities in JSON string that wants to merge to the default Node stereotype | | +| SE_SESSIONS_MAP_EXTERNAL_HOSTNAME | | | | +| SE_SESSIONS_MAP_EXTERNAL_IMPLEMENTATION | | | | +| SE_SESSIONS_MAP_EXTERNAL_JDBC_PASSWORD | | | | +| SE_SESSIONS_MAP_EXTERNAL_JDBC_URL | | | | +| SE_SESSIONS_MAP_EXTERNAL_JDBC_USER | | | | +| SE_SESSIONS_MAP_EXTERNAL_PORT | | | | +| SE_SESSIONS_MAP_EXTERNAL_SCHEME | | | | diff --git a/NodeBase/Dockerfile b/NodeBase/Dockerfile index a66bd76b1a..15bd35cc50 100644 --- a/NodeBase/Dockerfile +++ b/NodeBase/Dockerfile @@ -152,7 +152,8 @@ COPY --chown="${SEL_UID}:${SEL_GID}" start-selenium-node.sh \ start-xvfb.sh \ start-vnc.sh \ start-novnc.sh \ - generate_config generate_relay_config /opt/bin/ + generate_config generate_relay_config json_merge.py /opt/bin/ +RUN chmod +x /opt/bin/*.sh /opt/bin/*.py /opt/bin/generate_* # Selenium Grid logo as wallpaper for Fluxbox COPY selenium_grid_logo.png /usr/share/images/fluxbox/ubuntu-light.png diff --git a/NodeBase/generate_config b/NodeBase/generate_config index a0cf89b649..b4dd861eff 100755 --- a/NodeBase/generate_config +++ b/NodeBase/generate_config @@ -61,13 +61,23 @@ fi if [ -f /opt/selenium/browser_binary_location ] && [ -z "${SE_BROWSER_BINARY_LOCATION}" ]; then SE_BROWSER_BINARY_LOCATION=$(cat /opt/selenium/browser_binary_location) fi +SE_NODE_CONTAINER_NAME="${SE_NODE_CONTAINER_NAME:-$(hostname)}" # 'browserName' is mandatory for default stereotype if [[ -z "${SE_NODE_STEREOTYPE}" ]] && [[ -n "${SE_NODE_BROWSER_NAME}" ]]; then - SE_NODE_STEREOTYPE="{\"browserName\": \"${SE_NODE_BROWSER_NAME}\", \"browserVersion\": \"${SE_NODE_BROWSER_VERSION}\", \"platformName\": \"${SE_NODE_PLATFORM_NAME}\", ${SE_BROWSER_BINARY_LOCATION}, \"se:containerName\": \"${SE_NODE_CONTAINER_NAME}\"}" + SE_NODE_STEREOTYPE="{\"browserName\": \"${SE_NODE_BROWSER_NAME}\", \"browserVersion\": \"${SE_NODE_BROWSER_VERSION}\", \"platformName\": \"${SE_NODE_PLATFORM_NAME}\", ${SE_BROWSER_BINARY_LOCATION}, \"se:containerName\": \"${SE_NODE_CONTAINER_NAME}\", \"container:hostname\": \"$(hostname)\"}" else SE_NODE_STEREOTYPE="${SE_NODE_STEREOTYPE}" fi +if [[ -n "${SE_NODE_STEREOTYPE_EXTRA}" ]]; then + echo "Merging SE_NODE_STEREOTYPE_EXTRA=${SE_NODE_STEREOTYPE_EXTRA} to main stereotype" + SE_NODE_STEREOTYPE="$(python3 /opt/bin/json_merge.py "${SE_NODE_STEREOTYPE}" "${SE_NODE_STEREOTYPE_EXTRA}")" + if [[ $? -ne 0 ]]; then + echo "Failed to merge SE_NODE_STEREOTYPE_EXTRA. Please check the format of the JSON string. Keep using main stereotype." + else + echo "Merged stereotype: ${SE_NODE_STEREOTYPE}" + fi +fi # 'stereotype' setting is mandatory if [[ -n "${SE_NODE_STEREOTYPE}" ]]; then diff --git a/NodeBase/json_merge.py b/NodeBase/json_merge.py new file mode 100755 index 0000000000..9f7fafcf4b --- /dev/null +++ b/NodeBase/json_merge.py @@ -0,0 +1,20 @@ +import json +import sys + +json_str1 = sys.argv[1] +json_str2 = sys.argv[2] + +try: + # Parse JSON strings into dictionaries + dict1 = json.loads(json_str1) + dict2 = json.loads(json_str2) + # Merge dictionaries + merged_dict = {**dict1, **dict2} + # Convert merged dictionary back to JSON string + merged_json_str = json.dumps(merged_dict, separators=(',', ':'), ensure_ascii=True) + # Print the merged JSON string + print(merged_json_str) +except: + # Print the first JSON string if an error occurs + print(json_str1) + sys.exit(1) diff --git a/README.md b/README.md index c2547c774c..f36d563662 100644 --- a/README.md +++ b/README.md @@ -1112,10 +1112,61 @@ Here is an example with the default values of these environment variables: $ docker run -d \ -e SE_EVENT_BUS_HOST= \ -e SE_EVENT_BUS_PUBLISH_PORT=4442 \ - -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 -e SE_NODE_STEREOTYPE="{\"browserName\":\"${SE_NODE_BROWSER_NAME}\",\"browserVersion\":\"${SE_NODE_BROWSER_VERSION}\",\"platformName\": \"Linux\"}" \ + -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \ + -e SE_NODE_STEREOTYPE="{\"browserName\":\"${SE_NODE_BROWSER_NAME}\", \"browserVersion\":\"${SE_NODE_BROWSER_VERSION}\", \"platformName\":\"${SE_NODE_PLATFORM_NAME}\"}" \ --shm-size="2g" selenium/node-chrome:4.28.1-20250123 ``` +In another case, if you want to retain the default Node stereotype and append additional capabilities, you can use the `SE_NODE_STEREOTYPE_EXTRA` environment variable to set your capabilities. Those will be merged to the default stereotype. For example: +```bash +$ docker run -d \ + -e SE_EVENT_BUS_HOST= \ + -e SE_EVENT_BUS_PUBLISH_PORT=4442 \ + -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \ + -e SE_NODE_STEREOTYPE_EXTRA="{\"myApp:version\":\"beta\", \"myApp:publish:\":\"public\"}" \ + --shm-size="2g" selenium/node-chrome:4.28.1-20250123 +``` + +This help setting custom capabilities for matching specific Nodes. For example, you added your custom capabilities when starting the Node, and you want assign a test to run on that Node which matches your capabilities. For example in test code: + +```python +options = ChromeOptions() +options.set_capability('myApp:version', 'beta') +options.set_capability('myApp:publish', 'public') +driver = webdriver.Remote(options=options, command_executor=SELENIUM_GRID_URL) +``` + +Noted: Your custom capabilities with key values should be in W3C capabilities convention, extension capabilities key must contain a ":" (colon) character, denoting an implementation specific namespace. + +Noted: Ensure that Node config `detect-drivers = false` in `config.toml` (or `--detect-drivers false` in CLI option) to make feature setting custom capabilities for matching specific Nodes get working. + +In addition, default Node stereotype includes capability `se:containerName` which can visible in node capabilities, or session capabilities to identify the container name where the node/session is running. **The prefixed `se:containerName` is not included in slot matcher**. By default, value is getting from `hostname` command in container, this value is equivalent to the `container_id` that you saw via `docker ps` command. If you want to override this value, you can set the environment variable `SE_NODE_CONTAINER_NAME` to your desired value. For example, when deploy to Kubernetes cluster, you can assign Pod name to env var `SE_NODE_CONTAINER_NAME` to track a node is running in which Pod. + +```yaml + env: + - name: SE_NODE_CONTAINER_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name +``` + +In an advanced case, where you control to spawn up a Node container, let it register to Hub, and then trigger a test to be assigned exactly to run on that Node. By default, the value of command `$(hostname)` is added to capability name `container:hostname` in Node stereotype. Combine with above feature setting custom capabilities for matching specific Nodes. You can use the `hostname` of the Node container just spawned up and set it as a custom capability. For example, in Python binding: + +```bash +$ docker run -d --name my-node-1 -e SE_EVENT_BUS_HOST=localhost -e SE_EVENT_BUS_PUBLISH_PORT=4442 -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \ + --shm-size="2g" selenium/node-chrome:4.28.1-20250123 +$ docker exec -i my-node-1 hostname +a6971f95bbab +``` + +```python +options = ChromeOptions() +options.set_capability('container:hostname', 'a6971f95bbab') +driver = webdriver.Remote(options=options, command_executor=SELENIUM_GRID_URL) +``` + +_Noted: Those above changes require new image tag where the changeset is included & released._ + ### Node configuration relay commands Relaying commands to a service endpoint that supports WebDriver. diff --git a/Standalone/generate_config b/Standalone/generate_config index 87494f2f75..5ca98d017c 100755 --- a/Standalone/generate_config +++ b/Standalone/generate_config @@ -41,12 +41,22 @@ fi if [ -f /opt/selenium/browser_binary_location ] && [ -z "${SE_BROWSER_BINARY_LOCATION}" ]; then SE_BROWSER_BINARY_LOCATION=$(cat /opt/selenium/browser_binary_location) fi +SE_NODE_CONTAINER_NAME="${SE_NODE_CONTAINER_NAME:-$(hostname)}" if [[ -z "$SE_NODE_STEREOTYPE" ]]; then - SE_NODE_STEREOTYPE="{\"browserName\": \"${SE_NODE_BROWSER_NAME}\", \"browserVersion\": \"${SE_NODE_BROWSER_VERSION}\", \"platformName\": \"${SE_NODE_PLATFORM_NAME}\", ${SE_BROWSER_BINARY_LOCATION}, \"se:containerName\": \"${SE_NODE_CONTAINER_NAME}\"}" + SE_NODE_STEREOTYPE="{\"browserName\": \"${SE_NODE_BROWSER_NAME}\", \"browserVersion\": \"${SE_NODE_BROWSER_VERSION}\", \"platformName\": \"${SE_NODE_PLATFORM_NAME}\", ${SE_BROWSER_BINARY_LOCATION}, \"se:containerName\": \"${SE_NODE_CONTAINER_NAME}\", \"container:hostname\": \"$(hostname)\"}" else SE_NODE_STEREOTYPE="$SE_NODE_STEREOTYPE" fi +if [[ -n "${SE_NODE_STEREOTYPE_EXTRA}" ]]; then + echo "Merging SE_NODE_STEREOTYPE_EXTRA=${SE_NODE_STEREOTYPE_EXTRA} to main stereotype" + SE_NODE_STEREOTYPE="$(python3 /opt/bin/json_merge.py "${SE_NODE_STEREOTYPE}" "${SE_NODE_STEREOTYPE_EXTRA}")" + if [[ $? -ne 0 ]]; then + echo "Failed to merge SE_NODE_STEREOTYPE_EXTRA. Please check the format of the JSON string. Keep using main stereotype." + else + echo "Merged stereotype: ${SE_NODE_STEREOTYPE}" + fi +fi echo "[[node.driver-configuration]]" >>"$FILENAME" echo "display-name = \"${SE_NODE_BROWSER_NAME}\"" >>"$FILENAME" diff --git a/scripts/generate_list_env_vars/description.yaml b/scripts/generate_list_env_vars/description.yaml index dab0832814..c944040b93 100644 --- a/scripts/generate_list_env_vars/description.yaml +++ b/scripts/generate_list_env_vars/description.yaml @@ -366,3 +366,68 @@ - name: SE_AUDIO_SOURCE description: FFmpeg arguments to record the audio source cli: '' +- name: SE_BROWSER_BINARY_LOCATION + description: '' + cli: '' +- name: SE_NODE_BROWSER_NAME + description: '' + cli: '' +- name: SE_NODE_CONTAINER_NAME + description: '' + cli: '' +- name: SE_NODE_HOST + description: '' + cli: '' +- name: SE_NODE_MAX_CONCURRENCY + description: When node is handled both browser and relay, SE_NODE_MAX_CONCURRENCY + is used to configure max concurrency based on sum of them + cli: '' +- name: SE_NODE_RELAY_BROWSER_NAME + description: '' + cli: '' +- name: SE_NODE_RELAY_MAX_SESSIONS + description: '' + cli: '' +- name: SE_NODE_RELAY_PLATFORM_NAME + description: '' + cli: '' +- name: SE_NODE_RELAY_PLATFORM_VERSION + description: '' + cli: '' +- name: SE_NODE_RELAY_PROTOCOL_VERSION + description: '' + cli: '' +- name: SE_NODE_RELAY_STATUS_ENDPOINT + description: '' + cli: '' +- name: SE_NODE_RELAY_URL + description: '' + cli: '' +- name: SE_NODE_STEREOTYPE + description: Capabilities in JSON string to overwrite the default Node stereotype + cli: '' +- name: SE_NODE_STEREOTYPE_EXTRA + description: Extra capabilities in JSON string that wants to merge to the default + Node stereotype + cli: '' +- name: SE_SESSIONS_MAP_EXTERNAL_HOSTNAME + description: '' + cli: '' +- name: SE_SESSIONS_MAP_EXTERNAL_IMPLEMENTATION + description: '' + cli: '' +- name: SE_SESSIONS_MAP_EXTERNAL_JDBC_PASSWORD + description: '' + cli: '' +- name: SE_SESSIONS_MAP_EXTERNAL_JDBC_URL + description: '' + cli: '' +- name: SE_SESSIONS_MAP_EXTERNAL_JDBC_USER + description: '' + cli: '' +- name: SE_SESSIONS_MAP_EXTERNAL_PORT + description: '' + cli: '' +- name: SE_SESSIONS_MAP_EXTERNAL_SCHEME + description: '' + cli: '' diff --git a/scripts/generate_list_env_vars/extract_env.py b/scripts/generate_list_env_vars/extract_env.py index b024bccc5f..64dd20eb1e 100644 --- a/scripts/generate_list_env_vars/extract_env.py +++ b/scripts/generate_list_env_vars/extract_env.py @@ -8,7 +8,7 @@ def extract_variables_from_shell_scripts(directory_path): for root, _, files in os.walk(directory_path): files.sort() for file in files: - if file.endswith(".sh"): + if file.endswith(".sh") or file.startswith("generate_"): file_path = os.path.join(root, file) try: with open(file_path, 'r') as f: diff --git a/scripts/generate_list_env_vars/value.yaml b/scripts/generate_list_env_vars/value.yaml index 39e9579e2f..a0c5698b3f 100644 --- a/scripts/generate_list_env_vars/value.yaml +++ b/scripts/generate_list_env_vars/value.yaml @@ -2,6 +2,8 @@ default: -f pulse -ac 2 -i default - name: SE_BIND_HOST default: 'false' +- name: SE_BROWSER_BINARY_LOCATION + default: '' - name: SE_BROWSER_LEFTOVERS_INTERVAL_SECS default: '3600' - name: SE_BROWSER_LEFTOVERS_PROCESSES_SECS @@ -72,8 +74,12 @@ default: '%Y-%m-%d %H:%M:%S,%3N' - name: SE_NEW_SESSION_THREAD_POOL_SIZE default: '' +- name: SE_NODE_BROWSER_NAME + default: '' - name: SE_NODE_BROWSER_VERSION default: stable +- name: SE_NODE_CONTAINER_NAME + default: '' - name: SE_NODE_DOCKER_CONFIG_FILENAME default: '' - name: SE_NODE_ENABLE_CDP @@ -88,6 +94,10 @@ default: '' - name: SE_NODE_HEARTBEAT_PERIOD default: '30' +- name: SE_NODE_HOST + default: '' +- name: SE_NODE_MAX_CONCURRENCY + default: '' - name: SE_NODE_MAX_SESSIONS default: '1' - name: SE_NODE_OVERRIDE_MAX_SESSIONS @@ -102,8 +112,26 @@ default: '' - name: SE_NODE_REGISTER_PERIOD default: '' +- name: SE_NODE_RELAY_BROWSER_NAME + default: '' +- name: SE_NODE_RELAY_MAX_SESSIONS + default: '' +- name: SE_NODE_RELAY_PLATFORM_NAME + default: '' +- name: SE_NODE_RELAY_PLATFORM_VERSION + default: '' +- name: SE_NODE_RELAY_PROTOCOL_VERSION + default: '' +- name: SE_NODE_RELAY_STATUS_ENDPOINT + default: '' +- name: SE_NODE_RELAY_URL + default: '' - name: SE_NODE_SESSION_TIMEOUT default: '300' +- name: SE_NODE_STEREOTYPE + default: '' +- name: SE_NODE_STEREOTYPE_EXTRA + default: '' - name: SE_NO_VNC_PORT default: '7900' - name: SE_OFFLINE @@ -117,7 +145,7 @@ - name: SE_OTEL_JVM_ARGS default: '' - name: SE_OTEL_SERVICE_NAME - default: selenium-standalone-docker + default: selenium-router - name: SE_OTEL_TRACES_EXPORTER default: otlp - name: SE_PRESET @@ -158,6 +186,20 @@ default: '' - name: SE_SESSIONS_MAP_EXTERNAL_DATASTORE default: 'false' +- name: SE_SESSIONS_MAP_EXTERNAL_HOSTNAME + default: '' +- name: SE_SESSIONS_MAP_EXTERNAL_IMPLEMENTATION + default: '' +- name: SE_SESSIONS_MAP_EXTERNAL_JDBC_PASSWORD + default: '' +- name: SE_SESSIONS_MAP_EXTERNAL_JDBC_URL + default: '' +- name: SE_SESSIONS_MAP_EXTERNAL_JDBC_USER + default: '' +- name: SE_SESSIONS_MAP_EXTERNAL_PORT + default: '' +- name: SE_SESSIONS_MAP_EXTERNAL_SCHEME + default: '' - name: SE_SESSIONS_MAP_HOST default: '' - name: SE_SESSIONS_MAP_PORT diff --git a/tests/docker-compose-v3-test-parallel.yml b/tests/docker-compose-v3-test-parallel.yml index aae7ffc6b6..2d66ef144d 100644 --- a/tests/docker-compose-v3-test-parallel.yml +++ b/tests/docker-compose-v3-test-parallel.yml @@ -38,6 +38,7 @@ services: - SE_VIDEO_FILE_NAME=auto - SE_SERVER_PROTOCOL=https - SE_NODE_GRID_URL=https://selenium-hub:4444 + - SE_NODE_STEREOTYPE_EXTRA={"myApp:version":"beta","myApp:publish":"public"} restart: always firefox: @@ -73,6 +74,7 @@ services: - SE_VIDEO_FILE_NAME=auto - SE_SERVER_PROTOCOL=https - SE_NODE_GRID_URL=https://selenium-hub:4444 + - SE_NODE_STEREOTYPE_EXTRA={"myApp:version":"beta","myApp:publish":"public"} restart: always edge: @@ -106,6 +108,7 @@ services: - SE_VIDEO_FILE_NAME=auto - SE_SERVER_PROTOCOL=https - SE_NODE_GRID_URL=https://selenium-hub:4444 + - SE_NODE_STEREOTYPE_EXTRA={"myApp:version":"beta","myApp:publish":"public"} restart: always selenium-hub: diff --git a/tests/docker-compose-v3-test-standalone.yml b/tests/docker-compose-v3-test-standalone.yml index b83df82013..386197a0b8 100644 --- a/tests/docker-compose-v3-test-standalone.yml +++ b/tests/docker-compose-v3-test-standalone.yml @@ -20,6 +20,7 @@ services: - SE_ROUTER_USERNAME=${BASIC_AUTH_USERNAME} - SE_ROUTER_PASSWORD=${BASIC_AUTH_PASSWORD} - SE_SUB_PATH=${SUB_PATH} + - SE_NODE_STEREOTYPE_EXTRA={"myApp:version":"beta","myApp:publish":"public"} ports: - "4444:4444" healthcheck: