Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ These flags are available on all commands and take precedence over both environm
- `--config-dir <path>` - Path to configuration directory (default: `~/.config/tiger`)
- `--project-id <id>` - Specify project ID
- `--service-id <id>` - Specify service ID
- `-o, --output <format>` - Output format: `json`, `yaml`, or `table`
- `-o, --output <format>` - Output format: `json`, `yaml`, `env`, or `table`
- `--analytics` - Enable/disable analytics
- `--password-storage <method>` - Password storage method: `keyring`, `pgpass`, or `none`
- `--debug` - Enable/disable debug logging
Expand Down
2 changes: 2 additions & 0 deletions internal/tiger/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func buildConfigShowCmd() *cobra.Command {
return outputJSON(cfg, cmd)
case "yaml":
return outputYAML(cfg, cmd)
case "env":
return fmt.Errorf("environment variable output is not supported for config")
default:
return outputTable(cfg, cmd)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/tiger/cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Examples:
passwordMode = password.PasswordRequired
}

connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
connectionString, err := password.BuildConnectionString(service, password.ConnectionDetailsOptions{
Pooled: dbConnectionStringPooled,
Role: dbConnectionStringRole,
PasswordMode: passwordMode,
Expand Down Expand Up @@ -142,7 +142,7 @@ Examples:
return fmt.Errorf("psql client not found. Please install PostgreSQL client tools")
}

connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
connectionString, err := password.BuildConnectionString(service, password.ConnectionDetailsOptions{
Pooled: dbConnectPooled,
Role: dbConnectRole,
PasswordMode: password.PasswordExclude,
Expand Down Expand Up @@ -206,7 +206,7 @@ Examples:
}

// Build connection string for testing with password (if available)
connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
connectionString, err := password.BuildConnectionString(service, password.ConnectionDetailsOptions{
Pooled: dbTestConnectionPooled,
Role: dbTestConnectionRole,
PasswordMode: password.PasswordOptional,
Expand Down
8 changes: 4 additions & 4 deletions internal/tiger/cmd/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestDBConnectionString_PoolerWarning(t *testing.T) {
errBuf := new(bytes.Buffer)

// Request pooled connection when pooler is not available
connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
connectionString, err := password.BuildConnectionString(service, password.ConnectionDetailsOptions{
Pooled: true,
Role: "tsdbadmin",
PasswordMode: password.PasswordExclude,
Expand Down Expand Up @@ -805,7 +805,7 @@ func TestBuildConnectionString(t *testing.T) {
errBuf := new(bytes.Buffer)
cmd.SetErr(errBuf)

result, err := password.BuildConnectionString(tc.service, password.ConnectionStringOptions{
result, err := password.BuildConnectionString(tc.service, password.ConnectionDetailsOptions{
Pooled: tc.pooled,
Role: tc.role,
PasswordMode: password.PasswordExclude,
Expand Down Expand Up @@ -980,7 +980,7 @@ func TestDBConnectionString_WithPassword(t *testing.T) {

// Test password.BuildConnectionString without password (default behavior)
cmd := &cobra.Command{}
baseConnectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
baseConnectionString, err := password.BuildConnectionString(service, password.ConnectionDetailsOptions{
Pooled: false,
Role: "tsdbadmin",
PasswordMode: password.PasswordExclude,
Expand All @@ -1001,7 +1001,7 @@ func TestDBConnectionString_WithPassword(t *testing.T) {
}

// Test password.BuildConnectionString with password (simulating --with-password flag)
connectionStringWithPassword, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
connectionStringWithPassword, err := password.BuildConnectionString(service, password.ConnectionDetailsOptions{
Pooled: false,
Role: "tsdbadmin",
PasswordMode: password.PasswordRequired,
Expand Down
2 changes: 1 addition & 1 deletion internal/tiger/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ 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().VarP((*outputFlag)(&output), "output", "o", "output format (json, yaml, table)")
cmd.PersistentFlags().VarP((*outputFlag)(&output), "output", "o", "output format (json, yaml, env, table)")
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")
Expand Down
98 changes: 66 additions & 32 deletions internal/tiger/cmd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ Note: You can specify both CPU and memory together, or specify only one (the oth
cmd.Flags().DurationVar(&createWaitTimeout, "wait-timeout", 30*time.Minute, "Wait timeout duration (e.g., 30m, 1h30m, 90s)")
cmd.Flags().BoolVar(&createNoSetDefault, "no-set-default", false, "Don't set this service as the default service")
cmd.Flags().BoolVar(&createFree, "free", false, "Create a free tier service (limitations apply)")
cmd.Flags().BoolVar(&createWithPassword, "with-password", false, "Include initial password in output")
cmd.Flags().BoolVar(&createWithPassword, "with-password", false, "Include password in output")

return cmd
}
Expand Down Expand Up @@ -620,6 +620,10 @@ type OutputService struct {
api.Service
ConnectionString *string `json:"connection_string,omitempty" yaml:"connection_string,omitempty"`
Password *string `json:"password,omitempty" yaml:"password,omitempty"`
Role *string `json:"role,omitempty" yaml:"role,omitempty"`
Host *string `json:"host,omitempty" yaml:"host,omitempty"`
Port *int `json:"port,omitempty" yaml:"port,omitempty"`
Database *string `json:"database,omitempty" yaml:"database,omitempty"`
}

// Convert to JSON to respect omitempty tags, then unmarshal
Expand All @@ -640,50 +644,76 @@ func toJSON(v any) (any, error) {
func outputService(cmd *cobra.Command, service api.Service, format string, withPassword bool) error {
// Prepare the output service with computed fields
outputSvc := prepareServiceForOutput(service, withPassword, cmd.ErrOrStderr())
outputWriter := cmd.OutOrStdout()

switch strings.ToLower(format) {
case "json":
encoder := json.NewEncoder(cmd.OutOrStdout())
encoder := json.NewEncoder(outputWriter)
encoder.SetIndent("", " ")
return encoder.Encode(outputSvc)
case "yaml":
encoder := yaml.NewEncoder(cmd.OutOrStdout())
encoder := yaml.NewEncoder(outputWriter)
encoder.SetIndent(2)
jsonMap, err := toJSON(outputSvc)
if err != nil {
return fmt.Errorf("failed to convert service to map: %w", err)
}
return encoder.Encode(jsonMap)
case "env":
return outputServiceEnv(outputSvc, outputWriter)
default: // table format (default)
return outputServiceTable(cmd, outputSvc)
return outputServiceTable(outputSvc, outputWriter)
}
}

// outputServices formats and outputs the services list based on the specified format
func outputServices(cmd *cobra.Command, services []api.Service, format string, withPassword bool) error {
outputServices := prepareServicesForOutput(services, withPassword, cmd.ErrOrStderr())
outputWriter := cmd.OutOrStdout()

switch strings.ToLower(format) {
case "json":
encoder := json.NewEncoder(cmd.OutOrStdout())
encoder := json.NewEncoder(outputWriter)
encoder.SetIndent("", " ")
return encoder.Encode(outputServices)
case "yaml":
encoder := yaml.NewEncoder(cmd.OutOrStdout())
encoder := yaml.NewEncoder(outputWriter)
encoder.SetIndent(2)
jsonArray, err := toJSON(outputServices)
if err != nil {
return fmt.Errorf("failed to convert services to map: %w", err)
}
return encoder.Encode(jsonArray)
case "env":
return fmt.Errorf("environment variable output is not supported for multiple services")
default: // table format (default)
return outputServicesTable(cmd, outputServices)
return outputServicesTable(outputServices, outputWriter)
}
}

// outputServiceEnv outputs service details in environment variable format
func outputServiceEnv(service OutputService, output io.Writer) error {
if service.Host != nil {
fmt.Fprintf(output, "PGHOST=%s\n", *service.Host)
}
if service.Role != nil {
fmt.Fprintf(output, "PGUSER=%s\n", *service.Role)
}
if service.Password != nil {
fmt.Fprintf(output, "PGPASSWORD=%s\n", *service.Password)
}
if service.Database != nil {
fmt.Fprintf(output, "PGDATABASE=%s\n", *service.Database)
}
if service.Port != nil {
fmt.Fprintf(output, "PGPORT=%d\n", *service.Port)
}
return nil
}

// outputServiceTable outputs detailed service information in a formatted table
func outputServiceTable(cmd *cobra.Command, service OutputService) error {
table := tablewriter.NewWriter(cmd.OutOrStdout())
func outputServiceTable(service OutputService, output io.Writer) error {
table := tablewriter.NewWriter(output)
table.Header("PROPERTY", "VALUE")

// Basic service information
Expand Down Expand Up @@ -760,8 +790,6 @@ func outputServiceTable(cmd *cobra.Command, service OutputService) error {
// Output password if available
if service.Password != nil {
table.Append("Password", *service.Password)
} else if service.InitialPassword != nil {
table.Append("Initial Password", *service.InitialPassword)
}

// Output connection string if available
Expand All @@ -773,8 +801,8 @@ func outputServiceTable(cmd *cobra.Command, service OutputService) error {
}

// outputServicesTable outputs services in a formatted table using tablewriter
func outputServicesTable(cmd *cobra.Command, services []OutputService) error {
table := tablewriter.NewWriter(cmd.OutOrStdout())
func outputServicesTable(services []OutputService, output io.Writer) error {
table := tablewriter.NewWriter(output)
table.Header("SERVICE ID", "NAME", "STATUS", "TYPE", "REGION", "CREATED", "CONNECTION STRING")

for _, service := range services {
Expand All @@ -796,31 +824,37 @@ func prepareServiceForOutput(service api.Service, withPassword bool, output io.W
outputSvc := OutputService{
Service: service,
}

// Remove password if not requested
if !withPassword {
outputSvc.InitialPassword = nil
} else if service.InitialPassword == nil {
password, err := getServicePassword(service)
if err != nil {
fmt.Fprintf(output, "⚠️ Warning: Failed to retrieve stored password: %v\n", err)
}
outputSvc.Password = &password
}

// Build connection string
passwordMode := password.PasswordExclude
if withPassword {
passwordMode = password.PasswordRequired
outputSvc.Password = outputSvc.InitialPassword
}
connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
outputSvc.InitialPassword = nil

opts := password.ConnectionDetailsOptions{
Pooled: false,
Role: "tsdbadmin",
PasswordMode: passwordMode,
PasswordMode: password.PasswordExclude,
WarnWriter: output,
})
}
if withPassword {
opts.PasswordMode = password.PasswordRequired
}
connectionDetails, err := password.GetConnectionDetails(service, opts)
if err == nil {
outputSvc.Role = &connectionDetails.Role
outputSvc.Host = &connectionDetails.Host
outputSvc.Port = &connectionDetails.Port
outputSvc.Database = &connectionDetails.Database
if connectionDetails.Password != "" {
outputSvc.Password = &connectionDetails.Password
}
} else if output != nil {
fmt.Fprintf(output, "⚠️ Warning: Failed to get connection details: %v\n", err)
}
connectionString, err := password.BuildConnectionString(service, opts)
if err == nil {
outputSvc.ConnectionString = &connectionString
} else if output != nil {
fmt.Fprintf(output, "⚠️ Warning: Failed to get connection string: %v\n", err)
}

return outputSvc
Expand Down Expand Up @@ -1369,7 +1403,7 @@ Examples:
// Resource customization flags
cmd.Flags().IntVar(&forkCPU, "cpu", 0, "CPU allocation in millicores (inherits from source if not specified)")
cmd.Flags().IntVar(&forkMemory, "memory", 0, "Memory allocation in gigabytes (inherits from source if not specified)")
cmd.Flags().BoolVar(&forkWithPassword, "with-password", false, "Include initial password in output")
cmd.Flags().BoolVar(&forkWithPassword, "with-password", false, "Include password in output")

return cmd
}
23 changes: 19 additions & 4 deletions internal/tiger/cmd/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ func TestOutputService_JSON(t *testing.T) {
}

// Verify that initialpassword is NOT in the output
if strings.Contains(output, "secret-password-123") || strings.Contains(output, "initialpassword") || strings.Contains(output, "initial_password") {
if strings.Contains(output, "secret-password-123") || strings.Contains(output, "initialpassword") || strings.Contains(output, "initial_password") || strings.Contains(output, "password") {
t.Errorf("JSON output should not contain initialpassword field, got: %s", output)
}

Expand All @@ -683,6 +683,9 @@ func TestOutputService_JSON(t *testing.T) {
if _, exists := jsonMap["initialpassword"]; exists {
t.Error("JSON should not contain initialpassword field")
}
if _, exists := jsonMap["password"]; exists {
t.Error("JSON should not contain password field")
}
}

func TestOutputService_YAML(t *testing.T) {
Expand Down Expand Up @@ -723,7 +726,7 @@ func TestOutputService_YAML(t *testing.T) {
}

// Verify that initialpassword is NOT in the output
if strings.Contains(output, "secret-password-123") || strings.Contains(output, "initialpassword") {
if strings.Contains(output, "secret-password-123") || strings.Contains(output, "initialpassword") || strings.Contains(output, "password") {
t.Errorf("YAML output should not contain initialpassword field, got: %s", output)
}

Expand All @@ -749,6 +752,9 @@ func TestOutputService_YAML(t *testing.T) {
if _, exists := yamlMap["initialpassword"]; exists {
t.Error("YAML should not contain initialpassword field")
}
if _, exists := yamlMap["password"]; exists {
t.Error("YAML should not contain password field")
}
}

func TestOutputService_Table(t *testing.T) {
Expand Down Expand Up @@ -864,6 +870,9 @@ func TestPrepareServiceForOutput_WithoutPassword(t *testing.T) {
if outputSvc.Service.InitialPassword != nil {
t.Error("Expected InitialPassword to be nil when withPassword=false")
}
if outputSvc.Password != nil {
t.Error("Expected Password to be nil when withPassword=false")
}

// Verify that other fields are preserved
if outputSvc.Service.ServiceId == nil || *outputSvc.Service.ServiceId != serviceID {
Expand Down Expand Up @@ -896,8 +905,11 @@ func TestPrepareServiceForOutput_WithPassword(t *testing.T) {
outputSvc := prepareServiceForOutput(service, true, cmd.ErrOrStderr())

// Verify that password is preserved
if outputSvc.Service.InitialPassword == nil || *outputSvc.Service.InitialPassword != initialPassword {
t.Error("Expected InitialPassword to be preserved when withPassword=true")
if outputSvc.Service.InitialPassword != nil {
t.Error("Expected InitialPassword to be nil when withPassword=true")
}
if outputSvc.Password == nil || *outputSvc.Password != initialPassword {
t.Error("Expected Password to be preserved when withPassword=true")
}

// Verify that other fields are preserved
Expand Down Expand Up @@ -945,6 +957,9 @@ func TestSanitizeServicesForOutput(t *testing.T) {
if service.InitialPassword != nil {
t.Errorf("Expected InitialPassword to be nil in sanitized service %d", i)
}
if service.Password != nil {
t.Errorf("Expected Password to be nil in sanitized service %d", i)
}

// Verify that other fields are preserved
if service.ServiceId == nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/tiger/config/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"strings"
)

var ValidOutputFormats = []string{"json", "yaml", "table"}
var ValidOutputFormats = []string{"json", "yaml", "env", "table"}

func ValidateOutputFormat(format string) error {
for _, valid := range ValidOutputFormats {
Expand Down
2 changes: 1 addition & 1 deletion internal/tiger/mcp/db_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (s *Server) handleDBExecuteQuery(ctx context.Context, req *mcp.CallToolRequ
service := *serviceResp.JSON200

// Build connection string with password
connString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
connString, err := password.BuildConnectionString(service, password.ConnectionDetailsOptions{
Pooled: input.Pooled,
Role: input.Role,
PasswordMode: password.PasswordRequired, // MCP always requires password
Expand Down
Loading