@@ -2,18 +2,16 @@ package cliext
22
33import (
44 "context"
5- "crypto/tls"
6- "crypto/x509"
75 "fmt"
86 "log/slog"
97 "net/http"
10- "os"
118 "strings"
129
1310 "go.temporal.io/sdk/client"
1411 "go.temporal.io/sdk/contrib/envconfig"
1512 "go.temporal.io/sdk/converter"
1613 "go.temporal.io/sdk/log"
14+ "golang.org/x/oauth2"
1715 "google.golang.org/grpc"
1816)
1917
@@ -29,14 +27,14 @@ type ClientOptionsBuilder struct {
2927 // Logger is the slog logger to use for the client. If set, it will be
3028 // wrapped with the SDK's structured logger adapter.
3129 Logger * slog.Logger
30+ // oauthTokenSource is initialized during Build() if OAuth is configured.
31+ oauthTokenSource oauth2.TokenSource
3232}
3333
34- // BuildClientOptions creates SDK client options from a ClientOptionsBuilder.
35- // If OAuth is configured and no APIKey is set, OAuth will be used to obtain an access token.
36- // Returns the client options and the resolved namespace (which may differ from input if loaded from profile).
37- func BuildClientOptions (ctx context.Context , opts ClientOptionsBuilder ) (client.Options , string , error ) {
38- cfg := opts .ClientOptions
39- common := opts .CommonOptions
34+ // Build creates SDK client.Options from the ClientOptionsBuilder options.
35+ func (b * ClientOptionsBuilder ) Build (ctx context.Context ) (client.Options , error ) {
36+ cfg := b .ClientOptions
37+ common := b .CommonOptions
4038
4139 // Load a client config profile if configured
4240 var profile envconfig.ClientConfigProfile
@@ -47,22 +45,22 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
4745 ConfigFileProfile : common .Profile ,
4846 DisableFile : common .DisableConfigFile ,
4947 DisableEnv : common .DisableConfigEnv ,
50- EnvLookup : opts .EnvLookup ,
48+ EnvLookup : b .EnvLookup ,
5149 })
5250 if err != nil {
53- return client.Options {}, "" , fmt .Errorf ("failed loading client config: %w" , err )
51+ return client.Options {}, fmt .Errorf ("failed loading client config: %w" , err )
5452 }
5553 }
5654
5755 // To support legacy TLS environment variables, if they are present, we will
5856 // have them force-override anything loaded from existing file or env
59- if ! common .DisableConfigEnv && opts .EnvLookup != nil {
60- oldEnvTLSCert , _ := opts .EnvLookup .LookupEnv ("TEMPORAL_TLS_CERT" )
61- oldEnvTLSCertData , _ := opts .EnvLookup .LookupEnv ("TEMPORAL_TLS_CERT_DATA" )
62- oldEnvTLSKey , _ := opts .EnvLookup .LookupEnv ("TEMPORAL_TLS_KEY" )
63- oldEnvTLSKeyData , _ := opts .EnvLookup .LookupEnv ("TEMPORAL_TLS_KEY_DATA" )
64- oldEnvTLSCA , _ := opts .EnvLookup .LookupEnv ("TEMPORAL_TLS_CA" )
65- oldEnvTLSCAData , _ := opts .EnvLookup .LookupEnv ("TEMPORAL_TLS_CA_DATA" )
57+ if ! common .DisableConfigEnv && b .EnvLookup != nil {
58+ oldEnvTLSCert , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_CERT" )
59+ oldEnvTLSCertData , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_CERT_DATA" )
60+ oldEnvTLSKey , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_KEY" )
61+ oldEnvTLSKeyData , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_KEY_DATA" )
62+ oldEnvTLSCA , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_CA" )
63+ oldEnvTLSCAData , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_CA_DATA" )
6664 if oldEnvTLSCert != "" || oldEnvTLSCertData != "" ||
6765 oldEnvTLSKey != "" || oldEnvTLSKeyData != "" ||
6866 oldEnvTLSCA != "" || oldEnvTLSCAData != "" {
@@ -96,13 +94,10 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
9694 if cfg .FlagSet != nil && cfg .FlagSet .Changed ("address" ) {
9795 profile .Address = cfg .Address
9896 }
99- resolvedNamespace := profile .Namespace
10097 if cfg .FlagSet != nil && cfg .FlagSet .Changed ("namespace" ) {
10198 profile .Namespace = cfg .Namespace
102- resolvedNamespace = cfg .Namespace
10399 } else if profile .Namespace == "" {
104100 profile .Namespace = cfg .Namespace
105- resolvedNamespace = cfg .Namespace
106101 }
107102
108103 // Set API key on profile if provided (OAuth credentials are set later on clientOpts)
@@ -114,7 +109,7 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
114109 if len (cfg .GrpcMeta ) > 0 {
115110 grpcMetaFromArg , err := parseKeyValuePairs (cfg .GrpcMeta )
116111 if err != nil {
117- return client.Options {}, "" , fmt .Errorf ("invalid gRPC meta: %w" , err )
112+ return client.Options {}, fmt .Errorf ("invalid gRPC meta: %w" , err )
118113 }
119114 if len (profile .GRPCMeta ) == 0 {
120115 profile .GRPCMeta = make (map [string ]string , len (cfg .GrpcMeta ))
@@ -125,6 +120,8 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
125120 }
126121
127122 // If any of these TLS values are present, set TLS if not set, and set values.
123+ // NOTE: This means that tls=false does not explicitly disable TLS when set
124+ // via envconfig.
128125 if cfg .Tls ||
129126 cfg .TlsCertPath != "" || cfg .TlsKeyPath != "" || cfg .TlsCaPath != "" ||
130127 cfg .TlsCertData != "" || cfg .TlsKeyData != "" || cfg .TlsCaData != "" {
@@ -178,7 +175,12 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
178175 // Convert profile to client options.
179176 clientOpts , err := profile .ToClientOptions (envconfig.ToClientOptionsRequest {})
180177 if err != nil {
181- return client.Options {}, "" , fmt .Errorf ("failed to build client options: %w" , err )
178+ return client.Options {}, fmt .Errorf ("failed to build client options: %w" , err )
179+ }
180+
181+ // Set client authority if provided.
182+ if cfg .ClientAuthority != "" {
183+ clientOpts .ConnectionOptions .Authority = cfg .ClientAuthority
182184 }
183185
184186 // Set identity if provided.
@@ -187,43 +189,42 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
187189 }
188190
189191 // Set logger if provided.
190- if opts .Logger != nil {
191- clientOpts .Logger = log .NewStructuredLogger (opts .Logger )
192+ if b .Logger != nil {
193+ clientOpts .Logger = log .NewStructuredLogger (b .Logger )
192194 }
193195
194- // Set OAuth credentials if configured and no API key is set.
195- // OAuth config is loaded on-demand from the config file.
196+ // Initialize OAuth token source if no API key is set.
196197 if cfg .ApiKey == "" {
197- clientOpts .Credentials = client .NewAPIKeyDynamicCredentials (
198- NewOAuthDynamicTokenProvider (opts ))
199- }
200-
201- // Set client authority if provided.
202- if cfg .ClientAuthority != "" {
203- clientOpts .ConnectionOptions .Authority = cfg .ClientAuthority
204- }
205-
206- // Set connect timeout for GetSystemInfo if provided.
207- if common .ClientConnectTimeout != 0 {
208- clientOpts .ConnectionOptions .GetSystemInfoTimeout = common .ClientConnectTimeout .Duration ()
198+ if err := b .initOAuthTokenSource (ctx ); err != nil {
199+ return client.Options {}, fmt .Errorf ("failed to initialize OAuth: %w" , err )
200+ }
201+ // Only set credentials if OAuth is configured
202+ if b .oauthTokenSource != nil {
203+ clientOpts .Credentials = client .NewAPIKeyDynamicCredentials (b .getOAuthToken )
204+ }
209205 }
210206
211- // Add codec interceptor if codec endpoint is configured.
207+ // Remote codec
212208 if profile .Codec != nil && profile .Codec .Endpoint != "" {
213209 codecHeaders , err := parseKeyValuePairs (cfg .CodecHeader )
214210 if err != nil {
215- return client.Options {}, "" , fmt .Errorf ("invalid codec headers: %w" , err )
211+ return client.Options {}, fmt .Errorf ("invalid codec headers: %w" , err )
216212 }
217213 interceptor , err := newPayloadCodecInterceptor (
218214 profile .Namespace , profile .Codec .Endpoint , profile .Codec .Auth , codecHeaders )
219215 if err != nil {
220- return client.Options {}, "" , fmt .Errorf ("failed creating payload codec interceptor: %w" , err )
216+ return client.Options {}, fmt .Errorf ("failed creating payload codec interceptor: %w" , err )
221217 }
222218 clientOpts .ConnectionOptions .DialOptions = append (
223219 clientOpts .ConnectionOptions .DialOptions , grpc .WithChainUnaryInterceptor (interceptor ))
224220 }
225221
226- return clientOpts , resolvedNamespace , nil
222+ // Set connect timeout for GetSystemInfo if provided.
223+ if common .ClientConnectTimeout != 0 {
224+ clientOpts .ConnectionOptions .GetSystemInfoTimeout = common .ClientConnectTimeout .Duration ()
225+ }
226+
227+ return clientOpts , nil
227228}
228229
229230// parseKeyValuePairs parses a slice of "KEY=VALUE" strings into a map.
@@ -270,52 +271,36 @@ func newPayloadCodecInterceptor(
270271 )
271272}
272273
273- // BuildTLSConfig creates a TLS configuration from the ClientOptions TLS settings .
274- // This is useful when you need the TLS config separately from the full client options .
275- func BuildTLSConfig ( cfg ClientOptions ) ( * tls. Config , error ) {
276- if ! cfg . Tls && cfg . TlsCertPath == "" && cfg . TlsKeyPath == "" && cfg . TlsCaPath == "" &&
277- cfg . TlsCertData == "" && cfg . TlsKeyData == "" && cfg . TlsCaData == "" {
278- return nil , nil
274+ // getOAuthToken returns a valid OAuth access token from the builder's configuration .
275+ // It uses the oauth2.TokenSource to automatically refresh the token when needed .
276+ // Returns empty string if no OAuth is configured.
277+ func ( b * ClientOptionsBuilder ) getOAuthToken ( ctx context. Context ) ( string , error ) {
278+ if b . oauthTokenSource == nil {
279+ return "" , nil
279280 }
280-
281- tlsConfig := & tls.Config {
282- ServerName : cfg .TlsServerName ,
283- InsecureSkipVerify : cfg .TlsDisableHostVerification ,
281+ token , err := b .oauthTokenSource .Token ()
282+ if err != nil {
283+ return "" , err
284284 }
285+ return token .AccessToken , nil
286+ }
285287
286- // Load client certificate.
287- if cfg .TlsCertPath != "" && cfg .TlsKeyPath != "" {
288- cert , err := tls .LoadX509KeyPair (cfg .TlsCertPath , cfg .TlsKeyPath )
289- if err != nil {
290- return nil , fmt .Errorf ("failed to load client certificate: %w" , err )
291- }
292- tlsConfig .Certificates = []tls.Certificate {cert }
293- } else if cfg .TlsCertData != "" && cfg .TlsKeyData != "" {
294- cert , err := tls .X509KeyPair ([]byte (cfg .TlsCertData ), []byte (cfg .TlsKeyData ))
295- if err != nil {
296- return nil , fmt .Errorf ("failed to parse client certificate: %w" , err )
297- }
298- tlsConfig .Certificates = []tls.Certificate {cert }
288+ // initOAuthTokenSource initializes the OAuth token source from the configuration.
289+ // This should be called once during Build() to load the OAuth config and create
290+ // the token source. The token source is then bound to the builder and used for
291+ // all subsequent token requests.
292+ func (b * ClientOptionsBuilder ) initOAuthTokenSource (ctx context.Context ) error {
293+ result , err := LoadClientOAuth (LoadClientOAuthOptions {
294+ ConfigFilePath : b .CommonOptions .ConfigFile ,
295+ ProfileName : b .CommonOptions .Profile ,
296+ EnvLookup : b .EnvLookup ,
297+ })
298+ if err != nil {
299+ return err
299300 }
300301
301- // Load CA certificate.
302- if cfg .TlsCaPath != "" || cfg .TlsCaData != "" {
303- pool := x509 .NewCertPool ()
304- var caData []byte
305- if cfg .TlsCaPath != "" {
306- var err error
307- caData , err = os .ReadFile (cfg .TlsCaPath )
308- if err != nil {
309- return nil , fmt .Errorf ("failed to read CA certificate: %w" , err )
310- }
311- } else {
312- caData = []byte (cfg .TlsCaData )
313- }
314- if ! pool .AppendCertsFromPEM (caData ) {
315- return nil , fmt .Errorf ("failed to parse CA certificate" )
316- }
317- tlsConfig .RootCAs = pool
302+ if result .OAuth != nil && result .OAuth .RefreshToken != "" {
303+ b .oauthTokenSource = result .OAuth .newTokenSource (ctx )
318304 }
319-
320- return tlsConfig , nil
305+ return nil
321306}
0 commit comments