Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
91d7867
Add Google Cloud Run worker identity/deployment helper
seanbollin Aug 21, 2026
923be73
Set worker versioning behavior to PINNED in the Cloud Run worker apply
seanbollin Aug 25, 2026
31a6d7e
Add unit tests for GoogleCloudRunMetadata
seanbollin Aug 25, 2026
10aaa8c
Merge branch 'main' into cloud-run-worker-id
seanbollin Aug 25, 2026
b6552db
Avoid deprecated URL(String) constructor in GoogleCloudRunMetadata
seanbollin Aug 25, 2026
b2c5624
Re-architect Cloud Run worker-ID helper into a CloudRunPlugin
seanbollin Aug 31, 2026
cf0f4df
Rename CloudRunPlugin to WorkerIdPlugin
seanbollin Aug 31, 2026
ffd7d42
Merge main; move Cloud Run worker-ID plugin to its own module
seanbollin Sep 2, 2026
4d57399
Make Cloud Run WorkerID plugin identity-only
seanbollin Sep 9, 2026
710dfe4
Remove deployment-name/build-ID wording from Cloud Run worker-ID docs
seanbollin Sep 9, 2026
ed98088
Reflow worker-ID javadoc to satisfy spotless
seanbollin Sep 9, 2026
b7b1e12
Merge main into cloud-run-worker-id
seanbollin Sep 9, 2026
06e8b99
Simplify worker identity docs and comments
seanbollin Sep 14, 2026
03f4de1
Cleaning up Claude Code-isms
seanbollin Sep 15, 2026
d644666
Merge remote-tracking branch 'origin/main' into cloud-run-worker-id
seanbollin Sep 15, 2026
bc85e4f
Rename plugin to CloudRunIDPlugin
seanbollin Sep 15, 2026
358c135
Rename package leaf to id and metadata accessor to identity
seanbollin Sep 15, 2026
ca79e60
Move metadata-constructor Javadoc to that overload and reformat
seanbollin Sep 15, 2026
2547b45
Merge remote-tracking branch 'origin/main' into cloud-run-worker-id
seanbollin Sep 15, 2026
312a227
Make the metadata-injection constructor package-private
seanbollin Sep 15, 2026
5b053c1
Rename CloudRunIDPlugin to CloudRunIdPlugin
seanbollin Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions contrib/temporal-gcp-cloud-run-id/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Temporal Google Cloud Run worker identity support

This module derives a Temporal worker **identity** for Google Cloud Run from instance metadata, for both Cloud Run **worker pools** and Cloud Run **services**.

The primary API is `CloudRunIdPlugin`. Register it once on your workflow client and it sets the client identity automatically; every worker created from that client inherits it. This mirrors the `CloudRunOpenTelemetryPlugin` in the companion `temporal-gcp-cloud-run-opentelemetry` module.

> Experimental: Google Cloud Run support is experimental and may change without notice.

## Quick start

Add `temporal-gcp-cloud-run-id` next to your Temporal SDK dependency, then register the plugin on the workflow client options:

```java
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.gcp.cloudrun.id.CloudRunIdPlugin;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;

public final class Main {
public static void main(String[] args) {
WorkflowServiceStubs service =
WorkflowServiceStubs.newServiceStubs(
WorkflowServiceStubsOptions.newBuilder()
.setTarget("my-namespace.tmprl.cloud:7233")
.build());

// Registering the plugin on the client:
// - reads Cloud Run instance metadata once while the client is configured, and
// - sets the client identity to the derived worker identity (unless you set one yourself).
WorkflowClient client =
WorkflowClient.newInstance(
service,
WorkflowClientOptions.newBuilder()
.setNamespace("my-namespace")
.setPlugins(new CloudRunIdPlugin())
.build());

WorkerFactory factory = WorkerFactory.newInstance(client);

// Workers created from this client inherit the identity the plugin set on the client. No
// per-worker wiring needed.
Worker worker = factory.newWorker("orders");
worker.registerWorkflowImplementationTypes(OrderWorkflowImpl.class);
worker.registerActivitiesImplementations(new OrderActivitiesImpl());

factory.start();
}
}
```

You can also register the plugin on `WorkflowServiceStubsOptions.Builder.setPlugins(...)`; from there it propagates to the client and workers as well.

## How it works

`CloudRunIdPlugin` reads Cloud Run instance metadata through `GoogleCloudRunMetadata`, which resolves three values:

- **name**: the Cloud Run worker pool name — the first non-empty of `CLOUD_RUN_WORKER_POOL` (set on Cloud Run worker pools) then `K_SERVICE` (set on Cloud Run services).
- **revision**: the first non-empty of `CLOUD_RUN_REVISION` (worker pools) then `K_REVISION` (services).
- **instanceId**: read from the Cloud Run metadata server with a single HTTP `GET` to `http://metadata.google.internal/computeMetadata/v1/instance/id` with the required `Metadata-Flavor: Google` header. The metadata server is available on both worker pools and services.

Worker pools receive `CLOUD_RUN_WORKER_POOL` and `CLOUD_RUN_REVISION` and no `K_*` variables, while services receive `K_SERVICE` and `K_REVISION`, so resolving each value from the worker-pool variable first and the service variable second supports both.

The plugin then applies the metadata through the SDK's client plugin hook:

- **Client** (`configureWorkflowClient`): sets the client identity to `<instanceId>@<revision>` (falling back to `<instanceId>@<name>` and then the bare `<instanceId>`), but only when you have not already set an identity, so a user-provided identity always wins. The metadata is fetched here, once, and cached. Workers created from the client inherit this identity; the plugin sets nothing else on them.

