Skip to content

Commit ede149b

Browse files
committed
polish
1 parent d8ea803 commit ede149b

File tree

17 files changed

+459
-604
lines changed

17 files changed

+459
-604
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
- name: Regen code, confirm unchanged
6161
if: ${{ matrix.checkGenCommands }}
6262
run: |
63-
go run ./cmd/gen-commands -input internal/temporalcli/commands.yaml -input cliext/option-sets.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go
63+
go run ./cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go
6464
go run ./cmd/gen-commands -input cliext/option-sets.yaml -pkg cliext > cliext/flags.gen.go
6565
git diff --exit-code
6666

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ This will expect every non-parent command to have a `run` method, so for new com
3131
Once a command is updated, the CI will automatically generate new docs
3232
and create a PR in the Documentation repo with the corresponding updates. To generate these docs locally, run:
3333

34-
go run ./cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs
34+
go run ./cmd/gen-docs -input internal/temporalcli/commands.yaml -input cliext/option-sets.yaml -output dist/docs
3535

3636
This will auto-generate a new set of docs to `dist/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`.
3737

Makefile

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@ all: gen build
44

55
gen: internal/temporalcli/commands.gen.go cliext/flags.gen.go
66

7-
# Generate CLI commands code from commands.yaml, using option sets from cliext
8-
internal/temporalcli/commands.gen.go: internal/temporalcli/commands.yaml cliext/option-sets.yaml
7+
internal/temporalcli/commands.gen.go: internal/temporalcli/commands.yaml
98
go run ./cmd/gen-commands \
109
-input internal/temporalcli/commands.yaml \
11-
-input cliext/option-sets.yaml \
1210
-pkg temporalcli \
1311
-context "*CommandContext" > $@
1412

15-
# Generate cliext flags code from option-sets.yaml (no commands = cliext mode)
1613
cliext/flags.gen.go: cliext/option-sets.yaml
1714
go run ./cmd/gen-commands \
1815
-input cliext/option-sets.yaml \

cliext/client.go

Lines changed: 70 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ package cliext
22

33
import (
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
}

cliext/client.oauth.go

Lines changed: 0 additions & 98 deletions
This file was deleted.

0 commit comments

Comments
 (0)