11"""GCP Cloud Run deploy commands for the Reflex Cloud CLI.
22
3- Fetches a Dockerfile + bash deploy script from flexgen and runs the script
3+ Fetches a Dockerfile + bash deploy script from Reflex and runs the script
44against the user's source directory. The Dockerfile is materialized inside
55a Cloud Build job (via a ``cloudbuild.yaml`` written to a tempfile and
66referenced with ``gcloud builds submit --config=...``) — the user's project
1111
1212from __future__ import annotations
1313
14- import base64
1514import contextlib
1615import os
1716import re
4342ENV_REFLEX_CLOUDBUILD_YAML = "REFLEX_CLOUDBUILD_YAML"
4443
4544# Pattern for the start of the `gcloud builds submit` invocation in the
46- # flexgen deploy script. We rewrite that whole multi-line command to use
45+ # Reflex deploy script. We rewrite that whole multi-line command to use
4746# `--config=` so the Dockerfile lives inside a cloudbuild.yaml instead of
4847# being staged on disk next to the user's source.
4948_BUILDS_SUBMIT_PATTERN = re .compile (
5049 r"(?P<indent>^[ \t]*)gcloud[ \t]+builds[ \t]+submit\b" ,
5150 re .MULTILINE ,
5251)
5352
54- # Manifest response field names from flexgen .
53+ # Manifest response field names from Reflex .
5554FIELD_DOCKERFILE = "dockerfile"
5655FIELD_DEPLOY_COMMAND = "deploy_command"
5756
5857# Allowlist of host environment variables forwarded to the deploy script.
5958# We deliberately exclude things like AWS_*/GITHUB_TOKEN/SSH agent sockets so a
6059# compromised or tampered manifest cannot exfiltrate unrelated credentials.
61- DEPLOY_ENV_ALLOWLIST = frozenset ({
62- "PATH" ,
63- "HOME" ,
64- "USER" ,
65- "LOGNAME" ,
66- "SHELL" ,
67- "TERM" ,
68- "LANG" ,
69- "LC_ALL" ,
70- "LC_CTYPE" ,
71- "TMPDIR" ,
72- "TEMP" ,
73- "TMP" ,
74- "XDG_CONFIG_HOME" ,
75- # gcloud configuration
76- "CLOUDSDK_CONFIG" ,
77- "CLOUDSDK_ACTIVE_CONFIG_NAME" ,
78- "CLOUDSDK_CORE_PROJECT" ,
79- "CLOUDSDK_CORE_ACCOUNT" ,
80- "CLOUDSDK_AUTH_ACCESS_TOKEN_FILE" ,
81- "GOOGLE_APPLICATION_CREDENTIALS" ,
82- # docker configuration
83- "DOCKER_HOST" ,
84- "DOCKER_TLS_VERIFY" ,
85- "DOCKER_CERT_PATH" ,
86- "DOCKER_CONFIG" ,
87- "DOCKER_BUILDKIT" ,
88- # corporate proxy / TLS trust
89- "HTTP_PROXY" ,
90- "HTTPS_PROXY" ,
91- "NO_PROXY" ,
92- "http_proxy" ,
93- "https_proxy" ,
94- "no_proxy" ,
95- "SSL_CERT_FILE" ,
96- "SSL_CERT_DIR" ,
97- "REQUESTS_CA_BUNDLE" ,
98- "CURL_CA_BUNDLE" ,
99- })
60+ DEPLOY_ENV_ALLOWLIST = frozenset (
61+ {
62+ "PATH" ,
63+ "HOME" ,
64+ "USER" ,
65+ "LOGNAME" ,
66+ "SHELL" ,
67+ "TERM" ,
68+ "LANG" ,
69+ "LC_ALL" ,
70+ "LC_CTYPE" ,
71+ "TMPDIR" ,
72+ "TEMP" ,
73+ "TMP" ,
74+ "XDG_CONFIG_HOME" ,
75+ # gcloud configuration
76+ "CLOUDSDK_CONFIG" ,
77+ "CLOUDSDK_ACTIVE_CONFIG_NAME" ,
78+ "CLOUDSDK_CORE_PROJECT" ,
79+ "CLOUDSDK_CORE_ACCOUNT" ,
80+ "CLOUDSDK_AUTH_ACCESS_TOKEN_FILE" ,
81+ "GOOGLE_APPLICATION_CREDENTIALS" ,
82+ # docker configuration
83+ "DOCKER_HOST" ,
84+ "DOCKER_TLS_VERIFY" ,
85+ "DOCKER_CERT_PATH" ,
86+ "DOCKER_CONFIG" ,
87+ "DOCKER_BUILDKIT" ,
88+ # corporate proxy / TLS trust
89+ "HTTP_PROXY" ,
90+ "HTTPS_PROXY" ,
91+ "NO_PROXY" ,
92+ "http_proxy" ,
93+ "https_proxy" ,
94+ "no_proxy" ,
95+ "SSL_CERT_FILE" ,
96+ "SSL_CERT_DIR" ,
97+ "REQUESTS_CA_BUNDLE" ,
98+ "CURL_CA_BUNDLE" ,
99+ }
100+ )
100101
101102
102103@click .command (name = "deploy" )
@@ -180,7 +181,7 @@ def deploy_command(
180181 """Deploy a Reflex app to a cloud target.
181182
182183 Currently the only supported target is GCP Cloud Run via --gcp. The
183- command fetches a Dockerfile and bash deploy script from flexgen , stages
184+ command fetches a Dockerfile and bash deploy script from Reflex , stages
184185 them in an ephemeral build context alongside symlinked source entries
185186 (your project tree is never modified), and runs the script from there.
186187 """
@@ -253,7 +254,7 @@ def deploy_command(
253254 ENV_VERSION : version_value ,
254255 }
255256
256- console .info ("Received deploy manifest from flexgen ." )
257+ console .info ("Received deploy manifest from Reflex ." )
257258 console .print ("" )
258259 console .print (f"Source: { source_path } " )
259260 console .print ("Deploy environment:" )
@@ -343,7 +344,7 @@ def _get_active_gcp_account(gcloud_path: str) -> str | None:
343344
344345
345346def _request_manifest (token : str ) -> tuple [str , str ]:
346- """Fetch the Dockerfile + deploy script from flexgen .
347+ """Fetch the Dockerfile + deploy script from Reflex .
347348
348349 Args:
349350 token: The Reflex API token to authenticate with.
@@ -373,84 +374,103 @@ def _request_manifest(token: str) -> tuple[str, str]:
373374 detail = ex .response .json ().get ("detail" , detail )
374375 if ex .response .status_code == 403 :
375376 console .error (
376- "Flexgen denied the request (403). GCP Cloud Run deploys require an "
377+ "Reflex denied the request (403). GCP Cloud Run deploys require an "
377378 "Enterprise tier subscription."
378379 )
379380 else :
380- console .error (f"Flexgen rejected the manifest request: { detail } " )
381+ console .error (f"Reflex rejected the manifest request: { detail } " )
381382 raise click .exceptions .Exit (1 ) from ex
382383 except httpx .HTTPError as ex :
383- console .error (f"Failed to reach flexgen at { url } : { ex } " )
384+ console .error (f"Failed to reach Reflex at { url } : { ex } " )
384385 raise click .exceptions .Exit (1 ) from ex
385386
386387 try :
387388 body = response .json ()
388389 except ValueError as ex :
389- console .error ("Flexgen returned a non-JSON response." )
390+ console .error ("Reflex returned a non-JSON response." )
390391 raise click .exceptions .Exit (1 ) from ex
391392
392393 if not isinstance (body , dict ):
393- console .error ("Flexgen returned an unexpected response shape." )
394+ console .error ("Reflex returned an unexpected response shape." )
394395 raise click .exceptions .Exit (1 )
395396
396397 dockerfile = body .get (FIELD_DOCKERFILE )
397398 deploy_command = body .get (FIELD_DEPLOY_COMMAND )
398399 if not isinstance (dockerfile , str ) or not dockerfile .strip ():
399400 console .error (
400- f"Flexgen response is missing a non-empty { FIELD_DOCKERFILE !r} field."
401+ f"Reflex response is missing a non-empty { FIELD_DOCKERFILE !r} field."
401402 )
402403 raise click .exceptions .Exit (1 )
403404 if not isinstance (deploy_command , str ) or not deploy_command .strip ():
404405 console .error (
405- f"Flexgen response is missing a non-empty { FIELD_DEPLOY_COMMAND !r} field."
406+ f"Reflex response is missing a non-empty { FIELD_DEPLOY_COMMAND !r} field."
406407 )
407408 raise click .exceptions .Exit (1 )
408409
409410 return dockerfile , deploy_command
410411
411412
412413def _build_cloudbuild_yaml (dockerfile_contents : str ) -> str :
413- """Generate a Cloud Build config that materializes the Dockerfile inline.
414+ r """Generate a Cloud Build config that materializes the Dockerfile inline.
414415
415- The Dockerfile body is embedded as a single base64 line so we don't have
416- to worry about YAML literal-block indentation, bash here-doc markers, or
417- shell-meta characters in the Dockerfile leaking into the config. The
418- resulting build does ``docker build`` + ``docker push`` against the
419- user's source as the build context .
416+ The Dockerfile body is dropped into a bash heredoc (``cat <<'MARKER' >
417+ Dockerfile``) inside the build step. The marker is single-quoted so bash
418+ treats the body literally — no shell-meta expansion of ``$``, `` ` ``, or
419+ ``\``. YAML literal-block indentation gets stripped uniformly so the
420+ closing marker line ends up at column 0 where bash expects it .
420421
421422 Args:
422- dockerfile_contents: The Dockerfile body from flexgen .
423+ dockerfile_contents: The Dockerfile body from Reflex .
423424
424425 Returns:
425426 A complete ``cloudbuild.yaml`` body, ready to write to disk.
426427
428+ Raises:
429+ ValueError: If the Dockerfile contains a line that exactly matches the
430+ heredoc marker (would terminate the heredoc early).
431+
427432 """
428- b64 = base64 .b64encode (dockerfile_contents .encode ("utf-8" )).decode ("ascii" )
433+ marker = "REFLEX_FLEXGEN_DOCKERFILE_EOF"
434+ if any (line .rstrip () == marker for line in dockerfile_contents .splitlines ()):
435+ raise ValueError (
436+ f"Dockerfile content contains the reserved heredoc marker { marker !r} ."
437+ )
438+ # Cloud Build runs its own substitution pass over `args`, so any `$NAME` or
439+ # `${NAME}` in the Dockerfile (e.g. `ENV PATH="${UV_PROJECT_ENVIRONMENT}/bin"`)
440+ # would be treated as a Cloud Build variable and fail with
441+ # "not a valid built-in substitution". Escape literal `$` to `$$` so the
442+ # parser restores `$` before bash runs.
443+ escaped = dockerfile_contents .replace ("$" , "$$" )
444+ # 6 spaces to fit inside the YAML literal block under `args:\n - -c\n - |`.
445+ indent = " "
446+ body = "" .join (f"{ indent } { line } \n " for line in escaped .splitlines ())
429447 return (
430448 "steps:\n "
431449 "- name: gcr.io/cloud-builders/docker\n "
432450 " entrypoint: bash\n "
433451 " args:\n "
434452 " - -c\n "
435453 " - |\n "
436- f" printf '%s' '{ b64 } ' | base64 -d > Dockerfile\n "
437- ' docker build -t "$_IMAGE" .\n '
438- ' docker push "$_IMAGE"\n '
454+ f"{ indent } cat > Dockerfile <<'{ marker } '\n "
455+ f"{ body } "
456+ f"{ indent } { marker } \n "
457+ f'{ indent } docker build -t "$_IMAGE" .\n '
458+ f'{ indent } docker push "$_IMAGE"\n '
439459 "images:\n "
440460 " - $_IMAGE\n "
441461 )
442462
443463
444464def _rewrite_builds_submit (script : str ) -> str :
445- """Rewrite the flexgen script's `gcloud builds submit` invocation to use --config=.
465+ """Rewrite the Reflex script's `gcloud builds submit` invocation to use --config=.
446466
447467 Replaces the (possibly multi-line) ``gcloud builds submit --tag X .``
448468 command with one that references our generated cloudbuild.yaml via the
449469 ``REFLEX_CLOUDBUILD_YAML`` environment variable and passes the image tag
450470 through ``--substitutions=_IMAGE=...``.
451471
452472 Args:
453- script: The flexgen deploy script body.
473+ script: The Reflex deploy script body.
454474
455475 Returns:
456476 The script with the build-submit step rewritten.
@@ -463,7 +483,7 @@ def _rewrite_builds_submit(script: str) -> str:
463483 if not match :
464484 raise ValueError (
465485 "Couldn't find `gcloud builds submit` in the deploy script. The "
466- "flexgen manifest format may have changed; the CLI needs updating. "
486+ "manifest format may have changed; Contact support@reflex.dev "
467487 )
468488 indent = match .group ("indent" )
469489 line_start = script .rfind ("\n " , 0 , match .start ()) + 1
@@ -525,7 +545,7 @@ def _run_deploy_script(
525545
526546 Args:
527547 bash_path: Resolved path to the bash executable.
528- script: The bash script body received from flexgen .
548+ script: The bash script body received from Reflex .
529549 cwd: Working directory to run the script in.
530550 env_overrides: Environment variables required by the deploy script.
531551
0 commit comments