modified: .gitignore
modified: CLAUDE.md deleted: __pycache__/un.cpython-312.pyc deleted: __pycache__/un.cpython-313.pyc modified: clients/c/src/un.c modified: clients/java/sync/src/Un.java modified: clients/javascript/sync/src/un.js modified: clients/php/sync/src/un.php modified: clients/ruby/async/src/un_async.rb modified: clients/ruby/sync/src/un.rb modified: clients/rust/async/Cargo.toml modified: clients/rust/async/src/lib.rs new file: clients/rust/async/src/main.rs modified: clients/rust/sync/Cargo.toml modified: clients/rust/sync/src/lib.rs new file: clients/rust/sync/src/main.rs new file: clients/swift/sync/src/un.swift
This commit is contained in:
parent
6d91e987ce
commit
bf4417c914
17 changed files with 6154 additions and 25 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -14,6 +14,8 @@
|
|||
/un_zig
|
||||
/Un.class
|
||||
|
||||
__pycache__/
|
||||
|
||||
# Build directories
|
||||
_build/
|
||||
deps/
|
||||
|
|
|
|||
67
CLAUDE.md
67
CLAUDE.md
|
|
@ -24,39 +24,56 @@ On 2026-01-11, raw `lxc delete` destroyed 8 production services causing complete
|
|||
|
||||
UN CLI Inception - The UN CLI written in every language it can execute. 42+ implementations, one unified interface.
|
||||
|
||||
### SDK Architecture (In Growth)
|
||||
### SDK Architecture
|
||||
|
||||
**Current State**: Root-level implementations (un.py, un.c, un.go, etc.) serving as both CLI + embeddable libraries.
|
||||
|
||||
**Target State**: Migrate to `clients/` directory structure:
|
||||
**Directory Structure**:
|
||||
```
|
||||
clients/
|
||||
├── python/ # clients/python/un.py - sync/async client + CLI
|
||||
├── javascript/ # clients/javascript/un.js - SDK + CLI
|
||||
├── go/ # clients/go/un.go - SDK + CLI
|
||||
├── java/ # clients/java/Un.java - SDK + CLI
|
||||
├── ruby/ # clients/ruby/un.rb - SDK + CLI
|
||||
├── php/ # clients/php/un.php - SDK + CLI
|
||||
├── rust/ # clients/rust/un.rs - SDK + CLI
|
||||
├── {42+ more}/
|
||||
├── python/
|
||||
│ ├── sync/src/un.py # Synchronous (requests) - 2,698 lines
|
||||
│ └── async/src/un_async.py # Asynchronous (aiohttp) - 2,333 lines
|
||||
├── javascript/
|
||||
│ ├── sync/src/un.js # Synchronous (https) - 2,307 lines
|
||||
│ └── async/src/un_async.js # Asynchronous (fetch) - 2,131 lines
|
||||
├── go/
|
||||
│ ├── sync/src/un.go # Synchronous (net/http) - 2,652 lines
|
||||
│ └── async/src/un_async.go # Asynchronous (goroutines) - 3,011 lines
|
||||
├── java/
|
||||
│ ├── sync/src/Un.java # Synchronous (HttpURLConnection) - 3,051 lines
|
||||
│ └── async/src/UnsandboxAsync.java # Asynchronous (CompletableFuture) - 2,685 lines
|
||||
├── ruby/
|
||||
│ ├── sync/src/un.rb # Synchronous (net/http) - 2,423 lines
|
||||
│ └── async/src/un_async.rb # Asynchronous (Future) - 2,441 lines
|
||||
├── rust/
|
||||
│ ├── sync/src/lib.rs # Synchronous (reqwest blocking) - 3,665 lines
|
||||
│ └── async/src/lib.rs # Asynchronous (reqwest + tokio) - 3,375 lines
|
||||
├── php/
|
||||
│ ├── sync/src/un.php # Synchronous (cURL) - 2,818 lines
|
||||
│ └── async/src/UnsandboxAsync.php # Asynchronous (Guzzle promises) - 2,457 lines
|
||||
├── CLI_SPEC.md # Full CLI specification (all SDKs must match)
|
||||
└── README.md # SDK documentation
|
||||
```
|
||||
|
||||
**Each client implementation serves THREE purposes**:
|
||||
1. **Standalone CLI program** - Argparse/getopt with full command support (execute, session, service)
|
||||
2. **Importable client library** - Can import and use as an SDK in other code
|
||||
3. **Embeddable library** - Can be bundled into other language projects
|
||||
**Total: 38,125 lines across 14 SDK files (7 languages × 2 variants)**
|
||||
|
||||
**Example (Python)**:
|
||||
```python
|
||||
# As CLI: python clients/python/un.py test/fib.py
|
||||
# As library: from clients.python.un import UnsandboxClient
|
||||
# As embedded: copy un.py into your project, import locally
|
||||
**Each SDK is BOTH a library AND a CLI tool** (see `clients/CLI_SPEC.md`):
|
||||
```bash
|
||||
# Library usage
|
||||
python -c "from un import execute_code; print(execute_code('python', 'print(1)'))"
|
||||
|
||||
# CLI usage (identical across all languages)
|
||||
python un.py script.py # Execute code file
|
||||
python un.py -s bash 'echo hello' # Inline code
|
||||
python un.py session --tmux # Interactive session
|
||||
python un.py service --list # Manage services
|
||||
```
|
||||
|
||||
**Migration Path**:
|
||||
- Phase 1 (Current): Grow clients/ directory in parallel with root un.* files
|
||||
- Phase 2: Root files eventually deprecated in favor of clients/
|
||||
- Phase 3: Root files maintained for backwards compatibility only
|
||||
**Every SDK implements**:
|
||||
- **43+ API functions** (execute, jobs, sessions, services, snapshots, utilities)
|
||||
- **Full CLI** (execute, session, service, service env, snapshot, key subcommands)
|
||||
- **4-tier credential resolution** (args > env > ~/.unsandbox/accounts.csv > ./accounts.csv)
|
||||
- **HMAC-SHA256 request signing**
|
||||
- **1-hour languages cache** (~/.unsandbox/languages.json)
|
||||
|
||||
## Authentication
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -4673,6 +4673,584 @@ static int clone_snapshot(const UnsandboxCredentials *creds, const char *snapsho
|
|||
// End Service Management Support
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
// LXD Container Images API
|
||||
// ============================================================================
|
||||
|
||||
// Publish an image from a service or snapshot
|
||||
static char* image_publish(const UnsandboxCredentials *creds, const char *source_type,
|
||||
const char *source_id, const char *name, const char *description) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !source_type || !source_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "%s/images", API_BASE);
|
||||
|
||||
// Build JSON body
|
||||
char body[2048];
|
||||
char *p = body;
|
||||
p += sprintf(p, "{\"source_type\":\"%s\",\"source_id\":\"%s\"", source_type, source_id);
|
||||
if (name && strlen(name) > 0) {
|
||||
char *esc = escape_json_string(name);
|
||||
p += sprintf(p, ",\"name\":\"%s\"", esc);
|
||||
free(esc);
|
||||
}
|
||||
if (description && strlen(description) > 0) {
|
||||
char *esc = escape_json_string(description);
|
||||
p += sprintf(p, ",\"description\":\"%s\"", esc);
|
||||
free(esc);
|
||||
}
|
||||
p += sprintf(p, "}");
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", "/images", body);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// List images (filter_type can be NULL, "owned", "shared", or "public")
|
||||
static char* list_images(const UnsandboxCredentials *creds, const char *filter_type) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
if (filter_type && strlen(filter_type) > 0) {
|
||||
snprintf(url, sizeof(url), "%s/images/%s", API_BASE, filter_type);
|
||||
snprintf(path, sizeof(path), "/images/%s", filter_type);
|
||||
} else {
|
||||
snprintf(url, sizeof(url), "%s/images", API_BASE);
|
||||
snprintf(path, sizeof(path), "/images");
|
||||
}
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Get image details
|
||||
static char* get_image(const UnsandboxCredentials *creds, const char *image_id) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s", image_id);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Delete an image
|
||||
static int delete_image(const UnsandboxCredentials *creds, const char *image_id) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s", image_id);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = add_hmac_auth_headers(headers, creds, "DELETE", path, NULL);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
}
|
||||
|
||||
// Lock an image
|
||||
static int lock_image(const UnsandboxCredentials *creds, const char *image_id) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/lock", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/lock", image_id);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
}
|
||||
|
||||
// Unlock an image
|
||||
static int unlock_image(const UnsandboxCredentials *creds, const char *image_id) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/unlock", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/unlock", image_id);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
}
|
||||
|
||||
// Set image visibility (private, unlisted, public)
|
||||
static int set_image_visibility(const UnsandboxCredentials *creds, const char *image_id, const char *visibility) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id || !visibility) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/visibility", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/visibility", image_id);
|
||||
|
||||
char body[128];
|
||||
snprintf(body, sizeof(body), "{\"visibility\":\"%s\"}", visibility);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
}
|
||||
|
||||
// Grant image access to another API key
|
||||
static int grant_image_access(const UnsandboxCredentials *creds, const char *image_id, const char *trusted_api_key) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id || !trusted_api_key) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/grant", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/grant", image_id);
|
||||
|
||||
char body[256];
|
||||
snprintf(body, sizeof(body), "{\"trusted_api_key\":\"%s\"}", trusted_api_key);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
}
|
||||
|
||||
// Revoke image access from another API key
|
||||
static int revoke_image_access(const UnsandboxCredentials *creds, const char *image_id, const char *trusted_api_key) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id || !trusted_api_key) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/revoke", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/revoke", image_id);
|
||||
|
||||
char body[256];
|
||||
snprintf(body, sizeof(body), "{\"trusted_api_key\":\"%s\"}", trusted_api_key);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
}
|
||||
|
||||
// List API keys with access to an image
|
||||
static char* list_image_trusted(const UnsandboxCredentials *creds, const char *image_id) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/trusted", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/trusted", image_id);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Transfer image ownership to another API key
|
||||
static int transfer_image(const UnsandboxCredentials *creds, const char *image_id, const char *to_api_key) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id || !to_api_key) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/transfer", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/transfer", image_id);
|
||||
|
||||
char body[256];
|
||||
snprintf(body, sizeof(body), "{\"to_api_key\":\"%s\"}", to_api_key);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
}
|
||||
|
||||
// Spawn a new service from an image
|
||||
static char* spawn_from_image(const UnsandboxCredentials *creds, const char *image_id,
|
||||
const char *name, const char *ports, const char *bootstrap,
|
||||
const char *network_mode) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/spawn", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/spawn", image_id);
|
||||
|
||||
// Build JSON body
|
||||
char body[4096];
|
||||
char *p = body;
|
||||
p += sprintf(p, "{");
|
||||
int need_comma = 0;
|
||||
|
||||
if (name && strlen(name) > 0) {
|
||||
char *esc = escape_json_string(name);
|
||||
p += sprintf(p, "\"name\":\"%s\"", esc);
|
||||
free(esc);
|
||||
need_comma = 1;
|
||||
}
|
||||
if (ports && strlen(ports) > 0) {
|
||||
if (need_comma) p += sprintf(p, ",");
|
||||
p += sprintf(p, "\"ports\":\"%s\"", ports);
|
||||
need_comma = 1;
|
||||
}
|
||||
if (bootstrap && strlen(bootstrap) > 0) {
|
||||
if (need_comma) p += sprintf(p, ",");
|
||||
char *esc = escape_json_string(bootstrap);
|
||||
p += sprintf(p, "\"bootstrap\":\"%s\"", esc);
|
||||
free(esc);
|
||||
need_comma = 1;
|
||||
}
|
||||
if (network_mode && strlen(network_mode) > 0) {
|
||||
if (need_comma) p += sprintf(p, ",");
|
||||
p += sprintf(p, "\"network_mode\":\"%s\"", network_mode);
|
||||
}
|
||||
p += sprintf(p, "}");
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Clone an image to create a copy owned by the current user
|
||||
static char* clone_image(const UnsandboxCredentials *creds, const char *image_id,
|
||||
const char *name, const char *description) {
|
||||
if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
struct ResponseBuffer response = {0};
|
||||
response.data = malloc(1);
|
||||
response.size = 0;
|
||||
|
||||
char url[256];
|
||||
char path[128];
|
||||
snprintf(url, sizeof(url), "%s/images/%s/clone", API_BASE, image_id);
|
||||
snprintf(path, sizeof(path), "/images/%s/clone", image_id);
|
||||
|
||||
// Build JSON body
|
||||
char body[2048];
|
||||
char *p = body;
|
||||
p += sprintf(p, "{");
|
||||
int need_comma = 0;
|
||||
|
||||
if (name && strlen(name) > 0) {
|
||||
char *esc = escape_json_string(name);
|
||||
p += sprintf(p, "\"name\":\"%s\"", esc);
|
||||
free(esc);
|
||||
need_comma = 1;
|
||||
}
|
||||
if (description && strlen(description) > 0) {
|
||||
if (need_comma) p += sprintf(p, ",");
|
||||
char *esc = escape_json_string(description);
|
||||
p += sprintf(p, "\"description\":\"%s\"", esc);
|
||||
free(esc);
|
||||
}
|
||||
p += sprintf(p, "}");
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// End LXD Container Images API
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
// Key Validation Support
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -1718,6 +1718,369 @@ public class Un {
|
|||
return makeRequest("POST", "/snapshots/" + snapshotId + "/clone", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Images API Methods (LXD Container Images)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Publish a session or service as a reusable LXD container image.
|
||||
*
|
||||
* @param sourceType Source type: "session" or "service"
|
||||
* @param sourceId ID of the session or service to publish
|
||||
* @param name Name for the image
|
||||
* @param description Optional description for the image
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map containing image_id
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> imagePublish(
|
||||
String sourceType,
|
||||
String sourceId,
|
||||
String name,
|
||||
String description,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("source_type", sourceType);
|
||||
data.put("source_id", sourceId);
|
||||
data.put("name", name);
|
||||
if (description != null && !description.isEmpty()) {
|
||||
data.put("description", description);
|
||||
}
|
||||
return makeRequest("POST", "/images", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
/**
|
||||
* List container images with optional filtering.
|
||||
*
|
||||
* @param filterType Optional filter: "own", "shared", "public", or null for all
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return List of image maps containing id, name, description, visibility, etc.
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<Map<String, Object>> listImages(
|
||||
String filterType,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
String path = "/images";
|
||||
if (filterType != null && !filterType.isEmpty()) {
|
||||
path = "/images/" + filterType;
|
||||
}
|
||||
Map<String, Object> response = makeRequest("GET", path, creds[0], creds[1], null);
|
||||
Object images = response.get("images");
|
||||
if (images instanceof List) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object item : (List<?>) images) {
|
||||
if (item instanceof Map) {
|
||||
result.add((Map<String, Object>) item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of a specific image.
|
||||
*
|
||||
* @param imageId Image ID to retrieve
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Image details map
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> getImage(
|
||||
String imageId,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
return makeRequest("GET", "/images/" + imageId, creds[0], creds[1], null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an image.
|
||||
*
|
||||
* @param imageId Image ID to delete
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with deletion confirmation
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> deleteImage(
|
||||
String imageId,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
return makeRequest("DELETE", "/images/" + imageId, creds[0], creds[1], null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock an image (prevent deletion and modifications).
|
||||
*
|
||||
* @param imageId Image ID to lock
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with lock confirmation
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> lockImage(
|
||||
String imageId,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
return makeRequest("POST", "/images/" + imageId + "/lock", creds[0], creds[1], new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock an image (allow deletion and modifications).
|
||||
*
|
||||
* @param imageId Image ID to unlock
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with unlock confirmation
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> unlockImage(
|
||||
String imageId,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
return makeRequest("POST", "/images/" + imageId + "/unlock", creds[0], creds[1], new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visibility of an image.
|
||||
*
|
||||
* @param imageId Image ID to modify
|
||||
* @param visibility Visibility level: "private", "shared", or "public"
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with visibility change confirmation
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> setImageVisibility(
|
||||
String imageId,
|
||||
String visibility,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("visibility", visibility);
|
||||
return makeRequest("POST", "/images/" + imageId + "/visibility", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant access to an image for another API key (for shared images).
|
||||
*
|
||||
* @param imageId Image ID to grant access to
|
||||
* @param trustedApiKey Public API key to grant access to
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with grant confirmation
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> grantImageAccess(
|
||||
String imageId,
|
||||
String trustedApiKey,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("trusted_api_key", trustedApiKey);
|
||||
return makeRequest("POST", "/images/" + imageId + "/grant", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke access to an image from another API key.
|
||||
*
|
||||
* @param imageId Image ID to revoke access from
|
||||
* @param trustedApiKey Public API key to revoke access from
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with revoke confirmation
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> revokeImageAccess(
|
||||
String imageId,
|
||||
String trustedApiKey,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("trusted_api_key", trustedApiKey);
|
||||
return makeRequest("POST", "/images/" + imageId + "/revoke", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
/**
|
||||
* List API keys that have been granted access to an image.
|
||||
*
|
||||
* @param imageId Image ID to list trusted keys for
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return List of trusted API key maps
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<Map<String, Object>> listImageTrusted(
|
||||
String imageId,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> response = makeRequest("GET", "/images/" + imageId + "/trusted", creds[0], creds[1], null);
|
||||
Object trusted = response.get("trusted");
|
||||
if (trusted instanceof List) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object item : (List<?>) trusted) {
|
||||
if (item instanceof Map) {
|
||||
result.add((Map<String, Object>) item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer ownership of an image to another API key.
|
||||
*
|
||||
* @param imageId Image ID to transfer
|
||||
* @param toApiKey Public API key of the new owner
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with transfer confirmation
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> transferImage(
|
||||
String imageId,
|
||||
String toApiKey,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("to_api_key", toApiKey);
|
||||
return makeRequest("POST", "/images/" + imageId + "/transfer", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a new service from an image.
|
||||
*
|
||||
* @param imageId Image ID to spawn from
|
||||
* @param name Name for the new service
|
||||
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
|
||||
* @param bootstrap Optional bootstrap script or URL
|
||||
* @param networkMode Network mode: "zerotrust" or "semitrusted"
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map containing service_id
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> spawnFromImage(
|
||||
String imageId,
|
||||
String name,
|
||||
String ports,
|
||||
String bootstrap,
|
||||
String networkMode,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("name", name);
|
||||
if (ports != null && !ports.isEmpty()) {
|
||||
List<Integer> portList = new ArrayList<>();
|
||||
for (String p : ports.split(",")) {
|
||||
try {
|
||||
portList.add(Integer.parseInt(p.trim()));
|
||||
} catch (NumberFormatException e) {
|
||||
// Skip invalid port
|
||||
}
|
||||
}
|
||||
data.put("ports", portList);
|
||||
}
|
||||
if (bootstrap != null && !bootstrap.isEmpty()) {
|
||||
if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) {
|
||||
data.put("bootstrap_url", bootstrap);
|
||||
} else {
|
||||
data.put("bootstrap", bootstrap);
|
||||
}
|
||||
}
|
||||
if (networkMode != null && !networkMode.isEmpty()) {
|
||||
data.put("network_mode", networkMode);
|
||||
}
|
||||
return makeRequest("POST", "/images/" + imageId + "/spawn", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an image to create a new image with a different name.
|
||||
*
|
||||
* @param imageId Image ID to clone
|
||||
* @param name Name for the cloned image
|
||||
* @param description Optional description for the cloned image
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map containing new image_id
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> cloneImage(
|
||||
String imageId,
|
||||
String name,
|
||||
String description,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
if (name != null && !name.isEmpty()) {
|
||||
data.put("name", name);
|
||||
}
|
||||
if (description != null && !description.isEmpty()) {
|
||||
data.put("description", description);
|
||||
}
|
||||
return makeRequest("POST", "/images/" + imageId + "/clone", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Key Validation API
|
||||
// ========================================================================
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@
|
|||
* // Snapshot management
|
||||
* sessionSnapshot, serviceSnapshot, listSnapshots, restoreSnapshot,
|
||||
* deleteSnapshot, lockSnapshot, unlockSnapshot, cloneSnapshot,
|
||||
* // Images API (LXD container images)
|
||||
* imagePublish, listImages, getImage, deleteImage,
|
||||
* lockImage, unlockImage, setImageVisibility,
|
||||
* grantImageAccess, revokeImageAccess, listImageTrusted,
|
||||
* transferImage, spawnFromImage, cloneImage,
|
||||
* // Key validation
|
||||
* validateKeys,
|
||||
* } = require('./un.js');
|
||||
|
|
@ -1367,6 +1372,20 @@ module.exports = {
|
|||
lockSnapshot,
|
||||
unlockSnapshot,
|
||||
cloneSnapshot,
|
||||
// Images API (LXD container images)
|
||||
imagePublish,
|
||||
listImages,
|
||||
getImage,
|
||||
deleteImage,
|
||||
lockImage,
|
||||
unlockImage,
|
||||
setImageVisibility,
|
||||
grantImageAccess,
|
||||
revokeImageAccess,
|
||||
listImageTrusted,
|
||||
transferImage,
|
||||
spawnFromImage,
|
||||
cloneImage,
|
||||
// Key validation
|
||||
validateKeys,
|
||||
// Image generation
|
||||
|
|
|
|||
|
|
@ -510,6 +510,310 @@ class Unsandbox {
|
|||
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/clone", $publicKey, $secretKey, $data);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Images Methods (LXD Container Images)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Publish a new image from a session or service container.
|
||||
*
|
||||
* @param string $sourceType Source type: "session" or "service"
|
||||
* @param string $sourceId Session ID or Service ID to publish from
|
||||
* @param string|null $name Optional name for the image
|
||||
* @param string|null $description Optional description for the image
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with image_id and image details
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function imagePublish(
|
||||
string $sourceType,
|
||||
string $sourceId,
|
||||
?string $name = null,
|
||||
?string $description = null,
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
|
||||
$data = [
|
||||
'source_type' => $sourceType,
|
||||
'source_id' => $sourceId,
|
||||
];
|
||||
if ($name !== null) {
|
||||
$data['name'] = $name;
|
||||
}
|
||||
if ($description !== null) {
|
||||
$data['description'] = $description;
|
||||
}
|
||||
|
||||
return $this->makeRequest('POST', '/images', $publicKey, $secretKey, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all images owned by the authenticated account.
|
||||
*
|
||||
* @param string|null $filterType Optional filter: "owned", "shared", "public", or null for all
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array List of image arrays
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function listImages(?string $filterType = null, ?string $publicKey = null, ?string $secretKey = null): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
|
||||
$path = $filterType !== null ? "/images/{$filterType}" : '/images';
|
||||
$response = $this->makeRequest('GET', $path, $publicKey, $secretKey);
|
||||
return $response['images'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of a specific image.
|
||||
*
|
||||
* @param string $imageId Image ID
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Image details
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function getImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('GET', "/images/{$imageId}", $publicKey, $secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an image.
|
||||
*
|
||||
* @param string $imageId Image ID to delete
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with deletion confirmation
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function deleteImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('DELETE', "/images/{$imageId}", $publicKey, $secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock an image to prevent deletion.
|
||||
*
|
||||
* @param string $imageId Image ID to lock
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with lock confirmation
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function lockImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('POST', "/images/{$imageId}/lock", $publicKey, $secretKey, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock an image to allow deletion.
|
||||
*
|
||||
* @param string $imageId Image ID to unlock
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with unlock confirmation
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function unlockImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('POST', "/images/{$imageId}/unlock", $publicKey, $secretKey, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set visibility of an image.
|
||||
*
|
||||
* @param string $imageId Image ID
|
||||
* @param string $visibility Visibility level: "private" or "public"
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with visibility update confirmation
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function setImageVisibility(
|
||||
string $imageId,
|
||||
string $visibility,
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('POST', "/images/{$imageId}/visibility", $publicKey, $secretKey, [
|
||||
'visibility' => $visibility,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant access to an image for another API key.
|
||||
*
|
||||
* @param string $imageId Image ID
|
||||
* @param string $trustedApiKey The API key to grant access to
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with grant confirmation
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function grantImageAccess(
|
||||
string $imageId,
|
||||
string $trustedApiKey,
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('POST', "/images/{$imageId}/grant", $publicKey, $secretKey, [
|
||||
'trusted_api_key' => $trustedApiKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke access to an image from another API key.
|
||||
*
|
||||
* @param string $imageId Image ID
|
||||
* @param string $trustedApiKey The API key to revoke access from
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with revoke confirmation
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function revokeImageAccess(
|
||||
string $imageId,
|
||||
string $trustedApiKey,
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('POST', "/images/{$imageId}/revoke", $publicKey, $secretKey, [
|
||||
'trusted_api_key' => $trustedApiKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List API keys that have been granted access to an image.
|
||||
*
|
||||
* @param string $imageId Image ID
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array List of trusted API key information
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function listImageTrusted(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
$response = $this->makeRequest('GET', "/images/{$imageId}/trusted", $publicKey, $secretKey);
|
||||
return $response['trusted'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer ownership of an image to another API key.
|
||||
*
|
||||
* @param string $imageId Image ID to transfer
|
||||
* @param string $toApiKey The API key to transfer ownership to
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with transfer confirmation
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function transferImage(
|
||||
string $imageId,
|
||||
string $toApiKey,
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('POST', "/images/{$imageId}/transfer", $publicKey, $secretKey, [
|
||||
'to_api_key' => $toApiKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a new service from an image.
|
||||
*
|
||||
* @param string $imageId Image ID to spawn from
|
||||
* @param string|null $name Optional service name
|
||||
* @param array|string|null $ports Optional port(s) to expose (array of ints or comma-separated string)
|
||||
* @param string|null $bootstrap Optional bootstrap command or URL
|
||||
* @param string $networkMode Network mode: "zerotrust" or "semitrusted"
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with spawned service info
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function spawnFromImage(
|
||||
string $imageId,
|
||||
?string $name = null,
|
||||
$ports = null,
|
||||
?string $bootstrap = null,
|
||||
string $networkMode = 'zerotrust',
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
|
||||
$data = [
|
||||
'network_mode' => $networkMode,
|
||||
];
|
||||
if ($name !== null) {
|
||||
$data['name'] = $name;
|
||||
}
|
||||
if ($ports !== null) {
|
||||
// Convert ports to array if string
|
||||
if (is_string($ports)) {
|
||||
$ports = array_map('intval', explode(',', $ports));
|
||||
}
|
||||
$data['ports'] = $ports;
|
||||
}
|
||||
if ($bootstrap !== null) {
|
||||
$data['bootstrap'] = $bootstrap;
|
||||
}
|
||||
|
||||
return $this->makeRequest('POST', "/images/{$imageId}/spawn", $publicKey, $secretKey, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an image to create a new image with a different name/description.
|
||||
*
|
||||
* @param string $imageId Image ID to clone
|
||||
* @param string|null $name Optional name for the cloned image
|
||||
* @param string|null $description Optional description for the cloned image
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with cloned image info
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function cloneImage(
|
||||
string $imageId,
|
||||
?string $name = null,
|
||||
?string $description = null,
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
|
||||
$data = [];
|
||||
if ($name !== null) {
|
||||
$data['name'] = $name;
|
||||
}
|
||||
if ($description !== null) {
|
||||
$data['description'] = $description;
|
||||
}
|
||||
|
||||
return $this->makeRequest('POST', "/images/{$imageId}/clone", $publicKey, $secretKey, $data);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Session Methods
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -1549,4 +1549,893 @@ module UnAsync
|
|||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# CLI Implementation
|
||||
# ============================================================================
|
||||
|
||||
# Exit codes
|
||||
EXIT_SUCCESS = 0
|
||||
EXIT_ERROR = 1
|
||||
EXIT_INVALID_ARGS = 2
|
||||
EXIT_AUTH_ERROR = 3
|
||||
EXIT_API_ERROR = 4
|
||||
EXIT_TIMEOUT = 5
|
||||
|
||||
class << self
|
||||
# Main CLI entry point
|
||||
def cli_main
|
||||
# Global options
|
||||
options = {
|
||||
shell: nil,
|
||||
env: [],
|
||||
files: [],
|
||||
file_paths: [],
|
||||
public_key: nil,
|
||||
secret_key: nil,
|
||||
network: 'zerotrust',
|
||||
vcpu: 1,
|
||||
yes: false,
|
||||
artifacts: false,
|
||||
output: nil
|
||||
}
|
||||
|
||||
# Check for subcommands first
|
||||
if ARGV.empty?
|
||||
cli_show_help
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
|
||||
case ARGV[0]
|
||||
when 'session'
|
||||
ARGV.shift
|
||||
cli_session(options)
|
||||
when 'service'
|
||||
ARGV.shift
|
||||
cli_service(options)
|
||||
when 'snapshot'
|
||||
ARGV.shift
|
||||
cli_snapshot(options)
|
||||
when 'key'
|
||||
ARGV.shift
|
||||
cli_key(options)
|
||||
when '-h', '--help', 'help'
|
||||
cli_show_help
|
||||
exit(EXIT_SUCCESS)
|
||||
else
|
||||
cli_execute(options)
|
||||
end
|
||||
rescue CredentialsError => e
|
||||
$stderr.puts "Error: #{e.message}"
|
||||
exit(EXIT_AUTH_ERROR)
|
||||
rescue APIError => e
|
||||
$stderr.puts "Error: #{e.message}"
|
||||
exit(EXIT_API_ERROR)
|
||||
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
||||
$stderr.puts "Error: #{e.message}"
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
rescue Interrupt
|
||||
$stderr.puts "\nInterrupted"
|
||||
exit(EXIT_ERROR)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Show main help
|
||||
def cli_show_help
|
||||
puts <<~HELP
|
||||
unsandbox.com Ruby SDK (Async) - Secure Code Execution
|
||||
|
||||
Usage:
|
||||
ruby un_async.rb [options] <source_file> Execute code file
|
||||
ruby un_async.rb [options] -s LANG 'code' Execute inline code
|
||||
ruby un_async.rb session [options] Manage sessions
|
||||
ruby un_async.rb service [options] Manage services
|
||||
ruby un_async.rb snapshot [options] Manage snapshots
|
||||
ruby un_async.rb 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 mode: zerotrust or semitrusted
|
||||
-v, --vcpu N vCPU count (1-8)
|
||||
-y, --yes Skip confirmation prompts
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
ruby un_async.rb script.py
|
||||
ruby un_async.rb -s python 'print("hello")'
|
||||
ruby un_async.rb -n semitrusted crawler.py
|
||||
ruby un_async.rb session --list
|
||||
ruby un_async.rb service --name web --ports 80 --bootstrap "python -m http.server 80"
|
||||
HELP
|
||||
end
|
||||
|
||||
# Parse global options from ARGV
|
||||
def parse_global_options(options)
|
||||
OptionParser.new do |opts|
|
||||
opts.on('-s', '--shell LANG', 'Language for inline code') do |v|
|
||||
options[:shell] = v
|
||||
end
|
||||
opts.on('-e', '--env KEY=VAL', 'Set environment variable') do |v|
|
||||
options[:env] << v
|
||||
end
|
||||
opts.on('-f', '--file FILE', 'Add input file to /tmp/') do |v|
|
||||
options[:files] << v
|
||||
end
|
||||
opts.on('-F', '--file-path FILE', 'Add input file with path preserved') do |v|
|
||||
options[:file_paths] << v
|
||||
end
|
||||
opts.on('-a', '--artifacts', 'Return compiled artifacts') do
|
||||
options[:artifacts] = true
|
||||
end
|
||||
opts.on('-o', '--output DIR', 'Output directory for artifacts') do |v|
|
||||
options[:output] = v
|
||||
end
|
||||
opts.on('-p', '--public-key KEY', 'API public key') do |v|
|
||||
options[:public_key] = v
|
||||
end
|
||||
opts.on('-k', '--secret-key KEY', 'API secret key') do |v|
|
||||
options[:secret_key] = v
|
||||
end
|
||||
opts.on('-n', '--network MODE', 'Network mode') do |v|
|
||||
options[:network] = v
|
||||
end
|
||||
opts.on('-v', '--vcpu N', Integer, 'vCPU count (1-8)') do |v|
|
||||
options[:vcpu] = v
|
||||
end
|
||||
opts.on('-y', '--yes', 'Skip confirmation prompts') do
|
||||
options[:yes] = true
|
||||
end
|
||||
opts.on('-h', '--help', 'Show help') do
|
||||
yield if block_given?
|
||||
exit(EXIT_SUCCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Execute code command
|
||||
def cli_execute(options)
|
||||
parser = parse_global_options(options) do
|
||||
cli_show_help
|
||||
end
|
||||
parser.parse!(ARGV)
|
||||
|
||||
if ARGV.empty?
|
||||
$stderr.puts 'Error: No source file or code provided'
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
|
||||
code = nil
|
||||
language = nil
|
||||
|
||||
if options[:shell]
|
||||
# Inline code mode: -s LANG 'code'
|
||||
language = options[:shell]
|
||||
code = ARGV.join(' ')
|
||||
else
|
||||
# File mode: script.py
|
||||
filename = ARGV[0]
|
||||
unless File.exist?(filename)
|
||||
$stderr.puts "Error: File not found: #{filename}"
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
code = File.read(filename)
|
||||
language = detect_language(filename)
|
||||
unless language
|
||||
$stderr.puts "Error: Cannot detect language for: #{filename}"
|
||||
$stderr.puts 'Use -s/--shell to specify language'
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
end
|
||||
|
||||
# Execute the code (async call, await result)
|
||||
result = execute_code(
|
||||
language,
|
||||
code,
|
||||
public_key: options[:public_key],
|
||||
secret_key: options[:secret_key]
|
||||
).value
|
||||
|
||||
# Output results
|
||||
cli_print_execute_result(result)
|
||||
end
|
||||
|
||||
# Print execution result
|
||||
def cli_print_execute_result(result)
|
||||
puts result['stdout'] if result['stdout'] && !result['stdout'].empty?
|
||||
$stderr.puts result['stderr'] if result['stderr'] && !result['stderr'].empty?
|
||||
puts '---'
|
||||
puts "Exit code: #{result['exit_code'] || 0}"
|
||||
if result['execution_time_ms']
|
||||
puts "Execution time: #{result['execution_time_ms']}ms"
|
||||
end
|
||||
end
|
||||
|
||||
# Session subcommand
|
||||
def cli_session(options)
|
||||
session_opts = {
|
||||
list: false,
|
||||
attach: nil,
|
||||
kill: nil,
|
||||
freeze: nil,
|
||||
unfreeze: nil,
|
||||
boost: nil,
|
||||
unboost: nil,
|
||||
snapshot: nil,
|
||||
snapshot_name: nil,
|
||||
hot: false,
|
||||
tmux: false,
|
||||
screen: false,
|
||||
shell: 'bash',
|
||||
audit: false
|
||||
}
|
||||
|
||||
parser = parse_global_options(options) do
|
||||
cli_session_help
|
||||
end
|
||||
|
||||
parser.on('-l', '--list', 'List active sessions') do
|
||||
session_opts[:list] = true
|
||||
end
|
||||
parser.on('--attach ID', 'Reconnect to existing session') do |v|
|
||||
session_opts[:attach] = v
|
||||
end
|
||||
parser.on('--kill ID', 'Terminate a session') do |v|
|
||||
session_opts[:kill] = v
|
||||
end
|
||||
parser.on('--freeze ID', 'Pause session') do |v|
|
||||
session_opts[:freeze] = v
|
||||
end
|
||||
parser.on('--unfreeze ID', 'Resume session') do |v|
|
||||
session_opts[:unfreeze] = v
|
||||
end
|
||||
parser.on('--boost ID', 'Add vCPUs/RAM') do |v|
|
||||
session_opts[:boost] = v
|
||||
end
|
||||
parser.on('--unboost ID', 'Remove boost') do |v|
|
||||
session_opts[:unboost] = v
|
||||
end
|
||||
parser.on('--snapshot ID', 'Create snapshot') do |v|
|
||||
session_opts[:snapshot] = v
|
||||
end
|
||||
parser.on('--snapshot-name NAME', 'Name for snapshot') do |v|
|
||||
session_opts[:snapshot_name] = v
|
||||
end
|
||||
parser.on('--hot', 'Live snapshot (no freeze)') do
|
||||
session_opts[:hot] = true
|
||||
end
|
||||
parser.on('--tmux', 'Enable persistence with tmux') do
|
||||
session_opts[:tmux] = true
|
||||
end
|
||||
parser.on('--screen', 'Enable persistence with screen') do
|
||||
session_opts[:screen] = true
|
||||
end
|
||||
parser.on('--shell SHELL', 'Shell/REPL to use') do |v|
|
||||
session_opts[:shell] = v
|
||||
end
|
||||
parser.on('--audit', 'Record session') do
|
||||
session_opts[:audit] = true
|
||||
end
|
||||
|
||||
parser.parse!(ARGV)
|
||||
|
||||
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
|
||||
|
||||
if session_opts[:list]
|
||||
sessions = list_sessions(**creds).value
|
||||
cli_print_sessions_table(sessions)
|
||||
elsif session_opts[:kill]
|
||||
delete_session(session_opts[:kill], **creds).value
|
||||
puts "Session #{session_opts[:kill]} terminated"
|
||||
elsif session_opts[:freeze]
|
||||
freeze_session(session_opts[:freeze], **creds).value
|
||||
puts "Session #{session_opts[:freeze]} frozen"
|
||||
elsif session_opts[:unfreeze]
|
||||
unfreeze_session(session_opts[:unfreeze], **creds).value
|
||||
puts "Session #{session_opts[:unfreeze]} unfrozen"
|
||||
elsif session_opts[:boost]
|
||||
boost_session(session_opts[:boost], **creds).value
|
||||
puts "Session #{session_opts[:boost]} boosted"
|
||||
elsif session_opts[:unboost]
|
||||
unboost_session(session_opts[:unboost], **creds).value
|
||||
puts "Session #{session_opts[:unboost]} unboosted"
|
||||
elsif session_opts[:snapshot]
|
||||
snapshot_id = session_snapshot(
|
||||
session_opts[:snapshot],
|
||||
name: session_opts[:snapshot_name],
|
||||
ephemeral: session_opts[:hot],
|
||||
**creds
|
||||
).value
|
||||
puts "Snapshot created: #{snapshot_id}"
|
||||
elsif session_opts[:attach]
|
||||
# Attach to existing session - show info
|
||||
session = get_session(session_opts[:attach], **creds).value
|
||||
puts "Session: #{session['id']}"
|
||||
puts "Status: #{session['status']}"
|
||||
puts "WebSocket URL: #{session['websocket_url']}" if session['websocket_url']
|
||||
puts "\nNote: Use a WebSocket client to connect interactively"
|
||||
else
|
||||
# Create new session
|
||||
multiplexer = nil
|
||||
multiplexer = 'tmux' if session_opts[:tmux]
|
||||
multiplexer = 'screen' if session_opts[:screen]
|
||||
|
||||
result = create_session(
|
||||
session_opts[:shell],
|
||||
network_mode: options[:network],
|
||||
vcpu: options[:vcpu],
|
||||
multiplexer: multiplexer,
|
||||
**creds
|
||||
).value
|
||||
puts "Session created: #{result['session_id']}"
|
||||
puts "Container: #{result['container_name']}" if result['container_name']
|
||||
puts "WebSocket URL: #{result['websocket_url']}" if result['websocket_url']
|
||||
puts "\nNote: Use a WebSocket client to connect interactively"
|
||||
end
|
||||
end
|
||||
|
||||
# Print sessions table
|
||||
def cli_print_sessions_table(sessions)
|
||||
if sessions.empty?
|
||||
puts 'No active sessions'
|
||||
return
|
||||
end
|
||||
|
||||
# Header
|
||||
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'STATUS', 'CREATED')
|
||||
sessions.each do |s|
|
||||
puts format('%-38s %-20s %-10s %-20s',
|
||||
s['id'] || s['session_id'] || '-',
|
||||
s['name'] || '-',
|
||||
s['status'] || s['state'] || '-',
|
||||
s['created_at'] || '-')
|
||||
end
|
||||
end
|
||||
|
||||
# Session help
|
||||
def cli_session_help
|
||||
puts <<~HELP
|
||||
Session Management
|
||||
|
||||
Usage:
|
||||
ruby un_async.rb session [options]
|
||||
|
||||
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
|
||||
|
||||
Examples:
|
||||
ruby un_async.rb session # New bash session
|
||||
ruby un_async.rb session --shell python3 # Python REPL
|
||||
ruby un_async.rb session --tmux # Persistent session
|
||||
ruby un_async.rb session --list # List sessions
|
||||
ruby un_async.rb session --kill abc123 # Kill session
|
||||
HELP
|
||||
end
|
||||
|
||||
# Service subcommand
|
||||
def cli_service(options)
|
||||
service_opts = {
|
||||
list: false,
|
||||
name: nil,
|
||||
ports: nil,
|
||||
domains: nil,
|
||||
type: nil,
|
||||
bootstrap: nil,
|
||||
bootstrap_file: nil,
|
||||
env_file: nil,
|
||||
info: nil,
|
||||
logs: nil,
|
||||
tail: nil,
|
||||
freeze: nil,
|
||||
unfreeze: nil,
|
||||
destroy: nil,
|
||||
lock: nil,
|
||||
unlock: nil,
|
||||
resize: nil,
|
||||
redeploy: nil,
|
||||
execute: nil,
|
||||
execute_cmd: nil,
|
||||
snapshot: nil,
|
||||
snapshot_name: nil
|
||||
}
|
||||
|
||||
parser = parse_global_options(options) do
|
||||
cli_service_help
|
||||
end
|
||||
|
||||
parser.on('-l', '--list', 'List all services') do
|
||||
service_opts[:list] = true
|
||||
end
|
||||
parser.on('--name NAME', 'Service name (creates new)') do |v|
|
||||
service_opts[:name] = v
|
||||
end
|
||||
parser.on('--ports PORTS', 'Comma-separated ports') do |v|
|
||||
service_opts[:ports] = v.split(',').map(&:to_i)
|
||||
end
|
||||
parser.on('--domains DOMAINS', 'Custom domains') do |v|
|
||||
service_opts[:domains] = v.split(',')
|
||||
end
|
||||
parser.on('--type TYPE', 'Service type (minecraft, tcp, udp)') do |v|
|
||||
service_opts[:type] = v
|
||||
end
|
||||
parser.on('--bootstrap CMD', 'Bootstrap command') do |v|
|
||||
service_opts[:bootstrap] = v
|
||||
end
|
||||
parser.on('--bootstrap-file FILE', 'Bootstrap from file') do |v|
|
||||
service_opts[:bootstrap_file] = v
|
||||
end
|
||||
parser.on('--env-file FILE', 'Load env from .env file') do |v|
|
||||
service_opts[:env_file] = v
|
||||
end
|
||||
parser.on('--info ID', 'Get service details') do |v|
|
||||
service_opts[:info] = v
|
||||
end
|
||||
parser.on('--logs ID', 'Get all logs') do |v|
|
||||
service_opts[:logs] = v
|
||||
end
|
||||
parser.on('--tail ID', 'Get last 9000 lines') do |v|
|
||||
service_opts[:tail] = v
|
||||
end
|
||||
parser.on('--freeze ID', 'Pause service') do |v|
|
||||
service_opts[:freeze] = v
|
||||
end
|
||||
parser.on('--unfreeze ID', 'Resume service') do |v|
|
||||
service_opts[:unfreeze] = v
|
||||
end
|
||||
parser.on('--destroy ID', 'Delete service') do |v|
|
||||
service_opts[:destroy] = v
|
||||
end
|
||||
parser.on('--lock ID', 'Prevent deletion') do |v|
|
||||
service_opts[:lock] = v
|
||||
end
|
||||
parser.on('--unlock ID', 'Allow deletion') do |v|
|
||||
service_opts[:unlock] = v
|
||||
end
|
||||
parser.on('--resize ID', 'Resize (with --vcpu)') do |v|
|
||||
service_opts[:resize] = v
|
||||
end
|
||||
parser.on('--redeploy ID', 'Re-run bootstrap') do |v|
|
||||
service_opts[:redeploy] = v
|
||||
end
|
||||
parser.on('--execute ID', 'Run command in service') do |v|
|
||||
service_opts[:execute] = v
|
||||
end
|
||||
parser.on('--snapshot ID', 'Create snapshot') do |v|
|
||||
service_opts[:snapshot] = v
|
||||
end
|
||||
parser.on('--snapshot-name NAME', 'Name for snapshot') do |v|
|
||||
service_opts[:snapshot_name] = v
|
||||
end
|
||||
|
||||
parser.parse!(ARGV)
|
||||
|
||||
# Check for env subcommand
|
||||
if ARGV[0] == 'env'
|
||||
ARGV.shift
|
||||
cli_service_env(options, service_opts)
|
||||
return
|
||||
end
|
||||
|
||||
# Get command argument for execute
|
||||
service_opts[:execute_cmd] = ARGV.join(' ') if service_opts[:execute] && !ARGV.empty?
|
||||
|
||||
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
|
||||
|
||||
if service_opts[:list]
|
||||
services = list_services(**creds).value
|
||||
cli_print_services_table(services)
|
||||
elsif service_opts[:info]
|
||||
service = get_service(service_opts[:info], **creds).value
|
||||
cli_print_service_info(service)
|
||||
elsif service_opts[:logs]
|
||||
result = get_service_logs(service_opts[:logs], all: true, **creds).value
|
||||
puts result['log'] || result['logs'] || ''
|
||||
elsif service_opts[:tail]
|
||||
result = get_service_logs(service_opts[:tail], all: false, **creds).value
|
||||
puts result['log'] || result['logs'] || ''
|
||||
elsif service_opts[:freeze]
|
||||
freeze_service(service_opts[:freeze], **creds).value
|
||||
puts "Service #{service_opts[:freeze]} frozen"
|
||||
elsif service_opts[:unfreeze]
|
||||
unfreeze_service(service_opts[:unfreeze], **creds).value
|
||||
puts "Service #{service_opts[:unfreeze]} unfrozen"
|
||||
elsif service_opts[:destroy]
|
||||
delete_service(service_opts[:destroy], **creds).value
|
||||
puts "Service #{service_opts[:destroy]} destroyed"
|
||||
elsif service_opts[:lock]
|
||||
lock_service(service_opts[:lock], **creds).value
|
||||
puts "Service #{service_opts[:lock]} locked"
|
||||
elsif service_opts[:unlock]
|
||||
unlock_service(service_opts[:unlock], **creds).value
|
||||
puts "Service #{service_opts[:unlock]} unlocked"
|
||||
elsif service_opts[:resize]
|
||||
update_service(service_opts[:resize], vcpu: options[:vcpu], **creds).value
|
||||
puts "Service #{service_opts[:resize]} resized to #{options[:vcpu]} vCPUs"
|
||||
elsif service_opts[:redeploy]
|
||||
bootstrap = nil
|
||||
if service_opts[:bootstrap_file]
|
||||
bootstrap = File.read(service_opts[:bootstrap_file])
|
||||
elsif service_opts[:bootstrap]
|
||||
bootstrap = service_opts[:bootstrap]
|
||||
end
|
||||
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, **creds).value
|
||||
puts "Service #{service_opts[:redeploy]} redeployed"
|
||||
elsif service_opts[:execute]
|
||||
cmd = service_opts[:execute_cmd]
|
||||
if cmd.nil? || cmd.empty?
|
||||
$stderr.puts 'Error: No command provided for --execute'
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
result = execute_in_service(service_opts[:execute], cmd, **creds).value
|
||||
cli_print_execute_result(result)
|
||||
elsif service_opts[:snapshot]
|
||||
snapshot_id = service_snapshot(
|
||||
service_opts[:snapshot],
|
||||
name: service_opts[:snapshot_name],
|
||||
**creds
|
||||
).value
|
||||
puts "Snapshot created: #{snapshot_id}"
|
||||
elsif service_opts[:name]
|
||||
# Create new service
|
||||
bootstrap = service_opts[:bootstrap]
|
||||
if service_opts[:bootstrap_file]
|
||||
bootstrap = File.read(service_opts[:bootstrap_file])
|
||||
end
|
||||
|
||||
unless bootstrap
|
||||
$stderr.puts 'Error: --bootstrap or --bootstrap-file required'
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
unless service_opts[:ports]
|
||||
$stderr.puts 'Error: --ports required'
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
|
||||
result = create_service(
|
||||
service_opts[:name],
|
||||
service_opts[:ports],
|
||||
bootstrap,
|
||||
network_mode: options[:network],
|
||||
vcpu: options[:vcpu],
|
||||
custom_domains: service_opts[:domains],
|
||||
service_type: service_opts[:type],
|
||||
**creds
|
||||
).value
|
||||
puts "Service created: #{result['service_id']}"
|
||||
puts "URL: #{result['url']}" if result['url']
|
||||
else
|
||||
cli_service_help
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
end
|
||||
|
||||
# Print services table
|
||||
def cli_print_services_table(services)
|
||||
if services.empty?
|
||||
puts 'No services'
|
||||
return
|
||||
end
|
||||
|
||||
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'STATUS', 'CREATED')
|
||||
services.each do |s|
|
||||
puts format('%-38s %-20s %-10s %-20s',
|
||||
s['id'] || s['service_id'] || '-',
|
||||
s['name'] || '-',
|
||||
s['status'] || s['state'] || '-',
|
||||
s['created_at'] || '-')
|
||||
end
|
||||
end
|
||||
|
||||
# Print service info
|
||||
def cli_print_service_info(service)
|
||||
puts "ID: #{service['id'] || service['service_id']}"
|
||||
puts "Name: #{service['name']}"
|
||||
puts "Status: #{service['status'] || service['state']}"
|
||||
puts "URL: #{service['url']}" if service['url']
|
||||
puts "Ports: #{service['ports']&.join(', ')}" if service['ports']
|
||||
puts "vCPU: #{service['vcpu']}" if service['vcpu']
|
||||
puts "Network: #{service['network_mode']}" if service['network_mode']
|
||||
puts "Created: #{service['created_at']}" if service['created_at']
|
||||
puts "Locked: #{service['locked']}" if service.key?('locked')
|
||||
end
|
||||
|
||||
# Service help
|
||||
def cli_service_help
|
||||
puts <<~HELP
|
||||
Service Management
|
||||
|
||||
Usage:
|
||||
ruby un_async.rb service [options]
|
||||
ruby un_async.rb service env <command> <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 in service
|
||||
--snapshot ID Create snapshot
|
||||
|
||||
Env Subcommands:
|
||||
ruby un_async.rb service env status ID Show vault status
|
||||
ruby un_async.rb service env set ID Set from --env-file or stdin
|
||||
ruby un_async.rb service env export ID Export to stdout
|
||||
ruby un_async.rb service env delete ID Delete vault
|
||||
|
||||
Examples:
|
||||
ruby un_async.rb service --name web --ports 80 --bootstrap "python -m http.server 80"
|
||||
ruby un_async.rb service --list
|
||||
ruby un_async.rb service --logs abc123
|
||||
ruby un_async.rb service --execute abc123 'ls -la'
|
||||
ruby un_async.rb service env status abc123
|
||||
HELP
|
||||
end
|
||||
|
||||
# Service env subcommand
|
||||
def cli_service_env(options, service_opts)
|
||||
if ARGV.empty?
|
||||
$stderr.puts 'Error: env subcommand requires: status, set, export, or delete'
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
|
||||
cmd = ARGV.shift
|
||||
service_id = ARGV.shift
|
||||
|
||||
unless service_id
|
||||
$stderr.puts 'Error: Service ID required'
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
|
||||
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
|
||||
|
||||
case cmd
|
||||
when 'status'
|
||||
result = get_service_env(service_id, **creds).value
|
||||
puts "Has vault: #{result['has_vault'] || false}"
|
||||
puts "Variables: #{result['count'] || 0}"
|
||||
puts "Updated: #{result['updated_at']}" if result['updated_at']
|
||||
when 'set'
|
||||
env_content = nil
|
||||
if service_opts[:env_file]
|
||||
env_content = File.read(service_opts[:env_file])
|
||||
elsif !$stdin.tty?
|
||||
env_content = $stdin.read
|
||||
else
|
||||
$stderr.puts 'Error: Provide --env-file or pipe content to stdin'
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
set_service_env(service_id, env_content, **creds).value
|
||||
puts "Environment vault updated for #{service_id}"
|
||||
when 'export'
|
||||
result = export_service_env(service_id, **creds).value
|
||||
puts result['env'] || ''
|
||||
when 'delete'
|
||||
delete_service_env(service_id, **creds).value
|
||||
puts "Environment vault deleted for #{service_id}"
|
||||
else
|
||||
$stderr.puts "Error: Unknown env command: #{cmd}"
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
end
|
||||
|
||||
# Snapshot subcommand
|
||||
def cli_snapshot(options)
|
||||
snapshot_opts = {
|
||||
list: false,
|
||||
info: nil,
|
||||
delete: nil,
|
||||
lock: nil,
|
||||
unlock: nil,
|
||||
clone: nil,
|
||||
clone_type: 'session',
|
||||
clone_name: nil,
|
||||
clone_shell: nil,
|
||||
clone_ports: nil
|
||||
}
|
||||
|
||||
parser = parse_global_options(options) do
|
||||
cli_snapshot_help
|
||||
end
|
||||
|
||||
parser.on('-l', '--list', 'List all snapshots') do
|
||||
snapshot_opts[:list] = true
|
||||
end
|
||||
parser.on('--info ID', 'Get snapshot details') do |v|
|
||||
snapshot_opts[:info] = v
|
||||
end
|
||||
parser.on('--delete ID', 'Delete snapshot') do |v|
|
||||
snapshot_opts[:delete] = v
|
||||
end
|
||||
parser.on('--lock ID', 'Prevent deletion') do |v|
|
||||
snapshot_opts[:lock] = v
|
||||
end
|
||||
parser.on('--unlock ID', 'Allow deletion') do |v|
|
||||
snapshot_opts[:unlock] = v
|
||||
end
|
||||
parser.on('--clone ID', 'Clone snapshot') do |v|
|
||||
snapshot_opts[:clone] = v
|
||||
end
|
||||
parser.on('--type TYPE', 'Clone type: session or service') do |v|
|
||||
snapshot_opts[:clone_type] = v
|
||||
end
|
||||
parser.on('--name NAME', 'Name for cloned resource') do |v|
|
||||
snapshot_opts[:clone_name] = v
|
||||
end
|
||||
parser.on('--shell SHELL', 'Shell for cloned session') do |v|
|
||||
snapshot_opts[:clone_shell] = v
|
||||
end
|
||||
parser.on('--ports PORTS', 'Ports for cloned service') do |v|
|
||||
snapshot_opts[:clone_ports] = v.split(',').map(&:to_i)
|
||||
end
|
||||
|
||||
parser.parse!(ARGV)
|
||||
|
||||
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
|
||||
|
||||
if snapshot_opts[:list]
|
||||
snapshots = list_snapshots(**creds).value
|
||||
cli_print_snapshots_table(snapshots)
|
||||
elsif snapshot_opts[:info]
|
||||
# Get snapshot details via restore endpoint or list
|
||||
snapshots = list_snapshots(**creds).value
|
||||
snapshot = snapshots.find { |s| s['snapshot_id'] == snapshot_opts[:info] || s['id'] == snapshot_opts[:info] }
|
||||
if snapshot
|
||||
cli_print_snapshot_info(snapshot)
|
||||
else
|
||||
$stderr.puts "Error: Snapshot not found: #{snapshot_opts[:info]}"
|
||||
exit(EXIT_ERROR)
|
||||
end
|
||||
elsif snapshot_opts[:delete]
|
||||
delete_snapshot(snapshot_opts[:delete], **creds).value
|
||||
puts "Snapshot #{snapshot_opts[:delete]} deleted"
|
||||
elsif snapshot_opts[:lock]
|
||||
lock_snapshot(snapshot_opts[:lock], **creds).value
|
||||
puts "Snapshot #{snapshot_opts[:lock]} locked"
|
||||
elsif snapshot_opts[:unlock]
|
||||
unlock_snapshot(snapshot_opts[:unlock], **creds).value
|
||||
puts "Snapshot #{snapshot_opts[:unlock]} unlocked"
|
||||
elsif snapshot_opts[:clone]
|
||||
result = clone_snapshot(
|
||||
snapshot_opts[:clone],
|
||||
type: snapshot_opts[:clone_type],
|
||||
name: snapshot_opts[:clone_name],
|
||||
shell: snapshot_opts[:clone_shell],
|
||||
ports: snapshot_opts[:clone_ports],
|
||||
**creds
|
||||
).value
|
||||
if result['session_id']
|
||||
puts "Session created: #{result['session_id']}"
|
||||
elsif result['service_id']
|
||||
puts "Service created: #{result['service_id']}"
|
||||
else
|
||||
puts 'Clone completed'
|
||||
puts JSON.pretty_generate(result)
|
||||
end
|
||||
else
|
||||
cli_snapshot_help
|
||||
exit(EXIT_INVALID_ARGS)
|
||||
end
|
||||
end
|
||||
|
||||
# Print snapshots table
|
||||
def cli_print_snapshots_table(snapshots)
|
||||
if snapshots.empty?
|
||||
puts 'No snapshots'
|
||||
return
|
||||
end
|
||||
|
||||
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'TYPE', 'CREATED')
|
||||
snapshots.each do |s|
|
||||
puts format('%-38s %-20s %-10s %-20s',
|
||||
s['snapshot_id'] || s['id'] || '-',
|
||||
s['name'] || '-',
|
||||
s['type'] || s['source_type'] || '-',
|
||||
s['created_at'] || '-')
|
||||
end
|
||||
end
|
||||
|
||||
# Print snapshot info
|
||||
def cli_print_snapshot_info(snapshot)
|
||||
puts "ID: #{snapshot['snapshot_id'] || snapshot['id']}"
|
||||
puts "Name: #{snapshot['name']}" if snapshot['name']
|
||||
puts "Type: #{snapshot['type'] || snapshot['source_type']}"
|
||||
puts "Source ID: #{snapshot['source_id']}" if snapshot['source_id']
|
||||
puts "Size: #{snapshot['size']}" if snapshot['size']
|
||||
puts "Locked: #{snapshot['locked']}" if snapshot.key?('locked')
|
||||
puts "Created: #{snapshot['created_at']}" if snapshot['created_at']
|
||||
end
|
||||
|
||||
# Snapshot help
|
||||
def cli_snapshot_help
|
||||
puts <<~HELP
|
||||
Snapshot Management
|
||||
|
||||
Usage:
|
||||
ruby un_async.rb snapshot [options]
|
||||
|
||||
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 resource
|
||||
--shell SHELL Shell for cloned session
|
||||
--ports PORTS Ports for cloned service
|
||||
|
||||
Examples:
|
||||
ruby un_async.rb snapshot --list
|
||||
ruby un_async.rb snapshot --clone abc123 --type service --name myapp --ports 80
|
||||
HELP
|
||||
end
|
||||
|
||||
# Key command
|
||||
def cli_key(options)
|
||||
parser = parse_global_options(options) do
|
||||
puts 'Usage: ruby un_async.rb key [-p PUBLIC_KEY] [-k SECRET_KEY]'
|
||||
puts
|
||||
puts 'Check API key validity and show account info'
|
||||
end
|
||||
parser.parse!(ARGV)
|
||||
|
||||
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
|
||||
|
||||
begin
|
||||
result = validate_keys(**creds).value
|
||||
puts "Valid: #{result['valid']}"
|
||||
puts "Account: #{result['account'] || result['account_id']}" if result['account'] || result['account_id']
|
||||
puts "Email: #{result['email']}" if result['email']
|
||||
puts "Plan: #{result['plan']}" if result['plan']
|
||||
puts "Credits: #{result['credits']}" if result['credits']
|
||||
rescue APIError => e
|
||||
if e.status_code == 401 || e.status_code == 403
|
||||
puts 'Valid: false'
|
||||
puts "Error: #{e.message}"
|
||||
exit(EXIT_AUTH_ERROR)
|
||||
end
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# CLI entry point
|
||||
if __FILE__ == $0
|
||||
UnAsync.cli_main
|
||||
end
|
||||
|
|
|
|||
|
|
@ -436,6 +436,267 @@ module Un
|
|||
make_request('POST', "/snapshots/#{snapshot_id}/clone", pk, sk, data)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Image Functions (LXD Container Images)
|
||||
# ============================================================================
|
||||
|
||||
# Publish an LXD container image from a session or service
|
||||
#
|
||||
# @param source_type [String] Source type ("session" or "service")
|
||||
# @param source_id [String] ID of the session or service to publish
|
||||
# @param name [String, nil] Optional name for the image
|
||||
# @param description [String, nil] Optional description for the image
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with image_id and other metadata
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example Publish from session
|
||||
# result = Un.image_publish("session", session_id, name: "my-image")
|
||||
# puts result["image_id"]
|
||||
#
|
||||
# @example Publish from service
|
||||
# result = Un.image_publish("service", service_id, description: "Production snapshot")
|
||||
# puts result["image_id"]
|
||||
def image_publish(source_type, source_id, name: nil, description: nil, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
data = { source_type: source_type, source_id: source_id }
|
||||
data[:name] = name if name
|
||||
data[:description] = description if description
|
||||
make_request('POST', '/images', pk, sk, data)
|
||||
end
|
||||
|
||||
# List all images for the authenticated account
|
||||
#
|
||||
# @param filter_type [String, nil] Optional filter: "own", "shared", or "public"
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Array<Hash>] List of image hashes
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example List all images
|
||||
# images = Un.list_images
|
||||
# images.each { |img| puts "#{img['image_id']}: #{img['name']}" }
|
||||
#
|
||||
# @example List only owned images
|
||||
# owned = Un.list_images(filter_type: "own")
|
||||
#
|
||||
# @example List shared images
|
||||
# shared = Un.list_images(filter_type: "shared")
|
||||
def list_images(filter_type: nil, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
path = filter_type ? "/images/#{filter_type}" : '/images'
|
||||
response = make_request('GET', path, pk, sk)
|
||||
response['images'] || []
|
||||
end
|
||||
|
||||
# Get image details by ID
|
||||
#
|
||||
# @param image_id [String] Image ID to retrieve
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Image details hash
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# image = Un.get_image(image_id)
|
||||
# puts "#{image['name']}: #{image['description']}"
|
||||
def get_image(image_id, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('GET', "/images/#{image_id}", pk, sk)
|
||||
end
|
||||
|
||||
# Delete an image
|
||||
#
|
||||
# @param image_id [String] Image ID to delete
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with deletion confirmation
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# Un.delete_image(image_id)
|
||||
def delete_image(image_id, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('DELETE', "/images/#{image_id}", pk, sk)
|
||||
end
|
||||
|
||||
# Lock an image to prevent deletion
|
||||
#
|
||||
# @param image_id [String] Image ID to lock
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with lock confirmation
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# Un.lock_image(image_id)
|
||||
def lock_image(image_id, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('POST', "/images/#{image_id}/lock", pk, sk, {})
|
||||
end
|
||||
|
||||
# Unlock an image to allow deletion
|
||||
#
|
||||
# @param image_id [String] Image ID to unlock
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with unlock confirmation
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# Un.unlock_image(image_id)
|
||||
def unlock_image(image_id, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('POST', "/images/#{image_id}/unlock", pk, sk, {})
|
||||
end
|
||||
|
||||
# Set image visibility (private, public, or shared)
|
||||
#
|
||||
# @param image_id [String] Image ID to update
|
||||
# @param visibility [String] Visibility level: "private", "public", or "shared"
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with visibility confirmation
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example Make image public
|
||||
# Un.set_image_visibility(image_id, "public")
|
||||
#
|
||||
# @example Make image private
|
||||
# Un.set_image_visibility(image_id, "private")
|
||||
def set_image_visibility(image_id, visibility, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('POST', "/images/#{image_id}/visibility", pk, sk, { visibility: visibility })
|
||||
end
|
||||
|
||||
# Grant access to an image for another API key
|
||||
#
|
||||
# @param image_id [String] Image ID to share
|
||||
# @param trusted_api_key [String] Public API key to grant access to
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with grant confirmation
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# Un.grant_image_access(image_id, "unsb-pk-xxxxx-xxxxx-xxxxx-xxxxx")
|
||||
def grant_image_access(image_id, trusted_api_key, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('POST', "/images/#{image_id}/grant", pk, sk, { trusted_api_key: trusted_api_key })
|
||||
end
|
||||
|
||||
# Revoke access to an image from another API key
|
||||
#
|
||||
# @param image_id [String] Image ID to revoke access from
|
||||
# @param trusted_api_key [String] Public API key to revoke access from
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with revoke confirmation
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# Un.revoke_image_access(image_id, "unsb-pk-xxxxx-xxxxx-xxxxx-xxxxx")
|
||||
def revoke_image_access(image_id, trusted_api_key, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('POST', "/images/#{image_id}/revoke", pk, sk, { trusted_api_key: trusted_api_key })
|
||||
end
|
||||
|
||||
# List API keys with access to an image
|
||||
#
|
||||
# @param image_id [String] Image ID to list trusted keys for
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Array<Hash>] List of trusted API key hashes
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# trusted = Un.list_image_trusted(image_id)
|
||||
# trusted.each { |t| puts t["api_key"] }
|
||||
def list_image_trusted(image_id, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
response = make_request('GET', "/images/#{image_id}/trusted", pk, sk)
|
||||
response['trusted'] || []
|
||||
end
|
||||
|
||||
# Transfer image ownership to another API key
|
||||
#
|
||||
# @param image_id [String] Image ID to transfer
|
||||
# @param to_api_key [String] Public API key to transfer ownership to
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with transfer confirmation
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# Un.transfer_image(image_id, "unsb-pk-xxxxx-xxxxx-xxxxx-xxxxx")
|
||||
def transfer_image(image_id, to_api_key, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('POST', "/images/#{image_id}/transfer", pk, sk, { to_api_key: to_api_key })
|
||||
end
|
||||
|
||||
# Spawn a new service from an image
|
||||
#
|
||||
# @param image_id [String] Image ID to spawn from
|
||||
# @param name [String, nil] Optional name for the service
|
||||
# @param ports [Array<Integer>, nil] Optional ports to expose
|
||||
# @param bootstrap [String, nil] Optional bootstrap script
|
||||
# @param network_mode [String] Network mode ("zerotrust" or "semitrusted", default: "zerotrust")
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with service_id and other metadata
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example Spawn with defaults
|
||||
# result = Un.spawn_from_image(image_id)
|
||||
# puts result["service_id"]
|
||||
#
|
||||
# @example Spawn with custom config
|
||||
# result = Un.spawn_from_image(image_id, name: "web", ports: [80, 443], network_mode: "semitrusted")
|
||||
# puts result["service_id"]
|
||||
def spawn_from_image(image_id, name: nil, ports: nil, bootstrap: nil, network_mode: 'zerotrust', public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
data = { network_mode: network_mode }
|
||||
data[:name] = name if name
|
||||
data[:ports] = ports if ports
|
||||
data[:bootstrap] = bootstrap if bootstrap
|
||||
make_request('POST', "/images/#{image_id}/spawn", pk, sk, data)
|
||||
end
|
||||
|
||||
# Clone an image to create a new image
|
||||
#
|
||||
# @param image_id [String] Image ID to clone
|
||||
# @param name [String, nil] Optional name for the cloned image
|
||||
# @param description [String, nil] Optional description for the cloned image
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with new image_id
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# result = Un.clone_image(image_id, name: "my-clone", description: "Cloned from production")
|
||||
# puts result["image_id"]
|
||||
def clone_image(image_id, name: nil, description: nil, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
data = {}
|
||||
data[:name] = name if name
|
||||
data[:description] = description if description
|
||||
make_request('POST', "/images/#{image_id}/clone", pk, sk, data)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Session Functions
|
||||
# ============================================================================
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ categories = ["api-bindings", "development-tools", "asynchronous"]
|
|||
name = "un_async"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "un-async"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# HTTP client (async)
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
16
clients/rust/async/src/main.rs
Normal file
16
clients/rust/async/src/main.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
//
|
||||
// unsandbox.com CLI (Rust Async)
|
||||
//
|
||||
// Binary entry point for the CLI.
|
||||
//
|
||||
// Usage:
|
||||
// un script.py # Execute Python script
|
||||
// un -s bash 'echo hello' # Execute inline code
|
||||
// un session --list # List sessions
|
||||
// un service --list # List services
|
||||
// un key # Check API key
|
||||
|
||||
fn main() {
|
||||
std::process::exit(un_async::cli_main());
|
||||
}
|
||||
|
|
@ -20,6 +20,10 @@ categories = ["api-bindings", "development-tools"]
|
|||
name = "un"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "un"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# HTTP client (blocking feature for sync)
|
||||
reqwest = { version = "0.12", features = ["blocking", "json"] }
|
||||
|
|
|
|||
|
|
@ -541,6 +541,16 @@ struct EnvExportResponse {
|
|||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ImagesListResponse {
|
||||
images: Vec<LxdImage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TrustedKeysResponse {
|
||||
trusted_keys: Vec<String>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Language Detection
|
||||
// =============================================================================
|
||||
|
|
@ -1195,6 +1205,333 @@ pub fn clone_snapshot(snapshot_id: &str, name: &str, creds: &Credentials) -> Res
|
|||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Images API Functions (LXD Container Images)
|
||||
// =============================================================================
|
||||
|
||||
/// Publish an LXD container image from a session or service.
|
||||
///
|
||||
/// Creates a reusable container image from an existing session or service.
|
||||
/// The image can later be used to spawn new services.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `source_type` - Source type: "session" or "service"
|
||||
/// * `source_id` - ID of the session or service to publish from
|
||||
/// * `name` - Name for the new image
|
||||
/// * `description` - Optional description for the image
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// LxdImage information for the published image
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let image = image_publish("service", "svc-abc123", "my-app-image", Some("Production app v1.0"), &creds)?;
|
||||
/// println!("Published image: {}", image.image_id);
|
||||
/// ```
|
||||
pub fn image_publish(
|
||||
source_type: &str,
|
||||
source_id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
creds: &Credentials,
|
||||
) -> Result<LxdImage> {
|
||||
let mut body = serde_json::json!({
|
||||
"source_type": source_type,
|
||||
"source_id": source_id,
|
||||
"name": name
|
||||
});
|
||||
if let Some(desc) = description {
|
||||
body["description"] = serde_json::json!(desc);
|
||||
}
|
||||
make_request("POST", "/images", creds, Some(&body))
|
||||
}
|
||||
|
||||
/// List LXD container images.
|
||||
///
|
||||
/// Returns images based on the filter type:
|
||||
/// - None or "owned": Images owned by the authenticated user
|
||||
/// - "shared": Images shared with the authenticated user
|
||||
/// - "public": Publicly available images
|
||||
/// - "all": All images accessible to the authenticated user
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `filter_type` - Optional filter: "owned", "shared", "public", or "all"
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of LxdImage information
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
///
|
||||
/// // List owned images (default)
|
||||
/// let my_images = list_images(None, &creds)?;
|
||||
///
|
||||
/// // List shared images
|
||||
/// let shared = list_images(Some("shared"), &creds)?;
|
||||
///
|
||||
/// // List all accessible images
|
||||
/// let all = list_images(Some("all"), &creds)?;
|
||||
/// ```
|
||||
pub fn list_images(filter_type: Option<&str>, creds: &Credentials) -> Result<Vec<LxdImage>> {
|
||||
let path = match filter_type {
|
||||
Some(ft) => format!("/images/{}", ft),
|
||||
None => "/images".to_string(),
|
||||
};
|
||||
let response: ImagesListResponse = make_request("GET", &path, creds, None::<&()>)?;
|
||||
Ok(response.images)
|
||||
}
|
||||
|
||||
/// Get details of a specific LXD container image.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to retrieve
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// LxdImage information
|
||||
pub fn get_image(image_id: &str, creds: &Credentials) -> Result<LxdImage> {
|
||||
let path = format!("/images/{}", image_id);
|
||||
make_request("GET", &path, creds, None::<&()>)
|
||||
}
|
||||
|
||||
/// Delete an LXD container image.
|
||||
///
|
||||
/// The image must be unlocked to be deleted.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to delete
|
||||
/// * `creds` - API credentials
|
||||
pub fn delete_image(image_id: &str, creds: &Credentials) -> Result<()> {
|
||||
let path = format!("/images/{}", image_id);
|
||||
let _: serde_json::Value = make_request("DELETE", &path, creds, None::<&()>)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lock an LXD container image to prevent modification or deletion.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to lock
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Updated LxdImage information
|
||||
pub fn lock_image(image_id: &str, creds: &Credentials) -> Result<LxdImage> {
|
||||
let path = format!("/images/{}/lock", image_id);
|
||||
let body = serde_json::json!({});
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Unlock an LXD container image to allow modification or deletion.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to unlock
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Updated LxdImage information
|
||||
pub fn unlock_image(image_id: &str, creds: &Credentials) -> Result<LxdImage> {
|
||||
let path = format!("/images/{}/unlock", image_id);
|
||||
let body = serde_json::json!({});
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Set the visibility of an LXD container image.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to update
|
||||
/// * `visibility` - New visibility: "private", "shared", or "public"
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Updated LxdImage information
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
///
|
||||
/// // Make image public
|
||||
/// set_image_visibility("img-abc123", "public", &creds)?;
|
||||
///
|
||||
/// // Make image private
|
||||
/// set_image_visibility("img-abc123", "private", &creds)?;
|
||||
/// ```
|
||||
pub fn set_image_visibility(
|
||||
image_id: &str,
|
||||
visibility: &str,
|
||||
creds: &Credentials,
|
||||
) -> Result<LxdImage> {
|
||||
let path = format!("/images/{}/visibility", image_id);
|
||||
let body = serde_json::json!({
|
||||
"visibility": visibility
|
||||
});
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Grant access to an LXD container image for another API key.
|
||||
///
|
||||
/// The image visibility must be "shared" for this to take effect.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to grant access to
|
||||
/// * `trusted_api_key` - API key (public key) to grant access
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Updated LxdImage information
|
||||
pub fn grant_image_access(
|
||||
image_id: &str,
|
||||
trusted_api_key: &str,
|
||||
creds: &Credentials,
|
||||
) -> Result<LxdImage> {
|
||||
let path = format!("/images/{}/grant", image_id);
|
||||
let body = serde_json::json!({
|
||||
"trusted_api_key": trusted_api_key
|
||||
});
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Revoke access to an LXD container image from another API key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to revoke access from
|
||||
/// * `trusted_api_key` - API key (public key) to revoke access
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Updated LxdImage information
|
||||
pub fn revoke_image_access(
|
||||
image_id: &str,
|
||||
trusted_api_key: &str,
|
||||
creds: &Credentials,
|
||||
) -> Result<LxdImage> {
|
||||
let path = format!("/images/{}/revoke", image_id);
|
||||
let body = serde_json::json!({
|
||||
"trusted_api_key": trusted_api_key
|
||||
});
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// List API keys that have access to an LXD container image.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to list trusted keys for
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of trusted API keys (public keys)
|
||||
pub fn list_image_trusted(image_id: &str, creds: &Credentials) -> Result<Vec<String>> {
|
||||
let path = format!("/images/{}/trusted", image_id);
|
||||
let response: TrustedKeysResponse = make_request("GET", &path, creds, None::<&()>)?;
|
||||
Ok(response.trusted_keys)
|
||||
}
|
||||
|
||||
/// Transfer ownership of an LXD container image to another API key.
|
||||
///
|
||||
/// After transfer, the original owner loses ownership but may retain
|
||||
/// access if they are in the trusted keys list.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to transfer
|
||||
/// * `to_api_key` - API key (public key) of the new owner
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Updated LxdImage information
|
||||
pub fn transfer_image(image_id: &str, to_api_key: &str, creds: &Credentials) -> Result<LxdImage> {
|
||||
let path = format!("/images/{}/transfer", image_id);
|
||||
let body = serde_json::json!({
|
||||
"to_api_key": to_api_key
|
||||
});
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Spawn a new service from an LXD container image.
|
||||
///
|
||||
/// Creates a new running service based on the specified image.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to spawn from
|
||||
/// * `name` - Name for the new service
|
||||
/// * `ports` - Optional list of ports to expose
|
||||
/// * `bootstrap` - Optional bootstrap script to run after spawn
|
||||
/// * `network_mode` - Optional network mode: "zerotrust" or "semitrusted"
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// SpawnFromImageResult with the new service information
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
///
|
||||
/// // Spawn with default options
|
||||
/// let result = spawn_from_image("img-abc123", "my-service", None, None, None, &creds)?;
|
||||
/// println!("Service URL: {}", result.url);
|
||||
///
|
||||
/// // Spawn with custom ports and bootstrap
|
||||
/// let result = spawn_from_image(
|
||||
/// "img-abc123",
|
||||
/// "web-server",
|
||||
/// Some(&[80, 443]),
|
||||
/// Some("systemctl start nginx"),
|
||||
/// Some("semitrusted"),
|
||||
/// &creds
|
||||
/// )?;
|
||||
/// ```
|
||||
pub fn spawn_from_image(
|
||||
image_id: &str,
|
||||
name: &str,
|
||||
ports: Option<&[u16]>,
|
||||
bootstrap: Option<&str>,
|
||||
network_mode: Option<&str>,
|
||||
creds: &Credentials,
|
||||
) -> Result<SpawnFromImageResult> {
|
||||
let path = format!("/images/{}/spawn", image_id);
|
||||
let mut body = serde_json::json!({
|
||||
"name": name
|
||||
});
|
||||
if let Some(p) = ports {
|
||||
body["ports"] = serde_json::json!(p);
|
||||
}
|
||||
if let Some(b) = bootstrap {
|
||||
body["bootstrap"] = serde_json::json!(b);
|
||||
}
|
||||
if let Some(nm) = network_mode {
|
||||
body["network_mode"] = serde_json::json!(nm);
|
||||
}
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Clone an LXD container image to create a new image with a different name.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_id` - Image ID to clone
|
||||
/// * `name` - Name for the new image
|
||||
/// * `description` - Optional description for the new image
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// New LxdImage information
|
||||
pub fn clone_image(
|
||||
image_id: &str,
|
||||
name: &str,
|
||||
description: Option<&str>,
|
||||
creds: &Credentials,
|
||||
) -> Result<LxdImage> {
|
||||
let path = format!("/images/{}/clone", image_id);
|
||||
let mut body = serde_json::json!({
|
||||
"name": name
|
||||
});
|
||||
if let Some(desc) = description {
|
||||
body["description"] = serde_json::json!(desc);
|
||||
}
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Session API Functions
|
||||
// =============================================================================
|
||||
|
|
|
|||
16
clients/rust/sync/src/main.rs
Normal file
16
clients/rust/sync/src/main.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
//
|
||||
// unsandbox.com CLI (Rust Sync)
|
||||
//
|
||||
// Binary entry point for the CLI.
|
||||
//
|
||||
// Usage:
|
||||
// un script.py # Execute Python script
|
||||
// un -s bash 'echo hello' # Execute inline code
|
||||
// un session --list # List sessions
|
||||
// un service --list # List services
|
||||
// un key # Check API key
|
||||
|
||||
fn main() {
|
||||
std::process::exit(un::cli_main());
|
||||
}
|
||||
1893
clients/swift/sync/src/un.swift
Normal file
1893
clients/swift/sync/src/un.swift
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue