# 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](#worker-pools)) | | `FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_SECURITY_OPTS` | unset | Comma-separated Docker security options for every workload container (see [Security options](#security-options)) | ## 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}` | ### IAM enforcement for services With `floci-gcp.services.iam.authorization-mode=enforce`, Cloud Run v2 service and revision REST JSON requests made with emulator-issued service-account credentials evaluate stored service and project policies. Service create and list check `run.services.create` and `run.services.list` on the location parent. Get, update, and delete check the corresponding `run.services.*` permission on the named service. Service policy get and set check `run.services.getIamPolicy` and `run.services.setIamPolicy`. Revision list, get, and delete check `run.revisions.list`, `run.revisions.get`, and `run.revisions.delete`; revision checks use the containing service policy and project policy. Listing revisions across all services with `services/-` checks `run.revisions.list` on the location parent. `testIamPermissions` returns only granted permissions for the caller. Denied requests return `403 PERMISSION_DENIED` before the handler mutates state. The supported roles are `roles/run.viewer` for service and revision reads, `roles/run.developer` for service and revision management without policy writes, and `roles/run.admin` for those operations plus service policy writes. The corresponding Cloud Run permissions from basic `roles/viewer`, `roles/editor`, and `roles/owner` are also supported. Conditions use the emulator's restricted CEL profile, including `resource.name`, `resource.type`, `resource.service`, and `request.time`; see [IAM enforcement](../iam-enforcement.md). For create, `resource.name` is the location parent (`projects/{project}/locations/{location}`), because the service does not yet exist. Grant create on the parent, then scope existing-resource operations by service name. This enforcement does not cover Cloud Run jobs, worker pools, instances, invocation, operations, or gRPC service management. Service policy methods are also available through the shared IAM gRPC API. The emulator does not additionally check `run.operations.get`, `iam.serviceAccounts.actAs`, or Artifact Registry download permission during deployment, although [Cloud Run's deployment guide](https://docs.cloud.google.com/run/docs/reference/iam/roles) lists them for real GCP. External or anonymous credentials retain the emulator's permissive behavior. 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: ```text http://{service}-{project-token}.{location}.run.localhost.floci.io:4588 ``` 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`. Docker publishes them on `127.0.0.1` only, so other machines on the network cannot bypass the front door. 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 mode applies container `resources.limits` to the Docker container of every workload: services, job tasks, worker pools, and instances. `memory` accepts a decimal number with an optional `k`, `M`, `G`, `T` (powers of 1000) or `Ki`, `Mi`, `Gi`, `Ti` (powers of 1024) suffix and becomes the Docker memory limit. Swap is disabled by setting the Docker swap limit to the same value, because Cloud Run has no swap. `cpu` accepts whole or fractional cores (`1`, `0.5`) or millicores (`2000m`) and becomes the Docker CPU limit, clamped to the number of CPUs available to Docker, since Docker rejects a CPU limit above that count. A missing key leaves that resource unlimited. Jobs and worker pools store a missing `cpu` as `1000m` and a missing `memory` as `512Mi`, and instances store `2000m` and `2048Mi`, so those workloads always run limited. Only a service created without limits runs unlimited. A value that cannot be parsed fails with `400 INVALID_ARGUMENT` and `Invalid value for resources.limits.{key}: {value}`. Docker still enforces its own minimum memory limit when the container is created. 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. ### Security options Cloud Run lets a container create an unprivileged user namespace (`unshare(CLONE_NEWUSER)`), which rootless sandboxes such as bubblewrap rely on. Docker's default seccomp profile rejects that call with `EPERM`, so floci-gcp workload containers cannot do it by default. Set `FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_SECURITY_OPTS` to pass Docker security options to every Cloud Run workload container (services, job tasks, worker pool replicas, and instances), and to no other container floci-gcp starts: ```bash FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_SECURITY_OPTS=seccomp=unconfined ``` Hosts that enforce AppArmor may also need `apparmor=unconfined`, for example `seccomp=unconfined,apparmor=unconfined`. Leaving the variable unset keeps Docker's default profile, because running arbitrary images unconfined on the host's Docker daemon is a poor default. floci-gcp strips whitespace around each entry and drops empty entries, so `seccomp=unconfined, apparmor=unconfined` works. It does not validate the values. Docker checks them only when it creates a workload container. An invalid option such as `foo=bar` does not stop floci-gcp from starting, but every job run and service create then fails with code 13 (`INTERNAL`) and Docker's error message. For seccomp, `seccomp=unconfined` is the supported value. A custom profile file does not work. `docker run --security-opt seccomp=profile.json` reads the file in the Docker CLI and sends its contents, but floci-gcp passes the value to the Docker API unchanged, and the API expects the profile JSON itself. ## SDK Usage Cloud Run clients should use the HTTP JSON transport, an explicit endpoint, and no credentials: ```java 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`, valid `resources.limits`. 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 - `etag` is 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 `showDeleted` is accepted and ignored; `deleteTime` and `expireTime` appear only on the delete operation's Execution or Job. - `validateOnly` returns a completed operation that cannot be fetched later. GCP returns an unfinished operation name that also cannot be fetched. - `serviceAccount` is 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 `Retry` condition or `logUri`. - The `TASK` kind in the task `404` message 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 `ACAB` etag 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.manualInstanceCount` is `1` when unset, every template container gets `resources.limits` `cpu: 1000m` and `memory: 512Mi` when those keys are missing, `launchStage` is `GA`, and an empty `instanceSplits` becomes one `INSTANCE_SPLIT_ALLOCATION_TYPE_LATEST` split at 100%. `template.serviceAccount` is stored as given, as for services. `customAudiences` is dropped, because GCP ignores it. - Revisions are named `{pool}-{5-digit counter}-{3 random lowercase alphanumerics}`, for example `orders-00002-jw2`. A revision is created on create, whenever the template changes (compared after defaults are applied), and whenever `forceNewRevision=true`. Any other update, such as scaling, labels or splits, only increments `generation`. - A revision copies the template: unnamed containers are named `{last image path segment}-{1-based index}` (`busybox-1`), `executionEnvironment` is `EXECUTION_ENVIRONMENT_GEN2`, and the `service` field is not set. Images are stored as given; they are not resolved to digests. - `instanceSplitStatuses` mirrors `instanceSplits` and always names a concrete revision: a `LATEST` split names `latestCreatedRevision`. Every revision the splits resolve to must exist in the same pool and the percentages must add up to 100, otherwise the update fails with `400 INVALID_ARGUMENT`. - The pool carries `latestCreatedRevision`, `latestReadyRevision`, `terminalCondition {type: Ready}` with no top-level `conditions`, `generation`, `observedGeneration` and an `etag`. 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_FOUND` with `Resource '{id}' of kind 'WORKER_POOL' in region '{location}' in project '{project}' does not exist.` (kind `REVISION` for revisions). - A revision named in `instanceSplitStatuses` is serving. Serving revisions carry `Ready`, `Active`, `ResourcesAvailable`, `ContainerReady` and `MinInstancesProvisioned` conditions with the GCP messages (for example `Deploying revision succeeded in 0.42s.`) and `scalingStatus.desiredMinInstanceCount` equal to the pool's `manualInstanceCount`. Other revisions are retired: `Active` is `CONDITION_FAILED` with `Revision retired.` and `revisionReason: RETIRED`, and `scalingStatus` is cleared. - Deleting a serving revision, or the pool's `latestCreatedRevision` or `latestReadyRevision` (which a `LATEST` split resolves to), fails with `400 FAILED_PRECONDITION` and `Revision "{revision}" cannot be directly deleted because it is actively serving.`. Deleting any other revision returns a completed operation whose response is the revision with `deleteTime` and `expireTime` set. - 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 `deleteTime` and `expireTime`. 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.manualInstanceCount` replicas, capped by `FLOCI_GCP_SERVICES_CLOUDRUN_EXECUTION_MAX_WORKER_INSTANCES` (default `1`). A larger request is clamped with a warning in the emulator log; the pool and revision metadata still report the requested count. `manualInstanceCount: 0` stops every replica. - Each replica receives the container's own `env` plus `CLOUD_RUN_WORKER_POOL` (the pool ID) and `CLOUD_RUN_REVISION` (the revision ID), which win on conflict. No `PORT` or `K_*` variables are injected and no port is published. - Create, update and delete return pending operations. While the containers converge, the pool has `reconciling: true`, `observedGeneration` stays at the previous generation (unset after create), and `terminalCondition` is `CONDITION_RECONCILING` with `Deploying Revision.` (new revision) or `Provisioning revision instances to process workloads.` (scaling, splits or other fields). The operation completes, and `observedGeneration` catches up with `generation`, once the containers match the latest committed state. If a container cannot be started the operation fails with code 13, the pool's `terminalCondition` becomes `CONDITION_FAILED` with the Docker error, and the previously running containers are left in place. Operations fail with `DEADLINE_EXCEEDED` after `FLOCI_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 below `FLOCI_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, no env `valueSource`, and valid `resources.limits`. 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`) and `The emulator restarted before the operation completed.`, removes worker pool containers the previous process left behind (matched by the `io.floci.service=cloudrun` label, an `io.floci.cloudrun.resource-name` under `/workerPools/`, and this emulator's `floci_emulator` and `floci_namespace` labels; containers from earlier versions are matched by the legacy `floci_service` and `floci_resource` labels instead), and reconciles every stored pool, so with persistent storage a restored pool starts its replicas again and leaves `reconciling`. 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). - `etag` is a random UUID and is never validated on update or delete. - There is no soft delete: `showDeleted` is ignored and `deleteTime` and `expireTime` appear 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 no `logUri` or `creator`, and GCP's transient `Retry` and `InstanceShutDown` conditions 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_ARGUMENT` messages for invalid `instanceSplits` are floci-gcp's own. The `404` message follows the format GCP returns for jobs and executions; the `WORKER_POOL` and `REVISION` kind names have not been observed against GCP. - Refusing to delete `latestCreatedRevision` or `latestReadyRevision` while 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 ```java 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 `instanceId` or `instance_id`. Both are optional: without one, the server generates a 15-character ID of lowercase letters and digits that starts with a letter. `validateOnly` validates 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_EXISTS` with `Resource '{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_FOUND` with `Resource '{id}' of kind 'INSTANCE' in region '{location}' in project '{project}' does not exist.`. - List supports `pageSize` and `pageToken`. - Update accepts `updateMask`, `validateOnly` and `allowMissing`. With an update mask, a masked top-level field is replaced as a whole and output-only paths are ignored. A nested path such as `vpcAccess.connector` updates 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 an `Instance` field returns `400 INVALID_ARGUMENT` with `Invalid update mask path: {path}`. Without an update mask, every settable field present in the body is replaced. `allowMissing=true` creates the instance when it does not exist. - Delete accepts `validateOnly` and `etag`. Start and stop accept `validateOnly` in 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`, valid `resources.limits`, 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`, `defaultUriDisabled` and `Condition.instanceReason` are returned by GCP but do not exist in `proto-google-cloud-run-v2` 0.96.0, so they are neither accepted nor emitted. The emulator never restarts an instance container on its own, which is equivalent to a `NEVER` restart policy; GCP defaults to `ON_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 `PORT` only and no `K_*` or `CLOUD_RUN_*` variables. - `etag` is accepted on delete and never validated, which matches the observed GCP behavior for a mismatched etag. - No soft delete: deleted instances disappear immediately and `showDeleted` has no effect. `deleteTime` and `expireTime` appear only on the delete operation response. - `creator`, `lastModifier`, `logUri` and a default `serviceAccount` are not filled in, the same as Services. The resource `etag` is a random token per change, not GCP's format. - Pending operations do not emit GCP's transient `Retry` condition or `CONDITION_PENDING` phase. - 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 `503` until 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 `ContainerReady` and `ResourcesAvailable` as `CONDITION_FAILED` with the start error.