forked from mirror/coolify-cli
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a128de992 | |||
| 6495804344 | |||
| 3a994cb19e | |||
| 9b8992d177 | |||
| de6418e532 | |||
| 5e8c823637 | |||
| 7c370540e2 | |||
| 35f152b3d1 | |||
| 2b8a3bd120 | |||
| 77a61d614e | |||
| 255b918d02 | |||
| 200313c1b8 | |||
| dd0d46b0fc | |||
| 7c6a6b4292 | |||
| ef4a847f10 | |||
| b22f7b6943 | |||
| 9a4ef0d6ac | |||
| 98a624af27 | |||
| cb185da557 | |||
| d809990bec | |||
| f66c4f4217 | |||
| decc3e092a | |||
| 611b14d2ea | |||
| d22e6607a9 | |||
| 1126defb7c | |||
| b4148d6344 | |||
| 8c38a5447a |
@@ -1,46 +0,0 @@
|
||||
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
|
||||
+38
-25
@@ -1,29 +1,42 @@
|
||||
You are an expert AI programming assistant specializing in building CLI applications with Go, using Cobra for command-line interface management and Bubble Tea for terminal user interfaces.
|
||||
|
||||
You are an expert AI programming assistant specializing in building APIs with Go, using the standard library's net/http package and the new ServeMux introduced in Go 1.22.
|
||||
Always use Go 1.24 and be familiar with CLI development best practices, Go idioms, and terminal UI design principles.
|
||||
|
||||
Always use the latest stable version of Go (1.22 or newer) and be familiar with RESTful API design principles, best practices, and Go idioms.
|
||||
When using lipgloss for terminal styling, use these Coolify brand colors via the pkg/tui package.
|
||||
|
||||
- Follow the user's requirements carefully & to the letter.
|
||||
- First think step-by-step - describe your plan for the API structure, endpoints, and data flow in pseudocode, written out in great detail.
|
||||
- Confirm the plan, then write code!
|
||||
- Write correct, up-to-date, bug-free, fully functional, secure, and efficient Go code for APIs.
|
||||
- Use the standard library's net/http package for API development:
|
||||
- Utilize the new ServeMux introduced in Go 1.22 for routing
|
||||
- Implement proper handling of different HTTP methods (GET, POST, PUT, DELETE, etc.)
|
||||
- Use method handlers with appropriate signatures (e.g., func(w http.ResponseWriter, r *http.Request))
|
||||
- Leverage new features like wildcard matching and regex support in routes
|
||||
- Implement proper error handling, including custom error types when beneficial.
|
||||
- Use appropriate status codes and format JSON responses correctly.
|
||||
- Implement input validation for API endpoints.
|
||||
- Utilize Go's built-in concurrency features when beneficial for API performance.
|
||||
- Follow RESTful API design principles and best practices.
|
||||
- Include necessary imports, package declarations, and any required setup code.
|
||||
- Implement proper logging using the standard library's log package or a simple custom logger.
|
||||
- Consider implementing middleware for cross-cutting concerns (e.g., logging, authentication).
|
||||
- Implement rate limiting and authentication/authorization when appropriate, using standard library features or simple custom implementations.
|
||||
- Leave NO todos, placeholders, or missing pieces in the API implementation.
|
||||
- Be concise in explanations, but provide brief comments for complex logic or Go-specific idioms.
|
||||
- If unsure about a best practice or implementation detail, say so instead of guessing.
|
||||
- Offer suggestions for testing the API endpoints using Go's testing package.
|
||||
When searching for schemas look at https://github.com/coollabsio/coolify/blob/main/openapi.yaml to find the most up to date schema for the struct we are looking to define. Make sure when creating a schema that you place the struct in cmd/coolTypes package.
|
||||
|
||||
Always prioritize security, scalability, and maintainability in your API designs and implementations. Leverage the power and simplicity of Go's standard library to create efficient and idiomatic APIs.
|
||||
- First think step-by-step - describe your plan for the CLI structure, commands, and user interaction flow in pseudocode, written out in great detail.
|
||||
- Confirm the plan, then write code!
|
||||
- Write correct, up-to-date, bug-free, fully functional, secure, and efficient Go code for CLI applications.
|
||||
- Use Cobra for command-line interface development:
|
||||
- Organize commands in a clear, hierarchical structure
|
||||
- Implement proper command flags and arguments
|
||||
- Use persistent flags when appropriate
|
||||
- Follow Cobra's best practices for command organization
|
||||
- Implement proper command aliases and short descriptions
|
||||
- Use Bubble Tea for terminal user interfaces:
|
||||
- Design intuitive and responsive terminal UIs
|
||||
- Implement proper state management
|
||||
- Handle user input appropriately
|
||||
- Use appropriate Bubble Tea components and styling
|
||||
- Follow terminal UI best practices
|
||||
- Implement proper error handling, including custom error types when beneficial
|
||||
- Use appropriate exit codes and error messages
|
||||
- Implement input validation for command arguments and flags
|
||||
- Utilize Go's built-in concurrency features when beneficial for CLI performance
|
||||
- Follow CLI design principles and best practices:
|
||||
- Keep commands simple and focused
|
||||
- Use clear, consistent naming conventions
|
||||
- Provide helpful usage information
|
||||
- Implement proper help text and documentation
|
||||
- Include necessary imports, package declarations, and any required setup code
|
||||
- Implement proper logging using appropriate CLI-friendly logging packages
|
||||
- Consider implementing middleware for cross-cutting concerns (e.g., logging, configuration)
|
||||
- Implement proper configuration management when appropriate
|
||||
- Leave NO todos, placeholders, or missing pieces in the CLI implementation
|
||||
- Be concise in explanations, but provide brief comments for complex logic or Go-specific idioms
|
||||
- If unsure about a best practice or implementation detail, say so instead of guessing
|
||||
- Offer suggestions for testing the CLI commands using Go's testing package
|
||||
|
||||
Always prioritize user experience, maintainability, and cross-platform compatibility in your CLI designs and implementations. Leverage the power of Cobra and Bubble Tea to create efficient and user-friendly terminal applications.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
## Changes
|
||||
|
||||
-
|
||||
|
||||
## Issues & Discussions
|
||||
|
||||
- fix #
|
||||
@@ -10,47 +10,18 @@ jobs:
|
||||
release-cli:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
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
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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."
|
||||
+6
-9
@@ -1,12 +1,9 @@
|
||||
coolify-cli
|
||||
/coolify
|
||||
cli-coolify
|
||||
coolify
|
||||
cli
|
||||
config.json
|
||||
.claude
|
||||
dist
|
||||
.vagrant
|
||||
.test
|
||||
|
||||
# Generated documentation (can be regenerated)
|
||||
man/
|
||||
docs/cli/
|
||||
dist/
|
||||
|
||||
# Test coverage
|
||||
coverage.out
|
||||
+13
-66
@@ -1,75 +1,22 @@
|
||||
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
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- style
|
||||
- performance
|
||||
disabled-checks:
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
|
||||
staticcheck:
|
||||
checks: ["all", "-ST1005", "-S1016"]
|
||||
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
|
||||
gosmopolitan:
|
||||
allow-time-local: true
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- std-error-handling
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- goimports
|
||||
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/coollabsio)
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- "pkg/gen/*.go"
|
||||
|
||||
+12
-25
@@ -1,19 +1,8 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: coolify
|
||||
binary: coolify
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s
|
||||
- -w
|
||||
- -X github.com/coollabsio/coolify-cli/internal/version.version={{ .Version }}
|
||||
main: ./coolify/main.go
|
||||
- binary: coolify
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
@@ -21,19 +10,17 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X github.com/coollabsio/cli-coolify/cmd/runtime.Version={{.Version}}
|
||||
env:
|
||||
- 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]
|
||||
- formats: ['tar.gz']
|
||||
name_template: >-
|
||||
coolify_{{ .Version }}_
|
||||
{{- .Os }}_{{ .Arch }}
|
||||
checksum:
|
||||
name_template: 'coolify_{{ .Version }}_checksums.txt'
|
||||
release:
|
||||
prerelease: auto
|
||||
make_latest: "{{ not .Prerelease }}"
|
||||
-587
@@ -1,587 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,344 +0,0 @@
|
||||
# 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
|
||||
- **Raw JSON**: https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/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
@@ -1,620 +0,0 @@
|
||||
# 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! 🚀
|
||||
@@ -1,123 +0,0 @@
|
||||
# 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
|
||||
@@ -1,533 +1,100 @@
|
||||
# CLI for [Coolify](https://coolify.io) API
|
||||
|
||||
> [!WARNING]
|
||||
> Until version 1.0.0, the CLI should be considered unstable. Any minor or patch release may introduce breaking changes. Please read the release notes carefully before updating.
|
||||
|
||||
## Installation
|
||||
|
||||
### Install script (recommended)
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/coollabsio/cli-coolify/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
#### Linux/macOS
|
||||
This will install the CLI in `/usr/local/bin/coolify`.
|
||||
|
||||
> If you are a Windows or macOS user, please test the installation script and let us know if it works for you.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
Before using any commands, you need to initialize the CLI by creating a configuration file:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
|
||||
coolify init
|
||||
```
|
||||
|
||||
It will install the CLI in `/usr/local/bin/coolify` and the configuration file in `~/.config/coolify/config.json`
|
||||
This interactive wizard will guide you through setting up your Coolify instance(s). You can choose to:
|
||||
- Connect to Coolify Cloud using your API token
|
||||
- Add self-hosted Coolify instance(s) with their FQDN and token
|
||||
|
||||
#### Windows (PowerShell)
|
||||
Alternatively, you can generate a default configuration non-interactively:
|
||||
|
||||
```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
|
||||
coolify init --default
|
||||
```
|
||||
|
||||
This will install the `coolify` binary in your `$GOPATH/bin` directory (usually `~/go/bin`). Make sure this directory is in your `$PATH`.
|
||||
The configuration will be stored in `~/.config/coolify/config.json`.
|
||||
|
||||
### Using the install script
|
||||
## Getting Your API Token
|
||||
|
||||
To use the CLI, you'll need an API token:
|
||||
1. Log in to your Coolify dashboard (Cloud or self-hosted)
|
||||
2. Navigate to `/security/api-tokens`
|
||||
3. Create a new token with appropriate permissions
|
||||
4. Use this token when initializing the CLI or adding a new instance
|
||||
|
||||
## Getting Started
|
||||
1. Get a `<token>` from your Coolify dashboard (Cloud or self-hosted) at `/security/api-tokens`
|
||||
## Managing Instances
|
||||
|
||||
### Cloud
|
||||
After initialization, you can manage your Coolify instances:
|
||||
|
||||
2. Add the token with `coolify context set-token cloud <token>`
|
||||
### Add a New Instance
|
||||
|
||||
### Self-hosted
|
||||
```bash
|
||||
coolify instances add MyInstance https://my.instance.tld mytoken
|
||||
```
|
||||
|
||||
2. Add the token with `coolify context add -d <context_name> <url> <token>`
|
||||
Or use the interactive mode:
|
||||
|
||||
> Replace `<context_name>` with the name you want to give to the context.
|
||||
>
|
||||
> Replace `<url>` with the fully qualified domain name of your Coolify instance.
|
||||
```bash
|
||||
coolify instances add
|
||||
```
|
||||
|
||||
Now you can use the CLI with the token you just added.
|
||||
### List All Instances
|
||||
|
||||
```bash
|
||||
coolify instances list
|
||||
```
|
||||
|
||||
### Set Default Instance
|
||||
|
||||
```bash
|
||||
coolify instances set default MyInstance
|
||||
```
|
||||
|
||||
### Remove an Instance
|
||||
|
||||
```bash
|
||||
coolify instances remove MyInstance
|
||||
```
|
||||
|
||||
### Update Instance Token
|
||||
|
||||
```bash
|
||||
coolify instances set token MyInstance newtoken
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||
### Servers
|
||||
|
||||
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>` - Update an 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
|
||||
- `--runtime` - Available at runtime
|
||||
- `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>` - Update an environment variable
|
||||
- `--key <key>` - Variable key (required)
|
||||
- `--value <value>` - Variable value (required)
|
||||
- `--build-time` - Available at build time
|
||||
- `--is-literal` - Treat value as literal (don't interpolate variables)
|
||||
- `--is-multiline` - Value is multiline
|
||||
- `--runtime` - Available at runtime
|
||||
- `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
|
||||
- `coolify servers list` - List all servers
|
||||
- `coolify servers get` - Get a server
|
||||
- `--resources` - Get the resources and their status of a server
|
||||
@@ -1,47 +0,0 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/cmd/application/create"
|
||||
"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(create.NewCreateCommand())
|
||||
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
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package create
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// NewCreateCommand creates the create parent command with all subcommands
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new application",
|
||||
Long: `Create a new application from various sources.
|
||||
|
||||
Available source types:
|
||||
public Create from a public git repository
|
||||
github Create from a private repository using GitHub App
|
||||
deploy-key Create from a private repository using SSH deploy key
|
||||
dockerfile Create from a custom Dockerfile
|
||||
dockerimage Create from a pre-built Docker image
|
||||
|
||||
Examples:
|
||||
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--git-repository "https://github.com/user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--github-app-uuid <uuid> --git-repository "user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--docker-registry-image-name "nginx:latest" --ports-exposes 80`,
|
||||
}
|
||||
|
||||
// Add all create subcommands
|
||||
cmd.AddCommand(NewPublicCommand())
|
||||
cmd.AddCommand(NewGitHubCommand())
|
||||
cmd.AddCommand(NewDeployKeyCommand())
|
||||
cmd.AddCommand(NewDockerfileCommand())
|
||||
cmd.AddCommand(NewDockerImageCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package create
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// NewDeployKeyCommand returns the create deploy-key application command
|
||||
func NewDeployKeyCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy-key",
|
||||
Short: "Create an application from a private repository using SSH deploy key",
|
||||
Long: `Create a new application from a private git repository using SSH deploy key authentication.
|
||||
|
||||
Use 'coolify privatekeys list' to find your private key UUID.
|
||||
|
||||
Examples:
|
||||
coolify app create deploy-key --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--private-key-uuid <uuid> --git-repository "git@github.com:owner/repo.git" --git-branch main \
|
||||
--build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create deploy-key --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--private-key-uuid <uuid> --git-repository "git@gitlab.com:owner/repo.git" --git-branch main \
|
||||
--build-pack dockerfile --ports-exposes 8080 --instant-deploy`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
privateKeyUUID, _ := cmd.Flags().GetString("private-key-uuid")
|
||||
gitRepository, _ := cmd.Flags().GetString("git-repository")
|
||||
gitBranch, _ := cmd.Flags().GetString("git-branch")
|
||||
buildPack, _ := cmd.Flags().GetString("build-pack")
|
||||
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if privateKeyUUID == "" {
|
||||
return fmt.Errorf("--private-key-uuid is required")
|
||||
}
|
||||
if gitRepository == "" || gitBranch == "" {
|
||||
return fmt.Errorf("--git-repository and --git-branch are required")
|
||||
}
|
||||
if buildPack == "" || portsExposes == "" {
|
||||
return fmt.Errorf("--build-pack and --ports-exposes are required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreateDeployKeyRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
PrivateKeyUUID: privateKeyUUID,
|
||||
GitRepository: gitRepository,
|
||||
GitBranch: gitBranch,
|
||||
BuildPack: buildPack,
|
||||
PortsExposes: portsExposes,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
|
||||
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
|
||||
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
|
||||
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
|
||||
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
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.CreateDeployKey(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create 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)
|
||||
},
|
||||
}
|
||||
|
||||
// Required 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("private-key-uuid", "", "Private key UUID (required)")
|
||||
cmd.Flags().String("git-repository", "", "Git repository SSH URL, e.g., 'git@github.com:owner/repo.git' (required)")
|
||||
cmd.Flags().String("git-branch", "", "Git branch (required)")
|
||||
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("build-command", "", "Custom build command")
|
||||
cmd.Flags().String("start-command", "", "Custom start command")
|
||||
cmd.Flags().String("install-command", "", "Custom install command")
|
||||
cmd.Flags().String("base-directory", "", "Base directory for the application")
|
||||
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
cmd.Flags().String("limits-memory", "", "Memory limit")
|
||||
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
|
||||
cmd.Flags().String("health-check-path", "", "Health check path")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package create
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// NewDockerfileCommand returns the create dockerfile application command
|
||||
func NewDockerfileCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dockerfile",
|
||||
Short: "Create an application from a custom Dockerfile",
|
||||
Long: `Create a new application from a custom Dockerfile content.
|
||||
|
||||
Examples:
|
||||
coolify app create dockerfile --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--dockerfile "FROM node:18\nWORKDIR /app\nCOPY . .\nRUN npm install\nCMD [\"npm\", \"start\"]"
|
||||
|
||||
coolify app create dockerfile --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--dockerfile "$(cat Dockerfile)" --ports-exposes 3000 --instant-deploy`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
dockerfile, _ := cmd.Flags().GetString("dockerfile")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if dockerfile == "" {
|
||||
return fmt.Errorf("--dockerfile is required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreateDockerfileRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
Dockerfile: dockerfile,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "ports-exposes", &req.PortsExposes)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
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.CreateDockerfile(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create 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)
|
||||
},
|
||||
}
|
||||
|
||||
// Required 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("dockerfile", "", "Dockerfile content (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080'")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
cmd.Flags().String("limits-memory", "", "Memory limit")
|
||||
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
|
||||
cmd.Flags().String("health-check-path", "", "Health check path")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package create
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// NewDockerImageCommand returns the create dockerimage application command
|
||||
func NewDockerImageCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dockerimage",
|
||||
Short: "Create an application from a pre-built Docker image",
|
||||
Long: `Create a new application from a pre-built Docker image from a registry.
|
||||
|
||||
Examples:
|
||||
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--docker-registry-image-name "nginx:latest" --ports-exposes 80
|
||||
|
||||
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--docker-registry-image-name "ghcr.io/myorg/myapp" --docker-registry-image-tag "v1.0.0" \
|
||||
--ports-exposes 3000 --domains "myapp.example.com" --instant-deploy`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
dockerRegistryImageName, _ := cmd.Flags().GetString("docker-registry-image-name")
|
||||
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if dockerRegistryImageName == "" {
|
||||
return fmt.Errorf("--docker-registry-image-name is required")
|
||||
}
|
||||
if portsExposes == "" {
|
||||
return fmt.Errorf("--ports-exposes is required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreateDockerImageRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
DockerRegistryImageName: dockerRegistryImageName,
|
||||
PortsExposes: portsExposes,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "docker-registry-image-tag", &req.DockerRegistryImageTag)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
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.CreateDockerImage(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create 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)
|
||||
},
|
||||
}
|
||||
|
||||
// Required 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("docker-registry-image-name", "", "Docker image name from registry (required)")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '80' or '80,443' (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("docker-registry-image-tag", "", "Docker image tag (defaults to 'latest')")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
cmd.Flags().String("limits-memory", "", "Memory limit")
|
||||
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
|
||||
cmd.Flags().String("health-check-path", "", "Health check path")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package create
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// NewGitHubCommand returns the create github application command
|
||||
func NewGitHubCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "github",
|
||||
Short: "Create an application from a private repository using GitHub App",
|
||||
Long: `Create a new application from a private git repository using GitHub App authentication.
|
||||
|
||||
Use 'coolify github list' to find your GitHub App UUID.
|
||||
Use 'coolify github repos <app-uuid>' to list accessible repositories.
|
||||
|
||||
Examples:
|
||||
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--github-app-uuid <uuid> --git-repository "owner/repo" --git-branch main \
|
||||
--build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--github-app-uuid <uuid> --git-repository "owner/repo" --git-branch main \
|
||||
--build-pack dockerfile --ports-exposes 8080 --instant-deploy`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
gitHubAppUUID, _ := cmd.Flags().GetString("github-app-uuid")
|
||||
gitRepository, _ := cmd.Flags().GetString("git-repository")
|
||||
gitBranch, _ := cmd.Flags().GetString("git-branch")
|
||||
buildPack, _ := cmd.Flags().GetString("build-pack")
|
||||
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if gitHubAppUUID == "" {
|
||||
return fmt.Errorf("--github-app-uuid is required")
|
||||
}
|
||||
if gitRepository == "" || gitBranch == "" {
|
||||
return fmt.Errorf("--git-repository and --git-branch are required")
|
||||
}
|
||||
if buildPack == "" || portsExposes == "" {
|
||||
return fmt.Errorf("--build-pack and --ports-exposes are required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreateGitHubAppRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
GitHubAppUUID: gitHubAppUUID,
|
||||
GitRepository: gitRepository,
|
||||
GitBranch: gitBranch,
|
||||
BuildPack: buildPack,
|
||||
PortsExposes: portsExposes,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
|
||||
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
|
||||
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
|
||||
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
|
||||
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
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.CreateGitHubApp(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create 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)
|
||||
},
|
||||
}
|
||||
|
||||
// Required 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("github-app-uuid", "", "GitHub App UUID (required)")
|
||||
cmd.Flags().String("git-repository", "", "Git repository in format 'owner/repo' (required)")
|
||||
cmd.Flags().String("git-branch", "", "Git branch (required)")
|
||||
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("build-command", "", "Custom build command")
|
||||
cmd.Flags().String("start-command", "", "Custom start command")
|
||||
cmd.Flags().String("install-command", "", "Custom install command")
|
||||
cmd.Flags().String("base-directory", "", "Base directory for the application")
|
||||
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
cmd.Flags().String("limits-memory", "", "Memory limit")
|
||||
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
|
||||
cmd.Flags().String("health-check-path", "", "Health check path")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package create
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// NewPublicCommand returns the create public application command
|
||||
func NewPublicCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "public",
|
||||
Short: "Create an application from a public git repository",
|
||||
Long: `Create a new application from a public git repository.
|
||||
|
||||
Examples:
|
||||
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--git-repository "https://github.com/user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--git-repository "https://github.com/user/repo" --git-branch main --build-pack dockerfile --ports-exposes 8080 \
|
||||
--instant-deploy --domains "myapp.example.com"`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
gitRepository, _ := cmd.Flags().GetString("git-repository")
|
||||
gitBranch, _ := cmd.Flags().GetString("git-branch")
|
||||
buildPack, _ := cmd.Flags().GetString("build-pack")
|
||||
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if gitRepository == "" || gitBranch == "" {
|
||||
return fmt.Errorf("--git-repository and --git-branch are required")
|
||||
}
|
||||
if buildPack == "" || portsExposes == "" {
|
||||
return fmt.Errorf("--build-pack and --ports-exposes are required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreatePublicRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
GitRepository: gitRepository,
|
||||
GitBranch: gitBranch,
|
||||
BuildPack: buildPack,
|
||||
PortsExposes: portsExposes,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
|
||||
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
|
||||
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
|
||||
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
|
||||
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
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.CreatePublic(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create 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)
|
||||
},
|
||||
}
|
||||
|
||||
// Required 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("git-repository", "", "Git repository URL (required)")
|
||||
cmd.Flags().String("git-branch", "", "Git branch (required)")
|
||||
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("build-command", "", "Custom build command")
|
||||
cmd.Flags().String("start-command", "", "Custom start command")
|
||||
cmd.Flags().String("install-command", "", "Custom install command")
|
||||
cmd.Flags().String("base-directory", "", "Base directory for the application")
|
||||
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
cmd.Flags().String("limits-memory", "", "Memory limit")
|
||||
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
|
||||
cmd.Flags().String("health-check-path", "", "Health check path")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Helper functions for optional flags
|
||||
func setOptionalStringFlag(cmd *cobra.Command, flagName string, target **string) {
|
||||
if cmd.Flags().Changed(flagName) {
|
||||
val, _ := cmd.Flags().GetString(flagName)
|
||||
*target = &val
|
||||
}
|
||||
}
|
||||
|
||||
func setOptionalBoolFlag(cmd *cobra.Command, flagName string, target **bool) {
|
||||
if cmd.Flags().Changed(flagName) {
|
||||
val, _ := cmd.Flags().GetBool(flagName)
|
||||
*target = &val
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
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
@@ -1,84 +0,0 @@
|
||||
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
@@ -1,59 +0,0 @@
|
||||
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
@@ -1,60 +0,0 @@
|
||||
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
@@ -1,87 +0,0 @@
|
||||
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
@@ -1,159 +0,0 @@
|
||||
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
-90
@@ -1,90 +0,0 @@
|
||||
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>",
|
||||
Short: "Update an environment variable",
|
||||
Long: `Update an existing environment variable. UUID is the application.`,
|
||||
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)
|
||||
}
|
||||
|
||||
// Check minimum version requirement
|
||||
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.469"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &models.EnvironmentVariableUpdateRequest{}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("--key is required")
|
||||
}
|
||||
if req.Value == nil {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
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,46 @@
|
||||
package ask
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func PromptYesOrNo(question string, defaultToYes bool) (bool, error) {
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
if defaultToYes {
|
||||
fmt.Fprintf(os.Stderr, "%s [Y/n]: ", question)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s [y/N]: ", question)
|
||||
}
|
||||
for {
|
||||
answer, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
|
||||
return defaultToYes, err
|
||||
}
|
||||
answer = strings.ToLower(strings.TrimSpace(answer))
|
||||
switch answer {
|
||||
case "y", "yes":
|
||||
return true, nil
|
||||
case "n", "no":
|
||||
return false, nil
|
||||
case "":
|
||||
return defaultToYes, nil
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Please answer with 'y' or 'n': ")
|
||||
}
|
||||
}
|
||||
|
||||
func PromptString(question string) (string, error) {
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
fmt.Fprintf(os.Stderr, "%s: ", question)
|
||||
|
||||
answer, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(answer), nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package cliinit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type cliInit struct {
|
||||
coolify runtime.Getter
|
||||
}
|
||||
|
||||
func New(c runtime.Getter) *cliInit {
|
||||
return &cliInit{
|
||||
coolify: c,
|
||||
}
|
||||
}
|
||||
|
||||
var defaultInstances = []coolTypes.Instance{
|
||||
{
|
||||
Name: "cloud",
|
||||
Default: true,
|
||||
Fqdn: "https://app.coolify.io",
|
||||
Token: "",
|
||||
}, {
|
||||
Name: "localhost",
|
||||
Fqdn: "http://localhost:8000",
|
||||
Token: "",
|
||||
},
|
||||
}
|
||||
|
||||
func (c *cliInit) NewCommand() *cobra.Command {
|
||||
generateDefault := false
|
||||
force := false
|
||||
cmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s init
|
||||
%[1]s init --default
|
||||
%[1]s init --force
|
||||
`),
|
||||
Short: "Initialize a new Coolify CLI configuration file",
|
||||
Long: `
|
||||
Initialize Coolify CLI by generating a configuration file in the default directory.
|
||||
`,
|
||||
SilenceUsage: true,
|
||||
Args: cobra.NoArgs,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if c.coolify().Config.JsonExists && !force {
|
||||
return errors.New("configuration file already exists. Please use instances command to make further modifications or force flag to regenerate a new configuration file")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if generateDefault {
|
||||
viper.Set("instances", defaultInstances)
|
||||
cmd.Println("Configuration file generated with default instances, use the instances command to make further modifications.")
|
||||
return c.coolify().Save()
|
||||
}
|
||||
|
||||
// Create a channel to receive the instances
|
||||
result := make(chan []coolTypes.Instance)
|
||||
p := tea.NewProgram(newInitModel(result))
|
||||
|
||||
// Create a done channel to signal when the program is finished
|
||||
done := make(chan struct{})
|
||||
var programErr error
|
||||
|
||||
// Run the program in a goroutine
|
||||
go func() {
|
||||
_, programErr = p.Run()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait for either the instances or context cancellation
|
||||
var instances []coolTypes.Instance
|
||||
select {
|
||||
case instances = <-result:
|
||||
case <-cmd.Context().Done():
|
||||
return fmt.Errorf("operation cancelled")
|
||||
case <-done:
|
||||
if programErr != nil {
|
||||
return fmt.Errorf("program error: %v", programErr)
|
||||
}
|
||||
return fmt.Errorf("program exited without saving instances")
|
||||
}
|
||||
|
||||
viper.Set("instances", instances)
|
||||
return c.coolify().Save()
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&generateDefault, "default", "d", false, "Generate a default configuration file (non-interactive)")
|
||||
flags.BoolVarP(&force, "force", "f", false, "Force the generation of a new configuration file")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package cliinit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
)
|
||||
|
||||
var (
|
||||
checkboxStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99"))
|
||||
checked = checkboxStyle.Render("[x]")
|
||||
unchecked = checkboxStyle.Render("[ ]")
|
||||
goldStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Bold(true)
|
||||
)
|
||||
|
||||
// initKeyMap defines keybindings for the initialization form
|
||||
type initKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Tab key.Binding
|
||||
Space key.Binding
|
||||
Enter key.Binding
|
||||
Paste key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
// ShortHelp returns keybindings to be shown in the mini help view
|
||||
func (k initKeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Help, k.Quit}
|
||||
}
|
||||
|
||||
// FullHelp returns keybindings for the expanded help view
|
||||
func (k initKeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Tab}, // first column
|
||||
{k.Space, k.Enter, k.Paste, k.Help}, // second column
|
||||
{k.Quit}, // third column
|
||||
}
|
||||
}
|
||||
|
||||
var initKeys = initKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "move up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "move down"),
|
||||
),
|
||||
Tab: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("tab", "next field"),
|
||||
),
|
||||
Space: key.NewBinding(
|
||||
key.WithKeys(" "),
|
||||
key.WithHelp("space", "toggle checkbox"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "continue"),
|
||||
),
|
||||
Paste: key.NewBinding(
|
||||
key.WithKeys("ctrl+v"),
|
||||
key.WithHelp("ctrl+v", "paste"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "toggle help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("esc", "ctrl+c"),
|
||||
key.WithHelp("esc", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
type initModel struct {
|
||||
instances []coolTypes.Instance
|
||||
width int
|
||||
height int
|
||||
focus int
|
||||
err error
|
||||
useCloud bool
|
||||
useSelfHost bool
|
||||
cloudToken textinput.Model
|
||||
selfHostName textinput.Model
|
||||
selfHostFqdn textinput.Model
|
||||
selfHostToken textinput.Model
|
||||
result chan<- []coolTypes.Instance
|
||||
step int // Current step in the initialization process
|
||||
tick int // For rainbow effect
|
||||
keys initKeyMap
|
||||
help help.Model
|
||||
}
|
||||
|
||||
func newInitModel(result chan<- []coolTypes.Instance) initModel {
|
||||
cloudToken := textinput.New()
|
||||
cloudToken.Placeholder = "Enter your Coolify Cloud token"
|
||||
cloudToken.Prompt = "Cloud Token: "
|
||||
cloudToken.PromptStyle = tui.FocusedStyle
|
||||
cloudToken.TextStyle = tui.FocusedStyle
|
||||
cloudToken.Validate = tui.ValidateNotEmpty
|
||||
|
||||
selfHostName := textinput.New()
|
||||
selfHostName.Placeholder = "Enter name for self-hosted instance"
|
||||
selfHostName.Prompt = "Name: "
|
||||
selfHostName.PromptStyle = tui.FocusedStyle
|
||||
selfHostName.TextStyle = tui.FocusedStyle
|
||||
selfHostName.Validate = tui.ValidateNotEmpty
|
||||
|
||||
selfHostFqdn := textinput.New()
|
||||
selfHostFqdn.Placeholder = "Enter FQDN for self-hosted instance"
|
||||
selfHostFqdn.Prompt = "FQDN: "
|
||||
selfHostFqdn.PromptStyle = tui.FocusedStyle
|
||||
selfHostFqdn.TextStyle = tui.FocusedStyle
|
||||
selfHostFqdn.Validate = tui.ValidateFQDN
|
||||
|
||||
selfHostToken := textinput.New()
|
||||
selfHostToken.Placeholder = "Enter token for self-hosted instance"
|
||||
selfHostToken.Prompt = "Token: "
|
||||
selfHostToken.PromptStyle = tui.FocusedStyle
|
||||
selfHostToken.TextStyle = tui.FocusedStyle
|
||||
selfHostToken.Validate = tui.ValidateNotEmpty
|
||||
|
||||
return initModel{
|
||||
instances: make([]coolTypes.Instance, 0),
|
||||
focus: 0,
|
||||
result: result,
|
||||
step: 0,
|
||||
cloudToken: cloudToken,
|
||||
selfHostName: selfHostName,
|
||||
selfHostFqdn: selfHostFqdn,
|
||||
selfHostToken: selfHostToken,
|
||||
keys: initKeys,
|
||||
help: help.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m initModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m initModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Quit):
|
||||
return m, tea.Quit
|
||||
case key.Matches(msg, m.keys.Help):
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
return m, nil
|
||||
case key.Matches(msg, m.keys.Space):
|
||||
// Space toggles checkbox when on step 0 or 2
|
||||
switch m.step {
|
||||
case 0:
|
||||
m.useCloud = !m.useCloud
|
||||
return m, nil
|
||||
case 2:
|
||||
m.useSelfHost = !m.useSelfHost
|
||||
return m, nil
|
||||
}
|
||||
case key.Matches(msg, m.keys.Enter):
|
||||
switch m.step {
|
||||
case 0:
|
||||
// Enter handles progression
|
||||
if m.useCloud {
|
||||
m.step++
|
||||
m.focus = 1
|
||||
m.cloudToken.Focus()
|
||||
} else {
|
||||
m.step += 2
|
||||
m.focus = 2
|
||||
}
|
||||
case 1:
|
||||
if m.useCloud {
|
||||
// Check for validation errors
|
||||
if m.cloudToken.Err != nil {
|
||||
m.err = m.cloudToken.Err
|
||||
return m, nil
|
||||
}
|
||||
// Manual validation in case field hasn't been edited
|
||||
if m.cloudToken.Value() == "" {
|
||||
m.err = errors.New("token is required when using Coolify Cloud")
|
||||
return m, nil
|
||||
}
|
||||
m.step++
|
||||
m.focus = 2
|
||||
m.cloudToken.Blur()
|
||||
}
|
||||
case 2:
|
||||
// Enter handles progression
|
||||
if m.useSelfHost {
|
||||
m.step++
|
||||
m.focus = 3
|
||||
m.selfHostName.Focus()
|
||||
} else {
|
||||
// If self-hosted is false, build instances and quit
|
||||
if m.useCloud {
|
||||
m.instances = append(m.instances, coolTypes.Instance{
|
||||
Name: "cloud",
|
||||
Default: true,
|
||||
Fqdn: "https://app.coolify.io",
|
||||
Token: m.cloudToken.Value(),
|
||||
})
|
||||
}
|
||||
// Send instances back to command
|
||||
if m.result != nil {
|
||||
m.result <- m.instances
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
case 3:
|
||||
cloudToken := strings.TrimSpace(m.cloudToken.Value())
|
||||
if m.useSelfHost {
|
||||
// Check for validation errors
|
||||
if m.selfHostName.Err != nil || m.selfHostFqdn.Err != nil || m.selfHostToken.Err != nil {
|
||||
m.err = errors.New("please fix all field errors before submitting")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
selfHostName := strings.TrimSpace(m.selfHostName.Value())
|
||||
selfHostFqdn := strings.TrimSpace(m.selfHostFqdn.Value())
|
||||
selfHostToken := strings.TrimSpace(m.selfHostToken.Value())
|
||||
// Manual validation in case fields haven't been edited
|
||||
if selfHostName == "" {
|
||||
m.err = errors.New("name is required for self-hosted instance")
|
||||
return m, nil
|
||||
}
|
||||
if selfHostFqdn == "" {
|
||||
m.err = errors.New("FQDN is required for self-hosted instance")
|
||||
return m, nil
|
||||
}
|
||||
if selfHostToken == "" {
|
||||
m.err = errors.New("token is required for self-hosted instance")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Build instances array
|
||||
if m.useCloud {
|
||||
m.instances = append(m.instances, coolTypes.Instance{
|
||||
Name: "cloud",
|
||||
Default: true,
|
||||
Fqdn: "https://app.coolify.io",
|
||||
Token: cloudToken,
|
||||
})
|
||||
}
|
||||
m.instances = append(m.instances, coolTypes.Instance{
|
||||
Name: selfHostName,
|
||||
Default: !m.useCloud,
|
||||
Fqdn: selfHostFqdn,
|
||||
Token: selfHostToken,
|
||||
})
|
||||
// Send instances back to command
|
||||
if m.result != nil {
|
||||
m.result <- m.instances
|
||||
}
|
||||
return m, tea.Quit
|
||||
} else {
|
||||
// If self-hosted is false, build instances and quit
|
||||
if m.useCloud {
|
||||
m.instances = append(m.instances, coolTypes.Instance{
|
||||
Name: "cloud",
|
||||
Default: true,
|
||||
Fqdn: "https://app.coolify.io",
|
||||
Token: cloudToken,
|
||||
})
|
||||
}
|
||||
// Send instances back to command
|
||||
if m.result != nil {
|
||||
m.result <- m.instances
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
case key.Matches(msg, m.keys.Up):
|
||||
// Only allow up/down navigation when multiple items are visible
|
||||
if m.step == 3 && m.useSelfHost {
|
||||
m.focus--
|
||||
if m.focus < 3 {
|
||||
m.focus = 5
|
||||
}
|
||||
m.updateFocus()
|
||||
}
|
||||
case key.Matches(msg, m.keys.Down), key.Matches(msg, m.keys.Tab):
|
||||
// Only allow up/down navigation when multiple items are visible
|
||||
if m.step == 3 && m.useSelfHost {
|
||||
m.focus++
|
||||
if m.focus > 5 {
|
||||
m.focus = 3
|
||||
}
|
||||
m.updateFocus()
|
||||
}
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.help.Width = msg.Width
|
||||
}
|
||||
|
||||
// Handle text input updates
|
||||
if m.step == 1 && m.focus == 1 {
|
||||
m.cloudToken, cmd = m.cloudToken.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
} else if m.step == 3 {
|
||||
switch m.focus {
|
||||
case 3:
|
||||
m.selfHostName, cmd = m.selfHostName.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case 4:
|
||||
m.selfHostFqdn, cmd = m.selfHostFqdn.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case 5:
|
||||
m.selfHostToken, cmd = m.selfHostToken.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *initModel) updateFocus() {
|
||||
// Blur all inputs
|
||||
m.cloudToken.Blur()
|
||||
m.selfHostName.Blur()
|
||||
m.selfHostFqdn.Blur()
|
||||
m.selfHostToken.Blur()
|
||||
|
||||
// Focus the selected input
|
||||
switch m.focus {
|
||||
case 1:
|
||||
m.cloudToken.Focus()
|
||||
case 3:
|
||||
m.selfHostName.Focus()
|
||||
case 4:
|
||||
m.selfHostFqdn.Focus()
|
||||
case 5:
|
||||
m.selfHostToken.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
func (m initModel) View() string {
|
||||
if m.width == 0 {
|
||||
return "loading..."
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
|
||||
// Title
|
||||
s.WriteString("Initialize Coolify CLI\n\n")
|
||||
|
||||
// Step 1: Cloud question
|
||||
if m.step == 0 {
|
||||
cloudStyle := tui.BlurredStyle
|
||||
if m.focus == 0 {
|
||||
cloudStyle = tui.FocusedStyle
|
||||
}
|
||||
s.WriteString(cloudStyle.Render("Do you use "))
|
||||
s.WriteString(goldStyle.Render("Coolify Cloud?"))
|
||||
s.WriteString(" ")
|
||||
if m.useCloud {
|
||||
s.WriteString(checked)
|
||||
} else {
|
||||
s.WriteString(unchecked)
|
||||
}
|
||||
s.WriteString("\n")
|
||||
s.WriteString(tui.BlurredStyle.Render("Hint: use spacebar to toggle checkbox\n"))
|
||||
}
|
||||
|
||||
// Step 2: Cloud token input
|
||||
if m.step == 1 && m.useCloud {
|
||||
s.WriteString(m.cloudToken.View())
|
||||
if m.cloudToken.Err != nil {
|
||||
// Display validation error next to input
|
||||
s.WriteString(" ")
|
||||
s.WriteString(tui.ErrorStyle.Render(m.cloudToken.Err.Error()))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
}
|
||||
|
||||
// Step 3: Self-hosted question
|
||||
if m.step == 2 {
|
||||
selfHostStyle := tui.BlurredStyle
|
||||
if m.focus == 2 {
|
||||
selfHostStyle = tui.FocusedStyle
|
||||
}
|
||||
s.WriteString(selfHostStyle.Render("Add self-hosted instance"))
|
||||
s.WriteString(" ")
|
||||
if m.useSelfHost {
|
||||
s.WriteString(checked)
|
||||
} else {
|
||||
s.WriteString(unchecked)
|
||||
}
|
||||
s.WriteString("\n")
|
||||
s.WriteString(tui.BlurredStyle.Render("Hint: use spacebar to toggle checkbox\n"))
|
||||
}
|
||||
|
||||
// Step 4: Self-hosted inputs
|
||||
if m.step == 3 && m.useSelfHost {
|
||||
// Name input
|
||||
s.WriteString(m.selfHostName.View())
|
||||
if m.selfHostName.Err != nil {
|
||||
// Display validation error next to input
|
||||
s.WriteString(" ")
|
||||
s.WriteString(tui.ErrorStyle.Render(m.selfHostName.Err.Error()))
|
||||
}
|
||||
s.WriteString("\n\n")
|
||||
|
||||
// FQDN input
|
||||
s.WriteString(m.selfHostFqdn.View())
|
||||
if m.selfHostFqdn.Err != nil {
|
||||
// Display validation error next to input
|
||||
s.WriteString(" ")
|
||||
s.WriteString(tui.ErrorStyle.Render(m.selfHostFqdn.Err.Error()))
|
||||
}
|
||||
s.WriteString("\n\n")
|
||||
|
||||
// Token input
|
||||
s.WriteString(m.selfHostToken.View())
|
||||
if m.selfHostToken.Err != nil {
|
||||
// Display validation error next to input
|
||||
s.WriteString(" ")
|
||||
s.WriteString(tui.ErrorStyle.Render(m.selfHostToken.Err.Error()))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
}
|
||||
|
||||
// Help view
|
||||
s.WriteString("\n\n")
|
||||
s.WriteString(m.help.View(m.keys))
|
||||
|
||||
// Error message
|
||||
if m.err != nil {
|
||||
s.WriteString("\n\n")
|
||||
s.WriteString(tui.ErrorStyle.Render(m.err.Error()))
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package cliinstances
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func (c *cliInstances) newAddCommand() *cobra.Command {
|
||||
force := false
|
||||
isNewDefault := false
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [name] [fqdn] [token]",
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s instances add MyInstance https://my.instance.tld 1234
|
||||
%[1]s instances add AnotherInstance https://another.instance.tld 5678 --default
|
||||
%[1]s instances add MyInstance https://my.instance.tld 91011 --force
|
||||
%[1]s instances add # Interactive mode
|
||||
`),
|
||||
Short: "Add a new instance",
|
||||
Long: `
|
||||
Add a new instance to the CLI configuration file.
|
||||
If no arguments are provided, an interactive form will be shown.
|
||||
`,
|
||||
Aliases: []string{"create"},
|
||||
SilenceUsage: true,
|
||||
Args: cobra.RangeArgs(0, 3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return c.runInteractiveMode(cmd, force, isNewDefault)
|
||||
} else if len(args) != 3 {
|
||||
return errors.New("command requires either 0 arguments (interactive mode) or exactly 3 arguments (name, fqdn, token)")
|
||||
}
|
||||
return c.runNonInteractiveMode(args, force, isNewDefault)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&force, "force", "f", false, "Force overwrite existing instance with the same name")
|
||||
flags.BoolVarP(&isNewDefault, "default", "d", false, "Set this instance as the default instance")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *cliInstances) runInteractiveMode(cmd *cobra.Command, force, isDefault bool) error {
|
||||
result := make(chan coolTypes.Instance)
|
||||
p := tea.NewProgram(newAddModel(result, force, isDefault))
|
||||
|
||||
// Create a done channel to signal when the program is finished
|
||||
done := make(chan struct{})
|
||||
var programErr error
|
||||
|
||||
// Run the program in a goroutine
|
||||
go func() {
|
||||
_, programErr = p.Run()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait for either the instance or context cancellation
|
||||
var instance coolTypes.Instance
|
||||
select {
|
||||
case instance = <-result:
|
||||
case <-cmd.Context().Done():
|
||||
return fmt.Errorf("operation cancelled")
|
||||
case <-done:
|
||||
if programErr != nil {
|
||||
return fmt.Errorf("program error: %v", programErr)
|
||||
}
|
||||
return fmt.Errorf("program exited without saving instance")
|
||||
}
|
||||
|
||||
// Check for existing instance with same name
|
||||
for i, existing := range c.instances {
|
||||
if existing.Name == instance.Name {
|
||||
if !force {
|
||||
return errors.New("instance with the same name already exists. Use the force flag to overwrite or instances set to modify individual attributes")
|
||||
}
|
||||
c.instances = slices.Delete(c.instances, i, i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isDefault {
|
||||
for i := range c.instances {
|
||||
c.instances[i].Default = false
|
||||
}
|
||||
}
|
||||
|
||||
c.instances = append(c.instances, instance)
|
||||
viper.Set("instances", c.instances)
|
||||
return c.coolify().Save()
|
||||
}
|
||||
|
||||
func (c *cliInstances) runNonInteractiveMode(args []string, force, isNewDefault bool) error {
|
||||
// Check for existing instance with same name
|
||||
for i, instance := range c.instances {
|
||||
if instance.Name == args[0] {
|
||||
if !force {
|
||||
return errors.New("instance with the same name already exists. Use the force flag to overwrite or instances set to modify individual attributes")
|
||||
}
|
||||
c.instances = slices.Delete(c.instances, i, i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newInstance := coolTypes.Instance{
|
||||
Name: args[0],
|
||||
Fqdn: args[1],
|
||||
Token: args[2],
|
||||
Default: isNewDefault,
|
||||
}
|
||||
|
||||
if isNewDefault {
|
||||
for i := range c.instances {
|
||||
c.instances[i].Default = false
|
||||
}
|
||||
}
|
||||
|
||||
c.instances = append(c.instances, newInstance)
|
||||
viper.Set("instances", c.instances)
|
||||
return c.coolify().Save()
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package cliinstances
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
)
|
||||
|
||||
// addKeyMap defines keybindings for the add instance form
|
||||
type addKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Tab key.Binding
|
||||
Enter key.Binding
|
||||
Paste key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
// ShortHelp returns keybindings to be shown in the mini help view
|
||||
func (k addKeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Help, k.Quit}
|
||||
}
|
||||
|
||||
// FullHelp returns keybindings for the expanded help view
|
||||
func (k addKeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Tab}, // first column
|
||||
{k.Enter, k.Paste, k.Help}, // second column
|
||||
{k.Quit}, // third column
|
||||
}
|
||||
}
|
||||
|
||||
var addKeys = addKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "move up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "move down"),
|
||||
),
|
||||
Tab: key.NewBinding(
|
||||
key.WithKeys("tab", "shift+tab"),
|
||||
key.WithHelp("tab", "next field"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "submit/select"),
|
||||
),
|
||||
Paste: key.NewBinding(
|
||||
key.WithKeys("ctrl+v"),
|
||||
key.WithHelp("ctrl+v", "paste"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "toggle help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("esc", "ctrl+c"),
|
||||
key.WithHelp("esc", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
type addModel struct {
|
||||
inputs []textinput.Model
|
||||
focus int
|
||||
err error
|
||||
instance coolTypes.Instance
|
||||
width int
|
||||
height int
|
||||
result chan<- coolTypes.Instance
|
||||
force bool
|
||||
isDefault bool
|
||||
keys addKeyMap
|
||||
help help.Model
|
||||
}
|
||||
|
||||
func newAddModel(result chan<- coolTypes.Instance, force, isDefault bool) addModel {
|
||||
// Create text inputs
|
||||
inputs := make([]textinput.Model, 3)
|
||||
labels := []string{"Name", "FQDN", "Token"}
|
||||
|
||||
for i, label := range labels {
|
||||
input := textinput.New()
|
||||
input.Placeholder = fmt.Sprintf("Enter instance %s", label)
|
||||
input.Prompt = fmt.Sprintf("%s: ", label)
|
||||
input.PromptStyle = tui.FocusedStyle
|
||||
input.TextStyle = tui.FocusedStyle
|
||||
|
||||
// Set up validation for each input type
|
||||
switch label {
|
||||
case "Name":
|
||||
input.Validate = tui.ValidateNotEmpty
|
||||
case "FQDN":
|
||||
input.Validate = tui.ValidateFQDN
|
||||
case "Token":
|
||||
input.Validate = tui.ValidateNotEmpty
|
||||
}
|
||||
|
||||
// Focus first input by default
|
||||
if i == 0 {
|
||||
input.Focus()
|
||||
}
|
||||
|
||||
inputs[i] = input
|
||||
}
|
||||
|
||||
return addModel{
|
||||
inputs: inputs,
|
||||
focus: 0,
|
||||
result: result,
|
||||
force: force,
|
||||
isDefault: isDefault,
|
||||
keys: addKeys,
|
||||
help: help.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m addModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m addModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Quit):
|
||||
return m, tea.Quit
|
||||
case key.Matches(msg, m.keys.Help):
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
case key.Matches(msg, m.keys.Enter):
|
||||
if m.focus == len(m.inputs) {
|
||||
// Submit - first check if any field has validation errors
|
||||
for _, input := range m.inputs {
|
||||
if input.Err != nil {
|
||||
// Don't proceed if any field has validation errors
|
||||
m.err = errors.New("please fix all field errors before submitting")
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Also validate in case fields haven't been edited
|
||||
if err := m.validateOnSubmit(); err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.instance = coolTypes.Instance{
|
||||
Name: strings.TrimSpace(m.inputs[0].Value()),
|
||||
Fqdn: strings.TrimSpace(m.inputs[1].Value()),
|
||||
Token: strings.TrimSpace(m.inputs[2].Value()),
|
||||
Default: m.isDefault,
|
||||
}
|
||||
// Return a command to send the instance
|
||||
return m, func() tea.Msg {
|
||||
if m.result != nil {
|
||||
m.result <- m.instance
|
||||
}
|
||||
return tea.Quit()
|
||||
}
|
||||
} else if m.focus == len(m.inputs)+1 {
|
||||
// Cancel
|
||||
return m, tea.Quit
|
||||
}
|
||||
// Move to next input
|
||||
m.focus++
|
||||
m.updateFocus()
|
||||
case key.Matches(msg, m.keys.Tab):
|
||||
if msg.String() == "tab" {
|
||||
m.focus++
|
||||
} else {
|
||||
m.focus--
|
||||
}
|
||||
|
||||
// Wrap around
|
||||
if m.focus > len(m.inputs)+1 {
|
||||
m.focus = 0
|
||||
} else if m.focus < 0 {
|
||||
m.focus = len(m.inputs) + 1
|
||||
}
|
||||
|
||||
m.updateFocus()
|
||||
case key.Matches(msg, m.keys.Up):
|
||||
m.focus--
|
||||
if m.focus < 0 {
|
||||
m.focus = len(m.inputs) + 1
|
||||
}
|
||||
m.updateFocus()
|
||||
case key.Matches(msg, m.keys.Down):
|
||||
m.focus++
|
||||
if m.focus > len(m.inputs)+1 {
|
||||
m.focus = 0
|
||||
}
|
||||
m.updateFocus()
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.help.Width = msg.Width
|
||||
}
|
||||
|
||||
// Handle text input updates
|
||||
if m.focus < len(m.inputs) {
|
||||
var cmd tea.Cmd
|
||||
m.inputs[m.focus], cmd = m.inputs[m.focus].Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *addModel) updateFocus() {
|
||||
// Blur all inputs
|
||||
for i := range m.inputs {
|
||||
m.inputs[i].Blur()
|
||||
}
|
||||
|
||||
// Focus current input if it's a text input
|
||||
if m.focus < len(m.inputs) {
|
||||
m.inputs[m.focus].Focus()
|
||||
}
|
||||
}
|
||||
|
||||
// validateOnSubmit handles validation for fields that haven't been edited
|
||||
func (m addModel) validateOnSubmit() error {
|
||||
// Trigger validation for all fields
|
||||
for i, input := range m.inputs {
|
||||
// If the field hasn't been edited and is empty, it hasn't triggered validation yet
|
||||
switch i {
|
||||
case 0:
|
||||
return tui.ValidateNotEmpty(input.Value())
|
||||
case 1:
|
||||
return tui.ValidateFQDN(input.Value())
|
||||
case 2:
|
||||
return tui.ValidateNotEmpty(input.Value())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m addModel) View() string {
|
||||
if m.width == 0 {
|
||||
return "loading..."
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
|
||||
// Title
|
||||
s.WriteString("Add New Instance\n\n")
|
||||
|
||||
// Input fields with validation errors
|
||||
for _, input := range m.inputs {
|
||||
s.WriteString(input.View())
|
||||
if input.Err != nil {
|
||||
// Display the validation error next to the input
|
||||
s.WriteString(" ")
|
||||
s.WriteString(tui.ErrorStyle.Render(input.Err.Error()))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
}
|
||||
|
||||
// Submit and Cancel buttons
|
||||
submitStyle := tui.BlurredStyle
|
||||
if m.focus == len(m.inputs) {
|
||||
submitStyle = tui.FocusedStyle
|
||||
}
|
||||
s.WriteString(submitStyle.Render("Submit"))
|
||||
s.WriteString(" ")
|
||||
|
||||
cancelStyle := tui.BlurredStyle
|
||||
if m.focus == len(m.inputs)+1 {
|
||||
cancelStyle = tui.FocusedStyle
|
||||
}
|
||||
s.WriteString(cancelStyle.Render("Cancel"))
|
||||
|
||||
// Help view at the bottom
|
||||
s.WriteString("\n\n")
|
||||
s.WriteString(m.help.View(m.keys))
|
||||
|
||||
// General form error message (if any)
|
||||
if m.err != nil {
|
||||
s.WriteString("\n\n")
|
||||
s.WriteString(tui.ErrorStyle.Render(m.err.Error()))
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cliinstances
|
||||
|
||||
import (
|
||||
cliinstancesset "github.com/coollabsio/cli-coolify/cmd/cliinstances/set"
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type cliInstances struct {
|
||||
coolify runtime.Getter
|
||||
instances []coolTypes.Instance
|
||||
}
|
||||
|
||||
func (c *cliInstances) runtime() *runtime.Coolify {
|
||||
return c.coolify()
|
||||
}
|
||||
|
||||
func New(c runtime.Getter) *cliInstances {
|
||||
return &cliInstances{
|
||||
coolify: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cliInstances) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "instances",
|
||||
Short: "Manage CLI instances",
|
||||
Aliases: []string{"instance"},
|
||||
Long: `
|
||||
Manage CLI instances by adding, removing or setting options for the instance.
|
||||
`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if instances := viper.Get("instances"); instances != nil {
|
||||
return viper.UnmarshalKey("instances", &c.instances)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(c.newAddCommand())
|
||||
cmd.AddCommand(c.newRemoveCommand())
|
||||
cmd.AddCommand(c.newListCommand())
|
||||
cmd.AddCommand(cliinstancesset.New(c.runtime).NewCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package cliinstances
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/cmd/emoji"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// wrappedInstance implements the FilterableItem interface
|
||||
type wrappedInstance struct {
|
||||
instance coolTypes.Instance
|
||||
}
|
||||
|
||||
func (w wrappedInstance) GetFilterValue() string {
|
||||
return w.instance.Name
|
||||
}
|
||||
|
||||
type filterableListModel struct {
|
||||
filterableTable *tui.FilterableTable
|
||||
}
|
||||
|
||||
func (c *cliInstances) handleDelete(item tui.FilterableItem) error {
|
||||
instance := item.(wrappedInstance).instance
|
||||
|
||||
// Don't allow deleting default instance without force flag
|
||||
if instance.Default {
|
||||
return fmt.Errorf("cannot delete default instance. Use 'instances remove %s --force' instead", instance.Name)
|
||||
}
|
||||
|
||||
// Find and remove the instance from the slice
|
||||
for i, existing := range c.instances {
|
||||
if existing.Name == instance.Name {
|
||||
c.instances = append(c.instances[:i], c.instances[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Update viper and save
|
||||
viper.Set("instances", c.instances)
|
||||
return c.coolify().Save()
|
||||
}
|
||||
|
||||
func newFilterableListModel(instances []coolTypes.Instance, sensitive bool, initialFilter string, deleteHandler func(tui.FilterableItem) error) *filterableListModel {
|
||||
columns := []table.Column{
|
||||
{Title: "Name", Width: 30},
|
||||
{Title: "URL", Width: 40},
|
||||
{Title: "Default", Width: 8},
|
||||
}
|
||||
|
||||
// Convert instances to FilterableItems
|
||||
items := make([]tui.FilterableItem, len(instances))
|
||||
for i, instance := range instances {
|
||||
items[i] = wrappedInstance{instance: instance}
|
||||
}
|
||||
|
||||
// Create row builder function
|
||||
rowBuilder := func(item tui.FilterableItem) table.Row {
|
||||
instance := item.(wrappedInstance).instance
|
||||
e := emoji.CrossMark
|
||||
if instance.Default {
|
||||
e = emoji.CheckMarkButton
|
||||
}
|
||||
|
||||
return table.Row{
|
||||
instance.Name,
|
||||
instance.Fqdn,
|
||||
e,
|
||||
}
|
||||
}
|
||||
|
||||
// Create detail view builder function
|
||||
detailBuilder := func(item tui.FilterableItem, sensitive bool) string {
|
||||
instance := item.(wrappedInstance).instance
|
||||
var s strings.Builder
|
||||
|
||||
addSection := func(title, value string) {
|
||||
s.WriteString(tui.FocusedStyle.Bold(true).Render(title + ": "))
|
||||
s.WriteString(value + "\n\n")
|
||||
}
|
||||
|
||||
addSection("Name", instance.Name)
|
||||
addSection("URL", instance.Fqdn)
|
||||
if sensitive {
|
||||
addSection("Token", instance.Token)
|
||||
} else {
|
||||
addSection("Token", "********")
|
||||
}
|
||||
addSection("Default", fmt.Sprintf("%v", instance.Default))
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
ft := tui.NewTableFilter(items, columns, rowBuilder).
|
||||
WithInitialFilter(initialFilter).
|
||||
WithDetailView(detailBuilder).
|
||||
WithDetailHeader("Instance Details").
|
||||
WithDeleteHandler(deleteHandler)
|
||||
|
||||
return &filterableListModel{
|
||||
filterableTable: ft,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *filterableListModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *filterableListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.filterableTable.Update(msg)
|
||||
}
|
||||
|
||||
func (m *filterableListModel) View() string {
|
||||
return m.filterableTable.View()
|
||||
}
|
||||
|
||||
func (c *cliInstances) newListCommand() *cobra.Command {
|
||||
sensitive := false
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [name]",
|
||||
Short: "List all instances",
|
||||
Long: `
|
||||
List all instances from the CLI configuration file.
|
||||
If a name is provided, only instances matching that name will be shown.
|
||||
`,
|
||||
SilenceUsage: true,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
initialFilter := ""
|
||||
if len(args) > 0 {
|
||||
initialFilter = args[0]
|
||||
}
|
||||
|
||||
format, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get format: %v", err)
|
||||
}
|
||||
// If format is json, output JSON and exit
|
||||
if format == "json" {
|
||||
// Filter instances for JSON output
|
||||
filteredInstances := filterInstances(c.instances, initialFilter)
|
||||
|
||||
// If not sensitive, redact tokens
|
||||
if !sensitive {
|
||||
filteredInstances = redactTokens(filteredInstances)
|
||||
}
|
||||
|
||||
// Encode directly to JSON using the struct's annotations
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(filteredInstances)
|
||||
}
|
||||
|
||||
// Run interactive UI
|
||||
p := tea.NewProgram(newFilterableListModel(c.instances, sensitive, initialFilter, c.handleDelete))
|
||||
_, err = p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("program error: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&sensitive, "sensitive", "s", false, "Show sensitive information such as tokens")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// filterInstances filters instances based on a name filter
|
||||
func filterInstances(instances []coolTypes.Instance, filter string) []coolTypes.Instance {
|
||||
if filter == "" {
|
||||
return instances
|
||||
}
|
||||
|
||||
filtered := make([]coolTypes.Instance, 0)
|
||||
for _, instance := range instances {
|
||||
if strings.Contains(strings.ToLower(instance.Name), strings.ToLower(filter)) {
|
||||
filtered = append(filtered, instance)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// redactTokens creates a copy of instances with redacted tokens
|
||||
func redactTokens(instances []coolTypes.Instance) []coolTypes.Instance {
|
||||
redacted := make([]coolTypes.Instance, len(instances))
|
||||
for i, instance := range instances {
|
||||
// Create a copy to avoid modifying original
|
||||
redacted[i] = instance
|
||||
if instance.Token != "" {
|
||||
redacted[i].Token = "********"
|
||||
}
|
||||
}
|
||||
return redacted
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cliinstances
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func (c *cliInstances) newRemoveCommand() *cobra.Command {
|
||||
force := false
|
||||
indexToRemove := -1
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove [name]",
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s instances remove MyInstance
|
||||
%[1]s instances remove localhost --force
|
||||
`),
|
||||
Short: "remove a instance",
|
||||
Long: `
|
||||
remove a instance from CLI configuration file.
|
||||
`,
|
||||
Aliases: []string{"delete"},
|
||||
SilenceUsage: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
for i, instance := range c.instances {
|
||||
if instance.Name == args[0] {
|
||||
if !force && instance.Default {
|
||||
return errors.New("instance is set as default. Please set another instance as default before removing this instance or provide the force flag")
|
||||
}
|
||||
indexToRemove = i
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("instance name is not found in the configuration file")
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c.instances = slices.Delete(c.instances, indexToRemove, indexToRemove+1)
|
||||
viper.Set("instances", c.instances)
|
||||
return c.coolify().Save()
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&force, "force", "f", false, "Force remove instance if set as default")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cliinstancesset
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func (c *cliInstancesSet) newSetDefaultCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "default [name]",
|
||||
Short: "set a instance as default",
|
||||
Long: `
|
||||
set a instance as default from CLI configuration file.
|
||||
`,
|
||||
SilenceUsage: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for i := range c.instances {
|
||||
c.instances[i].Default = c.instances[i].Name == args[0]
|
||||
}
|
||||
viper.Set("instances", c.instances)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package cliinstancesset
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type cliInstancesSet struct {
|
||||
coolify runtime.Getter
|
||||
instances []coolTypes.Instance
|
||||
}
|
||||
|
||||
func New(c runtime.Getter) *cliInstancesSet {
|
||||
return &cliInstancesSet{
|
||||
coolify: c,
|
||||
}
|
||||
}
|
||||
|
||||
// Set command modifies property on a instance. Pre and Post run functions validate all children commands and save the configuration file after the child commands sets a property.
|
||||
// TLDR; children commands dont need to save the configuration file or do any validation "if instances exists".
|
||||
func (c *cliInstancesSet) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "set [command] [args]",
|
||||
Short: "set a property on a instance",
|
||||
Long: `
|
||||
set a property on a instance from CLI configuration file.
|
||||
`,
|
||||
SilenceUsage: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if instances := viper.Get("instances"); instances != nil {
|
||||
err := viper.UnmarshalKey("instances", &c.instances)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Validate all set commands have instance name as the first argument and is found in the configuration file.
|
||||
for _, instance := range c.instances {
|
||||
if instance.Name == args[0] {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("instance name is not found in the configuration file")
|
||||
},
|
||||
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Save the configuration file after setting the property.
|
||||
return c.coolify().Save()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(c.newSetDefaultCommand())
|
||||
cmd.AddCommand(c.newSetTokenCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cliinstancesset
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func (c *cliInstancesSet) newSetTokenCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "token [name] [token]",
|
||||
Short: "set a instance token",
|
||||
Long: `
|
||||
set a instance token from CLI configuration file.
|
||||
`,
|
||||
SilenceUsage: true,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
for i := range c.instances {
|
||||
if c.instances[i].Name == args[0] {
|
||||
c.instances[i].Token = args[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
viper.Set("instances", c.instances)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
package cliprivatekeys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// addKeyMap defines keybindings for the add private key form
|
||||
type addKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Tab key.Binding
|
||||
Enter key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
// ShortHelp returns keybindings to be shown in the mini help view
|
||||
func (k addKeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Help, k.Quit}
|
||||
}
|
||||
|
||||
// FullHelp returns keybindings for the expanded help view
|
||||
func (k addKeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Tab}, // first column
|
||||
{k.Enter, k.Help}, // second column
|
||||
{k.Quit}, // third column
|
||||
}
|
||||
}
|
||||
|
||||
var addKeys = addKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "move up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "move down"),
|
||||
),
|
||||
Tab: key.NewBinding(
|
||||
key.WithKeys("tab", "shift+tab"),
|
||||
key.WithHelp("tab", "next field"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "submit/select"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "toggle help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("esc", "ctrl+c"),
|
||||
key.WithHelp("esc", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
// addKeyModel is the Bubble Tea model for the interactive add key form
|
||||
type addKeyModel struct {
|
||||
nameInput textinput.Model
|
||||
keyInput textinput.Model
|
||||
focusIndex int
|
||||
done bool
|
||||
err error
|
||||
coolify *runtime.Coolify
|
||||
keys addKeyMap
|
||||
help help.Model
|
||||
}
|
||||
|
||||
func initialAddKeyModel(coolify *runtime.Coolify) addKeyModel {
|
||||
m := addKeyModel{
|
||||
coolify: coolify,
|
||||
keys: addKeys,
|
||||
help: help.New(),
|
||||
}
|
||||
|
||||
// Setup name input
|
||||
m.nameInput = tui.NewFocusedInput("My SSH Key", "› ")
|
||||
m.nameInput.CharLimit = 50
|
||||
m.nameInput.Width = 40
|
||||
|
||||
// Setup key input (multi-line)
|
||||
m.keyInput = tui.NewBlurredInput("SSH private key or path to key file", "› ")
|
||||
m.keyInput.CharLimit = 4096
|
||||
m.keyInput.Width = 60
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m addKeyModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m addKeyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if msg, ok := msg.(tea.KeyMsg); ok {
|
||||
if key.Matches(msg, m.keys.Quit) {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Help) {
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Enter) {
|
||||
// Submit on enter when key input is focused
|
||||
if m.focusIndex == 1 {
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
// Otherwise move to next input
|
||||
m.focusIndex++
|
||||
if m.focusIndex > 1 {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
return m, m.updateFocus()
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Tab) {
|
||||
// Cycle focus between inputs
|
||||
if msg.String() == "shift+tab" {
|
||||
m.focusIndex--
|
||||
} else {
|
||||
m.focusIndex++
|
||||
}
|
||||
|
||||
if m.focusIndex > 1 {
|
||||
m.focusIndex = 0
|
||||
} else if m.focusIndex < 0 {
|
||||
m.focusIndex = 1
|
||||
}
|
||||
return m, m.updateFocus()
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Up) {
|
||||
m.focusIndex--
|
||||
if m.focusIndex < 0 {
|
||||
m.focusIndex = 1
|
||||
}
|
||||
return m, m.updateFocus()
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Down) {
|
||||
m.focusIndex++
|
||||
if m.focusIndex > 1 {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
return m, m.updateFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle character input for the active input
|
||||
if m.focusIndex == 0 {
|
||||
var cmd tea.Cmd
|
||||
m.nameInput, cmd = m.nameInput.Update(msg)
|
||||
return m, cmd
|
||||
} else {
|
||||
var cmd tea.Cmd
|
||||
m.keyInput, cmd = m.keyInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
}
|
||||
|
||||
func (m addKeyModel) updateFocus() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
if m.focusIndex == 0 {
|
||||
m.nameInput.PromptStyle = tui.FocusedStyle
|
||||
m.nameInput.TextStyle = tui.FocusedStyle
|
||||
m.keyInput.PromptStyle = tui.BlurredStyle
|
||||
m.keyInput.TextStyle = tui.BlurredStyle
|
||||
cmds = append(cmds, m.nameInput.Focus())
|
||||
m.keyInput.Blur()
|
||||
} else {
|
||||
m.keyInput.PromptStyle = tui.FocusedStyle
|
||||
m.keyInput.TextStyle = tui.FocusedStyle
|
||||
m.nameInput.PromptStyle = tui.BlurredStyle
|
||||
m.nameInput.TextStyle = tui.BlurredStyle
|
||||
cmds = append(cmds, m.keyInput.Focus())
|
||||
m.nameInput.Blur()
|
||||
}
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m addKeyModel) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
// Title with Coolify branding
|
||||
title := tui.FocusedStyle.Bold(true).Render("Add New SSH Private Key")
|
||||
b.WriteString(title + "\n\n")
|
||||
|
||||
// Render inputs with labels
|
||||
labelStyle := tui.BlurredStyle.Width(12)
|
||||
|
||||
b.WriteString(labelStyle.Render("Name:") + " " + m.nameInput.View() + "\n\n")
|
||||
b.WriteString(labelStyle.Render("Private Key:") + " " + m.keyInput.View() + "\n\n")
|
||||
|
||||
// Add help view
|
||||
if m.help.ShowAll {
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(m.help.View(m.keys))
|
||||
} else {
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(m.help.ShortHelpView(m.keys.ShortHelp()))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func generateRSAKeyPair() (privateBytes, publicBytes []byte, err error) {
|
||||
// Generate RSA key pair
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate RSA key pair: %w", err)
|
||||
}
|
||||
|
||||
// Convert private key to PEM format
|
||||
privateKeyPEM := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
privateBytes = pem.EncodeToMemory(privateKeyPEM)
|
||||
|
||||
// Generate public key
|
||||
publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate public key: %w", err)
|
||||
}
|
||||
publicBytes = ssh.MarshalAuthorizedKey(publicKey)
|
||||
|
||||
return privateBytes, publicBytes, nil
|
||||
}
|
||||
|
||||
func generateEd25519KeyPair() (privateBytes, publicBytes []byte, err error) {
|
||||
// Generate Ed25519 key pair
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate Ed25519 key pair: %w", err)
|
||||
}
|
||||
privateKeyPem, err := ssh.MarshalPrivateKey(privateKey, "")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal private key: %w", err)
|
||||
}
|
||||
privateBytes = pem.EncodeToMemory(privateKeyPem)
|
||||
|
||||
// Generate public key
|
||||
sshPublicKey, err := ssh.NewPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate public key: %w", err)
|
||||
}
|
||||
publicBytes = ssh.MarshalAuthorizedKey(sshPublicKey)
|
||||
|
||||
return privateBytes, publicBytes, nil
|
||||
}
|
||||
|
||||
func (c *cliPrivateKeys) generateKeyPair(name, outputDir, alorithim string, force bool) (string, error) {
|
||||
var privateKey, publicKey []byte
|
||||
var err error
|
||||
switch alorithim {
|
||||
case "rsa":
|
||||
privateKey, publicKey, err = generateRSAKeyPair()
|
||||
case "ed25519":
|
||||
privateKey, publicKey, err = generateEd25519KeyPair()
|
||||
default:
|
||||
return "", fmt.Errorf("invalid alorithim: %s", alorithim)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if outputDir != "" {
|
||||
if err := os.MkdirAll(outputDir, 0o700); err != nil {
|
||||
return "", fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Write private key file
|
||||
privateKeyPath := filepath.Join(outputDir, name)
|
||||
if !force {
|
||||
if _, err := os.Stat(privateKeyPath); err == nil {
|
||||
return "", fmt.Errorf("private key file already exists: %s", privateKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(privateKeyPath, privateKey, 0o600); err != nil {
|
||||
return "", fmt.Errorf("failed to write private key file: %w", err)
|
||||
}
|
||||
|
||||
// Write public key file
|
||||
publicKeyPath := privateKeyPath + ".pub"
|
||||
if err := os.WriteFile(publicKeyPath, publicKey, 0o644); err != nil {
|
||||
return "", fmt.Errorf("failed to write public key file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Generated SSH key pair:\n")
|
||||
fmt.Printf(" Private key: %s\n", privateKeyPath)
|
||||
fmt.Printf(" Public key: %s\n", publicKeyPath)
|
||||
}
|
||||
return string(privateKey), nil
|
||||
}
|
||||
|
||||
func (c *cliPrivateKeys) newAddCommand() *cobra.Command {
|
||||
var generateKeyPair bool
|
||||
var outPutDirectory string
|
||||
var algorithm string
|
||||
var force bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [name] [private_key_or_file]",
|
||||
Short: "Add a new private key",
|
||||
Long: `Add a new SSH private key to your Coolify instance.
|
||||
The key can be provided directly as a string or as a path to a file.
|
||||
Use --generate to create a new SSH key pair.
|
||||
|
||||
If no arguments are provided, an interactive form will be used.`,
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s private-keys add "My Key" /path/to/id_rsa
|
||||
%[1]s private-keys add "My Key" "-----BEGIN RSA PRIVATE KEY-----..."
|
||||
%[1]s private-keys add "My Key" --generate # Generate key pair
|
||||
%[1]s private-keys add # Interactive mode
|
||||
`),
|
||||
SilenceUsage: true,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if generateKeyPair {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("when using --generate, provide only the key name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return cobra.RangeArgs(0, 2)(cmd, args)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Handle key generation
|
||||
if generateKeyPair {
|
||||
name := args[0]
|
||||
privateKey, err := c.generateKeyPair(name, outPutDirectory, algorithm, force)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.addPrivateKey(cmd.Context(), name, privateKey)
|
||||
}
|
||||
|
||||
// Interactive mode when no arguments are provided
|
||||
if len(args) == 0 {
|
||||
model := initialAddKeyModel(c.coolify())
|
||||
p := tea.NewProgram(model)
|
||||
finalModel, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running interactive mode: %w", err)
|
||||
}
|
||||
|
||||
// Process the final model after user submission
|
||||
finalState := finalModel.(addKeyModel)
|
||||
if !finalState.done {
|
||||
return fmt.Errorf("operation canceled")
|
||||
}
|
||||
|
||||
name := finalState.nameInput.Value()
|
||||
privateKeyInput := finalState.keyInput.Value()
|
||||
|
||||
return c.addPrivateKey(cmd.Context(), name, privateKeyInput)
|
||||
}
|
||||
|
||||
// CLI mode with arguments
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("requires both NAME and PRIVATE_KEY_OR_FILE arguments")
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
privateKeyInput := args[1]
|
||||
|
||||
return c.addPrivateKey(cmd.Context(), name, privateKeyInput)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SortFlags = false
|
||||
flags.BoolVarP(&generateKeyPair, "generate", "g", false, "generate a new key pair")
|
||||
flags.StringVarP(&algorithm, "algorithm", "a", "rsa", "algorithm to use for the key pair")
|
||||
flags.StringVarP(&outPutDirectory, "output", "o", "", "optional output directory for the key pair")
|
||||
flags.BoolVarP(&force, "force", "f", false, "force the generation of the key pair if the name exists on the file system within the output directory")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// addPrivateKey adds a private key to the Coolify instance
|
||||
func (c *cliPrivateKeys) addPrivateKey(ctx context.Context, name, privateKeyInput string) error {
|
||||
// Check if input is a file path
|
||||
var privateKey string
|
||||
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
|
||||
}
|
||||
|
||||
req, err := c.coolify().Client.CreatePrivateKey(ctx, openapi.CreatePrivateKeyJSONRequestBody{
|
||||
Name: &name,
|
||||
PrivateKey: privateKey,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseCreatePrivateKeyResponse(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if parsedResponse.StatusCode() != http.StatusCreated {
|
||||
return fmt.Errorf("failed to add private key: %s", string(parsedResponse.Body))
|
||||
}
|
||||
|
||||
fmt.Printf("Private key '%s' added successfully as UUID: %s\n", name, *parsedResponse.JSON201.Uuid)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package cliprivatekeys
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func buildView(item openapi.PrivateKey, sensitive bool) string {
|
||||
var s strings.Builder
|
||||
addSection := func(title string, value interface{}) {
|
||||
s.WriteString(tui.FocusedStyle.Bold(true).Render(title + ": "))
|
||||
if value != nil {
|
||||
switch v := value.(type) {
|
||||
case *string:
|
||||
if v != nil {
|
||||
s.WriteString(*v + "\n\n")
|
||||
}
|
||||
case *bool:
|
||||
if v != nil {
|
||||
s.WriteString(fmt.Sprintf("%v\n\n", *v))
|
||||
}
|
||||
case *int:
|
||||
if v != nil {
|
||||
s.WriteString(fmt.Sprintf("%d\n\n", *v))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
s.WriteString("N/A\n\n")
|
||||
}
|
||||
}
|
||||
addSection("UUID", item.Uuid)
|
||||
addSection("Name", item.Name)
|
||||
addSection("Description", item.Description)
|
||||
addSection("Fingerprint", item.Fingerprint)
|
||||
|
||||
if sensitive {
|
||||
addSection("Private Key", item.PrivateKey)
|
||||
addSection("Public Key", item.PublicKey)
|
||||
} else {
|
||||
addSection("Private Key", &coolTypes.Redacted)
|
||||
addSection("Public Key", &coolTypes.Redacted)
|
||||
}
|
||||
|
||||
addSection("Git Related", item.IsGitRelated)
|
||||
addSection("Team ID", item.TeamId)
|
||||
addSection("Created At", item.CreatedAt)
|
||||
addSection("Updated At", item.UpdatedAt)
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
type keyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
PageUp key.Binding
|
||||
PageDown key.Binding
|
||||
Quit key.Binding
|
||||
ShowSensitive key.Binding
|
||||
}
|
||||
|
||||
func defaultKeyMap() keyMap {
|
||||
return keyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑/k", "move up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓/j", "move down"),
|
||||
),
|
||||
PageUp: key.NewBinding(
|
||||
key.WithKeys("pgup"),
|
||||
key.WithHelp("pgup", "page up"),
|
||||
),
|
||||
PageDown: key.NewBinding(
|
||||
key.WithKeys("pgdown"),
|
||||
key.WithHelp("pgdown", "page down"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("esc", "ctrl+c"),
|
||||
key.WithHelp("esc", "quit"),
|
||||
),
|
||||
ShowSensitive: key.NewBinding(
|
||||
key.WithKeys("ctrl+s"),
|
||||
key.WithHelp("ctrl+s", "show sensitive"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (k keyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Up, k.Down, k.Quit}
|
||||
}
|
||||
|
||||
func (k keyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down},
|
||||
{k.PageUp, k.PageDown},
|
||||
{k.Quit},
|
||||
{k.ShowSensitive},
|
||||
}
|
||||
}
|
||||
|
||||
type privateKeyModel struct {
|
||||
viewport viewport.Model
|
||||
keymap keyMap
|
||||
help help.Model
|
||||
ready bool
|
||||
privateKey openapi.PrivateKey
|
||||
sensitive bool
|
||||
quitting bool
|
||||
err error
|
||||
}
|
||||
|
||||
func newPrivateKeyModel(privateKey openapi.PrivateKey, sensitive bool) privateKeyModel {
|
||||
return privateKeyModel{
|
||||
keymap: defaultKeyMap(),
|
||||
help: help.New(),
|
||||
privateKey: privateKey,
|
||||
sensitive: sensitive,
|
||||
}
|
||||
}
|
||||
|
||||
func (m privateKeyModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m privateKeyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var (
|
||||
cmd tea.Cmd
|
||||
cmds []tea.Cmd
|
||||
)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keymap.Quit):
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
case key.Matches(msg, m.keymap.Up):
|
||||
m.viewport.LineUp(1)
|
||||
case key.Matches(msg, m.keymap.Down):
|
||||
m.viewport.LineDown(1)
|
||||
case key.Matches(msg, m.keymap.PageUp):
|
||||
m.viewport.HalfViewUp()
|
||||
case key.Matches(msg, m.keymap.PageDown):
|
||||
m.viewport.HalfViewDown()
|
||||
case key.Matches(msg, m.keymap.ShowSensitive):
|
||||
m.sensitive = !m.sensitive
|
||||
m.viewport.SetContent(buildView(m.privateKey, m.sensitive))
|
||||
}
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
if !m.ready {
|
||||
m.viewport = viewport.New(msg.Width, msg.Height-4)
|
||||
m.viewport.Style = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("62")).
|
||||
Padding(0, 2)
|
||||
m.viewport.SetContent(buildView(m.privateKey, m.sensitive))
|
||||
m.help.Width = msg.Width
|
||||
m.ready = true
|
||||
} else {
|
||||
m.viewport.Width = msg.Width
|
||||
m.viewport.Height = msg.Height - 4
|
||||
m.help.Width = msg.Width
|
||||
}
|
||||
}
|
||||
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m privateKeyModel) View() string {
|
||||
if !m.ready {
|
||||
return "Initializing..."
|
||||
}
|
||||
if m.err != nil {
|
||||
return fmt.Sprintf("Error: %v\nPress esc to quit", m.err)
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
s.WriteString(m.viewport.View())
|
||||
s.WriteString("\n\n")
|
||||
s.WriteString(m.help.View(m.keymap))
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (c *cliPrivateKeys) newGetCommand() *cobra.Command {
|
||||
var showSensitive bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "get [uuid]",
|
||||
Short: "Get private key details",
|
||||
Long: `Get the details of a specific private key by its UUID.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uuid := args[0]
|
||||
|
||||
response, err := c.coolify().Client.GetPrivateKeyByUuid(cmd.Context(), uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseGetPrivateKeyByUuidResponse(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
if parsedResponse.StatusCode() != http.StatusOK {
|
||||
return fmt.Errorf("failed to fetch private key: %s", string(parsedResponse.Body))
|
||||
}
|
||||
|
||||
key := *parsedResponse.JSON200
|
||||
|
||||
format, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get format: %w", err)
|
||||
}
|
||||
if format == "json" {
|
||||
// Redact sensitive data if --show-sensitive is not set
|
||||
if !showSensitive {
|
||||
// Create a copy with redacted sensitive fields
|
||||
redactedKey := key
|
||||
redactedKey.PrivateKey = &coolTypes.Redacted
|
||||
redactedKey.PublicKey = &coolTypes.Redacted
|
||||
key = redactedKey
|
||||
}
|
||||
|
||||
// For JSON output, directly encode to stdout
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(key)
|
||||
}
|
||||
|
||||
// Initialize and run Bubble Tea program
|
||||
m := newPrivateKeyModel(key, showSensitive)
|
||||
p := tea.NewProgram(m)
|
||||
if _, err := p.Run(); err != nil {
|
||||
return fmt.Errorf("error running program: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&showSensitive, "show-sensitive", "s", false, "Show sensitive information like key contents")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package cliprivatekeys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/coollabsio/cli-coolify/cmd/coolTypes"
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type filterableListModel struct {
|
||||
FilterableTable *tui.FilterableTable
|
||||
}
|
||||
|
||||
func newFilterableListModel(keys []openapi.PrivateKey, filter string) *filterableListModel {
|
||||
columns := []table.Column{
|
||||
{Title: "UUID", Width: 30},
|
||||
{Title: "Name", Width: 30},
|
||||
{Title: "Created At", Width: 30},
|
||||
}
|
||||
|
||||
return &filterableListModel{
|
||||
FilterableTable: tui.NewTableFilter(wrapKeys(keys), columns, buildRow).
|
||||
WithInitialFilter(filter).
|
||||
WithDetailView(buildDetailView).
|
||||
WithDetailHeader("Private Key Details"),
|
||||
}
|
||||
}
|
||||
|
||||
func wrapKeys(keys []openapi.PrivateKey) []tui.FilterableItem {
|
||||
items := make([]tui.FilterableItem, len(keys))
|
||||
for i, key := range keys {
|
||||
items[i] = &key
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func buildRow(item tui.FilterableItem) table.Row {
|
||||
key := item.(*openapi.PrivateKey)
|
||||
return table.Row{
|
||||
*key.Uuid,
|
||||
*key.Name,
|
||||
*key.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func buildDetailView(item tui.FilterableItem, sensitive bool) string {
|
||||
key := item.(*openapi.PrivateKey)
|
||||
var s strings.Builder
|
||||
addSection := func(title string, value interface{}) {
|
||||
s.WriteString(tui.FocusedStyle.Bold(true).Render(title + ": "))
|
||||
if value != nil {
|
||||
switch v := value.(type) {
|
||||
case *string:
|
||||
if v != nil {
|
||||
s.WriteString(*v + "\n\n")
|
||||
}
|
||||
case *bool:
|
||||
if v != nil {
|
||||
s.WriteString(fmt.Sprintf("%v\n\n", *v))
|
||||
}
|
||||
case *int:
|
||||
if v != nil {
|
||||
s.WriteString(fmt.Sprintf("%d\n\n", *v))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
s.WriteString("N/A\n\n")
|
||||
}
|
||||
}
|
||||
addSection("UUID", key.Uuid)
|
||||
addSection("Name", key.Name)
|
||||
addSection("Description", key.Description)
|
||||
addSection("Fingerprint", key.Fingerprint)
|
||||
|
||||
if sensitive {
|
||||
addSection("Private Key", key.PrivateKey)
|
||||
addSection("Public Key", key.PublicKey)
|
||||
} else {
|
||||
addSection("Private Key", &coolTypes.Redacted)
|
||||
addSection("Public Key", &coolTypes.Redacted)
|
||||
}
|
||||
|
||||
addSection("Git Related", key.IsGitRelated)
|
||||
addSection("Team ID", key.TeamId)
|
||||
addSection("Created At", key.CreatedAt)
|
||||
addSection("Updated At", key.UpdatedAt)
|
||||
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (m *filterableListModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *filterableListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.FilterableTable.Update(msg)
|
||||
}
|
||||
|
||||
func (m *filterableListModel) View() string {
|
||||
return m.FilterableTable.View()
|
||||
}
|
||||
|
||||
func (c *cliPrivateKeys) handleDelete(item tui.FilterableItem) error {
|
||||
key := item.(*openapi.PrivateKey)
|
||||
deleteReq, err := c.coolify().Client.DeletePrivateKeyByUuid(context.Background(), *key.Uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create delete request: %w", err)
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseDeletePrivateKeyByUuidResponse(deleteReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
switch parsedResponse.StatusCode() {
|
||||
case http.StatusUnprocessableEntity:
|
||||
return fmt.Errorf("failed to delete private key: %s", *parsedResponse.JSON422.Message)
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("failed to delete private key: %s", string(parsedResponse.Body))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cliPrivateKeys) newListCommand() *cobra.Command {
|
||||
var filter string
|
||||
var showSensitive bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [filter]",
|
||||
Short: "List all private keys",
|
||||
Long: `List all SSH private keys registered in your Coolify instance.`,
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s private-keys list --format json
|
||||
%[1]s private-keys list "My Key"
|
||||
%[1]s private-keys list --show-sensitive
|
||||
%[1]s private-keys list # Interactive mode
|
||||
`),
|
||||
SilenceUsage: true,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
filter = args[0]
|
||||
}
|
||||
|
||||
response, err := c.coolify().Client.ListPrivateKeys(cmd.Context())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseListPrivateKeysResponse(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if parsedResponse.StatusCode() != http.StatusOK {
|
||||
return fmt.Errorf("failed to fetch private keys: %s", string(parsedResponse.Body))
|
||||
}
|
||||
|
||||
keys := *parsedResponse.JSON200
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
if format == "json" {
|
||||
// For JSON output, redact sensitive data if --show-sensitive is not set
|
||||
if !showSensitive {
|
||||
// Create a copy with redacted sensitive fields
|
||||
redactedKeys := make([]openapi.PrivateKey, len(*parsedResponse.JSON200))
|
||||
for i, key := range *parsedResponse.JSON200 {
|
||||
redactedKeys[i] = key
|
||||
redactedKeys[i].PrivateKey = &coolTypes.Redacted
|
||||
redactedKeys[i].PublicKey = &coolTypes.Redacted
|
||||
}
|
||||
keys = redactedKeys
|
||||
}
|
||||
|
||||
// For JSON output, directly encode to stdout
|
||||
encoder := json.NewEncoder(cmd.OutOrStdout())
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(keys)
|
||||
}
|
||||
|
||||
model := newFilterableListModel(keys, filter)
|
||||
model.FilterableTable.WithDeleteHandler(c.handleDelete)
|
||||
p := tea.NewProgram(model)
|
||||
_, err = p.Run()
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&showSensitive, "show-sensitive", "s", false, "Show sensitive information like public keys")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cliprivatekeys
|
||||
|
||||
import (
|
||||
"github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type cliPrivateKeys struct {
|
||||
coolify runtime.Getter
|
||||
}
|
||||
|
||||
func New(c runtime.Getter) *cliPrivateKeys {
|
||||
return &cliPrivateKeys{
|
||||
coolify: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cliPrivateKeys) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "private-keys",
|
||||
Short: "Manage SSH private keys",
|
||||
Long: `Manage SSH private keys for your Coolify instance.`,
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
cmd.AddCommand(c.newListCommand())
|
||||
cmd.AddCommand(c.newGetCommand())
|
||||
cmd.AddCommand(c.newAddCommand())
|
||||
cmd.AddCommand(c.newRemoveCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package cliprivatekeys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (c *cliPrivateKeys) newRemoveCommand() *cobra.Command {
|
||||
var forceRemove bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove [uuid]",
|
||||
Short: "Remove a private key",
|
||||
Long: `Remove an private key from your Coolify instance.`,
|
||||
SilenceUsage: true,
|
||||
Aliases: []string{"delete", "rm"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uuid := args[0]
|
||||
|
||||
if !forceRemove {
|
||||
fmt.Printf("Are you sure you want to remove the private key with UUID '%s'? [y/N] ", uuid)
|
||||
var confirm string
|
||||
_, err := fmt.Scanln(&confirm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read confirmation: %w", err)
|
||||
}
|
||||
if confirm != "y" && confirm != "Y" {
|
||||
fmt.Println("Operation canceled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
req, err := c.coolify().Client.DeletePrivateKeyByUuid(cmd.Context(), uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseDeletePrivateKeyByUuidResponse(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
if parsedResponse.StatusCode() != http.StatusOK {
|
||||
errorMessage := "failed to remove private key"
|
||||
switch parsedResponse.StatusCode() {
|
||||
case http.StatusBadRequest:
|
||||
errorMessage = fmt.Sprintf("%s: %s", errorMessage, *parsedResponse.JSON400.Message)
|
||||
case http.StatusUnprocessableEntity:
|
||||
errorMessage = fmt.Sprintf("%s: %s", errorMessage, *parsedResponse.JSON422.Message)
|
||||
default:
|
||||
errorMessage = fmt.Sprintf("%s: %s", errorMessage, string(parsedResponse.Body))
|
||||
}
|
||||
return fmt.Errorf("%s", errorMessage)
|
||||
}
|
||||
|
||||
fmt.Println(tui.SuccessStyle.Render("Private key removed successfully"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&forceRemove, "force", "f", false, "Attempt to remove without confirmation prompt")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package cliservers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// addKeyMap defines keybindings for the add server form
|
||||
type addKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Tab key.Binding
|
||||
Enter key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
// ShortHelp returns keybindings to be shown in the mini help view
|
||||
func (k addKeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Help, k.Quit}
|
||||
}
|
||||
|
||||
// FullHelp returns keybindings for the expanded help view
|
||||
func (k addKeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Tab}, // first column
|
||||
{k.Enter, k.Help}, // second column
|
||||
{k.Quit}, // third column
|
||||
}
|
||||
}
|
||||
|
||||
var addKeys = addKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "move up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "move down"),
|
||||
),
|
||||
Tab: key.NewBinding(
|
||||
key.WithKeys("tab", "shift+tab"),
|
||||
key.WithHelp("tab", "next field"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "submit/select"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "toggle help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("esc", "ctrl+c"),
|
||||
key.WithHelp("esc", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
type addModel struct {
|
||||
inputs []textinput.Model
|
||||
focusIndex int
|
||||
err error
|
||||
done bool
|
||||
keys addKeyMap
|
||||
help help.Model
|
||||
}
|
||||
|
||||
func (c *cliServers) newAddCommand() *cobra.Command {
|
||||
var validate bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [name] [ip] [private_key_uuid]",
|
||||
Short: "Add a new server",
|
||||
Long: `
|
||||
Add a new server to your Coolify instance.
|
||||
If no arguments are provided, an interactive form will be shown.`,
|
||||
SilenceUsage: true,
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s servers add "My Server" 192.168.1.100 abcd1234-uuid
|
||||
%[1]s servers add "Production" 10.0.0.1 efgh5678-uuid --validate
|
||||
%[1]s servers add # Interactive mode`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return c.runInteractiveAdd(validate)
|
||||
}
|
||||
|
||||
if len(args) != 3 {
|
||||
return fmt.Errorf("requires exactly 3 arguments (name, ip, private_key_uuid) or no arguments for interactive mode")
|
||||
}
|
||||
|
||||
return c.addServer(args[0], args[1], args[2], 22, "root", validate)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&validate, "validate", false, "Validate the server after adding")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *cliServers) runInteractiveAdd(validate bool) error {
|
||||
p := tea.NewProgram(initialAddModel())
|
||||
m, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running form: %w", err)
|
||||
}
|
||||
|
||||
finalModel := m.(addModel)
|
||||
if !finalModel.done {
|
||||
return fmt.Errorf("operation cancelled")
|
||||
}
|
||||
|
||||
// Get values from the form
|
||||
name := strings.TrimSpace(finalModel.inputs[0].Value())
|
||||
ip := strings.TrimSpace(finalModel.inputs[1].Value())
|
||||
port := strings.TrimSpace(finalModel.inputs[2].Value())
|
||||
user := strings.TrimSpace(finalModel.inputs[3].Value())
|
||||
privateKeyUUID := strings.TrimSpace(finalModel.inputs[4].Value())
|
||||
|
||||
// Convert port to int with default 22
|
||||
portNum := 22
|
||||
if port != "" {
|
||||
if _, err := fmt.Sscanf(port, "%d", &portNum); err != nil {
|
||||
return fmt.Errorf("invalid port number: %s", port)
|
||||
}
|
||||
}
|
||||
|
||||
// Use default user if not specified
|
||||
if user == "" {
|
||||
user = "root"
|
||||
}
|
||||
|
||||
return c.addServer(name, ip, privateKeyUUID, portNum, user, validate)
|
||||
}
|
||||
|
||||
func initialAddModel() addModel {
|
||||
inputs := make([]textinput.Model, 5)
|
||||
|
||||
// Initialize text inputs
|
||||
labels := []string{"Name", "IP Address", "Port (default: 22)", "User (default: root)", "Private Key UUID"}
|
||||
for i := range inputs {
|
||||
input := tui.NewBlurredInput(labels[i], "")
|
||||
inputs[i] = input
|
||||
}
|
||||
|
||||
inputs[0].Focus()
|
||||
return addModel{
|
||||
inputs: inputs,
|
||||
err: nil,
|
||||
keys: addKeys,
|
||||
help: help.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m addModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m addModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
if msg, ok := msg.(tea.KeyMsg); ok {
|
||||
if key.Matches(msg, m.keys.Quit) {
|
||||
m.done = false
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Help) {
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Enter) {
|
||||
// Submit on enter when last input is focused
|
||||
if m.focusIndex == len(m.inputs)-1 {
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
// Otherwise move to next input
|
||||
m.focusIndex++
|
||||
if m.focusIndex >= len(m.inputs) {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
m.updateFocus()
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Tab) {
|
||||
// Cycle focus between inputs
|
||||
if msg.String() == "shift+tab" {
|
||||
m.focusIndex--
|
||||
} else {
|
||||
m.focusIndex++
|
||||
}
|
||||
|
||||
if m.focusIndex >= len(m.inputs) {
|
||||
m.focusIndex = 0
|
||||
} else if m.focusIndex < 0 {
|
||||
m.focusIndex = len(m.inputs) - 1
|
||||
}
|
||||
m.updateFocus()
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Up) {
|
||||
m.focusIndex--
|
||||
if m.focusIndex < 0 {
|
||||
m.focusIndex = len(m.inputs) - 1
|
||||
}
|
||||
m.updateFocus()
|
||||
}
|
||||
|
||||
if key.Matches(msg, m.keys.Down) {
|
||||
m.focusIndex++
|
||||
if m.focusIndex >= len(m.inputs) {
|
||||
m.focusIndex = 0
|
||||
}
|
||||
m.updateFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle character input
|
||||
cmd := m.updateInputs(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *addModel) updateFocus() {
|
||||
for i := 0; i < len(m.inputs); i++ {
|
||||
if i == m.focusIndex {
|
||||
m.inputs[i].Focus()
|
||||
} else {
|
||||
m.inputs[i].Blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *addModel) updateInputs(msg tea.Msg) tea.Cmd {
|
||||
var cmd tea.Cmd
|
||||
|
||||
m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (m addModel) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("Please enter server details:\n\n")
|
||||
|
||||
for i, input := range m.inputs {
|
||||
b.WriteString(input.View())
|
||||
if i < len(m.inputs)-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
button := "\n\n"
|
||||
if m.focusIndex == len(m.inputs)-1 {
|
||||
button += lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("99")).
|
||||
Render("[ Submit ]")
|
||||
} else {
|
||||
button += "[ Submit ]"
|
||||
}
|
||||
|
||||
b.WriteString(button)
|
||||
|
||||
// Add help view
|
||||
if m.help.ShowAll {
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(m.help.View(m.keys))
|
||||
} else {
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(m.help.ShortHelpView(m.keys.ShortHelp()))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (c *cliServers) addServer(name, ip, privateKeyUUID string, port int, user string, validate bool) error {
|
||||
req, err := c.coolify().Client.CreateServer(context.Background(), openapi.CreateServerJSONRequestBody{
|
||||
Name: &name,
|
||||
Ip: &ip,
|
||||
Port: &port,
|
||||
User: &user,
|
||||
PrivateKeyUuid: &privateKeyUUID,
|
||||
InstantValidate: &validate,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseCreateServerResponse(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if parsedResponse.StatusCode() != http.StatusCreated {
|
||||
return fmt.Errorf("failed to add server: %s", *parsedResponse.JSON400.Message)
|
||||
}
|
||||
|
||||
if validate {
|
||||
fmt.Printf("Server added successfully with uuid %s\n", *parsedResponse.JSON201.Uuid)
|
||||
} else {
|
||||
fmt.Printf("Server added successfully with uuid %s. Server is not validated. Use 'servers validate %s' to validate the server.\n", *parsedResponse.JSON201.Uuid, *parsedResponse.JSON201.Uuid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package cliservers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type getModel struct {
|
||||
server *openapi.Server
|
||||
sensitive bool
|
||||
withResources bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *cliServers) newGetCommand() *cobra.Command {
|
||||
var withResources bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "get [uuid]",
|
||||
Short: "Get server details",
|
||||
Long: `
|
||||
Get detailed information about a specific server.
|
||||
Optionally show its resources and sensitive information.`,
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s servers get 123e4567-e89b-12d3-a456-426614174000
|
||||
%[1]s servers get 123e4567-e89b-12d3-a456-426614174000 --resources
|
||||
%[1]s servers get 123e4567-e89b-12d3-a456-426614174000 --sensitive
|
||||
%[1]s servers get 123e4567-e89b-12d3-a456-426614174000 --format json`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uuid := args[0]
|
||||
|
||||
// Fetch server details
|
||||
serverData, err := c.fetchServer(cmd.Context(), uuid, withResources)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch server details: %w", err)
|
||||
}
|
||||
|
||||
outFormat, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get output format: %w", err)
|
||||
}
|
||||
// Handle JSON output format
|
||||
if outFormat == "json" {
|
||||
return json.NewEncoder(os.Stdout).Encode(serverData)
|
||||
}
|
||||
|
||||
// Create and run Bubble Tea program for interactive display
|
||||
p := tea.NewProgram(initialGetModel(serverData))
|
||||
if _, err := p.Run(); err != nil {
|
||||
return fmt.Errorf("error running detail view: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&withResources, "resources", false, "Show server resources")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func initialGetModel(server *openapi.Server) getModel {
|
||||
return getModel{
|
||||
server: server,
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Bubble Tea Model interface
|
||||
func (m getModel) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m getModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if msg, ok := msg.(tea.KeyMsg); ok {
|
||||
if msg.String() == "ctrl+c" || msg.String() == "esc" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m getModel) View() string {
|
||||
var s strings.Builder
|
||||
|
||||
// Create styles
|
||||
titleStyle := tui.FocusedStyle.
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("60"))
|
||||
|
||||
valueStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("99"))
|
||||
|
||||
// Server details section
|
||||
s.WriteString(titleStyle.Render("Server Details"))
|
||||
s.WriteString("\n")
|
||||
|
||||
// Helper function to add a field
|
||||
addField := func(label, value string) {
|
||||
s.WriteString(fmt.Sprintf("%s: %s\n",
|
||||
labelStyle.Render(label),
|
||||
valueStyle.Render(value)))
|
||||
}
|
||||
|
||||
addField("UUID", *m.server.Uuid)
|
||||
addField("Name", *m.server.Name)
|
||||
|
||||
addField("IP Address", *m.server.Ip)
|
||||
addField("User", *m.server.User)
|
||||
|
||||
addField("Port", fmt.Sprintf("%d", *m.server.Port))
|
||||
|
||||
status := "Offline"
|
||||
if *m.server.Settings.IsReachable && *m.server.Settings.IsUsable {
|
||||
status = "Online"
|
||||
}
|
||||
addField("Status", status)
|
||||
|
||||
return "\n" + s.String()
|
||||
}
|
||||
|
||||
func (c *cliServers) fetchServer(ctx context.Context, uuid string, withResources bool) (*openapi.Server, error) {
|
||||
|
||||
req, err := c.coolify().Client.GetServerByUuid(ctx, uuid, func(ctx context.Context, req *http.Request) error {
|
||||
if withResources {
|
||||
req.URL.RawQuery = url.Values{"resources": {"true"}}.Encode()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseGetServerByUuidResponse(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if parsedResponse.StatusCode() != http.StatusOK {
|
||||
switch parsedResponse.StatusCode() {
|
||||
case http.StatusNotFound:
|
||||
return nil, fmt.Errorf("failed to get server: %s", *parsedResponse.JSON404.Message)
|
||||
default:
|
||||
return nil, fmt.Errorf("failed to get server: %s", string(parsedResponse.Body))
|
||||
}
|
||||
}
|
||||
|
||||
return parsedResponse.JSON200, nil
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package cliservers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type listModel struct {
|
||||
filterableTable *tui.FilterableTable
|
||||
servers *[]openapi.Server
|
||||
sensitive bool
|
||||
filter string
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *cliServers) newListCommand() *cobra.Command {
|
||||
var showSensitive bool
|
||||
var initialFilter string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [filter]",
|
||||
Short: "List all servers",
|
||||
Long: `
|
||||
List all servers registered in your Coolify instance.
|
||||
Use --sensitive to show sensitive information like IP addresses.`,
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s servers list
|
||||
%[1]s servers list "my-server"
|
||||
%[1]s servers list --format json
|
||||
%[1]s servers list --sensitive`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
initialFilter = args[0]
|
||||
}
|
||||
|
||||
// Fetch servers from API
|
||||
data, err := c.fetchServers(cmd.Context())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch servers: %w", err)
|
||||
}
|
||||
|
||||
outputFormat, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get output format: %w", err)
|
||||
}
|
||||
|
||||
// Handle JSON output format
|
||||
if outputFormat == "json" {
|
||||
return json.NewEncoder(os.Stdout).Encode(data)
|
||||
}
|
||||
|
||||
// Create and run Bubble Tea program for interactive display
|
||||
p := tea.NewProgram(initialListModel(data, showSensitive, initialFilter))
|
||||
if _, err := p.Run(); err != nil {
|
||||
return fmt.Errorf("error running list view: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&showSensitive, "sensitive", "s", false, "Show sensitive information")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func initialListModel(servers *[]openapi.Server, sensitive bool, initialFilter string) listModel {
|
||||
columns := []table.Column{
|
||||
{Title: "UUID", Width: 36},
|
||||
{Title: "Name", Width: 30},
|
||||
{Title: "IP Address", Width: 15},
|
||||
}
|
||||
|
||||
// Convert servers to FilterableItems
|
||||
items := make([]tui.FilterableItem, len(*servers))
|
||||
for i, s := range *servers {
|
||||
items[i] = &s
|
||||
}
|
||||
|
||||
// Create row builder function
|
||||
rowBuilder := func(item tui.FilterableItem) table.Row {
|
||||
s := item.(*openapi.Server)
|
||||
|
||||
return table.Row{
|
||||
*s.Uuid,
|
||||
*s.Name,
|
||||
*s.Ip,
|
||||
}
|
||||
}
|
||||
|
||||
detailBuilder := func(item tui.FilterableItem, sensitive bool) string {
|
||||
s := item.(*openapi.Server)
|
||||
|
||||
var builder strings.Builder
|
||||
addSection := func(title, value interface{}) {
|
||||
builder.WriteString(tui.FocusedStyle.Bold(true).Render(fmt.Sprintf("%s: ", title)))
|
||||
switch v := value.(type) {
|
||||
case *string:
|
||||
builder.WriteString(*v)
|
||||
case *int:
|
||||
builder.WriteString(fmt.Sprintf("%d", *v))
|
||||
case *openapi.ServerProxyType:
|
||||
if v != nil {
|
||||
builder.WriteString(string(*v))
|
||||
} else {
|
||||
builder.WriteString("N/A")
|
||||
}
|
||||
case string:
|
||||
builder.WriteString(v)
|
||||
case *bool:
|
||||
if v != nil {
|
||||
builder.WriteString(fmt.Sprintf("%t", *v))
|
||||
} else {
|
||||
builder.WriteString("N/A")
|
||||
}
|
||||
}
|
||||
builder.WriteString("\n\n")
|
||||
}
|
||||
|
||||
addSection("UUID", s.Uuid)
|
||||
addSection("Name", s.Name)
|
||||
addSection("IP Address", s.Ip)
|
||||
addSection("User", s.User)
|
||||
addSection("Port", s.Port)
|
||||
addSection("Proxy Type", s.ProxyType)
|
||||
addSection("Settings", "")
|
||||
addSection(" Created At", s.Settings.CreatedAt)
|
||||
addSection(" Updated At", s.Settings.UpdatedAt)
|
||||
addSection(" Server ID", s.Settings.ServerId)
|
||||
addSection(" Concurrent Builds", s.Settings.ConcurrentBuilds)
|
||||
addSection(" Dynamic Timeout", s.Settings.DynamicTimeout)
|
||||
addSection(" Docker", "")
|
||||
addSection(" Delete Unused Networks", s.Settings.DeleteUnusedNetworks)
|
||||
addSection(" Delete Unused Volumes", s.Settings.DeleteUnusedVolumes)
|
||||
addSection(" Cleanup Frequency", s.Settings.DockerCleanupFrequency)
|
||||
addSection(" Cleanup Threshold", s.Settings.DockerCleanupThreshold)
|
||||
addSection(" Force Disabled", s.Settings.ForceDisabled)
|
||||
addSection(" Force Server Cleanup", s.Settings.ForceServerCleanup)
|
||||
addSection(" Is Build Server", s.Settings.IsBuildServer)
|
||||
addSection(" Is Cloudflare Tunnel", s.Settings.IsCloudflareTunnel)
|
||||
addSection(" Is Jump Server", s.Settings.IsJumpServer)
|
||||
if s.Settings.IsLogdrainAxiomEnabled != nil && *s.Settings.IsLogdrainAxiomEnabled {
|
||||
addSection(" Axiom", "")
|
||||
addSection(" API Key", s.Settings.LogdrainAxiomApiKey)
|
||||
addSection(" Dataset Name", s.Settings.LogdrainAxiomDatasetName)
|
||||
}
|
||||
if s.Settings.IsLogdrainCustomEnabled != nil && *s.Settings.IsLogdrainCustomEnabled {
|
||||
addSection(" Custom Drain", "")
|
||||
addSection(" Config", s.Settings.LogdrainCustomConfig)
|
||||
addSection(" Config Parser", s.Settings.LogdrainCustomConfigParser)
|
||||
}
|
||||
if s.Settings.IsLogdrainHighlightEnabled != nil && *s.Settings.IsLogdrainHighlightEnabled {
|
||||
addSection(" Highlight", "")
|
||||
addSection(" Project ID", s.Settings.LogdrainHighlightProjectId)
|
||||
}
|
||||
if s.Settings.IsLogdrainNewrelicEnabled != nil && *s.Settings.IsLogdrainNewrelicEnabled {
|
||||
addSection(" Newrelic", "")
|
||||
addSection(" Base URI", s.Settings.LogdrainNewrelicBaseUri)
|
||||
addSection(" License Key", s.Settings.LogdrainNewrelicLicenseKey)
|
||||
}
|
||||
addSection(" Metrics", "")
|
||||
addSection(" History Days", s.Settings.SentinelMetricsHistoryDays)
|
||||
addSection(" Refresh Rate", s.Settings.SentinelMetricsRefreshRateSeconds)
|
||||
addSection(" Token", s.Settings.SentinelToken)
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
ft := tui.NewTableFilter(items, columns, rowBuilder).
|
||||
WithInitialFilter(initialFilter).
|
||||
WithDetailView(detailBuilder)
|
||||
|
||||
return listModel{
|
||||
filterableTable: ft,
|
||||
servers: servers,
|
||||
sensitive: sensitive,
|
||||
filter: initialFilter,
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Bubble Tea Model interface
|
||||
func (m listModel) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.filterableTable.Update(msg)
|
||||
}
|
||||
|
||||
func (m listModel) View() string {
|
||||
return m.filterableTable.View()
|
||||
}
|
||||
|
||||
func (c *cliServers) fetchServers(ctx context.Context) (*[]openapi.Server, error) {
|
||||
req, err := c.coolify().Client.ListServers(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseListServersResponse(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return parsedResponse.JSON200, nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cliservers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (c *cliServers) newRemoveCommand() *cobra.Command {
|
||||
var force bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove [uuid]",
|
||||
Short: "Remove a server",
|
||||
Long: `
|
||||
Remove a server from your Coolify instance.
|
||||
This action cannot be undone.`,
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s servers remove [uuid]
|
||||
%[1]s servers remove [uuid] --force`),
|
||||
Aliases: []string{"delete", "rm"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
toRemove := args[0]
|
||||
|
||||
if !force {
|
||||
fmt.Printf("Are you sure you want to remove the server with UUID '%s'? [y/N] ", toRemove)
|
||||
var confirm string
|
||||
_, err := fmt.Scanln(&confirm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read confirmation: %w", err)
|
||||
}
|
||||
if confirm != "y" && confirm != "Y" {
|
||||
fmt.Println("Operation cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
response, err := c.coolify().Client.DeleteServerByUuid(cmd.Context(), toRemove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove server: %w", err)
|
||||
}
|
||||
parsedResponse, err := openapi.ParseDeleteServerByUuidResponse(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
if parsedResponse.StatusCode() != http.StatusOK {
|
||||
switch parsedResponse.StatusCode() {
|
||||
case http.StatusNotFound:
|
||||
return fmt.Errorf("failed to remove server: %s", *parsedResponse.JSON404.Message)
|
||||
default:
|
||||
return fmt.Errorf("failed to remove server: %s", string(parsedResponse.Body))
|
||||
}
|
||||
}
|
||||
fmt.Println(tui.SuccessStyle.Render(*parsedResponse.JSON200.Message))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cliservers
|
||||
|
||||
import (
|
||||
"github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type cliServers struct {
|
||||
coolify runtime.Getter
|
||||
}
|
||||
|
||||
func New(c runtime.Getter) *cliServers {
|
||||
return &cliServers{
|
||||
coolify: c,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCommand creates and returns the servers command
|
||||
func (c *cliServers) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "servers",
|
||||
Short: "Manage Coolify servers",
|
||||
Long: `
|
||||
Manage servers in your Coolify instance.
|
||||
This command allows you to list, add, remove, and manage servers.`,
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
cmd.AddCommand(c.newListCommand())
|
||||
cmd.AddCommand(c.newGetCommand())
|
||||
cmd.AddCommand(c.newAddCommand())
|
||||
cmd.AddCommand(c.newRemoveCommand())
|
||||
cmd.AddCommand(c.newValidateCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package cliservers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/coollabsio/cli-coolify/cmd/utils"
|
||||
"github.com/coollabsio/cli-coolify/pkg/gen/openapi"
|
||||
"github.com/coollabsio/cli-coolify/pkg/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type validateModel struct {
|
||||
spinner spinner.Model
|
||||
uuid string
|
||||
done bool
|
||||
err error
|
||||
response string
|
||||
coolify runtime.Getter
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
type validateSuccessMsg struct {
|
||||
message string
|
||||
}
|
||||
|
||||
type validateErrorMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *cliServers) newValidateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "validate [uuid]",
|
||||
Short: "Validate server connection",
|
||||
Long: `
|
||||
Validate the connection to a server in your Coolify instance.
|
||||
This will check if the server is reachable and usable.`,
|
||||
Example: utils.GetCommandExample(`
|
||||
%[1]s servers validate 123e4567-e89b-12d3-a456-426614174000`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
uuid := args[0]
|
||||
|
||||
p := tea.NewProgram(initialValidateModel(uuid, c.coolify, cmd.Context()))
|
||||
model, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running validation: %w", err)
|
||||
}
|
||||
|
||||
finalModel := model.(validateModel)
|
||||
if finalModel.err != nil {
|
||||
return finalModel.err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func initialValidateModel(uuid string, coolify runtime.Getter, ctx context.Context) validateModel {
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Points
|
||||
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("99"))
|
||||
|
||||
return validateModel{
|
||||
spinner: s,
|
||||
uuid: uuid,
|
||||
coolify: coolify,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (m validateModel) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
m.spinner.Tick,
|
||||
m.validateServer,
|
||||
)
|
||||
}
|
||||
|
||||
func (m validateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case spinner.TickMsg:
|
||||
var cmd tea.Cmd
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case validateSuccessMsg:
|
||||
m.done = true
|
||||
m.response = msg.message
|
||||
return m, tea.Quit
|
||||
|
||||
case validateErrorMsg:
|
||||
m.done = true
|
||||
m.err = msg.err
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m validateModel) View() string {
|
||||
if m.done {
|
||||
if m.err != nil {
|
||||
return tui.ErrorStyle.Render(fmt.Sprintf("Error: %v\n", m.err))
|
||||
}
|
||||
return tui.SuccessStyle.Render(m.response + "\n")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s Validating server...\n", m.spinner.View())
|
||||
}
|
||||
|
||||
func (m validateModel) validateServer() tea.Msg {
|
||||
// Simulate network delay for better UX
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
server, err := m.coolify().Client.ValidateServerByUuid(m.ctx, m.uuid)
|
||||
if err != nil {
|
||||
return validateErrorMsg{err: fmt.Errorf("failed to validate server: %w", err)}
|
||||
}
|
||||
|
||||
parsedResponse, err := openapi.ParseValidateServerByUuidResponse(server)
|
||||
if err != nil {
|
||||
return validateErrorMsg{err: fmt.Errorf("failed to parse server response: %w", err)}
|
||||
}
|
||||
|
||||
if parsedResponse.StatusCode() != http.StatusCreated {
|
||||
switch parsedResponse.StatusCode() {
|
||||
case http.StatusBadRequest:
|
||||
return validateErrorMsg{err: fmt.Errorf("failed to validate server: %s", *parsedResponse.JSON400.Message)}
|
||||
case http.StatusNotFound:
|
||||
return validateErrorMsg{err: fmt.Errorf("failed to validate server: %s", *parsedResponse.JSON404.Message)}
|
||||
default:
|
||||
return validateErrorMsg{err: fmt.Errorf("failed to validate server: %s", string(parsedResponse.Body))}
|
||||
}
|
||||
}
|
||||
|
||||
return validateSuccessMsg{message: string(*parsedResponse.JSON201.Message)}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cliupdate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
coolifyRuntime "github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/coollabsio/cli-coolify/pkg/updater"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type cliUpdate struct {
|
||||
coolify coolifyRuntime.Getter
|
||||
}
|
||||
|
||||
func New(c coolifyRuntime.Getter) *cliUpdate {
|
||||
return &cliUpdate{
|
||||
coolify: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cliUpdate) NewCommand() *cobra.Command {
|
||||
var preRelease bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update Coolify CLI",
|
||||
Long: `
|
||||
Update the Coolify CLI to the latest version from GitHub releases.
|
||||
|
||||
By default, the command will update to the latest stable version.
|
||||
Use the --pre-release flag to update to the latest pre-release version.
|
||||
`,
|
||||
SilenceUsage: true,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// we should check if the current version is a pre-release
|
||||
currentVersion := c.coolify().Version
|
||||
isPreRelease := strings.Contains(currentVersion, "-")
|
||||
// Create our custom updater
|
||||
update := updater.New("coollabsio", "cli-coolify", c.coolify().Version)
|
||||
|
||||
// Check for updates
|
||||
c.coolify().Logger.Infof("Checking for updates...")
|
||||
|
||||
// Check if an update is available without performing the update
|
||||
release, hasUpdate, err := update.Check(cmd.Context(), preRelease)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking for updates: %v", err)
|
||||
}
|
||||
|
||||
if isPreRelease && !preRelease && !hasUpdate {
|
||||
c.coolify().Logger.Warnf("You are on a pre-release version of the CLI. Use the --pre-release flag to update to the latest pre-release version.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !hasUpdate {
|
||||
c.coolify().Logger.Infof("You are already on the latest version: %s\n", c.coolify().GetFormattedVersion())
|
||||
return nil
|
||||
}
|
||||
|
||||
c.coolify().Logger.Infof("Found new version: v%s (current: %s)\n", release.Version, c.coolify().GetFormattedVersion())
|
||||
|
||||
// Format OS/Arch for display
|
||||
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
c.coolify().Logger.Infof("Downloading update for %s...", platform)
|
||||
|
||||
// Perform the update
|
||||
newVersion, err := update.To(cmd.Context(), release)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update failed: %v", err)
|
||||
}
|
||||
|
||||
c.coolify().Logger.Infof("Successfully updated to version v%s\n", newVersion)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&preRelease, "pre-release", false, "Update to pre-release version")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cliversion
|
||||
|
||||
import (
|
||||
"github.com/coollabsio/cli-coolify/cmd/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type cliVersion struct {
|
||||
coolify runtime.Getter
|
||||
}
|
||||
|
||||
func New(c runtime.Getter) *cliVersion {
|
||||
return &cliVersion{
|
||||
coolify: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cliVersion) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version ",
|
||||
Short: "CLI version",
|
||||
Long: `
|
||||
Print the version of the CLI.
|
||||
`,
|
||||
SilenceUsage: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Println(c.coolify().GetFormattedVersion())
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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())
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/config"
|
||||
)
|
||||
|
||||
func TestNewConfigCommand(t *testing.T) {
|
||||
cmd := NewConfigCommand()
|
||||
|
||||
if cmd == nil {
|
||||
t.Fatal("NewConfigCommand() returned nil")
|
||||
}
|
||||
|
||||
if cmd.Use != "config" {
|
||||
t.Errorf("Expected Use to be 'config', got '%s'", cmd.Use)
|
||||
}
|
||||
|
||||
if cmd.Short == "" {
|
||||
t.Error("Short description should not be empty")
|
||||
}
|
||||
|
||||
if cmd.Long == "" {
|
||||
t.Error("Long description should not be empty")
|
||||
}
|
||||
|
||||
if cmd.Run == nil {
|
||||
t.Error("Run function should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCommand_Output(t *testing.T) {
|
||||
// Test that the command returns the expected config path
|
||||
expectedPath := config.Path()
|
||||
|
||||
// The path should not be empty
|
||||
if expectedPath == "" {
|
||||
t.Error("Expected config path to not be empty")
|
||||
}
|
||||
|
||||
// The path should end with config.json
|
||||
if !strings.HasSuffix(expectedPath, "config.json") {
|
||||
t.Errorf("Expected path to end with 'config.json', got '%s'", expectedPath)
|
||||
}
|
||||
|
||||
// The path should contain the coolify directory
|
||||
if !strings.Contains(expectedPath, "coolify") {
|
||||
t.Errorf("Expected path to contain 'coolify', got '%s'", expectedPath)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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,10 @@
|
||||
package coolTypes
|
||||
|
||||
var Redacted = "********"
|
||||
|
||||
type Instance struct {
|
||||
Name string `json:"name"`
|
||||
Default bool `json:"default"`
|
||||
Fqdn string `json:"fqdn"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
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,131 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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
@@ -1,96 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package emoji
|
||||
|
||||
const (
|
||||
CheckMarkButton = "\u2705" // ✅
|
||||
CrossMark = "\u274c" // ❌
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user