mirror of
https://github.com/coollabsio/coolify-cli.git
synced 2026-06-22 09:05:03 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 794025aee1 | |||
| 8d6da93aa1 | |||
| 22516fe51e | |||
| fe63c3a3b6 |
@@ -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
|
||||
|
||||
@@ -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())
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ func NewContextCommand() *cobra.Command {
|
||||
cmd.AddCommand(NewSetTokenCommand())
|
||||
cmd.AddCommand(NewSetDefaultCommand())
|
||||
cmd.AddCommand(NewVersionCommand())
|
||||
cmd.AddCommand(NewVerifyCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user