Skip to content

Commit

Permalink
add redis to uses_services plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
cognifloyd committed Feb 7, 2023
1 parent 8a92560 commit 9d311c5
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 0 deletions.
1 change: 1 addition & 0 deletions contrib/runners/orquesta_runner/tests/integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ __defaults__(

python_tests(
name="tests",
uses=["redis"],
)
1 change: 1 addition & 0 deletions pants-plugins/uses_services/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ python_tests(
# overrides={
# "mongo_rules_test.py": {"uses": ["mongo"]},
# "rabbitmq_rules_test.py": {"uses": ["rabbitmq"]},
# "redis_rules_test.py": {"uses": ["redis"]},
# },
)
168 changes: 168 additions & 0 deletions pants-plugins/uses_services/redis_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# 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.util_rules.pex import (
PexRequest,
PexRequirements,
VenvPex,
VenvPexProcess,
rules as pex_rules,
)
from pants.engine.fs import CreateDigest, Digest, FileContent
from pants.engine.rules import collect_rules, Get, MultiGet, 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, ServiceSpecificMessages
from uses_services.platform_rules import Platform
from uses_services.scripts.is_redis_running import (
__file__ as is_redis_running_full_path,
)
from uses_services.target_types import UsesServicesField


@dataclass(frozen=True)
class UsesRedisRequest:
"""One or more targets need a running redis service using these settings.
The coord_* attributes represent the coordination settings from st2.conf.
In st2 code, they come from:
oslo_config.cfg.CONF.coordination.url
"""

# These config opts for integration tests are in:
# conf/st2.dev.conf (copied to conf/st2.ci.conf)
# TODO: for int tests: set url by either modifying st2.{dev,ci}.conf on the fly or via env vars.

# with our version of oslo.config (newer are slower) we can't directly override opts w/ environment variables.

coord_url: str = "redis://127.0.0.1:6379"


@dataclass(frozen=True)
class RedisIsRunning:
pass


class PytestUsesRedisRequest(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 "redis" in uses


@rule(
desc="Ensure redis is running and accessible before running tests.",
level=LogLevel.DEBUG,
)
async def redis_is_running_for_pytest(
request: PytestUsesRedisRequest,
) -> PytestPluginSetup:
# this will raise an error if redis is not running
_ = await Get(RedisIsRunning, UsesRedisRequest())

return PytestPluginSetup()


@rule(
desc="Test to see if redis is running and accessible.",
level=LogLevel.DEBUG,
)
async def redis_is_running(
request: UsesRedisRequest, platform: Platform
) -> RedisIsRunning:
script_path = "./is_redis_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_redis_running_full_path, "rb") as script_file:
script_contents = script_file.read()

script_digest, tooz_pex = await MultiGet(
Get(Digest, CreateDigest([FileContent(script_path, script_contents)])),
Get(
VenvPex,
PexRequest(
output_filename="tooz.pex",
internal_only=True,
requirements=PexRequirements({"tooz", "redis"}),
),
),
)

result = await Get(
FallibleProcessResult,
VenvPexProcess(
tooz_pex,
argv=(
script_path,
request.coord_url,
),
input_digest=script_digest,
description="Checking to see if Redis 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 RedisIsRunning()

# redis is not running, so raise an error with instructions.
raise ServiceMissingError.generate(
platform=platform,
messages=ServiceSpecificMessages(
service="redis",
service_start_cmd_el_7="service redis start",
service_start_cmd_el="systemctl start redis",
not_installed_clause_el="this is one way to install it:",
install_instructions_el=dedent(
"""\
sudo yum -y install redis
# Don't forget to start redis.
"""
),
service_start_cmd_deb="systemctl start redis",
not_installed_clause_deb="this is one way to install it:",
install_instructions_deb=dedent(
"""\
sudo apt-get install -y mongodb redis
# Don't forget to start redis.
"""
),
service_start_cmd_generic="systemctl start redis",
),
)


def rules():
return [
*collect_rules(),
UnionRule(PytestPluginSetupRequest, PytestUsesRedisRequest),
*pex_rules(),
]
90 changes: 90 additions & 0 deletions pants-plugins/uses_services/redis_rules_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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 .redis_rules import (
RedisIsRunning,
UsesRedisRequest,
rules as redis_rules,
)
from .platform_rules import Platform


@pytest.fixture
def rule_runner() -> RuleRunner:
return RuleRunner(
rules=[
*redis_rules(),
QueryRule(RedisIsRunning, (UsesRedisRequest, Platform)),
],
target_types=[],
)


def run_redis_is_running(
rule_runner: RuleRunner,
uses_redis_request: UsesRedisRequest,
mock_platform: Platform,
*,
extra_args: list[str] | None = None,
) -> RedisIsRunning:
rule_runner.set_options(
[
"--backend-packages=uses_services",
*(extra_args or ()),
],
env_inherit={"PATH", "PYENV_ROOT", "HOME"},
)
result = rule_runner.request(
RedisIsRunning,
[uses_redis_request, mock_platform],
)
return result


# Warning this requires that redis be running
def test_redis_is_running(rule_runner: RuleRunner) -> None:
request = UsesRedisRequest()
mock_platform = platform(os="TestMock")

# we are asserting that this does not raise an exception
is_running = run_redis_is_running(rule_runner, request, mock_platform)
assert is_running


@pytest.mark.parametrize("mock_platform", platform_samples)
def test_redis_not_running(rule_runner: RuleRunner, mock_platform: Platform) -> None:
request = UsesRedisRequest(
coord_url="redis://127.100.20.7:10", # 10 is an unassigned port, unlikely to be used
)

with pytest.raises(ExecutionError) as exception_info:
run_redis_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 == "redis"
assert "The redis service does not seem to be running" in str(exc)
assert exc.instructions != ""
48 changes: 48 additions & 0 deletions pants-plugins/uses_services/scripts/is_redis_running.py
Original file line number Diff line number Diff line change
@@ -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 sys


def _is_redis_running(coord_url: str) -> bool:
"""Connect to redis with connection logic that mirrors the st2 code.
In particular, this is based on:
- st2common.services.coordination.coordinator_setup()
This should not import the st2 code as it should be self-contained.
"""
# late import so that __file__ can be imported in the pants plugin without these imports
from tooz import ToozError, coordination

member_id = "pants-uses_services-redis"
coordinator = coordination.get_coordinator(coord_url, member_id)
try:
coordinator.start(start_heart=False)
except ToozError:
return False
return True


if __name__ == "__main__":
args = dict((k, v) for k, v in enumerate(sys.argv))

# unit tests do not use redis, they use use an in-memory coordinator: "zake://"
# integration tests use this url with a conf file derived from conf/st2.dev.conf
coord_url = args.get(1, "redis://127.0.0.1:6379")

is_running = _is_redis_running(coord_url)
exit_code = 0 if is_running else 1
sys.exit(exit_code)
1 change: 1 addition & 0 deletions st2tests/integration/orquesta/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ python_sources()

python_tests(
name="tests",
uses=["redis"],
)

0 comments on commit 9d311c5

Please sign in to comment.