diff --git a/README.md b/README.md index 48d9f4d..6853e0a 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. @@ -34,6 +60,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 d3cb0a3..7c915ed 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ subprojects { ext { otelVersion = '1.26.0' otelVersionAlpha = "${otelVersion}-alpha" - javaSDKVersion = '1.25.1' + javaSDKVersion = '1.29.0' jarVersion = '1.0.0' } 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/TemporalClient.java b/core/src/main/java/io/temporal/samples/moneytransfer/TemporalClient.java index a49b0fe..ba8d16c 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; @@ -37,48 +37,86 @@ import javax.net.ssl.SSLException; public class TemporalClient { - public static WorkflowServiceStubs getWorkflowServiceStubs() + + /** + * Centralized method to create and configure WorkflowServiceStubs. This is the single source of + * 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() throws FileNotFoundException, SSLException { - WorkflowServiceStubsOptions.Builder workflowServiceStubsOptionsBuilder = - WorkflowServiceStubsOptions.newBuilder(); + String endpoint = ServerInfo.getAddress(); + String namespace = ServerInfo.getNamespace(); + String apiKey = ServerInfo.getApiKey(); + String certPath = ServerInfo.getCertPath(); + String keyPath = ServerInfo.getKeyPath(); - if (!ServerInfo.getCertPath().equals("") && !"".equals(ServerInfo.getKeyPath())) { - InputStream clientCert = new FileInputStream(ServerInfo.getCertPath()); + System.out.println("TEMPORAL_ADDRESS: " + endpoint); + System.out.println("TEMPORAL_NAMESPACE: " + namespace); - InputStream clientKey = new FileInputStream(ServerInfo.getKeyPath()); + WorkflowServiceStubsOptions.Builder optionsBuilder = + WorkflowServiceStubsOptions.newBuilder().setTarget(endpoint); - workflowServiceStubsOptionsBuilder.setSslContext( - SimpleSslContextBuilder.forPKCS8(clientCert, clientKey).build()); - } + // 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("---------------------------------"); - // For temporal cloud this would likely be ${namespace}.tmprl.cloud:7233 - String targetEndpoint = ServerInfo.getAddress(); - // Your registered namespace. + InputStream clientCert = new FileInputStream(certPath); + InputStream clientKey = new FileInputStream(keyPath); - workflowServiceStubsOptionsBuilder.setTarget(targetEndpoint); - WorkflowServiceStubs service = null; + 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( + optionsBuilder.setEnableHttps(true).addApiKey(() -> apiKey).build()); - if (!ServerInfo.getAddress().equals("localhost:7233")) { - // if not local server, then use the workflowServiceStubsOptionsBuilder - service = WorkflowServiceStubs.newServiceStubs(workflowServiceStubsOptionsBuilder.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 = createWorkflowServiceStubs(); - WorkflowServiceStubs service = getWorkflowServiceStubs(); + String namespace = ServerInfo.getNamespace(); + if (namespace == null || namespace.isEmpty()) { + namespace = "default"; + } WorkflowClientOptions.Builder builder = WorkflowClientOptions.newBuilder(); - // if environment variable ENCRYPT_PAYLOADS is set to true, then use CryptCodec + // 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( @@ -89,38 +127,26 @@ public static WorkflowClient get() throws FileNotFoundException, SSLException { } System.out.println("<<<>>>:\n " + ServerInfo.getServerInfo()); - WorkflowClientOptions clientOptions = builder.setNamespace(ServerInfo.getNamespace()).build(); + WorkflowClientOptions clientOptions = builder.setNamespace(namespace).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(); + WorkflowServiceStubs service = createWorkflowServiceStubs(); - // 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 */)); + String namespace = ServerInfo.getNamespace(); + 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/TransferLister.java b/core/src/main/java/io/temporal/samples/moneytransfer/TransferLister.java index d3a4703..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,18 +41,23 @@ 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()); + WorkflowClient client = TemporalClient.get(); + WorkflowServiceStubs service = client.getWorkflowServiceStubs(); + + // 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 5f50a69..8e02eeb 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; @@ -109,6 +105,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 = @@ -136,14 +141,19 @@ private static String generateReferenceNumber() { private static String getWorkflowStatus(String workflowId) throws FileNotFoundException, SSLException { - WorkflowServiceStubs service = getWorkflowServiceStubs(); - WorkflowServiceGrpc.WorkflowServiceBlockingStub stub = service.blockingStub(); + WorkflowClient client = TemporalClient.get(); + WorkflowStub workflowStub = client.newUntypedWorkflowStub(workflowId); + WorkflowExecution exec = workflowStub.getExecution(); + DescribeWorkflowExecutionRequest request = DescribeWorkflowExecutionRequest.newBuilder() - .setNamespace(ServerInfo.getNamespace()) - .setExecution(WorkflowExecution.newBuilder().setWorkflowId(workflowId)) + .setNamespace(client.getOptions().getNamespace()) + .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 8920564..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,6 @@ 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; @@ -104,7 +101,7 @@ public static String runSchedule(ScheduleParameterObj scheduleParameterObj) { WorkflowParameterObj params = new WorkflowParameterObj(amountCents, executionScenarioObj); - ScheduleClient scheduleClient = getScheduleClient(); + ScheduleClient scheduleClient = TemporalClient.getScheduleClient(); String referenceNumber = generateReferenceNumber(); // random reference number scheduleNumber = referenceNumber + "-schedule"; @@ -196,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() 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;