From 3300c4f111269260abef27f4d40dce537baaf825 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Fri, 17 Oct 2025 16:56:07 -0400 Subject: [PATCH 1/9] Remove project_id config and global flag, store in keyring instead --- internal/tiger/cmd/auth.go | 32 +-- internal/tiger/cmd/auth_test.go | 222 +++++++-------- internal/tiger/cmd/auth_validation_test.go | 71 +++-- internal/tiger/cmd/config.go | 3 - internal/tiger/cmd/config_test.go | 56 +--- internal/tiger/cmd/db.go | 15 +- internal/tiger/cmd/db_test.go | 90 +++--- internal/tiger/cmd/root.go | 3 - internal/tiger/cmd/root_test.go | 5 +- internal/tiger/cmd/service.go | 79 ++---- internal/tiger/cmd/service_test.go | 305 +++++++++------------ internal/tiger/config/api_key.go | 151 ++++++---- internal/tiger/config/api_key_test.go | 108 ++++---- internal/tiger/config/config.go | 17 -- internal/tiger/config/config_test.go | 56 +--- internal/tiger/mcp/db_tools.go | 14 +- internal/tiger/mcp/server.go | 23 +- internal/tiger/mcp/service_tools.go | 56 ++-- 18 files changed, 547 insertions(+), 759 deletions(-) diff --git a/internal/tiger/cmd/auth.go b/internal/tiger/cmd/auth.go index c18afef4..bb4117a9 100644 --- a/internal/tiger/cmd/auth.go +++ b/internal/tiger/cmd/auth.go @@ -117,7 +117,7 @@ Examples: } } - // Combine the keys in the format "public:secret" for storage + // Combine the keys in the format "public:secret" for validation apiKey := fmt.Sprintf("%s:%s", creds.publicKey, creds.secretKey) // Validate the API key by making a test API call @@ -126,17 +126,11 @@ Examples: return fmt.Errorf("API key validation failed: %w", err) } - // Store the API key securely - if err := config.StoreAPIKey(apiKey); err != nil { - return fmt.Errorf("failed to store API key: %w", err) + // Store the credentials (API key + project ID) together securely + if err := config.StoreCredentials(apiKey, creds.projectID); err != nil { + return fmt.Errorf("failed to store credentials: %w", err) } - fmt.Fprintln(cmd.OutOrStdout(), "Successfully logged in and stored API key") - - // Store project ID in config if provided - if err := storeProjectID(creds.projectID); err != nil { - return fmt.Errorf("failed to store project ID: %w", err) - } - fmt.Fprintf(cmd.OutOrStdout(), "Set default project ID to: %s\n", creds.projectID) + fmt.Fprintf(cmd.OutOrStdout(), "Successfully logged in (project: %s)\n", creds.projectID) // Show helpful next steps fmt.Fprint(cmd.OutOrStdout(), nextStepsMessage) @@ -161,8 +155,8 @@ func buildLogoutCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - if err := config.RemoveAPIKey(); err != nil { - return fmt.Errorf("failed to remove API key: %w", err) + if err := config.RemoveCredentials(); err != nil { + return fmt.Errorf("failed to remove credentials: %w", err) } fmt.Fprintln(cmd.OutOrStdout(), "Successfully logged out and removed stored credentials") @@ -179,7 +173,7 @@ func buildStatusCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - if _, err := config.GetAPIKey(); err != nil { + if _, _, err := config.GetCredentials(); err != nil { return err } @@ -256,13 +250,3 @@ func promptForCredentials(consoleURL string, creds credentials) (credentials, er return creds, nil } - -// storeProjectID stores the project ID in the configuration file -func storeProjectID(projectID string) error { - cfg, err := config.Load() - if err != nil { - return fmt.Errorf("failed to load config: %w", err) - } - - return cfg.Set("project_id", projectID) -} diff --git a/internal/tiger/cmd/auth_test.go b/internal/tiger/cmd/auth_test.go index cb998936..30c40f57 100644 --- a/internal/tiger/cmd/auth_test.go +++ b/internal/tiger/cmd/auth_test.go @@ -32,27 +32,29 @@ func setupAuthTest(t *testing.T) string { return nil } - // Aggressively clean up any existing keyring entries before starting - // Uses a test-specific keyring entry. - config.RemoveAPIKeyFromKeyring() - // Create temporary directory for test config tmpDir, err := os.MkdirTemp("", "tiger-auth-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - // Set temporary config directory + // Set TIGER_CONFIG_DIR environment variable so that when commands execute + // and reinitialize viper, they use the test directory os.Setenv("TIGER_CONFIG_DIR", tmpDir) // Reset global config and viper to ensure test isolation + // This ensures proper test isolation by resetting all viper state + // MUST be done before RemoveCredentials() so it uses the test directory! if _, err := config.UseTestConfig(tmpDir, map[string]any{}); err != nil { t.Fatalf("Failed to use test config: %v", err) } + // Clean up any existing test credentials + config.RemoveCredentials() + t.Cleanup(func() { - // Clean up test keyring - config.RemoveAPIKeyFromKeyring() + // Clean up test credentials + config.RemoveCredentials() // Reset global config and viper first config.ResetGlobalConfig() validateAPIKeyForLogin = originalValidator // Restore original validator @@ -82,7 +84,7 @@ func executeAuthCommand(args ...string) (string, error) { } func TestAuthLogin_KeyAndProjectIDFlags(t *testing.T) { - tmpDir := setupAuthTest(t) + setupAuthTest(t) // Execute login command with public and secret key flags and project ID output, err := executeAuthCommand("auth", "login", "--public-key", "test-public-key", "--secret-key", "test-secret-key", "--project-id", "test-project-123") @@ -90,38 +92,26 @@ func TestAuthLogin_KeyAndProjectIDFlags(t *testing.T) { t.Fatalf("Login failed: %v", err) } - expectedOutput := "Validating API key...\nSuccessfully logged in and stored API key\nSet default project ID to: test-project-123\n" + nextStepsMessage + expectedOutput := "Validating API key...\nSuccessfully logged in (project: test-project-123)\n" + nextStepsMessage if output != expectedOutput { t.Errorf("Unexpected output: '%s'", output) } - // Verify API key was stored (try keyring first, then file fallback) + // Verify credentials were stored (try keyring first, then file fallback) // The combined key should be in format "public:secret" expectedAPIKey := "test-public-key:test-secret-key" - apiKey, err := config.GetAPIKeyFromKeyring() + expectedProjectID := "test-project-123" + + apiKey, projectID, err := config.GetCredentials() if err != nil { - // Keyring failed, check file fallback - apiKeyFile := filepath.Join(tmpDir, "api-key") - data, err := os.ReadFile(apiKeyFile) - if err != nil { - t.Fatalf("API key not stored in keyring or file: %v", err) - } - if string(data) != expectedAPIKey { - t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, string(data)) - } - } else { - if apiKey != expectedAPIKey { - t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, apiKey) - } + t.Fatalf("Credentials not stored in keyring or file: %v", err) } - // Verify project ID was stored in config - cfg, err := config.Load() - if err != nil { - t.Fatalf("Failed to load config: %v", err) + if apiKey != expectedAPIKey { + t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, apiKey) } - if cfg.ProjectID != "test-project-123" { - t.Errorf("Expected project ID 'test-project-123', got '%s'", cfg.ProjectID) + if projectID != expectedProjectID { + t.Errorf("Expected project ID '%s', got '%s'", expectedProjectID, projectID) } } @@ -141,9 +131,9 @@ func TestAuthLogin_KeyFlags_NoProjectID(t *testing.T) { t.Errorf("Expected error to contain %q, got: %v", expectedErrorMsg, err) } - // Verify no API key was stored since login failed - if _, err = config.GetAPIKey(); err == nil { - t.Error("API key should not be stored when login fails") + // Verify no credentials were stored since login failed + if _, _, err = config.GetCredentials(); err == nil { + t.Error("Credentials should not be stored when login fails") } } @@ -164,20 +154,24 @@ func TestAuthLogin_KeyAndProjectIDEnvironmentVariables(t *testing.T) { t.Fatalf("Login failed: %v", err) } - expectedOutput := "Validating API key...\nSuccessfully logged in and stored API key\nSet default project ID to: env-project-id\n" + nextStepsMessage + expectedOutput := "Validating API key...\nSuccessfully logged in (project: env-project-id)\n" + nextStepsMessage if output != expectedOutput { t.Errorf("Unexpected output: '%s'", output) } - // Verify API key was stored (should be combined format) + // Verify credentials were stored expectedAPIKey := "env-public-key:env-secret-key" - storedKey, err := config.GetAPIKey() + expectedProjectID := "env-project-id" + storedKey, storedProjectID, err := config.GetCredentials() if err != nil { - t.Fatalf("Failed to get stored API key: %v", err) + t.Fatalf("Failed to get stored credentials: %v", err) } if storedKey != expectedAPIKey { t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, storedKey) } + if storedProjectID != expectedProjectID { + t.Errorf("Expected project ID '%s', got '%s'", expectedProjectID, storedProjectID) + } } func TestAuthLogin_KeyEnvironmentVariables_ProjectIDFlag(t *testing.T) { @@ -195,20 +189,24 @@ func TestAuthLogin_KeyEnvironmentVariables_ProjectIDFlag(t *testing.T) { t.Fatalf("Login failed: %v", err) } - expectedOutput := "Validating API key...\nSuccessfully logged in and stored API key\nSet default project ID to: test-project-456\n" + nextStepsMessage + expectedOutput := "Validating API key...\nSuccessfully logged in (project: test-project-456)\n" + nextStepsMessage if output != expectedOutput { t.Errorf("Unexpected output: '%s'", output) } - // Verify API key was stored (should be combined format) + // Verify credentials were stored expectedAPIKey := "env-public-key:env-secret-key" - storedKey, err := config.GetAPIKey() + expectedProjectID := "test-project-456" + storedKey, storedProjectID, err := config.GetCredentials() if err != nil { - t.Fatalf("Failed to get stored API key: %v", err) + t.Fatalf("Failed to get stored credentials: %v", err) } if storedKey != expectedAPIKey { t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, storedKey) } + if storedProjectID != expectedProjectID { + t.Errorf("Expected project ID '%s', got '%s'", expectedProjectID, storedProjectID) + } } // setupOAuthTest creates a complete OAuth test environment with mock server and browser @@ -447,8 +445,8 @@ func TestAuthLogin_OAuth_SingleProject(t *testing.T) { expectedPattern := fmt.Sprintf(`^Auth URL is: %s/oauth/authorize\?client_id=45e1b16d-e435-4049-97b2-8daad150818c&code_challenge=[A-Za-z0-9_-]+&code_challenge_method=S256&redirect_uri=http%%3A%%2F%%2Flocalhost%%3A\d+%%2Fcallback&response_type=code&state=[A-Za-z0-9_-]+\n`+ `Opening browser for authentication\.\.\.\n`+ `Validating API key\.\.\.\n`+ - `Successfully logged in and stored API key\n`+ - `Set default project ID to: project-123\n`+regexp.QuoteMeta(nextStepsMessage)+`$`, regexp.QuoteMeta(mockServerURL)) + `Successfully logged in \(project: project-123\)\n`+ + regexp.QuoteMeta(nextStepsMessage)+`$`, regexp.QuoteMeta(mockServerURL)) matched, err := regexp.MatchString(expectedPattern, output) if err != nil { @@ -458,25 +456,18 @@ func TestAuthLogin_OAuth_SingleProject(t *testing.T) { t.Errorf("Output doesn't match expected pattern.\nPattern: %s\nActual output: '%s'", expectedPattern, output) } - // Verify API key was stored - storedKey, err := config.GetAPIKey() + // Verify credentials were stored + expectedAPIKey := "test-access-key:test-secret-key" + expectedProjectID := "project-123" + storedKey, storedProjectID, err := config.GetCredentials() if err != nil { - t.Fatalf("Failed to get stored API key: %v", err) + t.Fatalf("Failed to get stored credentials: %v", err) } - - // Expected API key is "test-access-key:test-secret-key" - expectedAPIKey := "test-access-key:test-secret-key" if storedKey != expectedAPIKey { t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, storedKey) } - - // Verify project ID was stored in config - cfg, err := config.Load() - if err != nil { - t.Fatalf("Failed to load config: %v", err) - } - if cfg.ProjectID != "project-123" { - t.Errorf("Expected project ID 'project-123', got '%s'", cfg.ProjectID) + if storedProjectID != expectedProjectID { + t.Errorf("Expected project ID '%s', got '%s'", expectedProjectID, storedProjectID) } } @@ -510,8 +501,8 @@ func TestAuthLogin_OAuth_MultipleProjects(t *testing.T) { expectedPattern := fmt.Sprintf(`^Auth URL is: %s/oauth/authorize\?client_id=45e1b16d-e435-4049-97b2-8daad150818c&code_challenge=[A-Za-z0-9_-]+&code_challenge_method=S256&redirect_uri=http%%3A%%2F%%2Flocalhost%%3A\d+%%2Fcallback&response_type=code&state=[A-Za-z0-9_-]+\n`+ `Opening browser for authentication\.\.\.\n`+ `Validating API key\.\.\.\n`+ - `Successfully logged in and stored API key\n`+ - `Set default project ID to: project-789\n`+regexp.QuoteMeta(nextStepsMessage)+`$`, regexp.QuoteMeta(mockServerURL)) + `Successfully logged in \(project: project-789\)\n`+ + regexp.QuoteMeta(nextStepsMessage)+`$`, regexp.QuoteMeta(mockServerURL)) matched, err := regexp.MatchString(expectedPattern, output) if err != nil { @@ -521,25 +512,18 @@ func TestAuthLogin_OAuth_MultipleProjects(t *testing.T) { t.Errorf("Output doesn't match expected pattern.\nPattern: %s\nActual output: '%s'", expectedPattern, output) } - // Verify API key was stored - storedKey, err := config.GetAPIKey() + // Verify credentials were stored (should be the third project - project-789) + expectedAPIKey := "test-access-key:test-secret-key" + expectedProjectID := "project-789" + storedKey, storedProjectID, err := config.GetCredentials() if err != nil { - t.Fatalf("Failed to get stored API key: %v", err) + t.Fatalf("Failed to get stored credentials: %v", err) } - - // Expected API key is "test-access-key:test-secret-key" - expectedAPIKey := "test-access-key:test-secret-key" if storedKey != expectedAPIKey { t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, storedKey) } - - // Verify project ID was stored in config (should be the third project - project-789) - cfg, err := config.Load() - if err != nil { - t.Fatalf("Failed to load config: %v", err) - } - if cfg.ProjectID != "project-789" { - t.Errorf("Expected project ID 'project-789', got '%s'", cfg.ProjectID) + if storedProjectID != expectedProjectID { + t.Errorf("Expected project ID '%s', got '%s'", expectedProjectID, storedProjectID) } } @@ -556,32 +540,35 @@ func TestAuthLogin_KeyringFallback(t *testing.T) { t.Fatalf("Login failed: %v", err) } - expectedOutput := "Validating API key...\nSuccessfully logged in and stored API key\nSet default project ID to: test-project-fallback\n" + nextStepsMessage + expectedOutput := "Validating API key...\nSuccessfully logged in (project: test-project-fallback)\n" + nextStepsMessage if output != expectedOutput { t.Errorf("Unexpected output: '%s'", output) } // Force test file storage scenario by directly checking file - apiKeyFile := filepath.Join(tmpDir, "api-key") + credentialsFile := filepath.Join(tmpDir, "credentials") - // If keyring worked, manually create file scenario by removing keyring and adding file - config.RemoveAPIKeyFromKeyring() + // If keyring worked, manually create file scenario by clearing all credentials and adding to file + config.RemoveCredentials() - // Store to file manually to simulate fallback (combined format) + // Store to file manually to simulate fallback expectedAPIKey := "fallback-public:fallback-secret" - err = config.StoreAPIKeyToFile(expectedAPIKey) - if err != nil { - t.Fatalf("Failed to store API key to file: %v", err) + expectedProjectID := "test-project-fallback" + if err := config.StoreCredentialsToFile(expectedAPIKey, expectedProjectID); err != nil { + t.Fatalf("Failed to store credentials to file: %v", err) } // Verify file storage works - storedKey, err := config.GetAPIKey() + storedKey, storedProjectID, err := config.GetCredentials() if err != nil { - t.Fatalf("Failed to get API key from file fallback: %v", err) + t.Fatalf("Failed to get credentials from file fallback: %v", err) } if storedKey != expectedAPIKey { t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, storedKey) } + if storedProjectID != expectedProjectID { + t.Errorf("Expected project ID '%s', got '%s'", expectedProjectID, storedProjectID) + } // Test status with file-only storage output, err = executeAuthCommand("auth", "status") @@ -602,17 +589,14 @@ func TestAuthLogin_KeyringFallback(t *testing.T) { } // Verify file was removed - if _, err := os.Stat(apiKeyFile); !os.IsNotExist(err) { - t.Error("API key file should be removed after logout") + if _, err := os.Stat(credentialsFile); !os.IsNotExist(err) { + t.Error("Credentials file should be removed after logout") } } // TestAuthLogin_EnvironmentVariable_FileOnly tests env var login when only file storage is available func TestAuthLogin_EnvironmentVariable_FileOnly(t *testing.T) { - tmpDir := setupAuthTest(t) - - // Clear any keyring entries to force file-only storage - config.RemoveAPIKeyFromKeyring() + setupAuthTest(t) // Set environment variables for public key, secret key, and project ID os.Setenv("TIGER_PUBLIC_KEY", "env-file-public") @@ -628,51 +612,43 @@ func TestAuthLogin_EnvironmentVariable_FileOnly(t *testing.T) { t.Fatalf("Login failed: %v", err) } - expectedOutput := "Validating API key...\nSuccessfully logged in and stored API key\nSet default project ID to: test-project-env-file\n" + nextStepsMessage + expectedOutput := "Validating API key...\nSuccessfully logged in (project: test-project-env-file)\n" + nextStepsMessage if output != expectedOutput { t.Errorf("Unexpected output: '%s'", output) } - // Clear keyring again to ensure we're testing file-only retrieval - config.RemoveAPIKeyFromKeyring() + // Clear all credentials to ensure we're testing file-only retrieval + config.RemoveCredentials() - // Verify API key was stored in file (since keyring is cleared) + // Verify credentials were stored in file (since we'll manually write to file only) expectedAPIKey := "env-file-public:env-file-secret" - apiKeyFile := filepath.Join(tmpDir, "api-key") - data, err := os.ReadFile(apiKeyFile) - if err != nil { - // If file doesn't exist, the keyring might have worked, so manually ensure file storage - err = config.StoreAPIKeyToFile(expectedAPIKey) - if err != nil { - t.Fatalf("Failed to store API key to file: %v", err) - } - data, err = os.ReadFile(apiKeyFile) - if err != nil { - t.Fatalf("API key file should exist: %v", err) - } - } + expectedProjectID := "test-project-env-file" - if string(data) != expectedAPIKey { - t.Errorf("Expected API key '%s' in file, got '%s'", expectedAPIKey, string(data)) + // Store to file manually to simulate fallback scenario + if err := config.StoreCredentialsToFile(expectedAPIKey, expectedProjectID); err != nil { + t.Fatalf("Failed to store credentials to file: %v", err) } - // Verify getAPIKey works with file-only storage - storedKey, err := config.GetAPIKey() + // Verify getCredentials works with file-only storage + storedKey, storedProjectID, err := config.GetCredentials() if err != nil { - t.Fatalf("Failed to get API key from file: %v", err) + t.Fatalf("Failed to get credentials from file: %v", err) } if storedKey != expectedAPIKey { t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, storedKey) } + if storedProjectID != expectedProjectID { + t.Errorf("Expected project ID '%s', got '%s'", expectedProjectID, storedProjectID) + } } func TestAuthStatus_LoggedIn(t *testing.T) { setupAuthTest(t) - // Store API key first - err := config.StoreAPIKey("test-api-key-789") + // Store credentials first + err := config.StoreCredentials("test-api-key-789", "test-project-789") if err != nil { - t.Fatalf("Failed to store API key: %v", err) + t.Fatalf("Failed to store credentials: %v", err) } // Execute status command @@ -704,16 +680,16 @@ func TestAuthStatus_NotLoggedIn(t *testing.T) { func TestAuthLogout_Success(t *testing.T) { setupAuthTest(t) - // Store API key first - err := config.StoreAPIKey("test-api-key-logout") + // Store credentials first + err := config.StoreCredentials("test-api-key-logout", "test-project-logout") if err != nil { - t.Fatalf("Failed to store API key: %v", err) + t.Fatalf("Failed to store credentials: %v", err) } - // Verify API key is stored - _, err = config.GetAPIKey() + // Verify credentials are stored + _, _, err = config.GetCredentials() if err != nil { - t.Fatalf("API key should be stored: %v", err) + t.Fatalf("Credentials should be stored: %v", err) } // Execute logout command @@ -726,9 +702,9 @@ func TestAuthLogout_Success(t *testing.T) { t.Errorf("Unexpected output: '%s' (len=%d)", output, len(output)) } - // Verify API key is removed - _, err = config.GetAPIKey() + // Verify credentials are removed + _, _, err = config.GetCredentials() if err == nil { - t.Fatal("API key should be removed after logout") + t.Fatal("Credentials should be removed after logout") } } diff --git a/internal/tiger/cmd/auth_validation_test.go b/internal/tiger/cmd/auth_validation_test.go index c8888728..a49c2155 100644 --- a/internal/tiger/cmd/auth_validation_test.go +++ b/internal/tiger/cmd/auth_validation_test.go @@ -3,7 +3,6 @@ package cmd import ( "errors" "os" - "path/filepath" "strings" "testing" @@ -32,13 +31,15 @@ func TestAuthLogin_APIKeyValidationFailure(t *testing.T) { validateAPIKeyForLogin = originalValidator }() - // Set temporary config directory - os.Setenv("TIGER_CONFIG_DIR", tmpDir) - defer os.Unsetenv("TIGER_CONFIG_DIR") + // Initialize viper with test directory BEFORE calling RemoveCredentials() + // This ensures RemoveCredentials() operates on the test directory, not the user's real directory + if _, err := config.UseTestConfig(tmpDir, map[string]any{}); err != nil { + t.Fatalf("Failed to use test config: %v", err) + } - // Clean up keyring - config.RemoveAPIKeyFromKeyring() - defer config.RemoveAPIKeyFromKeyring() + // Clean up credentials + config.RemoveCredentials() + defer config.RemoveCredentials() // Execute login command with public and secret key flags - should fail validation output, err := executeAuthCommand("auth", "login", "--public-key", "invalid-public", "--secret-key", "invalid-secret", "--project-id", "test-project-invalid") @@ -56,15 +57,9 @@ func TestAuthLogin_APIKeyValidationFailure(t *testing.T) { t.Errorf("Expected output to contain validation message, got: %s", output) } - // Verify that no API key was stored - if _, err := config.GetAPIKeyFromKeyring(); err == nil { - t.Error("API key should not be stored when validation fails") - } - - // Also check file fallback - apiKeyFile := filepath.Join(tmpDir, "api-key") - if _, err := os.Stat(apiKeyFile); err == nil { - t.Error("API key file should not exist when validation fails") + // Verify that no credentials were stored + if _, _, err := config.GetCredentials(); err == nil { + t.Error("Credentials should not be stored when validation fails") } } @@ -90,13 +85,15 @@ func TestAuthLogin_APIKeyValidationSuccess(t *testing.T) { validateAPIKeyForLogin = originalValidator }() - // Set temporary config directory - os.Setenv("TIGER_CONFIG_DIR", tmpDir) - defer os.Unsetenv("TIGER_CONFIG_DIR") + // Initialize viper with test directory BEFORE calling RemoveCredentials() + // This ensures RemoveCredentials() operates on the test directory, not the user's real directory + if _, err := config.UseTestConfig(tmpDir, map[string]any{}); err != nil { + t.Fatalf("Failed to use test config: %v", err) + } - // Clean up keyring - config.RemoveAPIKeyFromKeyring() - defer config.RemoveAPIKeyFromKeyring() + // Clean up credentials + config.RemoveCredentials() + defer config.RemoveCredentials() // Execute login command with public and secret key flags - should succeed output, err := executeAuthCommand("auth", "login", "--public-key", "valid-public", "--secret-key", "valid-secret", "--project-id", "test-project-valid") @@ -104,28 +101,22 @@ func TestAuthLogin_APIKeyValidationSuccess(t *testing.T) { t.Fatalf("Expected login to succeed with valid keys, got error: %v", err) } - expectedOutput := "Validating API key...\nSuccessfully logged in and stored API key\nSet default project ID to: test-project-valid\n" + nextStepsMessage + expectedOutput := "Validating API key...\nSuccessfully logged in (project: test-project-valid)\n" + nextStepsMessage if output != expectedOutput { t.Errorf("Expected output %q, got %q", expectedOutput, output) } - // Verify that API key was stored (try keyring first, then file fallback) - apiKey, err := config.GetAPIKeyFromKeyring() + // Verify that credentials were stored + expectedAPIKey := "valid-public:valid-secret" + expectedProjectID := "test-project-valid" + apiKey, projectID, err := config.GetCredentials() if err != nil { - // Keyring failed, check file fallback - apiKeyFile := filepath.Join(tmpDir, "api-key") - data, err := os.ReadFile(apiKeyFile) - if err != nil { - t.Fatalf("API key not stored in keyring or file: %v", err) - } - expectedAPIKey := "valid-public:valid-secret" - if string(data) != expectedAPIKey { - t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, string(data)) - } - } else { - expectedAPIKey := "valid-public:valid-secret" - if apiKey != expectedAPIKey { - t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, apiKey) - } + t.Fatalf("Credentials not stored in keyring or file: %v", err) + } + if apiKey != expectedAPIKey { + t.Errorf("Expected API key '%s', got '%s'", expectedAPIKey, apiKey) + } + if projectID != expectedProjectID { + t.Errorf("Expected project ID '%s', got '%s'", expectedProjectID, projectID) } } diff --git a/internal/tiger/cmd/config.go b/internal/tiger/cmd/config.go index 5938fd34..cc4e36bc 100644 --- a/internal/tiger/cmd/config.go +++ b/internal/tiger/cmd/config.go @@ -212,9 +212,6 @@ func outputTable(w io.Writer, cfg *config.ConfigOutput) error { if cfg.PasswordStorage != nil { table.Append("password_storage", *cfg.PasswordStorage) } - if cfg.ProjectID != nil { - table.Append("project_id", *cfg.ProjectID) - } if cfg.ReleasesURL != nil { table.Append("releases_url", *cfg.ReleasesURL) } diff --git a/internal/tiger/cmd/config_test.go b/internal/tiger/cmd/config_test.go index c2eac38f..cd2e0ffb 100644 --- a/internal/tiger/cmd/config_test.go +++ b/internal/tiger/cmd/config_test.go @@ -62,7 +62,6 @@ func TestConfigShow_TableOutput(t *testing.T) { // Create config file with test data configContent := `api_url: https://test.api.com/v1 -project_id: test-project service_id: test-service output: table analytics: false @@ -86,7 +85,6 @@ password_storage: pgpass "gateway_url": "https://console.cloud.timescale.com/api", "docs_mcp": "true", "docs_mcp_url": "https://mcp.tigerdata.com/docs", - "project_id": "test-project", "service_id": "test-service", "output": "table", "analytics": "false", @@ -111,7 +109,6 @@ func TestConfigShow_JSONOutput(t *testing.T) { // Create config file with JSON output format configContent := `api_url: https://json.api.com/v1 -project_id: json-project output: json analytics: true password_storage: none @@ -141,7 +138,6 @@ version_check_last_time: ` + now.Format(time.RFC3339) + "\n" "gateway_url": "https://console.cloud.timescale.com/api", "docs_mcp": true, "docs_mcp_url": "https://mcp.tigerdata.com/docs", - "project_id": "json-project", "service_id": "", "output": "json", "analytics": true, @@ -172,7 +168,6 @@ func TestConfigShow_YAMLOutput(t *testing.T) { // Create config file with YAML output format configContent := `api_url: https://yaml.api.com/v1 -project_id: yaml-project output: yaml analytics: false password_storage: keyring @@ -201,7 +196,6 @@ version_check_last_time: ` + now.Format(time.RFC3339) + "\n" "gateway_url": "https://console.cloud.timescale.com/api", "docs_mcp": true, "docs_mcp_url": "https://mcp.tigerdata.com/docs", - "project_id": "yaml-project", "service_id": "", "output": "yaml", "analytics": false, @@ -270,7 +264,6 @@ func TestConfigShow_OutputValueUnaffectedByEnvVar(t *testing.T) { // Create config file with table as default output configContent := `api_url: https://test.api.com/v1 -project_id: test-project output: table analytics: true ` @@ -318,7 +311,6 @@ func TestConfigShow_ConfigDirFlag(t *testing.T) { // Create a config file with test data in the specified directory configContent := `api_url: https://flag-test.api.com/v1 -project_id: flag-test-project output: json analytics: false ` @@ -339,9 +331,6 @@ analytics: false t.Fatalf("Failed to parse JSON output: %v", err) } - if result["project_id"] != "flag-test-project" { - t.Errorf("Expected project_id 'flag-test-project', got %v", result["project_id"]) - } if result["api_url"] != "https://flag-test.api.com/v1" { t.Errorf("Expected api_url 'https://flag-test.api.com/v1', got %v", result["api_url"]) } @@ -359,7 +348,6 @@ func TestConfigSet_ValidValues(t *testing.T) { expectedOutput string }{ {"api_url", "https://new.api.com/v1", "Set api_url = https://new.api.com/v1"}, - {"project_id", "new-project", "Set project_id = new-project"}, {"service_id", "new-service", "Set service_id = new-service"}, {"output", "json", "Set output = json"}, {"analytics", "false", "Set analytics = false"}, @@ -391,10 +379,6 @@ func TestConfigSet_ValidValues(t *testing.T) { if cfg.APIURL != tt.value { t.Errorf("Expected APIURL %s, got %s", tt.value, cfg.APIURL) } - case "project_id": - if cfg.ProjectID != tt.value { - t.Errorf("Expected ProjectID %s, got %s", tt.value, cfg.ProjectID) - } case "service_id": if cfg.ServiceID != tt.value { t.Errorf("Expected ServiceID %s, got %s", tt.value, cfg.ServiceID) @@ -486,7 +470,7 @@ func TestConfigSet_ConfigDirFlag(t *testing.T) { }) // Execute config set with --config-dir flag - if _, err := executeConfigCommand("--config-dir", tmpDir, "config", "set", "project_id", "flag-set-project"); err != nil { + if _, err := executeConfigCommand("--config-dir", tmpDir, "config", "set", "service_id", "flag-set-service"); err != nil { t.Fatalf("Config set command failed: %v", err) } @@ -502,8 +486,8 @@ func TestConfigSet_ConfigDirFlag(t *testing.T) { t.Fatalf("Failed to read config file: %v", err) } - if !strings.Contains(string(content), "project_id: flag-set-project") { - t.Errorf("Config file should contain 'project_id: flag-set-project', got: %s", string(content)) + if !strings.Contains(string(content), "service_id: flag-set-service") { + t.Errorf("Config file should contain 'service_id: flag-set-service', got: %s", string(content)) } } @@ -516,7 +500,6 @@ func TestConfigUnset_ValidKeys(t *testing.T) { t.Fatalf("Failed to load config: %v", err) } - cfg.Set("project_id", "test-project") cfg.Set("service_id", "test-service") cfg.Set("output", "json") cfg.Set("password_storage", "pgpass") @@ -525,7 +508,6 @@ func TestConfigUnset_ValidKeys(t *testing.T) { key string expectedOutput string }{ - {"project_id", "Unset project_id"}, {"service_id", "Unset service_id"}, {"output", "Unset output"}, {"password_storage", "Unset password_storage"}, @@ -550,10 +532,6 @@ func TestConfigUnset_ValidKeys(t *testing.T) { // Check the value was unset correctly switch tt.key { - case "project_id": - if cfg.ProjectID != "" { - t.Errorf("Expected empty ProjectID, got %s", cfg.ProjectID) - } case "service_id": if cfg.ServiceID != "" { t.Errorf("Expected empty ServiceID, got %s", cfg.ServiceID) @@ -611,7 +589,6 @@ func TestConfigReset(t *testing.T) { t.Fatalf("Failed to load config: %v", err) } - cfg.Set("project_id", "custom-project") cfg.Set("service_id", "custom-service") cfg.Set("output", "json") cfg.Set("analytics", "false") @@ -631,12 +608,7 @@ func TestConfigReset(t *testing.T) { t.Fatalf("Failed to load config: %v", err) } - // ProjectID should be preserved - if cfg.ProjectID != "custom-project" { - t.Errorf("Expected ProjectID %s, got %s", "custom-project", cfg.ProjectID) - } - - // Verify all other values were reset to defaults + // Verify all values were reset to defaults if cfg.APIURL != config.DefaultAPIURL { t.Errorf("Expected default APIURL %s, got %s", config.DefaultAPIURL, cfg.APIURL) } @@ -657,9 +629,9 @@ func TestConfigCommands_Integration(t *testing.T) { // Test full workflow: set -> show -> unset -> reset // 1. Set some values - _, err := executeConfigCommand("config", "set", "project_id", "integration-test") + _, err := executeConfigCommand("config", "set", "service_id", "integration-test") if err != nil { - t.Fatalf("Failed to set project_id: %v", err) + t.Fatalf("Failed to set service_id: %v", err) } _, err = executeConfigCommand("config", "set", "output", "json") @@ -679,17 +651,17 @@ func TestConfigCommands_Integration(t *testing.T) { t.Fatalf("Expected JSON output, got: %s", showOutput) } - if result["project_id"] != "integration-test" { - t.Errorf("Expected project_id 'integration-test', got %v", result["project_id"]) + if result["service_id"] != "integration-test" { + t.Errorf("Expected service_id 'integration-test', got %v", result["service_id"]) } - // 3. Unset project_id - _, err = executeConfigCommand("config", "unset", "project_id") + // 3. Unset service_id + _, err = executeConfigCommand("config", "unset", "service_id") if err != nil { - t.Fatalf("Failed to unset project_id: %v", err) + t.Fatalf("Failed to unset service_id: %v", err) } - // 4. Verify project_id was unset + // 4. Verify service_id was unset showOutput, err = executeConfigCommand("config", "show") if err != nil { t.Fatalf("Failed to show config after unset: %v", err) @@ -697,8 +669,8 @@ func TestConfigCommands_Integration(t *testing.T) { result = make(map[string]any) json.Unmarshal([]byte(showOutput), &result) - if result["project_id"] != "" { - t.Errorf("Expected empty project_id after unset, got %v", result["project_id"]) + if result["service_id"] != "" { + t.Errorf("Expected empty service_id after unset, got %v", result["service_id"]) } // 5. Reset all config diff --git a/internal/tiger/cmd/db.go b/internal/tiger/cmd/db.go index b7d8fc4f..c413ff0b 100644 --- a/internal/tiger/cmd/db.go +++ b/internal/tiger/cmd/db.go @@ -18,8 +18,8 @@ import ( ) var ( - // getAPIKeyForDB can be overridden for testing - getAPIKeyForDB = config.GetAPIKey + // getCredentialsForDB can be overridden for testing + getCredentialsForDB = config.GetCredentials ) func buildDbConnectionStringCmd() *cobra.Command { @@ -251,11 +251,6 @@ func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) { return api.Service{}, fmt.Errorf("failed to load config: %w", err) } - projectID := cfg.ProjectID - if projectID == "" { - return api.Service{}, fmt.Errorf("project ID is required. Set it using login with --project-id") - } - // Determine service ID var serviceID string if len(args) > 0 { @@ -270,10 +265,10 @@ func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) { cmd.SilenceUsage = true - // Get API key for authentication - apiKey, err := getAPIKeyForDB() + // Get API key and project ID for authentication + apiKey, projectID, err := getCredentialsForDB() if err != nil { - return api.Service{}, exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w", err)) + return api.Service{}, exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err)) } // Create API client diff --git a/internal/tiger/cmd/db_test.go b/internal/tiger/cmd/db_test.go index eb449d1f..0936da2f 100644 --- a/internal/tiger/cmd/db_test.go +++ b/internal/tiger/cmd/db_test.go @@ -62,21 +62,20 @@ func executeDBCommand(args ...string) (string, error) { func TestDBConnectionString_NoServiceID(t *testing.T) { tmpDir := setupDBTest(t) - // Set up config with project ID but no default service ID + // Set up config with no default service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForDB - getAPIKeyForDB = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForDB + getCredentialsForDB = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForDB = originalGetAPIKey }() + defer func() { getCredentialsForDB = originalGetCredentials }() // Execute db connection-string command without service ID _, err = executeDBCommand("db", "connection-string") @@ -92,10 +91,9 @@ func TestDBConnectionString_NoServiceID(t *testing.T) { func TestDBConnectionString_NoAuth(t *testing.T) { tmpDir := setupDBTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "svc-12345", }) if err != nil { @@ -103,11 +101,11 @@ func TestDBConnectionString_NoAuth(t *testing.T) { } // Mock authentication failure - originalGetAPIKey := getAPIKeyForDB - getAPIKeyForDB = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForDB + getCredentialsForDB = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForDB = originalGetAPIKey }() + defer func() { getCredentialsForDB = originalGetCredentials }() // Execute db connection-string command _, err = executeDBCommand("db", "connection-string") @@ -169,21 +167,20 @@ func TestDBConnectionString_PoolerWarning(t *testing.T) { func TestDBConnect_NoServiceID(t *testing.T) { tmpDir := setupDBTest(t) - // Set up config with project ID but no default service ID + // Set up config with no default service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForDB - getAPIKeyForDB = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForDB + getCredentialsForDB = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForDB = originalGetAPIKey }() + defer func() { getCredentialsForDB = originalGetCredentials }() // Execute db connect command without service ID _, err = executeDBCommand("db", "connect") @@ -199,10 +196,9 @@ func TestDBConnect_NoServiceID(t *testing.T) { func TestDBConnect_NoAuth(t *testing.T) { tmpDir := setupDBTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "svc-12345", }) if err != nil { @@ -210,11 +206,11 @@ func TestDBConnect_NoAuth(t *testing.T) { } // Mock authentication failure - originalGetAPIKey := getAPIKeyForDB - getAPIKeyForDB = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForDB + getCredentialsForDB = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForDB = originalGetAPIKey }() + defer func() { getCredentialsForDB = originalGetCredentials }() // Execute db connect command _, err = executeDBCommand("db", "connect") @@ -233,7 +229,6 @@ func TestDBConnect_PsqlNotFound(t *testing.T) { // Set up config _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "http://localhost:9999", - "project_id": "test-project-123", "service_id": "svc-12345", }) if err != nil { @@ -241,11 +236,11 @@ func TestDBConnect_PsqlNotFound(t *testing.T) { } // Mock authentication - originalGetAPIKey := getAPIKeyForDB - getAPIKeyForDB = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForDB + getCredentialsForDB = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForDB = originalGetAPIKey }() + defer func() { getCredentialsForDB = originalGetCredentials }() // Test that psql alias works the same as connect _, err1 := executeDBCommand("db", "connect") @@ -511,21 +506,20 @@ func equalStringSlices(a, b []string) bool { func TestDBTestConnection_NoServiceID(t *testing.T) { tmpDir := setupDBTest(t) - // Set up config with project ID but no default service ID + // Set up config with no default service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForDB - getAPIKeyForDB = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForDB + getCredentialsForDB = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForDB = originalGetAPIKey }() + defer func() { getCredentialsForDB = originalGetCredentials }() // Execute db test-connection command without service ID _, err = executeDBCommand("db", "test-connection") @@ -541,10 +535,9 @@ func TestDBTestConnection_NoServiceID(t *testing.T) { func TestDBTestConnection_NoAuth(t *testing.T) { tmpDir := setupDBTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "svc-12345", }) if err != nil { @@ -552,11 +545,11 @@ func TestDBTestConnection_NoAuth(t *testing.T) { } // Mock authentication failure - originalGetAPIKey := getAPIKeyForDB - getAPIKeyForDB = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForDB + getCredentialsForDB = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForDB = originalGetAPIKey }() + defer func() { getCredentialsForDB = originalGetCredentials }() // Execute db test-connection command _, err = executeDBCommand("db", "test-connection") @@ -899,7 +892,6 @@ func TestDBTestConnection_TimeoutParsing(t *testing.T) { // Set up config _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "http://localhost:9999", // Non-existent server - "project_id": "test-project-123", "service_id": "svc-12345", }) if err != nil { @@ -907,11 +899,11 @@ func TestDBTestConnection_TimeoutParsing(t *testing.T) { } // Mock authentication - originalGetAPIKey := getAPIKeyForDB - getAPIKeyForDB = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForDB + getCredentialsForDB = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForDB = originalGetAPIKey }() + defer func() { getCredentialsForDB = originalGetCredentials }() // Execute db test-connection command with timeout flag _, err = executeDBCommand("db", "test-connection", "--timeout", tc.timeoutFlag) diff --git a/internal/tiger/cmd/root.go b/internal/tiger/cmd/root.go index dcf7aab7..8b6184f1 100644 --- a/internal/tiger/cmd/root.go +++ b/internal/tiger/cmd/root.go @@ -16,7 +16,6 @@ import ( func buildRootCmd() *cobra.Command { var configDir string var debug bool - var projectID string var serviceID string var analytics bool var passwordStorage string @@ -87,7 +86,6 @@ tiger auth login // Add persistent flags cmd.PersistentFlags().StringVar(&configDir, "config-dir", config.GetDefaultConfigDir(), "config directory") cmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging") - cmd.PersistentFlags().StringVar(&projectID, "project-id", "", "project ID") cmd.PersistentFlags().StringVar(&serviceID, "service-id", "", "service ID") cmd.PersistentFlags().BoolVar(&analytics, "analytics", true, "enable/disable usage analytics") cmd.PersistentFlags().StringVar(&passwordStorage, "password-storage", config.DefaultPasswordStorage, "password storage method (keyring, pgpass, none)") @@ -95,7 +93,6 @@ tiger auth login // Bind flags to viper viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")) - viper.BindPFlag("project_id", cmd.PersistentFlags().Lookup("project-id")) viper.BindPFlag("service_id", cmd.PersistentFlags().Lookup("service-id")) viper.BindPFlag("analytics", cmd.PersistentFlags().Lookup("analytics")) viper.BindPFlag("password_storage", cmd.PersistentFlags().Lookup("password-storage")) diff --git a/internal/tiger/cmd/root_test.go b/internal/tiger/cmd/root_test.go index c32185fb..6644870c 100644 --- a/internal/tiger/cmd/root_test.go +++ b/internal/tiger/cmd/root_test.go @@ -71,7 +71,6 @@ analytics: true // Set CLI flags (these should take precedence) args := []string{ "--config-dir", tmpDir, - "--project-id", "flag-project", "--service-id", "flag-service", "--analytics=false", "--debug", @@ -87,8 +86,8 @@ analytics: true } // Verify Viper reflects the CLI flag values (highest precedence) - if viper.GetString("project_id") != "flag-project" { - t.Errorf("Expected Viper project_id 'flag-project', got '%s'", viper.GetString("project_id")) + if viper.GetString("service_id") != "flag-service" { + t.Errorf("Expected Viper service_id 'flag-service', got '%s'", viper.GetString("service_id")) } } diff --git a/internal/tiger/cmd/service.go b/internal/tiger/cmd/service.go index 9bcda12b..dd765121 100644 --- a/internal/tiger/cmd/service.go +++ b/internal/tiger/cmd/service.go @@ -18,8 +18,8 @@ import ( ) var ( - // getAPIKeyForService can be overridden for testing - getAPIKeyForService = config.GetAPIKey + // getCredentialsForService can be overridden for testing + getCredentialsForService = config.GetCredentials ) // buildServiceCmd creates the main service command with all subcommands @@ -81,11 +81,6 @@ Examples: cfg.Output = output } - projectID := cfg.ProjectID - if projectID == "" { - return fmt.Errorf("project ID is required. Set it using login with --project-id") - } - // Determine service ID var serviceID string if len(args) > 0 { @@ -100,10 +95,10 @@ Examples: cmd.SilenceUsage = true - // Get API key for authentication - apiKey, err := getAPIKeyForService() + // Get API key and project ID for authentication + apiKey, projectID, err := getCredentialsForService() if err != nil { - return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w", err)) + return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err)) } // Create API client @@ -164,17 +159,12 @@ func buildServiceListCmd() *cobra.Command { cfg.Output = output } - projectID := cfg.ProjectID - if projectID == "" { - return fmt.Errorf("project ID is required. Set it using login with --project-id") - } - cmd.SilenceUsage = true - // Get API key for authentication - apiKey, err := getAPIKeyForService() + // Get API key and project ID for authentication + apiKey, projectID, err := getCredentialsForService() if err != nil { - return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w", err)) + return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err)) } // Create API client @@ -300,11 +290,6 @@ Note: You can specify both CPU and memory together, or specify only one (the oth cfg.Output = output } - projectID := cfg.ProjectID - if projectID == "" { - return fmt.Errorf("project ID is required. Set it using login with --project-id") - } - // Auto-generate service name if not provided if createServiceName == "" { createServiceName = util.GenerateServiceName() @@ -332,10 +317,10 @@ Note: You can specify both CPU and memory together, or specify only one (the oth cmd.SilenceUsage = true - // Get API key for authentication - apiKey, err := getAPIKeyForService() + // Get API key and project ID for authentication + apiKey, projectID, err := getCredentialsForService() if err != nil { - return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w", err)) + return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err)) } // Create API client @@ -481,11 +466,6 @@ Examples: return fmt.Errorf("failed to load config: %w", err) } - projectID := cfg.ProjectID - if projectID == "" { - return fmt.Errorf("project ID is required. Set it using login with --project-id") - } - // Determine service ID var serviceID string if len(args) > 0 { @@ -506,10 +486,10 @@ Examples: cmd.SilenceUsage = true - // Get API key for authentication - apiKey, err := getAPIKeyForService() + // Get API key and project ID for authentication + apiKey, projectID, err := getCredentialsForService() if err != nil { - return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w", err)) + return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err)) } // Create API client @@ -907,20 +887,10 @@ Examples: cmd.SilenceUsage = true - // Get project ID from config - cfg, err := config.Load() - if err != nil { - return fmt.Errorf("failed to load config: %w", err) - } - - if cfg.ProjectID == "" { - return fmt.Errorf("project ID is required. Set it using login with --project-id") - } - - // Get API key - apiKey, err := getAPIKeyForService() + // Get API key and project ID for authentication + apiKey, projectID, err := getCredentialsForService() if err != nil { - return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w", err)) + return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err)) } statusOutput := cmd.ErrOrStderr() @@ -946,7 +916,7 @@ Examples: // Make the delete request resp, err := client.DeleteProjectsProjectIdServicesServiceIdWithResponse( context.Background(), - api.ProjectId(cfg.ProjectID), + api.ProjectId(projectID), api.ServiceId(serviceID), ) if err != nil { @@ -967,7 +937,7 @@ Examples: } // Wait for deletion to complete - if err := waitForServiceDeletion(client, cfg.ProjectID, serviceID, deleteWaitTimeout, cmd); err != nil { + if err := waitForServiceDeletion(client, projectID, serviceID, deleteWaitTimeout, cmd); err != nil { // Return error for sake of exit code, but log ourselves for sake of icon fmt.Fprintf(statusOutput, "❌ Error: %s\n", err) cmd.SilenceErrors = true @@ -1119,11 +1089,6 @@ Examples: cfg.Output = output } - projectID := cfg.ProjectID - if projectID == "" { - return fmt.Errorf("project ID is required. Set it using login with --project-id") - } - // Determine source service ID var serviceID string if len(args) > 0 { @@ -1138,10 +1103,10 @@ Examples: cmd.SilenceUsage = true - // Get API key for authentication - apiKey, err := getAPIKeyForService() + // Get API key and project ID for authentication + apiKey, projectID, err := getCredentialsForService() if err != nil { - return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w", err)) + return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err)) } // Create API client diff --git a/internal/tiger/cmd/service_test.go b/internal/tiger/cmd/service_test.go index b1a4eb8b..19724bf9 100644 --- a/internal/tiger/cmd/service_test.go +++ b/internal/tiger/cmd/service_test.go @@ -66,21 +66,20 @@ func executeServiceCommand(args ...string) (string, error, *cobra.Command) { func TestServiceList_NoAuth(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and API URL + // Set up config with API URL _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication failure - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service list command _, err, _ = executeServiceCommand("service", "list") @@ -188,10 +187,9 @@ func TestOutputServices_Table(t *testing.T) { func TestServiceFork_NoAuth(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "source-service-123", }) if err != nil { @@ -199,11 +197,11 @@ func TestServiceFork_NoAuth(t *testing.T) { } // Mock authentication failure - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service fork command with required timing flag _, err, _ = executeServiceCommand("service", "fork", "--now") @@ -219,21 +217,20 @@ func TestServiceFork_NoAuth(t *testing.T) { func TestServiceFork_NoSourceService(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID but no service ID + // Set up config without service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication success - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service fork command without providing service ID but with timing flag _, err, _ = executeServiceCommand("service", "fork", "--now") @@ -249,10 +246,9 @@ func TestServiceFork_NoSourceService(t *testing.T) { func TestServiceFork_NoTimingFlag(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "source-service-123", }) if err != nil { @@ -260,11 +256,11 @@ func TestServiceFork_NoTimingFlag(t *testing.T) { } // Mock authentication success - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service fork command without any timing flag _, err, _ = executeServiceCommand("service", "fork", "source-service-123") @@ -280,10 +276,9 @@ func TestServiceFork_NoTimingFlag(t *testing.T) { func TestServiceFork_MultipleTiming(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "source-service-123", }) if err != nil { @@ -291,11 +286,11 @@ func TestServiceFork_MultipleTiming(t *testing.T) { } // Mock authentication success - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service fork command with multiple timing flags _, err, _ = executeServiceCommand("service", "fork", "source-service-123", "--now", "--last-snapshot") @@ -311,10 +306,9 @@ func TestServiceFork_MultipleTiming(t *testing.T) { func TestServiceFork_InvalidTimestamp(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "source-service-123", }) if err != nil { @@ -322,11 +316,11 @@ func TestServiceFork_InvalidTimestamp(t *testing.T) { } // Mock authentication success - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service fork command with invalid timestamp _, err, _ = executeServiceCommand("service", "fork", "source-service-123", "--to-timestamp", "invalid-timestamp") @@ -342,10 +336,9 @@ func TestServiceFork_InvalidTimestamp(t *testing.T) { func TestServiceFork_CPUMemoryValidation(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "source-service-123", }) if err != nil { @@ -353,11 +346,11 @@ func TestServiceFork_CPUMemoryValidation(t *testing.T) { } // Mock authentication success - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Test with invalid CPU/memory combination (this would fail at API call stage) // Since we don't want to make real API calls, we expect the command to fail during validation @@ -387,21 +380,20 @@ func TestFormatTimePtr(t *testing.T) { func TestServiceCreate_ValidationErrors(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and a mock API URL to prevent network calls + // Set up config with a mock API URL to prevent network calls _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "http://localhost:9999", // Use a local URL that will fail fast - "project_id": "test-project-123", + "api_url": "http://localhost:9999", // Use a local URL that will fail fast }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Test with no name (should auto-generate) - this should now work without error // Just test that it doesn't fail due to missing name @@ -424,21 +416,20 @@ func TestServiceCreate_ValidationErrors(t *testing.T) { func TestServiceCreate_NoAuth(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and API URL + // Set up config with API URL _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication failure - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service create command with valid parameters (name will be auto-generated) _, err, _ = executeServiceCommand("service", "create", "--addons", "none", "--region", "us-east-1", "--cpu", "1000", "--memory", "4", "--replicas", "1") @@ -490,21 +481,20 @@ func createTestServices() []api.Service { func TestAutoGeneratedServiceName(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and a mock API URL to prevent network calls + // Set up config with a mock API URL to prevent network calls _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "http://localhost:9999", // Use a local URL that will fail fast - "project_id": "test-project-123", + "api_url": "http://localhost:9999", // Use a local URL that will fail fast }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Test that service name is auto-generated when not provided // We expect this to fail at the API call stage, not at validation @@ -562,11 +552,11 @@ func TestServiceGet_NoServiceID(t *testing.T) { } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service get command without service ID _, err, _ = executeServiceCommand("service", "get") @@ -582,10 +572,9 @@ func TestServiceGet_NoServiceID(t *testing.T) { func TestServiceGet_NoAuth(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "svc-12345", }) if err != nil { @@ -593,11 +582,11 @@ func TestServiceGet_NoAuth(t *testing.T) { } // Mock authentication failure - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service get command _, err, _ = executeServiceCommand("service", "get") @@ -981,11 +970,11 @@ func TestServiceUpdatePassword_NoServiceID(t *testing.T) { } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service update-password command without service ID _, err, _ = executeServiceCommand("service", "update-password", "--new-password", "new-password") @@ -1001,10 +990,9 @@ func TestServiceUpdatePassword_NoServiceID(t *testing.T) { func TestServiceUpdatePassword_NoPassword(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "svc-12345", }) if err != nil { @@ -1012,11 +1000,11 @@ func TestServiceUpdatePassword_NoPassword(t *testing.T) { } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service update-password command without password _, err, _ = executeServiceCommand("service", "update-password") @@ -1032,10 +1020,9 @@ func TestServiceUpdatePassword_NoPassword(t *testing.T) { func TestServiceUpdatePassword_NoAuth(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID and service ID + // Set up config with service ID _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", "service_id": "svc-12345", }) if err != nil { @@ -1043,11 +1030,11 @@ func TestServiceUpdatePassword_NoAuth(t *testing.T) { } // Mock authentication failure - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service update-password command _, err, _ = executeServiceCommand("service", "update-password", "--new-password", "new-password") @@ -1063,10 +1050,9 @@ func TestServiceUpdatePassword_NoAuth(t *testing.T) { func TestServiceUpdatePassword_EnvironmentVariable(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID + // Set up config _, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "http://localhost:9999", // Use a local URL that will fail fast - "project_id": "test-project-123", "service_id": "test-service-456", }) if err != nil { @@ -1074,11 +1060,11 @@ func TestServiceUpdatePassword_EnvironmentVariable(t *testing.T) { } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Set environment variable BEFORE creating command (like root test does) originalEnv := os.Getenv("TIGER_NEW_PASSWORD") @@ -1113,21 +1099,20 @@ func TestServiceUpdatePassword_EnvironmentVariable(t *testing.T) { func TestServiceCreate_WaitTimeoutParsing(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID to get past initial validation + // Set up config to get past initial validation _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "http://localhost:9999", // Use local URL that will fail fast - "project_id": "test-project-123", + "api_url": "http://localhost:9999", // Use local URL that will fail fast }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() testCases := []struct { name string @@ -1215,19 +1200,18 @@ func TestWaitForServiceReady_Timeout(t *testing.T) { // Set up config _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "http://localhost:9999", // Non-existent server to force timeout - "project_id": "test-project-123", + "api_url": "http://localhost:9999", // Non-existent server to force timeout }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Create API client client, err := api.NewTigerClient("test-api-key") @@ -1316,10 +1300,9 @@ func TestServiceCommandAliases(t *testing.T) { func TestServiceDelete_NoServiceID(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID + // Set up config _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) @@ -1339,21 +1322,20 @@ func TestServiceDelete_NoServiceID(t *testing.T) { func TestServiceDelete_NoAuth(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID + // Set up config _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication failure - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "", fmt.Errorf("not logged in") + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "", "", fmt.Errorf("not logged in") } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service delete command _, err, _ = executeServiceCommand("service", "delete", "svc-12345", "--confirm") @@ -1366,53 +1348,23 @@ func TestServiceDelete_NoAuth(t *testing.T) { } } -func TestServiceDelete_NoProjectID(t *testing.T) { - tmpDir := setupServiceTest(t) - - // Set up config without project ID - _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - }) - if err != nil { - t.Fatalf("Failed to save test config: %v", err) - } - - // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil - } - defer func() { getAPIKeyForService = originalGetAPIKey }() - - // Execute service delete command - _, err, _ = executeServiceCommand("service", "delete", "svc-12345", "--confirm") - if err == nil { - t.Fatal("Expected error when no project ID is configured") - } - - if !strings.Contains(err.Error(), "project ID is required") { - t.Errorf("Expected project ID error, got: %v", err) - } -} - func TestServiceDelete_WithConfirmFlag(t *testing.T) { tmpDir := setupServiceTest(t) // Set up config with project ID _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "http://localhost:9999", // Non-existent server for testing - "project_id": "test-project-123", + "api_url": "http://localhost:9999", // Non-existent server for testing }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service delete command with --confirm flag // This should fail due to network error (which is expected in tests) @@ -1433,21 +1385,20 @@ func TestServiceDelete_ConfirmationPrompt(t *testing.T) { tmpDir := setupServiceTest(t) - // Set up config with project ID + // Set up config _, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "https://api.tigerdata.com/public/v1", - "project_id": "test-project-123", + "api_url": "https://api.tigerdata.com/public/v1", }) if err != nil { t.Fatalf("Failed to save test config: %v", err) } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service delete command without --confirm flag // This should try to read from stdin for confirmation, which will fail in test environment @@ -1507,11 +1458,11 @@ func TestServiceDelete_FlagsValidation(t *testing.T) { } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -1650,9 +1601,8 @@ func TestServiceCreate_OutputFlagDoesNotPersist(t *testing.T) { // Set up config with default output format (table) cfg, err := config.UseTestConfig(tmpDir, map[string]any{ - "api_url": "http://localhost:9999", - "project_id": "test-project-123", - "output": "table", // Explicitly set default + "api_url": "http://localhost:9999", + "output": "table", // Explicitly set default }) if err != nil { t.Fatalf("Failed to setup test config: %v", err) @@ -1666,11 +1616,11 @@ func TestServiceCreate_OutputFlagDoesNotPersist(t *testing.T) { } // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Execute service create with -o json flag // This will fail due to network error (localhost:9999 doesn't exist), but that's OK @@ -1689,7 +1639,6 @@ func TestServiceList_OutputFlagAffectsCommandOnly(t *testing.T) { // Set up config with output format explicitly set to "table" cfg, err := config.UseTestConfig(tmpDir, map[string]any{ "api_url": "http://localhost:9999", - "project_id": "test-project-123", "output": "table", "version_check_interval": 0, }) @@ -1699,11 +1648,11 @@ func TestServiceList_OutputFlagAffectsCommandOnly(t *testing.T) { configFile := cfg.GetConfigFile() // Mock authentication - originalGetAPIKey := getAPIKeyForService - getAPIKeyForService = func() (string, error) { - return "test-api-key", nil + originalGetCredentials := getCredentialsForService + getCredentialsForService = func() (string, string, error) { + return "test-api-key", "test-project-123", nil } - defer func() { getAPIKeyForService = originalGetAPIKey }() + defer func() { getCredentialsForService = originalGetCredentials }() // Store original config file content originalConfigBytes, err := os.ReadFile(configFile) diff --git a/internal/tiger/config/api_key.go b/internal/tiger/config/api_key.go index 4cd32389..3ea6e170 100644 --- a/internal/tiger/config/api_key.go +++ b/internal/tiger/config/api_key.go @@ -1,6 +1,7 @@ package config import ( + "encoding/json" "errors" "fmt" "os" @@ -13,9 +14,15 @@ import ( // Keyring parameters const ( keyringServiceName = "tiger-cli" - keyringUsername = "api-key" + keyringUsername = "credentials" ) +// storedCredentials represents the JSON structure for stored credentials +type storedCredentials struct { + APIKey string `json:"api_key"` + ProjectID string `json:"project_id"` +} + // testServiceNameOverride allows tests to override the service name for isolation var testServiceNameOverride string @@ -46,37 +53,63 @@ func SetTestServiceName(t *testing.T) { }) } -// storeAPIKey stores the API key using keyring with file fallback -func StoreAPIKey(apiKey string) error { +// StoreCredentials stores the API key (public:secret) and project ID together +// The credentials are stored as JSON with api_key and project_id fields +func StoreCredentials(apiKey, projectID string) error { + creds := storedCredentials{ + APIKey: apiKey, + ProjectID: projectID, + } + + credentialsJSON, err := json.Marshal(creds) + if err != nil { + return fmt.Errorf("failed to marshal credentials: %w", err) + } + // Try keyring first - if err := StoreAPIKeyToKeyring(apiKey); err == nil { + if err := storeToKeyring(string(credentialsJSON)); err == nil { return nil } // Fallback to file storage - return StoreAPIKeyToFile(apiKey) + return storeToFile(string(credentialsJSON)) } -func StoreAPIKeyToKeyring(apiKey string) error { - return keyring.Set(GetServiceName(), keyringUsername, apiKey) +// StoreCredentialsToFile stores credentials to file (test helper) +func StoreCredentialsToFile(apiKey, projectID string) error { + creds := storedCredentials{ + APIKey: apiKey, + ProjectID: projectID, + } + + credentialsJSON, err := json.Marshal(creds) + if err != nil { + return fmt.Errorf("failed to marshal credentials: %w", err) + } + + return storeToFile(string(credentialsJSON)) } -// StoreAPIKeyToFile stores API key to ~/.config/tiger/api-key with restricted permissions -func StoreAPIKeyToFile(apiKey string) error { +func storeToKeyring(credentials string) error { + return keyring.Set(GetServiceName(), keyringUsername, credentials) +} + +// storeToFile stores credentials to ~/.config/tiger/credentials with restricted permissions +func storeToFile(credentials string) error { configDir := GetConfigDir() if err := os.MkdirAll(configDir, 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } - apiKeyFile := fmt.Sprintf("%s/api-key", configDir) - file, err := os.OpenFile(apiKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + credentialsFile := fmt.Sprintf("%s/credentials", configDir) + file, err := os.OpenFile(credentialsFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { - return fmt.Errorf("failed to create API key file: %w", err) + return fmt.Errorf("failed to create credentials file: %w", err) } defer file.Close() - if _, err := file.WriteString(apiKey); err != nil { - return fmt.Errorf("failed to write API key to file: %w", err) + if _, err := file.WriteString(credentials); err != nil { + return fmt.Errorf("failed to write credentials to file: %w", err) } if err := file.Close(); err != nil { @@ -88,68 +121,84 @@ func StoreAPIKeyToFile(apiKey string) error { var ErrNotLoggedIn = errors.New("not logged in") -// GetAPIKey retrieves the API key from keyring or file fallback -func GetAPIKey() (string, error) { +// GetCredentials retrieves the API key and project ID from storage +// Returns (apiKey, projectID, error) where apiKey is in "publicKey:secretKey" format +func GetCredentials() (string, string, error) { // Try keyring first - apiKey, err := GetAPIKeyFromKeyring() - if err == nil && apiKey != "" { - return apiKey, nil + if apiKey, projectId, err := getCredentialsFromKeyring(); err == nil { + return apiKey, projectId, nil } // Fallback to file storage - return GetAPIKeyFromFile() + return getCredentialsFromFile() } -func GetAPIKeyFromKeyring() (string, error) { - return keyring.Get(GetServiceName(), keyringUsername) +// getCredentialsFromKeyring gets credentials from keyring. +func getCredentialsFromKeyring() (string, string, error) { + combined, err := keyring.Get(GetServiceName(), keyringUsername) + if err != nil { + return "", "", err + } + return parseCredentials(combined) } -// GetAPIKeyFromFile retrieves API key from ~/.config/tiger/api-key -func GetAPIKeyFromFile() (string, error) { +// getCredentialsFromFile retrieves credentials from file +func getCredentialsFromFile() (string, string, error) { configDir := GetConfigDir() - apiKeyFile := fmt.Sprintf("%s/api-key", configDir) + credentialsFile := fmt.Sprintf("%s/credentials", configDir) - data, err := os.ReadFile(apiKeyFile) + data, err := os.ReadFile(credentialsFile) if err != nil { - // If the file does not exist, treat as not logged in if os.IsNotExist(err) { - return "", ErrNotLoggedIn + return "", "", ErrNotLoggedIn } - return "", fmt.Errorf("failed to read API key file: %w", err) + return "", "", fmt.Errorf("failed to read credentials file: %w", err) } - apiKey := strings.TrimSpace(string(data)) - - // If file exists but is empty, treat as not logged in - if apiKey == "" { - return "", ErrNotLoggedIn + credentials := strings.TrimSpace(string(data)) + if credentials == "" { + return "", "", ErrNotLoggedIn } - return apiKey, nil + return parseCredentials(credentials) } -// RemoveAPIKey removes the API key from keyring and file fallback -func RemoveAPIKey() error { - RemoveAPIKeyFromKeyring() +// parseCredentials parses the stored credentials from JSON format +// Returns (apiKey, projectID, error) where apiKey is in "publicKey:secretKey" format +func parseCredentials(combined string) (string, string, error) { + var creds storedCredentials + if err := json.Unmarshal([]byte(combined), &creds); err != nil { + return "", "", fmt.Errorf("failed to parse credentials: %w", err) + } + + if creds.APIKey == "" { + return "", "", fmt.Errorf("API key not found in stored credentials") + } + if creds.ProjectID == "" { + return "", "", fmt.Errorf("project ID not found in stored credentials") + } - // Remove from file fallback - return RemoveAPIKeyFromFile() + return creds.APIKey, creds.ProjectID, nil } -func RemoveAPIKeyFromKeyring() error { - // Try to remove from keyring (ignore errors as it might not exist) - return keyring.Delete(GetServiceName(), keyringUsername) +// RemoveCredentials removes stored credentials from keyring and file fallback +func RemoveCredentials() error { + // Remove from keyring (ignore errors as it might not exist) + removeCredentialsFromKeyring() + return removeCredentialsFile() } -// RemoveAPIKeyFromFile removes the API key file -func RemoveAPIKeyFromFile() error { - configDir := GetConfigDir() - apiKeyFile := fmt.Sprintf("%s/api-key", configDir) +// removeCredentialsFromKeyring removes credentials from keyring (test helper) +func removeCredentialsFromKeyring() { + keyring.Delete(GetServiceName(), keyringUsername) +} - err := os.Remove(apiKeyFile) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove API key file: %w", err) +// removeCredentialsFile removes credentials file +func removeCredentialsFile() error { + configDir := GetConfigDir() + credentialsFile := fmt.Sprintf("%s/credentials", configDir) + if err := os.Remove(credentialsFile); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove credentials file: %w", err) } - return nil } diff --git a/internal/tiger/config/api_key_test.go b/internal/tiger/config/api_key_test.go index bc03f993..a7dab33d 100644 --- a/internal/tiger/config/api_key_test.go +++ b/internal/tiger/config/api_key_test.go @@ -6,15 +6,12 @@ import ( "testing" ) -func setupAPIKeyTest(t *testing.T) string { +func setupCredentialTest(t *testing.T) string { t.Helper() // Use a unique service name for this test to avoid conflicts SetTestServiceName(t) - // Clean up any existing keyring entries before test - RemoveAPIKeyFromKeyring() - // Create temporary directory for test config tmpDir, err := os.MkdirTemp("", "tiger-api-key-test-*") if err != nil { @@ -23,13 +20,17 @@ func setupAPIKeyTest(t *testing.T) string { // Reset viper completely and set up with test directory // This ensures proper test isolation by resetting all viper state + // MUST be done before RemoveCredentials() so it uses the test directory! if _, err := UseTestConfig(tmpDir, map[string]any{}); err != nil { t.Fatalf("Failed to use test config: %v", err) } + // Clean up any existing credentials in the test directory + RemoveCredentials() + t.Cleanup(func() { - // Clean up keyring entries - RemoveAPIKeyFromKeyring() + // Clean up credentials + RemoveCredentials() // Reset global config to ensure test isolation ResetGlobalConfig() @@ -41,18 +42,19 @@ func setupAPIKeyTest(t *testing.T) string { return tmpDir } -func TestStoreAPIKeyToFile(t *testing.T) { - tmpDir := setupAPIKeyTest(t) +func TestStoreCredentialsToFile(t *testing.T) { + tmpDir := setupCredentialTest(t) - if err := StoreAPIKeyToFile("file-test-key"); err != nil { - t.Fatalf("Failed to store API key to file: %v", err) + // Store credentials in new JSON format + if err := StoreCredentialsToFile("public:secret", "project123"); err != nil { + t.Fatalf("Failed to store credentials to file: %v", err) } - // Verify file exists and has correct permissions - apiKeyFile := filepath.Join(tmpDir, "api-key") - info, err := os.Stat(apiKeyFile) + // Verify file exists and has correct permissions (now stored as "credentials") + credentialsFile := filepath.Join(tmpDir, "credentials") + info, err := os.Stat(credentialsFile) if err != nil { - t.Fatalf("API key file should exist: %v", err) + t.Fatalf("Credentials file should exist: %v", err) } // Check file permissions (should be 0600) @@ -60,45 +62,51 @@ func TestStoreAPIKeyToFile(t *testing.T) { t.Errorf("Expected file permissions 0600, got %o", info.Mode().Perm()) } - // Verify file content - data, err := os.ReadFile(apiKeyFile) + // Verify file content is valid JSON + data, err := os.ReadFile(credentialsFile) if err != nil { - t.Fatalf("Failed to read API key file: %v", err) + t.Fatalf("Failed to read credentials file: %v", err) } - if string(data) != "file-test-key" { - t.Errorf("Expected 'file-test-key', got '%s'", string(data)) + expectedJSON := `{"api_key":"public:secret","project_id":"project123"}` + if string(data) != expectedJSON { + t.Errorf("Expected '%s', got '%s'", expectedJSON, string(data)) } } -func TestGetAPIKeyFromFile(t *testing.T) { - tmpDir := setupAPIKeyTest(t) +func TestGetCredentialsFromFile(t *testing.T) { + tmpDir := setupCredentialTest(t) - // Write API key to file - apiKeyFile := filepath.Join(tmpDir, "api-key") - if err := os.WriteFile(apiKeyFile, []byte("file-get-test-key"), 0600); err != nil { - t.Fatalf("Failed to write test API key file: %v", err) + // Write credentials to file in JSON format + credentialsFile := filepath.Join(tmpDir, "credentials") + jsonData := `{"api_key":"public:secret","project_id":"project456"}` + if err := os.WriteFile(credentialsFile, []byte(jsonData), 0600); err != nil { + t.Fatalf("Failed to write test credentials file: %v", err) } - // Get API key - should get from file since keyring is empty + // Get credentials - should get from file since keyring is empty // (each test uses a unique keyring service name) - apiKey, err := GetAPIKey() + apiKey, projectID, err := GetCredentials() if err != nil { - t.Fatalf("Failed to get API key from file: %v", err) + t.Fatalf("Failed to get credentials from file: %v", err) } - if apiKey != "file-get-test-key" { - t.Errorf("Expected 'file-get-test-key', got '%s'", apiKey) + // Should return combined API key (publicKey:secretKey) and project ID + if apiKey != "public:secret" { + t.Errorf("Expected API key 'public:secret', got '%s'", apiKey) + } + if projectID != "project456" { + t.Errorf("Expected project ID 'project456', got '%s'", projectID) } } -func TestGetAPIKeyFromFile_NotExists(t *testing.T) { - setupAPIKeyTest(t) +func TestGetCredentialsFromFile_NotExists(t *testing.T) { + setupCredentialTest(t) - // Try to get API key when file doesn't exist - _, err := GetAPIKey() + // Try to get credentials when file doesn't exist + _, _, err := GetCredentials() if err == nil { - t.Fatal("Expected error when API key file doesn't exist") + t.Fatal("Expected error when credentials file doesn't exist") } if err.Error() != "not logged in" { @@ -106,31 +114,31 @@ func TestGetAPIKeyFromFile_NotExists(t *testing.T) { } } -func TestRemoveAPIKeyFromFile(t *testing.T) { - tmpDir := setupAPIKeyTest(t) +func TestRemoveCredentialsFromFile(t *testing.T) { + tmpDir := setupCredentialTest(t) - // Write API key to file - apiKeyFile := filepath.Join(tmpDir, "api-key") - if err := os.WriteFile(apiKeyFile, []byte("remove-test-key"), 0600); err != nil { - t.Fatalf("Failed to write test API key file: %v", err) + // Write credentials to file + credentialsFile := filepath.Join(tmpDir, "credentials") + if err := os.WriteFile(credentialsFile, []byte(`{"api_key":"test:key","project_id":"test-proj"}`), 0600); err != nil { + t.Fatalf("Failed to write test credentials file: %v", err) } - // Remove API key file - if err := RemoveAPIKeyFromFile(); err != nil { - t.Fatalf("Failed to remove API key file: %v", err) + // Remove credentials file + if err := RemoveCredentials(); err != nil { + t.Fatalf("Failed to remove credentials file: %v", err) } // Verify file is removed - if _, err := os.Stat(apiKeyFile); !os.IsNotExist(err) { - t.Fatal("API key file should be removed") + if _, err := os.Stat(credentialsFile); !os.IsNotExist(err) { + t.Fatal("Credentials file should be removed") } } -func TestRemoveAPIKeyFromFile_NotExists(t *testing.T) { - setupAPIKeyTest(t) +func TestRemoveCredentialsFromFile_NotExists(t *testing.T) { + setupCredentialTest(t) - // Try to remove API key file when it doesn't exist (should not error) - if err := RemoveAPIKeyFromFile(); err != nil { + // Try to remove credentials file when it doesn't exist (should not error) + if err := RemoveCredentials(); err != nil { t.Fatalf("Should not error when removing non-existent file: %v", err) } } diff --git a/internal/tiger/config/config.go b/internal/tiger/config/config.go index 4524f419..a0bc5b8a 100644 --- a/internal/tiger/config/config.go +++ b/internal/tiger/config/config.go @@ -26,7 +26,6 @@ type Config struct { GatewayURL string `mapstructure:"gateway_url" yaml:"gateway_url"` Output string `mapstructure:"output" yaml:"output"` PasswordStorage string `mapstructure:"password_storage" yaml:"password_storage"` - ProjectID string `mapstructure:"project_id" yaml:"project_id"` ReleasesURL string `mapstructure:"releases_url" yaml:"releases_url"` ServiceID string `mapstructure:"service_id" yaml:"service_id"` VersionCheckInterval time.Duration `mapstructure:"version_check_interval" yaml:"version_check_interval"` @@ -44,7 +43,6 @@ type ConfigOutput struct { GatewayURL *string `mapstructure:"gateway_url" json:"gateway_url,omitempty" yaml:"gateway_url,omitempty"` Output *string `mapstructure:"output" json:"output,omitempty" yaml:"output,omitempty"` PasswordStorage *string `mapstructure:"password_storage" json:"password_storage,omitempty" yaml:"password_storage,omitempty"` - ProjectID *string `mapstructure:"project_id" json:"project_id,omitempty" yaml:"project_id,omitempty"` ReleasesURL *string `mapstructure:"releases_url" json:"releases_url,omitempty" yaml:"releases_url,omitempty"` ServiceID *string `mapstructure:"service_id" json:"service_id,omitempty" yaml:"service_id,omitempty"` VersionCheckInterval *time.Duration `mapstructure:"version_check_interval" json:"version_check_interval,omitempty" yaml:"version_check_interval,omitempty"` @@ -72,7 +70,6 @@ var defaultValues = map[string]any{ "gateway_url": DefaultGatewayURL, "docs_mcp": DefaultDocsMCP, "docs_mcp_url": DefaultDocsMCPURL, - "project_id": "", "service_id": "", "output": DefaultOutput, "analytics": DefaultAnalytics, @@ -299,14 +296,6 @@ func (c *Config) updateField(key string, value any) (any, error) { c.DocsMCPURL = s validated = s - case "project_id": - s, ok := value.(string) - if !ok { - return nil, fmt.Errorf("project_id must be string, got %T", value) - } - c.ProjectID = s - validated = s - case "service_id": s, ok := value.(string) if !ok { @@ -485,18 +474,12 @@ func (c *Config) Reset() error { v := viper.New() v.SetConfigFile(configFile) - // Preserve the project id, as this is part of the auth scheme - v.Set("project_id", c.ProjectID) - if err := v.WriteConfigAs(configFile); err != nil { return fmt.Errorf("error writing config file: %w", err) } // Apply all defaults to the current global viper state for key, value := range defaultValues { - if key == "project_id" { - continue - } if _, err := c.updateField(key, value); err != nil { return err } diff --git a/internal/tiger/config/config_test.go b/internal/tiger/config/config_test.go index a9789ec8..fbf733eb 100644 --- a/internal/tiger/config/config_test.go +++ b/internal/tiger/config/config_test.go @@ -79,7 +79,6 @@ func TestLoad_FromConfigFile(t *testing.T) { // Create config file configContent := `api_url: https://custom.api.com/v1 -project_id: test-project-123 service_id: test-service-456 output: json analytics: false @@ -104,9 +103,6 @@ analytics: false if cfg.APIURL != "https://custom.api.com/v1" { t.Errorf("Expected APIURL https://custom.api.com/v1, got %s", cfg.APIURL) } - if cfg.ProjectID != "test-project-123" { - t.Errorf("Expected ProjectID test-project-123, got %s", cfg.ProjectID) - } if cfg.ServiceID != "test-service-456" { t.Errorf("Expected ServiceID test-service-456, got %s", cfg.ServiceID) } @@ -124,7 +120,6 @@ func TestLoad_FromEnvironmentVariables(t *testing.T) { // Set environment variables os.Setenv("TIGER_CONFIG_DIR", tmpDir) os.Setenv("TIGER_API_URL", "https://env.api.com/v1") - os.Setenv("TIGER_PROJECT_ID", "env-project-789") os.Setenv("TIGER_SERVICE_ID", "env-service-101") os.Setenv("TIGER_OUTPUT", "yaml") os.Setenv("TIGER_ANALYTICS", "false") @@ -134,7 +129,6 @@ func TestLoad_FromEnvironmentVariables(t *testing.T) { defer func() { os.Unsetenv("TIGER_CONFIG_DIR") os.Unsetenv("TIGER_API_URL") - os.Unsetenv("TIGER_PROJECT_ID") os.Unsetenv("TIGER_SERVICE_ID") os.Unsetenv("TIGER_OUTPUT") os.Unsetenv("TIGER_ANALYTICS") @@ -149,9 +143,6 @@ func TestLoad_FromEnvironmentVariables(t *testing.T) { if cfg.APIURL != "https://env.api.com/v1" { t.Errorf("Expected APIURL https://env.api.com/v1, got %s", cfg.APIURL) } - if cfg.ProjectID != "env-project-789" { - t.Errorf("Expected ProjectID env-project-789, got %s", cfg.ProjectID) - } if cfg.ServiceID != "env-service-101" { t.Errorf("Expected ServiceID env-service-101, got %s", cfg.ServiceID) } @@ -168,7 +159,6 @@ func TestLoad_Precedence(t *testing.T) { // Create config file with some values configContent := `api_url: https://file.api.com/v1 -project_id: file-project output: table analytics: true ` @@ -179,14 +169,12 @@ analytics: true // Set environment variables that should override config file os.Setenv("TIGER_CONFIG_DIR", tmpDir) - os.Setenv("TIGER_PROJECT_ID", "env-project-override") os.Setenv("TIGER_OUTPUT", "json") setupViper(t, tmpDir) defer func() { os.Unsetenv("TIGER_CONFIG_DIR") - os.Unsetenv("TIGER_PROJECT_ID") os.Unsetenv("TIGER_OUTPUT") }() @@ -196,9 +184,6 @@ analytics: true } // Environment should override config file - if cfg.ProjectID != "env-project-override" { - t.Errorf("Expected ProjectID env-project-override (env override), got %s", cfg.ProjectID) - } if cfg.Output != "json" { t.Errorf("Expected Output json (env override), got %s", cfg.Output) } @@ -248,7 +233,6 @@ func TestSave(t *testing.T) { cfg, err := UseTestConfig(tmpDir, map[string]any{ "api_url": "https://test.api.com/v1", - "project_id": "test-project", "service_id": "test-service", "output": "json", "analytics": false, @@ -280,9 +264,6 @@ func TestSave(t *testing.T) { if loadedCfg.APIURL != cfg.APIURL { t.Errorf("Expected APIURL %s, got %s", cfg.APIURL, loadedCfg.APIURL) } - if loadedCfg.ProjectID != cfg.ProjectID { - t.Errorf("Expected ProjectID %s, got %s", cfg.ProjectID, loadedCfg.ProjectID) - } if loadedCfg.ServiceID != cfg.ServiceID { t.Errorf("Expected ServiceID %s, got %s", cfg.ServiceID, loadedCfg.ServiceID) } @@ -318,13 +299,6 @@ func TestSet(t *testing.T) { return cfg.APIURL == "https://new.api.com/v1" }, }, - { - key: "project_id", - value: "new-project-123", - checkFunc: func() bool { - return cfg.ProjectID == "new-project-123" - }, - }, { key: "service_id", value: "new-service-456", @@ -413,7 +387,6 @@ func TestUnset(t *testing.T) { cfg := &Config{ APIURL: "https://custom.api.com/v1", - ProjectID: "custom-project", ServiceID: "custom-service", Output: "json", Analytics: false, @@ -431,12 +404,6 @@ func TestUnset(t *testing.T) { return cfg.APIURL == DefaultAPIURL }, }, - { - key: "project_id", - checkFunc: func() bool { - return cfg.ProjectID == "" - }, - }, { key: "service_id", checkFunc: func() bool { @@ -490,7 +457,6 @@ func TestReset(t *testing.T) { cfg := &Config{ APIURL: "https://custom.api.com/v1", - ProjectID: "custom-project", ServiceID: "custom-service", Output: "json", Analytics: false, @@ -502,12 +468,7 @@ func TestReset(t *testing.T) { t.Fatalf("Reset() failed: %v", err) } - // ProjectID should be preserved - if cfg.ProjectID != "custom-project" { - t.Errorf("Expected ProjectID %s, got %s", "custom-project", cfg.ProjectID) - } - - // Verify all other values are reset to defaults + // Verify all values are reset to defaults if cfg.APIURL != DefaultAPIURL { t.Errorf("Expected APIURL %s, got %s", DefaultAPIURL, cfg.APIURL) } @@ -568,7 +529,6 @@ func TestLoad_ErrorHandling(t *testing.T) { // Create invalid YAML config file invalidConfig := `api_url: https://test.api.com/v1 -project_id: test-project invalid yaml content [ ` configFile := GetConfigFile(tmpDir) @@ -706,10 +666,10 @@ func TestResetGlobalConfig(t *testing.T) { // Set environment variable for test os.Setenv("TIGER_CONFIG_DIR", tmpDir) - os.Setenv("TIGER_PROJECT_ID", "test-project-before-reset") + os.Setenv("TIGER_SERVICE_ID", "test-service-before-reset") defer func() { os.Unsetenv("TIGER_CONFIG_DIR") - os.Unsetenv("TIGER_PROJECT_ID") + os.Unsetenv("TIGER_SERVICE_ID") }() // Load config first @@ -719,8 +679,8 @@ func TestResetGlobalConfig(t *testing.T) { } // Verify environment was used - if cfg1.ProjectID != "test-project-before-reset" { - t.Errorf("Expected project ID from env, got %s", cfg1.ProjectID) + if cfg1.ServiceID != "test-service-before-reset" { + t.Errorf("Expected service ID from env, got %s", cfg1.ServiceID) } // Reset global viper state @@ -730,7 +690,7 @@ func TestResetGlobalConfig(t *testing.T) { setupViper(t, tmpDir) // Change env var - os.Setenv("TIGER_PROJECT_ID", "test-project-after-reset") + os.Setenv("TIGER_SERVICE_ID", "test-service-after-reset") // Load again should pick up new env value cfg2, err := Load() @@ -744,7 +704,7 @@ func TestResetGlobalConfig(t *testing.T) { } // Should have new env value - if cfg2.ProjectID != "test-project-after-reset" { - t.Errorf("Expected new project ID after reset, got %s", cfg2.ProjectID) + if cfg2.ServiceID != "test-service-after-reset" { + t.Errorf("Expected new service ID after reset, got %s", cfg2.ServiceID) } } diff --git a/internal/tiger/mcp/db_tools.go b/internal/tiger/mcp/db_tools.go index 00e9a19c..3b96554a 100644 --- a/internal/tiger/mcp/db_tools.go +++ b/internal/tiger/mcp/db_tools.go @@ -111,14 +111,8 @@ WARNING: Use with caution - this tool can execute any SQL statement including IN // handleDBExecuteQuery handles the tiger_db_execute_query MCP tool func (s *Server) handleDBExecuteQuery(ctx context.Context, req *mcp.CallToolRequest, input DBExecuteQueryInput) (*mcp.CallToolResult, DBExecuteQueryOutput, error) { - // Load config and validate project ID - cfg, err := s.loadConfigWithProjectID() - if err != nil { - return nil, DBExecuteQueryOutput{}, err - } - - // Create fresh API client with current credentials - apiClient, err := s.createAPIClient() + // Create fresh API client and get project ID + apiClient, projectID, err := s.createAPIClient() if err != nil { return nil, DBExecuteQueryOutput{}, err } @@ -127,7 +121,7 @@ func (s *Server) handleDBExecuteQuery(ctx context.Context, req *mcp.CallToolRequ timeout := time.Duration(input.TimeoutSeconds) * time.Second logging.Debug("MCP: Executing database query", - zap.String("project_id", cfg.ProjectID), + zap.String("project_id", projectID), zap.String("service_id", input.ServiceID), zap.Duration("timeout", timeout), zap.String("role", input.Role), @@ -135,7 +129,7 @@ func (s *Server) handleDBExecuteQuery(ctx context.Context, req *mcp.CallToolRequ ) // Get service details to construct connection string - serviceResp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, cfg.ProjectID, input.ServiceID) + serviceResp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, input.ServiceID) if err != nil { return nil, DBExecuteQueryOutput{}, fmt.Errorf("failed to get service details: %w", err) } diff --git a/internal/tiger/mcp/server.go b/internal/tiger/mcp/server.go index d4143d04..94a15407 100644 --- a/internal/tiger/mcp/server.go +++ b/internal/tiger/mcp/server.go @@ -70,34 +70,29 @@ func (s *Server) registerTools(ctx context.Context) { logging.Info("MCP tools registered successfully") } -// createAPIClient loads fresh config and creates a new API client for each tool call -func (s *Server) createAPIClient() (*api.ClientWithResponses, error) { - // Get fresh API key - apiKey, err := config.GetAPIKey() +// createAPIClient creates a new API client and returns it with the project ID +func (s *Server) createAPIClient() (*api.ClientWithResponses, string, error) { + // Get credentials (API key + project ID) + apiKey, projectID, err := config.GetCredentials() if err != nil { - return nil, fmt.Errorf("authentication required: %w", err) + return nil, "", fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err) } // Create API client with fresh credentials apiClient, err := api.NewTigerClient(apiKey) if err != nil { - return nil, fmt.Errorf("failed to create API client: %w", err) + return nil, "", fmt.Errorf("failed to create API client: %w", err) } - return apiClient, nil + return apiClient, projectID, nil } -// loadConfigWithProjectID loads fresh config and validates that project ID is set -func (s *Server) loadConfigWithProjectID() (*config.Config, error) { - // Load fresh config +// loadConfig loads fresh config +func (s *Server) loadConfig() (*config.Config, error) { cfg, err := config.Load() if err != nil { return nil, fmt.Errorf("failed to load config: %w", err) } - - if cfg.ProjectID == "" { - return nil, fmt.Errorf("project ID is required. Please run 'tiger auth login'") - } return cfg, nil } diff --git a/internal/tiger/mcp/service_tools.go b/internal/tiger/mcp/service_tools.go index 6512a8d8..a368497c 100644 --- a/internal/tiger/mcp/service_tools.go +++ b/internal/tiger/mcp/service_tools.go @@ -295,25 +295,19 @@ WARNING: Creates billable resources.`, // handleServiceList handles the service_list MCP tool func (s *Server) handleServiceList(ctx context.Context, req *mcp.CallToolRequest, input ServiceListInput) (*mcp.CallToolResult, ServiceListOutput, error) { - // Load config and validate project ID - cfg, err := s.loadConfigWithProjectID() - if err != nil { - return nil, ServiceListOutput{}, err - } - - // Create fresh API client with current credentials - apiClient, err := s.createAPIClient() + // Create fresh API client and get project ID + apiClient, projectID, err := s.createAPIClient() if err != nil { return nil, ServiceListOutput{}, err } - logging.Debug("MCP: Listing services", zap.String("project_id", cfg.ProjectID)) + logging.Debug("MCP: Listing services", zap.String("project_id", projectID)) // Make API call to list services ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - resp, err := apiClient.GetProjectsProjectIdServicesWithResponse(ctx, cfg.ProjectID) + resp, err := apiClient.GetProjectsProjectIdServicesWithResponse(ctx, projectID) if err != nil { return nil, ServiceListOutput{}, fmt.Errorf("failed to list services: %w", err) } @@ -341,27 +335,21 @@ func (s *Server) handleServiceList(ctx context.Context, req *mcp.CallToolRequest // handleServiceGet handles the service_get MCP tool func (s *Server) handleServiceGet(ctx context.Context, req *mcp.CallToolRequest, input ServiceGetInput) (*mcp.CallToolResult, ServiceGetOutput, error) { - // Load config and validate project ID - cfg, err := s.loadConfigWithProjectID() - if err != nil { - return nil, ServiceGetOutput{}, err - } - - // Create fresh API client with current credentials - apiClient, err := s.createAPIClient() + // Create fresh API client and get project ID + apiClient, projectID, err := s.createAPIClient() if err != nil { return nil, ServiceGetOutput{}, err } logging.Debug("MCP: Getting service details", - zap.String("project_id", cfg.ProjectID), + zap.String("project_id", projectID), zap.String("service_id", input.ServiceID)) // Make API call to get service details ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - resp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, cfg.ProjectID, input.ServiceID) + resp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, input.ServiceID) if err != nil { return nil, ServiceGetOutput{}, fmt.Errorf("failed to get service details: %w", err) } @@ -395,13 +383,13 @@ func (s *Server) handleServiceGet(ctx context.Context, req *mcp.CallToolRequest, // handleServiceCreate handles the service_create MCP tool func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolRequest, input ServiceCreateInput) (*mcp.CallToolResult, ServiceCreateOutput, error) { // Load config and validate project ID - cfg, err := s.loadConfigWithProjectID() + cfg, err := s.loadConfig() if err != nil { return nil, ServiceCreateOutput{}, err } - // Create fresh API client with current credentials - apiClient, err := s.createAPIClient() + // Create fresh API client and get project ID + apiClient, projectID, err := s.createAPIClient() if err != nil { return nil, ServiceCreateOutput{}, err } @@ -421,7 +409,7 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque } logging.Debug("MCP: Creating service", - zap.String("project_id", cfg.ProjectID), + zap.String("project_id", projectID), zap.String("name", input.Name), zap.Strings("addons", input.Addons), zap.Stringp("region", input.Region), @@ -444,7 +432,7 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - resp, err := apiClient.PostProjectsProjectIdServicesWithResponse(ctx, cfg.ProjectID, serviceCreateReq) + resp, err := apiClient.PostProjectsProjectIdServicesWithResponse(ctx, projectID, serviceCreateReq) if err != nil { return nil, ServiceCreateOutput{}, fmt.Errorf("failed to create service: %w", err) } @@ -506,7 +494,7 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque if input.Wait { timeout := time.Duration(input.TimeoutMinutes) * time.Minute - if status, err := s.waitForServiceReady(apiClient, cfg.ProjectID, serviceID, timeout, service.Status); err != nil { + if status, err := s.waitForServiceReady(apiClient, projectID, serviceID, timeout, service.Status); err != nil { output.Message = fmt.Sprintf("Error: %s", err.Error()) } else { output.Service.Status = util.DerefStr(status) @@ -519,20 +507,14 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque // handleServiceUpdatePassword handles the service_update_password MCP tool func (s *Server) handleServiceUpdatePassword(ctx context.Context, req *mcp.CallToolRequest, input ServiceUpdatePasswordInput) (*mcp.CallToolResult, ServiceUpdatePasswordOutput, error) { - // Load config and validate project ID - cfg, err := s.loadConfigWithProjectID() - if err != nil { - return nil, ServiceUpdatePasswordOutput{}, err - } - - // Create fresh API client with current credentials - apiClient, err := s.createAPIClient() + // Create fresh API client and get project ID + apiClient, projectID, err := s.createAPIClient() if err != nil { return nil, ServiceUpdatePasswordOutput{}, err } logging.Debug("MCP: Updating service password", - zap.String("project_id", cfg.ProjectID), + zap.String("project_id", projectID), zap.String("service_id", input.ServiceID)) // Prepare password update request @@ -544,7 +526,7 @@ func (s *Server) handleServiceUpdatePassword(ctx context.Context, req *mcp.CallT ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - resp, err := apiClient.PostProjectsProjectIdServicesServiceIdUpdatePasswordWithResponse(ctx, cfg.ProjectID, input.ServiceID, updateReq) + resp, err := apiClient.PostProjectsProjectIdServicesServiceIdUpdatePasswordWithResponse(ctx, projectID, input.ServiceID, updateReq) if err != nil { return nil, ServiceUpdatePasswordOutput{}, fmt.Errorf("failed to update service password: %w", err) } @@ -559,7 +541,7 @@ func (s *Server) handleServiceUpdatePassword(ctx context.Context, req *mcp.CallT } // Get service details for password storage (similar to CLI implementation) - serviceResp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, cfg.ProjectID, input.ServiceID) + serviceResp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, input.ServiceID) if err == nil && serviceResp.StatusCode() == 200 && serviceResp.JSON200 != nil { // Save the new password using the shared util function result, err := password.SavePasswordWithResult(api.Service(*serviceResp.JSON200), input.Password) From e179aaf50dfddedf56dd0b7dd28021bae11bdcc4 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Fri, 17 Oct 2025 17:02:00 -0400 Subject: [PATCH 2/9] Update tiger auth status to display project ID --- internal/tiger/cmd/auth.go | 4 +++- internal/tiger/cmd/auth_test.go | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/tiger/cmd/auth.go b/internal/tiger/cmd/auth.go index bb4117a9..0ebcc3e6 100644 --- a/internal/tiger/cmd/auth.go +++ b/internal/tiger/cmd/auth.go @@ -173,12 +173,14 @@ func buildStatusCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - if _, _, err := config.GetCredentials(); err != nil { + _, projectID, err := config.GetCredentials() + if err != nil { return err } // TODO: Make API call to get token information fmt.Fprintln(cmd.OutOrStdout(), "Logged in (API key stored)") + fmt.Fprintf(cmd.OutOrStdout(), "Project ID: %s\n", projectID) return nil }, diff --git a/internal/tiger/cmd/auth_test.go b/internal/tiger/cmd/auth_test.go index 30c40f57..57ddca86 100644 --- a/internal/tiger/cmd/auth_test.go +++ b/internal/tiger/cmd/auth_test.go @@ -575,7 +575,7 @@ func TestAuthLogin_KeyringFallback(t *testing.T) { if err != nil { t.Fatalf("Status failed with file storage: %v", err) } - if output != "Logged in (API key stored)\n" { + if output != "Logged in (API key stored)\nProject ID: test-project-fallback\n" { t.Errorf("Unexpected status output: '%s'", output) } @@ -657,7 +657,7 @@ func TestAuthStatus_LoggedIn(t *testing.T) { t.Fatalf("Status failed: %v", err) } - if output != "Logged in (API key stored)\n" { + if output != "Logged in (API key stored)\nProject ID: test-project-789\n" { t.Errorf("Unexpected output: '%s' (len=%d)", output, len(output)) } } From f4ade05f64411612ba1c42d9c84e7d8ffb9aeebc Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Fri, 17 Oct 2025 17:04:02 -0400 Subject: [PATCH 3/9] Rename api_key.go -> credentials.go --- internal/tiger/config/{api_key.go => credentials.go} | 0 internal/tiger/config/{api_key_test.go => credentials_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename internal/tiger/config/{api_key.go => credentials.go} (100%) rename internal/tiger/config/{api_key_test.go => credentials_test.go} (100%) diff --git a/internal/tiger/config/api_key.go b/internal/tiger/config/credentials.go similarity index 100% rename from internal/tiger/config/api_key.go rename to internal/tiger/config/credentials.go diff --git a/internal/tiger/config/api_key_test.go b/internal/tiger/config/credentials_test.go similarity index 100% rename from internal/tiger/config/api_key_test.go rename to internal/tiger/config/credentials_test.go From fe946cc9fa7a6077ba34083a5bc5c6f0600037db Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Fri, 17 Oct 2025 17:32:32 -0400 Subject: [PATCH 4/9] Update documentation and specs --- CLAUDE.md | 4 ---- README.md | 5 +---- specs/spec.md | 41 ++++++++++++++++------------------------- specs/spec_mcp.md | 4 ++-- 4 files changed, 19 insertions(+), 35 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index df260bf1..24a369c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,7 +179,6 @@ Key configuration values: - `gateway_url`: Tiger Gateway URL - `docs_mcp`: Enable/disable proxied docs MCP tools - `docs_mcp_url`: URL for docs MCP server -- `project_id`: Default project ID - `service_id`: Default service ID - `output`: Output format (json, yaml, table) - `analytics`: Usage analytics toggle @@ -271,7 +270,6 @@ Global flags available on all commands: - `--config-dir`: Path to configuration directory - `--debug`: Enable debug logging - `--output/-o`: Set output format -- `--project-id`: Override project ID - `--service-id`: Override service ID - `--analytics`: Toggle analytics - `--password-storage`: Password storage method @@ -405,7 +403,6 @@ func buildRootCmd() *cobra.Command { // Declare ALL flag variables locally within this function var configDir string var debug bool - var projectID string var serviceID string var analytics bool var passwordStorage string @@ -428,7 +425,6 @@ func buildRootCmd() *cobra.Command { cobra.OnInitialize(initConfigFunc) cmd.PersistentFlags().StringVar(&configDir, "config-dir", config.GetDefaultConfigDir(), "config directory") cmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging") - cmd.PersistentFlags().StringVar(&projectID, "project-id", "", "project ID") cmd.PersistentFlags().StringVar(&serviceID, "service-id", "", "service ID") cmd.PersistentFlags().BoolVar(&analytics, "analytics", true, "enable/disable usage analytics") cmd.PersistentFlags().StringVar(&passwordStorage, "password-storage", config.DefaultPasswordStorage, "password storage method (keyring, pgpass, none)") diff --git a/README.md b/README.md index 6b42f911..1d026ea3 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Tiger CLI provides the following commands: - `tiger auth` - Authentication management - `login` - Log in to your Tiger account - `logout` - Log out from your Tiger account - - `status` - Show current authentication status + - `status` - Show current authentication status and project ID - `tiger service` - Service lifecycle management - `list` - List all services - `create` - Create a new service @@ -211,7 +211,6 @@ tiger config reset All configuration options can be set via `tiger config set `: - `docs_mcp` - Enable/disable docs MCP proxy (default: `true`) -- `project_id` - Default project ID (set via `tiger auth login`) - `service_id` - Default service ID - `output` - Output format: `json`, `yaml`, or `table` (default: `table`) - `analytics` - Enable/disable analytics (default: `true`) @@ -224,7 +223,6 @@ Environment variables override configuration file values. All variables use the - `TIGER_CONFIG_DIR` - Path to configuration directory (default: `~/.config/tiger`) - `TIGER_DOCS_MCP` - Enable/disable docs MCP proxy -- `TIGER_PROJECT_ID` - Default project ID - `TIGER_SERVICE_ID` - Default service ID - `TIGER_OUTPUT` - Output format: `json`, `yaml`, or `table` - `TIGER_ANALYTICS` - Enable/disable analytics @@ -238,7 +236,6 @@ Environment variables override configuration file values. All variables use the These flags are available on all commands and take precedence over both environment variables and configuration file values: - `--config-dir ` - Path to configuration directory (default: `~/.config/tiger`) -- `--project-id ` - Specify project ID - `--service-id ` - Specify service ID - `--analytics` - Enable/disable analytics - `--password-storage ` - Password storage method: `keyring`, `pgpass`, or `none` diff --git a/specs/spec.md b/specs/spec.md index 8098bfb5..5cfb34fe 100644 --- a/specs/spec.md +++ b/specs/spec.md @@ -23,9 +23,6 @@ mv tiger /usr/local/bin/ ### Environment Variables -- `TIGER_PUBLIC_KEY`: TigerData public key for authentication -- `TIGER_SECRET_KEY`: TigerData secret key for authentication -- `TIGER_PROJECT_ID`: Default project ID to use - `TIGER_API_URL`: Base URL for TigerData API (default: https://api.tigerdata.com/public/v1) - `TIGER_SERVICE_ID`: Default service ID to use - `TIGER_CONFIG_DIR`: Configuration directory (default: ~/.config/tiger) @@ -38,7 +35,6 @@ Location: `~/.config/tiger/config.yaml` ```yaml api_url: https://api.tigerdata.com/public/v1 -project_id: your-default-project-id service_id: your-default-service-id output: table analytics: true @@ -47,7 +43,6 @@ analytics: true ### Global Options - `--config-dir`: Path to configuration directory -- `--project-id`: Specify project ID - `--service-id`: Override default service ID (can also be specified positionally for single-service commands) - `--analytics`: Toggle analytics collection - `--password-storage`: Password storage method (keyring, pgpass, none) (default: keyring) @@ -105,7 +100,7 @@ Manage authentication and credentials (token-based only). **Subcommands:** - `login`: Authenticate with API token - `logout`: Remove stored credentials -- `status`: Show current user information +- `status`: Show current authentication status and project ID **Examples:** ```bash @@ -117,14 +112,14 @@ tiger auth login --project-id proj-123 # Login with credentials from environment variables export TIGER_PUBLIC_KEY="your-public-key" -export TIGER_SECRET_KEY="your-secret-key" +export TIGER_SECRET_KEY="your-secret-key" export TIGER_PROJECT_ID="proj-123" tiger auth login # Interactive login (will prompt for any missing credentials) tiger auth login -# Show current user +# Show current authentication status and project ID tiger auth status # Logout @@ -132,30 +127,29 @@ tiger auth logout ``` **Authentication Methods:** -1. `--public-key` and `--secret-key` flags: Provide public and secret keys directly -2. `TIGER_PUBLIC_KEY` and `TIGER_SECRET_KEY` environment variables -3. `TIGER_PROJECT_ID` environment variable for project ID -4. Interactive prompt for any missing credentials (requires TTY) +1. `--public-key`, `--secret-key`, and `--project-id` flags: Provide public key, secret key, and project ID directly +2. `TIGER_PUBLIC_KEY`, `TIGER_SECRET_KEY`, and `TIGER_PROJECT_ID` environment variables +3. Interactive prompt for any missing credentials (requires TTY) **Login Process:** When using `tiger auth login`, the CLI will: 1. Prompt for any missing credentials (public key, secret key, project ID) if not provided via flags or environment variables -2. Combine the public and secret keys into format `:` for internal storage +2. Combine the public and secret keys into format `:` 3. Validate the combined API key by making a test API call -4. Store the combined API key securely using system keyring or file fallback -5. Store the project ID in `~/.config/tiger/config.yaml` as the default project +4. Store credentials (API key + project ID) securely as JSON in system keyring or file fallback **Project ID Requirement:** Project ID is required during login. The CLI will: - Use `--project-id` flag if provided -- Use `TIGER_PROJECT_ID` environment variable if flag not provided -- Prompt interactively for project ID if neither flag nor environment variable is set -- Fail with an error in non-interactive environments (no TTY) if project ID is missing +- Prompt interactively for project ID if flag not provided +- Fail with an error in non-interactive environments (no TTY) if project ID is not provided via flag -**API Key Storage:** -The combined API key (`:`) is stored securely using: -1. **System keyring** (preferred): Uses [go-keyring](https://github.com/zalando/go-keyring) for secure storage in system credential managers (macOS Keychain, Windows Credential Manager, Linux Secret Service) -2. **File fallback**: If keyring is unavailable, stores in `~/.config/tiger/api-key` with restricted file permissions (600) +**Credentials Storage:** +Credentials are stored as a JSON object containing both the API key (`:`) and project ID: +1. **System keyring** (preferred): Uses [go-keyring](https://github.com/zalando/go-keyring) for secure storage in system credential managers (macOS Keychain, Windows Credential Manager, Linux Secret Service) +2. **File fallback**: If keyring is unavailable, stores in `~/.config/tiger/credentials` with restricted file permissions (600) + +Use `tiger auth status` to view your current authentication status and project ID. **Options:** - `--public-key`: Public key for authentication @@ -625,9 +619,6 @@ Manage CLI configuration. # Show config tiger config show -# Set default project -tiger config set project_id proj-12345 - # Set default service tiger config set service_id svc-12345 diff --git a/specs/spec_mcp.md b/specs/spec_mcp.md index bbd327a0..f47efa15 100644 --- a/specs/spec_mcp.md +++ b/specs/spec_mcp.md @@ -569,8 +569,8 @@ Common error codes: - The MCP server is embedded within the Tiger CLI binary - Shares the same API client library and configuration system as the CLI -- Uses the CLI's stored authentication (keyring or file-based) -- Inherits the CLI's project and service defaults from configuration +- Uses the CLI's stored authentication (keyring or file-based credentials) +- Inherits the CLI's project ID from stored credentials and service ID from configuration - Implements proper graceful shutdown and signal handling - Uses structured logging compatible with the CLI logging system - All tools are idempotent where possible From 2aaa886c375ef216927d21d173180eba94423995 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Fri, 17 Oct 2025 17:39:01 -0400 Subject: [PATCH 5/9] Update Logged in (API key stored) Project ID: lsof1xqjus help text --- internal/tiger/cmd/auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/tiger/cmd/auth.go b/internal/tiger/cmd/auth.go index 0ebcc3e6..b714c480 100644 --- a/internal/tiger/cmd/auth.go +++ b/internal/tiger/cmd/auth.go @@ -168,8 +168,8 @@ func buildLogoutCmd() *cobra.Command { func buildStatusCmd() *cobra.Command { return &cobra.Command{ Use: "status", - Short: "Show current auth information", - Long: `Show information about the currently authenticated token.`, + Short: "Show current authentication status and project ID", + Long: "Displays whether you are logged in and shows your currently configured project ID.", RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true From b3b9ae886c1aaaad1a169f583a3adbbfb311a940 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Fri, 17 Oct 2025 17:47:24 -0400 Subject: [PATCH 6/9] Revert comment --- internal/tiger/cmd/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tiger/cmd/auth.go b/internal/tiger/cmd/auth.go index b714c480..da7bc925 100644 --- a/internal/tiger/cmd/auth.go +++ b/internal/tiger/cmd/auth.go @@ -117,7 +117,7 @@ Examples: } } - // Combine the keys in the format "public:secret" for validation + // Combine the keys in the format "public:secret" for storage apiKey := fmt.Sprintf("%s:%s", creds.publicKey, creds.secretKey) // Validate the API key by making a test API call From a17ac6eb79931689fcfde6ea14ec789be9373fab Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Fri, 17 Oct 2025 17:53:30 -0400 Subject: [PATCH 7/9] Get rid of unnecessary loadConfig() method on mcp Server type --- internal/tiger/mcp/server.go | 9 --------- internal/tiger/mcp/service_tools.go | 7 ++++--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/internal/tiger/mcp/server.go b/internal/tiger/mcp/server.go index 94a15407..b914551b 100644 --- a/internal/tiger/mcp/server.go +++ b/internal/tiger/mcp/server.go @@ -87,15 +87,6 @@ func (s *Server) createAPIClient() (*api.ClientWithResponses, string, error) { return apiClient, projectID, nil } -// loadConfig loads fresh config -func (s *Server) loadConfig() (*config.Config, error) { - cfg, err := config.Load() - if err != nil { - return nil, fmt.Errorf("failed to load config: %w", err) - } - return cfg, nil -} - // Close gracefully shuts down the MCP server and all proxy connections func (s *Server) Close() error { logging.Debug("Closing MCP server and proxy connections") diff --git a/internal/tiger/mcp/service_tools.go b/internal/tiger/mcp/service_tools.go index a368497c..f92e93b8 100644 --- a/internal/tiger/mcp/service_tools.go +++ b/internal/tiger/mcp/service_tools.go @@ -11,6 +11,7 @@ import ( "go.uber.org/zap" "github.com/timescale/tiger-cli/internal/tiger/api" + "github.com/timescale/tiger-cli/internal/tiger/config" "github.com/timescale/tiger-cli/internal/tiger/logging" "github.com/timescale/tiger-cli/internal/tiger/password" "github.com/timescale/tiger-cli/internal/tiger/util" @@ -382,10 +383,10 @@ func (s *Server) handleServiceGet(ctx context.Context, req *mcp.CallToolRequest, // handleServiceCreate handles the service_create MCP tool func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolRequest, input ServiceCreateInput) (*mcp.CallToolResult, ServiceCreateOutput, error) { - // Load config and validate project ID - cfg, err := s.loadConfig() + // Load config + cfg, err := config.Load() if err != nil { - return nil, ServiceCreateOutput{}, err + return nil, ServiceCreateOutput{}, fmt.Errorf("failed to load config: %w", err) } // Create fresh API client and get project ID From 2b5720301cc65f34a0c41774d2e8cf36db748e99 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Mon, 20 Oct 2025 10:01:52 -0400 Subject: [PATCH 8/9] Remove unnecessary strings.TrimSpace --- internal/tiger/config/credentials.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/tiger/config/credentials.go b/internal/tiger/config/credentials.go index 3ea6e170..a8d250ae 100644 --- a/internal/tiger/config/credentials.go +++ b/internal/tiger/config/credentials.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "strings" "testing" "github.com/zalando/go-keyring" @@ -155,7 +154,7 @@ func getCredentialsFromFile() (string, string, error) { return "", "", fmt.Errorf("failed to read credentials file: %w", err) } - credentials := strings.TrimSpace(string(data)) + credentials := string(data) if credentials == "" { return "", "", ErrNotLoggedIn } From 76c63679ddfc3098278081f1041464b93da679eb Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Mon, 20 Oct 2025 10:20:25 -0400 Subject: [PATCH 9/9] Consolidate logic for getting credentials file path --- internal/tiger/config/config.go | 2 +- internal/tiger/config/credentials.go | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/tiger/config/config.go b/internal/tiger/config/config.go index a0bc5b8a..819bddaa 100644 --- a/internal/tiger/config/config.go +++ b/internal/tiger/config/config.go @@ -497,7 +497,7 @@ func (c *Config) GetConfigFile() string { } // TODO: This function is currently used to get the directory that the API -// key fallback file should be stored in (see api_key.go). But ideally, those +// key fallback file should be stored in (see credentials.go). But ideally, those // functions would take a Config struct and use the ConfigDir field instead. func GetConfigDir() string { return filepath.Dir(viper.ConfigFileUsed()) diff --git a/internal/tiger/config/credentials.go b/internal/tiger/config/credentials.go index a8d250ae..63c59421 100644 --- a/internal/tiger/config/credentials.go +++ b/internal/tiger/config/credentials.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "testing" "github.com/zalando/go-keyring" @@ -52,6 +53,11 @@ func SetTestServiceName(t *testing.T) { }) } +func getCredentialsFileName() string { + configDir := GetConfigDir() + return fmt.Sprintf("%s/credentials", configDir) +} + // StoreCredentials stores the API key (public:secret) and project ID together // The credentials are stored as JSON with api_key and project_id fields func StoreCredentials(apiKey, projectID string) error { @@ -95,12 +101,11 @@ func storeToKeyring(credentials string) error { // storeToFile stores credentials to ~/.config/tiger/credentials with restricted permissions func storeToFile(credentials string) error { - configDir := GetConfigDir() - if err := os.MkdirAll(configDir, 0755); err != nil { + credentialsFile := getCredentialsFileName() + if err := os.MkdirAll(filepath.Dir(credentialsFile), 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } - credentialsFile := fmt.Sprintf("%s/credentials", configDir) file, err := os.OpenFile(credentialsFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return fmt.Errorf("failed to create credentials file: %w", err) @@ -143,9 +148,7 @@ func getCredentialsFromKeyring() (string, string, error) { // getCredentialsFromFile retrieves credentials from file func getCredentialsFromFile() (string, string, error) { - configDir := GetConfigDir() - credentialsFile := fmt.Sprintf("%s/credentials", configDir) - + credentialsFile := getCredentialsFileName() data, err := os.ReadFile(credentialsFile) if err != nil { if os.IsNotExist(err) { @@ -194,8 +197,7 @@ func removeCredentialsFromKeyring() { // removeCredentialsFile removes credentials file func removeCredentialsFile() error { - configDir := GetConfigDir() - credentialsFile := fmt.Sprintf("%s/credentials", configDir) + credentialsFile := getCredentialsFileName() if err := os.Remove(credentialsFile); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove credentials file: %w", err) }