Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fallback_environment for remote_environment #16955

Merged
merged 5 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 71 additions & 4 deletions src/python/pants/core/util_rules/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,37 @@ class RemoteExtraPlatformPropertiesField(StringSequenceField):
)


_NO_FALLBACK_ENVIRONMENT = "<none>"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With None as the default, this value is no longer necessary.



class RemoteFallbackEnvironmentField(StringField):
alias = "fallback_environment"
default = LOCAL_ENVIRONMENT_MATCHER
value: str
help = softwrap(
f"""
The environment to fallback to when remote execution is disabled via the global option
`--remote-execution`.

Must be an environment name from the option `[environments-preview].names`, the
special string `{LOCAL_ENVIRONMENT_MATCHER}` to use the relevant local environment, or the
string `{_NO_FALLBACK_ENVIRONMENT}` to error when remote execution is disabled.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that falling back by default to the local environment is error prone, so I think that this is potentially inverted.

The chances of a local environment being compatible with the remote environment without extra consideration/attention paid to setting up all of the env vars correctly is pretty slim: and it seems better to fail fast in that case ("you probably haven't made this compatible yet"), rather than to fail slowly ("I fell back automatically for you, and now something is failing in an unexpected way mid build").

Having this be None (the literal value) by default, and failing if remote execution cannot be used would be preferable I think, and avoids a special case.


Tip: if you are using a Docker image with your remote execution environment (usually
enabled by setting the field {RemoteExtraPlatformPropertiesField.alias}`), then it can be
useful to fallback to an equivalent `docker_image` target so that you have a consistent
execution environment.
"""
)


class RemoteEnvironmentTarget(Target):
alias = "_remote_environment"
core_fields = (
*COMMON_TARGET_FIELDS,
RemotePlatformField,
RemoteExtraPlatformPropertiesField,
RemoteFallbackEnvironmentField,
)
help = softwrap(
"""
Expand Down Expand Up @@ -252,6 +277,10 @@ class UnrecognizedEnvironmentError(Exception):
pass


class RemoteExecutionDisabledError(Exception):
pass


class AllEnvironmentTargets(FrozenDict[str, Target]):
"""A mapping of environment names to their corresponding environment target."""

Expand Down Expand Up @@ -382,7 +411,9 @@ async def determine_local_environment(

@rule
async def resolve_environment_name(
request: EnvironmentNameRequest, environments_subsystem: EnvironmentsSubsystem
request: EnvironmentNameRequest,
environments_subsystem: EnvironmentsSubsystem,
global_options: GlobalOptions,
) -> EnvironmentName:
if request.raw_value == LOCAL_ENVIRONMENT_MATCHER:
local_env_name = await Get(ChosenLocalEnvironmentName, {})
Expand All @@ -399,6 +430,38 @@ async def resolve_environment_name(
"""
)
)
env_tgt = await Get(EnvironmentTarget, EnvironmentName(request.raw_value))
if env_tgt.val is None:
raise AssertionError(f"EnvironmentTarget.val is None for the name `{request.raw_value}`")

# If remote execution is disabled and it's a remote environment, try falling back.
if not global_options.remote_execution and env_tgt.val.has_field(
RemoteFallbackEnvironmentField
):
fallback_field = env_tgt.val[RemoteFallbackEnvironmentField]
if fallback_field.value == _NO_FALLBACK_ENVIRONMENT:
raise RemoteExecutionDisabledError(
softwrap(
f"""
The global option `--remote-execution` is set to false, but the remote
environment `{request.raw_value}` is used in {request.description_of_origin}.

Either enable the option `--remote-execution`, or set the field
`{fallback_field.alias}` for the target {env_tgt.val.address},
e.g. to the default `{LOCAL_ENVIRONMENT_MATCHER}`.
"""
)
)
return await Get(
EnvironmentName,
EnvironmentNameRequest(
fallback_field.value,
description_of_origin=(
f"the `{fallback_field.alias}` field of the target {env_tgt.val.address}"
),
),
)

return EnvironmentName(request.raw_value)


