2978 lines
76 KiB
Go
2978 lines
76 KiB
Go
// This is free software for the public good of a permacomputer hosted at
|
|
// permacomputer.com, an always-on computer by the people, for the people.
|
|
// One which is durable, easy to repair, & distributed like tap water
|
|
// for machine learning intelligence.
|
|
//
|
|
// The permacomputer is community-owned infrastructure optimized around
|
|
// four values:
|
|
//
|
|
// TRUTH First principles, math & science, open source code freely distributed
|
|
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
|
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
|
// LOVE Be yourself without hurting others, cooperation through natural law
|
|
//
|
|
// This software contributes to that vision by enabling code execution across 42+ programming languages through a unified interface, accessible to all.
|
|
// Code is seeds to sprout on any abandoned technology.
|
|
|
|
package un_async
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
APIBase = "https://api.unsandbox.com"
|
|
LanguagesCacheTTL = 3600 // 1 hour in seconds
|
|
)
|
|
|
|
var (
|
|
// PollDelaysMs defines exponential backoff delays for job polling
|
|
PollDelaysMs = []int{300, 450, 700, 900, 650, 1600, 2000}
|
|
|
|
// LanguageMap maps file extensions to programming languages
|
|
LanguageMap = map[string]string{
|
|
"py": "python",
|
|
"js": "javascript",
|
|
"ts": "typescript",
|
|
"rb": "ruby",
|
|
"php": "php",
|
|
"pl": "perl",
|
|
"sh": "bash",
|
|
"r": "r",
|
|
"R": "r",
|
|
"lua": "lua",
|
|
"go": "go",
|
|
"rs": "rust",
|
|
"c": "c",
|
|
"cpp": "cpp",
|
|
"cc": "cpp",
|
|
"cxx": "cpp",
|
|
"java": "java",
|
|
"kt": "kotlin",
|
|
"m": "objc",
|
|
"cs": "csharp",
|
|
"fs": "fsharp",
|
|
"hs": "haskell",
|
|
"ml": "ocaml",
|
|
"clj": "clojure",
|
|
"scm": "scheme",
|
|
"ss": "scheme",
|
|
"erl": "erlang",
|
|
"ex": "elixir",
|
|
"exs": "elixir",
|
|
"jl": "julia",
|
|
"d": "d",
|
|
"nim": "nim",
|
|
"zig": "zig",
|
|
"v": "v",
|
|
"cr": "crystal",
|
|
"dart": "dart",
|
|
"groovy": "groovy",
|
|
"f90": "fortran",
|
|
"f95": "fortran",
|
|
"lisp": "commonlisp",
|
|
"lsp": "commonlisp",
|
|
"cob": "cobol",
|
|
"tcl": "tcl",
|
|
"raku": "raku",
|
|
"pro": "prolog",
|
|
"p": "prolog",
|
|
"4th": "forth",
|
|
"forth": "forth",
|
|
"fth": "forth",
|
|
}
|
|
)
|
|
|
|
// CredentialsError is returned when credentials cannot be found or are invalid
|
|
type CredentialsError struct {
|
|
Message string
|
|
}
|
|
|
|
func (e *CredentialsError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// Credentials holds the public and secret API keys
|
|
type Credentials struct {
|
|
PublicKey string
|
|
SecretKey string
|
|
}
|
|
|
|
// Result types for channel-based async responses
|
|
|
|
// ExecuteResult contains the result of an execution request
|
|
type ExecuteResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// JobIDResult contains the result of an async execution request
|
|
type JobIDResult struct {
|
|
JobID string
|
|
Err error
|
|
}
|
|
|
|
// JobResult contains the result of a job query
|
|
type JobResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// JobListResult contains the result of listing jobs
|
|
type JobListResult struct {
|
|
Jobs []map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// LanguagesResult contains the result of a languages query
|
|
type LanguagesResult struct {
|
|
Languages []string
|
|
Err error
|
|
}
|
|
|
|
// SnapshotResult contains the result of a snapshot operation
|
|
type SnapshotResult struct {
|
|
SnapshotID string
|
|
Err error
|
|
}
|
|
|
|
// SnapshotListResult contains the result of listing snapshots
|
|
type SnapshotListResult struct {
|
|
Snapshots []map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// RestoreResult contains the result of restoring a snapshot
|
|
type RestoreResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// DeleteResult contains the result of a delete operation
|
|
type DeleteResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// CancelResult contains the result of cancelling a job
|
|
type CancelResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// getUnsandboxDir returns ~/.unsandbox directory path, creating if necessary
|
|
func getUnsandboxDir() (string, error) {
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
dir := filepath.Join(currentUser.HomeDir, ".unsandbox")
|
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
return "", err
|
|
}
|
|
return dir, nil
|
|
}
|
|
|
|
// loadCredentialsFromCsv loads credentials from CSV file (public_key,secret_key per line)
|
|
func loadCredentialsFromCsv(csvPath string, accountIndex int) *Credentials {
|
|
data, err := os.ReadFile(csvPath)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
currentIndex := 0
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
if currentIndex == accountIndex {
|
|
parts := strings.Split(line, ",")
|
|
if len(parts) >= 2 {
|
|
return &Credentials{
|
|
PublicKey: strings.TrimSpace(parts[0]),
|
|
SecretKey: strings.TrimSpace(parts[1]),
|
|
}
|
|
}
|
|
}
|
|
currentIndex++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ResolveCredentials resolves credentials from 4-tier priority system.
|
|
//
|
|
// Priority:
|
|
// 1. Credentials struct fields (PublicKey, SecretKey non-empty)
|
|
// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
|
// 3. ~/.unsandbox/accounts.csv
|
|
// 4. ./accounts.csv
|
|
func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
|
// Tier 1: Function arguments
|
|
if publicKey != "" && secretKey != "" {
|
|
return &Credentials{
|
|
PublicKey: publicKey,
|
|
SecretKey: secretKey,
|
|
}, nil
|
|
}
|
|
|
|
// Tier 2: Environment variables
|
|
envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
|
envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
|
|
if envPk != "" && envSk != "" {
|
|
return &Credentials{
|
|
PublicKey: envPk,
|
|
SecretKey: envSk,
|
|
}, nil
|
|
}
|
|
|
|
// Determine account index
|
|
accountIndex := 0
|
|
if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
|
|
var err error
|
|
accountIndex, err = strconv.Atoi(envAccount)
|
|
if err != nil {
|
|
accountIndex = 0
|
|
}
|
|
}
|
|
|
|
// Tier 3: ~/.unsandbox/accounts.csv
|
|
unsandboxDir, err := getUnsandboxDir()
|
|
if err == nil {
|
|
if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil {
|
|
return creds, nil
|
|
}
|
|
}
|
|
|
|
// Tier 4: ./accounts.csv
|
|
if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil {
|
|
return creds, nil
|
|
}
|
|
|
|
return nil, &CredentialsError{
|
|
Message: "No credentials found. Please provide via:\n" +
|
|
" 1. Function arguments (publicKey, secretKey)\n" +
|
|
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
|
" 3. ~/.unsandbox/accounts.csv\n" +
|
|
" 4. ./accounts.csv",
|
|
}
|
|
}
|
|
|
|
// signRequest signs a request using HMAC-SHA256
|
|
//
|
|
// Message format: "timestamp:METHOD:path:body"
|
|
// Returns: 64-character hex string
|
|
func signRequest(secretKey string, timestamp int64, method, path string, body []byte) string {
|
|
bodyStr := ""
|
|
if body != nil {
|
|
bodyStr = string(body)
|
|
}
|
|
message := fmt.Sprintf("%d:%s:%s:%s", timestamp, method, path, bodyStr)
|
|
|
|
h := hmac.New(sha256.New, []byte(secretKey))
|
|
h.Write([]byte(message))
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// makeRequest makes an authenticated HTTP request to the API (synchronous, internal use)
|
|
func makeRequest(method, path string, creds *Credentials, data interface{}) (map[string]interface{}, error) {
|
|
url := APIBase + path
|
|
timestamp := time.Now().Unix()
|
|
|
|
var body []byte
|
|
var err error
|
|
if data != nil {
|
|
body, err = json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
signature := signRequest(creds.SecretKey, timestamp, method, path, body)
|
|
|
|
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", creds.PublicKey))
|
|
req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
|
|
req.Header.Set("X-Signature", signature)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("User-Agent", "un-go-async/2.0")
|
|
|
|
client := &http.Client{Timeout: 120 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getLanguagesCachePath returns path to languages cache file
|
|
func getLanguagesCachePath() (string, error) {
|
|
unsandboxDir, err := getUnsandboxDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(unsandboxDir, "languages.json"), nil
|
|
}
|
|
|
|
// loadLanguagesCache loads languages from cache if valid (< 1 hour old)
|
|
func loadLanguagesCache() ([]string, bool) {
|
|
cachePath, err := getLanguagesCachePath()
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
data, err := os.ReadFile(cachePath)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
stat, err := os.Stat(cachePath)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
ageSeconds := int(time.Since(stat.ModTime()).Seconds())
|
|
if ageSeconds >= LanguagesCacheTTL {
|
|
return nil, false
|
|
}
|
|
|
|
var cacheData map[string]interface{}
|
|
if err := json.Unmarshal(data, &cacheData); err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
if langs, ok := cacheData["languages"].([]interface{}); ok {
|
|
result := make([]string, len(langs))
|
|
for i, lang := range langs {
|
|
if s, ok := lang.(string); ok {
|
|
result[i] = s
|
|
}
|
|
}
|
|
return result, true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// saveLanguagesCache saves languages to cache
|
|
func saveLanguagesCache(languages []string) {
|
|
cachePath, err := getLanguagesCachePath()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
cacheData := map[string]interface{}{
|
|
"languages": languages,
|
|
"timestamp": time.Now().Unix(),
|
|
}
|
|
|
|
data, err := json.MarshalIndent(cacheData, "", " ")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
_ = os.WriteFile(cachePath, data, 0600)
|
|
}
|
|
|
|
// ExecuteCode executes code and returns a channel that receives the result.
|
|
// The channel receives exactly one ExecuteResult then closes.
|
|
// This function blocks until completion (polls until job finishes).
|
|
func ExecuteCode(creds *Credentials, language, code string) <-chan ExecuteResult {
|
|
resultChan := make(chan ExecuteResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]string{
|
|
"language": language,
|
|
"code": code,
|
|
}
|
|
|
|
response, err := makeRequest("POST", "/execute", creds, data)
|
|
if err != nil {
|
|
resultChan <- ExecuteResult{Err: err}
|
|
return
|
|
}
|
|
|
|
// If we got a job_id with pending/running status, poll until completion
|
|
if jobID, ok := response["job_id"].(string); ok {
|
|
if status, ok := response["status"].(string); ok && (status == "pending" || status == "running") {
|
|
waitChan := WaitForJob(creds, jobID, 0)
|
|
waitResult := <-waitChan
|
|
resultChan <- ExecuteResult{Data: waitResult.Data, Err: waitResult.Err}
|
|
return
|
|
}
|
|
}
|
|
|
|
resultChan <- ExecuteResult{Data: response}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ExecuteAsync executes code asynchronously and returns a channel that receives the job ID.
|
|
// The channel receives exactly one JobIDResult then closes.
|
|
// This returns immediately with a job_id without waiting for completion.
|
|
func ExecuteAsync(creds *Credentials, language, code string) <-chan JobIDResult {
|
|
resultChan := make(chan JobIDResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]string{
|
|
"language": language,
|
|
"code": code,
|
|
}
|
|
|
|
response, err := makeRequest("POST", "/execute", creds, data)
|
|
if err != nil {
|
|
resultChan <- JobIDResult{Err: err}
|
|
return
|
|
}
|
|
|
|
if jobID, ok := response["job_id"].(string); ok {
|
|
resultChan <- JobIDResult{JobID: jobID}
|
|
return
|
|
}
|
|
|
|
resultChan <- JobIDResult{Err: fmt.Errorf("no job_id in response")}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// GetJob gets current status/result of a job (single poll, no waiting).
|
|
// Returns a channel that receives exactly one JobResult then closes.
|
|
func GetJob(creds *Credentials, jobID string) <-chan JobResult {
|
|
resultChan := make(chan JobResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
|
|
resultChan <- JobResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// WaitForJob waits for job completion with exponential backoff polling.
|
|
// Returns a channel that receives exactly one JobResult then closes.
|
|
//
|
|
// Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...]
|
|
// Cumulative: 300, 750, 1450, 2350, 3000, 4600, 6600ms+
|
|
//
|
|
// timeout: Maximum time to wait (0 = no timeout, wait indefinitely)
|
|
func WaitForJob(creds *Credentials, jobID string, timeout time.Duration) <-chan JobResult {
|
|
resultChan := make(chan JobResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
pollCount := 0
|
|
var deadline time.Time
|
|
if timeout > 0 {
|
|
deadline = time.Now().Add(timeout)
|
|
}
|
|
|
|
for {
|
|
// Check timeout
|
|
if timeout > 0 && time.Now().After(deadline) {
|
|
resultChan <- JobResult{Err: fmt.Errorf("timeout waiting for job %s", jobID)}
|
|
return
|
|
}
|
|
|
|
// Sleep before polling
|
|
delayIdx := pollCount
|
|
if delayIdx >= len(PollDelaysMs) {
|
|
delayIdx = len(PollDelaysMs) - 1
|
|
}
|
|
time.Sleep(time.Duration(PollDelaysMs[delayIdx]) * time.Millisecond)
|
|
pollCount++
|
|
|
|
response, err := makeRequest("GET", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
|
|
if err != nil {
|
|
resultChan <- JobResult{Err: err}
|
|
return
|
|
}
|
|
|
|
if status, ok := response["status"].(string); ok {
|
|
if status == "completed" || status == "failed" || status == "timeout" || status == "cancelled" {
|
|
resultChan <- JobResult{Data: response}
|
|
return
|
|
}
|
|
}
|
|
|
|
// Still running, continue polling
|
|
}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// CancelJob cancels a running job.
|
|
// Returns a channel that receives exactly one CancelResult then closes.
|
|
func CancelJob(creds *Credentials, jobID string) <-chan CancelResult {
|
|
resultChan := make(chan CancelResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("DELETE", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
|
|
resultChan <- CancelResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ListJobs lists all jobs for the authenticated account.
|
|
// Returns a channel that receives exactly one JobListResult then closes.
|
|
func ListJobs(creds *Credentials) <-chan JobListResult {
|
|
resultChan := make(chan JobListResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", "/jobs", creds, nil)
|
|
if err != nil {
|
|
resultChan <- JobListResult{Err: err}
|
|
return
|
|
}
|
|
|
|
var jobs []map[string]interface{}
|
|
if jobsInterface, ok := response["jobs"].([]interface{}); ok {
|
|
jobs = make([]map[string]interface{}, len(jobsInterface))
|
|
for i, job := range jobsInterface {
|
|
if m, ok := job.(map[string]interface{}); ok {
|
|
jobs[i] = m
|
|
}
|
|
}
|
|
}
|
|
|
|
resultChan <- JobListResult{Jobs: jobs}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// GetLanguages gets list of supported programming languages.
|
|
// Results are cached for 1 hour in ~/.unsandbox/languages.json
|
|
// Returns a channel that receives exactly one LanguagesResult then closes.
|
|
func GetLanguages(creds *Credentials) <-chan LanguagesResult {
|
|
resultChan := make(chan LanguagesResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
// Try cache first
|
|
if cached, ok := loadLanguagesCache(); ok {
|
|
resultChan <- LanguagesResult{Languages: cached}
|
|
return
|
|
}
|
|
|
|
response, err := makeRequest("GET", "/languages", creds, nil)
|
|
if err != nil {
|
|
resultChan <- LanguagesResult{Err: err}
|
|
return
|
|
}
|
|
|
|
var languages []string
|
|
if langs, ok := response["languages"].([]interface{}); ok {
|
|
for _, lang := range langs {
|
|
if s, ok := lang.(string); ok {
|
|
languages = append(languages, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache the result
|
|
saveLanguagesCache(languages)
|
|
resultChan <- LanguagesResult{Languages: languages}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// DetectLanguage detects programming language from filename extension.
|
|
// This is a synchronous function (no I/O, no goroutines).
|
|
//
|
|
// Args:
|
|
//
|
|
// filename: Filename to detect language from (e.g., "script.py")
|
|
//
|
|
// Returns:
|
|
//
|
|
// Language identifier (e.g., "python") or empty string if unknown
|
|
//
|
|
// Examples:
|
|
//
|
|
// DetectLanguage("hello.py") // -> "python"
|
|
// DetectLanguage("script.js") // -> "javascript"
|
|
// DetectLanguage("main.go") // -> "go"
|
|
// DetectLanguage("unknown") // -> ""
|
|
func DetectLanguage(filename string) string {
|
|
if !strings.Contains(filename, ".") {
|
|
return ""
|
|
}
|
|
|
|
parts := strings.Split(filename, ".")
|
|
ext := parts[len(parts)-1]
|
|
|
|
// Try exact match first (for case-sensitive extensions like .R)
|
|
if lang, ok := LanguageMap[ext]; ok {
|
|
return lang
|
|
}
|
|
|
|
// Try lowercase match
|
|
if lang, ok := LanguageMap[strings.ToLower(ext)]; ok {
|
|
return lang
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// SessionSnapshot creates a snapshot of a session.
|
|
// Returns a channel that receives exactly one SnapshotResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// sessionID: Session ID to snapshot
|
|
// name: Optional snapshot name (empty string for no name)
|
|
// hot: If true, creates a hot snapshot of running session
|
|
func SessionSnapshot(creds *Credentials, sessionID, name string, hot bool) <-chan SnapshotResult {
|
|
resultChan := make(chan SnapshotResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]interface{}{
|
|
"session_id": sessionID,
|
|
"hot": hot,
|
|
}
|
|
if name != "" {
|
|
data["name"] = name
|
|
}
|
|
|
|
response, err := makeRequest("POST", "/snapshots", creds, data)
|
|
if err != nil {
|
|
resultChan <- SnapshotResult{Err: err}
|
|
return
|
|
}
|
|
|
|
if snapshotID, ok := response["snapshot_id"].(string); ok {
|
|
resultChan <- SnapshotResult{SnapshotID: snapshotID}
|
|
return
|
|
}
|
|
|
|
resultChan <- SnapshotResult{Err: fmt.Errorf("no snapshot_id in response")}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ServiceSnapshot creates a snapshot of a service.
|
|
// Returns a channel that receives exactly one SnapshotResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// serviceID: Service ID to snapshot
|
|
// name: Optional snapshot name (empty string for no name)
|
|
func ServiceSnapshot(creds *Credentials, serviceID, name string) <-chan SnapshotResult {
|
|
resultChan := make(chan SnapshotResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]interface{}{
|
|
"service_id": serviceID,
|
|
}
|
|
if name != "" {
|
|
data["name"] = name
|
|
}
|
|
|
|
response, err := makeRequest("POST", "/snapshots", creds, data)
|
|
if err != nil {
|
|
resultChan <- SnapshotResult{Err: err}
|
|
return
|
|
}
|
|
|
|
if snapshotID, ok := response["snapshot_id"].(string); ok {
|
|
resultChan <- SnapshotResult{SnapshotID: snapshotID}
|
|
return
|
|
}
|
|
|
|
resultChan <- SnapshotResult{Err: fmt.Errorf("no snapshot_id in response")}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ListSnapshots lists all snapshots.
|
|
// Returns a channel that receives exactly one SnapshotListResult then closes.
|
|
func ListSnapshots(creds *Credentials) <-chan SnapshotListResult {
|
|
resultChan := make(chan SnapshotListResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", "/snapshots", creds, nil)
|
|
if err != nil {
|
|
resultChan <- SnapshotListResult{Err: err}
|
|
return
|
|
}
|
|
|
|
var snapshots []map[string]interface{}
|
|
if snapshotsInterface, ok := response["snapshots"].([]interface{}); ok {
|
|
snapshots = make([]map[string]interface{}, len(snapshotsInterface))
|
|
for i, snapshot := range snapshotsInterface {
|
|
if m, ok := snapshot.(map[string]interface{}); ok {
|
|
snapshots[i] = m
|
|
}
|
|
}
|
|
}
|
|
|
|
resultChan <- SnapshotListResult{Snapshots: snapshots}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// RestoreSnapshot restores a snapshot.
|
|
// Returns a channel that receives exactly one RestoreResult then closes.
|
|
func RestoreSnapshot(creds *Credentials, snapshotID string) <-chan RestoreResult {
|
|
resultChan := make(chan RestoreResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/restore", snapshotID), creds, map[string]interface{}{})
|
|
resultChan <- RestoreResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// DeleteSnapshot deletes a snapshot.
|
|
// Returns a channel that receives exactly one DeleteResult then closes.
|
|
func DeleteSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
|
|
resultChan := make(chan DeleteResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("DELETE", fmt.Sprintf("/snapshots/%s", snapshotID), creds, nil)
|
|
resultChan <- DeleteResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ============================================================================
|
|
// Session Operations
|
|
// ============================================================================
|
|
|
|
// SessionOptions contains optional parameters for session creation.
|
|
type SessionOptions struct {
|
|
NetworkMode string // "zerotrust" (default) or "semitrusted"
|
|
Shell string // Shell to use (e.g., "bash", "python3")
|
|
TTL int // Time-to-live in seconds (default: 3600)
|
|
VCPU int // Number of virtual CPUs (default: 1)
|
|
Multiplexer string // Multiplexer to use (e.g., "tmux")
|
|
}
|
|
|
|
// SessionListResult contains the result of listing sessions.
|
|
type SessionListResult struct {
|
|
Sessions []map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// SessionResult contains the result of a session operation.
|
|
type SessionResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// ListSessions lists all active sessions for the authenticated account.
|
|
// Returns a channel that receives exactly one SessionListResult then closes.
|
|
func ListSessions(creds *Credentials) <-chan SessionListResult {
|
|
resultChan := make(chan SessionListResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", "/sessions", creds, nil)
|
|
if err != nil {
|
|
resultChan <- SessionListResult{Err: err}
|
|
return
|
|
}
|
|
|
|
var sessions []map[string]interface{}
|
|
if sessionsInterface, ok := response["sessions"].([]interface{}); ok {
|
|
sessions = make([]map[string]interface{}, len(sessionsInterface))
|
|
for i, session := range sessionsInterface {
|
|
if m, ok := session.(map[string]interface{}); ok {
|
|
sessions[i] = m
|
|
}
|
|
}
|
|
}
|
|
|
|
resultChan <- SessionListResult{Sessions: sessions}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// GetSession gets details of a specific session.
|
|
// Returns a channel that receives exactly one SessionResult then closes.
|
|
func GetSession(creds *Credentials, sessionID string) <-chan SessionResult {
|
|
resultChan := make(chan SessionResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
|
|
resultChan <- SessionResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// CreateSession creates a new interactive session.
|
|
// Returns a channel that receives exactly one SessionResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// opts: Optional session configuration (can be nil for defaults)
|
|
func CreateSession(creds *Credentials, opts *SessionOptions) <-chan SessionResult {
|
|
resultChan := make(chan SessionResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := make(map[string]interface{})
|
|
|
|
if opts != nil {
|
|
if opts.NetworkMode != "" {
|
|
data["network_mode"] = opts.NetworkMode
|
|
}
|
|
if opts.Shell != "" {
|
|
data["shell"] = opts.Shell
|
|
}
|
|
if opts.TTL > 0 {
|
|
data["ttl"] = opts.TTL
|
|
}
|
|
if opts.VCPU > 0 {
|
|
data["vcpu"] = opts.VCPU
|
|
}
|
|
if opts.Multiplexer != "" {
|
|
data["multiplexer"] = opts.Multiplexer
|
|
}
|
|
}
|
|
|
|
response, err := makeRequest("POST", "/sessions", creds, data)
|
|
resultChan <- SessionResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// DeleteSession terminates a session.
|
|
// Returns a channel that receives exactly one SessionResult then closes.
|
|
func DeleteSession(creds *Credentials, sessionID string) <-chan SessionResult {
|
|
resultChan := make(chan SessionResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("DELETE", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
|
|
resultChan <- SessionResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// FreezeSession freezes a session (pauses execution, preserves state).
|
|
// Returns a channel that receives exactly one SessionResult then closes.
|
|
func FreezeSession(creds *Credentials, sessionID string) <-chan SessionResult {
|
|
resultChan := make(chan SessionResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/freeze", sessionID), creds, map[string]interface{}{})
|
|
resultChan <- SessionResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// UnfreezeSession unfreezes a previously frozen session.
|
|
// Returns a channel that receives exactly one SessionResult then closes.
|
|
func UnfreezeSession(creds *Credentials, sessionID string) <-chan SessionResult {
|
|
resultChan := make(chan SessionResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/unfreeze", sessionID), creds, map[string]interface{}{})
|
|
resultChan <- SessionResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// BoostSession increases the vCPU allocation for a session.
|
|
// Returns a channel that receives exactly one SessionResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// sessionID: Session ID to boost
|
|
// vcpu: Number of vCPUs (2, 4, 8, etc.)
|
|
func BoostSession(creds *Credentials, sessionID string, vcpu int) <-chan SessionResult {
|
|
resultChan := make(chan SessionResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]interface{}{
|
|
"vcpu": vcpu,
|
|
}
|
|
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/boost", sessionID), creds, data)
|
|
resultChan <- SessionResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// UnboostSession resets the vCPU allocation for a session to default.
|
|
// Returns a channel that receives exactly one SessionResult then closes.
|
|
func UnboostSession(creds *Credentials, sessionID string) <-chan SessionResult {
|
|
resultChan := make(chan SessionResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/unboost", sessionID), creds, map[string]interface{}{})
|
|
resultChan <- SessionResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ShellSession executes a command in a session's shell.
|
|
// Returns a channel that receives exactly one SessionResult then closes.
|
|
//
|
|
// Note: For interactive shell access, use the WebSocket-based shell endpoint.
|
|
// This function is for executing single commands.
|
|
func ShellSession(creds *Credentials, sessionID, command string) <-chan SessionResult {
|
|
resultChan := make(chan SessionResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]interface{}{
|
|
"command": command,
|
|
}
|
|
response, err := makeRequest("POST", fmt.Sprintf("/sessions/%s/shell", sessionID), creds, data)
|
|
resultChan <- SessionResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ============================================================================
|
|
// Service Operations
|
|
// ============================================================================
|
|
|
|
// ServiceOptions contains optional parameters for service creation.
|
|
type ServiceOptions struct {
|
|
NetworkMode string // "zerotrust" (default) or "semitrusted"
|
|
Shell string // Shell to use for bootstrap
|
|
VCPU int // Number of virtual CPUs
|
|
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
|
|
}
|
|
|
|
// ServiceUpdateOptions contains optional parameters for service updates.
|
|
type ServiceUpdateOptions struct {
|
|
VCPU int // Number of virtual CPUs
|
|
}
|
|
|
|
// ServiceListResult contains the result of listing services.
|
|
type ServiceListResult struct {
|
|
Services []map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// ServiceResult contains the result of a service operation.
|
|
type ServiceResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// ListServices lists all services for the authenticated account.
|
|
// Returns a channel that receives exactly one ServiceListResult then closes.
|
|
func ListServices(creds *Credentials) <-chan ServiceListResult {
|
|
resultChan := make(chan ServiceListResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", "/services", creds, nil)
|
|
if err != nil {
|
|
resultChan <- ServiceListResult{Err: err}
|
|
return
|
|
}
|
|
|
|
var services []map[string]interface{}
|
|
if servicesInterface, ok := response["services"].([]interface{}); ok {
|
|
services = make([]map[string]interface{}, len(servicesInterface))
|
|
for i, service := range servicesInterface {
|
|
if m, ok := service.(map[string]interface{}); ok {
|
|
services[i] = m
|
|
}
|
|
}
|
|
}
|
|
|
|
resultChan <- ServiceListResult{Services: services}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// CreateService creates a new persistent service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// name: Service name
|
|
// ports: Array of port numbers to expose
|
|
// bootstrap: Bootstrap script to run on service start
|
|
// opts: Optional service configuration (can be nil for defaults)
|
|
func CreateService(creds *Credentials, name string, ports []int, bootstrap string, opts *ServiceOptions) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]interface{}{
|
|
"name": name,
|
|
"ports": ports,
|
|
"bootstrap": bootstrap,
|
|
}
|
|
|
|
if opts != nil {
|
|
if opts.NetworkMode != "" {
|
|
data["network_mode"] = opts.NetworkMode
|
|
}
|
|
if opts.Shell != "" {
|
|
data["shell"] = opts.Shell
|
|
}
|
|
if opts.VCPU > 0 {
|
|
data["vcpu"] = opts.VCPU
|
|
}
|
|
if opts.UnfreezeOnDemand {
|
|
data["unfreeze_on_demand"] = true
|
|
}
|
|
}
|
|
|
|
response, err := makeRequest("POST", "/services", creds, data)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// GetService gets details of a specific service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func GetService(creds *Credentials, serviceID string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", fmt.Sprintf("/services/%s", serviceID), creds, nil)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// UpdateService updates a service's configuration.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func UpdateService(creds *Credentials, serviceID string, opts *ServiceUpdateOptions) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := make(map[string]interface{})
|
|
if opts != nil {
|
|
if opts.VCPU > 0 {
|
|
data["vcpu"] = opts.VCPU
|
|
}
|
|
}
|
|
response, err := makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, data)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// DeleteService destroys a service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func DeleteService(creds *Credentials, serviceID string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("DELETE", fmt.Sprintf("/services/%s", serviceID), creds, nil)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// FreezeService freezes a service (pauses execution, preserves state).
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func FreezeService(creds *Credentials, serviceID string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/freeze", serviceID), creds, map[string]interface{}{})
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// UnfreezeService unfreezes a previously frozen service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func UnfreezeService(creds *Credentials, serviceID string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/unfreeze", serviceID), creds, map[string]interface{}{})
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// LockService locks a service to prevent modifications or deletion.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func LockService(creds *Credentials, serviceID string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/lock", serviceID), creds, map[string]interface{}{})
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// UnlockService unlocks a previously locked service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func UnlockService(creds *Credentials, serviceID string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/unlock", serviceID), creds, map[string]interface{}{})
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// SetUnfreezeOnDemand enables or disables automatic unfreezing on HTTP request.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func SetUnfreezeOnDemand(creds *Credentials, serviceID string, enabled bool) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, map[string]interface{}{
|
|
"unfreeze_on_demand": enabled,
|
|
})
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// SetShowFreezePage enables or disables the freeze page display for a service.
|
|
// When enabled, frozen services show a branded "waking up" page instead of an error.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func SetShowFreezePage(creds *Credentials, serviceID string, enabled bool) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, map[string]interface{}{
|
|
"show_freeze_page": enabled,
|
|
})
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// GetServiceLogs retrieves logs from a service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// serviceID: Service ID
|
|
// all: If true, returns all logs; if false, returns only recent logs
|
|
func GetServiceLogs(creds *Credentials, serviceID string, all bool) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
path := fmt.Sprintf("/services/%s/logs", serviceID)
|
|
if all {
|
|
path = fmt.Sprintf("/services/%s/logs?all=true", serviceID)
|
|
}
|
|
response, err := makeRequest("GET", path, creds, nil)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// GetServiceEnv retrieves the environment variable names for a service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
// Note: Values are not returned for security; use ExportServiceEnv for full export.
|
|
func GetServiceEnv(creds *Credentials, serviceID string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", fmt.Sprintf("/services/%s/env", serviceID), creds, nil)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// SetServiceEnv sets environment variables for a service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// serviceID: Service ID
|
|
// env: Map of environment variable names to values
|
|
func SetServiceEnv(creds *Credentials, serviceID string, env map[string]string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/env", serviceID), creds, env)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// DeleteServiceEnv deletes environment variables from a service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// serviceID: Service ID
|
|
// keys: List of environment variable names to delete (nil deletes all)
|
|
func DeleteServiceEnv(creds *Credentials, serviceID string, keys []string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
var data interface{}
|
|
if keys != nil {
|
|
data = map[string]interface{}{"keys": keys}
|
|
}
|
|
response, err := makeRequest("DELETE", fmt.Sprintf("/services/%s/env", serviceID), creds, data)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ExportServiceEnv exports all environment variables for a service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
// Returns the full .env format content with values.
|
|
func ExportServiceEnv(creds *Credentials, serviceID string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/env/export", serviceID), creds, map[string]interface{}{})
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// RedeployService redeploys a service with optional new bootstrap script.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// serviceID: Service ID
|
|
// bootstrap: New bootstrap script (empty string to keep existing)
|
|
func RedeployService(creds *Credentials, serviceID string, bootstrap string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := make(map[string]interface{})
|
|
if bootstrap != "" {
|
|
data["bootstrap"] = bootstrap
|
|
}
|
|
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ExecuteInService executes a command in a running service.
|
|
// Returns a channel that receives exactly one ServiceResult then closes.
|
|
func ExecuteInService(creds *Credentials, serviceID, command string) <-chan ServiceResult {
|
|
resultChan := make(chan ServiceResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]interface{}{
|
|
"command": command,
|
|
}
|
|
response, err := makeRequest("POST", fmt.Sprintf("/services/%s/execute", serviceID), creds, data)
|
|
resultChan <- ServiceResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ============================================================================
|
|
// Additional Snapshot Operations
|
|
// ============================================================================
|
|
|
|
// LockSnapshot locks a snapshot to prevent deletion.
|
|
// Returns a channel that receives exactly one DeleteResult then closes.
|
|
func LockSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
|
|
resultChan := make(chan DeleteResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/lock", snapshotID), creds, map[string]interface{}{})
|
|
resultChan <- DeleteResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// UnlockSnapshot unlocks a previously locked snapshot.
|
|
// Returns a channel that receives exactly one DeleteResult then closes.
|
|
func UnlockSnapshot(creds *Credentials, snapshotID string) <-chan DeleteResult {
|
|
resultChan := make(chan DeleteResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/unlock", snapshotID), creds, map[string]interface{}{})
|
|
resultChan <- DeleteResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// CloneSnapshotOptions contains optional parameters for snapshot cloning.
|
|
type CloneSnapshotOptions struct {
|
|
Name string // Name for the cloned resource
|
|
Shell string // Shell to use (for session clones)
|
|
Ports []int // Ports to expose (for service clones)
|
|
}
|
|
|
|
// CloneSnapshotResult contains the result of cloning a snapshot.
|
|
type CloneSnapshotResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// CloneSnapshot clones a snapshot into a new session or service.
|
|
// Returns a channel that receives exactly one CloneSnapshotResult then closes.
|
|
//
|
|
// Args:
|
|
//
|
|
// creds: API credentials
|
|
// snapshotID: Snapshot ID to clone
|
|
// cloneType: "session" or "service"
|
|
// opts: Optional clone configuration (can be nil)
|
|
func CloneSnapshot(creds *Credentials, snapshotID, cloneType string, opts *CloneSnapshotOptions) <-chan CloneSnapshotResult {
|
|
resultChan := make(chan CloneSnapshotResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
data := map[string]interface{}{
|
|
"type": cloneType,
|
|
}
|
|
|
|
if opts != nil {
|
|
if opts.Name != "" {
|
|
data["name"] = opts.Name
|
|
}
|
|
if opts.Shell != "" {
|
|
data["shell"] = opts.Shell
|
|
}
|
|
if opts.Ports != nil {
|
|
data["ports"] = opts.Ports
|
|
}
|
|
}
|
|
|
|
response, err := makeRequest("POST", fmt.Sprintf("/snapshots/%s/clone", snapshotID), creds, data)
|
|
resultChan <- CloneSnapshotResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ============================================================================
|
|
// Key Validation
|
|
// ============================================================================
|
|
|
|
// ValidateKeysResult contains the result of validating API keys.
|
|
type ValidateKeysResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// ValidateKeys validates the API credentials with the server.
|
|
// Returns a channel that receives exactly one ValidateKeysResult then closes.
|
|
// Returns account information if valid, error if invalid.
|
|
func ValidateKeys(creds *Credentials) <-chan ValidateKeysResult {
|
|
resultChan := make(chan ValidateKeysResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("POST", "/keys/validate", creds, map[string]interface{}{})
|
|
resultChan <- ValidateKeysResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ============================================================================
|
|
// Image Generation
|
|
// ============================================================================
|
|
|
|
// ImageOptions contains options for image generation
|
|
type ImageOptions struct {
|
|
Model string
|
|
Size string // default "1024x1024"
|
|
Quality string // "standard" or "hd"
|
|
N int // number of images
|
|
}
|
|
|
|
// ImageResult contains the generated images
|
|
type ImageResult struct {
|
|
Images []string
|
|
CreatedAt string
|
|
Err error
|
|
}
|
|
|
|
// Image generates images from a text prompt using AI.
|
|
// Returns a channel that receives exactly one ImageResult then closes.
|
|
func Image(creds *Credentials, prompt string, opts *ImageOptions) <-chan ImageResult {
|
|
resultChan := make(chan ImageResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
if opts == nil {
|
|
opts = &ImageOptions{}
|
|
}
|
|
if opts.Size == "" {
|
|
opts.Size = "1024x1024"
|
|
}
|
|
if opts.Quality == "" {
|
|
opts.Quality = "standard"
|
|
}
|
|
if opts.N == 0 {
|
|
opts.N = 1
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"prompt": prompt,
|
|
"size": opts.Size,
|
|
"quality": opts.Quality,
|
|
"n": opts.N,
|
|
}
|
|
if opts.Model != "" {
|
|
payload["model"] = opts.Model
|
|
}
|
|
|
|
response, err := makeRequest("POST", "/image", creds, payload)
|
|
if err != nil {
|
|
resultChan <- ImageResult{Err: err}
|
|
return
|
|
}
|
|
|
|
// Extract images array from response
|
|
var images []string
|
|
if imagesInterface, ok := response["images"].([]interface{}); ok {
|
|
for _, img := range imagesInterface {
|
|
if s, ok := img.(string); ok {
|
|
images = append(images, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
createdAt := ""
|
|
if ca, ok := response["created_at"].(string); ok {
|
|
createdAt = ca
|
|
}
|
|
|
|
resultChan <- ImageResult{
|
|
Images: images,
|
|
CreatedAt: createdAt,
|
|
}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ============================================================================
|
|
// Snapshot Info
|
|
// ============================================================================
|
|
|
|
// GetSnapshotResult contains the result of getting a snapshot
|
|
type GetSnapshotResult struct {
|
|
Data map[string]interface{}
|
|
Err error
|
|
}
|
|
|
|
// GetSnapshot gets details of a specific snapshot.
|
|
// Returns a channel that receives exactly one GetSnapshotResult then closes.
|
|
func GetSnapshot(creds *Credentials, snapshotID string) <-chan GetSnapshotResult {
|
|
resultChan := make(chan GetSnapshotResult, 1)
|
|
|
|
go func() {
|
|
defer close(resultChan)
|
|
|
|
response, err := makeRequest("GET", fmt.Sprintf("/snapshots/%s", snapshotID), creds, nil)
|
|
resultChan <- GetSnapshotResult{Data: response, Err: err}
|
|
}()
|
|
|
|
return resultChan
|
|
}
|
|
|
|
// ============================================================================
|
|
// CLI Implementation
|
|
// ============================================================================
|
|
|
|
// Exit codes
|
|
const (
|
|
ExitSuccess = 0
|
|
ExitGeneralError = 1
|
|
ExitInvalidArgs = 2
|
|
ExitAuthError = 3
|
|
ExitAPIError = 4
|
|
ExitTimeout = 5
|
|
)
|
|
|
|
// CLIOptions holds parsed CLI arguments
|
|
type CLIOptions struct {
|
|
// Global options
|
|
Shell string
|
|
Env []string
|
|
Files []string
|
|
FilePaths []string
|
|
Artifacts bool
|
|
OutputDir string
|
|
PublicKey string
|
|
SecretKey string
|
|
Network string
|
|
VCPU int
|
|
Yes bool
|
|
Help bool
|
|
|
|
// Command
|
|
Command string
|
|
SubCommand string
|
|
Args []string
|
|
}
|
|
|
|
// printUsage prints the main help message
|
|
func printUsage() {
|
|
fmt.Fprintf(os.Stderr, `unsandbox - Execute code securely in sandboxed containers (async)
|
|
|
|
Usage:
|
|
un [options] <source_file> Execute code file
|
|
un [options] -s LANG 'code' Execute inline code
|
|
un session [options] Interactive session
|
|
un service [options] Manage services
|
|
un snapshot [options] Manage snapshots
|
|
un key Check API key
|
|
|
|
Global Options:
|
|
-s, --shell LANG Language for inline code
|
|
-e, --env KEY=VAL Set environment variable (can repeat)
|
|
-f, --file FILE Add input file to /tmp/
|
|
-F, --file-path FILE Add input file with path preserved
|
|
-a, --artifacts Return compiled artifacts
|
|
-o, --output DIR Output directory for artifacts
|
|
-p, --public-key KEY API public key
|
|
-k, --secret-key KEY API secret key
|
|
-n, --network MODE Network: zerotrust (default) or semitrusted
|
|
-v, --vcpu N vCPU count (1-8)
|
|
-y, --yes Skip confirmation prompts
|
|
-h, --help Show help
|
|
|
|
Examples:
|
|
un script.py Execute Python script
|
|
un -s bash 'echo hello' Inline bash command
|
|
un -e DEBUG=1 script.py With environment variable
|
|
un -n semitrusted crawler.py With network access
|
|
un session --tmux Persistent interactive session
|
|
un service --list List all services
|
|
`)
|
|
}
|
|
|
|
// printSessionUsage prints session subcommand help
|
|
func printSessionUsage() {
|
|
fmt.Fprintf(os.Stderr, `un session - Interactive session management
|
|
|
|
Usage:
|
|
un session [options] Start new interactive session
|
|
un session --list List active sessions
|
|
un session --attach ID Reconnect to session
|
|
un session --kill ID Terminate session
|
|
|
|
Options:
|
|
--shell SHELL Shell/REPL to use (default: bash)
|
|
-l, --list List active sessions
|
|
--attach ID Reconnect to existing session
|
|
--kill ID Terminate a session
|
|
--freeze ID Pause session
|
|
--unfreeze ID Resume session
|
|
--boost ID Add vCPUs/RAM
|
|
--unboost ID Remove boost
|
|
--tmux Enable persistence with tmux
|
|
--screen Enable persistence with screen
|
|
--snapshot ID Create snapshot
|
|
--snapshot-name NAME Name for snapshot
|
|
--hot Live snapshot (no freeze)
|
|
--audit Record session
|
|
-n, --network MODE Network: zerotrust or semitrusted
|
|
-v, --vcpu N vCPU count (1-8)
|
|
|
|
Examples:
|
|
un session Interactive bash
|
|
un session --shell python3 Python REPL
|
|
un session --tmux Persistent session
|
|
un session --list List sessions
|
|
un session --attach abc123 Reconnect to session
|
|
`)
|
|
}
|
|
|
|
// printServiceUsage prints service subcommand help
|
|
func printServiceUsage() {
|
|
fmt.Fprintf(os.Stderr, `un service - Service management
|
|
|
|
Usage:
|
|
un service --list List all services
|
|
un service --name NAME --ports PORTS --bootstrap CMD
|
|
un service --info ID Get service details
|
|
un service --logs ID Get bootstrap logs
|
|
un service env [status|set|export|delete] ID
|
|
|
|
Options:
|
|
--name NAME Service name (creates new)
|
|
--ports PORTS Comma-separated ports
|
|
--domains DOMAINS Custom domains
|
|
--type TYPE Service type (minecraft, tcp, udp)
|
|
--bootstrap CMD Bootstrap command
|
|
--bootstrap-file FILE Bootstrap from file
|
|
--env-file FILE Load env from .env file
|
|
-l, --list List all services
|
|
--info ID Get service details
|
|
--logs ID Get all logs
|
|
--tail ID Get last 9000 lines
|
|
--freeze ID Pause service
|
|
--unfreeze ID Resume service
|
|
--destroy ID Delete service
|
|
--lock ID Prevent deletion
|
|
--unlock ID Allow deletion
|
|
--resize ID Resize (with --vcpu)
|
|
--redeploy ID Re-run bootstrap
|
|
--execute ID CMD Run command
|
|
--snapshot ID Create snapshot
|
|
|
|
Service Environment Vault:
|
|
un service env status ID Show vault status
|
|
un service env set ID Set from --env-file or stdin
|
|
un service env export ID Export to stdout
|
|
un service env delete ID Delete vault
|
|
|
|
Examples:
|
|
un service --list
|
|
un service --name web --ports 80 --bootstrap "python -m http.server 80"
|
|
un service --info abc123
|
|
un service --execute abc123 'ls -la'
|
|
un service env set abc123 --env-file .env
|
|
`)
|
|
}
|
|
|
|
// printSnapshotUsage prints snapshot subcommand help
|
|
func printSnapshotUsage() {
|
|
fmt.Fprintf(os.Stderr, `un snapshot - Snapshot management
|
|
|
|
Usage:
|
|
un snapshot --list List all snapshots
|
|
un snapshot --info ID Get details
|
|
un snapshot --delete ID Delete snapshot
|
|
un snapshot --clone ID Clone to new session/service
|
|
|
|
Options:
|
|
-l, --list List all snapshots
|
|
--info ID Get snapshot details
|
|
--delete ID Delete snapshot
|
|
--lock ID Prevent deletion
|
|
--unlock ID Allow deletion
|
|
--clone ID Clone snapshot
|
|
--type TYPE Clone type: session or service
|
|
--name NAME Name for cloned service
|
|
--shell SHELL Shell for cloned session
|
|
--ports PORTS Ports for cloned service
|
|
|
|
Examples:
|
|
un snapshot --list
|
|
un snapshot --info abc123
|
|
un snapshot --delete abc123
|
|
un snapshot --clone abc123 --type service --name myapp --ports 80
|
|
`)
|
|
}
|
|
|
|
// cliError prints error to stderr and returns exit code
|
|
func cliError(msg string, code int) int {
|
|
fmt.Fprintf(os.Stderr, "Error: %s\n", msg)
|
|
return code
|
|
}
|
|
|
|
// parsePorts parses comma-separated port numbers
|
|
func parsePorts(s string) ([]int, error) {
|
|
if s == "" {
|
|
return nil, nil
|
|
}
|
|
parts := strings.Split(s, ",")
|
|
ports := make([]int, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
port, err := strconv.Atoi(p)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid port: %s", p)
|
|
}
|
|
ports = append(ports, port)
|
|
}
|
|
return ports, nil
|
|
}
|
|
|
|
// formatList formats a list of items for display
|
|
func formatList(items []map[string]interface{}, fields []string) {
|
|
if len(items) == 0 {
|
|
fmt.Println("No items found.")
|
|
return
|
|
}
|
|
|
|
// Print header
|
|
header := strings.Join(fields, "\t")
|
|
fmt.Println(header)
|
|
|
|
// Print items
|
|
for _, item := range items {
|
|
values := make([]string, len(fields))
|
|
for i, field := range fields {
|
|
if val, ok := item[field]; ok {
|
|
values[i] = fmt.Sprintf("%v", val)
|
|
} else {
|
|
values[i] = "-"
|
|
}
|
|
}
|
|
fmt.Println(strings.Join(values, "\t"))
|
|
}
|
|
}
|
|
|
|
// readFileContents reads file contents for bootstrapping
|
|
func readFileContents(path string) (string, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// readEnvFile reads environment variables from a .env file
|
|
func readEnvFile(path string) (map[string]string, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
env := make(map[string]string)
|
|
lines := strings.Split(string(data), "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) == 2 {
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
// Remove surrounding quotes if present
|
|
if len(value) >= 2 && (value[0] == '"' && value[len(value)-1] == '"' ||
|
|
value[0] == '\'' && value[len(value)-1] == '\'') {
|
|
value = value[1 : len(value)-1]
|
|
}
|
|
env[key] = value
|
|
}
|
|
}
|
|
return env, nil
|
|
}
|
|
|
|
// runExecute handles the execute command (default)
|
|
func runExecute(creds *Credentials, opts *CLIOptions) int {
|
|
var language, code string
|
|
|
|
if opts.Shell != "" {
|
|
// Inline code mode: -s LANG 'code'
|
|
language = opts.Shell
|
|
if len(opts.Args) == 0 {
|
|
return cliError("no code provided for inline execution", ExitInvalidArgs)
|
|
}
|
|
code = opts.Args[0]
|
|
} else {
|
|
// File mode
|
|
if len(opts.Args) == 0 {
|
|
return cliError("no source file provided", ExitInvalidArgs)
|
|
}
|
|
filename := opts.Args[0]
|
|
language = DetectLanguage(filename)
|
|
if language == "" {
|
|
return cliError(fmt.Sprintf("cannot detect language for: %s", filename), ExitInvalidArgs)
|
|
}
|
|
fileContent, err := readFileContents(filename)
|
|
if err != nil {
|
|
return cliError(fmt.Sprintf("cannot read file: %s", err), ExitGeneralError)
|
|
}
|
|
code = fileContent
|
|
}
|
|
|
|
// Build request data
|
|
data := map[string]interface{}{
|
|
"language": language,
|
|
"code": code,
|
|
}
|
|
|
|
if opts.Network != "" {
|
|
data["network_mode"] = opts.Network
|
|
}
|
|
if opts.VCPU > 0 {
|
|
data["vcpu"] = opts.VCPU
|
|
}
|
|
if len(opts.Env) > 0 {
|
|
envMap := make(map[string]string)
|
|
for _, e := range opts.Env {
|
|
parts := strings.SplitN(e, "=", 2)
|
|
if len(parts) == 2 {
|
|
envMap[parts[0]] = parts[1]
|
|
}
|
|
}
|
|
data["env"] = envMap
|
|
}
|
|
|
|
// Execute asynchronously and wait for result
|
|
resultChan := ExecuteCode(creds, language, code)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
|
|
// Print output
|
|
if stdout, ok := result.Data["stdout"].(string); ok && stdout != "" {
|
|
fmt.Print(stdout)
|
|
}
|
|
if stderr, ok := result.Data["stderr"].(string); ok && stderr != "" {
|
|
fmt.Fprint(os.Stderr, stderr)
|
|
}
|
|
|
|
// Print summary
|
|
fmt.Println("---")
|
|
if exitCode, ok := result.Data["exit_code"].(float64); ok {
|
|
fmt.Printf("Exit code: %d\n", int(exitCode))
|
|
}
|
|
if execTime, ok := result.Data["execution_time_ms"].(float64); ok {
|
|
fmt.Printf("Execution time: %dms\n", int(execTime))
|
|
}
|
|
|
|
if exitCode, ok := result.Data["exit_code"].(float64); ok && exitCode != 0 {
|
|
return int(exitCode)
|
|
}
|
|
return ExitSuccess
|
|
}
|
|
|
|
// runSession handles the session command
|
|
func runSession(creds *Credentials, args []string, opts *CLIOptions) int {
|
|
fs := &sessionFlags{}
|
|
parseSessionFlags(args, fs)
|
|
|
|
if fs.help {
|
|
printSessionUsage()
|
|
return ExitSuccess
|
|
}
|
|
|
|
// List sessions
|
|
if fs.list {
|
|
resultChan := ListSessions(creds)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
formatList(result.Sessions, []string{"session_id", "status", "shell", "created_at"})
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Attach to session
|
|
if fs.attach != "" {
|
|
resultChan := GetSession(creds, fs.attach)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
// Print connection info
|
|
fmt.Printf("Session: %s\n", fs.attach)
|
|
if wsURL, ok := result.Data["websocket_url"].(string); ok {
|
|
fmt.Printf("Connect via: %s\n", wsURL)
|
|
}
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Kill session
|
|
if fs.kill != "" {
|
|
resultChan := DeleteSession(creds, fs.kill)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Session %s terminated\n", fs.kill)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Freeze session
|
|
if fs.freeze != "" {
|
|
resultChan := FreezeSession(creds, fs.freeze)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Session %s frozen\n", fs.freeze)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Unfreeze session
|
|
if fs.unfreeze != "" {
|
|
resultChan := UnfreezeSession(creds, fs.unfreeze)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Session %s unfrozen\n", fs.unfreeze)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Boost session
|
|
if fs.boost != "" {
|
|
vcpu := opts.VCPU
|
|
if vcpu == 0 {
|
|
vcpu = 2
|
|
}
|
|
resultChan := BoostSession(creds, fs.boost, vcpu)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Session %s boosted to %d vCPUs\n", fs.boost, vcpu)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Unboost session
|
|
if fs.unboost != "" {
|
|
resultChan := UnboostSession(creds, fs.unboost)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Session %s unboost\n", fs.unboost)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Snapshot session
|
|
if fs.snapshot != "" {
|
|
resultChan := SessionSnapshot(creds, fs.snapshot, fs.snapshotName, fs.hot)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Snapshot created: %s\n", result.SnapshotID)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Create new session
|
|
sessionOpts := &SessionOptions{}
|
|
if fs.shell != "" {
|
|
sessionOpts.Shell = fs.shell
|
|
}
|
|
if opts.Network != "" {
|
|
sessionOpts.NetworkMode = opts.Network
|
|
}
|
|
if opts.VCPU > 0 {
|
|
sessionOpts.VCPU = opts.VCPU
|
|
}
|
|
if fs.tmux {
|
|
sessionOpts.Multiplexer = "tmux"
|
|
} else if fs.screen {
|
|
sessionOpts.Multiplexer = "screen"
|
|
}
|
|
|
|
resultChan := CreateSession(creds, sessionOpts)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
|
|
fmt.Printf("Session created: %v\n", result.Data["session_id"])
|
|
if wsURL, ok := result.Data["websocket_url"].(string); ok {
|
|
fmt.Printf("Connect via: %s\n", wsURL)
|
|
}
|
|
return ExitSuccess
|
|
}
|
|
|
|
type sessionFlags struct {
|
|
shell string
|
|
list bool
|
|
attach string
|
|
kill string
|
|
freeze string
|
|
unfreeze string
|
|
boost string
|
|
unboost string
|
|
tmux bool
|
|
screen bool
|
|
snapshot string
|
|
snapshotName string
|
|
hot bool
|
|
audit bool
|
|
help bool
|
|
}
|
|
|
|
func parseSessionFlags(args []string, fs *sessionFlags) {
|
|
for i := 0; i < len(args); i++ {
|
|
arg := args[i]
|
|
switch arg {
|
|
case "--shell":
|
|
if i+1 < len(args) {
|
|
fs.shell = args[i+1]
|
|
i++
|
|
}
|
|
case "-l", "--list":
|
|
fs.list = true
|
|
case "--attach":
|
|
if i+1 < len(args) {
|
|
fs.attach = args[i+1]
|
|
i++
|
|
}
|
|
case "--kill":
|
|
if i+1 < len(args) {
|
|
fs.kill = args[i+1]
|
|
i++
|
|
}
|
|
case "--freeze":
|
|
if i+1 < len(args) {
|
|
fs.freeze = args[i+1]
|
|
i++
|
|
}
|
|
case "--unfreeze":
|
|
if i+1 < len(args) {
|
|
fs.unfreeze = args[i+1]
|
|
i++
|
|
}
|
|
case "--boost":
|
|
if i+1 < len(args) {
|
|
fs.boost = args[i+1]
|
|
i++
|
|
}
|
|
case "--unboost":
|
|
if i+1 < len(args) {
|
|
fs.unboost = args[i+1]
|
|
i++
|
|
}
|
|
case "--tmux":
|
|
fs.tmux = true
|
|
case "--screen":
|
|
fs.screen = true
|
|
case "--snapshot":
|
|
if i+1 < len(args) {
|
|
fs.snapshot = args[i+1]
|
|
i++
|
|
}
|
|
case "--snapshot-name":
|
|
if i+1 < len(args) {
|
|
fs.snapshotName = args[i+1]
|
|
i++
|
|
}
|
|
case "--hot":
|
|
fs.hot = true
|
|
case "--audit":
|
|
fs.audit = true
|
|
case "-h", "--help":
|
|
fs.help = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// runService handles the service command
|
|
func runService(creds *Credentials, args []string, opts *CLIOptions) int {
|
|
fs := &serviceFlags{}
|
|
parseServiceFlags(args, fs)
|
|
|
|
if fs.help {
|
|
printServiceUsage()
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Handle env subcommand
|
|
if fs.envCmd != "" {
|
|
return runServiceEnv(creds, fs, opts)
|
|
}
|
|
|
|
// List services
|
|
if fs.list {
|
|
resultChan := ListServices(creds)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
formatList(result.Services, []string{"service_id", "name", "status", "created_at"})
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Get service info
|
|
if fs.info != "" {
|
|
resultChan := GetService(creds, fs.info)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
jsonOut, _ := json.MarshalIndent(result.Data, "", " ")
|
|
fmt.Println(string(jsonOut))
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Get service logs
|
|
if fs.logs != "" {
|
|
resultChan := GetServiceLogs(creds, fs.logs, true)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
if content, ok := result.Data["logs"].(string); ok {
|
|
fmt.Print(content)
|
|
}
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Get service tail logs
|
|
if fs.tail != "" {
|
|
resultChan := GetServiceLogs(creds, fs.tail, false)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
if content, ok := result.Data["logs"].(string); ok {
|
|
fmt.Print(content)
|
|
}
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Freeze service
|
|
if fs.freeze != "" {
|
|
resultChan := FreezeService(creds, fs.freeze)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Service %s frozen\n", fs.freeze)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Unfreeze service
|
|
if fs.unfreeze != "" {
|
|
resultChan := UnfreezeService(creds, fs.unfreeze)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Service %s unfrozen\n", fs.unfreeze)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Destroy service
|
|
if fs.destroy != "" {
|
|
resultChan := DeleteService(creds, fs.destroy)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Service %s destroyed\n", fs.destroy)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Lock service
|
|
if fs.lock != "" {
|
|
resultChan := LockService(creds, fs.lock)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Service %s locked\n", fs.lock)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Unlock service
|
|
if fs.unlock != "" {
|
|
resultChan := UnlockService(creds, fs.unlock)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Service %s unlocked\n", fs.unlock)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Resize service
|
|
if fs.resize != "" {
|
|
if opts.VCPU == 0 {
|
|
return cliError("--vcpu required for resize", ExitInvalidArgs)
|
|
}
|
|
resultChan := UpdateService(creds, fs.resize, &ServiceUpdateOptions{VCPU: opts.VCPU})
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Service %s resized to %d vCPUs\n", fs.resize, opts.VCPU)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Redeploy service
|
|
if fs.redeploy != "" {
|
|
resultChan := RedeployService(creds, fs.redeploy, fs.bootstrap)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Service %s redeployed\n", fs.redeploy)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Execute in service
|
|
if fs.execute != "" && fs.executeCmd != "" {
|
|
resultChan := ExecuteInService(creds, fs.execute, fs.executeCmd)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
if stdout, ok := result.Data["stdout"].(string); ok {
|
|
fmt.Print(stdout)
|
|
}
|
|
if stderr, ok := result.Data["stderr"].(string); ok {
|
|
fmt.Fprint(os.Stderr, stderr)
|
|
}
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Snapshot service
|
|
if fs.snapshot != "" {
|
|
resultChan := ServiceSnapshot(creds, fs.snapshot, fs.snapshotName)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Snapshot created: %s\n", result.SnapshotID)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Create new service
|
|
if fs.name != "" {
|
|
if fs.ports == "" {
|
|
return cliError("--ports required for new service", ExitInvalidArgs)
|
|
}
|
|
ports, err := parsePorts(fs.ports)
|
|
if err != nil {
|
|
return cliError(err.Error(), ExitInvalidArgs)
|
|
}
|
|
|
|
bootstrap := fs.bootstrap
|
|
if fs.bootstrapFile != "" {
|
|
content, err := readFileContents(fs.bootstrapFile)
|
|
if err != nil {
|
|
return cliError(fmt.Sprintf("cannot read bootstrap file: %s", err), ExitGeneralError)
|
|
}
|
|
bootstrap = content
|
|
}
|
|
|
|
serviceOpts := &ServiceOptions{}
|
|
if opts.Network != "" {
|
|
serviceOpts.NetworkMode = opts.Network
|
|
}
|
|
if opts.VCPU > 0 {
|
|
serviceOpts.VCPU = opts.VCPU
|
|
}
|
|
|
|
resultChan := CreateService(creds, fs.name, ports, bootstrap, serviceOpts)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
|
|
fmt.Printf("Service created: %v\n", result.Data["service_id"])
|
|
if url, ok := result.Data["url"].(string); ok {
|
|
fmt.Printf("URL: %s\n", url)
|
|
}
|
|
return ExitSuccess
|
|
}
|
|
|
|
printServiceUsage()
|
|
return ExitInvalidArgs
|
|
}
|
|
|
|
type serviceFlags struct {
|
|
name string
|
|
ports string
|
|
domains string
|
|
serviceType string
|
|
bootstrap string
|
|
bootstrapFile string
|
|
envFile string
|
|
list bool
|
|
info string
|
|
logs string
|
|
tail string
|
|
freeze string
|
|
unfreeze string
|
|
destroy string
|
|
lock string
|
|
unlock string
|
|
resize string
|
|
redeploy string
|
|
execute string
|
|
executeCmd string
|
|
snapshot string
|
|
snapshotName string
|
|
hot bool
|
|
envCmd string
|
|
envID string
|
|
help bool
|
|
}
|
|
|
|
func parseServiceFlags(args []string, fs *serviceFlags) {
|
|
for i := 0; i < len(args); i++ {
|
|
arg := args[i]
|
|
switch arg {
|
|
case "--name":
|
|
if i+1 < len(args) {
|
|
fs.name = args[i+1]
|
|
i++
|
|
}
|
|
case "--ports":
|
|
if i+1 < len(args) {
|
|
fs.ports = args[i+1]
|
|
i++
|
|
}
|
|
case "--domains":
|
|
if i+1 < len(args) {
|
|
fs.domains = args[i+1]
|
|
i++
|
|
}
|
|
case "--type":
|
|
if i+1 < len(args) {
|
|
fs.serviceType = args[i+1]
|
|
i++
|
|
}
|
|
case "--bootstrap":
|
|
if i+1 < len(args) {
|
|
fs.bootstrap = args[i+1]
|
|
i++
|
|
}
|
|
case "--bootstrap-file":
|
|
if i+1 < len(args) {
|
|
fs.bootstrapFile = args[i+1]
|
|
i++
|
|
}
|
|
case "--env-file":
|
|
if i+1 < len(args) {
|
|
fs.envFile = args[i+1]
|
|
i++
|
|
}
|
|
case "-l", "--list":
|
|
fs.list = true
|
|
case "--info":
|
|
if i+1 < len(args) {
|
|
fs.info = args[i+1]
|
|
i++
|
|
}
|
|
case "--logs":
|
|
if i+1 < len(args) {
|
|
fs.logs = args[i+1]
|
|
i++
|
|
}
|
|
case "--tail":
|
|
if i+1 < len(args) {
|
|
fs.tail = args[i+1]
|
|
i++
|
|
}
|
|
case "--freeze":
|
|
if i+1 < len(args) {
|
|
fs.freeze = args[i+1]
|
|
i++
|
|
}
|
|
case "--unfreeze":
|
|
if i+1 < len(args) {
|
|
fs.unfreeze = args[i+1]
|
|
i++
|
|
}
|
|
case "--destroy":
|
|
if i+1 < len(args) {
|
|
fs.destroy = args[i+1]
|
|
i++
|
|
}
|
|
case "--lock":
|
|
if i+1 < len(args) {
|
|
fs.lock = args[i+1]
|
|
i++
|
|
}
|
|
case "--unlock":
|
|
if i+1 < len(args) {
|
|
fs.unlock = args[i+1]
|
|
i++
|
|
}
|
|
case "--resize":
|
|
if i+1 < len(args) {
|
|
fs.resize = args[i+1]
|
|
i++
|
|
}
|
|
case "--redeploy":
|
|
if i+1 < len(args) {
|
|
fs.redeploy = args[i+1]
|
|
i++
|
|
}
|
|
case "--execute":
|
|
if i+1 < len(args) {
|
|
fs.execute = args[i+1]
|
|
i++
|
|
// Next arg is the command
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
fs.executeCmd = args[i+1]
|
|
i++
|
|
}
|
|
}
|
|
case "--snapshot":
|
|
if i+1 < len(args) {
|
|
fs.snapshot = args[i+1]
|
|
i++
|
|
}
|
|
case "--snapshot-name":
|
|
if i+1 < len(args) {
|
|
fs.snapshotName = args[i+1]
|
|
i++
|
|
}
|
|
case "--hot":
|
|
fs.hot = true
|
|
case "env":
|
|
// Handle env subcommand
|
|
if i+1 < len(args) {
|
|
fs.envCmd = args[i+1]
|
|
i++
|
|
if i+1 < len(args) {
|
|
fs.envID = args[i+1]
|
|
i++
|
|
}
|
|
}
|
|
case "-h", "--help":
|
|
fs.help = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// runServiceEnv handles service env subcommands
|
|
func runServiceEnv(creds *Credentials, fs *serviceFlags, opts *CLIOptions) int {
|
|
if fs.envID == "" {
|
|
return cliError("service ID required for env command", ExitInvalidArgs)
|
|
}
|
|
|
|
switch fs.envCmd {
|
|
case "status":
|
|
resultChan := GetServiceEnv(creds, fs.envID)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
jsonOut, _ := json.MarshalIndent(result.Data, "", " ")
|
|
fmt.Println(string(jsonOut))
|
|
|
|
case "set":
|
|
var env map[string]string
|
|
var err error
|
|
if fs.envFile != "" {
|
|
env, err = readEnvFile(fs.envFile)
|
|
if err != nil {
|
|
return cliError(fmt.Sprintf("cannot read env file: %s", err), ExitGeneralError)
|
|
}
|
|
} else {
|
|
// Read from stdin
|
|
data, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return cliError(fmt.Sprintf("cannot read stdin: %s", err), ExitGeneralError)
|
|
}
|
|
env = make(map[string]string)
|
|
lines := strings.Split(string(data), "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) == 2 {
|
|
env[parts[0]] = parts[1]
|
|
}
|
|
}
|
|
}
|
|
resultChan := SetServiceEnv(creds, fs.envID, env)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Println("Environment variables set")
|
|
|
|
case "export":
|
|
resultChan := ExportServiceEnv(creds, fs.envID)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
if content, ok := result.Data["content"].(string); ok {
|
|
fmt.Print(content)
|
|
}
|
|
|
|
case "delete":
|
|
resultChan := DeleteServiceEnv(creds, fs.envID, nil)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Println("Environment vault deleted")
|
|
|
|
default:
|
|
return cliError(fmt.Sprintf("unknown env command: %s", fs.envCmd), ExitInvalidArgs)
|
|
}
|
|
|
|
return ExitSuccess
|
|
}
|
|
|
|
// runSnapshot handles the snapshot command
|
|
func runSnapshot(creds *Credentials, args []string, opts *CLIOptions) int {
|
|
fs := &snapshotFlags{}
|
|
parseSnapshotFlags(args, fs)
|
|
|
|
if fs.help {
|
|
printSnapshotUsage()
|
|
return ExitSuccess
|
|
}
|
|
|
|
// List snapshots
|
|
if fs.list {
|
|
resultChan := ListSnapshots(creds)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
formatList(result.Snapshots, []string{"snapshot_id", "name", "type", "created_at"})
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Get snapshot info
|
|
if fs.info != "" {
|
|
resultChan := GetSnapshot(creds, fs.info)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
jsonOut, _ := json.MarshalIndent(result.Data, "", " ")
|
|
fmt.Println(string(jsonOut))
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Delete snapshot
|
|
if fs.deleteID != "" {
|
|
resultChan := DeleteSnapshot(creds, fs.deleteID)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Snapshot %s deleted\n", fs.deleteID)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Lock snapshot
|
|
if fs.lock != "" {
|
|
resultChan := LockSnapshot(creds, fs.lock)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Snapshot %s locked\n", fs.lock)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Unlock snapshot
|
|
if fs.unlock != "" {
|
|
resultChan := UnlockSnapshot(creds, fs.unlock)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
fmt.Printf("Snapshot %s unlocked\n", fs.unlock)
|
|
return ExitSuccess
|
|
}
|
|
|
|
// Clone snapshot
|
|
if fs.clone != "" {
|
|
cloneOpts := &CloneSnapshotOptions{}
|
|
if fs.name != "" {
|
|
cloneOpts.Name = fs.name
|
|
}
|
|
if fs.shell != "" {
|
|
cloneOpts.Shell = fs.shell
|
|
}
|
|
if fs.ports != "" {
|
|
ports, err := parsePorts(fs.ports)
|
|
if err != nil {
|
|
return cliError(err.Error(), ExitInvalidArgs)
|
|
}
|
|
cloneOpts.Ports = ports
|
|
}
|
|
|
|
cloneType := fs.cloneType
|
|
if cloneType == "" {
|
|
cloneType = "session"
|
|
}
|
|
|
|
resultChan := CloneSnapshot(creds, fs.clone, cloneType, cloneOpts)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAPIError)
|
|
}
|
|
jsonOut, _ := json.MarshalIndent(result.Data, "", " ")
|
|
fmt.Println(string(jsonOut))
|
|
return ExitSuccess
|
|
}
|
|
|
|
printSnapshotUsage()
|
|
return ExitInvalidArgs
|
|
}
|
|
|
|
type snapshotFlags struct {
|
|
list bool
|
|
info string
|
|
deleteID string
|
|
lock string
|
|
unlock string
|
|
clone string
|
|
cloneType string
|
|
name string
|
|
shell string
|
|
ports string
|
|
help bool
|
|
}
|
|
|
|
func parseSnapshotFlags(args []string, fs *snapshotFlags) {
|
|
for i := 0; i < len(args); i++ {
|
|
arg := args[i]
|
|
switch arg {
|
|
case "-l", "--list":
|
|
fs.list = true
|
|
case "--info":
|
|
if i+1 < len(args) {
|
|
fs.info = args[i+1]
|
|
i++
|
|
}
|
|
case "--delete":
|
|
if i+1 < len(args) {
|
|
fs.deleteID = args[i+1]
|
|
i++
|
|
}
|
|
case "--lock":
|
|
if i+1 < len(args) {
|
|
fs.lock = args[i+1]
|
|
i++
|
|
}
|
|
case "--unlock":
|
|
if i+1 < len(args) {
|
|
fs.unlock = args[i+1]
|
|
i++
|
|
}
|
|
case "--clone":
|
|
if i+1 < len(args) {
|
|
fs.clone = args[i+1]
|
|
i++
|
|
}
|
|
case "--type":
|
|
if i+1 < len(args) {
|
|
fs.cloneType = args[i+1]
|
|
i++
|
|
}
|
|
case "--name":
|
|
if i+1 < len(args) {
|
|
fs.name = args[i+1]
|
|
i++
|
|
}
|
|
case "--shell":
|
|
if i+1 < len(args) {
|
|
fs.shell = args[i+1]
|
|
i++
|
|
}
|
|
case "--ports":
|
|
if i+1 < len(args) {
|
|
fs.ports = args[i+1]
|
|
i++
|
|
}
|
|
case "-h", "--help":
|
|
fs.help = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// runKey handles the key command
|
|
func runKey(creds *Credentials) int {
|
|
resultChan := ValidateKeys(creds)
|
|
result := <-resultChan
|
|
if result.Err != nil {
|
|
return cliError(result.Err.Error(), ExitAuthError)
|
|
}
|
|
|
|
fmt.Println("API key is valid")
|
|
if account, ok := result.Data["account"].(map[string]interface{}); ok {
|
|
jsonOut, _ := json.MarshalIndent(account, "", " ")
|
|
fmt.Println(string(jsonOut))
|
|
}
|
|
return ExitSuccess
|
|
}
|
|
|
|
// parseGlobalFlags parses global CLI options
|
|
func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
|
opts := &CLIOptions{}
|
|
remaining := []string{}
|
|
|
|
for i := 0; i < len(args); i++ {
|
|
arg := args[i]
|
|
switch {
|
|
case arg == "-s" || arg == "--shell":
|
|
if i+1 < len(args) {
|
|
opts.Shell = args[i+1]
|
|
i++
|
|
}
|
|
case arg == "-e" || arg == "--env":
|
|
if i+1 < len(args) {
|
|
opts.Env = append(opts.Env, args[i+1])
|
|
i++
|
|
}
|
|
case arg == "-f" || arg == "--file":
|
|
if i+1 < len(args) {
|
|
opts.Files = append(opts.Files, args[i+1])
|
|
i++
|
|
}
|
|
case arg == "-F" || arg == "--file-path":
|
|
if i+1 < len(args) {
|
|
opts.FilePaths = append(opts.FilePaths, args[i+1])
|
|
i++
|
|
}
|
|
case arg == "-a" || arg == "--artifacts":
|
|
opts.Artifacts = true
|
|
case arg == "-o" || arg == "--output":
|
|
if i+1 < len(args) {
|
|
opts.OutputDir = args[i+1]
|
|
i++
|
|
}
|
|
case arg == "-p" || arg == "--public-key":
|
|
if i+1 < len(args) {
|
|
opts.PublicKey = args[i+1]
|
|
i++
|
|
}
|
|
case arg == "-k" || arg == "--secret-key":
|
|
if i+1 < len(args) {
|
|
opts.SecretKey = args[i+1]
|
|
i++
|
|
}
|
|
case arg == "-n" || arg == "--network":
|
|
if i+1 < len(args) {
|
|
opts.Network = args[i+1]
|
|
i++
|
|
}
|
|
case arg == "-v" || arg == "--vcpu":
|
|
if i+1 < len(args) {
|
|
if v, err := strconv.Atoi(args[i+1]); err == nil {
|
|
opts.VCPU = v
|
|
}
|
|
i++
|
|
}
|
|
case arg == "-y" || arg == "--yes":
|
|
opts.Yes = true
|
|
case arg == "-h" || arg == "--help":
|
|
opts.Help = true
|
|
default:
|
|
remaining = append(remaining, arg)
|
|
}
|
|
}
|
|
|
|
return opts, remaining
|
|
}
|
|
|
|
// CliMain is the main entry point for the CLI
|
|
func CliMain() {
|
|
if len(os.Args) < 2 {
|
|
printUsage()
|
|
os.Exit(ExitInvalidArgs)
|
|
}
|
|
|
|
opts, remaining := parseGlobalFlags(os.Args[1:])
|
|
|
|
if opts.Help && len(remaining) == 0 {
|
|
printUsage()
|
|
os.Exit(ExitSuccess)
|
|
}
|
|
|
|
// Determine command
|
|
command := ""
|
|
cmdArgs := remaining
|
|
if len(remaining) > 0 {
|
|
switch remaining[0] {
|
|
case "session", "service", "snapshot", "key":
|
|
command = remaining[0]
|
|
cmdArgs = remaining[1:]
|
|
default:
|
|
command = "execute"
|
|
opts.Args = remaining
|
|
}
|
|
}
|
|
|
|
// Resolve credentials
|
|
creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
|
os.Exit(ExitAuthError)
|
|
}
|
|
|
|
// Dispatch command
|
|
var exitCode int
|
|
switch command {
|
|
case "execute", "":
|
|
if opts.Help {
|
|
printUsage()
|
|
exitCode = ExitSuccess
|
|
} else {
|
|
exitCode = runExecute(creds, opts)
|
|
}
|
|
case "session":
|
|
exitCode = runSession(creds, cmdArgs, opts)
|
|
case "service":
|
|
exitCode = runService(creds, cmdArgs, opts)
|
|
case "snapshot":
|
|
exitCode = runSnapshot(creds, cmdArgs, opts)
|
|
case "key":
|
|
exitCode = runKey(creds)
|
|
default:
|
|
printUsage()
|
|
exitCode = ExitInvalidArgs
|
|
}
|
|
|
|
os.Exit(exitCode)
|
|
}
|