Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions internal/tiger/cmd/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
ExitPermissionDenied = 5 // Permission denied
ExitServiceNotFound = 6 // Service not found
ExitUpdateAvailable = 7 // Update available
ExitMultipleMatches = 8 // Multiple resources match
)

// exitCodeError creates an error that will cause the program to exit with the specified code
Expand Down
255 changes: 145 additions & 110 deletions internal/tiger/cmd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import (
var (
// getCredentialsForService can be overridden for testing
getCredentialsForService = config.GetCredentials
// fetchAllServicesFunc can be overridden for testing
fetchAllServicesFunc = fetchAllServices
// fetchServiceFunc can be overridden for testing
fetchServiceFunc = fetchService
)

// buildServiceCmd creates the main service command with all subcommands
Expand All @@ -42,6 +46,77 @@ func buildServiceCmd() *cobra.Command {
return cmd
}

// getProjectApiClient retrieves the API client and project ID, handling authentication errors
func getProjectApiClient() (*api.ClientWithResponses, string, error) {
apiKey, projectID, err := getCredentialsForService()
if err != nil {
return nil, "", exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
}

// Create API client
client, err := api.NewTigerClient(apiKey)
if err != nil {
return nil, "", fmt.Errorf("failed to create API client: %w", err)
}
return client, projectID, nil
}

// fetchAllServices fetches all services for a project, handling authentication and response errors
func fetchAllServices() ([]api.Service, error) {
client, projectID, err := getProjectApiClient()
if err != nil {
return nil, err
}

// Make API call to list services
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

resp, err := client.GetProjectsProjectIdServicesWithResponse(ctx, projectID)
if err != nil {
return nil, fmt.Errorf("failed to list services: %w", err)
}

// Handle API response
if resp.StatusCode() != 200 {
return nil, exitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
}

if resp.JSON200 == nil || len(*resp.JSON200) == 0 {
return []api.Service{}, nil
}

return *resp.JSON200, nil
}

// fetchService fetches a specific service by ID, handling authentication and response errors
func fetchService(serviceID string) (*api.Service, error) {
client, projectID, err := getProjectApiClient()
if err != nil {
return nil, err
}

// Make API call to get service details
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

resp, err := client.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, serviceID)
if err != nil {
return nil, fmt.Errorf("failed to get service details: %w", err)
}

// Handle API response
if resp.StatusCode() != 200 {
return nil, exitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
}

if resp.JSON200 == nil {
return nil, fmt.Errorf("empty response from API")
}

return resp.JSON200, nil
}