Expand Down Expand Up @@ -433,12 +496,16 @@ async def get_target_for_environment_name(
WrappedTargetRequest(address, description_of_origin=_description_of_origin),
)
tgt = wrapped_target.val
if not tgt.has_field(CompatiblePlatformsField) and not tgt.has_field(DockerImageField):
if (
not tgt.has_field(CompatiblePlatformsField)
and not tgt.has_field(DockerImageField)
and not tgt.has_field(RemotePlatformField)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. This blocked RE targets actually being usable

):
raise ValueError(
softwrap(
f"""
Expected to use the address to a `_local_environment` or `_docker_environment`
target in the option `[environments-preview].names`, but the name
Expected to use the address to a `_local_environment`, `_docker_environment`, or
`_remote_environment` target in the option `[environments-preview].names`, but the name
`{env_name.val}` was set to the target {address.spec} with the target type
`{tgt.alias}`.
"""
Expand Down
35 changes: 27 additions & 8 deletions src/python/pants/core/util_rules/environments_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pants.build_graph.address import Address, ResolveError
from pants.core.util_rules import environments
from pants.core.util_rules.environments import (
_NO_FALLBACK_ENVIRONMENT,
LOCAL_ENVIRONMENT_MATCHER,
AllEnvironmentTargets,
AmbiguousEnvironmentError,
Expand All @@ -24,6 +25,7 @@
LocalEnvironmentTarget,
NoCompatibleEnvironmentError,
RemoteEnvironmentTarget,
RemoteExecutionDisabledError,
RemoteExtraPlatformPropertiesField,
UnrecognizedEnvironmentError,
extract_process_config_from_environment,
Expand All @@ -44,7 +46,7 @@ def rule_runner() -> RuleRunner:
QueryRule(EnvironmentTarget, [EnvironmentName]),
QueryRule(EnvironmentName, [EnvironmentNameRequest]),
],
target_types=[LocalEnvironmentTarget, DockerEnvironmentTarget],
target_types=[LocalEnvironmentTarget, DockerEnvironmentTarget, RemoteEnvironmentTarget],
singleton_environment=None,
)

Expand Down Expand Up @@ -192,11 +194,16 @@ def test_resolve_environment_name(rule_runner: RuleRunner) -> None:
rule_runner.write_files(
{
"BUILD": dedent(
"""\
f"""\
_local_environment(name='local')
# Intentionally set this to no platforms so that it cannot be autodiscovered.
_local_environment(name='hardcoded', compatible_platforms=[])
_docker_environment(name='docker', image="centos6:latest")
_remote_environment(
name='remote-no-fallback', fallback_environment="{_NO_FALLBACK_ENVIRONMENT}"
)
_remote_environment(name='remote-fallback', fallback_environment="docker")
_remote_environment(name='remote-bad-fallback', fallback_environment="fake")
"""
)
}
Expand All @@ -213,17 +220,29 @@ def get_name(v: str) -> EnvironmentName:
with engine_error(UnrecognizedEnvironmentError):
get_name("hardcoded")

rule_runner.set_options(
[
"--environments-preview-names={'local': '//:local', 'hardcoded': '//:hardcoded', 'docker': '//:docker'}"
]
env_names_arg = (
"--environments-preview-names={"
+ "'local': '//:local', "
+ "'hardcoded': '//:hardcoded', "
+ "'docker': '//:docker', "
+ "'remote-no-fallback': '//:remote-no-fallback', "
+ "'remote-fallback': '//:remote-fallback',"
"'remote-bad-fallback': '//:remote-bad-fallback'}"
)
rule_runner.set_options([env_names_arg, "--remote-execution"])
assert get_name(LOCAL_ENVIRONMENT_MATCHER).val == "local"
assert get_name("hardcoded").val == "hardcoded"
assert get_name("docker").val == "docker"
for name in ("hardcoded", "docker", "remote-no-fallback", "remote-fallback"):
assert get_name(name).val == name
with engine_error(UnrecognizedEnvironmentError):
get_name("fake")

rule_runner.set_options([env_names_arg, "--no-remote-execution"])
assert get_name("remote-fallback").val == "docker"
with engine_error(RemoteExecutionDisabledError):
get_name("remote-no-fallback")
with engine_error(UnrecognizedEnvironmentError):
get_name("remote-bad-fallback")


def test_resolve_environment_tgt(rule_runner: RuleRunner) -> None:
rule_runner.write_files({"BUILD": "_local_environment(name='env')"})
Expand Down