mirror of
https://github.com/coollabsio/coolify-cli.git
synced 2026-06-20 16:15:04 +00:00
Compare commits
118 Commits
chore/refactor
...
v1.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 780b3674c7 | |||
| 77adbfaebc | |||
| 9215fd537e | |||
| 26c0925854 | |||
| 1f1b187ed2 | |||
| 4af598c213 | |||
| 6ca3b700ce | |||
| 8cf0b71ebf | |||
| 99a40bfa1d | |||
| 188834fd6d | |||
| f9c3b9869a | |||
| 6044a2107e | |||
| 2f9dc6e8d7 | |||
| 1e741309cb | |||
| 51f759c38f | |||
| 51e9ec5ec8 | |||
| e071fd81d4 | |||
| cb0bbfc5cb | |||
| 87b6b8fdf7 | |||
| 3dbe2507f4 | |||
| 1e82217a50 | |||
| 6f33fa00f1 | |||
| d7841b3b5a | |||
| 234f6e9ed6 | |||
| fa86ceb5cc | |||
| 646bf9de36 | |||
| be29a6e05d | |||
| 63a882107a | |||
| 08cd3b8ac7 | |||
| 06f191e9ba | |||
| 7a19a02c02 | |||
| 806a6b9716 | |||
| a18d751ad4 | |||
| 9283717821 | |||
| a44c712163 | |||
| 7c42af6203 | |||
| ed2dbd4947 | |||
| f1301c1dbd | |||
| 8a58cdd72f | |||
| 5ca4c3a8e3 | |||
| 89cd744696 | |||
| 694b4f8e32 | |||
| ef15d013da | |||
| 9f5a44be04 | |||
| 1078a9d3ca | |||
| 9cef9ebee7 | |||
| 51f33cfc5e | |||
| 559d4e2709 | |||
| 94e237e2b1 | |||
| 3dd0dfdce1 | |||
| aac44a0ddb | |||
| a4c96d2803 | |||
| dc9670992a | |||
| c369689131 | |||
| dcf0b39c1b | |||
| 14a3f00c57 | |||
| 86f77716ee | |||
| ef91ed987e | |||
| d8cf7a5986 | |||
| d30b0b90de | |||
| 4a8a659090 | |||
| aa1bda4063 | |||
| 69f2a7ac1f | |||
| ad185a42ee | |||
| 22e34fb72e | |||
| c5b0ad4218 | |||
| 1a6fa9e397 | |||
| b767468b29 | |||
| 6b742d37dd | |||
| 0b2d277e41 | |||
| ac9a486d46 | |||
| ba8f05769f | |||
| 5c799410e5 | |||
| d5dd1b5bdf | |||
| 0d6a2bb1e9 | |||
| 9eba5b97f7 | |||
| 76c1434711 | |||
| f35d299f6e | |||
| 680264ab3f | |||
| 794025aee1 | |||
| 8d6da93aa1 | |||
| 22516fe51e | |||
| fe63c3a3b6 | |||
| 6cad3ba0f7 | |||
| 1bc4625ef8 | |||
| d9494301f6 | |||
| 85cf8f6981 | |||
| c98a8330f8 | |||
| 49e5870c86 | |||
| 705fed3560 | |||
| 88d0b25537 | |||
| 3470b04235 | |||
| eb876e4bda | |||
| 7210b8df76 | |||
| 0546fd1932 | |||
| e2c1c86194 | |||
| 84e7e4921b | |||
| 0acb1fc512 | |||
| eaf9614bcc | |||
| 8ec750ecc6 | |||
| 76396c3c06 | |||
| 3286229a06 | |||
| 884b687947 | |||
| 11f9baafc6 | |||
| f4f628fdae | |||
| ae086bbbbd | |||
| 401f4bf317 | |||
| 095b5a5bc5 | |||
| 002e206c54 | |||
| c865aa7512 | |||
| 3788d6b812 | |||
| 7d23eac444 | |||
| 6aa77a4840 | |||
| 17f1435ce1 | |||
| 485b4c5f6b | |||
| d930e6cf9f | |||
| 519fe69ebf | |||
| 4e0575a156 |
@@ -0,0 +1,46 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./coolify"
|
||||
cmd = "go build -o ./coolify ./coolify"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", ".git", ".conductor"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = "echo 'Build complete. Binary: ./coolify/coolify'"
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = true
|
||||
keep_scroll = true
|
||||
@@ -0,0 +1,7 @@
|
||||
## Changes
|
||||
|
||||
-
|
||||
|
||||
## Issues & Discussions
|
||||
|
||||
- fix #
|
||||
@@ -10,18 +10,47 @@ jobs:
|
||||
release-cli:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: ${{ env.GITHUB_REF_NAME }}
|
||||
args: release --clean
|
||||
workdir: ./
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
update-version:
|
||||
needs: [release-cli]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: v4.x
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Update version file
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo "Updating version to $TAG"
|
||||
sed -i "s/^\tversion = \".*\"/\tversion = \"$TAG\"/" internal/version/checker.go
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add internal/version/checker.go
|
||||
git commit -m "chore: bump version to $TAG"
|
||||
git push origin v4.x
|
||||
|
||||
# Move the tag to point to the new commit with updated version
|
||||
git tag -d "$TAG" || true
|
||||
git tag "$TAG"
|
||||
git push origin "refs/tags/$TAG" --force
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Testing CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["v4.x"]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Run gofmt
|
||||
run: diff -u <(echo -n) <(gofmt -d -s .)
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.5.0 # pin version for consistency
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v -race -cover ./...
|
||||
|
||||
go-mod-tidy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run go mod tidy
|
||||
run: go mod tidy
|
||||
|
||||
- name: Check uncommitted changes
|
||||
run: git diff --exit-code
|
||||
|
||||
- if: failure()
|
||||
run: echo "::error::Check failed, please run 'go mod tidy' and commit the changes."
|
||||
+11
-3
@@ -1,4 +1,12 @@
|
||||
coolify-cli
|
||||
coolify
|
||||
cli
|
||||
config.json
|
||||
/coolify
|
||||
config.json
|
||||
.claude
|
||||
|
||||
# Generated documentation (can be regenerated)
|
||||
man/
|
||||
docs/cli/
|
||||
dist/
|
||||
|
||||
# Test coverage
|
||||
coverage.out
|
||||
@@ -0,0 +1,75 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- contextcheck
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- gocheckcompilerdirectives
|
||||
- gochecksumtype
|
||||
- gocritic
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- loggercheck
|
||||
- makezero
|
||||
- musttag
|
||||
- nilerr
|
||||
- nilnesserr
|
||||
- noctx
|
||||
- protogetter
|
||||
- reassign
|
||||
- recvcheck
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- spancheck
|
||||
- sqlclosecheck
|
||||
- testifylint
|
||||
- unparam
|
||||
- zerologlint
|
||||
|
||||
settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
|
||||
staticcheck:
|
||||
checks: ["all", "-ST1005", "-S1016"]
|
||||
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
|
||||
gosmopolitan:
|
||||
allow-time-local: true
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- std-error-handling
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- goimports
|
||||
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/coollabsio)
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
+27
-2
@@ -1,8 +1,19 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- binary: coolify
|
||||
- id: coolify
|
||||
binary: coolify
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s
|
||||
- -w
|
||||
- -X github.com/coollabsio/coolify-cli/internal/version.version={{ .Version }}
|
||||
main: ./coolify/main.go
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
@@ -11,4 +22,18 @@ builds:
|
||||
- amd64
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- CGO_ENABLED=0
|
||||
|
||||
checksum:
|
||||
name_template: checksums.txt
|
||||
algorithm: sha256
|
||||
|
||||
archives:
|
||||
- id: coolify-archive
|
||||
ids:
|
||||
- coolify
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
+587
@@ -0,0 +1,587 @@
|
||||
# Coolify CLI Architecture
|
||||
|
||||
This document describes the architecture and design principles of the Coolify CLI.
|
||||
|
||||
## Overview
|
||||
|
||||
The Coolify CLI is a command-line interface for managing Coolify instances, servers, projects, and deployments. It follows a layered architecture pattern that separates concerns and promotes maintainability.
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ User Interface │
|
||||
│ (Terminal/Shell) │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────▼────────────────────────────────┐
|
||||
│ Command Layer (cmd/) │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ servers │ │ deploy │ │ projects │ ... │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ • CLI parsing & validation │
|
||||
│ • Flag handling │
|
||||
│ • Output formatting │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────▼────────────────────────────────┐
|
||||
│ Service Layer (internal/service/) │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ServerService│ │DeployService │ │ProjectService│ │
|
||||
│ └─────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ • Business logic │
|
||||
│ • Request validation │
|
||||
│ • Response transformation │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────▼────────────────────────────────┐
|
||||
│ API Client Layer (internal/api/) │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ HTTP Client (api.Client) │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ • HTTP requests/responses │
|
||||
│ • Authentication (Bearer tokens) │
|
||||
│ • Retry logic │
|
||||
│ • Error handling │
|
||||
└────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────▼────────────────────────────────┐
|
||||
│ Coolify API (External) │
|
||||
│ https://instance.coolify.io/api/v1/ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Supporting Components
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Configuration (internal/config/) │
|
||||
│ • Multi-instance management │
|
||||
│ • Default instance selection │
|
||||
│ • Token storage │
|
||||
│ • ~/.config/coolify/config.json │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Output Formatters (internal/output/) │
|
||||
│ ┌─────────┐ ┌────────┐ ┌─────────┐ │
|
||||
│ │ Table │ │ JSON │ │ Pretty │ │
|
||||
│ └─────────┘ └────────┘ └─────────┘ │
|
||||
│ • Flexible output formats │
|
||||
│ • Sensitive data masking │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Data Models (internal/models/) │
|
||||
│ • Server, Project, Resource, Deployment │
|
||||
│ • Request/Response structures │
|
||||
│ • JSON marshaling/unmarshaling │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Layer Responsibilities
|
||||
|
||||
### 1. Command Layer (`cmd/`)
|
||||
|
||||
**Purpose**: Handle CLI user interface and interaction
|
||||
|
||||
**Responsibilities**:
|
||||
- Parse command-line arguments and flags
|
||||
- Validate user input
|
||||
- Coordinate with service layer
|
||||
- Format and display output
|
||||
- Handle errors gracefully
|
||||
|
||||
**Key Files**:
|
||||
- `root.go` - Root command, global flags, initialization
|
||||
- `servers.go` - Server management commands
|
||||
- `deploy.go` - Deployment commands
|
||||
- `context.go` - Context (instance) configuration commands
|
||||
- `projects.go` - Project listing and inspection
|
||||
- etc.
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
var serversListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all servers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Get API client
|
||||
client, err := getAPIClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use service layer
|
||||
service := service.NewServerService(client)
|
||||
servers, err := service.List(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Format and display output
|
||||
formatter, _ := getFormatter(cmd)
|
||||
return formatter.Format(servers)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Service Layer (`internal/service/`)
|
||||
|
||||
**Purpose**: Implement business logic and coordinate API calls
|
||||
|
||||
**Responsibilities**:
|
||||
- Validate business rules
|
||||
- Coordinate multiple API calls if needed
|
||||
- Transform API responses to CLI-friendly format
|
||||
- Handle service-specific error cases
|
||||
|
||||
**Key Files**:
|
||||
- `server.go` - Server operations
|
||||
- `deployment.go` - Deployment operations
|
||||
- `project.go` - Project operations
|
||||
- `resource.go` - Resource operations
|
||||
- `privatekey.go` - SSH key operations
|
||||
- `domain.go` - Domain operations
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
type ServerService struct {
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func (s *ServerService) List(ctx context.Context) ([]models.Server, error) {
|
||||
var servers []models.Server
|
||||
err := s.client.Get(ctx, "servers", &servers)
|
||||
return servers, err
|
||||
}
|
||||
```
|
||||
|
||||
### 3. API Client Layer (`internal/api/`)
|
||||
|
||||
**Purpose**: Handle all HTTP communication with Coolify API
|
||||
|
||||
**Responsibilities**:
|
||||
- Construct HTTP requests
|
||||
- Add authentication headers
|
||||
- Retry failed requests with exponential backoff
|
||||
- Parse HTTP responses
|
||||
- Convert HTTP errors to meaningful error messages
|
||||
|
||||
**Key Files**:
|
||||
- `client.go` - HTTP client implementation
|
||||
- `error.go` - API error handling
|
||||
- `options.go` - Client configuration options
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
type Client struct {
|
||||
baseURL string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
retries int
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (c *Client) Get(ctx context.Context, path string, result interface{}) error {
|
||||
return c.doRequest(ctx, "GET", path, nil, result)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Configuration Layer (`internal/config/`)
|
||||
|
||||
**Purpose**: Manage CLI configuration and multiple instances
|
||||
|
||||
**Responsibilities**:
|
||||
- Load/save configuration from disk
|
||||
- Manage multiple Coolify instances
|
||||
- Select default instance
|
||||
- Store API tokens securely (file permissions)
|
||||
|
||||
**Key Files**:
|
||||
- `config.go` - Configuration structure and methods
|
||||
- `instance.go` - Instance definition
|
||||
- `loader.go` - File I/O operations
|
||||
|
||||
**Configuration File** (`~/.config/coolify/config.json`):
|
||||
```json
|
||||
{
|
||||
"instances": [
|
||||
{
|
||||
"name": "prod",
|
||||
"fqdn": "https://coolify.example.com",
|
||||
"token": "your-api-token",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "staging",
|
||||
"fqdn": "https://staging.coolify.example.com",
|
||||
"token": "staging-token"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Output Layer (`internal/output/`)
|
||||
|
||||
**Purpose**: Format data for display to users
|
||||
|
||||
**Responsibilities**:
|
||||
- Format data as tables, JSON, or pretty-printed JSON
|
||||
- Hide sensitive information unless `--show-sensitive` is used
|
||||
- Handle different data types (slices, structs, primitives)
|
||||
|
||||
**Key Files**:
|
||||
- `formatter.go` - Formatter interface
|
||||
- `table.go` - Table formatting
|
||||
- `json.go` - JSON formatting
|
||||
- `pretty.go` - Pretty JSON formatting
|
||||
|
||||
**Supported Formats**:
|
||||
- `table` - Default, human-readable tables
|
||||
- `json` - Compact JSON for scripting
|
||||
- `pretty` - Indented JSON for debugging
|
||||
|
||||
### 6. Models Layer (`internal/models/`)
|
||||
|
||||
**Purpose**: Define data structures
|
||||
|
||||
**Responsibilities**:
|
||||
- Define API request/response structures
|
||||
- JSON tags for marshaling
|
||||
- Common types and timestamps
|
||||
|
||||
**Key Files**:
|
||||
- `server.go` - Server-related types
|
||||
- `project.go` - Project-related types
|
||||
- `resource.go` - Resource types
|
||||
- `deployment.go` - Deployment types
|
||||
- `common.go` - Shared types
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Example: Listing Servers
|
||||
|
||||
1. **User Input**: `coolify servers list --format=table`
|
||||
|
||||
2. **Command Layer** (`cmd/servers.go`):
|
||||
- Cobra parses the command
|
||||
- `serversListCmd.RunE` is executed
|
||||
- Gets API client using `getAPIClient()`
|
||||
- Creates ServerService instance
|
||||
|
||||
3. **Service Layer** (`internal/service/server.go`):
|
||||
- `ServerService.List()` is called
|
||||
- Validates context (if needed)
|
||||
- Calls API client
|
||||
|
||||
4. **API Client Layer** (`internal/api/client.go`):
|
||||
- Constructs GET request to `/api/v1/servers`
|
||||
- Adds Bearer token authentication
|
||||
- Sends HTTP request
|
||||
- Retries on failure (with backoff)
|
||||
- Parses JSON response
|
||||
|
||||
5. **Response Processing**:
|
||||
- JSON unmarshaled to `[]models.Server`
|
||||
- Returns to service layer
|
||||
- Returns to command layer
|
||||
|
||||
6. **Output Layer** (`internal/output/table.go`):
|
||||
- Command layer creates table formatter
|
||||
- Formatter processes server data
|
||||
- Formats as table with columns
|
||||
- Writes to stdout
|
||||
|
||||
7. **User Output**: Table displayed in terminal
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### 1. Dependency Injection
|
||||
|
||||
Services receive the API client as a constructor parameter:
|
||||
|
||||
```go
|
||||
func NewServerService(client *api.Client) *ServerService {
|
||||
return &ServerService{client: client}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Easy to test (can inject mock client)
|
||||
- Clear dependencies
|
||||
- Flexible configuration
|
||||
|
||||
### 2. Strategy Pattern (Output Formatters)
|
||||
|
||||
Different formatters implement the same interface:
|
||||
|
||||
```go
|
||||
type Formatter interface {
|
||||
Format(data interface{}) error
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Easy to add new formats
|
||||
- Consistent API
|
||||
- Runtime format selection
|
||||
|
||||
### 3. Options Pattern (API Client)
|
||||
|
||||
Client configuration uses functional options:
|
||||
|
||||
```go
|
||||
client := api.NewClient(url, token,
|
||||
api.WithDebug(true),
|
||||
api.WithRetries(5),
|
||||
api.WithTimeout(60 * time.Second),
|
||||
)
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Optional parameters
|
||||
- Clear intent
|
||||
- Backward compatible
|
||||
|
||||
### 4. Error Wrapping
|
||||
|
||||
Errors are wrapped with context at each layer:
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list servers: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Error context preserved
|
||||
- Stack trace maintained
|
||||
- Better debugging
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Each layer has comprehensive unit tests:
|
||||
|
||||
- **Commands**: Mock services, test flag parsing
|
||||
- **Services**: Mock API client, test business logic
|
||||
- **API Client**: Use `httptest.Server`, test HTTP handling
|
||||
- **Config**: Test file I/O with temp directories
|
||||
- **Output**: Test formatting with buffers
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Test multiple layers together:
|
||||
|
||||
- Commands + Services + Mock API
|
||||
- Config + File System
|
||||
- End-to-end workflows
|
||||
|
||||
### Coverage Goals
|
||||
|
||||
- Overall: 70%+
|
||||
- New features: 80%+
|
||||
- Critical paths: 90%+
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### CLI Configuration
|
||||
|
||||
**Location**: `~/.config/coolify/config.json` (Linux/macOS)
|
||||
**Location**: `%APPDATA%\coolify\config.json` (Windows)
|
||||
|
||||
**Structure**:
|
||||
```json
|
||||
{
|
||||
"instances": [
|
||||
{
|
||||
"name": "prod",
|
||||
"fqdn": "https://coolify.example.com",
|
||||
"token": "your-token",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"lastUpdateCheckTime": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## API Communication
|
||||
|
||||
### Base URL
|
||||
|
||||
All API calls use: `{fqdn}/api/v1/{endpoint}`
|
||||
|
||||
Example: `https://coolify.example.com/api/v1/servers`
|
||||
|
||||
### Authentication
|
||||
|
||||
Bearer token authentication:
|
||||
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### Request/Response
|
||||
|
||||
**Content-Type**: `application/json`
|
||||
|
||||
**Request Body** (POST):
|
||||
```json
|
||||
{
|
||||
"name": "my-server",
|
||||
"ip": "192.168.1.100"
|
||||
}
|
||||
```
|
||||
|
||||
**Response Body**:
|
||||
```json
|
||||
{
|
||||
"uuid": "abc123",
|
||||
"name": "my-server",
|
||||
"ip": "192.168.1.100"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
HTTP errors are converted to CLI-friendly messages:
|
||||
|
||||
- `401` → "Unauthenticated. Check your API token."
|
||||
- `404` → "Resource not found."
|
||||
- `500` → "Server error. Please try again."
|
||||
|
||||
### Retry Logic
|
||||
|
||||
Failed requests are retried with exponential backoff:
|
||||
|
||||
- Attempt 1: Immediate
|
||||
- Attempt 2: Wait 1s
|
||||
- Attempt 3: Wait 2s
|
||||
- Attempt 4: Wait 4s
|
||||
|
||||
Does not retry on 4xx errors (except 429 rate limit).
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### API Token Storage
|
||||
|
||||
- Stored in config file with restricted permissions (0600)
|
||||
- Never logged (even in debug mode)
|
||||
- Masked in output by default (use `-s` to show)
|
||||
|
||||
### Sensitive Data Handling
|
||||
|
||||
- Tokens masked as `********` in output
|
||||
- Use `--show-sensitive` flag to reveal
|
||||
- Debug logs sanitize sensitive data
|
||||
|
||||
### HTTPS
|
||||
|
||||
- All API communication uses HTTPS
|
||||
- Certificate validation enabled
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Concurrent Operations
|
||||
|
||||
Batch deployments run in parallel:
|
||||
|
||||
```go
|
||||
// Deploy multiple resources concurrently
|
||||
var wg sync.WaitGroup
|
||||
for _, name := range names {
|
||||
wg.Add(1)
|
||||
go func(n string) {
|
||||
defer wg.Done()
|
||||
deployResource(n)
|
||||
}(name)
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
### Connection Reuse
|
||||
|
||||
HTTP client reuses connections:
|
||||
|
||||
```go
|
||||
c.httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Dependencies
|
||||
|
||||
- Use Go standard library when possible
|
||||
- Only essential external dependencies
|
||||
- Keep binary size small
|
||||
|
||||
## Extensibility
|
||||
|
||||
### Adding a New Command
|
||||
|
||||
1. Create `cmd/newfeature.go`
|
||||
2. Define Cobra command
|
||||
3. Create service if needed (`internal/service/newfeature.go`)
|
||||
4. Add models if needed (`internal/models/newfeature.go`)
|
||||
5. Register command in `init()`
|
||||
6. Write tests
|
||||
|
||||
### Adding a New Output Format
|
||||
|
||||
1. Create `internal/output/newformat.go`
|
||||
2. Implement `Formatter` interface
|
||||
3. Add format constant
|
||||
4. Update `NewFormatter()` switch
|
||||
|
||||
### Adding API Client Features
|
||||
|
||||
1. Add method to `internal/api/client.go`
|
||||
2. Add tests in `internal/api/client_test.go`
|
||||
3. Use in service layer
|
||||
|
||||
## Build & Release
|
||||
|
||||
### Build Process
|
||||
|
||||
```bash
|
||||
# Local build
|
||||
go build -o coolify ./coolify
|
||||
|
||||
# Install locally
|
||||
go install ./coolify
|
||||
|
||||
# Multi-platform release
|
||||
goreleaser release --clean
|
||||
```
|
||||
|
||||
### Release Artifacts
|
||||
|
||||
- Linux: amd64, arm64
|
||||
- macOS: amd64, arm64 (Apple Silicon)
|
||||
- Windows: amd64
|
||||
|
||||
### Distribution
|
||||
|
||||
- GitHub Releases
|
||||
- Install script: `scripts/install.sh`
|
||||
- Package managers (planned)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Shell completion improvements
|
||||
- [ ] Interactive mode
|
||||
- [ ] Configuration wizard
|
||||
- [ ] Plugin system
|
||||
- [ ] Telemetry (opt-in)
|
||||
- [ ] Cache layer for frequent queries
|
||||
|
||||
## References
|
||||
|
||||
- [Cobra Documentation](https://cobra.dev/)
|
||||
- [Coolify API Specification](https://github.com/coollabsio/coolify/blob/v4.x/openapi.json)
|
||||
- [Go Project Layout](https://github.com/golang-standards/project-layout)
|
||||
@@ -0,0 +1,343 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a CLI tool for interacting with the Coolify API, built with Go using the Cobra framework. The CLI allows users to manage Coolify instances (both cloud and self-hosted), servers, projects, resources, deployments, domains, and private keys.
|
||||
|
||||
### API Specification
|
||||
This CLI is a client for the Coolify API. The API specification is defined in the OpenAPI schema:
|
||||
- **Source**: https://github.com/coollabsio/coolify/blob/v4.x/openapi.json
|
||||
- **Base Path**: `/api/v1/`
|
||||
- **Authentication**: Bearer token (API tokens from Coolify dashboard at `/security/api-tokens`)
|
||||
|
||||
All commands in this CLI are wrappers around API endpoints defined in the OpenAPI specification. When adding new features or endpoints:
|
||||
1. Check the OpenAPI spec for available endpoints and their request/response schemas
|
||||
2. Ensure the CLI command structure follows the API resource hierarchy
|
||||
3. Match the API's data types and validation rules
|
||||
|
||||
## Architecture
|
||||
|
||||
### Command Structure
|
||||
The codebase follows Cobra's command pattern with a root command and subcommands:
|
||||
- Entry point: `coolify/main.go` calls `cmd.Execute()`
|
||||
- Root command: `cmd/root.go` - contains core utilities (HTTP client, authentication, version checking, config management)
|
||||
- Subcommands: Each command is in its own file in `cmd/`:
|
||||
- `context.go` - manage Coolify context (add, remove, list, set default/token)
|
||||
- `servers.go` - list and get server information
|
||||
- `projects.go` - list projects with environments and applications
|
||||
- `resources.go` - list resources
|
||||
- `deploy.go` - deploy resources
|
||||
- `domains.go` - manage domains
|
||||
- `privatekeys.go` - manage SSH keys
|
||||
- `update.go` - self-update CLI
|
||||
- `version.go` - show CLI version
|
||||
|
||||
### Configuration Management
|
||||
- Uses Viper for configuration management
|
||||
- Config file location: `~/.config/coolify/config.json` (via xdg package)
|
||||
- Config stores multiple instances with tokens, default instance selection
|
||||
- Global flags available: `--token`, `--host`, `--format`, `--show-sensitive`, `--force`, `--debug`
|
||||
|
||||
### API Communication
|
||||
Core API functions in `cmd/root.go`:
|
||||
- `Fetch(url string)` - GET requests
|
||||
- `Post(url, input)` - POST requests
|
||||
- `Delete(url)` - DELETE requests
|
||||
All API calls use `Fqdn + "/api/v1/" + url` pattern with Bearer token authentication
|
||||
|
||||
### Version Management
|
||||
- CLI version tracking with auto-update check (10 minute interval)
|
||||
- API version checking and minimum version enforcement via `CheckMinimumVersion()`
|
||||
- Self-update capability using `go-selfupdate` library
|
||||
|
||||
### Output Formatting
|
||||
Three output modes supported via `--format` flag:
|
||||
- `table` (default) - tabwriter formatted output
|
||||
- `json` - compact JSON
|
||||
- `pretty` - indented JSON
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Build
|
||||
```bash
|
||||
go build -o coolify ./coolify
|
||||
```
|
||||
|
||||
### Run locally
|
||||
```bash
|
||||
go run ./coolify [command]
|
||||
```
|
||||
|
||||
### Test a command
|
||||
```bash
|
||||
go run ./coolify context list
|
||||
go run ./coolify servers list --debug
|
||||
```
|
||||
|
||||
### Install locally
|
||||
```bash
|
||||
go install ./coolify
|
||||
```
|
||||
|
||||
### Run tests
|
||||
```bash
|
||||
# Run all tests (tests are in internal/ directory)
|
||||
go test ./internal/...
|
||||
|
||||
# Run with coverage
|
||||
go test ./internal/... -cover
|
||||
|
||||
# Run with verbose output
|
||||
go test ./internal/... -v
|
||||
|
||||
# Run specific package
|
||||
go test ./internal/api/... -v
|
||||
go test ./internal/service/... -v
|
||||
|
||||
# Run specific test
|
||||
go test ./internal/api -run TestClient_Get_Success -v
|
||||
```
|
||||
|
||||
### Before committing
|
||||
```bash
|
||||
# 1. Run tests
|
||||
go test ./internal/...
|
||||
|
||||
# 2. Check coverage
|
||||
go test ./internal/... -cover
|
||||
|
||||
# 3. Run linter (if available)
|
||||
golangci-lint run
|
||||
|
||||
# 4. Format code
|
||||
go fmt ./...
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
- Uses GoReleaser for multi-platform builds (Linux, Darwin, Windows on amd64/arm64)
|
||||
- Release workflow: `.github/workflows/release-cli.yml` triggers on GitHub releases
|
||||
- GoReleaser config: `.goreleaser.yml`
|
||||
- Install script: `scripts/install.sh` downloads from GitHub releases
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Adding a New Command
|
||||
1. Create new file in `cmd/` (e.g., `cmd/newfeature.go`)
|
||||
2. Define command struct with cobra.Command
|
||||
3. Implement Run function with:
|
||||
- Call `CheckDefaultThings(nil)` to validate version and format
|
||||
- Use `Fetch()`, `Post()`, or `Delete()` helpers
|
||||
- Handle JSON unmarshaling into typed structs
|
||||
- Support all three output formats
|
||||
4. Register command in `init()` function: `rootCmd.AddCommand(yourCmd)`
|
||||
|
||||
### API Version Requirements
|
||||
If a command requires a specific Coolify API version, pass it to `CheckDefaultThings()`:
|
||||
```go
|
||||
minimumVersion := "4.0.0"
|
||||
CheckDefaultThings(&minimumVersion)
|
||||
```
|
||||
|
||||
### Handling Sensitive Data
|
||||
- Use `ShowSensitive` flag to control display of tokens/secrets
|
||||
- Default overlay: `SensitiveInformationOverlay = "********"`
|
||||
|
||||
### UUID vs ID Pattern
|
||||
**CRITICAL: Always use UUIDs for user-facing interactions, never internal database IDs.**
|
||||
|
||||
When adding new commands or models:
|
||||
1. **Command Arguments**: Always accept UUIDs as string arguments (e.g., `<resource_uuid>`), never integer IDs
|
||||
2. **API Endpoints**: Construct API paths using UUIDs (e.g., `resources/{uuid}`), not IDs
|
||||
3. **Service Layer**: Methods should accept `uuid string` parameters, not `id int`
|
||||
4. **Table Output**: Hide internal IDs from table output using `table:"-"` struct tags
|
||||
5. **Model Fields**:
|
||||
- Keep `ID int` field with `json:"id" table:"-"` (for API responses, hidden from users)
|
||||
- Always include `UUID string` field with `json:"uuid"` (visible to users)
|
||||
|
||||
**Example model:**
|
||||
```go
|
||||
type Resource struct {
|
||||
ID int `json:"id" table:"-"` // Hidden from table output
|
||||
UUID string `json:"uuid"` // Shown in table output
|
||||
Name string `json:"name"`
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
**Why UUIDs?**
|
||||
- UUIDs are stable across environments (dev, staging, prod)
|
||||
- IDs are internal implementation details that can change
|
||||
- UUIDs are more secure (don't expose database sequencing)
|
||||
- Coolify API uses UUIDs as the primary resource identifier
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
**CRITICAL: All code changes MUST include tests. This is non-negotiable.**
|
||||
|
||||
### Test Coverage Requirements
|
||||
- **Minimum coverage**: 70% for all packages
|
||||
- **New features**: Must have 80%+ coverage
|
||||
- **Bug fixes**: Must include regression tests
|
||||
- **Refactoring**: Must maintain or improve existing coverage
|
||||
|
||||
### Testing Structure
|
||||
```
|
||||
test/
|
||||
├── fixtures/ # Test data, mock API responses
|
||||
├── mocks/ # Mock implementations of interfaces
|
||||
└── integration/ # Integration tests with test server
|
||||
```
|
||||
|
||||
### Test Requirements by Package Type
|
||||
|
||||
#### 1. Command Tests (`cmd/*_test.go`)
|
||||
- Test command parsing and flag handling
|
||||
- Test output formatting (table, json, pretty)
|
||||
- Use mock API client to avoid real API calls
|
||||
- Test error handling and validation
|
||||
- Example:
|
||||
```go
|
||||
func TestServersListCmd(t *testing.T) {
|
||||
// Test with mock client
|
||||
// Verify output format
|
||||
// Test error cases
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. API Client Tests (`internal/api/*_test.go`)
|
||||
- Test request building
|
||||
- Test response parsing
|
||||
- Test error handling (4xx, 5xx status codes)
|
||||
- Test retry logic
|
||||
- Test timeout behavior
|
||||
- **IMPORTANT**: Use `httptest.NewServer()` for mock HTTP responses (NOT real APIs)
|
||||
- All API tests must use local mock servers, never call real Coolify cloud or external APIs
|
||||
|
||||
#### 3. Service Tests (`internal/service/*_test.go`)
|
||||
- Test business logic
|
||||
- Mock API client
|
||||
- Test complex workflows
|
||||
- Test error propagation
|
||||
|
||||
#### 4. Model Tests (`internal/models/*_test.go`)
|
||||
- Test JSON marshaling/unmarshaling
|
||||
- Test validation logic
|
||||
- Test helper methods
|
||||
|
||||
#### 5. Integration Tests (`test/integration/*_test.go`)
|
||||
- Test full command execution
|
||||
- Test with real HTTP server (httptest)
|
||||
- Test config file operations
|
||||
- Test version checking
|
||||
- Can be run with `-short` flag to skip
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests (tests are in internal/ directory)
|
||||
go test ./internal/...
|
||||
|
||||
# Run with coverage
|
||||
go test ./internal/... -cover
|
||||
|
||||
# Generate coverage report
|
||||
go test ./internal/... -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Run with verbose output
|
||||
go test ./internal/... -v
|
||||
|
||||
# Run only unit tests (skip integration)
|
||||
go test ./internal/... -short
|
||||
|
||||
# Run specific package
|
||||
go test ./internal/api/... -v
|
||||
go test ./internal/service/... -v
|
||||
```
|
||||
|
||||
### Test Guidelines
|
||||
|
||||
1. **Table-driven tests**: Use for testing multiple scenarios
|
||||
2. **Test naming**: `TestFunctionName_Scenario_ExpectedBehavior`
|
||||
3. **Subtests**: Use `t.Run()` for related test cases
|
||||
4. **Setup/Teardown**: Use `TestMain()` for package-level setup
|
||||
5. **Parallel tests**: Use `t.Parallel()` when tests are independent
|
||||
6. **Mock dependencies**: Never call real APIs in unit tests
|
||||
7. **Test fixtures**: Store mock API responses in `test/fixtures/`
|
||||
|
||||
### Example Test Structure
|
||||
|
||||
```go
|
||||
func TestServersList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
response string
|
||||
wantErr bool
|
||||
wantCount int
|
||||
}{
|
||||
{
|
||||
name: "successful list",
|
||||
response: readFixture("servers_list.json"),
|
||||
wantErr: false,
|
||||
wantCount: 3,
|
||||
},
|
||||
{
|
||||
name: "empty list",
|
||||
response: "[]",
|
||||
wantErr: false,
|
||||
wantCount: 0,
|
||||
},
|
||||
{
|
||||
name: "api error",
|
||||
response: `{"error":"unauthorized"}`,
|
||||
wantErr: true,
|
||||
wantCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test implementation
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### When Adding a New Command
|
||||
|
||||
**CHECKLIST** (must complete ALL items):
|
||||
- [ ] Create command implementation in `cmd/`
|
||||
- [ ] Create corresponding test file in `internal/service/*_test.go` or `internal/api/*_test.go`
|
||||
- [ ] Test all flags and arguments
|
||||
- [ ] Test all output formats (table, json, pretty)
|
||||
- [ ] Test error cases (missing args, API errors, invalid input)
|
||||
- [ ] Add integration test if command has complex workflow
|
||||
- [ ] Update README.md with command documentation
|
||||
- [ ] Run `go test ./internal/...` and ensure all tests pass
|
||||
- [ ] Verify coverage: `go test ./internal/... -cover`
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
Tests run automatically on:
|
||||
- Every pull request
|
||||
- Every commit to main branch
|
||||
- Before releases
|
||||
|
||||
**Pull requests will be blocked if:**
|
||||
- Any test fails
|
||||
- Coverage drops below 70%
|
||||
- New code has no tests
|
||||
|
||||
## .cursorrules Context
|
||||
|
||||
The project follows Go 1.22+ idioms with standard library preference:
|
||||
- Use `net/http` standard library (no external HTTP frameworks)
|
||||
- Leverage Go 1.22 ServeMux features for any routing needs
|
||||
- Follow RESTful patterns for API interactions
|
||||
- Implement proper error handling with custom types when needed
|
||||
- Use Go's concurrency features appropriately
|
||||
- Write secure, efficient, and maintainable code
|
||||
- **ALWAYS write tests** - see Testing Requirements section above
|
||||
+620
@@ -0,0 +1,620 @@
|
||||
# Contributing to Coolify CLI
|
||||
|
||||
Thank you for your interest in contributing to the Coolify CLI! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Project Architecture](#project-architecture)
|
||||
- [Adding a New Command](#adding-a-new-command)
|
||||
- [Testing Requirements](#testing-requirements)
|
||||
- [Code Style & Conventions](#code-style--conventions)
|
||||
- [Submitting Changes](#submitting-changes)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Before you start contributing:
|
||||
|
||||
1. **Read the [ARCHITECTURE.md](ARCHITECTURE.md)** for detailed architectural guidance
|
||||
2. **Review the [OpenAPI specification](https://github.com/coollabsio/coolify/blob/v4.x/openapi.json)** to understand available API endpoints
|
||||
3. **Check existing issues** to see if your feature/bug is already being worked on
|
||||
4. **Open an issue** to discuss your proposed changes (for large features)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.24 or higher
|
||||
- Git
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Clone and Build
|
||||
|
||||
```bash
|
||||
# Fork the repository on GitHub
|
||||
# Clone your fork
|
||||
git clone https://github.com/YOUR_USERNAME/coolify-cli.git
|
||||
cd coolify-cli
|
||||
|
||||
# Build the CLI
|
||||
go build -o coolify ./coolify
|
||||
|
||||
# Install locally
|
||||
go install
|
||||
```
|
||||
|
||||
### Running the CLI
|
||||
|
||||
```bash
|
||||
# Run without installing
|
||||
go run ./coolify [command]
|
||||
|
||||
# Example commands
|
||||
go run ./coolify context list
|
||||
go run ./coolify server list --debug
|
||||
|
||||
# With flags
|
||||
go run ./coolify server list --format json --debug
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
cmd/ # CLI commands (organized by feature)
|
||||
├── root.go # Root command and global flags
|
||||
├── application/ # Application management commands
|
||||
├── context/ # Manage Coolify instances
|
||||
├── server/ # Server management
|
||||
├── project/ # Project management
|
||||
├── database/ # Database management
|
||||
├── deployment/ # Deployment operations
|
||||
├── service/ # Service management
|
||||
└── ...
|
||||
|
||||
internal/ # Internal packages
|
||||
├── api/ # API client (HTTP communication)
|
||||
├── cli/ # CLI utilities (GetAPIClient helper)
|
||||
├── config/ # Configuration management
|
||||
├── models/ # Data models and structs
|
||||
├── output/ # Output formatters (table, json, pretty)
|
||||
├── parser/ # Input parsing utilities
|
||||
├── service/ # Business logic layer
|
||||
└── version/ # Version management
|
||||
|
||||
test/ # Test utilities and fixtures
|
||||
└── fixtures/ # Mock API response data
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
|
||||
The Coolify CLI follows a **layered architecture**:
|
||||
|
||||
```
|
||||
User → Commands (cmd/) → Services (internal/service/) → API Client (internal/api/) → Coolify API
|
||||
```
|
||||
|
||||
### Layer Responsibilities
|
||||
|
||||
1. **Command Layer** (`cmd/`)
|
||||
- Parse CLI arguments and flags
|
||||
- Call service layer methods
|
||||
- Format output using output formatters
|
||||
|
||||
2. **Service Layer** (`internal/service/`)
|
||||
- Business logic
|
||||
- Coordinate API calls
|
||||
- Transform data
|
||||
|
||||
3. **API Client Layer** (`internal/api/`)
|
||||
- HTTP communication
|
||||
- Retry logic with exponential backoff
|
||||
- Authentication (Bearer tokens)
|
||||
- Error handling
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
- **cobra**: CLI framework
|
||||
- **viper**: Configuration management
|
||||
- **stretchr/testify**: Testing assertions
|
||||
|
||||
## Adding a New Command
|
||||
|
||||
Follow these steps to add a new command:
|
||||
|
||||
### 1. Create Command Directory Structure
|
||||
|
||||
```bash
|
||||
# Create directory for your command
|
||||
mkdir -p cmd/myfeature
|
||||
```
|
||||
|
||||
### 2. Create Parent Command
|
||||
|
||||
Create `cmd/myfeature/myfeature.go`:
|
||||
|
||||
```go
|
||||
package myfeature
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// NewMyFeatureCommand creates the myfeature parent command
|
||||
func NewMyFeatureCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "myfeature",
|
||||
Aliases: []string{"mf"},
|
||||
Short: "MyFeature related commands",
|
||||
Long: `Manage MyFeature resources.`,
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
// ... more subcommands
|
||||
|
||||
return cmd
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Subcommand
|
||||
|
||||
Create `cmd/myfeature/list.go`:
|
||||
|
||||
```go
|
||||
package myfeature
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all myfeature resources",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get API client
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Use service layer
|
||||
svc := service.NewMyFeatureService(client)
|
||||
items, err := svc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list items: %w", err)
|
||||
}
|
||||
|
||||
// Format output
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(items)
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Create Service Layer
|
||||
|
||||
Create `internal/service/myfeature.go`:
|
||||
|
||||
```go
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/api"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
)
|
||||
|
||||
type MyFeatureService struct {
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewMyFeatureService(client *api.Client) *MyFeatureService {
|
||||
return &MyFeatureService{client: client}
|
||||
}
|
||||
|
||||
func (s *MyFeatureService) List(ctx context.Context) ([]models.MyFeature, error) {
|
||||
var items []models.MyFeature
|
||||
err := s.client.Get(ctx, "myfeature", &items)
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *MyFeatureService) Get(ctx context.Context, uuid string) (*models.MyFeature, error) {
|
||||
var item models.MyFeature
|
||||
err := s.client.Get(ctx, "myfeature/"+uuid, &item)
|
||||
return &item, err
|
||||
}
|
||||
|
||||
func (s *MyFeatureService) Create(ctx context.Context, req models.MyFeatureCreateRequest) (*models.Response, error) {
|
||||
var response models.Response
|
||||
err := s.client.Post(ctx, "myfeature", req, &response)
|
||||
return &response, err
|
||||
}
|
||||
|
||||
func (s *MyFeatureService) Delete(ctx context.Context, uuid string) error {
|
||||
return s.client.Delete(ctx, "myfeature/"+uuid)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Create Models
|
||||
|
||||
Create `internal/models/myfeature.go`:
|
||||
|
||||
```go
|
||||
package models
|
||||
|
||||
type MyFeature struct {
|
||||
ID int `json:"id" table:"-"` // Hidden from table output
|
||||
UUID string `json:"uuid"` // Shown to users
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
// Add more fields...
|
||||
}
|
||||
|
||||
type MyFeatureCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: Always use `UUID` for user-facing identifiers, not database `ID`. Hide `ID` field from table output using `table:"-"` tag.
|
||||
|
||||
### 6. Register Command
|
||||
|
||||
Add your command to `cmd/root.go`:
|
||||
|
||||
```go
|
||||
import (
|
||||
// ... existing imports
|
||||
"github.com/coollabsio/coolify-cli/cmd/myfeature"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// ... existing code
|
||||
rootCmd.AddCommand(myfeature.NewMyFeatureCommand())
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Create Tests
|
||||
|
||||
Create `internal/service/myfeature_test.go`:
|
||||
|
||||
```go
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/api"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMyFeatureService_List(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/myfeature", r.URL.Path)
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
|
||||
items := []models.MyFeature{
|
||||
{UUID: "uuid-1", Name: "item-1"},
|
||||
{UUID: "uuid-2", Name: "item-2"},
|
||||
}
|
||||
json.NewEncoder(w).Encode(items)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewMyFeatureService(client)
|
||||
|
||||
items, err := svc.List(cmd.Context())
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 2)
|
||||
assert.Equal(t, "uuid-1", items[0].UUID)
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Update Documentation
|
||||
|
||||
- Add command documentation to `README.md`
|
||||
- Include usage examples and flag descriptions
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
**All code changes MUST include tests.** This is non-negotiable.
|
||||
|
||||
### Coverage Requirements
|
||||
|
||||
- **Minimum coverage**: 70% for all packages
|
||||
- **New features**: 80%+ coverage required
|
||||
- **Bug fixes**: Must include regression tests
|
||||
- **Refactoring**: Must maintain or improve existing coverage
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
go test ./internal/...
|
||||
|
||||
# Run with coverage
|
||||
go test ./internal/... -cover
|
||||
|
||||
# Run specific package
|
||||
go test ./internal/service/... -v
|
||||
|
||||
# Run specific test
|
||||
go test ./internal/service -run TestServerService_List -v
|
||||
|
||||
# Generate coverage report
|
||||
go test ./internal/... -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
#### Use Table-Driven Tests
|
||||
|
||||
```go
|
||||
func TestMyFunction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "successful case",
|
||||
input: "test",
|
||||
want: "expected",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "error case",
|
||||
input: "",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := MyFunction(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("MyFunction() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("MyFunction() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Mock HTTP Requests
|
||||
|
||||
**IMPORTANT**: Never call real APIs in tests. Use `httptest.NewServer()`:
|
||||
|
||||
```go
|
||||
func TestServiceMethod(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify request
|
||||
assert.Equal(t, "/api/v1/endpoint", r.URL.Path)
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
|
||||
// Return mock response
|
||||
response := models.MyResponse{Data: "test"}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
// ... test your service
|
||||
}
|
||||
```
|
||||
|
||||
### Test Guidelines
|
||||
|
||||
- **Test naming**: `TestFunctionName_Scenario_ExpectedBehavior`
|
||||
- **Use subtests**: `t.Run()` for related test cases
|
||||
- **Use testify**: `require.NoError()` for must-pass assertions, `assert.Equal()` for comparisons
|
||||
- **Mock HTTP**: Use `httptest.NewServer()` for all API tests
|
||||
- **Test contexts**: Always pass `context.Background()` in tests
|
||||
- **Test errors**: Verify error messages and types
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
### Go Standards
|
||||
|
||||
- Follow standard Go idioms and conventions
|
||||
- Use `gofmt` for code formatting
|
||||
- Run `go vet` to catch common issues
|
||||
- Prefer standard library over external dependencies
|
||||
|
||||
### Project Conventions
|
||||
|
||||
#### API Client Usage
|
||||
|
||||
```go
|
||||
// Create client (usually done via cli.GetAPIClient())
|
||||
client := api.NewClient(baseURL, token, api.WithDebug(true))
|
||||
|
||||
// GET request
|
||||
var result MyStruct
|
||||
err := client.Get(ctx, "endpoint", &result)
|
||||
|
||||
// POST request
|
||||
err := client.Post(ctx, "endpoint", requestBody, &result)
|
||||
|
||||
// DELETE request
|
||||
err := client.Delete(ctx, "endpoint")
|
||||
|
||||
// PATCH request
|
||||
err := client.Patch(ctx, "endpoint", requestBody, &result)
|
||||
```
|
||||
|
||||
#### Service Layer Pattern
|
||||
|
||||
```go
|
||||
type MyService struct {
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewMyService(client *api.Client) *MyService {
|
||||
return &MyService{client: client}
|
||||
}
|
||||
|
||||
func (s *MyService) List(ctx context.Context) ([]models.Item, error) {
|
||||
var items []models.Item
|
||||
err := s.client.Get(ctx, "items", &items)
|
||||
return items, err
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Handling
|
||||
|
||||
```go
|
||||
// Wrap errors with context
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch data: %w", err)
|
||||
}
|
||||
|
||||
// Check and handle specific error types
|
||||
if apiErr, ok := err.(*api.Error); ok {
|
||||
if apiErr.StatusCode == 404 {
|
||||
return fmt.Errorf("resource not found")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Global Flags
|
||||
|
||||
All commands automatically inherit these global flags:
|
||||
|
||||
- `--format` (table|json|pretty) - Output format
|
||||
- `--show-sensitive` - Show sensitive information
|
||||
- `--debug` - Enable debug mode
|
||||
- `--context` - Use specific context by name
|
||||
- `--token` - Override context token
|
||||
|
||||
Access flags in commands:
|
||||
|
||||
```go
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
debug, _ := cmd.Flags().GetBool("debug")
|
||||
```
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
### Before Committing
|
||||
|
||||
```bash
|
||||
# 1. Format code
|
||||
go fmt ./...
|
||||
|
||||
# 2. Run tests
|
||||
go test ./internal/...
|
||||
|
||||
# 3. Check coverage
|
||||
go test ./internal/... -cover
|
||||
|
||||
# 4. Run vet
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Write clear, descriptive commit messages following conventional commits format:
|
||||
|
||||
```
|
||||
<type>: <short summary>
|
||||
|
||||
<detailed description>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
feat: add server domains list command
|
||||
|
||||
- Implement GET /servers/{uuid}/domains endpoint
|
||||
- Add server domains subcommand
|
||||
- Include tests for domain listing
|
||||
- Update README with new command documentation
|
||||
```
|
||||
|
||||
### Pull Requests
|
||||
|
||||
1. **Fork** the repository
|
||||
2. **Create a branch** from `v4.x`: `git checkout -b feature/my-feature v4.x`
|
||||
3. **Make your changes** with tests
|
||||
4. **Push** to your fork: `git push origin feature/my-feature`
|
||||
5. **Open a pull request** against the `v4.x` branch
|
||||
6. **Describe your changes** clearly in the PR description
|
||||
7. **Link related issues** using "Fixes #123" or "Closes #123"
|
||||
|
||||
### PR Checklist
|
||||
|
||||
- [ ] Tests pass locally (`go test ./internal/...`)
|
||||
- [ ] Code coverage meets requirements (70%+ minimum)
|
||||
- [ ] Code is formatted (`go fmt ./...`)
|
||||
- [ ] README.md updated (if adding new commands)
|
||||
- [ ] CLAUDE.md updated (if changing architecture)
|
||||
- [ ] Commit messages are descriptive
|
||||
- [ ] PR description explains the changes
|
||||
- [ ] All global flags are supported (format, show-sensitive, debug)
|
||||
- [ ] Used UUIDs (not IDs) for resource identifiers
|
||||
|
||||
## Release Process (not for contributors :) )
|
||||
|
||||
Releases are automated using GoReleaser:
|
||||
|
||||
1. Tag a new version: `git tag v1.2.3`
|
||||
2. Push the tag: `git push origin v1.2.3`
|
||||
3. Create a GitHub release
|
||||
4. GoReleaser builds binaries for all platforms automatically
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Discord**: https://coolify.io/discord
|
||||
- **Issues**: [Open an issue](https://github.com/coollabsio/coolify-cli/issues) for bugs or feature requests
|
||||
- **Architecture**: Read [ARCHITECTURE.md](ARCHITECTURE.md) for detailed design documentation
|
||||
- **API Reference**: See the [OpenAPI specification](https://github.com/coollabsio/coolify/blob/v4.x/openapi.json)
|
||||
- **Code Guidance**: See [CLAUDE.md](CLAUDE.md) for AI assistant guidance
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the same license as the project.
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to Coolify CLI! 🚀
|
||||
@@ -0,0 +1,123 @@
|
||||
# How to Release Coolify CLI
|
||||
|
||||
This guide explains the release process for the Coolify CLI.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Write access to the `coollabsio/coolify-cli` repository
|
||||
- All changes merged to the target branch (`v4.x`)
|
||||
- All tests passing (`go test ./internal/...`)
|
||||
|
||||
## Release Process
|
||||
|
||||
### 1. Create a GitHub Release
|
||||
|
||||
1. Go to https://github.com/coollabsio/coolify-cli/releases/new
|
||||
2. Click "Choose a tag" and create a new tag:
|
||||
- **Tag name**: `v1.x.x` (must start with `v`, e.g., `v1.2.3`)
|
||||
- **Target**: `v4.x` (or your target branch)
|
||||
3. **Release title**: `v1.x.x` (same as tag name)
|
||||
4. **Description**: Write release notes describing:
|
||||
- New features
|
||||
- Bug fixes
|
||||
- Breaking changes (if any)
|
||||
- Example:
|
||||
```markdown
|
||||
## What's New
|
||||
- Added support for database management
|
||||
- Improved error messages for API failures
|
||||
|
||||
## Bug Fixes
|
||||
- Fixed panic when config file is missing
|
||||
|
||||
## Breaking Changes
|
||||
- None
|
||||
```
|
||||
5. Click "Publish release"
|
||||
|
||||
### 2. Automated Build Process
|
||||
|
||||
Once you publish the release:
|
||||
|
||||
1. GitHub Actions automatically triggers the `release-cli.yml` workflow
|
||||
2. GoReleaser builds binaries for:
|
||||
- **Linux**: amd64, arm64
|
||||
- **macOS (Darwin)**: amd64, arm64
|
||||
- **Windows**: amd64, arm64
|
||||
3. Goreleaser injects the version from the tag into the binaries
|
||||
4. Binaries are automatically uploaded to the release
|
||||
5. The release becomes available at:
|
||||
- GitHub: `https://github.com/coollabsio/coolify-cli/releases/tag/v1.x.x`
|
||||
- Install script: `curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash`
|
||||
- `go install`: `go install github.com/coollabsio/coolify-cli/coolify@v1.x.x`
|
||||
|
||||
### 3. Verify the Release
|
||||
|
||||
After the workflow completes (usually 2-5 minutes):
|
||||
|
||||
1. Check the release page has all platform binaries
|
||||
2. Test the install script:
|
||||
```bash
|
||||
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
|
||||
coolify version
|
||||
```
|
||||
3. Test the auto-update functionality:
|
||||
```bash
|
||||
# If you have an older version installed
|
||||
coolify update
|
||||
coolify version # Should show the new version
|
||||
```
|
||||
4. Verify the version matches your release
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Failed
|
||||
- Check the GitHub Actions logs at https://github.com/coollabsio/coolify-cli/actions
|
||||
- Common issues:
|
||||
- Syntax errors in Go code
|
||||
- Test failures
|
||||
- GoReleaser configuration issues
|
||||
|
||||
### Version Not Updating
|
||||
- Ensure you committed the version change in `cmd/root.go`
|
||||
- The tag must start with `v` (e.g., `v1.2.3`, not `1.2.3`)
|
||||
- Check that the workflow has write permissions
|
||||
|
||||
### Install Script Not Finding New Version
|
||||
- Wait a few minutes for GitHub's CDN to update
|
||||
- Check that binaries were uploaded to the release
|
||||
- Verify the tag format is correct (`v1.x.x`)
|
||||
|
||||
## Release Checklist
|
||||
|
||||
Before creating a release:
|
||||
|
||||
- [ ] All tests pass: `go test ./internal/...`
|
||||
- [ ] Code is formatted: `go fmt ./...`
|
||||
- [ ] Version updated in `cmd/root.go`
|
||||
- [ ] Changes merged to `v4.x` branch
|
||||
- [ ] Release notes prepared
|
||||
|
||||
After creating a release:
|
||||
|
||||
- [ ] GitHub Actions workflow completed successfully
|
||||
- [ ] All platform binaries are present on the release page
|
||||
- [ ] Install script downloads the new version
|
||||
- [ ] `coolify version` returns the correct version
|
||||
|
||||
## Configuration Files
|
||||
|
||||
The release process uses these configuration files:
|
||||
|
||||
- `.goreleaser.yml` - GoReleaser configuration (build matrix, archives, etc.) - points to `/coolify` as entry point
|
||||
- `.github/workflows/release-cli.yml` - GitHub Actions workflow
|
||||
- `scripts/install.sh` - User-facing install script
|
||||
- `internal/version/checker.go` - Contains `GetVersion()` function that returns the current version
|
||||
- `coolify/main.go` - Binary entry point for `go install` support
|
||||
|
||||
## Notes
|
||||
|
||||
- The CLI has auto-update checking built-in (checks every 10 minutes)
|
||||
- Users can manually update with `coolify update`
|
||||
- Install script supports version pinning: `bash install.sh v1.2.3`
|
||||
- Releases are immutable - if you need to fix something, create a new patch version
|
||||
@@ -2,46 +2,519 @@
|
||||
|
||||
## Installation
|
||||
|
||||
### Install script (recommended)
|
||||
|
||||
#### Linux/macOS
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
It will install the CLI in `/usr/local/bin/coolify` and the configuration file in `~/.config/coolify/config.json`
|
||||
|
||||
> If you are a windows or mac user, please test the installation script and let us know if it works for you.
|
||||
#### Windows (PowerShell)
|
||||
|
||||
## Configuration
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
It will install the CLI in `%ProgramFiles%\Coolify\coolify.exe` and the configuration file in `%USERPROFILE%\.config\coolify\config.json`
|
||||
|
||||
For user installation (no admin rights required):
|
||||
```powershell
|
||||
$env:COOLIFY_USER_INSTALL=1; irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
For a specific version:
|
||||
```powershell
|
||||
$env:COOLIFY_VERSION='v1.0.0'; irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
### Using `go install`
|
||||
|
||||
```bash
|
||||
go install github.com/coollabsio/coolify-cli/coolify@latest
|
||||
```
|
||||
|
||||
This will install the `coolify` binary in your `$GOPATH/bin` directory (usually `~/go/bin`). Make sure this directory is in your `$PATH`.
|
||||
|
||||
### Using the install script
|
||||
|
||||
|
||||
## Getting Started
|
||||
1. Get a `<token>` from your Coolify dashboard (Cloud or self-hosted) at `/security/api-tokens`
|
||||
|
||||
### Cloud
|
||||
|
||||
2. Add the token with `coolify instances set token cloud <token>`
|
||||
2. Add the token with `coolify context set-token cloud <token>`
|
||||
|
||||
### Self-hosted
|
||||
|
||||
2. Add the token with `coolify instances add -d <name> <fqdn> <token>`
|
||||
|
||||
> Replace `<name>` with the name you want to give to the instance.
|
||||
2. Add the token with `coolify context add -d <context_name> <url> <token>`
|
||||
|
||||
> Replace `<context_name>` with the name you want to give to the context.
|
||||
>
|
||||
> Replace `<fqdn>` with the fully qualified domain name of your Coolify instance.
|
||||
> Replace `<url>` with the fully qualified domain name of your Coolify instance.
|
||||
|
||||
Now you can use the CLI with the token you just added.
|
||||
|
||||
## Change default instance
|
||||
You can change the default instance with `coolify instances set default <name>`
|
||||
## Change default context
|
||||
You can change the default context with `coolify context use <context_name>` or `coolify context set-default <context_name>`
|
||||
## Currently Supported Commands
|
||||
|
||||
### Update
|
||||
- `coolify update` - Update the CLI to the latest version
|
||||
|
||||
### Instances
|
||||
- `coolify instances list` - List all instances
|
||||
- `coolify instances add` - Create a new instance configuration
|
||||
- `coolify instances remove` - Remove an instance configuration
|
||||
- `coolify instances get` - Get an instance configuration
|
||||
- `coolify instances set <default>|<token>` - Set an instance as default or set a token for an instance
|
||||
- `coolify instances version` - Get the version of the Coolify API for an instance
|
||||
|
||||
### Configuration
|
||||
- `coolify config` - Show configuration file location
|
||||
|
||||
### Shell Completion
|
||||
- `coolify completion <shell>` - Generate shell completion script
|
||||
- Supported shells: `bash`, `zsh`, `fish`, `powershell`
|
||||
|
||||
### Context Management
|
||||
- `coolify context list` - List all configured contexts
|
||||
- `coolify context add <context_name> <url> <token>` - Add a new context
|
||||
- `-d, --default` - Set as default context
|
||||
- `-f, --force` - Force overwrite if context already exists
|
||||
- `coolify context delete <context_name>` - Delete a context
|
||||
- `coolify context get <context_name>` - Get details of a specific context
|
||||
- `coolify context set-token <context_name> <token>` - Update the API token for a context
|
||||
- `coolify context set-default <context_name>` - Set a context as the default
|
||||
- `coolify context update <context_name>` - Update a context's properties
|
||||
- `--name <new_name>` - Change the context name
|
||||
- `--url <new_url>` - Change the context URL
|
||||
- `--token <new_token>` - Change the context token
|
||||
- `coolify context use <context_name>` - Switch to a different context (set as default)
|
||||
- `coolify context verify` - Verify current context connection and authentication
|
||||
- `coolify context version` - Get the Coolify API version of the current context
|
||||
|
||||
### Servers
|
||||
- `coolify servers list` - List all servers
|
||||
- `coolify servers get` - Get a server
|
||||
- `--resources` - Get the resources and their status of a server
|
||||
|
||||
Commands can use `server` or `servers` interchangeably.
|
||||
|
||||
- `coolify server list` - List all servers
|
||||
- `coolify server get <uuid>` - Get a server by UUID
|
||||
- `--resources` - Get the resources and their status of a server
|
||||
- `coolify server add <name> <ip> <private_key_uuid>` - Add a new server
|
||||
- `-p, --port <port>` - SSH port (default: 22)
|
||||
- `-u, --user <user>` - SSH user (default: root)
|
||||
- `--validate` - Validate server immediately after adding
|
||||
- `coolify server remove <uuid>` - Remove a server
|
||||
- `coolify server validate <uuid>` - Validate a server connection
|
||||
- `coolify server domains <uuid>` - Get server domains by UUID
|
||||
|
||||
### Projects
|
||||
- `coolify projects list` - List all projects
|
||||
- `coolify projects get <uuid>` - Get project environments
|
||||
|
||||
### Resources
|
||||
- `coolify resources list` - List all resources
|
||||
|
||||
### Applications
|
||||
- `coolify app list` - List all applications
|
||||
- `coolify app get <uuid>` - Get application details
|
||||
- `coolify app update <uuid>` - Update application configuration
|
||||
- `--name <name>` - Application name
|
||||
- `--description <description>` - Application description
|
||||
- `--git-branch <branch>` - Git branch
|
||||
- `--git-repository <url>` - Git repository URL
|
||||
- `--domains <domains>` - Domains (comma-separated)
|
||||
- `--build-command <cmd>` - Build command
|
||||
- `--start-command <cmd>` - Start command
|
||||
- `--install-command <cmd>` - Install command
|
||||
- `--base-directory <path>` - Base directory
|
||||
- `--publish-directory <path>` - Publish directory
|
||||
- `--dockerfile <content>` - Dockerfile content
|
||||
- `--docker-image <image>` - Docker image name
|
||||
- `--docker-tag <tag>` - Docker image tag
|
||||
- `--ports-exposes <ports>` - Exposed ports
|
||||
- `--ports-mappings <mappings>` - Port mappings
|
||||
- `--health-check-enabled` - Enable health check
|
||||
- `--health-check-path <path>` - Health check path
|
||||
- `coolify app delete <uuid>` - Delete an application
|
||||
- `-f, --force` - Skip confirmation prompt
|
||||
- `coolify app start <uuid>` - Start an application
|
||||
- `coolify app stop <uuid>` - Stop an application
|
||||
- `coolify app restart <uuid>` - Restart an application
|
||||
- `coolify app logs <uuid>` - Get application logs
|
||||
|
||||
#### Application Environment Variables
|
||||
- `coolify app env list <app_uuid>` - List all environment variables
|
||||
- `coolify app env get <app_uuid> <env_uuid_or_key>` - Get a specific environment variable
|
||||
- `coolify app env create <app_uuid>` - Create a new environment variable
|
||||
- `--key <key>` - Variable key (required)
|
||||
- `--value <value>` - Variable value (required)
|
||||
- `--preview` - Available in preview deployments
|
||||
- `--build-time` - Available at build time
|
||||
- `--is-literal` - Treat value as literal (don't interpolate variables)
|
||||
- `--is-multiline` - Value is multiline
|
||||
- `coolify app env update <app_uuid> <env_uuid>` - Update an environment variable
|
||||
- `coolify app env delete <app_uuid> <env_uuid>` - Delete an environment variable
|
||||
- `coolify app env sync <app_uuid>` - Sync environment variables from a .env file
|
||||
- `--file <path>` - Path to .env file (required)
|
||||
- `--build-time` - Make all variables available at build time
|
||||
- `--preview` - Make all variables available in preview deployments
|
||||
- `--is-literal` - Treat all values as literal (don't interpolate variables)
|
||||
- **Behavior**: Updates existing variables, creates missing ones. Does NOT delete variables not in the file.
|
||||
|
||||
#### Application Deployments
|
||||
- `coolify app deployments list <app-uuid>` - List all deployments for an application
|
||||
- `coolify app deployments logs <app-uuid> [deployment-uuid]` - Get deployment logs (formatted as human-readable text)
|
||||
- If only `app-uuid` is provided: retrieves logs from the **latest/most recent deployment only**
|
||||
- If `deployment-uuid` is also provided: retrieves logs for that **specific deployment**
|
||||
- `-n, --lines <n>` - Number of log lines to display (default: 0 = all lines)
|
||||
- `-f, --follow` - Follow log output in real-time (like tail -f)
|
||||
- `--debuglogs` - Show debug logs (includes hidden commands and internal operations)
|
||||
|
||||
### Databases
|
||||
- `coolify database list` - List all databases
|
||||
- `coolify database get <uuid>` - Get database details
|
||||
- `coolify database create <type>` - Create a new database
|
||||
- Supported types: `postgresql`, `mysql`, `mariadb`, `mongodb`, `redis`, `keydb`, `clickhouse`, `dragonfly`
|
||||
- `--server-uuid <uuid>` - Server UUID (required)
|
||||
- `--project-uuid <uuid>` - Project UUID (required)
|
||||
- `--environment-name <name>` - Environment name (required unless using --environment-uuid)
|
||||
- `--environment-uuid <uuid>` - Environment UUID (required unless using --environment-name)
|
||||
- `--destination-uuid <uuid>` - Destination UUID if server has multiple destinations
|
||||
- `--name <name>` - Database name
|
||||
- `--description <description>` - Database description
|
||||
- `--image <image>` - Docker image
|
||||
- `--instant-deploy` - Deploy immediately after creation
|
||||
- `--is-public` - Make database publicly accessible
|
||||
- `--public-port <port>` - Public port number
|
||||
- `--limits-memory <size>` - Memory limit (e.g., '512m', '2g')
|
||||
- `--limits-cpus <cpus>` - CPU limit (e.g., '0.5', '2')
|
||||
- Database-specific flags (postgres-user, mysql-root-password, etc.)
|
||||
- `coolify database update <uuid>` - Update database configuration
|
||||
- `coolify database delete <uuid>` - Delete a database
|
||||
- `--delete-configurations` - Delete configurations (default: true)
|
||||
- `--delete-volumes` - Delete volumes (default: true)
|
||||
- `--docker-cleanup` - Run docker cleanup (default: true)
|
||||
- `--delete-connected-networks` - Delete connected networks (default: true)
|
||||
- `coolify database start <uuid>` - Start a database
|
||||
- `coolify database stop <uuid>` - Stop a database
|
||||
- `coolify database restart <uuid>` - Restart a database
|
||||
|
||||
#### Database Backups
|
||||
- `coolify database backup list <database_uuid>` - List all backup configurations
|
||||
- `coolify database backup create <database_uuid>` - Create a new backup configuration
|
||||
- `--frequency <cron>` - Backup frequency (cron expression)
|
||||
- `--enabled` - Enable backup schedule
|
||||
- `--save-s3` - Save backups to S3
|
||||
- `--s3-storage-uuid <uuid>` - S3 storage UUID
|
||||
- `--databases-to-backup <list>` - Comma-separated list of databases to backup
|
||||
- `--dump-all` - Dump all databases
|
||||
- `--retention-amount-local <n>` - Number of backups to retain locally
|
||||
- `--retention-days-local <n>` - Days to retain backups locally
|
||||
- `--retention-storage-local <size>` - Max storage for local backups (e.g., '1GB', '500MB')
|
||||
- `--retention-amount-s3 <n>` - Number of backups to retain in S3
|
||||
- `--retention-days-s3 <n>` - Days to retain backups in S3
|
||||
- `--retention-storage-s3 <size>` - Max storage for S3 backups (e.g., '1GB', '500MB')
|
||||
- `--timeout <seconds>` - Backup timeout in seconds
|
||||
- `--disable-local` - Disable local backup storage
|
||||
- `coolify database backup update <database_uuid> <backup_uuid>` - Update a backup configuration
|
||||
- `coolify database backup delete <database_uuid> <backup_uuid>` - Delete a backup configuration
|
||||
- `coolify database backup trigger <database_uuid> <backup_uuid>` - Trigger an immediate backup
|
||||
- `coolify database backup executions <database_uuid> <backup_uuid>` - List backup executions
|
||||
- `coolify database backup delete-execution <database_uuid> <backup_uuid> <execution_uuid>` - Delete a backup execution
|
||||
|
||||
### Services
|
||||
- `coolify service list` - List all services
|
||||
- `coolify service get <uuid>` - Get service details
|
||||
- `coolify service start <uuid>` - Start a service
|
||||
- `coolify service stop <uuid>` - Stop a service
|
||||
- `coolify service restart <uuid>` - Restart a service
|
||||
- `coolify service delete <uuid>` - Delete a service
|
||||
|
||||
#### Service Environment Variables
|
||||
- `coolify service env list <service_uuid>` - List all environment variables
|
||||
- `coolify service env get <service_uuid> <env_uuid_or_key>` - Get a specific environment variable
|
||||
- `coolify service env create <service_uuid>` - Create a new environment variable
|
||||
- Same flags as application environment variables
|
||||
- `coolify service env update <service_uuid> <env_uuid>` - Update an environment variable
|
||||
- `coolify service env delete <service_uuid> <env_uuid>` - Delete an environment variable
|
||||
- `coolify service env sync <service_uuid>` - Sync environment variables from a .env file
|
||||
- `--file <path>` - Path to .env file (required)
|
||||
- `--build-time` - Make all variables available at build time
|
||||
- `--preview` - Make all variables available in preview deployments
|
||||
- `--is-literal` - Treat all values as literal (don't interpolate variables)
|
||||
- **Behavior**: Updates existing variables, creates missing ones. Does NOT delete variables not in the file.
|
||||
|
||||
### Deployments
|
||||
- `coolify deploy uuid <uuid>` - Deploy a resource by UUID
|
||||
- `-f, --force` - Force deployment
|
||||
- `coolify deploy name <name>` - Deploy a resource by name
|
||||
- `-f, --force` - Force deployment
|
||||
- `coolify deploy batch <name1,name2,...>` - Deploy multiple resources at once
|
||||
- `-f, --force` - Force all deployments
|
||||
- `coolify deploy list` - List all deployments
|
||||
- `coolify deploy get <uuid>` - Get deployment details
|
||||
- `coolify deploy cancel <uuid>` - Cancel a deployment
|
||||
- `-f, --force` - Skip confirmation prompt
|
||||
|
||||
### GitHub Apps
|
||||
- `coolify github list` - List all GitHub App integrations
|
||||
- `coolify github get <app_uuid>` - Get GitHub App details
|
||||
- `coolify github create` - Create a new GitHub App integration
|
||||
- `--name <name>` - GitHub App name (required)
|
||||
- `--api-url <url>` - GitHub API URL (required, e.g., https://api.github.com)
|
||||
- `--html-url <url>` - GitHub HTML URL (required, e.g., https://github.com)
|
||||
- `--app-id <id>` - GitHub App ID (required)
|
||||
- `--installation-id <id>` - GitHub Installation ID (required)
|
||||
- `--client-id <id>` - GitHub OAuth Client ID (required)
|
||||
- `--client-secret <secret>` - GitHub OAuth Client Secret (required)
|
||||
- `--private-key-uuid <uuid>` - UUID of existing private key (required)
|
||||
- `--organization <org>` - GitHub organization
|
||||
- `--custom-user <user>` - Custom user for SSH (default: git)
|
||||
- `--custom-port <port>` - Custom port for SSH (default: 22)
|
||||
- `--webhook-secret <secret>` - GitHub Webhook Secret
|
||||
- `--system-wide` - Is this app system-wide (cloud only)
|
||||
- `coolify github update <app_uuid>` - Update a GitHub App
|
||||
- `coolify github delete <app_uuid>` - Delete a GitHub App
|
||||
- `-f, --force` - Skip confirmation prompt
|
||||
- `coolify github repos <app_uuid>` - List repositories accessible by a GitHub App
|
||||
- `coolify github branches <app_uuid> <owner/repo>` - List branches for a repository
|
||||
|
||||
### Teams
|
||||
- `coolify team list` - List all teams
|
||||
- `coolify team get <team_id>` - Get team details
|
||||
- `coolify team current` - Get current team
|
||||
- `coolify team members list [team_id]` - List team members
|
||||
|
||||
### Private Keys
|
||||
|
||||
Commands can use `private-key`, `private-keys`, `key`, or `keys` interchangeably.
|
||||
|
||||
- `coolify private-key list` - List all private keys
|
||||
- `coolify private-key add <key_name> <private-key>` - Add a new private key
|
||||
- Use `@filename` to read from file: `coolify private-key add mykey @~/.ssh/id_rsa`
|
||||
- `coolify private-key remove <uuid>` - Remove a private key
|
||||
|
||||
## Global Flags
|
||||
|
||||
All commands support these global flags:
|
||||
|
||||
- `--context <name>` - Use a specific context instead of default
|
||||
- `--host <fqdn>` - Override the Coolify instance hostname
|
||||
- `--token <token>` - Override the authentication token
|
||||
- `--format <format>` - Output format: `table` (default), `json`, or `pretty`
|
||||
- `-s, --show-sensitive` - Show sensitive information (tokens, IPs, etc.)
|
||||
- `-f, --force` - Force operation (skip confirmations)
|
||||
- `--debug` - Enable debug mode
|
||||
|
||||
## Examples
|
||||
|
||||
### Multi-Environment Workflows
|
||||
|
||||
```bash
|
||||
# Add multiple contexts
|
||||
coolify context add prod https://prod.coolify.io <prod-token>
|
||||
coolify context add staging https://staging.coolify.io <staging-token>
|
||||
coolify context add dev https://dev.coolify.io <dev-token>
|
||||
|
||||
# Set default
|
||||
coolify context use prod
|
||||
|
||||
# Use different contexts
|
||||
coolify --context=staging servers list
|
||||
coolify --context=prod deploy name api
|
||||
coolify --context=dev resources list
|
||||
|
||||
# Default context (prod in this case)
|
||||
coolify servers list
|
||||
```
|
||||
|
||||
### Application Management
|
||||
|
||||
```bash
|
||||
# List all applications
|
||||
coolify app list
|
||||
|
||||
# Get application details
|
||||
coolify app get <uuid>
|
||||
|
||||
# Manage application lifecycle
|
||||
coolify app start <uuid>
|
||||
coolify app stop <uuid>
|
||||
coolify app restart <uuid>
|
||||
|
||||
# View application logs
|
||||
coolify app logs <uuid>
|
||||
|
||||
# Environment variables
|
||||
coolify app env list <uuid>
|
||||
coolify app env create <uuid> --key API_KEY --value secret123
|
||||
|
||||
# Sync from .env file (updates existing, creates new, keeps others unchanged)
|
||||
coolify app env sync <uuid> --file .env
|
||||
coolify app env sync <uuid> --file .env.production --build-time --preview
|
||||
```
|
||||
|
||||
### Database Management
|
||||
|
||||
```bash
|
||||
# List databases
|
||||
coolify database list
|
||||
|
||||
# Create a PostgreSQL database
|
||||
coolify database create postgresql \
|
||||
--server-uuid <server-uuid> \
|
||||
--project-uuid <project-uuid> \
|
||||
--name mydb \
|
||||
--instant-deploy
|
||||
|
||||
# Manage database lifecycle
|
||||
coolify database start <uuid>
|
||||
coolify database stop <uuid>
|
||||
coolify database restart <uuid>
|
||||
|
||||
# Backup management
|
||||
coolify database backup list <database-uuid>
|
||||
coolify database backup create <database-uuid> \
|
||||
--frequency "0 2 * * *" \
|
||||
--enabled \
|
||||
--save-s3 \
|
||||
--retention-days-locally 7
|
||||
coolify database backup trigger <database-uuid> <backup-uuid>
|
||||
```
|
||||
|
||||
### Service Management
|
||||
|
||||
```bash
|
||||
# List services
|
||||
coolify service list
|
||||
|
||||
# Get service details
|
||||
coolify service get <uuid>
|
||||
|
||||
# Manage services
|
||||
coolify service start <uuid>
|
||||
coolify service restart <uuid>
|
||||
|
||||
# Environment variables (same as applications)
|
||||
coolify service env sync <uuid> --file .env
|
||||
```
|
||||
|
||||
### Deploy Workflows
|
||||
|
||||
```bash
|
||||
# Deploy single app by name (easier than UUID)
|
||||
coolify deploy name my-application
|
||||
|
||||
# Deploy multiple apps at once
|
||||
coolify deploy batch api,worker,frontend
|
||||
|
||||
# Force deploy with specific context
|
||||
coolify --context=prod deploy batch api,worker --force
|
||||
|
||||
# Traditional UUID deployment still works
|
||||
coolify deploy uuid abc123-def456-...
|
||||
|
||||
# Monitor deployments
|
||||
coolify deploy list
|
||||
coolify deploy get <deployment-uuid>
|
||||
|
||||
# Cancel a deployment
|
||||
coolify deploy cancel <deployment-uuid>
|
||||
```
|
||||
|
||||
### GitHub Apps Integration
|
||||
|
||||
```bash
|
||||
# List GitHub Apps
|
||||
coolify github list
|
||||
|
||||
# Create a GitHub App integration
|
||||
coolify github create \
|
||||
--name "My GitHub App" \
|
||||
--api-url "https://api.github.com" \
|
||||
--html-url "https://github.com" \
|
||||
--app-id 123456 \
|
||||
--installation-id 789012 \
|
||||
--client-id "Iv1.abc123" \
|
||||
--client-secret "secret" \
|
||||
--private-key-uuid <key-uuid>
|
||||
|
||||
# List repositories accessible by the app
|
||||
coolify github repos <app-uuid>
|
||||
|
||||
# List branches for a repository
|
||||
coolify github branches <app-uuid> owner/repo
|
||||
|
||||
# Delete a GitHub App
|
||||
coolify github delete <app-uuid>
|
||||
```
|
||||
|
||||
### Team Management
|
||||
|
||||
```bash
|
||||
# List teams
|
||||
coolify team list
|
||||
|
||||
# Get current team
|
||||
coolify team current
|
||||
|
||||
# List team members
|
||||
coolify team members list
|
||||
```
|
||||
|
||||
### Server Management
|
||||
|
||||
```bash
|
||||
# List servers in production
|
||||
coolify --context=prod server list
|
||||
|
||||
# Add a server with validation
|
||||
coolify server add myserver 192.168.1.100 <key-uuid> --validate
|
||||
|
||||
# Get server details with resources
|
||||
coolify server get <uuid> --resources
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
The CLI supports three output formats:
|
||||
|
||||
```bash
|
||||
# Table format (default, human-readable)
|
||||
coolify server list
|
||||
|
||||
# JSON format (for scripts)
|
||||
coolify server list --format=json
|
||||
|
||||
# Pretty JSON (for debugging)
|
||||
coolify server list --format=pretty
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
This CLI follows a clean architecture with:
|
||||
- **Service Layer**: Business logic and API interactions
|
||||
- **Output Layer**: Consistent formatting across all commands
|
||||
- **Config Layer**: Multi-context configuration management
|
||||
- **Models Layer**: Type-safe data structures
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build -o coolify ./coolify
|
||||
|
||||
# Run tests
|
||||
go test ./...
|
||||
|
||||
# Run with coverage
|
||||
go test -cover ./...
|
||||
|
||||
# Install locally
|
||||
go install ./coolify
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome!
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/cmd/application/env"
|
||||
)
|
||||
|
||||
// NewAppCommand creates the app parent command
|
||||
func NewAppCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "app",
|
||||
Aliases: []string{"apps", "application", "applications"},
|
||||
Short: "Application related commands",
|
||||
Long: `Manage Coolify applications - list, get, create, update, delete, and control application lifecycle.`,
|
||||
}
|
||||
|
||||
// Add main subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(NewUpdateCommand())
|
||||
cmd.AddCommand(NewDeleteCommand())
|
||||
cmd.AddCommand(NewStartCommand())
|
||||
cmd.AddCommand(NewStopCommand())
|
||||
cmd.AddCommand(NewRestartCommand())
|
||||
cmd.AddCommand(NewLogsCommand())
|
||||
cmd.AddCommand(NewDeploymentsCommand())
|
||||
|
||||
// Add env subcommand with its children
|
||||
envCmd := &cobra.Command{
|
||||
Use: "env",
|
||||
Aliases: []string{"envs", "environment"},
|
||||
Short: "Manage application environment variables",
|
||||
Long: `List and manage environment variables for applications. All commands require the application UUID first to establish context.`,
|
||||
}
|
||||
envCmd.AddCommand(env.NewListEnvCommand())
|
||||
envCmd.AddCommand(env.NewGetEnvCommand())
|
||||
envCmd.AddCommand(env.NewCreateEnvCommand())
|
||||
envCmd.AddCommand(env.NewUpdateEnvCommand())
|
||||
envCmd.AddCommand(env.NewDeleteEnvCommand())
|
||||
envCmd.AddCommand(env.NewSyncEnvCommand())
|
||||
cmd.AddCommand(envCmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewDeleteCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <uuid>",
|
||||
Short: "Delete an application",
|
||||
Long: `Delete an application. This action cannot be undone.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
if !force {
|
||||
var response string
|
||||
fmt.Printf("Are you sure you want to delete application %s? This cannot be undone. (yes/no): ", uuid)
|
||||
_, err := fmt.Scanln(&response)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read input: %w", err)
|
||||
}
|
||||
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Println("Delete cancelled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
err = appSvc.Delete(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete application: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Application %s deleted successfully.\n", uuid)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewDeploymentsCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "deployments",
|
||||
Short: "Deployment related commands for an application",
|
||||
Long: `Manage deployments for a specific application. List deployments or view deployment logs.`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(NewListDeploymentsCommand())
|
||||
cmd.AddCommand(NewLogsDeploymentsCommand())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewListDeploymentsCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list <app-uuid>",
|
||||
Short: "List all deployments for an application",
|
||||
Long: `Retrieve a list of all deployments for a specific application.`,
|
||||
Args: cli.ExactArgs(1, "<app-uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
appUUID := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
deployments, err := deploySvc.ListByApplication(ctx, appUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list deployments: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return formatter.Format(deployments)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewLogsDeploymentsCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "logs <app-uuid> [deployment-uuid]",
|
||||
Short: "Get deployment logs for an application",
|
||||
Long: `Retrieve deployment logs for a specific application or deployment.
|
||||
|
||||
If only app-uuid is provided, retrieves logs from the latest deployment.
|
||||
If deployment-uuid is also provided, retrieves logs for that specific deployment.
|
||||
|
||||
Use --follow to continuously stream new logs.`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
appUUID := args[0]
|
||||
var deploymentUUID string
|
||||
if len(args) == 2 {
|
||||
deploymentUUID = args[1]
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
lines, _ := cmd.Flags().GetInt("lines")
|
||||
follow, _ := cmd.Flags().GetBool("follow")
|
||||
debugLogs, _ := cmd.Flags().GetBool("debuglogs")
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
|
||||
// Function to get logs based on whether we have a deployment UUID
|
||||
// Returns raw or formatted based on format flag
|
||||
getLogs := func() (string, error) {
|
||||
if deploymentUUID != "" {
|
||||
return deploySvc.GetLogsByDeploymentWithFormat(ctx, deploymentUUID, debugLogs, format)
|
||||
}
|
||||
// Get logs from the latest deployment
|
||||
// Use take=1 internally to efficiently fetch only the most recent deployment
|
||||
return deploySvc.GetLogsByApplicationWithFormat(ctx, appUUID, 1, debugLogs, format)
|
||||
}
|
||||
|
||||
if !follow {
|
||||
logs, err := getLogs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get deployment logs: %w", err)
|
||||
}
|
||||
|
||||
// Apply line limit if specified (only for text output)
|
||||
if lines > 0 && format == "table" {
|
||||
logs = limitLogLines(logs, lines)
|
||||
}
|
||||
|
||||
fmt.Print(logs)
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
lastLogs := ""
|
||||
|
||||
logs, err := getLogs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get deployment logs: %w", err)
|
||||
}
|
||||
fmt.Print(logs)
|
||||
lastLogs = logs
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigChan:
|
||||
fmt.Println("\nStopping log follow...")
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
logs, err := getLogs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if logs != lastLogs {
|
||||
if len(logs) > len(lastLogs) && strings.HasPrefix(logs, lastLogs) {
|
||||
fmt.Print(logs[len(lastLogs):])
|
||||
} else {
|
||||
fmt.Print(logs)
|
||||
}
|
||||
lastLogs = logs
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntP("lines", "n", 0, "Number of log lines to display (0 = all)")
|
||||
cmd.Flags().BoolP("follow", "f", false, "Follow log output (like tail -f)")
|
||||
cmd.Flags().Bool("debuglogs", false, "Show debug logs (includes hidden commands and internal operations)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// limitLogLines limits the output to the last N lines
|
||||
func limitLogLines(logs string, n int) string {
|
||||
if n <= 0 {
|
||||
return logs
|
||||
}
|
||||
|
||||
// Trim trailing newline to avoid empty element at the end
|
||||
logs = strings.TrimRight(logs, "\n")
|
||||
lines := strings.Split(logs, "\n")
|
||||
|
||||
// If we have fewer lines than requested, return all
|
||||
if len(lines) <= n {
|
||||
return logs + "\n"
|
||||
}
|
||||
|
||||
// Get the last N lines
|
||||
lastLines := lines[len(lines)-n:]
|
||||
return strings.Join(lastLines, "\n") + "\n"
|
||||
}
|
||||
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewCreateEnvCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create <app_uuid>",
|
||||
Short: "Create an environment variable for an application",
|
||||
Long: `Create a new environment variable for a specific application. Use --key and --value flags to specify the variable.`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
value, _ := cmd.Flags().GetString("value")
|
||||
isBuildTime, _ := cmd.Flags().GetBool("build-time")
|
||||
isPreview, _ := cmd.Flags().GetBool("preview")
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
|
||||
if key == "" {
|
||||
return fmt.Errorf("--key is required")
|
||||
}
|
||||
if value == "" {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
req := &models.EnvironmentVariableCreateRequest{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("build-time") {
|
||||
req.IsBuildTime = &isBuildTime
|
||||
}
|
||||
if cmd.Flags().Changed("preview") {
|
||||
req.IsPreview = &isPreview
|
||||
}
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
if cmd.Flags().Changed("is-multiline") {
|
||||
req.IsMultiline = &isMultiline
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
env, err := appSvc.CreateEnv(ctx, appUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create environment variable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Environment variable '%s' created successfully.\n", env.Key)
|
||||
fmt.Printf("UUID: %s\n", env.UUID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("key", "", "Environment variable key (required)")
|
||||
cmd.Flags().String("value", "", "Environment variable value (required)")
|
||||
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
|
||||
cmd.Flags().Bool("preview", false, "Available in preview deployments")
|
||||
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
|
||||
return cmd
|
||||
}
|
||||
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewDeleteEnvCommand() *cobra.Command {
|
||||
deleteEnvCmd := &cobra.Command{
|
||||
Use: "delete <app_uuid> <env_uuid>",
|
||||
Short: "Delete an environment variable",
|
||||
Long: `Delete an environment variable from an application. First UUID is the application, second is the specific environment variable to delete.`,
|
||||
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
envUUID := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
// Prompt for confirmation unless --force is used
|
||||
if !force {
|
||||
var response string
|
||||
fmt.Printf("Are you sure you want to delete this environment variable? (yes/no): ")
|
||||
_, err := fmt.Scanln(&response)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read confirmation: %w", err)
|
||||
}
|
||||
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Println("Delete cancelled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
err = appSvc.DeleteEnv(ctx, appUUID, envUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete environment variable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Environment variable deleted successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
deleteEnvCmd.Flags().Bool("force", false, "Skip confirmation prompt")
|
||||
return deleteEnvCmd
|
||||
}
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewGetEnvCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "get <app_uuid> <env_uuid_or_key>",
|
||||
Short: "Get environment variable details",
|
||||
Long: `Get detailed information about a specific environment variable by UUID or key name.`,
|
||||
Args: cli.ExactArgs(2, "<app_uuid> <env_uuid_or_key>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
envUUIDOrKey := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
|
||||
// First try to get by the identifier directly
|
||||
env, err := appSvc.GetEnv(ctx, appUUID, envUUIDOrKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get environment variable: %w", err)
|
||||
}
|
||||
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
if !showSensitive {
|
||||
env.Value = "********"
|
||||
if env.RealValue != nil {
|
||||
masked := "********"
|
||||
env.RealValue = &masked
|
||||
}
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(env)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewListEnvCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list <app_uuid>",
|
||||
Short: "List all environment variables for an application",
|
||||
Long: `List all environment variables for a specific application. By default, only non-preview environment variables are shown. Use --preview to show preview environment variables instead, or --all to show all variables (non-preview first, then preview).`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
envs, err := appSvc.ListEnvs(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list environment variables: %w", err)
|
||||
}
|
||||
|
||||
// Filter by preview/all flags
|
||||
showAll, _ := cmd.Flags().GetBool("all")
|
||||
showPreview, _ := cmd.Flags().GetBool("preview")
|
||||
|
||||
if showAll {
|
||||
// Sort: non-preview first, then preview
|
||||
sort.SliceStable(envs, func(i, j int) bool {
|
||||
if envs[i].IsPreview != envs[j].IsPreview {
|
||||
return !envs[i].IsPreview // non-preview (false) comes before preview (true)
|
||||
}
|
||||
return false // maintain original order within groups
|
||||
})
|
||||
} else {
|
||||
// Filter by preview flag
|
||||
var filtered []models.EnvironmentVariable
|
||||
for _, env := range envs {
|
||||
if env.IsPreview == showPreview {
|
||||
filtered = append(filtered, env)
|
||||
}
|
||||
}
|
||||
envs = filtered
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
if !showSensitive {
|
||||
for i := range envs {
|
||||
envs[i].Value = "********"
|
||||
if envs[i].RealValue != nil {
|
||||
masked := "********"
|
||||
envs[i].RealValue = &masked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(envs)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("preview", false, "Show preview environment variables instead of regular ones")
|
||||
cmd.Flags().Bool("all", false, "Show all environment variables (non-preview first, then preview)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
Vendored
+159
@@ -0,0 +1,159 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/parser"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewSyncEnvCommand() *cobra.Command {
|
||||
syncEnvCmd := &cobra.Command{
|
||||
Use: "sync <app_uuid>",
|
||||
Short: "Sync environment variables from a .env file",
|
||||
Long: `Sync environment variables from a .env file. This command intelligently:
|
||||
- Updates existing environment variables with new values
|
||||
- Creates new environment variables that don't exist yet
|
||||
- Uses efficient bulk operations where possible
|
||||
|
||||
Example: coolify app env sync abc123 --file .env.production`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
filePath, _ := cmd.Flags().GetString("file")
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("--file is required")
|
||||
}
|
||||
|
||||
isBuildTime, _ := cmd.Flags().GetBool("build-time")
|
||||
isPreview, _ := cmd.Flags().GetBool("preview")
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
|
||||
// Parse the .env file
|
||||
envVars, err := parser.ParseEnvFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse .env file: %w", err)
|
||||
}
|
||||
|
||||
if len(envVars) == 0 {
|
||||
fmt.Println("No environment variables found in file.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d environment variables in file. Syncing...\n", len(envVars))
|
||||
|
||||
// Fetch existing environment variables
|
||||
appSvc := service.NewApplicationService(client)
|
||||
existingEnvs, err := appSvc.ListEnvs(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list existing environment variables: %w", err)
|
||||
}
|
||||
|
||||
// Build a map of existing env vars by key
|
||||
existingMap := make(map[string]models.EnvironmentVariable)
|
||||
for _, env := range existingEnvs {
|
||||
existingMap[env.Key] = env
|
||||
}
|
||||
|
||||
// Separate into updates and creates
|
||||
var toUpdate []models.EnvironmentVariableCreateRequest
|
||||
var toCreate []models.EnvironmentVariableCreateRequest
|
||||
|
||||
for _, envVar := range envVars {
|
||||
req := models.EnvironmentVariableCreateRequest{
|
||||
Key: envVar.Key,
|
||||
Value: envVar.Value,
|
||||
}
|
||||
|
||||
// Apply flags if explicitly provided
|
||||
if cmd.Flags().Changed("build-time") {
|
||||
req.IsBuildTime = &isBuildTime
|
||||
}
|
||||
if cmd.Flags().Changed("preview") {
|
||||
req.IsPreview = &isPreview
|
||||
}
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
// Auto-detect multiline values
|
||||
if strings.Contains(envVar.Value, "\n") {
|
||||
multiline := true
|
||||
req.IsMultiline = &multiline
|
||||
}
|
||||
|
||||
if _, exists := existingMap[envVar.Key]; exists {
|
||||
toUpdate = append(toUpdate, req)
|
||||
} else {
|
||||
toCreate = append(toCreate, req)
|
||||
}
|
||||
}
|
||||
|
||||
updateCount := 0
|
||||
createCount := 0
|
||||
failCount := 0
|
||||
|
||||
// Perform bulk update if there are vars to update
|
||||
if len(toUpdate) > 0 {
|
||||
fmt.Printf("Updating %d existing variables...\n", len(toUpdate))
|
||||
bulkReq := &service.BulkUpdateEnvsRequest{
|
||||
Data: toUpdate,
|
||||
}
|
||||
_, err := appSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ Bulk update failed: %v\n", err)
|
||||
failCount += len(toUpdate)
|
||||
} else {
|
||||
updateCount = len(toUpdate)
|
||||
fmt.Printf(" ✓ Successfully updated %d variables\n", updateCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new variables one by one
|
||||
if len(toCreate) > 0 {
|
||||
fmt.Printf("Creating %d new variables...\n", len(toCreate))
|
||||
for _, req := range toCreate {
|
||||
_, err := appSvc.CreateEnv(ctx, uuid, &req)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ Failed to create '%s': %v\n", req.Key, err)
|
||||
failCount++
|
||||
} else {
|
||||
fmt.Printf(" ✓ Created '%s'\n", req.Key)
|
||||
createCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nSync complete: %d updated, %d created, %d failed\n", updateCount, createCount, failCount)
|
||||
|
||||
if failCount > 0 {
|
||||
return fmt.Errorf("some environment variables failed to sync")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
syncEnvCmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
|
||||
syncEnvCmd.Flags().Bool("build-time", true, "Make all variables available at build time (default: true)")
|
||||
syncEnvCmd.Flags().Bool("preview", false, "Make all variables available in preview deployments")
|
||||
syncEnvCmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
|
||||
syncEnvCmd.Flags().Bool("runtime", true, "Make all variables available at runtime (default: true)")
|
||||
return syncEnvCmd
|
||||
}
|
||||
Vendored
+85
@@ -0,0 +1,85 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewUpdateEnvCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <app_uuid> <env_uuid>",
|
||||
Short: "Update an environment variable",
|
||||
Long: `Update an existing environment variable. First UUID is the application, second is the specific environment variable to update.`,
|
||||
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
envUUID := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
req := &models.EnvironmentVariableUpdateRequest{
|
||||
UUID: envUUID,
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("key") {
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
req.Key = &key
|
||||
}
|
||||
if cmd.Flags().Changed("value") {
|
||||
value, _ := cmd.Flags().GetString("value")
|
||||
req.Value = &value
|
||||
}
|
||||
if cmd.Flags().Changed("build-time") {
|
||||
isBuildTime, _ := cmd.Flags().GetBool("build-time")
|
||||
req.IsBuildTime = &isBuildTime
|
||||
}
|
||||
if cmd.Flags().Changed("preview") {
|
||||
isPreview, _ := cmd.Flags().GetBool("preview")
|
||||
req.IsPreview = &isPreview
|
||||
}
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
if cmd.Flags().Changed("is-multiline") {
|
||||
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
|
||||
req.IsMultiline = &isMultiline
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil && req.IsRuntime == nil {
|
||||
return fmt.Errorf("at least one field must be provided to update")
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
env, err := appSvc.UpdateEnv(ctx, appUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update environment variable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Environment variable '%s' updated successfully.\n", env.Key)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("key", "", "New environment variable key")
|
||||
cmd.Flags().String("value", "", "New environment variable value")
|
||||
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
|
||||
cmd.Flags().Bool("preview", false, "Available in preview deployments")
|
||||
cmd.Flags().Bool("is-literal", false, "Treat value as literal")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewGetCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get <uuid>",
|
||||
Short: "Get application details by UUID",
|
||||
Long: `Retrieve detailed information about a specific application.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
app, err := appSvc.Get(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get application: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(app)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all applications",
|
||||
Long: `List all applications in Coolify.`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
apps, err := appSvc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list applications: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
// For JSON/pretty formats, return the full application structure
|
||||
if format != output.FormatTable {
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return formatter.Format(apps)
|
||||
}
|
||||
|
||||
// For table format, convert to simplified rows
|
||||
var rows []models.ApplicationListItem
|
||||
for _, app := range apps {
|
||||
rows = append(rows, models.ApplicationListItem{
|
||||
UUID: app.UUID,
|
||||
Name: app.Name,
|
||||
Description: app.Description,
|
||||
Status: app.Status,
|
||||
GitBranch: app.GitBranch,
|
||||
FQDN: app.FQDN,
|
||||
})
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(rows)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewLogsCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "logs <uuid>",
|
||||
Short: "Get application logs",
|
||||
Long: `Retrieve logs for an application. Use --follow to continuously stream new logs.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
lines, _ := cmd.Flags().GetInt("lines")
|
||||
follow, _ := cmd.Flags().GetBool("follow")
|
||||
appSvc := service.NewApplicationService(client)
|
||||
|
||||
if !follow {
|
||||
resp, err := appSvc.Logs(ctx, uuid, lines)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get logs: %w", err)
|
||||
}
|
||||
fmt.Print(resp.Logs)
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
lastLogs := ""
|
||||
|
||||
resp, err := appSvc.Logs(ctx, uuid, lines)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get logs: %w", err)
|
||||
}
|
||||
fmt.Print(resp.Logs)
|
||||
lastLogs = resp.Logs
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigChan:
|
||||
fmt.Println("\nStopping log follow...")
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
resp, err := appSvc.Logs(ctx, uuid, lines)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if resp.Logs != lastLogs {
|
||||
if len(resp.Logs) > len(lastLogs) && strings.HasPrefix(resp.Logs, lastLogs) {
|
||||
fmt.Print(resp.Logs[len(lastLogs):])
|
||||
} else {
|
||||
fmt.Print(resp.Logs)
|
||||
}
|
||||
lastLogs = resp.Logs
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntP("lines", "n", 100, "Number of log lines to retrieve")
|
||||
cmd.Flags().BoolP("follow", "f", false, "Follow log output (like tail -f)")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewRestartCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "restart <uuid>",
|
||||
Short: "Restart an application",
|
||||
Long: `Restart a running application.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
resp, err := appSvc.Restart(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart application: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(resp.Message)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewStartCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "start <uuid>",
|
||||
Aliases: []string{"deploy"},
|
||||
Short: "Start an application",
|
||||
Long: `Start an application (initiates a deployment).`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
instantDeploy, _ := cmd.Flags().GetBool("instant-deploy")
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
resp, err := appSvc.Start(ctx, uuid, force, instantDeploy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start application: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(resp.Message)
|
||||
if resp.DeploymentUUID != nil && *resp.DeploymentUUID != "" {
|
||||
fmt.Printf("Deployment UUID: %s\n", *resp.DeploymentUUID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("force", false, "Force rebuild")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Instant deploy (skip queuing)")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewStopCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stop <uuid>",
|
||||
Short: "Stop an application",
|
||||
Long: `Stop a running application.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
resp, err := appSvc.Stop(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop application: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(resp.Message)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewUpdateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <uuid>",
|
||||
Short: "Update application configuration",
|
||||
Long: `Update configuration for a specific application. Only specified fields will be updated.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
req := models.ApplicationUpdateRequest{}
|
||||
hasUpdates := false
|
||||
|
||||
if cmd.Flags().Changed("name") {
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
req.Name = &name
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("description") {
|
||||
desc, _ := cmd.Flags().GetString("description")
|
||||
req.Description = &desc
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("git-branch") {
|
||||
branch, _ := cmd.Flags().GetString("git-branch")
|
||||
req.GitBranch = &branch
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("git-repository") {
|
||||
repo, _ := cmd.Flags().GetString("git-repository")
|
||||
req.GitRepository = &repo
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("domains") {
|
||||
domains, _ := cmd.Flags().GetString("domains")
|
||||
req.Domains = &domains
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("build-command") {
|
||||
buildCmd, _ := cmd.Flags().GetString("build-command")
|
||||
req.BuildCommand = &buildCmd
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("start-command") {
|
||||
startCmd, _ := cmd.Flags().GetString("start-command")
|
||||
req.StartCommand = &startCmd
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("install-command") {
|
||||
installCmd, _ := cmd.Flags().GetString("install-command")
|
||||
req.InstallCommand = &installCmd
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("base-directory") {
|
||||
baseDir, _ := cmd.Flags().GetString("base-directory")
|
||||
req.BaseDirectory = &baseDir
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("publish-directory") {
|
||||
publishDir, _ := cmd.Flags().GetString("publish-directory")
|
||||
req.PublishDirectory = &publishDir
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("dockerfile") {
|
||||
dockerfile, _ := cmd.Flags().GetString("dockerfile")
|
||||
req.Dockerfile = &dockerfile
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("docker-image") {
|
||||
image, _ := cmd.Flags().GetString("docker-image")
|
||||
req.DockerRegistryImageName = &image
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("docker-tag") {
|
||||
tag, _ := cmd.Flags().GetString("docker-tag")
|
||||
req.DockerRegistryImageTag = &tag
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("ports-exposes") {
|
||||
ports, _ := cmd.Flags().GetString("ports-exposes")
|
||||
req.PortsExposes = &ports
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("ports-mappings") {
|
||||
ports, _ := cmd.Flags().GetString("ports-mappings")
|
||||
req.PortsMappings = &ports
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("health-check-enabled") {
|
||||
enabled, _ := cmd.Flags().GetBool("health-check-enabled")
|
||||
req.HealthCheckEnabled = &enabled
|
||||
hasUpdates = true
|
||||
}
|
||||
if cmd.Flags().Changed("health-check-path") {
|
||||
path, _ := cmd.Flags().GetString("health-check-path")
|
||||
req.HealthCheckPath = &path
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
if !hasUpdates {
|
||||
return fmt.Errorf("no fields to update. Use --help to see available flags")
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
app, err := appSvc.Update(ctx, uuid, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update application: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(app)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("git-branch", "", "Git branch")
|
||||
cmd.Flags().String("git-repository", "", "Git repository URL")
|
||||
cmd.Flags().String("domains", "", "Domains (comma-separated)")
|
||||
cmd.Flags().String("build-command", "", "Build command")
|
||||
cmd.Flags().String("start-command", "", "Start command")
|
||||
cmd.Flags().String("install-command", "", "Install command")
|
||||
cmd.Flags().String("base-directory", "", "Base directory")
|
||||
cmd.Flags().String("publish-directory", "", "Publish directory")
|
||||
cmd.Flags().String("dockerfile", "", "Dockerfile content")
|
||||
cmd.Flags().String("docker-image", "", "Docker image name")
|
||||
cmd.Flags().String("docker-tag", "", "Docker image tag")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings")
|
||||
cmd.Flags().Bool("health-check-enabled", false, "Enable health check")
|
||||
cmd.Flags().String("health-check-path", "", "Health check path")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package completion
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
)
|
||||
|
||||
func NewCompletionsCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "completion <shell>",
|
||||
Short: "Output shell completion code for the specified shell",
|
||||
Long: `To load completions:
|
||||
|
||||
### Bash
|
||||
|
||||
To load completions into the current shell execute:
|
||||
|
||||
source <(coolify completion bash)
|
||||
|
||||
In order to make the completions permanent, append the line above to
|
||||
your .bashrc.
|
||||
|
||||
### Zsh
|
||||
|
||||
If shell completions are not already enabled for your environment need
|
||||
to enable them. Add the following line to your ~/.zshrc file:
|
||||
|
||||
autoload -Uz compinit; compinit
|
||||
|
||||
To load completions for each session execute the following commands:
|
||||
|
||||
mkdir -p ~/.config/coolify/completion/zsh
|
||||
coolify completion zsh > ~/.config/coolify/completion/zsh/_coolify
|
||||
|
||||
Finally add the following line to your ~/.zshrc file, *before* you
|
||||
call the compinit function:
|
||||
|
||||
fpath+=(~/.config/coolify/completion/zsh)
|
||||
|
||||
In the end your ~/.zshrc file should contain the following two lines
|
||||
in the order given here.
|
||||
|
||||
fpath+=(~/.config/coolify/completion/zsh)
|
||||
# ... anything else that needs to be done before compinit
|
||||
autoload -Uz compinit; compinit
|
||||
# ...
|
||||
|
||||
You will need to start a new shell for this setup to take effect.
|
||||
|
||||
### Fish
|
||||
|
||||
To load completions into the current shell execute:
|
||||
|
||||
coolify completion fish | source
|
||||
|
||||
In order to make the completions permanent execute once:
|
||||
|
||||
coolify completion fish > ~/.config/fish/completions/coolify.fish
|
||||
|
||||
### PowerShell:
|
||||
|
||||
To load completions into the current shell execute:
|
||||
|
||||
PS> coolify completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
To load completions for every new session, run
|
||||
and source this file from your PowerShell profile.
|
||||
|
||||
PS> coolify completion powershell > coolify.ps1
|
||||
`,
|
||||
Args: cli.ExactArgs(1, "<shell>"),
|
||||
ValidArgs: []string{"bash", "fish", "zsh", "powershell"},
|
||||
DisableFlagsInUseLine: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
err = cmd.Root().GenBashCompletion(os.Stdout)
|
||||
case "fish":
|
||||
err = cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||
case "zsh":
|
||||
err = cmd.Root().GenZshCompletion(os.Stdout)
|
||||
case "powershell":
|
||||
err = cmd.Root().GenPowerShellCompletion(os.Stdout)
|
||||
default:
|
||||
err = fmt.Errorf("Unsupported shell: %s", args[0])
|
||||
}
|
||||
return err
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/config"
|
||||
)
|
||||
|
||||
// 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(_ *cobra.Command, _ []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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/config"
|
||||
)
|
||||
|
||||
// NewAddCommand creates the add command
|
||||
func NewAddCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add <context_name> <url> <token>",
|
||||
Example: `context add myserver https://coolify.example.com your-api-token`,
|
||||
Args: cli.ExactArgs(3, "<context_name> <url> <token>"),
|
||||
Short: "Add a new context",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
host := args[1]
|
||||
token := args[2]
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
setDefault, _ := cmd.Flags().GetBool("default")
|
||||
|
||||
instances := viper.Get("instances").([]any)
|
||||
|
||||
// Check if instance already exists
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]any)
|
||||
if instanceMap["name"] == name {
|
||||
if force {
|
||||
instanceMap["token"] = token
|
||||
if setDefault {
|
||||
// Remove default from all instances
|
||||
for _, inst := range instances {
|
||||
instMap := inst.(map[string]any)
|
||||
instMap["default"] = false
|
||||
}
|
||||
instanceMap["default"] = true
|
||||
fmt.Printf("%s already exists. Force overwriting. Setting it as default.\n", name)
|
||||
} else {
|
||||
fmt.Printf("%s already exists. Force overwriting.\n", name)
|
||||
}
|
||||
viper.Set("instances", instances)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s already exists.\n", name)
|
||||
fmt.Println("\nNote: Use --force to force overwrite.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Add new instance
|
||||
newInstance := config.Instance{
|
||||
Name: name,
|
||||
FQDN: host,
|
||||
Token: token,
|
||||
Default: false,
|
||||
}
|
||||
|
||||
if setDefault {
|
||||
// Remove default from all instances
|
||||
for _, inst := range instances {
|
||||
instMap := inst.(map[string]any)
|
||||
instMap["default"] = false
|
||||
}
|
||||
newInstance.Default = true
|
||||
fmt.Printf("Context '%s' added and set as default.\n", newInstance.Name)
|
||||
} else {
|
||||
fmt.Printf("Context '%s' added successfully.\n", newInstance.Name)
|
||||
}
|
||||
|
||||
instances = append(instances, newInstance)
|
||||
|
||||
viper.Set("instances", instances)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolP("default", "d", false, "Set as default context")
|
||||
cmd.Flags().BoolP("force", "f", false, "Force overwrite if context already exists")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewContextCommand creates the context parent command
|
||||
func NewContextCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "context",
|
||||
Short: "Manage Coolify contexts",
|
||||
Long: `Manage Coolify contexts. A context contains the configuration (URL and token) for connecting to Coolify.`,
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewAddCommand())
|
||||
cmd.AddCommand(NewDeleteCommand())
|
||||
cmd.AddCommand(NewUseCommand())
|
||||
cmd.AddCommand(NewUpdateCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(NewSetTokenCommand())
|
||||
cmd.AddCommand(NewSetDefaultCommand())
|
||||
cmd.AddCommand(NewVersionCommand())
|
||||
cmd.AddCommand(NewVerifyCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
)
|
||||
|
||||
// NewDeleteCommand creates the delete command
|
||||
func NewDeleteCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delete <context_name>",
|
||||
Example: `context delete myserver`,
|
||||
Args: cli.ExactArgs(1, "<context_name>"),
|
||||
Short: "Delete a context",
|
||||
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
Name := args[0]
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
for i, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == Name {
|
||||
instances = slices.Delete(instances, i, i+1)
|
||||
viper.Set("instances", instances)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
if instanceMap["default"] == true {
|
||||
if len(instances) > 0 {
|
||||
instances[0].(map[string]interface{})["default"] = true
|
||||
viper.Set("instances", instances)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
newDefaultName := instances[0].(map[string]interface{})["name"]
|
||||
fmt.Printf("Context '%s' deleted. '%s' is now the default context.\n", Name, newDefaultName)
|
||||
} else {
|
||||
fmt.Printf("Context '%s' deleted. No contexts remaining.\n", Name)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Context '%s' deleted.\n", Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("context '%s' not found", Name)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/config"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
)
|
||||
|
||||
// NewGetCommand creates the get command
|
||||
func NewGetCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get <context_name>",
|
||||
Example: `context get myserver`,
|
||||
Args: cli.ExactArgs(1, "<context_name>"),
|
||||
Short: "Get details of a specific context",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
instancesRaw := viper.Get("instances")
|
||||
if instancesRaw == nil {
|
||||
instancesRaw = []any{}
|
||||
}
|
||||
instancesInterface := instancesRaw.([]any)
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
// Convert interface{} to config.Instance structs
|
||||
var instances []config.Instance
|
||||
for _, item := range instancesInterface {
|
||||
itemMap := item.(map[string]any)
|
||||
|
||||
instance := config.Instance{
|
||||
Name: getString(itemMap, "name"),
|
||||
FQDN: getString(itemMap, "fqdn"),
|
||||
Token: getString(itemMap, "token"),
|
||||
Default: getBool(itemMap, "default"),
|
||||
}
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
|
||||
// If a name was provided, filter to that single instance
|
||||
var results []config.Instance
|
||||
for _, inst := range instances {
|
||||
if inst.Name == name {
|
||||
results = append(results, inst)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return fmt.Errorf("Context '%s' not found", name)
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(results)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/config"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
)
|
||||
|
||||
// NewListCommand creates the list command
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all configured contexts",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
// Get instances from viper (returns []interface{})
|
||||
instancesRaw := viper.Get("instances")
|
||||
if instancesRaw == nil {
|
||||
instancesRaw = []interface{}{}
|
||||
}
|
||||
instancesInterface := instancesRaw.([]interface{})
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
// Convert interface{} to config.Instance structs
|
||||
var instances []config.Instance
|
||||
for _, item := range instancesInterface {
|
||||
itemMap := item.(map[string]any)
|
||||
|
||||
instance := config.Instance{
|
||||
Name: getString(itemMap, "name"),
|
||||
FQDN: getString(itemMap, "fqdn"),
|
||||
Token: getString(itemMap, "token"),
|
||||
Default: getBool(itemMap, "default"),
|
||||
}
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(instances)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to safely extract values from map
|
||||
func getString(m map[string]interface{}, key string) string {
|
||||
if val, ok := m[key]; ok {
|
||||
if str, ok := val.(string); ok {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getBool(m map[string]interface{}, key string) bool {
|
||||
if val, ok := m[key]; ok {
|
||||
if b, ok := val.(bool); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
)
|
||||
|
||||
// NewSetTokenCommand creates the set-token command
|
||||
func NewSetDefaultCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "set-default <context_name>",
|
||||
Example: `context set-default myserver`,
|
||||
Args: cli.ExactArgs(1, "<context_name>"),
|
||||
Short: "Set a context as the default",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
raw := viper.Get("instances")
|
||||
instances, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid instances configuration")
|
||||
}
|
||||
|
||||
// Check if instance exists
|
||||
var found bool
|
||||
for _, instance := range instances {
|
||||
instanceMap, ok := instance.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid instance configuration")
|
||||
}
|
||||
if val, ok := instanceMap["name"].(string); ok && val == name {
|
||||
found = true
|
||||
instanceMap["default"] = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("Context '%s' not found", name)
|
||||
}
|
||||
|
||||
// Only unset other defaults if we found the target instance
|
||||
for _, instance := range instances {
|
||||
instanceMap, ok := instance.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid instance configuration")
|
||||
}
|
||||
|
||||
if val, ok := instanceMap["name"].(string); ok && val != name {
|
||||
instanceMap["default"] = false
|
||||
}
|
||||
}
|
||||
|
||||
viper.Set("instances", instances)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
// Show the list after updating
|
||||
return NewListCommand().RunE(cmd, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
)
|
||||
|
||||
// NewSetTokenCommand creates the set-token command
|
||||
func NewSetTokenCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "set-token <context_name> <token>",
|
||||
Example: `context set-token myserver your-new-api-token`,
|
||||
Args: cli.ExactArgs(2, "<context_name> <token>"),
|
||||
Short: "Update the API token for a context",
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
token := args[1]
|
||||
var found interface{}
|
||||
for _, instance := range viper.Get("instances").([]interface{}) {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == name {
|
||||
found = instanceMap
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return fmt.Errorf("context '%s' not found", name)
|
||||
}
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == name {
|
||||
instanceMap["token"] = token
|
||||
}
|
||||
}
|
||||
viper.Set("instances", instances)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to update token for context '%s': %w", name, err)
|
||||
}
|
||||
fmt.Printf("Token updated for context '%s'.\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
)
|
||||
|
||||
// NewUpdateCommand creates the update command
|
||||
func NewUpdateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <context_name>",
|
||||
Example: `context update myserver --name newname --url https://new.coolify.com --token newtoken`,
|
||||
Args: cli.ExactArgs(1, "<context_name>"),
|
||||
Short: "Update a context's properties (name, URL, token)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
oldName := args[0]
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
|
||||
// Get flags
|
||||
newName, _ := cmd.Flags().GetString("name")
|
||||
newURL, _ := cmd.Flags().GetString("url")
|
||||
newToken, _ := cmd.Flags().GetString("token")
|
||||
|
||||
// Check if at least one flag is provided
|
||||
if newName == "" && newURL == "" && newToken == "" {
|
||||
return fmt.Errorf("at least one of --name, --url, or --token must be provided")
|
||||
}
|
||||
|
||||
// Find the context
|
||||
var found bool
|
||||
var contextToUpdate map[string]interface{}
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == oldName {
|
||||
found = true
|
||||
contextToUpdate = instanceMap
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("context '%s' not found", oldName)
|
||||
}
|
||||
|
||||
// If renaming, check if new name already exists
|
||||
if newName != "" && newName != oldName {
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == newName {
|
||||
return fmt.Errorf("context with name '%s' already exists", newName)
|
||||
}
|
||||
}
|
||||
contextToUpdate["name"] = newName
|
||||
}
|
||||
|
||||
// Update URL if provided
|
||||
if newURL != "" {
|
||||
contextToUpdate["fqdn"] = newURL
|
||||
}
|
||||
|
||||
// Update token if provided
|
||||
if newToken != "" {
|
||||
contextToUpdate["token"] = newToken
|
||||
}
|
||||
|
||||
// Save changes
|
||||
viper.Set("instances", instances)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
// Use the new name if renamed, otherwise use old name
|
||||
finalName := oldName
|
||||
if newName != "" {
|
||||
finalName = newName
|
||||
}
|
||||
fmt.Printf("Context '%s' updated successfully.\n", finalName)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("name", "n", "", "New name for the context")
|
||||
cmd.Flags().StringP("url", "u", "", "New URL for the context")
|
||||
cmd.Flags().StringP("token", "t", "", "New token for the context")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
)
|
||||
|
||||
// NewUseCommand creates the use command
|
||||
func NewUseCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "use <context_name>",
|
||||
Example: `context use myserver`,
|
||||
Args: cli.ExactArgs(1, "<context_name>"),
|
||||
Short: "Switch to a different context (set as default)",
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
raw := viper.Get("instances")
|
||||
|
||||
instances, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid instances configuration")
|
||||
}
|
||||
// Check if instance exists
|
||||
var found bool
|
||||
for _, instance := range instances {
|
||||
instanceMap, ok := instance.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid instance configuration")
|
||||
}
|
||||
if val, ok := instanceMap["name"].(string); ok && val == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("Context '%s' not found", name)
|
||||
}
|
||||
|
||||
// Update default
|
||||
for _, instance := range instances {
|
||||
instanceMap, ok := instance.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid instance configuration")
|
||||
}
|
||||
|
||||
if val, ok := instanceMap["name"].(string); ok && val == name {
|
||||
instanceMap["default"] = true
|
||||
} else {
|
||||
delete(instanceMap, "default")
|
||||
}
|
||||
}
|
||||
|
||||
viper.Set("instances", instances)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Switched to context '%s'.\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
)
|
||||
|
||||
// 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, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// 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,106 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/api"
|
||||
)
|
||||
|
||||
// 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, _ *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, _ *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, _ *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)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
)
|
||||
|
||||
// NewVersionCommand creates the version command for contexts
|
||||
func NewVersionCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Get current context's Coolify version",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get API client
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Get version using API client
|
||||
version, err := client.GetVersion(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get version: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewCreateCommand creates a new database
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
createBackupCmd := &cobra.Command{
|
||||
Use: "create <database_uuid>",
|
||||
Short: "Create a new scheduled backup configuration",
|
||||
Long: `Create a new scheduled backup configuration for a database. Configure frequency, retention, S3 storage, and other backup options.
|
||||
|
||||
Example: coolify database backup create abc123 --frequency "0 0 * * *" --enabled`,
|
||||
Args: cli.ExactArgs(1, "<database_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Check minimum version requirement
|
||||
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.436"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &models.DatabaseBackupCreateRequest{}
|
||||
|
||||
// Apply flags if provided
|
||||
if cmd.Flags().Changed("frequency") {
|
||||
frequency, _ := cmd.Flags().GetString("frequency")
|
||||
req.Frequency = &frequency
|
||||
}
|
||||
if cmd.Flags().Changed("enabled") {
|
||||
enabled, _ := cmd.Flags().GetBool("enabled")
|
||||
req.Enabled = &enabled
|
||||
}
|
||||
if cmd.Flags().Changed("save-s3") {
|
||||
saveS3, _ := cmd.Flags().GetBool("save-s3")
|
||||
req.SaveS3 = &saveS3
|
||||
}
|
||||
if cmd.Flags().Changed("s3-storage-uuid") {
|
||||
s3UUID, _ := cmd.Flags().GetString("s3-storage-uuid")
|
||||
req.S3StorageUUID = &s3UUID
|
||||
}
|
||||
if cmd.Flags().Changed("databases") {
|
||||
databases, _ := cmd.Flags().GetString("databases")
|
||||
req.DatabasesToBackup = &databases
|
||||
}
|
||||
if cmd.Flags().Changed("dump-all") {
|
||||
dumpAll, _ := cmd.Flags().GetBool("dump-all")
|
||||
req.DumpAll = &dumpAll
|
||||
}
|
||||
if cmd.Flags().Changed("retention-amount-locally") {
|
||||
amount, _ := cmd.Flags().GetInt("retention-amount-locally")
|
||||
req.DatabaseBackupRetentionAmountLocally = &amount
|
||||
}
|
||||
if cmd.Flags().Changed("retention-days-locally") {
|
||||
days, _ := cmd.Flags().GetInt("retention-days-locally")
|
||||
req.DatabaseBackupRetentionDaysLocally = &days
|
||||
}
|
||||
if cmd.Flags().Changed("retention-storage-locally") {
|
||||
storage, _ := cmd.Flags().GetString("retention-storage-locally")
|
||||
req.DatabaseBackupRetentionMaxStorageLocally = &storage
|
||||
}
|
||||
if cmd.Flags().Changed("retention-amount-s3") {
|
||||
amount, _ := cmd.Flags().GetInt("retention-amount-s3")
|
||||
req.DatabaseBackupRetentionAmountS3 = &amount
|
||||
}
|
||||
if cmd.Flags().Changed("retention-days-s3") {
|
||||
days, _ := cmd.Flags().GetInt("retention-days-s3")
|
||||
req.DatabaseBackupRetentionDaysS3 = &days
|
||||
}
|
||||
if cmd.Flags().Changed("retention-storage-s3") {
|
||||
storage, _ := cmd.Flags().GetString("retention-storage-s3")
|
||||
req.DatabaseBackupRetentionMaxStorageS3 = &storage
|
||||
}
|
||||
if cmd.Flags().Changed("timeout") {
|
||||
timeout, _ := cmd.Flags().GetInt("timeout")
|
||||
req.Timeout = &timeout
|
||||
}
|
||||
if cmd.Flags().Changed("disable-local") {
|
||||
disableLocal, _ := cmd.Flags().GetBool("disable-local")
|
||||
req.DisableLocalBackup = &disableLocal
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
backup, err := dbService.CreateBackup(ctx, dbUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create backup: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(backup)
|
||||
},
|
||||
}
|
||||
|
||||
createBackupCmd.Flags().String("frequency", "", "Backup frequency (cron expression, e.g., '0 0 * * *' for daily)")
|
||||
createBackupCmd.Flags().Bool("enabled", false, "Enable backup schedule")
|
||||
createBackupCmd.Flags().Bool("save-s3", false, "Save backups to S3")
|
||||
createBackupCmd.Flags().String("s3-storage-uuid", "", "S3 storage UUID")
|
||||
createBackupCmd.Flags().String("databases-to-backup", "", "Comma-separated list of databases to backup")
|
||||
createBackupCmd.Flags().Bool("dump-all", false, "Dump all databases")
|
||||
createBackupCmd.Flags().Int("retention-amount-locally", 0, "Number of backups to retain locally")
|
||||
createBackupCmd.Flags().Int("retention-days-locally", 0, "Days to retain backups locally")
|
||||
createBackupCmd.Flags().String("retention-max-storage-locally", "", "Max storage for local backups (e.g., '1GB', '500MB')")
|
||||
createBackupCmd.Flags().Int("retention-amount-s3", 0, "Number of backups to retain in S3")
|
||||
createBackupCmd.Flags().Int("retention-days-s3", 0, "Days to retain backups in S3")
|
||||
createBackupCmd.Flags().String("retention-max-storage-s3", "", "Max storage for S3 backups (e.g., '1GB', '500MB')")
|
||||
createBackupCmd.Flags().Int("timeout", 0, "Backup timeout in seconds")
|
||||
createBackupCmd.Flags().Bool("disable-local-backup", false, "Disable local backup storage")
|
||||
|
||||
return createBackupCmd
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewDeleteExecutionCommand lists all databases
|
||||
func NewDeleteExecutionCommand() *cobra.Command {
|
||||
deleteBackupExecutionCmd := &cobra.Command{
|
||||
Use: "delete-execution <database_uuid> <backup_uuid> <execution_uuid>",
|
||||
Short: "Delete backup execution",
|
||||
Long: `Delete a specific backup execution and optionally from S3. First UUID is the database, second is the backup configuration, third is the specific execution.`,
|
||||
Args: cli.ExactArgs(3, "<database_uuid> <backup_uuid> <execution_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
backupUUID := args[1]
|
||||
executionUUID := args[2]
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deleteS3, _ := cmd.Flags().GetBool("delete-s3")
|
||||
|
||||
if !force {
|
||||
fmt.Printf("Are you sure you want to delete backup execution %s? (y/N): ", executionUUID)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading input: %w", err)
|
||||
}
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response != "y" && response != "yes" {
|
||||
fmt.Println("Delete cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
err = dbService.DeleteBackupExecution(ctx, dbUUID, backupUUID, executionUUID, deleteS3)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete backup execution: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Backup execution deleted successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
deleteBackupExecutionCmd.Flags().Bool("delete-s3", false, "Delete backup file from S3")
|
||||
return deleteBackupExecutionCmd
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewDeleteCommand deletes a database
|
||||
func NewDeleteCommand() *cobra.Command {
|
||||
deleteBackupCmd := &cobra.Command{
|
||||
Use: "delete <database_uuid> <backup_uuid>",
|
||||
Short: "Delete backup configuration",
|
||||
Long: `Delete a backup configuration and optionally all its executions from S3. First UUID is the database, second is the specific backup configuration.`,
|
||||
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
backupUUID := args[1]
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deleteS3, _ := cmd.Flags().GetBool("delete-s3")
|
||||
|
||||
if !force {
|
||||
fmt.Printf("Are you sure you want to delete backup configuration %s? (y/N): ", backupUUID)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading input: %w", err)
|
||||
}
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response != "y" && response != "yes" {
|
||||
fmt.Println("Delete cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
err = dbService.DeleteBackup(ctx, dbUUID, backupUUID, deleteS3)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete backup: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Backup configuration deleted successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
deleteBackupCmd.Flags().Bool("delete-s3", false, "Delete backup files from S3")
|
||||
return deleteBackupCmd
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewExecutionCommand lists all databases
|
||||
func NewExecutionCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "executions <database_uuid> <backup_uuid>",
|
||||
Short: "List backup executions",
|
||||
Long: `List all executions for a backup configuration. First UUID is the database, second is the specific backup configuration.`,
|
||||
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
backupUUID := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
executions, err := dbService.ListBackupExecutions(ctx, dbUUID, backupUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list backup executions: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(executions)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewListCommand lists all databases
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list <database_uuid>",
|
||||
Short: "List all backup configurations for a database",
|
||||
Long: `List all backup configurations for a specific database.`,
|
||||
Args: cli.ExactArgs(1, "<database_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
backups, err := dbService.ListBackups(ctx, dbUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list backups: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(backups)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewTriggerCommand triggers a database backup
|
||||
func NewTriggerCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "trigger <database_uuid> <backup_uuid>",
|
||||
Short: "Trigger immediate backup",
|
||||
Long: `Trigger an immediate backup for a specific backup configuration. First UUID is the database, second is the specific backup configuration to trigger.`,
|
||||
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
backupUUID := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
|
||||
// Trigger immediate backup by updating with backup_now flag
|
||||
req := &models.DatabaseBackupUpdateRequest{
|
||||
BackupNow: cli.BoolPtr(true),
|
||||
}
|
||||
|
||||
err = dbService.UpdateBackup(ctx, dbUUID, backupUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to trigger backup: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Immediate backup triggered successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewUpdateCommand updates a database
|
||||
func NewUpdateCommand() *cobra.Command {
|
||||
updateBackupCmd := &cobra.Command{
|
||||
Use: "update <database_uuid> <backup_uuid>",
|
||||
Short: "Update backup configuration",
|
||||
Long: `Update a backup configuration settings (frequency, retention, S3, etc.). First UUID is the database, second is the specific backup configuration.`,
|
||||
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
backupUUID := args[1]
|
||||
|
||||
req := &models.DatabaseBackupUpdateRequest{}
|
||||
hasChanges := false
|
||||
|
||||
if cmd.Flags().Changed("enabled") {
|
||||
enabled, _ := cmd.Flags().GetBool("enabled")
|
||||
req.Enabled = &enabled
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("frequency") {
|
||||
freq, _ := cmd.Flags().GetString("frequency")
|
||||
req.Frequency = &freq
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("save-s3") {
|
||||
saveS3, _ := cmd.Flags().GetBool("save-s3")
|
||||
req.SaveS3 = &saveS3
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("s3-storage-uuid") {
|
||||
s3UUID, _ := cmd.Flags().GetString("s3-storage-uuid")
|
||||
req.S3StorageUUID = &s3UUID
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("databases-to-backup") {
|
||||
dbs, _ := cmd.Flags().GetString("databases-to-backup")
|
||||
req.DatabasesToBackup = &dbs
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("dump-all") {
|
||||
dumpAll, _ := cmd.Flags().GetBool("dump-all")
|
||||
req.DumpAll = &dumpAll
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
// Retention settings
|
||||
if cmd.Flags().Changed("retention-amount-locally") {
|
||||
amount, _ := cmd.Flags().GetInt("retention-amount-locally")
|
||||
req.DatabaseBackupRetentionAmountLocally = &amount
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("retention-days-locally") {
|
||||
days, _ := cmd.Flags().GetInt("retention-days-locally")
|
||||
req.DatabaseBackupRetentionDaysLocally = &days
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("retention-max-storage-locally") {
|
||||
storage, _ := cmd.Flags().GetInt("retention-max-storage-locally")
|
||||
req.DatabaseBackupRetentionMaxStorageLocally = &storage
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("retention-amount-s3") {
|
||||
amount, _ := cmd.Flags().GetInt("retention-amount-s3")
|
||||
req.DatabaseBackupRetentionAmountS3 = &amount
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("retention-days-s3") {
|
||||
days, _ := cmd.Flags().GetInt("retention-days-s3")
|
||||
req.DatabaseBackupRetentionDaysS3 = &days
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("retention-max-storage-s3") {
|
||||
storage, _ := cmd.Flags().GetInt("retention-max-storage-s3")
|
||||
req.DatabaseBackupRetentionMaxStorageS3 = &storage
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
if !hasChanges {
|
||||
return fmt.Errorf("no fields to update")
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
err = dbService.UpdateBackup(ctx, dbUUID, backupUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update backup: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Backup configuration updated successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
updateBackupCmd.Flags().Bool("enabled", false, "Enable or disable backup")
|
||||
updateBackupCmd.Flags().String("frequency", "", "Backup frequency (cron expression)")
|
||||
updateBackupCmd.Flags().Bool("save-s3", false, "Save backups to S3")
|
||||
updateBackupCmd.Flags().String("s3-storage-uuid", "", "S3 storage UUID")
|
||||
updateBackupCmd.Flags().String("databases-to-backup", "", "Comma-separated list of databases to backup")
|
||||
updateBackupCmd.Flags().Bool("dump-all", false, "Dump all databases")
|
||||
updateBackupCmd.Flags().Int("retention-amount-locally", 0, "Number of backups to retain locally")
|
||||
updateBackupCmd.Flags().Int("retention-days-locally", 0, "Days to retain backups locally")
|
||||
updateBackupCmd.Flags().Int("retention-max-storage-locally", 0, "Max storage for local backups (MB)")
|
||||
updateBackupCmd.Flags().Int("retention-amount-s3", 0, "Number of backups to retain in S3")
|
||||
updateBackupCmd.Flags().Int("retention-days-s3", 0, "Days to retain backups in S3")
|
||||
updateBackupCmd.Flags().Int("retention-max-storage-s3", 0, "Max storage for S3 backups (MB)")
|
||||
|
||||
return updateBackupCmd
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create <type>",
|
||||
Short: "Create a new database",
|
||||
Long: `Create a new database of the specified type.
|
||||
|
||||
Supported types: postgresql, mysql, mariadb, mongodb, redis, keydb, clickhouse, dragonfly
|
||||
|
||||
Examples:
|
||||
coolify databases create postgresql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production
|
||||
coolify databases create mysql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production --name="My MySQL"`,
|
||||
Args: cli.ExactArgs(1, "<type>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbType := args[0]
|
||||
validTypes := []string{"postgresql", "mysql", "mariadb", "mongodb", "redis", "keydb", "clickhouse", "dragonfly"}
|
||||
isValid := false
|
||||
for _, t := range validTypes {
|
||||
if t == dbType {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isValid {
|
||||
return fmt.Errorf("invalid database type '%s'. Valid types: %s", dbType, strings.Join(validTypes, ", "))
|
||||
}
|
||||
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.DatabaseCreateRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Common flags
|
||||
if cmd.Flags().Changed("name") {
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
req.Name = &name
|
||||
}
|
||||
if cmd.Flags().Changed("description") {
|
||||
desc, _ := cmd.Flags().GetString("description")
|
||||
req.Description = &desc
|
||||
}
|
||||
if cmd.Flags().Changed("image") {
|
||||
image, _ := cmd.Flags().GetString("image")
|
||||
req.Image = &image
|
||||
}
|
||||
if cmd.Flags().Changed("destination-uuid") {
|
||||
dest, _ := cmd.Flags().GetString("destination-uuid")
|
||||
req.DestinationUUID = &dest
|
||||
}
|
||||
if cmd.Flags().Changed("instant-deploy") {
|
||||
instant, _ := cmd.Flags().GetBool("instant-deploy")
|
||||
req.InstantDeploy = &instant
|
||||
}
|
||||
if cmd.Flags().Changed("is-public") {
|
||||
isPublic, _ := cmd.Flags().GetBool("is-public")
|
||||
req.IsPublic = &isPublic
|
||||
}
|
||||
if cmd.Flags().Changed("public-port") {
|
||||
port, _ := cmd.Flags().GetInt("public-port")
|
||||
req.PublicPort = &port
|
||||
}
|
||||
|
||||
// Resource limits
|
||||
if cmd.Flags().Changed("limits-memory") {
|
||||
mem, _ := cmd.Flags().GetString("limits-memory")
|
||||
req.LimitsMemory = &mem
|
||||
}
|
||||
if cmd.Flags().Changed("limits-cpus") {
|
||||
cpus, _ := cmd.Flags().GetString("limits-cpus")
|
||||
req.LimitsCpus = &cpus
|
||||
}
|
||||
|
||||
// PostgreSQL specific
|
||||
if dbType == "postgresql" {
|
||||
if cmd.Flags().Changed("postgres-user") {
|
||||
user, _ := cmd.Flags().GetString("postgres-user")
|
||||
req.PostgresUser = &user
|
||||
}
|
||||
if cmd.Flags().Changed("postgres-password") {
|
||||
pass, _ := cmd.Flags().GetString("postgres-password")
|
||||
req.PostgresPassword = &pass
|
||||
}
|
||||
if cmd.Flags().Changed("postgres-db") {
|
||||
db, _ := cmd.Flags().GetString("postgres-db")
|
||||
req.PostgresDB = &db
|
||||
}
|
||||
}
|
||||
|
||||
// MySQL specific
|
||||
if dbType == "mysql" {
|
||||
if cmd.Flags().Changed("mysql-root-password") {
|
||||
pass, _ := cmd.Flags().GetString("mysql-root-password")
|
||||
req.MysqlRootPassword = &pass
|
||||
}
|
||||
if cmd.Flags().Changed("mysql-user") {
|
||||
user, _ := cmd.Flags().GetString("mysql-user")
|
||||
req.MysqlUser = &user
|
||||
}
|
||||
if cmd.Flags().Changed("mysql-password") {
|
||||
pass, _ := cmd.Flags().GetString("mysql-password")
|
||||
req.MysqlPassword = &pass
|
||||
}
|
||||
if cmd.Flags().Changed("mysql-database") {
|
||||
db, _ := cmd.Flags().GetString("mysql-database")
|
||||
req.MysqlDatabase = &db
|
||||
}
|
||||
}
|
||||
|
||||
// MariaDB specific
|
||||
if dbType == "mariadb" {
|
||||
if cmd.Flags().Changed("mariadb-root-password") {
|
||||
pass, _ := cmd.Flags().GetString("mariadb-root-password")
|
||||
req.MariadbRootPassword = &pass
|
||||
}
|
||||
if cmd.Flags().Changed("mariadb-user") {
|
||||
user, _ := cmd.Flags().GetString("mariadb-user")
|
||||
req.MariadbUser = &user
|
||||
}
|
||||
if cmd.Flags().Changed("mariadb-password") {
|
||||
pass, _ := cmd.Flags().GetString("mariadb-password")
|
||||
req.MariadbPassword = &pass
|
||||
}
|
||||
if cmd.Flags().Changed("mariadb-database") {
|
||||
db, _ := cmd.Flags().GetString("mariadb-database")
|
||||
req.MariadbDatabase = &db
|
||||
}
|
||||
}
|
||||
|
||||
// MongoDB specific
|
||||
if dbType == "mongodb" {
|
||||
if cmd.Flags().Changed("mongo-root-username") {
|
||||
user, _ := cmd.Flags().GetString("mongo-root-username")
|
||||
req.MongoInitdbRootUsername = &user
|
||||
}
|
||||
if cmd.Flags().Changed("mongo-root-password") {
|
||||
pass, _ := cmd.Flags().GetString("mongo-root-password")
|
||||
req.MongoInitdbRootPassword = &pass
|
||||
}
|
||||
if cmd.Flags().Changed("mongo-database") {
|
||||
db, _ := cmd.Flags().GetString("mongo-database")
|
||||
req.MongoInitdbDatabase = &db
|
||||
}
|
||||
}
|
||||
|
||||
// Redis specific
|
||||
if dbType == "redis" {
|
||||
if cmd.Flags().Changed("redis-password") {
|
||||
pass, _ := cmd.Flags().GetString("redis-password")
|
||||
req.RedisPassword = &pass
|
||||
}
|
||||
}
|
||||
|
||||
// KeyDB specific
|
||||
if dbType == "keydb" {
|
||||
if cmd.Flags().Changed("keydb-password") {
|
||||
pass, _ := cmd.Flags().GetString("keydb-password")
|
||||
req.KeydbPassword = &pass
|
||||
}
|
||||
}
|
||||
|
||||
// Clickhouse specific
|
||||
if dbType == "clickhouse" {
|
||||
if cmd.Flags().Changed("clickhouse-admin-user") {
|
||||
user, _ := cmd.Flags().GetString("clickhouse-admin-user")
|
||||
req.ClickhouseAdminUser = &user
|
||||
}
|
||||
if cmd.Flags().Changed("clickhouse-admin-password") {
|
||||
pass, _ := cmd.Flags().GetString("clickhouse-admin-password")
|
||||
req.ClickhouseAdminPassword = &pass
|
||||
}
|
||||
}
|
||||
|
||||
// Dragonfly specific
|
||||
if dbType == "dragonfly" {
|
||||
if cmd.Flags().Changed("dragonfly-password") {
|
||||
pass, _ := cmd.Flags().GetString("dragonfly-password")
|
||||
req.DragonflyPassword = &pass
|
||||
}
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
database, err := dbService.Create(ctx, dbType, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create database: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(database)
|
||||
},
|
||||
}
|
||||
|
||||
// Common flags
|
||||
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
|
||||
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
|
||||
cmd.Flags().String("environment-name", "", "Environment name")
|
||||
cmd.Flags().String("environment-uuid", "", "Environment UUID")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("name", "", "Database name")
|
||||
cmd.Flags().String("description", "", "Database description")
|
||||
cmd.Flags().String("image", "", "Docker image")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().Bool("is-public", false, "Make database publicly accessible")
|
||||
cmd.Flags().Int("public-port", 0, "Public port")
|
||||
cmd.Flags().String("limits-memory", "", "Memory limit (e.g., '512m', '2g')")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit (e.g., '0.5', '2')")
|
||||
|
||||
// PostgreSQL flags
|
||||
cmd.Flags().String("postgres-user", "", "PostgreSQL user")
|
||||
cmd.Flags().String("postgres-password", "", "PostgreSQL password")
|
||||
cmd.Flags().String("postgres-db", "", "PostgreSQL database name")
|
||||
|
||||
// MySQL flags
|
||||
cmd.Flags().String("mysql-root-password", "", "MySQL root password")
|
||||
cmd.Flags().String("mysql-user", "", "MySQL user")
|
||||
cmd.Flags().String("mysql-password", "", "MySQL password")
|
||||
cmd.Flags().String("mysql-database", "", "MySQL database name")
|
||||
|
||||
// MariaDB flags
|
||||
cmd.Flags().String("mariadb-root-password", "", "MariaDB root password")
|
||||
cmd.Flags().String("mariadb-user", "", "MariaDB user")
|
||||
cmd.Flags().String("mariadb-password", "", "MariaDB password")
|
||||
cmd.Flags().String("mariadb-database", "", "MariaDB database name")
|
||||
|
||||
// MongoDB flags
|
||||
cmd.Flags().String("mongo-root-username", "", "MongoDB root username")
|
||||
cmd.Flags().String("mongo-root-password", "", "MongoDB root password")
|
||||
cmd.Flags().String("mongo-database", "", "MongoDB database name")
|
||||
|
||||
// Redis flags
|
||||
cmd.Flags().String("redis-password", "", "Redis password")
|
||||
|
||||
// KeyDB flags
|
||||
cmd.Flags().String("keydb-password", "", "KeyDB password")
|
||||
|
||||
// Clickhouse flags
|
||||
cmd.Flags().String("clickhouse-admin-user", "", "Clickhouse admin user")
|
||||
cmd.Flags().String("clickhouse-admin-password", "", "Clickhouse admin password")
|
||||
|
||||
// Dragonfly flags
|
||||
cmd.Flags().String("dragonfly-password", "", "Dragonfly password")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/cmd/database/backup"
|
||||
)
|
||||
|
||||
// NewDatabaseCommand creates the database parent command with all subcommands
|
||||
func NewDatabaseCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "database",
|
||||
Aliases: []string{"databases", "db", "dbs"},
|
||||
Short: "Manage Coolify databases",
|
||||
Long: `Manage Coolify databases (PostgreSQL, MySQL, MongoDB, Redis, MariaDB, KeyDB, Clickhouse, Dragonfly).`,
|
||||
}
|
||||
|
||||
// Add main database commands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(NewStartCommand())
|
||||
cmd.AddCommand(NewStopCommand())
|
||||
cmd.AddCommand(NewRestartCommand())
|
||||
cmd.AddCommand(NewCreateCommand())
|
||||
cmd.AddCommand(NewUpdateCommand())
|
||||
cmd.AddCommand(NewDeleteCommand())
|
||||
|
||||
// Add backup subcommand
|
||||
backupCmd := &cobra.Command{
|
||||
Use: "backup",
|
||||
Short: "Manage database backups",
|
||||
}
|
||||
backupCmd.AddCommand(backup.NewCreateCommand())
|
||||
backupCmd.AddCommand(backup.NewListCommand())
|
||||
backupCmd.AddCommand(backup.NewDeleteCommand())
|
||||
backupCmd.AddCommand(backup.NewUpdateCommand())
|
||||
backupCmd.AddCommand(backup.NewTriggerCommand())
|
||||
backupCmd.AddCommand(backup.NewExecutionCommand())
|
||||
backupCmd.AddCommand(backup.NewDeleteExecutionCommand())
|
||||
cmd.AddCommand(backupCmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewDeleteCommand deletes a database
|
||||
func NewDeleteCommand() *cobra.Command {
|
||||
deleteDatabaseCmd := &cobra.Command{
|
||||
Use: "delete <uuid>",
|
||||
Short: "Delete a database",
|
||||
Long: `Delete a database and optionally clean up its configurations, volumes, and networks.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deleteConfigurations, _ := cmd.Flags().GetBool("delete-configurations")
|
||||
deleteVolumes, _ := cmd.Flags().GetBool("delete-volumes")
|
||||
dockerCleanup, _ := cmd.Flags().GetBool("docker-cleanup")
|
||||
deleteConnectedNetworks, _ := cmd.Flags().GetBool("delete-connected-networks")
|
||||
|
||||
if !force {
|
||||
fmt.Printf("Are you sure you want to delete database %s? (y/N): ", uuid)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading input: %w", err)
|
||||
}
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response != "y" && response != "yes" {
|
||||
fmt.Println("Delete cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
err = dbService.Delete(ctx, uuid, deleteConfigurations, deleteVolumes, dockerCleanup, deleteConnectedNetworks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete database: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Database deleted successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
deleteDatabaseCmd.Flags().Bool("delete-configurations", true, "Delete configurations")
|
||||
deleteDatabaseCmd.Flags().Bool("delete-volumes", true, "Delete volumes")
|
||||
deleteDatabaseCmd.Flags().Bool("docker-cleanup", true, "Run docker cleanup")
|
||||
deleteDatabaseCmd.Flags().Bool("delete-connected-networks", true, "Delete connected networks")
|
||||
|
||||
return deleteDatabaseCmd
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewGetCommand gets database details
|
||||
func NewGetCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get <uuid>",
|
||||
Short: "Get database details",
|
||||
Long: `Get detailed information about a specific database by UUID.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
database, err := dbService.Get(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database: %w", err)
|
||||
}
|
||||
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
formatter, err := output.NewFormatter("table", output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(database)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewListCommand lists all databases
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all databases",
|
||||
Long: `List all databases in Coolify.`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
databases, err := dbService.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list databases: %w", err)
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter("table", output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(databases)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewRestartCommand restarts a database
|
||||
func NewRestartCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "restart <uuid>",
|
||||
Short: "Restart a database",
|
||||
Long: `Restart a database by UUID.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
response, err := dbService.Restart(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart database: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(response.Message)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewStartCommand starts a database
|
||||
func NewStartCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "start <uuid>",
|
||||
Short: "Start a database",
|
||||
Long: `Start a database by UUID.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
response, err := dbService.Start(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start database: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(response.Message)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewStopCommand stops a database
|
||||
func NewStopCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stop <uuid>",
|
||||
Short: "Stop a database",
|
||||
Long: `Stop a database by UUID.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
response, err := dbService.Stop(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop database: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(response.Message)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewUpdateCommand updates a database
|
||||
func NewUpdateCommand() *cobra.Command {
|
||||
updateDatabaseCmd := &cobra.Command{
|
||||
Use: "update <uuid>",
|
||||
Short: "Update a database",
|
||||
Long: `Update a database's configuration by UUID.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
req := &models.DatabaseUpdateRequest{}
|
||||
hasChanges := false
|
||||
|
||||
if cmd.Flags().Changed("name") {
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
req.Name = &name
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("description") {
|
||||
desc, _ := cmd.Flags().GetString("description")
|
||||
req.Description = &desc
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("image") {
|
||||
image, _ := cmd.Flags().GetString("image")
|
||||
req.Image = &image
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("is-public") {
|
||||
isPublic, _ := cmd.Flags().GetBool("is-public")
|
||||
req.IsPublic = &isPublic
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("public-port") {
|
||||
port, _ := cmd.Flags().GetInt("public-port")
|
||||
req.PublicPort = &port
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
// Resource limits
|
||||
if cmd.Flags().Changed("limits-memory") {
|
||||
mem, _ := cmd.Flags().GetString("limits-memory")
|
||||
req.LimitsMemory = &mem
|
||||
hasChanges = true
|
||||
}
|
||||
if cmd.Flags().Changed("limits-cpus") {
|
||||
cpus, _ := cmd.Flags().GetString("limits-cpus")
|
||||
req.LimitsCpus = &cpus
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
if !hasChanges {
|
||||
return fmt.Errorf("no fields to update")
|
||||
}
|
||||
|
||||
// Validate is-public requires public-port
|
||||
if req.IsPublic != nil && *req.IsPublic {
|
||||
// If setting to public, check if port is provided or fetch current database to check existing port
|
||||
if req.PublicPort == nil || *req.PublicPort == 0 {
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
currentDB, err := dbService.Get(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current database: %w", err)
|
||||
}
|
||||
|
||||
// Check if database already has a public port
|
||||
if currentDB.PublicPort == nil || *currentDB.PublicPort == 0 {
|
||||
return fmt.Errorf("cannot set database as public without a public port. Please provide --public-port")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbService := service.NewDatabaseService(client)
|
||||
err = dbService.Update(ctx, uuid, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update database: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Database updated successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
updateDatabaseCmd.Flags().String("name", "", "Database name")
|
||||
updateDatabaseCmd.Flags().String("description", "", "Database description")
|
||||
updateDatabaseCmd.Flags().String("image", "", "Docker image")
|
||||
updateDatabaseCmd.Flags().Bool("is-public", false, "Make database publicly accessible")
|
||||
updateDatabaseCmd.Flags().Int("public-port", 0, "Public port")
|
||||
updateDatabaseCmd.Flags().String("limits-memory", "", "Memory limit")
|
||||
updateDatabaseCmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
|
||||
return updateDatabaseCmd
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Deploy struct {
|
||||
Deployments []Deployment `json:"deployments"`
|
||||
}
|
||||
|
||||
type Deployment struct {
|
||||
Message string `json:"message"`
|
||||
ResourceUuid string `json:"resource_uuid"`
|
||||
DeploymentUuid string `json:"deployment_uuid"`
|
||||
}
|
||||
|
||||
var deployCmd = &cobra.Command{
|
||||
Use: "deploy",
|
||||
Short: "Deploy related commands",
|
||||
}
|
||||
|
||||
var deployByUuidCmd = &cobra.Command{
|
||||
Use: "uuid <uuid>",
|
||||
Short: "Deploy by uuid",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
var CsvUuids = ""
|
||||
for _, uuid := range args {
|
||||
CsvUuids += uuid + ","
|
||||
}
|
||||
CsvUuids = CsvUuids[:len(CsvUuids)-1]
|
||||
data, err := Fetch("deploy?uuid=" + CsvUuids)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata Deploy
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "Message\tResource Uuid\tDeployment Uuid")
|
||||
for _, resource := range jsondata.Deployments {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", resource.Message, resource.ResourceUuid, resource.DeploymentUuid)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
// TODO deployByTagCmd
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(deployCmd)
|
||||
deployCmd.AddCommand(deployByUuidCmd)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewBatchCommand deploys multiple resources by name
|
||||
func NewBatchCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "batch <name1,name2,...>",
|
||||
Short: "Deploy multiple resources by name",
|
||||
Long: `Deploy multiple resources at once.
|
||||
Provide resource names as comma-separated values.
|
||||
Example: coolify deploy batch app1,app2,app3`,
|
||||
Args: cli.ExactArgs(1, "<names>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
namesStr := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Parse comma-separated names
|
||||
names := make([]string, 0)
|
||||
for _, name := range strings.Split(namesStr, ",") {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
return fmt.Errorf("no resource names provided")
|
||||
}
|
||||
|
||||
// Find resources by name
|
||||
resourceSvc := service.NewResourceService(client)
|
||||
resources, err := resourceSvc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list resources: %w", err)
|
||||
}
|
||||
|
||||
// Build map of name -> UUID
|
||||
nameToUUID := make(map[string]string)
|
||||
for _, r := range resources {
|
||||
nameToUUID[r.Name] = r.UUID
|
||||
}
|
||||
|
||||
// Validate all names exist
|
||||
var notFound []string
|
||||
for _, name := range names {
|
||||
if _, exists := nameToUUID[name]; !exists {
|
||||
notFound = append(notFound, name)
|
||||
}
|
||||
}
|
||||
if len(notFound) > 0 {
|
||||
return fmt.Errorf("resources not found: %v", notFound)
|
||||
}
|
||||
|
||||
// Deploy all resources
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
|
||||
type result struct {
|
||||
Name string
|
||||
UUID string
|
||||
Success bool
|
||||
Message string
|
||||
Error string
|
||||
}
|
||||
|
||||
results := make([]result, 0, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
uuid := nameToUUID[name]
|
||||
fmt.Printf("Deploying %s...\n", name)
|
||||
|
||||
res, err := deploySvc.Deploy(ctx, uuid, force)
|
||||
if err != nil {
|
||||
results = append(results, result{
|
||||
Name: name,
|
||||
UUID: uuid,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
fmt.Printf(" ❌ Failed: %v\n", err)
|
||||
} else {
|
||||
// Get first deployment message from the array
|
||||
message := ""
|
||||
if len(res.Deployments) > 0 {
|
||||
message = res.Deployments[0].Message
|
||||
}
|
||||
results = append(results, result{
|
||||
Name: name,
|
||||
UUID: uuid,
|
||||
Success: true,
|
||||
Message: message,
|
||||
})
|
||||
fmt.Printf(" ✅ Success: %s\n", message)
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
successCount := 0
|
||||
for _, r := range results {
|
||||
if r.Success {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nBatch deployment complete: %d/%d succeeded\n", successCount, len(results))
|
||||
|
||||
if successCount < len(results) {
|
||||
return fmt.Errorf("some deployments failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("force", false, "Force deployment")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewCancelCommand cancels a deployment
|
||||
func NewCancelCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cancel <uuid>",
|
||||
Short: "Cancel a deployment by UUID",
|
||||
Long: `Cancel an in-progress deployment. This will stop the deployment process and clean up any temporary resources.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Check minimum version requirement
|
||||
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.436"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
force, err := cmd.Flags().GetBool("force")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse force flag: %w", err)
|
||||
}
|
||||
|
||||
// Prompt for confirmation unless --force is used
|
||||
if !force {
|
||||
fmt.Printf("Are you sure you want to cancel deployment %s? (yes/no): ", uuid)
|
||||
var response string
|
||||
if _, err := fmt.Scanln(&response); err != nil {
|
||||
return fmt.Errorf("failed to read confirmation: %w", err)
|
||||
}
|
||||
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Println("Cancel aborted.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
result, err := deploySvc.Cancel(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to cancel deployment: %w", err)
|
||||
}
|
||||
|
||||
format, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get format flag: %w", err)
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(result)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package deployment
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// NewDeploymentCommand creates the deployment parent command with all subcommands
|
||||
func NewDeploymentCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy",
|
||||
Short: "Deploy related commands",
|
||||
}
|
||||
|
||||
// Add all deployment subcommands
|
||||
cmd.AddCommand(NewUUIDCommand())
|
||||
cmd.AddCommand(NewNameCommand())
|
||||
cmd.AddCommand(NewBatchCommand())
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(NewCancelCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewGetCommand gets deployment details
|
||||
func NewGetCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get <uuid>",
|
||||
Short: "Get deployment details by UUID",
|
||||
Long: `Get detailed information about a specific deployment by its UUID.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
deployment, err := deploySvc.Get(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get deployment: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(deployment)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewListCommand lists all deployments
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all deployments",
|
||||
Long: `List all currently running deployments across all resources.`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
deployments, err := deploySvc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list deployments: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(deployments)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewNameCommand deploys a resource by name
|
||||
func NewNameCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "name <resource_name>",
|
||||
Short: "Deploy by resource name",
|
||||
Args: cli.ExactArgs(1, "<resource_name>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
name := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Find resource by name
|
||||
resourceSvc := service.NewResourceService(client)
|
||||
resources, err := resourceSvc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list resources: %w", err)
|
||||
}
|
||||
|
||||
var matchedUUID string
|
||||
for _, r := range resources {
|
||||
if r.Name == name {
|
||||
matchedUUID = r.UUID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchedUUID == "" {
|
||||
return fmt.Errorf("resource with name '%s' not found", name)
|
||||
}
|
||||
|
||||
// Deploy using the found UUID
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
result, err := deploySvc.Deploy(ctx, matchedUUID, force)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy resource: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// For table format, convert deployment info array to display format
|
||||
if format == output.FormatTable {
|
||||
displays := make([]ResultDisplay, len(result.Deployments))
|
||||
for i, dep := range result.Deployments {
|
||||
displays[i] = ResultDisplay{
|
||||
Message: dep.Message,
|
||||
DeploymentUUID: dep.DeploymentUUID,
|
||||
}
|
||||
}
|
||||
return formatter.Format(displays)
|
||||
}
|
||||
|
||||
return formatter.Format(result)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("force", false, "Force deployment")
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// ResultDisplay represents a deploy result for table display
|
||||
type ResultDisplay struct {
|
||||
Message string `json:"message"`
|
||||
DeploymentUUID string `json:"deployment_uuid"`
|
||||
}
|
||||
|
||||
// NewUUIDCommand deploys a resource by UUID
|
||||
func NewUUIDCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "uuid <uuid>",
|
||||
Short: "Deploy by uuid",
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
result, err := deploySvc.Deploy(ctx, uuid, force)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy resource: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// For table format, convert deployment info array to display format
|
||||
if format == output.FormatTable {
|
||||
displays := make([]ResultDisplay, len(result.Deployments))
|
||||
for i, dep := range result.Deployments {
|
||||
displays[i] = ResultDisplay{
|
||||
Message: dep.Message,
|
||||
DeploymentUUID: dep.DeploymentUUID,
|
||||
}
|
||||
}
|
||||
return formatter.Format(displays)
|
||||
}
|
||||
|
||||
return formatter.Format(result)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("force", false, "Force deployment")
|
||||
return cmd
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
var docsCmd = &cobra.Command{
|
||||
Use: "docs",
|
||||
Short: "Generate documentation",
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
var manCmd = &cobra.Command{
|
||||
Use: "man",
|
||||
Short: "Generate man pages",
|
||||
Long: `Generate man pages for all Coolify CLI commands.
|
||||
|
||||
The man pages will be written to the specified directory (default: ./man).`,
|
||||
Example: ` coolify docs man
|
||||
coolify docs man --output-dir=/usr/local/share/man/man1`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
outputDir, _ := cmd.Flags().GetString("output-dir")
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if err := os.MkdirAll(outputDir, 0750); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate man pages
|
||||
header := &doc.GenManHeader{
|
||||
Title: "COOLIFY",
|
||||
Section: "1",
|
||||
Source: "Coolify CLI",
|
||||
}
|
||||
|
||||
if err := doc.GenManTree(rootCmd, header, outputDir); err != nil {
|
||||
return fmt.Errorf("failed to generate man pages: %w", err)
|
||||
}
|
||||
|
||||
absPath, _ := filepath.Abs(outputDir)
|
||||
fmt.Printf("Man pages generated successfully in: %s\n", absPath)
|
||||
fmt.Println("\nTo install the man pages system-wide:")
|
||||
fmt.Println(" sudo cp man/*.1 /usr/local/share/man/man1/")
|
||||
fmt.Println(" sudo mandb")
|
||||
fmt.Println("\nTo view a man page:")
|
||||
fmt.Println(" man coolify")
|
||||
fmt.Println(" man coolify-servers")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var markdownCmd = &cobra.Command{
|
||||
Use: "markdown",
|
||||
Aliases: []string{"md"},
|
||||
|
||||
Short: "Generate markdown documentation",
|
||||
Long: `Generate markdown documentation for all Coolify CLI commands.
|
||||
|
||||
The markdown files will be written to the specified directory (default: ./docs).`,
|
||||
Example: ` coolify docs markdown
|
||||
coolify docs markdown --output-dir=./documentation`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
outputDir, _ := cmd.Flags().GetString("output-dir")
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if err := os.MkdirAll(outputDir, 0750); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate markdown docs
|
||||
if err := doc.GenMarkdownTree(rootCmd, outputDir); err != nil {
|
||||
return fmt.Errorf("failed to generate markdown docs: %w", err)
|
||||
}
|
||||
|
||||
absPath, _ := filepath.Abs(outputDir)
|
||||
fmt.Printf("Markdown documentation generated successfully in: %s\n", absPath)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func NewDocsCommand() *cobra.Command {
|
||||
docsCmd.AddCommand(manCmd)
|
||||
docsCmd.AddCommand(markdownCmd)
|
||||
|
||||
manCmd.Flags().StringP("output-dir", "o", "./man", "Output directory for man pages")
|
||||
markdownCmd.Flags().StringP("output-dir", "o", "./docs", "Output directory for markdown files")
|
||||
|
||||
return docsCmd
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
IP string `json:"ip"`
|
||||
Domains []string `json:"domains"`
|
||||
}
|
||||
|
||||
var domainsCmd = &cobra.Command{
|
||||
Use: "domains",
|
||||
Short: "Domain related commands",
|
||||
}
|
||||
|
||||
var listDomainsCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all domains",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
data, err := Fetch("domains")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata []Domain
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "IP Address\tDomains")
|
||||
for _, resource := range jsondata {
|
||||
for _, domain := range resource.Domains {
|
||||
fmt.Fprintf(w, "%s\t%s\n", resource.IP, domain)
|
||||
}
|
||||
|
||||
}
|
||||
w.Flush()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// rootCmd.AddCommand(domainsCmd)
|
||||
// domainsCmd.AddCommand(listDomainsCmd)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewListBranchesCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "branches <app_uuid> <owner/repo>",
|
||||
Short: "List branches for a repository",
|
||||
Long: `List all branches for a specific repository. Provide the app UUID and repository in owner/repo format.
|
||||
|
||||
Example: coolify github branches abc-123-def owner/repository`,
|
||||
Args: cli.ExactArgs(2, "<app_uuid> <owner/repo>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
|
||||
// Parse owner/repo
|
||||
ownerRepo := args[1]
|
||||
parts := cli.SplitOwnerRepo(ownerRepo)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid repository format. Expected 'owner/repo', got '%s'", ownerRepo)
|
||||
}
|
||||
owner, repo := parts[0], parts[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
svc := service.NewGitHubAppService(client)
|
||||
branches, err := svc.ListBranches(ctx, appUUID, owner, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list branches: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(branches)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
createCmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a GitHub App integration",
|
||||
Long: `Create a new GitHub App integration. This allows you to deploy private repositories from GitHub.
|
||||
|
||||
Required flags: --name, --api-url, --html-url, --app-id, --installation-id, --client-id, --client-secret, --private-key-uuid
|
||||
|
||||
Example: coolify github create --name "My GitHub App" --api-url "https://api.github.com" --html-url "https://github.com" --app-id 123456 --installation-id 789012 --client-id "Iv1.abc123" --client-secret "secret123" --private-key-uuid "abc-123-def-456"`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
apiURL, _ := cmd.Flags().GetString("api-url")
|
||||
htmlURL, _ := cmd.Flags().GetString("html-url")
|
||||
appID, _ := cmd.Flags().GetInt("app-id")
|
||||
installationID, _ := cmd.Flags().GetInt("installation-id")
|
||||
clientID, _ := cmd.Flags().GetString("client-id")
|
||||
clientSecret, _ := cmd.Flags().GetString("client-secret")
|
||||
privateKeyUUID, _ := cmd.Flags().GetString("private-key-uuid")
|
||||
|
||||
req := &models.GitHubAppCreateRequest{
|
||||
Name: name,
|
||||
APIURL: apiURL,
|
||||
HTMLURL: htmlURL,
|
||||
AppID: appID,
|
||||
InstallationID: installationID,
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
PrivateKeyUUID: privateKeyUUID,
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
if cmd.Flags().Changed("organization") {
|
||||
org, _ := cmd.Flags().GetString("organization")
|
||||
req.Organization = &org
|
||||
}
|
||||
if cmd.Flags().Changed("custom-user") {
|
||||
user, _ := cmd.Flags().GetString("custom-user")
|
||||
req.CustomUser = &user
|
||||
}
|
||||
if cmd.Flags().Changed("custom-port") {
|
||||
port, _ := cmd.Flags().GetInt("custom-port")
|
||||
req.CustomPort = &port
|
||||
}
|
||||
if cmd.Flags().Changed("webhook-secret") {
|
||||
secret, _ := cmd.Flags().GetString("webhook-secret")
|
||||
req.WebhookSecret = &secret
|
||||
}
|
||||
if cmd.Flags().Changed("system-wide") {
|
||||
systemWide, _ := cmd.Flags().GetBool("system-wide")
|
||||
req.IsSystemWide = &systemWide
|
||||
}
|
||||
|
||||
svc := service.NewGitHubAppService(client)
|
||||
app, err := svc.Create(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub App: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(app)
|
||||
},
|
||||
}
|
||||
|
||||
createCmd.Flags().String("name", "", "GitHub App name (required)")
|
||||
createCmd.Flags().String("organization", "", "GitHub organization")
|
||||
createCmd.Flags().String("api-url", "", "GitHub API URL (required, e.g., https://api.github.com)")
|
||||
createCmd.Flags().String("html-url", "", "GitHub HTML URL (required, e.g., https://github.com)")
|
||||
createCmd.Flags().String("custom-user", "", "Custom user for SSH (default: git)")
|
||||
createCmd.Flags().Int("custom-port", 0, "Custom port for SSH (default: 22)")
|
||||
createCmd.Flags().Int("app-id", 0, "GitHub App ID (required)")
|
||||
createCmd.Flags().Int("installation-id", 0, "GitHub Installation ID (required)")
|
||||
createCmd.Flags().String("client-id", "", "GitHub OAuth Client ID (required)")
|
||||
createCmd.Flags().String("client-secret", "", "GitHub OAuth Client Secret (required)")
|
||||
createCmd.Flags().String("webhook-secret", "", "GitHub Webhook Secret")
|
||||
createCmd.Flags().String("private-key-uuid", "", "UUID of existing private key (required)")
|
||||
createCmd.Flags().Bool("system-wide", false, "Is this app system-wide (cloud only)")
|
||||
|
||||
_ = createCmd.MarkFlagRequired("name")
|
||||
_ = createCmd.MarkFlagRequired("api-url")
|
||||
_ = createCmd.MarkFlagRequired("html-url")
|
||||
_ = createCmd.MarkFlagRequired("app-id")
|
||||
_ = createCmd.MarkFlagRequired("installation-id")
|
||||
_ = createCmd.MarkFlagRequired("client-id")
|
||||
_ = createCmd.MarkFlagRequired("client-secret")
|
||||
_ = createCmd.MarkFlagRequired("private-key-uuid")
|
||||
|
||||
return createCmd
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewDeleteCommand() *cobra.Command {
|
||||
deleteCmd := &cobra.Command{
|
||||
Use: "delete <app_uuid>",
|
||||
Short: "Delete a GitHub App integration",
|
||||
Long: `Delete a GitHub App integration. The app must not be used by any applications.`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
// Prompt for confirmation unless --force is used
|
||||
if !force {
|
||||
var response string
|
||||
fmt.Printf("Are you sure you want to delete GitHub App %s? This cannot be undone. (yes/no): ", appUUID)
|
||||
_, err := fmt.Scanln(&response)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read confirmation: %w", err)
|
||||
}
|
||||
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Println("Delete cancelled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
svc := service.NewGitHubAppService(client)
|
||||
err = svc.Delete(ctx, appUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete GitHub App: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("GitHub App deleted successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
deleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
|
||||
|
||||
return deleteCmd
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewGetCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get <app_uuid>",
|
||||
Short: "Get GitHub App details by UUID",
|
||||
Long: `Get detailed information about a specific GitHub App integration.`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
svc := service.NewGitHubAppService(client)
|
||||
app, err := svc.Get(ctx, appUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get GitHub App: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(app)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewGitHubCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "github",
|
||||
Aliases: []string{"gh", "github-app", "github-apps"},
|
||||
Short: "Manage GitHub App integrations",
|
||||
Long: `Manage GitHub App integrations for private repository deployments.`,
|
||||
}
|
||||
|
||||
// Add main database commands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(NewCreateCommand())
|
||||
cmd.AddCommand(NewUpdateCommand())
|
||||
cmd.AddCommand(NewDeleteCommand())
|
||||
cmd.AddCommand(NewListRepositoriesCommand())
|
||||
cmd.AddCommand(NewListBranchesCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all GitHub App integrations",
|
||||
Long: `List all GitHub App integrations configured in Coolify.`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Check minimum version requirement
|
||||
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.436"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc := service.NewGitHubAppService(client)
|
||||
apps, err := svc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list GitHub Apps: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(apps)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewListRepositoriesCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "repos <app_uuid>",
|
||||
Short: "List repositories accessible by a GitHub App",
|
||||
Long: `List all repositories that are accessible by the specified GitHub App.`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
svc := service.NewGitHubAppService(client)
|
||||
repos, err := svc.ListRepositories(ctx, appUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list repositories: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(repos)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewUpdateCommand() *cobra.Command {
|
||||
updateCmd := &cobra.Command{
|
||||
Use: "update <app_uuid>",
|
||||
Short: "Update a GitHub App integration",
|
||||
Long: `Update an existing GitHub App integration. Provide the app UUID and the fields you want to update.`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
req := &models.GitHubAppUpdateRequest{}
|
||||
|
||||
// Update only fields that were explicitly provided
|
||||
if cmd.Flags().Changed("name") {
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
req.Name = &name
|
||||
}
|
||||
if cmd.Flags().Changed("organization") {
|
||||
org, _ := cmd.Flags().GetString("organization")
|
||||
req.Organization = &org
|
||||
}
|
||||
if cmd.Flags().Changed("api-url") {
|
||||
apiURL, _ := cmd.Flags().GetString("api-url")
|
||||
req.APIURL = &apiURL
|
||||
}
|
||||
if cmd.Flags().Changed("html-url") {
|
||||
htmlURL, _ := cmd.Flags().GetString("html-url")
|
||||
req.HTMLURL = &htmlURL
|
||||
}
|
||||
if cmd.Flags().Changed("custom-user") {
|
||||
user, _ := cmd.Flags().GetString("custom-user")
|
||||
req.CustomUser = &user
|
||||
}
|
||||
if cmd.Flags().Changed("custom-port") {
|
||||
port, _ := cmd.Flags().GetInt("custom-port")
|
||||
req.CustomPort = &port
|
||||
}
|
||||
if cmd.Flags().Changed("app-id") {
|
||||
id, _ := cmd.Flags().GetInt("app-id")
|
||||
req.AppID = &id
|
||||
}
|
||||
if cmd.Flags().Changed("installation-id") {
|
||||
id, _ := cmd.Flags().GetInt("installation-id")
|
||||
req.InstallationID = &id
|
||||
}
|
||||
if cmd.Flags().Changed("client-id") {
|
||||
clientID, _ := cmd.Flags().GetString("client-id")
|
||||
req.ClientID = &clientID
|
||||
}
|
||||
if cmd.Flags().Changed("client-secret") {
|
||||
clientSecret, _ := cmd.Flags().GetString("client-secret")
|
||||
req.ClientSecret = &clientSecret
|
||||
}
|
||||
if cmd.Flags().Changed("webhook-secret") {
|
||||
secret, _ := cmd.Flags().GetString("webhook-secret")
|
||||
req.WebhookSecret = &secret
|
||||
}
|
||||
if cmd.Flags().Changed("private-key-uuid") {
|
||||
uuid, _ := cmd.Flags().GetString("private-key-uuid")
|
||||
req.PrivateKeyUUID = &uuid
|
||||
}
|
||||
if cmd.Flags().Changed("system-wide") {
|
||||
systemWide, _ := cmd.Flags().GetBool("system-wide")
|
||||
req.IsSystemWide = &systemWide
|
||||
}
|
||||
|
||||
svc := service.NewGitHubAppService(client)
|
||||
err = svc.Update(ctx, appUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update GitHub App: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("GitHub App updated successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
updateCmd.Flags().String("name", "", "GitHub App name")
|
||||
updateCmd.Flags().String("organization", "", "GitHub organization")
|
||||
updateCmd.Flags().String("api-url", "", "GitHub API URL")
|
||||
updateCmd.Flags().String("html-url", "", "GitHub HTML URL")
|
||||
updateCmd.Flags().String("custom-user", "", "Custom user for SSH")
|
||||
updateCmd.Flags().Int("custom-port", 0, "Custom port for SSH")
|
||||
updateCmd.Flags().Int("app-id", 0, "GitHub App ID")
|
||||
updateCmd.Flags().Int("installation-id", 0, "GitHub Installation ID")
|
||||
updateCmd.Flags().String("client-id", "", "GitHub OAuth Client ID")
|
||||
updateCmd.Flags().String("client-secret", "", "GitHub OAuth Client Secret")
|
||||
updateCmd.Flags().String("webhook-secret", "", "GitHub Webhook Secret")
|
||||
updateCmd.Flags().String("private-key-uuid", "", "UUID of private key")
|
||||
updateCmd.Flags().Bool("system-wide", false, "Is this app system-wide")
|
||||
|
||||
return updateCmd
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var instancesCmd = &cobra.Command{
|
||||
Use: "instances",
|
||||
Short: "Coolify instance related commands.",
|
||||
}
|
||||
|
||||
var instanceVersionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Get instance version.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
data, err := Fetch("version")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(data)
|
||||
},
|
||||
}
|
||||
var listInstancesCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all Coolify instances.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
instancesBytes, err := json.Marshal(instances)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
err = json.Indent(&prettyJSON, instancesBytes, "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(prettyJSON.String())
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
instancesBytes, err := json.Marshal(instances)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(instancesBytes))
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "#\tName\tFqdn\tToken\tDefault")
|
||||
for index, entry := range instances {
|
||||
entryMap, ok := entry.(map[string]interface{})
|
||||
if !ok {
|
||||
fmt.Println("Error")
|
||||
return
|
||||
}
|
||||
if ShowSensitive {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", index+1, entryMap["name"], entryMap["fqdn"], entryMap["token"], map[bool]string{true: "true", false: ""}[entryMap["default"] == true])
|
||||
} else {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", index+1, entryMap["name"], entryMap["fqdn"], SensitiveInformationOverlay, map[bool]string{true: "true", false: ""}[entryMap["default"] == true])
|
||||
}
|
||||
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
},
|
||||
}
|
||||
var addInstanceCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Example: `add <instanceName> <fqdn> <token>`,
|
||||
Args: cobra.ExactArgs(3),
|
||||
Short: "Add a Coolify instance.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Name := args[0]
|
||||
Host := args[1]
|
||||
Token := args[2]
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == Name {
|
||||
if Force {
|
||||
instanceMap["token"] = Token
|
||||
if SetDefaultInstance {
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
delete(instanceMap, "default")
|
||||
}
|
||||
instanceMap["default"] = true
|
||||
fmt.Printf("%s already exists. Force overwriting. Setting it as default. \n", Name)
|
||||
} else {
|
||||
fmt.Printf("%s already exists. Force overwriting. \n", Name)
|
||||
}
|
||||
viper.Set("instances", instances)
|
||||
viper.WriteConfig()
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s already exists. \n", Name)
|
||||
fmt.Println("\nNote: Use -f to force overwrite.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
instances = append(instances, map[string]interface{}{
|
||||
"name": Name,
|
||||
"fqdn": Host,
|
||||
"token": Token,
|
||||
})
|
||||
|
||||
if SetDefaultInstance {
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
delete(instanceMap, "default")
|
||||
}
|
||||
instances[len(instances)-1].(map[string]interface{})["default"] = true
|
||||
}
|
||||
viper.Set("instances", instances)
|
||||
viper.WriteConfig()
|
||||
listInstancesCmd.Run(cmd, args)
|
||||
},
|
||||
}
|
||||
var removeInstanceCmd = &cobra.Command{
|
||||
Use: "remove",
|
||||
Example: `remove <instanceName>`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Remove a Coolify instance.",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Name := args[0]
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
for i, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == Name {
|
||||
instances = append(instances[:i], instances[i+1:]...)
|
||||
viper.Set("instances", instances)
|
||||
viper.WriteConfig()
|
||||
fmt.Printf("%s removed. \n", Name)
|
||||
if instanceMap["default"] == true {
|
||||
fmt.Println("Note: The default instance has been removed.")
|
||||
if len(instances) > 0 {
|
||||
instances[0].(map[string]interface{})["default"] = true
|
||||
viper.Set("instances", instances)
|
||||
viper.WriteConfig()
|
||||
fmt.Printf("%s set as default. \n", instances[0].(map[string]interface{})["fqdn"])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Printf("%s not found. \n", Name)
|
||||
},
|
||||
}
|
||||
var setCmd = &cobra.Command{
|
||||
Use: "set",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Set default instance or token.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
},
|
||||
}
|
||||
var setTokenCmd = &cobra.Command{
|
||||
Use: "token",
|
||||
Example: `set token <instanceName> "<token>"`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Set token for the given Coolify instance.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Name = args[0]
|
||||
Token = args[1]
|
||||
var found interface{}
|
||||
for _, instance := range viper.Get("instances").([]interface{}) {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == Name {
|
||||
found = instanceMap
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
fmt.Printf("%s instance is not found. \n", Name)
|
||||
return
|
||||
}
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == Name {
|
||||
instanceMap["token"] = Token
|
||||
}
|
||||
}
|
||||
viper.Set("instances", instances)
|
||||
viper.WriteConfig()
|
||||
listInstancesCmd.Run(cmd, args)
|
||||
},
|
||||
}
|
||||
var setDefaultCmd = &cobra.Command{
|
||||
Use: "default",
|
||||
Example: `set default <instanceName>`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Set the default Coolify instance.",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Name := args[0]
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
var found interface{}
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == Name {
|
||||
found = instanceMap
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
fmt.Printf("%s not found. \n", Name)
|
||||
return
|
||||
}
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == Name {
|
||||
instanceMap["default"] = true
|
||||
} else {
|
||||
delete(instanceMap, "default")
|
||||
}
|
||||
}
|
||||
viper.Set("instances", instances)
|
||||
viper.WriteConfig()
|
||||
listInstancesCmd.Run(cmd, args)
|
||||
},
|
||||
}
|
||||
var getInstanceCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Example: `config get <instanceName>`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Get a Coolify instance.",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Name := args[0]
|
||||
instances := viper.Get("instances").([]interface{})
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
instanceMap["token"] = SensitiveInformationOverlay
|
||||
}
|
||||
instancesBytes, err := json.Marshal(instances)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
err = json.Indent(&prettyJSON, instancesBytes, "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(prettyJSON.String())
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
instanceMap["token"] = SensitiveInformationOverlay
|
||||
}
|
||||
instancesBytes, err := json.Marshal(instances)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(instancesBytes))
|
||||
return
|
||||
}
|
||||
for _, instance := range instances {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["name"] == Name {
|
||||
fmt.Fprintln(w, "Name\tHost\tToken")
|
||||
if ShowSensitive {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", Name, instanceMap["fqdn"], instanceMap["token"])
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", Name, instanceMap["fqdn"], SensitiveInformationOverlay)
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Printf("%s not found. \n", Name)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
addInstanceCmd.Flags().BoolVarP(&SetDefaultInstance, "default", "d", false, "Set default instance")
|
||||
|
||||
rootCmd.AddCommand(instancesCmd)
|
||||
instancesCmd.AddCommand(instanceVersionCmd)
|
||||
instancesCmd.AddCommand(listInstancesCmd)
|
||||
instancesCmd.AddCommand(addInstanceCmd)
|
||||
instancesCmd.AddCommand(removeInstanceCmd)
|
||||
instancesCmd.AddCommand(setCmd)
|
||||
instancesCmd.AddCommand(getInstanceCmd)
|
||||
setCmd.AddCommand(setTokenCmd)
|
||||
setCmd.AddCommand(setDefaultCmd)
|
||||
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type PrivateKeys struct {
|
||||
PrivateKeys []PrivateKey `json:"private_keys"`
|
||||
}
|
||||
|
||||
type PrivateKey struct {
|
||||
ID int `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"public_key"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
|
||||
var privateKeysCmd = &cobra.Command{
|
||||
Use: "private-keys",
|
||||
Short: "Private key related commands",
|
||||
}
|
||||
|
||||
var listPrivateKeysCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all private keys",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
|
||||
baseUrl := "security/keys"
|
||||
data, err := Fetch(baseUrl)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata []PrivateKey
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "Uuid\tName")
|
||||
for _, resource := range jsondata {
|
||||
if ShowSensitive {
|
||||
fmt.Fprintf(w, "%s\t%s\n", resource.UUID, resource.Name)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\n", resource.UUID, resource.Name)
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
},
|
||||
}
|
||||
var onePrivateKeyCmd = &cobra.Command{
|
||||
Use: "get [uuid]",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Get private key details by uuid",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
baseUrl := "security/keys/"
|
||||
|
||||
uuid := args[0]
|
||||
var url = baseUrl + uuid
|
||||
|
||||
data, err := Fetch(url)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata PrivateKey
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "Uuid\tName\tPublicKey\tPrivateKey")
|
||||
if ShowSensitive {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", jsondata.UUID, jsondata.Name, jsondata.PublicKey, strings.ReplaceAll(jsondata.PrivateKey, "\n", "\\n"))
|
||||
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", jsondata.UUID, jsondata.Name, SensitiveInformationOverlay, SensitiveInformationOverlay)
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
var addPrivateKeyCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Example: `add <name> <private_key_or_file>`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Add a private key",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version := "4.0.0-beta.383"
|
||||
CheckDefaultThings(&version)
|
||||
baseUrl := "security/keys"
|
||||
name := args[0]
|
||||
privateKeyInput := args[1]
|
||||
|
||||
var privateKey string
|
||||
// Check if input is a file path
|
||||
if _, err := os.Stat(privateKeyInput); err == nil {
|
||||
keyBytes, err := os.ReadFile(privateKeyInput)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading private key file: %v\n", err)
|
||||
return
|
||||
}
|
||||
privateKey = string(keyBytes)
|
||||
} else {
|
||||
privateKey = privateKeyInput
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"name": name,
|
||||
"private_key": privateKey,
|
||||
}
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating request: %v\n", err)
|
||||
return
|
||||
}
|
||||
_, err = Post(baseUrl, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
fmt.Printf("Error adding private key: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Private key '%s' added successfully\n", name)
|
||||
},
|
||||
}
|
||||
|
||||
var removePrivateKeyCmd = &cobra.Command{
|
||||
Use: "remove [uuid]",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Remove a private key",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version := "4.0.0-beta.383"
|
||||
CheckDefaultThings(&version)
|
||||
baseUrl := "security/keys/"
|
||||
uuid := args[0]
|
||||
_, err := Delete(baseUrl + uuid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Private key removed successfully")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(privateKeysCmd)
|
||||
privateKeysCmd.AddCommand(listPrivateKeysCmd)
|
||||
privateKeysCmd.AddCommand(onePrivateKeyCmd)
|
||||
privateKeysCmd.AddCommand(addPrivateKeyCmd)
|
||||
privateKeysCmd.AddCommand(removePrivateKeyCmd)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package privatekeys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewCreateCommand creates the create command
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "add <key_name> <private_key_or_file>",
|
||||
Example: `add mykey ~/.ssh/id_rsa`,
|
||||
Args: cli.ExactArgs(2, "<key_name> <private_key_or_file>"),
|
||||
Short: "Add a private key",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
name := args[0]
|
||||
privateKeyInput := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
var privateKey string
|
||||
// Check if input is a file path
|
||||
if _, err := os.Stat(privateKeyInput); err == nil {
|
||||
keyBytes, err := os.ReadFile(privateKeyInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading private key file: %w", err)
|
||||
}
|
||||
privateKey = string(keyBytes)
|
||||
} else {
|
||||
privateKey = privateKeyInput
|
||||
}
|
||||
|
||||
keySvc := service.NewPrivateKeyService(client)
|
||||
req := models.PrivateKeyCreateRequest{
|
||||
Name: name,
|
||||
PrivateKey: privateKey,
|
||||
}
|
||||
|
||||
key, err := keySvc.Create(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add private key: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Private key '%s' added successfully (UUID: %s)\n", key.Name, key.UUID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package privatekeys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewDeleteCommand creates the delete command
|
||||
func NewDeleteCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "remove <uuid>",
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
Short: "Remove a private key",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
keySvc := service.NewPrivateKeyService(client)
|
||||
err = keySvc.Delete(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove private key: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Private key removed successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package privatekeys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewListCommand creates the list command
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all private keys",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
keySvc := service.NewPrivateKeyService(client)
|
||||
keys, err := keySvc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list private keys: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := formatter.Format(keys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !showSensitive && format == output.FormatTable {
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package privatekeys
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewDomainsCommand creates the domains parent command
|
||||
func NewPrivateKeysCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "private-key",
|
||||
Aliases: []string{"private-keys", "key", "keys"},
|
||||
Short: "Private key related commands",
|
||||
Long: `Manage SSH private keys for server authentication - list, add, and remove keys.`,
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewCreateCommand())
|
||||
cmd.AddCommand(NewDeleteCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// EnvironmentRow represents an environment for display
|
||||
type EnvironmentRow struct {
|
||||
UUID string `json:"environment_uuid"`
|
||||
EnvironmentName string `json:"environment_name"`
|
||||
}
|
||||
|
||||
// NewGetCommand returns the get project command
|
||||
func NewGetCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get <uuid>",
|
||||
Short: "Get a project by uuid",
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
projectSvc := service.NewProjectService(client)
|
||||
project, err := projectSvc.Get(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get project: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
// For JSON/pretty formats, return the full project structure
|
||||
if format != output.FormatTable {
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return formatter.Format(project)
|
||||
}
|
||||
|
||||
// For table format, expand environments into separate rows
|
||||
var rows []EnvironmentRow
|
||||
|
||||
// If the project has environments, expand them
|
||||
if len(project.Environments) > 0 {
|
||||
for _, env := range project.Environments {
|
||||
rows = append(rows, EnvironmentRow{
|
||||
UUID: env.UUID,
|
||||
EnvironmentName: env.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(rows)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// ListRow represents a project for list display (without environments)
|
||||
type ListRow struct {
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// NewListCommand returns the list projects command
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all projects",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
projectSvc := service.NewProjectService(client)
|
||||
projects, err := projectSvc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list projects: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
// For JSON/pretty formats, return the full project structure
|
||||
if format != output.FormatTable {
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return formatter.Format(projects)
|
||||
}
|
||||
|
||||
// For table format, convert to simplified rows without environments
|
||||
var rows []ListRow
|
||||
for _, p := range projects {
|
||||
desc := ""
|
||||
if p.Description != nil {
|
||||
desc = *p.Description
|
||||
}
|
||||
rows = append(rows, ListRow{
|
||||
UUID: p.UUID,
|
||||
Name: p.Name,
|
||||
Description: desc,
|
||||
})
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(rows)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package project
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// NewProjectCommand creates the project parent command with all subcommands
|
||||
func NewProjectCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "project",
|
||||
Aliases: []string{"projects"},
|
||||
Short: "Project related commands",
|
||||
Long: `Manage Coolify projects - list all projects or get details about a specific project.`,
|
||||
}
|
||||
|
||||
// Add all project subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
-211
@@ -1,211 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
ID int `json:"id"`
|
||||
Uuid string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
type Environment struct {
|
||||
ID int `json:"id"`
|
||||
Uuid string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Description *string `json:"description"`
|
||||
Applications []Application `json:"applications"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Environments []Environment `json:"environments"`
|
||||
}
|
||||
|
||||
var projectsCmd = &cobra.Command{
|
||||
Use: "projects",
|
||||
Short: "Project related commands",
|
||||
}
|
||||
|
||||
var listProjectsCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all projects",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
baseUrl := "projects"
|
||||
data, err := Fetch(baseUrl)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata []Project
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "Uuid\tName")
|
||||
for _, resource := range jsondata {
|
||||
fmt.Fprintf(w, "%s\t%s\n", resource.Uuid, resource.Name)
|
||||
}
|
||||
w.Flush()
|
||||
},
|
||||
}
|
||||
|
||||
var oneProjectCmd = &cobra.Command{
|
||||
Use: "get [uuid]",
|
||||
Short: "Get a project by uuid",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
uuid := args[0]
|
||||
environment, _ := cmd.Flags().GetString("environment")
|
||||
if environment != "" {
|
||||
url := "projects/" + uuid + "/" + environment
|
||||
data, err := Fetch(url)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata Environment
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "Uuid\tName\tStatus")
|
||||
for _, resource := range jsondata.Applications {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", resource.Uuid, resource.Name, resource.Status)
|
||||
}
|
||||
w.Flush()
|
||||
return
|
||||
}
|
||||
data, err := Fetch("projects/" + uuid)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata Project
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "Uuid\tName\tEnvironments")
|
||||
envNames := make([]string, len(jsondata.Environments))
|
||||
for i, env := range jsondata.Environments {
|
||||
envNames[i] = env.Name + " (" + env.Uuid + ")"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", jsondata.Uuid, jsondata.Name, strings.Join(envNames, ", "))
|
||||
w.Flush()
|
||||
},
|
||||
}
|
||||
var addProjectCmd = &cobra.Command{
|
||||
Use: "add [name]",
|
||||
Short: "Add a project",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
baseUrl := "projects"
|
||||
name := args[0]
|
||||
data := map[string]string{
|
||||
"name": name,
|
||||
}
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
response, err := Post(baseUrl, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
msg := map[string]string{}
|
||||
json.Unmarshal([]byte(response), &msg)
|
||||
fmt.Println("Project added successfully with uuid " + msg["uuid"])
|
||||
},
|
||||
}
|
||||
|
||||
var removeProjectCmd = &cobra.Command{
|
||||
Use: "remove [uuid]",
|
||||
Short: "Remove a project",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
baseUrl := "projects/"
|
||||
uuid := args[0]
|
||||
response, err := Delete(baseUrl + uuid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
msg := map[string]string{}
|
||||
json.Unmarshal([]byte(response), &msg)
|
||||
fmt.Println(msg["message"])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(projectsCmd)
|
||||
projectsCmd.AddCommand(listProjectsCmd)
|
||||
oneProjectCmd.Flags().StringP("environment", "e", "", "Environment")
|
||||
projectsCmd.AddCommand(oneProjectCmd)
|
||||
|
||||
projectsCmd.AddCommand(addProjectCmd)
|
||||
projectsCmd.AddCommand(removeProjectCmd)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var resourcesCmd = &cobra.Command{
|
||||
Use: "resources",
|
||||
Short: "Resource related commands",
|
||||
}
|
||||
|
||||
var listResourcesCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all resources",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
data, err := Fetch("resources")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata []Resource
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "Uuid\tName\tType\tStatus")
|
||||
for _, resource := range jsondata {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", resource.Uuid, resource.Name, resource.Type, resource.Status)
|
||||
}
|
||||
w.Flush()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(resourcesCmd)
|
||||
resourcesCmd.AddCommand(listResourcesCmd)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewListCommand returns the list projects command
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all resources",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
resourceSvc := service.NewResourceService(client)
|
||||
resources, err := resourceSvc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list resources: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Format(resources)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewResourceCommand creates the resource parent command with all subcommands
|
||||
func NewResourceCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "resource",
|
||||
Aliases: []string{"resources"},
|
||||
Short: "Resource related commands",
|
||||
Long: `List all resources (applications, services, databases) in Coolify.`,
|
||||
}
|
||||
|
||||
// Add all resource subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
+102
-306
@@ -1,352 +1,148 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
compareVersion "github.com/hashicorp/go-version"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"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"
|
||||
"github.com/coollabsio/coolify-cli/cmd/github"
|
||||
"github.com/coollabsio/coolify-cli/cmd/privatekeys"
|
||||
"github.com/coollabsio/coolify-cli/cmd/project"
|
||||
"github.com/coollabsio/coolify-cli/cmd/resources"
|
||||
"github.com/coollabsio/coolify-cli/cmd/server"
|
||||
"github.com/coollabsio/coolify-cli/cmd/service"
|
||||
"github.com/coollabsio/coolify-cli/cmd/teams"
|
||||
"github.com/coollabsio/coolify-cli/cmd/update"
|
||||
cliversion "github.com/coollabsio/coolify-cli/cmd/version"
|
||||
"github.com/coollabsio/coolify-cli/internal/config"
|
||||
"github.com/coollabsio/coolify-cli/internal/version"
|
||||
)
|
||||
|
||||
var CliVersion = "0.0.1"
|
||||
var LastUpdateCheckTime time.Time
|
||||
var CheckInverval = 10 * time.Minute
|
||||
|
||||
var ConfigDir = xdg.ConfigHome
|
||||
|
||||
var Version string
|
||||
var Name string
|
||||
var Fqdn string
|
||||
var Token string
|
||||
var Instance http.Client
|
||||
var SensitiveInformationOverlay = "********"
|
||||
|
||||
// Flags
|
||||
var Debug bool
|
||||
var ShowSensitive bool
|
||||
var Force bool
|
||||
var Format string
|
||||
|
||||
var JsonMode bool
|
||||
var PrettyMode bool
|
||||
var SetDefaultInstance bool
|
||||
|
||||
var w = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)
|
||||
|
||||
type Tag struct {
|
||||
Ref string `json:"ref"`
|
||||
}
|
||||
// Legacy global variables - kept for backward compatibility during migration
|
||||
// TODO: Remove these once all commands are refactored
|
||||
var (
|
||||
Version string
|
||||
Name string
|
||||
Fqdn string
|
||||
Token string
|
||||
ContextName string
|
||||
Debug bool
|
||||
ShowSensitive bool
|
||||
Format string
|
||||
JSONMode bool
|
||||
PrettyMode bool
|
||||
SetDefaultInstance bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "coolify",
|
||||
Short: "Coolify CLI",
|
||||
Long: `A CLI tool to interact with Coolify API.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
Use: "coolify",
|
||||
Short: "Coolify CLI",
|
||||
Long: `A CLI tool to interact with Coolify API.`,
|
||||
SilenceUsage: true, // Don't show usage on errors
|
||||
SilenceErrors: false, // Still print errors
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func CheckFormat(format string) {
|
||||
if format == "json" {
|
||||
JsonMode = true
|
||||
return
|
||||
}
|
||||
if format == "pretty" {
|
||||
PrettyMode = true
|
||||
return
|
||||
}
|
||||
if format == "table" {
|
||||
return
|
||||
}
|
||||
fmt.Println("Invalid format", format)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func CheckDefaultThings(version *string) {
|
||||
FetchVersion()
|
||||
CheckFormat(Format)
|
||||
if version == nil {
|
||||
CheckMinimumVersion(Version)
|
||||
} else {
|
||||
CheckMinimumVersion(*version)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckMinimumVersion(version string) {
|
||||
requiredVersion, err := compareVersion.NewVersion(version)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
currentVersion, err := compareVersion.NewVersion(Version)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
if currentVersion.LessThan(requiredVersion) {
|
||||
log.Printf("Minimum required Coolify API version is: %s\n", version)
|
||||
log.Print("Please upgrade your Coolify instance for this command.\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
func FetchVersion() (string, error) {
|
||||
data, err := Fetch("version")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(0)
|
||||
return "", err
|
||||
}
|
||||
Version = data
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func Fetch(url string) (string, error) {
|
||||
url = Fqdn + "/api/v1/" + url
|
||||
if Debug {
|
||||
log.Println("Fetching data from", url)
|
||||
}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Add("Authorization", "Bearer "+Token)
|
||||
resp, err := Instance.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
func Post(url string, input io.Reader) (string, error) {
|
||||
url = Fqdn + "/api/v1/" + url
|
||||
if Debug {
|
||||
log.Println("Posting data to", url)
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Add("Authorization", "Bearer "+Token)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err := Instance.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
message := string(body)
|
||||
if message == "" {
|
||||
message = "Unknown error"
|
||||
} else {
|
||||
msg := map[string]string{}
|
||||
json.Unmarshal(body, &msg)
|
||||
message = msg["message"]
|
||||
}
|
||||
return "", fmt.Errorf("%s (rc: %d)", message, resp.StatusCode)
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func Delete(url string) (string, error) {
|
||||
url = Fqdn + "/api/v1/" + url
|
||||
if Debug {
|
||||
log.Println("Deleting data from", url)
|
||||
}
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Add("Authorization", "Bearer "+Token)
|
||||
resp, err := Instance.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
message := string(body)
|
||||
if message == "" {
|
||||
message = "Unknown error"
|
||||
} else {
|
||||
msg := map[string]string{}
|
||||
json.Unmarshal(body, &msg)
|
||||
message = msg["message"]
|
||||
}
|
||||
return "", fmt.Errorf("%s (rc: %d)", message, resp.StatusCode)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func CheckLatestVersionOfCli() (string, error) {
|
||||
getLastUpdateCheckTime()
|
||||
if LastUpdateCheckTime.Add(CheckInverval).After(time.Now()) {
|
||||
if Debug {
|
||||
log.Println("Skipping update check. Last check was less than 10 minutes ago.")
|
||||
}
|
||||
return CliVersion, nil
|
||||
}
|
||||
setLastUpdateCheckTime()
|
||||
url := "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
|
||||
}
|
||||
|
||||
var tags []Tag
|
||||
if err := json.Unmarshal(body, &tags); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
versionsRaw := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
versionStr := tag.Ref[10:]
|
||||
versionsRaw = append(versionsRaw, versionStr)
|
||||
}
|
||||
|
||||
versions := make([]*compareVersion.Version, len(versionsRaw))
|
||||
for i, raw := range versionsRaw {
|
||||
v, err := compareVersion.NewVersion(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
versions[i] = v
|
||||
}
|
||||
|
||||
sort.Sort(compareVersion.Collection(versions))
|
||||
latestVersion := versions[len(versions)-1].String()
|
||||
if latestVersion != CliVersion {
|
||||
fmt.Printf("There is a new version of Coolify CLI available.\nPlease update with 'coolify --update'.\n\n")
|
||||
}
|
||||
return latestVersion, nil
|
||||
|
||||
}
|
||||
// Execute runs the root command
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(0)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "coolify",
|
||||
Short: "Coolify CLI",
|
||||
Long: fmt.Sprintf("A CLI tool to interact with Coolify API.\nVersion: %s", version.GetVersion()),
|
||||
SilenceUsage: true, // Don't show usage on errors
|
||||
SilenceErrors: false, // Still print errors
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&Token, "token", "", "", "Token for authentication (https://app.coolify.io/security/api-tokens)")
|
||||
rootCmd.PersistentFlags().StringVarP(&Fqdn, "host", "", "", "Coolify instance hostname")
|
||||
rootCmd.PersistentFlags().StringVarP(&Token, "token", "", "", "Token for authentication (override context token)")
|
||||
rootCmd.PersistentFlags().StringVarP(&ContextName, "context", "", "", "Use specific context by name")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&Format, "format", "", "table", "Format output (table|json|pretty)")
|
||||
rootCmd.PersistentFlags().BoolVarP(&ShowSensitive, "show-sensitive", "s", false, "Show sensitive information")
|
||||
rootCmd.PersistentFlags().BoolVarP(&Force, "force", "f", false, "Force")
|
||||
rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "", false, "Debug mode")
|
||||
}
|
||||
func setLastUpdateCheckTime() {
|
||||
timeNow := time.Now()
|
||||
viper.Set("lastupdatechecktime", timeNow)
|
||||
viper.WriteConfig()
|
||||
LastUpdateCheckTime = timeNow
|
||||
}
|
||||
func getLastUpdateCheckTime() {
|
||||
lastUpdateCheckTimeString := viper.Get("lastupdatechecktime").(string)
|
||||
lastUpdateCheckTime, err := time.Parse(time.RFC3339, lastUpdateCheckTimeString)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing time: %v", err)
|
||||
}
|
||||
LastUpdateCheckTime = lastUpdateCheckTime
|
||||
|
||||
// Register all subcommands
|
||||
rootCmd.AddCommand(application.NewAppCommand())
|
||||
rootCmd.AddCommand(completion.NewCompletionsCommand())
|
||||
rootCmd.AddCommand(configcmd.NewConfigCommand())
|
||||
rootCmd.AddCommand(context.NewContextCommand())
|
||||
rootCmd.AddCommand(database.NewDatabaseCommand())
|
||||
rootCmd.AddCommand(deployment.NewDeploymentCommand())
|
||||
rootCmd.AddCommand(github.NewGitHubCommand())
|
||||
rootCmd.AddCommand(privatekeys.NewPrivateKeysCommand())
|
||||
rootCmd.AddCommand(project.NewProjectCommand())
|
||||
rootCmd.AddCommand(resources.NewResourceCommand())
|
||||
rootCmd.AddCommand(server.NewServerCommand())
|
||||
rootCmd.AddCommand(service.NewServiceCommand())
|
||||
rootCmd.AddCommand(teams.NewTeamsCommand())
|
||||
rootCmd.AddCommand(update.NewUpdateCommand())
|
||||
rootCmd.AddCommand(cliversion.NewVersionCommand())
|
||||
rootCmd.AddCommand(NewDocsCommand())
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("json")
|
||||
viper.AddConfigPath(ConfigDir + "/coolify")
|
||||
if _, err := os.Stat(ConfigDir + "/coolify"); os.IsNotExist(err) {
|
||||
os.MkdirAll(ConfigDir+"/coolify", 0755)
|
||||
viper.AddConfigPath(config.Path()[:len(config.Path())-len("/config.json")])
|
||||
|
||||
// Ensure config directory exists
|
||||
configDir := config.Path()[:len(config.Path())-len("/config.json")]
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(configDir, 0750); err != nil {
|
||||
log.Printf("Failed to create config directory: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
log.Println("Config file not found. Creating a new one at", ConfigDir+"/coolify/config.json")
|
||||
viper.Set("lastUpdateCheckTime", time.Now())
|
||||
viper.Set("instances", []interface{}{map[string]interface{}{
|
||||
"name": "cloud",
|
||||
"default": true,
|
||||
"fqdn": "https://app.coolify.io",
|
||||
"token": "",
|
||||
},
|
||||
})
|
||||
viper.Set("instances", append(viper.Get("instances").([]interface{}), map[string]interface{}{
|
||||
"name": "localhost",
|
||||
"fqdn": "http://localhost:8000",
|
||||
"token": "",
|
||||
}))
|
||||
viper.SafeWriteConfig()
|
||||
return
|
||||
// Config file not found; ignore error if desired
|
||||
var notFoundErr viper.ConfigFileNotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
log.Println("Config file not found. Creating a new one at", config.Path())
|
||||
if err := config.CreateDefault(); err != nil {
|
||||
log.Printf("Failed to create default config: %v\n", err)
|
||||
return
|
||||
}
|
||||
// Reload config after creating default
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Printf("Failed to read newly created config: %v\n", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Error reading config file, ", err)
|
||||
fmt.Println("Error reading config file:", err)
|
||||
return
|
||||
// Config file was found but another error was produced
|
||||
}
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.Println("Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
instancesMap := viper.Get("instances").([]interface{})
|
||||
for _, instance := range instancesMap {
|
||||
instanceMap := instance.(map[string]interface{})
|
||||
if instanceMap["default"] == true {
|
||||
if Fqdn == "" {
|
||||
Fqdn = instanceMap["fqdn"].(string)
|
||||
}
|
||||
if Token == "" {
|
||||
Token = instanceMap["token"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
data, err := CheckLatestVersionOfCli()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if data != CliVersion {
|
||||
log.Printf("New version of Coolify CLI is available: %s\n", data)
|
||||
}
|
||||
|
||||
// Note: We don't pre-populate Fqdn/Token here anymore
|
||||
// They are loaded on-demand by getAPIClient() based on --instance or default instance
|
||||
// This allows --instance flag to work correctly
|
||||
|
||||
// Check for updates (errors are handled silently inside the function)
|
||||
_, _ = version.CheckLatestVersionOfCli(Debug)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewAddCommand creates the add command
|
||||
func NewAddCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add <server_name> <ip_address> <private_key_uuid>",
|
||||
Args: cli.ExactArgs(3, "<server_name> <ip_address> <private_key_uuid>"),
|
||||
Short: "Add a server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get API client
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Parse arguments and flags
|
||||
name := args[0]
|
||||
ip := args[1]
|
||||
privateKeyUUID := args[2]
|
||||
port, _ := cmd.Flags().GetInt("port")
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
validate, _ := cmd.Flags().GetBool("validate")
|
||||
|
||||
// Create request
|
||||
req := models.ServerCreateRequest{
|
||||
Name: name,
|
||||
IP: ip,
|
||||
Port: port,
|
||||
User: user,
|
||||
PrivateKeyUUID: privateKeyUUID,
|
||||
InstantValidate: validate,
|
||||
}
|
||||
|
||||
// Use service layer
|
||||
serverSvc := service.NewServerService(client)
|
||||
response, err := serverSvc.Create(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create server: %w", err)
|
||||
}
|
||||
|
||||
if validate {
|
||||
fmt.Printf("Server added successfully with uuid %s\n", response.UUID)
|
||||
} else {
|
||||
fmt.Printf("Server added successfully with uuid %s. Server is not validated. Use 'servers validate %s' to validate the server.\n", response.UUID, response.UUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntP("port", "p", 22, "Port")
|
||||
cmd.Flags().StringP("user", "u", "root", "User")
|
||||
cmd.Flags().Bool("validate", false, "Validate the server")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewGetCommand creates the get command
|
||||
func NewGetDomainsCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "domains <uuid>",
|
||||
Aliases: []string{"domain"},
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
Short: "Get server domains by uuid",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get API client
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Use service layer
|
||||
serverSvc := service.NewServerService(client)
|
||||
uuid := args[0]
|
||||
|
||||
// Get format flags
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
domains, err := serverSvc.GetDomains(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get server domains: %w", err)
|
||||
}
|
||||
|
||||
// Use output formatter
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := formatter.Format(domains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !showSensitive && format == output.FormatTable {
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewGetCommand creates the get command
|
||||
func NewGetCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "get <uuid>",
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
Short: "Get server details by uuid",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get API client
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Use service layer
|
||||
serverSvc := service.NewServerService(client)
|
||||
uuid := args[0]
|
||||
|
||||
// Get format flags
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
withResources, _ := cmd.Flags().GetBool("resources")
|
||||
|
||||
var data interface{}
|
||||
if withResources {
|
||||
resources, err := serverSvc.GetResources(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get server resources: %w", err)
|
||||
}
|
||||
data = resources.Resources
|
||||
} else {
|
||||
server, err := serverSvc.Get(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get server: %w", err)
|
||||
}
|
||||
data = server
|
||||
}
|
||||
|
||||
// Use output formatter
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := formatter.Format(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !showSensitive && format == output.FormatTable && !withResources {
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("resources", false, "With resources")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewListCommand creates the list command
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all servers",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get API client
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Use service layer
|
||||
serverSvc := service.NewServerService(client)
|
||||
servers, err := serverSvc.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list servers: %w", err)
|
||||
}
|
||||
|
||||
// Use output formatter
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := formatter.Format(servers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !showSensitive && format == output.FormatTable {
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewRemoveCommand creates the remove command
|
||||
func NewRemoveCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "remove <uuid>",
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
Short: "Remove a server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get API client
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Use service layer
|
||||
serverSvc := service.NewServerService(client)
|
||||
uuid := args[0]
|
||||
|
||||
if err := serverSvc.Delete(ctx, uuid); err != nil {
|
||||
return fmt.Errorf("failed to delete server: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Server %s deleted successfully\n", uuid)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewServerCommand creates the server parent command
|
||||
func NewServerCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "server",
|
||||
Aliases: []string{"servers"},
|
||||
Short: "Server related commands",
|
||||
Long: `Manage Coolify servers - list, get details, add new servers, validate connections, and remove servers.`,
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(NewGetDomainsCommand())
|
||||
cmd.AddCommand(NewAddCommand())
|
||||
cmd.AddCommand(NewRemoveCommand())
|
||||
cmd.AddCommand(NewValidateCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewValidateCommand creates the validate command
|
||||
func NewValidateCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "validate <uuid>",
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
Short: "Validate a server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get API client
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Use service layer
|
||||
serverSvc := service.NewServerService(client)
|
||||
uuid := args[0]
|
||||
|
||||
response, err := serverSvc.Validate(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate server: %w", err)
|
||||
}
|
||||
|
||||
if response.Message != "" {
|
||||
fmt.Println(response.Message)
|
||||
} else {
|
||||
fmt.Printf("Server %s validated successfully\n", uuid)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
-243
@@ -1,243 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var WithResources bool
|
||||
|
||||
type Resource struct {
|
||||
ID int `json:"id"`
|
||||
Uuid string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type Resources struct {
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
ID int `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
User string `json:"user"`
|
||||
Port int `json:"port"`
|
||||
Settings struct {
|
||||
Reachable bool `json:"is_reachable"`
|
||||
Usable bool `json:"is_usable"`
|
||||
} `json:"settings"`
|
||||
}
|
||||
|
||||
var serversCmd = &cobra.Command{
|
||||
Use: "servers",
|
||||
Short: "Server related commands",
|
||||
}
|
||||
|
||||
var listServersCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all servers",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
|
||||
baseUrl := "servers"
|
||||
data, err := Fetch(baseUrl)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
var jsondata []Server
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "Uuid\tName\tIP Address\tUser\tPort\tReachable\tUsable")
|
||||
for _, resource := range jsondata {
|
||||
if ShowSensitive {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%t\t%t\n", resource.UUID, resource.Name, resource.IP, resource.User, resource.Port, resource.Settings.Reachable, resource.Settings.Usable)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%t\t%t\n", resource.UUID, resource.Name, SensitiveInformationOverlay, SensitiveInformationOverlay, SensitiveInformationOverlay, resource.Settings.Reachable, resource.Settings.Usable)
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
},
|
||||
}
|
||||
var oneServerCmd = &cobra.Command{
|
||||
Use: "get [uuid]",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Short: "Get server details by uuid",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
baseUrl := "servers/"
|
||||
|
||||
uuid := args[0]
|
||||
var url = baseUrl + uuid
|
||||
if WithResources {
|
||||
url = baseUrl + uuid + "?resources=true"
|
||||
}
|
||||
|
||||
data, err := Fetch(url)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if PrettyMode {
|
||||
var prettyJSON bytes.Buffer
|
||||
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(prettyJSON.String()))
|
||||
return
|
||||
}
|
||||
if JsonMode {
|
||||
fmt.Println(data)
|
||||
return
|
||||
}
|
||||
if WithResources {
|
||||
var jsondata Resources
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "Uuid\tName\tType\tStatus")
|
||||
for _, resource := range jsondata.Resources {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", resource.Uuid, resource.Name, resource.Type, resource.Status)
|
||||
}
|
||||
w.Flush()
|
||||
} else {
|
||||
var jsondata Server
|
||||
err = json.Unmarshal([]byte(data), &jsondata)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "Uuid\tName\tIP Address\tUser\tPort\tReachable\tUsable")
|
||||
if ShowSensitive {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%t\t%t\n", jsondata.UUID, jsondata.Name, jsondata.IP, jsondata.User, jsondata.Port, jsondata.Settings.Reachable, jsondata.Settings.Usable)
|
||||
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%t\t%t\n", jsondata.UUID, jsondata.Name, SensitiveInformationOverlay, SensitiveInformationOverlay, SensitiveInformationOverlay, jsondata.Settings.Reachable, jsondata.Settings.Usable)
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
var removeServerCmd = &cobra.Command{
|
||||
Use: "remove [uuid]",
|
||||
Short: "Remove a server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
baseUrl := "servers/"
|
||||
uuid := args[0]
|
||||
response, err := Delete(baseUrl + uuid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
msg := map[string]string{}
|
||||
json.Unmarshal([]byte(response), &msg)
|
||||
fmt.Println(msg["message"])
|
||||
},
|
||||
}
|
||||
|
||||
var addServerCmd = &cobra.Command{
|
||||
Use: "add [name] [ip] [private_key_uuid]",
|
||||
Short: "Add a server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
baseUrl := "servers"
|
||||
name := args[0]
|
||||
ip := args[1]
|
||||
privateKeyUuid := args[2]
|
||||
port, _ := cmd.Flags().GetInt("port")
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
validate, _ := cmd.Flags().GetBool("validate")
|
||||
jsonData, err := json.Marshal(map[string]interface{}{
|
||||
"name": name,
|
||||
"ip": ip,
|
||||
"port": port,
|
||||
"user": user,
|
||||
"private_key_uuid": privateKeyUuid,
|
||||
"instant_validate": validate,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
response, err := Post(baseUrl, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
msg := map[string]string{}
|
||||
json.Unmarshal([]byte(response), &msg)
|
||||
if validate {
|
||||
fmt.Println("Server added successfully with uuid " + msg["uuid"])
|
||||
} else {
|
||||
fmt.Println("Server added successfully with uuid " + msg["uuid"] + ". Server is not validated. Use 'servers validate " + msg["uuid"] + "' to validate the server.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var validateServerCmd = &cobra.Command{
|
||||
Use: "validate [uuid]",
|
||||
Short: "Validate a server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckDefaultThings(nil)
|
||||
baseUrl := "servers/"
|
||||
uuid := args[0]
|
||||
var url = baseUrl + uuid + "/validate"
|
||||
response, err := Fetch(url)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
msg := map[string]string{}
|
||||
json.Unmarshal([]byte(response), &msg)
|
||||
fmt.Println(msg["message"])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
oneServerCmd.Flags().BoolVarP(&WithResources, "resources", "", false, "With resources")
|
||||
rootCmd.AddCommand(serversCmd)
|
||||
serversCmd.AddCommand(listServersCmd)
|
||||
serversCmd.AddCommand(oneServerCmd)
|
||||
|
||||
addServerCmd.Flags().IntP("port", "p", 22, "Port")
|
||||
addServerCmd.Flags().StringP("user", "u", "root", "User")
|
||||
addServerCmd.Flags().BoolP("validate", "", false, "Validate the server")
|
||||
serversCmd.AddCommand(addServerCmd)
|
||||
serversCmd.AddCommand(validateServerCmd)
|
||||
serversCmd.AddCommand(removeServerCmd)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewDeleteCommand deletes a service
|
||||
func NewDeleteCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <uuid>",
|
||||
Short: "Delete a service",
|
||||
Long: `Delete a service and optionally clean up its configurations, volumes, and networks.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deleteConfigurations, _ := cmd.Flags().GetBool("delete-configurations")
|
||||
deleteVolumes, _ := cmd.Flags().GetBool("delete-volumes")
|
||||
dockerCleanup, _ := cmd.Flags().GetBool("docker-cleanup")
|
||||
deleteConnectedNetworks, _ := cmd.Flags().GetBool("delete-connected-networks")
|
||||
|
||||
// Prompt for confirmation unless --force is used
|
||||
if !force {
|
||||
var response string
|
||||
fmt.Printf("Are you sure you want to delete this service? (yes/no): ")
|
||||
_, err := fmt.Scanln(&response)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read confirmation: %w", err)
|
||||
}
|
||||
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Println("Delete cancelled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
serviceSvc := service.NewService(client)
|
||||
err = serviceSvc.Delete(ctx, uuid, deleteConfigurations, deleteVolumes, dockerCleanup, deleteConnectedNetworks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete service: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Service deletion request queued.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
|
||||
cmd.Flags().Bool("delete-configurations", true, "Delete configurations")
|
||||
cmd.Flags().Bool("delete-volumes", true, "Delete volumes")
|
||||
cmd.Flags().Bool("docker-cleanup", true, "Run docker cleanup")
|
||||
cmd.Flags().Bool("delete-connected-networks", true, "Delete connected networks")
|
||||
|
||||
return cmd
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user