cli/pkg/deploy/coolify/client.go
Snider a794f6b55f feat(deploy): add pure-Go Ansible executor and Coolify API integration
Implement infrastructure deployment system with:

- pkg/ansible: Pure Go Ansible executor
  - Playbook/inventory parsing (types.go, parser.go)
  - Full execution engine with variable templating, loops, blocks,
    conditionals, handlers, and fact gathering (executor.go)
  - SSH client with key/password auth and privilege escalation (ssh.go)
  - 35+ module implementations: shell, command, copy, template, file,
    apt, service, systemd, user, group, git, docker_compose, etc. (modules.go)

- pkg/deploy/coolify: Coolify API client wrapping Python swagger client
  - List/get servers, projects, applications, databases, services
  - Generic Call() for any OpenAPI operation

- pkg/deploy/python: Embedded Python runtime for swagger client integration

- internal/cmd/deploy: CLI commands
  - core deploy servers/projects/apps/databases/services/team
  - core deploy call <operation> [params-json]

This enables Docker-free infrastructure deployment with Ansible-compatible
playbooks executed natively in Go.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 18:01:10 +00:00

216 lines
6 KiB
Go

package coolify
import (
"context"
"encoding/json"
"fmt"
"os"
"sync"
"github.com/host-uk/core/pkg/deploy/python"
)
// Client wraps the Python CoolifyClient for Go usage.
type Client struct {
baseURL string
apiToken string
timeout int
verifySSL bool
mu sync.Mutex
}
// Config holds Coolify client configuration.
type Config struct {
BaseURL string
APIToken string
Timeout int
VerifySSL bool
}
// DefaultConfig returns default configuration from environment.
func DefaultConfig() Config {
return Config{
BaseURL: os.Getenv("COOLIFY_URL"),
APIToken: os.Getenv("COOLIFY_TOKEN"),
Timeout: 30,
VerifySSL: true,
}
}
// NewClient creates a new Coolify client.
func NewClient(cfg Config) (*Client, error) {
if cfg.BaseURL == "" {
return nil, fmt.Errorf("COOLIFY_URL not set")
}
if cfg.APIToken == "" {
return nil, fmt.Errorf("COOLIFY_TOKEN not set")
}
// Initialize Python runtime
if err := python.Init(); err != nil {
return nil, fmt.Errorf("failed to initialize Python: %w", err)
}
return &Client{
baseURL: cfg.BaseURL,
apiToken: cfg.APIToken,
timeout: cfg.Timeout,
verifySSL: cfg.VerifySSL,
}, nil
}
// Call invokes a Coolify API operation by operationId.
func (c *Client) Call(ctx context.Context, operationID string, params map[string]any) (map[string]any, error) {
c.mu.Lock()
defer c.mu.Unlock()
if params == nil {
params = map[string]any{}
}
// Generate and run Python script
script := python.CoolifyScript(c.baseURL, c.apiToken, operationID, params)
output, err := python.RunScript(ctx, script)
if err != nil {
return nil, fmt.Errorf("API call %s failed: %w", operationID, err)
}
// Parse JSON result
var result map[string]any
if err := json.Unmarshal([]byte(output), &result); err != nil {
// Try parsing as array
var arrResult []any
if err2 := json.Unmarshal([]byte(output), &arrResult); err2 == nil {
return map[string]any{"result": arrResult}, nil
}
return nil, fmt.Errorf("failed to parse response: %w (output: %s)", err, output)
}
return result, nil
}
// ListServers returns all servers.
func (c *Client) ListServers(ctx context.Context) ([]map[string]any, error) {
result, err := c.Call(ctx, "list-servers", nil)
if err != nil {
return nil, err
}
return extractArray(result)
}
// GetServer returns a server by UUID.
func (c *Client) GetServer(ctx context.Context, uuid string) (map[string]any, error) {
return c.Call(ctx, "get-server-by-uuid", map[string]any{"uuid": uuid})
}
// ValidateServer validates a server by UUID.
func (c *Client) ValidateServer(ctx context.Context, uuid string) (map[string]any, error) {
return c.Call(ctx, "validate-server-by-uuid", map[string]any{"uuid": uuid})
}
// ListProjects returns all projects.
func (c *Client) ListProjects(ctx context.Context) ([]map[string]any, error) {
result, err := c.Call(ctx, "list-projects", nil)
if err != nil {
return nil, err
}
return extractArray(result)
}
// GetProject returns a project by UUID.
func (c *Client) GetProject(ctx context.Context, uuid string) (map[string]any, error) {
return c.Call(ctx, "get-project-by-uuid", map[string]any{"uuid": uuid})
}
// CreateProject creates a new project.
func (c *Client) CreateProject(ctx context.Context, name, description string) (map[string]any, error) {
return c.Call(ctx, "create-project", map[string]any{
"name": name,
"description": description,
})
}
// ListApplications returns all applications.
func (c *Client) ListApplications(ctx context.Context) ([]map[string]any, error) {
result, err := c.Call(ctx, "list-applications", nil)
if err != nil {
return nil, err
}
return extractArray(result)
}
// GetApplication returns an application by UUID.
func (c *Client) GetApplication(ctx context.Context, uuid string) (map[string]any, error) {
return c.Call(ctx, "get-application-by-uuid", map[string]any{"uuid": uuid})
}
// DeployApplication triggers deployment of an application.
func (c *Client) DeployApplication(ctx context.Context, uuid string) (map[string]any, error) {
return c.Call(ctx, "deploy-by-tag-or-uuid", map[string]any{"uuid": uuid})
}
// ListDatabases returns all databases.
func (c *Client) ListDatabases(ctx context.Context) ([]map[string]any, error) {
result, err := c.Call(ctx, "list-databases", nil)
if err != nil {
return nil, err
}
return extractArray(result)
}
// GetDatabase returns a database by UUID.
func (c *Client) GetDatabase(ctx context.Context, uuid string) (map[string]any, error) {
return c.Call(ctx, "get-database-by-uuid", map[string]any{"uuid": uuid})
}
// ListServices returns all services.
func (c *Client) ListServices(ctx context.Context) ([]map[string]any, error) {
result, err := c.Call(ctx, "list-services", nil)
if err != nil {
return nil, err
}
return extractArray(result)
}
// GetService returns a service by UUID.
func (c *Client) GetService(ctx context.Context, uuid string) (map[string]any, error) {
return c.Call(ctx, "get-service-by-uuid", map[string]any{"uuid": uuid})
}
// ListEnvironments returns environments for a project.
func (c *Client) ListEnvironments(ctx context.Context, projectUUID string) ([]map[string]any, error) {
result, err := c.Call(ctx, "get-environments", map[string]any{"project_uuid": projectUUID})
if err != nil {
return nil, err
}
return extractArray(result)
}
// GetTeam returns the current team.
func (c *Client) GetTeam(ctx context.Context) (map[string]any, error) {
return c.Call(ctx, "get-current-team", nil)
}
// GetTeamMembers returns members of the current team.
func (c *Client) GetTeamMembers(ctx context.Context) ([]map[string]any, error) {
result, err := c.Call(ctx, "get-current-team-members", nil)
if err != nil {
return nil, err
}
return extractArray(result)
}
// extractArray extracts an array from result["result"] or returns empty.
func extractArray(result map[string]any) ([]map[string]any, error) {
if arr, ok := result["result"].([]any); ok {
items := make([]map[string]any, 0, len(arr))
for _, item := range arr {
if m, ok := item.(map[string]any); ok {
items = append(items, m)
}
}
return items, nil
}
return nil, nil
}