Cloud Run
floci-gcp emulates the Cloud Run Admin API v2 control plane over REST JSON using Google's published protobuf types.
| Config | Default | Description |
|---|---|---|
FLOCI_GCP_SERVICES_CLOUDRUN_ENABLED |
true |
Enable/disable Cloud Run |
FLOCI_GCP_SERVICES_CLOUDRUN_MOCK |
false |
Mock mode: control plane only, no Docker-backed image-based service execution |
FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_DEFAULT_PORT |
8080 |
Container port used when the service template omits a port |
FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_STARTUP_TIMEOUT |
240s |
Time to wait for the container TCP port to become reachable |
FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_REQUEST_TIMEOUT |
300s |
Default invocation proxy timeout |
FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_OPERATION_TIMEOUT |
300s |
Maximum time for asynchronous Cloud Run execution operations before their LRO fails |
FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_CLEANUP_TIMEOUT |
15s |
Maximum time to wait for best-effort Docker cleanup after an operation is already resolved |
FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_URL_HOST_SUFFIX |
localhost.floci.io or FLOCI_GCP_HOSTNAME |
Host suffix used for generated Cloud Run execution URLs |
FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_MAX_WORKER_INSTANCES |
1 |
Maximum replica containers run per worker pool (see Worker Pools) |
Supported API Surface
| Operation | Path |
|---|---|
| Create service | POST /v2/projects/{project}/locations/{location}/services |
| List services | GET /v2/projects/{project}/locations/{location}/services |
| Get service | GET /v2/projects/{project}/locations/{location}/services/{service} |
| Update service | PATCH /v2/projects/{project}/locations/{location}/services/{service} |
| Delete service | DELETE /v2/projects/{project}/locations/{location}/services/{service} |
| Get IAM policy | GET /v2/projects/{project}/locations/{location}/services/{service}:getIamPolicy |
| Set IAM policy | POST /v2/projects/{project}/locations/{location}/services/{service}:setIamPolicy |
| Test IAM permissions | POST /v2/projects/{project}/locations/{location}/services/{service}:testIamPermissions |
| List revisions | GET /v2/projects/{project}/locations/{location}/services/{service}/revisions |
| Get revision | GET /v2/projects/{project}/locations/{location}/services/{service}/revisions/{revision} |
| Delete revision | DELETE /v2/projects/{project}/locations/{location}/services/{service}/revisions/{revision} |
As on GCP, list services rejects the - location wildcard with INVALID_ARGUMENT. List revisions accepts - as the service (.../services/-/revisions) to list revisions across all services in the location.
When execution is disabled, create, update, and delete return completed google.longrunning.Operation resources immediately. When execution is enabled, create, template-changing update, and delete return pending operations and complete or fail after runtime startup or cleanup. Operations can be read, listed, waited on, and deleted under /v2/projects/{project}/locations/{location}/operations.
Behavior
In mock mode (FLOCI_GCP_SERVICES_CLOUDRUN_MOCK=true) Cloud Run services are metadata only. Creating a service synthesizes the service URL, timestamps, etag, ready condition, traffic status, latest revision fields, and one read-only revision. No container image is pulled and no request-serving runtime is started.
Image-based service execution runs by default (FLOCI_GCP_SERVICES_CLOUDRUN_MOCK=false); set mock=true to keep services metadata-only: no Docker containers and no execution-mode template validation. In execution mode, image-based service creation starts one Docker container for the created revision, injects PORT, K_SERVICE, K_REVISION, and K_CONFIGURATION, waits for the ingress TCP port, and returns a deterministic app-root invocation URL on the floci-gcp front door:
The URL scheme and port come from floci-gcp.effectiveBaseUrl(). The project token is a deterministic SHA-256 based token derived from the project ID. FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_URL_HOST_SUFFIX can override the host suffix; when it is unset, floci-gcp uses FLOCI_GCP_HOSTNAME if configured, otherwise localhost.floci.io. Direct Docker runtime container ports are internal implementation details and are not returned in uri, urls[], or trafficStatuses[].uri.
Generated Cloud Run hosts must route back to the floci-gcp front door. When floci-gcp runs in Docker, the embedded DNS server resolves *.localhost.floci.io for other containers on the same Docker network. When running floci-gcp directly on the host with ./mvnw quarkus:dev, local DNS does not automatically resolve those generated hosts; use the legacy /run/v2/projects/{project}/locations/{location}/services/{service} invocation path, configure local DNS, or send the request to localhost:4588 with the generated Host header.
While the create operation is pending, the stored service has Ready=CONDITION_PENDING, reconciling=true, and no latestReadyRevision. After the runtime port becomes reachable, the operation completes and the service is updated with Ready=CONDITION_SUCCEEDED and the ready revision name. If runtime startup fails, the operation fails and both the service and created revision are updated with Ready=CONDITION_FAILED, reconciling=false, and the startup error message.
PATCH accepts a Cloud Run v2 Service body and an optional updateMask query parameter. Template-changing updates create a new revision. allow_missing upsert behavior and etag preconditions are not implemented. With execution enabled, the previous ready revision remains invokable while the replacement container starts; after the replacement is ready, latestReadyRevision moves to the new revision and older runtime containers for that service are stopped.
Execution mode supports Cloud Run GCS volumes declared with template.volumes[].gcs and mounted with container volumeMounts. The referenced bucket must already exist in the floci-gcp GCS emulator. At runtime startup, floci-gcp snapshots the bucket's current live objects into a Docker named volume and mounts that volume into the workload container with Docker NoCopy enabled. readOnly=true becomes a Docker read-only volume mount. readOnly=false allows writes into the mounted volume and syncs regular files back to emulator GCS when the runtime container is stopped during service delete or revision replacement. Writable cleanup mirrors the mounted volume back to the bucket prefix, so bucket objects that are no longer present in the mounted filesystem are deleted. Writes are not live-synced during request handling, so other GCS clients only observe them after runtime cleanup. Snapshot copy uses a short-lived alpine:3.20 helper container, so Docker must be able to use that image when GCS volumes are mounted.
GCS volume subPath is supported as a path inside the materialized bucket root. GCS volume mountOptions are rejected. Secret, Cloud SQL, emptyDir, NFS, and other volume sources are still unsupported in execution mode.
Execution-backed create, template-changing update, and delete run on a bounded background executor. FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_OPERATION_TIMEOUT caps these operations and fails the LRO with DEADLINE_EXCEEDED if Docker startup or metadata deletion does not complete in time. Docker API calls made by the shared container lifecycle manager are also capped by FLOCI_GCP_DOCKER_API_TIMEOUT; when that timeout is reached, floci-gcp resets its Docker client before later calls. Delete removes service and revision metadata and completes the LRO before stopping runtime containers, so slow Docker cleanup does not keep Terraform replacement destroys pending; container cleanup is best-effort after the resource is gone and capped by FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_CLEANUP_TIMEOUT.
The invocation proxy accepts both generated host-routed URLs and the legacy prefixed path /run/v2/projects/{project}/locations/{location}/services/{service} for compatibility. Host-routed requests preserve the original app path and query string, so GET $uri/api/database?x=1 reaches the container as /api/database?x=1. The proxy forwards HTTP methods, trailing paths, query strings, request bodies, safe headers, and X-Forwarded-* headers to the latest ready revision. Missing services return 404, services without a ready runtime return 503, runtime connection failures return 502, and proxy timeouts return 504.
validateOnly=true returns a successful completed operation without storing or deleting resources. Validate-only operations are not retained for later operation get/list calls.
Deleting a service revision that is the service's latestReadyRevision or is named in its trafficStatuses fails with 400 FAILED_PRECONDITION and Revision "{revision}" cannot be directly deleted because it is actively serving.. Other revisions are removed and the completed operation returns the revision with deleteTime and expireTime set. Runtime containers are not touched by revision delete; retired service revisions have no running container.
SDK Usage
Cloud Run clients should use the HTTP JSON transport, an explicit endpoint, and no credentials:
ServicesSettings settings = ServicesSettings.newHttpJsonBuilder()
.setEndpoint("http://localhost:4588")
.setCredentialsProvider(NoCredentialsProvider.create())
.build();
Not Implemented
- Source builds and buildpacks
- Traffic splitting
- Autoscaling and scale-to-zero
- Sidecars, non-GCS volumes, secrets, startup probes
- Cloud Functions execution
- IAM invocation enforcement
Jobs
Jobs run batch work to completion. floci-gcp implements the Cloud Run Admin API v2 jobs resource with its executions and tasks over REST JSON. gcloud is not supported: gcloud run jobs uses the v1 Knative API, which floci-gcp does not serve for Cloud Run.
Supported API Surface
| Operation | Path |
|---|---|
| Create job | POST /v2/projects/{project}/locations/{location}/jobs?jobId={id} |
| List jobs | GET /v2/projects/{project}/locations/{location}/jobs |
| Get job | GET /v2/projects/{project}/locations/{location}/jobs/{job} |
| Update job | PATCH /v2/projects/{project}/locations/{location}/jobs/{job} |
| Delete job | DELETE /v2/projects/{project}/locations/{location}/jobs/{job} |
| Run job | POST /v2/projects/{project}/locations/{location}/jobs/{job}:run |
| Get IAM policy | GET /v2/projects/{project}/locations/{location}/jobs/{job}:getIamPolicy |
| Set IAM policy | POST /v2/projects/{project}/locations/{location}/jobs/{job}:setIamPolicy |
| Test IAM permissions | POST /v2/projects/{project}/locations/{location}/jobs/{job}:testIamPermissions |
| List executions | GET /v2/projects/{project}/locations/{location}/jobs/{job}/executions |
| Get execution | GET /v2/projects/{project}/locations/{location}/jobs/{job}/executions/{execution} |
| Delete execution | DELETE /v2/projects/{project}/locations/{location}/jobs/{job}/executions/{execution} |
| Cancel execution | POST /v2/projects/{project}/locations/{location}/jobs/{job}/executions/{execution}:cancel |
| List tasks | GET /v2/projects/{project}/locations/{location}/jobs/{job}/executions/{execution}/tasks |
| Get task | GET /v2/projects/{project}/locations/{location}/jobs/{job}/executions/{execution}/tasks/{task} |
{job} may be - when listing executions, and {job} and {execution} may each be - when listing tasks. Lists page with pageSize and pageToken. Jobs are listed by name, executions newest first, tasks by execution and index.
Create accepts jobId and validateOnly. Update replaces the job with the request body (there is no updateMask) and accepts validateOnly and allowMissing; a patch that changes nothing keeps the generation. Delete accepts validateOnly and etag. Run and cancel take validateOnly, etag and, for run, overrides in the request body. validateOnly returns a completed operation and stores nothing.
Defaults and Naming
A created job gets launchStage: GA, template.taskCount: 1, template.template.maxRetries: 3, template.template.timeout: 600s, template.template.executionEnvironment: EXECUTION_ENVIRONMENT_GEN2, and container resource limits cpu: 1000m and memory: 512Mi. client and clientVersion are preserved. The job reports generation, observedGeneration, executionCount, latestCreatedExecution (name, create and completion time, completion status, delete time) and a Ready terminal condition with an empty conditions list. A duplicate jobId fails with 409 ALREADY_EXISTS and Resource '{id}' already exists., also with validateOnly=true. A missing job, execution or task fails with 404 NOT_FOUND and Resource '{id}' of kind 'JOB' in region '{location}' in project '{project}' does not exist. (kind EXECUTION or TASK respectively).
Executions are named {job}-{5 lowercase alphanumerics} and tasks {execution}-task{index}. parallelism defaults to the task count; a larger value is accepted as given. startExecutionToken and runExecutionToken on create or update start an execution named {job}-{token} when the token differs from the stored one; the operation completes when the execution is created (start token) or finished (run token), and re-sending the same token, or switching back to a token that already named an execution, starts nothing. A token is rejected with 400 INVALID_ARGUMENT when the job ID and the token together are 63 characters or more, or when {job}-{token} is not lowercase letters, digits and hyphens starting and ending with a letter or digit.
jobs:run overrides replace taskCount and timeout for that execution. A container override is matched by name; when no container has that name and the template has a single container the override applies to it, otherwise the request fails with 400 INVALID_ARGUMENT. Override args replace the container args, env merges by name, and clearArgs removes the args. The effective template is stored on the execution.
Execution Semantics
jobs:run returns a long-running operation whose metadata and response are the Execution. It is not bounded by FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_OPERATION_TIMEOUT; a run is bounded by the task timeout plus the cleanup grace, over every retry and every wave of parallelism, plus the startup timeout for image preparation. The operation outcome follows GCP:
| Outcome | Operation |
|---|---|
| Every task succeeded | response is the Execution |
| A task exited non-zero on its last attempt | error {code: 10, message: "Task {task} failed with exit code: {n} and message: The container exited with an error."} |
| A task timed out on its last attempt | error {code: 4, message: "Task {task} failed with exit code: 0 and message: The configured timeout was reached."} |
| A task container disappeared on its last attempt without a stop request | error {code: 13, message: "Task {task} failed with message: The task container stopped unexpectedly."} |
| The execution was cancelled, deleted or its job deleted | response is the Execution, whose Completed condition is CONDITION_FAILED with Cancelled by user. |
Executions carry the Started, Completed, ContainerReady and ResourcesAvailable conditions with the GCP messages and measured durations, and runningCount, succeededCount, failedCount, cancelledCount and retriedCount. An execution is failed only after every task is terminal. Each task reports retried and lastAttemptResult: status: {} on success, {code: 10, message: "The container exited with an error."} with exitCode for a non-zero exit, {code: 4, message: "The configured timeout was reached."} for a timeout, {code: 13, message: "The task container stopped unexpectedly."} for a container that was removed or lost outside the emulator's control, and {code: 1, message: "Cancelled by user."} for a cancelled task. An unexpectedly stopped container is a failed attempt and is retried like a non-zero exit.
Cancelling a running execution stops its containers and never starts its pending tasks; the cancel operation completes with the Execution. Cancelling an execution that is not running fails with 400 FAILED_PRECONDITION and Execution '{id}' cannot be cancelled because it is not running. Deleting a running execution cancels it first. A cancel or delete that arrives while the run bound is already stopping the execution does not replace that failure. Deleting a job cancels its running executions and removes the job, its executions and its tasks. Until that cleanup finishes, creating or upserting a job with the same name fails with 409 ABORTED and Job '{id}' is being deleted. Several executions of one job may run at the same time.
In mock mode (FLOCI_GCP_SERVICES_CLOUDRUN_MOCK=true) jobs:run creates the execution and its tasks already succeeded, with exit code 0 and timestamps set, and the operation is done with the Execution.
In execution mode each task attempt runs as a fresh Docker container. At most parallelism containers run at once and tasks start in index order. Non-zero exits and timeouts are retried up to maxRetries with CLOUD_RUN_TASK_ATTEMPT incremented. The per-attempt timeout sends SIGTERM and kills the container after FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_CLEANUP_TIMEOUT. Container stdout and stderr are written to the emulator log at debug level (category io.floci.gcp.services.cloudrun.CloudRunJobsRuntime), and the container is removed once its exit code is captured. GCS volumes are materialized per attempt as for services, and writable volumes are written back before the attempt's outcome is recorded. Unlike services, the write-back merges instead of mirroring, so parallel tasks writing the same bucket keep each other's outputs: a file is uploaded only when it is new or its content changed since the attempt's snapshot, and an object is deleted only when the attempt removed it and its generation is unchanged since the snapshot. When two attempts change the same object, the last write-back wins. If the write-back of an attempt that exited 0 fails, the attempt is a failed attempt and is retried, then fails with code 13 and The task's GCS volume could not be written back: {detail}.; the materialized volumes are removed either way. Execution mode applies the same template constraints as services, with the same messages: one container, GCS volumes only, at most one port, an image, no env valueSource.
Each task container receives:
| Variable | Value |
|---|---|
CLOUD_RUN_JOB |
Job ID |
CLOUD_RUN_EXECUTION |
Execution ID |
CLOUD_RUN_TASK_INDEX |
Task index, from 0 |
CLOUD_RUN_TASK_ATTEMPT |
Attempt number, from 0 |
CLOUD_RUN_TASK_COUNT |
Task count of the execution |
PORT and the K_* variables are not set for job tasks.
Every change to an execution and its tasks is applied by a single per-execution coordinator, so task exits, cancel, execution delete, job delete and startup reconciliation cannot interleave. When the emulator stops, running task containers are removed. On the next start with persistent storage, executions and tasks that were still running are marked failed with The emulator restarted before the execution completed. (tasks: The emulator restarted before the task completed.), and their pending run operations fail with code 10. The run, delete and job operations of an execution that was deleted while its containers were still stopping complete with the cancelled Execution, as the delete path does. Job task containers left behind by an emulator process that did not stop cleanly (same floci_emulator and floci_namespace labels) are removed at startup.
Deviations from GCP
etagis accepted on delete, run and cancel but never validated, which matches the observed GCP behavior.- Deletion is immediate. There is no soft delete or 30-day retention, and
showDeletedis accepted and ignored;deleteTimeandexpireTimeappear only on the delete operation's Execution or Job. validateOnlyreturns a completed operation that cannot be fetched later. GCP returns an unfinished operation name that also cannot be fetched.serviceAccountis not defaulted, as for services; GCP fills in the project's default compute service account.- Container images are stored as given, and unnamed job containers stay unnamed; GCP resolves the execution image to a digest.
- Executions do not emit the GCP
Retrycondition orlogUri. - The
TASKkind in the task404message was not observed on GCP; the job and execution messages were. - The execution token character rule is inferred from the execution naming; only the length limit is documented in
job.proto. - Recreating a job while its executions are still being cleaned up fails with
409 ABORTED; the GCP behavior was not observed. - A task container that stops without a stop request (removed outside the emulator) fails its attempt with code 13; GCP has no equivalent observable case.
- IAM policies use the same codec as services, so the initial
ACABetag is returned as the base64 of its UTF-8 bytes. - gcloud is not supported.
Worker Pools
Worker pools run pull-based workloads (queue consumers, Kafka or Pub/Sub pullers) that serve no HTTP traffic. floci-gcp implements the Cloud Run Admin API v2 workerPools resource and its revisions over REST JSON. gcloud is not supported: gcloud run worker-pools reads through the v1 Knative API and writes through gRPC, neither of which floci-gcp serves for Cloud Run.
Supported API Surface
| Operation | Path |
|---|---|
| Create worker pool | POST /v2/projects/{project}/locations/{location}/workerPools?workerPoolId={id} |
| List worker pools | GET /v2/projects/{project}/locations/{location}/workerPools |
| Get worker pool | GET /v2/projects/{project}/locations/{location}/workerPools/{pool} |
| Update worker pool | PATCH /v2/projects/{project}/locations/{location}/workerPools/{pool} |
| Delete worker pool | DELETE /v2/projects/{project}/locations/{location}/workerPools/{pool} |
| Get IAM policy | GET /v2/projects/{project}/locations/{location}/workerPools/{pool}:getIamPolicy |
| Set IAM policy | POST /v2/projects/{project}/locations/{location}/workerPools/{pool}:setIamPolicy |
| Test IAM permissions | POST /v2/projects/{project}/locations/{location}/workerPools/{pool}:testIamPermissions |
| List revisions | GET /v2/projects/{project}/locations/{location}/workerPools/{pool}/revisions |
| Get revision | GET /v2/projects/{project}/locations/{location}/workerPools/{pool}/revisions/{revision} |
| Delete revision | DELETE /v2/projects/{project}/locations/{location}/workerPools/{pool}/revisions/{revision} |
Create accepts workerPoolId and validateOnly. Update accepts updateMask, validateOnly, allowMissing (creates the pool when it does not exist) and forceNewRevision; without updateMask every updatable field is replaced from the body, which is what the Terraform provider sends. A top-level mask path such as template or scaling replaces that whole field; a nested path such as template.containers or scaling.manualInstanceCount replaces only that sub-field and keeps its siblings. Unknown, output-only and non-updatable paths fail with 400 INVALID_ARGUMENT and Invalid update mask path: {path}. Delete accepts validateOnly and etag. List accepts pageSize and pageToken; showDeleted is accepted and ignored. Creating a pool whose ID already exists fails with 409 ALREADY_EXISTS and Resource '{id}' already exists..
Resource Behavior
- Defaults:
scaling.manualInstanceCountis1when unset, every template container getsresources.limitscpu: 1000mandmemory: 512Miwhen those keys are missing,launchStageisGA, and an emptyinstanceSplitsbecomes oneINSTANCE_SPLIT_ALLOCATION_TYPE_LATESTsplit at 100%.template.serviceAccountis stored as given, as for services.customAudiencesis dropped, because GCP ignores it. - Revisions are named
{pool}-{5-digit counter}-{3 random lowercase alphanumerics}, for exampleorders-00002-jw2. A revision is created on create, whenever the template changes (compared after defaults are applied), and wheneverforceNewRevision=true. Any other update, such as scaling, labels or splits, only incrementsgeneration. - A revision copies the template: unnamed containers are named
{last image path segment}-{1-based index}(busybox-1),executionEnvironmentisEXECUTION_ENVIRONMENT_GEN2, and theservicefield is not set. Images are stored as given; they are not resolved to digests. instanceSplitStatusesmirrorsinstanceSplitsand always names a concrete revision: aLATESTsplit nameslatestCreatedRevision. Every revision the splits resolve to must exist in the same pool and the percentages must add up to 100, otherwise the update fails with400 INVALID_ARGUMENT.- The pool carries
latestCreatedRevision,latestReadyRevision,terminalCondition {type: Ready}with no top-levelconditions,generation,observedGenerationand anetag. Output-only fields in a create request (creator,lastModifier,satisfiesPzs,threatDetectionEnabled,reconciling,observedGeneration, the revision and status fields) are ignored. - A missing pool or revision returns
404 NOT_FOUNDwithResource '{id}' of kind 'WORKER_POOL' in region '{location}' in project '{project}' does not exist.(kindREVISIONfor revisions). - A revision named in
instanceSplitStatusesis serving. Serving revisions carryReady,Active,ResourcesAvailable,ContainerReadyandMinInstancesProvisionedconditions with the GCP messages (for exampleDeploying revision succeeded in 0.42s.) andscalingStatus.desiredMinInstanceCountequal to the pool'smanualInstanceCount. Other revisions are retired:ActiveisCONDITION_FAILEDwithRevision retired.andrevisionReason: RETIRED, andscalingStatusis cleared. - Deleting a serving revision, or the pool's
latestCreatedRevisionorlatestReadyRevision(which aLATESTsplit resolves to), fails with400 FAILED_PRECONDITIONandRevision "{revision}" cannot be directly deleted because it is actively serving.. Deleting any other revision returns a completed operation whose response is the revision withdeleteTimeandexpireTimeset. - Revisions are listed newest first. Listing the revisions of a pool that does not exist returns an empty response, as GCP does after a pool is deleted.
- IAM get, set and test delegate to the shared IAM service. As for jobs, get and set on a pool that does not exist fail with
404 NOT_FOUND, so no policy is stored for a missing or deleted pool, and test returns no permissions. - Deleting a pool removes the pool, all of its revisions and its IAM policy immediately. The operation response carries
deleteTimeandexpireTime.
In mock mode (FLOCI_GCP_SERVICES_CLOUDRUN_MOCK=true) worker pools are metadata only: every operation is returned completed, the pool is ready at once and no container is started.
Execution
With execution enabled (FLOCI_GCP_SERVICES_CLOUDRUN_MOCK=false, the default) floci-gcp runs worker pool replicas as Docker containers.
- Only one revision runs: the one with the largest percent in
instanceSplitStatuses(the first on ties). The split between several revisions is metadata only. - The serving revision runs
scaling.manualInstanceCountreplicas, capped byFLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_MAX_WORKER_INSTANCES(default1). A larger request is clamped with a warning in the emulator log; the pool and revision metadata still report the requested count.manualInstanceCount: 0stops every replica. - Each replica receives the container's own
envplusCLOUD_RUN_WORKER_POOL(the pool ID) andCLOUD_RUN_REVISION(the revision ID), which win on conflict. NoPORTorK_*variables are injected and no port is published. - Create, update and delete return pending operations. While the containers converge, the pool has
reconciling: true,observedGenerationstays at the previous generation (unset after create), andterminalConditionisCONDITION_RECONCILINGwithDeploying Revision.(new revision) orProvisioning revision instances to process workloads.(scaling, splits or other fields). The operation completes, andobservedGenerationcatches up withgeneration, once the containers match the latest committed state. If a container cannot be started the operation fails with code 13, the pool'sterminalConditionbecomesCONDITION_FAILEDwith the Docker error, and the previously running containers are left in place. Operations fail withDEADLINE_EXCEEDEDafterFLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_OPERATION_TIMEOUT. - When the serving revision changes, the new revision's replicas are started before the old revision's replicas are stopped.
- Replicas are stopped with SIGTERM and killed after
FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_CLEANUP_TIMEOUT(kept belowFLOCI_GCP_DOCKER_API_TIMEOUT). Deleting a pool stops all of its replicas before the delete operation completes. - The template must satisfy the same execution-mode constraints as services: exactly one container, GCS volumes only (no
mountOptions), at most one container port, an image, and no envvalueSource. Each replica gets its own snapshot of a GCS volume; writable volumes are written back to the bucket when that replica stops, so with several replicas the last one stopped wins. - A replica container that exits is replaced on the next update of the pool, not automatically. Replica containers are removed when the emulator shuts down.
- At startup, floci-gcp fails worker pool operations left pending by the previous process with code 10 (
ABORTED) andThe emulator restarted before the operation completed., removes worker pool containers the previous process left behind (matched by theio.floci.service=cloudrunlabel, anio.floci.cloudrun.resource-nameunder/workerPools/, and this emulator'sfloci_emulatorandfloci_namespacelabels; containers from earlier versions are matched by the legacyfloci_serviceandfloci_resourcelabels instead), and reconciles every stored pool, so with persistent storage a restored pool starts its replicas again and leavesreconciling. In mock mode, pools still marked reconciling are marked ready. A pending delete operation is found only when its location still has a pool or a leftover container.
Terraform and OpenTofu can manage google_cloud_run_v2_worker_pool with the cloud_run_v2_custom_endpoint shown for services; set deletion_protection = false so the resource can be destroyed.
Deviations from GCP
- Only the revision with the largest split percent runs containers; the split is metadata only.
- The worker pool revision name format (
{pool}-00001-abc) differs from the one floci-gcp uses for services ({service}-00001, no random suffix). etagis a random UUID and is never validated on update or delete.- There is no soft delete:
showDeletedis ignored anddeleteTimeandexpireTimeappear only on the delete operation response. - While reconciling, GCP keeps reporting the previous
instanceSplitStatuses; floci-gcp reports the new statuses at once. template.revision(a caller-chosen revision name) is ignored. Revisions have nologUriorcreator, and GCP's transientRetryandInstanceShutDownconditions are not emitted.- Scaling fields newer than the Cloud Run v2 protos floci-gcp is built on (
scalingMode,minInstanceCount,maxInstanceCount) are dropped. - The
400 INVALID_ARGUMENTmessages for invalidinstanceSplitsare floci-gcp's own. The404message follows the format GCP returns for jobs and executions; theWORKER_POOLandREVISIONkind names have not been observed against GCP. - Refusing to delete
latestCreatedRevisionorlatestReadyRevisionwhile splits pin another revision mirrors floci-gcp's services behavior; GCP's answer in that case has not been observed. - gcloud is not supported.
SDK Usage
WorkerPoolsSettings settings = WorkerPoolsSettings.newHttpJsonBuilder()
.setEndpoint("http://localhost:4588")
.setCredentialsProvider(NoCredentialsProvider.create())
.build();
RevisionsClient lists and deletes worker pool revisions with a projects/{project}/locations/{location}/workerPools/{pool} parent.
Instances
floci-gcp emulates Cloud Run v2 Instances (projects.locations.instances) over REST JSON with the google.cloud.run.v2.Instance message from proto-google-cloud-run-v2 0.96.0. An instance is a single long-lived HTTP container with its own URL that can be stopped and started.
| Operation | Path |
|---|---|
| Create instance | POST /v2/projects/{project}/locations/{location}/instances |
| List instances | GET /v2/projects/{project}/locations/{location}/instances |
| Get instance | GET /v2/projects/{project}/locations/{location}/instances/{instance} |
| Update instance | PATCH /v2/projects/{project}/locations/{location}/instances/{instance} |
| Delete instance | DELETE /v2/projects/{project}/locations/{location}/instances/{instance} |
| Start instance | POST /v2/projects/{project}/locations/{location}/instances/{instance}:start |
| Stop instance | POST /v2/projects/{project}/locations/{location}/instances/{instance}:stop |
| Get IAM policy | GET /v2/projects/{project}/locations/{location}/instances/{instance}:getIamPolicy |
| Set IAM policy | POST /v2/projects/{project}/locations/{location}/instances/{instance}:setIamPolicy |
| Test IAM permissions | POST /v2/projects/{project}/locations/{location}/instances/{instance}:testIamPermissions |
Create, update, start, stop and delete return google.longrunning.Operation resources whose metadata and response are the Instance. The request body of create and update is an Instance, with containers at the top level (there is no template).
- Create accepts
instanceIdorinstance_id. Both are optional: without one, the server generates a 15-character ID of lowercase letters and digits that starts with a letter.validateOnlyvalidates without storing anything. - A duplicate instance ID, or an ID already used by a Cloud Run Service in the same project and location, returns
409 ALREADY_EXISTSwithResource '{id}' already exists.. Creating a Service with the ID of an existing instance in the same project and location returns the same error. - Missing instances return
404 NOT_FOUNDwithResource '{id}' of kind 'INSTANCE' in region '{location}' in project '{project}' does not exist.. - List supports
pageSizeandpageToken. - Update accepts
updateMask,validateOnlyandallowMissing. With an update mask, a masked top-level field is replaced as a whole and output-only paths are ignored. A nested path such asvpcAccess.connectorupdates only that subfield and keeps its siblings; map and repeated fields (labels,containers,volumes, ...) can only be masked as a whole. A path that does not name anInstancefield returns400 INVALID_ARGUMENTwithInvalid update mask path: {path}. Without an update mask, every settable field present in the body is replaced.allowMissing=truecreates the instance when it does not exist. - Delete accepts
validateOnlyandetag. Start and stop acceptvalidateOnlyin the query string or the request body. - IAM methods use the shared IAM policy store in the same way as Services. Deleting an instance deletes its policy.
Default values filled in on create and update match GCP: each container gets resources.limits cpu: 2000m and memory: 2048Mi when unset and ports: [{name: http1, containerPort: 8080}] when no port is given; ingress defaults to INGRESS_TRAFFIC_ALL and launchStage to GA. Unnamed containers stay unnamed, as observed on GCP.
State model
The instance state is reported by terminalCondition, whose type is always Running:
| Phase | terminalCondition.state |
terminalCondition.message |
|---|---|---|
| Starting | CONDITION_RECONCILING |
Waiting for instance to start. |
| Running | CONDITION_SUCCEEDED |
Started instance in {seconds}s. |
| Stopping | CONDITION_RECONCILING |
Waiting for instance to be stopped. |
| Stopped | CONDITION_FAILED |
Instance stopped. |
| Deleted (delete operation response) | CONDITION_FAILED |
Instance completed for deletion. |
conditions holds ContainerReady (Imported container image in {seconds}s.) and ResourcesAvailable (Provisioned imported containers.). Every create, update, start, stop and delete increments generation; observedGeneration catches up when the transition finishes and reconciling is true while it is in progress. Stopping an instance that is not running returns 400 FAILED_PRECONDITION with Instance '{id}' cannot be stopped because it is not running.; starting a running (or starting) instance returns 400 FAILED_PRECONDITION with Instance '{id}' cannot be started because it is already running.. A start whose container fails to come up leaves the instance in CONDITION_FAILED with the error message and fails the operation; it can be started again. After such a failure ContainerReady and ResourcesAvailable are CONDITION_FAILED with the same message, and containerStatuses lists the current containers without an imageDigest, so no state from an earlier successful start survives.
urls and containerStatuses are kept while the instance is stopped. The delete operation response carries deleteTime and expireTime (30 days later), but the instance is removed immediately.
Execution
In mock mode (FLOCI_GCP_SERVICES_CLOUDRUN_MOCK=true) instances are metadata only: create and start complete immediately in the running phase with a URL, stop and start only flip the terminal condition, and containerStatuses[].imageDigest is empty.
With execution enabled, an instance runs one Docker container. Create, start and container-changing updates return pending operations that complete once the container port accepts connections (bounded by FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_STARTUP_TIMEOUT). Stop sends SIGTERM, waits up to FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_CLEANUP_TIMEOUT, then removes the container; start creates a new one; delete removes it. An update restarts the container only when containers or volumes change and the instance is running or starting; a stopped instance picks up the change on its next start. containerStatuses[].imageDigest is the first repository digest reported by Docker for the image. The Docker-mode constraints are the same as for Services: exactly one container, at most one port, an image, GCS volumes only (no mountOptions), no env valueSource, with the same error messages. GCS volumes are materialized and written back with the same code as Services, when the container is stopped or replaced.
The container receives only PORT from the emulator, in addition to the environment variables declared on the container.
For one instance, create, update, start, stop and delete are applied in request order: each request checks its precondition against the stored instance while holding that instance's lock, and container work runs on a per-instance queue. A transition that has been superseded by a later start, stop, restart or delete before its container work finished does not overwrite the later transition's state; a deleted instance is never written back. A transition only writes into the instance it was requested for, identified by uid: when an instance is deleted and re-created with the same ID while an earlier start is still running, that start completes its own operation with the deleted instance and leaves the new instance untouched.
A container that exits on its own is not restarted and its instance keeps its phase; requests to its URL return 503 until the instance is stopped, updated or deleted, which removes the container and writes its GCS volumes back. On emulator shutdown floci-gcp waits up to 5 seconds for queued instance work to finish, then removes every instance container that has a runtime record and writes its writable GCS volumes back. A start still creating its container after that wait may not have recorded it yet, so that container can be left running without the shutdown writeback.
URL routing
Instances get a URL of the same form as Services, http://{instance}-{project-token}.{location}.run.localhost.floci.io:4588, with the same host suffix and port rules. Requests whose Host is a generated Cloud Run host are resolved against Services first and then against Instances. Instance and Service creation each refuse an ID already used by the other kind in the same project and location, so a host names at most one resource. Requests to a stopped instance, or to an instance in mock mode, return 503; requests to an unknown generated host return 404. The proxy is the one Services use and forwards methods, paths, query strings, bodies, safe headers and X-Forwarded-* headers. Internally, routed requests are served under /run/v2/projects/{project}/locations/{location}/instances/{instance}/.
Use the Java InstancesClient with the HTTP JSON transport, as for ServicesClient. google-cloud-run 0.96.0 or later is needed for InstancesClient.
Instance deviations from GCP
restartPolicy,sshEnabled,defaultUriDisabledandCondition.instanceReasonare returned by GCP but do not exist inproto-google-cloud-run-v20.96.0, so they are neither accepted nor emitted. The emulator never restarts an instance container on its own, which is equivalent to aNEVERrestart policy; GCP defaults toON_FAILURE. Revisit this when the published proto gains these fields.- GCP environment variables for instances could not be observed (invoking the test instance failed authentication), so the container receives
PORTonly and noK_*orCLOUD_RUN_*variables. etagis accepted on delete and never validated, which matches the observed GCP behavior for a mismatched etag.- No soft delete: deleted instances disappear immediately and
showDeletedhas no effect.deleteTimeandexpireTimeappear only on the delete operation response. creator,lastModifier,logUriand a defaultserviceAccountare not filled in, the same as Services. The resourceetagis a random token per change, not GCP's format.- Pending operations do not emit GCP's transient
Retrycondition orCONDITION_PENDINGphase. - gcloud is not supported: gcloud uses the v1 Knative API for instances.
- Instances are not reconciled after an emulator restart. With persistent storage, an instance that was running is still reported in its last phase but has no container, since shutdown removed it; requests to its URL return
503until it is stopped and started again. Containers left behind by a crashed emulator (no graceful shutdown) are not removed at startup. - The Service/Instance ID check is not atomic with the other kind's create: a Service and an Instance with the same ID created concurrently can both succeed, and the Service then owns the URL.
- GCP's conditions after a failed start were not observed; the emulator marks
ContainerReadyandResourcesAvailableasCONDITION_FAILEDwith the start error.