From 51a5a7590fa75a372be162abf68ccd08f4e4fdf7 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Tue, 5 Nov 2024 19:16:56 -0600 Subject: [PATCH 1/6] pants-plugins/uses_services: add support for uses=st2cluster metadata --- pants-plugins/uses_services/register.py | 2 + .../scripts/is_st2cluster_running.py | 48 +++++ .../uses_services/st2cluster_rules.py | 164 ++++++++++++++++++ .../uses_services/st2cluster_rules_test.py | 95 ++++++++++ pants-plugins/uses_services/target_types.py | 2 +- 5 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 pants-plugins/uses_services/scripts/is_st2cluster_running.py create mode 100644 pants-plugins/uses_services/st2cluster_rules.py create mode 100644 pants-plugins/uses_services/st2cluster_rules_test.py diff --git a/pants-plugins/uses_services/register.py b/pants-plugins/uses_services/register.py index 346f4ecf2e..bbe6833ee9 100644 --- a/pants-plugins/uses_services/register.py +++ b/pants-plugins/uses_services/register.py @@ -21,6 +21,7 @@ platform_rules, rabbitmq_rules, redis_rules, + st2cluster_rules, system_user_rules, ) from uses_services.target_types import UsesServicesField @@ -36,5 +37,6 @@ def rules(): *mongo_rules.rules(), *rabbitmq_rules.rules(), *redis_rules.rules(), + *st2cluster_rules.rules(), *system_user_rules.rules(), ] diff --git a/pants-plugins/uses_services/scripts/is_st2cluster_running.py b/pants-plugins/uses_services/scripts/is_st2cluster_running.py new file mode 100644 index 0000000000..c5286c4e87 --- /dev/null +++ b/pants-plugins/uses_services/scripts/is_st2cluster_running.py @@ -0,0 +1,48 @@ +# Copyright 2023 The StackStorm Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import socket +import sys + +from contextlib import closing + + +def _is_st2cluster_running(host: str, ports: list[str]) -> bool: + """Check for listening ports of st2auth, st2api, and st2stream services. + + This should not import the st2 code as it should be self-contained. + """ + # TODO: Once each service gains a reliable health check endpoint, use that. + # https://github.com/StackStorm/st2/issues/4020 + for port in ports: + # based on https://stackoverflow.com/a/35370008/1134951 + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + # errno=0 means the connection succeeded + if sock.connect_ex((host, int(port))) != 0: + # failed to create a connection to the port. + return False + return True + + +if __name__ == "__main__": + hostname = "127.0.0.1" + service_ports = list(sys.argv[1:]) + if not service_ports: + # st2.tests*.conf ends in /, but the default ends in // + service_ports = ["9100", "9101", "9102"] + + is_running = _is_st2cluster_running(hostname, service_ports) + exit_code = 0 if is_running else 1 + sys.exit(exit_code) diff --git a/pants-plugins/uses_services/st2cluster_rules.py b/pants-plugins/uses_services/st2cluster_rules.py new file mode 100644 index 0000000000..eb8e3a6b2b --- /dev/null +++ b/pants-plugins/uses_services/st2cluster_rules.py @@ -0,0 +1,164 @@ +# Copyright 2023 The StackStorm Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +from dataclasses import dataclass +from textwrap import dedent + +from pants.backend.python.goals.pytest_runner import ( + PytestPluginSetupRequest, + PytestPluginSetup, +) +from pants.backend.python.target_types import Executable +from pants.backend.python.util_rules.pex import ( + PexRequest, + VenvPex, + VenvPexProcess, + rules as pex_rules, +) +from pants.engine.fs import CreateDigest, Digest, FileContent +from pants.engine.rules import collect_rules, Get, rule +from pants.engine.process import FallibleProcessResult, ProcessCacheScope +from pants.engine.target import Target +from pants.engine.unions import UnionRule +from pants.util.logging import LogLevel + +from uses_services.exceptions import ServiceMissingError +from uses_services.platform_rules import Platform +from uses_services.scripts.is_st2cluster_running import ( + __file__ as is_st2cluster_running_full_path, +) +from uses_services.target_types import UsesServicesField + + +@dataclass(frozen=True) +class UsesSt2ClusterRequest: + """One or more targets need a running st2 cluster with all st2* services.""" + + auth_port: int = 9100 + api_port: int = 9101 + stream_port: int = 9102 + + @property + def ports(self) -> tuple[str, ...]: + return str(self.auth_port), str(self.api_port), str(self.stream_port) + + +@dataclass(frozen=True) +class St2ClusterIsRunning: + pass + + +class PytestUsesSt2ClusterRequest(PytestPluginSetupRequest): + @classmethod + def is_applicable(cls, target: Target) -> bool: + if not target.has_field(UsesServicesField): + return False + uses = target.get(UsesServicesField).value + return uses is not None and "st2cluster" in uses + + +@rule( + desc="Ensure ST2 Cluster is running and accessible before running tests.", + level=LogLevel.DEBUG, +) +async def st2cluster_is_running_for_pytest( + request: PytestUsesSt2ClusterRequest, +) -> PytestPluginSetup: + # this will raise an error if st2cluster is not running + _ = await Get(St2ClusterIsRunning, UsesSt2ClusterRequest()) + + return PytestPluginSetup() + + +@rule( + desc="Test to see if ST2 Cluster is running and accessible.", + level=LogLevel.DEBUG, +) +async def st2cluster_is_running( + request: UsesSt2ClusterRequest, platform: Platform +) -> St2ClusterIsRunning: + script_path = "./is_st2cluster_running.py" + + # pants is already watching this directory as it is under a source root. + # So, we don't need to double watch with PathGlobs, just open it. + with open(is_st2cluster_running_full_path, "rb") as script_file: + script_contents = script_file.read() + + script_digest = await Get( + Digest, CreateDigest([FileContent(script_path, script_contents)]) + ) + script_pex = await Get( + VenvPex, + PexRequest( + output_filename="script.pex", + internal_only=True, + sources=script_digest, + main=Executable(script_path), + ), + ) + + result = await Get( + FallibleProcessResult, + VenvPexProcess( + script_pex, + argv=request.ports, + input_digest=script_digest, + description="Checking to see if ST2 Cluster is up and accessible.", + # this can change from run to run, so don't cache results. + cache_scope=ProcessCacheScope.PER_SESSION, + level=LogLevel.DEBUG, + ), + ) + is_running = result.exit_code == 0 + + if is_running: + return St2ClusterIsRunning() + + # st2cluster is not running, so raise an error with instructions. + instructions = dedent( + """\ + A full StackStorm cluster is required to run some integration tests. + To start the dev StackStorm cluster, run this from the repo root + (probably in a new terminal/window, as the output is quite verbose): + + tools/launchdev.sh start -x + + This runs each StackStorm microservice in a tmux session. You can + inspect the logs for this service in the `logs/` directory. + + If tmux is not installed, please install it with a package manager, + or use vagrant for local development with something like: + + vagrant init stackstorm/st2 + vagrant up + vagrant ssh + + Please see: https://docs.stackstorm.com/install/vagrant.html + """ + ) + raise ServiceMissingError( + service="st2cluster", + platform=platform, + instructions=instructions, + msg=f"The dev StackStorm cluster seems to be down.\n{instructions}", + ) + + +def rules(): + return [ + *collect_rules(), + UnionRule(PytestPluginSetupRequest, PytestUsesSt2ClusterRequest), + *pex_rules(), + ] diff --git a/pants-plugins/uses_services/st2cluster_rules_test.py b/pants-plugins/uses_services/st2cluster_rules_test.py new file mode 100644 index 0000000000..ec62d3b862 --- /dev/null +++ b/pants-plugins/uses_services/st2cluster_rules_test.py @@ -0,0 +1,95 @@ +# Copyright 2023 The StackStorm Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import pytest + +from pants.engine.internals.scheduler import ExecutionError +from pants.testutil.rule_runner import QueryRule, RuleRunner + +from .data_fixtures import platform, platform_samples +from .exceptions import ServiceMissingError +from .st2cluster_rules import ( + St2ClusterIsRunning, + UsesSt2ClusterRequest, + rules as st2cluster_rules, +) +from .platform_rules import Platform + + +@pytest.fixture +def rule_runner() -> RuleRunner: + return RuleRunner( + rules=[ + *st2cluster_rules(), + QueryRule(St2ClusterIsRunning, (UsesSt2ClusterRequest, Platform)), + ], + target_types=[], + ) + + +def run_st2cluster_is_running( + rule_runner: RuleRunner, + uses_st2cluster_request: UsesSt2ClusterRequest, + mock_platform: Platform, + *, + extra_args: list[str] | None = None, +) -> St2ClusterIsRunning: + rule_runner.set_options( + [ + "--backend-packages=uses_services", + *(extra_args or ()), + ], + env_inherit={"PATH", "PYENV_ROOT", "HOME"}, + ) + result = rule_runner.request( + St2ClusterIsRunning, + [uses_st2cluster_request, mock_platform], + ) + return result + + +# Warning this requires that st2cluster be running +def test_st2cluster_is_running(rule_runner: RuleRunner) -> None: + request = UsesSt2ClusterRequest() + mock_platform = platform(os="TestMock") + + # we are asserting that this does not raise an exception + is_running = run_st2cluster_is_running(rule_runner, request, mock_platform) + assert is_running + + +@pytest.mark.parametrize("mock_platform", platform_samples) +def test_st2cluster_not_running( + rule_runner: RuleRunner, mock_platform: Platform +) -> None: + request = UsesSt2ClusterRequest( + # some unassigned ports that are unlikely to be used + auth_port=10, + api_port=12, + stream_port=14, + ) + + with pytest.raises(ExecutionError) as exception_info: + run_st2cluster_is_running(rule_runner, request, mock_platform) + + execution_error = exception_info.value + assert len(execution_error.wrapped_exceptions) == 1 + + exc = execution_error.wrapped_exceptions[0] + assert isinstance(exc, ServiceMissingError) + + assert exc.service == "st2cluster" + assert "The dev StackStorm cluster seems to be down" in str(exc) + assert exc.instructions != "" diff --git a/pants-plugins/uses_services/target_types.py b/pants-plugins/uses_services/target_types.py index 0a0f2d89bd..386da388c0 100644 --- a/pants-plugins/uses_services/target_types.py +++ b/pants-plugins/uses_services/target_types.py @@ -14,7 +14,7 @@ from pants.engine.target import StringSequenceField -supported_services = ("mongo", "rabbitmq", "redis", "system_user") +supported_services = ("mongo", "rabbitmq", "redis", "st2cluster", "system_user") class UsesServicesField(StringSequenceField): From 2c16249eca7dfb828788cc48f2893b3e5609eef1 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Tue, 5 Nov 2024 22:18:15 -0600 Subject: [PATCH 2/6] pants-plugins/uses_services: mock st2cluster in test --- .../uses_services/st2cluster_rules_test.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/pants-plugins/uses_services/st2cluster_rules_test.py b/pants-plugins/uses_services/st2cluster_rules_test.py index ec62d3b862..3e2c426313 100644 --- a/pants-plugins/uses_services/st2cluster_rules_test.py +++ b/pants-plugins/uses_services/st2cluster_rules_test.py @@ -13,6 +13,10 @@ # limitations under the License. from __future__ import annotations +import socket + +from contextlib import closing + import pytest from pants.engine.internals.scheduler import ExecutionError @@ -60,9 +64,33 @@ def run_st2cluster_is_running( return result +@pytest.fixture +def mock_st2cluster() -> tuple[int, int, int]: + sock1: socket.socket + sock2: socket.socket + sock3: socket.socket + with ( + closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock1, + closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock2, + closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock3, + ): + socks = (sock1, sock2, sock3) + for sock in socks: + sock.bind(("127.0.0.1", 0)) + sock.listen(1) + ports = tuple(sock.getsockname()[1] for sock in socks) + yield ports + + # Warning this requires that st2cluster be running -def test_st2cluster_is_running(rule_runner: RuleRunner) -> None: - request = UsesSt2ClusterRequest() +def test_st2cluster_is_running( + rule_runner: RuleRunner, mock_st2cluster: tuple[int, int, int] +) -> None: + request = UsesSt2ClusterRequest( + auth_port=mock_st2cluster[0], + api_port=mock_st2cluster[1], + stream_port=mock_st2cluster[2], + ) mock_platform = platform(os="TestMock") # we are asserting that this does not raise an exception From 30275b99d49612a0f8398acfebc0d4e782e9a583 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Tue, 5 Nov 2024 19:16:18 -0600 Subject: [PATCH 3/6] pants-plugins/uses_services: add uses=st2cluster BUILD metadata --- contrib/runners/orquesta_runner/tests/integration/BUILD | 3 ++- st2tests/integration/orquesta/BUILD | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/runners/orquesta_runner/tests/integration/BUILD b/contrib/runners/orquesta_runner/tests/integration/BUILD index 8d33e316f4..3b3c37c180 100644 --- a/contrib/runners/orquesta_runner/tests/integration/BUILD +++ b/contrib/runners/orquesta_runner/tests/integration/BUILD @@ -5,5 +5,6 @@ __defaults__( python_tests( name="tests", - uses=["mongo", "rabbitmq", "redis", "system_user"], + uses=["mongo", "rabbitmq", "redis", "st2cluster", "system_user"], + tags=["integration", "st2cluster"], ) diff --git a/st2tests/integration/orquesta/BUILD b/st2tests/integration/orquesta/BUILD index 8073d3f7a5..cde5b3e56d 100644 --- a/st2tests/integration/orquesta/BUILD +++ b/st2tests/integration/orquesta/BUILD @@ -1,6 +1,7 @@ python_tests( name="tests", - uses=["mongo", "rabbitmq", "redis", "system_user"], + uses=["mongo", "rabbitmq", "redis", "st2cluster", "system_user"], + tags=["integration", "st2cluster"], ) python_test_utils( From 284436c68f299c647dc5ef87849cf7bf6bf82d2f Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Sat, 16 Nov 2024 23:34:13 -0600 Subject: [PATCH 4/6] update changelog entry --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c3ae075a31..105aed43d1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -69,7 +69,7 @@ Added working on StackStorm, improve our security posture, and improve CI reliability thanks in part to pants' use of PEX lockfiles. This is not a user-facing addition. #6118 #6141 #6133 #6120 #6181 #6183 #6200 #6237 #6229 #6240 #6241 #6244 #6251 #6253 - #6254 #6258 #6259 #6260 #6269 #6275 + #6254 #6258 #6259 #6260 #6269 #6275 #6279 Contributed by @cognifloyd * Build of ST2 EL9 packages #6153 Contributed by @amanda11 From 50c91ccb3302b15d041acfb369ce9c1f8557200d Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Mon, 18 Nov 2024 09:36:20 -0600 Subject: [PATCH 5/6] pants-plugins/uses_services: correct copyright year to 2024 The copyrights were copy/pasta. These files were written in 2024, not 2023. --- pants-plugins/uses_services/scripts/is_st2cluster_running.py | 2 +- pants-plugins/uses_services/st2cluster_rules.py | 2 +- pants-plugins/uses_services/st2cluster_rules_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pants-plugins/uses_services/scripts/is_st2cluster_running.py b/pants-plugins/uses_services/scripts/is_st2cluster_running.py index c5286c4e87..fce910c786 100644 --- a/pants-plugins/uses_services/scripts/is_st2cluster_running.py +++ b/pants-plugins/uses_services/scripts/is_st2cluster_running.py @@ -1,4 +1,4 @@ -# Copyright 2023 The StackStorm Authors. +# Copyright 2024 The StackStorm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/pants-plugins/uses_services/st2cluster_rules.py b/pants-plugins/uses_services/st2cluster_rules.py index eb8e3a6b2b..5b4a55768e 100644 --- a/pants-plugins/uses_services/st2cluster_rules.py +++ b/pants-plugins/uses_services/st2cluster_rules.py @@ -1,4 +1,4 @@ -# Copyright 2023 The StackStorm Authors. +# Copyright 2024 The StackStorm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/pants-plugins/uses_services/st2cluster_rules_test.py b/pants-plugins/uses_services/st2cluster_rules_test.py index 3e2c426313..d884ef7ac1 100644 --- a/pants-plugins/uses_services/st2cluster_rules_test.py +++ b/pants-plugins/uses_services/st2cluster_rules_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 The StackStorm Authors. +# Copyright 2024 The StackStorm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From b0767b9c29c5c562f227d5be34a89f631710c962 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Mon, 18 Nov 2024 11:59:56 -0600 Subject: [PATCH 6/6] pants-plugins/uses_services: prepare to allow alternative st2cluster hosts/ports In the uses_services plugin, I am now adding env var support (in several PRs), but so far I've avoiding parsing conf files. Plus, allowing reconfiguration of the st2cluster hosts/ports would require the st2client and st2 dev cluster config to align. Today, the st2cluster integration tests hardcode st2client's base_url to `http://127.0.0.1`. Technically someone could override the endpoint URLs with ST2_{AUTH,API,STREAM}_URL env vars, but they would also need to make sure whatever cluster they use has the examples pack and other things handled by `launchdev.sh`. Today, the other alternative for using a different cluster would be to forward the service ports to the localhost at the standard ports. For now, just use a separate host for each endpoint so it is easier to support overriding via env vars and/or conf files at some point in the future--if someone needs that. Until then, we'll go with the minimal implementation that assumes the default hosts and ports. --- .../scripts/is_st2cluster_running.py | 22 ++++++++----- .../uses_services/st2cluster_rules.py | 33 +++++++++++++++++-- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/pants-plugins/uses_services/scripts/is_st2cluster_running.py b/pants-plugins/uses_services/scripts/is_st2cluster_running.py index fce910c786..46ad55ca6e 100644 --- a/pants-plugins/uses_services/scripts/is_st2cluster_running.py +++ b/pants-plugins/uses_services/scripts/is_st2cluster_running.py @@ -19,14 +19,14 @@ from contextlib import closing -def _is_st2cluster_running(host: str, ports: list[str]) -> bool: +def _is_st2cluster_running(endpoints: list[tuple[str, str]]) -> bool: """Check for listening ports of st2auth, st2api, and st2stream services. This should not import the st2 code as it should be self-contained. """ # TODO: Once each service gains a reliable health check endpoint, use that. # https://github.com/StackStorm/st2/issues/4020 - for port in ports: + for host, port in endpoints: # based on https://stackoverflow.com/a/35370008/1134951 with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: # errno=0 means the connection succeeded @@ -37,12 +37,16 @@ def _is_st2cluster_running(host: str, ports: list[str]) -> bool: if __name__ == "__main__": - hostname = "127.0.0.1" - service_ports = list(sys.argv[1:]) - if not service_ports: - # st2.tests*.conf ends in /, but the default ends in // - service_ports = ["9100", "9101", "9102"] - - is_running = _is_st2cluster_running(hostname, service_ports) + args_iter = iter(sys.argv[1:]) + # Turn the list into 2 tuples (zip with query the same iterator twice for each entry) + endpoints = list(zip(args_iter, args_iter)) + if not endpoints: + endpoints = [ + ("127.0.0.1", "9100"), + ("127.0.0.1", "9101"), + ("127.0.0.1", "9102"), + ] + + is_running = _is_st2cluster_running(endpoints) exit_code = 0 if is_running else 1 sys.exit(exit_code) diff --git a/pants-plugins/uses_services/st2cluster_rules.py b/pants-plugins/uses_services/st2cluster_rules.py index 5b4a55768e..4f2eb02929 100644 --- a/pants-plugins/uses_services/st2cluster_rules.py +++ b/pants-plugins/uses_services/st2cluster_rules.py @@ -46,13 +46,36 @@ class UsesSt2ClusterRequest: """One or more targets need a running st2 cluster with all st2* services.""" + auth_host: str = "127.0.0.1" auth_port: int = 9100 + api_host: str = "127.0.0.1" api_port: int = 9101 + stream_host: str = "127.0.0.1" stream_port: int = 9102 @property - def ports(self) -> tuple[str, ...]: - return str(self.auth_port), str(self.api_port), str(self.stream_port) + def endpoints(self) -> tuple[tuple[str, str], ...]: + return ( + (self.auth_host, str(self.auth_port)), + (self.api_host, str(self.api_port)), + (self.stream_host, str(self.stream_port)), + ) + + # @classmethod + # def from_env(cls, env: EnvironmentVars) -> UsesSt2ClusterRequest: + # return cls() + # TODO: consider adding a from_env method using one or both of client => server vars: + # ST2_CONFIG_FILE => ST2_CONFIG_PATH (used by many tests, so not safe) or + # ST2_CONF (only in launchdev.sh and st2ctl) + # ST2_BASE_URL => ST2_WEBUI__WEBUI_BASE_URL + # ST2_API_URL => ST2_AUTH__API_URL or + # http{'s' if ST2_API__USE_SSL else ''}://{ST2_API__HOST}:{ST2_API__PORT} + # ST2_AUTH_URL => http://{ST2_AUTH__HOST}:{ST2_AUTH__PORT} + # ST2_STREAM_URL => http://{ST2_STREAM__HOST}:{ST2_STREAM__PORT} + # ST2_CACERT (might be needed if using a self-signed cert) => n/a + # These st2client env vars are irrelevant for the connectivity check: + # ST2_AUTH_TOKEN or ST2_API_KEY + # ST2_API_VERSION (always "v1" since we don't have anything else) @dataclass(frozen=True) @@ -113,7 +136,11 @@ async def st2cluster_is_running( FallibleProcessResult, VenvPexProcess( script_pex, - argv=request.ports, + argv=[ + host_or_port + for endpoint in request.endpoints + for host_or_port in endpoint + ], input_digest=script_digest, description="Checking to see if ST2 Cluster is up and accessible.", # this can change from run to run, so don't cache results.