Skip to content

Commit 237c602

Browse files
cevianclaude
andauthored
Add password_storage config file support for persistent configuration (#19)
* Add password_storage config file support for persistent configuration The global --password-storage flag was available but not persisted in config files, making it impossible to configure password storage globally and persistently. This was particularly problematic for MCP tools that needed consistent configuration. Changes: - Add PasswordStorage field to Config struct with proper mapstructure/yaml tags - Add DefaultPasswordStorage constant and use it consistently for CLI flag and config defaults - Add password_storage support to all config operations (set, unset, reset, show) - Add validation for password_storage values (keyring, pgpass, none) - Add password_storage to config show output (table, JSON, and YAML formats) - Add comprehensive test coverage for all password_storage config operations Now users can persistently configure password storage: - tiger config set password_storage pgpass - tiger config show (displays Password Storage: pgpass) - MCP tools can rely on this setting being globally persistent All precedence levels work correctly: CLI flag > Environment variable > Config file > Default 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * test: add password_storage cases to config test switch statements Address PR feedback from nathanjcochran: - Add password_storage case to TestConfigSet_ValidValues switch statement - Add password_storage case to TestConfigUnset_ValidKeys switch statement - Add default cases to both switch statements to catch unhandled test cases This ensures that all test cases for password_storage configuration are properly validated and prevents missing test coverage for future config options. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 14b831c commit 237c602

File tree

4 files changed

+81
-35
lines changed

4 files changed

+81
-35
lines changed

internal/tiger/cmd/config.go

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -140,22 +140,24 @@ func outputTable(cfg *config.Config, cmd *cobra.Command) error {
140140
fmt.Fprintf(out, " Service ID: %s\n", valueOrEmpty(cfg.ServiceID))
141141
fmt.Fprintf(out, " Output: %s\n", cfg.Output)
142142
fmt.Fprintf(out, " Analytics: %t\n", cfg.Analytics)
143+
fmt.Fprintf(out, " Password Storage: %s\n", cfg.PasswordStorage)
143144
fmt.Fprintf(out, " Debug: %t\n", cfg.Debug)
144145
fmt.Fprintf(out, " Config Dir: %s\n", cfg.ConfigDir)
145146
return nil
146147
}
147148

148149
func outputJSON(cfg *config.Config, cmd *cobra.Command) error {
149150
data := map[string]interface{}{
150-
"api_url": cfg.APIURL,
151-
"console_url": cfg.ConsoleURL,
152-
"gateway_url": cfg.GatewayURL,
153-
"project_id": cfg.ProjectID,
154-
"service_id": cfg.ServiceID,
155-
"output": cfg.Output,
156-
"analytics": cfg.Analytics,
157-
"debug": cfg.Debug,
158-
"config_dir": cfg.ConfigDir,
151+
"api_url": cfg.APIURL,
152+
"console_url": cfg.ConsoleURL,
153+
"gateway_url": cfg.GatewayURL,
154+
"project_id": cfg.ProjectID,
155+
"service_id": cfg.ServiceID,
156+
"output": cfg.Output,
157+
"analytics": cfg.Analytics,
158+
"password_storage": cfg.PasswordStorage,
159+
"debug": cfg.Debug,
160+
"config_dir": cfg.ConfigDir,
159161
}
160162

161163
encoder := json.NewEncoder(cmd.OutOrStdout())
@@ -165,15 +167,16 @@ func outputJSON(cfg *config.Config, cmd *cobra.Command) error {
165167

166168
func outputYAML(cfg *config.Config, cmd *cobra.Command) error {
167169
data := map[string]interface{}{
168-
"api_url": cfg.APIURL,
169-
"console_url": cfg.ConsoleURL,
170-
"gateway_url": cfg.GatewayURL,
171-
"project_id": cfg.ProjectID,
172-
"service_id": cfg.ServiceID,
173-
"output": cfg.Output,
174-
"analytics": cfg.Analytics,
175-
"debug": cfg.Debug,
176-
"config_dir": cfg.ConfigDir,
170+
"api_url": cfg.APIURL,
171+
"console_url": cfg.ConsoleURL,
172+
"gateway_url": cfg.GatewayURL,
173+
"project_id": cfg.ProjectID,
174+
"service_id": cfg.ServiceID,
175+
"output": cfg.Output,
176+
"analytics": cfg.Analytics,
177+
"password_storage": cfg.PasswordStorage,
178+
"debug": cfg.Debug,
179+
"config_dir": cfg.ConfigDir,
177180
}
178181

179182
encoder := yaml.NewEncoder(cmd.OutOrStdout())

internal/tiger/cmd/config_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ project_id: test-project
7070
service_id: test-service
7171
output: table
7272
analytics: false
73+
password_storage: pgpass
7374
`
7475
configFile := config.GetConfigFile(tmpDir)
7576
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
@@ -100,6 +101,9 @@ analytics: false
100101
if !strings.Contains(output, tmpDir) {
101102
t.Errorf("Output should contain config directory %s, got: %s", tmpDir, output)
102103
}
104+
if !strings.Contains(output, "pgpass") {
105+
t.Errorf("Output should contain password storage setting, got: %s", output)
106+
}
103107
}
104108

105109
func TestConfigShow_JSONOutput(t *testing.T) {
@@ -110,6 +114,7 @@ func TestConfigShow_JSONOutput(t *testing.T) {
110114
project_id: json-project
111115
output: json
112116
analytics: true
117+
password_storage: none
113118
`
114119
configFile := config.GetConfigFile(tmpDir)
115120
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
@@ -143,6 +148,9 @@ analytics: true
143148
if result["config_dir"] != tmpDir {
144149
t.Errorf("Expected config_dir '%s', got %v", tmpDir, result["config_dir"])
145150
}
151+
if result["password_storage"] != "none" {
152+
t.Errorf("Expected password_storage 'none', got %v", result["password_storage"])
153+
}
146154
}
147155

148156
func TestConfigShow_YAMLOutput(t *testing.T) {
@@ -153,6 +161,7 @@ func TestConfigShow_YAMLOutput(t *testing.T) {
153161
project_id: yaml-project
154162
output: yaml
155163
analytics: false
164+
password_storage: keyring
156165
`
157166
configFile := config.GetConfigFile(tmpDir)
158167
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
@@ -186,6 +195,9 @@ analytics: false
186195
if result["config_dir"] != tmpDir {
187196
t.Errorf("Expected config_dir '%s', got %v", tmpDir, result["config_dir"])
188197
}
198+
if result["password_storage"] != "keyring" {
199+
t.Errorf("Expected password_storage 'keyring', got %v", result["password_storage"])
200+
}
189201
}
190202

191203
func TestConfigShow_EmptyValues(t *testing.T) {
@@ -273,6 +285,9 @@ func TestConfigSet_ValidValues(t *testing.T) {
273285
{"service_id", "new-service", "Set service_id = new-service"},
274286
{"output", "json", "Set output = json"},
275287
{"analytics", "false", "Set analytics = false"},
288+
{"password_storage", "pgpass", "Set password_storage = pgpass"},
289+
{"password_storage", "none", "Set password_storage = none"},
290+
{"password_storage", "keyring", "Set password_storage = keyring"},
276291
}
277292

278293
for _, tt := range tests {
@@ -315,6 +330,12 @@ func TestConfigSet_ValidValues(t *testing.T) {
315330
if cfg.Analytics != expected {
316331
t.Errorf("Expected Analytics %t, got %t", expected, cfg.Analytics)
317332
}
333+
case "password_storage":
334+
if cfg.PasswordStorage != tt.value {
335+
t.Errorf("Expected PasswordStorage %s, got %s", tt.value, cfg.PasswordStorage)
336+
}
337+
default:
338+
t.Fatalf("Unhandled test case for key: %s", tt.key)
318339
}
319340
})
320341
}
@@ -330,6 +351,8 @@ func TestConfigSet_InvalidValues(t *testing.T) {
330351
}{
331352
{"output", "invalid", "invalid output format"},
332353
{"analytics", "maybe", "invalid analytics value"},
354+
{"password_storage", "invalid", "invalid password_storage value"},
355+
{"password_storage", "secure", "invalid password_storage value"},
333356
{"unknown", "value", "unknown configuration key"},
334357
}
335358

@@ -418,6 +441,7 @@ func TestConfigUnset_ValidKeys(t *testing.T) {
418441
cfg.Set("project_id", "test-project")
419442
cfg.Set("service_id", "test-service")
420443
cfg.Set("output", "json")
444+
cfg.Set("password_storage", "pgpass")
421445

422446
tests := []struct {
423447
key string
@@ -426,6 +450,7 @@ func TestConfigUnset_ValidKeys(t *testing.T) {
426450
{"project_id", "Unset project_id"},
427451
{"service_id", "Unset service_id"},
428452
{"output", "Unset output"},
453+
{"password_storage", "Unset password_storage"},
429454
}
430455

431456
for _, tt := range tests {
@@ -459,6 +484,12 @@ func TestConfigUnset_ValidKeys(t *testing.T) {
459484
if cfg.Output != config.DefaultOutput {
460485
t.Errorf("Expected default Output %s, got %s", config.DefaultOutput, cfg.Output)
461486
}
487+
case "password_storage":
488+
if cfg.PasswordStorage != config.DefaultPasswordStorage {
489+
t.Errorf("Expected default PasswordStorage %s, got %s", config.DefaultPasswordStorage, cfg.PasswordStorage)
490+
}
491+
default:
492+
t.Fatalf("Unhandled test case for key: %s", tt.key)
462493
}
463494
})
464495
}

internal/tiger/cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ tiger auth login
7575
cmd.PersistentFlags().StringVar(&projectID, "project-id", "", "project ID")
7676
cmd.PersistentFlags().StringVar(&serviceID, "service-id", "", "service ID")
7777
cmd.PersistentFlags().BoolVar(&analytics, "analytics", true, "enable/disable usage analytics")
78-
cmd.PersistentFlags().StringVar(&passwordStorage, "password-storage", "keyring", "password storage method (keyring, pgpass, none)")
78+
cmd.PersistentFlags().StringVar(&passwordStorage, "password-storage", config.DefaultPasswordStorage, "password storage method (keyring, pgpass, none)")
7979

8080
// Bind flags to viper
8181
viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug"))

internal/tiger/config/config.go

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,27 @@ import (
1010
)
1111

1212
type Config struct {
13-
APIURL string `mapstructure:"api_url" yaml:"api_url"`
14-
ConsoleURL string `mapstructure:"console_url" yaml:"console_url"`
15-
GatewayURL string `mapstructure:"gateway_url" yaml:"gateway_url"`
16-
ProjectID string `mapstructure:"project_id" yaml:"project_id"`
17-
ServiceID string `mapstructure:"service_id" yaml:"service_id"`
18-
Output string `mapstructure:"output" yaml:"output"`
19-
Analytics bool `mapstructure:"analytics" yaml:"analytics"`
20-
ConfigDir string `mapstructure:"config_dir" yaml:"-"`
21-
Debug bool `mapstructure:"debug" yaml:"debug"`
13+
APIURL string `mapstructure:"api_url" yaml:"api_url"`
14+
ConsoleURL string `mapstructure:"console_url" yaml:"console_url"`
15+
GatewayURL string `mapstructure:"gateway_url" yaml:"gateway_url"`
16+
ProjectID string `mapstructure:"project_id" yaml:"project_id"`
17+
ServiceID string `mapstructure:"service_id" yaml:"service_id"`
18+
Output string `mapstructure:"output" yaml:"output"`
19+
Analytics bool `mapstructure:"analytics" yaml:"analytics"`
20+
PasswordStorage string `mapstructure:"password_storage" yaml:"password_storage"`
21+
ConfigDir string `mapstructure:"config_dir" yaml:"-"`
22+
Debug bool `mapstructure:"debug" yaml:"debug"`
2223
}
2324

2425
const (
25-
DefaultAPIURL = "https://console.cloud.timescale.com/public/api/v1"
26-
DefaultConsoleURL = "https://console.cloud.timescale.com"
27-
DefaultGatewayURL = "https://console.cloud.timescale.com/api"
28-
DefaultOutput = "table"
29-
DefaultAnalytics = true
30-
DefaultDebug = false
31-
ConfigFileName = "config.yaml"
26+
DefaultAPIURL = "https://console.cloud.timescale.com/public/api/v1"
27+
DefaultConsoleURL = "https://console.cloud.timescale.com"
28+
DefaultGatewayURL = "https://console.cloud.timescale.com/api"
29+
DefaultOutput = "table"
30+
DefaultAnalytics = true
31+
DefaultPasswordStorage = "keyring"
32+
DefaultDebug = false
33+
ConfigFileName = "config.yaml"
3234
)
3335

3436
// SetupViper configures the global Viper instance with defaults, env vars, and config file
@@ -49,6 +51,7 @@ func SetupViper(configDir string) error {
4951
viper.SetDefault("service_id", "")
5052
viper.SetDefault("output", DefaultOutput)
5153
viper.SetDefault("analytics", DefaultAnalytics)
54+
viper.SetDefault("password_storage", DefaultPasswordStorage)
5255
viper.SetDefault("debug", DefaultDebug)
5356

5457
// Try to read config file if it exists
@@ -97,6 +100,7 @@ func (c *Config) Save() error {
97100
viper.Set("service_id", c.ServiceID)
98101
viper.Set("output", c.Output)
99102
viper.Set("analytics", c.Analytics)
103+
viper.Set("password_storage", c.PasswordStorage)
100104
viper.Set("debug", c.Debug)
101105

102106
if err := viper.WriteConfigAs(configFile); err != nil {
@@ -139,6 +143,11 @@ func (c *Config) Set(key, value string) error {
139143
} else {
140144
return fmt.Errorf("invalid debug value: %s (must be true or false)", value)
141145
}
146+
case "password_storage":
147+
if value != "keyring" && value != "pgpass" && value != "none" {
148+
return fmt.Errorf("invalid password_storage value: %s (must be keyring, pgpass, or none)", value)
149+
}
150+
c.PasswordStorage = value
142151
default:
143152
return fmt.Errorf("unknown configuration key: %s", key)
144153
}
@@ -164,6 +173,8 @@ func (c *Config) Unset(key string) error {
164173
c.Analytics = DefaultAnalytics
165174
case "debug":
166175
c.Debug = DefaultDebug
176+
case "password_storage":
177+
c.PasswordStorage = DefaultPasswordStorage
167178
default:
168179
return fmt.Errorf("unknown configuration key: %s", key)
169180
}
@@ -179,6 +190,7 @@ func (c *Config) Reset() error {
179190
c.ServiceID = ""
180191
c.Output = DefaultOutput
181192
c.Analytics = DefaultAnalytics
193+
c.PasswordStorage = DefaultPasswordStorage
182194
c.Debug = DefaultDebug
183195

184196
return c.Save()

0 commit comments

Comments
 (0)