// buildServiceGetCmd represents the get command under service
func buildServiceGetCmd() *cobra.Command {
var withPassword bool
Expand All @@ -53,82 +128,97 @@ func buildServiceGetCmd() *cobra.Command {
Short: "Show detailed information about a service",
Long: `Show detailed information about a specific database service.

The service ID can be provided as an argument or will use the default service
The service ID or name can be provided as an argument or will use the default service
from your configuration. This command displays comprehensive information about
the service including configuration, status, endpoints, and resource usage.

If the provided name is ambiguous and matches multiple services, an error message will
list the matching services, and the process will exit with code ` + fmt.Sprintf("%d", ExitMultipleMatches) + `.

Examples:
# Get default service details
tiger service get

# Get specific service details
tiger service get svc-12345
# Get specific service details by ID
tiger service get b0ysmfnr0y

# Get specific service details by name
tiger service get my-service

# Get service details in JSON format
tiger service get svc-12345 --output json
tiger service get my-service -o json

# Get service details in YAML format
tiger service get svc-12345 --output yaml`,
tiger service get my-service -o yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
// Get config
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

// Use flag value if provided, otherwise use config value
if cmd.Flags().Changed("output") {
cfg.Output = output
}

// Determine service ID
var serviceID string
idArg := ""
if len(args) > 0 {
serviceID = args[0]
} else {
serviceID = cfg.ServiceID
idArg = args[0]
}

if serviceID == "" {
return fmt.Errorf("service ID is required. Provide it as an argument or set a default with 'tiger config set service_id <service-id>'")
if idArg == "" && cfg.ServiceID == "" {
return fmt.Errorf("target service was not specified. Provide it as an argument or set a default with 'tiger config set service_id <service-id>'")
}

cmd.SilenceUsage = true

// Get API key and project ID for authentication
apiKey, projectID, err := getCredentialsForService()
if err != nil {
return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
// Use flag value if provided, otherwise use config value
if cmd.Flags().Changed("output") {
cfg.Output = output
}

// Create API client
client, err := api.NewTigerClient(apiKey)
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
}
// Determine service ID
var service *api.Service
if idArg == "" {
service, err = fetchServiceFunc(cfg.ServiceID)
if err != nil {
return err
}
} else {
services, err := fetchAllServicesFunc()
if err != nil {
return err
}

// Make API call to get service details
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if len(services) == 0 {
return exitWithCode(ExitGeneralError, fmt.Errorf("you have no services"))
}

resp, err := client.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, serviceID)
if err != nil {
return fmt.Errorf("failed to get service details: %w", err)
}
// Filter services by exact name or id match
var matches []api.Service
for _, service := range services {
if (service.ServiceId != nil && *service.ServiceId == idArg) || (service.Name != nil && *service.Name == idArg) {
matches = append(matches, service)
}
}

// Handle API response
if resp.StatusCode() != 200 {
return exitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
}
// Handle no matches
if len(matches) == 0 {
return exitWithCode(ExitServiceNotFound, fmt.Errorf("no services found matching '%s'", idArg))
}

if resp.JSON200 == nil {
return fmt.Errorf("empty response from API")
if len(matches) > 1 {
// Multiple matches - output like 'service list' as error
if err := outputServices(cmd, matches, cfg.Output); err != nil {
return err
}
return exitWithCode(ExitMultipleMatches, fmt.Errorf("multiple services found matching '%s'", idArg))
}
service = &matches[0]
}

service := *resp.JSON200
if service == nil {
return exitWithCode(ExitServiceNotFound, fmt.Errorf("service not found"))
}

// Output service in requested format
return outputService(cmd, service, cfg.Output, withPassword, true)
return outputService(cmd, *service, cfg.Output, withPassword, true)
},
}

Expand Down Expand Up @@ -160,45 +250,18 @@ func buildServiceListCmd() *cobra.Command {

cmd.SilenceUsage = true

// Get API key and project ID for authentication
apiKey, projectID, err := getCredentialsForService()
if err != nil {
return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
}

// Create API client
client, err := api.NewTigerClient(apiKey)
// Fetch all services using shared function
services, err := fetchAllServicesFunc()
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
}

// Make API call to list services
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

resp, err := client.GetProjectsProjectIdServicesWithResponse(ctx, projectID)
if err != nil {
return fmt.Errorf("failed to list services: %w", err)
}

statusOutput := cmd.ErrOrStderr()

// Handle API response
if resp.StatusCode() != 200 {
return exitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
return err
}

services := *resp.JSON200
if len(services) == 0 {
statusOutput := cmd.ErrOrStderr()
fmt.Fprintln(statusOutput, "🏜️ No services found! Your project is looking a bit empty.")
fmt.Fprintln(statusOutput, "🚀 Ready to get started? Create your first service with: tiger service create")
return nil
}

if resp.JSON200 == nil {
fmt.Fprintln(statusOutput, "🏜️ No services found! Your project is looking a bit empty.")
fmt.Fprintln(statusOutput, "🚀 Ready to get started? Create your first service with: tiger service create")
return nil
cmd.SilenceErrors = true
return exitWithCode(ExitGeneralError, nil)
}

// Output services in requested format
Expand Down Expand Up @@ -315,16 +378,9 @@ Note: You can specify both CPU and memory together, or specify only one (the oth

cmd.SilenceUsage = true

// Get API key and project ID for authentication
apiKey, projectID, err := getCredentialsForService()
if err != nil {
return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
}

// Create API client
client, err := api.NewTigerClient(apiKey)
client, projectID, err := getProjectApiClient()
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
return err
}

// Prepare service creation request
Expand Down Expand Up @@ -484,16 +540,9 @@ Examples:

cmd.SilenceUsage = true

// Get API key and project ID for authentication
apiKey, projectID, err := getCredentialsForService()
if err != nil {
return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
}

// Create API client
client, err := api.NewTigerClient(apiKey)
client, projectID, err := getProjectApiClient()
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
return err
}

// Prepare password update request
Expand Down Expand Up @@ -885,10 +934,9 @@ Examples:

cmd.SilenceUsage = true

// Get API key and project ID for authentication
apiKey, projectID, err := getCredentialsForService()
client, projectID, err := getProjectApiClient()
if err != nil {
return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
return err
}

statusOutput := cmd.ErrOrStderr()
Expand All @@ -905,12 +953,6 @@ Examples:
}
}

// Create API client
client, err := api.NewTigerClient(apiKey)
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
}

// Make the delete request
resp, err := client.DeleteProjectsProjectIdServicesServiceIdWithResponse(
context.Background(),
Expand Down Expand Up @@ -1101,16 +1143,9 @@ Examples:

cmd.SilenceUsage = true

// Get API key and project ID for authentication
apiKey, projectID, err := getCredentialsForService()
if err != nil {
return exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
}

// Create API client
client, err := api.NewTigerClient(apiKey)
client, projectID, err := getProjectApiClient()
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
return err
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
Expand Down
Loading