diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9e05940..f40387cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,3 +94,4 @@ jobs: python-repo-path: ${{github.event.pull_request.head.repo.full_name}} version: ${{github.event.pull_request.head.ref}} version-is-repo-ref: true + features-repo-ref: http-connect-proxy-python diff --git a/temporalio/bridge/Cargo.lock b/temporalio/bridge/Cargo.lock index dfc9387c..dcb79889 100644 --- a/temporalio/bridge/Cargo.lock +++ b/temporalio/bridge/Cargo.lock @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.6" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -2414,11 +2414,13 @@ dependencies = [ "anyhow", "async-trait", "backoff", + "base64", "derive_builder", "derive_more", "futures", "futures-retry", "http 0.2.11", + "hyper 0.14.28", "once_cell", "parking_lot", "prost-types", diff --git a/temporalio/bridge/client.py b/temporalio/bridge/client.py index 16d7b50d..4cfc3067 100644 --- a/temporalio/bridge/client.py +++ b/temporalio/bridge/client.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import timedelta -from typing import Mapping, Optional, Type, TypeVar +from typing import Mapping, Optional, Tuple, Type, TypeVar import google.protobuf.message @@ -46,6 +46,14 @@ class ClientKeepAliveConfig: timeout_millis: int +@dataclass +class ClientHttpConnectProxyConfig: + """Python representation of the Rust struct for configuring HTTP proxy.""" + + target_host: str + basic_auth: Optional[Tuple[str, str]] + + @dataclass class ClientConfig: """Python representation of the Rust struct for configuring the client.""" @@ -59,6 +67,7 @@ class ClientConfig: keep_alive_config: Optional[ClientKeepAliveConfig] client_name: str client_version: str + http_connect_proxy_config: Optional[ClientHttpConnectProxyConfig] @dataclass diff --git a/temporalio/bridge/sdk-core b/temporalio/bridge/sdk-core index 00b55079..409e74ec 160000 --- a/temporalio/bridge/sdk-core +++ b/temporalio/bridge/sdk-core @@ -1 +1 @@ -Subproject commit 00b550794d413274b829eed269312ce56c52abde +Subproject commit 409e74ec8e80ae4c1f9043e8b413b1371b65f946 diff --git a/temporalio/bridge/src/client.rs b/temporalio/bridge/src/client.rs index 995f0003..f6f9bc3f 100644 --- a/temporalio/bridge/src/client.rs +++ b/temporalio/bridge/src/client.rs @@ -6,7 +6,7 @@ use std::time::Duration; use temporal_client::{ ClientKeepAliveConfig as CoreClientKeepAliveConfig, ClientOptions, ClientOptionsBuilder, ConfiguredClient, HealthService, OperatorService, RetryClient, RetryConfig, - TemporalServiceClientWithMetrics, TestService, TlsConfig, WorkflowService, + TemporalServiceClientWithMetrics, TestService, TlsConfig, WorkflowService, HttpConnectProxyOptions, }; use tonic::metadata::MetadataKey; use url::Url; @@ -34,6 +34,7 @@ pub struct ClientConfig { tls_config: Option, retry_config: Option, keep_alive_config: Option, + http_connect_proxy_config: Option, } #[derive(FromPyObject)] @@ -60,6 +61,12 @@ struct ClientKeepAliveConfig { pub timeout_millis: u64, } +#[derive(FromPyObject)] +struct ClientHttpConnectProxyConfig { + pub target_host: String, + pub basic_auth: Option<(String, String)>, +} + #[derive(FromPyObject)] struct RpcCall { rpc: String, @@ -392,6 +399,7 @@ impl TryFrom for ClientOptions { .map_or(RetryConfig::default(), |c| c.into()), ) .keep_alive(opts.keep_alive_config.map(Into::into)) + .http_connect_proxy(opts.http_connect_proxy_config.map(Into::into)) .headers(Some(opts.metadata)) .api_key(opts.api_key); // Builder does not allow us to set option here, so we have to make @@ -451,3 +459,12 @@ impl From for CoreClientKeepAliveConfig { } } } + +impl From for HttpConnectProxyOptions { + fn from(conf: ClientHttpConnectProxyConfig) -> Self { + HttpConnectProxyOptions { + target_addr: conf.target_host, + basic_auth: conf.basic_auth, + } + } +} \ No newline at end of file diff --git a/temporalio/client.py b/temporalio/client.py index 001f29f9..ce16feb5 100644 --- a/temporalio/client.py +++ b/temporalio/client.py @@ -54,6 +54,7 @@ import temporalio.service import temporalio.workflow from temporalio.service import ( + HttpConnectProxyConfig, KeepAliveConfig, RetryConfig, RPCError, @@ -110,6 +111,7 @@ async def connect( identity: Optional[str] = None, lazy: bool = False, runtime: Optional[temporalio.runtime.Runtime] = None, + http_connect_proxy_config: Optional[HttpConnectProxyConfig] = None, ) -> Client: """Connect to a Temporal server. @@ -153,6 +155,7 @@ async def connect( attempted or a worker is created with it. Lazy clients cannot be used for workers. runtime: The runtime for this client, or the default if unset. + http_connect_proxy_config: Configuration for HTTP CONNECT proxy. """ connect_config = temporalio.service.ConnectConfig( target_host=target_host, @@ -164,6 +167,7 @@ async def connect( identity=identity or "", lazy=lazy, runtime=runtime, + http_connect_proxy_config=http_connect_proxy_config, ) return Client( await temporalio.service.ServiceClient.connect(connect_config), diff --git a/temporalio/service.py b/temporalio/service.py index 6969ab6e..1aeef367 100644 --- a/temporalio/service.py +++ b/temporalio/service.py @@ -11,7 +11,7 @@ from dataclasses import dataclass, field from datetime import timedelta from enum import IntEnum -from typing import ClassVar, Generic, Mapping, Optional, Type, TypeVar, Union +from typing import ClassVar, Generic, Mapping, Optional, Tuple, Type, TypeVar, Union import google.protobuf.empty_pb2 import google.protobuf.message @@ -115,6 +115,24 @@ def _to_bridge_config(self) -> temporalio.bridge.client.ClientKeepAliveConfig: KeepAliveConfig.default = KeepAliveConfig() +@dataclass(frozen=True) +class HttpConnectProxyConfig: + """Configuration for HTTP CONNECT proxy for client connections.""" + + target_host: str + """Target host:port for the HTTP CONNECT proxy.""" + basic_auth: Optional[Tuple[str, str]] = None + """Basic auth for the HTTP CONNECT proxy if any as a user/pass tuple.""" + + def _to_bridge_config( + self, + ) -> temporalio.bridge.client.ClientHttpConnectProxyConfig: + return temporalio.bridge.client.ClientHttpConnectProxyConfig( + target_host=self.target_host, + basic_auth=self.basic_auth, + ) + + @dataclass class ConnectConfig: """Config for connecting to the server.""" @@ -128,6 +146,7 @@ class ConnectConfig: identity: str = "" lazy: bool = False runtime: Optional[temporalio.runtime.Runtime] = None + http_connect_proxy_config: Optional[HttpConnectProxyConfig] = None def __post_init__(self) -> None: """Set extra defaults on unset properties.""" @@ -174,6 +193,9 @@ def _to_bridge_config(self) -> temporalio.bridge.client.ClientConfig: identity=self.identity, client_name="temporal-python", client_version=__version__, + http_connect_proxy_config=self.http_connect_proxy_config._to_bridge_config() + if self.http_connect_proxy_config + else None, )