4 Commits

Author SHA1 Message Date
Andras Bacsai 794025aee1 Changes auto-committed by Conductor (#25) 2025-10-17 11:14:24 +02:00
Andras Bacsai 8d6da93aa1 Merge pull request #24 from coollabsio/andrasbacsai/show-config-path
Add coolify config command
2025-10-17 11:05:57 +02:00
Andras Bacsai 22516fe51e Changes auto-committed by Conductor 2025-10-17 11:00:09 +02:00
Andras Bacsai fe63c3a3b6 Changes auto-committed by Conductor 2025-10-17 10:59:12 +02:00
15 changed files with 296 additions and 72 deletions
+3
View File
@@ -34,6 +34,9 @@ You can change the default context with `coolify context use <context_name>` or
### Update
- `coolify update` - Update the CLI to the latest version
### Configuration
- `coolify config` - Show configuration file location
### Context Management
- `coolify context list` - List all configured contexts
- `coolify context add <context_name> <url> <token>` - Add a new context
+20
View File
@@ -0,0 +1,20 @@
package config
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/spf13/cobra"
)
// NewConfigCommand creates the config command
func NewConfigCommand() *cobra.Command {
return &cobra.Command{
Use: "config",
Short: "Show configuration file location",
Long: "Display the path to the Coolify CLI configuration file",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(config.Path())
},
}
}
+52
View File
@@ -0,0 +1,52 @@
package config
import (
"strings"
"testing"
"github.com/coollabsio/coolify-cli/internal/config"
)
func TestNewConfigCommand(t *testing.T) {
cmd := NewConfigCommand()
if cmd == nil {
t.Fatal("NewConfigCommand() returned nil")
}
if cmd.Use != "config" {
t.Errorf("Expected Use to be 'config', got '%s'", cmd.Use)
}
if cmd.Short == "" {
t.Error("Short description should not be empty")
}
if cmd.Long == "" {
t.Error("Long description should not be empty")
}
if cmd.Run == nil {
t.Error("Run function should not be nil")
}
}
func TestConfigCommand_Output(t *testing.T) {
// Test that the command returns the expected config path
expectedPath := config.Path()
// The path should not be empty
if expectedPath == "" {
t.Error("Expected config path to not be empty")
}
// The path should end with config.json
if !strings.HasSuffix(expectedPath, "config.json") {
t.Errorf("Expected path to end with 'config.json', got '%s'", expectedPath)
}
// The path should contain the coolify directory
if !strings.Contains(expectedPath, "coolify") {
t.Errorf("Expected path to contain 'coolify', got '%s'", expectedPath)
}
}
+1
View File
@@ -22,6 +22,7 @@ func NewContextCommand() *cobra.Command {
cmd.AddCommand(NewSetTokenCommand())
cmd.AddCommand(NewSetDefaultCommand())
cmd.AddCommand(NewVersionCommand())
cmd.AddCommand(NewVerifyCommand())
return cmd
}
+41
View File
@@ -0,0 +1,41 @@
package context
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
)
// NewVerifyCommand creates the verify command for contexts
func NewVerifyCommand() *cobra.Command {
return &cobra.Command{
Use: "verify",
Short: "Verify current context connection and authentication",
Long: `Verify that the current context is properly configured by testing the connection
to the Coolify instance and validating the API token.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client - this will use the current default context
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Try to get version - this verifies both connection and authentication
version, err := client.GetVersion(ctx)
if err != nil {
return fmt.Errorf("verification failed: %w", err)
}
// If we got here, connection and authentication are working
fmt.Printf("✓ Connection successful\n")
fmt.Printf("✓ Authentication valid\n")
fmt.Printf("✓ Coolify version: %s\n", version)
return nil
},
}
}
+105
View File
@@ -0,0 +1,105 @@
package context
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/coollabsio/coolify-cli/internal/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestVerifyCommand_APIIntegration tests the verify logic using the API client directly
// This tests the core functionality that the verify command relies on
func TestVerifyCommand_APIIntegration(t *testing.T) {
t.Run("successful verification", func(t *testing.T) {
// Create a test HTTP server that responds to /api/v1/version
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/version", r.URL.Path)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte("4.0.0-beta.383"))
}))
defer server.Close()
// Create API client and verify connection
client := api.NewClient(server.URL, "test-token")
version, err := client.GetVersion(context.Background())
// Verify results
require.NoError(t, err)
assert.Equal(t, "4.0.0-beta.383", version)
})
t.Run("unauthorized - invalid token", func(t *testing.T) {
// Create a test HTTP server that returns 401
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{
"message": "Invalid token",
})
}))
defer server.Close()
// Create API client with invalid token
client := api.NewClient(server.URL, "invalid-token")
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
assert.True(t, api.IsUnauthorized(err))
})
t.Run("server error", func(t *testing.T) {
// Create a test HTTP server that returns 500
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"error": "Internal server error",
})
}))
defer server.Close()
// Create API client
client := api.NewClient(server.URL, "test-token", api.WithRetries(0))
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
var apiErr *api.Error
require.ErrorAs(t, err, &apiErr)
assert.Equal(t, 500, apiErr.StatusCode)
})
t.Run("not found", func(t *testing.T) {
// Create a test HTTP server that returns 404
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{
"message": "Endpoint not found",
})
}))
defer server.Close()
// Create API client
client := api.NewClient(server.URL, "test-token")
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
assert.True(t, api.IsNotFound(err))
})
}
// TestNewVerifyCommand tests that the command is properly configured
func TestNewVerifyCommand(t *testing.T) {
cmd := NewVerifyCommand()
assert.Equal(t, "verify", cmd.Use)
assert.NotEmpty(t, cmd.Short)
assert.NotEmpty(t, cmd.Long)
assert.NotNil(t, cmd.RunE)
}
+3 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/coollabsio/coolify-cli/cmd/application"
"github.com/coollabsio/coolify-cli/cmd/completion"
configcmd "github.com/coollabsio/coolify-cli/cmd/config"
"github.com/coollabsio/coolify-cli/cmd/context"
"github.com/coollabsio/coolify-cli/cmd/database"
"github.com/coollabsio/coolify-cli/cmd/deployment"
@@ -86,8 +87,9 @@ func init() {
// Register all subcommands
rootCmd.AddCommand(application.NewAppCommand())
rootCmd.AddCommand(context.NewContextCommand())
rootCmd.AddCommand(completion.NewCompletionsCommand())
rootCmd.AddCommand(configcmd.NewConfigCommand())
rootCmd.AddCommand(context.NewContextCommand())
rootCmd.AddCommand(database.NewDatabaseCommand())
rootCmd.AddCommand(deployment.NewDeploymentCommand())
rootCmd.AddCommand(github.NewGitHubCommand())
+1 -1
View File
@@ -556,7 +556,7 @@ func TestPath(t *testing.T) {
// Windows path should contain either AppData or backslashes
assert.True(t,
filepath.Separator == '\\' &&
(os.Getenv("APPDATA") != "" || filepath.IsAbs(path)),
(os.Getenv("APPDATA") != "" || filepath.IsAbs(path)),
"Windows path should be valid",
)
}
+30 -30
View File
@@ -26,20 +26,20 @@ type ApplicationListItem struct {
// ApplicationUpdateRequest represents the request to update an application
// All fields are optional - only provided fields will be updated
type ApplicationUpdateRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
GitBranch *string `json:"git_branch,omitempty"`
GitRepository *string `json:"git_repository,omitempty"`
GitCommitSHA *string `json:"git_commit_sha,omitempty"`
Domains *string `json:"domains,omitempty"`
BuildCommand *string `json:"build_command,omitempty"`
StartCommand *string `json:"start_command,omitempty"`
InstallCommand *string `json:"install_command,omitempty"`
BaseDirectory *string `json:"base_directory,omitempty"`
PublishDirectory *string `json:"publish_directory,omitempty"`
BuildPack *string `json:"build_pack,omitempty"`
PortsExposes *string `json:"ports_exposes,omitempty"`
PortsMappings *string `json:"ports_mappings,omitempty"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
GitBranch *string `json:"git_branch,omitempty"`
GitRepository *string `json:"git_repository,omitempty"`
GitCommitSHA *string `json:"git_commit_sha,omitempty"`
Domains *string `json:"domains,omitempty"`
BuildCommand *string `json:"build_command,omitempty"`
StartCommand *string `json:"start_command,omitempty"`
InstallCommand *string `json:"install_command,omitempty"`
BaseDirectory *string `json:"base_directory,omitempty"`
PublishDirectory *string `json:"publish_directory,omitempty"`
BuildPack *string `json:"build_pack,omitempty"`
PortsExposes *string `json:"ports_exposes,omitempty"`
PortsMappings *string `json:"ports_mappings,omitempty"`
// Docker configuration
Dockerfile *string `json:"dockerfile,omitempty"`
@@ -72,9 +72,9 @@ type ApplicationUpdateRequest struct {
LimitsMemorySwappiness *int `json:"limits_memory_swappiness,omitempty"`
// Deployment hooks
PreDeploymentCommand *string `json:"pre_deployment_command,omitempty"`
PreDeploymentCommandContainer *string `json:"pre_deployment_command_container,omitempty"`
PostDeploymentCommand *string `json:"post_deployment_command,omitempty"`
PreDeploymentCommand *string `json:"pre_deployment_command,omitempty"`
PreDeploymentCommandContainer *string `json:"pre_deployment_command_container,omitempty"`
PostDeploymentCommand *string `json:"post_deployment_command,omitempty"`
PostDeploymentCommandContainer *string `json:"post_deployment_command_container,omitempty"`
// Misc
@@ -112,21 +112,21 @@ type EnvironmentVariable struct {
// EnvironmentVariableCreateRequest represents the request to create an environment variable
type EnvironmentVariableCreateRequest struct {
Key string `json:"key"`
Value string `json:"value"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsPreview *bool `json:"is_preview,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
Key string `json:"key"`
Value string `json:"value"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsPreview *bool `json:"is_preview,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
}
// EnvironmentVariableUpdateRequest represents the request to update an environment variable
type EnvironmentVariableUpdateRequest struct {
UUID string `json:"uuid"`
Key *string `json:"key,omitempty"`
Value *string `json:"value,omitempty"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsPreview *bool `json:"is_preview,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
UUID string `json:"uuid"`
Key *string `json:"key,omitempty"`
Value *string `json:"value,omitempty"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsPreview *bool `json:"is_preview,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
}
+1 -1
View File
@@ -13,7 +13,7 @@ type UUID struct {
}
// Timestamps for created/updated times
type Timestamps struct{
type Timestamps struct {
CreatedAt string `json:"-" table:"-"`
UpdatedAt string `json:"-" table:"-"`
}
+22 -22
View File
@@ -166,28 +166,28 @@ type DatabaseLifecycleResponse struct {
// DatabaseBackup represents a scheduled database backup configuration
type DatabaseBackup struct {
ID int `json:"-" table:"-"`
UUID string `json:"uuid"`
Description *string `json:"description,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Frequency *string `json:"frequency,omitempty"`
SaveS3 *bool `json:"save_s3,omitempty"`
S3StorageID *int `json:"-" table:"-"`
DatabasesToBackup *string `json:"databases_to_backup,omitempty"`
DumpAll *bool `json:"dump_all,omitempty"`
DatabaseBackupRetentionAmountLocally *int `json:"database_backup_retention_amount_locally,omitempty"`
DatabaseBackupRetentionDaysLocally *int `json:"database_backup_retention_days_locally,omitempty"`
DatabaseBackupRetentionMaxStorageLocally *string `json:"database_backup_retention_max_storage_locally,omitempty"`
DatabaseBackupRetentionAmountS3 *int `json:"database_backup_retention_amount_s3,omitempty"`
DatabaseBackupRetentionDaysS3 *int `json:"database_backup_retention_days_s3,omitempty"`
DatabaseBackupRetentionMaxStorageS3 *string `json:"database_backup_retention_max_storage_s3,omitempty"`
DatabaseType *string `json:"database_type,omitempty" table:"-"`
DatabaseID *int `json:"-" table:"-"`
TeamID *int `json:"-" table:"-"`
Timeout *int `json:"timeout,omitempty"`
DisableLocalBackup *bool `json:"disable_local_backup,omitempty"`
CreatedAt string `json:"-" table:"-"`
UpdatedAt string `json:"-" table:"-"`
ID int `json:"-" table:"-"`
UUID string `json:"uuid"`
Description *string `json:"description,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Frequency *string `json:"frequency,omitempty"`
SaveS3 *bool `json:"save_s3,omitempty"`
S3StorageID *int `json:"-" table:"-"`
DatabasesToBackup *string `json:"databases_to_backup,omitempty"`
DumpAll *bool `json:"dump_all,omitempty"`
DatabaseBackupRetentionAmountLocally *int `json:"database_backup_retention_amount_locally,omitempty"`
DatabaseBackupRetentionDaysLocally *int `json:"database_backup_retention_days_locally,omitempty"`
DatabaseBackupRetentionMaxStorageLocally *string `json:"database_backup_retention_max_storage_locally,omitempty"`
DatabaseBackupRetentionAmountS3 *int `json:"database_backup_retention_amount_s3,omitempty"`
DatabaseBackupRetentionDaysS3 *int `json:"database_backup_retention_days_s3,omitempty"`
DatabaseBackupRetentionMaxStorageS3 *string `json:"database_backup_retention_max_storage_s3,omitempty"`
DatabaseType *string `json:"database_type,omitempty" table:"-"`
DatabaseID *int `json:"-" table:"-"`
TeamID *int `json:"-" table:"-"`
Timeout *int `json:"timeout,omitempty"`
DisableLocalBackup *bool `json:"disable_local_backup,omitempty"`
CreatedAt string `json:"-" table:"-"`
UpdatedAt string `json:"-" table:"-"`
}
// DatabaseBackupCreateRequest represents the request to create a backup configuration
+2 -2
View File
@@ -246,8 +246,8 @@ func TestTableFormatter_NilPointer(t *testing.T) {
func TestTableFormatter_SliceField(t *testing.T) {
type TestStruct struct {
Name string `json:"name"`
Tags []string `json:"tags"`
Name string `json:"name"`
Tags []string `json:"tags"`
}
data := []TestStruct{
+9 -9
View File
@@ -280,15 +280,15 @@ func TestDatabaseService_Update(t *testing.T) {
func TestDatabaseService_Delete(t *testing.T) {
tests := []struct {
name string
uuid string
deleteConfigurations bool
deleteVolumes bool
dockerCleanup bool
deleteConnectedNetworks bool
statusCode int
wantErr bool
expectedQueryString string
name string
uuid string
deleteConfigurations bool
deleteVolumes bool
dockerCleanup bool
deleteConnectedNetworks bool
statusCode int
wantErr bool
expectedQueryString string
}{
{
name: "successful delete with all cleanup",
+5 -5
View File
@@ -14,11 +14,11 @@ import (
func TestDeploymentService_Deploy(t *testing.T) {
tests := []struct {
name string
uuid string
force bool
expectedPath string
response DeployResponse
name string
uuid string
force bool
expectedPath string
response DeployResponse
}{
{
name: "deploy without force",
+1 -1
View File
@@ -14,7 +14,7 @@ import (
)
// CliVersion is the CLI version
const CliVersion = "1.0.1"
const CliVersion = "1.0.2"
// CheckInterval for version checking
const CheckInterval = 10 * time.Minute