diff --git a/cmd/configure.go b/cmd/configure.go index 9bd82c6273..1394bbf13d 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -223,6 +223,7 @@ func NewCmdConfig() *cobra.Command { PublicKey: cfg.PublicKey, Profile: cfg.Profile, BaseURL: cfg.BaseURL, + ChannelKey: cfg.ChannelKey, Timeout: cfg.Timeout, Active: cfg.Active, Region: cfg.Region, @@ -247,6 +248,14 @@ func NewCmdConfig() *cobra.Command { cacheConfig.BaseURL = cfg.BaseURL } + //channel-key 属连接类参数,与 base-url 同批应用:必须早于下方 region/project + //远程校验,否则校验请求不带 key,专属云 profile 在配置时即报错而配不上。 + //用 Changed() 而非空值判断:空是合法值(专属云切回主站需清除它), + //--channel-key "" 应能清空,这与 base-url「空=不改」的既有局限不同。 + if c.Flags().Changed("channel-key") { + cacheConfig.ChannelKey = cfg.ChannelKey + } + if cfg.Timeout == 0 { if cacheConfig.Timeout == 0 { cacheConfig.Timeout = platform.DefaultTimeoutSec @@ -342,6 +351,7 @@ func NewCmdConfig() *cobra.Command { flags.StringVar(&cfg.Zone, "zone", "", "Optional. Set default zone. For instance 'cn-bj2-02'. See 'ucloud region'") flags.StringVar(&cfg.ProjectID, "project-id", "", "Optional. Set default project. For instance 'org-xxxxxx'. See 'ucloud project list") flags.StringVar(&cfg.BaseURL, "base-url", "", "Optional. Set default base url. For instance 'https://api.ucloud.cn/'") + flags.StringVar(&cfg.ChannelKey, "channel-key", "", "Optional. Set channel-key for a dedicated cloud channel that reuses the main-site domain. For instance 'ch_xxx'. Leave empty for the main site or a channel with its own domain") flags.IntVar(&cfg.Timeout, "timeout-sec", 0, "Optional. Set default timeout for requesting API. Unit: seconds") cfg.MaxRetryTimes = flags.Int("max-retry-times", 0, "Optional. Set default max-retry-times for idempotent APIs which can be called many times without side effect, for example 'ReleaseEIP'") flags.StringVar(&active, "active", "", "Optional. Mark the profile to be effective or not. Accept valeus: true or false") @@ -423,6 +433,7 @@ func NewCmdConfigAdd() *cobra.Command { flags.StringVar(&cfg.Zone, "zone", "", "Optional. Set default zone. For instance 'cn-bj2-02'. See 'ucloud region'") flags.StringVar(&cfg.ProjectID, "project-id", "", "Optional. Set default project. For instance 'org-xxxxxx'. See 'ucloud project list") flags.StringVar(&cfg.BaseURL, "base-url", platform.DefaultBaseURL, "Optional. Set default base url. For instance 'https://api.ucloud.cn/'") + flags.StringVar(&cfg.ChannelKey, "channel-key", "", "Optional. Set channel-key for a dedicated cloud channel that reuses the main-site domain. For instance 'ch_xxx'. Leave empty for the main site or a channel with its own domain") flags.IntVar(&cfg.Timeout, "timeout-sec", platform.DefaultTimeoutSec, "Optional. Set default timeout for requesting API. Unit: seconds") cfg.MaxRetryTimes = flags.Int("max-retry-times", platform.DefaultMaxRetryTimes, "Optional. Set default max-retry-times for idempotent APIs which can be called many times without side effect, for example 'ReleaseEIP'") flags.StringVar(&active, "active", "false", "Optional. Mark the profile to be effective or not. Accept valeus: true or false") @@ -472,12 +483,19 @@ func NewCmdConfigUpdate() *cobra.Command { platform.AggConfigListIns.UpdateAggConfig(cacheConfig) } - //先应用连接类参数(base-url/timeout-sec/max-retry-times),确保接下来的远程校验 + //先应用连接类参数(base-url/channel-key/timeout-sec/max-retry-times),确保接下来的远程校验 //打到新网关而不是旧的(可能已不可用的)网关,避免旧base-url坏掉后无法改回的死锁 if cfg.BaseURL != "" { cacheConfig.BaseURL = cfg.BaseURL } + //channel-key 同属连接类参数:专属云 profile 的远程校验请求必须带上它, + //否则网关报 174 而校验失败,profile 永远配不上。 + //Changed() 使 --channel-key "" 可清除(专属云切回主站的场景)。 + if c.Flags().Changed("channel-key") { + cacheConfig.ChannelKey = cfg.ChannelKey + } + if timeout != "" { seconds, err := strconv.Atoi(timeout) if err != nil { @@ -561,6 +579,7 @@ func NewCmdConfigUpdate() *cobra.Command { flags.StringVar(&cfg.Zone, "zone", "", "Optional. Set default zone. For instance 'cn-bj2-02'. See 'ucloud region'") flags.StringVar(&cfg.ProjectID, "project-id", "", "Optional. Set default project. For instance 'org-xxxxxx'. See 'ucloud project list") flags.StringVar(&cfg.BaseURL, "base-url", "", "Optional. Set default base url. For instance 'https://api.ucloud.cn/'") + flags.StringVar(&cfg.ChannelKey, "channel-key", "", "Optional. Set channel-key for a dedicated cloud channel that reuses the main-site domain. For instance 'ch_xxx'. Pass an empty value to clear it") flags.StringVar(&timeout, "timeout-sec", "", "Optional. Set default timeout for requesting API. Unit: seconds") flags.StringVar(&maxRetries, "max-retry-times", "", "Optional. Set default max retry times for idempotent APIs which can be called many times without side effect, for example 'ReleaseEIP'") flags.StringVar(&active, "active", "", "Optional. Mark the profile to be effective") diff --git a/cmd/internal/platform/client.go b/cmd/internal/platform/client.go index 00410e08c4..e714daa213 100644 --- a/cmd/internal/platform/client.go +++ b/cmd/internal/platform/client.go @@ -71,13 +71,45 @@ func newCredHeaderInjector(credConfig *CredentialConfig) sdk.HttpRequestHandler } } +// newChannelHeaderInjector 返回专属云渠道头注入 handler。 +// +// 网关据 channel-key 识别「复用主站域名」的专属云渠道(如 api.ucloud-global.com); +// 独立域名渠道与主站用户没有这个 key,故空值必须完全不注入该头 —— 与本文件中 +// Cookie/Csrf-Token「空值也照旧 set」的历史行为刻意相反:那是存量契约,而这是新增头, +// 给全部存量用户平白加一个空头即构成回归。 +// +// 取值源是 ac 而非 credConfig:channel-key 是接入点配置(与 base_url 同类、成对配置), +// 不是凭据机制,与 auth_mode 正交(spec auth-guidelines「一个请求只携带一种凭据机制」 +// 说的是凭据,不含本头)。且 newServiceClientForConfig 传入的 ac 正是用户此刻正在配置 +// 的 profile,config 子命令自身的 region/project 远程校验请求因此天然带上正确的 key, +// 不会出现「配置时校验失败 → 配不上」的死锁。 +// +// 实测(2026-07-16,真实 combo 账号 + 真实网关):同一 token 同一域名,唯一变量为本头, +// RetCode 174 → 0;且 Go 的 Header.Set 规范化为 Channel-Key 后网关照常接受。 +func newChannelHeaderInjector(ac *AggConfig) sdk.HttpRequestHandler { + return func(c *sdk.Client, req *http.HttpRequest) (*http.HttpRequest, error) { + if ac == nil || ac.ChannelKey == "" { + return req, nil + } + return req, req.SetHeader("channel-key", ac.ChannelKey) + } +} + // authRetCodeWhitelist 鉴权类 RetCode 白名单(D6)。实测网关(2026-06-11 实探): // 鉴权失败以 HTTP 200 + RetCode 返回,401 仅作防御性分支保留。 // 174 "Token Not Exists":伪造与已过期的 Bearer 同为 174(已实测确认);属网关 // 前置鉴权拒绝,业务必未执行,重放一次安全。网关团队书面确认仍待补档(spec §7)。 // 170(缺签名,oauth 请求恒带 Bearer 不会触发)、171/172(AK/SK 路径)不入列。 +// +// 174 的第三种成因(2026-07-16 实测发现,已知且刻意保留现状):channel-key 与账号 +// 所属渠道不匹配时网关同样返回 174(同一有效 token 换成别的渠道 key 即报 174)。 +// 于是本白名单会把「配错 channel-key」误判为 token 过期 → 刷新(refresh_token 轮转、 +// 旧的立即作废)→ 重放 → 仍 174。后果是每次请求白耗一轮刷新,且错误文案把用户导向 +// 重新登录 —— 而重新登录永远治不好。本次不改控制流(改了会波及正常的过期刷新路径)。 +// 排障:OAuth profile 报 174 且重新登录无效时,优先检查 channel_key 是否与账号所属 +// 渠道匹配。 var authRetCodeWhitelist = map[int]bool{ - 174: true, // Token Not Exists:无效或过期 Bearer + 174: true, // Token Not Exists:无效或过期 Bearer(亦可能是 channel-key 不匹配,见上) } // isAuthFailure 判定是否鉴权类失败:HTTP 401 或 body RetCode 在白名单(网关前置鉴权,业务必未执行)。 @@ -216,8 +248,8 @@ func normalizeProjectID(req request.Common) (request.Common, error) { return req, nil } -// attachHandlers 把三个平台 handler 挂到 service client 上: -// project-id 归一化、凭据头注入、oauth 反应式重试。 +// attachHandlers 把平台 handler 挂到 service client 上: +// project-id 归一化、请求日志、凭据头注入、专属云渠道头注入、oauth 反应式重试。 // credConfig 与 ac 显式传入:NewClient 借此传它自己的构造来源 profile(ac), // 重试目标必须是构造本 client 的 profile,而非包级 ConfigIns // (详见 newOAuthRetryHandler 的注释:os.Args 扫描识别不了 -p X/--profile=X, @@ -239,6 +271,7 @@ func attachHandlersWithManager(sc sdk.ServiceClient, credConfig *CredentialConfi return req, nil }) sc.AddHttpRequestHandler(newCredHeaderInjector(credConfig)) + sc.AddHttpRequestHandler(newChannelHeaderInjector(ac)) sc.AddHttpResponseHandler(newOAuthRetryHandler(credConfig, ac, manager)) } diff --git a/cmd/internal/platform/client_test.go b/cmd/internal/platform/client_test.go index 9b5b023550..8b565ec19e 100644 --- a/cmd/internal/platform/client_test.go +++ b/cmd/internal/platform/client_test.go @@ -276,6 +276,132 @@ func TestOAuthProfileWithRetainedKeysDoesNotSign(t *testing.T) { } } +// channelInjectorHeaders 单独驱动渠道头注入 handler,取其产出的 header map。 +// 注意查的是 SetHeader 原样存入的 map(未经 Go 规范化),故 key 必须与注入端字面一致。 +func channelInjectorHeaders(t *testing.T, ac *AggConfig) map[string]string { + t.Helper() + h := newChannelHeaderInjector(ac) + req, err := h(nil, uhttp.NewHttpRequest()) + if err != nil { + t.Fatal(err) + } + return req.GetHeaderMap() +} + +// 配了 channel_key 的 profile:注入 channel-key 头(AC1) +func TestChannelInjectorSetsHeader(t *testing.T) { + headers := channelInjectorHeaders(t, &AggConfig{ChannelKey: "ch_combo_test"}) + if headers["channel-key"] != "ch_combo_test" { + t.Errorf("channel-key = %q, want ch_combo_test", headers["channel-key"]) + } +} + +// CRITICAL 零回归(AC2):未配 channel_key 时该头必须完全不存在——不是空值,是键不存在。 +// 主站用户与独立域名专属云渠道恒走此路径,线路字节必须与引入本特性前逐字节一致。 +// 与同文件 Cookie/Csrf-Token「空值也照旧 set」的存量契约刻意相反:那是历史行为, +// 本头是新增的,注入空头会给全部存量用户构成回归。 +func TestChannelInjectorAbsentWhenEmpty(t *testing.T) { + for _, tc := range []struct { + name string + ac *AggConfig + }{ + {"empty channel key", &AggConfig{}}, + {"nil agg config", nil}, // AttachHandlersWith(sc, nil, nil, nil) 降级路径确实存在 + } { + t.Run(tc.name, func(t *testing.T) { + if v, ok := channelInjectorHeaders(t, tc.ac)["channel-key"]; ok { + t.Errorf("channel-key header must be absent, got present with value %q", v) + } + }) + } +} + +// channel-key 与凭据机制正交(AC3):auth_mode 不影响其注入。 +// spec auth-guidelines 的「一个请求只携带一种凭据机制」约束的是凭据,channel-key 是 +// 渠道路由标识,不在其列。2026-07-16 真实网关实测:Bearer 与 channel-key 同时上行被接受。 +func TestChannelInjectorOrthogonalToAuthMode(t *testing.T) { + for _, tc := range []struct { + name string + ac *AggConfig + }{ + {"aksk", &AggConfig{ChannelKey: "ch_x", PublicKey: "pub", PrivateKey: "pri"}}, + {"oauth", &AggConfig{ChannelKey: "ch_x", AuthMode: AuthModeOAuth, AccessToken: "tok"}}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := channelInjectorHeaders(t, tc.ac)["channel-key"]; got != "ch_x" { + t.Errorf("channel-key = %q, want ch_x", got) + } + }) + } +} + +// 端到端:经完整 handler 链发真实请求,断言服务端侧确实收到该头。 +// 这是唯一能验证「Go 的 Header.Set 规范化为 Channel-Key 后网关仍可取到」的用例 +// (单测查的 map 未规范化,验证不到线路形式),也顺带证明 handler 确实被挂上了。 +func TestChannelKeyHeaderEndToEnd(t *testing.T) { + rec := &recordedRequest{} + s := bizRecorderServer(t, rec) + defer s.Close() + + ac := &AggConfig{ + Profile: "combo", BaseURL: s.URL, Timeout: 15, MaxRetryTimes: intPtr(0), + Region: "hk", ChannelKey: "ch_combo_e2e", + PublicKey: "pub", PrivateKey: "pri", + } + callGetRegion(t, ac, rec) + + // http.Header.Get 大小写不敏感:线路上是 Channel-Key,此处照样取得到 + if got := rec.header.Get("channel-key"); got != "ch_combo_e2e" { + t.Errorf("server received channel-key = %q, want ch_combo_e2e", got) + } +} + +// AttachHandlers(读包级 ConfigIns 的那条路径,GetUserInfo 等在用)同样注入 channel-key。 +// 该路径实测难以触发(仅 agree_upload_log=true 的 DAS 日志上传会走 GetUserInfo),故以单测钉死。 +func TestAttachHandlersInjectsChannelKeyFromConfigIns(t *testing.T) { + rec := &recordedRequest{} + s := bizRecorderServer(t, rec) + defer s.Close() + + oldConfig, oldClientConfig, oldCred := ConfigIns, ClientConfig, AuthCredential + t.Cleanup(func() { ConfigIns, ClientConfig, AuthCredential = oldConfig, oldClientConfig, oldCred }) + + ac := &AggConfig{ + Profile: "combo", BaseURL: s.URL, Timeout: 15, MaxRetryTimes: intPtr(0), + Region: "hk", ChannelKey: "ch_from_configins", + PublicKey: "pub", PrivateKey: "pri", + } + ConfigIns = ac + if err := InitClientRuntime(ac); err != nil { + t.Fatal(err) + } + client := uaccount.NewClient(ClientConfig, BuildCredential()) + AttachHandlers(client) // 包级路径:等价于 AttachHandlersWith(sc, AuthCredential, ConfigIns, ...) + if _, err := client.GetRegion(client.NewGetRegionRequest()); err != nil { + t.Fatalf("GetRegion failed: %v", err) + } + if got := rec.header.Get("channel-key"); got != "ch_from_configins" { + t.Errorf("AttachHandlers must inject channel-key from ConfigIns, got %q", got) + } +} + +// 端到端零回归:未配 channel_key 时服务端不得看到该头 +func TestChannelKeyHeaderAbsentEndToEnd(t *testing.T) { + rec := &recordedRequest{} + s := bizRecorderServer(t, rec) + defer s.Close() + + ac := &AggConfig{ + Profile: "mainsite", BaseURL: s.URL, Timeout: 15, MaxRetryTimes: intPtr(0), + Region: "cn-bj2", PublicKey: "pub", PrivateKey: "pri", + } + callGetRegion(t, ac, rec) + + if _, ok := rec.header["Channel-Key"]; ok { + t.Errorf("main-site profile must not send channel-key header, got %q", rec.header.Get("channel-key")) + } +} + // 401 自动重放矩阵(D6 反应式兜底):401→刷新→重放成功;aksk 模式不重放。 // 注意 SDK 行为:HttpClient.Send 对 status>=400 返回 (nil, StatusError), // 401 的 body 在 handler 层不可见,鉴权失败只能从 err 判定。 diff --git a/cmd/internal/platform/config.go b/cmd/internal/platform/config.go index 7c6a7da8f0..12c140018b 100644 --- a/cmd/internal/platform/config.go +++ b/cmd/internal/platform/config.go @@ -102,6 +102,7 @@ type GlobalFlag struct { PublicKey string PrivateKey string BaseURL string + ChannelKey string Timeout int WaitTimeout int // 同步轮询总超时(秒),0 表示用内置默认(600s) MaxRetryTimes int @@ -119,6 +120,10 @@ type CLIConfig struct { MaxRetryTimes *int `json:"max_retry_times"` AgreeUploadLog bool `json:"agree_upload_log"` OAuthBaseURL string `json:"oauth_base_url,omitempty"` + // ChannelKey 专属云渠道标识,与 base_url 成对配置(复用主站域名的专属云渠道必填, + // 独立域名渠道与主站用户为空)。属接入点配置而非用户凭据,故落在 config.json, + // 不进 credential.json。 + ChannelKey string `json:"channel_key,omitempty"` } // CredentialConfig credential element @@ -155,6 +160,7 @@ type AggConfig struct { RefreshToken string `json:"refresh_token,omitempty"` ExpiresAt int64 `json:"expires_at,omitempty"` OAuthBaseURL string `json:"oauth_base_url,omitempty"` + ChannelKey string `json:"channel_key,omitempty"` } // ConfigPublicKey 输入公钥 @@ -248,6 +254,7 @@ func (p *AggConfig) copyToCLIConfig(target *CLIConfig) { target.MaxRetryTimes = p.MaxRetryTimes target.AgreeUploadLog = p.AgreeUploadLog target.OAuthBaseURL = p.OAuthBaseURL + target.ChannelKey = p.ChannelKey } func (p *AggConfig) copyToCredentialConfig(target *CredentialConfig) { @@ -389,6 +396,7 @@ func (p *AggConfigManager) Load() error { RefreshToken: cred.RefreshToken, ExpiresAt: cred.ExpiresAt, OAuthBaseURL: config.OAuthBaseURL, + ChannelKey: config.ChannelKey, } } @@ -668,7 +676,7 @@ func ListAggConfig(json bool) { HandleError(err) } } else { - PrintTable(aggConfigs, []string{"Profile", "Active", "AuthMode", "ProjectID", "Region", "Zone", "BaseURL", "Timeout", "PublicKey", "PrivateKey", "MaxRetryTimes", "AgreeUploadLog"}) + PrintTable(aggConfigs, []string{"Profile", "Active", "AuthMode", "ProjectID", "Region", "Zone", "BaseURL", "ChannelKey", "Timeout", "PublicKey", "PrivateKey", "MaxRetryTimes", "AgreeUploadLog"}) } } @@ -897,6 +905,9 @@ func mergeConfigIns(ins *AggConfig) { if Global.BaseURL != "" { ins.BaseURL = Global.BaseURL } + if Global.ChannelKey != "" { + ins.ChannelKey = Global.ChannelKey + } if Global.Timeout != 0 { ins.Timeout = Global.Timeout } diff --git a/cmd/internal/platform/config_test.go b/cmd/internal/platform/config_test.go index 7d2a556322..307fa93e36 100644 --- a/cmd/internal/platform/config_test.go +++ b/cmd/internal/platform/config_test.go @@ -3,6 +3,7 @@ package platform import ( "io/ioutil" "os" + "strings" "testing" ) @@ -127,6 +128,70 @@ func TestOAuthFieldsRoundTrip(t *testing.T) { } } +// channel_key 写入后能读回(AC4)。覆盖跨层链路 copyToCLIConfig → config.json → Load, +// 任一层漏改都会表现为「能存不能读」或「存了不落盘」。 +func TestChannelKeyRoundTrip(t *testing.T) { + os.MkdirAll(".ucloud", 0700) + defer os.RemoveAll(".ucloud") + m, err := NewAggConfigManager(".ucloud/config.json", ".ucloud/credential.json") + if err != nil { + t.Fatal(err) + } + ac := &AggConfig{ + Profile: "combo", Active: true, BaseURL: "https://api.ucloud-global.com/", Timeout: 15, + MaxRetryTimes: intPtr(3), ChannelKey: "ch_combo_roundtrip", + } + if err := m.Append(ac); err != nil { + t.Fatal(err) + } + + m2, err := NewAggConfigManager(".ucloud/config.json", ".ucloud/credential.json") + if err != nil { + t.Fatal(err) + } + got, ok := m2.GetAggConfigByProfile("combo") + if !ok { + t.Fatal("profile combo missing after reload") + } + if got.ChannelKey != "ch_combo_roundtrip" { + t.Errorf("ChannelKey lost on round trip: got %q, want ch_combo_roundtrip", got.ChannelKey) + } + + // channel-key 是接入点配置而非凭据:绝不能落进 credential.json + raw, err := os.ReadFile(".ucloud/credential.json") + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(raw), "ch_combo_roundtrip") { + t.Error("channel_key must not be persisted to credential.json (it is an endpoint config, not a credential)") + } +} + +// 向后兼容(AC6):不含 channel_key 字段的旧 config.json 照常加载,且该值为空。 +func TestConfigWithoutChannelKeyLoads(t *testing.T) { + os.MkdirAll(".ucloud", 0700) + defer os.RemoveAll(".ucloud") + if err := os.WriteFile(".ucloud/config.json", + []byte(`[{"profile":"legacy","active":true,"base_url":"https://api.ucloud.cn/","timeout_sec":15,"max_retry_times":3}]`), 0600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(".ucloud/credential.json", + []byte(`[{"profile":"legacy","public_key":"pub","private_key":"pri"}]`), 0600); err != nil { + t.Fatal(err) + } + m, err := NewAggConfigManager(".ucloud/config.json", ".ucloud/credential.json") + if err != nil { + t.Fatal(err) + } + got, ok := m.GetAggConfigByProfile("legacy") + if !ok { + t.Fatal("legacy profile missing") + } + if got.ChannelKey != "" { + t.Errorf("ChannelKey = %q, want empty for a legacy config", got.ChannelKey) + } +} + // UpdateAggConfig 必须以传入的 config 为准:当传入指针与 map 内条目不是同一个对象时 // (如 `ucloud --profile <不存在>` 回退到包级默认 ConfigIns 而盘上已有同名 profile), // 不能静默把 map 里的旧数据存盘、丢掉调用方的数据。 diff --git a/cmd/root.go b/cmd/root.go index 40bd350a94..3230eaf332 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -180,16 +180,21 @@ func addProductCommands(root *cobra.Command, products []cli.Product, ctx *cli.Co } } -// applyGlobalOverrideFlags adds the five per-invocation override flags to +// applyGlobalOverrideFlags adds the per-invocation override flags to // every top-level command that is not in the exempt list. Running this after // both addPlatformCommands and addProductCommands ensures product commands // also receive the flags. +// +// Each flag registered here must also be scanned from os.Args in initGlobals: +// mergeConfigIns reads global.* before cobra parses flags, so registration +// alone is not enough for the override to take effect. func applyGlobalOverrideFlags(root *cobra.Command) { for _, c := range root.Commands() { if c.Name() != "init" && c.Name() != "gendoc" && c.Name() != "config" && c.Name() != "auth" { c.PersistentFlags().StringVar(&global.PublicKey, "public-key", global.PublicKey, "Set public-key to override the public-key in local config file") c.PersistentFlags().StringVar(&global.PrivateKey, "private-key", global.PrivateKey, "Set private-key to override the private-key in local config file") c.PersistentFlags().StringVar(&global.BaseURL, "base-url", "", "Set base-url to override the base-url in local config file") + c.PersistentFlags().StringVar(&global.ChannelKey, "channel-key", "", "Set channel-key to override the channel-key in local config file") c.PersistentFlags().IntVar(&global.Timeout, "timeout-sec", 0, "Set timeout-sec to override the timeout-sec in local config file") c.PersistentFlags().IntVar(&global.WaitTimeout, "wait-timeout-sec", 0, "Set the total timeout in seconds for synchronous wait/poll (e.g. cluster/host create). 0 uses the built-in default (600s)") c.PersistentFlags().IntVar(&global.MaxRetryTimes, "max-retry-times", -1, "Set max-retry-times to override the max-retry-times in local config file") @@ -316,6 +321,9 @@ func init() { if arg == "--base-url" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { global.BaseURL = os.Args[idx+1] } + if arg == "--channel-key" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { + global.ChannelKey = os.Args[idx+1] + } if arg == "--timeout-sec" && len(os.Args) > idx+1 && os.Args[idx+1] != "" { sec, err := strconv.Atoi(os.Args[idx+1]) if err != nil { diff --git a/hack/snapshot/testdata/cmdtree.golden b/hack/snapshot/testdata/cmdtree.golden index 193adca1c3..614457878d 100644 --- a/hack/snapshot/testdata/cmdtree.golden +++ b/hack/snapshot/testdata/cmdtree.golden @@ -14,6 +14,7 @@ ucloud config use=config short=add or update configurations flag=active short= default= required= flag=agree-upload-log short= default=false required= flag=base-url short= default= required= + flag=channel-key short= default= required= flag=max-retry-times short= default=0 required= flag=private-key short= default= required= flag=profile short= default= required= @@ -26,6 +27,7 @@ ucloud config add use=add short=add configuration flag=active short= default=false required= flag=agree-upload-log short= default=false required= flag=base-url short= default=https://api.ucloud.cn/ required= + flag=channel-key short= default= required= flag=max-retry-times short= default=3 required= flag=private-key short= default= required=true flag=profile short= default= required=true @@ -41,6 +43,7 @@ ucloud config update use=update short=update configurations flag=active short= default= required= flag=agree-upload-log short= default= required= flag=base-url short= default= required= + flag=channel-key short= default= required= flag=max-retry-times short= default= required= flag=private-key short= default= required= flag=profile short= default= required=true