From 2fe82e063b838e2253fdff8d0d27c04c3a6f0b78 Mon Sep 17 00:00:00 2001 From: Kastier1 <40179067+Kastier1@users.noreply.github.com.> Date: Fri, 22 May 2026 09:07:19 -0700 Subject: [PATCH] hosting-cli(gcp): add --service-account flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forward the new CLOUD_RUN_SERVICE_ACCOUNT env var to the deploy script so the Cloud Run service can run as a user-specified IAM SA instead of the project's default compute SA (broad permissions; a known security antipattern for production workloads). Flag is optional. When unset, the key is omitted from env_overrides entirely — the deploy script defaults it to empty, and the bash ${VAR:+...} expansion drops `--service-account` from `gcloud run deploy`, preserving today's behavior. Caveat surfaced in --help: the deploying principal needs roles/iam.serviceAccountUser on the target SA, or gcloud will 403. Requires the matching backend change in flexgen so the deploy script honors CLOUD_RUN_SERVICE_ACCOUNT. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/reflex_cli/v2/gcp.py | 12 ++++- tests/units/reflex_cli/v2/test_gcp.py | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py b/packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py index 755b3410831..68687f8dcc9 100644 --- a/packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py +++ b/packages/reflex-hosting-cli/src/reflex_cli/v2/gcp.py @@ -7,7 +7,7 @@ tree is never modified. The script reads its parameters from environment variables (GCP_PROJECT, GCP_REGION, SERVICE_NAME, AR_REPO, VERSION, CLOUD_RUN_CPU, CLOUD_RUN_MEMORY, CLOUD_RUN_MIN_INSTANCES, -REFLEX_CLOUDBUILD_YAML). +CLOUD_RUN_SERVICE_ACCOUNT, REFLEX_CLOUDBUILD_YAML). """ from __future__ import annotations @@ -41,6 +41,7 @@ ENV_CPU = "CLOUD_RUN_CPU" ENV_MEMORY = "CLOUD_RUN_MEMORY" ENV_MIN_INSTANCES = "CLOUD_RUN_MIN_INSTANCES" +ENV_SERVICE_ACCOUNT = "CLOUD_RUN_SERVICE_ACCOUNT" # Path to the Cloud Build config file written by the CLI. The rewritten # deploy script references it as ``--config="${REFLEX_CLOUDBUILD_YAML}"``. ENV_REFLEX_CLOUDBUILD_YAML = "REFLEX_CLOUDBUILD_YAML" @@ -162,6 +163,12 @@ type=click.IntRange(min=0), help="Minimum number of Cloud Run instances to keep warm (sets CLOUD_RUN_MIN_INSTANCES). Set to 0 to scale to zero.", ) +@click.option( + "--service-account", + "service_account", + default=None, + help="IAM service account email the Cloud Run service runs as (sets CLOUD_RUN_SERVICE_ACCOUNT). If omitted, Cloud Run uses the project's default compute SA. The deploying principal needs roles/iam.serviceAccountUser on the target SA.", +) @click.option( "--source", "source_dir", @@ -199,6 +206,7 @@ def deploy_command( cpu: str, memory: str, min_instances: int, + service_account: str | None, source_dir: str, token: str | None, interactive: bool, @@ -285,6 +293,8 @@ def deploy_command( ENV_MEMORY: memory, ENV_MIN_INSTANCES: str(min_instances), } + if service_account: + deploy_env[ENV_SERVICE_ACCOUNT] = service_account console.info("Received deploy manifest from Reflex.") console.print("") diff --git a/tests/units/reflex_cli/v2/test_gcp.py b/tests/units/reflex_cli/v2/test_gcp.py index 8109504133f..da84f523b01 100644 --- a/tests/units/reflex_cli/v2/test_gcp.py +++ b/tests/units/reflex_cli/v2/test_gcp.py @@ -206,6 +206,56 @@ def test_gcp_deploy_resource_flags_have_defaults(mocker: MockFixture, tmp_path: assert env_overrides["CLOUD_RUN_MIN_INSTANCES"] == "1" +def test_gcp_deploy_forwards_service_account(mocker: MockFixture, tmp_path: Path): + """--service-account threads through to CLOUD_RUN_SERVICE_ACCOUNT.""" + run_mock = _patch_environment(mocker) + _mock_manifest_response(mocker) + + result = runner.invoke( + hosting_cli, + [ + "deploy", + "--gcp", + "--gcp-project", + "p", + "--source", + str(tmp_path), + "--service-account", + "my-sa@p.iam.gserviceaccount.com", + ], + input="y\n", + ) + + assert result.exit_code == 0, result.output + env_overrides = run_mock.call_args.kwargs["env_overrides"] + assert ( + env_overrides["CLOUD_RUN_SERVICE_ACCOUNT"] == "my-sa@p.iam.gserviceaccount.com" + ) + + +def test_gcp_deploy_omits_service_account_when_unset( + mocker: MockFixture, tmp_path: Path +): + """Without --service-account, CLOUD_RUN_SERVICE_ACCOUNT is not in env_overrides. + + The deploy script defaults it to empty, so Cloud Run falls back to the + project's default compute SA. Leaving the key out of env_overrides (rather + than sending an empty string) keeps the dry-run output tidy. + """ + run_mock = _patch_environment(mocker) + _mock_manifest_response(mocker) + + result = runner.invoke( + hosting_cli, + ["deploy", "--gcp", "--gcp-project", "p", "--source", str(tmp_path)], + input="y\n", + ) + + assert result.exit_code == 0, result.output + env_overrides = run_mock.call_args.kwargs["env_overrides"] + assert "CLOUD_RUN_SERVICE_ACCOUNT" not in env_overrides + + def test_gcp_deploy_rejects_negative_min_instances(mocker: MockFixture, tmp_path: Path): """--min-instances is IntRange(min=0); negative values fail at the CLI layer.""" run_mock = _patch_environment(mocker)