add region check upfront when user deploys interactively (#2030)

This commit is contained in:
Martin Xu 2023-10-26 10:07:49 -07:00 committed by GitHub
parent 92dd68c51f
commit fe01f0cf11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 23 deletions

View File

@ -2,7 +2,7 @@
import os import os
from types import SimpleNamespace from types import SimpleNamespace
from reflex.constants.base import Dirs from reflex.constants.base import Dirs, Reflex
from .compiler import Ext from .compiler import Ext
@ -47,7 +47,7 @@ class RequirementsTxt(SimpleNamespace):
# The requirements.txt file. # The requirements.txt file.
FILE = "requirements.txt" FILE = "requirements.txt"
# The partial text used to form requirement that pins a reflex version # The partial text used to form requirement that pins a reflex version
DEFAULTS_STUB = "reflex==" DEFAULTS_STUB = f"{Reflex.MODULE_NAME}=="
# The deployment URL. # The deployment URL.

View File

@ -494,18 +494,22 @@ def deploy(
console.set_log_level(loglevel) console.set_log_level(loglevel)
if not interactive and not key: if not interactive and not key:
console.error("Please provide a deployment key when not in interactive mode.") console.error(
"Please provide a name for the deployed instance when not in interactive mode."
)
raise typer.Exit(1) raise typer.Exit(1)
try: if not hosting.check_requirements_for_non_reflex_packages():
hosting.check_requirements_txt_exist() console.ask(
except Exception as ex: f"Make sure {constants.RequirementsTxt.FILE} has all the dependencies. Enter to proceed"
)
if not hosting.check_requirements_txt_exist():
console.error(f"{constants.RequirementsTxt.FILE} required for deployment") console.error(f"{constants.RequirementsTxt.FILE} required for deployment")
raise typer.Exit(1) from ex raise typer.Exit(1)
# Check if we are set up. # Check if we are set up.
prerequisites.check_initialized(frontend=True) prerequisites.check_initialized(frontend=True)
enabled_regions = None
try: try:
# Send a request to server to obtain necessary information # Send a request to server to obtain necessary information
# in preparation of a deployment. For example, # in preparation of a deployment. For example,
@ -515,6 +519,10 @@ def deploy(
pre_deploy_response = hosting.prepare_deploy( pre_deploy_response = hosting.prepare_deploy(
app_name, key=key, frontend_hostname=frontend_hostname app_name, key=key, frontend_hostname=frontend_hostname
) )
# Note: we likely won't need to fetch this twice
if pre_deploy_response.enabled_regions is not None:
enabled_regions = pre_deploy_response.enabled_regions
except Exception as ex: except Exception as ex:
console.error(f"Unable to prepare deployment due to: {ex}") console.error(f"Unable to prepare deployment due to: {ex}")
raise typer.Exit(1) from ex raise typer.Exit(1) from ex
@ -544,9 +552,19 @@ def deploy(
key = key_candidate key = key_candidate
# Then CP needs to know the user's location, which requires user permission # Then CP needs to know the user's location, which requires user permission
region_input = console.ask( console.debug(f"{enabled_regions=}")
"Region to deploy to", default=regions[0] if regions else "sjc" while True:
) region_input = console.ask(
"Region to deploy to", default=regions[0] if regions else "sjc"
)
if enabled_regions is None or region_input in enabled_regions:
break
else:
console.warn(
f"{region_input} is not a valid region. Must be one of {enabled_regions}"
)
console.warn("Run `reflex deploymemts regions` to see details.")
regions = regions or [region_input] regions = regions or [region_input]
# process the envs # process the envs
@ -557,6 +575,8 @@ def deploy(
if not key or not regions or not app_name or not app_prefix or not api_url: if not key or not regions or not app_name or not app_prefix or not api_url:
console.error("Please provide all the required parameters.") console.error("Please provide all the required parameters.")
raise typer.Exit(1) raise typer.Exit(1)
# Note: if the user uses --no-interactive mode, there was no prepare_deploy call
# so we do not check the regions until the call to hosting server
processed_envs = hosting.process_envs(envs) if envs else None processed_envs = hosting.process_envs(envs) if envs else None

View File

@ -223,6 +223,7 @@ class DeploymentPrepareResponse(Base):
# This is for a new deployment, user has not deployed this app before. # This is for a new deployment, user has not deployed this app before.
# The server returns key suggestion based on the app name. # The server returns key suggestion based on the app name.
suggestion: Optional[DeploymentPrepInfo] = None suggestion: Optional[DeploymentPrepInfo] = None
enabled_regions: Optional[List[str]] = None
@root_validator(pre=True) @root_validator(pre=True)
def ensure_at_least_one_deploy_params(cls, values): def ensure_at_least_one_deploy_params(cls, values):
@ -303,6 +304,7 @@ def prepare_deploy(
reply=response_json["reply"], reply=response_json["reply"],
suggestion=response_json["suggestion"], suggestion=response_json["suggestion"],
existing=response_json["existing"], existing=response_json["existing"],
enabled_regions=response_json.get("enabled_regions"),
) )
except httpx.RequestError as re: except httpx.RequestError as re:
console.debug(f"Unable to prepare launch due to {re}.") console.debug(f"Unable to prepare launch due to {re}.")
@ -450,7 +452,7 @@ def deploy(
# If the server explicitly states bad request, # If the server explicitly states bad request,
# display a different error # display a different error
if response.status_code == HTTPStatus.BAD_REQUEST: if response.status_code == HTTPStatus.BAD_REQUEST:
raise AssertionError("Server rejected this request") raise AssertionError(f"Server rejected this request: {response.text}")
response.raise_for_status() response.raise_for_status()
response_json = response.json() response_json = response.json()
return DeploymentPostResponse( return DeploymentPostResponse(
@ -843,16 +845,37 @@ async def get_logs(
) )
def check_requirements_txt_exist(): def check_requirements_txt_exist() -> bool:
"""Check if requirements.txt exists in the current directory. """Check if requirements.txt exists in the top level app directory.
Raises: Returns:
Exception: If the requirements.txt does not exist. True if requirements.txt exists, False otherwise.
""" """
if not os.path.exists(constants.RequirementsTxt.FILE): return os.path.exists(constants.RequirementsTxt.FILE)
raise Exception(
f"Unable to find {constants.RequirementsTxt.FILE} in the current directory."
) def check_requirements_for_non_reflex_packages() -> bool:
"""Check the requirements.txt file for packages other than reflex.
Returns:
True if packages other than reflex are found, False otherwise.
"""
if not check_requirements_txt_exist():
return False
try:
with open(constants.RequirementsTxt.FILE) as fp:
for req_line in fp.readlines():
package_name = re.search(r"^([^=<>!~]+)", req_line.lstrip())
# If we find a package that is not reflex
if (
package_name
and package_name.group(1) != constants.Reflex.MODULE_NAME
):
return True
except Exception as ex:
console.warn(f"Unable to scan requirements.txt for dependencies due to {ex}")
return False
def authenticate_on_browser( def authenticate_on_browser(
@ -948,7 +971,9 @@ def interactive_get_deployment_key_from_user_input(
deploy_url = suggestion.deploy_url deploy_url = suggestion.deploy_url
# If user takes the suggestion, we will use the suggested key and proceed # If user takes the suggestion, we will use the suggested key and proceed
while key_input := console.ask(f"Name of deployment", default=key_candidate): while key_input := console.ask(
f"Choose a name for your deployed app", default=key_candidate
):
try: try:
pre_deploy_response = prepare_deploy( pre_deploy_response = prepare_deploy(
app_name, app_name,
@ -1083,7 +1108,7 @@ def interactive_prompt_for_envs() -> list[str]:
envs_finished = False envs_finished = False
env_count = 1 env_count = 1
env_key_prompt = f" * env-{env_count} name (enter to skip)" env_key_prompt = f" * env-{env_count} name (enter to skip)"
console.print("Environment variables ...") console.print("Environment variables for your production App ...")
while not envs_finished: while not envs_finished:
env_key = console.ask(env_key_prompt) env_key = console.ask(env_key_prompt)
if not env_key: if not env_key:
@ -1120,7 +1145,7 @@ def get_regions() -> list[dict]:
if ( if (
response_json response_json
and response_json[0] is not None and response_json[0] is not None
and isinstance(response_json[0], dict) and not isinstance(response_json[0], dict)
): ):
console.debug("Expect return values are dict's") console.debug("Expect return values are dict's")
return [] return []

View File

@ -98,6 +98,7 @@ def test_deploy_non_interactive_prepare_failed(
def test_deploy_non_interactive_success( def test_deploy_non_interactive_success(
mocker, setup_env_authentication, optional_args, values mocker, setup_env_authentication, optional_args, values
): ):
mocker.patch("reflex.utils.console.ask")
app_prefix = "fake-prefix" app_prefix = "fake-prefix"
mocker.patch( mocker.patch(
"reflex.utils.hosting.prepare_deploy", "reflex.utils.hosting.prepare_deploy",
@ -201,6 +202,7 @@ def test_deploy_interactive_prepare_failed(
key=get_suggested_key(), key=get_suggested_key(),
), ),
existing=None, existing=None,
enabled_regions=["sjc"],
), ),
["sjc"], ["sjc"],
[], [],
@ -220,6 +222,7 @@ def test_deploy_interactive_prepare_failed(
key=get_suggested_key(), key=get_suggested_key(),
), ),
existing=None, existing=None,
enabled_regions=["sjc"],
), ),
["sjc"], ["sjc"],
["k1=v1", "k2=v2"], ["k1=v1", "k2=v2"],
@ -239,6 +242,7 @@ def test_deploy_interactive_prepare_failed(
key=get_suggested_key(), key=get_suggested_key(),
), ),
existing=None, existing=None,
enabled_regions=["sjc"],
), ),
["sjc"], ["sjc"],
[], [],
@ -258,6 +262,7 @@ def test_deploy_interactive_prepare_failed(
key=get_suggested_key(), key=get_suggested_key(),
), ),
existing=None, existing=None,
enabled_regions=["sjc"],
), ),
["sjc"], ["sjc"],
["k1=v1", "k3=v3"], ["k1=v1", "k3=v3"],
@ -279,6 +284,7 @@ def test_deploy_interactive_prepare_failed(
) )
), ),
suggestion=None, suggestion=None,
enabled_regions=["sjc"],
), ),
["sjc"], ["sjc"],
[], [],
@ -300,6 +306,7 @@ def test_deploy_interactive_prepare_failed(
) )
), ),
suggestion=None, suggestion=None,
enabled_regions=["sjc"],
), ),
["sjc"], ["sjc"],
["k4=v4"], ["k4=v4"],
@ -319,6 +326,10 @@ def test_deploy_interactive(
expected_key, expected_key,
args_patch, args_patch,
): ):
mocker.patch(
"reflex.utils.hosting.check_requirements_for_non_reflex_packages",
return_value=True,
)
mocker.patch( mocker.patch(
"reflex.utils.hosting.prepare_deploy", "reflex.utils.hosting.prepare_deploy",
return_value=prepare_responses, return_value=prepare_responses,

View File

@ -353,3 +353,17 @@ def test_process_envs():
def test_interactive_prompt_for_envs(mocker, inputs, expected): def test_interactive_prompt_for_envs(mocker, inputs, expected):
mocker.patch("reflex.utils.console.ask", side_effect=inputs) mocker.patch("reflex.utils.console.ask", side_effect=inputs)
assert hosting.interactive_prompt_for_envs() == expected assert hosting.interactive_prompt_for_envs() == expected
def test_requirements_txt_only_contains_reflex(mocker):
mocker.patch("reflex.utils.hosting.check_requirements_txt_exist", return_value=True)
mocker.patch("builtins.open", mock_open(read_data="\nreflex=1.2.3\n\n"))
assert hosting.check_requirements_for_non_reflex_packages() is False
def test_requirements_txt_only_contains_other_packages(mocker):
mocker.patch("reflex.utils.hosting.check_requirements_txt_exist", return_value=True)
mocker.patch(
"builtins.open", mock_open(read_data="\nreflex=1.2.3\n\npynonexist=3.2.1")
)
assert hosting.check_requirements_for_non_reflex_packages() is True