CTM-397: Replace GKE/Helm Galaxy path with GCE VM-based deployment - #4904
CTM-397: Replace GKE/Helm Galaxy path with GCE VM-based deployment#4904LizBaldo wants to merge 55 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #4904 +/- ##
===========================================
- Coverage 74.07% 73.85% -0.23%
===========================================
Files 131 131
Lines 11101 11202 +101
Branches 891 918 +27
===========================================
+ Hits 8223 8273 +50
- Misses 2878 2929 +51
... and 1 file with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
|
I am currently blocked fro testing further because of a lack of permission on the galaxy image to use on the boot VM: I think I would need the Galaxy team to make the image allAuthenticatedUsers in the anvil-and-terra-development project |
The Galaxy team made the image public so I am currently unblocked :) |
I'm not sure this is a concern, don't the instances live in a VPC with NAT? |
I agree, but this is a departure from how we used to handle it, I am not sure that the compliance review covered this so I want to triple check before merging |
|
@afgane I was able to deploy a new Galaxy app, then delete it while keeping the disk and then recreating a new app using the same disk 🎉 I'll do one final round of testing once you have the latest image and then we should be good to go :) |
At Galaxy VM creation, grant the pet SA roles/batch.jobsEditor on the user's project so it can submit and monitor GCP Batch jobs. When the Batch SA lives in the same project, also grant serviceAccountUser on it; cross-project Batch SAs must have that binding configured externally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, gcpBatchSaProject parsing - Pass app.auditInfo.creator as galaxy-user-email GCE metadata so the actual workspace user (not dev@galaxyproject.org) becomes the Galaxy admin - Fix scala.io.Source resource leak in installGalaxyVm using scala.util.Using - Update sourceImage to galaxy-k8s-boot-v2026-06-10 and gitBranch to "anvil" - Fix HOST_IP to use GCE metadata server instead of external ifconfig.me - Fix gcpBatchSaProject SA email parsing: lift(1) + stripSuffix instead of lastOption + replace to avoid matching suffix in unexpected positions - Correct stale comments: galaxy_url_prefix → galaxy_prefix, dev → anvil branch, wrong "internal IP" comment corrected to "external IP" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The pre-baked galaxy-k8s-boot image carries cloud-init state from its build, so cloud-init treats new VM launches as subsequent boots and skips runcmd — causing "No startup scripts to run" in Guest Agent logs and a VM that never bootstraps Galaxy. Fix: pass the bootstrap script as the "startup-script" metadata key instead of "user-data". The GCE Guest Agent always executes startup-script on boot, regardless of cloud-init state. galaxy-user-data.sh is reformatted from cloud-config YAML to a plain bash script. The sudo -u debian block now uses a single-quoted heredoc delimiter (<<'DEBIAN_EOF') to avoid apostrophes in comments breaking shell quoting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Picks up rke2 v1.36.2+rke2r1, Helm v4.2.2, ingress-nginx 4.13.2, and Galaxy Helm chart 6.8.1 baked into the new galaxy-k8s-boot image. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ecord When two Galaxy VM apps share a project, they reused the same Leo cluster record via `saveOrGetClusterForApp`. When the second VM started, it called `updateAsyncFields` and overwrote the shared cluster's `loadBalancerIp` with its own external IP. Since `kubernetesProxyHost` is keyed on cluster ID, both apps produced the same proxy hostname, causing all requests to be routed to the second user's VM (resulting in 404/401 for the first user). Fix: Galaxy apps now always create a fresh Leo cluster record via `saveNewClusterForApp`. Each VM gets a unique cluster ID → unique proxy hostname → no IP collision in `hostToIpMapping`. GKE-based apps (Cromwell, Allowed, Custom) continue to share a cluster per project. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…project Drop IDX_KUBERNETES_CLUSTER_UNIQUE_V2 to permit more than one active KubernetesCluster row per cloud context. Galaxy VM apps need their own cluster record so each VM stores its external IP independently via updateAsyncFields; without the constraint drop, saveNewClusterForApp throws SQLIntegrityConstraintViolationException when a second Galaxy app is created in the same project. Also add an early disk-attachment check in createApp before the cluster record is saved, so DiskAlreadyAttachedException is still raised (rather than the constraint violation) when the same disk is reused across apps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cluster support The IDX_KUBERNETES_CLUSTER_UNIQUE_V2 constraint was dropped to allow multiple active cluster records per cloud context for Galaxy VM apps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5236f89 to
ed0aef1
Compare
…sible playbook Supplies terra_workspace, terra_namespace, terra_drs_url, and terra_api_url as ansible extra-vars so galaxy-k8s-boot can configure Galaxy's Terra integration (required by cloudve/galaxy Helm chart 6.8.2). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… proxy hops Leo terminates TLS and speaks plain HTTP to Galaxy VM ingress-nginx. Without this header, ingress-nginx (use-forwarded-headers: false by default) stamps X-Forwarded-Proto: http on requests to tusd. tusd runs with -behind-proxy and trusts that header, so it generates Location: http://... for TUS upload sessions. Browsers block the subsequent PATCH as mixed content, silently dropping uploads. Adding X-Forwarded-Proto: https only for useHttp=true (Galaxy VM) backends; all other backends use HTTPS natively and are unaffected. The Galaxy team also needs to enable use-forwarded-headers: true in the ingress-nginx configmap so the header reaches tusd, and to update anvil branch to galaxy Helm chart 6.8.2 which fixes the tusd Ingress routing (6.8.1 had an annotation that caused ingress-nginx to silently discard the tusd Ingress). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r Galaxy VM Leo terminates TLS and speaks plain HTTP to the Galaxy VM. ingress-nginx must be told to trust Leo's X-Forwarded-Proto header (now set to "https" by ProxyService) rather than deriving the scheme from its own plaintext connection, so tusd receives X-Forwarded-Proto: https and generates Location: https://... for TUS upload sessions. The galaxy-k8s-boot anvil branch exposes this as ingress_use_forwarded_headers (default false for non-Leo deployments); we set it to true via ansible-pull extra-vars. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…laxy VM responses Leo terminates TLS and speaks plain HTTP to the Galaxy VM, so tusd's `-behind-proxy` flag generates `Location: http://...` URLs for TUS uploads. The browser blocks the subsequent PATCH as mixed content from an https page. Rewrite Location scheme to https for all useHttp=true (Galaxy VM) backends in the proxy response path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Batch jobs GCP Batch jobs were 403-ing because galaxy-k8s-boot values.yml hardcodes project_id: anvil-and-terra-development. Pass the actual VM project (already available as terra-namespace metadata) to ansible-pull as gcp_project_id so galaxy-k8s-boot can use it to set the correct project. The galaxy-k8s-boot anvil branch also needs to accept this variable and use it to override project_id in the Helm values. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… file source label Terra passes WORKSPACE_NAME in customEnvironmentVariables (already used for disk restore validation). Forward it to the VM as terra-workspace-name metadata and pass to ansible-pull as terra_workspace_name so galaxy-k8s-boot can populate the workspace: field in the anvil file source config. Without this the field is empty, causing Galaxy to show "Unlabeled Rfs File Source" and fail to list workspace files. galaxy-k8s-boot also needs a matching change to use terra_workspace_name for the workspace: field. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
706dd5e to
c8aaee9
Compare
ORCH_URL was already mapped to gke.galaxyApp.orchUrl but not to galaxyVm.orchUrl, so the Galaxy VM Terra file source api_url always defaulted to the dsde-dev hardcoded value in reference.conf. No helmfile change needed; terra-helmfile already sets ORCH_URL per environment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… no restore info A Galaxy VM disk gets formattedBy=Galaxy when created but lastUsedBy is only written after a successful install. If the previous app errored during provisioning, the disk has no data and should be reusable for a fresh install rather than returning a 500. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…project to Galaxy VM terra-namespace was being set to the GCP project (e.g. terra-quality-50d1be3e) instead of the Terra billing namespace (e.g. broad-dsp-liz). The startup script was using terra-namespace for both gcp_project_id and terra_namespace in ansible, conflating two distinct values. Fix: terra-namespace now carries the Terra billing namespace (from WORKSPACE_NAMESPACE custom env var). A new gcp-project-id metadata key carries the GCP project, and the startup script reads it separately for gcp_project_id. This fixes the "Problem listing file source path gxfiles://terra-launch-workspace/" error in Galaxy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rates https:// links Without this, Galaxy infers http:// from its internal Leo connection and generates http:// absolute URLs (e.g. history export links). Browsers refuse to send the Secure-flagged LeoToken cookie on http:// requests, causing 401s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…viz plugin requests Galaxy's visualization plugin loader (analysis.bundled.js) fetches index.js and index.css without a Referer header, causing Leo's checkReferer to 401 before the LeoToken cookie is ever checked. The Origin header is present on these requests and provides equivalent CSRF protection (browsers set it; HTML forms cannot forge it). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… GCP Batch GCP Batch jobs fail with CODE_GCE_RESOURCE_NOT_FOUND because galaxy-k8s-boot hardcodes network/subnet to 'default', which doesn't exist in Terra projects. Read gcp-network and gcp-subnet from VM instance metadata (already set by Leo) and forward as gcp_batch_network and gcp_batch_subnet extra-vars so galaxy-k8s-boot can configure the Batch runner to use the correct VPC. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…account Without `userinfo.email` and `userinfo.profile` scopes, the VM pet SA token cannot be validated by Terra's Sam (Google's userinfo endpoint won't return the caller's email), causing all `anvilfs` calls to Rawls/Orchestration to fail with 401 Unauthorized. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nsible GCP Batch was looking for the boot image by name in the job's project (terra-quality-ff0f1af3), but the image lives in anvil-and-terra-development. Passes the full image resource URL from Leo config as `gcp-batch-boot-image` VM metadata so galaxy-k8s-boot can supply it to the Batch runner config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Galaxy team is fixing the image path regression in galaxy-k8s-boot directly; passing it from Leo is not needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-vars Ansible splits --extra-vars key=value on whitespace, so workspace names with spaces (e.g. "Galaxy Testing Party - 080426") were silently truncated to the first word. JSON format preserves the full value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… Origin Galaxy visualization plugin assets (CSS, JS loaded by index.js) are fetched via no-cors <link>/<script> tags from a page with a strict referrer policy. These requests strip Referer and, for no-cors GET requests, also omit Origin — so neither of the existing fallbacks helps. Add a final Sec-Fetch-Site: same-origin check as a third- layer fallback. This header is set unconditionally by all modern browsers and cannot be forged by page scripts (Sec-* is a forbidden header prefix), so it provides the same CSRF guarantee as Referer without requiring the page to expose its full URL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…subnets; warn on pre-created subnets
GCP Batch VMs run without an external IP (org policy blocks them).
They need Private Google Access on the subnet to reach batch.googleapis.com.
- buildSubnetwork: add .setPrivateIpGoogleAccess(true) so all Leo-created
subnets get PGA automatically
- setUpProjectNetworkAndFirewalls (high-security path): check the pre-created
subnet and log an actionable warning with the gcloud command to fix it when
PGA is off
For the current BEE (terra-quality-ff0f1af3), PGA must be enabled manually
since the subnet is pre-created:
gcloud compute networks subnets update <subnet> \
--region us-central1 --project terra-quality-ff0f1af3 \
--enable-private-ip-google-access
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…created subnets; warn on pre-created subnets" This reverts commit dd7c1a2.
…-runner SA The Batch VM agent authenticates as the galaxy-batch-runner SA to phone home to batch.googleapis.com. Without roles/batch.agentReporter the agent starts (agent,start) but cannot report status, causing the 1080s timeout. Also missing from the galaxy-k8s-boot README — needs a follow-up doc fix there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…unner SA Batch VMs need this role to write logs to Cloud Logging. Without it jobs run successfully but no logs appear in the GCP Batch console. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Replaces the GKE/Helm-based Galaxy deployment path with a GCE VM-based deployment using galaxy-k8s-boot. Rather than provisioning a full GKE cluster and deploying Galaxy via Helm, Leo now creates a single GCE VM that runs Galaxy via Ansible/microk8s.
What changed
Galaxy VM provisioning (
GKEInterpreter.installGalaxyVm)galaxy-user-emailGCE metadata (fromapp.auditInfo.creator) so the workspace user becomes the Galaxy admingalaxy-batch-runnerservice account in the user's projectroles/batch.jobsEditorat the project level androles/iam.serviceAccountUseron the Batch SA, so the VM can submit GCP Batch jobsleonardo-galaxy-allow-nfs-for-batch) so Batch VMs can reach the Galaxy VM's NFS server (TCP/UDP 2049 and 111)loadBalancerIpso the Leo proxy can reach the VM across VPC boundariesNetwork topology and IP choice
10.x.x.x) is not routable from Leo's podleonardo-allow-httpfirewall rule (TCP port 80, source0.0.0.0/0, targeting VMs with theleonardonetwork tag) allows Leo to reach the VM's external IP. Galaxy VMs are created with theleonardotagisVmReachable) and the Akka HTTP proxy use the external IPGalaxy VM readiness health check
isProxyAvailableroutes through Leo's own proxy hostname; in BEE environments the wildcard DNS resolves to the ingress controller's external IP, unreachable via hairpin NAT from within the GKE pod → TCP timeoutAppDAO.isVmReachable(ip, port)— a direct http4s HTTP GET tohttp://<externalIp>:80/. No proxy hostname resolution required.MockAppDAOreturnsIO.pure(isUp)for testsLeo proxy: HTTP support for Galaxy VM backends
useHttp: Boolean = falsetoHostReady— when true the proxy connects via plain HTTP port 80 (ws:// for WebSocket) instead of HTTPS port 443KubernetesDnsCachesetsuseHttp = trueforAppType.Galaxyapps and maps the fake proxy hostname to the VM's external IPProxyService.handleHttpRequest/handleWebSocketRequestbranch onuseHttp; all non-Galaxy backends are unchangedLeo proxy: path handling for Galaxy VM (
ProxyService.proxyAppRequest)/proxy/google/v1/apps/{project}/{app}/galaxy/...) to the Galaxy VM unchangedingress.pathto the value ofgalaxy_prefix, which Leo passes as a GCE metadata item (galaxy-url-prefix). Galaxy's nginx therefore serves at the full Leo proxy path, so all requests route correctly without any path rewriting in LeoNFS PVC size: GB → GiB conversion fix (
GKEInterpreter.installGalaxyVm)pvSizeGi = nfsDisk.size.gb - 11treated decimal GB as binary GiB. For a 500 GB disk: the disk holds ~466 GiB but Leo requested 489 GiB → NFS provisioner fails withinsufficient available space, leaving all Galaxy podsPendingdiskSizeGiB = (nfsDisk.size.gb.toLong * 1000^3) / 1024^3, then subtract 11 GiB overheadLifecycle: restore from existing disks
restore = msg.appType == AppType.Galaxy && msg.createDisk.isEmpty: when a Galaxy app is created without a new disk, the disks already exist (prior app was deleted keeping disks)restore_galaxy=truemetadata to AnsibleCreateAppParams.restore: Booleanpropagates the flag through toinstallGalaxyVmLifecycle: delete keeping disks
DeleteAppMessage(diskId = None)→ VM is deleted, both disks are preservedDeleteAppMessage(diskId = Some(...))→ VM + both disks deletedConfig cleanup
gcpBatchServiceAccountEmailfromreference.conf/GalaxyVmConfig/Config.scala— Leo now creates the SA dynamically viagetOrCreateServiceAccountinstead of relying on a pre-configured emailArchitecture notes
anvilbranch)user-datacloud-init via guest agentleonardo-allow-httpfirewall (0.0.0.0/0 → port 80,leonardotag)isProxyAvailablevia proxy hostnameisVmReachabledirect HTTP to external IPgalaxy-batch-runnerSASecurity comparison: old GKE-based vs. new VM-based
0.0.0.0/00.0.0.0/0Regressions introduced:
0.0.0.0/0, so anyone who discovers the VM's external IP can reach Galaxy directly, bypassing Leo's authenticationAlternatives for follow-up:
0.0.0.0/0firewall rule is no longer neededuseHttp = falsefor Galaxy; requires provisioning Leo certs onto the VM duringinstallGalaxyVmTest plan
GKEInterpreterSpec,LeoPubsubMessageSubscriberSpec)restore_galaxy=truepassed to Ansible, Galaxy restores stateleonardo-allow-httpsource range from0.0.0.0/0to Leo's GKE node CIDR