From b86a19bf76a5609b05556014319a8ea878d028d7 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 20 Jun 2025 18:33:35 -0700 Subject: [PATCH 1/4] trying api keys --- .../samples/moneytransfer/TemporalClient.java | 167 +++++++++--------- .../moneytransfer/TransferScheduler.java | 76 +------- 2 files changed, 80 insertions(+), 163 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java b/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java index a49b0fe..9a00078 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java @@ -1,20 +1,20 @@ /* - * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved + * Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved * - * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * - * Modifications copyright (C) 2017 Uber Technologies, Inc. + * Modifications copyright (C) 2017 Uber Technologies, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not - * use this file except in compliance with the License. A copy of the License is - * located at + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at * - * http://aws.amazon.com/apache2.0 + * http://aws.amazon.com/apache2.0 * - * or in the "license" file accompanying this file. This file is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. */ package io.temporal.samples.moneytransfer; @@ -23,104 +23,95 @@ import io.temporal.client.WorkflowClientOptions; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; -import io.temporal.common.converter.CodecDataConverter; -import io.temporal.common.converter.DefaultDataConverter; -import io.temporal.samples.moneytransfer.dataconverter.CryptCodec; -import io.temporal.samples.moneytransfer.web.ServerInfo; -import io.temporal.serviceclient.SimpleSslContextBuilder; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; -import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.Collections; import javax.net.ssl.SSLException; public class TemporalClient { - public static WorkflowServiceStubs getWorkflowServiceStubs() - throws FileNotFoundException, SSLException { - WorkflowServiceStubsOptions.Builder workflowServiceStubsOptionsBuilder = - WorkflowServiceStubsOptions.newBuilder(); - - if (!ServerInfo.getCertPath().equals("") && !"".equals(ServerInfo.getKeyPath())) { - InputStream clientCert = new FileInputStream(ServerInfo.getCertPath()); - - InputStream clientKey = new FileInputStream(ServerInfo.getKeyPath()); - - workflowServiceStubsOptionsBuilder.setSslContext( - SimpleSslContextBuilder.forPKCS8(clientCert, clientKey).build()); - } - - // For temporal cloud this would likely be ${namespace}.tmprl.cloud:7233 - String targetEndpoint = ServerInfo.getAddress(); - // Your registered namespace. - workflowServiceStubsOptionsBuilder.setTarget(targetEndpoint); - WorkflowServiceStubs service = null; - - if (!ServerInfo.getAddress().equals("localhost:7233")) { - // if not local server, then use the workflowServiceStubsOptionsBuilder - service = WorkflowServiceStubs.newServiceStubs(workflowServiceStubsOptionsBuilder.build()); + /** + * Centralized method to create and configure WorkflowServiceStubs. This is the single source of + * truth for connecting to Temporal. + */ + private static WorkflowServiceStubs createWorkflowServiceStubs() { + // These are the values for connecting to Temporal Cloud + String temporalCloudEndpoint = System.getenv("TEMPORAL_ADDRESS"); + String temporalCloudNamespace = System.getenv("TEMPORAL_NAMESPACE"); + String temporalApiKey = System.getenv("TEMPORAL_API_KEY"); + + // If the environment variables for cloud are not set, assume local connection. + // This check makes the code work for both local dev and cloud deployments. + boolean isCloudConnection = + temporalCloudEndpoint != null + && !temporalCloudEndpoint.isEmpty() + && temporalCloudNamespace != null + && !temporalCloudNamespace.isEmpty() + && temporalApiKey != null + && !temporalApiKey.isEmpty(); + + if (isCloudConnection) { + System.out.println("--- Connecting to Temporal Cloud ---"); + System.out.println("Endpoint: " + temporalCloudEndpoint); + System.out.println("Namespace: " + temporalCloudNamespace); + // System.out.println("API Key: " + temporalApiKey); + System.out.println("---------------------------------"); + + return WorkflowServiceStubs.newServiceStubs( + WorkflowServiceStubsOptions.newBuilder() + .setTarget(temporalCloudEndpoint) + .setEnableHttps(true) + .addApiKey(() -> temporalApiKey) + .build()); } else { - service = WorkflowServiceStubs.newLocalServiceStubs(); + System.out.println("--- Connecting to Local Temporal ---"); + return WorkflowServiceStubs.newLocalServiceStubs(); } + } - return service; + /** + * This method is preserved to prevent build failures across the project. It now uses the new, + * correct connection logic. + */ + public static WorkflowServiceStubs getWorkflowServiceStubs() + throws FileNotFoundException, SSLException { + return createWorkflowServiceStubs(); } + /** Gets a fully configured WorkflowClient. */ public static WorkflowClient get() throws FileNotFoundException, SSLException { - // TODO support local server - // Get worker to poll the common task queue. - // gRPC stubs wrapper that talks to the local docker instance of temporal service. - // WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); - - WorkflowServiceStubs service = getWorkflowServiceStubs(); - - WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - - // if environment variable ENCRYPT_PAYLOADS is set to true, then use CryptCodec - if (System.getenv("ENCRYPT_PAYLOADS") != null - && System.getenv("ENCRYPT_PAYLOADS").equals("true")) { - builder.setDataConverter( - new CodecDataConverter( - DefaultDataConverter.newDefaultInstance(), - Collections.singletonList(new CryptCodec()), - true /* encode failure attributes */)); + WorkflowServiceStubs service = createWorkflowServiceStubs(); + + // Use the correct namespace for the client options + String namespace = System.getenv("TEMPORAL_NAMESPACE"); + if (namespace == null || namespace.isEmpty()) { + namespace = "default"; // Fallback for local development } - System.out.println("<<<>>>:\n " + ServerInfo.getServerInfo()); - WorkflowClientOptions clientOptions = builder.setNamespace(ServerInfo.getNamespace()).build(); + WorkflowClientOptions clientOptions = + WorkflowClientOptions.newBuilder() + .setNamespace(namespace) + // .setDataConverter(...) // Your custom data converter can be added here + .build(); - // client that can be used to start and signal workflows - WorkflowClient client = WorkflowClient.newInstance(service, clientOptions); - return client; + return WorkflowClient.newInstance(service, clientOptions); } + /** Gets a fully configured ScheduleClient. */ public static ScheduleClient getScheduleClient() throws FileNotFoundException, SSLException { - // TODO support local server - // Get worker to poll the common task queue. - // gRPC stubs wrapper that talks to the local docker instance of temporal service. - // WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); - - WorkflowServiceStubs service = getWorkflowServiceStubs(); - - ScheduleClientOptions.Builder builder = ScheduleClientOptions.newBuilder(); - - // if environment variable ENCRYPT_PAYLOADS is set to true, then use CryptCodec - if (System.getenv("ENCRYPT_PAYLOADS") != null - && System.getenv("ENCRYPT_PAYLOADS").equals("true")) { - builder.setDataConverter( - new CodecDataConverter( - DefaultDataConverter.newDefaultInstance(), - Collections.singletonList(new CryptCodec()), - true /* encode failure attributes */)); + WorkflowServiceStubs service = createWorkflowServiceStubs(); + + String namespace = System.getenv("TEMPORAL_NAMESPACE"); + if (namespace == null || namespace.isEmpty()) { + namespace = "default"; // Fallback for local development } - System.out.println("<<<>>>:\n " + ServerInfo.getServerInfo()); - ScheduleClientOptions clientOptions = builder.setNamespace(ServerInfo.getNamespace()).build(); + ScheduleClientOptions clientOptions = + ScheduleClientOptions.newBuilder() + .setNamespace(namespace) + // .setDataConverter(...) // Your custom data converter can be added here + .build(); - // client that can be used to start and signal workflows - ScheduleClient client = ScheduleClient.newInstance(service, clientOptions); - return client; + return ScheduleClient.newInstance(service, clientOptions); } } diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TransferScheduler.java b/core/src/main/java/io/temporal/samples/moneytransfer/TransferScheduler.java index 8920564..0cc5ea3 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TransferScheduler.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TransferScheduler.java @@ -19,11 +19,9 @@ package io.temporal.samples.moneytransfer; -import static io.temporal.samples.moneytransfer.TemporalClient.getScheduleClient; import static io.temporal.samples.moneytransfer.TemporalClient.getWorkflowServiceStubs; import io.temporal.api.common.v1.WorkflowExecution; -import io.temporal.api.enums.v1.ScheduleOverlapPolicy; import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest; import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse; import io.temporal.api.workflowservice.v1.WorkflowServiceGrpc; @@ -35,8 +33,6 @@ import io.temporal.samples.moneytransfer.web.ServerInfo; import io.temporal.serviceclient.WorkflowServiceStubs; import java.io.FileNotFoundException; -import java.time.Duration; -import java.util.Collections; import javax.net.ssl.SSLException; public class TransferScheduler { @@ -97,77 +93,7 @@ public static String runWorkflow(WorkflowParameterObj workflowParameterObj) public static String runSchedule(ScheduleParameterObj scheduleParameterObj) { - String scheduleNumber = null; - try { - int amountCents = scheduleParameterObj.getAmount(); // amount to transfer - ExecutionScenarioObj executionScenarioObj = scheduleParameterObj.getScenario(); - - WorkflowParameterObj params = new WorkflowParameterObj(amountCents, executionScenarioObj); - - ScheduleClient scheduleClient = getScheduleClient(); - - String referenceNumber = generateReferenceNumber(); // random reference number - scheduleNumber = referenceNumber + "-schedule"; - final String TASK_QUEUE = ServerInfo.getTaskqueue(); - - WorkflowOptions options = - WorkflowOptions.newBuilder() - .setWorkflowId(referenceNumber) - .setTaskQueue(TASK_QUEUE) - .build(); - - ScheduleActionStartWorkflow action = - ScheduleActionStartWorkflow.newBuilder() - .setWorkflowType(AccountTransferWorkflow.class) - .setArguments(params) - .setOptions(options) - .build(); - - // Define the schedule we want to create - Schedule schedule = - Schedule.newBuilder() - .setAction(action) - .setSpec(ScheduleSpec.newBuilder().build()) - .build(); - - ScheduleHandle handle = - scheduleClient.createSchedule( - scheduleNumber, schedule, ScheduleOptions.newBuilder().build()); - - // Update the schedule with a spec, so it will run periodically - handle.update( - (ScheduleUpdateInput input) -> { - Schedule.Builder builder = Schedule.newBuilder(input.getDescription().getSchedule()); - - builder.setSpec( - ScheduleSpec.newBuilder() - .setIntervals( - Collections.singletonList( - new ScheduleIntervalSpec( - Duration.ofSeconds(scheduleParameterObj.getInterval())))) - .build()); - // Make the schedule paused to demonstrate how to unpause a schedule - builder.setState( - ScheduleState.newBuilder() - .setLimitedAction(true) - .setRemainingActions(scheduleParameterObj.getCount()) - .build()); - - // Temporal's default schedule policy is 'skip' - builder.setPolicy( - SchedulePolicy.newBuilder() - .setOverlap(ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_SKIP) - .build()); - - return new ScheduleUpdate(builder.build()); - }); - - // Unpause schedule - // handle.unpause(); - } catch (Exception e) { - System.out.println("Exception: " + e); - } - return scheduleNumber; + return "not implemented"; } @SuppressWarnings("CatchAndPrintStackTrace") From 6478949c6df8fffd90642d4380af78872b29cfab Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Sat, 21 Jun 2025 07:38:55 -0700 Subject: [PATCH 2/4] version bump, trying to make api keys work for describe --- README.md | 12 ++++++++ build.gradle | 2 +- .../samples/moneytransfer/TemporalClient.java | 21 ++++++++++---- .../samples/moneytransfer/TransferLister.java | 28 +++++++++++-------- .../moneytransfer/TransferRequester.java | 12 +++++++- .../samples/moneytransfer/web/ServerInfo.java | 5 ++++ 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 7b2cd60..7f1a0f1 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,18 @@ Start a worker: ENCRYPT_PAYLOADS=true ./gradlew -q execute -PmainClass=io.temporal.samples.moneytransfer.AccountTransferWorker --console=plain ``` +Start a new workflow: + +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.moneytransfer.TransferRequester --console=plain +``` + +Check the status of an existing workflow: + +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.moneytransfer.TransferRequester --args="--status WORKFLOW_ID" --console=plain +``` + Run the money transfer web UI: ```bash diff --git a/build.gradle b/build.gradle index a853a33..7c915ed 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ subprojects { ext { otelVersion = '1.26.0' otelVersionAlpha = "${otelVersion}-alpha" - javaSDKVersion = '1.24.3' + javaSDKVersion = '1.29.0' jarVersion = '1.0.0' } diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java b/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java index 9a00078..a23adde 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java @@ -23,6 +23,7 @@ import io.temporal.client.WorkflowClientOptions; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.samples.moneytransfer.web.ServerInfo; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; import java.io.FileNotFoundException; @@ -36,9 +37,17 @@ public class TemporalClient { */ private static WorkflowServiceStubs createWorkflowServiceStubs() { // These are the values for connecting to Temporal Cloud - String temporalCloudEndpoint = System.getenv("TEMPORAL_ADDRESS"); - String temporalCloudNamespace = System.getenv("TEMPORAL_NAMESPACE"); - String temporalApiKey = System.getenv("TEMPORAL_API_KEY"); + String temporalCloudEndpoint = ServerInfo.getAddress(); + String temporalCloudNamespace = ServerInfo.getNamespace(); + String temporalApiKey = ServerInfo.getApiKey(); + + System.out.println("TEMPORAL_ADDRESS: " + temporalCloudEndpoint); + System.out.println("TEMPORAL_NAMESPACE: " + temporalCloudNamespace); + if (temporalApiKey != null && !temporalApiKey.isEmpty()) { + System.out.println("TEMPORAL_API_KEY length: " + temporalApiKey.length()); + } else { + System.out.println("TEMPORAL_API_KEY: Not set"); + } // If the environment variables for cloud are not set, assume local connection. // This check makes the code work for both local dev and cloud deployments. @@ -54,7 +63,7 @@ private static WorkflowServiceStubs createWorkflowServiceStubs() { System.out.println("--- Connecting to Temporal Cloud ---"); System.out.println("Endpoint: " + temporalCloudEndpoint); System.out.println("Namespace: " + temporalCloudNamespace); - // System.out.println("API Key: " + temporalApiKey); + System.out.println("API key length: " + temporalApiKey.length()); System.out.println("---------------------------------"); return WorkflowServiceStubs.newServiceStubs( @@ -83,7 +92,7 @@ public static WorkflowClient get() throws FileNotFoundException, SSLException { WorkflowServiceStubs service = createWorkflowServiceStubs(); // Use the correct namespace for the client options - String namespace = System.getenv("TEMPORAL_NAMESPACE"); + String namespace = ServerInfo.getNamespace(); if (namespace == null || namespace.isEmpty()) { namespace = "default"; // Fallback for local development } @@ -101,7 +110,7 @@ public static WorkflowClient get() throws FileNotFoundException, SSLException { public static ScheduleClient getScheduleClient() throws FileNotFoundException, SSLException { WorkflowServiceStubs service = createWorkflowServiceStubs(); - String namespace = System.getenv("TEMPORAL_NAMESPACE"); + String namespace = ServerInfo.getNamespace(); if (namespace == null || namespace.isEmpty()) { namespace = "default"; // Fallback for local development } diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java b/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java index d3a4703..38f295e 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java @@ -43,17 +43,21 @@ public class TransferLister { public static List listWorkflows() throws FileNotFoundException, SSLException { WorkflowServiceStubs service = getWorkflowServiceStubs(); - ListOpenWorkflowExecutionsResponse responseOpen = - service - .blockingStub() - .listOpenWorkflowExecutions( - ListOpenWorkflowExecutionsRequest.newBuilder() - .setStartTimeFilter( - StartTimeFilter.newBuilder().setEarliestTime(getOneHourAgo()).build()) - .setTypeFilter( - WorkflowTypeFilter.newBuilder().setName("moneyTransferWorkflow").build()) - .setNamespace(ServerInfo.getNamespace()) - .build()); + + // Try with minimal request first + ListOpenWorkflowExecutionsResponse responseOpen; + try { + responseOpen = + service + .blockingStub() + .listOpenWorkflowExecutions( + ListOpenWorkflowExecutionsRequest.newBuilder() + .setNamespace(ServerInfo.getNamespace()) + .build()); + } catch (Exception e) { + System.err.println("Failed to list open workflows: " + e.getMessage()); + throw e; + } ListClosedWorkflowExecutionsResponse responseClosed = service @@ -62,9 +66,9 @@ public static List listWorkflows() throws FileNotFoundExcepti ListClosedWorkflowExecutionsRequest.newBuilder() .setStartTimeFilter( StartTimeFilter.newBuilder().setEarliestTime(getOneHourAgo()).build()) + .setNamespace(ServerInfo.getNamespace()) .setTypeFilter( WorkflowTypeFilter.newBuilder().setName("moneyTransferWorkflow").build()) - .setNamespace(ServerInfo.getNamespace()) .build()); // array of WorkflowStatusObj diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java b/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java index c50c132..56b37e0 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java @@ -109,6 +109,15 @@ public static String runWorkflow(WorkflowParameterObj workflowParameterObj) @SuppressWarnings("CatchAndPrintStackTrace") public static void main(String[] args) throws Exception { + // Check if workflow ID is provided as argument to get status + if (args.length > 0 && args[0].equals("--status") && args.length > 1) { + String workflowId = args[1]; + String status = getWorkflowStatus(workflowId); + System.out.println("Workflow " + workflowId + " status: " + status); + System.exit(0); + } + + // Default behavior: start a new workflow int amountCents = 45; // amount to transfer WorkflowParameterObj params = @@ -132,11 +141,12 @@ private static String generateReferenceNumber() { private static String getWorkflowStatus(String workflowId) throws FileNotFoundException, SSLException { + WorkflowClient client = TemporalClient.get(); WorkflowServiceStubs service = getWorkflowServiceStubs(); WorkflowServiceGrpc.WorkflowServiceBlockingStub stub = service.blockingStub(); DescribeWorkflowExecutionRequest request = DescribeWorkflowExecutionRequest.newBuilder() - .setNamespace(ServerInfo.getNamespace()) + .setNamespace(client.getOptions().getNamespace()) .setExecution(WorkflowExecution.newBuilder().setWorkflowId(workflowId)) .build(); DescribeWorkflowExecutionResponse response = stub.describeWorkflowExecution(request); diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/web/ServerInfo.java b/core/src/main/java/io/temporal/samples/moneytransfer/web/ServerInfo.java index f9d593f..320d5b8 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/web/ServerInfo.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/web/ServerInfo.java @@ -52,6 +52,11 @@ public static String getWebServerURL() { return webServerURL != null && !webServerURL.isEmpty() ? webServerURL : "http://localhost:7070"; } + public static String getApiKey() { + String apiKey = System.getenv("TEMPORAL_API_KEY"); + return apiKey != null && !apiKey.isEmpty() ? apiKey : ""; + } + public static int getWorkflowSleepDuration() { String workflowSleepDurationString = System.getenv("TEMPORAL_MONEYTRANSFER_SLEEP"); int workflowSleepDuration = 0; From 549063a14b2fff4d2eef986015bd3f5dab4ddd51 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Sat, 21 Jun 2025 08:57:29 -0700 Subject: [PATCH 3/4] Add support for multiple Temporal Cloud authentication methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhanced TemporalClient to support three connection methods with priority order: certificate-based (mTLS), API key authentication, and local fallback connections. • Refactored connection logic to prioritize mTLS certificates over API keys • Added comprehensive authentication method detection and logging • Updated README with clear examples for both certificate and API key authentication • Restored encryption codec support for ENCRYPT_PAYLOADS environment variable 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 30 ++++- .../samples/moneytransfer/TemporalClient.java | 108 +++++++++++------- 2 files changed, 95 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 7f1a0f1..5be6e98 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,46 @@ A [Typescript SDK version](https://github.com/steveandroulakis/temporal-money-tr The sample is configured by default to connect to a [local Temporal Server](https://docs.temporal.io/cli#starting-the-temporal-server) running on localhost:7233. -To instead connect to Temporal Cloud, set the following environment variables, replacing them with your own Temporal Cloud credentials: +To connect to Temporal Cloud, you have two authentication options: + +### Option 1: Certificate-based Authentication (mTLS) + +Set the following environment variables with your certificate paths: ```bash TEMPORAL_ADDRESS=testnamespace.sdvdw.tmprl.cloud:7233 TEMPORAL_NAMESPACE=testnamespace.sdvdw TEMPORAL_CERT_PATH="/path/to/file.pem" TEMPORAL_KEY_PATH="/path/to/file.key" -```` +``` + +### Option 2: API Key Authentication + +Set the following environment variables with your API key: + +```bash +TEMPORAL_ADDRESS=us-west-2.aws.api.temporal.io:7233 +TEMPORAL_NAMESPACE=testnamespace.sdvdw +TEMPORAL_API_KEY="your-api-key-here" +``` + +For more information about API keys, see the [Temporal Cloud API Keys documentation](https://docs.temporal.io/cloud/api-keys). + +**Note:** The application will prioritize certificate-based authentication if both certificate paths and API key are provided. (optional) set a task queue name ```bash export TEMPORAL_MONEYTRANSFER_TASKQUEUE="MoneyTransferJava" ``` +## Run Tests + +Run all tests: + +```bash +./gradlew test +``` + ## Run a Workflow Note: Use a Java 18 SDK. diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java b/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java index a23adde..ba8d16c 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java @@ -23,55 +23,73 @@ import io.temporal.client.WorkflowClientOptions; import io.temporal.client.schedules.ScheduleClient; import io.temporal.client.schedules.ScheduleClientOptions; +import io.temporal.common.converter.CodecDataConverter; +import io.temporal.common.converter.DefaultDataConverter; +import io.temporal.samples.moneytransfer.dataconverter.CryptCodec; import io.temporal.samples.moneytransfer.web.ServerInfo; +import io.temporal.serviceclient.SimpleSslContextBuilder; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Collections; import javax.net.ssl.SSLException; public class TemporalClient { /** * Centralized method to create and configure WorkflowServiceStubs. This is the single source of - * truth for connecting to Temporal. + * truth for connecting to Temporal. Supports three connection methods: 1. Certificate-based + * (mTLS) - requires TEMPORAL_CERT_PATH and TEMPORAL_KEY_PATH 2. API Key-based - requires + * TEMPORAL_API_KEY 3. Local - fallback when neither certificates nor API key are provided */ - private static WorkflowServiceStubs createWorkflowServiceStubs() { - // These are the values for connecting to Temporal Cloud - String temporalCloudEndpoint = ServerInfo.getAddress(); - String temporalCloudNamespace = ServerInfo.getNamespace(); - String temporalApiKey = ServerInfo.getApiKey(); - - System.out.println("TEMPORAL_ADDRESS: " + temporalCloudEndpoint); - System.out.println("TEMPORAL_NAMESPACE: " + temporalCloudNamespace); - if (temporalApiKey != null && !temporalApiKey.isEmpty()) { - System.out.println("TEMPORAL_API_KEY length: " + temporalApiKey.length()); - } else { - System.out.println("TEMPORAL_API_KEY: Not set"); - } + private static WorkflowServiceStubs createWorkflowServiceStubs() + throws FileNotFoundException, SSLException { + String endpoint = ServerInfo.getAddress(); + String namespace = ServerInfo.getNamespace(); + String apiKey = ServerInfo.getApiKey(); + String certPath = ServerInfo.getCertPath(); + String keyPath = ServerInfo.getKeyPath(); + + System.out.println("TEMPORAL_ADDRESS: " + endpoint); + System.out.println("TEMPORAL_NAMESPACE: " + namespace); + + WorkflowServiceStubsOptions.Builder optionsBuilder = + WorkflowServiceStubsOptions.newBuilder().setTarget(endpoint); + + // Check if certificates are provided (mTLS connection) + boolean hasCertificates = !certPath.isEmpty() && !keyPath.isEmpty(); + + // Check if API key is provided + boolean hasApiKey = apiKey != null && !apiKey.isEmpty(); + + // Check if using local server + boolean isLocal = "localhost:7233".equals(endpoint); + + if (hasCertificates) { + System.out.println("--- Connecting with Certificate Authentication ---"); + System.out.println("Cert path: " + certPath); + System.out.println("Key path: " + keyPath); + System.out.println("Endpoint: " + endpoint); + System.out.println("---------------------------------"); - // If the environment variables for cloud are not set, assume local connection. - // This check makes the code work for both local dev and cloud deployments. - boolean isCloudConnection = - temporalCloudEndpoint != null - && !temporalCloudEndpoint.isEmpty() - && temporalCloudNamespace != null - && !temporalCloudNamespace.isEmpty() - && temporalApiKey != null - && !temporalApiKey.isEmpty(); - - if (isCloudConnection) { - System.out.println("--- Connecting to Temporal Cloud ---"); - System.out.println("Endpoint: " + temporalCloudEndpoint); - System.out.println("Namespace: " + temporalCloudNamespace); - System.out.println("API key length: " + temporalApiKey.length()); + InputStream clientCert = new FileInputStream(certPath); + InputStream clientKey = new FileInputStream(keyPath); + + optionsBuilder.setSslContext(SimpleSslContextBuilder.forPKCS8(clientCert, clientKey).build()); + + return WorkflowServiceStubs.newServiceStubs(optionsBuilder.build()); + + } else if (hasApiKey && !isLocal) { + System.out.println("--- Connecting with API Key Authentication ---"); + System.out.println("Endpoint: " + endpoint); + System.out.println("API key length: " + apiKey.length()); System.out.println("---------------------------------"); return WorkflowServiceStubs.newServiceStubs( - WorkflowServiceStubsOptions.newBuilder() - .setTarget(temporalCloudEndpoint) - .setEnableHttps(true) - .addApiKey(() -> temporalApiKey) - .build()); + optionsBuilder.setEnableHttps(true).addApiKey(() -> apiKey).build()); + } else { System.out.println("--- Connecting to Local Temporal ---"); return WorkflowServiceStubs.newLocalServiceStubs(); @@ -91,17 +109,25 @@ public static WorkflowServiceStubs getWorkflowServiceStubs() public static WorkflowClient get() throws FileNotFoundException, SSLException { WorkflowServiceStubs service = createWorkflowServiceStubs(); - // Use the correct namespace for the client options String namespace = ServerInfo.getNamespace(); if (namespace == null || namespace.isEmpty()) { - namespace = "default"; // Fallback for local development + namespace = "default"; } - WorkflowClientOptions clientOptions = - WorkflowClientOptions.newBuilder() - .setNamespace(namespace) - // .setDataConverter(...) // Your custom data converter can be added here - .build(); + WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); + + // If environment variable ENCRYPT_PAYLOADS is set to true, then use CryptCodec + if (System.getenv("ENCRYPT_PAYLOADS") != null + && System.getenv("ENCRYPT_PAYLOADS").equals("true")) { + builder.setDataConverter( + new CodecDataConverter( + DefaultDataConverter.newDefaultInstance(), + Collections.singletonList(new CryptCodec()), + true /* encode failure attributes */)); + } + + System.out.println("<<<>>>:\n " + ServerInfo.getServerInfo()); + WorkflowClientOptions clientOptions = builder.setNamespace(namespace).build(); return WorkflowClient.newInstance(service, clientOptions); } From e93acf387900fefc13e78da9a16fcd6d58272311 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Sat, 21 Jun 2025 08:58:25 -0700 Subject: [PATCH 4/4] api key and cert support --- .../moneytransfer/DescribeTaskQueue.java | 6 +- .../moneytransfer/RecentHistoryReplayer.java | 6 +- .../samples/moneytransfer/TransferLister.java | 6 +- .../moneytransfer/TransferRequester.java | 16 ++-- .../moneytransfer/TransferScheduler.java | 80 ++++++++++++++++++- 5 files changed, 93 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/DescribeTaskQueue.java b/core/src/main/java/io/temporal/samples/moneytransfer/DescribeTaskQueue.java index 30e5612..2cc6ef8 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/DescribeTaskQueue.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/DescribeTaskQueue.java @@ -1,12 +1,11 @@ package io.temporal.samples.moneytransfer; -import static io.temporal.samples.moneytransfer.TemporalClient.getWorkflowServiceStubs; - import io.temporal.api.enums.v1.TaskQueueKind; import io.temporal.api.enums.v1.TaskQueueType; import io.temporal.api.taskqueue.v1.TaskQueue; import io.temporal.api.workflowservice.v1.DescribeTaskQueueRequest; import io.temporal.api.workflowservice.v1.DescribeTaskQueueResponse; +import io.temporal.client.WorkflowClient; import io.temporal.samples.moneytransfer.web.ServerInfo; import io.temporal.serviceclient.WorkflowServiceStubs; import java.io.FileNotFoundException; @@ -15,7 +14,8 @@ public class DescribeTaskQueue { public static DescribeTaskQueueResponse getTaskQueueInfo() throws FileNotFoundException, SSLException { - WorkflowServiceStubs service = getWorkflowServiceStubs(); + WorkflowClient client = TemporalClient.get(); + WorkflowServiceStubs service = client.getWorkflowServiceStubs(); DescribeTaskQueueResponse res = service diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/RecentHistoryReplayer.java b/core/src/main/java/io/temporal/samples/moneytransfer/RecentHistoryReplayer.java index a3041c5..571c3ee 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/RecentHistoryReplayer.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/RecentHistoryReplayer.java @@ -19,9 +19,8 @@ package io.temporal.samples.moneytransfer; -import static io.temporal.samples.moneytransfer.TemporalClient.getWorkflowServiceStubs; - import io.temporal.api.workflowservice.v1.*; +import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.common.WorkflowExecutionHistory; import io.temporal.common.converter.CodecDataConverter; @@ -44,7 +43,8 @@ public class RecentHistoryReplayer { public static List getWorkflowHistories() throws FileNotFoundException, SSLException { - WorkflowServiceStubs service = getWorkflowServiceStubs(); + WorkflowClient client = TemporalClient.get(); + WorkflowServiceStubs service = client.getWorkflowServiceStubs(); String query = "WorkflowType = 'moneyTransferWorkflow'"; diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java b/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java index 38f295e..2a3073b 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java @@ -19,14 +19,13 @@ package io.temporal.samples.moneytransfer; -import static io.temporal.samples.moneytransfer.TemporalClient.getWorkflowServiceStubs; - import com.google.common.base.Splitter; import com.google.protobuf.Timestamp; import io.temporal.api.filter.v1.StartTimeFilter; import io.temporal.api.filter.v1.WorkflowTypeFilter; import io.temporal.api.workflow.v1.WorkflowExecutionInfo; import io.temporal.api.workflowservice.v1.*; +import io.temporal.client.WorkflowClient; import io.temporal.samples.moneytransfer.dataclasses.WorkflowStatusObj; import io.temporal.samples.moneytransfer.web.ServerInfo; import io.temporal.serviceclient.WorkflowServiceStubs; @@ -42,7 +41,8 @@ public class TransferLister { public static List listWorkflows() throws FileNotFoundException, SSLException { - WorkflowServiceStubs service = getWorkflowServiceStubs(); + WorkflowClient client = TemporalClient.get(); + WorkflowServiceStubs service = client.getWorkflowServiceStubs(); // Try with minimal request first ListOpenWorkflowExecutionsResponse responseOpen; diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java b/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java index 56b37e0..c3f1c1e 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TransferRequester.java @@ -19,12 +19,9 @@ package io.temporal.samples.moneytransfer; -import static io.temporal.samples.moneytransfer.TemporalClient.getWorkflowServiceStubs; - import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest; import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse; -import io.temporal.api.workflowservice.v1.WorkflowServiceGrpc; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; import io.temporal.client.WorkflowStub; @@ -33,7 +30,6 @@ import io.temporal.samples.moneytransfer.dataclasses.StateObj; import io.temporal.samples.moneytransfer.dataclasses.WorkflowParameterObj; import io.temporal.samples.moneytransfer.web.ServerInfo; -import io.temporal.serviceclient.WorkflowServiceStubs; import java.io.FileNotFoundException; import javax.net.ssl.SSLException; @@ -142,14 +138,18 @@ private static String generateReferenceNumber() { private static String getWorkflowStatus(String workflowId) throws FileNotFoundException, SSLException { WorkflowClient client = TemporalClient.get(); - WorkflowServiceStubs service = getWorkflowServiceStubs(); - WorkflowServiceGrpc.WorkflowServiceBlockingStub stub = service.blockingStub(); + WorkflowStub workflowStub = client.newUntypedWorkflowStub(workflowId); + WorkflowExecution exec = workflowStub.getExecution(); + DescribeWorkflowExecutionRequest request = DescribeWorkflowExecutionRequest.newBuilder() .setNamespace(client.getOptions().getNamespace()) - .setExecution(WorkflowExecution.newBuilder().setWorkflowId(workflowId)) + .setExecution(exec) .build(); - DescribeWorkflowExecutionResponse response = stub.describeWorkflowExecution(request); + + DescribeWorkflowExecutionResponse response = + client.getWorkflowServiceStubs().blockingStub().describeWorkflowExecution(request); + return response.getWorkflowExecutionInfo().getStatus().name(); } } diff --git a/core/src/main/java/io/temporal/samples/moneytransfer/TransferScheduler.java b/core/src/main/java/io/temporal/samples/moneytransfer/TransferScheduler.java index 0cc5ea3..cedaca8 100644 --- a/core/src/main/java/io/temporal/samples/moneytransfer/TransferScheduler.java +++ b/core/src/main/java/io/temporal/samples/moneytransfer/TransferScheduler.java @@ -19,9 +19,8 @@ package io.temporal.samples.moneytransfer; -import static io.temporal.samples.moneytransfer.TemporalClient.getWorkflowServiceStubs; - import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.api.enums.v1.ScheduleOverlapPolicy; import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionRequest; import io.temporal.api.workflowservice.v1.DescribeWorkflowExecutionResponse; import io.temporal.api.workflowservice.v1.WorkflowServiceGrpc; @@ -33,6 +32,8 @@ import io.temporal.samples.moneytransfer.web.ServerInfo; import io.temporal.serviceclient.WorkflowServiceStubs; import java.io.FileNotFoundException; +import java.time.Duration; +import java.util.Collections; import javax.net.ssl.SSLException; public class TransferScheduler { @@ -93,7 +94,77 @@ public static String runWorkflow(WorkflowParameterObj workflowParameterObj) public static String runSchedule(ScheduleParameterObj scheduleParameterObj) { - return "not implemented"; + String scheduleNumber = null; + try { + int amountCents = scheduleParameterObj.getAmount(); // amount to transfer + ExecutionScenarioObj executionScenarioObj = scheduleParameterObj.getScenario(); + + WorkflowParameterObj params = new WorkflowParameterObj(amountCents, executionScenarioObj); + + ScheduleClient scheduleClient = TemporalClient.getScheduleClient(); + + String referenceNumber = generateReferenceNumber(); // random reference number + scheduleNumber = referenceNumber + "-schedule"; + final String TASK_QUEUE = ServerInfo.getTaskqueue(); + + WorkflowOptions options = + WorkflowOptions.newBuilder() + .setWorkflowId(referenceNumber) + .setTaskQueue(TASK_QUEUE) + .build(); + + ScheduleActionStartWorkflow action = + ScheduleActionStartWorkflow.newBuilder() + .setWorkflowType(AccountTransferWorkflow.class) + .setArguments(params) + .setOptions(options) + .build(); + + // Define the schedule we want to create + Schedule schedule = + Schedule.newBuilder() + .setAction(action) + .setSpec(ScheduleSpec.newBuilder().build()) + .build(); + + ScheduleHandle handle = + scheduleClient.createSchedule( + scheduleNumber, schedule, ScheduleOptions.newBuilder().build()); + + // Update the schedule with a spec, so it will run periodically + handle.update( + (ScheduleUpdateInput input) -> { + Schedule.Builder builder = Schedule.newBuilder(input.getDescription().getSchedule()); + + builder.setSpec( + ScheduleSpec.newBuilder() + .setIntervals( + Collections.singletonList( + new ScheduleIntervalSpec( + Duration.ofSeconds(scheduleParameterObj.getInterval())))) + .build()); + // Make the schedule paused to demonstrate how to unpause a schedule + builder.setState( + ScheduleState.newBuilder() + .setLimitedAction(true) + .setRemainingActions(scheduleParameterObj.getCount()) + .build()); + + // Temporal's default schedule policy is 'skip' + builder.setPolicy( + SchedulePolicy.newBuilder() + .setOverlap(ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_SKIP) + .build()); + + return new ScheduleUpdate(builder.build()); + }); + + // Unpause schedule + // handle.unpause(); + } catch (Exception e) { + System.out.println("Exception: " + e); + } + return scheduleNumber; } @SuppressWarnings("CatchAndPrintStackTrace") @@ -122,7 +193,8 @@ private static String generateReferenceNumber() { private static String getWorkflowStatus(String workflowId) throws FileNotFoundException, SSLException { - WorkflowServiceStubs service = getWorkflowServiceStubs(); + WorkflowClient client = TemporalClient.get(); + WorkflowServiceStubs service = client.getWorkflowServiceStubs(); WorkflowServiceGrpc.WorkflowServiceBlockingStub stub = service.blockingStub(); DescribeWorkflowExecutionRequest request = DescribeWorkflowExecutionRequest.newBuilder()