From 18c04684ae55ced3dfd706f332259ae2d40a6708 Mon Sep 17 00:00:00 2001 From: Gregory Mankes Date: Tue, 20 Jan 2026 13:27:56 -0500 Subject: [PATCH 1/4] add profile and config to oauth config store/loading --- temporalcloudcli/cloud.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/temporalcloudcli/cloud.go b/temporalcloudcli/cloud.go index 501fb29..56b735b 100644 --- a/temporalcloudcli/cloud.go +++ b/temporalcloudcli/cloud.go @@ -10,7 +10,10 @@ import ( ) func (c *CloudCommand) GetAPIKey(ctx context.Context) (string, error) { - loadClientOauthRes, err := cliext.LoadClientOAuth(cliext.LoadClientOAuthOptions{}) + loadClientOauthRes, err := cliext.LoadClientOAuth(cliext.LoadClientOAuthOptions{ + ConfigFilePath: c.ConfigFile, + ProfileName: c.Profile, + }) if err != nil { return "", fmt.Errorf("failed to load login configuration: %w, please run `temporal cloud login --reset`", err) } @@ -30,7 +33,9 @@ func (c *CloudCommand) GetAPIKey(ctx context.Context) (string, error) { if refreshed { loadClientOauthRes.OAuth.Token = token if err := cliext.StoreClientOAuth(cliext.StoreClientOAuthOptions{ - OAuth: loadClientOauthRes.OAuth, + OAuth: loadClientOauthRes.OAuth, + ConfigFilePath: c.ConfigFile, + ProfileName: c.Profile, }); err != nil { return "", fmt.Errorf("failed to write config file: %w", err) } From 718c3cf4e56fb6e40867e494f720c21e31525b7b Mon Sep 17 00:00:00 2001 From: Gregory Mankes Date: Tue, 20 Jan 2026 13:29:05 -0500 Subject: [PATCH 2/4] use external package to generate commands --- Makefile | 7 +- go.mod | 2 +- go.sum | 4 +- temporalcloudcli/commands.gen.go | 195 +++++-------------------------- temporalcloudcli/commands.yml | 136 ++++----------------- 5 files changed, 60 insertions(+), 284 deletions(-) diff --git a/Makefile b/Makefile index 6e0197a..2235b4f 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,13 @@ ifneq (,$(wildcard .env)) export $(shell sed 's/=.*//' .env) endif -all: gen build test +all: install gen build test + +install: + go install github.com/temporalio/cli/cmd/gen-commands@latest gen: - go tool gen-commands -input ./temporalcloudcli/commands.yml -pkg temporalcloudcli > ./temporalcloudcli/commands.gen.go + gen-commands -input ./temporalcloudcli/commands.yml -pkg temporalcloudcli > ./temporalcloudcli/commands.gen.go build: go build ./cmd/temporal-cloud diff --git a/go.mod b/go.mod index c78abd8..93c5055 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - github.com/temporalio/cli/cliext v0.0.0-20260107234359-aa925d353b8e + github.com/temporalio/cli/cliext v0.0.0-20260112210410-f2d230be226c github.com/temporalio/ui-server/v2 v2.42.1 go.temporal.io/api v1.59.0 go.temporal.io/cloud-sdk v0.6.0 diff --git a/go.sum b/go.sum index ec523ad..eb63845 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/temporalio/cli v1.5.2-0.20251212213638-36bff7182259 h1:HB3j4k3hgg9fINQhIOnR5PDlbrHeuVFLuExDIs5RlOk= github.com/temporalio/cli v1.5.2-0.20251212213638-36bff7182259/go.mod h1:8lfULNGZ1Y2sobXSpY+2qQ4vgR4hYLuTLpqnCIl9Au4= -github.com/temporalio/cli/cliext v0.0.0-20260107234359-aa925d353b8e h1:Aud45gx+HInyiTidP6N+LmG7yaayY3QYWN7hJlocNJQ= -github.com/temporalio/cli/cliext v0.0.0-20260107234359-aa925d353b8e/go.mod h1:A9EHuWgszyY1o70CwnJXLkgqfoGKKHXjkKlpE2fj3Uc= +github.com/temporalio/cli/cliext v0.0.0-20260112210410-f2d230be226c h1:E+NxnXPj6XeDN8uUTI3veXYt7PVqfZOrOMh7kRkAEI4= +github.com/temporalio/cli/cliext v0.0.0-20260112210410-f2d230be226c/go.mod h1:A9EHuWgszyY1o70CwnJXLkgqfoGKKHXjkKlpE2fj3Uc= github.com/temporalio/ui-server/v2 v2.42.1 h1:ajeOxqCnUiCRQQhQYLxaT7wUgF/slqZJtdW4pLjVqCs= github.com/temporalio/ui-server/v2 v2.42.1/go.mod h1:lKTnn50t8yQvcrarxAOjX33YcfkomkiNB5BH06wQwEE= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/temporalcloudcli/commands.gen.go b/temporalcloudcli/commands.gen.go index 0bb9959..a1c3b6a 100644 --- a/temporalcloudcli/commands.gen.go +++ b/temporalcloudcli/commands.gen.go @@ -3,44 +3,39 @@ package temporalcloudcli import ( - "fmt" - "github.com/mattn/go-isatty" "github.com/spf13/cobra" - "os" - - "regexp" - - "strconv" + "github.com/spf13/pflag" - "strings" + "github.com/temporalio/cli/cliext" - "time" + "os" ) var hasHighlighting = isatty.IsTerminal(os.Stdout.Fd()) +type ClientOptions struct { + ApiKey string + Server string + FlagSet *pflag.FlagSet +} + +func (v *ClientOptions) BuildFlags(f *pflag.FlagSet) { + v.FlagSet = f + f.StringVar(&v.ApiKey, "api-key", "", "API key for authenticating with Temporal Cloud. Can be used instead of interactive login for automation and CI/CD pipelines.") + f.StringVar(&v.Server, "server", "saas-api.tmprl-test.cloud:443", "Override the Temporal Cloud API server address. Used for connecting to non-production environments.") + _ = f.MarkHidden("server") +} + type CloudCommand struct { - Command cobra.Command - ConfigFile string - Profile string - DisableConfigFile bool - DisableConfigEnv bool - LogLevel StringEnum - LogFormat StringEnum - Output StringEnum - TimeFormat StringEnum - Color StringEnum - NoJsonShorthandPayloads bool - CommandTimeout Duration - ClientConnectTimeout Duration - ConfigDir string - DisablePopUp bool - ApiKey string - Server string - AutoConfirm bool + Command cobra.Command + ClientOptions + cliext.CommonOptions + ConfigDir string + DisablePopUp bool + AutoConfirm bool } func NewCloudCommand(cctx *CommandContext) *CloudCommand { @@ -56,32 +51,11 @@ func NewCloudCommand(cctx *CommandContext) *CloudCommand { s.Command.AddCommand(&NewCloudLoginCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudLogoutCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceCommand(cctx, &s).Command) - s.Command.PersistentFlags().StringVar(&s.ConfigFile, "config-file", "", "Path to the TOML configuration file. Defaults to `$CONFIG_PATH/temporal/temporal.toml` where `$CONFIG_PATH` is `$HOME/.config` on Linux, `$HOME/Library/Application Support` on macOS, and `%AppData%` on Windows. EXPERIMENTAL.") - s.Command.PersistentFlags().StringVar(&s.Profile, "profile", "", "Name of the configuration profile to use from the config file. Profiles allow you to maintain multiple sets of settings. EXPERIMENTAL.") - s.Command.PersistentFlags().BoolVar(&s.DisableConfigFile, "disable-config-file", false, "Disable loading configuration from the config file. When set, only command-line flags and environment variables are used. EXPERIMENTAL.") - s.Command.PersistentFlags().BoolVar(&s.DisableConfigEnv, "disable-config-env", false, "Disable loading configuration from environment variables. When set, only command-line flags and the config file are used. EXPERIMENTAL.") - s.LogLevel = NewStringEnum([]string{"debug", "info", "warn", "error", "never"}, "info") - s.Command.PersistentFlags().Var(&s.LogLevel, "log-level", "Set the logging verbosity level. Use 'debug' for troubleshooting, 'never' to suppress all logs. Accepted values: debug, info, warn, error, never.") - s.LogFormat = NewStringEnum([]string{"text", "json", "pretty"}, "text") - s.Command.PersistentFlags().Var(&s.LogFormat, "log-format", "Format for log output. Use 'json' for structured logging suitable for log aggregation systems. Accepted values: text, json.") - s.Output = NewStringEnum([]string{"text", "json", "jsonl", "none"}, "text") - s.Command.PersistentFlags().VarP(&s.Output, "output", "o", "Format for command output (excludes log messages). Use 'json' for scripting, 'jsonl' for streaming JSON, 'none' to suppress output. Accepted values: text, json, jsonl, none.") - s.TimeFormat = NewStringEnum([]string{"relative", "iso", "raw"}, "relative") - s.Command.PersistentFlags().Var(&s.TimeFormat, "time-format", "Format for displaying timestamps. 'relative' shows human-readable durations (e.g., \"2 hours ago\"), 'iso' shows ISO 8601 format, 'raw' shows Unix timestamps. Accepted values: relative, iso, raw.") - s.Color = NewStringEnum([]string{"always", "never", "auto"}, "auto") - s.Command.PersistentFlags().Var(&s.Color, "color", "Control colored output. 'auto' enables color when outputting to a terminal and disables it otherwise. Accepted values: always, never, auto.") - s.Command.PersistentFlags().BoolVar(&s.NoJsonShorthandPayloads, "no-json-shorthand-payloads", false, "Display payloads in their raw binary format instead of attempting to decode them as JSON. Useful when payloads contain non-JSON data.") - s.CommandTimeout = 0 - s.Command.PersistentFlags().Var(&s.CommandTimeout, "command-timeout", "Maximum time to wait for a command to complete. Use '0s' for no timeout. Example: '30s', '5m'.") - s.ClientConnectTimeout = 0 - s.Command.PersistentFlags().Var(&s.ClientConnectTimeout, "client-connect-timeout", "Maximum time to wait when establishing a connection to Temporal Cloud. Use '0s' for no timeout. Example: '10s', '1m'.") s.Command.PersistentFlags().StringVar(&s.ConfigDir, "config-dir", "", "Directory path where CLI configuration files are stored, including authentication tokens and settings.") s.Command.PersistentFlags().BoolVar(&s.DisablePopUp, "disable-pop-up", false, "Prevent the CLI from opening a browser window during authentication. Useful for headless environments or when using alternative auth methods.") - s.Command.PersistentFlags().StringVar(&s.ApiKey, "api-key", "", "API key for authenticating with Temporal Cloud. Can be used instead of interactive login for automation and CI/CD pipelines.") - cctx.BindFlagEnvVar(s.Command.PersistentFlags().Lookup("api-key"), "TEMPORAL_API_KEY") - s.Command.PersistentFlags().StringVar(&s.Server, "server", "saas-api.tmprl-test.cloud:443", "Override the Temporal Cloud API server address. Used for connecting to non-production environments.") - cctx.BindFlagEnvVar(s.Command.PersistentFlags().Lookup("server"), "TEMPORAL_CLOUD_SERVER") s.Command.PersistentFlags().BoolVar(&s.AutoConfirm, "auto-confirm", false, "Automatically confirm prompts and actions that require user confirmation. Useful for scripting and automation.") + s.ClientOptions.BuildFlags(s.Command.PersistentFlags()) + s.CommonOptions.BuildFlags(s.Command.PersistentFlags()) s.initCommand(cctx) return &s } @@ -109,9 +83,13 @@ func NewCloudLoginCommand(cctx *CommandContext, parent *CloudCommand) *CloudLogi } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.Domain, "domain", "login.tmprl-test.cloud", "Authentication domain for the OAuth provider.") + _ = s.Command.Flags().MarkHidden("domain") s.Command.Flags().StringVar(&s.Audience, "audience", "https://saas-api.tmprl-test.cloud", "OAuth audience parameter for token generation.") + _ = s.Command.Flags().MarkHidden("audience") s.Command.Flags().StringVar(&s.ClientId, "client-id", "XBimMwn90eAnjsiGVbAJ3Hgd9z06jjJB", "OAuth client identifier for authentication.") + _ = s.Command.Flags().MarkHidden("client-id") s.Command.Flags().StringVar(&s.RedirectUrl, "redirect-url", "http://127.0.0.1:56628/callback", "Redirect URL for OAuth authentication flow.") + _ = s.Command.Flags().MarkHidden("redirect-url") s.Command.Flags().BoolVar(&s.Reset, "reset", false, "Clear stored login credentials and configuration, then re-authenticate. Use this if you need to switch accounts or fix authentication issues.") s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { @@ -140,6 +118,7 @@ func NewCloudLogoutCommand(cctx *CommandContext, parent *CloudCommand) *CloudLog } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.Domain, "domain", "login.tmprl-test.cloud", "Authentication domain for the OAuth provider.") + _ = s.Command.Flags().MarkHidden("domain") s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -508,119 +487,3 @@ func NewCloudNamespaceRetentionSetCommand(cctx *CommandContext, parent *CloudNam } return &s } - -var reDays = regexp.MustCompile(`(\d+(\.\d*)?|(\.\d+))d`) - -type Duration time.Duration - -// ParseDuration is like time.ParseDuration, but supports unit "d" for days -// (always interpreted as exactly 24 hours). -func ParseDuration(s string) (time.Duration, error) { - s = reDays.ReplaceAllStringFunc(s, func(v string) string { - fv, err := strconv.ParseFloat(strings.TrimSuffix(v, "d"), 64) - if err != nil { - return v // will cause time.ParseDuration to return an error - } - return fmt.Sprintf("%fh", 24*fv) - }) - return time.ParseDuration(s) -} - -func (d Duration) Duration() time.Duration { - return time.Duration(d) -} - -func (d *Duration) String() string { - return d.Duration().String() -} - -func (d *Duration) Set(s string) error { - p, err := ParseDuration(s) - if err != nil { - return err - } - *d = Duration(p) - return nil -} - -func (d *Duration) Type() string { - return "duration" -} - -type StringEnum struct { - Allowed []string - Value string - ChangedFromDefault bool -} - -func NewStringEnum(allowed []string, value string) StringEnum { - return StringEnum{Allowed: allowed, Value: value} -} - -func (s *StringEnum) String() string { return s.Value } - -func (s *StringEnum) Set(p string) error { - for _, allowed := range s.Allowed { - if p == allowed { - s.Value = p - s.ChangedFromDefault = true - return nil - } - } - return fmt.Errorf("%v is not one of required values of %v", p, strings.Join(s.Allowed, ", ")) -} - -func (*StringEnum) Type() string { return "string" } - -type StringEnumArray struct { - Allowed map[string]string - Values []string -} - -func NewStringEnumArray(allowed []string, values []string) StringEnumArray { - var allowedMap = make(map[string]string) - for _, str := range allowed { - allowedMap[strings.ToLower(str)] = str - } - return StringEnumArray{Allowed: allowedMap, Values: values} -} - -func (s *StringEnumArray) String() string { return strings.Join(s.Values, ",") } - -func (s *StringEnumArray) Set(p string) error { - val, ok := s.Allowed[strings.ToLower(p)] - if !ok { - values := make([]string, 0, len(s.Allowed)) - for _, v := range s.Allowed { - values = append(values, v) - } - return fmt.Errorf("invalid value: %s, allowed values are: %s", p, strings.Join(values, ", ")) - } - s.Values = append(s.Values, val) - return nil -} - -func (*StringEnumArray) Type() string { return "string" } - -type Timestamp time.Time - -func (t Timestamp) Time() time.Time { - return time.Time(t) -} - -func (t *Timestamp) String() string { - return t.Time().Format(time.RFC3339) -} - -func (t *Timestamp) Set(s string) error { - p, err := time.Parse(time.RFC3339, s) - if err != nil { - return err - } - *t = Timestamp(p) - return nil -} - -func (t *Timestamp) Type() string { - return "timestamp" -} diff --git a/temporalcloudcli/commands.yml b/temporalcloudcli/commands.yml index 40b1334..d67a1ea 100644 --- a/temporalcloudcli/commands.yml +++ b/temporalcloudcli/commands.yml @@ -12,106 +12,10 @@ commands: cloud namespace get --namespace my-namespace.my-account ``` has-init: true + option-sets: + - client + - common options: - - name: config-file - type: string - description: | - Path to the TOML configuration file. Defaults to - `$CONFIG_PATH/temporal/temporal.toml` where `$CONFIG_PATH` is - `$HOME/.config` on Linux, `$HOME/Library/Application Support` on - macOS, and `%AppData%` on Windows. - experimental: true - implied-env: TEMPORAL_CONFIG_FILE - - name: profile - type: string - description: | - Name of the configuration profile to use from the config file. - Profiles allow you to maintain multiple sets of settings. - experimental: true - implied-env: TEMPORAL_PROFILE - - name: disable-config-file - type: bool - description: | - Disable loading configuration from the config file. When set, only - command-line flags and environment variables are used. - experimental: true - - name: disable-config-env - type: bool - description: | - Disable loading configuration from environment variables. When set, - only command-line flags and the config file are used. - experimental: true - - name: log-level - type: string-enum - enum-values: - - debug - - info - - warn - - error - - never - description: | - Set the logging verbosity level. Use 'debug' for troubleshooting, - 'never' to suppress all logs. - default: info - - name: log-format - type: string-enum - description: | - Format for log output. Use 'json' for structured logging suitable - for log aggregation systems. - enum-values: - - text - - json - hidden-legacy-values: - - pretty - default: text - - name: output - type: string-enum - short: o - description: | - Format for command output (excludes log messages). Use 'json' for - scripting, 'jsonl' for streaming JSON, 'none' to suppress output. - enum-values: - - text - - json - - jsonl - - none - default: text - - name: time-format - type: string-enum - description: | - Format for displaying timestamps. 'relative' shows human-readable - durations (e.g., "2 hours ago"), 'iso' shows ISO 8601 format, - 'raw' shows Unix timestamps. - enum-values: - - relative - - iso - - raw - default: relative - - name: color - type: string-enum - description: | - Control colored output. 'auto' enables color when outputting to a - terminal and disables it otherwise. - enum-values: - - always - - never - - auto - default: auto - - name: no-json-shorthand-payloads - type: bool - description: | - Display payloads in their raw binary format instead of attempting - to decode them as JSON. Useful when payloads contain non-JSON data. - - name: command-timeout - type: duration - description: | - Maximum time to wait for a command to complete. Use '0s' for no - timeout. Example: '30s', '5m'. - - name: client-connect-timeout - type: duration - description: | - Maximum time to wait when establishing a connection to Temporal - Cloud. Use '0s' for no timeout. Example: '10s', '1m'. - name: config-dir type: string description: | @@ -122,20 +26,6 @@ commands: description: | Prevent the CLI from opening a browser window during authentication. Useful for headless environments or when using alternative auth methods. - - name: api-key - type: string - env: TEMPORAL_API_KEY - description: | - API key for authenticating with Temporal Cloud. Can be used instead - of interactive login for automation and CI/CD pipelines. - - name: server - type: string - env: TEMPORAL_CLOUD_SERVER - description: | - Override the Temporal Cloud API server address. Used for connecting - to non-production environments. - hidden: true - default: saas-api.tmprl-test.cloud:443 - name: auto-confirm type: bool description: | @@ -587,3 +477,23 @@ commands: description: | Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically. + +option-sets: + - name: client + options: + - name: api-key + type: string + env: TEMPORAL_API_KEY + description: | + API key for authenticating with Temporal Cloud. Can be used instead + of interactive login for automation and CI/CD pipelines. + - name: server + type: string + env: TEMPORAL_CLOUD_SERVER + description: | + Override the Temporal Cloud API server address. Used for connecting + to non-production environments. + hidden: true + default: saas-api.tmprl-test.cloud:443 + - name: common + external-package: github.com/temporalio/cli/cliext From 2d2a7d909409a060fb0e4eecf2c71d50ec255ab1 Mon Sep 17 00:00:00 2001 From: Gregory Mankes Date: Tue, 20 Jan 2026 13:31:46 -0500 Subject: [PATCH 3/4] use envlookup os in loading from env --- temporalcloudcli/cloud.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/temporalcloudcli/cloud.go b/temporalcloudcli/cloud.go index 56b735b..eae1496 100644 --- a/temporalcloudcli/cloud.go +++ b/temporalcloudcli/cloud.go @@ -7,12 +7,14 @@ import ( "github.com/temporalio/cli/cliext" "go.temporal.io/cloud-sdk/cloudclient" + "go.temporal.io/sdk/contrib/envconfig" ) func (c *CloudCommand) GetAPIKey(ctx context.Context) (string, error) { loadClientOauthRes, err := cliext.LoadClientOAuth(cliext.LoadClientOAuthOptions{ ConfigFilePath: c.ConfigFile, ProfileName: c.Profile, + EnvLookup: envconfig.EnvLookupOS, }) if err != nil { return "", fmt.Errorf("failed to load login configuration: %w, please run `temporal cloud login --reset`", err) @@ -36,6 +38,7 @@ func (c *CloudCommand) GetAPIKey(ctx context.Context) (string, error) { OAuth: loadClientOauthRes.OAuth, ConfigFilePath: c.ConfigFile, ProfileName: c.Profile, + EnvLookup: envconfig.EnvLookupOS, }); err != nil { return "", fmt.Errorf("failed to write config file: %w", err) } From 087e3ba0abd08091775f8256a5999456055c7eb0 Mon Sep 17 00:00:00 2001 From: Gregory Mankes Date: Tue, 3 Feb 2026 10:40:37 -0500 Subject: [PATCH 4/4] add profile fallthrough and common options --- Makefile | 5 +- temporalcloudcli/cloud.go | 88 ++++++++++++++----- temporalcloudcli/commands.gen.go | 54 ++++++++---- temporalcloudcli/commands.go | 53 +++++++++++ temporalcloudcli/commands.namespace.go | 16 ++-- .../commands.namespace.lifecycle.go | 6 +- .../commands.namespace.retention.go | 6 +- temporalcloudcli/commands.namespace_test.go | 41 ++++++--- temporalcloudcli/commands.yml | 21 ++++- temporalcloudcli/common.go | 12 +-- 10 files changed, 221 insertions(+), 81 deletions(-) diff --git a/Makefile b/Makefile index 2235b4f..1608e61 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,7 @@ ifneq (,$(wildcard .env)) export $(shell sed 's/=.*//' .env) endif -all: install gen build test - -install: - go install github.com/temporalio/cli/cmd/gen-commands@latest +all: gen build test gen: gen-commands -input ./temporalcloudcli/commands.yml -pkg temporalcloudcli > ./temporalcloudcli/commands.gen.go diff --git a/temporalcloudcli/cloud.go b/temporalcloudcli/cloud.go index f602711..fc3de2c 100644 --- a/temporalcloudcli/cloud.go +++ b/temporalcloudcli/cloud.go @@ -4,13 +4,75 @@ import ( "context" "errors" "fmt" + "log/slog" "github.com/temporalio/cli/cliext" - "go.temporal.io/cloud-sdk/cloudclient" "go.temporal.io/sdk/contrib/envconfig" + "go.temporal.io/sdk/log" ) -func (c *CloudCommand) GetAPIKey(ctx context.Context) (string, error) { +type CloudOptions struct { + ApiKey string + Server string + cliext.CommonOptions + Logger log.Logger +} + +type CloudOptionsBuilder struct { + // CommonOptions contains common CLI options including profile config. + CommonOptions cliext.CommonOptions + // ClientOptions contains the client configuration from flags. + ClientOptions ClientOptions + // EnvLookup is the environment variable lookup function. + // If nil, environment variables are not used for profile loading. + EnvLookup envconfig.EnvLookup + // Logger is the slog logger to use for the client. If set, it will be + // wrapped with the SDK's structured logger adapter. + Logger *slog.Logger +} + +func (b *CloudOptionsBuilder) Build(ctx context.Context) (*CloudOptions, error) { + cfg := b.ClientOptions + common := b.CommonOptions + + // Load a client config profile if configured + var profile envconfig.ClientConfigProfile + if !common.DisableConfigFile || !common.DisableConfigEnv { + var err error + profile, err = envconfig.LoadClientConfigProfile(envconfig.LoadClientConfigProfileOptions{ + ConfigFilePath: common.ConfigFile, + ConfigFileProfile: common.Profile, + DisableFile: common.DisableConfigFile, + DisableEnv: common.DisableConfigEnv, + EnvLookup: b.EnvLookup, + }) + if err != nil { + return nil, fmt.Errorf("failed loading client config: %w", err) + } + } + + cloudOpts := &CloudOptions{} + + // Set logger if provided. + if b.Logger != nil { + cloudOpts.Logger = log.NewStructuredLogger(b.Logger) + } + + // Set API key on profile if provided + if cfg.ApiKey != "" { + cloudOpts.ApiKey = cfg.ApiKey + } else if profile.APIKey != "" { + cloudOpts.ApiKey = profile.APIKey + } + + if cfg.Server != "" { + cloudOpts.Server = cfg.Server + } + + return cloudOpts, nil +} + +func (c *CloudOptions) GetAPIKey(ctx context.Context) (string, error) { loadClientOauthRes, err := cliext.LoadClientOAuth(cliext.LoadClientOAuthOptions{ ConfigFilePath: c.ConfigFile, ProfileName: c.Profile, @@ -45,25 +107,3 @@ func (c *CloudCommand) GetAPIKey(ctx context.Context) (string, error) { } return token.AccessToken, nil } - -func newCloudClient(cctx *CommandContext) (*cloudclient.Client, error) { - opts := cloudclient.Options{ - UserAgent: fmt.Sprintf("temporalio-cloud-cli/%s", VersionString()), - } - if cctx.RootCommand.Server != "" { - opts.HostPort = cctx.RootCommand.Server - } - if cctx.RootCommand.ApiKey != "" { - // an explicit api key was provided, use it - opts.APIKey = cctx.RootCommand.ApiKey - } else { - // fallaback to the oauth based sso token provider - opts.APIKeyReader = cctx.RootCommand - } - - cloudClient, err := cloudclient.New(opts) - if err != nil { - return nil, err - } - return cloudClient, nil -} diff --git a/temporalcloudcli/commands.gen.go b/temporalcloudcli/commands.gen.go index a1c3b6a..8163c17 100644 --- a/temporalcloudcli/commands.gen.go +++ b/temporalcloudcli/commands.gen.go @@ -150,8 +150,9 @@ func NewCloudNamespaceCommand(cctx *CommandContext, parent *CloudCommand) *Cloud } type CloudNamespaceApplyCommand struct { - Parent *CloudNamespaceCommand - Command cobra.Command + Parent *CloudNamespaceCommand + Command cobra.Command + ClientOptions Spec string AsyncOperationId string Idempotent bool @@ -179,6 +180,7 @@ func NewCloudNamespaceApplyCommand(cctx *CommandContext, parent *CloudNamespaceC s.Command.Flags().BoolVar(&s.Async, "async", false, "Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later.") s.Command.Flags().BoolVar(&s.VerboseDiff, "verbose-diff", false, "Show detailed differences between the current and desired namespace configurations when changes are detected.") s.Command.Flags().StringVarP(&s.ResourceVersion, "resource-version", "v", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -188,8 +190,9 @@ func NewCloudNamespaceApplyCommand(cctx *CommandContext, parent *CloudNamespaceC } type CloudNamespaceDeleteCommand struct { - Parent *CloudNamespaceCommand - Command cobra.Command + Parent *CloudNamespaceCommand + Command cobra.Command + ClientOptions Namespace string AsyncOperationId string Async bool @@ -215,6 +218,7 @@ func NewCloudNamespaceDeleteCommand(cctx *CommandContext, parent *CloudNamespace s.Command.Flags().BoolVar(&s.Async, "async", false, "Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later.") s.Command.Flags().BoolVar(&s.Idempotent, "idempotent", false, "Succeed silently if the namespace does not exist. Without this flag, the command errors if the namespace is not found.") s.Command.Flags().StringVarP(&s.ResourceVersion, "resource-version", "v", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -224,8 +228,9 @@ func NewCloudNamespaceDeleteCommand(cctx *CommandContext, parent *CloudNamespace } type CloudNamespaceEditCommand struct { - Parent *CloudNamespaceCommand - Command cobra.Command + Parent *CloudNamespaceCommand + Command cobra.Command + ClientOptions Namespace string AsyncOperationId string Idempotent bool @@ -251,6 +256,7 @@ func NewCloudNamespaceEditCommand(cctx *CommandContext, parent *CloudNamespaceCo s.Command.Flags().BoolVar(&s.Idempotent, "idempotent", false, "Succeed silently if no changes were made in the editor. Without this flag, the command errors when the configuration is unchanged.") s.Command.Flags().BoolVar(&s.Async, "async", false, "Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later.") s.Command.Flags().StringVarP(&s.ResourceVersion, "resource-version", "v", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -260,8 +266,9 @@ func NewCloudNamespaceEditCommand(cctx *CommandContext, parent *CloudNamespaceCo } type CloudNamespaceGetCommand struct { - Parent *CloudNamespaceCommand - Command cobra.Command + Parent *CloudNamespaceCommand + Command cobra.Command + ClientOptions Namespace string Spec bool } @@ -281,6 +288,7 @@ func NewCloudNamespaceGetCommand(cctx *CommandContext, parent *CloudNamespaceCom s.Command.Flags().StringVarP(&s.Namespace, "namespace", "n", "", "The fully qualified namespace name in the format 'namespace.account' (e.g., 'my-namespace.my-account'). Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "namespace") s.Command.Flags().BoolVar(&s.Spec, "spec", false, "Output only the namespace specification in JSON format, omitting metadata and status information.") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -307,8 +315,9 @@ func NewCloudNamespaceLifecycleCommand(cctx *CommandContext, parent *CloudNamesp } type CloudNamespaceLifecycleGetCommand struct { - Parent *CloudNamespaceLifecycleCommand - Command cobra.Command + Parent *CloudNamespaceLifecycleCommand + Command cobra.Command + ClientOptions Namespace string } @@ -326,6 +335,7 @@ func NewCloudNamespaceLifecycleGetCommand(cctx *CommandContext, parent *CloudNam s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.Namespace, "namespace", "n", "", "The fully qualified namespace name in the format 'namespace.account' (e.g., 'my-namespace.my-account'). Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "namespace") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -335,8 +345,9 @@ func NewCloudNamespaceLifecycleGetCommand(cctx *CommandContext, parent *CloudNam } type CloudNamespaceLifecycleSetCommand struct { - Parent *CloudNamespaceLifecycleCommand - Command cobra.Command + Parent *CloudNamespaceLifecycleCommand + Command cobra.Command + ClientOptions Namespace string EnableDeleteProtection bool AsyncOperationId string @@ -365,6 +376,7 @@ func NewCloudNamespaceLifecycleSetCommand(cctx *CommandContext, parent *CloudNam s.Command.Flags().BoolVar(&s.Async, "async", false, "Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later.") s.Command.Flags().BoolVar(&s.Idempotent, "idempotent", false, "Succeed silently if the lifecycle configuration is already set to the specified value. Without this flag, the command errors when no change is needed.") s.Command.Flags().StringVar(&s.ResourceVersion, "resource-version", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -374,8 +386,9 @@ func NewCloudNamespaceLifecycleSetCommand(cctx *CommandContext, parent *CloudNam } type CloudNamespaceListCommand struct { - Parent *CloudNamespaceCommand - Command cobra.Command + Parent *CloudNamespaceCommand + Command cobra.Command + ClientOptions PageSize int PageToken string Name string @@ -396,6 +409,7 @@ func NewCloudNamespaceListCommand(cctx *CommandContext, parent *CloudNamespaceCo s.Command.Flags().IntVar(&s.PageSize, "page-size", 0, "Number of namespaces to return per page. Use for paginated results.") s.Command.Flags().StringVar(&s.PageToken, "page-token", "", "Token for retrieving the next page of results in a paginated list.") s.Command.Flags().StringVar(&s.Name, "name", "", "Filter namespaces by the name as defined in the specification of the namespace.") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -422,8 +436,9 @@ func NewCloudNamespaceRetentionCommand(cctx *CommandContext, parent *CloudNamesp } type CloudNamespaceRetentionGetCommand struct { - Parent *CloudNamespaceRetentionCommand - Command cobra.Command + Parent *CloudNamespaceRetentionCommand + Command cobra.Command + ClientOptions Namespace string } @@ -441,6 +456,7 @@ func NewCloudNamespaceRetentionGetCommand(cctx *CommandContext, parent *CloudNam s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.Namespace, "namespace", "n", "", "The fully qualified namespace name in the format 'namespace.account' (e.g., 'my-namespace.my-account'). Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "namespace") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -450,8 +466,9 @@ func NewCloudNamespaceRetentionGetCommand(cctx *CommandContext, parent *CloudNam } type CloudNamespaceRetentionSetCommand struct { - Parent *CloudNamespaceRetentionCommand - Command cobra.Command + Parent *CloudNamespaceRetentionCommand + Command cobra.Command + ClientOptions Namespace string AsyncOperationId string Async bool @@ -480,6 +497,7 @@ func NewCloudNamespaceRetentionSetCommand(cctx *CommandContext, parent *CloudNam s.Command.Flags().IntVar(&s.RetentionDays, "retention-days", 0, "New retention period in days for closed workflow history data. Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "retention-days") s.Command.Flags().StringVar(&s.ResourceVersion, "resource-version", "", "Resource version for optimistic concurrency control. If not provided, the current version is fetched automatically.") + s.ClientOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) diff --git a/temporalcloudcli/commands.go b/temporalcloudcli/commands.go index 8926ec1..755424d 100644 --- a/temporalcloudcli/commands.go +++ b/temporalcloudcli/commands.go @@ -24,6 +24,7 @@ import ( commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/failure/v1" "go.temporal.io/api/temporalproto" + "go.temporal.io/cloud-sdk/cloudclient" "go.temporal.io/sdk/contrib/envconfig" "go.temporal.io/sdk/converter" "go.temporal.io/sdk/temporal" @@ -96,6 +97,58 @@ func NewCommandContext(ctx context.Context, options CommandOptions) (*CommandCon return cctx, stop, nil } +// BuildCloudOptions creates CloudOptions from the command's ClientOptions. +// It uses the RootCommand's CommonOptions and the CommandContext's logger. +// +// This method encapsulates the CloudOptionsBuilder pattern and should be used +// by all commands that need CloudOptions. +// +// AIDEV-NOTE: This is the standard way for commands to create CloudOptions. +// It automatically uses cctx.RootCommand.CommonOptions regardless of command depth. +func (cctx *CommandContext) BuildCloudOptions(clientOpts ClientOptions) (*CloudOptions, error) { + builder := CloudOptionsBuilder{ + ClientOptions: clientOpts, + CommonOptions: cctx.RootCommand.CommonOptions, + Logger: cctx.Logger, + EnvLookup: envconfig.EnvLookupOS, + } + return builder.Build(cctx.Context) +} + +// BuildCloudClient creates a CloudClient from the command's ClientOptions. +// It builds CloudOptions internally and then creates the client. +// +// This is a convenience method for commands that need the CloudClient directly +// without needing to keep a reference to CloudOptions. +// +// AIDEV-NOTE: Use this method in command run functions instead of manually +// creating CloudOptions and CloudClient separately. +func (cctx *CommandContext) BuildCloudClient(clientOpts ClientOptions) (*cloudclient.Client, error) { + cloudOpts, err := cctx.BuildCloudOptions(clientOpts) + if err != nil { + return nil, err + } + opts := cloudclient.Options{ + UserAgent: fmt.Sprintf("temporalio-cloud-cli/%s", VersionString()), + } + if cloudOpts.Server != "" { + opts.HostPort = cloudOpts.Server + } + if cloudOpts.ApiKey != "" { + // an explicit api key was provided, use it + opts.APIKey = cloudOpts.ApiKey + } else { + // fallaback to the oauth based sso token provider + opts.APIKeyReader = cloudOpts + } + + cloudClient, err := cloudclient.New(opts) + if err != nil { + return nil, err + } + return cloudClient, nil +} + func (c *CommandContext) preprocessOptions() error { if len(c.Options.Args) == 0 { c.Options.Args = os.Args[1:] diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index 2e8fb5d..7bc5930 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -9,7 +9,7 @@ import ( ) func (c *CloudNamespaceGetCommand) run(cctx *CommandContext, _ []string) error { - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } @@ -28,7 +28,7 @@ func (c *CloudNamespaceGetCommand) run(cctx *CommandContext, _ []string) error { } func (c *CloudNamespaceEditCommand) run(cctx *CommandContext, _ []string) error { - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } @@ -91,7 +91,7 @@ func (c *CloudNamespaceEditCommand) run(cctx *CommandContext, _ []string) error } // Poll for completion - return pollAsyncOperation(cctx, res.asyncOp.Id, res.Namespace) + return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) } func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error { @@ -108,7 +108,7 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error } // Step 3: Create cloud and namespace clients - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } @@ -182,11 +182,11 @@ func (c *CloudNamespaceApplyCommand) run(cctx *CommandContext, _ []string) error } // Step 7: Poll for completion - return pollAsyncOperation(cctx, res.asyncOp.Id, res.Namespace) + return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) } func (c *CloudNamespaceDeleteCommand) run(cctx *CommandContext, _ []string) error { - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } @@ -234,11 +234,11 @@ func (c *CloudNamespaceDeleteCommand) run(cctx *CommandContext, _ []string) erro } // Poll for completion - return pollAsyncOperation(cctx, asyncOp.Id, c.Namespace) + return pollAsyncOperation(cctx, cloudClient, asyncOp.Id, c.Namespace) } func (c *CloudNamespaceListCommand) run(cctx *CommandContext, _ []string) error { - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } diff --git a/temporalcloudcli/commands.namespace.lifecycle.go b/temporalcloudcli/commands.namespace.lifecycle.go index 1a7d112..6e2898b 100644 --- a/temporalcloudcli/commands.namespace.lifecycle.go +++ b/temporalcloudcli/commands.namespace.lifecycle.go @@ -8,7 +8,7 @@ import ( ) func (c *CloudNamespaceLifecycleGetCommand) run(cctx *CommandContext, _ []string) error { - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } @@ -39,7 +39,7 @@ func (c *CloudNamespaceLifecycleGetCommand) run(cctx *CommandContext, _ []string } func (c *CloudNamespaceLifecycleSetCommand) run(cctx *CommandContext, _ []string) error { - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } @@ -105,5 +105,5 @@ func (c *CloudNamespaceLifecycleSetCommand) run(cctx *CommandContext, _ []string } // Poll for completion - return pollAsyncOperation(cctx, res.asyncOp.Id, res.Namespace) + return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) } diff --git a/temporalcloudcli/commands.namespace.retention.go b/temporalcloudcli/commands.namespace.retention.go index f87489b..907a450 100644 --- a/temporalcloudcli/commands.namespace.retention.go +++ b/temporalcloudcli/commands.namespace.retention.go @@ -8,7 +8,7 @@ import ( ) func (c *CloudNamespaceRetentionSetCommand) run(cctx *CommandContext, _ []string) error { - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } @@ -66,11 +66,11 @@ func (c *CloudNamespaceRetentionSetCommand) run(cctx *CommandContext, _ []string } // Poll for completion - return pollAsyncOperation(cctx, res.asyncOp.Id, res.Namespace) + return pollAsyncOperation(cctx, cloudClient, res.asyncOp.Id, res.Namespace) } func (c *CloudNamespaceRetentionGetCommand) run(cctx *CommandContext, _ []string) error { - cloudClient, err := newCloudClient(cctx) + cloudClient, err := cctx.BuildCloudClient(c.ClientOptions) if err != nil { return err } diff --git a/temporalcloudcli/commands.namespace_test.go b/temporalcloudcli/commands.namespace_test.go index b9a9eaf..76453e0 100644 --- a/temporalcloudcli/commands.namespace_test.go +++ b/temporalcloudcli/commands.namespace_test.go @@ -51,7 +51,9 @@ func (s *SharedServerSuite) testnamespaceCRUD() { s.Suite.Require().NoError(err) res := s.Execute( - "namespace", "apply", + "namespace", + fmt.Sprintf("--server=%s", s.server), // TODO (gmankes): remove this when the server is defaulted back to prod + "apply", "--auto-confirm=true", "--spec", fmt.Sprintf(`%s`, string(buf)), "-o=json", @@ -68,11 +70,13 @@ func (s *SharedServerSuite) testnamespaceCRUD() { // get the namespace res = s.Execute( - "namespace", "get", + "namespace", + fmt.Sprintf("--server=%s", s.server), // TODO (gmankes): remove this when the server is defaulted back to prod + "get", "-n", namespaceID, "-o=json", ) - s.Suite.Require().NoError(err) + s.Suite.Require().NoError(res.Err) buf, err = io.ReadAll(&res.Stdout) s.Suite.Require().NoError(err) @@ -91,11 +95,13 @@ func (s *SharedServerSuite) testnamespaceCRUD() { // get the namespace via listing res = s.Execute( - "namespace", "list", - "--name", newNamespaceName, + "namespace", + fmt.Sprintf("--server=%s", s.server), // TODO (gmankes): remove this when the server is defaulted back to prod + "list", + fmt.Sprintf("--name=%s", newNamespaceName), "-o=json", ) - s.Suite.Require().NoError(err) + s.Suite.Require().NoError(res.Err) buf, err = io.ReadAll(&res.Stdout) s.Suite.Require().NoError(err) @@ -110,7 +116,9 @@ func (s *SharedServerSuite) testnamespaceCRUD() { s.Suite.Require().NoError(err) res = s.Execute( - "namespace", "apply", + "namespace", + fmt.Sprintf("--server=%s", s.server), // TODO (gmankes): remove this when the server is defaulted back to prod + "apply", "--auto-confirm=true", "--spec", fmt.Sprintf(`%s`, string(buf)), "-o=json", @@ -119,15 +127,16 @@ func (s *SharedServerSuite) testnamespaceCRUD() { // get the namespace (after updating) res = s.Execute( - "namespace", "get", + "namespace", + fmt.Sprintf("--server=%s", s.server), // TODO (gmankes): remove this when the server is defaulted back to prod + "get", "-n", namespaceID, "-o=json", ) - s.Suite.Require().NoError(err) + s.Suite.Require().NoError(res.Err) buf, err = io.ReadAll(&res.Stdout) s.Suite.Require().NoError(err) - fmt.Println(string(buf)) readNamespace = &namespace.Namespace{} err = protojson.Unmarshal(buf, readNamespace) @@ -143,21 +152,25 @@ func (s *SharedServerSuite) testnamespaceCRUD() { // delete the namespace res = s.Execute( - "namespace", "delete", + "namespace", + fmt.Sprintf("--server=%s", s.server), // TODO (gmankes): remove this when the server is defaulted back to prod + "delete", "-n", namespaceID, "--idempotent", "--auto-confirm=true", "-o=json", ) - s.Suite.Require().NoError(err) + s.Suite.Require().NoError(res.Err) // try to get the namespace res = s.Execute( - "namespace", "get", + "namespace", + fmt.Sprintf("--server=%s", s.server), // TODO (gmankes): remove this when the server is defaulted back to prod + "get", "-n", namespaceID, ) - s.Suite.Require().NoError(err) + s.Suite.Require().Error(res.Err) // should say not found stdOut, err := io.ReadAll(&res.Stdout) s.Suite.Require().NoError(err) diff --git a/temporalcloudcli/commands.yml b/temporalcloudcli/commands.yml index d67a1ea..bc4ae09 100644 --- a/temporalcloudcli/commands.yml +++ b/temporalcloudcli/commands.yml @@ -142,6 +142,8 @@ commands: cloud namespace get --namespace my-namespace.my-account ``` has-init: false + option-sets: + - client docs: keywords: - namespace @@ -183,6 +185,8 @@ commands: cloud namespace apply --spec @namespace-spec.json ``` has-init: false + option-sets: + - client options: - name: spec type: string @@ -232,6 +236,8 @@ commands: cloud namespace edit --namespace my-namespace.my-account ``` has-init: false + option-sets: + - client options: - name: namespace type: string @@ -274,6 +280,8 @@ commands: cloud namespace delete --namespace my-namespace.my-account ``` has-init: false + option-sets: + - client options: - name: namespace type: string @@ -315,6 +323,8 @@ commands: cloud namespace list ``` has-init: false + option-sets: + - client options: - name: page-size type: int @@ -348,6 +358,8 @@ commands: cloud namespace retention get --namespace my-namespace.my-account ``` has-init: false + option-sets: + - client options: - name: namespace type: string @@ -368,6 +380,8 @@ commands: cloud namespace retention set --namespace my-namespace.my-account --retention-days 14 ``` has-init: false + option-sets: + - client options: - name: namespace type: string @@ -422,6 +436,8 @@ commands: cloud namespace lifecycle get --namespace my-namespace.my-account ``` has-init: false + option-sets: + - client options: - name: namespace type: string @@ -442,6 +458,8 @@ commands: cloud namespace lifecycle set --namespace my-namespace.my-account --enable-delete-protection true ``` has-init: false + option-sets: + - client options: - name: namespace type: string @@ -483,13 +501,12 @@ option-sets: options: - name: api-key type: string - env: TEMPORAL_API_KEY + implied-env: TEMPORAL_API_KEY description: | API key for authenticating with Temporal Cloud. Can be used instead of interactive login for automation and CI/CD pipelines. - name: server type: string - env: TEMPORAL_CLOUD_SERVER description: | Override the Temporal Cloud API server address. Used for connecting to non-production environments. diff --git a/temporalcloudcli/common.go b/temporalcloudcli/common.go index 5add511..9c051d7 100644 --- a/temporalcloudcli/common.go +++ b/temporalcloudcli/common.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "go.temporal.io/cloud-sdk/cloudclient" cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" operation "go.temporal.io/cloud-sdk/api/operation/v1" "google.golang.org/grpc/codes" @@ -144,16 +145,17 @@ func promptApplyResource(cctx *CommandContext, existing, actual proto.Message, v // pollAsyncOperation polls an async operation until it reaches a terminal state. // It prints status updates every second and returns the final AsyncOperation. +// +// The cloudClient should be pre-built using cctx.BuildCloudClient(). +// +// AIDEV-NOTE: This function takes a pre-built cloudClient. Commands should +// build the client using cctx.BuildCloudClient() and pass it directly. func pollAsyncOperation( cctx *CommandContext, + cloudClient *cloudclient.Client, operationID string, id string, ) error { - cloudClient, err := newCloudClient(cctx) - if err != nil { - return err - } - ticker := time.NewTicker(1 * time.Second) defer ticker.Stop()