diff --git a/README.md b/README.md index 15e12b8e2..4d00e320b 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,9 @@ See the README.md file in each main sample directory for cut/paste Gradle comman - [**Worker Versioning**](/core/src/main/java/io/temporal/samples/workerversioning): Demonstrates how to use worker versioning to manage workflow code changes. +- [**Environment Configuration**](/core/src/main/java/io/temporal/samples/envconfig): +Load client configuration from TOML files with programmatic overrides. + #### API demonstrations - [**Async Untyped Child Workflow**](/core/src/main/java/io/temporal/samples/asyncuntypedchild): Demonstrates how to invoke an untyped child workflow async, that can complete after parent workflow is already completed. diff --git a/core/build.gradle b/core/build.gradle index 42ad4be2b..62fbfa8e9 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -4,6 +4,9 @@ dependencies { implementation "io.temporal:temporal-opentracing:$javaSDKVersion" testImplementation("io.temporal:temporal-testing:$javaSDKVersion") + // Environment configuration + implementation "io.temporal:temporal-envconfig:$javaSDKVersion" + // Needed for SDK related functionality implementation(platform("com.fasterxml.jackson:jackson-bom:2.17.2")) implementation "com.fasterxml.jackson.core:jackson-databind" diff --git a/core/src/main/java/io/temporal/samples/envconfig/LoadFromFile.java b/core/src/main/java/io/temporal/samples/envconfig/LoadFromFile.java new file mode 100644 index 000000000..b6a9f7187 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/envconfig/LoadFromFile.java @@ -0,0 +1,79 @@ +package io.temporal.samples.envconfig; + +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowClientOptions; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.envconfig.LoadClientConfigProfileOptions; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import java.nio.file.Paths; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This sample demonstrates loading the default environment configuration profile from a TOML file. + */ +public class LoadFromFile { + + private static final Logger logger = LoggerFactory.getLogger(LoadFromFile.class); + + public static void main(String[] args) { + try { + // For this sample to be self-contained, we explicitly provide the path to + // the config.toml file included in this directory. + // By default though, the config.toml file will be loaded from + // ~/.config/temporal/temporal.toml (or the equivalent standard config directory on your OS). + String configFilePath = + Paths.get(LoadFromFile.class.getResource("/config.toml").toURI()).toString(); + + logger.info("--- Loading 'default' profile from {} ---", configFilePath); + + // Load client profile from file. By default, this loads the "default" profile + // and applies any environment variable overrides. + ClientConfigProfile profile = + ClientConfigProfile.load( + LoadClientConfigProfileOptions.newBuilder() + .setConfigFilePath(configFilePath) + .build()); + + // Convert profile to client options (equivalent to Python's load_client_connect_config) + WorkflowServiceStubsOptions serviceStubsOptions = profile.toWorkflowServiceStubsOptions(); + WorkflowClientOptions clientOptions = profile.toWorkflowClientOptions(); + + logger.info("Loaded 'default' profile from {}", configFilePath); + logger.info(" Address: {}", serviceStubsOptions.getTarget()); + logger.info(" Namespace: {}", clientOptions.getNamespace()); + if (serviceStubsOptions.getHeaders() != null + && !serviceStubsOptions.getHeaders().keys().isEmpty()) { + logger.info(" gRPC Metadata keys: {}", serviceStubsOptions.getHeaders().keys()); + } + + logger.info("\nAttempting to connect to client..."); + + try { + // Create the workflow client using the loaded configuration + WorkflowClient client = + WorkflowClient.newInstance( + WorkflowServiceStubs.newServiceStubs(serviceStubsOptions), clientOptions); + + // Test the connection by getting system info + var systemInfo = + client + .getWorkflowServiceStubs() + .blockingStub() + .getSystemInfo( + io.temporal.api.workflowservice.v1.GetSystemInfoRequest.getDefaultInstance()); + + logger.info("✅ Client connected successfully!"); + logger.info(" Server version: {}", systemInfo.getServerVersion()); + + } catch (Exception e) { + logger.error("❌ Failed to connect: {}", e.getMessage()); + } + + } catch (Exception e) { + logger.error("Failed to load configuration: {}", e.getMessage(), e); + System.exit(1); + } + } +} diff --git a/core/src/main/java/io/temporal/samples/envconfig/LoadProfile.java b/core/src/main/java/io/temporal/samples/envconfig/LoadProfile.java new file mode 100644 index 000000000..9d05b21f3 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/envconfig/LoadProfile.java @@ -0,0 +1,87 @@ +package io.temporal.samples.envconfig; + +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowClientOptions; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.envconfig.LoadClientConfigProfileOptions; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import java.nio.file.Paths; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This sample demonstrates loading a specific profile from a TOML configuration file with + * programmatic overrides. + */ +public class LoadProfile { + + private static final Logger logger = LoggerFactory.getLogger(LoadProfile.class); + + public static void main(String[] args) { + String profileName = "staging"; + + try { + // For this sample to be self-contained, we explicitly provide the path to + // the config.toml file included in this directory. + String configFilePath = + Paths.get(LoadProfile.class.getResource("/config.toml").toURI()).toString(); + + logger.info("--- Loading '{}' profile from {} ---", profileName, configFilePath); + + // Load specific profile from file with environment variable overrides + ClientConfigProfile profile = + ClientConfigProfile.load( + LoadClientConfigProfileOptions.newBuilder() + .setConfigFilePath(configFilePath) + .setConfigFileProfile(profileName) + .build()); + + // Demonstrate programmatic override - fix the incorrect address from staging profile + logger.info("\n--- Applying programmatic override ---"); + ClientConfigProfile.Builder profileBuilder = profile.toBuilder(); + profileBuilder.setAddress("localhost:7233"); // Override the incorrect address + profile = profileBuilder.build(); + logger.info(" Overridden address to: {}", profile.getAddress()); + + // Convert profile to client options (equivalent to Python's load_client_connect_config) + WorkflowServiceStubsOptions serviceStubsOptions = profile.toWorkflowServiceStubsOptions(); + WorkflowClientOptions clientOptions = profile.toWorkflowClientOptions(); + + logger.info("Loaded '{}' profile from {}", profileName, configFilePath); + logger.info(" Address: {}", serviceStubsOptions.getTarget()); + logger.info(" Namespace: {}", clientOptions.getNamespace()); + if (serviceStubsOptions.getHeaders() != null + && !serviceStubsOptions.getHeaders().keys().isEmpty()) { + logger.info(" gRPC Metadata keys: {}", serviceStubsOptions.getHeaders().keys()); + } + + logger.info("\nAttempting to connect to client..."); + + try { + // Create the workflow client using the loaded configuration + WorkflowClient client = + WorkflowClient.newInstance( + WorkflowServiceStubs.newServiceStubs(serviceStubsOptions), clientOptions); + + // Test the connection by getting system info + var systemInfo = + client + .getWorkflowServiceStubs() + .blockingStub() + .getSystemInfo( + io.temporal.api.workflowservice.v1.GetSystemInfoRequest.getDefaultInstance()); + + logger.info("✅ Client connected successfully!"); + logger.info(" Server version: {}", systemInfo.getServerVersion()); + + } catch (Exception e) { + logger.error("❌ Failed to connect: {}", e.getMessage()); + } + + } catch (Exception e) { + logger.error("Failed to load configuration: {}", e.getMessage(), e); + System.exit(1); + } + } +} diff --git a/core/src/main/java/io/temporal/samples/envconfig/README.md b/core/src/main/java/io/temporal/samples/envconfig/README.md new file mode 100644 index 000000000..0cb5df5c1 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/envconfig/README.md @@ -0,0 +1,18 @@ +# Environment Configuration Sample + +This sample demonstrates how to configure a Temporal client using TOML configuration files. This allows you to manage connection settings across different environments without hardcoding them. + +The `config.toml` file defines three profiles: +- `[profile.default]`: Local development configuration +- `[profile.staging]`: Configuration with incorrect address to demonstrate overrides +- `[profile.prod]`: Example production configuration (not runnable) + +**Load from file (default profile):** +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.envconfig.LoadFromFile +``` + +**Load specific profile with overrides:** +```bash +./gradlew -q execute -PmainClass=io.temporal.samples.envconfig.LoadProfile +``` \ No newline at end of file diff --git a/core/src/main/resources/config.toml b/core/src/main/resources/config.toml new file mode 100644 index 000000000..81f07f786 --- /dev/null +++ b/core/src/main/resources/config.toml @@ -0,0 +1,40 @@ +# This is a sample configuration file for demonstrating Temporal's environment +# configuration feature. It defines multiple profiles for different environments, +# such as local development, production, and staging. + +# Default profile for local development +[profile.default] +address = "localhost:7233" +namespace = "default" + +# Optional: Add custom gRPC headers +[profile.default.grpc_meta] +my-custom-header = "development-value" +trace-id = "dev-trace-123" + +# Staging profile with inline certificate data +[profile.staging] +address = "localhost:9999" +namespace = "staging" + +# An example production profile for Temporal Cloud +[profile.prod] +address = "your-namespace.a1b2c.tmprl.cloud:7233" +namespace = "your-namespace" +# Replace with your actual Temporal Cloud API key +api_key = "your-api-key-here" + +# TLS configuration for production +[profile.prod.tls] +# TLS is auto-enabled when an API key is present, but you can configure it +# explicitly. +# disabled = false + +# Use certificate files for mTLS. Replace with actual paths. +client_cert_path = "/etc/temporal/certs/client.pem" +client_key_path = "/etc/temporal/certs/client.key" + +# Custom headers for production +[profile.prod.grpc_meta] +environment = "production" +service-version = "v1.2.3" \ No newline at end of file