The metadata server is only reachable from a Cloud Run instance, so the fetch in `configureWorkflowClient` throws `IllegalStateException` when it cannot be reached (usually because the process is not running on Google Cloud Run).

## Reading the metadata directly

If you prefer to read the values yourself, use `GoogleCloudRunMetadata` directly:

```java
GoogleCloudRunMetadata metadata = GoogleCloudRunMetadata.fetch();
String identity = metadata.identity();
```

`GoogleCloudRunMetadata.fetch(String metadataUrl, Duration timeout)` overrides the metadata URL or the request timeout.

This module depends only on the Temporal SDK at compile time and uses the JDK's `HttpURLConnection` for the metadata request, so it adds no additional runtime dependencies.
12 changes: 12 additions & 0 deletions contrib/temporal-gcp-cloud-run-id/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
description = '''Temporal Java SDK Google Cloud Run Worker Identity Support Module'''

dependencies {
// This module shouldn't carry temporal-sdk with it, especially for situations when users may
// be using a shaded artifact.
compileOnly project(':temporal-sdk')

testImplementation project(':temporal-sdk')
testImplementation "junit:junit:${junitVersion}"

testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package io.temporal.gcp.cloudrun.id;

import io.temporal.client.WorkflowClientOptions;
import io.temporal.common.Experimental;
import io.temporal.common.SimplePlugin;
import java.util.Objects;
import java.util.function.Supplier;

/**
* Plugin that configures a Temporal worker for Google Cloud Run from instance metadata, for both
* Cloud Run <b>worker pools</b> and Cloud Run <b>services</b>.
*
* <p>Register the plugin once on the workflow client and it propagates to every worker created from
* that client. It reads {@link GoogleCloudRunMetadata Cloud Run instance metadata} once while the
* client is configured, caches it, and sets the workflow client <b>identity</b> to the {@linkplain
* GoogleCloudRunMetadata#identity() derived worker identity}, but only when the caller has not
* already set an identity (a user-provided identity always wins). The workers created from that
* client inherit the client identity.
*
* <p>The metadata is fetched lazily when the client is configured. The metadata server is only
* reachable from a Cloud Run instance, so the fetch throws {@link IllegalStateException} when this
* process is not running on Cloud Run.
*
* <p>Register the plugin with {@link WorkflowClientOptions.Builder#setPlugins}:
*
* <pre>{@code
* WorkflowClient client =
* WorkflowClient.newInstance(
* service,
* WorkflowClientOptions.newBuilder()
* .setNamespace(namespace)
* .setPlugins(new CloudRunIdPlugin())
* .build());
*
* WorkerFactory factory = WorkerFactory.newInstance(client);
* Worker worker = factory.newWorker("my-task-queue");
* }</pre>
*
* <p><b>Experimental:</b> Google Cloud Run support is experimental and may change without notice.
*/
@Experimental
public final class CloudRunIdPlugin extends SimplePlugin {
/** Unique plugin name, used for logging and duplicate detection. */
public static final String NAME = "io.temporal.gcp.cloudrun.id.CloudRunIdPlugin";

private final Supplier<GoogleCloudRunMetadata> metadataSupplier;
private volatile GoogleCloudRunMetadata metadata;

/**
* Creates a plugin that fetches Cloud Run instance metadata from the {@linkplain
* GoogleCloudRunMetadata#DEFAULT_METADATA_URL default metadata server} while the workflow client
* is configured.
*/
public CloudRunIdPlugin() {
this(GoogleCloudRunMetadata::fetch);
}

/**
* Package-private seam that builds a plugin from already-resolved {@link GoogleCloudRunMetadata},
* skipping the fetch. Used by tests.
*
* @param metadata previously fetched Cloud Run instance metadata.
*/
CloudRunIdPlugin(GoogleCloudRunMetadata metadata) {
this(fixedSupplier(metadata));
}

/**
* Package-private test seam that supplies the {@link GoogleCloudRunMetadata} lazily. It lets unit
* tests point the fetch at an in-process metadata server and injected environment through the
* {@link GoogleCloudRunMetadata#fetch(String, java.time.Duration, java.util.function.Function)}
* seam, and to exercise the off-platform fail-fast path. It is not part of the public API.
*
* @param metadataSupplier supplier invoked once, at client-configure time, to resolve the
* metadata.
*/
CloudRunIdPlugin(Supplier<GoogleCloudRunMetadata> metadataSupplier) {
super(NAME);
this.metadataSupplier = Objects.requireNonNull(metadataSupplier, "metadataSupplier");
}

/**
* Fetches (once) and caches the Cloud Run instance metadata, then sets the derived worker
* identity on the client options when the caller has not already set an identity.
*
* @param builder the workflow client options builder to configure.
* @throws IllegalStateException if the Cloud Run metadata server cannot be reached, which usually
* means this process is not running on Google Cloud Run.
*/
@Override
public void configureWorkflowClient(WorkflowClientOptions.Builder builder) {
GoogleCloudRunMetadata resolved = metadata();
if (isBlank(builder.build().getIdentity())) {
builder.setIdentity(resolved.identity());
}
}

private GoogleCloudRunMetadata metadata() {
GoogleCloudRunMetadata local = metadata;
if (local == null) {
synchronized (this) {
local = metadata;
if (local == null) {
local = Objects.requireNonNull(metadataSupplier.get(), "Cloud Run metadata");
metadata = local;
}
}
}
return local;
}

private static Supplier<GoogleCloudRunMetadata> fixedSupplier(GoogleCloudRunMetadata metadata) {
Objects.requireNonNull(metadata, "metadata");
return () -> metadata;
}

private static boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
}
Loading
Loading