diff --git a/cmd/config.go b/cmd/config.go index bbb51bc..f211565 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -22,14 +22,16 @@ func NewConfigCmd() *cobra.Command { } masked := *cfg - masked.APIKey = maskAPIKey(masked.APIKey) + masked.APIKey = maskSecret(masked.APIKey) + masked.RegistryPassword = maskSecret(masked.RegistryPassword) return writeConfig(cmd.OutOrStdout(), &masked) }, } } -func maskAPIKey(apiKey string) string { - if apiKey == "" { +// maskSecret masks a sensitive config value. +func maskSecret(secret string) string { + if secret == "" { return "" } return "****" diff --git a/cmd/config_test.go b/cmd/config_test.go index c98a01b..cc1eeb6 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -17,6 +17,8 @@ func TestConfigCmd(t *testing.T) { t.Setenv("UCLOUD_SANDBOX_REGION", "cn-sh") t.Setenv("UCLOUD_SANDBOX_DOMAIN", "") t.Setenv("UCLOUD_SANDBOX_INSECURE_HTTP", "true") + t.Setenv("UCLOUD_SANDBOX_REGISTRY_USERNAME", "registry-user") + t.Setenv("UCLOUD_SANDBOX_REGISTRY_PASSWORD", "registry-pass") var output bytes.Buffer cmd := NewConfigCmd() @@ -26,9 +28,12 @@ func TestConfigCmd(t *testing.T) { assert.JSONEq(t, `{ "api_key": "****", "region": "cn-sh", - "insecure_http": true + "insecure_http": true, + "registry_username": "registry-user", + "registry_password": "****" }`, output.String()) assert.NotContains(t, output.String(), "abcd12345678wxyz") + assert.NotContains(t, output.String(), "registry-pass") } func TestConfigCmd_UsesEffectiveConfig(t *testing.T) { @@ -43,7 +48,9 @@ func TestConfigCmd_UsesEffectiveConfig(t *testing.T) { require.NoError(t, os.MkdirAll(configDir, 0700)) require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.json"), []byte(`{ "api_key": "file-api-key", - "region": "cn-bj" + "region": "cn-bj", + "registry_username": "file-user", + "registry_password": "file-pass" }`), 0600)) var output bytes.Buffer @@ -53,12 +60,15 @@ func TestConfigCmd_UsesEffectiveConfig(t *testing.T) { assert.Contains(t, output.String(), `"api_key": "****"`) assert.Contains(t, output.String(), `"region": "cn-bj"`) + assert.Contains(t, output.String(), `"registry_username": "file-user"`) + assert.Contains(t, output.String(), `"registry_password": "****"`) assert.NotContains(t, output.String(), "environment-key") assert.NotContains(t, output.String(), "file-api-key") + assert.NotContains(t, output.String(), "file-pass") } -func TestMaskAPIKey(t *testing.T) { - assert.Empty(t, maskAPIKey("")) - assert.Equal(t, "****", maskAPIKey("short")) - assert.Equal(t, "****", maskAPIKey("abcd1234wxyz")) +func TestMaskSecret(t *testing.T) { + assert.Empty(t, maskSecret("")) + assert.Equal(t, "****", maskSecret("short")) + assert.Equal(t, "****", maskSecret("abcd1234wxyz")) } diff --git a/cmd/template/build.go b/cmd/template/build.go index 66a784d..f914f20 100644 --- a/cmd/template/build.go +++ b/cmd/template/build.go @@ -109,14 +109,16 @@ func runBuild(name string, flags *buildFlags) error { if flags.startCmd == "" && flags.readyCmd != "" { return fmt.Errorf("both --cmd and --ready-cmd must be provided together") } - if (flags.registryUsername == "") != (flags.registryPassword == "") { - return fmt.Errorf("both --registry-username and --registry-password must be provided together") - } cfg, err := config.Load() if err != nil { return err } + applyRegistryCredentials(flags, cfg) + if (flags.registryUsername == "") != (flags.registryPassword == "") { + return fmt.Errorf("both --registry-username and --registry-password must be provided together (or set them in the config file)") + } + client, err := config.NewClient(cfg) if err != nil { return err @@ -160,6 +162,17 @@ func runBuild(name string, flags *buildFlags) error { return nil } +// applyRegistryCredentials fills missing registry credentials from the global +// config; explicit flags take precedence. +func applyRegistryCredentials(flags *buildFlags, cfg *config.Config) { + if flags.registryUsername == "" { + flags.registryUsername = cfg.RegistryUsername + } + if flags.registryPassword == "" { + flags.registryPassword = cfg.RegistryPassword + } +} + // resolveBuildContext prefers the template's named directory, then --path itself. func resolveBuildContext(name, path string) (string, *LocalConfig) { candidate := filepath.Join(path, name) diff --git a/internal/config/config.go b/internal/config/config.go index dea05cb..12392a0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,14 +21,19 @@ const ( envRegion = "UCLOUD_SANDBOX_REGION" envDomain = "UCLOUD_SANDBOX_DOMAIN" envInsecureHTTP = "UCLOUD_SANDBOX_INSECURE_HTTP" + + envRegistryUsername = "UCLOUD_SANDBOX_REGISTRY_USERNAME" + envRegistryPassword = "UCLOUD_SANDBOX_REGISTRY_PASSWORD" ) // Config holds the CLI configuration. type Config struct { - APIKey string `json:"api_key,omitempty"` - Region string `json:"region,omitempty"` - Domain string `json:"domain,omitempty"` - InsecureHTTP bool `json:"insecure_http,omitempty"` + APIKey string `json:"api_key,omitempty"` + Region string `json:"region,omitempty"` + Domain string `json:"domain,omitempty"` + InsecureHTTP bool `json:"insecure_http,omitempty"` + RegistryUsername string `json:"registry_username,omitempty"` + RegistryPassword string `json:"registry_password,omitempty"` } // configPath returns the path to the config file. @@ -75,6 +80,12 @@ func Load() (*Config, error) { } cfg.InsecureHTTP = insecureHTTP } + if v := os.Getenv(envRegistryUsername); v != "" { + cfg.RegistryUsername = v + } + if v := os.Getenv(envRegistryPassword); v != "" { + cfg.RegistryPassword = v + } return cfg, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 16325d3..a5b751e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -27,7 +27,7 @@ func writeConfig(t *testing.T, home string, cfg *Config) { func clearEnv(t *testing.T) { t.Helper() - for _, k := range []string{envAPIKey, envRegion, envDomain, envInsecureHTTP} { + for _, k := range []string{envAPIKey, envRegion, envDomain, envInsecureHTTP, envRegistryUsername, envRegistryPassword} { t.Setenv(k, "") } } @@ -35,13 +35,16 @@ func clearEnv(t *testing.T) { func TestLoad_FileOnly(t *testing.T) { home := setupHome(t) clearEnv(t) - writeConfig(t, home, &Config{APIKey: "key1", Region: "cn-sh", InsecureHTTP: true}) + writeConfig(t, home, &Config{APIKey: "key1", Region: "cn-sh", InsecureHTTP: true, + RegistryUsername: "file-user", RegistryPassword: "file-pass"}) cfg, err := Load() require.NoError(t, err) assert.Equal(t, "key1", cfg.APIKey) assert.Equal(t, "cn-sh", cfg.Region) assert.True(t, cfg.InsecureHTTP) + assert.Equal(t, "file-user", cfg.RegistryUsername) + assert.Equal(t, "file-pass", cfg.RegistryPassword) } func TestLoad_EnvOverride(t *testing.T) { @@ -51,6 +54,8 @@ func TestLoad_EnvOverride(t *testing.T) { t.Setenv(envRegion, "env-region") t.Setenv(envDomain, "env.example.com") t.Setenv(envInsecureHTTP, "false") + t.Setenv(envRegistryUsername, "env-user") + t.Setenv(envRegistryPassword, "env-pass") cfg, err := Load() require.NoError(t, err) @@ -58,6 +63,8 @@ func TestLoad_EnvOverride(t *testing.T) { assert.Equal(t, "env-region", cfg.Region) assert.Equal(t, "env.example.com", cfg.Domain) assert.False(t, cfg.InsecureHTTP) + assert.Equal(t, "env-user", cfg.RegistryUsername) + assert.Equal(t, "env-pass", cfg.RegistryPassword) } func TestLoad_EnvInsecureHTTPTrue(t *testing.T) { @@ -94,7 +101,8 @@ func TestLoad_NoFile(t *testing.T) { func TestSave(t *testing.T) { home := setupHome(t) - in := &Config{APIKey: "save-key", Region: "cn-bj", InsecureHTTP: true} + in := &Config{APIKey: "save-key", Region: "cn-bj", InsecureHTTP: true, + RegistryUsername: "save-user", RegistryPassword: "save-pass"} require.NoError(t, Save(in)) data, err := os.ReadFile(filepath.Join(home, configDir, configFile)) @@ -104,6 +112,8 @@ func TestSave(t *testing.T) { assert.Equal(t, in.APIKey, out.APIKey) assert.Equal(t, in.Region, out.Region) assert.Equal(t, in.InsecureHTTP, out.InsecureHTTP) + assert.Equal(t, in.RegistryUsername, out.RegistryUsername) + assert.Equal(t, in.RegistryPassword, out.RegistryPassword) assert.Contains(t, string(data), `"insecure_http": true`) assert.NotContains(t, string(data), `"insecure":`) }