@@ -2,12 +2,9 @@ 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"
@@ -17,7 +14,7 @@ import (
1714 "google.golang.org/grpc"
1815)
1916
20- // ClientOptionsBuilder contains options for building Temporal client options.
17+ // ClientOptionsBuilder contains options for building SDK client.Options
2118type ClientOptionsBuilder struct {
2219 // CommonOptions contains common CLI options including profile config.
2320 CommonOptions CommonOptions
@@ -29,14 +26,14 @@ type ClientOptionsBuilder struct {
2926 // Logger is the slog logger to use for the client. If set, it will be
3027 // wrapped with the SDK's structured logger adapter.
3128 Logger * slog.Logger
29+ // oauthConfig is initialized during Build() if OAuth is configured.
30+ oauthConfig * OAuthConfig
3231}
3332
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
33+ // Build creates SDK client.Options
34+ func (b * ClientOptionsBuilder ) Build (ctx context.Context ) (client.Options , error ) {
35+ cfg := b .ClientOptions
36+ common := b .CommonOptions
4037
4138 // Load a client config profile if configured
4239 var profile envconfig.ClientConfigProfile
@@ -47,22 +44,22 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
4744 ConfigFileProfile : common .Profile ,
4845 DisableFile : common .DisableConfigFile ,
4946 DisableEnv : common .DisableConfigEnv ,
50- EnvLookup : opts .EnvLookup ,
47+ EnvLookup : b .EnvLookup ,
5148 })
5249 if err != nil {
53- return client.Options {}, "" , fmt .Errorf ("failed loading client config: %w" , err )
50+ return client.Options {}, fmt .Errorf ("failed loading client config: %w" , err )
5451 }
5552 }
5653
5754 // To support legacy TLS environment variables, if they are present, we will
5855 // 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" )
56+ if ! common .DisableConfigEnv && b .EnvLookup != nil {
57+ oldEnvTLSCert , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_CERT" )
58+ oldEnvTLSCertData , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_CERT_DATA" )
59+ oldEnvTLSKey , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_KEY" )
60+ oldEnvTLSKeyData , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_KEY_DATA" )
61+ oldEnvTLSCA , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_CA" )
62+ oldEnvTLSCAData , _ := b .EnvLookup .LookupEnv ("TEMPORAL_TLS_CA_DATA" )
6663 if oldEnvTLSCert != "" || oldEnvTLSCertData != "" ||
6764 oldEnvTLSKey != "" || oldEnvTLSKeyData != "" ||
6865 oldEnvTLSCA != "" || oldEnvTLSCAData != "" {
@@ -96,13 +93,10 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
9693 if cfg .FlagSet != nil && cfg .FlagSet .Changed ("address" ) {
9794 profile .Address = cfg .Address
9895 }
99- resolvedNamespace := profile .Namespace
10096 if cfg .FlagSet != nil && cfg .FlagSet .Changed ("namespace" ) {
10197 profile .Namespace = cfg .Namespace
102- resolvedNamespace = cfg .Namespace
10398 } else if profile .Namespace == "" {
10499 profile .Namespace = cfg .Namespace
105- resolvedNamespace = cfg .Namespace
106100 }
107101
108102 // Set API key on profile if provided (OAuth credentials are set later on clientOpts)
@@ -114,7 +108,7 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
114108 if len (cfg .GrpcMeta ) > 0 {
115109 grpcMetaFromArg , err := parseKeyValuePairs (cfg .GrpcMeta )
116110 if err != nil {
117- return client.Options {}, "" , fmt .Errorf ("invalid gRPC meta: %w" , err )
111+ return client.Options {}, fmt .Errorf ("invalid gRPC meta: %w" , err )
118112 }
119113 if len (profile .GRPCMeta ) == 0 {
120114 profile .GRPCMeta = make (map [string ]string , len (cfg .GrpcMeta ))
@@ -125,6 +119,8 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
125119 }
126120
127121 // If any of these TLS values are present, set TLS if not set, and set values.
122+ // NOTE: This means that tls=false does not explicitly disable TLS when set
123+ // via envconfig.
128124 if cfg .Tls ||
129125 cfg .TlsCertPath != "" || cfg .TlsKeyPath != "" || cfg .TlsCaPath != "" ||
130126 cfg .TlsCertData != "" || cfg .TlsKeyData != "" || cfg .TlsCaData != "" {
@@ -178,7 +174,12 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
178174 // Convert profile to client options.
179175 clientOpts , err := profile .ToClientOptions (envconfig.ToClientOptionsRequest {})
180176 if err != nil {
181- return client.Options {}, "" , fmt .Errorf ("failed to build client options: %w" , err )
177+ return client.Options {}, fmt .Errorf ("failed to build client options: %w" , err )
178+ }
179+
180+ // Set client authority if provided.
181+ if cfg .ClientAuthority != "" {
182+ clientOpts .ConnectionOptions .Authority = cfg .ClientAuthority
182183 }
183184
184185 // Set identity if provided.
@@ -187,43 +188,48 @@ func BuildClientOptions(ctx context.Context, opts ClientOptionsBuilder) (client.
187188 }
188189
189190 // Set logger if provided.
190- if opts .Logger != nil {
191- clientOpts .Logger = log .NewStructuredLogger (opts .Logger )
191+ if b .Logger != nil {
192+ clientOpts .Logger = log .NewStructuredLogger (b .Logger )
192193 }
193194
194- // Set OAuth credentials if configured and no API key is set.
195- // OAuth config is loaded on-demand from the config file.
195+ // Attempt to configure OAuth config if no API key is set.
196196 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 ()
197+ result , err := LoadClientOAuth (LoadClientOAuthOptions {
198+ ConfigFilePath : common .ConfigFile ,
199+ ProfileName : common .Profile ,
200+ EnvLookup : b .EnvLookup ,
201+ })
202+ if err != nil {
203+ return client.Options {}, fmt .Errorf ("failed to load OAuth config: %w" , err )
204+ }
205+ // Only set credentials if OAuth is configured with an access token
206+ if result .OAuth != nil && result .OAuth .AccessToken != "" {
207+ b .oauthConfig = result .OAuth
208+ clientOpts .Credentials = client .NewAPIKeyDynamicCredentials (b .getOAuthToken )
209+ }
209210 }
210211
211- // Add codec interceptor if codec endpoint is configured.
212+ // Remote codec
212213 if profile .Codec != nil && profile .Codec .Endpoint != "" {
213214 codecHeaders , err := parseKeyValuePairs (cfg .CodecHeader )
214215 if err != nil {
215- return client.Options {}, "" , fmt .Errorf ("invalid codec headers: %w" , err )
216+ return client.Options {}, fmt .Errorf ("invalid codec headers: %w" , err )
216217 }
217218 interceptor , err := newPayloadCodecInterceptor (
218219 profile .Namespace , profile .Codec .Endpoint , profile .Codec .Auth , codecHeaders )
219220 if err != nil {
220- return client.Options {}, "" , fmt .Errorf ("failed creating payload codec interceptor: %w" , err )
221+ return client.Options {}, fmt .Errorf ("failed creating payload codec interceptor: %w" , err )
221222 }
222223 clientOpts .ConnectionOptions .DialOptions = append (
223224 clientOpts .ConnectionOptions .DialOptions , grpc .WithChainUnaryInterceptor (interceptor ))
224225 }
225226
226- return clientOpts , resolvedNamespace , nil
227+ // Set connect timeout for GetSystemInfo if provided.
228+ if common .ClientConnectTimeout != 0 {
229+ clientOpts .ConnectionOptions .GetSystemInfoTimeout = common .ClientConnectTimeout .Duration ()
230+ }
231+
232+ return clientOpts , nil
227233}
228234
229235// parseKeyValuePairs parses a slice of "KEY=VALUE" strings into a map.
@@ -270,52 +276,13 @@ func newPayloadCodecInterceptor(
270276 )
271277}
272278
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
279- }
280-
281- tlsConfig := & tls.Config {
282- ServerName : cfg .TlsServerName ,
283- InsecureSkipVerify : cfg .TlsDisableHostVerification ,
284- }
285-
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 }
299- }
300-
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
279+ // getOAuthToken returns a valid OAuth access token from the builder's configuration.
280+ // It uses oauth2.TokenSource to automatically refresh the token when needed.
281+ func (b * ClientOptionsBuilder ) getOAuthToken (ctx context.Context ) (string , error ) {
282+ tokenSource := b .oauthConfig .newTokenSource (ctx )
283+ token , err := tokenSource .Token ()
284+ if err != nil {
285+ return "" , err
318286 }
319-
320- return tlsConfig , nil
287+ return token .AccessToken , nil
321288}
0 commit comments