Move module identity to our own Forgejo instance. All import paths updated across 434 Go files, sub-module go.mod files, and go.work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
351 lines
9.4 KiB
Go
351 lines
9.4 KiB
Go
package php
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"forge.lthn.ai/core/cli/pkg/cli"
|
|
)
|
|
|
|
// CoolifyClient is an HTTP client for the Coolify API.
|
|
type CoolifyClient struct {
|
|
BaseURL string
|
|
Token string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// CoolifyConfig holds configuration loaded from environment.
|
|
type CoolifyConfig struct {
|
|
URL string
|
|
Token string
|
|
AppID string
|
|
StagingAppID string
|
|
}
|
|
|
|
// CoolifyDeployment represents a deployment from the Coolify API.
|
|
type CoolifyDeployment struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
CommitSHA string `json:"commit_sha,omitempty"`
|
|
CommitMsg string `json:"commit_message,omitempty"`
|
|
Branch string `json:"branch,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
FinishedAt time.Time `json:"finished_at,omitempty"`
|
|
Log string `json:"log,omitempty"`
|
|
DeployedURL string `json:"deployed_url,omitempty"`
|
|
}
|
|
|
|
// CoolifyApp represents an application from the Coolify API.
|
|
type CoolifyApp struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
FQDN string `json:"fqdn,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Repository string `json:"repository,omitempty"`
|
|
Branch string `json:"branch,omitempty"`
|
|
Environment string `json:"environment,omitempty"`
|
|
}
|
|
|
|
// NewCoolifyClient creates a new Coolify API client.
|
|
func NewCoolifyClient(baseURL, token string) *CoolifyClient {
|
|
// Ensure baseURL doesn't have trailing slash
|
|
baseURL = strings.TrimSuffix(baseURL, "/")
|
|
|
|
return &CoolifyClient{
|
|
BaseURL: baseURL,
|
|
Token: token,
|
|
HTTPClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// LoadCoolifyConfig loads Coolify configuration from .env file in the given directory.
|
|
func LoadCoolifyConfig(dir string) (*CoolifyConfig, error) {
|
|
envPath := filepath.Join(dir, ".env")
|
|
return LoadCoolifyConfigFromFile(envPath)
|
|
}
|
|
|
|
// LoadCoolifyConfigFromFile loads Coolify configuration from a specific .env file.
|
|
func LoadCoolifyConfigFromFile(path string) (*CoolifyConfig, error) {
|
|
m := getMedium()
|
|
config := &CoolifyConfig{}
|
|
|
|
// First try environment variables
|
|
config.URL = os.Getenv("COOLIFY_URL")
|
|
config.Token = os.Getenv("COOLIFY_TOKEN")
|
|
config.AppID = os.Getenv("COOLIFY_APP_ID")
|
|
config.StagingAppID = os.Getenv("COOLIFY_STAGING_APP_ID")
|
|
|
|
// Then try .env file
|
|
if !m.Exists(path) {
|
|
// No .env file, just use env vars
|
|
return validateCoolifyConfig(config)
|
|
}
|
|
|
|
content, err := m.Read(path)
|
|
if err != nil {
|
|
return nil, cli.WrapVerb(err, "read", ".env file")
|
|
}
|
|
|
|
// Parse .env file
|
|
lines := strings.Split(content, "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
// Remove quotes if present
|
|
value = strings.Trim(value, `"'`)
|
|
|
|
// Only override if not already set from env
|
|
switch key {
|
|
case "COOLIFY_URL":
|
|
if config.URL == "" {
|
|
config.URL = value
|
|
}
|
|
case "COOLIFY_TOKEN":
|
|
if config.Token == "" {
|
|
config.Token = value
|
|
}
|
|
case "COOLIFY_APP_ID":
|
|
if config.AppID == "" {
|
|
config.AppID = value
|
|
}
|
|
case "COOLIFY_STAGING_APP_ID":
|
|
if config.StagingAppID == "" {
|
|
config.StagingAppID = value
|
|
}
|
|
}
|
|
}
|
|
|
|
return validateCoolifyConfig(config)
|
|
}
|
|
|
|
// validateCoolifyConfig checks that required fields are set.
|
|
func validateCoolifyConfig(config *CoolifyConfig) (*CoolifyConfig, error) {
|
|
if config.URL == "" {
|
|
return nil, cli.Err("COOLIFY_URL is not set")
|
|
}
|
|
if config.Token == "" {
|
|
return nil, cli.Err("COOLIFY_TOKEN is not set")
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// TriggerDeploy triggers a deployment for the specified application.
|
|
func (c *CoolifyClient) TriggerDeploy(ctx context.Context, appID string, force bool) (*CoolifyDeployment, error) {
|
|
endpoint := cli.Sprintf("%s/api/v1/applications/%s/deploy", c.BaseURL, appID)
|
|
|
|
payload := map[string]interface{}{}
|
|
if force {
|
|
payload["force"] = true
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, cli.WrapVerb(err, "marshal", "request")
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, cli.WrapVerb(err, "create", "request")
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, cli.Wrap(err, "request failed")
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
|
|
return nil, c.parseError(resp)
|
|
}
|
|
|
|
var deployment CoolifyDeployment
|
|
if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil {
|
|
// Some Coolify versions return minimal response
|
|
return &CoolifyDeployment{
|
|
Status: "queued",
|
|
CreatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
return &deployment, nil
|
|
}
|
|
|
|
// GetDeployment retrieves a specific deployment by ID.
|
|
func (c *CoolifyClient) GetDeployment(ctx context.Context, appID, deploymentID string) (*CoolifyDeployment, error) {
|
|
endpoint := cli.Sprintf("%s/api/v1/applications/%s/deployments/%s", c.BaseURL, appID, deploymentID)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, cli.WrapVerb(err, "create", "request")
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, cli.Wrap(err, "request failed")
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, c.parseError(resp)
|
|
}
|
|
|
|
var deployment CoolifyDeployment
|
|
if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil {
|
|
return nil, cli.WrapVerb(err, "decode", "response")
|
|
}
|
|
|
|
return &deployment, nil
|
|
}
|
|
|
|
// ListDeployments retrieves deployments for an application.
|
|
func (c *CoolifyClient) ListDeployments(ctx context.Context, appID string, limit int) ([]CoolifyDeployment, error) {
|
|
endpoint := cli.Sprintf("%s/api/v1/applications/%s/deployments", c.BaseURL, appID)
|
|
if limit > 0 {
|
|
endpoint = cli.Sprintf("%s?limit=%d", endpoint, limit)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, cli.WrapVerb(err, "create", "request")
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, cli.Wrap(err, "request failed")
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, c.parseError(resp)
|
|
}
|
|
|
|
var deployments []CoolifyDeployment
|
|
if err := json.NewDecoder(resp.Body).Decode(&deployments); err != nil {
|
|
return nil, cli.WrapVerb(err, "decode", "response")
|
|
}
|
|
|
|
return deployments, nil
|
|
}
|
|
|
|
// Rollback triggers a rollback to a previous deployment.
|
|
func (c *CoolifyClient) Rollback(ctx context.Context, appID, deploymentID string) (*CoolifyDeployment, error) {
|
|
endpoint := cli.Sprintf("%s/api/v1/applications/%s/rollback", c.BaseURL, appID)
|
|
|
|
payload := map[string]interface{}{
|
|
"deployment_id": deploymentID,
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, cli.WrapVerb(err, "marshal", "request")
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, cli.WrapVerb(err, "create", "request")
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, cli.Wrap(err, "request failed")
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
|
|
return nil, c.parseError(resp)
|
|
}
|
|
|
|
var deployment CoolifyDeployment
|
|
if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil {
|
|
return &CoolifyDeployment{
|
|
Status: "rolling_back",
|
|
CreatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
return &deployment, nil
|
|
}
|
|
|
|
// GetApp retrieves application details.
|
|
func (c *CoolifyClient) GetApp(ctx context.Context, appID string) (*CoolifyApp, error) {
|
|
endpoint := cli.Sprintf("%s/api/v1/applications/%s", c.BaseURL, appID)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, cli.WrapVerb(err, "create", "request")
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, cli.Wrap(err, "request failed")
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, c.parseError(resp)
|
|
}
|
|
|
|
var app CoolifyApp
|
|
if err := json.NewDecoder(resp.Body).Decode(&app); err != nil {
|
|
return nil, cli.WrapVerb(err, "decode", "response")
|
|
}
|
|
|
|
return &app, nil
|
|
}
|
|
|
|
// setHeaders sets common headers for API requests.
|
|
func (c *CoolifyClient) setHeaders(req *http.Request) {
|
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
}
|
|
|
|
// parseError extracts error information from an API response.
|
|
func (c *CoolifyClient) parseError(resp *http.Response) error {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
var errResp struct {
|
|
Message string `json:"message"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &errResp); err == nil {
|
|
if errResp.Message != "" {
|
|
return cli.Err("API error (%d): %s", resp.StatusCode, errResp.Message)
|
|
}
|
|
if errResp.Error != "" {
|
|
return cli.Err("API error (%d): %s", resp.StatusCode, errResp.Error)
|
|
}
|
|
}
|
|
|
|
return cli.Err("API error (%d): %s", resp.StatusCode, string(body))
|
|
}
|