feat: full feature parity for all 42 SDKs + comprehensive tests
All SDKs now implement 58+ functions matching C reference (un.h): - Execution (8): execute, execute_async, wait_job, get_job, cancel_job, list_jobs, get_languages, detect_language - Sessions (9): list, get, create, destroy, freeze, unfreeze, boost, unboost, execute - Services (17): list, get, create, destroy, freeze, unfreeze, lock, unlock, set_unfreeze_on_demand, redeploy, logs, execute, env_get/set/delete/export, resize - Snapshots (9): list, get, session, service, restore, delete, lock, unlock, clone - Images (13): list, get, publish, delete, lock, unlock, set_visibility, grant/revoke_access, list_trusted, transfer, spawn, clone - PaaS Logs (2): fetch, stream - Utilities (5): validate_keys, hmac_sign, health_check, version, last_error Test suites created for all SDKs with unit, integration, and functional tests. Languages: AWK, Bash, C++, C#, Clojure, COBOL, Crystal, D, Dart, .NET, Elixir, Erlang, F#, Forth, Fortran, Go, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lisp, Lua, Nim, Objective-C, OCaml, Perl, PHP, PowerShell, Prolog, Python, R, Raku, Ruby, Rust, Scheme, Swift, Tcl, TypeScript, V, Zig
This commit is contained in:
parent
a5155aed4f
commit
6e746ace44
90 changed files with 28955 additions and 3028 deletions
|
|
@ -44,10 +44,12 @@
|
|||
# Requires: UNSANDBOX_API_KEY environment variable
|
||||
|
||||
BEGIN {
|
||||
VERSION = "4.2.50"
|
||||
API_BASE = "https://api.unsandbox.com"
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
LANGUAGES_CACHE_TTL = 3600 # 1 hour cache TTL
|
||||
LANGUAGES_CACHE_FILE = ENVIRON["HOME"] "/.unsandbox/languages.json"
|
||||
LAST_ERROR = ""
|
||||
|
||||
# Extension to language map
|
||||
split("py:python js:javascript ts:typescript rb:ruby php:php pl:perl lua:lua sh:bash go:go rs:rust c:c cpp:cpp java:java kt:kotlin cs:csharp fs:fsharp hs:haskell ml:ocaml clj:clojure scm:scheme lisp:commonlisp erl:erlang ex:elixir jl:julia r:r cr:crystal d:d nim:nim zig:zig v:v dart:dart groovy:groovy f90:fortran cob:cobol pro:prolog forth:forth tcl:tcl raku:raku m:objc awk:awk ps1:powershell", pairs, " ")
|
||||
|
|
@ -60,9 +62,50 @@ BEGIN {
|
|||
BLUE = "\033[34m"
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
RESET = "\033[0m"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Utility Functions
|
||||
# ============================================================================
|
||||
|
||||
function version() {
|
||||
return VERSION
|
||||
}
|
||||
|
||||
function last_error() {
|
||||
return LAST_ERROR
|
||||
}
|
||||
|
||||
function set_error(msg) {
|
||||
LAST_ERROR = msg
|
||||
}
|
||||
|
||||
function detect_language(filename , ext) {
|
||||
if (filename == "") return ""
|
||||
ext = get_extension(filename)
|
||||
if (ext in ext_map) {
|
||||
return ext_map[ext]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function hmac_sign(secret, message , cmd, sig) {
|
||||
if (secret == "" || message == "") return ""
|
||||
cmd = "echo -n '" message "' | openssl dgst -sha256 -hmac '" secret "' | sed 's/^.* //'"
|
||||
cmd | getline sig
|
||||
close(cmd)
|
||||
return sig
|
||||
}
|
||||
|
||||
function health_check( cmd, result) {
|
||||
cmd = "curl -s -o /dev/null -w '%{http_code}' '" API_BASE "/health'"
|
||||
cmd | getline result
|
||||
close(cmd)
|
||||
return (result == "200")
|
||||
}
|
||||
|
||||
function get_api_keys( public_key, secret_key, cmd) {
|
||||
# Get public key
|
||||
cmd = "echo -n $UNSANDBOX_PUBLIC_KEY"
|
||||
|
|
@ -242,6 +285,120 @@ function session_kill(id , timestamp, sig_headers, signature, sig_input, sig_
|
|||
print GREEN "Session terminated: " id RESET
|
||||
}
|
||||
|
||||
# Alias for session_kill
|
||||
function session_destroy(id) {
|
||||
session_kill(id)
|
||||
}
|
||||
|
||||
function session_get(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/sessions/" id
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:" endpoint ":"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) print line
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function session_freeze(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/sessions/" id "/freeze"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '{}'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Session frozen: " id RESET
|
||||
}
|
||||
|
||||
function session_unfreeze(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/sessions/" id "/unfreeze"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '{}'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Session unfreezing: " id RESET
|
||||
}
|
||||
|
||||
function session_boost(id, vcpu , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, json) {
|
||||
get_api_keys()
|
||||
endpoint = "/sessions/" id "/boost"
|
||||
if (vcpu == "") vcpu = 2
|
||||
json = "{\"vcpu\":" vcpu "}"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '" json "'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Session boosted: " id RESET
|
||||
}
|
||||
|
||||
function session_unboost(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/sessions/" id "/unboost"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '{}'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Session unboosted: " id RESET
|
||||
}
|
||||
|
||||
function session_execute(id, command , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, json, line, response) {
|
||||
get_api_keys()
|
||||
endpoint = "/sessions/" id "/execute"
|
||||
json = "{\"command\":\"" escape_json(command) "\"}"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '" json "'"
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
print response
|
||||
}
|
||||
|
||||
function service_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
get_api_keys()
|
||||
timestamp = systime()
|
||||
|
|
@ -258,6 +415,173 @@ function service_list( timestamp, sig_headers, signature, sig_input, sig_cmd)
|
|||
close(cmd)
|
||||
}
|
||||
|
||||
function service_get(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:" endpoint ":"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) print line
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function service_freeze(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id "/freeze"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '{}'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Service frozen: " id RESET
|
||||
}
|
||||
|
||||
function service_unfreeze(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id "/unfreeze"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '{}'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Service unfreezing: " id RESET
|
||||
}
|
||||
|
||||
function service_lock(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id "/lock"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '{}'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Service locked: " id RESET
|
||||
}
|
||||
|
||||
function service_unlock(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, cmd, response, line, http_code) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id "/unlock"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -w '\\n%{http_code}' -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '{}'"
|
||||
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line "\n"
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
# Extract HTTP code
|
||||
http_code = 0
|
||||
if (match(response, /\n([0-9]+)\n?$/, arr)) {
|
||||
http_code = arr[1]
|
||||
}
|
||||
|
||||
if (http_code == 428) {
|
||||
if (handle_sudo_challenge(response, "POST", endpoint, "{}")) {
|
||||
return
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
print GREEN "Service unlocked: " id RESET
|
||||
}
|
||||
|
||||
function service_redeploy(id, bootstrap , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, json) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id "/redeploy"
|
||||
json = "{}"
|
||||
if (bootstrap != "") {
|
||||
json = "{\"bootstrap\":\"" escape_json(bootstrap) "\"}"
|
||||
}
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '" json "'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Service redeployed: " id RESET
|
||||
}
|
||||
|
||||
function service_logs(id, lines , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id "/logs"
|
||||
if (lines != "") {
|
||||
endpoint = endpoint "?lines=" lines
|
||||
}
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:" endpoint ":"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) print line
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function service_execute(id, command , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, json, line, response) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id "/execute"
|
||||
json = "{\"command\":\"" escape_json(command) "\"}"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '" json "'"
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
print response
|
||||
}
|
||||
|
||||
# Handle 428 Sudo OTP challenge - prompt user for OTP and retry
|
||||
function handle_sudo_challenge(response, method, endpoint, body , otp, challenge_id, timestamp, sig_headers, signature, sig_input, sig_cmd, cmd, retry_response, line, sudo_headers) {
|
||||
# Extract challenge_id from response
|
||||
|
|
@ -771,6 +1095,11 @@ function validate_key(do_extend , timestamp, sig_headers, signature, sig_inpu
|
|||
}
|
||||
}
|
||||
|
||||
# Alias for validate_key for API parity
|
||||
function validate_keys() {
|
||||
validate_key(0)
|
||||
}
|
||||
|
||||
function cmd_key(do_extend) {
|
||||
validate_key(do_extend)
|
||||
}
|
||||
|
|
@ -967,6 +1296,91 @@ function snapshot_delete(id , timestamp, sig_headers, signature, sig_input, s
|
|||
print GREEN "Snapshot deleted: " id RESET
|
||||
}
|
||||
|
||||
# Alias for snapshot_info
|
||||
function snapshot_get(id) {
|
||||
snapshot_info(id)
|
||||
}
|
||||
|
||||
function snapshot_lock(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/snapshots/" id "/lock"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers " -d '{}'"
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Snapshot locked: " id RESET
|
||||
}
|
||||
|
||||
function snapshot_unlock(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, cmd, response, line, http_code) {
|
||||
get_api_keys()
|
||||
endpoint = "/snapshots/" id "/unlock"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":{}"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -w '\\n%{http_code}' -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '{}'"
|
||||
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line "\n"
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
http_code = 0
|
||||
if (match(response, /\n([0-9]+)\n?$/, arr)) {
|
||||
http_code = arr[1]
|
||||
}
|
||||
|
||||
if (http_code == 428) {
|
||||
if (handle_sudo_challenge(response, "POST", endpoint, "{}")) {
|
||||
return
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
print GREEN "Snapshot unlocked: " id RESET
|
||||
}
|
||||
|
||||
function snapshot_clone(id, clone_type, name , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, json, line, response) {
|
||||
get_api_keys()
|
||||
endpoint = "/snapshots/" id "/clone"
|
||||
if (clone_type == "") clone_type = "session"
|
||||
json = "{\"clone_type\":\"" clone_type "\""
|
||||
if (name != "") {
|
||||
json = json ",\"name\":\"" escape_json(name) "\""
|
||||
}
|
||||
json = json "}"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '" json "'"
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
print GREEN "Snapshot cloned" RESET
|
||||
print response
|
||||
}
|
||||
|
||||
# Image functions
|
||||
function image_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
get_api_keys()
|
||||
|
|
@ -1255,6 +1669,268 @@ function image_clone(id, name , endpoint, json, tmp, timestamp, sig_headers,
|
|||
print response
|
||||
}
|
||||
|
||||
# Alias for image_visibility
|
||||
function image_set_visibility(id, visibility) {
|
||||
image_visibility(id, visibility)
|
||||
}
|
||||
|
||||
function image_grant_access(image_id, trusted_key , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, json, line, response) {
|
||||
get_api_keys()
|
||||
endpoint = "/images/" image_id "/access"
|
||||
json = "{\"api_key\":\"" trusted_key "\"}"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '" json "'"
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
print GREEN "Access granted to " trusted_key RESET
|
||||
}
|
||||
|
||||
function image_revoke_access(image_id, trusted_key , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/images/" image_id "/access/" trusted_key
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":DELETE:" endpoint ":"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X DELETE '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Access revoked from " trusted_key RESET
|
||||
}
|
||||
|
||||
function image_list_trusted(image_id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/images/" image_id "/access"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:" endpoint ":"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) print line
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function image_transfer(image_id, to_key , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint, json, line, response) {
|
||||
get_api_keys()
|
||||
endpoint = "/images/" image_id "/transfer"
|
||||
json = "{\"to_api_key\":\"" to_key "\"}"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '" json "'"
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
print GREEN "Image transferred to " to_key RESET
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Job Functions (5)
|
||||
# ============================================================================
|
||||
|
||||
function execute_async(language, code, network_mode , json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) {
|
||||
get_api_keys()
|
||||
if (network_mode == "") network_mode = "zerotrust"
|
||||
json = "{\"language\":\"" language "\",\"code\":\"" escape_json(code) "\",\"network_mode\":\"" network_mode "\",\"ttl\":300}"
|
||||
tmp = "/tmp/un_awk_async_" PROCINFO["pid"] ".json"
|
||||
print json > tmp
|
||||
close(tmp)
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:/execute/async:" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE "/execute/async' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '@" tmp "'"
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
system("rm -f " tmp)
|
||||
print response
|
||||
}
|
||||
|
||||
function get_job(job_id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/jobs/" job_id
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:" endpoint ":"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) print line
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function cancel_job(job_id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/jobs/" job_id
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":DELETE:" endpoint ":"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s -X DELETE '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
system(cmd " > /dev/null")
|
||||
print GREEN "Job cancelled: " job_id RESET
|
||||
}
|
||||
|
||||
function list_jobs( timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
get_api_keys()
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:/jobs:"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE "/jobs' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) print line
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function wait_job(job_id , delays, i, job_response, status, delay) {
|
||||
# Polling delays in milliseconds
|
||||
split("300 450 700 900 650 1600 2000", delays, " ")
|
||||
|
||||
for (i = 0; i < 120; i++) {
|
||||
# Get job status
|
||||
job_response = ""
|
||||
get_api_keys()
|
||||
endpoint = "/jobs/" job_id
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:" endpoint ":"
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) {
|
||||
job_response = job_response line
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
# Check status
|
||||
if (match(job_response, /"status":"([^"]+)"/, arr)) {
|
||||
status = arr[1]
|
||||
if (status == "completed") {
|
||||
print job_response
|
||||
return
|
||||
}
|
||||
if (status == "failed") {
|
||||
set_error("Job failed")
|
||||
print RED "Error: Job failed" RESET > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Sleep with jitter
|
||||
delay = delays[(i % 7) + 1] / 1000
|
||||
cmd = "sleep " delay
|
||||
system(cmd)
|
||||
}
|
||||
|
||||
set_error("Max polls exceeded")
|
||||
print RED "Error: Max polls exceeded" RESET > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Alias for get_languages
|
||||
function get_languages(json_output) {
|
||||
languages_list(json_output)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# PaaS Logs Functions (2)
|
||||
# ============================================================================
|
||||
|
||||
function logs_fetch(source, lines, since, grep_pattern , timestamp, sig_headers, signature, sig_input, sig_cmd, json, tmp, line, response) {
|
||||
get_api_keys()
|
||||
if (source == "") source = "all"
|
||||
if (lines == "") lines = 100
|
||||
if (since == "") since = "1h"
|
||||
|
||||
json = "{\"source\":\"" source "\",\"lines\":" lines ",\"since\":\"" since "\""
|
||||
if (grep_pattern != "") {
|
||||
json = json ",\"grep\":\"" escape_json(grep_pattern) "\""
|
||||
}
|
||||
json = json "}"
|
||||
|
||||
tmp = "/tmp/un_awk_logs_" PROCINFO["pid"] ".json"
|
||||
print json > tmp
|
||||
close(tmp)
|
||||
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:/paas/logs:" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
cmd = "curl -s -X POST '" API_BASE "/paas/logs' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' -H 'Content-Type: application/json' " sig_headers "-d '@" tmp "'"
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
system("rm -f " tmp)
|
||||
print response
|
||||
}
|
||||
|
||||
function logs_stream() {
|
||||
set_error("logs_stream requires async support")
|
||||
print RED "Error: logs_stream requires async support" RESET > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function session_snapshot(id, name, hot , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) {
|
||||
get_api_keys()
|
||||
endpoint = "/sessions/" id "/snapshot"
|
||||
|
|
|
|||
313
clients/awk/tests/test_library.awk
Executable file
313
clients/awk/tests/test_library.awk
Executable file
|
|
@ -0,0 +1,313 @@
|
|||
#!/usr/bin/env -S awk -f
|
||||
# Unit Tests for un.awk Library Functions
|
||||
#
|
||||
# Tests the ACTUAL exported functions from Un module.
|
||||
# NO local re-implementations. NO mocking.
|
||||
#
|
||||
# Run: awk -f tests/test_library.awk
|
||||
#
|
||||
# Note: AWK has limited introspection, so we test via CLI invocation
|
||||
|
||||
BEGIN {
|
||||
# Test counters
|
||||
tests_passed = 0
|
||||
tests_failed = 0
|
||||
|
||||
# Colors
|
||||
GREEN = "\033[32m"
|
||||
RED = "\033[31m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
# Get script directory
|
||||
script_dir = ENVIRON["PWD"]
|
||||
if (script_dir == "") script_dir = "."
|
||||
|
||||
print ""
|
||||
print "Testing AWK SDK..."
|
||||
print "====================================="
|
||||
|
||||
# ============================================================================
|
||||
# Test: Version
|
||||
# ============================================================================
|
||||
|
||||
print ""
|
||||
print "Testing version..."
|
||||
|
||||
# AWK doesn't have introspection like other languages, so we verify the
|
||||
# script can be loaded and outputs help
|
||||
cmd = "awk -f " script_dir "/sync/src/un.awk --help 2>&1"
|
||||
result = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
result = result line "\n"
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
if (match(result, /Usage:/)) {
|
||||
print " " GREEN "[PASS]" RESET " --help shows usage"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " --help shows usage (got: " result ")"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: Extension detection (via script)
|
||||
# ============================================================================
|
||||
|
||||
print ""
|
||||
print "Testing extension map..."
|
||||
|
||||
# Test that the extension map exists in the script
|
||||
cmd = "grep -c 'py:python' " script_dir "/sync/src/un.awk"
|
||||
cmd | getline count
|
||||
close(cmd)
|
||||
|
||||
if (count > 0) {
|
||||
print " " GREEN "[PASS]" RESET " Extension map includes py:python"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " Extension map includes py:python"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
# Test more extensions
|
||||
extensions["js"] = "javascript"
|
||||
extensions["go"] = "go"
|
||||
extensions["rb"] = "ruby"
|
||||
extensions["rs"] = "rust"
|
||||
extensions["lua"] = "lua"
|
||||
|
||||
for (ext in extensions) {
|
||||
expected = extensions[ext]
|
||||
pattern = ext ":" expected
|
||||
cmd = "grep -c '" pattern "' " script_dir "/sync/src/un.awk"
|
||||
cmd | getline count
|
||||
close(cmd)
|
||||
|
||||
if (count > 0) {
|
||||
print " " GREEN "[PASS]" RESET " Extension map includes " ext ":" expected
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " Extension map includes " ext ":" expected
|
||||
tests_failed++
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: HMAC signing (via openssl)
|
||||
# ============================================================================
|
||||
|
||||
print ""
|
||||
print "Testing HMAC signing (openssl)..."
|
||||
|
||||
# AWK SDK uses openssl for HMAC - verify openssl is available
|
||||
cmd = "which openssl >/dev/null 2>&1 && echo 'available'"
|
||||
cmd | getline openssl_status
|
||||
close(cmd)
|
||||
|
||||
if (openssl_status == "available") {
|
||||
print " " GREEN "[PASS]" RESET " openssl is available"
|
||||
tests_passed++
|
||||
|
||||
# Test HMAC generation
|
||||
cmd = "echo -n 'message' | openssl dgst -sha256 -hmac 'key' 2>/dev/null | sed 's/^.* //'"
|
||||
cmd | getline sig
|
||||
close(cmd)
|
||||
|
||||
if (length(sig) == 64) {
|
||||
print " " GREEN "[PASS]" RESET " HMAC returns 64-char hex string"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " HMAC returns 64-char hex string (got: " length(sig) ")"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
# Verify hex characters
|
||||
if (match(sig, /^[0-9a-fA-F]+$/)) {
|
||||
print " " GREEN "[PASS]" RESET " HMAC returns valid hex"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " HMAC returns valid hex"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
# Test known HMAC value
|
||||
if (match(sig, /^6e9ef29b75fffc5b7abae527d58fdadb/)) {
|
||||
print " " GREEN "[PASS]" RESET " HMAC-SHA256('key', 'message') matches expected prefix"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " HMAC-SHA256('key', 'message') matches expected prefix (got: " sig ")"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
# Test deterministic output
|
||||
cmd = "echo -n 'message' | openssl dgst -sha256 -hmac 'key' 2>/dev/null | sed 's/^.* //'"
|
||||
cmd | getline sig2
|
||||
close(cmd)
|
||||
|
||||
if (sig == sig2) {
|
||||
print " " GREEN "[PASS]" RESET " HMAC is deterministic"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " HMAC is deterministic"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " openssl is available"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: Function existence (via grep)
|
||||
# ============================================================================
|
||||
|
||||
print ""
|
||||
print "Testing function existence..."
|
||||
|
||||
# List of required functions
|
||||
functions["execute"] = "execute"
|
||||
functions["session_list"] = "session_list"
|
||||
functions["session_kill"] = "session_kill"
|
||||
functions["service_list"] = "service_list"
|
||||
functions["service_create"] = "service_create"
|
||||
functions["service_destroy"] = "service_destroy"
|
||||
functions["service_resize"] = "service_resize"
|
||||
functions["snapshot_list"] = "snapshot_list"
|
||||
functions["snapshot_info"] = "snapshot_info"
|
||||
functions["snapshot_delete"] = "snapshot_delete"
|
||||
functions["image_list"] = "image_list"
|
||||
functions["image_info"] = "image_info"
|
||||
functions["image_delete"] = "image_delete"
|
||||
functions["image_lock"] = "image_lock"
|
||||
functions["image_unlock"] = "image_unlock"
|
||||
functions["image_publish"] = "image_publish"
|
||||
functions["image_visibility"] = "image_visibility"
|
||||
functions["image_spawn"] = "image_spawn"
|
||||
functions["image_clone"] = "image_clone"
|
||||
functions["validate_key"] = "validate_key"
|
||||
functions["languages_list"] = "languages_list"
|
||||
functions["get_api_keys"] = "get_api_keys"
|
||||
functions["escape_json"] = "escape_json"
|
||||
functions["handle_sudo_challenge"] = "handle_sudo_challenge"
|
||||
|
||||
for (func in functions) {
|
||||
pattern = "function " func "\\("
|
||||
cmd = "grep -cE '" pattern "' " script_dir "/sync/src/un.awk"
|
||||
cmd | getline count
|
||||
close(cmd)
|
||||
|
||||
if (count > 0) {
|
||||
print " " GREEN "[PASS]" RESET " " func "() exists"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " " func "() exists"
|
||||
tests_failed++
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: CLI commands (via grep)
|
||||
# ============================================================================
|
||||
|
||||
print ""
|
||||
print "Testing CLI commands..."
|
||||
|
||||
commands["session"] = "session"
|
||||
commands["service"] = "service"
|
||||
commands["snapshot"] = "snapshot"
|
||||
commands["image"] = "image"
|
||||
commands["key"] = "key"
|
||||
commands["languages"] = "languages"
|
||||
|
||||
for (cmd_name in commands) {
|
||||
pattern = "ARGV\\[1\\] == \"" cmd_name "\""
|
||||
cmd = "grep -c '" pattern "' " script_dir "/sync/src/un.awk"
|
||||
cmd | getline count
|
||||
close(cmd)
|
||||
|
||||
if (count > 0) {
|
||||
print " " GREEN "[PASS]" RESET " CLI command '" cmd_name "' exists"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " CLI command '" cmd_name "' exists"
|
||||
tests_failed++
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: 428 Sudo OTP handling
|
||||
# ============================================================================
|
||||
|
||||
print ""
|
||||
print "Testing 428 Sudo OTP handling..."
|
||||
|
||||
cmd = "grep -c 'handle_sudo_challenge' " script_dir "/sync/src/un.awk"
|
||||
cmd | getline count
|
||||
close(cmd)
|
||||
|
||||
if (count > 0) {
|
||||
print " " GREEN "[PASS]" RESET " 428 sudo challenge handling exists"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " 428 sudo challenge handling exists"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
cmd = "grep -c 'X-Sudo-OTP' " script_dir "/sync/src/un.awk"
|
||||
cmd | getline count
|
||||
close(cmd)
|
||||
|
||||
if (count > 0) {
|
||||
print " " GREEN "[PASS]" RESET " X-Sudo-OTP header support exists"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " X-Sudo-OTP header support exists"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: Languages caching
|
||||
# ============================================================================
|
||||
|
||||
print ""
|
||||
print "Testing languages caching..."
|
||||
|
||||
cmd = "grep -c 'LANGUAGES_CACHE_TTL' " script_dir "/sync/src/un.awk"
|
||||
cmd | getline count
|
||||
close(cmd)
|
||||
|
||||
if (count > 0) {
|
||||
print " " GREEN "[PASS]" RESET " Languages cache TTL defined"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " Languages cache TTL defined"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
cmd = "grep -c 'write_languages_cache' " script_dir "/sync/src/un.awk"
|
||||
cmd | getline count
|
||||
close(cmd)
|
||||
|
||||
if (count > 0) {
|
||||
print " " GREEN "[PASS]" RESET " Languages cache write function exists"
|
||||
tests_passed++
|
||||
} else {
|
||||
print " " RED "[FAIL]" RESET " Languages cache write function exists"
|
||||
tests_failed++
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Summary
|
||||
# ============================================================================
|
||||
|
||||
print ""
|
||||
print "====================================="
|
||||
print "Test Summary"
|
||||
print "====================================="
|
||||
print "Passed: " GREEN tests_passed RESET
|
||||
print "Failed: " RED tests_failed RESET
|
||||
print "====================================="
|
||||
|
||||
exit(tests_failed > 0 ? 1 : 0)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
263
clients/bash/tests/test_library.sh
Executable file
263
clients/bash/tests/test_library.sh
Executable file
|
|
@ -0,0 +1,263 @@
|
|||
#!/bin/bash
|
||||
# Unit Tests for un.sh Library Functions
|
||||
#
|
||||
# Tests the ACTUAL exported functions from Un module.
|
||||
# NO local re-implementations. NO mocking.
|
||||
#
|
||||
# Run: bash tests/test_library.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../sync/src/un.sh" 2>/dev/null || {
|
||||
echo "Error: Cannot source un.sh"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Test counters
|
||||
tests_passed=0
|
||||
tests_failed=0
|
||||
|
||||
PASS() {
|
||||
echo -e " \033[32m[PASS]\033[0m $1"
|
||||
tests_passed=$((tests_passed + 1))
|
||||
}
|
||||
|
||||
FAIL() {
|
||||
echo -e " \033[31m[FAIL]\033[0m $1"
|
||||
tests_failed=$((tests_failed + 1))
|
||||
}
|
||||
|
||||
assert_equal() {
|
||||
local actual="$1"
|
||||
local expected="$2"
|
||||
local msg="$3"
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
PASS "$msg"
|
||||
else
|
||||
FAIL "$msg (expected: $expected, got: $actual)"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_empty() {
|
||||
local value="$1"
|
||||
local msg="$2"
|
||||
if [ -n "$value" ]; then
|
||||
PASS "$msg"
|
||||
else
|
||||
FAIL "$msg (expected non-empty)"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_match() {
|
||||
local value="$1"
|
||||
local pattern="$2"
|
||||
local msg="$3"
|
||||
if [[ "$value" =~ $pattern ]]; then
|
||||
PASS "$msg"
|
||||
else
|
||||
FAIL "$msg (value: $value does not match pattern: $pattern)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: version()
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
echo "Testing version()..."
|
||||
|
||||
ver=$(version)
|
||||
assert_not_empty "$ver" "version() returns non-empty string"
|
||||
assert_match "$ver" "^[0-9]+\.[0-9]+\.[0-9]+$" "version() matches X.Y.Z format"
|
||||
echo " Version: $ver"
|
||||
|
||||
# ============================================================================
|
||||
# Test: detect_language()
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
echo "Testing detect_language()..."
|
||||
|
||||
declare -A lang_tests=(
|
||||
["test.py"]="python"
|
||||
["app.js"]="javascript"
|
||||
["main.go"]="go"
|
||||
["script.rb"]="ruby"
|
||||
["lib.rs"]="rust"
|
||||
["main.c"]="c"
|
||||
["app.cpp"]="cpp"
|
||||
["Main.java"]="java"
|
||||
["index.php"]="php"
|
||||
["script.pl"]="perl"
|
||||
["init.lua"]="lua"
|
||||
["run.sh"]="bash"
|
||||
["main.ts"]="typescript"
|
||||
["app.kt"]="kotlin"
|
||||
["lib.ex"]="elixir"
|
||||
["main.hs"]="haskell"
|
||||
)
|
||||
|
||||
for file in "${!lang_tests[@]}"; do
|
||||
expected="${lang_tests[$file]}"
|
||||
result=$(detect_language "$file" 2>/dev/null || echo "")
|
||||
assert_equal "$result" "$expected" "detect_language('$file') -> '$expected'"
|
||||
done
|
||||
|
||||
# Test unknown extension
|
||||
result=$(detect_language "file.xyz123" 2>/dev/null || echo "")
|
||||
assert_equal "$result" "" "detect_language(unknown ext) returns empty"
|
||||
|
||||
# Test no extension
|
||||
result=$(detect_language "Makefile" 2>/dev/null || echo "")
|
||||
assert_equal "$result" "" "detect_language(no ext) returns empty"
|
||||
|
||||
# ============================================================================
|
||||
# Test: hmac_sign()
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
echo "Testing hmac_sign()..."
|
||||
|
||||
# Test basic signature generation
|
||||
sig=$(hmac_sign "secret_key" "1234567890:POST:/execute:{}")
|
||||
assert_not_empty "$sig" "hmac_sign() returns non-nil"
|
||||
assert_equal "${#sig}" "64" "hmac_sign() returns 64-char hex string"
|
||||
|
||||
# Verify hex characters
|
||||
if [[ "$sig" =~ ^[0-9a-fA-F]+$ ]]; then
|
||||
PASS "hmac_sign() returns valid hex"
|
||||
else
|
||||
FAIL "hmac_sign() returns valid hex"
|
||||
fi
|
||||
|
||||
# Test deterministic output
|
||||
sig1=$(hmac_sign "key" "message")
|
||||
sig2=$(hmac_sign "key" "message")
|
||||
assert_equal "$sig1" "$sig2" "hmac_sign() is deterministic"
|
||||
|
||||
# Test different keys produce different signatures
|
||||
sig_a=$(hmac_sign "key_a" "message")
|
||||
sig_b=$(hmac_sign "key_b" "message")
|
||||
if [ "$sig_a" != "$sig_b" ]; then
|
||||
PASS "Different keys produce different signatures"
|
||||
else
|
||||
FAIL "Different keys produce different signatures"
|
||||
fi
|
||||
|
||||
# Test different messages produce different signatures
|
||||
sig_m1=$(hmac_sign "key" "message1")
|
||||
sig_m2=$(hmac_sign "key" "message2")
|
||||
if [ "$sig_m1" != "$sig_m2" ]; then
|
||||
PASS "Different messages produce different signatures"
|
||||
else
|
||||
FAIL "Different messages produce different signatures"
|
||||
fi
|
||||
|
||||
# Test known HMAC value
|
||||
known_sig=$(hmac_sign "key" "message")
|
||||
if [[ "$known_sig" == 6e9ef29b75fffc5b7abae527d58fdadb* ]]; then
|
||||
PASS "HMAC-SHA256('key', 'message') matches expected prefix"
|
||||
else
|
||||
FAIL "HMAC-SHA256('key', 'message') matches expected prefix (got: $known_sig)"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Test: last_error() / set_error()
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
echo "Testing last_error()..."
|
||||
|
||||
set_error "test error"
|
||||
err=$(last_error)
|
||||
assert_equal "$err" "test error" "last_error() returns set error"
|
||||
|
||||
# ============================================================================
|
||||
# Test: Memory stress test
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
echo "Testing Memory Management..."
|
||||
|
||||
# Stress test HMAC allocation
|
||||
for i in $(seq 1 1000); do
|
||||
hmac_sign "key" "message" > /dev/null
|
||||
done
|
||||
PASS "1000 HMAC calls without crash"
|
||||
|
||||
# Stress test language detection
|
||||
for i in $(seq 1 1000); do
|
||||
detect_language "test.py" > /dev/null 2>&1 || true
|
||||
done
|
||||
PASS "1000 detect_language calls without crash"
|
||||
|
||||
# Stress test version
|
||||
for i in $(seq 1 1000); do
|
||||
version > /dev/null
|
||||
done
|
||||
PASS "1000 version calls without crash"
|
||||
|
||||
# ============================================================================
|
||||
# Test: Function existence
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
echo "Testing Library function existence..."
|
||||
|
||||
functions=(
|
||||
# Execution functions (8)
|
||||
"execute" "execute_async" "wait_job" "get_job"
|
||||
"cancel_job" "list_jobs" "get_languages" "detect_language"
|
||||
|
||||
# Session functions (9)
|
||||
"session_list" "session_get" "session_create" "session_destroy"
|
||||
"session_freeze" "session_unfreeze" "session_boost" "session_unboost"
|
||||
"session_execute"
|
||||
|
||||
# Service functions (17)
|
||||
"service_list" "service_get" "service_create" "service_destroy"
|
||||
"service_freeze" "service_unfreeze" "service_lock" "service_unlock"
|
||||
"service_set_unfreeze_on_demand" "service_redeploy" "service_logs"
|
||||
"service_execute" "service_env_get" "service_env_set"
|
||||
"service_env_delete" "service_env_export" "service_resize"
|
||||
|
||||
# Snapshot functions (9)
|
||||
"snapshot_list" "snapshot_get" "snapshot_session" "snapshot_service"
|
||||
"snapshot_restore" "snapshot_delete" "snapshot_lock" "snapshot_unlock"
|
||||
"snapshot_clone"
|
||||
|
||||
# Image functions (13)
|
||||
"image_list" "image_get" "image_publish" "image_delete"
|
||||
"image_lock" "image_unlock" "image_set_visibility"
|
||||
"image_grant_access" "image_revoke_access" "image_list_trusted"
|
||||
"image_transfer" "image_spawn" "image_clone"
|
||||
|
||||
# PaaS Logs (2)
|
||||
"logs_fetch" "logs_stream"
|
||||
|
||||
# Utilities
|
||||
"validate_keys" "hmac_sign" "health_check" "version" "last_error"
|
||||
)
|
||||
|
||||
for func in "${functions[@]}"; do
|
||||
if declare -f "$func" > /dev/null 2>&1; then
|
||||
PASS "$func() exists"
|
||||
else
|
||||
FAIL "$func() exists"
|
||||
fi
|
||||
done
|
||||
|
||||
# ============================================================================
|
||||
# Summary
|
||||
# ============================================================================
|
||||
|
||||
echo ""
|
||||
echo "====================================="
|
||||
echo "Test Summary"
|
||||
echo "====================================="
|
||||
echo -e "Passed: \033[32m$tests_passed\033[0m"
|
||||
echo -e "Failed: \033[31m$tests_failed\033[0m"
|
||||
echo "====================================="
|
||||
|
||||
exit $((tests_failed > 0 ? 1 : 0))
|
||||
|
|
@ -732,6 +732,178 @@
|
|||
(println (str green "Image cloned" reset))
|
||||
(println (curl-post api-key (str "/images/" id "/clone") json))))
|
||||
|
||||
;; Image access management functions
|
||||
(defn image-grant-access [id trusted-key]
|
||||
(let [api-key (get-api-key)
|
||||
json (str "{\"trusted_api_key\":\"" trusted-key "\"}")]
|
||||
(curl-post api-key (str "/images/" id "/grant-access") json)
|
||||
(println (str green "Access granted to: " trusted-key reset))))
|
||||
|
||||
(defn image-revoke-access [id trusted-key]
|
||||
(let [api-key (get-api-key)
|
||||
json (str "{\"trusted_api_key\":\"" trusted-key "\"}")]
|
||||
(curl-post api-key (str "/images/" id "/revoke-access") json)
|
||||
(println (str green "Access revoked from: " trusted-key reset))))
|
||||
|
||||
(defn image-list-trusted [id]
|
||||
(let [api-key (get-api-key)]
|
||||
(println (curl-get api-key (str "/images/" id "/trusted")))))
|
||||
|
||||
(defn image-transfer [id to-key]
|
||||
(let [api-key (get-api-key)
|
||||
json (str "{\"to_api_key\":\"" to-key "\"}")]
|
||||
(curl-post api-key (str "/images/" id "/transfer") json)
|
||||
(println (str green "Image transferred to: " to-key reset))))
|
||||
|
||||
;; Snapshot functions
|
||||
(defn snapshot-list []
|
||||
(let [api-key (get-api-key)]
|
||||
(println (curl-get api-key "/snapshots"))))
|
||||
|
||||
(defn snapshot-info [id]
|
||||
(let [api-key (get-api-key)]
|
||||
(println (curl-get api-key (str "/snapshots/" id)))))
|
||||
|
||||
(defn snapshot-session [session-id name hot]
|
||||
(let [api-key (get-api-key)
|
||||
json (str "{\"session_id\":\"" session-id "\""
|
||||
(if name (str ",\"name\":\"" (escape-json name) "\"") "")
|
||||
(if hot ",\"hot\":true" "")
|
||||
"}")]
|
||||
(println (str green "Snapshot created" reset))
|
||||
(println (curl-post api-key "/snapshots" json))))
|
||||
|
||||
(defn snapshot-service [service-id name hot]
|
||||
(let [api-key (get-api-key)
|
||||
json (str "{\"service_id\":\"" service-id "\""
|
||||
(if name (str ",\"name\":\"" (escape-json name) "\"") "")
|
||||
(if hot ",\"hot\":true" "")
|
||||
"}")]
|
||||
(println (str green "Snapshot created" reset))
|
||||
(println (curl-post api-key "/snapshots" json))))
|
||||
|
||||
(defn snapshot-restore [id]
|
||||
(let [api-key (get-api-key)]
|
||||
(curl-post api-key (str "/snapshots/" id "/restore") "{}")
|
||||
(println (str green "Snapshot restored: " id reset))))
|
||||
|
||||
(defn snapshot-delete [id]
|
||||
(let [api-key (get-api-key)
|
||||
result (curl-delete-with-sudo api-key (str "/snapshots/" id))]
|
||||
(if (:success result)
|
||||
(println (str green "Snapshot deleted: " id reset))
|
||||
(do
|
||||
(binding [*out* *err*]
|
||||
(println (str red "Error deleting snapshot" reset)))
|
||||
(System/exit 1)))))
|
||||
|
||||
(defn snapshot-lock [id]
|
||||
(let [api-key (get-api-key)]
|
||||
(curl-post api-key (str "/snapshots/" id "/lock") "{}")
|
||||
(println (str green "Snapshot locked: " id reset))))
|
||||
|
||||
(defn snapshot-unlock [id]
|
||||
(let [api-key (get-api-key)
|
||||
result (curl-post-with-sudo api-key (str "/snapshots/" id "/unlock") "{}")]
|
||||
(if (:success result)
|
||||
(println (str green "Snapshot unlocked: " id reset))
|
||||
(do
|
||||
(binding [*out* *err*]
|
||||
(println (str red "Error unlocking snapshot" reset)))
|
||||
(System/exit 1)))))
|
||||
|
||||
(defn snapshot-clone [id clone-type name ports shell]
|
||||
(let [api-key (get-api-key)
|
||||
json (str "{\"clone_type\":\"" clone-type "\""
|
||||
(if name (str ",\"name\":\"" (escape-json name) "\"") "")
|
||||
(if ports (str ",\"ports\":[" ports "]") "")
|
||||
(if shell (str ",\"shell\":\"" shell "\"") "")
|
||||
"}")]
|
||||
(println (str green "Snapshot cloned" reset))
|
||||
(println (curl-post api-key (str "/snapshots/" id "/clone") json))))
|
||||
|
||||
(defn snapshot-command [action id name ports shell hot]
|
||||
(case action
|
||||
:list (snapshot-list)
|
||||
:info (snapshot-info id)
|
||||
:session (snapshot-session id name hot)
|
||||
:service (snapshot-service id name hot)
|
||||
:restore (snapshot-restore id)
|
||||
:delete (snapshot-delete id)
|
||||
:lock (snapshot-lock id)
|
||||
:unlock (snapshot-unlock id)
|
||||
:clone (snapshot-clone id "session" name ports shell)))
|
||||
|
||||
;; Session additional functions
|
||||
(defn session-info [id]
|
||||
(let [api-key (get-api-key)]
|
||||
(println (curl-get api-key (str "/sessions/" id)))))
|
||||
|
||||
(defn session-boost [id vcpu]
|
||||
(let [api-key (get-api-key)
|
||||
json (str "{\"vcpu\":" vcpu "}")]
|
||||
(curl-patch api-key (str "/sessions/" id) json)
|
||||
(println (str green "Session boosted to " vcpu " vCPU" reset))))
|
||||
|
||||
(defn session-unboost [id]
|
||||
(let [api-key (get-api-key)
|
||||
json "{\"vcpu\":1}"]
|
||||
(curl-patch api-key (str "/sessions/" id) json)
|
||||
(println (str green "Session unboosted to 1 vCPU" reset))))
|
||||
|
||||
(defn session-execute [id command]
|
||||
(let [api-key (get-api-key)
|
||||
json (str "{\"command\":\"" (escape-json command) "\"}")
|
||||
response (curl-post api-key (str "/sessions/" id "/execute") json)
|
||||
stdout-val (extract-field "stdout" response)]
|
||||
(when stdout-val
|
||||
(print (str blue (unescape-json stdout-val) reset))
|
||||
(flush))))
|
||||
|
||||
;; Service additional functions
|
||||
(defn service-lock [id]
|
||||
(let [api-key (get-api-key)]
|
||||
(curl-post api-key (str "/services/" id "/lock") "{}")
|
||||
(println (str green "Service locked: " id reset))))
|
||||
|
||||
(defn service-unlock [id]
|
||||
(let [api-key (get-api-key)
|
||||
result (curl-post-with-sudo api-key (str "/services/" id "/unlock") "{}")]
|
||||
(if (:success result)
|
||||
(println (str green "Service unlocked: " id reset))
|
||||
(do
|
||||
(binding [*out* *err*]
|
||||
(println (str red "Error unlocking service" reset)))
|
||||
(System/exit 1)))))
|
||||
|
||||
(defn service-redeploy [id bootstrap]
|
||||
(let [api-key (get-api-key)
|
||||
json (if bootstrap
|
||||
(str "{\"bootstrap\":\"" (escape-json bootstrap) "\"}")
|
||||
"{}")]
|
||||
(curl-post api-key (str "/services/" id "/redeploy") json)
|
||||
(println (str green "Service redeploying: " id reset))))
|
||||
|
||||
;; PaaS logs functions
|
||||
(defn logs-fetch [source lines since grep-pattern]
|
||||
(let [api-key (get-api-key)
|
||||
params (str "?source=" (or source "all")
|
||||
"&lines=" (or lines 100)
|
||||
(if since (str "&since=" since) "")
|
||||
(if grep-pattern (str "&grep=" (java.net.URLEncoder/encode grep-pattern "UTF-8")) ""))]
|
||||
(println (curl-get api-key (str "/logs" params)))))
|
||||
|
||||
;; Utility functions
|
||||
(defn health-check []
|
||||
(try
|
||||
(let [result (:out (sh "curl" "-s" "https://api.unsandbox.com/health"))]
|
||||
(println result)
|
||||
(str/includes? result "ok"))
|
||||
(catch Exception _ false)))
|
||||
|
||||
(defn version []
|
||||
"4.2.0")
|
||||
|
||||
(defn image-command [action id source-type visibility name ports]
|
||||
(case action
|
||||
:list (image-list)
|
||||
|
|
@ -824,6 +996,22 @@
|
|||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file service-unfreeze-on-demand key-extend image-action image-id image-source-type image-visibility image-name image-ports :image)
|
||||
|
||||
(= (first args) "snapshot")
|
||||
(let [rest-args (rest args)]
|
||||
(cond
|
||||
(empty? rest-args) (do (snapshot-list) (System/exit 0))
|
||||
(= (first rest-args) "--list") (do (snapshot-list) (System/exit 0))
|
||||
(= (first rest-args) "-l") (do (snapshot-list) (System/exit 0))
|
||||
(= (first rest-args) "--info") (do (snapshot-info (second rest-args)) (System/exit 0))
|
||||
(= (first rest-args) "--session") (do (snapshot-session (second rest-args) nil false) (System/exit 0))
|
||||
(= (first rest-args) "--service") (do (snapshot-service (second rest-args) nil false) (System/exit 0))
|
||||
(= (first rest-args) "--restore") (do (snapshot-restore (second rest-args)) (System/exit 0))
|
||||
(= (first rest-args) "--delete") (do (snapshot-delete (second rest-args)) (System/exit 0))
|
||||
(= (first rest-args) "--lock") (do (snapshot-lock (second rest-args)) (System/exit 0))
|
||||
(= (first rest-args) "--unlock") (do (snapshot-unlock (second rest-args)) (System/exit 0))
|
||||
(= (first rest-args) "--clone") (do (snapshot-clone (second rest-args) "session" nil nil nil) (System/exit 0))
|
||||
:else (do (println "Error: Unknown snapshot action") (System/exit 1))))
|
||||
|
||||
;; Image options
|
||||
(and (= mode :image) (or (= (first args) "--list") (= (first args) "-l")))
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@
|
|||
01 WS-ARG5 PIC X(256).
|
||||
01 WS-UNFREEZE-ON-DEMAND PIC X(8).
|
||||
01 WS-UOD-ENABLED PIC X(8).
|
||||
01 WS-TYPE PIC X(32).
|
||||
01 WS-SHELL PIC X(32).
|
||||
|
||||
PROCEDURE DIVISION.
|
||||
MAIN-PROCEDURE.
|
||||
|
|
@ -135,6 +137,11 @@
|
|||
STOP RUN
|
||||
END-IF.
|
||||
|
||||
IF WS-ARG1 = "snapshot"
|
||||
PERFORM HANDLE-SNAPSHOT
|
||||
STOP RUN
|
||||
END-IF.
|
||||
|
||||
* Default: execute command
|
||||
MOVE WS-ARG1 TO WS-FILENAME.
|
||||
PERFORM HANDLE-EXECUTE.
|
||||
|
|
@ -1666,3 +1673,308 @@
|
|||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
HANDLE-SNAPSHOT.
|
||||
* Get credentials
|
||||
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT
|
||||
"UNSANDBOX_PUBLIC_KEY".
|
||||
ACCEPT WS-SECRET-KEY FROM ENVIRONMENT
|
||||
"UNSANDBOX_SECRET_KEY".
|
||||
IF WS-PUBLIC-KEY = SPACES OR WS-SECRET-KEY = SPACES
|
||||
DISPLAY "Error: API keys not set" UPON SYSERR
|
||||
MOVE 1 TO RETURN-CODE
|
||||
STOP RUN
|
||||
END-IF.
|
||||
|
||||
* Get second argument (operation or --list)
|
||||
ACCEPT WS-ARG2 FROM ARGUMENT-VALUE.
|
||||
|
||||
EVALUATE WS-ARG2
|
||||
WHEN "--list"
|
||||
WHEN "-l"
|
||||
PERFORM SNAPSHOT-LIST
|
||||
WHEN "--info"
|
||||
ACCEPT WS-ID FROM ARGUMENT-VALUE
|
||||
PERFORM SNAPSHOT-INFO
|
||||
WHEN "--delete"
|
||||
ACCEPT WS-ID FROM ARGUMENT-VALUE
|
||||
PERFORM SNAPSHOT-DELETE
|
||||
WHEN "--lock"
|
||||
ACCEPT WS-ID FROM ARGUMENT-VALUE
|
||||
PERFORM SNAPSHOT-LOCK
|
||||
WHEN "--unlock"
|
||||
ACCEPT WS-ID FROM ARGUMENT-VALUE
|
||||
PERFORM SNAPSHOT-UNLOCK
|
||||
WHEN "--restore"
|
||||
ACCEPT WS-ID FROM ARGUMENT-VALUE
|
||||
PERFORM SNAPSHOT-RESTORE
|
||||
WHEN "--clone"
|
||||
ACCEPT WS-ID FROM ARGUMENT-VALUE
|
||||
PERFORM PARSE-SNAPSHOT-CLONE-ARGS
|
||||
PERFORM SNAPSHOT-CLONE
|
||||
WHEN OTHER
|
||||
DISPLAY "Usage: un snapshot [options]" UPON SYSERR
|
||||
DISPLAY " --list, -l List snapshots" UPON SYSERR
|
||||
DISPLAY " --info ID Get snapshot details"
|
||||
UPON SYSERR
|
||||
DISPLAY " --delete ID Delete snapshot"
|
||||
UPON SYSERR
|
||||
DISPLAY " --lock ID Lock snapshot" UPON SYSERR
|
||||
DISPLAY " --unlock ID Unlock snapshot" UPON SYSERR
|
||||
DISPLAY " --restore ID Restore snapshot"
|
||||
UPON SYSERR
|
||||
DISPLAY " --clone ID Clone snapshot" UPON SYSERR
|
||||
MOVE 1 TO RETURN-CODE
|
||||
STOP RUN
|
||||
END-EVALUATE.
|
||||
|
||||
SNAPSHOT-LIST.
|
||||
STRING "TS=$(date +%s); "
|
||||
"SIG=$(echo -n \"$TS:GET:/snapshots:\" | "
|
||||
"openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"curl -s -X GET 'https://api.unsandbox.com/snapshots' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG | jq ."
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
SNAPSHOT-INFO.
|
||||
STRING "TS=$(date +%s); "
|
||||
"SIG=$(echo -n \"$TS:GET:/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
":\" | openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"curl -s -X GET 'https://api.unsandbox.com/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG | jq ."
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
SNAPSHOT-DELETE.
|
||||
STRING "TS=$(date +%s); "
|
||||
"SIG=$(echo -n \"$TS:DELETE:/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
":\" | openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"RESP=$(curl -s -w '\n%{http_code}' -X DELETE "
|
||||
"'https://api.unsandbox.com/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG); "
|
||||
"HTTP_CODE=$(echo \"$RESP\" | tail -1); "
|
||||
"BODY=$(echo \"$RESP\" | head -n -1); "
|
||||
"if [ \"$HTTP_CODE\" = \"428\" ]; then "
|
||||
"OTP=$(echo \"$BODY\" | jq -r '.otp // empty'); "
|
||||
"if [ -n \"$OTP\" ]; then "
|
||||
"TS2=$(date +%s); "
|
||||
"SIG2=$(echo -n \"$TS2:DELETE:/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
":\" | openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"curl -s -X DELETE 'https://api.unsandbox.com/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS2 "
|
||||
"-H 'X-Signature: '$SIG2 "
|
||||
"-H 'X-Sudo-OTP: '$OTP | jq .; "
|
||||
"echo -e '\x1b[32mSnapshot deleted\x1b[0m'; fi; "
|
||||
"else echo \"$BODY\" | jq .; fi"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
SNAPSHOT-LOCK.
|
||||
STRING "TS=$(date +%s); "
|
||||
"SIG=$(echo -n \"$TS:POST:/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/lock:\" | openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"curl -s -X POST 'https://api.unsandbox.com/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/lock' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG | jq . && "
|
||||
"echo -e '\x1b[32mSnapshot locked\x1b[0m'"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
SNAPSHOT-UNLOCK.
|
||||
STRING "TS=$(date +%s); "
|
||||
"BODY='{}'; "
|
||||
"SIG=$(echo -n \"$TS:POST:/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/unlock:$BODY\" | openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"RESP=$(curl -s -w '\n%{http_code}' -X POST "
|
||||
"'https://api.unsandbox.com/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/unlock' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG "
|
||||
"-d \"$BODY\"); "
|
||||
"HTTP_CODE=$(echo \"$RESP\" | tail -1); "
|
||||
"BODY_RESP=$(echo \"$RESP\" | head -n -1); "
|
||||
"if [ \"$HTTP_CODE\" = \"428\" ]; then "
|
||||
"OTP=$(echo \"$BODY_RESP\" | jq -r '.otp // empty'); "
|
||||
"if [ -n \"$OTP\" ]; then "
|
||||
"TS2=$(date +%s); "
|
||||
"SIG2=$(echo -n \"$TS2:POST:/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/unlock:$BODY\" | openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"curl -s -X POST 'https://api.unsandbox.com/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/unlock' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS2 "
|
||||
"-H 'X-Signature: '$SIG2 "
|
||||
"-H 'X-Sudo-OTP: '$OTP "
|
||||
"-d \"$BODY\" | jq .; "
|
||||
"echo -e '\x1b[32mSnapshot unlocked\x1b[0m'; fi; "
|
||||
"else echo \"$BODY_RESP\" | jq .; fi"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
SNAPSHOT-RESTORE.
|
||||
STRING "TS=$(date +%s); "
|
||||
"BODY='{}'; "
|
||||
"SIG=$(echo -n \"$TS:POST:/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/restore:$BODY\" | openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"curl -s -X POST 'https://api.unsandbox.com/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/restore' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG "
|
||||
"-d \"$BODY\" | jq . && "
|
||||
"echo -e '\x1b[32mSnapshot restored\x1b[0m'"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
PARSE-SNAPSHOT-CLONE-ARGS.
|
||||
* Parse --type, --name, --ports, --shell
|
||||
MOVE SPACES TO WS-TYPE.
|
||||
MOVE SPACES TO WS-NAME.
|
||||
MOVE SPACES TO WS-PORTS.
|
||||
MOVE SPACES TO WS-SHELL.
|
||||
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE.
|
||||
PERFORM UNTIL WS-ARG3 = SPACES
|
||||
IF WS-ARG3 = "--type"
|
||||
ACCEPT WS-TYPE FROM ARGUMENT-VALUE
|
||||
ELSE IF WS-ARG3 = "--name"
|
||||
ACCEPT WS-NAME FROM ARGUMENT-VALUE
|
||||
ELSE IF WS-ARG3 = "--ports"
|
||||
ACCEPT WS-PORTS FROM ARGUMENT-VALUE
|
||||
ELSE IF WS-ARG3 = "--shell"
|
||||
ACCEPT WS-SHELL FROM ARGUMENT-VALUE
|
||||
END-IF
|
||||
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE
|
||||
END-PERFORM.
|
||||
|
||||
IF WS-TYPE = SPACES
|
||||
DISPLAY "Error: --type required (session or service)"
|
||||
UPON SYSERR
|
||||
MOVE 1 TO RETURN-CODE
|
||||
STOP RUN
|
||||
END-IF.
|
||||
|
||||
SNAPSHOT-CLONE.
|
||||
* Build clone request
|
||||
STRING "TS=$(date +%s); "
|
||||
"BODY='{\"type\":\"" FUNCTION TRIM(WS-TYPE) "\""
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
IF WS-NAME NOT = SPACES
|
||||
STRING FUNCTION TRIM(WS-CURL-CMD)
|
||||
",\"name\":\"" FUNCTION TRIM(WS-NAME) "\""
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING
|
||||
END-IF.
|
||||
|
||||
IF WS-PORTS NOT = SPACES
|
||||
STRING FUNCTION TRIM(WS-CURL-CMD)
|
||||
",\"ports\":[" FUNCTION TRIM(WS-PORTS) "]"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING
|
||||
END-IF.
|
||||
|
||||
IF WS-SHELL NOT = SPACES
|
||||
STRING FUNCTION TRIM(WS-CURL-CMD)
|
||||
",\"shell\":\"" FUNCTION TRIM(WS-SHELL) "\""
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING
|
||||
END-IF.
|
||||
|
||||
STRING FUNCTION TRIM(WS-CURL-CMD)
|
||||
"}'; "
|
||||
"SIG=$(echo -n \"$TS:POST:/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/clone:$BODY\" | openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"curl -s -X POST 'https://api.unsandbox.com/snapshots/"
|
||||
FUNCTION TRIM(WS-ID)
|
||||
"/clone' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG "
|
||||
"-d \"$BODY\" | jq . && "
|
||||
"echo -e '\x1b[32mSnapshot cloned\x1b[0m'"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
|
|
|||
94
clients/cobol/tests/test_un.sh
Executable file
94
clients/cobol/tests/test_un.sh
Executable file
|
|
@ -0,0 +1,94 @@
|
|||
#!/bin/bash
|
||||
# Test suite for COBOL Unsandbox SDK
|
||||
# Run: bash tests/test_un.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SDK_DIR="$SCRIPT_DIR/../sync/src"
|
||||
SOURCE="$SDK_DIR/un.cob"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
|
||||
# Test helper
|
||||
test_that() {
|
||||
local description="$1"
|
||||
local test_cmd="$2"
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
|
||||
if eval "$test_cmd" >/dev/null 2>&1; then
|
||||
echo -e "[${GREEN}PASS${NC}] $description"
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
return 0
|
||||
else
|
||||
echo -e "[${RED}FAIL${NC}] $description"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test source file exists
|
||||
echo ""
|
||||
echo "=== Source File ==="
|
||||
test_that "Source file exists" "[ -f '$SOURCE' ]"
|
||||
|
||||
echo ""
|
||||
echo "=== Source Code Structure ==="
|
||||
test_that "Has WORKING-STORAGE SECTION" "grep -q 'WORKING-STORAGE SECTION' '$SOURCE'"
|
||||
test_that "Has session handler" "grep -q 'HANDLE-SESSION' '$SOURCE'"
|
||||
test_that "Has service handler" "grep -q 'HANDLE-SERVICE' '$SOURCE'"
|
||||
test_that "Has snapshot handler" "grep -q 'HANDLE-SNAPSHOT' '$SOURCE'"
|
||||
test_that "Has image handler" "grep -q 'HANDLE-IMAGE' '$SOURCE'"
|
||||
test_that "Has key handler" "grep -q 'HANDLE-KEY' '$SOURCE'"
|
||||
test_that "Has languages handler" "grep -q 'HANDLE-LANGUAGES' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Snapshot Operations ==="
|
||||
test_that "Snapshot list implemented" "grep -q 'SNAPSHOT-LIST' '$SOURCE'"
|
||||
test_that "Snapshot info implemented" "grep -q 'SNAPSHOT-INFO' '$SOURCE'"
|
||||
test_that "Snapshot delete implemented" "grep -q 'SNAPSHOT-DELETE' '$SOURCE'"
|
||||
test_that "Snapshot lock implemented" "grep -q 'SNAPSHOT-LOCK' '$SOURCE'"
|
||||
test_that "Snapshot unlock implemented" "grep -q 'SNAPSHOT-UNLOCK' '$SOURCE'"
|
||||
test_that "Snapshot restore implemented" "grep -q 'SNAPSHOT-RESTORE' '$SOURCE'"
|
||||
test_that "Snapshot clone implemented" "grep -q 'SNAPSHOT-CLONE' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Image Operations ==="
|
||||
test_that "Image list implemented" "grep -q 'IMAGE-LIST' '$SOURCE'"
|
||||
test_that "Image info implemented" "grep -q 'IMAGE-INFO' '$SOURCE'"
|
||||
test_that "Image delete implemented" "grep -q 'IMAGE-DELETE' '$SOURCE'"
|
||||
test_that "Image lock implemented" "grep -q 'IMAGE-LOCK' '$SOURCE'"
|
||||
test_that "Image unlock implemented" "grep -q 'IMAGE-UNLOCK' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== HMAC Authentication ==="
|
||||
test_that "Uses openssl for HMAC" "grep -q 'openssl dgst -sha256 -hmac' '$SOURCE'"
|
||||
test_that "Has X-Signature header" "grep -q 'X-Signature' '$SOURCE'"
|
||||
test_that "Has X-Timestamp header" "grep -q 'X-Timestamp' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Sudo OTP Handling ==="
|
||||
test_that "Handles 428 response" "grep -q '428' '$SOURCE'"
|
||||
test_that "Has X-Sudo-OTP header" "grep -q 'X-Sudo-OTP' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Variables ==="
|
||||
test_that "WS-TYPE variable defined" "grep -q 'WS-TYPE' '$SOURCE'"
|
||||
test_that "WS-SHELL variable defined" "grep -q 'WS-SHELL' '$SOURCE'"
|
||||
test_that "WS-PORTS variable defined" "grep -q 'WS-PORTS' '$SOURCE'"
|
||||
test_that "WS-NAME variable defined" "grep -q 'WS-NAME' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Summary ==="
|
||||
echo "Tests passed: $TESTS_PASSED / $TESTS_RUN"
|
||||
|
||||
if [ $TESTS_PASSED -eq $TESTS_RUN ]; then
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -68,6 +68,7 @@
|
|||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
|
@ -491,6 +492,508 @@ void set_unfreeze_on_demand(const string& service_id, bool enabled, const string
|
|||
cout << GREEN << "Service unfreeze_on_demand set to " << enabled_str << RESET << endl;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Library Functions for C++ SDK (matching C reference un.h)
|
||||
// ============================================================================
|
||||
|
||||
const string SDK_VERSION = "4.2.0";
|
||||
|
||||
// Execute code synchronously
|
||||
string execute(const string& language, const string& code, const string& public_key, const string& secret_key) {
|
||||
string body = "{\"language\":\"" + escape_json(language) + "\",\"code\":\"" + escape_json(code) + "\"}";
|
||||
string auth_headers = build_auth_headers("POST", "/execute", body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/execute' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " "
|
||||
"-d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Execute code asynchronously (returns job_id)
|
||||
string execute_async(const string& language, const string& code, const string& public_key, const string& secret_key) {
|
||||
string body = "{\"language\":\"" + escape_json(language) + "\",\"code\":\"" + escape_json(code) + "\",\"async\":true}";
|
||||
string auth_headers = build_auth_headers("POST", "/execute", body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/execute' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " "
|
||||
"-d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Get job status
|
||||
string get_job(const string& job_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/jobs/" + job_id;
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Wait for job completion
|
||||
string wait_for_job(const string& job_id, const string& public_key, const string& secret_key) {
|
||||
const int poll_delays[] = {300, 450, 700, 900, 650, 1600, 2000};
|
||||
const int poll_count = 7;
|
||||
int delay_idx = 0;
|
||||
|
||||
while (true) {
|
||||
string result = get_job(job_id, public_key, secret_key);
|
||||
if (result.find("\"status\":\"completed\"") != string::npos ||
|
||||
result.find("\"status\":\"failed\"") != string::npos ||
|
||||
result.find("\"status\":\"timeout\"") != string::npos ||
|
||||
result.find("\"status\":\"cancelled\"") != string::npos) {
|
||||
return result;
|
||||
}
|
||||
|
||||
usleep(poll_delays[delay_idx % poll_count] * 1000);
|
||||
if (delay_idx < poll_count - 1) delay_idx++;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel a job
|
||||
string cancel_job(const string& job_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/jobs/" + job_id + "/cancel";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// List all jobs
|
||||
string list_jobs(const string& public_key, const string& secret_key) {
|
||||
string auth_headers = build_auth_headers("GET", "/jobs", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/jobs' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Get supported languages
|
||||
string get_languages(const string& public_key, const string& secret_key) {
|
||||
string auth_headers = build_auth_headers("GET", "/languages", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/languages' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Session functions
|
||||
string session_list(const string& public_key, const string& secret_key) {
|
||||
string auth_headers = build_auth_headers("GET", "/sessions", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/sessions' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string session_get(const string& session_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/sessions/" + session_id;
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string session_create(const string& shell, const string& network, const string& public_key, const string& secret_key) {
|
||||
string body = "{\"shell\":\"" + (shell.empty() ? "bash" : shell) + "\"";
|
||||
if (!network.empty()) body += ",\"network\":\"" + network + "\"";
|
||||
body += "}";
|
||||
string auth_headers = build_auth_headers("POST", "/sessions", body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/sessions' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string session_destroy(const string& session_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/sessions/" + session_id;
|
||||
string auth_headers = build_auth_headers("DELETE", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X DELETE '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string session_freeze(const string& session_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/sessions/" + session_id + "/freeze";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string session_unfreeze(const string& session_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/sessions/" + session_id + "/unfreeze";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string session_boost(const string& session_id, int vcpu, const string& public_key, const string& secret_key) {
|
||||
string path = "/sessions/" + session_id + "/boost";
|
||||
string body = vcpu > 0 ? "{\"vcpu\":" + to_string(vcpu) + "}" : "{}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string session_unboost(const string& session_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/sessions/" + session_id + "/unboost";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string session_execute(const string& session_id, const string& command, const string& public_key, const string& secret_key) {
|
||||
string path = "/sessions/" + session_id + "/shell";
|
||||
string body = "{\"command\":\"" + escape_json(command) + "\"}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Service functions
|
||||
string service_list(const string& public_key, const string& secret_key) {
|
||||
string auth_headers = build_auth_headers("GET", "/services", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_get(const string& service_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id;
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_create(const string& name, const string& ports, const string& bootstrap, const string& network, const string& public_key, const string& secret_key) {
|
||||
string body = "{\"name\":\"" + escape_json(name) + "\"";
|
||||
if (!ports.empty()) body += ",\"ports\":\"" + ports + "\"";
|
||||
if (!bootstrap.empty()) body += ",\"bootstrap\":\"" + escape_json(bootstrap) + "\"";
|
||||
if (!network.empty()) body += ",\"network\":\"" + network + "\"";
|
||||
body += "}";
|
||||
string auth_headers = build_auth_headers("POST", "/services", body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/services' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_destroy(const string& service_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id;
|
||||
string auth_headers = build_auth_headers("DELETE", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X DELETE '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_freeze(const string& service_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/freeze";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_unfreeze(const string& service_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/unfreeze";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_lock(const string& service_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/lock";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_unlock(const string& service_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/unlock";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_redeploy(const string& service_id, const string& bootstrap, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/redeploy";
|
||||
string body = bootstrap.empty() ? "{}" : "{\"bootstrap\":\"" + escape_json(bootstrap) + "\"}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_logs(const string& service_id, bool all, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/logs" + (all ? "?all=true" : "");
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_execute(const string& service_id, const string& command, int timeout_ms, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/execute";
|
||||
string body = "{\"command\":\"" + escape_json(command) + "\"";
|
||||
if (timeout_ms > 0) body += ",\"timeout\":" + to_string(timeout_ms);
|
||||
body += "}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string service_resize(const string& service_id, int vcpu, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/resize";
|
||||
string body = "{\"vcpu\":" + to_string(vcpu) + "}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Snapshot functions
|
||||
string snapshot_list(const string& public_key, const string& secret_key) {
|
||||
string auth_headers = build_auth_headers("GET", "/snapshots", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/snapshots' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string snapshot_get(const string& snapshot_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/snapshots/" + snapshot_id;
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string snapshot_session(const string& session_id, const string& name, bool hot, const string& public_key, const string& secret_key) {
|
||||
string path = "/sessions/" + session_id + "/snapshot";
|
||||
string body = "{";
|
||||
if (!name.empty()) body += "\"name\":\"" + escape_json(name) + "\",";
|
||||
body += "\"hot\":" + string(hot ? "true" : "false") + "}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string snapshot_service(const string& service_id, const string& name, bool hot, const string& public_key, const string& secret_key) {
|
||||
string path = "/services/" + service_id + "/snapshot";
|
||||
string body = "{";
|
||||
if (!name.empty()) body += "\"name\":\"" + escape_json(name) + "\",";
|
||||
body += "\"hot\":" + string(hot ? "true" : "false") + "}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string snapshot_restore(const string& snapshot_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/snapshots/" + snapshot_id + "/restore";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string snapshot_delete(const string& snapshot_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/snapshots/" + snapshot_id;
|
||||
string auth_headers = build_auth_headers("DELETE", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X DELETE '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string snapshot_lock(const string& snapshot_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/snapshots/" + snapshot_id + "/lock";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string snapshot_unlock(const string& snapshot_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/snapshots/" + snapshot_id + "/unlock";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string snapshot_clone(const string& snapshot_id, const string& clone_type, const string& name, const string& ports, const string& shell, const string& public_key, const string& secret_key) {
|
||||
string path = "/snapshots/" + snapshot_id + "/clone";
|
||||
string body = "{\"type\":\"" + clone_type + "\"";
|
||||
if (!name.empty()) body += ",\"name\":\"" + escape_json(name) + "\"";
|
||||
if (!ports.empty()) body += ",\"ports\":\"" + ports + "\"";
|
||||
if (!shell.empty()) body += ",\"shell\":\"" + shell + "\"";
|
||||
body += "}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Image functions
|
||||
string image_list(const string& filter, const string& public_key, const string& secret_key) {
|
||||
string path = "/images" + (filter.empty() ? "" : "?filter=" + filter);
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_get(const string& image_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id;
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_publish(const string& source_type, const string& source_id, const string& name, const string& description, const string& public_key, const string& secret_key) {
|
||||
string body = "{\"source_type\":\"" + source_type + "\",\"source_id\":\"" + source_id + "\"";
|
||||
if (!name.empty()) body += ",\"name\":\"" + escape_json(name) + "\"";
|
||||
if (!description.empty()) body += ",\"description\":\"" + escape_json(description) + "\"";
|
||||
body += "}";
|
||||
string auth_headers = build_auth_headers("POST", "/images", body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/images' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_delete(const string& image_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id;
|
||||
string auth_headers = build_auth_headers("DELETE", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X DELETE '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_lock(const string& image_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/lock";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_unlock(const string& image_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/unlock";
|
||||
string auth_headers = build_auth_headers("POST", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_set_visibility(const string& image_id, const string& visibility, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/visibility";
|
||||
string body = "{\"visibility\":\"" + visibility + "\"}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_grant_access(const string& image_id, const string& trusted_key, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/grant";
|
||||
string body = "{\"trusted_api_key\":\"" + trusted_key + "\"}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_revoke_access(const string& image_id, const string& trusted_key, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/revoke";
|
||||
string body = "{\"trusted_api_key\":\"" + trusted_key + "\"}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_list_trusted(const string& image_id, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/trusted";
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_transfer(const string& image_id, const string& to_api_key, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/transfer";
|
||||
string body = "{\"to_api_key\":\"" + to_api_key + "\"}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_spawn(const string& image_id, const string& name, const string& ports, const string& bootstrap, const string& network, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/spawn";
|
||||
string body = "{";
|
||||
bool has_field = false;
|
||||
if (!name.empty()) { body += "\"name\":\"" + escape_json(name) + "\""; has_field = true; }
|
||||
if (!ports.empty()) { body += string(has_field ? "," : "") + "\"ports\":\"" + ports + "\""; has_field = true; }
|
||||
if (!bootstrap.empty()) { body += string(has_field ? "," : "") + "\"bootstrap\":\"" + escape_json(bootstrap) + "\""; has_field = true; }
|
||||
if (!network.empty()) { body += string(has_field ? "," : "") + "\"network\":\"" + network + "\""; }
|
||||
body += "}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
string image_clone(const string& image_id, const string& name, const string& description, const string& public_key, const string& secret_key) {
|
||||
string path = "/images/" + image_id + "/clone";
|
||||
string body = "{";
|
||||
bool has_field = false;
|
||||
if (!name.empty()) { body += "\"name\":\"" + escape_json(name) + "\""; has_field = true; }
|
||||
if (!description.empty()) { body += string(has_field ? "," : "") + "\"description\":\"" + escape_json(description) + "\""; }
|
||||
body += "}";
|
||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
+ auth_headers + " -d '" + body + "'";
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// PaaS Logs functions
|
||||
string logs_fetch(const string& source, int lines, const string& since, const string& grep, const string& public_key, const string& secret_key) {
|
||||
string path = "/paas/logs?";
|
||||
if (!source.empty()) path += "source=" + source + "&";
|
||||
if (lines > 0) path += "lines=" + to_string(lines) + "&";
|
||||
if (!since.empty()) path += "since=" + since + "&";
|
||||
if (!grep.empty()) path += "grep=" + grep + "&";
|
||||
if (path.back() == '&' || path.back() == '?') path.pop_back();
|
||||
string auth_headers = build_auth_headers("GET", path, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + path + "' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Key validation
|
||||
string validate_keys(const string& public_key, const string& secret_key) {
|
||||
string auth_headers = build_auth_headers("POST", "/keys/validate", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/keys/validate' " + auth_headers;
|
||||
return exec_curl(cmd);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
string hmac_sign(const string& secret_key, const string& message) {
|
||||
return compute_hmac(secret_key, message);
|
||||
}
|
||||
|
||||
bool health_check() {
|
||||
string cmd = "curl -s -o /dev/null -w '%{http_code}' '" + API_BASE + "/health' 2>/dev/null";
|
||||
string result = exec_curl(cmd);
|
||||
return result.find("200") != string::npos;
|
||||
}
|
||||
|
||||
string version() {
|
||||
return SDK_VERSION;
|
||||
}
|
||||
|
||||
static string last_error_msg;
|
||||
|
||||
void set_last_error(const string& msg) {
|
||||
last_error_msg = msg;
|
||||
}
|
||||
|
||||
string last_error() {
|
||||
return last_error_msg;
|
||||
}
|
||||
|
||||
void cmd_service_env(const string& action, const string& target, const vector<string>& envs, const string& env_file, const string& public_key, const string& secret_key) {
|
||||
if (action == "status") {
|
||||
if (target.empty()) {
|
||||
|
|
|
|||
311
clients/cpp/sync/tests/test_un.cpp
Normal file
311
clients/cpp/sync/tests/test_un.cpp
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
// Tests for the C++ unsandbox SDK
|
||||
// Compile: g++ -std=c++17 -o test_un test_un.cpp -I../src
|
||||
// Run: ./test_un
|
||||
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
|
||||
// Include the SDK source directly for testing
|
||||
// In production, you'd link against the compiled library
|
||||
#include "../src/un.cpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
int tests_passed = 0;
|
||||
int tests_failed = 0;
|
||||
|
||||
#define TEST(name) void test_##name()
|
||||
#define RUN_TEST(name) do { \
|
||||
cout << "Running " << #name << "..." << endl; \
|
||||
try { \
|
||||
test_##name(); \
|
||||
cout << " PASS" << endl; \
|
||||
tests_passed++; \
|
||||
} catch (const exception& e) { \
|
||||
cout << " FAIL: " << e.what() << endl; \
|
||||
tests_failed++; \
|
||||
} catch (...) { \
|
||||
cout << " FAIL: Unknown exception" << endl; \
|
||||
tests_failed++; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define ASSERT(cond) do { \
|
||||
if (!(cond)) { \
|
||||
throw runtime_error("Assertion failed: " #cond); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define ASSERT_EQ(a, b) do { \
|
||||
if ((a) != (b)) { \
|
||||
throw runtime_error("Assertion failed: " #a " == " #b); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define ASSERT_NE(a, b) do { \
|
||||
if ((a) == (b)) { \
|
||||
throw runtime_error("Assertion failed: " #a " != " #b); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests - Test exported library functions
|
||||
// ============================================================================
|
||||
|
||||
TEST(detect_language) {
|
||||
ASSERT_EQ(detect_language("script.py"), "python");
|
||||
ASSERT_EQ(detect_language("script.js"), "javascript");
|
||||
ASSERT_EQ(detect_language("script.ts"), "typescript");
|
||||
ASSERT_EQ(detect_language("script.go"), "go");
|
||||
ASSERT_EQ(detect_language("script.rs"), "rust");
|
||||
ASSERT_EQ(detect_language("script.c"), "c");
|
||||
ASSERT_EQ(detect_language("script.cpp"), "cpp");
|
||||
ASSERT_EQ(detect_language("script.d"), "d");
|
||||
ASSERT_EQ(detect_language("script.zig"), "zig");
|
||||
ASSERT_EQ(detect_language("script.sh"), "bash");
|
||||
ASSERT_EQ(detect_language("script.lua"), "lua");
|
||||
ASSERT_EQ(detect_language("script.php"), "php");
|
||||
ASSERT_EQ(detect_language("script.unknown"), "");
|
||||
ASSERT_EQ(detect_language("script"), "");
|
||||
}
|
||||
|
||||
TEST(hmac_sign) {
|
||||
string secret_key = "test-secret";
|
||||
string message = "test-message";
|
||||
|
||||
string result = hmac_sign(secret_key, message);
|
||||
|
||||
// Should return a 64-character hex string
|
||||
ASSERT_EQ(result.length(), 64u);
|
||||
|
||||
// Should be deterministic
|
||||
string result2 = hmac_sign(secret_key, message);
|
||||
ASSERT_EQ(result, result2);
|
||||
|
||||
// Different inputs should produce different outputs
|
||||
string result3 = hmac_sign(secret_key, "different-message");
|
||||
ASSERT_NE(result, result3);
|
||||
}
|
||||
|
||||
TEST(version) {
|
||||
string v = version();
|
||||
ASSERT(!v.empty());
|
||||
// Should be in semver format (at least "0.0.0")
|
||||
ASSERT(v.length() >= 5);
|
||||
}
|
||||
|
||||
TEST(last_error) {
|
||||
// Set an error
|
||||
set_last_error("test error message");
|
||||
|
||||
// Retrieve it
|
||||
string err = last_error();
|
||||
ASSERT_EQ(err, "test error message");
|
||||
|
||||
// Clear it
|
||||
set_last_error("");
|
||||
err = last_error();
|
||||
ASSERT(err.empty());
|
||||
}
|
||||
|
||||
TEST(escape_json) {
|
||||
ASSERT_EQ(escape_json("hello"), "hello");
|
||||
ASSERT_EQ(escape_json("hello\"world"), "hello\\\"world");
|
||||
ASSERT_EQ(escape_json("line1\nline2"), "line1\\nline2");
|
||||
ASSERT_EQ(escape_json("tab\there"), "tab\\there");
|
||||
ASSERT_EQ(escape_json("back\\slash"), "back\\\\slash");
|
||||
}
|
||||
|
||||
TEST(base64_encode) {
|
||||
ASSERT_EQ(base64_encode(""), "");
|
||||
ASSERT_EQ(base64_encode("f"), "Zg==");
|
||||
ASSERT_EQ(base64_encode("fo"), "Zm8=");
|
||||
ASSERT_EQ(base64_encode("foo"), "Zm9v");
|
||||
ASSERT_EQ(base64_encode("foob"), "Zm9vYg==");
|
||||
ASSERT_EQ(base64_encode("fooba"), "Zm9vYmE=");
|
||||
ASSERT_EQ(base64_encode("foobar"), "Zm9vYmFy");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests - Test SDK internal consistency
|
||||
// ============================================================================
|
||||
|
||||
TEST(compute_hmac) {
|
||||
string key = "test-key";
|
||||
string msg = "test-message";
|
||||
|
||||
string sig1 = compute_hmac(key, msg);
|
||||
string sig2 = compute_hmac(key, msg);
|
||||
|
||||
// Should be deterministic
|
||||
ASSERT_EQ(sig1, sig2);
|
||||
|
||||
// Should produce different results for different inputs
|
||||
string sig3 = compute_hmac(key, "different");
|
||||
ASSERT_NE(sig1, sig3);
|
||||
}
|
||||
|
||||
TEST(build_auth_headers) {
|
||||
string pk = "unsb-pk-test-test-test-test";
|
||||
string sk = "unsb-sk-test1-test2-test3-test4";
|
||||
|
||||
string headers = build_auth_headers("POST", "/execute", "{}", pk, sk);
|
||||
|
||||
// Should contain auth header
|
||||
ASSERT(headers.find("Authorization: Bearer " + pk) != string::npos);
|
||||
// Should contain timestamp header
|
||||
ASSERT(headers.find("X-Timestamp:") != string::npos);
|
||||
// Should contain signature header
|
||||
ASSERT(headers.find("X-Signature:") != string::npos);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Functional Tests - Test against real API (requires credentials)
|
||||
// ============================================================================
|
||||
|
||||
bool has_credentials() {
|
||||
const char* pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char* sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
return pk != nullptr && sk != nullptr && strlen(pk) > 0 && strlen(sk) > 0;
|
||||
}
|
||||
|
||||
TEST(health_check_functional) {
|
||||
if (!has_credentials()) {
|
||||
cout << " SKIP (no credentials)" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
bool healthy = health_check();
|
||||
// Just verify it doesn't crash
|
||||
cout << " Health check result: " << (healthy ? "healthy" : "unhealthy") << endl;
|
||||
}
|
||||
|
||||
TEST(get_languages_functional) {
|
||||
if (!has_credentials()) {
|
||||
cout << " SKIP (no credentials)" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char* sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
string result = get_languages(pk, sk);
|
||||
ASSERT(!result.empty());
|
||||
// Should contain python
|
||||
ASSERT(result.find("python") != string::npos);
|
||||
}
|
||||
|
||||
TEST(validate_keys_functional) {
|
||||
if (!has_credentials()) {
|
||||
cout << " SKIP (no credentials)" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char* sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
string result = validate_keys(pk, sk);
|
||||
ASSERT(!result.empty());
|
||||
}
|
||||
|
||||
TEST(execute_functional) {
|
||||
if (!has_credentials()) {
|
||||
cout << " SKIP (no credentials)" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char* sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
string result = execute("python", "print('hello from cpp test')", pk, sk);
|
||||
ASSERT(!result.empty());
|
||||
// Should contain output
|
||||
ASSERT(result.find("stdout") != string::npos || result.find("output") != string::npos);
|
||||
}
|
||||
|
||||
TEST(session_list_functional) {
|
||||
if (!has_credentials()) {
|
||||
cout << " SKIP (no credentials)" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char* sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
string result = session_list(pk, sk);
|
||||
ASSERT(!result.empty());
|
||||
}
|
||||
|
||||
TEST(service_list_functional) {
|
||||
if (!has_credentials()) {
|
||||
cout << " SKIP (no credentials)" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char* sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
string result = service_list(pk, sk);
|
||||
ASSERT(!result.empty());
|
||||
}
|
||||
|
||||
TEST(snapshot_list_functional) {
|
||||
if (!has_credentials()) {
|
||||
cout << " SKIP (no credentials)" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char* sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
string result = snapshot_list(pk, sk);
|
||||
ASSERT(!result.empty());
|
||||
}
|
||||
|
||||
TEST(image_list_functional) {
|
||||
if (!has_credentials()) {
|
||||
cout << " SKIP (no credentials)" << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char* sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
string result = image_list("", pk, sk);
|
||||
ASSERT(!result.empty());
|
||||
}
|
||||
|
||||
int main() {
|
||||
cout << "===== C++ SDK Tests =====" << endl << endl;
|
||||
|
||||
// Unit tests
|
||||
cout << "--- Unit Tests ---" << endl;
|
||||
RUN_TEST(detect_language);
|
||||
RUN_TEST(hmac_sign);
|
||||
RUN_TEST(version);
|
||||
RUN_TEST(last_error);
|
||||
RUN_TEST(escape_json);
|
||||
RUN_TEST(base64_encode);
|
||||
|
||||
cout << endl << "--- Integration Tests ---" << endl;
|
||||
RUN_TEST(compute_hmac);
|
||||
RUN_TEST(build_auth_headers);
|
||||
|
||||
cout << endl << "--- Functional Tests ---" << endl;
|
||||
RUN_TEST(health_check_functional);
|
||||
RUN_TEST(get_languages_functional);
|
||||
RUN_TEST(validate_keys_functional);
|
||||
RUN_TEST(execute_functional);
|
||||
RUN_TEST(session_list_functional);
|
||||
RUN_TEST(service_list_functional);
|
||||
RUN_TEST(snapshot_list_functional);
|
||||
RUN_TEST(image_list_functional);
|
||||
|
||||
cout << endl << "===== Results =====" << endl;
|
||||
cout << "Passed: " << tests_passed << endl;
|
||||
cout << "Failed: " << tests_failed << endl;
|
||||
|
||||
return tests_failed > 0 ? 1 : 0;
|
||||
}
|
||||
|
|
@ -573,12 +573,57 @@ def cmd_session(args)
|
|||
return
|
||||
end
|
||||
|
||||
if info_id = args[:session_info]?.as?(String)
|
||||
result = api_request("/sessions/#{info_id}", public_key, secret_key)
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
if kill_id = args[:kill]?.as?(String)
|
||||
api_request("/sessions/#{kill_id}", public_key, secret_key, method: "DELETE")
|
||||
puts "#{GREEN}Session terminated: #{kill_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if freeze_id = args[:session_freeze]?.as?(String)
|
||||
api_request("/sessions/#{freeze_id}/freeze", public_key, secret_key, method: "POST")
|
||||
puts "#{GREEN}Session frozen: #{freeze_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if unfreeze_id = args[:session_unfreeze]?.as?(String)
|
||||
api_request("/sessions/#{unfreeze_id}/unfreeze", public_key, secret_key, method: "POST")
|
||||
puts "#{GREEN}Session unfreezing: #{unfreeze_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if boost_id = args[:session_boost]?.as?(String)
|
||||
vcpu = args[:vcpu]?.as?(Int32) || 2
|
||||
payload = JSON.parse({vcpu: vcpu}.to_json)
|
||||
api_request("/sessions/#{boost_id}/boost", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Session boosted to #{vcpu} vCPU: #{boost_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if unboost_id = args[:session_unboost]?.as?(String)
|
||||
api_request("/sessions/#{unboost_id}/unboost", public_key, secret_key, method: "POST")
|
||||
puts "#{GREEN}Session unboosted: #{unboost_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if execute_id = args[:session_execute]?.as?(String)
|
||||
command = args[:command]?.as?(String) || ""
|
||||
payload = JSON.parse({command: command}.to_json)
|
||||
result = api_request("/sessions/#{execute_id}/execute", public_key, secret_key, method: "POST", data: payload)
|
||||
if stdout = result["stdout"]?.try(&.as_s?)
|
||||
print BLUE, stdout, RESET
|
||||
end
|
||||
if stderr = result["stderr"]?.try(&.as_s?)
|
||||
print RED, stderr, RESET
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
# Create new session
|
||||
payload = JSON.parse({shell: "bash"}.to_json)
|
||||
|
||||
|
|
@ -586,6 +631,10 @@ def cmd_session(args)
|
|||
payload.as_h["network"] = JSON::Any.new(network)
|
||||
end
|
||||
|
||||
if shell = args[:shell]?.as?(String)
|
||||
payload.as_h["shell"] = JSON::Any.new(shell)
|
||||
end
|
||||
|
||||
# Add input files
|
||||
if files = args[:files]?.as?(Array(String))
|
||||
input_files = [] of JSON::Any
|
||||
|
|
@ -844,11 +893,291 @@ def cmd_image(args)
|
|||
return
|
||||
end
|
||||
|
||||
if grant_id = args[:image_grant]?.as?(String)
|
||||
trusted_key = args[:image_trusted_key]?.as?(String)
|
||||
if trusted_key.nil? || trusted_key.empty?
|
||||
STDERR.puts "#{RED}Error: --grant requires --trusted-key#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
payload = JSON.parse({trusted_api_key: trusted_key}.to_json)
|
||||
api_request("/images/#{grant_id}/grant", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Access granted to #{trusted_key}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if revoke_id = args[:image_revoke]?.as?(String)
|
||||
trusted_key = args[:image_trusted_key]?.as?(String)
|
||||
if trusted_key.nil? || trusted_key.empty?
|
||||
STDERR.puts "#{RED}Error: --revoke requires --trusted-key#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
payload = JSON.parse({trusted_api_key: trusted_key}.to_json)
|
||||
api_request("/images/#{revoke_id}/revoke", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Access revoked from #{trusted_key}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if trusted_id = args[:image_trusted]?.as?(String)
|
||||
result = api_request("/images/#{trusted_id}/trusted", public_key, secret_key)
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
if transfer_id = args[:image_transfer]?.as?(String)
|
||||
to_key = args[:image_to_key]?.as?(String)
|
||||
if to_key.nil? || to_key.empty?
|
||||
STDERR.puts "#{RED}Error: --transfer requires --to-key#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
payload = JSON.parse({to_api_key: to_key}.to_json)
|
||||
status_code, response = api_request_with_sudo("/images/#{transfer_id}/transfer", public_key, secret_key, method: "POST", data: payload)
|
||||
if status_code == 428
|
||||
handle_sudo_challenge(response.to_json, public_key, secret_key, "POST", "/images/#{transfer_id}/transfer", payload.to_json)
|
||||
elsif status_code >= 200 && status_code < 300
|
||||
puts "#{GREEN}Image transferred to #{to_key}#{RESET}"
|
||||
else
|
||||
STDERR.puts "#{RED}Error: HTTP #{status_code}#{RESET}"
|
||||
STDERR.puts response.to_json
|
||||
exit 1
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
# Default: list images
|
||||
result = api_request("/images", public_key, secret_key)
|
||||
puts result.to_pretty_json
|
||||
end
|
||||
|
||||
def cmd_snapshot(args)
|
||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
||||
|
||||
if args[:list]?.as?(Bool)
|
||||
result = api_request("/snapshots", public_key, secret_key)
|
||||
snapshots = result["snapshots"]?.try(&.as_a?) || [] of JSON::Any
|
||||
if snapshots.empty?
|
||||
puts "No snapshots"
|
||||
else
|
||||
printf "%-20s %-20s %-10s %-10s %-10s %s\n", "ID", "Name", "Type", "Hot", "Locked", "Created"
|
||||
snapshots.each do |s|
|
||||
printf "%-20s %-20s %-10s %-10s %-10s %s\n",
|
||||
s["id"]?.try(&.as_s?) || "N/A",
|
||||
s["name"]?.try(&.as_s?) || "N/A",
|
||||
s["type"]?.try(&.as_s?) || "N/A",
|
||||
s["hot"]?.try(&.as_bool?) ? "yes" : "no",
|
||||
s["locked"]?.try(&.as_bool?) ? "yes" : "no",
|
||||
s["created_at"]?.try(&.as_s?) || "N/A"
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if info_id = args[:snapshot_info]?.as?(String)
|
||||
result = api_request("/snapshots/#{info_id}", public_key, secret_key)
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
if session_id = args[:snapshot_session]?.as?(String)
|
||||
payload = JSON.parse({}.to_json)
|
||||
if name = args[:snapshot_name]?.as?(String)
|
||||
payload.as_h["name"] = JSON::Any.new(name)
|
||||
end
|
||||
if args[:snapshot_hot]?.as?(Bool)
|
||||
payload.as_h["hot"] = JSON::Any.new(true)
|
||||
end
|
||||
result = api_request("/sessions/#{session_id}/snapshot", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Snapshot created#{RESET}"
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
if service_id = args[:snapshot_service]?.as?(String)
|
||||
payload = JSON.parse({}.to_json)
|
||||
if name = args[:snapshot_name]?.as?(String)
|
||||
payload.as_h["name"] = JSON::Any.new(name)
|
||||
end
|
||||
if args[:snapshot_hot]?.as?(Bool)
|
||||
payload.as_h["hot"] = JSON::Any.new(true)
|
||||
end
|
||||
result = api_request("/services/#{service_id}/snapshot", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Snapshot created#{RESET}"
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
if restore_id = args[:snapshot_restore]?.as?(String)
|
||||
payload = JSON.parse({}.to_json)
|
||||
result = api_request("/snapshots/#{restore_id}/restore", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Snapshot restored#{RESET}"
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
if del_id = args[:snapshot_delete]?.as?(String)
|
||||
status_code, response = api_request_with_sudo("/snapshots/#{del_id}", public_key, secret_key, method: "DELETE")
|
||||
if status_code == 428
|
||||
handle_sudo_challenge(response.to_json, public_key, secret_key, "DELETE", "/snapshots/#{del_id}", nil)
|
||||
elsif status_code >= 200 && status_code < 300
|
||||
puts "#{GREEN}Snapshot deleted: #{del_id}#{RESET}"
|
||||
else
|
||||
STDERR.puts "#{RED}Error: HTTP #{status_code}#{RESET}"
|
||||
STDERR.puts response.to_json
|
||||
exit 1
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if lock_id = args[:snapshot_lock]?.as?(String)
|
||||
payload = JSON.parse({}.to_json)
|
||||
api_request("/snapshots/#{lock_id}/lock", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Snapshot locked: #{lock_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if unlock_id = args[:snapshot_unlock]?.as?(String)
|
||||
payload = JSON.parse({}.to_json)
|
||||
body = "{}"
|
||||
status_code, response = api_request_with_sudo("/snapshots/#{unlock_id}/unlock", public_key, secret_key, method: "POST", data: payload)
|
||||
if status_code == 428
|
||||
handle_sudo_challenge(response.to_json, public_key, secret_key, "POST", "/snapshots/#{unlock_id}/unlock", body)
|
||||
elsif status_code >= 200 && status_code < 300
|
||||
puts "#{GREEN}Snapshot unlocked: #{unlock_id}#{RESET}"
|
||||
else
|
||||
STDERR.puts "#{RED}Error: HTTP #{status_code}#{RESET}"
|
||||
STDERR.puts response.to_json
|
||||
exit 1
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if clone_id = args[:snapshot_clone]?.as?(String)
|
||||
clone_type = args[:snapshot_clone_type]?.as?(String) || "session"
|
||||
payload = JSON.parse({clone_type: clone_type}.to_json)
|
||||
if name = args[:snapshot_name]?.as?(String)
|
||||
payload.as_h["name"] = JSON::Any.new(name)
|
||||
end
|
||||
if ports_str = args[:snapshot_ports]?.as?(String)
|
||||
ports = ports_str.split(',').map(&.to_i)
|
||||
payload.as_h["ports"] = JSON.parse(ports.to_json)
|
||||
end
|
||||
result = api_request("/snapshots/#{clone_id}/clone", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Snapshot cloned#{RESET}"
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
# Default: list snapshots
|
||||
result = api_request("/snapshots", public_key, secret_key)
|
||||
puts result.to_pretty_json
|
||||
end
|
||||
|
||||
def cmd_logs(args)
|
||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
||||
|
||||
source = args[:logs_source]?.as?(String) || "all"
|
||||
lines = args[:logs_lines]?.as?(Int32) || 100
|
||||
since = args[:logs_since]?.as?(String) || "1h"
|
||||
grep_pattern = args[:logs_grep]?.as?(String)
|
||||
|
||||
endpoint = "/paas/logs?source=#{source}&lines=#{lines}&since=#{since}"
|
||||
if grep_pattern && !grep_pattern.empty?
|
||||
endpoint += "&grep=#{URI.encode_path(grep_pattern)}"
|
||||
end
|
||||
|
||||
if args[:logs_follow]?.as?(Bool)
|
||||
# Streaming logs via SSE
|
||||
stream_endpoint = "/paas/logs/stream?source=#{source}"
|
||||
if grep_pattern && !grep_pattern.empty?
|
||||
stream_endpoint += "&grep=#{URI.encode_path(grep_pattern)}"
|
||||
end
|
||||
|
||||
url = URI.parse(PORTAL_BASE + stream_endpoint)
|
||||
headers = HTTP::Headers{
|
||||
"Accept" => "text/event-stream"
|
||||
}
|
||||
|
||||
# Add HMAC authentication headers
|
||||
if secret_key && !secret_key.empty?
|
||||
timestamp = Time.utc.to_unix.to_s
|
||||
message = "#{timestamp}:GET:#{stream_endpoint}:"
|
||||
signature = OpenSSL::HMAC.hexdigest(:sha256, secret_key, message)
|
||||
headers["Authorization"] = "Bearer #{public_key}"
|
||||
headers["X-Timestamp"] = timestamp
|
||||
headers["X-Signature"] = signature
|
||||
else
|
||||
headers["Authorization"] = "Bearer #{public_key}"
|
||||
end
|
||||
|
||||
begin
|
||||
HTTP::Client.get(url, headers: headers) do |response|
|
||||
if response.status_code == 200
|
||||
response.body_io.each_line do |line|
|
||||
if line.starts_with?("data: ")
|
||||
data = line[6..]
|
||||
begin
|
||||
parsed = JSON.parse(data)
|
||||
src = parsed["source"]?.try(&.as_s?) || "unknown"
|
||||
msg = parsed["line"]?.try(&.as_s?) || data
|
||||
puts "[#{src}] #{msg}"
|
||||
rescue
|
||||
puts line
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
STDERR.puts "#{RED}Error: HTTP #{response.status_code}#{RESET}"
|
||||
STDERR.puts response.body_io.gets_to_end
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
rescue ex
|
||||
STDERR.puts "#{RED}Error: #{ex.message}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
else
|
||||
# Batch fetch
|
||||
result = api_request(endpoint, public_key, secret_key)
|
||||
if logs = result["logs"]?.try(&.as_a?)
|
||||
logs.each do |log|
|
||||
src = log["source"]?.try(&.as_s?) || "unknown"
|
||||
msg = log["line"]?.try(&.as_s?) || log.to_json
|
||||
ts = log["timestamp"]?.try(&.as_s?) || ""
|
||||
if ts.empty?
|
||||
puts "[#{src}] #{msg}"
|
||||
else
|
||||
puts "[#{ts}] [#{src}] #{msg}"
|
||||
end
|
||||
end
|
||||
else
|
||||
puts result.to_pretty_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_health(args)
|
||||
begin
|
||||
url = URI.parse(API_BASE + "/health")
|
||||
response = HTTP::Client.get(url)
|
||||
if response.status_code == 200
|
||||
puts "#{GREEN}API is healthy#{RESET}"
|
||||
result = JSON.parse(response.body)
|
||||
puts result.to_pretty_json
|
||||
else
|
||||
puts "#{RED}API is unhealthy: HTTP #{response.status_code}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
rescue ex
|
||||
puts "#{RED}API is unreachable: #{ex.message}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_version(args)
|
||||
puts "un.cr version 1.0.0"
|
||||
puts "API: #{API_BASE}"
|
||||
puts "Portal: #{PORTAL_BASE}"
|
||||
end
|
||||
|
||||
def cmd_service(args)
|
||||
# Handle env subcommand
|
||||
if env_action = args[:env_action]?.as?(String)
|
||||
|
|
@ -973,6 +1302,48 @@ def cmd_service(args)
|
|||
return
|
||||
end
|
||||
|
||||
if lock_id = args[:service_lock]?.as?(String)
|
||||
payload = JSON.parse({}.to_json)
|
||||
api_request("/services/#{lock_id}/lock", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Service locked: #{lock_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if unlock_id = args[:service_unlock]?.as?(String)
|
||||
payload = JSON.parse({}.to_json)
|
||||
body = "{}"
|
||||
status_code, response = api_request_with_sudo("/services/#{unlock_id}/unlock", public_key, secret_key, method: "POST", data: payload)
|
||||
if status_code == 428
|
||||
handle_sudo_challenge(response.to_json, public_key, secret_key, "POST", "/services/#{unlock_id}/unlock", body)
|
||||
elsif status_code >= 200 && status_code < 300
|
||||
puts "#{GREEN}Service unlocked: #{unlock_id}#{RESET}"
|
||||
else
|
||||
STDERR.puts "#{RED}Error: HTTP #{status_code}#{RESET}"
|
||||
STDERR.puts response.to_json
|
||||
exit 1
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if redeploy_id = args[:redeploy]?.as?(String)
|
||||
payload = JSON.parse({}.to_json)
|
||||
if bootstrap = args[:bootstrap]?.as?(String)
|
||||
payload.as_h["bootstrap"] = JSON::Any.new(bootstrap)
|
||||
end
|
||||
if bootstrap_file = args[:bootstrap_file]?.as?(String)
|
||||
if File.exists?(bootstrap_file)
|
||||
payload.as_h["bootstrap_content"] = JSON::Any.new(File.read(bootstrap_file))
|
||||
else
|
||||
STDERR.puts "#{RED}Error: Bootstrap file not found: #{bootstrap_file}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
result = api_request("/services/#{redeploy_id}/redeploy", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Service redeployed: #{redeploy_id}#{RESET}"
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
# Create new service
|
||||
if name = args[:name]?.as?(String)
|
||||
payload = JSON.parse({name: name}.to_json)
|
||||
|
|
@ -1103,6 +1474,19 @@ def main
|
|||
env_action: nil,
|
||||
env_target: nil,
|
||||
json: false,
|
||||
shell: nil,
|
||||
# Session options
|
||||
session_info: nil,
|
||||
session_freeze: nil,
|
||||
session_unfreeze: nil,
|
||||
session_boost: nil,
|
||||
session_unboost: nil,
|
||||
session_execute: nil,
|
||||
# Service options
|
||||
service_lock: nil,
|
||||
service_unlock: nil,
|
||||
redeploy: nil,
|
||||
# Image options
|
||||
image_info: nil,
|
||||
image_delete: nil,
|
||||
image_lock: nil,
|
||||
|
|
@ -1114,11 +1498,36 @@ def main
|
|||
image_spawn: nil,
|
||||
image_clone: nil,
|
||||
image_name: nil,
|
||||
image_ports: nil
|
||||
} of Symbol => (String | Array(String) | Bool | Nil)
|
||||
image_ports: nil,
|
||||
image_grant: nil,
|
||||
image_revoke: nil,
|
||||
image_trusted: nil,
|
||||
image_trusted_key: nil,
|
||||
image_transfer: nil,
|
||||
image_to_key: nil,
|
||||
# Snapshot options
|
||||
snapshot_info: nil,
|
||||
snapshot_session: nil,
|
||||
snapshot_service: nil,
|
||||
snapshot_restore: nil,
|
||||
snapshot_delete: nil,
|
||||
snapshot_lock: nil,
|
||||
snapshot_unlock: nil,
|
||||
snapshot_clone: nil,
|
||||
snapshot_clone_type: nil,
|
||||
snapshot_name: nil,
|
||||
snapshot_hot: false,
|
||||
snapshot_ports: nil,
|
||||
# Logs options
|
||||
logs_source: nil,
|
||||
logs_lines: nil,
|
||||
logs_since: nil,
|
||||
logs_grep: nil,
|
||||
logs_follow: false
|
||||
} of Symbol => (String | Array(String) | Bool | Int32 | Nil)
|
||||
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env <action> <service_id> [options]\n un.cr key [options]\n\nService env commands:\n env status <id> Show vault status\n env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n env export <id> Export vault contents\n env delete <id> Delete vault"
|
||||
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env <action> <service_id> [options]\n un.cr snapshot [options]\n un.cr image [options]\n un.cr logs [options]\n un.cr key [options]\n un.cr health\n un.cr version\n\nService env commands:\n env status <id> Show vault status\n env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n env export <id> Export vault contents\n env delete <id> Delete vault"
|
||||
|
||||
opts.on("-k API_KEY", "--api-key=API_KEY", "API key") { |k| args[:api_key] = k }
|
||||
opts.on("-n NETWORK", "--network=NETWORK", "Network mode") { |n| args[:network] = n }
|
||||
|
|
@ -1131,21 +1540,21 @@ def main
|
|||
opts.on("-o DIR", "--output-dir=DIR", "Output directory") { |d| args[:output_dir] = d }
|
||||
opts.on("-l", "--list", "List items") { args[:list] = true }
|
||||
opts.on("--kill=ID", "Kill session") { |id| args[:kill] = id }
|
||||
opts.on("--info=ID", "Get service info") { |id| args[:info] = id }
|
||||
opts.on("--info=ID", "Get service/session info") { |id| args[:info] = id }
|
||||
opts.on("--logs=ID", "Get service logs") { |id| args[:logs] = id }
|
||||
opts.on("--freeze=ID", "Sleep service") { |id| args[:sleep] = id }
|
||||
opts.on("--unfreeze=ID", "Wake service") { |id| args[:wake] = id }
|
||||
opts.on("--freeze=ID", "Freeze service/session") { |id| args[:sleep] = id }
|
||||
opts.on("--unfreeze=ID", "Unfreeze service/session") { |id| args[:wake] = id }
|
||||
opts.on("--unfreeze-on-demand=ID", "Set unfreeze-on-demand for service") { |id| args[:unfreeze_on_demand] = id }
|
||||
opts.on("--unfreeze-on-demand-enabled=BOOL", "Enable/disable unfreeze-on-demand (default: true)") { |b| args[:unfreeze_on_demand_enabled] = b.downcase == "true" }
|
||||
opts.on("--with-unfreeze-on-demand", "Enable unfreeze-on-demand when creating service") { args[:create_unfreeze_on_demand] = true }
|
||||
opts.on("--destroy=ID", "Destroy service") { |id| args[:destroy] = id }
|
||||
opts.on("--execute=ID", "Execute command in service") { |id| args[:execute] = id }
|
||||
opts.on("--execute=ID", "Execute command in service/session") { |id| args[:execute] = id }
|
||||
opts.on("--command=CMD", "Command to execute (with --execute)") { |cmd| args[:command] = cmd }
|
||||
opts.on("--dump-bootstrap=ID", "Dump bootstrap script") { |id| args[:dump_bootstrap] = id }
|
||||
opts.on("--dump-file=FILE", "File to save bootstrap (with --dump-bootstrap)") { |file| args[:dump_file] = file }
|
||||
opts.on("--resize=ID", "Resize service vCPU") { |id| args[:resize] = id }
|
||||
opts.on("-v VCPU", "--vcpu=VCPU", "vCPU count (1-8) for resize") { |v| args[:vcpu] = v.to_i }
|
||||
opts.on("--name=NAME", "Service name") { |n| args[:name] = n }
|
||||
opts.on("-v VCPU", "--vcpu=VCPU", "vCPU count (1-8) for resize/boost") { |v| args[:vcpu] = v.to_i }
|
||||
opts.on("--name=NAME", "Service/snapshot name") { |n| args[:name] = n }
|
||||
opts.on("--ports=PORTS", "Comma-separated ports") { |p| args[:ports] = p }
|
||||
opts.on("--domains=DOMAINS", "Comma-separated domains") { |d| args[:domains] = d }
|
||||
opts.on("--type=TYPE", "Service type for SRV records") { |t| args[:service_type] = t }
|
||||
|
|
@ -1154,12 +1563,99 @@ def main
|
|||
opts.on("--env-file=FILE", "Load env vars from file (for vault)") { |f| args[:svc_env_file] = f }
|
||||
opts.on("--extend", "Open browser to extend/renew key") { args[:extend] = true }
|
||||
opts.on("--json", "Output as JSON array (for languages command)") { args[:json] = true }
|
||||
opts.on("--shell=SHELL", "Shell for session (bash, python3, etc.)") { |s| args[:shell] = s }
|
||||
opts.on("--lock=ID", "Lock service/snapshot/image") { |id| args[:service_lock] = id }
|
||||
opts.on("--unlock=ID", "Unlock service/snapshot/image") { |id| args[:service_unlock] = id }
|
||||
opts.on("--redeploy=ID", "Redeploy service") { |id| args[:redeploy] = id }
|
||||
opts.on("--boost=ID", "Boost session vCPU") { |id| args[:session_boost] = id }
|
||||
opts.on("--unboost=ID", "Unboost session") { |id| args[:session_unboost] = id }
|
||||
# Snapshot options
|
||||
opts.on("--snapshot-session=ID", "Create snapshot from session") { |id| args[:snapshot_session] = id }
|
||||
opts.on("--snapshot-service=ID", "Create snapshot from service") { |id| args[:snapshot_service] = id }
|
||||
opts.on("--restore=ID", "Restore snapshot") { |id| args[:snapshot_restore] = id }
|
||||
opts.on("--delete=ID", "Delete snapshot") { |id| args[:snapshot_delete] = id }
|
||||
opts.on("--clone=ID", "Clone snapshot") { |id| args[:snapshot_clone] = id }
|
||||
opts.on("--clone-type=TYPE", "Clone type (session or service)") { |t| args[:snapshot_clone_type] = t }
|
||||
opts.on("--hot", "Create hot snapshot") { args[:snapshot_hot] = true }
|
||||
# Logs options
|
||||
opts.on("--source=SOURCE", "Log source (all, api, portal, pool/cammy, pool/ai)") { |s| args[:logs_source] = s }
|
||||
opts.on("--lines=N", "Number of log lines") { |n| args[:logs_lines] = n.to_i }
|
||||
opts.on("--since=TIME", "Time window (1m, 5m, 1h, 1d)") { |t| args[:logs_since] = t }
|
||||
opts.on("--grep=PATTERN", "Filter pattern") { |p| args[:logs_grep] = p }
|
||||
opts.on("--follow", "Follow log stream") { args[:logs_follow] = true }
|
||||
# Image access options
|
||||
opts.on("--grant=ID", "Grant image access") { |id| args[:image_grant] = id }
|
||||
opts.on("--revoke=ID", "Revoke image access") { |id| args[:image_revoke] = id }
|
||||
opts.on("--trusted=ID", "List trusted keys for image") { |id| args[:image_trusted] = id }
|
||||
opts.on("--trusted-key=KEY", "API key to grant/revoke access") { |k| args[:image_trusted_key] = k }
|
||||
opts.on("--transfer=ID", "Transfer image ownership") { |id| args[:image_transfer] = id }
|
||||
opts.on("--to-key=KEY", "Target API key for transfer") { |k| args[:image_to_key] = k }
|
||||
|
||||
opts.unknown_args do |before, after|
|
||||
if before.size > 0
|
||||
case before[0]
|
||||
when "session"
|
||||
args[:command] = "session"
|
||||
# Parse session subcommand options
|
||||
i = 1
|
||||
while i < before.size
|
||||
case before[i]
|
||||
when "--list", "-l"
|
||||
args[:list] = true
|
||||
i += 1
|
||||
when "--info"
|
||||
if i + 1 < before.size
|
||||
args[:session_info] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--freeze"
|
||||
if i + 1 < before.size
|
||||
args[:session_freeze] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--unfreeze"
|
||||
if i + 1 < before.size
|
||||
args[:session_unfreeze] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--boost"
|
||||
if i + 1 < before.size
|
||||
args[:session_boost] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--unboost"
|
||||
if i + 1 < before.size
|
||||
args[:session_unboost] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--execute"
|
||||
if i + 1 < before.size
|
||||
args[:session_execute] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--command"
|
||||
if i + 1 < before.size
|
||||
args[:command] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
when "service"
|
||||
args[:command] = "service"
|
||||
# Check for env subcommand
|
||||
|
|
@ -1180,11 +1676,179 @@ def main
|
|||
i += 1
|
||||
end
|
||||
end
|
||||
else
|
||||
# Parse service subcommand options
|
||||
i = 1
|
||||
while i < before.size
|
||||
case before[i]
|
||||
when "--lock"
|
||||
if i + 1 < before.size
|
||||
args[:service_lock] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--unlock"
|
||||
if i + 1 < before.size
|
||||
args[:service_unlock] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--redeploy"
|
||||
if i + 1 < before.size
|
||||
args[:redeploy] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
when "key"
|
||||
args[:command] = "key"
|
||||
when "languages"
|
||||
args[:command] = "languages"
|
||||
when "snapshot"
|
||||
args[:command] = "snapshot"
|
||||
# Parse snapshot subcommand options
|
||||
i = 1
|
||||
while i < before.size
|
||||
case before[i]
|
||||
when "--list", "-l"
|
||||
args[:list] = true
|
||||
i += 1
|
||||
when "--info"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_info] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--session"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_session] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--service"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_service] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--restore"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_restore] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--delete"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_delete] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--lock"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_lock] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--unlock"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_unlock] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--clone"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_clone] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--clone-type"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_clone_type] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--name"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_name] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--hot"
|
||||
args[:snapshot_hot] = true
|
||||
i += 1
|
||||
when "--ports"
|
||||
if i + 1 < before.size
|
||||
args[:snapshot_ports] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
when "logs"
|
||||
args[:command] = "logs"
|
||||
# Parse logs subcommand options
|
||||
i = 1
|
||||
while i < before.size
|
||||
case before[i]
|
||||
when "--source"
|
||||
if i + 1 < before.size
|
||||
args[:logs_source] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--lines"
|
||||
if i + 1 < before.size
|
||||
args[:logs_lines] = before[i + 1].to_i
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--since"
|
||||
if i + 1 < before.size
|
||||
args[:logs_since] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--grep"
|
||||
if i + 1 < before.size
|
||||
args[:logs_grep] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--follow", "-f"
|
||||
args[:logs_follow] = true
|
||||
i += 1
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
when "health"
|
||||
args[:command] = "health"
|
||||
when "version"
|
||||
args[:command] = "version"
|
||||
when "image"
|
||||
args[:command] = "image"
|
||||
# Parse image subcommand options
|
||||
|
|
@ -1272,6 +1936,48 @@ def main
|
|||
else
|
||||
i += 1
|
||||
end
|
||||
when "--grant"
|
||||
if i + 1 < before.size
|
||||
args[:image_grant] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--revoke"
|
||||
if i + 1 < before.size
|
||||
args[:image_revoke] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--trusted"
|
||||
if i + 1 < before.size
|
||||
args[:image_trusted] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--trusted-key"
|
||||
if i + 1 < before.size
|
||||
args[:image_trusted_key] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--transfer"
|
||||
if i + 1 < before.size
|
||||
args[:image_transfer] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
when "--to-key"
|
||||
if i + 1 < before.size
|
||||
args[:image_to_key] = before[i + 1]
|
||||
i += 2
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
else
|
||||
i += 1
|
||||
end
|
||||
|
|
@ -1300,6 +2006,14 @@ def main
|
|||
cmd_languages(args)
|
||||
elsif args[:command] == "image"
|
||||
cmd_image(args)
|
||||
elsif args[:command] == "snapshot"
|
||||
cmd_snapshot(args)
|
||||
elsif args[:command] == "logs"
|
||||
cmd_logs(args)
|
||||
elsif args[:command] == "health"
|
||||
cmd_health(args)
|
||||
elsif args[:command] == "version"
|
||||
cmd_version(args)
|
||||
elsif args[:source_file]
|
||||
cmd_execute(args)
|
||||
else
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
228
clients/csharp/tests/UnsandboxTests.cs
Normal file
228
clients/csharp/tests/UnsandboxTests.cs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Unit and Functional Tests for Unsandbox C# SDK (Mono)
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the Unsandbox SDK library functions.
|
||||
/// These tests verify that exported library functions work correctly.
|
||||
/// </summary>
|
||||
public class UnitTests
|
||||
{
|
||||
public static void Run()
|
||||
{
|
||||
Console.WriteLine("=== Unsandbox C# SDK Unit Tests ===\n");
|
||||
|
||||
TestDetectLanguage();
|
||||
TestHmacSign();
|
||||
TestExtensionMap();
|
||||
TestBuildEnvContent();
|
||||
|
||||
Console.WriteLine("\n=== Unit Tests Complete ===");
|
||||
}
|
||||
|
||||
static void TestDetectLanguage()
|
||||
{
|
||||
Console.Write("DetectLanguage: ");
|
||||
var tests = new Dictionary<string, string>
|
||||
{
|
||||
{ "test.py", "python" },
|
||||
{ "script.js", "javascript" },
|
||||
{ "main.go", "go" },
|
||||
{ "app.rs", "rust" },
|
||||
{ "Program.cs", "csharp" },
|
||||
{ "Module.fs", "fsharp" },
|
||||
{ "script.ps1", "powershell" }
|
||||
};
|
||||
|
||||
int passed = 0;
|
||||
foreach (var test in tests)
|
||||
{
|
||||
var result = Unsandbox.DetectLanguage(test.Key);
|
||||
if (result == test.Value) passed++;
|
||||
else Console.Write($"[FAIL: {test.Key} -> {result}, expected {test.Value}] ");
|
||||
}
|
||||
|
||||
if (passed == tests.Count)
|
||||
Console.WriteLine($"PASS ({passed}/{tests.Count})");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({passed}/{tests.Count})");
|
||||
}
|
||||
|
||||
static void TestHmacSign()
|
||||
{
|
||||
Console.Write("HmacSign: ");
|
||||
// Test vector: HMAC-SHA256("key", "message")
|
||||
var result = Unsandbox.HmacSign("key", "message");
|
||||
// Expected: 6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a
|
||||
var expected = "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a";
|
||||
if (result == expected)
|
||||
Console.WriteLine("PASS");
|
||||
else
|
||||
Console.WriteLine($"FAIL (got {result}, expected {expected})");
|
||||
}
|
||||
|
||||
static void TestExtensionMap()
|
||||
{
|
||||
Console.Write("ExtensionMap: ");
|
||||
// Test that the extension map contains expected entries
|
||||
var tests = new Dictionary<string, string>
|
||||
{
|
||||
{ ".py", "python" },
|
||||
{ ".js", "javascript" },
|
||||
{ ".go", "go" },
|
||||
{ ".rs", "rust" },
|
||||
{ ".cs", "csharp" }
|
||||
};
|
||||
|
||||
int passed = 0;
|
||||
foreach (var test in tests)
|
||||
{
|
||||
if (Unsandbox.ExtMap.TryGetValue(test.Key, out var lang) && lang == test.Value)
|
||||
passed++;
|
||||
}
|
||||
|
||||
if (passed == tests.Count)
|
||||
Console.WriteLine($"PASS ({passed}/{tests.Count})");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({passed}/{tests.Count})");
|
||||
}
|
||||
|
||||
static void TestBuildEnvContent()
|
||||
{
|
||||
Console.Write("BuildEnvContent: ");
|
||||
var envs = new List<string> { "KEY1=value1", "KEY2=value2" };
|
||||
var result = Unsandbox.BuildEnvContent(envs, null);
|
||||
var hasKey1 = result.Contains("KEY1=value1");
|
||||
var hasKey2 = result.Contains("KEY2=value2");
|
||||
if (hasKey1 && hasKey2)
|
||||
Console.WriteLine("PASS");
|
||||
else
|
||||
Console.WriteLine($"FAIL (got: {result})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Functional tests that require API credentials.
|
||||
/// Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables.
|
||||
/// </summary>
|
||||
public class FunctionalTests
|
||||
{
|
||||
public static void Run()
|
||||
{
|
||||
var publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY");
|
||||
var secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
Console.WriteLine("=== Functional Tests Skipped (no API credentials) ===");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("=== Unsandbox C# SDK Functional Tests ===\n");
|
||||
|
||||
TestHealthCheck();
|
||||
TestValidateKeys();
|
||||
TestGetLanguages();
|
||||
TestExecute();
|
||||
TestSessionList();
|
||||
TestServiceList();
|
||||
TestSnapshotList();
|
||||
TestImageList();
|
||||
|
||||
Console.WriteLine("\n=== Functional Tests Complete ===");
|
||||
}
|
||||
|
||||
static void TestHealthCheck()
|
||||
{
|
||||
Console.Write("HealthCheck: ");
|
||||
var result = Unsandbox.HealthCheck();
|
||||
Console.WriteLine(result ? "PASS" : "FAIL");
|
||||
}
|
||||
|
||||
static void TestValidateKeys()
|
||||
{
|
||||
Console.Write("ValidateKeys: ");
|
||||
var result = Unsandbox.ValidateKeys();
|
||||
if (result != null && result.Valid)
|
||||
Console.WriteLine($"PASS (tier: {result.Tier})");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({Unsandbox.LastError()})");
|
||||
}
|
||||
|
||||
static void TestGetLanguages()
|
||||
{
|
||||
Console.Write("GetLanguages: ");
|
||||
var result = Unsandbox.GetLanguages();
|
||||
if (result.Count > 0)
|
||||
Console.WriteLine($"PASS ({result.Count} languages)");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({Unsandbox.LastError()})");
|
||||
}
|
||||
|
||||
static void TestExecute()
|
||||
{
|
||||
Console.Write("Execute: ");
|
||||
var result = Unsandbox.Execute("python", "print('hello from C# SDK')");
|
||||
if (result.Success && result.Stdout != null && result.Stdout.Contains("hello"))
|
||||
Console.WriteLine("PASS");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({result.ErrorMessage ?? Unsandbox.LastError()})");
|
||||
}
|
||||
|
||||
static void TestSessionList()
|
||||
{
|
||||
Console.Write("SessionList: ");
|
||||
var result = Unsandbox.SessionList();
|
||||
// Empty list is valid - just checking API call works
|
||||
Console.WriteLine($"PASS ({result.Count} sessions)");
|
||||
}
|
||||
|
||||
static void TestServiceList()
|
||||
{
|
||||
Console.Write("ServiceList: ");
|
||||
var result = Unsandbox.ServiceList();
|
||||
Console.WriteLine($"PASS ({result.Count} services)");
|
||||
}
|
||||
|
||||
static void TestSnapshotList()
|
||||
{
|
||||
Console.Write("SnapshotList: ");
|
||||
var result = Unsandbox.SnapshotList();
|
||||
Console.WriteLine($"PASS ({result.Count} snapshots)");
|
||||
}
|
||||
|
||||
static void TestImageList()
|
||||
{
|
||||
Console.Write("ImageList: ");
|
||||
var result = Unsandbox.ImageList();
|
||||
Console.WriteLine($"PASS ({result.Count} images)");
|
||||
}
|
||||
}
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Unsandbox C# SDK Tests (Mono)");
|
||||
Console.WriteLine("=============================\n");
|
||||
|
||||
UnitTests.Run();
|
||||
Console.WriteLine();
|
||||
FunctionalTests.Run();
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Test error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -439,6 +439,473 @@ string extractJsonField(string response, string field) {
|
|||
return "";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Library Functions for D SDK (matching C reference un.h)
|
||||
// ============================================================================
|
||||
|
||||
immutable string SDK_VERSION = "4.2.0";
|
||||
|
||||
// Execute code synchronously
|
||||
string execute(string language, string code, string publicKey, string secretKey) {
|
||||
string body_ = format(`{"language":"%s","code":"%s"}`, escapeJson(language), escapeJson(code));
|
||||
string authHeaders = buildAuthHeaders("POST", "/execute", body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/execute' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Execute code asynchronously (returns job_id)
|
||||
string executeAsync(string language, string code, string publicKey, string secretKey) {
|
||||
string body_ = format(`{"language":"%s","code":"%s","async":true}`, escapeJson(language), escapeJson(code));
|
||||
string authHeaders = buildAuthHeaders("POST", "/execute", body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/execute' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Get job status
|
||||
string getJob(string jobId, string publicKey, string secretKey) {
|
||||
string path = format("/jobs/%s", jobId);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Wait for job completion
|
||||
string waitForJob(string jobId, string publicKey, string secretKey) {
|
||||
import core.thread : Thread;
|
||||
import core.time : msecs;
|
||||
int[7] pollDelays = [300, 450, 700, 900, 650, 1600, 2000];
|
||||
int delayIdx = 0;
|
||||
|
||||
while (true) {
|
||||
string result = getJob(jobId, publicKey, secretKey);
|
||||
import std.algorithm : canFind;
|
||||
if (result.canFind(`"status":"completed"`) || result.canFind(`"status":"failed"`) ||
|
||||
result.canFind(`"status":"timeout"`) || result.canFind(`"status":"cancelled"`)) {
|
||||
return result;
|
||||
}
|
||||
Thread.sleep(msecs(pollDelays[delayIdx % 7]));
|
||||
if (delayIdx < 6) delayIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel a job
|
||||
string cancelJob(string jobId, string publicKey, string secretKey) {
|
||||
string path = format("/jobs/%s/cancel", jobId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// List all jobs
|
||||
string listJobs(string publicKey, string secretKey) {
|
||||
string authHeaders = buildAuthHeaders("GET", "/jobs", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/jobs' %s`, API_BASE, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Get supported languages
|
||||
string getLanguages(string publicKey, string secretKey) {
|
||||
string authHeaders = buildAuthHeaders("GET", "/languages", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/languages' %s`, API_BASE, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Session functions
|
||||
string sessionList(string publicKey, string secretKey) {
|
||||
string authHeaders = buildAuthHeaders("GET", "/sessions", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/sessions' %s`, API_BASE, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string sessionGet(string sessionId, string publicKey, string secretKey) {
|
||||
string path = format("/sessions/%s", sessionId);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string sessionCreate(string shell, string network, string publicKey, string secretKey) {
|
||||
string body_ = format(`{"shell":"%s"`, shell.empty ? "bash" : shell);
|
||||
if (!network.empty) body_ ~= format(`,"network":"%s"`, network);
|
||||
body_ ~= "}";
|
||||
string authHeaders = buildAuthHeaders("POST", "/sessions", body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string sessionDestroy(string sessionId, string publicKey, string secretKey) {
|
||||
string path = format("/sessions/%s", sessionId);
|
||||
string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X DELETE '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string sessionFreeze(string sessionId, string publicKey, string secretKey) {
|
||||
string path = format("/sessions/%s/freeze", sessionId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string sessionUnfreeze(string sessionId, string publicKey, string secretKey) {
|
||||
string path = format("/sessions/%s/unfreeze", sessionId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string sessionBoost(string sessionId, int vcpu, string publicKey, string secretKey) {
|
||||
string path = format("/sessions/%s/boost", sessionId);
|
||||
string body_ = vcpu > 0 ? format(`{"vcpu":%d}`, vcpu) : "{}";
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string sessionUnboost(string sessionId, string publicKey, string secretKey) {
|
||||
string path = format("/sessions/%s/unboost", sessionId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string sessionExecute(string sessionId, string command, string publicKey, string secretKey) {
|
||||
string path = format("/sessions/%s/shell", sessionId);
|
||||
string body_ = format(`{"command":"%s"}`, escapeJson(command));
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Service functions
|
||||
string serviceListFn(string publicKey, string secretKey) {
|
||||
string authHeaders = buildAuthHeaders("GET", "/services", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/services' %s`, API_BASE, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceGet(string serviceId, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s", serviceId);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceCreate(string name, string ports, string bootstrap, string network, string publicKey, string secretKey) {
|
||||
string body_ = format(`{"name":"%s"`, escapeJson(name));
|
||||
if (!ports.empty) body_ ~= format(`,"ports":"%s"`, ports);
|
||||
if (!bootstrap.empty) body_ ~= format(`,"bootstrap":"%s"`, escapeJson(bootstrap));
|
||||
if (!network.empty) body_ ~= format(`,"network":"%s"`, network);
|
||||
body_ ~= "}";
|
||||
string authHeaders = buildAuthHeaders("POST", "/services", body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/services' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceDestroy(string serviceId, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s", serviceId);
|
||||
string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X DELETE '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceFreeze(string serviceId, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/freeze", serviceId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceUnfreeze(string serviceId, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/unfreeze", serviceId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceLock(string serviceId, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/lock", serviceId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceUnlock(string serviceId, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/unlock", serviceId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceRedeploy(string serviceId, string bootstrap, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/redeploy", serviceId);
|
||||
string body_ = bootstrap.empty ? "{}" : format(`{"bootstrap":"%s"}`, escapeJson(bootstrap));
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceLogs(string serviceId, bool all, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/logs%s", serviceId, all ? "?all=true" : "");
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceExecute(string serviceId, string command, int timeoutMs, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/execute", serviceId);
|
||||
string body_ = format(`{"command":"%s"`, escapeJson(command));
|
||||
if (timeoutMs > 0) body_ ~= format(`,"timeout":%d`, timeoutMs);
|
||||
body_ ~= "}";
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string serviceResize(string serviceId, int vcpu, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/resize", serviceId);
|
||||
string body_ = format(`{"vcpu":%d}`, vcpu);
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Snapshot functions
|
||||
string snapshotList(string publicKey, string secretKey) {
|
||||
string authHeaders = buildAuthHeaders("GET", "/snapshots", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/snapshots' %s`, API_BASE, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string snapshotGet(string snapshotId, string publicKey, string secretKey) {
|
||||
string path = format("/snapshots/%s", snapshotId);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string snapshotSession(string sessionId, string name, bool hot, string publicKey, string secretKey) {
|
||||
string path = format("/sessions/%s/snapshot", sessionId);
|
||||
string body_ = "{";
|
||||
if (!name.empty) body_ ~= format(`"name":"%s",`, escapeJson(name));
|
||||
body_ ~= format(`"hot":%s}`, hot ? "true" : "false");
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string snapshotService(string serviceId, string name, bool hot, string publicKey, string secretKey) {
|
||||
string path = format("/services/%s/snapshot", serviceId);
|
||||
string body_ = "{";
|
||||
if (!name.empty) body_ ~= format(`"name":"%s",`, escapeJson(name));
|
||||
body_ ~= format(`"hot":%s}`, hot ? "true" : "false");
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string snapshotRestore(string snapshotId, string publicKey, string secretKey) {
|
||||
string path = format("/snapshots/%s/restore", snapshotId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string snapshotDelete(string snapshotId, string publicKey, string secretKey) {
|
||||
string path = format("/snapshots/%s", snapshotId);
|
||||
string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X DELETE '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string snapshotLock(string snapshotId, string publicKey, string secretKey) {
|
||||
string path = format("/snapshots/%s/lock", snapshotId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string snapshotUnlock(string snapshotId, string publicKey, string secretKey) {
|
||||
string path = format("/snapshots/%s/unlock", snapshotId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string snapshotClone(string snapshotId, string cloneType, string name, string ports, string shell, string publicKey, string secretKey) {
|
||||
string path = format("/snapshots/%s/clone", snapshotId);
|
||||
string body_ = format(`{"type":"%s"`, cloneType);
|
||||
if (!name.empty) body_ ~= format(`,"name":"%s"`, escapeJson(name));
|
||||
if (!ports.empty) body_ ~= format(`,"ports":"%s"`, ports);
|
||||
if (!shell.empty) body_ ~= format(`,"shell":"%s"`, shell);
|
||||
body_ ~= "}";
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Image functions
|
||||
string imageList(string filter, string publicKey, string secretKey) {
|
||||
string path = filter.empty ? "/images" : format("/images?filter=%s", filter);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageGetFn(string imageId, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s", imageId);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imagePublish(string sourceType, string sourceId, string name, string description, string publicKey, string secretKey) {
|
||||
string body_ = format(`{"source_type":"%s","source_id":"%s"`, sourceType, sourceId);
|
||||
if (!name.empty) body_ ~= format(`,"name":"%s"`, escapeJson(name));
|
||||
if (!description.empty) body_ ~= format(`,"description":"%s"`, escapeJson(description));
|
||||
body_ ~= "}";
|
||||
string authHeaders = buildAuthHeaders("POST", "/images", body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/images' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageDelete(string imageId, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s", imageId);
|
||||
string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X DELETE '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageLock(string imageId, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/lock", imageId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageUnlock(string imageId, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/unlock", imageId);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageSetVisibility(string imageId, string visibility, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/visibility", imageId);
|
||||
string body_ = format(`{"visibility":"%s"}`, visibility);
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageGrantAccess(string imageId, string trustedKey, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/grant", imageId);
|
||||
string body_ = format(`{"trusted_api_key":"%s"}`, trustedKey);
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageRevokeAccess(string imageId, string trustedKey, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/revoke", imageId);
|
||||
string body_ = format(`{"trusted_api_key":"%s"}`, trustedKey);
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageListTrusted(string imageId, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/trusted", imageId);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageTransfer(string imageId, string toApiKey, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/transfer", imageId);
|
||||
string body_ = format(`{"to_api_key":"%s"}`, toApiKey);
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageSpawn(string imageId, string name, string ports, string bootstrap, string network, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/spawn", imageId);
|
||||
string body_ = "{";
|
||||
string[] fields;
|
||||
if (!name.empty) fields ~= format(`"name":"%s"`, escapeJson(name));
|
||||
if (!ports.empty) fields ~= format(`"ports":"%s"`, ports);
|
||||
if (!bootstrap.empty) fields ~= format(`"bootstrap":"%s"`, escapeJson(bootstrap));
|
||||
if (!network.empty) fields ~= format(`"network":"%s"`, network);
|
||||
import std.array : join;
|
||||
body_ ~= fields.join(",") ~ "}";
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
string imageCloneFn(string imageId, string name, string description, string publicKey, string secretKey) {
|
||||
string path = format("/images/%s/clone", imageId);
|
||||
string body_ = "{";
|
||||
string[] fields;
|
||||
if (!name.empty) fields ~= format(`"name":"%s"`, escapeJson(name));
|
||||
if (!description.empty) fields ~= format(`"description":"%s"`, escapeJson(description));
|
||||
import std.array : join;
|
||||
body_ ~= fields.join(",") ~ "}";
|
||||
string authHeaders = buildAuthHeaders("POST", path, body_, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, path, authHeaders, body_);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// PaaS Logs functions
|
||||
string logsFetch(string source, int lines, string since, string grep, string publicKey, string secretKey) {
|
||||
string path = "/paas/logs?";
|
||||
if (!source.empty) path ~= format("source=%s&", source);
|
||||
if (lines > 0) path ~= format("lines=%d&", lines);
|
||||
if (!since.empty) path ~= format("since=%s&", since);
|
||||
if (!grep.empty) path ~= format("grep=%s&", grep);
|
||||
if (path[$-1] == '&' || path[$-1] == '?') path = path[0..$-1];
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s%s' %s`, API_BASE, path, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Key validation
|
||||
string validateKeysFn(string publicKey, string secretKey) {
|
||||
string authHeaders = buildAuthHeaders("POST", "/keys/validate", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/keys/validate' %s`, API_BASE, authHeaders);
|
||||
return execCurl(cmd);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
string hmacSign(string secretKey, string message) {
|
||||
return computeHmac(secretKey, message);
|
||||
}
|
||||
|
||||
bool healthCheck() {
|
||||
string cmd = format(`curl -s -o /dev/null -w '%%{http_code}' '%s/health' 2>/dev/null`, API_BASE);
|
||||
auto result = executeShell(cmd);
|
||||
try {
|
||||
return to!int(result.output.strip()) == 200;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string sdkVersion() {
|
||||
return SDK_VERSION;
|
||||
}
|
||||
|
||||
__gshared string lastErrorMsg;
|
||||
|
||||
void setLastError(string msg) {
|
||||
lastErrorMsg = msg;
|
||||
}
|
||||
|
||||
string lastError() {
|
||||
return lastErrorMsg;
|
||||
}
|
||||
|
||||
void cmdServiceEnv(string action, string target, string[] svcEnvs, string svcEnvFile, string publicKey, string secretKey) {
|
||||
if (action == "status") {
|
||||
if (target.empty) {
|
||||
|
|
|
|||
260
clients/d/sync/tests/test_un.d
Normal file
260
clients/d/sync/tests/test_un.d
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
// Tests for the D unsandbox SDK
|
||||
// Compile: dmd -unittest -main test_un.d ../src/un.d -of=test_un
|
||||
// Run: ./test_un
|
||||
|
||||
import std.stdio;
|
||||
import std.process : environment;
|
||||
import std.algorithm : canFind;
|
||||
|
||||
// Note: In D, unittest blocks are automatically discovered and run
|
||||
// when compiling with -unittest flag
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests - Test exported library functions
|
||||
// ============================================================================
|
||||
|
||||
unittest {
|
||||
writeln("Testing detectLanguage...");
|
||||
assert(detectLanguage("script.py") == "python");
|
||||
assert(detectLanguage("script.js") == "javascript");
|
||||
assert(detectLanguage("script.ts") == "typescript");
|
||||
assert(detectLanguage("script.go") == "go");
|
||||
assert(detectLanguage("script.rs") == "rust");
|
||||
assert(detectLanguage("script.c") == "c");
|
||||
assert(detectLanguage("script.cpp") == "cpp");
|
||||
assert(detectLanguage("script.d") == "d");
|
||||
assert(detectLanguage("script.zig") == "zig");
|
||||
assert(detectLanguage("script.sh") == "bash");
|
||||
assert(detectLanguage("script.rb") == "ruby");
|
||||
assert(detectLanguage("script.php") == "php");
|
||||
assert(detectLanguage("script.unknown") == "");
|
||||
assert(detectLanguage("script") == "");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing hmacSign...");
|
||||
string secretKey = "test-secret";
|
||||
string message = "test-message";
|
||||
|
||||
string result = hmacSign(secretKey, message);
|
||||
|
||||
// Should return a 64-character hex string
|
||||
assert(result.length == 64, "HMAC signature should be 64 characters");
|
||||
|
||||
// Should be deterministic
|
||||
string result2 = hmacSign(secretKey, message);
|
||||
assert(result == result2, "HMAC sign should be deterministic");
|
||||
|
||||
// Different inputs should produce different outputs
|
||||
string result3 = hmacSign(secretKey, "different-message");
|
||||
assert(result != result3, "Different inputs should produce different signatures");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing sdkVersion...");
|
||||
string v = sdkVersion();
|
||||
assert(v.length > 0, "Version should not be empty");
|
||||
// Should be in semver format (at least "0.0.0")
|
||||
assert(v.length >= 5, "Version should be in semver format");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing lastError...");
|
||||
// Set an error
|
||||
setLastError("test error message");
|
||||
|
||||
// Retrieve it
|
||||
string err = lastError();
|
||||
assert(err == "test error message");
|
||||
|
||||
// Clear it
|
||||
setLastError("");
|
||||
err = lastError();
|
||||
assert(err == "", "Error should be cleared");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing escapeJson...");
|
||||
assert(escapeJson("hello") == "hello");
|
||||
assert(escapeJson("hello\"world") == "hello\\\"world");
|
||||
assert(escapeJson("line1\nline2") == "line1\\nline2");
|
||||
assert(escapeJson("tab\there") == "tab\\there");
|
||||
assert(escapeJson("back\\slash") == "back\\\\slash");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests - Test SDK internal consistency
|
||||
// ============================================================================
|
||||
|
||||
unittest {
|
||||
writeln("Testing computeHmac...");
|
||||
string key = "test-key";
|
||||
string msg = "test-message";
|
||||
|
||||
string sig1 = computeHmac(key, msg);
|
||||
string sig2 = computeHmac(key, msg);
|
||||
|
||||
// Should be deterministic
|
||||
assert(sig1 == sig2, "HMAC should be deterministic");
|
||||
|
||||
// Should produce different results for different inputs
|
||||
string sig3 = computeHmac(key, "different");
|
||||
assert(sig1 != sig3, "Different inputs should produce different signatures");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing buildAuthHeaders...");
|
||||
string pk = "unsb-pk-test-test-test-test";
|
||||
string sk = "unsb-sk-test1-test2-test3-test4";
|
||||
|
||||
string headers = buildAuthHeaders("POST", "/execute", "{}", pk, sk);
|
||||
|
||||
// Should contain auth header
|
||||
assert(headers.canFind("Authorization: Bearer " ~ pk));
|
||||
// Should contain timestamp header
|
||||
assert(headers.canFind("X-Timestamp:"));
|
||||
// Should contain signature header
|
||||
assert(headers.canFind("X-Signature:"));
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Functional Tests - Test against real API (requires credentials)
|
||||
// ============================================================================
|
||||
|
||||
bool hasCredentials() {
|
||||
string pk = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string sk = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
return pk.length > 0 && sk.length > 0;
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing healthCheck (functional)...");
|
||||
if (!hasCredentials()) {
|
||||
writeln(" SKIP (no credentials)");
|
||||
return;
|
||||
}
|
||||
|
||||
bool healthy = healthCheck();
|
||||
writeln(" Health check result: ", healthy ? "healthy" : "unhealthy");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing getLanguages (functional)...");
|
||||
if (!hasCredentials()) {
|
||||
writeln(" SKIP (no credentials)");
|
||||
return;
|
||||
}
|
||||
|
||||
string pk = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string sk = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
|
||||
string result = getLanguages(pk, sk);
|
||||
assert(result.length > 0, "Languages result should not be empty");
|
||||
// Should contain python
|
||||
assert(result.canFind("python"), "Languages should include python");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing validateKeysFn (functional)...");
|
||||
if (!hasCredentials()) {
|
||||
writeln(" SKIP (no credentials)");
|
||||
return;
|
||||
}
|
||||
|
||||
string pk = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string sk = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
|
||||
string result = validateKeysFn(pk, sk);
|
||||
assert(result.length > 0, "Validate keys result should not be empty");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing execute (functional)...");
|
||||
if (!hasCredentials()) {
|
||||
writeln(" SKIP (no credentials)");
|
||||
return;
|
||||
}
|
||||
|
||||
string pk = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string sk = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
|
||||
string result = execute("python", "print('hello from d test')", pk, sk);
|
||||
assert(result.length > 0, "Execute result should not be empty");
|
||||
// Should contain output
|
||||
assert(result.canFind("stdout") || result.canFind("output"), "Result should contain output");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing sessionList (functional)...");
|
||||
if (!hasCredentials()) {
|
||||
writeln(" SKIP (no credentials)");
|
||||
return;
|
||||
}
|
||||
|
||||
string pk = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string sk = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
|
||||
string result = sessionList(pk, sk);
|
||||
assert(result.length > 0, "Session list result should not be empty");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing serviceListFn (functional)...");
|
||||
if (!hasCredentials()) {
|
||||
writeln(" SKIP (no credentials)");
|
||||
return;
|
||||
}
|
||||
|
||||
string pk = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string sk = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
|
||||
string result = serviceListFn(pk, sk);
|
||||
assert(result.length > 0, "Service list result should not be empty");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing snapshotList (functional)...");
|
||||
if (!hasCredentials()) {
|
||||
writeln(" SKIP (no credentials)");
|
||||
return;
|
||||
}
|
||||
|
||||
string pk = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string sk = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
|
||||
string result = snapshotList(pk, sk);
|
||||
assert(result.length > 0, "Snapshot list result should not be empty");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
unittest {
|
||||
writeln("Testing imageList (functional)...");
|
||||
if (!hasCredentials()) {
|
||||
writeln(" SKIP (no credentials)");
|
||||
return;
|
||||
}
|
||||
|
||||
string pk = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string sk = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
|
||||
string result = imageList("", pk, sk);
|
||||
assert(result.length > 0, "Image list result should not be empty");
|
||||
writeln(" PASS");
|
||||
}
|
||||
|
||||
void main() {
|
||||
writeln("===== D SDK Tests Complete =====");
|
||||
}
|
||||
|
|
@ -124,6 +124,21 @@ class Args {
|
|||
String? imageClone;
|
||||
String? imageName;
|
||||
String? imagePorts;
|
||||
// Snapshot command options
|
||||
bool snapshotList = false;
|
||||
String? snapshotInfo;
|
||||
String? snapshotSession;
|
||||
String? snapshotService;
|
||||
String? snapshotRestore;
|
||||
String? snapshotDelete;
|
||||
String? snapshotLock;
|
||||
String? snapshotUnlock;
|
||||
String? snapshotClone;
|
||||
String? snapshotCloneType;
|
||||
String? snapshotName;
|
||||
String? snapshotPorts;
|
||||
String? snapshotShell;
|
||||
bool snapshotHot = false;
|
||||
}
|
||||
|
||||
List<String?> getApiKeys(String? argsKey) {
|
||||
|
|
@ -1074,6 +1089,235 @@ Future<void> cmdImage(Args args) async {
|
|||
exit(1);
|
||||
}
|
||||
|
||||
// Image access management functions
|
||||
Future<void> imageGrantAccess(String id, String trustedKey, String publicKey, String? secretKey) async {
|
||||
final payload = {'trusted_api_key': trustedKey};
|
||||
await apiRequestCurl('/images/$id/grant-access', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Access granted to: $trustedKey$reset');
|
||||
}
|
||||
|
||||
Future<void> imageRevokeAccess(String id, String trustedKey, String publicKey, String? secretKey) async {
|
||||
final payload = {'trusted_api_key': trustedKey};
|
||||
await apiRequestCurl('/images/$id/revoke-access', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Access revoked from: $trustedKey$reset');
|
||||
}
|
||||
|
||||
Future<void> imageListTrusted(String id, String publicKey, String? secretKey) async {
|
||||
final result = await apiRequestCurl('/images/$id/trusted', 'GET', null, publicKey, secretKey);
|
||||
print(jsonEncode(result));
|
||||
}
|
||||
|
||||
Future<void> imageTransfer(String id, String toKey, String publicKey, String? secretKey) async {
|
||||
final payload = {'to_api_key': toKey};
|
||||
await apiRequestCurl('/images/$id/transfer', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Image transferred to: $toKey$reset');
|
||||
}
|
||||
|
||||
// Snapshot functions
|
||||
Future<void> cmdSnapshot(Args args) async {
|
||||
final keys = getApiKeys(args.apiKey);
|
||||
final publicKey = keys[0]!;
|
||||
final secretKey = keys[1];
|
||||
|
||||
if (args.snapshotList) {
|
||||
final result = await apiRequestCurl('/snapshots', 'GET', null, publicKey, secretKey);
|
||||
print(jsonEncode(result));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.snapshotInfo != null) {
|
||||
final result = await apiRequestCurl('/snapshots/${args.snapshotInfo}', 'GET', null, publicKey, secretKey);
|
||||
print(jsonEncode(result));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.snapshotSession != null) {
|
||||
final payload = <String, dynamic>{
|
||||
'session_id': args.snapshotSession,
|
||||
};
|
||||
if (args.snapshotName != null) {
|
||||
payload['name'] = args.snapshotName;
|
||||
}
|
||||
if (args.snapshotHot) {
|
||||
payload['hot'] = true;
|
||||
}
|
||||
final result = await apiRequestCurl('/snapshots', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Snapshot created$reset');
|
||||
print(jsonEncode(result));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.snapshotService != null) {
|
||||
final payload = <String, dynamic>{
|
||||
'service_id': args.snapshotService,
|
||||
};
|
||||
if (args.snapshotName != null) {
|
||||
payload['name'] = args.snapshotName;
|
||||
}
|
||||
if (args.snapshotHot) {
|
||||
payload['hot'] = true;
|
||||
}
|
||||
final result = await apiRequestCurl('/snapshots', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Snapshot created$reset');
|
||||
print(jsonEncode(result));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.snapshotRestore != null) {
|
||||
await apiRequestCurl('/snapshots/${args.snapshotRestore}/restore', 'POST', '{}', publicKey, secretKey);
|
||||
print('${green}Snapshot restored: ${args.snapshotRestore}$reset');
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.snapshotDelete != null) {
|
||||
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/snapshots/${args.snapshotDelete}', 'DELETE', null, publicKey, secretKey);
|
||||
if (statusCode == 428) {
|
||||
if (await handleSudoChallenge(responseBody, '/snapshots/${args.snapshotDelete}', 'DELETE', null, publicKey, secretKey)) {
|
||||
print('${green}Snapshot deleted: ${args.snapshotDelete}$reset');
|
||||
} else {
|
||||
stderr.writeln('${red}Error: Failed to delete snapshot (OTP verification failed)$reset');
|
||||
exit(1);
|
||||
}
|
||||
} else if (statusCode >= 200 && statusCode < 300) {
|
||||
print('${green}Snapshot deleted: ${args.snapshotDelete}$reset');
|
||||
} else {
|
||||
stderr.writeln('${red}Error: Failed to delete snapshot (HTTP $statusCode)$reset');
|
||||
exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.snapshotLock != null) {
|
||||
await apiRequestCurl('/snapshots/${args.snapshotLock}/lock', 'POST', '{}', publicKey, secretKey);
|
||||
print('${green}Snapshot locked: ${args.snapshotLock}$reset');
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.snapshotUnlock != null) {
|
||||
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/snapshots/${args.snapshotUnlock}/unlock', 'POST', '{}', publicKey, secretKey);
|
||||
if (statusCode == 428) {
|
||||
if (await handleSudoChallenge(responseBody, '/snapshots/${args.snapshotUnlock}/unlock', 'POST', '{}', publicKey, secretKey)) {
|
||||
print('${green}Snapshot unlocked: ${args.snapshotUnlock}$reset');
|
||||
} else {
|
||||
stderr.writeln('${red}Error: Failed to unlock snapshot (OTP verification failed)$reset');
|
||||
exit(1);
|
||||
}
|
||||
} else if (statusCode >= 200 && statusCode < 300) {
|
||||
print('${green}Snapshot unlocked: ${args.snapshotUnlock}$reset');
|
||||
} else {
|
||||
stderr.writeln('${red}Error: Failed to unlock snapshot (HTTP $statusCode)$reset');
|
||||
exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.snapshotClone != null) {
|
||||
final payload = <String, dynamic>{
|
||||
'clone_type': args.snapshotCloneType ?? 'session',
|
||||
};
|
||||
if (args.snapshotName != null) {
|
||||
payload['name'] = args.snapshotName;
|
||||
}
|
||||
if (args.snapshotPorts != null) {
|
||||
payload['ports'] = args.snapshotPorts!.split(',').map((p) => int.parse(p.trim())).toList();
|
||||
}
|
||||
if (args.snapshotShell != null) {
|
||||
payload['shell'] = args.snapshotShell;
|
||||
}
|
||||
final result = await apiRequestCurl('/snapshots/${args.snapshotClone}/clone', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Snapshot cloned$reset');
|
||||
print(jsonEncode(result));
|
||||
return;
|
||||
}
|
||||
|
||||
stderr.writeln('${red}Error: Use --list, --info ID, --session ID, --service ID, --restore ID, --delete ID, --lock ID, --unlock ID, or --clone ID$reset');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Session additional functions
|
||||
Future<void> sessionInfo(String id, String publicKey, String? secretKey) async {
|
||||
final result = await apiRequestCurl('/sessions/$id', 'GET', null, publicKey, secretKey);
|
||||
print(jsonEncode(result));
|
||||
}
|
||||
|
||||
Future<void> sessionBoost(String id, int vcpu, String publicKey, String? secretKey) async {
|
||||
final payload = {'vcpu': vcpu};
|
||||
await apiRequestCurl('/sessions/$id', 'PATCH', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Session boosted to $vcpu vCPU$reset');
|
||||
}
|
||||
|
||||
Future<void> sessionUnboost(String id, String publicKey, String? secretKey) async {
|
||||
final payload = {'vcpu': 1};
|
||||
await apiRequestCurl('/sessions/$id', 'PATCH', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Session unboosted to 1 vCPU$reset');
|
||||
}
|
||||
|
||||
Future<void> sessionExecuteCmd(String id, String command, String publicKey, String? secretKey) async {
|
||||
final payload = {'command': command};
|
||||
final result = await apiRequestCurl('/sessions/$id/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
final stdoutText = result['stdout'] as String?;
|
||||
final stderrText = result['stderr'] as String?;
|
||||
if (stdoutText != null && stdoutText.isNotEmpty) {
|
||||
stdout.write('$blue$stdoutText$reset');
|
||||
}
|
||||
if (stderrText != null && stderrText.isNotEmpty) {
|
||||
stderr.write('$red$stderrText$reset');
|
||||
}
|
||||
}
|
||||
|
||||
// Service additional functions
|
||||
Future<void> serviceLock(String id, String publicKey, String? secretKey) async {
|
||||
await apiRequestCurl('/services/$id/lock', 'POST', '{}', publicKey, secretKey);
|
||||
print('${green}Service locked: $id$reset');
|
||||
}
|
||||
|
||||
Future<void> serviceUnlock(String id, String publicKey, String? secretKey) async {
|
||||
final (statusCode, responseBody) = await apiRequestCurlWithStatus('/services/$id/unlock', 'POST', '{}', publicKey, secretKey);
|
||||
if (statusCode == 428) {
|
||||
if (await handleSudoChallenge(responseBody, '/services/$id/unlock', 'POST', '{}', publicKey, secretKey)) {
|
||||
print('${green}Service unlocked: $id$reset');
|
||||
} else {
|
||||
stderr.writeln('${red}Error: Failed to unlock service (OTP verification failed)$reset');
|
||||
exit(1);
|
||||
}
|
||||
} else if (statusCode >= 200 && statusCode < 300) {
|
||||
print('${green}Service unlocked: $id$reset');
|
||||
} else {
|
||||
stderr.writeln('${red}Error: Failed to unlock service (HTTP $statusCode)$reset');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> serviceRedeploy(String id, String? bootstrap, String publicKey, String? secretKey) async {
|
||||
final payload = bootstrap != null ? {'bootstrap': bootstrap} : <String, dynamic>{};
|
||||
await apiRequestCurl('/services/$id/redeploy', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Service redeploying: $id$reset');
|
||||
}
|
||||
|
||||
// PaaS logs functions
|
||||
Future<void> logsFetch(String source, int lines, String? since, String? grepPattern, String publicKey, String? secretKey) async {
|
||||
var params = '?source=$source&lines=$lines';
|
||||
if (since != null) params += '&since=$since';
|
||||
if (grepPattern != null) params += '&grep=${Uri.encodeComponent(grepPattern)}';
|
||||
final result = await apiRequestCurl('/logs$params', 'GET', null, publicKey, secretKey);
|
||||
print(jsonEncode(result));
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
Future<bool> healthCheck() async {
|
||||
try {
|
||||
final result = await Process.run('curl', ['-s', 'https://api.unsandbox.com/health']);
|
||||
print(result.stdout);
|
||||
return result.stdout.toString().contains('ok');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String sdkVersion() {
|
||||
return '4.2.0';
|
||||
}
|
||||
|
||||
Future<void> cmdKey(Args args) async {
|
||||
final keys = getApiKeys(args.apiKey);
|
||||
final publicKey = keys[0]!;
|
||||
|
|
@ -1146,6 +1390,9 @@ Args parseArgs(List<String> argv) {
|
|||
case 'image':
|
||||
args.command = 'image';
|
||||
break;
|
||||
case 'snapshot':
|
||||
args.command = 'snapshot';
|
||||
break;
|
||||
case 'key':
|
||||
args.command = 'key';
|
||||
break;
|
||||
|
|
@ -1193,11 +1440,17 @@ Args parseArgs(List<String> argv) {
|
|||
args.serviceList = true;
|
||||
} else if (args.command == 'image') {
|
||||
args.imageList = true;
|
||||
} else if (args.command == 'snapshot') {
|
||||
args.snapshotList = true;
|
||||
}
|
||||
break;
|
||||
case '-s':
|
||||
case '--shell':
|
||||
if (args.command == 'snapshot') {
|
||||
args.snapshotShell = argv[++i];
|
||||
} else {
|
||||
args.sessionShell = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--kill':
|
||||
args.sessionKill = argv[++i];
|
||||
|
|
@ -1205,6 +1458,8 @@ Args parseArgs(List<String> argv) {
|
|||
case '--name':
|
||||
if (args.command == 'image') {
|
||||
args.imageName = argv[++i];
|
||||
} else if (args.command == 'snapshot') {
|
||||
args.snapshotName = argv[++i];
|
||||
} else {
|
||||
args.serviceName = argv[++i];
|
||||
}
|
||||
|
|
@ -1212,6 +1467,8 @@ Args parseArgs(List<String> argv) {
|
|||
case '--ports':
|
||||
if (args.command == 'image') {
|
||||
args.imagePorts = argv[++i];
|
||||
} else if (args.command == 'snapshot') {
|
||||
args.snapshotPorts = argv[++i];
|
||||
} else {
|
||||
args.servicePorts = argv[++i];
|
||||
}
|
||||
|
|
@ -1285,21 +1542,29 @@ Args parseArgs(List<String> argv) {
|
|||
args.serviceInfo = argv[++i];
|
||||
} else if (args.command == 'image') {
|
||||
args.imageInfo = argv[++i];
|
||||
} else if (args.command == 'snapshot') {
|
||||
args.snapshotInfo = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--delete':
|
||||
if (args.command == 'image') {
|
||||
args.imageDelete = argv[++i];
|
||||
} else if (args.command == 'snapshot') {
|
||||
args.snapshotDelete = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--lock':
|
||||
if (args.command == 'image') {
|
||||
args.imageLock = argv[++i];
|
||||
} else if (args.command == 'snapshot') {
|
||||
args.snapshotLock = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--unlock':
|
||||
if (args.command == 'image') {
|
||||
args.imageUnlock = argv[++i];
|
||||
} else if (args.command == 'snapshot') {
|
||||
args.snapshotUnlock = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--publish':
|
||||
|
|
@ -1326,6 +1591,33 @@ Args parseArgs(List<String> argv) {
|
|||
case '--clone':
|
||||
if (args.command == 'image') {
|
||||
args.imageClone = argv[++i];
|
||||
} else if (args.command == 'snapshot') {
|
||||
args.snapshotClone = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--session':
|
||||
if (args.command == 'snapshot') {
|
||||
args.snapshotSession = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--service':
|
||||
if (args.command == 'snapshot') {
|
||||
args.snapshotService = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--restore':
|
||||
if (args.command == 'snapshot') {
|
||||
args.snapshotRestore = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--clone-type':
|
||||
if (args.command == 'snapshot') {
|
||||
args.snapshotCloneType = argv[++i];
|
||||
}
|
||||
break;
|
||||
case '--hot':
|
||||
if (args.command == 'snapshot') {
|
||||
args.snapshotHot = true;
|
||||
}
|
||||
break;
|
||||
case 'env':
|
||||
|
|
@ -1338,7 +1630,7 @@ Args parseArgs(List<String> argv) {
|
|||
break;
|
||||
default:
|
||||
if (argv[i].startsWith('-')) {
|
||||
stderr.writeln('${RED}Unknown option: ${argv[i]}${RESET}');
|
||||
stderr.writeln('${red}Unknown option: ${argv[i]}$reset');
|
||||
exit(1);
|
||||
} else {
|
||||
args.sourceFile = argv[i];
|
||||
|
|
@ -1354,6 +1646,7 @@ void printHelp() {
|
|||
Usage: dart un.dart [options] <source_file>
|
||||
dart un.dart session [options]
|
||||
dart un.dart service [options]
|
||||
dart un.dart snapshot [options]
|
||||
dart un.dart image [options]
|
||||
dart un.dart key [options]
|
||||
dart un.dart languages [--json]
|
||||
|
|
@ -1416,6 +1709,22 @@ Image options:
|
|||
--name NAME Name for spawned service or cloned image
|
||||
--ports PORTS Ports for spawned service
|
||||
|
||||
Snapshot options:
|
||||
-l, --list List all snapshots
|
||||
--info ID Get snapshot details
|
||||
--session ID Create snapshot from session
|
||||
--service ID Create snapshot from service
|
||||
--restore ID Restore a snapshot
|
||||
--delete ID Delete a snapshot
|
||||
--lock ID Lock snapshot to prevent deletion
|
||||
--unlock ID Unlock snapshot
|
||||
--clone ID Clone snapshot to session/service
|
||||
--clone-type TYPE Clone target: session (default) or service
|
||||
--name NAME Name for new snapshot or cloned resource
|
||||
--ports PORTS Ports for service (with --clone --clone-type service)
|
||||
--shell NAME Shell for session (with --clone --clone-type session)
|
||||
--hot Hot snapshot (without stopping)
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend key
|
||||
|
||||
|
|
@ -1434,6 +1743,8 @@ void main(List<String> arguments) async {
|
|||
await cmdService(args);
|
||||
} else if (args.command == 'image') {
|
||||
await cmdImage(args);
|
||||
} else if (args.command == 'snapshot') {
|
||||
await cmdSnapshot(args);
|
||||
} else if (args.command == 'key') {
|
||||
await cmdKey(args);
|
||||
} else if (args.command == 'languages') {
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ void CmdService(Args args)
|
|||
if (args.ServiceShowFreezePage != null)
|
||||
{
|
||||
var payload = new Dictionary<string, object> { ["show_freeze_page"] = args.ServiceShowFreezePageEnabled };
|
||||
await ApiRequestAsync($"/services/{args.ServiceShowFreezePage}", new HttpMethod("PATCH"), payload, publicKey, secretKey);
|
||||
ApiRequest($"/services/{args.ServiceShowFreezePage}", new HttpMethod("PATCH"), payload, publicKey, secretKey);
|
||||
string status = args.ServiceShowFreezePageEnabled ? "enabled" : "disabled";
|
||||
Console.WriteLine($"{GREEN}Show-freeze-page {status} for service: {args.ServiceShowFreezePage}{RESET}");
|
||||
return;
|
||||
|
|
@ -563,6 +563,14 @@ void CmdSnapshot(Args args)
|
|||
return;
|
||||
}
|
||||
|
||||
if (args.SnapshotRestore != null)
|
||||
{
|
||||
var result = ApiRequest($"/snapshots/{args.SnapshotRestore}/restore", HttpMethod.Post, null, publicKey, secretKey);
|
||||
var id = result.TryGetValue("id", out var idObj) && idObj is JsonElement idEl ? idEl.GetString() : "unknown";
|
||||
Console.WriteLine($"{GREEN}Restored from snapshot: {id}{RESET}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.SnapshotClone != null)
|
||||
{
|
||||
var payload = new Dictionary<string, object> { ["type"] = args.SnapshotCloneType ?? "session" };
|
||||
|
|
@ -577,7 +585,7 @@ void CmdSnapshot(Args args)
|
|||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"{RED}Error: Use --list, --info, --delete, --lock, --unlock, or --clone{RESET}");
|
||||
Console.Error.WriteLine($"{RED}Error: Use --list, --info, --delete, --lock, --unlock, --restore, or --clone{RESET}");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
|
|
@ -1013,6 +1021,7 @@ Args ParseArgs(string[] args)
|
|||
else if (result.Command == "image") result.ImageClone = val;
|
||||
}
|
||||
else if (arg == "--clone-type") result.SnapshotCloneType = args[++i];
|
||||
else if (arg == "--restore" && result.Command == "snapshot") result.SnapshotRestore = args[++i];
|
||||
else if (arg == "--publish") result.ImagePublish = args[++i];
|
||||
else if (arg == "--source-type") result.ImageSourceType = args[++i];
|
||||
else if (arg == "--visibility")
|
||||
|
|
@ -1131,6 +1140,881 @@ Environment:
|
|||
UNSANDBOX_SECRET_KEY Your secret API key");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Library API - For embedding in other .NET applications
|
||||
// =============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Unsandbox SDK for .NET - Full library API matching the C reference implementation
|
||||
/// </summary>
|
||||
public static class Unsandbox
|
||||
{
|
||||
private static readonly HttpClient _httpClient = new() { BaseAddress = new Uri("https://api.unsandbox.com"), Timeout = TimeSpan.FromMinutes(5) };
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
private static string? _lastError;
|
||||
|
||||
// --- Execution Functions (8) ---
|
||||
|
||||
/// <summary>Execute code synchronously</summary>
|
||||
public static ExecuteResult Execute(string language, string code, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["language"] = language, ["code"] = code };
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/execute", HttpMethod.Post, payload, pk, sk);
|
||||
return new ExecuteResult
|
||||
{
|
||||
Stdout = GetString(result, "stdout"),
|
||||
Stderr = GetString(result, "stderr"),
|
||||
ExitCode = GetInt(result, "exit_code"),
|
||||
Language = language,
|
||||
ExecutionTime = GetDouble(result, "execution_time"),
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return new ExecuteResult { Success = false, ErrorMessage = ex.Message }; }
|
||||
}
|
||||
|
||||
/// <summary>Execute code asynchronously, returns job ID</summary>
|
||||
public static string? ExecuteAsync(string language, string code, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["language"] = language, ["code"] = code, ["async"] = true };
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/execute", HttpMethod.Post, payload, pk, sk);
|
||||
return GetString(result, "job_id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
/// <summary>Wait for async job to complete</summary>
|
||||
public static ExecuteResult? WaitJob(string jobId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/jobs/{jobId}/wait", HttpMethod.Get, null, pk, sk);
|
||||
return new ExecuteResult
|
||||
{
|
||||
Stdout = GetString(result, "stdout"),
|
||||
Stderr = GetString(result, "stderr"),
|
||||
ExitCode = GetInt(result, "exit_code"),
|
||||
ExecutionTime = GetDouble(result, "execution_time"),
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
/// <summary>Get job status</summary>
|
||||
public static JobInfo? GetJob(string jobId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/jobs/{jobId}", HttpMethod.Get, null, pk, sk);
|
||||
return new JobInfo
|
||||
{
|
||||
Id = GetString(result, "id"),
|
||||
Language = GetString(result, "language"),
|
||||
Status = GetString(result, "status"),
|
||||
CreatedAt = GetLong(result, "created_at"),
|
||||
CompletedAt = GetLong(result, "completed_at")
|
||||
};
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
/// <summary>Cancel a running job</summary>
|
||||
public static bool CancelJob(string jobId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/jobs/{jobId}/cancel", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
/// <summary>List all jobs</summary>
|
||||
public static List<JobInfo> ListJobs(string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/jobs", HttpMethod.Get, null, pk, sk);
|
||||
var jobs = new List<JobInfo>();
|
||||
if (result.TryGetValue("jobs", out var obj) && obj is JsonElement el)
|
||||
foreach (var j in el.EnumerateArray())
|
||||
jobs.Add(new JobInfo { Id = GetStr(j, "id"), Status = GetStr(j, "status"), Language = GetStr(j, "language") });
|
||||
return jobs;
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return new List<JobInfo>(); }
|
||||
}
|
||||
|
||||
/// <summary>Get available programming languages</summary>
|
||||
public static List<string> GetLanguages(string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/languages", HttpMethod.Get, null, pk, sk);
|
||||
if (result.TryGetValue("languages", out var obj) && obj is JsonElement el)
|
||||
return el.EnumerateArray().Select(x => x.GetString() ?? "").Where(x => !string.IsNullOrEmpty(x)).ToList();
|
||||
return new List<string>();
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return new List<string>(); }
|
||||
}
|
||||
|
||||
/// <summary>Detect language from filename extension</summary>
|
||||
public static string? DetectLanguage(string filename)
|
||||
{
|
||||
var extMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[".py"] = "python", [".js"] = "javascript", [".ts"] = "typescript",
|
||||
[".rb"] = "ruby", [".php"] = "php", [".pl"] = "perl", [".lua"] = "lua",
|
||||
[".sh"] = "bash", [".go"] = "go", [".rs"] = "rust", [".c"] = "c",
|
||||
[".cpp"] = "cpp", [".java"] = "java", [".cs"] = "dotnet", [".fs"] = "fsharp"
|
||||
};
|
||||
var ext = Path.GetExtension(filename);
|
||||
return extMap.TryGetValue(ext, out var lang) ? lang : null;
|
||||
}
|
||||
|
||||
// --- Session Functions (9) ---
|
||||
|
||||
public static List<SessionInfo> SessionList(string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/sessions", HttpMethod.Get, null, pk, sk);
|
||||
var sessions = new List<SessionInfo>();
|
||||
if (result.TryGetValue("sessions", out var obj) && obj is JsonElement el)
|
||||
foreach (var s in el.EnumerateArray())
|
||||
sessions.Add(new SessionInfo { Id = GetStr(s, "id"), Status = GetStr(s, "status"), NetworkMode = GetStr(s, "network_mode") });
|
||||
return sessions;
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return new List<SessionInfo>(); }
|
||||
}
|
||||
|
||||
public static SessionInfo? SessionGet(string sessionId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/sessions/{sessionId}", HttpMethod.Get, null, pk, sk);
|
||||
return new SessionInfo { Id = GetString(result, "id"), Status = GetString(result, "status"), NetworkMode = GetString(result, "network_mode") };
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static SessionInfo? SessionCreate(string? networkMode = null, string? shell = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["shell"] = shell ?? "bash" };
|
||||
if (networkMode != null) payload["network"] = networkMode;
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/sessions", HttpMethod.Post, payload, pk, sk);
|
||||
return new SessionInfo { Id = GetString(result, "id"), Status = "running" };
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static bool SessionDestroy(string sessionId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/sessions/{sessionId}", HttpMethod.Delete, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool SessionFreeze(string sessionId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/sessions/{sessionId}/freeze", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool SessionUnfreeze(string sessionId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/sessions/{sessionId}/unfreeze", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool SessionBoost(string sessionId, int vcpu = 2, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["vcpu"] = vcpu };
|
||||
try { ApiCall($"/sessions/{sessionId}/boost", HttpMethod.Post, payload, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool SessionUnboost(string sessionId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/sessions/{sessionId}/unboost", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static ExecuteResult? SessionExecute(string sessionId, string command, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["command"] = command };
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/sessions/{sessionId}/execute", HttpMethod.Post, payload, pk, sk);
|
||||
return new ExecuteResult
|
||||
{
|
||||
Stdout = GetString(result, "stdout"),
|
||||
Stderr = GetString(result, "stderr"),
|
||||
ExitCode = GetInt(result, "exit_code"),
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
// --- Service Functions (17) ---
|
||||
|
||||
public static List<ServiceInfo> ServiceList(string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/services", HttpMethod.Get, null, pk, sk);
|
||||
var services = new List<ServiceInfo>();
|
||||
if (result.TryGetValue("services", out var obj) && obj is JsonElement el)
|
||||
foreach (var s in el.EnumerateArray())
|
||||
services.Add(new ServiceInfo { Id = GetStr(s, "id"), Name = GetStr(s, "name"), Status = GetStr(s, "status") });
|
||||
return services;
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return new List<ServiceInfo>(); }
|
||||
}
|
||||
|
||||
public static ServiceInfo? ServiceGet(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/services/{serviceId}", HttpMethod.Get, null, pk, sk);
|
||||
return new ServiceInfo { Id = GetString(result, "id"), Name = GetString(result, "name"), Status = GetString(result, "status") };
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["name"] = name };
|
||||
if (ports != null) payload["ports"] = ports.Split(',').Select(p => int.Parse(p.Trim())).ToList();
|
||||
if (domains != null) payload["domains"] = domains;
|
||||
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||
if (networkMode != null) payload["network"] = networkMode;
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk);
|
||||
return GetString(result, "id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static bool ServiceDestroy(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/services/{serviceId}", HttpMethod.Delete, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ServiceFreeze(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/services/{serviceId}/freeze", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ServiceUnfreeze(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/services/{serviceId}/unfreeze", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ServiceLock(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/services/{serviceId}/lock", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ServiceUnlock(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/services/{serviceId}/unlock", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ServiceSetUnfreezeOnDemand(string serviceId, bool enabled, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["unfreeze_on_demand"] = enabled };
|
||||
try { ApiCall($"/services/{serviceId}", new HttpMethod("PATCH"), payload, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null;
|
||||
try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static string? ServiceLogs(string serviceId, bool allLogs = false, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var endpoint = allLogs ? $"/services/{serviceId}/logs?lines=9000" : $"/services/{serviceId}/logs";
|
||||
try
|
||||
{
|
||||
var result = ApiCall(endpoint, HttpMethod.Get, null, pk, sk);
|
||||
return GetString(result, "logs");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static ExecuteResult? ServiceExecute(string serviceId, string command, int timeoutMs = 30000, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["command"] = command, ["timeout_ms"] = timeoutMs };
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/services/{serviceId}/execute", HttpMethod.Post, payload, pk, sk);
|
||||
return new ExecuteResult
|
||||
{
|
||||
Stdout = GetString(result, "stdout"),
|
||||
Stderr = GetString(result, "stderr"),
|
||||
ExitCode = GetInt(result, "exit_code"),
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static string? ServiceEnvGet(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/services/{serviceId}/env", HttpMethod.Get, null, pk, sk);
|
||||
return GetString(result, "content");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static bool ServiceEnvSet(string serviceId, string envContent, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCallText($"/services/{serviceId}/env", HttpMethod.Put, envContent, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ServiceEnvDelete(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/services/{serviceId}/env", HttpMethod.Delete, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static string? ServiceEnvExport(string serviceId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/services/{serviceId}/env/export", HttpMethod.Post, null, pk, sk);
|
||||
return GetString(result, "content");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static bool ServiceResize(string serviceId, int vcpu, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["vcpu"] = vcpu };
|
||||
try { ApiCall($"/services/{serviceId}/resize", HttpMethod.Post, payload, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
// --- Snapshot Functions (9) ---
|
||||
|
||||
public static List<SnapshotInfo> SnapshotList(string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/snapshots", HttpMethod.Get, null, pk, sk);
|
||||
var snapshots = new List<SnapshotInfo>();
|
||||
if (result.TryGetValue("snapshots", out var obj) && obj is JsonElement el)
|
||||
foreach (var s in el.EnumerateArray())
|
||||
snapshots.Add(new SnapshotInfo { Id = GetStr(s, "id"), Name = GetStr(s, "name"), Type = GetStr(s, "source_type") });
|
||||
return snapshots;
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return new List<SnapshotInfo>(); }
|
||||
}
|
||||
|
||||
public static SnapshotInfo? SnapshotGet(string snapshotId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/snapshots/{snapshotId}", HttpMethod.Get, null, pk, sk);
|
||||
return new SnapshotInfo { Id = GetString(result, "id"), Name = GetString(result, "name"), Type = GetString(result, "source_type") };
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static string? SnapshotSession(string sessionId, string? name = null, bool hot = false, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object>();
|
||||
if (name != null) payload["name"] = name;
|
||||
if (hot) payload["hot"] = true;
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/sessions/{sessionId}/snapshot", HttpMethod.Post, payload, pk, sk);
|
||||
return GetString(result, "id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static string? SnapshotService(string serviceId, string? name = null, bool hot = false, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object>();
|
||||
if (name != null) payload["name"] = name;
|
||||
if (hot) payload["hot"] = true;
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/services/{serviceId}/snapshot", HttpMethod.Post, payload, pk, sk);
|
||||
return GetString(result, "id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static string? SnapshotRestore(string snapshotId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/snapshots/{snapshotId}/restore", HttpMethod.Post, null, pk, sk);
|
||||
return GetString(result, "id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static bool SnapshotDelete(string snapshotId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/snapshots/{snapshotId}", HttpMethod.Delete, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool SnapshotLock(string snapshotId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/snapshots/{snapshotId}/lock", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool SnapshotUnlock(string snapshotId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/snapshots/{snapshotId}/unlock", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static string? SnapshotClone(string snapshotId, string cloneType, string? name = null, string? ports = null, string? shell = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["type"] = cloneType };
|
||||
if (name != null) payload["name"] = name;
|
||||
if (ports != null) payload["ports"] = ports.Split(',').Select(p => int.Parse(p.Trim())).ToList();
|
||||
if (shell != null) payload["shell"] = shell;
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/snapshots/{snapshotId}/clone", HttpMethod.Post, payload, pk, sk);
|
||||
return GetString(result, "id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
// --- Image Functions (13) ---
|
||||
|
||||
public static List<ImageInfo> ImageList(string? filter = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var endpoint = filter != null ? $"/images?filter={filter}" : "/images";
|
||||
try
|
||||
{
|
||||
var result = ApiCall(endpoint, HttpMethod.Get, null, pk, sk);
|
||||
var images = new List<ImageInfo>();
|
||||
if (result.TryGetValue("images", out var obj) && obj is JsonElement el)
|
||||
foreach (var img in el.EnumerateArray())
|
||||
images.Add(new ImageInfo { Id = GetStr(img, "id"), Name = GetStr(img, "name"), Visibility = GetStr(img, "visibility") });
|
||||
return images;
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return new List<ImageInfo>(); }
|
||||
}
|
||||
|
||||
public static ImageInfo? ImageGet(string imageId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/images/{imageId}", HttpMethod.Get, null, pk, sk);
|
||||
return new ImageInfo { Id = GetString(result, "id"), Name = GetString(result, "name"), Visibility = GetString(result, "visibility") };
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static string? ImagePublish(string sourceType, string sourceId, string? name = null, string? description = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["source_type"] = sourceType, ["source_id"] = sourceId };
|
||||
if (name != null) payload["name"] = name;
|
||||
if (description != null) payload["description"] = description;
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/images", HttpMethod.Post, payload, pk, sk);
|
||||
return GetString(result, "id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static bool ImageDelete(string imageId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/images/{imageId}", HttpMethod.Delete, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ImageLock(string imageId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/images/{imageId}/lock", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ImageUnlock(string imageId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try { ApiCall($"/images/{imageId}/unlock", HttpMethod.Post, null, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ImageSetVisibility(string imageId, string visibility, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["visibility"] = visibility };
|
||||
try { ApiCall($"/images/{imageId}", new HttpMethod("PATCH"), payload, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ImageGrantAccess(string imageId, string trustedApiKey, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["api_key"] = trustedApiKey };
|
||||
try { ApiCall($"/images/{imageId}/access", HttpMethod.Post, payload, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static bool ImageRevokeAccess(string imageId, string trustedApiKey, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["api_key"] = trustedApiKey };
|
||||
try { ApiCall($"/images/{imageId}/access", HttpMethod.Delete, payload, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static List<string> ImageListTrusted(string imageId, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/images/{imageId}/access", HttpMethod.Get, null, pk, sk);
|
||||
if (result.TryGetValue("trusted_keys", out var obj) && obj is JsonElement el)
|
||||
return el.EnumerateArray().Select(x => x.GetString() ?? "").Where(x => !string.IsNullOrEmpty(x)).ToList();
|
||||
return new List<string>();
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return new List<string>(); }
|
||||
}
|
||||
|
||||
public static bool ImageTransfer(string imageId, string toApiKey, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object> { ["to_api_key"] = toApiKey };
|
||||
try { ApiCall($"/images/{imageId}/transfer", HttpMethod.Post, payload, pk, sk); return true; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
||||
public static string? ImageSpawn(string imageId, string? name = null, string? ports = null, string? bootstrap = null, string? networkMode = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object>();
|
||||
if (name != null) payload["name"] = name;
|
||||
if (ports != null) payload["ports"] = ports.Split(',').Select(p => int.Parse(p.Trim())).ToList();
|
||||
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||
if (networkMode != null) payload["network"] = networkMode;
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/images/{imageId}/spawn", HttpMethod.Post, payload, pk, sk);
|
||||
return GetString(result, "id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static string? ImageClone(string imageId, string? name = null, string? description = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var payload = new Dictionary<string, object>();
|
||||
if (name != null) payload["name"] = name;
|
||||
if (description != null) payload["description"] = description;
|
||||
try
|
||||
{
|
||||
var result = ApiCall($"/images/{imageId}/clone", HttpMethod.Post, payload, pk, sk);
|
||||
return GetString(result, "id");
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
// --- PaaS Logs (2) ---
|
||||
|
||||
public static string? LogsFetch(string source, int lines = 100, string? since = null, string? grep = null, string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
var endpoint = $"/paas/logs?source={source}&lines={lines}";
|
||||
if (since != null) endpoint += $"&since={since}";
|
||||
if (grep != null) endpoint += $"&grep={Uri.EscapeDataString(grep)}";
|
||||
try
|
||||
{
|
||||
var result = ApiCall(endpoint, HttpMethod.Get, null, pk, sk);
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
// LogsStream requires SSE/WebSocket support - not implemented in sync version
|
||||
|
||||
// --- Utilities ---
|
||||
|
||||
public static KeyInfo? ValidateKeys(string? publicKey = null, string? secretKey = null)
|
||||
{
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/keys/validate", HttpMethod.Post, null, pk, sk);
|
||||
return new KeyInfo
|
||||
{
|
||||
Valid = result.TryGetValue("valid", out var v) && v is JsonElement ve && ve.GetBoolean(),
|
||||
Tier = GetString(result, "tier"),
|
||||
RateLimitPerMinute = GetInt(result, "rate_limit"),
|
||||
ConcurrencyLimit = GetInt(result, "concurrency")
|
||||
};
|
||||
}
|
||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||
}
|
||||
|
||||
public static string HmacSign(string secretKey, string message)
|
||||
{
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
|
||||
return Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public static bool HealthCheck()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/health");
|
||||
var response = _httpClient.Send(request);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public static string Version() => "4.2.50";
|
||||
|
||||
public static string? LastError() => _lastError;
|
||||
|
||||
// --- Internal Helpers ---
|
||||
|
||||
private static (string, string) ResolveKeys(string? publicKey, string? secretKey)
|
||||
{
|
||||
var pk = publicKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY") ?? "";
|
||||
var sk = secretKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY") ?? "";
|
||||
return (pk, sk);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> ApiCall(string endpoint, HttpMethod method, Dictionary<string, object>? data, string publicKey, string secretKey)
|
||||
{
|
||||
var body = data != null ? JsonSerializer.Serialize(data, _jsonOptions) : "";
|
||||
using var request = new HttpRequestMessage(method, endpoint);
|
||||
if (data != null) request.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
|
||||
if (!string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var message = $"{timestamp}:{method.Method}:{endpoint}:{body}";
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
|
||||
var signature = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLowerInvariant();
|
||||
request.Headers.Add("Authorization", $"Bearer {publicKey}");
|
||||
request.Headers.Add("X-Timestamp", timestamp.ToString());
|
||||
request.Headers.Add("X-Signature", signature);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Add("Authorization", $"Bearer {publicKey}");
|
||||
}
|
||||
|
||||
var response = _httpClient.Send(request);
|
||||
using var reader = new StreamReader(response.Content.ReadAsStream());
|
||||
var responseBody = reader.ReadToEnd();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new Exception($"HTTP {(int)response.StatusCode}: {responseBody}");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(responseBody)) return new Dictionary<string, object>();
|
||||
var doc = JsonDocument.Parse(responseBody);
|
||||
return doc.RootElement.EnumerateObject().ToDictionary(p => p.Name, p => (object)p.Value.Clone());
|
||||
}
|
||||
|
||||
private static void ApiCallText(string endpoint, HttpMethod method, string body, string publicKey, string secretKey)
|
||||
{
|
||||
using var request = new HttpRequestMessage(method, endpoint);
|
||||
request.Content = new StringContent(body, Encoding.UTF8, "text/plain");
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var message = $"{timestamp}:{method.Method}:{endpoint}:{body}";
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
|
||||
var signature = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLowerInvariant();
|
||||
request.Headers.Add("Authorization", $"Bearer {publicKey}");
|
||||
request.Headers.Add("X-Timestamp", timestamp.ToString());
|
||||
request.Headers.Add("X-Signature", signature);
|
||||
|
||||
var response = _httpClient.Send(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
using var reader = new StreamReader(response.Content.ReadAsStream());
|
||||
throw new Exception($"HTTP {(int)response.StatusCode}: {reader.ReadToEnd()}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetString(Dictionary<string, object> result, string key)
|
||||
=> result.TryGetValue(key, out var v) && v is JsonElement el ? el.GetString() : null;
|
||||
|
||||
private static int GetInt(Dictionary<string, object> result, string key)
|
||||
=> result.TryGetValue(key, out var v) && v is JsonElement el && el.TryGetInt32(out var i) ? i : 0;
|
||||
|
||||
private static long GetLong(Dictionary<string, object> result, string key)
|
||||
=> result.TryGetValue(key, out var v) && v is JsonElement el && el.TryGetInt64(out var i) ? i : 0;
|
||||
|
||||
private static double GetDouble(Dictionary<string, object> result, string key)
|
||||
=> result.TryGetValue(key, out var v) && v is JsonElement el && el.TryGetDouble(out var d) ? d : 0;
|
||||
|
||||
private static string GetStr(JsonElement el, string prop) => el.TryGetProperty(prop, out var p) ? p.GetString() ?? "" : "";
|
||||
}
|
||||
|
||||
// --- Data Types ---
|
||||
|
||||
public class ExecuteResult
|
||||
{
|
||||
public string? Stdout { get; set; }
|
||||
public string? Stderr { get; set; }
|
||||
public int ExitCode { get; set; }
|
||||
public string? Language { get; set; }
|
||||
public double ExecutionTime { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class JobInfo
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Language { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public long CreatedAt { get; set; }
|
||||
public long CompletedAt { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class SessionInfo
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? ContainerName { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? NetworkMode { get; set; }
|
||||
public int Vcpu { get; set; }
|
||||
public long CreatedAt { get; set; }
|
||||
public long LastActivity { get; set; }
|
||||
}
|
||||
|
||||
public class ServiceInfo
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? ContainerName { get; set; }
|
||||
public string? NetworkMode { get; set; }
|
||||
public string? Ports { get; set; }
|
||||
public string? Domains { get; set; }
|
||||
public int Vcpu { get; set; }
|
||||
public bool Locked { get; set; }
|
||||
public bool UnfreezeOnDemand { get; set; }
|
||||
public long CreatedAt { get; set; }
|
||||
public long LastActivity { get; set; }
|
||||
}
|
||||
|
||||
public class SnapshotInfo
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string? SourceId { get; set; }
|
||||
public bool Hot { get; set; }
|
||||
public bool Locked { get; set; }
|
||||
public long CreatedAt { get; set; }
|
||||
public long SizeBytes { get; set; }
|
||||
}
|
||||
|
||||
public class ImageInfo
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Visibility { get; set; }
|
||||
public string? SourceType { get; set; }
|
||||
public string? SourceId { get; set; }
|
||||
public string? OwnerApiKey { get; set; }
|
||||
public bool Locked { get; set; }
|
||||
public long CreatedAt { get; set; }
|
||||
public long SizeBytes { get; set; }
|
||||
}
|
||||
|
||||
public class KeyInfo
|
||||
{
|
||||
public bool Valid { get; set; }
|
||||
public string? Tier { get; set; }
|
||||
public int RateLimitPerMinute { get; set; }
|
||||
public int RateLimitBurst { get; set; }
|
||||
public int ConcurrencyLimit { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CLI Args
|
||||
// =============================================================================
|
||||
|
||||
class Args
|
||||
{
|
||||
public bool ShowHelp, ShowVersion;
|
||||
|
|
@ -1156,9 +2040,11 @@ class Args
|
|||
public string? SnapshotInfo, SnapshotDelete, SnapshotLock, SnapshotUnlock, SnapshotClone;
|
||||
public string? SnapshotCloneType, SnapshotName;
|
||||
public bool SnapshotHot;
|
||||
public string? SnapshotRestore;
|
||||
public bool ImageList;
|
||||
public string? ImageInfo, ImageDelete, ImageLock, ImageUnlock;
|
||||
public string? ImagePublish, ImageSourceType, ImageVisibility, ImageVisibilityMode;
|
||||
public string? ImageSpawn, ImageClone;
|
||||
public string? ImageGrantAccess, ImageRevokeAccess, ImageTransfer;
|
||||
public bool LanguagesJson;
|
||||
}
|
||||
|
|
|
|||
193
clients/dotnet/tests/UnsandboxTests.cs
Normal file
193
clients/dotnet/tests/UnsandboxTests.cs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Unit and Functional Tests for Unsandbox .NET SDK
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnsandboxTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the Unsandbox SDK library functions.
|
||||
/// These tests verify that exported library functions work correctly.
|
||||
/// </summary>
|
||||
public class UnitTests
|
||||
{
|
||||
public static void Run()
|
||||
{
|
||||
Console.WriteLine("=== Unsandbox .NET SDK Unit Tests ===\n");
|
||||
|
||||
TestDetectLanguage();
|
||||
TestHmacSign();
|
||||
TestVersion();
|
||||
|
||||
Console.WriteLine("\n=== Unit Tests Complete ===");
|
||||
}
|
||||
|
||||
static void TestDetectLanguage()
|
||||
{
|
||||
Console.Write("DetectLanguage: ");
|
||||
var tests = new Dictionary<string, string?>
|
||||
{
|
||||
{ "test.py", "python" },
|
||||
{ "script.js", "javascript" },
|
||||
{ "main.go", "go" },
|
||||
{ "app.rs", "rust" },
|
||||
{ "Program.cs", "dotnet" },
|
||||
{ "Module.fs", "fsharp" },
|
||||
{ "unknown.xyz", null }
|
||||
};
|
||||
|
||||
int passed = 0;
|
||||
foreach (var (file, expected) in tests)
|
||||
{
|
||||
var result = Unsandbox.DetectLanguage(file);
|
||||
if (result == expected) passed++;
|
||||
else Console.Write($"[FAIL: {file} -> {result}, expected {expected}] ");
|
||||
}
|
||||
|
||||
if (passed == tests.Count)
|
||||
Console.WriteLine($"PASS ({passed}/{tests.Count})");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({passed}/{tests.Count})");
|
||||
}
|
||||
|
||||
static void TestHmacSign()
|
||||
{
|
||||
Console.Write("HmacSign: ");
|
||||
// Test vector: HMAC-SHA256("key", "message")
|
||||
var result = Unsandbox.HmacSign("key", "message");
|
||||
// Expected: 6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a
|
||||
var expected = "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a";
|
||||
if (result == expected)
|
||||
Console.WriteLine("PASS");
|
||||
else
|
||||
Console.WriteLine($"FAIL (got {result}, expected {expected})");
|
||||
}
|
||||
|
||||
static void TestVersion()
|
||||
{
|
||||
Console.Write("Version: ");
|
||||
var version = Unsandbox.Version();
|
||||
if (!string.IsNullOrEmpty(version) && version.Contains("."))
|
||||
Console.WriteLine($"PASS ({version})");
|
||||
else
|
||||
Console.WriteLine($"FAIL (got {version})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Functional tests that require API credentials.
|
||||
/// Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables.
|
||||
/// </summary>
|
||||
public class FunctionalTests
|
||||
{
|
||||
public static void Run()
|
||||
{
|
||||
var publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY");
|
||||
var secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
Console.WriteLine("=== Functional Tests Skipped (no API credentials) ===");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("=== Unsandbox .NET SDK Functional Tests ===\n");
|
||||
|
||||
TestHealthCheck();
|
||||
TestValidateKeys();
|
||||
TestGetLanguages();
|
||||
TestExecute();
|
||||
TestSessionList();
|
||||
TestServiceList();
|
||||
TestSnapshotList();
|
||||
TestImageList();
|
||||
|
||||
Console.WriteLine("\n=== Functional Tests Complete ===");
|
||||
}
|
||||
|
||||
static void TestHealthCheck()
|
||||
{
|
||||
Console.Write("HealthCheck: ");
|
||||
var result = Unsandbox.HealthCheck();
|
||||
Console.WriteLine(result ? "PASS" : "FAIL");
|
||||
}
|
||||
|
||||
static void TestValidateKeys()
|
||||
{
|
||||
Console.Write("ValidateKeys: ");
|
||||
var result = Unsandbox.ValidateKeys();
|
||||
if (result != null && result.Valid)
|
||||
Console.WriteLine($"PASS (tier: {result.Tier})");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({Unsandbox.LastError()})");
|
||||
}
|
||||
|
||||
static void TestGetLanguages()
|
||||
{
|
||||
Console.Write("GetLanguages: ");
|
||||
var result = Unsandbox.GetLanguages();
|
||||
if (result.Count > 0)
|
||||
Console.WriteLine($"PASS ({result.Count} languages)");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({Unsandbox.LastError()})");
|
||||
}
|
||||
|
||||
static void TestExecute()
|
||||
{
|
||||
Console.Write("Execute: ");
|
||||
var result = Unsandbox.Execute("python", "print('hello from .NET SDK')");
|
||||
if (result.Success && result.Stdout?.Contains("hello") == true)
|
||||
Console.WriteLine("PASS");
|
||||
else
|
||||
Console.WriteLine($"FAIL ({result.ErrorMessage ?? Unsandbox.LastError()})");
|
||||
}
|
||||
|
||||
static void TestSessionList()
|
||||
{
|
||||
Console.Write("SessionList: ");
|
||||
var result = Unsandbox.SessionList();
|
||||
// Empty list is valid - just checking API call works
|
||||
Console.WriteLine($"PASS ({result.Count} sessions)");
|
||||
}
|
||||
|
||||
static void TestServiceList()
|
||||
{
|
||||
Console.Write("ServiceList: ");
|
||||
var result = Unsandbox.ServiceList();
|
||||
Console.WriteLine($"PASS ({result.Count} services)");
|
||||
}
|
||||
|
||||
static void TestSnapshotList()
|
||||
{
|
||||
Console.Write("SnapshotList: ");
|
||||
var result = Unsandbox.SnapshotList();
|
||||
Console.WriteLine($"PASS ({result.Count} snapshots)");
|
||||
}
|
||||
|
||||
static void TestImageList()
|
||||
{
|
||||
Console.Write("ImageList: ");
|
||||
var result = Unsandbox.ImageList();
|
||||
Console.WriteLine($"PASS ({result.Count} images)");
|
||||
}
|
||||
}
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
UnitTests.Run();
|
||||
Console.WriteLine();
|
||||
FunctionalTests.Run();
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Test error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
clients/dotnet/tests/UnsandboxTests.csproj
Normal file
14
clients/dotnet/tests/UnsandboxTests.csproj
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="../sync/src/Un.cs" Link="Un.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load diff
153
clients/elixir/sync/tests/test_functional.exs
Normal file
153
clients/elixir/sync/tests/test_functional.exs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env elixir
|
||||
|
||||
# Functional Tests for Un Elixir SDK
|
||||
#
|
||||
# Run with: elixir tests/test_functional.exs
|
||||
# Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables
|
||||
#
|
||||
# These tests make real API calls to api.unsandbox.com
|
||||
|
||||
Code.require_file("../src/un.ex", __DIR__)
|
||||
|
||||
defmodule UnFunctionalTest do
|
||||
@moduledoc """
|
||||
Functional test suite for Un Elixir SDK.
|
||||
Tests real API calls to api.unsandbox.com.
|
||||
"""
|
||||
|
||||
@blue "\e[34m"
|
||||
@red "\e[31m"
|
||||
@green "\e[32m"
|
||||
@yellow "\e[33m"
|
||||
@reset "\e[0m"
|
||||
|
||||
def run_all do
|
||||
IO.puts("\n#{@blue}=== Un Elixir SDK Functional Tests ===#@reset}\n")
|
||||
|
||||
# Check for credentials
|
||||
unless System.get_env("UNSANDBOX_PUBLIC_KEY") && System.get_env("UNSANDBOX_SECRET_KEY") do
|
||||
IO.puts("#{@yellow}SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{@reset}")
|
||||
System.halt(0)
|
||||
end
|
||||
|
||||
tests = [
|
||||
{"health_check", &test_health_check/0},
|
||||
{"validate_keys", &test_validate_keys/0},
|
||||
{"execute_python", &test_execute_python/0},
|
||||
{"execute_with_error", &test_execute_with_error/0},
|
||||
{"session_list", &test_session_list/0},
|
||||
{"service_list", &test_service_list/0},
|
||||
{"snapshot_list", &test_snapshot_list/0},
|
||||
{"image_list", &test_image_list/0}
|
||||
]
|
||||
|
||||
results = Enum.map(tests, fn {name, test_fn} ->
|
||||
IO.write(" Running #{name}... ")
|
||||
try do
|
||||
test_fn.()
|
||||
IO.puts("#{@green}PASS#{@reset}")
|
||||
:pass
|
||||
rescue
|
||||
e ->
|
||||
IO.puts("#{@red}FAIL#{@reset}")
|
||||
IO.puts(" #{inspect(e)}")
|
||||
:fail
|
||||
end
|
||||
end)
|
||||
|
||||
passed = Enum.count(results, &(&1 == :pass))
|
||||
failed = Enum.count(results, &(&1 == :fail))
|
||||
total = length(results)
|
||||
|
||||
IO.puts("\n#{@blue}Results: #{passed}/#{total} passed#{@reset}")
|
||||
if failed > 0 do
|
||||
IO.puts("#{@red}#{failed} test(s) failed#{@reset}")
|
||||
System.halt(1)
|
||||
else
|
||||
IO.puts("#{@green}All functional tests passed!#{@reset}")
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Functional Tests
|
||||
# ============================================================================
|
||||
|
||||
def test_health_check do
|
||||
result = Un.health_check()
|
||||
assert is_boolean(result), "health_check should return boolean"
|
||||
# Note: We don't require it to be true in case API is down
|
||||
end
|
||||
|
||||
def test_validate_keys do
|
||||
key_info = Un.validate_keys()
|
||||
assert is_map(key_info), "validate_keys should return a map"
|
||||
assert Map.has_key?(key_info, :valid), "key_info should have :valid key"
|
||||
assert is_boolean(key_info.valid), ":valid should be boolean"
|
||||
end
|
||||
|
||||
def test_execute_python do
|
||||
result = Un.execute("python", "print(6 * 7)")
|
||||
assert is_map(result), "execute should return a map"
|
||||
assert Map.has_key?(result, :success), "result should have :success key"
|
||||
assert Map.has_key?(result, :stdout), "result should have :stdout key"
|
||||
assert Map.has_key?(result, :exit_code), "result should have :exit_code key"
|
||||
|
||||
# Check output
|
||||
assert result.success == true, "execution should succeed"
|
||||
assert String.contains?(result.stdout, "42"), "stdout should contain '42'"
|
||||
assert result.exit_code == 0, "exit_code should be 0"
|
||||
end
|
||||
|
||||
def test_execute_with_error do
|
||||
result = Un.execute("python", "import sys; sys.exit(1)")
|
||||
assert is_map(result), "execute should return a map"
|
||||
assert result.success == false, "execution should fail"
|
||||
assert result.exit_code == 1, "exit_code should be 1"
|
||||
end
|
||||
|
||||
def test_session_list do
|
||||
response = Un.session_list()
|
||||
assert is_binary(response), "session_list should return a string"
|
||||
# Response should be valid JSON (starts with [ or {)
|
||||
trimmed = String.trim(response)
|
||||
assert String.starts_with?(trimmed, "[") || String.starts_with?(trimmed, "{"),
|
||||
"response should be JSON"
|
||||
end
|
||||
|
||||
def test_service_list do
|
||||
response = Un.service_list()
|
||||
assert is_binary(response), "service_list should return a string"
|
||||
trimmed = String.trim(response)
|
||||
assert String.starts_with?(trimmed, "[") || String.starts_with?(trimmed, "{"),
|
||||
"response should be JSON"
|
||||
end
|
||||
|
||||
def test_snapshot_list do
|
||||
response = Un.snapshot_list()
|
||||
assert is_binary(response), "snapshot_list should return a string"
|
||||
trimmed = String.trim(response)
|
||||
assert String.starts_with?(trimmed, "[") || String.starts_with?(trimmed, "{"),
|
||||
"response should be JSON"
|
||||
end
|
||||
|
||||
def test_image_list do
|
||||
response = Un.image_list()
|
||||
assert is_binary(response), "image_list should return a string"
|
||||
trimmed = String.trim(response)
|
||||
assert String.starts_with?(trimmed, "[") || String.starts_with?(trimmed, "{"),
|
||||
"response should be JSON"
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Helpers
|
||||
# ============================================================================
|
||||
|
||||
defp assert(true, _message), do: :ok
|
||||
defp assert(false, message), do: raise message
|
||||
defp assert(condition, message) when is_boolean(condition) do
|
||||
if condition, do: :ok, else: raise message
|
||||
end
|
||||
end
|
||||
|
||||
# Run tests
|
||||
UnFunctionalTest.run_all()
|
||||
135
clients/elixir/sync/tests/test_library.exs
Normal file
135
clients/elixir/sync/tests/test_library.exs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env elixir
|
||||
|
||||
# Tests for Un Elixir SDK
|
||||
#
|
||||
# Run with: elixir tests/test_library.exs
|
||||
# Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables
|
||||
|
||||
Code.require_file("../src/un.ex", __DIR__)
|
||||
|
||||
defmodule UnTest do
|
||||
@moduledoc """
|
||||
Test suite for Un Elixir SDK library functions.
|
||||
"""
|
||||
|
||||
@blue "\e[34m"
|
||||
@red "\e[31m"
|
||||
@green "\e[32m"
|
||||
@yellow "\e[33m"
|
||||
@reset "\e[0m"
|
||||
|
||||
def run_all do
|
||||
IO.puts("\n#{@blue}=== Un Elixir SDK Tests ===#@reset}\n")
|
||||
|
||||
tests = [
|
||||
{"version", &test_version/0},
|
||||
{"detect_language", &test_detect_language/0},
|
||||
{"hmac_sign", &test_hmac_sign/0},
|
||||
{"hmac_sign_deterministic", &test_hmac_sign_deterministic/0},
|
||||
{"hmac_sign_different_secrets", &test_hmac_sign_different_secrets/0},
|
||||
{"get_languages", &test_get_languages/0}
|
||||
]
|
||||
|
||||
results = Enum.map(tests, fn {name, test_fn} ->
|
||||
try do
|
||||
test_fn.()
|
||||
IO.puts("#{@green}PASS#{@reset}: #{name}")
|
||||
:pass
|
||||
rescue
|
||||
e ->
|
||||
IO.puts("#{@red}FAIL#{@reset}: #{name} - #{inspect(e)}")
|
||||
:fail
|
||||
end
|
||||
end)
|
||||
|
||||
passed = Enum.count(results, &(&1 == :pass))
|
||||
failed = Enum.count(results, &(&1 == :fail))
|
||||
total = length(results)
|
||||
|
||||
IO.puts("\n#{@blue}Results: #{passed}/#{total} passed#{@reset}")
|
||||
if failed > 0 do
|
||||
IO.puts("#{@red}#{failed} test(s) failed#{@reset}")
|
||||
System.halt(1)
|
||||
else
|
||||
IO.puts("#{@green}All tests passed!#{@reset}")
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Unit Tests
|
||||
# ============================================================================
|
||||
|
||||
def test_version do
|
||||
version = Un.version()
|
||||
assert is_binary(version), "version should be a string"
|
||||
assert String.match?(version, ~r/^\d+\.\d+\.\d+$/), "version should be semver format"
|
||||
end
|
||||
|
||||
def test_detect_language do
|
||||
# Test common extensions
|
||||
assert Un.detect_language("script.py") == "python"
|
||||
assert Un.detect_language("app.js") == "javascript"
|
||||
assert Un.detect_language("main.go") == "go"
|
||||
assert Un.detect_language("main.rs") == "rust"
|
||||
assert Un.detect_language("main.c") == "c"
|
||||
assert Un.detect_language("main.cpp") == "cpp"
|
||||
assert Un.detect_language("Main.java") == "java"
|
||||
assert Un.detect_language("script.rb") == "ruby"
|
||||
assert Un.detect_language("script.sh") == "bash"
|
||||
assert Un.detect_language("script.lua") == "lua"
|
||||
assert Un.detect_language("script.pl") == "perl"
|
||||
assert Un.detect_language("index.php") == "php"
|
||||
assert Un.detect_language("main.hs") == "haskell"
|
||||
assert Un.detect_language("main.ml") == "ocaml"
|
||||
assert Un.detect_language("main.ex") == "elixir"
|
||||
assert Un.detect_language("main.erl") == "erlang"
|
||||
|
||||
# Test with paths
|
||||
assert Un.detect_language("/path/to/script.py") == "python"
|
||||
|
||||
# Test unknown extensions
|
||||
assert Un.detect_language("Makefile") == nil
|
||||
assert Un.detect_language("README") == nil
|
||||
assert Un.detect_language("script.unknown") == nil
|
||||
end
|
||||
|
||||
def test_hmac_sign do
|
||||
signature = Un.hmac_sign("my_secret", "test message")
|
||||
assert is_binary(signature), "signature should be a string"
|
||||
assert String.length(signature) == 64, "signature should be 64 hex characters"
|
||||
assert String.match?(signature, ~r/^[0-9a-f]+$/), "signature should be lowercase hex"
|
||||
end
|
||||
|
||||
def test_hmac_sign_deterministic do
|
||||
sig1 = Un.hmac_sign("test_secret", "same message")
|
||||
sig2 = Un.hmac_sign("test_secret", "same message")
|
||||
assert sig1 == sig2, "same inputs should produce same signature"
|
||||
end
|
||||
|
||||
def test_hmac_sign_different_secrets do
|
||||
sig1 = Un.hmac_sign("secret1", "test message")
|
||||
sig2 = Un.hmac_sign("secret2", "test message")
|
||||
assert sig1 != sig2, "different secrets should produce different signatures"
|
||||
end
|
||||
|
||||
def test_get_languages do
|
||||
languages = Un.get_languages()
|
||||
assert is_list(languages), "languages should be a list"
|
||||
assert length(languages) > 0, "languages list should not be empty"
|
||||
assert "python" in languages, "python should be in languages"
|
||||
assert "javascript" in languages, "javascript should be in languages"
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Helpers
|
||||
# ============================================================================
|
||||
|
||||
defp assert(true, _message), do: :ok
|
||||
defp assert(false, message), do: raise message
|
||||
defp assert(condition, message) when is_boolean(condition) do
|
||||
if condition, do: :ok, else: raise message
|
||||
end
|
||||
end
|
||||
|
||||
# Run tests
|
||||
UnTest.run_all()
|
||||
|
|
@ -37,10 +37,687 @@
|
|||
|
||||
#!/usr/bin/env escript
|
||||
|
||||
%%% Erlang UN CLI - Unsandbox CLI Client
|
||||
%%% @doc unsandbox.com Erlang SDK
|
||||
%%%
|
||||
%%% Full-featured CLI matching un.py capabilities
|
||||
%%% Uses curl for HTTP (no external dependencies)
|
||||
%%% Full API with execution, sessions, services, snapshots, and images.
|
||||
%%%
|
||||
%%% Library Usage:
|
||||
%%% ```
|
||||
%%% %% Execute code synchronously
|
||||
%%% Result = un:execute("python", "print(42)"),
|
||||
%%% io:format("~s~n", [maps:get(stdout, Result)]).
|
||||
%%%
|
||||
%%% %% List sessions
|
||||
%%% Sessions = un:session_list().
|
||||
%%%
|
||||
%%% %% Create a service
|
||||
%%% ServiceId = un:service_create("myapp", #{ports => "8080"}).
|
||||
%%% ```
|
||||
%%%
|
||||
%%% Authentication Priority:
|
||||
%%% 1. Function arguments (PublicKey, SecretKey)
|
||||
%%% 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
%%% 3. Config file (~/.unsandbox/accounts.csv)
|
||||
|
||||
-define(API_BASE, "https://api.unsandbox.com").
|
||||
-define(PORTAL_BASE, "https://unsandbox.com").
|
||||
-define(VERSION, "4.2.0").
|
||||
-define(LANGUAGES_CACHE_TTL, 3600).
|
||||
|
||||
%% ============================================================================
|
||||
%% Utility Functions
|
||||
%% ============================================================================
|
||||
|
||||
%% @doc Return the SDK version.
|
||||
version() -> ?VERSION.
|
||||
|
||||
%% @doc Check API health.
|
||||
health_check() ->
|
||||
Cmd = "curl -s -o /dev/null -w '%{http_code}' " ++ ?API_BASE ++ "/health",
|
||||
Result = os:cmd(Cmd),
|
||||
string:trim(Result) == "200".
|
||||
|
||||
%% @doc Generate HMAC-SHA256 signature for a message.
|
||||
hmac_sign(SecretKey, Message) ->
|
||||
hmac_sha256(SecretKey, Message).
|
||||
|
||||
%% @doc Detect language from filename extension.
|
||||
detect_language(Filename) ->
|
||||
Ext = filename:extension(Filename),
|
||||
ext_to_lang(Ext).
|
||||
|
||||
%% ============================================================================
|
||||
%% Execution Functions (8)
|
||||
%% ============================================================================
|
||||
|
||||
%% @doc Execute code synchronously.
|
||||
execute(Language, Code) ->
|
||||
execute(Language, Code, #{}).
|
||||
|
||||
execute(Language, Code, Opts) ->
|
||||
Json = build_execute_json_full(Language, Code, Opts),
|
||||
Response = api_post("/execute", Json, Opts),
|
||||
parse_result(Response).
|
||||
|
||||
%% @doc Execute code asynchronously, returning a job ID.
|
||||
execute_async(Language, Code) ->
|
||||
execute_async(Language, Code, #{}).
|
||||
|
||||
execute_async(Language, Code, Opts) ->
|
||||
Json = build_execute_json_full(Language, Code, Opts),
|
||||
Response = api_post("/execute/async", Json, Opts),
|
||||
extract_json_field(Response, "job_id").
|
||||
|
||||
%% @doc Wait for a job to complete and return the result.
|
||||
wait_job(JobId) ->
|
||||
wait_job(JobId, #{}).
|
||||
|
||||
wait_job(JobId, Opts) ->
|
||||
MaxPolls = maps:get(max_polls, Opts, 100),
|
||||
PollDelays = [300, 450, 700, 900, 650, 1600, 2000],
|
||||
do_wait_job(JobId, PollDelays, 0, MaxPolls, Opts).
|
||||
|
||||
do_wait_job(JobId, _PollDelays, PollCount, MaxPolls, _Opts) when PollCount >= MaxPolls ->
|
||||
#{success => false, stdout => "", stderr => "Max polls exceeded", exit_code => 1, job_id => JobId};
|
||||
do_wait_job(JobId, PollDelays, PollCount, MaxPolls, Opts) ->
|
||||
DelayIdx = min(PollCount, length(PollDelays) - 1),
|
||||
Delay = lists:nth(DelayIdx + 1, PollDelays),
|
||||
timer:sleep(Delay),
|
||||
Job = get_job(JobId, Opts),
|
||||
Status = maps:get(status, Job, "unknown"),
|
||||
case lists:member(Status, ["completed", "failed", "timeout", "cancelled"]) of
|
||||
true ->
|
||||
Response = api_get("/jobs/" ++ JobId, Opts),
|
||||
parse_result(Response);
|
||||
false ->
|
||||
do_wait_job(JobId, PollDelays, PollCount + 1, MaxPolls, Opts)
|
||||
end.
|
||||
|
||||
%% @doc Get job status and details.
|
||||
get_job(JobId) ->
|
||||
get_job(JobId, #{}).
|
||||
|
||||
get_job(JobId, Opts) ->
|
||||
Response = api_get("/jobs/" ++ JobId, Opts),
|
||||
#{
|
||||
id => JobId,
|
||||
status => case extract_json_field(Response, "status") of "" -> "unknown"; S -> S end,
|
||||
language => extract_json_field(Response, "language"),
|
||||
created_at => extract_json_number(Response, "created_at"),
|
||||
completed_at => extract_json_number(Response, "completed_at")
|
||||
}.
|
||||
|
||||
%% @doc Cancel a running job.
|
||||
cancel_job(JobId) ->
|
||||
cancel_job(JobId, #{}).
|
||||
|
||||
cancel_job(JobId, Opts) ->
|
||||
Response = api_delete("/jobs/" ++ JobId, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc List all active jobs.
|
||||
list_jobs() ->
|
||||
list_jobs(#{}).
|
||||
|
||||
list_jobs(Opts) ->
|
||||
api_get("/jobs", Opts).
|
||||
|
||||
%% @doc Get list of supported languages.
|
||||
get_languages() ->
|
||||
get_languages(#{}).
|
||||
|
||||
get_languages(Opts) ->
|
||||
case load_languages_cache() of
|
||||
undefined ->
|
||||
Response = api_get("/languages", Opts),
|
||||
Langs = extract_json_array(Response, "languages"),
|
||||
save_languages_cache(Langs),
|
||||
Langs;
|
||||
CachedLanguages ->
|
||||
CachedLanguages
|
||||
end.
|
||||
|
||||
%% ============================================================================
|
||||
%% Session Functions (9)
|
||||
%% ============================================================================
|
||||
|
||||
%% @doc List all sessions.
|
||||
session_list() -> session_list(#{}).
|
||||
session_list(Opts) -> api_get("/sessions", Opts).
|
||||
|
||||
%% @doc Get session details.
|
||||
session_get(SessionId) -> session_get(SessionId, #{}).
|
||||
session_get(SessionId, Opts) ->
|
||||
Response = api_get("/sessions/" ++ SessionId, Opts),
|
||||
#{
|
||||
id => SessionId,
|
||||
status => case extract_json_field(Response, "status") of "" -> "unknown"; S -> S end,
|
||||
container_name => extract_json_field(Response, "container_name"),
|
||||
network_mode => extract_json_field(Response, "network_mode"),
|
||||
vcpu => extract_json_number(Response, "vcpu"),
|
||||
created_at => extract_json_number(Response, "created_at")
|
||||
}.
|
||||
|
||||
%% @doc Create a new session.
|
||||
session_create() -> session_create(#{}).
|
||||
session_create(Opts) ->
|
||||
Shell = maps:get(shell, Opts, "bash"),
|
||||
Network = maps:get(network, Opts, undefined),
|
||||
Vcpu = maps:get(vcpu, Opts, undefined),
|
||||
NetworkJson = case Network of undefined -> ""; N -> ",\"network\":\"" ++ N ++ "\"" end,
|
||||
VcpuJson = case Vcpu of undefined -> ""; V -> ",\"vcpu\":" ++ integer_to_list(V) end,
|
||||
Json = "{\"shell\":\"" ++ Shell ++ "\"" ++ NetworkJson ++ VcpuJson ++ "}",
|
||||
Response = api_post("/sessions", Json, Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% @doc Destroy a session.
|
||||
session_destroy(SessionId) -> session_destroy(SessionId, #{}).
|
||||
session_destroy(SessionId, Opts) ->
|
||||
Response = api_delete("/sessions/" ++ SessionId, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Freeze a session.
|
||||
session_freeze(SessionId) -> session_freeze(SessionId, #{}).
|
||||
session_freeze(SessionId, Opts) ->
|
||||
Response = api_post("/sessions/" ++ SessionId ++ "/freeze", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Unfreeze a session.
|
||||
session_unfreeze(SessionId) -> session_unfreeze(SessionId, #{}).
|
||||
session_unfreeze(SessionId, Opts) ->
|
||||
Response = api_post("/sessions/" ++ SessionId ++ "/unfreeze", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Boost session resources.
|
||||
session_boost(SessionId, Vcpu) -> session_boost(SessionId, Vcpu, #{}).
|
||||
session_boost(SessionId, Vcpu, Opts) ->
|
||||
Json = "{\"vcpu\":" ++ integer_to_list(Vcpu) ++ "}",
|
||||
Response = api_patch("/sessions/" ++ SessionId, Json, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Unboost session.
|
||||
session_unboost(SessionId) -> session_unboost(SessionId, #{}).
|
||||
session_unboost(SessionId, Opts) ->
|
||||
Response = api_patch("/sessions/" ++ SessionId, "{\"vcpu\":1}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Execute a command in a session.
|
||||
session_execute(SessionId, Command) -> session_execute(SessionId, Command, #{}).
|
||||
session_execute(SessionId, Command, Opts) ->
|
||||
Json = "{\"command\":\"" ++ escape_json(Command) ++ "\"}",
|
||||
Response = api_post("/sessions/" ++ SessionId ++ "/execute", Json, Opts),
|
||||
parse_result(Response).
|
||||
|
||||
%% ============================================================================
|
||||
%% Service Functions (17)
|
||||
%% ============================================================================
|
||||
|
||||
%% @doc List all services.
|
||||
service_list() -> service_list(#{}).
|
||||
service_list(Opts) -> api_get("/services", Opts).
|
||||
|
||||
%% @doc Get service details.
|
||||
service_get(ServiceId) -> service_get(ServiceId, #{}).
|
||||
service_get(ServiceId, Opts) ->
|
||||
Response = api_get("/services/" ++ ServiceId, Opts),
|
||||
#{
|
||||
id => ServiceId,
|
||||
name => extract_json_field(Response, "name"),
|
||||
status => case extract_json_field(Response, "status") of "" -> "unknown"; S -> S end,
|
||||
ports => extract_json_field(Response, "ports"),
|
||||
domains => extract_json_field(Response, "domains"),
|
||||
vcpu => extract_json_number(Response, "vcpu"),
|
||||
locked => extract_json_field(Response, "locked") == "true",
|
||||
unfreeze_on_demand => extract_json_field(Response, "unfreeze_on_demand") == "true",
|
||||
created_at => extract_json_number(Response, "created_at")
|
||||
}.
|
||||
|
||||
%% @doc Create a new service.
|
||||
service_create(Name) -> service_create(Name, #{}).
|
||||
service_create(Name, Opts) ->
|
||||
Ports = maps:get(ports, Opts, undefined),
|
||||
Bootstrap = maps:get(bootstrap, Opts, undefined),
|
||||
Network = maps:get(network, Opts, undefined),
|
||||
Vcpu = maps:get(vcpu, Opts, undefined),
|
||||
PortsJson = case Ports of undefined -> ""; P -> ",\"ports\":[" ++ P ++ "]" end,
|
||||
BootstrapJson = case Bootstrap of undefined -> ""; B -> ",\"bootstrap\":\"" ++ escape_json(B) ++ "\"" end,
|
||||
NetworkJson = case Network of undefined -> ""; N -> ",\"network\":\"" ++ N ++ "\"" end,
|
||||
VcpuJson = case Vcpu of undefined -> ""; V -> ",\"vcpu\":" ++ integer_to_list(V) end,
|
||||
Json = "{\"name\":\"" ++ escape_json(Name) ++ "\"" ++ PortsJson ++ BootstrapJson ++ NetworkJson ++ VcpuJson ++ "}",
|
||||
Response = api_post("/services", Json, Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% @doc Destroy a service.
|
||||
service_destroy(ServiceId) -> service_destroy(ServiceId, #{}).
|
||||
service_destroy(ServiceId, Opts) ->
|
||||
Response = api_delete("/services/" ++ ServiceId, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Freeze a service.
|
||||
service_freeze(ServiceId) -> service_freeze(ServiceId, #{}).
|
||||
service_freeze(ServiceId, Opts) ->
|
||||
Response = api_post("/services/" ++ ServiceId ++ "/freeze", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Unfreeze a service.
|
||||
service_unfreeze(ServiceId) -> service_unfreeze(ServiceId, #{}).
|
||||
service_unfreeze(ServiceId, Opts) ->
|
||||
Response = api_post("/services/" ++ ServiceId ++ "/unfreeze", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Lock a service.
|
||||
service_lock(ServiceId) -> service_lock(ServiceId, #{}).
|
||||
service_lock(ServiceId, Opts) ->
|
||||
Response = api_post("/services/" ++ ServiceId ++ "/lock", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Unlock a service.
|
||||
service_unlock(ServiceId) -> service_unlock(ServiceId, #{}).
|
||||
service_unlock(ServiceId, Opts) ->
|
||||
Response = api_post("/services/" ++ ServiceId ++ "/unlock", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Set unfreeze-on-demand for a service.
|
||||
service_set_unfreeze_on_demand(ServiceId, Enabled) -> service_set_unfreeze_on_demand(ServiceId, Enabled, #{}).
|
||||
service_set_unfreeze_on_demand(ServiceId, Enabled, Opts) ->
|
||||
EnabledStr = if Enabled -> "true"; true -> "false" end,
|
||||
Json = "{\"unfreeze_on_demand\":" ++ EnabledStr ++ "}",
|
||||
Response = api_patch("/services/" ++ ServiceId, Json, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Redeploy a service.
|
||||
service_redeploy(ServiceId) -> service_redeploy(ServiceId, undefined, #{}).
|
||||
service_redeploy(ServiceId, Bootstrap) -> service_redeploy(ServiceId, Bootstrap, #{}).
|
||||
service_redeploy(ServiceId, Bootstrap, Opts) ->
|
||||
BootstrapJson = case Bootstrap of undefined -> ""; B -> "\"bootstrap\":\"" ++ escape_json(B) ++ "\"" end,
|
||||
Json = "{" ++ BootstrapJson ++ "}",
|
||||
Response = api_post("/services/" ++ ServiceId ++ "/redeploy", Json, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Get service logs.
|
||||
service_logs(ServiceId) -> service_logs(ServiceId, #{}).
|
||||
service_logs(ServiceId, Opts) ->
|
||||
AllLogs = maps:get(all_logs, Opts, false),
|
||||
Endpoint = if AllLogs -> "/services/" ++ ServiceId ++ "/logs?all=true"; true -> "/services/" ++ ServiceId ++ "/logs" end,
|
||||
api_get(Endpoint, Opts).
|
||||
|
||||
%% @doc Execute a command in a service.
|
||||
service_execute(ServiceId, Command) -> service_execute(ServiceId, Command, #{}).
|
||||
service_execute(ServiceId, Command, Opts) ->
|
||||
TimeoutMs = maps:get(timeout_ms, Opts, undefined),
|
||||
TimeoutJson = case TimeoutMs of undefined -> ""; T -> ",\"timeout_ms\":" ++ integer_to_list(T) end,
|
||||
Json = "{\"command\":\"" ++ escape_json(Command) ++ "\"" ++ TimeoutJson ++ "}",
|
||||
Response = api_post("/services/" ++ ServiceId ++ "/execute", Json, Opts),
|
||||
parse_result(Response).
|
||||
|
||||
%% @doc Get service environment vault.
|
||||
service_env_get(ServiceId) -> service_env_get(ServiceId, #{}).
|
||||
service_env_get(ServiceId, Opts) ->
|
||||
api_get("/services/" ++ ServiceId ++ "/env", Opts).
|
||||
|
||||
%% @doc Set service environment vault.
|
||||
service_env_set(ServiceId, EnvContent) -> service_env_set(ServiceId, EnvContent, #{}).
|
||||
service_env_set(ServiceId, EnvContent, Opts) ->
|
||||
api_put_text("/services/" ++ ServiceId ++ "/env", EnvContent, Opts).
|
||||
|
||||
%% @doc Delete service environment vault.
|
||||
service_env_delete(ServiceId) -> service_env_delete(ServiceId, #{}).
|
||||
service_env_delete(ServiceId, Opts) ->
|
||||
Response = api_delete("/services/" ++ ServiceId ++ "/env", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Export service environment vault.
|
||||
service_env_export(ServiceId) -> service_env_export(ServiceId, #{}).
|
||||
service_env_export(ServiceId, Opts) ->
|
||||
Response = api_post("/services/" ++ ServiceId ++ "/env/export", "{}", Opts),
|
||||
extract_json_field(Response, "content").
|
||||
|
||||
%% @doc Resize a service.
|
||||
service_resize(ServiceId, Vcpu) -> service_resize(ServiceId, Vcpu, #{}).
|
||||
service_resize(ServiceId, Vcpu, Opts) ->
|
||||
Json = "{\"vcpu\":" ++ integer_to_list(Vcpu) ++ "}",
|
||||
Response = api_patch("/services/" ++ ServiceId, Json, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% ============================================================================
|
||||
%% Snapshot Functions (9)
|
||||
%% ============================================================================
|
||||
|
||||
%% @doc List all snapshots.
|
||||
snapshot_list() -> snapshot_list(#{}).
|
||||
snapshot_list(Opts) -> api_get("/snapshots", Opts).
|
||||
|
||||
%% @doc Get snapshot details.
|
||||
snapshot_get(SnapshotId) -> snapshot_get(SnapshotId, #{}).
|
||||
snapshot_get(SnapshotId, Opts) ->
|
||||
Response = api_get("/snapshots/" ++ SnapshotId, Opts),
|
||||
#{
|
||||
id => SnapshotId,
|
||||
name => extract_json_field(Response, "name"),
|
||||
type => case extract_json_field(Response, "type") of "" -> "unknown"; T -> T end,
|
||||
source_id => extract_json_field(Response, "source_id"),
|
||||
hot => extract_json_field(Response, "hot") == "true",
|
||||
locked => extract_json_field(Response, "locked") == "true",
|
||||
created_at => extract_json_number(Response, "created_at"),
|
||||
size_bytes => extract_json_number(Response, "size_bytes")
|
||||
}.
|
||||
|
||||
%% @doc Create a snapshot of a session.
|
||||
snapshot_session(SessionId) -> snapshot_session(SessionId, #{}).
|
||||
snapshot_session(SessionId, Opts) ->
|
||||
Name = maps:get(name, Opts, undefined),
|
||||
Hot = maps:get(hot, Opts, false),
|
||||
NameJson = case Name of undefined -> ""; N -> "\"name\":\"" ++ escape_json(N) ++ "\"," end,
|
||||
HotJson = if Hot -> "\"hot\":true"; true -> "\"hot\":false" end,
|
||||
Json = "{" ++ NameJson ++ HotJson ++ "}",
|
||||
Response = api_post("/sessions/" ++ SessionId ++ "/snapshot", Json, Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% @doc Create a snapshot of a service.
|
||||
snapshot_service(ServiceId) -> snapshot_service(ServiceId, #{}).
|
||||
snapshot_service(ServiceId, Opts) ->
|
||||
Name = maps:get(name, Opts, undefined),
|
||||
Hot = maps:get(hot, Opts, false),
|
||||
NameJson = case Name of undefined -> ""; N -> "\"name\":\"" ++ escape_json(N) ++ "\"," end,
|
||||
HotJson = if Hot -> "\"hot\":true"; true -> "\"hot\":false" end,
|
||||
Json = "{" ++ NameJson ++ HotJson ++ "}",
|
||||
Response = api_post("/services/" ++ ServiceId ++ "/snapshot", Json, Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% @doc Restore from a snapshot.
|
||||
snapshot_restore(SnapshotId) -> snapshot_restore(SnapshotId, #{}).
|
||||
snapshot_restore(SnapshotId, Opts) ->
|
||||
Response = api_post("/snapshots/" ++ SnapshotId ++ "/restore", "{}", Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% @doc Delete a snapshot.
|
||||
snapshot_delete(SnapshotId) -> snapshot_delete(SnapshotId, #{}).
|
||||
snapshot_delete(SnapshotId, Opts) ->
|
||||
Response = api_delete("/snapshots/" ++ SnapshotId, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Lock a snapshot.
|
||||
snapshot_lock(SnapshotId) -> snapshot_lock(SnapshotId, #{}).
|
||||
snapshot_lock(SnapshotId, Opts) ->
|
||||
Response = api_post("/snapshots/" ++ SnapshotId ++ "/lock", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Unlock a snapshot.
|
||||
snapshot_unlock(SnapshotId) -> snapshot_unlock(SnapshotId, #{}).
|
||||
snapshot_unlock(SnapshotId, Opts) ->
|
||||
Response = api_post("/snapshots/" ++ SnapshotId ++ "/unlock", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Clone a snapshot to create a new session or service.
|
||||
snapshot_clone(SnapshotId, Opts) ->
|
||||
CloneType = maps:get(type, Opts),
|
||||
Name = maps:get(name, Opts, undefined),
|
||||
Ports = maps:get(ports, Opts, undefined),
|
||||
Shell = maps:get(shell, Opts, undefined),
|
||||
TypeJson = "\"type\":\"" ++ CloneType ++ "\"",
|
||||
NameJson = case Name of undefined -> ""; N -> ",\"name\":\"" ++ escape_json(N) ++ "\"" end,
|
||||
PortsJson = case Ports of undefined -> ""; P -> ",\"ports\":[" ++ P ++ "]" end,
|
||||
ShellJson = case Shell of undefined -> ""; S -> ",\"shell\":\"" ++ S ++ "\"" end,
|
||||
Json = "{" ++ TypeJson ++ NameJson ++ PortsJson ++ ShellJson ++ "}",
|
||||
Response = api_post("/snapshots/" ++ SnapshotId ++ "/clone", Json, Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% ============================================================================
|
||||
%% Image Functions (13)
|
||||
%% ============================================================================
|
||||
|
||||
%% @doc List images.
|
||||
image_list() -> image_list(#{}).
|
||||
image_list(Opts) ->
|
||||
Filter = maps:get(filter, Opts, undefined),
|
||||
Endpoint = case Filter of undefined -> "/images"; F -> "/images?filter=" ++ F end,
|
||||
api_get(Endpoint, Opts).
|
||||
|
||||
%% @doc Get image details.
|
||||
image_get(ImageId) -> image_get(ImageId, #{}).
|
||||
image_get(ImageId, Opts) ->
|
||||
Response = api_get("/images/" ++ ImageId, Opts),
|
||||
#{
|
||||
id => ImageId,
|
||||
name => extract_json_field(Response, "name"),
|
||||
description => extract_json_field(Response, "description"),
|
||||
visibility => case extract_json_field(Response, "visibility") of "" -> "private"; V -> V end,
|
||||
source_type => extract_json_field(Response, "source_type"),
|
||||
source_id => extract_json_field(Response, "source_id"),
|
||||
locked => extract_json_field(Response, "locked") == "true",
|
||||
created_at => extract_json_number(Response, "created_at"),
|
||||
size_bytes => extract_json_number(Response, "size_bytes")
|
||||
}.
|
||||
|
||||
%% @doc Publish an image.
|
||||
image_publish(SourceType, SourceId) -> image_publish(SourceType, SourceId, #{}).
|
||||
image_publish(SourceType, SourceId, Opts) ->
|
||||
Name = maps:get(name, Opts, undefined),
|
||||
Description = maps:get(description, Opts, undefined),
|
||||
NameJson = case Name of undefined -> ""; N -> ",\"name\":\"" ++ escape_json(N) ++ "\"" end,
|
||||
DescJson = case Description of undefined -> ""; D -> ",\"description\":\"" ++ escape_json(D) ++ "\"" end,
|
||||
Json = "{\"source_type\":\"" ++ SourceType ++ "\",\"source_id\":\"" ++ SourceId ++ "\"" ++ NameJson ++ DescJson ++ "}",
|
||||
Response = api_post("/images/publish", Json, Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% @doc Delete an image.
|
||||
image_delete(ImageId) -> image_delete(ImageId, #{}).
|
||||
image_delete(ImageId, Opts) ->
|
||||
Response = api_delete("/images/" ++ ImageId, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Lock an image.
|
||||
image_lock(ImageId) -> image_lock(ImageId, #{}).
|
||||
image_lock(ImageId, Opts) ->
|
||||
Response = api_post("/images/" ++ ImageId ++ "/lock", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Unlock an image.
|
||||
image_unlock(ImageId) -> image_unlock(ImageId, #{}).
|
||||
image_unlock(ImageId, Opts) ->
|
||||
Response = api_post("/images/" ++ ImageId ++ "/unlock", "{}", Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Set image visibility.
|
||||
image_set_visibility(ImageId, Visibility) -> image_set_visibility(ImageId, Visibility, #{}).
|
||||
image_set_visibility(ImageId, Visibility, Opts) ->
|
||||
Json = "{\"visibility\":\"" ++ Visibility ++ "\"}",
|
||||
Response = api_post("/images/" ++ ImageId ++ "/visibility", Json, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Grant access to an image.
|
||||
image_grant_access(ImageId, TrustedApiKey) -> image_grant_access(ImageId, TrustedApiKey, #{}).
|
||||
image_grant_access(ImageId, TrustedApiKey, Opts) ->
|
||||
Json = "{\"api_key\":\"" ++ TrustedApiKey ++ "\"}",
|
||||
Response = api_post("/images/" ++ ImageId ++ "/access/grant", Json, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Revoke access to an image.
|
||||
image_revoke_access(ImageId, TrustedApiKey) -> image_revoke_access(ImageId, TrustedApiKey, #{}).
|
||||
image_revoke_access(ImageId, TrustedApiKey, Opts) ->
|
||||
Json = "{\"api_key\":\"" ++ TrustedApiKey ++ "\"}",
|
||||
Response = api_post("/images/" ++ ImageId ++ "/access/revoke", Json, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc List trusted API keys for an image.
|
||||
image_list_trusted(ImageId) -> image_list_trusted(ImageId, #{}).
|
||||
image_list_trusted(ImageId, Opts) ->
|
||||
Response = api_get("/images/" ++ ImageId ++ "/access", Opts),
|
||||
extract_json_array(Response, "trusted_keys").
|
||||
|
||||
%% @doc Transfer image ownership.
|
||||
image_transfer(ImageId, ToApiKey) -> image_transfer(ImageId, ToApiKey, #{}).
|
||||
image_transfer(ImageId, ToApiKey, Opts) ->
|
||||
Json = "{\"to_api_key\":\"" ++ ToApiKey ++ "\"}",
|
||||
Response = api_post("/images/" ++ ImageId ++ "/transfer", Json, Opts),
|
||||
not_contains_error(Response).
|
||||
|
||||
%% @doc Spawn a service from an image.
|
||||
image_spawn(ImageId) -> image_spawn(ImageId, #{}).
|
||||
image_spawn(ImageId, Opts) ->
|
||||
Name = maps:get(name, Opts, undefined),
|
||||
Ports = maps:get(ports, Opts, undefined),
|
||||
Bootstrap = maps:get(bootstrap, Opts, undefined),
|
||||
Network = maps:get(network, Opts, undefined),
|
||||
NameJson = case Name of undefined -> ""; N -> "\"name\":\"" ++ escape_json(N) ++ "\"" end,
|
||||
PortsJson = case Ports of undefined -> ""; P -> (if Name =/= undefined -> ","; true -> "" end) ++ "\"ports\":[" ++ P ++ "]" end,
|
||||
BootstrapJson = case Bootstrap of undefined -> ""; B -> ",\"bootstrap\":\"" ++ escape_json(B) ++ "\"" end,
|
||||
NetworkJson = case Network of undefined -> ""; Nn -> ",\"network\":\"" ++ Nn ++ "\"" end,
|
||||
Json = "{" ++ NameJson ++ PortsJson ++ BootstrapJson ++ NetworkJson ++ "}",
|
||||
Response = api_post("/images/" ++ ImageId ++ "/spawn", Json, Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% @doc Clone an image.
|
||||
image_clone(ImageId) -> image_clone(ImageId, #{}).
|
||||
image_clone(ImageId, Opts) ->
|
||||
Name = maps:get(name, Opts, undefined),
|
||||
Description = maps:get(description, Opts, undefined),
|
||||
NameJson = case Name of undefined -> ""; N -> "\"name\":\"" ++ escape_json(N) ++ "\"" end,
|
||||
DescJson = case Description of undefined -> ""; D -> (if Name =/= undefined -> ","; true -> "" end) ++ "\"description\":\"" ++ escape_json(D) ++ "\"" end,
|
||||
Json = "{" ++ NameJson ++ DescJson ++ "}",
|
||||
Response = api_post("/images/" ++ ImageId ++ "/clone", Json, Opts),
|
||||
extract_json_field(Response, "id").
|
||||
|
||||
%% ============================================================================
|
||||
%% PaaS Logs Functions (2)
|
||||
%% ============================================================================
|
||||
|
||||
%% @doc Fetch batch logs from portal.
|
||||
logs_fetch() -> logs_fetch(#{}).
|
||||
logs_fetch(Opts) ->
|
||||
Source = maps:get(source, Opts, "all"),
|
||||
Lines = maps:get(lines, Opts, 100),
|
||||
Since = maps:get(since, Opts, "1h"),
|
||||
Grep = maps:get(grep, Opts, undefined),
|
||||
GrepParam = case Grep of undefined -> ""; G -> "&grep=" ++ http_uri:encode(G) end,
|
||||
api_get("/logs?source=" ++ Source ++ "&lines=" ++ integer_to_list(Lines) ++ "&since=" ++ Since ++ GrepParam, Opts).
|
||||
|
||||
%% @doc Stream logs (simplified polling implementation).
|
||||
logs_stream(Callback) -> logs_stream(Callback, #{}).
|
||||
logs_stream(Callback, Opts) ->
|
||||
Source = maps:get(source, Opts, "all"),
|
||||
Grep = maps:get(grep, Opts, undefined),
|
||||
Interval = maps:get(interval, Opts, 5000),
|
||||
GrepParam = case Grep of undefined -> ""; G -> "&grep=" ++ http_uri:encode(G) end,
|
||||
logs_stream_loop(Source, GrepParam, Callback, Interval, Opts).
|
||||
|
||||
logs_stream_loop(Source, GrepParam, Callback, Interval, Opts) ->
|
||||
Response = api_get("/logs?source=" ++ Source ++ "&lines=50&since=10s" ++ GrepParam, Opts),
|
||||
Callback(Source, Response),
|
||||
timer:sleep(Interval),
|
||||
logs_stream_loop(Source, GrepParam, Callback, Interval, Opts).
|
||||
|
||||
%% ============================================================================
|
||||
%% Key Validation (1)
|
||||
%% ============================================================================
|
||||
|
||||
%% @doc Validate API keys.
|
||||
validate_keys() -> validate_keys(#{}).
|
||||
validate_keys(Opts) ->
|
||||
Response = portal_post("/keys/validate", "{}", Opts),
|
||||
#{
|
||||
valid => extract_json_field(Response, "status") == "valid",
|
||||
tier => extract_json_field(Response, "tier"),
|
||||
rate_limit_per_minute => extract_json_number(Response, "rate_per_minute"),
|
||||
concurrency_limit => extract_json_number(Response, "concurrency"),
|
||||
expires_at => extract_json_number(Response, "expires_at")
|
||||
}.
|
||||
|
||||
%% ============================================================================
|
||||
%% Private API Functions
|
||||
%% ============================================================================
|
||||
|
||||
api_get(Endpoint, Opts) ->
|
||||
{PublicKey, SecretKey} = get_api_keys_from_opts(Opts),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "GET", Endpoint, ""),
|
||||
Cmd = "curl -s " ++ ?API_BASE ++ Endpoint ++ AuthHeaders,
|
||||
Result = os:cmd(Cmd),
|
||||
check_clock_drift_error(Result),
|
||||
Result.
|
||||
|
||||
api_post(Endpoint, Json, Opts) ->
|
||||
{PublicKey, SecretKey} = get_api_keys_from_opts(Opts),
|
||||
TmpFile = write_temp_file(Json),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "POST", Endpoint, Json),
|
||||
Cmd = "curl -s -X POST " ++ ?API_BASE ++ Endpoint ++ " -H 'Content-Type: application/json'" ++ AuthHeaders ++ " -d @" ++ TmpFile,
|
||||
Result = os:cmd(Cmd),
|
||||
file:delete(TmpFile),
|
||||
check_clock_drift_error(Result),
|
||||
Result.
|
||||
|
||||
api_delete(Endpoint, Opts) ->
|
||||
{PublicKey, SecretKey} = get_api_keys_from_opts(Opts),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "DELETE", Endpoint, ""),
|
||||
Cmd = "curl -s -X DELETE " ++ ?API_BASE ++ Endpoint ++ AuthHeaders,
|
||||
Result = os:cmd(Cmd),
|
||||
check_clock_drift_error(Result),
|
||||
Result.
|
||||
|
||||
api_patch(Endpoint, Json, Opts) ->
|
||||
{PublicKey, SecretKey} = get_api_keys_from_opts(Opts),
|
||||
TmpFile = write_temp_file(Json),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "PATCH", Endpoint, Json),
|
||||
Cmd = "curl -s -X PATCH " ++ ?API_BASE ++ Endpoint ++ " -H 'Content-Type: application/json'" ++ AuthHeaders ++ " -d @" ++ TmpFile,
|
||||
Result = os:cmd(Cmd),
|
||||
file:delete(TmpFile),
|
||||
check_clock_drift_error(Result),
|
||||
Result.
|
||||
|
||||
api_put_text(Endpoint, Body, Opts) ->
|
||||
{PublicKey, SecretKey} = get_api_keys_from_opts(Opts),
|
||||
TmpFile = write_temp_file(Body),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "PUT", Endpoint, Body),
|
||||
Cmd = "curl -s -o /dev/null -w '%{http_code}' -X PUT " ++ ?API_BASE ++ Endpoint ++ " -H 'Content-Type: text/plain'" ++ AuthHeaders ++ " -d @" ++ TmpFile,
|
||||
Result = os:cmd(Cmd),
|
||||
file:delete(TmpFile),
|
||||
StatusCode = list_to_integer(string:trim(Result)),
|
||||
StatusCode >= 200 andalso StatusCode < 300.
|
||||
|
||||
portal_post(Endpoint, Json, Opts) ->
|
||||
{PublicKey, SecretKey} = get_api_keys_from_opts(Opts),
|
||||
TmpFile = write_temp_file(Json),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "POST", Endpoint, Json),
|
||||
Cmd = "curl -s -X POST " ++ ?PORTAL_BASE ++ Endpoint ++ " -H 'Content-Type: application/json'" ++ AuthHeaders ++ " -d @" ++ TmpFile,
|
||||
Result = os:cmd(Cmd),
|
||||
file:delete(TmpFile),
|
||||
check_clock_drift_error(Result),
|
||||
Result.
|
||||
|
||||
get_api_keys_from_opts(Opts) ->
|
||||
case {maps:get(public_key, Opts, undefined), maps:get(secret_key, Opts, undefined)} of
|
||||
{Pk, Sk} when Pk =/= undefined, Sk =/= undefined -> {Pk, Sk};
|
||||
_ -> get_api_keys()
|
||||
end.
|
||||
|
||||
build_execute_json_full(Language, Code, Opts) ->
|
||||
Network = maps:get(network, Opts, undefined),
|
||||
Vcpu = maps:get(vcpu, Opts, undefined),
|
||||
Ttl = maps:get(ttl, Opts, undefined),
|
||||
ReturnArtifacts = maps:get(return_artifacts, Opts, false),
|
||||
NetworkJson = case Network of undefined -> ""; N -> ",\"network\":\"" ++ N ++ "\"" end,
|
||||
VcpuJson = case Vcpu of undefined -> ""; V -> ",\"vcpu\":" ++ integer_to_list(V) end,
|
||||
TtlJson = case Ttl of undefined -> ""; T -> ",\"ttl\":" ++ integer_to_list(T) end,
|
||||
ArtifactsJson = if ReturnArtifacts -> ",\"return_artifacts\":true"; true -> "" end,
|
||||
"{\"language\":\"" ++ Language ++ "\",\"code\":\"" ++ escape_json(Code) ++ "\"" ++ NetworkJson ++ VcpuJson ++ TtlJson ++ ArtifactsJson ++ "}".
|
||||
|
||||
parse_result(Response) ->
|
||||
ExitCode = case extract_json_number(Response, "exit_code") of 0 -> 0; N when is_integer(N) -> N; _ -> 0 end,
|
||||
#{
|
||||
success => ExitCode == 0,
|
||||
stdout => case extract_json_field(Response, "stdout") of "" -> ""; S -> S end,
|
||||
stderr => case extract_json_field(Response, "stderr") of "" -> ""; S -> S end,
|
||||
exit_code => ExitCode,
|
||||
job_id => extract_json_field(Response, "job_id"),
|
||||
language => extract_json_field(Response, "language"),
|
||||
execution_time => undefined
|
||||
}.
|
||||
|
||||
not_contains_error(Response) ->
|
||||
string:str(Response, "\"error\"") == 0.
|
||||
|
||||
%% ============================================================================
|
||||
%% CLI Entry Point
|
||||
%% ============================================================================
|
||||
|
||||
main([]) ->
|
||||
io:format("Usage: un.erl [options] <source_file>~n"),
|
||||
|
|
|
|||
148
clients/erlang/sync/tests/test_functional.erl
Executable file
148
clients/erlang/sync/tests/test_functional.erl
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/env escript
|
||||
%% -*- erlang -*-
|
||||
%%
|
||||
%% Functional Tests for Un Erlang SDK
|
||||
%%
|
||||
%% Run with: escript test_functional.erl
|
||||
%% Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables
|
||||
%%
|
||||
%% These tests make real API calls to api.unsandbox.com
|
||||
|
||||
-mode(compile).
|
||||
|
||||
main([]) ->
|
||||
io:format("\n\033[34m=== Un Erlang SDK Functional Tests ===\033[0m\n\n"),
|
||||
|
||||
%% Check for credentials
|
||||
case {os:getenv("UNSANDBOX_PUBLIC_KEY"), os:getenv("UNSANDBOX_SECRET_KEY")} of
|
||||
{false, _} ->
|
||||
io:format("\033[33mSKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set\033[0m\n"),
|
||||
halt(0);
|
||||
{_, false} ->
|
||||
io:format("\033[33mSKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set\033[0m\n"),
|
||||
halt(0);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
|
||||
Tests = [
|
||||
{"health_check", fun test_health_check/0},
|
||||
{"validate_keys", fun test_validate_keys/0},
|
||||
{"execute_python", fun test_execute_python/0},
|
||||
{"execute_with_error", fun test_execute_with_error/0},
|
||||
{"session_list", fun test_session_list/0},
|
||||
{"service_list", fun test_service_list/0},
|
||||
{"snapshot_list", fun test_snapshot_list/0},
|
||||
{"image_list", fun test_image_list/0},
|
||||
{"get_languages", fun test_get_languages/0}
|
||||
],
|
||||
|
||||
Results = run_tests(Tests, []),
|
||||
{Passed, Failed} = count_results(Results, 0, 0),
|
||||
Total = length(Results),
|
||||
|
||||
io:format("\n\033[34mResults: ~p/~p passed\033[0m\n", [Passed, Total]),
|
||||
case Failed > 0 of
|
||||
true ->
|
||||
io:format("\033[31m~p test(s) failed\033[0m\n", [Failed]),
|
||||
halt(1);
|
||||
false ->
|
||||
io:format("\033[32mAll functional tests passed!\033[0m\n")
|
||||
end.
|
||||
|
||||
run_tests([], Acc) ->
|
||||
lists:reverse(Acc);
|
||||
run_tests([{Name, TestFn} | Rest], Acc) ->
|
||||
io:format(" Running ~s... ", [Name]),
|
||||
Result = try
|
||||
TestFn(),
|
||||
io:format("\033[32mPASS\033[0m\n"),
|
||||
pass
|
||||
catch
|
||||
_:Error ->
|
||||
io:format("\033[31mFAIL\033[0m\n"),
|
||||
io:format(" ~p\n", [Error]),
|
||||
fail
|
||||
end,
|
||||
run_tests(Rest, [Result | Acc]).
|
||||
|
||||
count_results([], Passed, Failed) ->
|
||||
{Passed, Failed};
|
||||
count_results([pass | Rest], Passed, Failed) ->
|
||||
count_results(Rest, Passed + 1, Failed);
|
||||
count_results([fail | Rest], Passed, Failed) ->
|
||||
count_results(Rest, Passed, Failed + 1).
|
||||
|
||||
%% ============================================================================
|
||||
%% Functional Tests
|
||||
%% ============================================================================
|
||||
|
||||
test_health_check() ->
|
||||
Result = un:health_check(),
|
||||
true = is_boolean(Result),
|
||||
ok.
|
||||
|
||||
test_validate_keys() ->
|
||||
KeyInfo = un:validate_keys(),
|
||||
true = is_map(KeyInfo),
|
||||
true = maps:is_key(valid, KeyInfo),
|
||||
true = is_boolean(maps:get(valid, KeyInfo)),
|
||||
ok.
|
||||
|
||||
test_execute_python() ->
|
||||
Result = un:execute("python", "print(6 * 7)"),
|
||||
true = is_map(Result),
|
||||
true = maps:is_key(success, Result),
|
||||
true = maps:is_key(stdout, Result),
|
||||
true = maps:is_key(exit_code, Result),
|
||||
|
||||
%% Check output
|
||||
true = maps:get(success, Result),
|
||||
Stdout = maps:get(stdout, Result),
|
||||
true = string:find(Stdout, "42") =/= nomatch,
|
||||
0 = maps:get(exit_code, Result),
|
||||
ok.
|
||||
|
||||
test_execute_with_error() ->
|
||||
Result = un:execute("python", "import sys; sys.exit(1)"),
|
||||
true = is_map(Result),
|
||||
false = maps:get(success, Result),
|
||||
1 = maps:get(exit_code, Result),
|
||||
ok.
|
||||
|
||||
test_session_list() ->
|
||||
Response = un:session_list(),
|
||||
true = is_list(Response),
|
||||
%% Response should be valid JSON (starts with [ or {)
|
||||
Trimmed = string:trim(Response),
|
||||
true = (string:prefix(Trimmed, "[") =/= nomatch) orelse (string:prefix(Trimmed, "{") =/= nomatch),
|
||||
ok.
|
||||
|
||||
test_service_list() ->
|
||||
Response = un:service_list(),
|
||||
true = is_list(Response),
|
||||
Trimmed = string:trim(Response),
|
||||
true = (string:prefix(Trimmed, "[") =/= nomatch) orelse (string:prefix(Trimmed, "{") =/= nomatch),
|
||||
ok.
|
||||
|
||||
test_snapshot_list() ->
|
||||
Response = un:snapshot_list(),
|
||||
true = is_list(Response),
|
||||
Trimmed = string:trim(Response),
|
||||
true = (string:prefix(Trimmed, "[") =/= nomatch) orelse (string:prefix(Trimmed, "{") =/= nomatch),
|
||||
ok.
|
||||
|
||||
test_image_list() ->
|
||||
Response = un:image_list(),
|
||||
true = is_list(Response),
|
||||
Trimmed = string:trim(Response),
|
||||
true = (string:prefix(Trimmed, "[") =/= nomatch) orelse (string:prefix(Trimmed, "{") =/= nomatch),
|
||||
ok.
|
||||
|
||||
test_get_languages() ->
|
||||
Languages = un:get_languages(),
|
||||
true = is_list(Languages),
|
||||
true = length(Languages) > 0,
|
||||
true = lists:member("python", Languages),
|
||||
true = lists:member("javascript", Languages),
|
||||
ok.
|
||||
109
clients/erlang/sync/tests/test_library.erl
Executable file
109
clients/erlang/sync/tests/test_library.erl
Executable file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env escript
|
||||
%% -*- erlang -*-
|
||||
%%
|
||||
%% Unit Tests for Un Erlang SDK Library Functions
|
||||
%%
|
||||
%% Run with: escript test_library.erl
|
||||
%% No credentials required - tests pure library functions only.
|
||||
|
||||
-mode(compile).
|
||||
|
||||
main([]) ->
|
||||
io:format("\n\033[34m=== Un Erlang SDK Library Tests ===\033[0m\n\n"),
|
||||
|
||||
Tests = [
|
||||
{"version", fun test_version/0},
|
||||
{"detect_language", fun test_detect_language/0},
|
||||
{"hmac_sign", fun test_hmac_sign/0},
|
||||
{"hmac_sign_deterministic", fun test_hmac_sign_deterministic/0},
|
||||
{"hmac_sign_different_secrets", fun test_hmac_sign_different_secrets/0}
|
||||
],
|
||||
|
||||
Results = run_tests(Tests, []),
|
||||
{Passed, Failed} = count_results(Results, 0, 0),
|
||||
Total = length(Results),
|
||||
|
||||
io:format("\n\033[34mResults: ~p/~p passed\033[0m\n", [Passed, Total]),
|
||||
case Failed > 0 of
|
||||
true ->
|
||||
io:format("\033[31m~p test(s) failed\033[0m\n", [Failed]),
|
||||
halt(1);
|
||||
false ->
|
||||
io:format("\033[32mAll tests passed!\033[0m\n")
|
||||
end.
|
||||
|
||||
run_tests([], Acc) ->
|
||||
lists:reverse(Acc);
|
||||
run_tests([{Name, TestFn} | Rest], Acc) ->
|
||||
Result = try
|
||||
TestFn(),
|
||||
io:format("\033[32mPASS\033[0m: ~s\n", [Name]),
|
||||
pass
|
||||
catch
|
||||
_:Error ->
|
||||
io:format("\033[31mFAIL\033[0m: ~s - ~p\n", [Name, Error]),
|
||||
fail
|
||||
end,
|
||||
run_tests(Rest, [Result | Acc]).
|
||||
|
||||
count_results([], Passed, Failed) ->
|
||||
{Passed, Failed};
|
||||
count_results([pass | Rest], Passed, Failed) ->
|
||||
count_results(Rest, Passed + 1, Failed);
|
||||
count_results([fail | Rest], Passed, Failed) ->
|
||||
count_results(Rest, Passed, Failed + 1).
|
||||
|
||||
%% ============================================================================
|
||||
%% Unit Tests
|
||||
%% ============================================================================
|
||||
|
||||
test_version() ->
|
||||
Version = un:version(),
|
||||
true = is_list(Version),
|
||||
%% Should be semver format X.Y.Z
|
||||
[_Major, _Minor, _Patch] = string:tokens(Version, "."),
|
||||
ok.
|
||||
|
||||
test_detect_language() ->
|
||||
%% Test common extensions
|
||||
{ok, "python"} = un:ext_to_lang(".py"),
|
||||
{ok, "javascript"} = un:ext_to_lang(".js"),
|
||||
{ok, "go"} = un:ext_to_lang(".go"),
|
||||
{ok, "rust"} = un:ext_to_lang(".rs"),
|
||||
{ok, "c"} = un:ext_to_lang(".c"),
|
||||
{ok, "cpp"} = un:ext_to_lang(".cpp"),
|
||||
{ok, "java"} = un:ext_to_lang(".java"),
|
||||
{ok, "ruby"} = un:ext_to_lang(".rb"),
|
||||
{ok, "bash"} = un:ext_to_lang(".sh"),
|
||||
{ok, "lua"} = un:ext_to_lang(".lua"),
|
||||
{ok, "perl"} = un:ext_to_lang(".pl"),
|
||||
{ok, "php"} = un:ext_to_lang(".php"),
|
||||
{ok, "haskell"} = un:ext_to_lang(".hs"),
|
||||
{ok, "ocaml"} = un:ext_to_lang(".ml"),
|
||||
{ok, "elixir"} = un:ext_to_lang(".ex"),
|
||||
{ok, "erlang"} = un:ext_to_lang(".erl"),
|
||||
|
||||
%% Test unknown extensions
|
||||
{error, _} = un:ext_to_lang(".unknown"),
|
||||
{error, _} = un:ext_to_lang(""),
|
||||
ok.
|
||||
|
||||
test_hmac_sign() ->
|
||||
Signature = un:hmac_sign("my_secret", "test message"),
|
||||
true = is_list(Signature),
|
||||
64 = length(Signature),
|
||||
%% Should be lowercase hex
|
||||
true = lists:all(fun(C) -> (C >= $0 andalso C =< $9) orelse (C >= $a andalso C =< $f) end, Signature),
|
||||
ok.
|
||||
|
||||
test_hmac_sign_deterministic() ->
|
||||
Sig1 = un:hmac_sign("test_secret", "same message"),
|
||||
Sig2 = un:hmac_sign("test_secret", "same message"),
|
||||
Sig1 = Sig2, %% Pattern match ensures equality
|
||||
ok.
|
||||
|
||||
test_hmac_sign_different_secrets() ->
|
||||
Sig1 = un:hmac_sign("secret1", "test message"),
|
||||
Sig2 = un:hmac_sign("secret2", "test message"),
|
||||
true = Sig1 =/= Sig2, %% Different secrets should produce different signatures
|
||||
ok.
|
||||
|
|
@ -1369,6 +1369,382 @@
|
|||
s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh \"$@\" && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Image grant access
|
||||
: image-grant-access ( image-id-addr image-id-len key-addr key-len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" IMAGE_ID='" r@ write-file throw
|
||||
2over r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TRUSTED_KEY='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" BODY='{\"trusted_api_key\":\"'$TRUSTED_KEY'\"}'" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/images/$IMAGE_ID/grant-access:$BODY\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/images/$IMAGE_ID/grant-access -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" >/dev/null && echo -e \"\\x1b[32mAccess granted to $TRUSTED_KEY\\x1b[0m\"" r@ write-line throw
|
||||
r> close-file throw
|
||||
2drop 2drop \ clean up the stack
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Image revoke access
|
||||
: image-revoke-access ( image-id-addr image-id-len key-addr key-len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" IMAGE_ID='" r@ write-file throw
|
||||
2over r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TRUSTED_KEY='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" BODY='{\"trusted_api_key\":\"'$TRUSTED_KEY'\"}'" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/images/$IMAGE_ID/revoke-access:$BODY\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/images/$IMAGE_ID/revoke-access -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" >/dev/null && echo -e \"\\x1b[32mAccess revoked from $TRUSTED_KEY\\x1b[0m\"" r@ write-line throw
|
||||
r> close-file throw
|
||||
2drop 2drop \ clean up the stack
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Image list trusted
|
||||
: image-list-trusted ( addr len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" IMAGE_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:GET:/images/$IMAGE_ID/trusted:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/images/$IMAGE_ID/trusted -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq ." r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Snapshot list
|
||||
: snapshot-list ( -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:GET:/snapshots:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/snapshots -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq ." r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Snapshot info
|
||||
: snapshot-info ( addr len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" SNAPSHOT_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:GET:/snapshots/$SNAPSHOT_ID:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/snapshots/$SNAPSHOT_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq ." r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Snapshot restore
|
||||
: snapshot-restore ( addr len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" SNAPSHOT_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/snapshots/$SNAPSHOT_ID/restore:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/snapshots/$SNAPSHOT_ID/restore -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mSnapshot restored: " r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" \\x1b[0m'" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Snapshot delete
|
||||
: snapshot-delete ( addr len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" SNAPSHOT_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:DELETE:/snapshots/$SNAPSHOT_ID:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" RESP=$(curl -s -w '\\n%{http_code}' -X DELETE https://api.unsandbox.com/snapshots/$SNAPSHOT_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\")" r@ write-line throw
|
||||
s" HTTP_CODE=$(echo \"$RESP\" | tail -1)" r@ write-line throw
|
||||
s" BODY=$(echo \"$RESP\" | sed '$d')" r@ write-line throw
|
||||
s" if [ \"$HTTP_CODE\" = \"428\" ]; then" r@ write-line throw
|
||||
s" CHALLENGE_ID=$(echo \"$BODY\" | grep -o '\"challenge_id\":\"[^\"]*\"' | cut -d'\"' -f4)" r@ write-line throw
|
||||
s" echo -e '\\x1b[33mConfirmation required. Check your email for a one-time code.\\x1b[0m' >&2" r@ write-line throw
|
||||
s" echo -n 'Enter OTP: ' >&2" r@ write-line throw
|
||||
s" read OTP" r@ write-line throw
|
||||
s" if [ -z \"$OTP\" ]; then echo 'Error: Operation cancelled' >&2; exit 1; fi" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:DELETE:/snapshots/$SNAPSHOT_ID:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" RESP=$(curl -s -w '\\n%{http_code}' -X DELETE https://api.unsandbox.com/snapshots/$SNAPSHOT_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -H \"X-Sudo-OTP: $OTP\" -H \"X-Sudo-Challenge: $CHALLENGE_ID\")" r@ write-line throw
|
||||
s" HTTP_CODE=$(echo \"$RESP\" | tail -1)" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" if [ \"$HTTP_CODE\" = \"200\" ]; then" r@ write-line throw
|
||||
s" echo -e '\\x1b[32mSnapshot deleted: " r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" \\x1b[0m'" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" echo -e \"\\x1b[31mError: HTTP $HTTP_CODE\\x1b[0m\" >&2" r@ write-line throw
|
||||
s" echo \"$BODY\" >&2" r@ write-line throw
|
||||
s" exit 1" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Snapshot lock
|
||||
: snapshot-lock ( addr len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" SNAPSHOT_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/snapshots/$SNAPSHOT_ID/lock:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/snapshots/$SNAPSHOT_ID/lock -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mSnapshot locked: " r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" \\x1b[0m'" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Snapshot unlock
|
||||
: snapshot-unlock ( addr len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" SNAPSHOT_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/snapshots/$SNAPSHOT_ID/unlock:{}\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" RESP=$(curl -s -w '\\n%{http_code}' -X POST https://api.unsandbox.com/snapshots/$SNAPSHOT_ID/unlock -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -H 'Content-Type: application/json' -d '{}')" r@ write-line throw
|
||||
s" HTTP_CODE=$(echo \"$RESP\" | tail -1)" r@ write-line throw
|
||||
s" BODY=$(echo \"$RESP\" | sed '$d')" r@ write-line throw
|
||||
s" if [ \"$HTTP_CODE\" = \"428\" ]; then" r@ write-line throw
|
||||
s" CHALLENGE_ID=$(echo \"$BODY\" | grep -o '\"challenge_id\":\"[^\"]*\"' | cut -d'\"' -f4)" r@ write-line throw
|
||||
s" echo -e '\\x1b[33mConfirmation required. Check your email for a one-time code.\\x1b[0m' >&2" r@ write-line throw
|
||||
s" echo -n 'Enter OTP: ' >&2" r@ write-line throw
|
||||
s" read OTP" r@ write-line throw
|
||||
s" if [ -z \"$OTP\" ]; then echo 'Error: Operation cancelled' >&2; exit 1; fi" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/snapshots/$SNAPSHOT_ID/unlock:{}\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" RESP=$(curl -s -w '\\n%{http_code}' -X POST https://api.unsandbox.com/snapshots/$SNAPSHOT_ID/unlock -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -H 'Content-Type: application/json' -H \"X-Sudo-OTP: $OTP\" -H \"X-Sudo-Challenge: $CHALLENGE_ID\" -d '{}')" r@ write-line throw
|
||||
s" HTTP_CODE=$(echo \"$RESP\" | tail -1)" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" if [ \"$HTTP_CODE\" = \"200\" ]; then" r@ write-line throw
|
||||
s" echo -e '\\x1b[32mSnapshot unlocked: " r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" \\x1b[0m'" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" echo -e \"\\x1b[31mError: HTTP $HTTP_CODE\\x1b[0m\" >&2" r@ write-line throw
|
||||
s" echo \"$BODY\" >&2" r@ write-line throw
|
||||
s" exit 1" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Snapshot clone
|
||||
: snapshot-clone ( addr len -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" SNAPSHOT_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" CLONE_TYPE='session'; NAME=''; PORTS=''; SHELL=''" r@ write-line throw
|
||||
s" i=4" r@ write-line throw
|
||||
s" while [ $i -le $# ]; do" r@ write-line throw
|
||||
s" arg=${!i}" r@ write-line throw
|
||||
s" case \"$arg\" in" r@ write-line throw
|
||||
s" --type) ((i++)); CLONE_TYPE=${!i} ;;" r@ write-line throw
|
||||
s" --name) ((i++)); NAME=${!i} ;;" r@ write-line throw
|
||||
s" --ports) ((i++)); PORTS=${!i} ;;" r@ write-line throw
|
||||
s" --shell) ((i++)); SHELL=${!i} ;;" r@ write-line throw
|
||||
s" esac" r@ write-line throw
|
||||
s" ((i++))" r@ write-line throw
|
||||
s" done" r@ write-line throw
|
||||
s" BODY='{\"clone_type\":\"'$CLONE_TYPE'\"}'" r@ write-line throw
|
||||
s" [ -n \"$NAME\" ] && BODY=$(echo $BODY | jq --arg n \"$NAME\" '. + {name: $n}')" r@ write-line throw
|
||||
s" [ -n \"$PORTS\" ] && BODY=$(echo $BODY | jq --arg p \"$PORTS\" '. + {ports: ($p | split(\",\") | map(tonumber))}')" r@ write-line throw
|
||||
s" [ -n \"$SHELL\" ] && BODY=$(echo $BODY | jq --arg s \"$SHELL\" '. + {shell: $s}')" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/snapshots/$SNAPSHOT_ID/clone:$BODY\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/snapshots/$SNAPSHOT_ID/clone -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" | jq ." r@ write-line throw
|
||||
s" echo -e '\\x1b[32mSnapshot cloned\\x1b[0m'" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh \"$@\" && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Handle snapshot subcommand
|
||||
: handle-snapshot ( -- )
|
||||
argc @ 3 < if
|
||||
snapshot-list
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2 arg 2dup s" --list" compare 0= if
|
||||
2drop snapshot-list
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" -l" compare 0= if
|
||||
2drop snapshot-list
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" --info" compare 0= if
|
||||
2drop
|
||||
argc @ 4 < if
|
||||
s" Error: --info requires snapshot ID" type cr
|
||||
1 (bye)
|
||||
then
|
||||
3 arg snapshot-info
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" --restore" compare 0= if
|
||||
2drop
|
||||
argc @ 4 < if
|
||||
s" Error: --restore requires snapshot ID" type cr
|
||||
1 (bye)
|
||||
then
|
||||
3 arg snapshot-restore
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" --delete" compare 0= if
|
||||
2drop
|
||||
argc @ 4 < if
|
||||
s" Error: --delete requires snapshot ID" type cr
|
||||
1 (bye)
|
||||
then
|
||||
3 arg snapshot-delete
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" --lock" compare 0= if
|
||||
2drop
|
||||
argc @ 4 < if
|
||||
s" Error: --lock requires snapshot ID" type cr
|
||||
1 (bye)
|
||||
then
|
||||
3 arg snapshot-lock
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" --unlock" compare 0= if
|
||||
2drop
|
||||
argc @ 4 < if
|
||||
s" Error: --unlock requires snapshot ID" type cr
|
||||
1 (bye)
|
||||
then
|
||||
3 arg snapshot-unlock
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" --clone" compare 0= if
|
||||
2drop
|
||||
argc @ 4 < if
|
||||
s" Error: --clone requires snapshot ID" type cr
|
||||
1 (bye)
|
||||
then
|
||||
3 arg snapshot-clone
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2drop
|
||||
s" Error: Use --list, --info ID, --restore ID, --delete ID, --lock ID, --unlock ID, or --clone ID" type cr
|
||||
1 (bye)
|
||||
;
|
||||
|
||||
\ Handle image subcommand
|
||||
: handle-image ( -- )
|
||||
argc @ 3 < if
|
||||
|
|
@ -1503,6 +1879,11 @@
|
|||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" snapshot" compare 0= if
|
||||
2drop handle-snapshot
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" key" compare 0= if
|
||||
2drop handle-key
|
||||
0 (bye)
|
||||
|
|
|
|||
|
|
@ -891,6 +891,9 @@ program unsandbox_cli
|
|||
else if (trim(arg) == 'image') then
|
||||
call handle_image()
|
||||
stop 0
|
||||
else if (trim(arg) == 'snapshot') then
|
||||
call handle_snapshot()
|
||||
stop 0
|
||||
else
|
||||
! Default execute command
|
||||
filename = trim(arg)
|
||||
|
|
@ -907,6 +910,7 @@ contains
|
|||
write(*, '(A)') 'Usage: ./un [options] <source_file>'
|
||||
write(*, '(A)') ' ./un session [options]'
|
||||
write(*, '(A)') ' ./un service [options]'
|
||||
write(*, '(A)') ' ./un snapshot [options]'
|
||||
write(*, '(A)') ' ./un image [options]'
|
||||
write(*, '(A)') ' ./un key [--extend]'
|
||||
write(*, '(A)') ' ./un languages [--json]'
|
||||
|
|
@ -937,6 +941,19 @@ contains
|
|||
write(*, '(A)') ' service env export <id> Export vault'
|
||||
write(*, '(A)') ' service env delete <id> Delete vault'
|
||||
write(*, '(A)') ''
|
||||
write(*, '(A)') 'Snapshot options:'
|
||||
write(*, '(A)') ' -l, --list List all snapshots'
|
||||
write(*, '(A)') ' --info ID Get snapshot details'
|
||||
write(*, '(A)') ' --delete ID Delete a snapshot'
|
||||
write(*, '(A)') ' --lock ID Lock snapshot'
|
||||
write(*, '(A)') ' --unlock ID Unlock snapshot'
|
||||
write(*, '(A)') ' --restore ID Restore from snapshot'
|
||||
write(*, '(A)') ' --clone ID Clone snapshot (requires --type)'
|
||||
write(*, '(A)') ' --type TYPE Clone type: session or service'
|
||||
write(*, '(A)') ' --name NAME Name for cloned resource'
|
||||
write(*, '(A)') ' --shell SHELL Shell for cloned session'
|
||||
write(*, '(A)') ' --ports PORTS Ports for cloned service'
|
||||
write(*, '(A)') ''
|
||||
write(*, '(A)') 'Image options:'
|
||||
write(*, '(A)') ' -l, --list List all images'
|
||||
write(*, '(A)') ' --info ID Get image details'
|
||||
|
|
@ -1530,6 +1547,214 @@ contains
|
|||
end if
|
||||
end subroutine handle_service
|
||||
|
||||
subroutine handle_snapshot()
|
||||
character(len=8192) :: full_cmd
|
||||
character(len=256) :: arg, snapshot_id, operation, clone_type, name, ports, shell
|
||||
character(len=1024) :: public_key, secret_key
|
||||
integer :: i, stat
|
||||
logical :: list_mode
|
||||
|
||||
snapshot_id = ''
|
||||
operation = ''
|
||||
clone_type = ''
|
||||
name = ''
|
||||
ports = ''
|
||||
shell = ''
|
||||
list_mode = .false.
|
||||
|
||||
! Parse arguments
|
||||
do i = 2, command_argument_count()
|
||||
call get_command_argument(i, arg)
|
||||
if (trim(arg) == '-l' .or. trim(arg) == '--list') then
|
||||
list_mode = .true.
|
||||
else if (trim(arg) == '--info') then
|
||||
operation = 'info'
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, snapshot_id)
|
||||
end if
|
||||
else if (trim(arg) == '--delete') then
|
||||
operation = 'delete'
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, snapshot_id)
|
||||
end if
|
||||
else if (trim(arg) == '--lock') then
|
||||
operation = 'lock'
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, snapshot_id)
|
||||
end if
|
||||
else if (trim(arg) == '--unlock') then
|
||||
operation = 'unlock'
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, snapshot_id)
|
||||
end if
|
||||
else if (trim(arg) == '--restore') then
|
||||
operation = 'restore'
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, snapshot_id)
|
||||
end if
|
||||
else if (trim(arg) == '--clone') then
|
||||
operation = 'clone'
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, snapshot_id)
|
||||
end if
|
||||
else if (trim(arg) == '--type') then
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, clone_type)
|
||||
end if
|
||||
else if (trim(arg) == '--name') then
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, name)
|
||||
end if
|
||||
else if (trim(arg) == '--ports') then
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, ports)
|
||||
end if
|
||||
else if (trim(arg) == '--shell') then
|
||||
if (i < command_argument_count()) then
|
||||
call get_command_argument(i + 1, shell)
|
||||
end if
|
||||
end if
|
||||
end do
|
||||
|
||||
! Get credentials
|
||||
call get_credentials(public_key, secret_key, stat)
|
||||
if (stat /= 0) then
|
||||
write(0, '(A)') 'Error: No credentials found'
|
||||
stop 1
|
||||
end if
|
||||
|
||||
if (list_mode) then
|
||||
! List snapshots
|
||||
write(full_cmd, '(20A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:GET:/snapshots:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X GET https://api.unsandbox.com/snapshots ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" | jq .'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else if (trim(operation) == 'info' .and. len_trim(snapshot_id) > 0) then
|
||||
! Get snapshot info
|
||||
write(full_cmd, '(20A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:GET:/snapshots/', trim(snapshot_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X GET https://api.unsandbox.com/snapshots/', trim(snapshot_id), ' ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" | jq .'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else if (trim(operation) == 'delete' .and. len_trim(snapshot_id) > 0) then
|
||||
! Delete snapshot (with sudo)
|
||||
write(full_cmd, '(30A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:DELETE:/snapshots/', trim(snapshot_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'RESP=$(curl -s -w "\n%{http_code}" -X DELETE https://api.unsandbox.com/snapshots/', trim(snapshot_id), ' ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG"); ', &
|
||||
'HTTP_CODE=$(echo "$RESP" | tail -1); ', &
|
||||
'BODY=$(echo "$RESP" | head -n -1); ', &
|
||||
'if [ "$HTTP_CODE" = "428" ]; then ', &
|
||||
'OTP=$(echo "$BODY" | jq -r ".otp // empty"); ', &
|
||||
'if [ -n "$OTP" ]; then ', &
|
||||
'TS2=$(date +%s); ', &
|
||||
'SIG2=$(echo -n "$TS2:DELETE:/snapshots/', trim(snapshot_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X DELETE https://api.unsandbox.com/snapshots/', trim(snapshot_id), ' ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS2" ', &
|
||||
'-H "X-Signature: $SIG2" ', &
|
||||
'-H "X-Sudo-OTP: $OTP" | jq .; ', &
|
||||
'echo -e "\x1b[32mSnapshot deleted\x1b[0m"; fi; ', &
|
||||
'else echo "$BODY" | jq .; fi'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else if (trim(operation) == 'lock' .and. len_trim(snapshot_id) > 0) then
|
||||
! Lock snapshot
|
||||
write(full_cmd, '(20A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:POST:/snapshots/', trim(snapshot_id), '/lock:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/lock ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" | jq . && ', &
|
||||
'echo -e "\x1b[32mSnapshot locked\x1b[0m"'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else if (trim(operation) == 'unlock' .and. len_trim(snapshot_id) > 0) then
|
||||
! Unlock snapshot (with sudo)
|
||||
write(full_cmd, '(30A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'BODY="{}"; ', &
|
||||
'SIG=$(echo -n "$TS:POST:/snapshots/', trim(snapshot_id), '/unlock:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'RESP=$(curl -s -w "\n%{http_code}" -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/unlock ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-d "$BODY"); ', &
|
||||
'HTTP_CODE=$(echo "$RESP" | tail -1); ', &
|
||||
'BODY_RESP=$(echo "$RESP" | head -n -1); ', &
|
||||
'if [ "$HTTP_CODE" = "428" ]; then ', &
|
||||
'OTP=$(echo "$BODY_RESP" | jq -r ".otp // empty"); ', &
|
||||
'if [ -n "$OTP" ]; then ', &
|
||||
'TS2=$(date +%s); ', &
|
||||
'SIG2=$(echo -n "$TS2:POST:/snapshots/', trim(snapshot_id), '/unlock:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/unlock ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS2" ', &
|
||||
'-H "X-Signature: $SIG2" ', &
|
||||
'-H "X-Sudo-OTP: $OTP" ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-d "$BODY" | jq .; ', &
|
||||
'echo -e "\x1b[32mSnapshot unlocked\x1b[0m"; fi; ', &
|
||||
'else echo "$BODY_RESP" | jq .; fi'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else if (trim(operation) == 'restore' .and. len_trim(snapshot_id) > 0) then
|
||||
! Restore snapshot
|
||||
write(full_cmd, '(20A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'BODY="{}"; ', &
|
||||
'SIG=$(echo -n "$TS:POST:/snapshots/', trim(snapshot_id), '/restore:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/restore ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-d "$BODY" | jq . && ', &
|
||||
'echo -e "\x1b[32mSnapshot restored\x1b[0m"'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else if (trim(operation) == 'clone' .and. len_trim(snapshot_id) > 0) then
|
||||
! Clone snapshot
|
||||
if (len_trim(clone_type) == 0) then
|
||||
write(0, '(A)') 'Error: --type required for --clone (session or service)'
|
||||
stop 1
|
||||
end if
|
||||
write(full_cmd, '(30A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'BODY=''{"type":"', trim(clone_type), '"'
|
||||
if (len_trim(name) > 0) then
|
||||
write(full_cmd, '(A,A)') trim(full_cmd), ',"name":"' // trim(name) // '"'
|
||||
end if
|
||||
if (len_trim(ports) > 0) then
|
||||
write(full_cmd, '(A,A)') trim(full_cmd), ',"ports":[' // trim(ports) // ']'
|
||||
end if
|
||||
if (len_trim(shell) > 0) then
|
||||
write(full_cmd, '(A,A)') trim(full_cmd), ',"shell":"' // trim(shell) // '"'
|
||||
end if
|
||||
write(full_cmd, '(A,20A)') trim(full_cmd), '}''; ', &
|
||||
'SIG=$(echo -n "$TS:POST:/snapshots/', trim(snapshot_id), '/clone:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/clone ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-d "$BODY" | jq . && ', &
|
||||
'echo -e "\x1b[32mSnapshot cloned\x1b[0m"'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else
|
||||
write(0, '(A)') 'Error: Use --list, --info, --delete, --lock, --unlock, --restore, or --clone'
|
||||
stop 1
|
||||
end if
|
||||
end subroutine handle_snapshot
|
||||
|
||||
subroutine handle_image()
|
||||
character(len=8192) :: full_cmd
|
||||
character(len=256) :: arg, image_id, operation, source_type, name, ports, visibility_mode
|
||||
|
|
|
|||
100
clients/fortran/tests/test_un.sh
Executable file
100
clients/fortran/tests/test_un.sh
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/bin/bash
|
||||
# Test suite for Fortran Unsandbox SDK
|
||||
# Run: bash tests/test_un.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SDK_DIR="$SCRIPT_DIR/../sync/src"
|
||||
SOURCE="$SDK_DIR/un.f90"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
|
||||
# Test helper
|
||||
test_that() {
|
||||
local description="$1"
|
||||
local test_cmd="$2"
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
|
||||
if eval "$test_cmd" >/dev/null 2>&1; then
|
||||
echo -e "[${GREEN}PASS${NC}] $description"
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
return 0
|
||||
else
|
||||
echo -e "[${RED}FAIL${NC}] $description"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Source File ==="
|
||||
test_that "Source file exists" "[ -f '$SOURCE' ]"
|
||||
|
||||
echo ""
|
||||
echo "=== Command Handlers ==="
|
||||
test_that "Session handler defined" "grep -q 'subroutine handle_session' '$SOURCE'"
|
||||
test_that "Service handler defined" "grep -q 'subroutine handle_service' '$SOURCE'"
|
||||
test_that "Snapshot handler defined" "grep -q 'subroutine handle_snapshot' '$SOURCE'"
|
||||
test_that "Image handler defined" "grep -q 'subroutine handle_image' '$SOURCE'"
|
||||
test_that "Key handler defined" "grep -q 'subroutine handle_key' '$SOURCE'"
|
||||
test_that "Languages handler defined" "grep -q 'subroutine handle_languages' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Command Dispatch ==="
|
||||
test_that "Session dispatch" "grep -q \"trim(arg) == 'session'\" '$SOURCE'"
|
||||
test_that "Service dispatch" "grep -q \"trim(arg) == 'service'\" '$SOURCE'"
|
||||
test_that "Snapshot dispatch" "grep -q \"trim(arg) == 'snapshot'\" '$SOURCE'"
|
||||
test_that "Image dispatch" "grep -q \"trim(arg) == 'image'\" '$SOURCE'"
|
||||
test_that "Key dispatch" "grep -q \"trim(arg) == 'key'\" '$SOURCE'"
|
||||
test_that "Languages dispatch" "grep -q \"trim(arg) == 'languages'\" '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Snapshot Operations ==="
|
||||
test_that "Snapshot --list" "grep -q \"operation = 'list'\" '$SOURCE' || grep -q 'list_mode' '$SOURCE'"
|
||||
test_that "Snapshot --info" "grep -q \"operation = 'info'\" '$SOURCE'"
|
||||
test_that "Snapshot --delete" "grep -q \"operation = 'delete'\" '$SOURCE'"
|
||||
test_that "Snapshot --lock" "grep -q \"operation = 'lock'\" '$SOURCE'"
|
||||
test_that "Snapshot --unlock" "grep -q \"operation = 'unlock'\" '$SOURCE'"
|
||||
test_that "Snapshot --restore" "grep -q \"operation = 'restore'\" '$SOURCE'"
|
||||
test_that "Snapshot --clone" "grep -q \"operation = 'clone'\" '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Help Text ==="
|
||||
test_that "Help shows snapshot" "grep -q 'snapshot' '$SOURCE'"
|
||||
test_that "Help shows --list" "grep -q '\\-\\-list' '$SOURCE'"
|
||||
test_that "Help shows --info" "grep -q '\\-\\-info' '$SOURCE'"
|
||||
test_that "Help shows --restore" "grep -q '\\-\\-restore' '$SOURCE'"
|
||||
test_that "Help shows --clone" "grep -q '\\-\\-clone' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== HMAC Authentication ==="
|
||||
test_that "Uses openssl for HMAC" "grep -q 'openssl dgst -sha256 -hmac' '$SOURCE'"
|
||||
test_that "Has X-Signature header" "grep -q 'X-Signature' '$SOURCE'"
|
||||
test_that "Has X-Timestamp header" "grep -q 'X-Timestamp' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Sudo OTP Handling ==="
|
||||
test_that "Handles 428 response" "grep -q '428' '$SOURCE'"
|
||||
test_that "Has X-Sudo-OTP header" "grep -q 'X-Sudo-OTP' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Module Structure ==="
|
||||
test_that "Has unsandbox_sdk module" "grep -q 'module unsandbox_sdk' '$SOURCE'"
|
||||
test_that "Has unsandbox_client type" "grep -q 'type :: unsandbox_client' '$SOURCE'"
|
||||
test_that "Has execution_result type" "grep -q 'type :: execution_result' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Summary ==="
|
||||
echo "Tests passed: $TESTS_PASSED / $TESTS_RUN"
|
||||
|
||||
if [ $TESTS_PASSED -eq $TESTS_RUN ]; then
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -740,14 +740,14 @@ let openBrowser (url: string) =
|
|||
eprintfn "%sError opening browser: %s%s" red ex.Message reset
|
||||
|
||||
let cmdKey (args: Args) =
|
||||
let apiKey = getApiKey args.ApiKey
|
||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||
|
||||
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
|
||||
|
||||
let request = WebRequest.Create(portalBase + "/keys/validate") :?> HttpWebRequest
|
||||
request.Method <- "POST"
|
||||
request.ContentType <- "application/json"
|
||||
request.Headers.Add("Authorization", sprintf "Bearer %s" apiKey)
|
||||
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
|
||||
request.Timeout <- 30000
|
||||
|
||||
try
|
||||
|
|
@ -756,7 +756,7 @@ let cmdKey (args: Args) =
|
|||
let responseText = reader.ReadToEnd()
|
||||
let result = parseJson responseText
|
||||
|
||||
let publicKey = match result.TryFind "public_key" with | Some v -> v.ToString() | None -> "N/A"
|
||||
let resultPublicKey = match result.TryFind "public_key" with | Some v -> v.ToString() | None -> "N/A"
|
||||
let tier = match result.TryFind "tier" with | Some v -> v.ToString() | None -> "N/A"
|
||||
let status = match result.TryFind "status" with | Some v -> v.ToString() | None -> "N/A"
|
||||
let expiresAt = match result.TryFind "expires_at" with | Some v -> v.ToString() | None -> "N/A"
|
||||
|
|
@ -766,20 +766,20 @@ let cmdKey (args: Args) =
|
|||
let concurrency = match result.TryFind "concurrency" with | Some v -> v.ToString() | None -> "N/A"
|
||||
let expired = match result.TryFind "expired" with | Some v -> v.ToString() = "True" | None -> false
|
||||
|
||||
if args.KeyExtend && publicKey <> "N/A" then
|
||||
let extendUrl = sprintf "%s/keys/extend?pk=%s" portalBase publicKey
|
||||
if args.KeyExtend && resultPublicKey <> "N/A" then
|
||||
let extendUrl = sprintf "%s/keys/extend?pk=%s" portalBase resultPublicKey
|
||||
printfn "%sOpening browser to extend key...%s" blue reset
|
||||
openBrowser extendUrl
|
||||
elif expired then
|
||||
printfn "%sExpired%s" red reset
|
||||
printfn "Public Key: %s" publicKey
|
||||
printfn "Public Key: %s" resultPublicKey
|
||||
printfn "Tier: %s" tier
|
||||
printfn "Expired: %s" expiresAt
|
||||
printfn "%sTo renew: Visit https://unsandbox.com/keys/extend%s" yellow reset
|
||||
exit 1
|
||||
else
|
||||
printfn "%sValid%s" green reset
|
||||
printfn "Public Key: %s" publicKey
|
||||
printfn "Public Key: %s" resultPublicKey
|
||||
printfn "Tier: %s" tier
|
||||
printfn "Status: %s" status
|
||||
printfn "Expires: %s" expiresAt
|
||||
|
|
|
|||
265
clients/fsharp/tests/UnsandboxTests.fs
Normal file
265
clients/fsharp/tests/UnsandboxTests.fs
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Unit and Functional Tests for Unsandbox F# SDK
|
||||
|
||||
open System
|
||||
open System.Collections.Generic
|
||||
open System.Security.Cryptography
|
||||
open System.Text
|
||||
|
||||
// Source the main module (when running as script)
|
||||
// For compiled tests, include un.fs in the project
|
||||
|
||||
/// Unit tests for the Unsandbox SDK library functions.
|
||||
module UnitTests =
|
||||
let run () =
|
||||
printfn "=== Unsandbox F# SDK Unit Tests ===\n"
|
||||
|
||||
testDetectLanguage ()
|
||||
testHmacSign ()
|
||||
testExtensionMap ()
|
||||
|
||||
printfn "\n=== Unit Tests Complete ==="
|
||||
|
||||
and testDetectLanguage () =
|
||||
printf "DetectLanguage: "
|
||||
let tests = [
|
||||
("test.py", "python")
|
||||
("script.js", "javascript")
|
||||
("main.go", "go")
|
||||
("app.rs", "rust")
|
||||
("Program.cs", "csharp")
|
||||
("Module.fs", "fsharp")
|
||||
]
|
||||
|
||||
let mutable passed = 0
|
||||
for (filename, expected) in tests do
|
||||
let ext = filename.Substring(filename.LastIndexOf('.'))
|
||||
match Map.tryFind ext extMap with
|
||||
| Some lang when lang = expected -> passed <- passed + 1
|
||||
| Some lang -> printf "[FAIL: %s -> %s, expected %s] " filename lang expected
|
||||
| None -> printf "[FAIL: %s -> None, expected %s] " filename expected
|
||||
|
||||
if passed = tests.Length then
|
||||
printfn "PASS (%d/%d)" passed tests.Length
|
||||
else
|
||||
printfn "FAIL (%d/%d)" passed tests.Length
|
||||
|
||||
and testHmacSign () =
|
||||
printf "HmacSign: "
|
||||
// Test vector: HMAC-SHA256("key", "message")
|
||||
use hmac = new HMACSHA256(Encoding.UTF8.GetBytes("key"))
|
||||
let hash = hmac.ComputeHash(Encoding.UTF8.GetBytes("message"))
|
||||
let signature = BitConverter.ToString(hash).Replace("-", "").ToLower()
|
||||
let expected = "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a"
|
||||
if signature = expected then
|
||||
printfn "PASS"
|
||||
else
|
||||
printfn "FAIL (got %s, expected %s)" signature expected
|
||||
|
||||
and testExtensionMap () =
|
||||
printf "ExtensionMap: "
|
||||
let tests = [
|
||||
(".py", "python")
|
||||
(".js", "javascript")
|
||||
(".go", "go")
|
||||
(".rs", "rust")
|
||||
(".fs", "fsharp")
|
||||
]
|
||||
|
||||
let mutable passed = 0
|
||||
for (ext, expected) in tests do
|
||||
match Map.tryFind ext extMap with
|
||||
| Some lang when lang = expected -> passed <- passed + 1
|
||||
| _ -> ()
|
||||
|
||||
if passed = tests.Length then
|
||||
printfn "PASS (%d/%d)" passed tests.Length
|
||||
else
|
||||
printfn "FAIL (%d/%d)" passed tests.Length
|
||||
|
||||
/// Functional tests that require API credentials.
|
||||
module FunctionalTests =
|
||||
let run () =
|
||||
let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
|
||||
let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if String.IsNullOrEmpty(publicKey) || String.IsNullOrEmpty(secretKey) then
|
||||
printfn "=== Functional Tests Skipped (no API credentials) ==="
|
||||
else
|
||||
printfn "=== Unsandbox F# SDK Functional Tests ===\n"
|
||||
|
||||
testValidateKeys ()
|
||||
testGetLanguages ()
|
||||
testExecute ()
|
||||
testSessionList ()
|
||||
testServiceList ()
|
||||
testSnapshotList ()
|
||||
testImageList ()
|
||||
|
||||
printfn "\n=== Functional Tests Complete ==="
|
||||
|
||||
and testValidateKeys () =
|
||||
printf "ValidateKeys: "
|
||||
try
|
||||
let result = apiRequest "/keys/validate" "POST" None publicKey secretKey
|
||||
match result.TryFind "valid" with
|
||||
| Some v when v.ToString() = "True" ->
|
||||
let tier = match result.TryFind "tier" with | Some t -> t.ToString() | None -> "N/A"
|
||||
printfn "PASS (tier: %s)" tier
|
||||
| _ -> printfn "FAIL"
|
||||
with ex ->
|
||||
printfn "FAIL (%s)" ex.Message
|
||||
|
||||
and testGetLanguages () =
|
||||
printf "GetLanguages: "
|
||||
try
|
||||
let result = apiRequest "/languages" "GET" None publicKey secretKey
|
||||
match result.TryFind "languages" with
|
||||
| Some langs -> printfn "PASS (languages received)"
|
||||
| None -> printfn "FAIL (no languages in response)"
|
||||
with ex ->
|
||||
printfn "FAIL (%s)" ex.Message
|
||||
|
||||
and testExecute () =
|
||||
printf "Execute: "
|
||||
try
|
||||
let payload = [("language", box "python"); ("code", box "print('hello from F# SDK')")]
|
||||
let result = apiRequest "/execute" "POST" (Some payload) publicKey secretKey
|
||||
match result.TryFind "stdout" with
|
||||
| Some stdout when stdout.ToString().Contains("hello") -> printfn "PASS"
|
||||
| _ -> printfn "FAIL (no expected output)"
|
||||
with ex ->
|
||||
printfn "FAIL (%s)" ex.Message
|
||||
|
||||
and testSessionList () =
|
||||
printf "SessionList: "
|
||||
try
|
||||
let result = apiRequest "/sessions" "GET" None publicKey secretKey
|
||||
printfn "PASS (sessions endpoint responded)"
|
||||
with ex ->
|
||||
printfn "FAIL (%s)" ex.Message
|
||||
|
||||
and testServiceList () =
|
||||
printf "ServiceList: "
|
||||
try
|
||||
let result = apiRequest "/services" "GET" None publicKey secretKey
|
||||
printfn "PASS (services endpoint responded)"
|
||||
with ex ->
|
||||
printfn "FAIL (%s)" ex.Message
|
||||
|
||||
and testSnapshotList () =
|
||||
printf "SnapshotList: "
|
||||
try
|
||||
let result = apiRequest "/snapshots" "GET" None publicKey secretKey
|
||||
printfn "PASS (snapshots endpoint responded)"
|
||||
with ex ->
|
||||
printfn "FAIL (%s)" ex.Message
|
||||
|
||||
and testImageList () =
|
||||
printf "ImageList: "
|
||||
try
|
||||
let result = apiRequest "/images" "GET" None publicKey secretKey
|
||||
printfn "PASS (images endpoint responded)"
|
||||
with ex ->
|
||||
printfn "FAIL (%s)" ex.Message
|
||||
|
||||
// Get the API keys from environment
|
||||
and publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
|
||||
and secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
// Extension map (duplicated here for standalone testing)
|
||||
let extMap =
|
||||
Map.ofList [
|
||||
(".py", "python"); (".js", "javascript"); (".ts", "typescript")
|
||||
(".rb", "ruby"); (".php", "php"); (".pl", "perl"); (".lua", "lua")
|
||||
(".sh", "bash"); (".go", "go"); (".rs", "rust"); (".c", "c")
|
||||
(".cpp", "cpp"); (".cc", "cpp"); (".cxx", "cpp")
|
||||
(".java", "java"); (".kt", "kotlin"); (".cs", "csharp"); (".fs", "fsharp")
|
||||
(".hs", "haskell"); (".ml", "ocaml"); (".clj", "clojure"); (".scm", "scheme")
|
||||
(".lisp", "commonlisp"); (".erl", "erlang"); (".ex", "elixir"); (".exs", "elixir")
|
||||
(".jl", "julia"); (".r", "r"); (".R", "r"); (".cr", "crystal")
|
||||
(".d", "d"); (".nim", "nim"); (".zig", "zig"); (".v", "v")
|
||||
(".dart", "dart"); (".groovy", "groovy"); (".scala", "scala")
|
||||
(".f90", "fortran"); (".f95", "fortran"); (".cob", "cobol")
|
||||
(".pro", "prolog"); (".forth", "forth"); (".4th", "forth")
|
||||
(".tcl", "tcl"); (".raku", "raku"); (".m", "objc")
|
||||
]
|
||||
|
||||
// API request function (simplified for testing)
|
||||
open System.Net
|
||||
open System.IO
|
||||
|
||||
let apiBase = "https://api.unsandbox.com"
|
||||
|
||||
let toJson (obj: obj) =
|
||||
match obj with
|
||||
| :? string as s -> sprintf "\"%s\"" (s.Replace("\\", "\\\\").Replace("\"", "\\\""))
|
||||
| :? int as i -> i.ToString()
|
||||
| :? bool as b -> b.ToString().ToLower()
|
||||
| :? (string * obj) list as lst ->
|
||||
let entries = lst |> List.map (fun (k, v) -> sprintf "\"%s\":%s" k (toJson v)) |> String.concat ","
|
||||
sprintf "{%s}" entries
|
||||
| _ -> sprintf "\"%s\"" (obj.ToString())
|
||||
|
||||
let apiRequest (endpoint: string) (method: string) (data: (string * obj) list option) (publicKey: string) (secretKey: string) =
|
||||
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
|
||||
|
||||
let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest
|
||||
request.Method <- method
|
||||
request.ContentType <- "application/json"
|
||||
request.Timeout <- 300000
|
||||
|
||||
let body = match data with | Some d -> toJson (box d) | None -> ""
|
||||
|
||||
if not (String.IsNullOrEmpty(secretKey)) then
|
||||
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
let message = sprintf "%d:%s:%s:%s" timestamp method endpoint body
|
||||
|
||||
use hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey))
|
||||
let hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message))
|
||||
let signature = BitConverter.ToString(hash).Replace("-", "").ToLower()
|
||||
|
||||
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
|
||||
request.Headers.Add("X-Timestamp", timestamp.ToString())
|
||||
request.Headers.Add("X-Signature", signature)
|
||||
else
|
||||
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
|
||||
|
||||
match data with
|
||||
| Some d ->
|
||||
let bytes = Encoding.UTF8.GetBytes(body)
|
||||
request.ContentLength <- int64 bytes.Length
|
||||
use stream = request.GetRequestStream()
|
||||
stream.Write(bytes, 0, bytes.Length)
|
||||
| None -> ()
|
||||
|
||||
use response = request.GetResponse() :?> HttpWebResponse
|
||||
use reader = new StreamReader(response.GetResponseStream())
|
||||
let responseText = reader.ReadToEnd()
|
||||
|
||||
// Simple JSON parsing - return as string map
|
||||
Map.empty<string, obj>
|
||||
|> fun m -> if responseText.Contains("\"valid\"") then Map.add "valid" (box true) m else m
|
||||
|> fun m -> if responseText.Contains("\"tier\"") then Map.add "tier" (box "unknown") m else m
|
||||
|> fun m -> if responseText.Contains("\"languages\"") then Map.add "languages" (box []) m else m
|
||||
|> fun m -> if responseText.Contains("\"stdout\"") then
|
||||
let start = responseText.IndexOf("\"stdout\":\"") + 10
|
||||
let endIdx = responseText.IndexOf("\"", start)
|
||||
if start > 10 && endIdx > start then
|
||||
Map.add "stdout" (box (responseText.Substring(start, endIdx - start))) m
|
||||
else m
|
||||
else m
|
||||
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
try
|
||||
printfn "Unsandbox F# SDK Tests"
|
||||
printfn "======================\n"
|
||||
|
||||
UnitTests.run ()
|
||||
printfn ""
|
||||
FunctionalTests.run ()
|
||||
0
|
||||
with ex ->
|
||||
eprintfn "Test error: %s" ex.Message
|
||||
1
|
||||
|
|
@ -1096,6 +1096,20 @@ func ExecuteInService(creds *Credentials, serviceID, command string) (map[string
|
|||
return makeRequest("POST", fmt.Sprintf("/services/%s/execute", serviceID), creds, data)
|
||||
}
|
||||
|
||||
// ResizeService changes the vCPU count for a running service.
|
||||
//
|
||||
// Args:
|
||||
//
|
||||
// creds: API credentials
|
||||
// serviceID: Service ID
|
||||
// vcpu: New vCPU count (1-8)
|
||||
func ResizeService(creds *Credentials, serviceID string, vcpu int) (map[string]interface{}, error) {
|
||||
data := map[string]interface{}{
|
||||
"vcpu": vcpu,
|
||||
}
|
||||
return makeRequest("POST", fmt.Sprintf("/services/%s/resize", serviceID), creds, data)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Additional Snapshot Operations
|
||||
// ============================================================================
|
||||
|
|
@ -1480,6 +1494,190 @@ func CloneImage(creds *Credentials, imageID string, opts *CloneImageOptions) (ma
|
|||
return makeRequest("POST", fmt.Sprintf("/images/%s/clone", imageID), creds, data)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PaaS Logs API
|
||||
// ============================================================================
|
||||
|
||||
// LogsFetchOptions contains options for fetching logs.
|
||||
type LogsFetchOptions struct {
|
||||
Lines int // Number of lines (1-10000)
|
||||
Since string // Time window ("1m", "5m", "1h", "1d")
|
||||
Grep string // Optional filter pattern
|
||||
}
|
||||
|
||||
// LogsFetch fetches batch logs from the portal.
|
||||
//
|
||||
// Args:
|
||||
//
|
||||
// creds: API credentials
|
||||
// source: Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
// opts: Fetch options (can be nil for defaults)
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// JSON response with log entries
|
||||
func LogsFetch(creds *Credentials, source string, opts *LogsFetchOptions) (map[string]interface{}, error) {
|
||||
path := "/paas/logs"
|
||||
params := []string{}
|
||||
|
||||
if source != "" {
|
||||
params = append(params, fmt.Sprintf("source=%s", source))
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
if opts.Lines > 0 {
|
||||
params = append(params, fmt.Sprintf("lines=%d", opts.Lines))
|
||||
}
|
||||
if opts.Since != "" {
|
||||
params = append(params, fmt.Sprintf("since=%s", opts.Since))
|
||||
}
|
||||
if opts.Grep != "" {
|
||||
params = append(params, fmt.Sprintf("grep=%s", opts.Grep))
|
||||
}
|
||||
}
|
||||
|
||||
if len(params) > 0 {
|
||||
path = path + "?" + strings.Join(params, "&")
|
||||
}
|
||||
|
||||
return makeRequest("GET", path, creds, nil)
|
||||
}
|
||||
|
||||
// LogCallback is called for each log line received during streaming.
|
||||
type LogCallback func(source, line string)
|
||||
|
||||
// LogsStream streams logs via Server-Sent Events.
|
||||
// This function blocks until the stream is closed or an error occurs.
|
||||
//
|
||||
// Args:
|
||||
//
|
||||
// creds: API credentials
|
||||
// source: Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
// grep: Optional filter pattern (empty string for no filter)
|
||||
// callback: Function called for each log line
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// nil on clean shutdown, error on failure
|
||||
func LogsStream(creds *Credentials, source, grep string, callback LogCallback) error {
|
||||
path := "/paas/logs/stream"
|
||||
params := []string{}
|
||||
|
||||
if source != "" {
|
||||
params = append(params, fmt.Sprintf("source=%s", source))
|
||||
}
|
||||
if grep != "" {
|
||||
params = append(params, fmt.Sprintf("grep=%s", grep))
|
||||
}
|
||||
|
||||
if len(params) > 0 {
|
||||
path = path + "?" + strings.Join(params, "&")
|
||||
}
|
||||
|
||||
url := APIBase + path
|
||||
timestamp := time.Now().Unix()
|
||||
message := fmt.Sprintf("%d:GET:%s:", timestamp, path)
|
||||
mac := hmac.New(sha256.New, []byte(creds.SecretKey))
|
||||
mac.Write([]byte(message))
|
||||
signature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+creds.PublicKey)
|
||||
req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
|
||||
req.Header.Set("X-Signature", signature)
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
|
||||
client := &http.Client{Timeout: 0} // No timeout for streaming
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("stream error (HTTP %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
currentSource := source
|
||||
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil // Clean shutdown
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse SSE format
|
||||
if strings.HasPrefix(line, "event:") {
|
||||
// New source from event type
|
||||
currentSource = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
||||
} else if strings.HasPrefix(line, "data:") {
|
||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if callback != nil && data != "" {
|
||||
callback(currentSource, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
// SDKVersion is the version of this SDK.
|
||||
const SDKVersion = "4.2.0"
|
||||
|
||||
// HmacSign computes an HMAC-SHA256 signature for the given message using the secret key.
|
||||
// Returns the signature as a lowercase hex string.
|
||||
func HmacSign(secretKey, message string) string {
|
||||
mac := hmac.New(sha256.New, []byte(secretKey))
|
||||
mac.Write([]byte(message))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// HealthCheck checks if the API is reachable and responding.
|
||||
// Returns true if healthy, false otherwise.
|
||||
func HealthCheck() bool {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(APIBase + "/health")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == 200
|
||||
}
|
||||
|
||||
// Version returns the SDK version string.
|
||||
func Version() string {
|
||||
return SDKVersion
|
||||
}
|
||||
|
||||
// lastError holds the most recent error message for thread-safe access.
|
||||
var lastError string
|
||||
|
||||
// SetLastError sets the last error message (internal use).
|
||||
func SetLastError(msg string) {
|
||||
lastError = msg
|
||||
}
|
||||
|
||||
// LastError returns the most recent error message from the SDK.
|
||||
func LastError() string {
|
||||
return lastError
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CLI Implementation
|
||||
// ============================================================================
|
||||
|
|
|
|||
363
clients/go/sync/tests/un_test.go
Normal file
363
clients/go/sync/tests/un_test.go
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
// Tests for the Go unsandbox SDK
|
||||
// Run with: go test -v ./tests/
|
||||
package un
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests - Test exported library functions
|
||||
// ============================================================================
|
||||
|
||||
func TestDetectLanguage(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
expected string
|
||||
}{
|
||||
{"script.py", "python"},
|
||||
{"script.js", "javascript"},
|
||||
{"script.ts", "typescript"},
|
||||
{"script.rb", "ruby"},
|
||||
{"script.go", "go"},
|
||||
{"script.rs", "rust"},
|
||||
{"script.c", "c"},
|
||||
{"script.cpp", "cpp"},
|
||||
{"script.d", "d"},
|
||||
{"script.zig", "zig"},
|
||||
{"script.sh", "bash"},
|
||||
{"script.lua", "lua"},
|
||||
{"script.php", "php"},
|
||||
{"script.unknown", ""},
|
||||
{"script", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.filename, func(t *testing.T) {
|
||||
result := DetectLanguage(tt.filename)
|
||||
if result != tt.expected {
|
||||
t.Errorf("DetectLanguage(%q) = %q, want %q", tt.filename, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHmacSign(t *testing.T) {
|
||||
// Test with known values
|
||||
secretKey := "test-secret"
|
||||
message := "test-message"
|
||||
|
||||
result := HmacSign(secretKey, message)
|
||||
|
||||
// Should return a 64-character hex string
|
||||
if len(result) != 64 {
|
||||
t.Errorf("HmacSign returned %d characters, want 64", len(result))
|
||||
}
|
||||
|
||||
// Should be deterministic
|
||||
result2 := HmacSign(secretKey, message)
|
||||
if result != result2 {
|
||||
t.Error("HmacSign is not deterministic")
|
||||
}
|
||||
|
||||
// Different inputs should produce different outputs
|
||||
result3 := HmacSign(secretKey, "different-message")
|
||||
if result == result3 {
|
||||
t.Error("HmacSign returned same result for different inputs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
version := Version()
|
||||
if version == "" {
|
||||
t.Error("Version() returned empty string")
|
||||
}
|
||||
// Should be in semver format
|
||||
if len(version) < 5 { // At minimum "0.0.0"
|
||||
t.Errorf("Version() = %q, expected semver format", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastError(t *testing.T) {
|
||||
// Set an error
|
||||
SetLastError("test error message")
|
||||
|
||||
// Retrieve it
|
||||
err := LastError()
|
||||
if err != "test error message" {
|
||||
t.Errorf("LastError() = %q, want %q", err, "test error message")
|
||||
}
|
||||
|
||||
// Clear it
|
||||
SetLastError("")
|
||||
err = LastError()
|
||||
if err != "" {
|
||||
t.Errorf("LastError() after clear = %q, want empty", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsNew(t *testing.T) {
|
||||
pk := "unsb-pk-test-test-test-test"
|
||||
sk := "unsb-sk-test1-test2-test3-test4"
|
||||
|
||||
creds := &Credentials{
|
||||
PublicKey: pk,
|
||||
SecretKey: sk,
|
||||
}
|
||||
|
||||
if creds.PublicKey != pk {
|
||||
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, pk)
|
||||
}
|
||||
if creds.SecretKey != sk {
|
||||
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, sk)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests - Test SDK internal consistency
|
||||
// ============================================================================
|
||||
|
||||
func TestSignRequest(t *testing.T) {
|
||||
secretKey := "test-secret-key"
|
||||
timestamp := int64(1704067200) // 2024-01-01 00:00:00 UTC
|
||||
method := "POST"
|
||||
path := "/execute"
|
||||
body := `{"language":"python","code":"print(1)"}`
|
||||
|
||||
signature := signRequest(secretKey, timestamp, method, path, []byte(body))
|
||||
|
||||
// Should return a 64-character hex string
|
||||
if len(signature) != 64 {
|
||||
t.Errorf("signRequest returned %d characters, want 64", len(signature))
|
||||
}
|
||||
|
||||
// Should be deterministic
|
||||
signature2 := signRequest(secretKey, timestamp, method, path, []byte(body))
|
||||
if signature != signature2 {
|
||||
t.Error("signRequest is not deterministic")
|
||||
}
|
||||
|
||||
// Different timestamps should produce different signatures
|
||||
signature3 := signRequest(secretKey, timestamp+1, method, path, []byte(body))
|
||||
if signature == signature3 {
|
||||
t.Error("signRequest returned same result for different timestamps")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCredentialsFromEnv(t *testing.T) {
|
||||
// Save original env vars
|
||||
origPK := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
origSK := os.Getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
// Set test env vars
|
||||
testPK := "unsb-pk-test-test-test-test"
|
||||
testSK := "unsb-sk-test1-test2-test3-test4"
|
||||
os.Setenv("UNSANDBOX_PUBLIC_KEY", testPK)
|
||||
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
||||
|
||||
// Test
|
||||
creds, err := ResolveCredentials("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||
}
|
||||
if creds.PublicKey != testPK {
|
||||
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, testPK)
|
||||
}
|
||||
if creds.SecretKey != testSK {
|
||||
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, testSK)
|
||||
}
|
||||
|
||||
// Restore original env vars
|
||||
if origPK != "" {
|
||||
os.Setenv("UNSANDBOX_PUBLIC_KEY", origPK)
|
||||
} else {
|
||||
os.Unsetenv("UNSANDBOX_PUBLIC_KEY")
|
||||
}
|
||||
if origSK != "" {
|
||||
os.Setenv("UNSANDBOX_SECRET_KEY", origSK)
|
||||
} else {
|
||||
os.Unsetenv("UNSANDBOX_SECRET_KEY")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCredentialsFromArgs(t *testing.T) {
|
||||
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
||||
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
||||
|
||||
creds, err := ResolveCredentials(testPK, testSK)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||
}
|
||||
if creds.PublicKey != testPK {
|
||||
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, testPK)
|
||||
}
|
||||
if creds.SecretKey != testSK {
|
||||
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, testSK)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Functional Tests - Test against real API (requires credentials)
|
||||
// ============================================================================
|
||||
|
||||
func getTestCredentials(t *testing.T) *Credentials {
|
||||
creds, err := ResolveCredentials("", "")
|
||||
if err != nil {
|
||||
t.Skip("No credentials available for functional tests")
|
||||
}
|
||||
return creds
|
||||
}
|
||||
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
healthy := HealthCheck()
|
||||
if !healthy {
|
||||
t.Log("API health check returned unhealthy (API may be unreachable)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLanguages(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
languages, err := GetLanguages(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLanguages failed: %v", err)
|
||||
}
|
||||
|
||||
if len(languages) == 0 {
|
||||
t.Error("GetLanguages returned empty list")
|
||||
}
|
||||
|
||||
// Should include common languages
|
||||
hasPython := false
|
||||
hasJavascript := false
|
||||
for _, lang := range languages {
|
||||
if lang == "python" {
|
||||
hasPython = true
|
||||
}
|
||||
if lang == "javascript" {
|
||||
hasJavascript = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPython {
|
||||
t.Error("GetLanguages missing 'python'")
|
||||
}
|
||||
if !hasJavascript {
|
||||
t.Error("GetLanguages missing 'javascript'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateKeys(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
result, err := ValidateKeys(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Error("ValidateKeys returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCode(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
result, err := ExecuteCode(creds, "python", "print('hello from go test')")
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteCode failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Error("ExecuteCode returned nil")
|
||||
}
|
||||
|
||||
// Check for stdout in result
|
||||
if stdout, ok := result["stdout"].(string); ok {
|
||||
if stdout == "" {
|
||||
t.Error("ExecuteCode returned empty stdout")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSessions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
sessions, err := ListSessions(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSessions failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return a list (possibly empty)
|
||||
if sessions == nil {
|
||||
t.Error("ListSessions returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListServices(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
services, err := ListServices(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("ListServices failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return a list (possibly empty)
|
||||
if services == nil {
|
||||
t.Error("ListServices returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSnapshots(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
snapshots, err := ListSnapshots(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSnapshots failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return a list (possibly empty)
|
||||
if snapshots == nil {
|
||||
t.Error("ListSnapshots returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImages(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
images, err := ListImages(creds, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListImages failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return a list (possibly empty)
|
||||
if images == nil {
|
||||
t.Error("ListImages returned nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -922,6 +922,597 @@ def languages(Map options = [:]) {
|
|||
return result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get SDK version string.
|
||||
*/
|
||||
def version() {
|
||||
return "4.2.0"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check API health status.
|
||||
*/
|
||||
def healthCheck() {
|
||||
try {
|
||||
def url = new URL("${API_BASE}/health")
|
||||
def connection = url.openConnection() as java.net.HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 5000
|
||||
connection.readTimeout = 5000
|
||||
return connection.responseCode == 200
|
||||
} catch (Exception e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HMAC-SHA256 signature.
|
||||
*/
|
||||
def hmacSign(String secretKey, String message) {
|
||||
def mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
|
||||
return mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* List all sessions.
|
||||
*/
|
||||
def sessionList(Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def result = apiRequest('/sessions', 'GET', null, publicKey, secretKey)
|
||||
return result.sessions ?: []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session details.
|
||||
*/
|
||||
def sessionGet(String sessionId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/sessions/${sessionId}", 'GET', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session.
|
||||
*/
|
||||
def sessionCreate(Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [
|
||||
network_mode: options.networkMode ?: 'zerotrust',
|
||||
shell: options.shell ?: 'bash'
|
||||
]
|
||||
if (options.vcpu) payload.vcpu = options.vcpu
|
||||
return apiRequest('/sessions', 'POST', payload, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a session.
|
||||
*/
|
||||
def sessionDestroy(String sessionId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/sessions/${sessionId}", 'DELETE', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Freeze a session.
|
||||
*/
|
||||
def sessionFreeze(String sessionId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/sessions/${sessionId}/freeze", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfreeze a session.
|
||||
*/
|
||||
def sessionUnfreeze(String sessionId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/sessions/${sessionId}/unfreeze", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Boost a session.
|
||||
*/
|
||||
def sessionBoost(String sessionId, int vcpu = 2, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/sessions/${sessionId}/boost", 'POST', [vcpu: vcpu], publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unboost a session.
|
||||
*/
|
||||
def sessionUnboost(String sessionId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/sessions/${sessionId}/unboost", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute command in a session.
|
||||
*/
|
||||
def sessionExecute(String sessionId, String command, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/sessions/${sessionId}/shell", 'POST', [command: command], publicKey, secretKey)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Service Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* List all services.
|
||||
*/
|
||||
def serviceList(Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def result = apiRequest('/services', 'GET', null, publicKey, secretKey)
|
||||
return result.services ?: []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service details.
|
||||
*/
|
||||
def serviceGet(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/services/${serviceId}", 'GET', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new service.
|
||||
*/
|
||||
def serviceCreate(String name, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [name: name]
|
||||
if (options.ports) payload.ports = options.ports.split(',').collect { it.trim().toInteger() }
|
||||
if (options.domains) payload.domains = options.domains
|
||||
if (options.bootstrap) payload.bootstrap = options.bootstrap
|
||||
if (options.networkMode) payload.network_mode = options.networkMode
|
||||
def result = apiRequest('/services', 'POST', payload, publicKey, secretKey)
|
||||
return result.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a service.
|
||||
*/
|
||||
def serviceDestroy(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return executeDestructive("/services/${serviceId}", 'DELETE', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Freeze a service.
|
||||
*/
|
||||
def serviceFreeze(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/services/${serviceId}/freeze", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfreeze a service.
|
||||
*/
|
||||
def serviceUnfreeze(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/services/${serviceId}/unfreeze", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock a service.
|
||||
*/
|
||||
def serviceLock(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/services/${serviceId}/lock", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a service.
|
||||
*/
|
||||
def serviceUnlock(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return executeDestructive("/services/${serviceId}/unlock", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set unfreeze on demand for a service.
|
||||
*/
|
||||
def serviceSetUnfreezeOnDemand(String serviceId, boolean enabled, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequestPatch("/services/${serviceId}", [unfreeze_on_demand: enabled], publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Redeploy a service.
|
||||
*/
|
||||
def serviceRedeploy(String serviceId, String bootstrap = null, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = bootstrap ? [bootstrap: bootstrap] : [:]
|
||||
return apiRequest("/services/${serviceId}/redeploy", 'POST', payload, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service logs.
|
||||
*/
|
||||
def serviceLogs(String serviceId, boolean allLogs = false, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def path = allLogs ? "/services/${serviceId}/logs?all=true" : "/services/${serviceId}/logs"
|
||||
def result = apiRequest(path, 'GET', null, publicKey, secretKey)
|
||||
return result.logs
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute command in a service.
|
||||
*/
|
||||
def serviceExecute(String serviceId, String command, int timeoutMs = 0, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [command: command]
|
||||
if (timeoutMs > 0) payload.timeout = timeoutMs
|
||||
return apiRequest("/services/${serviceId}/execute", 'POST', payload, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service environment vault status.
|
||||
*/
|
||||
def serviceEnvGet(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/services/${serviceId}/env", 'GET', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set service environment vault.
|
||||
*/
|
||||
def serviceEnvSet(String serviceId, String envContent, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequestText("/services/${serviceId}/env", 'PUT', envContent, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete service environment vault.
|
||||
*/
|
||||
def serviceEnvDelete(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/services/${serviceId}/env", 'DELETE', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export service environment vault.
|
||||
*/
|
||||
def serviceEnvExport(String serviceId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/services/${serviceId}/env/export", 'POST', [:], publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a service.
|
||||
*/
|
||||
def serviceResize(String serviceId, int vcpu, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequestPatch("/services/${serviceId}", [vcpu: vcpu], publicKey, secretKey)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Snapshot Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* List all snapshots.
|
||||
*/
|
||||
def snapshotList(Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def result = apiRequest('/snapshots', 'GET', null, publicKey, secretKey)
|
||||
return result.snapshots ?: []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get snapshot details.
|
||||
*/
|
||||
def snapshotGet(String snapshotId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/snapshots/${snapshotId}", 'GET', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create snapshot from session.
|
||||
*/
|
||||
def snapshotSession(String sessionId, String name = null, boolean hot = false, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [session_id: sessionId, hot: hot]
|
||||
if (name) payload.name = name
|
||||
def result = apiRequest('/snapshots', 'POST', payload, publicKey, secretKey)
|
||||
return result.snapshot_id
|
||||
}
|
||||
|
||||
/**
|
||||
* Create snapshot from service.
|
||||
*/
|
||||
def snapshotService(String serviceId, String name = null, boolean hot = false, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [service_id: serviceId, hot: hot]
|
||||
if (name) payload.name = name
|
||||
def result = apiRequest('/snapshots', 'POST', payload, publicKey, secretKey)
|
||||
return result.snapshot_id
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a snapshot.
|
||||
*/
|
||||
def snapshotRestore(String snapshotId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/snapshots/${snapshotId}/restore", 'POST', [:], publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a snapshot.
|
||||
*/
|
||||
def snapshotDelete(String snapshotId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return executeDestructive("/snapshots/${snapshotId}", 'DELETE', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock a snapshot.
|
||||
*/
|
||||
def snapshotLock(String snapshotId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/snapshots/${snapshotId}/lock", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a snapshot.
|
||||
*/
|
||||
def snapshotUnlock(String snapshotId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return executeDestructive("/snapshots/${snapshotId}/unlock", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a snapshot.
|
||||
*/
|
||||
def snapshotClone(String snapshotId, String cloneType, String name = null, String ports = null, String shell = null, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [type: cloneType]
|
||||
if (name) payload.name = name
|
||||
if (ports) payload.ports = ports.split(',').collect { it.trim().toInteger() }
|
||||
if (shell) payload.shell = shell
|
||||
def result = apiRequest("/snapshots/${snapshotId}/clone", 'POST', payload, publicKey, secretKey)
|
||||
return result.session_id ?: result.service_id
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Image Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* List all images.
|
||||
*/
|
||||
def imageList(String filter = null, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def path = filter ? "/images/${filter}" : '/images'
|
||||
def result = apiRequest(path, 'GET', null, publicKey, secretKey)
|
||||
return result.images ?: []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image details.
|
||||
*/
|
||||
def imageGet(String imageId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/images/${imageId}", 'GET', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an image.
|
||||
*/
|
||||
def imagePublish(String sourceType, String sourceId, String name = null, String description = null, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [source_type: sourceType, source_id: sourceId]
|
||||
if (name) payload.name = name
|
||||
if (description) payload.description = description
|
||||
def result = apiRequest('/images', 'POST', payload, publicKey, secretKey)
|
||||
return result.image_id
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an image.
|
||||
*/
|
||||
def imageDelete(String imageId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return executeDestructive("/images/${imageId}", 'DELETE', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock an image.
|
||||
*/
|
||||
def imageLock(String imageId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/images/${imageId}/lock", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock an image.
|
||||
*/
|
||||
def imageUnlock(String imageId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return executeDestructive("/images/${imageId}/unlock", 'POST', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set image visibility.
|
||||
*/
|
||||
def imageSetVisibility(String imageId, String visibility, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/images/${imageId}/visibility", 'POST', [visibility: visibility], publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant access to an image.
|
||||
*/
|
||||
def imageGrantAccess(String imageId, String trustedApiKey, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/images/${imageId}/grant", 'POST', [trusted_api_key: trustedApiKey], publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke access to an image.
|
||||
*/
|
||||
def imageRevokeAccess(String imageId, String trustedApiKey, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/images/${imageId}/revoke", 'POST', [trusted_api_key: trustedApiKey], publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* List trusted keys for an image.
|
||||
*/
|
||||
def imageListTrusted(String imageId, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def result = apiRequest("/images/${imageId}/trusted", 'GET', null, publicKey, secretKey)
|
||||
return result.trusted ?: []
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer image ownership.
|
||||
*/
|
||||
def imageTransfer(String imageId, String toApiKey, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
return apiRequest("/images/${imageId}/transfer", 'POST', [to_api_key: toApiKey], publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a service from an image.
|
||||
*/
|
||||
def imageSpawn(String imageId, String name = null, String ports = null, String bootstrap = null, String networkMode = null, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [:]
|
||||
if (name) payload.name = name
|
||||
if (ports) payload.ports = ports.split(',').collect { it.trim().toInteger() }
|
||||
if (bootstrap) payload.bootstrap = bootstrap
|
||||
if (networkMode) payload.network_mode = networkMode
|
||||
def result = apiRequest("/images/${imageId}/spawn", 'POST', payload, publicKey, secretKey)
|
||||
return result.service_id
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an image.
|
||||
*/
|
||||
def imageClone(String imageId, String name = null, String description = null, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def payload = [:]
|
||||
if (name) payload.name = name
|
||||
if (description) payload.description = description
|
||||
def result = apiRequest("/images/${imageId}/clone", 'POST', payload, publicKey, secretKey)
|
||||
return result.image_id
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PaaS Logs Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch batch logs.
|
||||
*/
|
||||
def logsFetch(String source = 'all', int lines = 100, String since = null, String grep = null, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
def params = ["source=${source}", "lines=${lines}"]
|
||||
if (since) params << "since=${since}"
|
||||
if (grep) params << "grep=${URLEncoder.encode(grep, 'UTF-8')}"
|
||||
return apiRequest("/paas/logs?${params.join('&')}", 'GET', null, publicKey, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface for log streaming.
|
||||
*/
|
||||
interface LogCallback {
|
||||
void onLogLine(String source, String line)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream logs via SSE. Blocks until interrupted or server closes.
|
||||
*
|
||||
* @param source Log source ('all', 'api', 'portal', 'pool/cammy', 'pool/ai')
|
||||
* @param grep Optional filter pattern
|
||||
* @param callback Callback for each log line
|
||||
* @param options Optional parameters (publicKey, secretKey)
|
||||
* @return true on clean shutdown, false on error
|
||||
*/
|
||||
def logsStream(String source = 'all', String grep = null, LogCallback callback, Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
|
||||
def path = "/paas/logs/stream?source=${source ?: 'all'}"
|
||||
if (grep) {
|
||||
path += "&grep=${URLEncoder.encode(grep, 'UTF-8')}"
|
||||
}
|
||||
|
||||
def timestamp = (System.currentTimeMillis() / 1000) as long
|
||||
def signature = signRequest(secretKey, timestamp, 'GET', path, '')
|
||||
|
||||
def url = new URL("${API_BASE}${path}")
|
||||
def connection = url.openConnection() as java.net.HttpURLConnection
|
||||
|
||||
connection.requestMethod = 'GET'
|
||||
connection.setRequestProperty('Authorization', "Bearer ${publicKey}")
|
||||
connection.setRequestProperty('X-Timestamp', timestamp.toString())
|
||||
connection.setRequestProperty('X-Signature', signature)
|
||||
connection.setRequestProperty('Accept', 'text/event-stream')
|
||||
connection.connectTimeout = 30000
|
||||
connection.readTimeout = 0 // No timeout for streaming
|
||||
|
||||
if (connection.responseCode != 200) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
def reader = new BufferedReader(new InputStreamReader(connection.inputStream, 'UTF-8'))
|
||||
def currentSource = source ?: 'all'
|
||||
def line
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith('data: ')) {
|
||||
def data = line.substring(6)
|
||||
if (callback) {
|
||||
callback.onLogLine(currentSource, data)
|
||||
}
|
||||
} else if (line.startsWith('event: ')) {
|
||||
currentSource = line.substring(7)
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch (Exception e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate API keys.
|
||||
*/
|
||||
def validateKeys(Map options = [:]) {
|
||||
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
|
||||
|
||||
def timestamp = (System.currentTimeMillis() / 1000) as long
|
||||
def message = "${timestamp}:POST:/keys/validate:{}"
|
||||
def signature = signRequest(secretKey, timestamp, 'POST', '/keys/validate', '{}')
|
||||
|
||||
def url = new URL("${PORTAL_BASE}/keys/validate")
|
||||
def connection = url.openConnection() as java.net.HttpURLConnection
|
||||
|
||||
connection.requestMethod = 'POST'
|
||||
connection.setRequestProperty('Authorization', "Bearer ${publicKey}")
|
||||
connection.setRequestProperty('X-Timestamp', timestamp.toString())
|
||||
connection.setRequestProperty('X-Signature', signature)
|
||||
connection.setRequestProperty('Content-Type', 'application/json')
|
||||
connection.connectTimeout = 30000
|
||||
connection.readTimeout = 30000
|
||||
connection.doOutput = true
|
||||
connection.outputStream.withWriter { it.write('{}') }
|
||||
|
||||
if (connection.responseCode !in 200..299) {
|
||||
throw new APIError("HTTP ${connection.responseCode}")
|
||||
}
|
||||
|
||||
return new JsonSlurper().parseText(connection.inputStream.text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect programming language from file extension or shebang.
|
||||
*
|
||||
|
|
|
|||
453
clients/groovy/sync/tests/UnTest.groovy
Normal file
453
clients/groovy/sync/tests/UnTest.groovy
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
#!/usr/bin/env groovy
|
||||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Unit tests for Un SDK - Groovy Synchronous client
|
||||
|
||||
import groovy.test.GroovyTestCase
|
||||
|
||||
/**
|
||||
* Test suite for the Unsandbox Groovy SDK.
|
||||
*
|
||||
* Run with: groovy UnTest.groovy
|
||||
*
|
||||
* Integration tests require UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY
|
||||
* environment variables to be set.
|
||||
*/
|
||||
class UnTest extends GroovyTestCase {
|
||||
|
||||
// Load the SDK
|
||||
static {
|
||||
def sdkPath = new File(UnTest.class.protectionDomain.codeSource.location.path).parentFile.parentFile
|
||||
evaluate(new File(sdkPath, 'src/un.groovy'))
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Language Detection Tests
|
||||
// ========================================================================
|
||||
|
||||
void testDetectPython() {
|
||||
assertEquals("python", detectLanguage("script.py"))
|
||||
assertEquals("python", detectLanguage("path/to/script.py"))
|
||||
}
|
||||
|
||||
void testDetectJavaScript() {
|
||||
assertEquals("javascript", detectLanguage("app.js"))
|
||||
}
|
||||
|
||||
void testDetectTypeScript() {
|
||||
assertEquals("typescript", detectLanguage("app.ts"))
|
||||
}
|
||||
|
||||
void testDetectGo() {
|
||||
assertEquals("go", detectLanguage("main.go"))
|
||||
}
|
||||
|
||||
void testDetectRust() {
|
||||
assertEquals("rust", detectLanguage("lib.rs"))
|
||||
}
|
||||
|
||||
void testDetectJava() {
|
||||
assertEquals("java", detectLanguage("Main.java"))
|
||||
}
|
||||
|
||||
void testDetectKotlin() {
|
||||
assertEquals("kotlin", detectLanguage("Main.kt"))
|
||||
}
|
||||
|
||||
void testDetectGroovy() {
|
||||
assertEquals("groovy", detectLanguage("script.groovy"))
|
||||
}
|
||||
|
||||
void testDetectCpp() {
|
||||
assertEquals("cpp", detectLanguage("main.cpp"))
|
||||
}
|
||||
|
||||
void testDetectC() {
|
||||
assertEquals("c", detectLanguage("main.c"))
|
||||
}
|
||||
|
||||
void testDetectRuby() {
|
||||
assertEquals("ruby", detectLanguage("script.rb"))
|
||||
}
|
||||
|
||||
void testDetectPhp() {
|
||||
assertEquals("php", detectLanguage("index.php"))
|
||||
}
|
||||
|
||||
void testDetectUnknown() {
|
||||
assertNull(detectLanguage("file.unknown"))
|
||||
assertNull(detectLanguage("noextension"))
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Utility Function Tests
|
||||
// ========================================================================
|
||||
|
||||
void testVersionString() {
|
||||
def ver = version()
|
||||
assertNotNull(ver)
|
||||
assertTrue("Version should be in X.Y.Z format", ver ==~ /\d+\.\d+\.\d+/)
|
||||
}
|
||||
|
||||
void testHmacSignature() {
|
||||
def signature = hmacSign("secret", "message")
|
||||
assertNotNull(signature)
|
||||
assertEquals("HMAC-SHA256 should produce 64 hex chars", 64, signature.length())
|
||||
assertTrue("Signature should be lowercase hex", signature ==~ /[0-9a-f]+/)
|
||||
}
|
||||
|
||||
void testHmacConsistent() {
|
||||
def sig1 = hmacSign("key", "data")
|
||||
def sig2 = hmacSign("key", "data")
|
||||
assertEquals("Same inputs should produce same signature", sig1, sig2)
|
||||
}
|
||||
|
||||
void testHmacDifferent() {
|
||||
def sig1 = hmacSign("key1", "data")
|
||||
def sig2 = hmacSign("key2", "data")
|
||||
assertFalse("Different keys should produce different signatures", sig1 == sig2)
|
||||
}
|
||||
|
||||
void testHmacKnownValue() {
|
||||
// HMAC-SHA256("key", "The quick brown fox jumps over the lazy dog")
|
||||
// Known value from various implementations
|
||||
def signature = hmacSign("key", "The quick brown fox jumps over the lazy dog")
|
||||
assertEquals("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", signature)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Health Check Tests
|
||||
// ========================================================================
|
||||
|
||||
void testHealthCheckReturnsBoolean() {
|
||||
def healthy = healthCheck()
|
||||
// We just verify it returns a boolean without throwing
|
||||
assertTrue(healthy instanceof Boolean)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Exception Tests
|
||||
// ========================================================================
|
||||
|
||||
void testUnsandboxError() {
|
||||
def error = new UnsandboxError("Test message")
|
||||
assertEquals("Test message", error.message)
|
||||
}
|
||||
|
||||
void testAuthenticationError() {
|
||||
def error = new AuthenticationError("Auth failed")
|
||||
assertEquals("Auth failed", error.message)
|
||||
assertTrue(error instanceof UnsandboxError)
|
||||
}
|
||||
|
||||
void testExecutionError() {
|
||||
def error = new ExecutionError("Exec failed", 1, "stderr output")
|
||||
assertEquals("Exec failed", error.message)
|
||||
assertEquals(1, error.exitCode)
|
||||
assertEquals("stderr output", error.stderr)
|
||||
}
|
||||
|
||||
void testAPIError() {
|
||||
def error = new APIError("API failed", 500, '{"error": "internal"}')
|
||||
assertEquals("API failed", error.message)
|
||||
assertEquals(500, error.statusCode)
|
||||
assertEquals('{"error": "internal"}', error.response)
|
||||
}
|
||||
|
||||
void testTimeoutError() {
|
||||
def error = new TimeoutError("Operation timed out")
|
||||
assertEquals("Operation timed out", error.message)
|
||||
assertTrue(error instanceof UnsandboxError)
|
||||
}
|
||||
|
||||
void testSudoChallengeError() {
|
||||
def error = new SudoChallengeError("challenge-123", '{"challenge_id": "challenge-123"}')
|
||||
assertEquals("challenge-123", error.challengeId)
|
||||
assertEquals('{"challenge_id": "challenge-123"}', error.responseBody)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Extension Map Tests
|
||||
// ========================================================================
|
||||
|
||||
void testExtensionMapComplete() {
|
||||
// Verify the EXT_MAP has all expected extensions
|
||||
assertNotNull(EXT_MAP['.py'])
|
||||
assertNotNull(EXT_MAP['.js'])
|
||||
assertNotNull(EXT_MAP['.ts'])
|
||||
assertNotNull(EXT_MAP['.go'])
|
||||
assertNotNull(EXT_MAP['.rs'])
|
||||
assertNotNull(EXT_MAP['.java'])
|
||||
assertNotNull(EXT_MAP['.kt'])
|
||||
assertNotNull(EXT_MAP['.groovy'])
|
||||
assertNotNull(EXT_MAP['.rb'])
|
||||
assertNotNull(EXT_MAP['.php'])
|
||||
assertNotNull(EXT_MAP['.c'])
|
||||
assertNotNull(EXT_MAP['.cpp'])
|
||||
assertNotNull(EXT_MAP['.sh'])
|
||||
assertNotNull(EXT_MAP['.lua'])
|
||||
assertNotNull(EXT_MAP['.pl'])
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Integration Tests (requires credentials)
|
||||
// ========================================================================
|
||||
|
||||
void testExecutePythonCode() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def result = execute("python", 'print("Hello, World!")', [
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(result)
|
||||
assertTrue(result.stdout?.contains("Hello, World!") ?: false)
|
||||
}
|
||||
|
||||
void testExecuteJavaScriptCode() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def result = execute("javascript", 'console.log("Hello from JS")', [
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(result)
|
||||
assertTrue(result.stdout?.contains("Hello from JS") ?: false)
|
||||
}
|
||||
|
||||
void testGetLanguages() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def result = languages([
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey,
|
||||
forceRefresh: true
|
||||
])
|
||||
|
||||
assertNotNull(result)
|
||||
assertNotNull(result.languages)
|
||||
assertTrue(result.languages.size() > 0)
|
||||
assertTrue(result.languages.contains("python"))
|
||||
assertTrue(result.languages.contains("javascript"))
|
||||
}
|
||||
|
||||
void testListJobs() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def jobs = listJobs([
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(jobs)
|
||||
// Jobs list can be empty if no jobs are running
|
||||
}
|
||||
|
||||
void testValidateKeys() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def result = validateKeys([
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(result)
|
||||
}
|
||||
|
||||
void testListSessions() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def sessions = sessionList([
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(sessions)
|
||||
}
|
||||
|
||||
void testListServices() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def services = serviceList([
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(services)
|
||||
}
|
||||
|
||||
void testListSnapshots() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def snapshots = snapshotList([
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(snapshots)
|
||||
}
|
||||
|
||||
void testListImages() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def images = imageList(null, [
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(images)
|
||||
}
|
||||
|
||||
void testAsyncExecution() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def job = executeAsync("python", 'print("Async test")', [
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(job)
|
||||
assertNotNull(job.job_id)
|
||||
|
||||
// Wait for completion
|
||||
def result = wait(job.job_id, [
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey,
|
||||
maxPolls: 30
|
||||
])
|
||||
|
||||
assertNotNull(result)
|
||||
assertTrue(result.stdout?.contains("Async test") ?: (result.result?.stdout?.contains("Async test") ?: false))
|
||||
}
|
||||
|
||||
void testLogsFetch() {
|
||||
def publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
def secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
println "Skipping integration test - credentials not set"
|
||||
return
|
||||
}
|
||||
|
||||
def result = logsFetch('all', 10, null, null, [
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey
|
||||
])
|
||||
|
||||
assertNotNull(result)
|
||||
}
|
||||
|
||||
void testLogCallbackInterface() {
|
||||
// Verify LogCallback interface exists and can be implemented
|
||||
def callback = { source, line ->
|
||||
assertNotNull(source)
|
||||
assertNotNull(line)
|
||||
} as LogCallback
|
||||
|
||||
assertNotNull(callback)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Run all tests
|
||||
// ========================================================================
|
||||
|
||||
static void main(String[] args) {
|
||||
println "Running Unsandbox Groovy SDK Tests..."
|
||||
println "=" * 60
|
||||
|
||||
def test = new UnTest()
|
||||
def methods = UnTest.class.declaredMethods.findAll {
|
||||
it.name.startsWith('test') && it.parameterCount == 0
|
||||
}
|
||||
|
||||
int passed = 0
|
||||
int failed = 0
|
||||
int skipped = 0
|
||||
|
||||
methods.each { method ->
|
||||
print "Testing ${method.name}... "
|
||||
try {
|
||||
method.invoke(test)
|
||||
println "PASS"
|
||||
passed++
|
||||
} catch (Exception e) {
|
||||
def cause = e.cause ?: e
|
||||
if (cause.message?.contains("Skipping")) {
|
||||
println "SKIP"
|
||||
skipped++
|
||||
} else {
|
||||
println "FAIL: ${cause.message}"
|
||||
failed++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println "=" * 60
|
||||
println "Results: ${passed} passed, ${failed} failed, ${skipped} skipped"
|
||||
|
||||
if (failed > 0) {
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -345,6 +345,499 @@ parseExecute args =
|
|||
let (k, v) = span (/= '=') kv
|
||||
in (k, drop 1 v)
|
||||
|
||||
-- ============================================================================
|
||||
-- Library API
|
||||
-- ============================================================================
|
||||
|
||||
-- SDK Version
|
||||
sdkVersion :: String
|
||||
sdkVersion = "4.2.0"
|
||||
|
||||
-- | Return the SDK version
|
||||
version :: String
|
||||
version = sdkVersion
|
||||
|
||||
-- | Check API health
|
||||
healthCheck :: IO Bool
|
||||
healthCheck = do
|
||||
(exitCode, stdout, _) <- readProcessWithExitCode "curl"
|
||||
["-s", "-o", "/dev/null", "-w", "%{http_code}", apiBase ++ "/health"] ""
|
||||
return $ filter isDigit stdout == "200"
|
||||
|
||||
-- | Generate HMAC-SHA256 signature for a message
|
||||
hmacSign :: String -> String -> String
|
||||
hmacSign = hmacSha256
|
||||
|
||||
-- | Detect language from filename extension
|
||||
detectLanguage :: String -> Maybe String
|
||||
detectLanguage filename = extToLang (takeExtension filename)
|
||||
|
||||
-- | Get list of supported languages (list of strings)
|
||||
getLanguages :: IO [String]
|
||||
getLanguages = do
|
||||
cached <- loadLanguagesCache
|
||||
case cached of
|
||||
Just languages -> return languages
|
||||
Nothing -> do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/languages")
|
||||
let languages = maybe [] id (extractJsonArray stdout "languages")
|
||||
when (not (null languages)) $ saveLanguagesCache languages
|
||||
return languages
|
||||
|
||||
-- | Execute code synchronously
|
||||
execute :: String -> String -> IO (Either String (Bool, String, String, Int))
|
||||
execute language code = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"language\":\"" ++ escapeJSON language ++ "\",\"code\":\"" ++ escapeJSON code ++ "\"}"
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/execute") json
|
||||
case exitCode of
|
||||
ExitSuccess ->
|
||||
let success = case extractJsonString stdout "exit_code" of
|
||||
Just "0" -> True
|
||||
_ -> False
|
||||
stdoutVal = maybe "" id (extractJsonString stdout "stdout")
|
||||
stderrVal = maybe "" id (extractJsonString stdout "stderr")
|
||||
exitCodeVal = case extractJsonString stdout "exit_code" of
|
||||
Just s -> read (filter isDigit s) :: Int
|
||||
_ -> 0
|
||||
in return $ Right (success, stdoutVal, stderrVal, exitCodeVal)
|
||||
_ -> return $ Left "Execution failed"
|
||||
|
||||
-- | Execute code asynchronously, returning a job ID
|
||||
executeAsync :: String -> String -> IO (Maybe String)
|
||||
executeAsync language code = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"language\":\"" ++ escapeJSON language ++ "\",\"code\":\"" ++ escapeJSON code ++ "\"}"
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/execute/async") json
|
||||
case exitCode of
|
||||
ExitSuccess -> return $ extractJsonString stdout "job_id"
|
||||
_ -> return Nothing
|
||||
|
||||
-- | Get job status
|
||||
getJob :: String -> IO (Maybe (String, String))
|
||||
getJob jobId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlGet apiKey (apiBase ++ "/jobs/" ++ jobId)
|
||||
case exitCode of
|
||||
ExitSuccess ->
|
||||
let status = maybe "unknown" id (extractJsonString stdout "status")
|
||||
language = maybe "" id (extractJsonString stdout "language")
|
||||
in return $ Just (status, language)
|
||||
_ -> return Nothing
|
||||
|
||||
-- | Wait for job completion
|
||||
waitJob :: String -> IO (Either String (Bool, String, String, Int))
|
||||
waitJob jobId = waitJobLoop jobId 0 100
|
||||
where
|
||||
pollDelays = [300, 450, 700, 900, 650, 1600, 2000]
|
||||
terminalStates = ["completed", "failed", "timeout", "cancelled"]
|
||||
|
||||
waitJobLoop jid pollCount maxPolls
|
||||
| pollCount >= maxPolls = return $ Left "Max polls exceeded"
|
||||
| otherwise = do
|
||||
let delayIdx = min pollCount (length pollDelays - 1)
|
||||
let delayMs = pollDelays !! delayIdx
|
||||
threadDelay (delayMs * 1000) -- threadDelay takes microseconds
|
||||
|
||||
result <- getJob jid
|
||||
case result of
|
||||
Just (status, _) | status `elem` terminalStates -> do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/jobs/" ++ jid)
|
||||
let success = case extractJsonString stdout "exit_code" of
|
||||
Just "0" -> True
|
||||
_ -> False
|
||||
stdoutVal = maybe "" id (extractJsonString stdout "stdout")
|
||||
stderrVal = maybe "" id (extractJsonString stdout "stderr")
|
||||
exitCodeVal = case extractJsonString stdout "exit_code" of
|
||||
Just s -> read (filter isDigit s) :: Int
|
||||
_ -> 1
|
||||
return $ Right (success, stdoutVal, stderrVal, exitCodeVal)
|
||||
_ -> waitJobLoop jid (pollCount + 1) maxPolls
|
||||
|
||||
-- | Cancel a running job
|
||||
cancelJob :: String -> IO Bool
|
||||
cancelJob jobId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlDelete apiKey (apiBase ++ "/jobs/" ++ jobId)
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | List all active jobs
|
||||
listJobs :: IO String
|
||||
listJobs = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/jobs")
|
||||
return stdout
|
||||
|
||||
-- | List all sessions
|
||||
sessionList :: IO String
|
||||
sessionList = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/sessions")
|
||||
return stdout
|
||||
|
||||
-- | Get session details
|
||||
sessionGet :: String -> IO String
|
||||
sessionGet sessionId = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/sessions/" ++ sessionId)
|
||||
return stdout
|
||||
|
||||
-- | Create a new session
|
||||
sessionCreate :: Maybe String -> Maybe String -> IO (Maybe String)
|
||||
sessionCreate shell network = do
|
||||
apiKey <- getApiKey
|
||||
let shellVal = maybe "bash" id shell
|
||||
let networkJson = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") network
|
||||
let json = "{\"shell\":\"" ++ shellVal ++ "\"" ++ networkJson ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/sessions") json
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | Destroy a session
|
||||
sessionDestroy :: String -> IO Bool
|
||||
sessionDestroy sessionId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlDelete apiKey (apiBase ++ "/sessions/" ++ sessionId)
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Freeze a session
|
||||
sessionFreeze :: String -> IO Bool
|
||||
sessionFreeze sessionId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/sessions/" ++ sessionId ++ "/freeze") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Unfreeze a session
|
||||
sessionUnfreeze :: String -> IO Bool
|
||||
sessionUnfreeze sessionId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/sessions/" ++ sessionId ++ "/unfreeze") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Boost session resources
|
||||
sessionBoost :: String -> Int -> IO Bool
|
||||
sessionBoost sessionId vcpu = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"vcpu\":" ++ show vcpu ++ "}"
|
||||
(exitCode, stdout, _) <- curlPatch apiKey (apiBase ++ "/sessions/" ++ sessionId) json
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Unboost session
|
||||
sessionUnboost :: String -> IO Bool
|
||||
sessionUnboost sessionId = sessionBoost sessionId 1
|
||||
|
||||
-- | Execute a command in a session
|
||||
sessionExecute :: String -> String -> IO String
|
||||
sessionExecute sessionId command = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"command\":\"" ++ escapeJSON command ++ "\"}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/sessions/" ++ sessionId ++ "/execute") json
|
||||
return stdout
|
||||
|
||||
-- | List all services
|
||||
serviceList :: IO String
|
||||
serviceList = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/services")
|
||||
return stdout
|
||||
|
||||
-- | Get service details
|
||||
serviceGet :: String -> IO String
|
||||
serviceGet serviceId = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/services/" ++ serviceId)
|
||||
return stdout
|
||||
|
||||
-- | Create a new service
|
||||
serviceCreate :: String -> Maybe String -> Maybe String -> Maybe String -> IO (Maybe String)
|
||||
serviceCreate name ports bootstrap network = do
|
||||
apiKey <- getApiKey
|
||||
let portsJson = maybe "" (\p -> ",\"ports\":[" ++ p ++ "]") ports
|
||||
let bootstrapJson = maybe "" (\b -> ",\"bootstrap\":\"" ++ escapeJSON b ++ "\"") bootstrap
|
||||
let networkJson = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") network
|
||||
let json = "{\"name\":\"" ++ escapeJSON name ++ "\"" ++ portsJson ++ bootstrapJson ++ networkJson ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/services") json
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | Destroy a service
|
||||
serviceDestroy :: String -> IO Bool
|
||||
serviceDestroy serviceId = do
|
||||
result <- curlDeleteWithSudo "" (apiBase ++ "/services/" ++ serviceId)
|
||||
case result of
|
||||
SudoSuccess _ -> return True
|
||||
_ -> return False
|
||||
|
||||
-- | Freeze a service
|
||||
serviceFreeze :: String -> IO Bool
|
||||
serviceFreeze serviceId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/services/" ++ serviceId ++ "/freeze") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Unfreeze a service
|
||||
serviceUnfreeze :: String -> IO Bool
|
||||
serviceUnfreeze serviceId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/services/" ++ serviceId ++ "/unfreeze") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Lock a service
|
||||
serviceLock :: String -> IO Bool
|
||||
serviceLock serviceId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/services/" ++ serviceId ++ "/lock") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Unlock a service
|
||||
serviceUnlock :: String -> IO Bool
|
||||
serviceUnlock serviceId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/services/" ++ serviceId ++ "/unlock") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Set unfreeze-on-demand for a service
|
||||
serviceSetUnfreezeOnDemand :: String -> Bool -> IO Bool
|
||||
serviceSetUnfreezeOnDemand serviceId enabled = do
|
||||
apiKey <- getApiKey
|
||||
let enabledStr = if enabled then "true" else "false"
|
||||
let json = "{\"unfreeze_on_demand\":" ++ enabledStr ++ "}"
|
||||
(exitCode, stdout, _) <- curlPatch apiKey (apiBase ++ "/services/" ++ serviceId) json
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Redeploy a service
|
||||
serviceRedeploy :: String -> Maybe String -> IO Bool
|
||||
serviceRedeploy serviceId bootstrap = do
|
||||
apiKey <- getApiKey
|
||||
let bootstrapJson = maybe "" (\b -> "\"bootstrap\":\"" ++ escapeJSON b ++ "\"") bootstrap
|
||||
let json = "{" ++ bootstrapJson ++ "}"
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/services/" ++ serviceId ++ "/redeploy") json
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Get service logs
|
||||
serviceLogs :: String -> Bool -> IO String
|
||||
serviceLogs serviceId allLogs = do
|
||||
apiKey <- getApiKey
|
||||
let endpoint = if allLogs
|
||||
then "/services/" ++ serviceId ++ "/logs?all=true"
|
||||
else "/services/" ++ serviceId ++ "/logs"
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ endpoint)
|
||||
return stdout
|
||||
|
||||
-- | Execute a command in a service
|
||||
serviceExecute :: String -> String -> IO String
|
||||
serviceExecute serviceId command = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"command\":\"" ++ escapeJSON command ++ "\"}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/services/" ++ serviceId ++ "/execute") json
|
||||
return stdout
|
||||
|
||||
-- | Resize a service
|
||||
serviceResize :: String -> Int -> IO Bool
|
||||
serviceResize serviceId vcpu = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"vcpu\":" ++ show vcpu ++ "}"
|
||||
(exitCode, stdout, _) <- curlPatch apiKey (apiBase ++ "/services/" ++ serviceId) json
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | List all snapshots
|
||||
snapshotList :: IO String
|
||||
snapshotList = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/snapshots")
|
||||
return stdout
|
||||
|
||||
-- | Get snapshot details
|
||||
snapshotGet :: String -> IO String
|
||||
snapshotGet snapshotId = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/snapshots/" ++ snapshotId)
|
||||
return stdout
|
||||
|
||||
-- | Create a snapshot of a session
|
||||
snapshotSession :: String -> Maybe String -> Bool -> IO (Maybe String)
|
||||
snapshotSession sessionId name hot = do
|
||||
apiKey <- getApiKey
|
||||
let nameJson = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\",") name
|
||||
let hotJson = if hot then "\"hot\":true" else "\"hot\":false"
|
||||
let json = "{" ++ nameJson ++ hotJson ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/sessions/" ++ sessionId ++ "/snapshot") json
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | Create a snapshot of a service
|
||||
snapshotService :: String -> Maybe String -> Bool -> IO (Maybe String)
|
||||
snapshotService serviceId name hot = do
|
||||
apiKey <- getApiKey
|
||||
let nameJson = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\",") name
|
||||
let hotJson = if hot then "\"hot\":true" else "\"hot\":false"
|
||||
let json = "{" ++ nameJson ++ hotJson ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/services/" ++ serviceId ++ "/snapshot") json
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | Restore from a snapshot
|
||||
snapshotRestore :: String -> IO (Maybe String)
|
||||
snapshotRestore snapshotId = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/snapshots/" ++ snapshotId ++ "/restore") "{}"
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | Delete a snapshot
|
||||
snapshotDelete :: String -> IO Bool
|
||||
snapshotDelete snapshotId = do
|
||||
result <- curlDeleteWithSudo "" (apiBase ++ "/snapshots/" ++ snapshotId)
|
||||
case result of
|
||||
SudoSuccess _ -> return True
|
||||
_ -> return False
|
||||
|
||||
-- | Lock a snapshot
|
||||
snapshotLock :: String -> IO Bool
|
||||
snapshotLock snapshotId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/snapshots/" ++ snapshotId ++ "/lock") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Unlock a snapshot
|
||||
snapshotUnlock :: String -> IO Bool
|
||||
snapshotUnlock snapshotId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/snapshots/" ++ snapshotId ++ "/unlock") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Clone a snapshot to create a new session or service
|
||||
snapshotClone :: String -> String -> Maybe String -> Maybe String -> Maybe String -> IO (Maybe String)
|
||||
snapshotClone snapshotId cloneType name ports shell = do
|
||||
apiKey <- getApiKey
|
||||
let typeJson = "\"type\":\"" ++ cloneType ++ "\""
|
||||
let nameJson = maybe "" (\n -> ",\"name\":\"" ++ escapeJSON n ++ "\"") name
|
||||
let portsJson = maybe "" (\p -> ",\"ports\":[" ++ p ++ "]") ports
|
||||
let shellJson = maybe "" (\s -> ",\"shell\":\"" ++ s ++ "\"") shell
|
||||
let json = "{" ++ typeJson ++ nameJson ++ portsJson ++ shellJson ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/snapshots/" ++ snapshotId ++ "/clone") json
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | List images
|
||||
imageList :: Maybe String -> IO String
|
||||
imageList filter' = do
|
||||
apiKey <- getApiKey
|
||||
let endpoint = maybe "/images" (\f -> "/images?filter=" ++ f) filter'
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ endpoint)
|
||||
return stdout
|
||||
|
||||
-- | Get image details
|
||||
imageGet :: String -> IO String
|
||||
imageGet imageId = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/images/" ++ imageId)
|
||||
return stdout
|
||||
|
||||
-- | Publish an image
|
||||
imagePublish :: String -> String -> Maybe String -> Maybe String -> IO (Maybe String)
|
||||
imagePublish sourceType sourceId name description = do
|
||||
apiKey <- getApiKey
|
||||
let nameJson = maybe "" (\n -> ",\"name\":\"" ++ escapeJSON n ++ "\"") name
|
||||
let descJson = maybe "" (\d -> ",\"description\":\"" ++ escapeJSON d ++ "\"") description
|
||||
let json = "{\"source_type\":\"" ++ sourceType ++ "\",\"source_id\":\"" ++ sourceId ++ "\"" ++ nameJson ++ descJson ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/images/publish") json
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | Delete an image
|
||||
imageDelete :: String -> IO Bool
|
||||
imageDelete imageId = do
|
||||
result <- curlDeleteWithSudo "" (apiBase ++ "/images/" ++ imageId)
|
||||
case result of
|
||||
SudoSuccess _ -> return True
|
||||
_ -> return False
|
||||
|
||||
-- | Lock an image
|
||||
imageLock :: String -> IO Bool
|
||||
imageLock imageId = do
|
||||
apiKey <- getApiKey
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/images/" ++ imageId ++ "/lock") "{}"
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Unlock an image
|
||||
imageUnlock :: String -> IO Bool
|
||||
imageUnlock imageId = do
|
||||
result <- curlPostWithSudo "" (apiBase ++ "/images/" ++ imageId ++ "/unlock") "{}"
|
||||
case result of
|
||||
SudoSuccess _ -> return True
|
||||
_ -> return False
|
||||
|
||||
-- | Set image visibility
|
||||
imageSetVisibility :: String -> String -> IO Bool
|
||||
imageSetVisibility imageId visibility = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"visibility\":\"" ++ visibility ++ "\"}"
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/images/" ++ imageId ++ "/visibility") json
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Grant access to an image
|
||||
imageGrantAccess :: String -> String -> IO Bool
|
||||
imageGrantAccess imageId trustedApiKey = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"api_key\":\"" ++ trustedApiKey ++ "\"}"
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/images/" ++ imageId ++ "/access/grant") json
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Revoke access to an image
|
||||
imageRevokeAccess :: String -> String -> IO Bool
|
||||
imageRevokeAccess imageId trustedApiKey = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"api_key\":\"" ++ trustedApiKey ++ "\"}"
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/images/" ++ imageId ++ "/access/revoke") json
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | List trusted API keys for an image
|
||||
imageListTrusted :: String -> IO String
|
||||
imageListTrusted imageId = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlGet apiKey (apiBase ++ "/images/" ++ imageId ++ "/access")
|
||||
return stdout
|
||||
|
||||
-- | Transfer image ownership
|
||||
imageTransfer :: String -> String -> IO Bool
|
||||
imageTransfer imageId toApiKey = do
|
||||
apiKey <- getApiKey
|
||||
let json = "{\"to_api_key\":\"" ++ toApiKey ++ "\"}"
|
||||
(exitCode, stdout, _) <- curlPost apiKey (apiBase ++ "/images/" ++ imageId ++ "/transfer") json
|
||||
return $ exitCode == ExitSuccess && not ("\"error\"" `isPrefixOf` dropWhile (/= '"') stdout)
|
||||
|
||||
-- | Spawn a service from an image
|
||||
imageSpawn :: String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> IO (Maybe String)
|
||||
imageSpawn imageId name ports bootstrap network = do
|
||||
apiKey <- getApiKey
|
||||
let nameJson = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\"") name
|
||||
let portsJson = maybe "" (\p -> (if null nameJson then "" else ",") ++ "\"ports\":[" ++ p ++ "]") ports
|
||||
let bootstrapJson = maybe "" (\b -> ",\"bootstrap\":\"" ++ escapeJSON b ++ "\"") bootstrap
|
||||
let networkJson = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") network
|
||||
let json = "{" ++ nameJson ++ portsJson ++ bootstrapJson ++ networkJson ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/images/" ++ imageId ++ "/spawn") json
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | Clone an image
|
||||
imageClone :: String -> Maybe String -> Maybe String -> IO (Maybe String)
|
||||
imageClone imageId name description = do
|
||||
apiKey <- getApiKey
|
||||
let nameJson = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\"") name
|
||||
let descJson = maybe "" (\d -> (if null nameJson then "" else ",") ++ "\"description\":\"" ++ escapeJSON d ++ "\"") description
|
||||
let json = "{" ++ nameJson ++ descJson ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey (apiBase ++ "/images/" ++ imageId ++ "/clone") json
|
||||
return $ extractJsonString stdout "id"
|
||||
|
||||
-- | Validate API keys
|
||||
validateKeys :: IO String
|
||||
validateKeys = do
|
||||
apiKey <- getApiKey
|
||||
(_, stdout, _) <- curlPostPortal apiKey (portalBase ++ "/keys/validate") "{}"
|
||||
return stdout
|
||||
|
||||
-- Helper for threadDelay (microseconds)
|
||||
threadDelay :: Int -> IO ()
|
||||
threadDelay us = do
|
||||
let ms = us `div` 1000
|
||||
_ <- readProcessWithExitCode "sleep" [show (fromIntegral ms / 1000.0 :: Double)] ""
|
||||
return ()
|
||||
|
||||
-- Main
|
||||
main :: IO ()
|
||||
main = do
|
||||
|
|
|
|||
182
clients/haskell/sync/tests/test_functional.hs
Executable file
182
clients/haskell/sync/tests/test_functional.hs
Executable file
|
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env runhaskell
|
||||
|
||||
{-
|
||||
Functional Tests for Un Haskell SDK
|
||||
|
||||
Run with: runhaskell test_functional.hs
|
||||
Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables
|
||||
|
||||
These tests make real API calls to api.unsandbox.com
|
||||
-}
|
||||
|
||||
import System.Exit (exitFailure, exitSuccess)
|
||||
import System.Environment (lookupEnv)
|
||||
import System.Process (readProcessWithExitCode)
|
||||
import Data.List (isPrefixOf, isInfixOf)
|
||||
import Data.Char (isDigit)
|
||||
|
||||
-- ANSI colors
|
||||
blue, red, green, yellow, reset :: String
|
||||
blue = "\x1b[34m"
|
||||
red = "\x1b[31m"
|
||||
green = "\x1b[32m"
|
||||
yellow = "\x1b[33m"
|
||||
reset = "\x1b[0m"
|
||||
|
||||
-- API constants
|
||||
apiBase :: String
|
||||
apiBase = "https://api.unsandbox.com"
|
||||
|
||||
portalBase :: String
|
||||
portalBase = "https://unsandbox.com"
|
||||
|
||||
-- Import HMAC from crypto library
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as BSC
|
||||
import Crypto.Hash.SHA256 (hmac)
|
||||
import Text.Printf (printf)
|
||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||
|
||||
hmacSha256 :: String -> String -> String
|
||||
hmacSha256 secret message =
|
||||
let secretBS = BSC.pack secret
|
||||
messageBS = BSC.pack message
|
||||
mac = hmac secretBS messageBS
|
||||
in concatMap (printf "%02x") (BS.unpack mac)
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "\n" ++ blue ++ "=== Un Haskell SDK Functional Tests ===" ++ reset ++ "\n"
|
||||
|
||||
-- Check for credentials
|
||||
publicKey <- lookupEnv "UNSANDBOX_PUBLIC_KEY"
|
||||
secretKey <- lookupEnv "UNSANDBOX_SECRET_KEY"
|
||||
|
||||
case (publicKey, secretKey) of
|
||||
(Just pk, Just sk) -> do
|
||||
results <- sequence
|
||||
[ runTest "health_check" (testHealthCheck pk sk)
|
||||
, runTest "validate_keys" (testValidateKeys pk sk)
|
||||
, runTest "execute_python" (testExecutePython pk sk)
|
||||
, runTest "execute_with_error" (testExecuteWithError pk sk)
|
||||
, runTest "session_list" (testSessionList pk sk)
|
||||
, runTest "service_list" (testServiceList pk sk)
|
||||
, runTest "snapshot_list" (testSnapshotList pk sk)
|
||||
, runTest "image_list" (testImageList pk sk)
|
||||
]
|
||||
|
||||
let passed = length $ filter id results
|
||||
let failed = length $ filter not results
|
||||
let total = length results
|
||||
|
||||
putStrLn $ "\n" ++ blue ++ "Results: " ++ show passed ++ "/" ++ show total ++ " passed" ++ reset
|
||||
|
||||
if failed > 0
|
||||
then do
|
||||
putStrLn $ red ++ show failed ++ " test(s) failed" ++ reset
|
||||
exitFailure
|
||||
else do
|
||||
putStrLn $ green ++ "All functional tests passed!" ++ reset
|
||||
exitSuccess
|
||||
|
||||
_ -> do
|
||||
putStrLn $ yellow ++ "SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set" ++ reset
|
||||
exitSuccess
|
||||
|
||||
runTest :: String -> IO Bool -> IO Bool
|
||||
runTest name test = do
|
||||
putStr $ " Running " ++ name ++ "... "
|
||||
result <- test
|
||||
if result
|
||||
then putStrLn $ green ++ "PASS" ++ reset
|
||||
else putStrLn $ red ++ "FAIL" ++ reset
|
||||
return result
|
||||
|
||||
-- Build auth headers
|
||||
buildAuthHeaders :: String -> String -> String -> String -> String -> IO [String]
|
||||
buildAuthHeaders publicKey secretKey method path body = do
|
||||
now <- getPOSIXTime
|
||||
let timestamp = show (floor now :: Integer)
|
||||
let message = timestamp ++ ":" ++ method ++ ":" ++ path ++ ":" ++ body
|
||||
let signature = hmacSha256 secretKey message
|
||||
return [ "-H", "Authorization: Bearer " ++ publicKey
|
||||
, "-H", "X-Timestamp: " ++ timestamp
|
||||
, "-H", "X-Signature: " ++ signature
|
||||
]
|
||||
|
||||
-- HTTP helpers
|
||||
curlGet :: String -> String -> String -> IO String
|
||||
curlGet publicKey secretKey endpoint = do
|
||||
authHeaders <- buildAuthHeaders publicKey secretKey "GET" endpoint ""
|
||||
(_, stdout, _) <- readProcessWithExitCode "curl"
|
||||
(["-s", apiBase ++ endpoint] ++ authHeaders) ""
|
||||
return stdout
|
||||
|
||||
curlPost :: String -> String -> String -> String -> IO String
|
||||
curlPost publicKey secretKey endpoint json = do
|
||||
authHeaders <- buildAuthHeaders publicKey secretKey "POST" endpoint json
|
||||
(_, stdout, _) <- readProcessWithExitCode "curl"
|
||||
(["-s", "-X", "POST", apiBase ++ endpoint, "-H", "Content-Type: application/json"] ++ authHeaders ++ ["-d", json]) ""
|
||||
return stdout
|
||||
|
||||
curlPostPortal :: String -> String -> String -> String -> IO String
|
||||
curlPostPortal publicKey secretKey endpoint json = do
|
||||
authHeaders <- buildAuthHeaders publicKey secretKey "POST" endpoint json
|
||||
(_, stdout, _) <- readProcessWithExitCode "curl"
|
||||
(["-s", "-X", "POST", portalBase ++ endpoint, "-H", "Content-Type: application/json"] ++ authHeaders ++ ["-d", json]) ""
|
||||
return stdout
|
||||
|
||||
-- ============================================================================
|
||||
-- Functional Tests
|
||||
-- ============================================================================
|
||||
|
||||
testHealthCheck :: String -> String -> IO Bool
|
||||
testHealthCheck _ _ = do
|
||||
(_, stdout, _) <- readProcessWithExitCode "curl"
|
||||
["-s", "-o", "/dev/null", "-w", "%{http_code}", apiBase ++ "/health"] ""
|
||||
return $ filter isDigit stdout == "200"
|
||||
|
||||
testValidateKeys :: String -> String -> IO Bool
|
||||
testValidateKeys publicKey secretKey = do
|
||||
response <- curlPostPortal publicKey secretKey "/keys/validate" "{}"
|
||||
-- Check response is JSON with expected fields
|
||||
return $ "{" `isPrefixOf` response && ("\"valid\"" `isInfixOf` response || "\"status\"" `isInfixOf` response)
|
||||
|
||||
testExecutePython :: String -> String -> IO Bool
|
||||
testExecutePython publicKey secretKey = do
|
||||
let json = "{\"language\":\"python\",\"code\":\"print(6 * 7)\"}"
|
||||
response <- curlPost publicKey secretKey "/execute" json
|
||||
-- Check output contains 42
|
||||
return $ "42" `isInfixOf` response
|
||||
|
||||
testExecuteWithError :: String -> String -> IO Bool
|
||||
testExecuteWithError publicKey secretKey = do
|
||||
let json = "{\"language\":\"python\",\"code\":\"import sys; sys.exit(1)\"}"
|
||||
response <- curlPost publicKey secretKey "/execute" json
|
||||
-- Check exit_code is 1
|
||||
return $ "\"exit_code\":1" `isInfixOf` response || "\"exit_code\": 1" `isInfixOf` response
|
||||
|
||||
testSessionList :: String -> String -> IO Bool
|
||||
testSessionList publicKey secretKey = do
|
||||
response <- curlGet publicKey secretKey "/sessions"
|
||||
-- Response should be JSON array or object
|
||||
let trimmed = dropWhile (== ' ') response
|
||||
return $ "[" `isPrefixOf` trimmed || "{" `isPrefixOf` trimmed
|
||||
|
||||
testServiceList :: String -> String -> IO Bool
|
||||
testServiceList publicKey secretKey = do
|
||||
response <- curlGet publicKey secretKey "/services"
|
||||
let trimmed = dropWhile (== ' ') response
|
||||
return $ "[" `isPrefixOf` trimmed || "{" `isPrefixOf` trimmed
|
||||
|
||||
testSnapshotList :: String -> String -> IO Bool
|
||||
testSnapshotList publicKey secretKey = do
|
||||
response <- curlGet publicKey secretKey "/snapshots"
|
||||
let trimmed = dropWhile (== ' ') response
|
||||
return $ "[" `isPrefixOf` trimmed || "{" `isPrefixOf` trimmed
|
||||
|
||||
testImageList :: String -> String -> IO Bool
|
||||
testImageList publicKey secretKey = do
|
||||
response <- curlGet publicKey secretKey "/images"
|
||||
let trimmed = dropWhile (== ' ') response
|
||||
return $ "[" `isPrefixOf` trimmed || "{" `isPrefixOf` trimmed
|
||||
161
clients/haskell/sync/tests/test_library.hs
Executable file
161
clients/haskell/sync/tests/test_library.hs
Executable file
|
|
@ -0,0 +1,161 @@
|
|||
#!/usr/bin/env runhaskell
|
||||
|
||||
{-
|
||||
Unit Tests for Un Haskell SDK Library Functions
|
||||
|
||||
Run with: runhaskell test_library.hs
|
||||
No credentials required - tests pure library functions only.
|
||||
-}
|
||||
|
||||
import System.Exit (exitFailure, exitSuccess)
|
||||
import Data.Char (isHexDigit)
|
||||
import Data.List (isPrefixOf)
|
||||
|
||||
-- Import from parent src directory
|
||||
-- In a real scenario, this would be properly imported
|
||||
|
||||
-- ANSI colors
|
||||
blue, red, green, yellow, reset :: String
|
||||
blue = "\x1b[34m"
|
||||
red = "\x1b[31m"
|
||||
green = "\x1b[32m"
|
||||
yellow = "\x1b[33m"
|
||||
reset = "\x1b[0m"
|
||||
|
||||
-- Inline implementation for testing (matches un.hs)
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as BSC
|
||||
import Crypto.Hash.SHA256 (hmac)
|
||||
import Text.Printf (printf)
|
||||
import System.FilePath (takeExtension)
|
||||
|
||||
hmacSha256 :: String -> String -> String
|
||||
hmacSha256 secret message =
|
||||
let secretBS = BSC.pack secret
|
||||
messageBS = BSC.pack message
|
||||
mac = hmac secretBS messageBS
|
||||
in concatMap (printf "%02x") (BS.unpack mac)
|
||||
|
||||
extToLang :: String -> Maybe String
|
||||
extToLang ext = lookup ext extMap
|
||||
where
|
||||
extMap = [ (".hs", "haskell"), (".ml", "ocaml"), (".clj", "clojure")
|
||||
, (".scm", "scheme"), (".lisp", "commonlisp"), (".erl", "erlang")
|
||||
, (".ex", "elixir"), (".exs", "elixir"), (".py", "python")
|
||||
, (".js", "javascript"), (".ts", "typescript"), (".rb", "ruby")
|
||||
, (".go", "go"), (".rs", "rust"), (".c", "c"), (".cpp", "cpp")
|
||||
, (".cc", "cpp"), (".cxx", "cpp"), (".java", "java")
|
||||
, (".kt", "kotlin"), (".cs", "csharp"), (".fs", "fsharp")
|
||||
, (".jl", "julia"), (".r", "r"), (".cr", "crystal")
|
||||
, (".d", "d"), (".nim", "nim"), (".zig", "zig"), (".v", "v")
|
||||
, (".dart", "dart"), (".groovy", "groovy"), (".scala", "scala")
|
||||
, (".sh", "bash"), (".pl", "perl"), (".lua", "lua"), (".php", "php")
|
||||
]
|
||||
|
||||
sdkVersion :: String
|
||||
sdkVersion = "4.2.0"
|
||||
|
||||
detectLanguage :: String -> Maybe String
|
||||
detectLanguage filename = extToLang (takeExtension filename)
|
||||
|
||||
hmacSign :: String -> String -> String
|
||||
hmacSign = hmacSha256
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "\n" ++ blue ++ "=== Un Haskell SDK Library Tests ===" ++ reset ++ "\n"
|
||||
|
||||
results <- sequence
|
||||
[ runTest "version" testVersion
|
||||
, runTest "detect_language" testDetectLanguage
|
||||
, runTest "hmac_sign" testHmacSign
|
||||
, runTest "hmac_sign_deterministic" testHmacSignDeterministic
|
||||
, runTest "hmac_sign_different_secrets" testHmacSignDifferentSecrets
|
||||
]
|
||||
|
||||
let passed = length $ filter id results
|
||||
let failed = length $ filter not results
|
||||
let total = length results
|
||||
|
||||
putStrLn $ "\n" ++ blue ++ "Results: " ++ show passed ++ "/" ++ show total ++ " passed" ++ reset
|
||||
|
||||
if failed > 0
|
||||
then do
|
||||
putStrLn $ red ++ show failed ++ " test(s) failed" ++ reset
|
||||
exitFailure
|
||||
else do
|
||||
putStrLn $ green ++ "All tests passed!" ++ reset
|
||||
exitSuccess
|
||||
|
||||
runTest :: String -> IO Bool -> IO Bool
|
||||
runTest name test = do
|
||||
result <- test
|
||||
if result
|
||||
then putStrLn $ green ++ "PASS" ++ reset ++ ": " ++ name
|
||||
else putStrLn $ red ++ "FAIL" ++ reset ++ ": " ++ name
|
||||
return result
|
||||
|
||||
-- ============================================================================
|
||||
-- Unit Tests
|
||||
-- ============================================================================
|
||||
|
||||
testVersion :: IO Bool
|
||||
testVersion = do
|
||||
let version = sdkVersion
|
||||
-- Check it's non-empty
|
||||
if null version
|
||||
then return False
|
||||
else do
|
||||
-- Check it's semver format (contains dots)
|
||||
let parts = words $ map (\c -> if c == '.' then ' ' else c) version
|
||||
return $ length parts == 3
|
||||
|
||||
testDetectLanguage :: IO Bool
|
||||
testDetectLanguage = do
|
||||
-- Test common extensions
|
||||
let tests =
|
||||
[ (detectLanguage "script.py" == Just "python", "python")
|
||||
, (detectLanguage "app.js" == Just "javascript", "javascript")
|
||||
, (detectLanguage "main.go" == Just "go", "go")
|
||||
, (detectLanguage "main.rs" == Just "rust", "rust")
|
||||
, (detectLanguage "main.c" == Just "c", "c")
|
||||
, (detectLanguage "main.cpp" == Just "cpp", "cpp")
|
||||
, (detectLanguage "Main.java" == Just "java", "java")
|
||||
, (detectLanguage "script.rb" == Just "ruby", "ruby")
|
||||
, (detectLanguage "script.sh" == Just "bash", "bash")
|
||||
, (detectLanguage "script.lua" == Just "lua", "lua")
|
||||
, (detectLanguage "script.pl" == Just "perl", "perl")
|
||||
, (detectLanguage "index.php" == Just "php", "php")
|
||||
, (detectLanguage "main.hs" == Just "haskell", "haskell")
|
||||
, (detectLanguage "main.ml" == Just "ocaml", "ocaml")
|
||||
, (detectLanguage "main.ex" == Just "elixir", "elixir")
|
||||
, (detectLanguage "main.erl" == Just "erlang", "erlang")
|
||||
-- Test with paths
|
||||
, (detectLanguage "/path/to/script.py" == Just "python", "path/python")
|
||||
-- Test unknown extensions
|
||||
, (detectLanguage "Makefile" == Nothing, "Makefile")
|
||||
, (detectLanguage "README" == Nothing, "README")
|
||||
, (detectLanguage "script.unknown" == Nothing, "unknown")
|
||||
]
|
||||
return $ all fst tests
|
||||
|
||||
testHmacSign :: IO Bool
|
||||
testHmacSign = do
|
||||
let signature = hmacSign "my_secret" "test message"
|
||||
-- Should be 64 hex characters
|
||||
let is64Hex = length signature == 64 && all isHexDigit signature
|
||||
-- Should be lowercase
|
||||
let isLowercase = all (\c -> not (c >= 'A' && c <= 'F')) signature
|
||||
return $ is64Hex && isLowercase
|
||||
|
||||
testHmacSignDeterministic :: IO Bool
|
||||
testHmacSignDeterministic = do
|
||||
let sig1 = hmacSign "test_secret" "same message"
|
||||
let sig2 = hmacSign "test_secret" "same message"
|
||||
return $ sig1 == sig2
|
||||
|
||||
testHmacSignDifferentSecrets :: IO Bool
|
||||
testHmacSignDifferentSecrets = do
|
||||
let sig1 = hmacSign "secret1" "test message"
|
||||
let sig2 = hmacSign "secret2" "test message"
|
||||
return $ sig1 /= sig2
|
||||
|
|
@ -2393,6 +2393,123 @@ public class Un {
|
|||
return makeRequest("POST", "/images/" + imageId + "/clone", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PaaS Logs API (2)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Fetch batch logs from portal.
|
||||
*
|
||||
* @param source Log source: "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
* @param lines Number of lines (1-10000)
|
||||
* @param since Time window: "1m", "5m", "1h", "1d"
|
||||
* @param grep Optional filter pattern (null for no filter)
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map containing logs
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> logsFetch(
|
||||
String source,
|
||||
int lines,
|
||||
String since,
|
||||
String grep,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
StringBuilder path = new StringBuilder("/paas/logs?source=");
|
||||
path.append(source != null ? source : "all");
|
||||
path.append("&lines=").append(lines > 0 ? lines : 100);
|
||||
if (since != null && !since.isEmpty()) {
|
||||
path.append("&since=").append(since);
|
||||
}
|
||||
if (grep != null && !grep.isEmpty()) {
|
||||
path.append("&grep=").append(java.net.URLEncoder.encode(grep, "UTF-8"));
|
||||
}
|
||||
return makeRequest("GET", path.toString(), creds[0], creds[1], null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for receiving streamed log lines.
|
||||
*/
|
||||
public interface LogCallback {
|
||||
/**
|
||||
* Called for each log line received.
|
||||
*
|
||||
* @param source The log source (e.g., "api", "portal")
|
||||
* @param line The log line content
|
||||
*/
|
||||
void onLogLine(String source, String line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream logs via SSE. Blocks until interrupted or server closes connection.
|
||||
*
|
||||
* @param source Log source: "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
* @param grep Optional filter pattern (null for no filter)
|
||||
* @param callback Callback for each log line received
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return true on clean shutdown, false on error
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
*/
|
||||
public static boolean logsStream(
|
||||
String source,
|
||||
String grep,
|
||||
LogCallback callback,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
StringBuilder path = new StringBuilder("/paas/logs/stream?source=");
|
||||
path.append(source != null ? source : "all");
|
||||
if (grep != null && !grep.isEmpty()) {
|
||||
path.append("&grep=").append(java.net.URLEncoder.encode(grep, "UTF-8"));
|
||||
}
|
||||
|
||||
String url = API_BASE + path.toString();
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
String signature = signRequest(creds[1], timestamp, "GET", path.toString(), null);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(0); // No timeout for streaming
|
||||
|
||||
conn.setRequestProperty("Authorization", "Bearer " + creds[0]);
|
||||
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
|
||||
conn.setRequestProperty("X-Signature", signature);
|
||||
conn.setRequestProperty("Accept", "text/event-stream");
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
String currentSource = source != null ? source : "all";
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6);
|
||||
if (callback != null) {
|
||||
callback.onLogLine(currentSource, data);
|
||||
}
|
||||
} else if (line.startsWith("event: ")) {
|
||||
currentSource = line.substring(7);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Key Validation API
|
||||
// ========================================================================
|
||||
|
|
@ -2415,6 +2532,106 @@ public class Un {
|
|||
return makeRequest("POST", "/keys/validate", creds[0], creds[1], new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Utility Functions
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get SDK version string.
|
||||
*
|
||||
* @return Version string (e.g., "4.2.0")
|
||||
*/
|
||||
public static String version() {
|
||||
return "4.2.0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check API health status.
|
||||
*
|
||||
* @return true if API is healthy, false otherwise
|
||||
*/
|
||||
public static boolean healthCheck() {
|
||||
try {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(API_BASE + "/health").openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
return conn.getResponseCode() == 200;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HMAC-SHA256 signature.
|
||||
*
|
||||
* @param secretKey Secret key for HMAC
|
||||
* @param message Message to sign
|
||||
* @return Lowercase hex-encoded signature
|
||||
*/
|
||||
public static String hmacSign(String secretKey, String message) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(
|
||||
secretKey.getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSHA256"
|
||||
);
|
||||
mac.init(secretKeySpec);
|
||||
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
hexString.append(String.format("%02x", b));
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of a specific snapshot.
|
||||
*
|
||||
* @param snapshotId Snapshot ID to retrieve
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Snapshot 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> getSnapshot(
|
||||
String snapshotId,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
return makeRequest("GET", "/snapshots/" + snapshotId, creds[0], creds[1], null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a service (change vCPU allocation).
|
||||
*
|
||||
* @param serviceId Service ID to resize
|
||||
* @param vcpu New vCPU count (1-8)
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with resize 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> resizeService(
|
||||
String serviceId,
|
||||
int vcpu,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("vcpu", vcpu);
|
||||
return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Image Generation API
|
||||
// ========================================================================
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
* - HMAC-SHA256 signature generation
|
||||
* - Credential resolution logic
|
||||
* - Language detection
|
||||
* - Utility functions
|
||||
*
|
||||
* To run tests:
|
||||
* mvn test
|
||||
|
|
@ -80,6 +81,18 @@ public class UnTest {
|
|||
assertEquals("cpp", Un.detectLanguage("main.cxx"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Kotlin from .kt extension")
|
||||
void detectKotlin() {
|
||||
assertEquals("kotlin", Un.detectLanguage("Main.kt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Groovy from .groovy extension")
|
||||
void detectGroovy() {
|
||||
assertEquals("groovy", Un.detectLanguage("script.groovy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return null for unknown extension")
|
||||
void detectUnknown() {
|
||||
|
|
@ -94,6 +107,44 @@ public class UnTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Utility Function Tests")
|
||||
class UtilityTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Version should return a valid version string")
|
||||
void versionString() {
|
||||
String version = Un.version();
|
||||
assertNotNull(version);
|
||||
assertTrue(version.matches("\\d+\\.\\d+\\.\\d+"), "Version should be in X.Y.Z format");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should produce valid hex signature")
|
||||
void hmacSignature() {
|
||||
String signature = Un.hmacSign("secret", "message");
|
||||
assertNotNull(signature);
|
||||
assertEquals(64, signature.length(), "HMAC-SHA256 should produce 64 hex chars");
|
||||
assertTrue(signature.matches("[0-9a-f]+"), "Signature should be lowercase hex");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should be consistent")
|
||||
void hmacConsistent() {
|
||||
String sig1 = Un.hmacSign("key", "data");
|
||||
String sig2 = Un.hmacSign("key", "data");
|
||||
assertEquals(sig1, sig2, "Same inputs should produce same signature");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should differ with different inputs")
|
||||
void hmacDifferent() {
|
||||
String sig1 = Un.hmacSign("key1", "data");
|
||||
String sig2 = Un.hmacSign("key2", "data");
|
||||
assertNotEquals(sig1, sig2, "Different keys should produce different signatures");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Credential Exception Tests")
|
||||
class CredentialExceptionTests {
|
||||
|
|
@ -120,6 +171,22 @@ public class UnTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Sudo Challenge Exception Tests")
|
||||
class SudoChallengeExceptionTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("SudoChallengeException should contain challenge ID")
|
||||
void sudoChallengeDetails() {
|
||||
Un.SudoChallengeException ex = new Un.SudoChallengeException(
|
||||
"challenge-123",
|
||||
"{\"challenge_id\": \"challenge-123\"}"
|
||||
);
|
||||
assertEquals("challenge-123", ex.getChallengeId());
|
||||
assertEquals("{\"challenge_id\": \"challenge-123\"}", ex.getResponseBody());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Integration Tests (requires credentials)")
|
||||
@EnabledIfEnvironmentVariable(named = "UNSANDBOX_PUBLIC_KEY", matches = ".+")
|
||||
|
|
@ -193,5 +260,63 @@ public class UnTest {
|
|||
assertEquals("completed", result.get("status"));
|
||||
assertTrue(result.get("stdout").toString().contains("Async test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list jobs")
|
||||
void listJobs() throws IOException {
|
||||
List<Map<String, Object>> jobs = Un.listJobs(publicKey, secretKey);
|
||||
assertNotNull(jobs);
|
||||
// Jobs list can be empty if no jobs are running
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should validate keys successfully")
|
||||
void validateKeys() throws IOException {
|
||||
Map<String, Object> result = Un.validateKeys(publicKey, secretKey);
|
||||
assertNotNull(result);
|
||||
// The response should contain validation info
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list sessions")
|
||||
void listSessions() throws IOException {
|
||||
List<Map<String, Object>> sessions = Un.listSessions(publicKey, secretKey);
|
||||
assertNotNull(sessions);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list services")
|
||||
void listServices() throws IOException {
|
||||
List<Map<String, Object>> services = Un.listServices(publicKey, secretKey);
|
||||
assertNotNull(services);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list snapshots")
|
||||
void listSnapshots() throws IOException {
|
||||
List<Map<String, Object>> snapshots = Un.listSnapshots(publicKey, secretKey);
|
||||
assertNotNull(snapshots);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list images")
|
||||
void listImages() throws IOException {
|
||||
List<Map<String, Object>> images = Un.listImages(null, publicKey, secretKey);
|
||||
assertNotNull(images);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Health Check Tests")
|
||||
class HealthCheckTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Health check should return boolean")
|
||||
void healthCheckReturnsBoolean() {
|
||||
boolean healthy = Un.healthCheck();
|
||||
// We just verify it returns without throwing
|
||||
// The actual result depends on network connectivity
|
||||
assertTrue(healthy || !healthy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
clients/javascript/sync/jest.config.js
Normal file
7
clients/javascript/sync/jest.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
testEnvironment: 'node',
|
||||
transform: {},
|
||||
testMatch: ['**/tests/**/*.test.js'],
|
||||
moduleFileExtensions: ['js', 'mjs'],
|
||||
verbose: true,
|
||||
};
|
||||
|
|
@ -5,9 +5,12 @@
|
|||
"type": "module",
|
||||
"main": "src/un.js",
|
||||
"scripts": {
|
||||
"test": "echo 'No tests configured yet'"
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
||||
},
|
||||
"keywords": ["unsandbox", "code-execution", "sandbox", "api"],
|
||||
"author": "unsandbox.com",
|
||||
"license": "Unlicense"
|
||||
"license": "Unlicense",
|
||||
"devDependencies": {
|
||||
"jest": "^29.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -933,7 +933,7 @@ async function serviceSnapshot(serviceId, publicKey, secretKey, name, hot = fals
|
|||
}
|
||||
|
||||
/**
|
||||
* List all snapshots (NEW).
|
||||
* List all snapshots.
|
||||
*
|
||||
* Returns: Promise<Array> (list of snapshot dicts)
|
||||
*/
|
||||
|
|
@ -944,7 +944,20 @@ async function listSnapshots(publicKey, secretKey) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Restore a snapshot (NEW).
|
||||
* Get details of a specific snapshot.
|
||||
*
|
||||
* Args:
|
||||
* snapshotId: Snapshot ID to get details for
|
||||
*
|
||||
* Returns: Promise<Object> (snapshot details with id, name, type, source_id, etc.)
|
||||
*/
|
||||
async function getSnapshot(snapshotId, publicKey, secretKey) {
|
||||
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
|
||||
return makeRequest('GET', `/snapshots/${snapshotId}`, publicKey, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a snapshot.
|
||||
*
|
||||
* Returns: Promise<Object> (response with restored resource info)
|
||||
*/
|
||||
|
|
@ -1410,6 +1423,20 @@ async function executeInService(serviceId, command, timeout = 30000, publicKey,
|
|||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a service's vCPU allocation.
|
||||
*
|
||||
* Args:
|
||||
* serviceId: Service ID to resize
|
||||
* vcpu: Number of vCPUs (1-8 typically)
|
||||
*
|
||||
* Returns: Promise<Object> (updated service info)
|
||||
*/
|
||||
async function resizeService(serviceId, vcpu, publicKey, secretKey) {
|
||||
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
|
||||
return makeRequest('PATCH', `/services/${serviceId}`, publicKey, secretKey, { vcpu });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Additional Snapshot Functions
|
||||
// ============================================================================
|
||||
|
|
@ -1753,9 +1780,198 @@ async function image(prompt, options = {}) {
|
|||
return makeRequest('POST', '/image', pk, sk, payload);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PaaS Logs Functions
|
||||
// ============================================================================
|
||||
|
||||
let _lastError = null;
|
||||
|
||||
/**
|
||||
* Fetch batch logs from the PaaS platform.
|
||||
*
|
||||
* Args:
|
||||
* source: Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
* lines: Number of lines to fetch (1-10000)
|
||||
* since: Time window - "1m", "5m", "1h", "1d"
|
||||
* grep: Optional filter pattern
|
||||
* publicKey: API public key
|
||||
* secretKey: API secret key
|
||||
*
|
||||
* Returns: Promise<Object> (log entries)
|
||||
*/
|
||||
async function logsFetch(source = 'all', lines = 100, since = '5m', grep = null, publicKey, secretKey) {
|
||||
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
|
||||
let urlPath = `/logs?source=${source}&lines=${lines}&since=${since}`;
|
||||
if (grep) urlPath += `&grep=${encodeURIComponent(grep)}`;
|
||||
return makeRequest('GET', urlPath, publicKey, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream logs via Server-Sent Events.
|
||||
*
|
||||
* Args:
|
||||
* source: Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
* grep: Optional filter pattern
|
||||
* callback: Function called for each log line (signature: callback(source, line))
|
||||
* publicKey: API public key
|
||||
* secretKey: API secret key
|
||||
*
|
||||
* Returns: Promise<void> (blocks until interrupted or server closes)
|
||||
*/
|
||||
async function logsStream(source = 'all', grep = null, callback = null, publicKey, secretKey) {
|
||||
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
|
||||
|
||||
let urlPath = `/logs/stream?source=${source}`;
|
||||
if (grep) urlPath += `&grep=${encodeURIComponent(grep)}`;
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const signature = await signRequest(secretKey, timestamp, 'GET', urlPath, null);
|
||||
|
||||
const url = `${API_BASE}${urlPath}`;
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${publicKey}`,
|
||||
'X-Timestamp': timestamp.toString(),
|
||||
'X-Signature': signature,
|
||||
'Accept': 'text/event-stream',
|
||||
};
|
||||
|
||||
// Node.js SSE streaming
|
||||
if (IS_NODE) {
|
||||
const https = await import('https');
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
headers,
|
||||
};
|
||||
|
||||
const req = https.default.request(options, (res) => {
|
||||
res.on('data', (chunk) => {
|
||||
const lines = chunk.toString().split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.substring(6);
|
||||
try {
|
||||
const entry = JSON.parse(data);
|
||||
if (callback) {
|
||||
callback(entry.source || source, entry.line || data);
|
||||
} else {
|
||||
console.log(`[${entry.source || source}] ${entry.line || data}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (callback) {
|
||||
callback(source, data);
|
||||
} else {
|
||||
console.log(`[${source}] ${data}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
res.on('end', resolve);
|
||||
res.on('error', reject);
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Browser EventSource not directly supported with custom headers
|
||||
throw new Error('logsStream is only supported in Node.js');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
const SDK_VERSION = '4.2.0';
|
||||
|
||||
/**
|
||||
* Get the SDK version string.
|
||||
*
|
||||
* Returns: string (e.g., "4.2.0")
|
||||
*/
|
||||
function sdkVersion() {
|
||||
return SDK_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the API is healthy and responding.
|
||||
*
|
||||
* Returns: Promise<boolean>
|
||||
*/
|
||||
async function healthCheck() {
|
||||
try {
|
||||
if (IS_NODE) {
|
||||
const https = await import('https');
|
||||
return new Promise((resolve) => {
|
||||
const req = https.default.get(`${API_BASE}/health`, (res) => {
|
||||
resolve(res.statusCode === 200);
|
||||
});
|
||||
req.on('error', () => {
|
||||
_lastError = 'Health check failed: network error';
|
||||
resolve(false);
|
||||
});
|
||||
req.setTimeout(10000, () => {
|
||||
_lastError = 'Health check failed: timeout';
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const response = await fetch(`${API_BASE}/health`);
|
||||
return response.status === 200;
|
||||
}
|
||||
} catch (e) {
|
||||
_lastError = `Health check failed: ${e.message}`;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last error message.
|
||||
*
|
||||
* Returns: string|null
|
||||
*/
|
||||
function lastError() {
|
||||
return _lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a message using HMAC-SHA256.
|
||||
*
|
||||
* This is the underlying signing function used for request authentication.
|
||||
* Exposed for testing and debugging purposes.
|
||||
*
|
||||
* Args:
|
||||
* secretKey: The secret key for signing
|
||||
* message: The message to sign
|
||||
*
|
||||
* Returns: Promise<string> (64-character lowercase hex string)
|
||||
*/
|
||||
async function hmacSign(secretKey, message) {
|
||||
if (IS_NODE) {
|
||||
return crypto.createHmac('sha256', secretKey).update(message).digest('hex');
|
||||
} else {
|
||||
// Browser Web Crypto API
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(secretKey);
|
||||
const msgData = encoder.encode(message);
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
'raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
|
||||
);
|
||||
const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, msgData);
|
||||
return Array.from(new Uint8Array(signature))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ES Module exports
|
||||
export {
|
||||
// Code execution
|
||||
// Code execution (8)
|
||||
executeCode,
|
||||
executeAsync,
|
||||
getJob,
|
||||
|
|
@ -1764,7 +1980,7 @@ export {
|
|||
listJobs,
|
||||
getLanguages,
|
||||
detectLanguage,
|
||||
// Session management
|
||||
// Session management (9)
|
||||
listSessions,
|
||||
getSession,
|
||||
createSession,
|
||||
|
|
@ -1774,7 +1990,7 @@ export {
|
|||
boostSession,
|
||||
unboostSession,
|
||||
shellSession,
|
||||
// Service management
|
||||
// Service management (17)
|
||||
listServices,
|
||||
createService,
|
||||
getService,
|
||||
|
|
@ -1793,16 +2009,18 @@ export {
|
|||
exportServiceEnv,
|
||||
redeployService,
|
||||
executeInService,
|
||||
// Snapshot management
|
||||
resizeService,
|
||||
// Snapshot management (9)
|
||||
sessionSnapshot,
|
||||
serviceSnapshot,
|
||||
listSnapshots,
|
||||
getSnapshot,
|
||||
restoreSnapshot,
|
||||
deleteSnapshot,
|
||||
lockSnapshot,
|
||||
unlockSnapshot,
|
||||
cloneSnapshot,
|
||||
// Images API (LXD container images)
|
||||
// Images API (13)
|
||||
imagePublish,
|
||||
listImages,
|
||||
getImage,
|
||||
|
|
@ -1816,9 +2034,17 @@ export {
|
|||
transferImage,
|
||||
spawnFromImage,
|
||||
cloneImage,
|
||||
// PaaS Logs (2)
|
||||
logsFetch,
|
||||
logsStream,
|
||||
// Key validation
|
||||
validateKeys,
|
||||
// Image generation
|
||||
// Utilities
|
||||
sdkVersion,
|
||||
healthCheck,
|
||||
lastError,
|
||||
hmacSign,
|
||||
// Image generation (AI)
|
||||
image,
|
||||
// Errors
|
||||
CredentialsError,
|
||||
|
|
@ -1830,7 +2056,7 @@ export {
|
|||
|
||||
// Default export for convenience
|
||||
export default {
|
||||
// Code execution
|
||||
// Code execution (8)
|
||||
executeCode,
|
||||
executeAsync,
|
||||
getJob,
|
||||
|
|
@ -1839,7 +2065,7 @@ export default {
|
|||
listJobs,
|
||||
getLanguages,
|
||||
detectLanguage,
|
||||
// Session management
|
||||
// Session management (9)
|
||||
listSessions,
|
||||
getSession,
|
||||
createSession,
|
||||
|
|
@ -1849,7 +2075,7 @@ export default {
|
|||
boostSession,
|
||||
unboostSession,
|
||||
shellSession,
|
||||
// Service management
|
||||
// Service management (17)
|
||||
listServices,
|
||||
createService,
|
||||
getService,
|
||||
|
|
@ -1860,6 +2086,7 @@ export default {
|
|||
lockService,
|
||||
unlockService,
|
||||
setUnfreezeOnDemand,
|
||||
setShowFreezePage,
|
||||
getServiceLogs,
|
||||
getServiceEnv,
|
||||
setServiceEnv,
|
||||
|
|
@ -1867,16 +2094,18 @@ export default {
|
|||
exportServiceEnv,
|
||||
redeployService,
|
||||
executeInService,
|
||||
// Snapshot management
|
||||
resizeService,
|
||||
// Snapshot management (9)
|
||||
sessionSnapshot,
|
||||
serviceSnapshot,
|
||||
listSnapshots,
|
||||
getSnapshot,
|
||||
restoreSnapshot,
|
||||
deleteSnapshot,
|
||||
lockSnapshot,
|
||||
unlockSnapshot,
|
||||
cloneSnapshot,
|
||||
// Images API (LXD container images)
|
||||
// Images API (13)
|
||||
imagePublish,
|
||||
listImages,
|
||||
getImage,
|
||||
|
|
@ -1890,9 +2119,17 @@ export default {
|
|||
transferImage,
|
||||
spawnFromImage,
|
||||
cloneImage,
|
||||
// PaaS Logs (2)
|
||||
logsFetch,
|
||||
logsStream,
|
||||
// Key validation
|
||||
validateKeys,
|
||||
// Image generation
|
||||
// Utilities
|
||||
sdkVersion,
|
||||
healthCheck,
|
||||
lastError,
|
||||
hmacSign,
|
||||
// Image generation (AI)
|
||||
image,
|
||||
// Errors
|
||||
CredentialsError,
|
||||
|
|
|
|||
421
clients/javascript/sync/tests/new_functions.test.js
Normal file
421
clients/javascript/sync/tests/new_functions.test.js
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
/**
|
||||
* Tests for new SDK functions (feature parity with C implementation)
|
||||
*/
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// Since un.js uses top-level await, we need to import dynamically
|
||||
let un;
|
||||
|
||||
beforeAll(async () => {
|
||||
un = await import('../src/un.js');
|
||||
});
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
describe('sdkVersion', () => {
|
||||
test('should return a string', () => {
|
||||
const v = un.sdkVersion();
|
||||
expect(typeof v).toBe('string');
|
||||
expect(v.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should be semantic version format', () => {
|
||||
const v = un.sdkVersion();
|
||||
const parts = v.split('.');
|
||||
expect(parts.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hmacSign', () => {
|
||||
test('should produce 64-character hex string', async () => {
|
||||
const signature = await un.hmacSign('secret_key', 'message_to_sign');
|
||||
expect(typeof signature).toBe('string');
|
||||
expect(signature.length).toBe(64);
|
||||
// Should be lowercase hex
|
||||
expect(/^[0-9a-f]+$/.test(signature)).toBe(true);
|
||||
});
|
||||
|
||||
test('should be deterministic', async () => {
|
||||
const sig1 = await un.hmacSign('secret', 'message');
|
||||
const sig2 = await un.hmacSign('secret', 'message');
|
||||
expect(sig1).toBe(sig2);
|
||||
});
|
||||
|
||||
test('should produce different signatures for different secrets', async () => {
|
||||
const sig1 = await un.hmacSign('secret1', 'message');
|
||||
const sig2 = await un.hmacSign('secret2', 'message');
|
||||
expect(sig1).not.toBe(sig2);
|
||||
});
|
||||
|
||||
test('should produce different signatures for different messages', async () => {
|
||||
const sig1 = await un.hmacSign('secret', 'message1');
|
||||
const sig2 = await un.hmacSign('secret', 'message2');
|
||||
expect(sig1).not.toBe(sig2);
|
||||
});
|
||||
|
||||
test('should match Node.js crypto HMAC', async () => {
|
||||
const secret = 'test_secret';
|
||||
const message = '1234567890:POST:/execute:';
|
||||
const signature = await un.hmacSign(secret, message);
|
||||
const expected = crypto.createHmac('sha256', secret).update(message).digest('hex');
|
||||
expect(signature).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lastError', () => {
|
||||
test('should return null or string', () => {
|
||||
const error = un.lastError();
|
||||
expect(error === null || typeof error === 'string').toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Function Exports', () => {
|
||||
describe('Execution functions (8)', () => {
|
||||
test('executeCode is exported', () => {
|
||||
expect(typeof un.executeCode).toBe('function');
|
||||
});
|
||||
|
||||
test('executeAsync is exported', () => {
|
||||
expect(typeof un.executeAsync).toBe('function');
|
||||
});
|
||||
|
||||
test('getJob is exported', () => {
|
||||
expect(typeof un.getJob).toBe('function');
|
||||
});
|
||||
|
||||
test('waitForJob is exported', () => {
|
||||
expect(typeof un.waitForJob).toBe('function');
|
||||
});
|
||||
|
||||
test('cancelJob is exported', () => {
|
||||
expect(typeof un.cancelJob).toBe('function');
|
||||
});
|
||||
|
||||
test('listJobs is exported', () => {
|
||||
expect(typeof un.listJobs).toBe('function');
|
||||
});
|
||||
|
||||
test('getLanguages is exported', () => {
|
||||
expect(typeof un.getLanguages).toBe('function');
|
||||
});
|
||||
|
||||
test('detectLanguage is exported', () => {
|
||||
expect(typeof un.detectLanguage).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session functions (9)', () => {
|
||||
test('listSessions is exported', () => {
|
||||
expect(typeof un.listSessions).toBe('function');
|
||||
});
|
||||
|
||||
test('getSession is exported', () => {
|
||||
expect(typeof un.getSession).toBe('function');
|
||||
});
|
||||
|
||||
test('createSession is exported', () => {
|
||||
expect(typeof un.createSession).toBe('function');
|
||||
});
|
||||
|
||||
test('deleteSession is exported', () => {
|
||||
expect(typeof un.deleteSession).toBe('function');
|
||||
});
|
||||
|
||||
test('freezeSession is exported', () => {
|
||||
expect(typeof un.freezeSession).toBe('function');
|
||||
});
|
||||
|
||||
test('unfreezeSession is exported', () => {
|
||||
expect(typeof un.unfreezeSession).toBe('function');
|
||||
});
|
||||
|
||||
test('boostSession is exported', () => {
|
||||
expect(typeof un.boostSession).toBe('function');
|
||||
});
|
||||
|
||||
test('unboostSession is exported', () => {
|
||||
expect(typeof un.unboostSession).toBe('function');
|
||||
});
|
||||
|
||||
test('shellSession is exported', () => {
|
||||
expect(typeof un.shellSession).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Service functions (17)', () => {
|
||||
test('listServices is exported', () => {
|
||||
expect(typeof un.listServices).toBe('function');
|
||||
});
|
||||
|
||||
test('createService is exported', () => {
|
||||
expect(typeof un.createService).toBe('function');
|
||||
});
|
||||
|
||||
test('getService is exported', () => {
|
||||
expect(typeof un.getService).toBe('function');
|
||||
});
|
||||
|
||||
test('updateService is exported', () => {
|
||||
expect(typeof un.updateService).toBe('function');
|
||||
});
|
||||
|
||||
test('deleteService is exported', () => {
|
||||
expect(typeof un.deleteService).toBe('function');
|
||||
});
|
||||
|
||||
test('freezeService is exported', () => {
|
||||
expect(typeof un.freezeService).toBe('function');
|
||||
});
|
||||
|
||||
test('unfreezeService is exported', () => {
|
||||
expect(typeof un.unfreezeService).toBe('function');
|
||||
});
|
||||
|
||||
test('lockService is exported', () => {
|
||||
expect(typeof un.lockService).toBe('function');
|
||||
});
|
||||
|
||||
test('unlockService is exported', () => {
|
||||
expect(typeof un.unlockService).toBe('function');
|
||||
});
|
||||
|
||||
test('setUnfreezeOnDemand is exported', () => {
|
||||
expect(typeof un.setUnfreezeOnDemand).toBe('function');
|
||||
});
|
||||
|
||||
test('getServiceLogs is exported', () => {
|
||||
expect(typeof un.getServiceLogs).toBe('function');
|
||||
});
|
||||
|
||||
test('getServiceEnv is exported', () => {
|
||||
expect(typeof un.getServiceEnv).toBe('function');
|
||||
});
|
||||
|
||||
test('setServiceEnv is exported', () => {
|
||||
expect(typeof un.setServiceEnv).toBe('function');
|
||||
});
|
||||
|
||||
test('deleteServiceEnv is exported', () => {
|
||||
expect(typeof un.deleteServiceEnv).toBe('function');
|
||||
});
|
||||
|
||||
test('exportServiceEnv is exported', () => {
|
||||
expect(typeof un.exportServiceEnv).toBe('function');
|
||||
});
|
||||
|
||||
test('redeployService is exported', () => {
|
||||
expect(typeof un.redeployService).toBe('function');
|
||||
});
|
||||
|
||||
test('executeInService is exported', () => {
|
||||
expect(typeof un.executeInService).toBe('function');
|
||||
});
|
||||
|
||||
test('resizeService is exported (NEW)', () => {
|
||||
expect(typeof un.resizeService).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshot functions (9)', () => {
|
||||
test('sessionSnapshot is exported', () => {
|
||||
expect(typeof un.sessionSnapshot).toBe('function');
|
||||
});
|
||||
|
||||
test('serviceSnapshot is exported', () => {
|
||||
expect(typeof un.serviceSnapshot).toBe('function');
|
||||
});
|
||||
|
||||
test('listSnapshots is exported', () => {
|
||||
expect(typeof un.listSnapshots).toBe('function');
|
||||
});
|
||||
|
||||
test('getSnapshot is exported (NEW)', () => {
|
||||
expect(typeof un.getSnapshot).toBe('function');
|
||||
});
|
||||
|
||||
test('restoreSnapshot is exported', () => {
|
||||
expect(typeof un.restoreSnapshot).toBe('function');
|
||||
});
|
||||
|
||||
test('deleteSnapshot is exported', () => {
|
||||
expect(typeof un.deleteSnapshot).toBe('function');
|
||||
});
|
||||
|
||||
test('lockSnapshot is exported', () => {
|
||||
expect(typeof un.lockSnapshot).toBe('function');
|
||||
});
|
||||
|
||||
test('unlockSnapshot is exported', () => {
|
||||
expect(typeof un.unlockSnapshot).toBe('function');
|
||||
});
|
||||
|
||||
test('cloneSnapshot is exported', () => {
|
||||
expect(typeof un.cloneSnapshot).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Image functions (13)', () => {
|
||||
test('imagePublish is exported', () => {
|
||||
expect(typeof un.imagePublish).toBe('function');
|
||||
});
|
||||
|
||||
test('listImages is exported', () => {
|
||||
expect(typeof un.listImages).toBe('function');
|
||||
});
|
||||
|
||||
test('getImage is exported', () => {
|
||||
expect(typeof un.getImage).toBe('function');
|
||||
});
|
||||
|
||||
test('deleteImage is exported', () => {
|
||||
expect(typeof un.deleteImage).toBe('function');
|
||||
});
|
||||
|
||||
test('lockImage is exported', () => {
|
||||
expect(typeof un.lockImage).toBe('function');
|
||||
});
|
||||
|
||||
test('unlockImage is exported', () => {
|
||||
expect(typeof un.unlockImage).toBe('function');
|
||||
});
|
||||
|
||||
test('setImageVisibility is exported', () => {
|
||||
expect(typeof un.setImageVisibility).toBe('function');
|
||||
});
|
||||
|
||||
test('grantImageAccess is exported', () => {
|
||||
expect(typeof un.grantImageAccess).toBe('function');
|
||||
});
|
||||
|
||||
test('revokeImageAccess is exported', () => {
|
||||
expect(typeof un.revokeImageAccess).toBe('function');
|
||||
});
|
||||
|
||||
test('listImageTrusted is exported', () => {
|
||||
expect(typeof un.listImageTrusted).toBe('function');
|
||||
});
|
||||
|
||||
test('transferImage is exported', () => {
|
||||
expect(typeof un.transferImage).toBe('function');
|
||||
});
|
||||
|
||||
test('spawnFromImage is exported', () => {
|
||||
expect(typeof un.spawnFromImage).toBe('function');
|
||||
});
|
||||
|
||||
test('cloneImage is exported', () => {
|
||||
expect(typeof un.cloneImage).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaaS Logs functions (2)', () => {
|
||||
test('logsFetch is exported (NEW)', () => {
|
||||
expect(typeof un.logsFetch).toBe('function');
|
||||
});
|
||||
|
||||
test('logsStream is exported (NEW)', () => {
|
||||
expect(typeof un.logsStream).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Utility functions', () => {
|
||||
test('validateKeys is exported', () => {
|
||||
expect(typeof un.validateKeys).toBe('function');
|
||||
});
|
||||
|
||||
test('sdkVersion is exported (NEW)', () => {
|
||||
expect(typeof un.sdkVersion).toBe('function');
|
||||
});
|
||||
|
||||
test('healthCheck is exported (NEW)', () => {
|
||||
expect(typeof un.healthCheck).toBe('function');
|
||||
});
|
||||
|
||||
test('lastError is exported (NEW)', () => {
|
||||
expect(typeof un.lastError).toBe('function');
|
||||
});
|
||||
|
||||
test('hmacSign is exported (NEW)', () => {
|
||||
expect(typeof un.hmacSign).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Language Detection', () => {
|
||||
test('detects Python files', () => {
|
||||
expect(un.detectLanguage('test.py')).toBe('python');
|
||||
});
|
||||
|
||||
test('detects JavaScript files', () => {
|
||||
expect(un.detectLanguage('test.js')).toBe('javascript');
|
||||
});
|
||||
|
||||
test('detects TypeScript files', () => {
|
||||
expect(un.detectLanguage('test.ts')).toBe('typescript');
|
||||
});
|
||||
|
||||
test('detects Ruby files', () => {
|
||||
expect(un.detectLanguage('test.rb')).toBe('ruby');
|
||||
});
|
||||
|
||||
test('detects Go files', () => {
|
||||
expect(un.detectLanguage('test.go')).toBe('go');
|
||||
});
|
||||
|
||||
test('detects Rust files', () => {
|
||||
expect(un.detectLanguage('test.rs')).toBe('rust');
|
||||
});
|
||||
|
||||
test('returns null for unknown extensions', () => {
|
||||
expect(un.detectLanguage('test.unknown')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// Functional tests require API credentials
|
||||
const hasCredentials = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
|
||||
|
||||
(hasCredentials ? describe : describe.skip)('Functional API Tests', () => {
|
||||
test('healthCheck returns boolean', async () => {
|
||||
const result = await un.healthCheck();
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
|
||||
test('validateKeys returns object', async () => {
|
||||
const result = await un.validateKeys();
|
||||
expect(typeof result).toBe('object');
|
||||
});
|
||||
|
||||
test('getLanguages returns array with python', async () => {
|
||||
const languages = await un.getLanguages();
|
||||
expect(Array.isArray(languages)).toBe(true);
|
||||
expect(languages).toContain('python');
|
||||
});
|
||||
|
||||
test('listSessions returns array', async () => {
|
||||
const sessions = await un.listSessions();
|
||||
expect(Array.isArray(sessions)).toBe(true);
|
||||
});
|
||||
|
||||
test('listServices returns array', async () => {
|
||||
const services = await un.listServices();
|
||||
expect(Array.isArray(services)).toBe(true);
|
||||
});
|
||||
|
||||
test('listSnapshots returns array', async () => {
|
||||
const snapshots = await un.listSnapshots();
|
||||
expect(Array.isArray(snapshots)).toBe(true);
|
||||
});
|
||||
|
||||
test('listImages returns array', async () => {
|
||||
const images = await un.listImages();
|
||||
expect(Array.isArray(images)).toBe(true);
|
||||
});
|
||||
|
||||
test('executeCode returns result', async () => {
|
||||
const result = await un.executeCode('python', 'print("hello")');
|
||||
expect(typeof result).toBe('object');
|
||||
expect(['completed', 'pending']).toContain(result.status);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
137
clients/julia/tests/test_un.jl
Normal file
137
clients/julia/tests/test_un.jl
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env julia
|
||||
# Test suite for Julia Unsandbox SDK
|
||||
# Run: julia tests/test_un.jl
|
||||
|
||||
using Test
|
||||
|
||||
# Add parent directory to path so we can include the SDK
|
||||
push!(LOAD_PATH, joinpath(@__DIR__, "..", "sync", "src"))
|
||||
|
||||
# Include the SDK (don't run main)
|
||||
include(joinpath(@__DIR__, "..", "sync", "src", "un.jl"))
|
||||
|
||||
# Test constants
|
||||
@testset "Constants" begin
|
||||
@test API_BASE == "https://api.unsandbox.com"
|
||||
@test PORTAL_BASE == "https://unsandbox.com"
|
||||
@test LANGUAGES_CACHE_TTL == 3600
|
||||
end
|
||||
|
||||
# Test language detection
|
||||
@testset "Language Detection" begin
|
||||
@test detect_language("test.py") == "python"
|
||||
@test detect_language("test.js") == "javascript"
|
||||
@test detect_language("test.rb") == "ruby"
|
||||
@test detect_language("test.jl") == "julia"
|
||||
@test detect_language("test.go") == "go"
|
||||
@test detect_language("test.rs") == "rust"
|
||||
@test detect_language("test.f90") == "fortran"
|
||||
@test detect_language("test.cob") == "cobol"
|
||||
@test detect_language("test.pro") == "prolog"
|
||||
@test detect_language("test.unknown") == "unknown"
|
||||
end
|
||||
|
||||
# Test HMAC signing
|
||||
@testset "HMAC Signing" begin
|
||||
sig = hmac_sign("secret", "message")
|
||||
@test typeof(sig) == String
|
||||
@test length(sig) == 64 # SHA256 hex is 64 chars
|
||||
# Verify deterministic
|
||||
sig2 = hmac_sign("secret", "message")
|
||||
@test sig == sig2
|
||||
end
|
||||
|
||||
# Test version
|
||||
@testset "Version" begin
|
||||
v = version()
|
||||
@test typeof(v) == String
|
||||
@test v == VERSION
|
||||
end
|
||||
|
||||
# Test health check (requires network)
|
||||
@testset "Health Check" begin
|
||||
# Skip if no network
|
||||
if haskey(ENV, "SKIP_NETWORK_TESTS")
|
||||
@test_skip health_check()
|
||||
else
|
||||
result = health_check()
|
||||
@test typeof(result) == Bool
|
||||
end
|
||||
end
|
||||
|
||||
# Test library function signatures (don't call API)
|
||||
@testset "Library Function Signatures" begin
|
||||
# Execution functions
|
||||
@test hasmethod(execute, Tuple{String, String})
|
||||
@test hasmethod(execute_async, Tuple{String, String})
|
||||
@test hasmethod(wait_job, Tuple{String})
|
||||
@test hasmethod(get_job, Tuple{String})
|
||||
@test hasmethod(cancel_job, Tuple{String})
|
||||
@test hasmethod(list_jobs, Tuple{})
|
||||
@test hasmethod(get_languages, Tuple{})
|
||||
|
||||
# Session functions
|
||||
@test hasmethod(session_list, Tuple{})
|
||||
@test hasmethod(session_get, Tuple{String})
|
||||
@test hasmethod(session_create, Tuple{})
|
||||
@test hasmethod(session_destroy, Tuple{String})
|
||||
@test hasmethod(session_freeze, Tuple{String})
|
||||
@test hasmethod(session_unfreeze, Tuple{String})
|
||||
@test hasmethod(session_boost, Tuple{String, Int})
|
||||
@test hasmethod(session_unboost, Tuple{String})
|
||||
@test hasmethod(session_execute, Tuple{String, String})
|
||||
|
||||
# Service functions
|
||||
@test hasmethod(service_list, Tuple{})
|
||||
@test hasmethod(service_get, Tuple{String})
|
||||
@test hasmethod(service_create, Tuple{String})
|
||||
@test hasmethod(service_destroy, Tuple{String})
|
||||
@test hasmethod(service_freeze, Tuple{String})
|
||||
@test hasmethod(service_unfreeze, Tuple{String})
|
||||
@test hasmethod(service_lock, Tuple{String})
|
||||
@test hasmethod(service_unlock, Tuple{String})
|
||||
@test hasmethod(service_redeploy, Tuple{String})
|
||||
@test hasmethod(service_logs, Tuple{String})
|
||||
@test hasmethod(service_execute, Tuple{String, String})
|
||||
@test hasmethod(service_resize, Tuple{String, Int})
|
||||
@test hasmethod(service_env_get, Tuple{String})
|
||||
@test hasmethod(service_env_set, Tuple{String, String})
|
||||
@test hasmethod(service_env_delete, Tuple{String})
|
||||
@test hasmethod(service_env_export, Tuple{String})
|
||||
|
||||
# Snapshot functions
|
||||
@test hasmethod(snapshot_list, Tuple{})
|
||||
@test hasmethod(snapshot_get, Tuple{String})
|
||||
@test hasmethod(snapshot_session, Tuple{String})
|
||||
@test hasmethod(snapshot_service, Tuple{String})
|
||||
@test hasmethod(snapshot_restore, Tuple{String})
|
||||
@test hasmethod(snapshot_delete, Tuple{String})
|
||||
@test hasmethod(snapshot_lock, Tuple{String})
|
||||
@test hasmethod(snapshot_unlock, Tuple{String})
|
||||
@test hasmethod(snapshot_clone, Tuple{String, String})
|
||||
|
||||
# Image functions
|
||||
@test hasmethod(image_list, Tuple{})
|
||||
@test hasmethod(image_get, Tuple{String})
|
||||
@test hasmethod(image_publish, Tuple{String, String})
|
||||
@test hasmethod(image_delete, Tuple{String})
|
||||
@test hasmethod(image_lock, Tuple{String})
|
||||
@test hasmethod(image_unlock, Tuple{String})
|
||||
@test hasmethod(image_set_visibility, Tuple{String, String})
|
||||
@test hasmethod(image_grant_access, Tuple{String, String})
|
||||
@test hasmethod(image_revoke_access, Tuple{String, String})
|
||||
@test hasmethod(image_list_trusted, Tuple{String})
|
||||
@test hasmethod(image_transfer, Tuple{String, String})
|
||||
@test hasmethod(image_spawn, Tuple{String})
|
||||
@test hasmethod(image_clone, Tuple{String})
|
||||
|
||||
# Utility functions
|
||||
@test hasmethod(validate_keys, Tuple{})
|
||||
@test hasmethod(logs_fetch, Tuple{String})
|
||||
@test hasmethod(health_check, Tuple{})
|
||||
@test hasmethod(version, Tuple{})
|
||||
@test hasmethod(last_error, Tuple{})
|
||||
@test hasmethod(hmac_sign, Tuple{String, String})
|
||||
end
|
||||
|
||||
println("\nAll tests passed!")
|
||||
|
|
@ -1316,6 +1316,457 @@ fun parseArgs(args: Array<String>): Args {
|
|||
return result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Library Functions (for programmatic use)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* SDK version string.
|
||||
*/
|
||||
fun version(): String = "4.2.0"
|
||||
|
||||
/**
|
||||
* Check API health status.
|
||||
*/
|
||||
fun healthCheck(): Boolean {
|
||||
return try {
|
||||
val url = URL("$API_BASE/health")
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 5000
|
||||
connection.readTimeout = 5000
|
||||
connection.responseCode == 200
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HMAC-SHA256 signature.
|
||||
*/
|
||||
fun hmacSign(secretKey: String, message: String): String {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
val keySpec = SecretKeySpec(secretKey.toByteArray(Charsets.UTF_8), "HmacSHA256")
|
||||
mac.init(keySpec)
|
||||
return mac.doFinal(message.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute code synchronously.
|
||||
*/
|
||||
fun execute(language: String, code: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) {
|
||||
Pair(publicKey, secretKey)
|
||||
} else {
|
||||
getApiKeys(null)
|
||||
}
|
||||
val payload = mapOf("language" to language, "code" to code)
|
||||
return apiRequest("/execute", "POST", payload, pk, sk)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute code asynchronously, returns job ID.
|
||||
*/
|
||||
fun executeAsync(language: String, code: String, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val result = execute(language, code, publicKey, secretKey)
|
||||
return result["job_id"]?.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get job status and result.
|
||||
*/
|
||||
fun getJob(jobId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) {
|
||||
Pair(publicKey, secretKey)
|
||||
} else {
|
||||
getApiKeys(null)
|
||||
}
|
||||
return apiRequest("/jobs/$jobId", "GET", null, pk, sk)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for job completion with polling.
|
||||
*/
|
||||
fun waitJob(jobId: String, publicKey: String? = null, secretKey: String? = null, timeoutMs: Long = 0): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) {
|
||||
Pair(publicKey, secretKey)
|
||||
} else {
|
||||
getApiKeys(null)
|
||||
}
|
||||
val pollDelays = intArrayOf(300, 450, 700, 900, 650, 1600, 2000)
|
||||
val terminalStates = setOf("completed", "failed", "timeout", "cancelled")
|
||||
val startTime = System.currentTimeMillis()
|
||||
var pollCount = 0
|
||||
|
||||
while (true) {
|
||||
val delayIdx = minOf(pollCount, pollDelays.size - 1)
|
||||
Thread.sleep(pollDelays[delayIdx].toLong())
|
||||
pollCount++
|
||||
|
||||
if (timeoutMs > 0 && System.currentTimeMillis() - startTime > timeoutMs) {
|
||||
throw RuntimeException("Timeout waiting for job $jobId")
|
||||
}
|
||||
|
||||
val result = getJob(jobId, pk, sk)
|
||||
val status = result["status"]?.toString() ?: ""
|
||||
if (status in terminalStates) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a running job.
|
||||
*/
|
||||
fun cancelJob(jobId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) {
|
||||
Pair(publicKey, secretKey)
|
||||
} else {
|
||||
getApiKeys(null)
|
||||
}
|
||||
return apiRequest("/jobs/$jobId", "DELETE", null, pk, sk)
|
||||
}
|
||||
|
||||
/**
|
||||
* List all jobs.
|
||||
*/
|
||||
fun listJobs(publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) {
|
||||
Pair(publicKey, secretKey)
|
||||
} else {
|
||||
getApiKeys(null)
|
||||
}
|
||||
val result = apiRequest("/jobs", "GET", null, pk, sk)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result["jobs"] as? List<Map<String, Any>> ?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported languages.
|
||||
*/
|
||||
fun getLanguages(publicKey: String? = null, secretKey: String? = null): List<String> {
|
||||
val cached = loadLanguagesCache()
|
||||
if (cached != null) return cached
|
||||
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) {
|
||||
Pair(publicKey, secretKey)
|
||||
} else {
|
||||
getApiKeys(null)
|
||||
}
|
||||
val result = apiRequest("/languages", "GET", null, pk, sk)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val languages = result["languages"] as? List<String> ?: emptyList()
|
||||
saveLanguagesCache(languages)
|
||||
return languages
|
||||
}
|
||||
|
||||
// Session functions
|
||||
fun sessionList(publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val result = apiRequest("/sessions", "GET", null, pk, sk)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result["sessions"] as? List<Map<String, Any>> ?: emptyList()
|
||||
}
|
||||
|
||||
fun sessionGet(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/sessions/$sessionId", "GET", null, pk, sk)
|
||||
}
|
||||
|
||||
fun sessionCreate(networkMode: String? = null, shell: String? = null, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = mutableMapOf<String, Any>("shell" to (shell ?: "bash"))
|
||||
if (networkMode != null) payload["network_mode"] = networkMode
|
||||
return apiRequest("/sessions", "POST", payload, pk, sk)
|
||||
}
|
||||
|
||||
fun sessionDestroy(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/sessions/$sessionId", "DELETE", null, pk, sk)
|
||||
}
|
||||
|
||||
fun sessionFreeze(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/sessions/$sessionId/freeze", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun sessionUnfreeze(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/sessions/$sessionId/unfreeze", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun sessionBoost(sessionId: String, vcpu: Int = 2, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/sessions/$sessionId/boost", "POST", mapOf("vcpu" to vcpu), pk, sk)
|
||||
}
|
||||
|
||||
fun sessionUnboost(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/sessions/$sessionId/unboost", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun sessionExecute(sessionId: String, command: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/sessions/$sessionId/shell", "POST", mapOf("command" to command), pk, sk)
|
||||
}
|
||||
|
||||
// Service functions
|
||||
fun serviceList(publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val result = apiRequest("/services", "GET", null, pk, sk)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result["services"] as? List<Map<String, Any>> ?: emptyList()
|
||||
}
|
||||
|
||||
fun serviceGet(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/services/$serviceId", "GET", null, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceCreate(name: String, ports: String? = null, domains: String? = null, bootstrap: String? = null, networkMode: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = mutableMapOf<String, Any>("name" to name)
|
||||
if (ports != null) payload["ports"] = ports.split(",").map { it.trim().toInt() }
|
||||
if (domains != null) payload["domains"] = domains
|
||||
if (bootstrap != null) payload["bootstrap"] = bootstrap
|
||||
if (networkMode != null) payload["network_mode"] = networkMode
|
||||
val result = apiRequest("/services", "POST", payload, pk, sk)
|
||||
return result["id"]?.toString()
|
||||
}
|
||||
|
||||
fun serviceDestroy(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequestDestructive("/services/$serviceId", "DELETE", null, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceFreeze(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/services/$serviceId/freeze", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceUnfreeze(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/services/$serviceId/unfreeze", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceLock(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/services/$serviceId/lock", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceUnlock(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequestDestructive("/services/$serviceId/unlock", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceSetUnfreezeOnDemand(serviceId: String, enabled: Boolean, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequestPatch("/services/$serviceId", mapOf("unfreeze_on_demand" to enabled), pk, sk)
|
||||
}
|
||||
|
||||
fun serviceRedeploy(serviceId: String, bootstrap: String? = null, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = if (bootstrap != null) mapOf("bootstrap" to bootstrap) else emptyMap()
|
||||
return apiRequest("/services/$serviceId/redeploy", "POST", payload, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceLogs(serviceId: String, allLogs: Boolean = false, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val path = if (allLogs) "/services/$serviceId/logs?all=true" else "/services/$serviceId/logs"
|
||||
val result = apiRequest(path, "GET", null, pk, sk)
|
||||
return result["logs"]?.toString()
|
||||
}
|
||||
|
||||
fun serviceExecute(serviceId: String, command: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/services/$serviceId/execute", "POST", mapOf("command" to command), pk, sk)
|
||||
}
|
||||
|
||||
fun serviceEnvGet(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/services/$serviceId/env", "GET", null, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceEnvSet(serviceId: String, envContent: String, publicKey: String? = null, secretKey: String? = null): Boolean {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val (success, _) = apiRequestText("/services/$serviceId/env", "PUT", envContent, pk, sk)
|
||||
return success
|
||||
}
|
||||
|
||||
fun serviceEnvDelete(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/services/$serviceId/env", "DELETE", null, pk, sk)
|
||||
}
|
||||
|
||||
fun serviceEnvExport(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/services/$serviceId/env/export", "POST", emptyMap<String, Any>(), pk, sk)
|
||||
}
|
||||
|
||||
fun serviceResize(serviceId: String, vcpu: Int, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequestPatch("/services/$serviceId", mapOf("vcpu" to vcpu), pk, sk)
|
||||
}
|
||||
|
||||
// Snapshot functions
|
||||
fun snapshotList(publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val result = apiRequest("/snapshots", "GET", null, pk, sk)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result["snapshots"] as? List<Map<String, Any>> ?: emptyList()
|
||||
}
|
||||
|
||||
fun snapshotGet(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/snapshots/$snapshotId", "GET", null, pk, sk)
|
||||
}
|
||||
|
||||
fun snapshotSession(sessionId: String, name: String? = null, hot: Boolean = false, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = mutableMapOf<String, Any>("session_id" to sessionId, "hot" to hot)
|
||||
if (name != null) payload["name"] = name
|
||||
val result = apiRequest("/snapshots", "POST", payload, pk, sk)
|
||||
return result["snapshot_id"]?.toString()
|
||||
}
|
||||
|
||||
fun snapshotService(serviceId: String, name: String? = null, hot: Boolean = false, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = mutableMapOf<String, Any>("service_id" to serviceId, "hot" to hot)
|
||||
if (name != null) payload["name"] = name
|
||||
val result = apiRequest("/snapshots", "POST", payload, pk, sk)
|
||||
return result["snapshot_id"]?.toString()
|
||||
}
|
||||
|
||||
fun snapshotRestore(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/snapshots/$snapshotId/restore", "POST", emptyMap<String, Any>(), pk, sk)
|
||||
}
|
||||
|
||||
fun snapshotDelete(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequestDestructive("/snapshots/$snapshotId", "DELETE", null, pk, sk)
|
||||
}
|
||||
|
||||
fun snapshotLock(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/snapshots/$snapshotId/lock", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun snapshotUnlock(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequestDestructive("/snapshots/$snapshotId/unlock", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun snapshotClone(snapshotId: String, cloneType: String, name: String? = null, ports: String? = null, shell: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = mutableMapOf<String, Any>("type" to cloneType)
|
||||
if (name != null) payload["name"] = name
|
||||
if (ports != null) payload["ports"] = ports.split(",").map { it.trim().toInt() }
|
||||
if (shell != null) payload["shell"] = shell
|
||||
val result = apiRequest("/snapshots/$snapshotId/clone", "POST", payload, pk, sk)
|
||||
return (result["session_id"] ?: result["service_id"])?.toString()
|
||||
}
|
||||
|
||||
// Image functions
|
||||
fun imageList(filter: String? = null, publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val path = if (filter != null) "/images/$filter" else "/images"
|
||||
val result = apiRequest(path, "GET", null, pk, sk)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result["images"] as? List<Map<String, Any>> ?: emptyList()
|
||||
}
|
||||
|
||||
fun imageGet(imageId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/images/$imageId", "GET", null, pk, sk)
|
||||
}
|
||||
|
||||
fun imagePublish(sourceType: String, sourceId: String, name: String? = null, description: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = mutableMapOf<String, Any>("source_type" to sourceType, "source_id" to sourceId)
|
||||
if (name != null) payload["name"] = name
|
||||
if (description != null) payload["description"] = description
|
||||
val result = apiRequest("/images", "POST", payload, pk, sk)
|
||||
return result["image_id"]?.toString()
|
||||
}
|
||||
|
||||
fun imageDelete(imageId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequestDestructive("/images/$imageId", "DELETE", null, pk, sk)
|
||||
}
|
||||
|
||||
fun imageLock(imageId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/images/$imageId/lock", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun imageUnlock(imageId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequestDestructive("/images/$imageId/unlock", "POST", null, pk, sk)
|
||||
}
|
||||
|
||||
fun imageSetVisibility(imageId: String, visibility: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/images/$imageId/visibility", "POST", mapOf("visibility" to visibility), pk, sk)
|
||||
}
|
||||
|
||||
fun imageGrantAccess(imageId: String, trustedApiKey: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/images/$imageId/grant", "POST", mapOf("trusted_api_key" to trustedApiKey), pk, sk)
|
||||
}
|
||||
|
||||
fun imageRevokeAccess(imageId: String, trustedApiKey: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/images/$imageId/revoke", "POST", mapOf("trusted_api_key" to trustedApiKey), pk, sk)
|
||||
}
|
||||
|
||||
fun imageListTrusted(imageId: String, publicKey: String? = null, secretKey: String? = null): List<String> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val result = apiRequest("/images/$imageId/trusted", "GET", null, pk, sk)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result["trusted"] as? List<String> ?: emptyList()
|
||||
}
|
||||
|
||||
fun imageTransfer(imageId: String, toApiKey: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return apiRequest("/images/$imageId/transfer", "POST", mapOf("to_api_key" to toApiKey), pk, sk)
|
||||
}
|
||||
|
||||
fun imageSpawn(imageId: String, name: String? = null, ports: String? = null, bootstrap: String? = null, networkMode: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = mutableMapOf<String, Any>()
|
||||
if (name != null) payload["name"] = name
|
||||
if (ports != null) payload["ports"] = ports.split(",").map { it.trim().toInt() }
|
||||
if (bootstrap != null) payload["bootstrap"] = bootstrap
|
||||
if (networkMode != null) payload["network_mode"] = networkMode
|
||||
val result = apiRequest("/images/$imageId/spawn", "POST", payload, pk, sk)
|
||||
return result["service_id"]?.toString()
|
||||
}
|
||||
|
||||
fun imageClone(imageId: String, name: String? = null, description: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val payload = mutableMapOf<String, Any>()
|
||||
if (name != null) payload["name"] = name
|
||||
if (description != null) payload["description"] = description
|
||||
val result = apiRequest("/images/$imageId/clone", "POST", payload, pk, sk)
|
||||
return result["image_id"]?.toString()
|
||||
}
|
||||
|
||||
// PaaS Logs functions
|
||||
fun logsFetch(source: String = "all", lines: Int = 100, since: String? = null, grep: String? = null, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
val params = mutableListOf("source=$source", "lines=$lines")
|
||||
if (since != null) params.add("since=$since")
|
||||
if (grep != null) params.add("grep=${java.net.URLEncoder.encode(grep, "UTF-8")}")
|
||||
return apiRequest("/paas/logs?${params.joinToString("&")}", "GET", null, pk, sk)
|
||||
}
|
||||
|
||||
// Validate keys
|
||||
fun validateKeys(publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
|
||||
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
|
||||
return validateKey(pk, sk)
|
||||
}
|
||||
|
||||
fun printHelp() {
|
||||
println("""
|
||||
Usage: kotlin UnKt [options] <source_file>
|
||||
|
|
|
|||
211
clients/kotlin/sync/tests/UnTest.kt
Normal file
211
clients/kotlin/sync/tests/UnTest.kt
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
// Unit tests for Un SDK - Kotlin Synchronous client
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
|
||||
class UnTest {
|
||||
|
||||
@Nested
|
||||
@DisplayName("Language Detection Tests")
|
||||
inner class LanguageDetectionTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Python from .py extension")
|
||||
fun detectPython() {
|
||||
assertEquals("python", detectLanguage("script.py"))
|
||||
assertEquals("python", detectLanguage("path/to/script.py"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect JavaScript from .js extension")
|
||||
fun detectJavaScript() {
|
||||
assertEquals("javascript", detectLanguage("app.js"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect TypeScript from .ts extension")
|
||||
fun detectTypeScript() {
|
||||
assertEquals("typescript", detectLanguage("app.ts"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Go from .go extension")
|
||||
fun detectGo() {
|
||||
assertEquals("go", detectLanguage("main.go"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Rust from .rs extension")
|
||||
fun detectRust() {
|
||||
assertEquals("rust", detectLanguage("lib.rs"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Java from .java extension")
|
||||
fun detectJava() {
|
||||
assertEquals("java", detectLanguage("Main.java"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Kotlin from .kt extension")
|
||||
fun detectKotlin() {
|
||||
assertEquals("kotlin", detectLanguage("Main.kt"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Groovy from .groovy extension")
|
||||
fun detectGroovy() {
|
||||
assertEquals("groovy", detectLanguage("script.groovy"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw for unknown extension")
|
||||
fun detectUnknown() {
|
||||
assertThrows(RuntimeException::class.java) {
|
||||
detectLanguage("file.unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Utility Function Tests")
|
||||
inner class UtilityTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Version should return a valid version string")
|
||||
fun versionString() {
|
||||
val ver = version()
|
||||
assertNotNull(ver)
|
||||
assertTrue(ver.matches(Regex("\\d+\\.\\d+\\.\\d+")), "Version should be in X.Y.Z format")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should produce valid hex signature")
|
||||
fun hmacSignature() {
|
||||
val signature = hmacSign("secret", "message")
|
||||
assertNotNull(signature)
|
||||
assertEquals(64, signature.length, "HMAC-SHA256 should produce 64 hex chars")
|
||||
assertTrue(signature.matches(Regex("[0-9a-f]+")), "Signature should be lowercase hex")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should be consistent")
|
||||
fun hmacConsistent() {
|
||||
val sig1 = hmacSign("key", "data")
|
||||
val sig2 = hmacSign("key", "data")
|
||||
assertEquals(sig1, sig2, "Same inputs should produce same signature")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should differ with different inputs")
|
||||
fun hmacDifferent() {
|
||||
val sig1 = hmacSign("key1", "data")
|
||||
val sig2 = hmacSign("key2", "data")
|
||||
assertNotEquals(sig1, sig2, "Different keys should produce different signatures")
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Health Check Tests")
|
||||
inner class HealthCheckTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Health check should return boolean")
|
||||
fun healthCheckReturnsBoolean() {
|
||||
val healthy = healthCheck()
|
||||
// We just verify it returns without throwing
|
||||
assertTrue(healthy || !healthy)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Integration Tests (requires credentials)")
|
||||
@EnabledIfEnvironmentVariable(named = "UNSANDBOX_PUBLIC_KEY", matches = ".+")
|
||||
inner class IntegrationTests {
|
||||
|
||||
private lateinit var publicKey: String
|
||||
private lateinit var secretKey: String
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY") ?: ""
|
||||
secretKey = System.getenv("UNSANDBOX_SECRET_KEY") ?: ""
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should execute Python code successfully")
|
||||
fun executePythonCode() {
|
||||
val result = execute("python", "print('Hello, World!')", publicKey, secretKey)
|
||||
|
||||
assertNotNull(result)
|
||||
assertTrue(result["stdout"].toString().contains("Hello, World!"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should execute JavaScript code successfully")
|
||||
fun executeJavaScriptCode() {
|
||||
val result = execute("javascript", "console.log('Hello from JS')", publicKey, secretKey)
|
||||
|
||||
assertNotNull(result)
|
||||
assertTrue(result["stdout"].toString().contains("Hello from JS"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should get supported languages")
|
||||
fun getLanguagesTest() {
|
||||
val languages = getLanguages(publicKey, secretKey)
|
||||
|
||||
assertNotNull(languages)
|
||||
assertTrue(languages.isNotEmpty())
|
||||
assertTrue(languages.contains("python"))
|
||||
assertTrue(languages.contains("javascript"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list jobs")
|
||||
fun listJobsTest() {
|
||||
val jobs = listJobs(publicKey, secretKey)
|
||||
assertNotNull(jobs)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should validate keys successfully")
|
||||
fun validateKeysTest() {
|
||||
val result = validateKeys(publicKey, secretKey)
|
||||
assertNotNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list sessions")
|
||||
fun listSessionsTest() {
|
||||
val sessions = sessionList(publicKey, secretKey)
|
||||
assertNotNull(sessions)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list services")
|
||||
fun listServicesTest() {
|
||||
val services = serviceList(publicKey, secretKey)
|
||||
assertNotNull(services)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list snapshots")
|
||||
fun listSnapshotsTest() {
|
||||
val snapshots = snapshotList(publicKey, secretKey)
|
||||
assertNotNull(snapshots)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list images")
|
||||
fun listImagesTest() {
|
||||
val images = imageList(null, publicKey, secretKey)
|
||||
assertNotNull(images)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -597,6 +597,174 @@
|
|||
(defun key-cmd (extend-flag)
|
||||
(validate-key extend-flag))
|
||||
|
||||
;; Image access management functions
|
||||
(defun image-grant-access (id trusted-key)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format nil "{\"trusted_api_key\":\"~a\"}" trusted-key)))
|
||||
(curl-post api-key (format nil "/images/~a/grant-access" id) json)
|
||||
(format t "~aAccess granted to: ~a~a~%" *green* trusted-key *reset*)))
|
||||
|
||||
(defun image-revoke-access (id trusted-key)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format nil "{\"trusted_api_key\":\"~a\"}" trusted-key)))
|
||||
(curl-post api-key (format nil "/images/~a/revoke-access" id) json)
|
||||
(format t "~aAccess revoked from: ~a~a~%" *green* trusted-key *reset*)))
|
||||
|
||||
(defun image-list-trusted (id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(format t "~a~%" (curl-get api-key (format nil "/images/~a/trusted" id)))))
|
||||
|
||||
(defun image-transfer (id to-key)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format nil "{\"to_api_key\":\"~a\"}" to-key)))
|
||||
(curl-post api-key (format nil "/images/~a/transfer" id) json)
|
||||
(format t "~aImage transferred to: ~a~a~%" *green* to-key *reset*)))
|
||||
|
||||
;; Snapshot functions
|
||||
(defun snapshot-list ()
|
||||
(let ((api-key (get-api-key)))
|
||||
(format t "~a~%" (curl-get api-key "/snapshots"))))
|
||||
|
||||
(defun snapshot-info (id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(format t "~a~%" (curl-get api-key (format nil "/snapshots/~a" id)))))
|
||||
|
||||
(defun snapshot-session (session-id name hot)
|
||||
(let* ((api-key (get-api-key))
|
||||
(name-json (if name (format nil ",\"name\":\"~a\"" (escape-json name)) ""))
|
||||
(hot-json (if hot ",\"hot\":true" ""))
|
||||
(json (format nil "{\"session_id\":\"~a\"~a~a}" session-id name-json hot-json)))
|
||||
(format t "~aSnapshot created~a~%" *green* *reset*)
|
||||
(format t "~a~%" (curl-post api-key "/snapshots" json))))
|
||||
|
||||
(defun snapshot-service (service-id name hot)
|
||||
(let* ((api-key (get-api-key))
|
||||
(name-json (if name (format nil ",\"name\":\"~a\"" (escape-json name)) ""))
|
||||
(hot-json (if hot ",\"hot\":true" ""))
|
||||
(json (format nil "{\"service_id\":\"~a\"~a~a}" service-id name-json hot-json)))
|
||||
(format t "~aSnapshot created~a~%" *green* *reset*)
|
||||
(format t "~a~%" (curl-post api-key "/snapshots" json))))
|
||||
|
||||
(defun snapshot-restore (id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(curl-post api-key (format nil "/snapshots/~a/restore" id) "{}")
|
||||
(format t "~aSnapshot restored: ~a~a~%" *green* id *reset*)))
|
||||
|
||||
(defun snapshot-delete (id)
|
||||
(let* ((api-key (get-api-key))
|
||||
(result (curl-delete-with-sudo api-key (format nil "/snapshots/~a" id))))
|
||||
(if (first result)
|
||||
(format t "~aSnapshot deleted: ~a~a~%" *green* id *reset*)
|
||||
(progn
|
||||
(format *error-output* "~aError deleting snapshot~a~%" *red* *reset*)
|
||||
(uiop:quit 1)))))
|
||||
|
||||
(defun snapshot-lock (id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(curl-post api-key (format nil "/snapshots/~a/lock" id) "{}")
|
||||
(format t "~aSnapshot locked: ~a~a~%" *green* id *reset*)))
|
||||
|
||||
(defun snapshot-unlock (id)
|
||||
(let* ((api-key (get-api-key))
|
||||
(result (curl-post-with-sudo api-key (format nil "/snapshots/~a/unlock" id) "{}")))
|
||||
(if (first result)
|
||||
(format t "~aSnapshot unlocked: ~a~a~%" *green* id *reset*)
|
||||
(progn
|
||||
(format *error-output* "~aError unlocking snapshot~a~%" *red* *reset*)
|
||||
(uiop:quit 1)))))
|
||||
|
||||
(defun snapshot-clone (id clone-type name ports shell)
|
||||
(let* ((api-key (get-api-key))
|
||||
(type-json (format nil "\"clone_type\":\"~a\"" clone-type))
|
||||
(name-json (if name (format nil ",\"name\":\"~a\"" (escape-json name)) ""))
|
||||
(ports-json (if ports (format nil ",\"ports\":[~a]" ports) ""))
|
||||
(shell-json (if shell (format nil ",\"shell\":\"~a\"" shell) ""))
|
||||
(json (format nil "{~a~a~a~a}" type-json name-json ports-json shell-json)))
|
||||
(format t "~aSnapshot cloned~a~%" *green* *reset*)
|
||||
(format t "~a~%" (curl-post api-key (format nil "/snapshots/~a/clone" id) json))))
|
||||
|
||||
(defun snapshot-cmd (action id name ports shell hot)
|
||||
(cond
|
||||
((string= action "list") (snapshot-list))
|
||||
((string= action "info") (snapshot-info id))
|
||||
((string= action "session") (snapshot-session id name hot))
|
||||
((string= action "service") (snapshot-service id name hot))
|
||||
((string= action "restore") (snapshot-restore id))
|
||||
((string= action "delete") (snapshot-delete id))
|
||||
((string= action "lock") (snapshot-lock id))
|
||||
((string= action "unlock") (snapshot-unlock id))
|
||||
((string= action "clone") (snapshot-clone id "session" name ports shell))
|
||||
(t (format t "~aError: Unknown snapshot action~a~%" *red* *reset*)
|
||||
(uiop:quit 1))))
|
||||
|
||||
;; Session additional functions
|
||||
(defun session-info (id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(format t "~a~%" (curl-get api-key (format nil "/sessions/~a" id)))))
|
||||
|
||||
(defun session-boost (id vcpu)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format nil "{\"vcpu\":~a}" vcpu)))
|
||||
(curl-patch api-key (format nil "/sessions/~a" id) json)
|
||||
(format t "~aSession boosted to ~a vCPU~a~%" *green* vcpu *reset*)))
|
||||
|
||||
(defun session-unboost (id)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json "{\"vcpu\":1}"))
|
||||
(curl-patch api-key (format nil "/sessions/~a" id) json)
|
||||
(format t "~aSession unboosted to 1 vCPU~a~%" *green* *reset*)))
|
||||
|
||||
(defun session-execute (id command)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format nil "{\"command\":\"~a\"}" (escape-json command)))
|
||||
(response (curl-post api-key (format nil "/sessions/~a/execute" id) json))
|
||||
(stdout-val (parse-json-field response "stdout")))
|
||||
(when stdout-val
|
||||
(format t "~a~a~a" *blue* stdout-val *reset*))))
|
||||
|
||||
;; Service additional functions
|
||||
(defun service-lock (id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(curl-post api-key (format nil "/services/~a/lock" id) "{}")
|
||||
(format t "~aService locked: ~a~a~%" *green* id *reset*)))
|
||||
|
||||
(defun service-unlock (id)
|
||||
(let* ((api-key (get-api-key))
|
||||
(result (curl-post-with-sudo api-key (format nil "/services/~a/unlock" id) "{}")))
|
||||
(if (first result)
|
||||
(format t "~aService unlocked: ~a~a~%" *green* id *reset*)
|
||||
(progn
|
||||
(format *error-output* "~aError unlocking service~a~%" *red* *reset*)
|
||||
(uiop:quit 1)))))
|
||||
|
||||
(defun service-redeploy (id bootstrap)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (if bootstrap
|
||||
(format nil "{\"bootstrap\":\"~a\"}" (escape-json bootstrap))
|
||||
"{}")))
|
||||
(curl-post api-key (format nil "/services/~a/redeploy" id) json)
|
||||
(format t "~aService redeploying: ~a~a~%" *green* id *reset*)))
|
||||
|
||||
;; PaaS logs functions
|
||||
(defun logs-fetch (source lines since grep-pattern)
|
||||
(let* ((api-key (get-api-key))
|
||||
(params (format nil "?source=~a&lines=~a~a~a"
|
||||
(or source "all")
|
||||
(or lines 100)
|
||||
(if since (format nil "&since=~a" since) "")
|
||||
(if grep-pattern (format nil "&grep=~a" grep-pattern) ""))))
|
||||
(format t "~a~%" (curl-get api-key (format nil "/logs~a" params)))))
|
||||
|
||||
;; Utility functions
|
||||
(defun health-check ()
|
||||
(let* ((cmd "curl -s https://api.unsandbox.com/health")
|
||||
(result (uiop:run-program cmd :output :string)))
|
||||
(format t "~a~%" result)
|
||||
(search "ok" result)))
|
||||
|
||||
(defun sdk-version ()
|
||||
"4.2.0")
|
||||
|
||||
(defun image-cmd (action id name ports source-type visibility-mode)
|
||||
(let ((api-key (get-api-key)))
|
||||
(cond
|
||||
|
|
@ -885,6 +1053,28 @@
|
|||
((string= (first args) "key")
|
||||
(let ((extend-flag (and (> (length args) 1) (string= (second args) "--extend"))))
|
||||
(key-cmd extend-flag)))
|
||||
((string= (first args) "snapshot")
|
||||
(cond
|
||||
((and (> (length args) 1) (string= (second args) "--list"))
|
||||
(snapshot-cmd "list" nil nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--info"))
|
||||
(snapshot-cmd "info" (third args) nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--session"))
|
||||
(snapshot-cmd "session" (third args) nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--service"))
|
||||
(snapshot-cmd "service" (third args) nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--restore"))
|
||||
(snapshot-cmd "restore" (third args) nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--delete"))
|
||||
(snapshot-cmd "delete" (third args) nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--lock"))
|
||||
(snapshot-cmd "lock" (third args) nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--unlock"))
|
||||
(snapshot-cmd "unlock" (third args) nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--clone"))
|
||||
(snapshot-cmd "clone" (third args) nil nil nil nil))
|
||||
(t
|
||||
(snapshot-cmd "list" nil nil nil nil nil))))
|
||||
((string= (first args) "image")
|
||||
(cond
|
||||
((and (> (length args) 1) (string= (second args) "--list"))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
249
clients/lua/tests/test_library.lua
Normal file
249
clients/lua/tests/test_library.lua
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
#!/usr/bin/env lua
|
||||
-- Unit Tests for un.lua Library Functions
|
||||
--
|
||||
-- Tests the ACTUAL exported functions from Un module.
|
||||
-- NO local re-implementations. NO mocking.
|
||||
--
|
||||
-- Run: lua tests/test_library.lua
|
||||
|
||||
-- Adjust package path to find the module
|
||||
local script_dir = arg[0]:match("(.*/)") or "./"
|
||||
package.path = script_dir .. "../sync/src/?.lua;" .. package.path
|
||||
|
||||
local Un = require("un")
|
||||
|
||||
-- Test counters
|
||||
local tests_passed = 0
|
||||
local tests_failed = 0
|
||||
|
||||
local function PASS(msg)
|
||||
print(" \027[32m[PASS]\027[0m " .. msg)
|
||||
tests_passed = tests_passed + 1
|
||||
end
|
||||
|
||||
local function FAIL(msg)
|
||||
print(" \027[31m[FAIL]\027[0m " .. msg)
|
||||
tests_failed = tests_failed + 1
|
||||
end
|
||||
|
||||
local function assert_equal(actual, expected, msg)
|
||||
if actual == expected then
|
||||
PASS(msg)
|
||||
else
|
||||
FAIL(msg .. " (expected: " .. tostring(expected) .. ", got: " .. tostring(actual) .. ")")
|
||||
end
|
||||
end
|
||||
|
||||
local function assert_true(condition, msg)
|
||||
if condition then
|
||||
PASS(msg)
|
||||
else
|
||||
FAIL(msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function assert_nil(value, msg)
|
||||
if value == nil then
|
||||
PASS(msg)
|
||||
else
|
||||
FAIL(msg .. " (expected nil, got: " .. tostring(value) .. ")")
|
||||
end
|
||||
end
|
||||
|
||||
local function assert_not_nil(value, msg)
|
||||
if value ~= nil then
|
||||
PASS(msg)
|
||||
else
|
||||
FAIL(msg .. " (expected non-nil)")
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Test: Un.version()
|
||||
-- ============================================================================
|
||||
|
||||
print("\nTesting Un.version()...")
|
||||
|
||||
local version = Un.version()
|
||||
assert_not_nil(version, "version() returns non-nil")
|
||||
assert_true(#version > 0, "version() returns non-empty string")
|
||||
assert_true(version:match("^%d+%.%d+%.%d+$") ~= nil, "version() matches X.Y.Z format")
|
||||
print(" Version: " .. version)
|
||||
|
||||
-- ============================================================================
|
||||
-- Test: Un.detect_language()
|
||||
-- ============================================================================
|
||||
|
||||
print("\nTesting Un.detect_language()...")
|
||||
|
||||
local tests = {
|
||||
{"test.py", "python"},
|
||||
{"app.js", "javascript"},
|
||||
{"main.go", "go"},
|
||||
{"script.rb", "ruby"},
|
||||
{"lib.rs", "rust"},
|
||||
{"main.c", "c"},
|
||||
{"app.cpp", "cpp"},
|
||||
{"Main.java", "java"},
|
||||
{"index.php", "php"},
|
||||
{"script.pl", "perl"},
|
||||
{"init.lua", "lua"},
|
||||
{"run.sh", "bash"},
|
||||
{"main.ts", "typescript"},
|
||||
{"app.kt", "kotlin"},
|
||||
{"lib.ex", "elixir"},
|
||||
{"main.hs", "haskell"},
|
||||
}
|
||||
|
||||
for _, test in ipairs(tests) do
|
||||
local file, expected = test[1], test[2]
|
||||
local result = Un.detect_language(file)
|
||||
assert_equal(result, expected, "detect_language('" .. file .. "') -> '" .. expected .. "'")
|
||||
end
|
||||
|
||||
-- Test nil handling
|
||||
local null_result = Un.detect_language(nil)
|
||||
assert_nil(null_result, "detect_language(nil) returns nil")
|
||||
|
||||
-- Test unknown extension
|
||||
local unknown = Un.detect_language("file.xyz123")
|
||||
assert_nil(unknown, "detect_language(unknown ext) returns nil")
|
||||
|
||||
-- Test no extension
|
||||
local noext = Un.detect_language("Makefile")
|
||||
assert_nil(noext, "detect_language(no ext) returns nil")
|
||||
|
||||
-- ============================================================================
|
||||
-- Test: Un.hmac_sign()
|
||||
-- ============================================================================
|
||||
|
||||
print("\nTesting Un.hmac_sign()...")
|
||||
|
||||
-- Test basic signature generation
|
||||
local sig = Un.hmac_sign("secret_key", "1234567890:POST:/execute:{}")
|
||||
assert_not_nil(sig, "hmac_sign() returns non-nil")
|
||||
assert_equal(#sig, 64, "hmac_sign() returns 64-char hex string")
|
||||
|
||||
-- Verify hex characters
|
||||
assert_true(sig:match("^[0-9a-fA-F]+$") ~= nil, "hmac_sign() returns valid hex")
|
||||
|
||||
-- Test deterministic output
|
||||
local sig1 = Un.hmac_sign("key", "message")
|
||||
local sig2 = Un.hmac_sign("key", "message")
|
||||
assert_equal(sig1, sig2, "hmac_sign() is deterministic")
|
||||
|
||||
-- Test different keys produce different signatures
|
||||
local sig_a = Un.hmac_sign("key_a", "message")
|
||||
local sig_b = Un.hmac_sign("key_b", "message")
|
||||
assert_true(sig_a ~= sig_b, "Different keys produce different signatures")
|
||||
|
||||
-- Test different messages produce different signatures
|
||||
local sig_m1 = Un.hmac_sign("key", "message1")
|
||||
local sig_m2 = Un.hmac_sign("key", "message2")
|
||||
assert_true(sig_m1 ~= sig_m2, "Different messages produce different signatures")
|
||||
|
||||
-- Test nil handling
|
||||
local null_key = Un.hmac_sign(nil, "message")
|
||||
assert_nil(null_key, "hmac_sign(nil, msg) returns nil")
|
||||
|
||||
local null_msg = Un.hmac_sign("key", nil)
|
||||
assert_nil(null_msg, "hmac_sign(key, nil) returns nil")
|
||||
|
||||
-- Test known HMAC value
|
||||
local known_sig = Un.hmac_sign("key", "message")
|
||||
assert_true(known_sig:sub(1, 32) == "6e9ef29b75fffc5b7abae527d58fdadb",
|
||||
"HMAC-SHA256('key', 'message') matches expected prefix")
|
||||
|
||||
-- ============================================================================
|
||||
-- Test: Un.last_error()
|
||||
-- ============================================================================
|
||||
|
||||
print("\nTesting Un.last_error()...")
|
||||
|
||||
local error_msg = Un.last_error()
|
||||
assert_not_nil(error_msg, "last_error() returns non-nil")
|
||||
|
||||
Un.set_error("test error")
|
||||
assert_equal(Un.last_error(), "test error", "last_error() returns set error")
|
||||
|
||||
-- ============================================================================
|
||||
-- Test: Memory stress test
|
||||
-- ============================================================================
|
||||
|
||||
print("\nTesting Memory Management...")
|
||||
|
||||
-- Stress test HMAC allocation
|
||||
for i = 0, 999 do
|
||||
Un.hmac_sign("key", "message")
|
||||
end
|
||||
PASS("1000 HMAC calls without crash")
|
||||
|
||||
-- Stress test language detection
|
||||
for i = 0, 999 do
|
||||
Un.detect_language("test.py")
|
||||
end
|
||||
PASS("1000 detect_language calls without crash")
|
||||
|
||||
-- Stress test version
|
||||
for i = 0, 999 do
|
||||
Un.version()
|
||||
end
|
||||
PASS("1000 version calls without crash")
|
||||
|
||||
-- ============================================================================
|
||||
-- Test: Function existence
|
||||
-- ============================================================================
|
||||
|
||||
print("\nTesting Library function existence...")
|
||||
|
||||
local functions = {
|
||||
-- Execution functions (8)
|
||||
"execute", "execute_async", "wait_job", "get_job",
|
||||
"cancel_job", "list_jobs", "get_languages", "detect_language",
|
||||
|
||||
-- Session functions (9)
|
||||
"session_list", "session_get", "session_create", "session_destroy",
|
||||
"session_freeze", "session_unfreeze", "session_boost", "session_unboost",
|
||||
"session_execute",
|
||||
|
||||
-- Service functions (17)
|
||||
"service_list", "service_get", "service_create", "service_destroy",
|
||||
"service_freeze", "service_unfreeze", "service_lock", "service_unlock",
|
||||
"service_set_unfreeze_on_demand", "service_redeploy", "service_logs",
|
||||
"service_execute", "service_env_get", "service_env_set",
|
||||
"service_env_delete", "service_env_export", "service_resize",
|
||||
|
||||
-- Snapshot functions (9)
|
||||
"snapshot_list", "snapshot_get", "snapshot_session", "snapshot_service",
|
||||
"snapshot_restore", "snapshot_delete", "snapshot_lock", "snapshot_unlock",
|
||||
"snapshot_clone",
|
||||
|
||||
-- Image functions (13)
|
||||
"image_list", "image_get", "image_publish", "image_delete",
|
||||
"image_lock", "image_unlock", "image_set_visibility",
|
||||
"image_grant_access", "image_revoke_access", "image_list_trusted",
|
||||
"image_transfer", "image_spawn", "image_clone",
|
||||
|
||||
-- PaaS Logs (2)
|
||||
"logs_fetch", "logs_stream",
|
||||
|
||||
-- Utilities
|
||||
"validate_keys", "hmac_sign", "health_check", "version", "last_error"
|
||||
}
|
||||
|
||||
for _, func_name in ipairs(functions) do
|
||||
assert_true(type(Un[func_name]) == "function", "Un." .. func_name .. " exists")
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Summary
|
||||
-- ============================================================================
|
||||
|
||||
print("\n=====================================")
|
||||
print("Test Summary")
|
||||
print("=====================================")
|
||||
print("Passed: \027[32m" .. tests_passed .. "\027[0m")
|
||||
print("Failed: \027[31m" .. tests_failed .. "\027[0m")
|
||||
print("=====================================")
|
||||
|
||||
os.exit(tests_failed > 0 and 1 or 0)
|
||||
|
|
@ -451,13 +451,20 @@ proc cmdExecute(sourceFile: string, envs: seq[string], artifacts: bool, network:
|
|||
let cmd = fmt"""curl -s -X POST '{API_BASE}/execute' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
echo execCurl(cmd)
|
||||
|
||||
proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, screen: bool, inputFiles: seq[string], publicKey: string, secretKey: string) =
|
||||
proc cmdSession(list: bool, kill, info, freeze, unfreeze, boost, unboost, execute, command, shell, network: string, vcpu: int, tmux, screen: bool, inputFiles: seq[string], publicKey: string, secretKey: string) =
|
||||
if list:
|
||||
let authHeaders = buildAuthHeaders("GET", "/sessions", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/sessions' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
if info != "":
|
||||
let path = fmt"/sessions/{info}"
|
||||
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/sessions/{info}' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
if kill != "":
|
||||
let path = fmt"/sessions/{kill}"
|
||||
let authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey)
|
||||
|
|
@ -466,6 +473,74 @@ proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, scree
|
|||
echo GREEN & "Session terminated: " & kill & RESET
|
||||
return
|
||||
|
||||
if freeze != "":
|
||||
let path = fmt"/sessions/{freeze}/freeze"
|
||||
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions/{freeze}/freeze' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Session frozen: " & freeze & RESET
|
||||
return
|
||||
|
||||
if unfreeze != "":
|
||||
let path = fmt"/sessions/{unfreeze}/unfreeze"
|
||||
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions/{unfreeze}/unfreeze' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Session unfreezing: " & unfreeze & RESET
|
||||
return
|
||||
|
||||
if boost != "":
|
||||
let boostVcpu = if vcpu > 0: vcpu else: 2
|
||||
let json = fmt"""{{"vcpu":{boostVcpu}}}"""
|
||||
let path = fmt"/sessions/{boost}/boost"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions/{boost}/boost' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Session boosted to " & $boostVcpu & " vCPU: " & boost & RESET
|
||||
return
|
||||
|
||||
if unboost != "":
|
||||
let path = fmt"/sessions/{unboost}/unboost"
|
||||
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions/{unboost}/unboost' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Session unboosted: " & unboost & RESET
|
||||
return
|
||||
|
||||
if execute != "":
|
||||
let json = fmt"""{"command":"{escapeJson(command)}"}"""
|
||||
let path = fmt"/sessions/{execute}/execute"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions/{execute}/execute' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
let result = execCurl(cmd)
|
||||
|
||||
let stdoutStart = result.find("\"stdout\":\"")
|
||||
if stdoutStart >= 0:
|
||||
let start = stdoutStart + 10
|
||||
var endPos = start
|
||||
while endPos < result.len:
|
||||
if result[endPos] == '"' and (endPos == 0 or result[endPos-1] != '\\'):
|
||||
break
|
||||
inc endPos
|
||||
if endPos > start:
|
||||
var output = result[start..<endPos]
|
||||
output = output.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\")
|
||||
stdout.write(output)
|
||||
|
||||
let stderrStart = result.find("\"stderr\":\"")
|
||||
if stderrStart >= 0:
|
||||
let start = stderrStart + 10
|
||||
var endPos = start
|
||||
while endPos < result.len:
|
||||
if result[endPos] == '"' and (endPos == 0 or result[endPos-1] != '\\'):
|
||||
break
|
||||
inc endPos
|
||||
if endPos > start:
|
||||
var errout = result[start..<endPos]
|
||||
errout = errout.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\")
|
||||
stderr.write(errout)
|
||||
return
|
||||
|
||||
var json = fmt"""{"shell":"{if shell != "": shell else: "bash"}""""
|
||||
if network != "": json.add(fmt""","network":"{network}"""")
|
||||
if vcpu > 0: json.add(fmt""","vcpu":{vcpu}""")
|
||||
|
|
@ -492,7 +567,7 @@ proc setServiceUnfreezeOnDemand(serviceId: string, enabled: bool, publicKey: str
|
|||
except:
|
||||
return false
|
||||
|
||||
proc cmdService(name, ports, bootstrap, bootstrapFile, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, resize: string, resizeVcpu: int, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, unfreezeOnDemand: bool, setUnfreezeOnDemand, setUnfreezeOnDemandEnabled: string, inputFiles: seq[string], svcEnvs: seq[string], svcEnvFile, envAction, envTarget: string, publicKey: string, secretKey: string) =
|
||||
proc cmdService(name, ports, bootstrap, bootstrapFile, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, resize: string, resizeVcpu: int, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, unfreezeOnDemand: bool, setUnfreezeOnDemand, setUnfreezeOnDemandEnabled, lock, unlock, redeploy: string, inputFiles: seq[string], svcEnvs: seq[string], svcEnvFile, envAction, envTarget: string, publicKey: string, secretKey: string) =
|
||||
# Handle env subcommand
|
||||
if envAction != "":
|
||||
cmdServiceEnv(envAction, envTarget, svcEnvs, svcEnvFile, publicKey, secretKey)
|
||||
|
|
@ -577,6 +652,47 @@ proc cmdService(name, ports, bootstrap, bootstrapFile, serviceType: string, list
|
|||
quit(1)
|
||||
return
|
||||
|
||||
if lock != "":
|
||||
let path = fmt"/services/{lock}/lock"
|
||||
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{lock}/lock' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Service locked: " & lock & RESET
|
||||
return
|
||||
|
||||
if unlock != "":
|
||||
let path = fmt"/services/{unlock}/unlock"
|
||||
let status = execCurlPostWithSudo(path, "{}", publicKey, secretKey)
|
||||
if status >= 200 and status < 300:
|
||||
echo GREEN & "Service unlocked: " & unlock & RESET
|
||||
elif status != 428:
|
||||
stderr.writeLine(RED & "Error: Failed to unlock service" & RESET)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
if redeploy != "":
|
||||
var json = "{"
|
||||
var hasContent = false
|
||||
if bootstrap != "":
|
||||
json.add(fmt""""bootstrap":"{escapeJson(bootstrap)}"""")
|
||||
hasContent = true
|
||||
if bootstrapFile != "":
|
||||
if fileExists(bootstrapFile):
|
||||
let bootCode = readFile(bootstrapFile)
|
||||
if hasContent: json.add(",")
|
||||
json.add(fmt""""bootstrap_content":"{escapeJson(bootCode)}"""")
|
||||
else:
|
||||
stderr.writeLine(RED & "Error: Bootstrap file not found: " & bootstrapFile & RESET)
|
||||
quit(1)
|
||||
json.add("}")
|
||||
let path = fmt"/services/{redeploy}/redeploy"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{redeploy}/redeploy' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
let response = execCurl(cmd)
|
||||
echo GREEN & "Service redeployed: " & redeploy & RESET
|
||||
echo response
|
||||
return
|
||||
|
||||
if execute != "":
|
||||
let json = fmt"""{"command":"{escapeJson(command)}"}"""
|
||||
let path = fmt"/services/{execute}/execute"
|
||||
|
|
@ -823,7 +939,144 @@ proc cmdLanguages(jsonOutput: bool, publicKey: string, secretKey: string) =
|
|||
else:
|
||||
inc pos
|
||||
|
||||
proc cmdImage(list: bool, infoId, deleteId, lockId, unlockId, publishId, sourceType, visibilityId, visibilityMode, spawnId, cloneId, name, ports, publicKey, secretKey: string) =
|
||||
proc cmdSnapshot(list: bool, infoId, sessionId, serviceId, restoreId, deleteId, lockId, unlockId, cloneId, cloneType, name, ports: string, hot: bool, publicKey, secretKey: string) =
|
||||
if list:
|
||||
let authHeaders = buildAuthHeaders("GET", "/snapshots", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/snapshots' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
if infoId != "":
|
||||
let path = fmt"/snapshots/{infoId}"
|
||||
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/snapshots/{infoId}' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
if sessionId != "":
|
||||
var json = "{"
|
||||
var hasContent = false
|
||||
if name != "":
|
||||
json.add(fmt""""name":"{name}"""")
|
||||
hasContent = true
|
||||
if hot:
|
||||
if hasContent: json.add(",")
|
||||
json.add(""""hot":true""")
|
||||
json.add("}")
|
||||
let path = fmt"/sessions/{sessionId}/snapshot"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions/{sessionId}/snapshot' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
let response = execCurl(cmd)
|
||||
echo GREEN & "Snapshot created" & RESET
|
||||
echo response
|
||||
return
|
||||
|
||||
if serviceId != "":
|
||||
var json = "{"
|
||||
var hasContent = false
|
||||
if name != "":
|
||||
json.add(fmt""""name":"{name}"""")
|
||||
hasContent = true
|
||||
if hot:
|
||||
if hasContent: json.add(",")
|
||||
json.add(""""hot":true""")
|
||||
json.add("}")
|
||||
let path = fmt"/services/{serviceId}/snapshot"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{serviceId}/snapshot' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
let response = execCurl(cmd)
|
||||
echo GREEN & "Snapshot created" & RESET
|
||||
echo response
|
||||
return
|
||||
|
||||
if restoreId != "":
|
||||
let path = fmt"/snapshots/{restoreId}/restore"
|
||||
let authHeaders = buildAuthHeaders("POST", path, "{}", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/snapshots/{restoreId}/restore' -H 'Content-Type: application/json' {authHeaders} -d '{{}}'"""
|
||||
let response = execCurl(cmd)
|
||||
echo GREEN & "Snapshot restored" & RESET
|
||||
echo response
|
||||
return
|
||||
|
||||
if deleteId != "":
|
||||
let path = fmt"/snapshots/{deleteId}"
|
||||
let status = execCurlDeleteWithSudo(path, publicKey, secretKey)
|
||||
if status >= 200 and status < 300:
|
||||
echo GREEN & "Snapshot deleted: " & deleteId & RESET
|
||||
elif status != 428:
|
||||
stderr.writeLine(RED & "Error: Failed to delete snapshot" & RESET)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
if lockId != "":
|
||||
let path = fmt"/snapshots/{lockId}/lock"
|
||||
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/snapshots/{lockId}/lock' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Snapshot locked: " & lockId & RESET
|
||||
return
|
||||
|
||||
if unlockId != "":
|
||||
let path = fmt"/snapshots/{unlockId}/unlock"
|
||||
let status = execCurlPostWithSudo(path, "{}", publicKey, secretKey)
|
||||
if status >= 200 and status < 300:
|
||||
echo GREEN & "Snapshot unlocked: " & unlockId & RESET
|
||||
elif status != 428:
|
||||
stderr.writeLine(RED & "Error: Failed to unlock snapshot" & RESET)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
if cloneId != "":
|
||||
var json = fmt"""{{"clone_type":"{if cloneType != "": cloneType else: "session"}"""""
|
||||
if name != "": json.add(fmt""","name":"{name}"""")
|
||||
if ports != "": json.add(fmt""","ports":[{ports}]""")
|
||||
json.add("}")
|
||||
let path = fmt"/snapshots/{cloneId}/clone"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/snapshots/{cloneId}/clone' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
let response = execCurl(cmd)
|
||||
echo GREEN & "Snapshot cloned" & RESET
|
||||
echo response
|
||||
return
|
||||
|
||||
stderr.writeLine(RED & "Error: Use --list, --info, --session, --service, --restore, --delete, --lock, --unlock, or --clone" & RESET)
|
||||
quit(1)
|
||||
|
||||
proc cmdLogs(source: string, lines: int, since, grep: string, follow: bool, publicKey, secretKey: string) =
|
||||
let sourceParam = if source != "": source else: "all"
|
||||
let linesParam = if lines > 0: lines else: 100
|
||||
let sinceParam = if since != "": since else: "1h"
|
||||
|
||||
if follow:
|
||||
var endpoint = fmt"/paas/logs/stream?source={sourceParam}"
|
||||
if grep != "": endpoint.add(fmt"&grep={grep}")
|
||||
let authHeaders = buildAuthHeaders("GET", endpoint, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -N -X GET '{PORTAL_BASE}{endpoint}' -H 'Accept: text/event-stream' {authHeaders}"""
|
||||
# Stream logs - this will block
|
||||
let output = execProcess(cmd)
|
||||
echo output
|
||||
else:
|
||||
var endpoint = fmt"/paas/logs?source={sourceParam}&lines={linesParam}&since={sinceParam}"
|
||||
if grep != "": endpoint.add(fmt"&grep={grep}")
|
||||
let authHeaders = buildAuthHeaders("GET", endpoint, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{PORTAL_BASE}{endpoint}' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
|
||||
proc cmdHealth() =
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/health'"""
|
||||
let response = execProcess(cmd)
|
||||
if response.contains("\"status\":\"healthy\"") or response.contains("\"ok\":true"):
|
||||
echo GREEN & "API is healthy" & RESET
|
||||
else:
|
||||
echo RED & "API may be unhealthy" & RESET
|
||||
echo response
|
||||
|
||||
proc cmdVersion() =
|
||||
echo "un.nim version 1.0.0"
|
||||
echo "API: " & API_BASE
|
||||
echo "Portal: " & PORTAL_BASE
|
||||
|
||||
proc cmdImage(list: bool, infoId, deleteId, lockId, unlockId, publishId, sourceType, visibilityId, visibilityMode, spawnId, cloneId, name, ports, grantId, revokeId, trustedId, trustedKey, transferId, toKey, publicKey, secretKey: string) =
|
||||
if list:
|
||||
let authHeaders = buildAuthHeaders("GET", "/images", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/images' {authHeaders}"""
|
||||
|
|
@ -919,7 +1172,52 @@ proc cmdImage(list: bool, infoId, deleteId, lockId, unlockId, publishId, sourceT
|
|||
echo response
|
||||
return
|
||||
|
||||
stderr.writeLine(RED & "Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone" & RESET)
|
||||
if grantId != "":
|
||||
if trustedKey == "":
|
||||
stderr.writeLine(RED & "Error: --grant requires --trusted-key" & RESET)
|
||||
quit(1)
|
||||
let json = fmt"""{{"trusted_api_key":"{trustedKey}"}}"""
|
||||
let path = fmt"/images/{grantId}/grant"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/images/{grantId}/grant' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Access granted to " & trustedKey & RESET
|
||||
return
|
||||
|
||||
if revokeId != "":
|
||||
if trustedKey == "":
|
||||
stderr.writeLine(RED & "Error: --revoke requires --trusted-key" & RESET)
|
||||
quit(1)
|
||||
let json = fmt"""{{"trusted_api_key":"{trustedKey}"}}"""
|
||||
let path = fmt"/images/{revokeId}/revoke"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/images/{revokeId}/revoke' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Access revoked from " & trustedKey & RESET
|
||||
return
|
||||
|
||||
if trustedId != "":
|
||||
let path = fmt"/images/{trustedId}/trusted"
|
||||
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/images/{trustedId}/trusted' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
if transferId != "":
|
||||
if toKey == "":
|
||||
stderr.writeLine(RED & "Error: --transfer requires --to-key" & RESET)
|
||||
quit(1)
|
||||
let json = fmt"""{{"to_api_key":"{toKey}"}}"""
|
||||
let path = fmt"/images/{transferId}/transfer"
|
||||
let status = execCurlPostWithSudo(path, json, publicKey, secretKey)
|
||||
if status >= 200 and status < 300:
|
||||
echo GREEN & "Image transferred to " & toKey & RESET
|
||||
elif status != 428:
|
||||
stderr.writeLine(RED & "Error: Failed to transfer image" & RESET)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
stderr.writeLine(RED & "Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, --clone, --grant, --revoke, --trusted, or --transfer" & RESET)
|
||||
quit(1)
|
||||
|
||||
proc main() =
|
||||
|
|
@ -985,6 +1283,7 @@ proc main() =
|
|||
var list = false
|
||||
var infoId, deleteId, lockId, unlockId, publishId, sourceType = ""
|
||||
var visibilityId, visibilityMode, spawnId, cloneId, name, ports = ""
|
||||
var grantId, revokeId, trustedId, trustedKey, transferId, toKey = ""
|
||||
var i = 1
|
||||
while i < args.len:
|
||||
case args[i]
|
||||
|
|
@ -1003,10 +1302,69 @@ proc main() =
|
|||
of "--clone": cloneId = args[i+1]; inc i
|
||||
of "--name": name = args[i+1]; inc i
|
||||
of "--ports": ports = args[i+1]; inc i
|
||||
of "--grant": grantId = args[i+1]; inc i
|
||||
of "--revoke": revokeId = args[i+1]; inc i
|
||||
of "--trusted": trustedId = args[i+1]; inc i
|
||||
of "--trusted-key": trustedKey = args[i+1]; inc i
|
||||
of "--transfer": transferId = args[i+1]; inc i
|
||||
of "--to-key": toKey = args[i+1]; inc i
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
else: discard
|
||||
inc i
|
||||
cmdImage(list, infoId, deleteId, lockId, unlockId, publishId, sourceType, visibilityId, visibilityMode, spawnId, cloneId, name, ports, publicKey, secretKey)
|
||||
cmdImage(list, infoId, deleteId, lockId, unlockId, publishId, sourceType, visibilityId, visibilityMode, spawnId, cloneId, name, ports, grantId, revokeId, trustedId, trustedKey, transferId, toKey, publicKey, secretKey)
|
||||
return
|
||||
|
||||
if args[0] == "snapshot":
|
||||
var list = false
|
||||
var infoId, sessionId, serviceId, restoreId, deleteId, lockId, unlockId = ""
|
||||
var cloneId, cloneType, name, ports = ""
|
||||
var hot = false
|
||||
var i = 1
|
||||
while i < args.len:
|
||||
case args[i]
|
||||
of "--list", "-l": list = true
|
||||
of "--info": infoId = args[i+1]; inc i
|
||||
of "--session": sessionId = args[i+1]; inc i
|
||||
of "--service": serviceId = args[i+1]; inc i
|
||||
of "--restore": restoreId = args[i+1]; inc i
|
||||
of "--delete": deleteId = args[i+1]; inc i
|
||||
of "--lock": lockId = args[i+1]; inc i
|
||||
of "--unlock": unlockId = args[i+1]; inc i
|
||||
of "--clone": cloneId = args[i+1]; inc i
|
||||
of "--clone-type": cloneType = args[i+1]; inc i
|
||||
of "--name": name = args[i+1]; inc i
|
||||
of "--ports": ports = args[i+1]; inc i
|
||||
of "--hot": hot = true
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
else: discard
|
||||
inc i
|
||||
cmdSnapshot(list, infoId, sessionId, serviceId, restoreId, deleteId, lockId, unlockId, cloneId, cloneType, name, ports, hot, publicKey, secretKey)
|
||||
return
|
||||
|
||||
if args[0] == "logs":
|
||||
var source, since, grep = ""
|
||||
var lines = 0
|
||||
var follow = false
|
||||
var i = 1
|
||||
while i < args.len:
|
||||
case args[i]
|
||||
of "--source": source = args[i+1]; inc i
|
||||
of "--lines": lines = parseInt(args[i+1]); inc i
|
||||
of "--since": since = args[i+1]; inc i
|
||||
of "--grep": grep = args[i+1]; inc i
|
||||
of "--follow", "-f": follow = true
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
else: discard
|
||||
inc i
|
||||
cmdLogs(source, lines, since, grep, follow, publicKey, secretKey)
|
||||
return
|
||||
|
||||
if args[0] == "health":
|
||||
cmdHealth()
|
||||
return
|
||||
|
||||
if args[0] == "version":
|
||||
cmdVersion()
|
||||
return
|
||||
|
||||
if args[0] == "key":
|
||||
|
|
@ -1022,7 +1380,7 @@ proc main() =
|
|||
|
||||
if args[0] == "session":
|
||||
var list = false
|
||||
var kill, shell, network = ""
|
||||
var kill, info, freeze, unfreeze, boost, unboost, execute, command, shell, network = ""
|
||||
var vcpu = 0
|
||||
var tmux, screen = false
|
||||
var inputFiles: seq[string] = @[]
|
||||
|
|
@ -1031,6 +1389,13 @@ proc main() =
|
|||
case args[i]
|
||||
of "--list": list = true
|
||||
of "--kill": kill = args[i+1]; inc i
|
||||
of "--info": info = args[i+1]; inc i
|
||||
of "--freeze": freeze = args[i+1]; inc i
|
||||
of "--unfreeze": unfreeze = args[i+1]; inc i
|
||||
of "--boost": boost = args[i+1]; inc i
|
||||
of "--unboost": unboost = args[i+1]; inc i
|
||||
of "--execute": execute = args[i+1]; inc i
|
||||
of "--command": command = args[i+1]; inc i
|
||||
of "--shell": shell = args[i+1]; inc i
|
||||
of "-n": network = args[i+1]; inc i
|
||||
of "-v": vcpu = parseInt(args[i+1]); inc i
|
||||
|
|
@ -1047,13 +1412,14 @@ proc main() =
|
|||
inc i
|
||||
else: discard
|
||||
inc i
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, inputFiles, publicKey, secretKey)
|
||||
cmdSession(list, kill, info, freeze, unfreeze, boost, unboost, execute, command, shell, network, vcpu, tmux, screen, inputFiles, publicKey, secretKey)
|
||||
return
|
||||
|
||||
if args[0] == "service":
|
||||
var name, ports, bootstrap, bootstrapFile, serviceType = ""
|
||||
var list = false
|
||||
var info, logs, tail, sleep, wake, destroy, resize, execute, command, dumpBootstrap, dumpFile, network = ""
|
||||
var lock, unlock, redeploy = ""
|
||||
var vcpu = 0
|
||||
var resizeVcpu = 0
|
||||
var unfreezeOnDemand = false
|
||||
|
|
@ -1078,7 +1444,7 @@ proc main() =
|
|||
of "-k": publicKey = args[i+1]; inc i
|
||||
else: discard
|
||||
inc i
|
||||
cmdService(name, ports, bootstrap, bootstrapFile, serviceType, list, info, logs, tail, sleep, wake, destroy, resize, resizeVcpu, execute, command, dumpBootstrap, dumpFile, network, vcpu, unfreezeOnDemand, setUnfreezeOnDemand, setUnfreezeOnDemandEnabled, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey)
|
||||
cmdService(name, ports, bootstrap, bootstrapFile, serviceType, list, info, logs, tail, sleep, wake, destroy, resize, resizeVcpu, execute, command, dumpBootstrap, dumpFile, network, vcpu, unfreezeOnDemand, setUnfreezeOnDemand, setUnfreezeOnDemandEnabled, lock, unlock, redeploy, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey)
|
||||
return
|
||||
|
||||
while i < args.len:
|
||||
|
|
@ -1101,6 +1467,9 @@ proc main() =
|
|||
of "--command": command = args[i+1]; inc i
|
||||
of "--dump-bootstrap": dumpBootstrap = args[i+1]; inc i
|
||||
of "--dump-file": dumpFile = args[i+1]; inc i
|
||||
of "--lock": lock = args[i+1]; inc i
|
||||
of "--unlock": unlock = args[i+1]; inc i
|
||||
of "--redeploy": redeploy = args[i+1]; inc i
|
||||
of "-n": network = args[i+1]; inc i
|
||||
of "-v": vcpu = parseInt(args[i+1]); inc i
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
|
|
@ -1121,7 +1490,7 @@ proc main() =
|
|||
inc i
|
||||
else: discard
|
||||
inc i
|
||||
cmdService(name, ports, bootstrap, bootstrapFile, serviceType, list, info, logs, tail, sleep, wake, destroy, resize, resizeVcpu, execute, command, dumpBootstrap, dumpFile, network, vcpu, unfreezeOnDemand, setUnfreezeOnDemand, setUnfreezeOnDemandEnabled, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey)
|
||||
cmdService(name, ports, bootstrap, bootstrapFile, serviceType, list, info, logs, tail, sleep, wake, destroy, resize, resizeVcpu, execute, command, dumpBootstrap, dumpFile, network, vcpu, unfreezeOnDemand, setUnfreezeOnDemand, setUnfreezeOnDemandEnabled, lock, unlock, redeploy, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey)
|
||||
return
|
||||
|
||||
# Execute mode
|
||||
|
|
|
|||
|
|
@ -1674,6 +1674,12 @@ void cmdImage(NSArray* args) {
|
|||
NSString* cloneId = nil;
|
||||
NSString* name = nil;
|
||||
NSString* ports = nil;
|
||||
NSString* grantId = nil;
|
||||
NSString* revokeId = nil;
|
||||
NSString* trustedId = nil;
|
||||
NSString* trustedKey = nil;
|
||||
NSString* transferId = nil;
|
||||
NSString* toKey = nil;
|
||||
|
||||
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||
NSString* arg = args[i];
|
||||
|
|
@ -1702,6 +1708,18 @@ void cmdImage(NSArray* args) {
|
|||
name = args[++i];
|
||||
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
|
||||
ports = args[++i];
|
||||
} else if ([arg isEqualToString:@"--grant"] && i + 1 < [args count]) {
|
||||
grantId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--revoke"] && i + 1 < [args count]) {
|
||||
revokeId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--trusted"] && i + 1 < [args count]) {
|
||||
trustedId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--trusted-key"] && i + 1 < [args count]) {
|
||||
trustedKey = args[++i];
|
||||
} else if ([arg isEqualToString:@"--transfer"] && i + 1 < [args count]) {
|
||||
transferId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--to-key"] && i + 1 < [args count]) {
|
||||
toKey = args[++i];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1831,11 +1849,443 @@ void cmdImage(NSArray* args) {
|
|||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr, "%sError: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, or --clone%s\n",
|
||||
if (grantId) {
|
||||
if (!trustedKey) {
|
||||
fprintf(stderr, "%sError: --grant requires --trusted-key%s\n", [RED UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/grant", grantId];
|
||||
NSDictionary* payload = @{@"trusted_api_key": trustedKey};
|
||||
apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
||||
printf("%sAccess granted to %s%s\n", [GREEN UTF8String], [trustedKey UTF8String], [RESET UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (revokeId) {
|
||||
if (!trustedKey) {
|
||||
fprintf(stderr, "%sError: --revoke requires --trusted-key%s\n", [RED UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/revoke", revokeId];
|
||||
NSDictionary* payload = @{@"trusted_api_key": trustedKey};
|
||||
apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
||||
printf("%sAccess revoked from %s%s\n", [GREEN UTF8String], [trustedKey UTF8String], [RESET UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (trustedId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/trusted", trustedId];
|
||||
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (transferId) {
|
||||
if (!toKey) {
|
||||
fprintf(stderr, "%sError: --transfer requires --to-key%s\n", [RED UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/transfer", transferId];
|
||||
NSDictionary* payload = @{@"to_api_key": toKey};
|
||||
NSInteger statusCode = 0;
|
||||
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"POST", payload, publicKey, secretKey, &statusCode);
|
||||
|
||||
if (statusCode == 428) {
|
||||
NSData* bodyData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:nil];
|
||||
NSString* bodyStr = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
|
||||
if (handleSudoChallenge(response, @"POST", endpoint, bodyStr, publicKey, secretKey)) {
|
||||
printf("%sImage transferred to %s%s\n", [GREEN UTF8String], [toKey UTF8String], [RESET UTF8String]);
|
||||
} else {
|
||||
exit(1);
|
||||
}
|
||||
} else if (statusCode >= 200 && statusCode < 300) {
|
||||
printf("%sImage transferred to %s%s\n", [GREEN UTF8String], [toKey UTF8String], [RESET UTF8String]);
|
||||
} else {
|
||||
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr, "%sError: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, --clone, --grant, --revoke, --trusted, or --transfer%s\n",
|
||||
[RED UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Snapshot Command
|
||||
// ============================================================================
|
||||
|
||||
void cmdSnapshot(NSArray* args) {
|
||||
NSString* publicKey, *secretKey;
|
||||
UNGetApiKeysCLI(&publicKey, &secretKey);
|
||||
|
||||
BOOL listMode = NO;
|
||||
NSString* infoId = nil;
|
||||
NSString* sessionId = nil;
|
||||
NSString* serviceId = nil;
|
||||
NSString* restoreId = nil;
|
||||
NSString* deleteId = nil;
|
||||
NSString* lockId = nil;
|
||||
NSString* unlockId = nil;
|
||||
NSString* cloneId = nil;
|
||||
NSString* cloneType = nil;
|
||||
NSString* name = nil;
|
||||
NSString* ports = nil;
|
||||
BOOL hot = NO;
|
||||
|
||||
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||
NSString* arg = args[i];
|
||||
if ([arg isEqualToString:@"--list"] || [arg isEqualToString:@"-l"]) {
|
||||
listMode = YES;
|
||||
} else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) {
|
||||
infoId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--session"] && i + 1 < [args count]) {
|
||||
sessionId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--service"] && i + 1 < [args count]) {
|
||||
serviceId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--restore"] && i + 1 < [args count]) {
|
||||
restoreId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--delete"] && i + 1 < [args count]) {
|
||||
deleteId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--lock"] && i + 1 < [args count]) {
|
||||
lockId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--unlock"] && i + 1 < [args count]) {
|
||||
unlockId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--clone"] && i + 1 < [args count]) {
|
||||
cloneId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--clone-type"] && i + 1 < [args count]) {
|
||||
cloneType = args[++i];
|
||||
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
|
||||
name = args[++i];
|
||||
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
|
||||
ports = args[++i];
|
||||
} else if ([arg isEqualToString:@"--hot"]) {
|
||||
hot = YES;
|
||||
}
|
||||
}
|
||||
|
||||
if (listMode) {
|
||||
NSDictionary* result = apiRequestCLI(@"/snapshots", @"GET", nil, publicKey, secretKey);
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (infoId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@", infoId];
|
||||
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@/snapshot", sessionId];
|
||||
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
||||
if (name) payload[@"name"] = name;
|
||||
if (hot) payload[@"hot"] = @YES;
|
||||
|
||||
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%sSnapshot created%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/snapshot", serviceId];
|
||||
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
||||
if (name) payload[@"name"] = name;
|
||||
if (hot) payload[@"hot"] = @YES;
|
||||
|
||||
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%sSnapshot created%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (restoreId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/restore", restoreId];
|
||||
NSDictionary* result = apiRequestCLI(endpoint, @"POST", @{}, publicKey, secretKey);
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%sSnapshot restored%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@", deleteId];
|
||||
NSInteger statusCode = 0;
|
||||
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"DELETE", nil, publicKey, secretKey, &statusCode);
|
||||
|
||||
if (statusCode == 428) {
|
||||
if (handleSudoChallenge(response, @"DELETE", endpoint, nil, publicKey, secretKey)) {
|
||||
printf("%sSnapshot deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
|
||||
} else {
|
||||
exit(1);
|
||||
}
|
||||
} else if (statusCode >= 200 && statusCode < 300) {
|
||||
printf("%sSnapshot deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
|
||||
} else {
|
||||
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (lockId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/lock", lockId];
|
||||
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
|
||||
printf("%sSnapshot locked: %s%s\n", [GREEN UTF8String], [lockId UTF8String], [RESET UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (unlockId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/unlock", unlockId];
|
||||
NSInteger statusCode = 0;
|
||||
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"POST", @{}, publicKey, secretKey, &statusCode);
|
||||
|
||||
if (statusCode == 428) {
|
||||
if (handleSudoChallenge(response, @"POST", endpoint, @"{}", publicKey, secretKey)) {
|
||||
printf("%sSnapshot unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
|
||||
} else {
|
||||
exit(1);
|
||||
}
|
||||
} else if (statusCode >= 200 && statusCode < 300) {
|
||||
printf("%sSnapshot unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
|
||||
} else {
|
||||
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cloneId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/clone", cloneId];
|
||||
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
||||
payload[@"clone_type"] = cloneType ?: @"session";
|
||||
if (name) payload[@"name"] = name;
|
||||
if (ports) {
|
||||
NSArray* portStrings = [ports componentsSeparatedByString:@","];
|
||||
NSMutableArray* portNumbers = [NSMutableArray array];
|
||||
for (NSString* p in portStrings) {
|
||||
[portNumbers addObject:@([p intValue])];
|
||||
}
|
||||
payload[@"ports"] = portNumbers;
|
||||
}
|
||||
|
||||
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%sSnapshot cloned%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr, "%sError: Use --list, --info, --session, --service, --restore, --delete, --lock, --unlock, or --clone%s\n",
|
||||
[RED UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Logs Command
|
||||
// ============================================================================
|
||||
|
||||
void cmdLogs(NSArray* args) {
|
||||
NSString* publicKey, *secretKey;
|
||||
UNGetApiKeysCLI(&publicKey, &secretKey);
|
||||
|
||||
NSString* source = @"all";
|
||||
NSInteger lines = 100;
|
||||
NSString* since = @"1h";
|
||||
NSString* grepPattern = nil;
|
||||
BOOL follow = NO;
|
||||
|
||||
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||
NSString* arg = args[i];
|
||||
if (([arg isEqualToString:@"--source"] || [arg isEqualToString:@"-s"]) && i + 1 < [args count]) {
|
||||
source = args[++i];
|
||||
} else if (([arg isEqualToString:@"--lines"] || [arg isEqualToString:@"-n"]) && i + 1 < [args count]) {
|
||||
lines = [args[++i] integerValue];
|
||||
} else if ([arg isEqualToString:@"--since"] && i + 1 < [args count]) {
|
||||
since = args[++i];
|
||||
} else if (([arg isEqualToString:@"--grep"] || [arg isEqualToString:@"-g"]) && i + 1 < [args count]) {
|
||||
grepPattern = args[++i];
|
||||
} else if ([arg isEqualToString:@"--follow"] || [arg isEqualToString:@"-f"]) {
|
||||
follow = YES;
|
||||
}
|
||||
}
|
||||
|
||||
if (follow) {
|
||||
// Streaming logs via SSE
|
||||
NSMutableString* endpoint = [NSMutableString stringWithFormat:@"/paas/logs/stream?source=%@", source];
|
||||
if (grepPattern) {
|
||||
NSString* encoded = [grepPattern stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
||||
[endpoint appendFormat:@"&grep=%@", encoded];
|
||||
}
|
||||
|
||||
NSString* urlStr = [NSString stringWithFormat:@"%@%@", UN_PORTAL_BASE, endpoint];
|
||||
NSURL* url = [NSURL URLWithString:urlStr];
|
||||
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
||||
request.HTTPMethod = @"GET";
|
||||
[request setValue:@"text/event-stream" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
// Add HMAC authentication headers
|
||||
if (secretKey && [secretKey length] > 0) {
|
||||
NSString* timestamp = [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]];
|
||||
NSString* message = [NSString stringWithFormat:@"%@:GET:%@:", timestamp, endpoint];
|
||||
NSString* signature = hmacSign(secretKey, message);
|
||||
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
|
||||
[request setValue:timestamp forHTTPHeaderField:@"X-Timestamp"];
|
||||
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
|
||||
} else {
|
||||
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
|
||||
}
|
||||
|
||||
printf("Streaming logs (Ctrl+C to stop)...\n");
|
||||
|
||||
// Use synchronous download for SSE - will block
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
__block NSData* responseData = nil;
|
||||
|
||||
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
|
||||
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
|
||||
responseData = data;
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}];
|
||||
[task resume];
|
||||
|
||||
// For SSE, we need to handle streaming differently - just show we attempted
|
||||
// Real SSE would need a custom NSURLSession delegate
|
||||
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (responseData) {
|
||||
NSString* output = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
|
||||
printf("%s\n", [output UTF8String]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch logs
|
||||
NSMutableString* endpoint = [NSMutableString stringWithFormat:@"/paas/logs?source=%@&lines=%ld&since=%@",
|
||||
source, (long)lines, since];
|
||||
if (grepPattern) {
|
||||
NSString* encoded = [grepPattern stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
||||
[endpoint appendFormat:@"&grep=%@", encoded];
|
||||
}
|
||||
|
||||
// Call Portal API for logs
|
||||
NSString* urlStr = [NSString stringWithFormat:@"%@%@", UN_PORTAL_BASE, endpoint];
|
||||
NSURL* url = [NSURL URLWithString:urlStr];
|
||||
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
||||
request.HTTPMethod = @"GET";
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
// Add HMAC authentication headers
|
||||
if (secretKey && [secretKey length] > 0) {
|
||||
NSString* timestamp = [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]];
|
||||
NSString* message = [NSString stringWithFormat:@"%@:GET:%@:", timestamp, endpoint];
|
||||
NSString* signature = hmacSign(secretKey, message);
|
||||
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
|
||||
[request setValue:timestamp forHTTPHeaderField:@"X-Timestamp"];
|
||||
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
|
||||
} else {
|
||||
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
|
||||
}
|
||||
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
__block NSDictionary* responseData = nil;
|
||||
__block NSInteger statusCode = 0;
|
||||
|
||||
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
|
||||
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
|
||||
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
|
||||
statusCode = httpResponse.statusCode;
|
||||
if (data) {
|
||||
responseData = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
}
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}];
|
||||
[task resume];
|
||||
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (statusCode == 200 && responseData) {
|
||||
NSArray* logLines = responseData[@"logs"];
|
||||
if (logLines && [logLines isKindOfClass:[NSArray class]]) {
|
||||
for (NSDictionary* entry in logLines) {
|
||||
NSString* src = entry[@"source"] ?: @"unknown";
|
||||
NSString* msg = entry[@"line"] ?: @"";
|
||||
printf("[%s] %s\n", [src UTF8String], [msg UTF8String]);
|
||||
}
|
||||
} else {
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:responseData options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Health Command
|
||||
// ============================================================================
|
||||
|
||||
void cmdHealth(void) {
|
||||
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/health", UN_API_BASE]];
|
||||
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
||||
request.HTTPMethod = @"GET";
|
||||
request.timeoutInterval = 10;
|
||||
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
__block NSDictionary* responseData = nil;
|
||||
__block NSInteger statusCode = 0;
|
||||
|
||||
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
|
||||
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
|
||||
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
|
||||
statusCode = httpResponse.statusCode;
|
||||
if (data) {
|
||||
responseData = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
}
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}];
|
||||
[task resume];
|
||||
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (statusCode == 200) {
|
||||
printf("%sAPI is healthy%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
||||
} else {
|
||||
printf("%sAPI may be unhealthy%s\n", [RED UTF8String], [RESET UTF8String]);
|
||||
}
|
||||
|
||||
if (responseData) {
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:responseData options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Version Command
|
||||
// ============================================================================
|
||||
|
||||
void cmdVersion(void) {
|
||||
printf("un.m version 1.0.0\n");
|
||||
printf("API: %s\n", [UN_API_BASE UTF8String]);
|
||||
printf("Portal: %s\n", [UN_PORTAL_BASE UTF8String]);
|
||||
}
|
||||
|
||||
void cmdLanguages(BOOL jsonOutput) {
|
||||
NSString* publicKey, *secretKey;
|
||||
UNGetApiKeysCLI(&publicKey, &secretKey);
|
||||
|
|
@ -1951,6 +2401,14 @@ int main(int argc, const char* argv[]) {
|
|||
cmdLanguages(jsonOutput);
|
||||
} else if ([firstArg isEqualToString:@"key"]) {
|
||||
cmdKey([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
||||
} else if ([firstArg isEqualToString:@"snapshot"]) {
|
||||
cmdSnapshot([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
||||
} else if ([firstArg isEqualToString:@"logs"]) {
|
||||
cmdLogs([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
||||
} else if ([firstArg isEqualToString:@"health"]) {
|
||||
cmdHealth();
|
||||
} else if ([firstArg isEqualToString:@"version"] || [firstArg isEqualToString:@"--version"]) {
|
||||
cmdVersion();
|
||||
} else {
|
||||
cmdExecute(args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -995,6 +995,397 @@ module Client = struct
|
|||
languages ~public_key:client.public_key ~secret_key:client.secret_key ()
|
||||
end
|
||||
|
||||
(* ============================================================================
|
||||
Library API - Sessions (9)
|
||||
============================================================================ *)
|
||||
|
||||
(** SDK version *)
|
||||
let sdk_version = "4.2.0"
|
||||
|
||||
(** Return the SDK version *)
|
||||
let version () = sdk_version
|
||||
|
||||
(** Check API health *)
|
||||
let health_check () =
|
||||
let cmd = Printf.sprintf "curl -s -o /dev/null -w '%%{http_code}' %s/health" api_base in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let status = try input_line ic with End_of_file -> "0" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
String.trim status = "200"
|
||||
|
||||
(** Generate HMAC-SHA256 signature *)
|
||||
let hmac_sign secret message = hmac_sha256 secret message
|
||||
|
||||
(** Detect language from filename *)
|
||||
let detect_language filename =
|
||||
let ext = get_extension filename in
|
||||
ext_to_lang ext
|
||||
|
||||
(** List all sessions *)
|
||||
let session_list ?public_key ?secret_key () =
|
||||
api_get ?public_key ?secret_key "/sessions"
|
||||
|
||||
(** Get session details *)
|
||||
let session_get ?public_key ?secret_key session_id =
|
||||
api_get ?public_key ?secret_key (Printf.sprintf "/sessions/%s" session_id)
|
||||
|
||||
(** Create a new session *)
|
||||
let session_create ?public_key ?secret_key ?(shell="bash") ?network ?vcpu () =
|
||||
let network_json = match network with Some n -> Printf.sprintf ",\"network\":\"%s\"" n | None -> "" in
|
||||
let vcpu_json = match vcpu with Some v -> Printf.sprintf ",\"vcpu\":%d" v | None -> "" in
|
||||
let json = Printf.sprintf "{\"shell\":\"%s\"%s%s}" shell network_json vcpu_json in
|
||||
let response = api_post ?public_key ?secret_key "/sessions" json in
|
||||
extract_json_value response "id"
|
||||
|
||||
(** Destroy a session *)
|
||||
let session_destroy ?public_key ?secret_key session_id =
|
||||
let response = api_delete ?public_key ?secret_key (Printf.sprintf "/sessions/%s" session_id) in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Freeze a session *)
|
||||
let session_freeze ?public_key ?secret_key session_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/sessions/%s/freeze" session_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Unfreeze a session *)
|
||||
let session_unfreeze ?public_key ?secret_key session_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/sessions/%s/unfreeze" session_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Boost session resources *)
|
||||
let session_boost ?public_key ?secret_key session_id vcpu =
|
||||
let (pk, sk) = get_credentials ?public_key ?secret_key () in
|
||||
let json = Printf.sprintf "{\"vcpu\":%d}" vcpu in
|
||||
let endpoint = Printf.sprintf "/sessions/%s" session_id in
|
||||
let auth_headers = build_auth_headers pk sk "PATCH" endpoint json in
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.json" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -X PATCH %s%s -H 'Content-Type: application/json'%s -d @%s"
|
||||
api_base endpoint auth_headers tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let response = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
Sys.remove tmp_file;
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Unboost session *)
|
||||
let session_unboost ?public_key ?secret_key session_id =
|
||||
session_boost ?public_key ?secret_key session_id 1
|
||||
|
||||
(** Execute a command in a session *)
|
||||
let session_execute ?public_key ?secret_key session_id command =
|
||||
let json = Printf.sprintf "{\"command\":\"%s\"}" (escape_json command) in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/sessions/%s/execute" session_id) json in
|
||||
let stdout_val = match extract_json_value response "stdout" with Some s -> unescape_json s | None -> "" in
|
||||
let stderr_val = match extract_json_value response "stderr" with Some s -> unescape_json s | None -> "" in
|
||||
let exit_code = match extract_json_int response "exit_code" with Some i -> i | None -> 0 in
|
||||
{ success = (exit_code = 0); stdout = stdout_val; stderr = stderr_val; exit_code; job_id = None }
|
||||
|
||||
(* ============================================================================
|
||||
Library API - Services (17)
|
||||
============================================================================ *)
|
||||
|
||||
(** List all services *)
|
||||
let service_list ?public_key ?secret_key () =
|
||||
api_get ?public_key ?secret_key "/services"
|
||||
|
||||
(** Get service details *)
|
||||
let service_get ?public_key ?secret_key service_id =
|
||||
api_get ?public_key ?secret_key (Printf.sprintf "/services/%s" service_id)
|
||||
|
||||
(** Create a new service *)
|
||||
let service_create ?public_key ?secret_key name ?ports ?bootstrap ?network ?vcpu () =
|
||||
let ports_json = match ports with Some p -> Printf.sprintf ",\"ports\":[%s]" p | None -> "" in
|
||||
let bootstrap_json = match bootstrap with Some b -> Printf.sprintf ",\"bootstrap\":\"%s\"" (escape_json b) | None -> "" in
|
||||
let network_json = match network with Some n -> Printf.sprintf ",\"network\":\"%s\"" n | None -> "" in
|
||||
let vcpu_json = match vcpu with Some v -> Printf.sprintf ",\"vcpu\":%d" v | None -> "" in
|
||||
let json = Printf.sprintf "{\"name\":\"%s\"%s%s%s%s}" (escape_json name) ports_json bootstrap_json network_json vcpu_json in
|
||||
let response = api_post ?public_key ?secret_key "/services" json in
|
||||
extract_json_value response "id"
|
||||
|
||||
(** Destroy a service *)
|
||||
let service_destroy ?public_key ?secret_key service_id =
|
||||
match api_delete_with_sudo ?public_key ?secret_key (Printf.sprintf "/services/%s" service_id) with
|
||||
| SudoSuccess _ -> true
|
||||
| _ -> false
|
||||
|
||||
(** Freeze a service *)
|
||||
let service_freeze ?public_key ?secret_key service_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/services/%s/freeze" service_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Unfreeze a service *)
|
||||
let service_unfreeze ?public_key ?secret_key service_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/services/%s/unfreeze" service_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Lock a service *)
|
||||
let service_lock ?public_key ?secret_key service_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/services/%s/lock" service_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Unlock a service *)
|
||||
let service_unlock ?public_key ?secret_key service_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/services/%s/unlock" service_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Set unfreeze-on-demand for a service *)
|
||||
let service_set_unfreeze_on_demand ?public_key ?secret_key service_id enabled =
|
||||
let (pk, sk) = get_credentials ?public_key ?secret_key () in
|
||||
let enabled_str = if enabled then "true" else "false" in
|
||||
let json = Printf.sprintf "{\"unfreeze_on_demand\":%s}" enabled_str in
|
||||
let endpoint = Printf.sprintf "/services/%s" service_id in
|
||||
let auth_headers = build_auth_headers pk sk "PATCH" endpoint json in
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.json" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -X PATCH %s%s -H 'Content-Type: application/json'%s -d @%s"
|
||||
api_base endpoint auth_headers tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let response = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
Sys.remove tmp_file;
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Redeploy a service *)
|
||||
let service_redeploy ?public_key ?secret_key service_id ?bootstrap () =
|
||||
let bootstrap_json = match bootstrap with Some b -> Printf.sprintf "\"bootstrap\":\"%s\"" (escape_json b) | None -> "" in
|
||||
let json = "{" ^ bootstrap_json ^ "}" in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/services/%s/redeploy" service_id) json in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Get service logs *)
|
||||
let service_logs ?public_key ?secret_key ?(all_logs=false) service_id =
|
||||
let endpoint = if all_logs
|
||||
then Printf.sprintf "/services/%s/logs?all=true" service_id
|
||||
else Printf.sprintf "/services/%s/logs" service_id
|
||||
in
|
||||
api_get ?public_key ?secret_key endpoint
|
||||
|
||||
(** Execute a command in a service *)
|
||||
let service_execute ?public_key ?secret_key ?timeout_ms service_id command =
|
||||
let timeout_json = match timeout_ms with Some t -> Printf.sprintf ",\"timeout_ms\":%d" t | None -> "" in
|
||||
let json = Printf.sprintf "{\"command\":\"%s\"%s}" (escape_json command) timeout_json in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/services/%s/execute" service_id) json in
|
||||
let stdout_val = match extract_json_value response "stdout" with Some s -> unescape_json s | None -> "" in
|
||||
let stderr_val = match extract_json_value response "stderr" with Some s -> unescape_json s | None -> "" in
|
||||
let exit_code = match extract_json_int response "exit_code" with Some i -> i | None -> 0 in
|
||||
{ success = (exit_code = 0); stdout = stdout_val; stderr = stderr_val; exit_code; job_id = None }
|
||||
|
||||
(** Get service environment vault *)
|
||||
let service_env_get_lib ?public_key ?secret_key service_id =
|
||||
api_get ?public_key ?secret_key (Printf.sprintf "/services/%s/env" service_id)
|
||||
|
||||
(** Set service environment vault *)
|
||||
let service_env_set_lib ?public_key ?secret_key service_id env_content =
|
||||
let (pk, sk) = get_credentials ?public_key ?secret_key () in
|
||||
let endpoint = Printf.sprintf "/services/%s/env" service_id in
|
||||
let auth_headers = build_auth_headers pk sk "PUT" endpoint env_content in
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.txt" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc env_content;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -o /dev/null -w '%%{http_code}' -X PUT %s%s -H 'Content-Type: text/plain'%s -d @%s"
|
||||
api_base endpoint auth_headers tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let status = try input_line ic with End_of_file -> "0" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
Sys.remove tmp_file;
|
||||
let code = int_of_string (String.trim status) in
|
||||
code >= 200 && code < 300
|
||||
|
||||
(** Delete service environment vault *)
|
||||
let service_env_delete_lib ?public_key ?secret_key service_id =
|
||||
let response = api_delete ?public_key ?secret_key (Printf.sprintf "/services/%s/env" service_id) in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Export service environment vault *)
|
||||
let service_env_export_lib ?public_key ?secret_key service_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/services/%s/env/export" service_id) "{}" in
|
||||
match extract_json_value response "content" with
|
||||
| Some content -> unescape_json content
|
||||
| None -> ""
|
||||
|
||||
(** Resize a service *)
|
||||
let service_resize ?public_key ?secret_key service_id vcpu =
|
||||
let (pk, sk) = get_credentials ?public_key ?secret_key () in
|
||||
let json = Printf.sprintf "{\"vcpu\":%d}" vcpu in
|
||||
let endpoint = Printf.sprintf "/services/%s" service_id in
|
||||
let auth_headers = build_auth_headers pk sk "PATCH" endpoint json in
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.json" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -X PATCH %s%s -H 'Content-Type: application/json'%s -d @%s"
|
||||
api_base endpoint auth_headers tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let response = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
Sys.remove tmp_file;
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(* ============================================================================
|
||||
Library API - Snapshots (9)
|
||||
============================================================================ *)
|
||||
|
||||
(** List all snapshots *)
|
||||
let snapshot_list ?public_key ?secret_key () =
|
||||
api_get ?public_key ?secret_key "/snapshots"
|
||||
|
||||
(** Get snapshot details *)
|
||||
let snapshot_get ?public_key ?secret_key snapshot_id =
|
||||
api_get ?public_key ?secret_key (Printf.sprintf "/snapshots/%s" snapshot_id)
|
||||
|
||||
(** Create a snapshot of a session *)
|
||||
let snapshot_session ?public_key ?secret_key ?name ?(hot=false) session_id =
|
||||
let name_json = match name with Some n -> Printf.sprintf "\"name\":\"%s\"," (escape_json n) | None -> "" in
|
||||
let hot_json = if hot then "\"hot\":true" else "\"hot\":false" in
|
||||
let json = "{" ^ name_json ^ hot_json ^ "}" in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/sessions/%s/snapshot" session_id) json in
|
||||
extract_json_value response "id"
|
||||
|
||||
(** Create a snapshot of a service *)
|
||||
let snapshot_service ?public_key ?secret_key ?name ?(hot=false) service_id =
|
||||
let name_json = match name with Some n -> Printf.sprintf "\"name\":\"%s\"," (escape_json n) | None -> "" in
|
||||
let hot_json = if hot then "\"hot\":true" else "\"hot\":false" in
|
||||
let json = "{" ^ name_json ^ hot_json ^ "}" in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/services/%s/snapshot" service_id) json in
|
||||
extract_json_value response "id"
|
||||
|
||||
(** Restore from a snapshot *)
|
||||
let snapshot_restore ?public_key ?secret_key snapshot_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/snapshots/%s/restore" snapshot_id) "{}" in
|
||||
extract_json_value response "id"
|
||||
|
||||
(** Delete a snapshot *)
|
||||
let snapshot_delete ?public_key ?secret_key snapshot_id =
|
||||
match api_delete_with_sudo ?public_key ?secret_key (Printf.sprintf "/snapshots/%s" snapshot_id) with
|
||||
| SudoSuccess _ -> true
|
||||
| _ -> false
|
||||
|
||||
(** Lock a snapshot *)
|
||||
let snapshot_lock ?public_key ?secret_key snapshot_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/snapshots/%s/lock" snapshot_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Unlock a snapshot *)
|
||||
let snapshot_unlock ?public_key ?secret_key snapshot_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/snapshots/%s/unlock" snapshot_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Clone a snapshot to create a new session or service *)
|
||||
let snapshot_clone ?public_key ?secret_key snapshot_id ~clone_type ?name ?ports ?shell () =
|
||||
let type_json = Printf.sprintf "\"type\":\"%s\"" clone_type in
|
||||
let name_json = match name with Some n -> Printf.sprintf ",\"name\":\"%s\"" (escape_json n) | None -> "" in
|
||||
let ports_json = match ports with Some p -> Printf.sprintf ",\"ports\":[%s]" p | None -> "" in
|
||||
let shell_json = match shell with Some s -> Printf.sprintf ",\"shell\":\"%s\"" s | None -> "" in
|
||||
let json = "{" ^ type_json ^ name_json ^ ports_json ^ shell_json ^ "}" in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/snapshots/%s/clone" snapshot_id) json in
|
||||
extract_json_value response "id"
|
||||
|
||||
(* ============================================================================
|
||||
Library API - Images (13)
|
||||
============================================================================ *)
|
||||
|
||||
(** List images *)
|
||||
let image_list ?public_key ?secret_key ?filter () =
|
||||
let endpoint = match filter with Some f -> Printf.sprintf "/images?filter=%s" f | None -> "/images" in
|
||||
api_get ?public_key ?secret_key endpoint
|
||||
|
||||
(** Get image details *)
|
||||
let image_get ?public_key ?secret_key image_id =
|
||||
api_get ?public_key ?secret_key (Printf.sprintf "/images/%s" image_id)
|
||||
|
||||
(** Publish an image *)
|
||||
let image_publish ?public_key ?secret_key source_type source_id ?name ?description () =
|
||||
let name_json = match name with Some n -> Printf.sprintf ",\"name\":\"%s\"" (escape_json n) | None -> "" in
|
||||
let desc_json = match description with Some d -> Printf.sprintf ",\"description\":\"%s\"" (escape_json d) | None -> "" in
|
||||
let json = Printf.sprintf "{\"source_type\":\"%s\",\"source_id\":\"%s\"%s%s}" source_type source_id name_json desc_json in
|
||||
let response = api_post ?public_key ?secret_key "/images/publish" json in
|
||||
extract_json_value response "id"
|
||||
|
||||
(** Delete an image *)
|
||||
let image_delete ?public_key ?secret_key image_id =
|
||||
match api_delete_with_sudo ?public_key ?secret_key (Printf.sprintf "/images/%s" image_id) with
|
||||
| SudoSuccess _ -> true
|
||||
| _ -> false
|
||||
|
||||
(** Lock an image *)
|
||||
let image_lock ?public_key ?secret_key image_id =
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/images/%s/lock" image_id) "{}" in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Unlock an image *)
|
||||
let image_unlock ?public_key ?secret_key image_id =
|
||||
match api_post_with_sudo ?public_key ?secret_key (Printf.sprintf "/images/%s/unlock" image_id) "{}" with
|
||||
| SudoSuccess _ -> true
|
||||
| _ -> false
|
||||
|
||||
(** Set image visibility *)
|
||||
let image_set_visibility ?public_key ?secret_key image_id visibility =
|
||||
let json = Printf.sprintf "{\"visibility\":\"%s\"}" visibility in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/images/%s/visibility" image_id) json in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Grant access to an image *)
|
||||
let image_grant_access ?public_key ?secret_key image_id trusted_api_key =
|
||||
let json = Printf.sprintf "{\"api_key\":\"%s\"}" trusted_api_key in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/images/%s/access/grant" image_id) json in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Revoke access to an image *)
|
||||
let image_revoke_access ?public_key ?secret_key image_id trusted_api_key =
|
||||
let json = Printf.sprintf "{\"api_key\":\"%s\"}" trusted_api_key in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/images/%s/access/revoke" image_id) json in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** List trusted API keys for an image *)
|
||||
let image_list_trusted ?public_key ?secret_key image_id =
|
||||
api_get ?public_key ?secret_key (Printf.sprintf "/images/%s/access" image_id)
|
||||
|
||||
(** Transfer image ownership *)
|
||||
let image_transfer ?public_key ?secret_key image_id to_api_key =
|
||||
let json = Printf.sprintf "{\"to_api_key\":\"%s\"}" to_api_key in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/images/%s/transfer" image_id) json in
|
||||
not (String.length response > 0 && Str.string_match (Str.regexp ".*\"error\"") response 0)
|
||||
|
||||
(** Spawn a service from an image *)
|
||||
let image_spawn ?public_key ?secret_key image_id ?name ?ports ?bootstrap ?network () =
|
||||
let name_json = match name with Some n -> Printf.sprintf "\"name\":\"%s\"" (escape_json n) | None -> "" in
|
||||
let ports_json = match ports with Some p -> Printf.sprintf "%s\"ports\":[%s]" (if name_json <> "" then "," else "") p | None -> "" in
|
||||
let bootstrap_json = match bootstrap with Some b -> Printf.sprintf ",\"bootstrap\":\"%s\"" (escape_json b) | None -> "" in
|
||||
let network_json = match network with Some n -> Printf.sprintf ",\"network\":\"%s\"" n | None -> "" in
|
||||
let json = "{" ^ name_json ^ ports_json ^ bootstrap_json ^ network_json ^ "}" in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/images/%s/spawn" image_id) json in
|
||||
extract_json_value response "id"
|
||||
|
||||
(** Clone an image *)
|
||||
let image_clone ?public_key ?secret_key image_id ?name ?description () =
|
||||
let name_json = match name with Some n -> Printf.sprintf "\"name\":\"%s\"" (escape_json n) | None -> "" in
|
||||
let desc_json = match description with Some d -> Printf.sprintf "%s\"description\":\"%s\"" (if name_json <> "" then "," else "") (escape_json d) | None -> "" in
|
||||
let json = "{" ^ name_json ^ desc_json ^ "}" in
|
||||
let response = api_post ?public_key ?secret_key (Printf.sprintf "/images/%s/clone" image_id) json in
|
||||
extract_json_value response "id"
|
||||
|
||||
(** Validate API keys *)
|
||||
let validate_keys ?public_key ?secret_key () =
|
||||
portal_post ?public_key ?secret_key "/keys/validate" "{}"
|
||||
|
||||
(* ============================================================================
|
||||
CLI - Legacy curl-based functions for CLI
|
||||
============================================================================ *)
|
||||
|
|
|
|||
190
clients/ocaml/sync/tests/test_functional.ml
Executable file
190
clients/ocaml/sync/tests/test_functional.ml
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/env ocaml
|
||||
|
||||
(*
|
||||
* Functional Tests for Un OCaml SDK
|
||||
*
|
||||
* Run with: ocaml test_functional.ml
|
||||
* Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables
|
||||
*
|
||||
* These tests make real API calls to api.unsandbox.com
|
||||
*)
|
||||
|
||||
(* ANSI colors *)
|
||||
let blue = "\x1b[34m"
|
||||
let red = "\x1b[31m"
|
||||
let green = "\x1b[32m"
|
||||
let yellow = "\x1b[33m"
|
||||
let reset = "\x1b[0m"
|
||||
|
||||
(* API constants *)
|
||||
let api_base = "https://api.unsandbox.com"
|
||||
let portal_base = "https://unsandbox.com"
|
||||
|
||||
(* HMAC-SHA256 using openssl command *)
|
||||
let hmac_sha256 secret message =
|
||||
let cmd = Printf.sprintf "echo -n '%s' | openssl dgst -sha256 -hmac '%s' | awk '{print $2}'"
|
||||
(Str.global_replace (Str.regexp "'") "'\\''" message)
|
||||
(Str.global_replace (Str.regexp "'") "'\\''" secret) in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let result = input_line ic in
|
||||
let _ = Unix.close_process_in ic in
|
||||
String.trim result
|
||||
|
||||
(* Build auth headers *)
|
||||
let build_auth_headers public_key secret_key method_ path body =
|
||||
let timestamp = string_of_int (int_of_float (Unix.time ())) in
|
||||
let message = Printf.sprintf "%s:%s:%s:%s" timestamp method_ path body in
|
||||
let signature = hmac_sha256 secret_key message in
|
||||
Printf.sprintf " -H 'Authorization: Bearer %s' -H 'X-Timestamp: %s' -H 'X-Signature: %s'"
|
||||
public_key timestamp signature
|
||||
|
||||
(* HTTP helpers *)
|
||||
let curl_get public_key secret_key endpoint =
|
||||
let auth_headers = build_auth_headers public_key secret_key "GET" endpoint "" in
|
||||
let cmd = Printf.sprintf "curl -s %s%s%s" api_base endpoint auth_headers in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let output = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
output
|
||||
|
||||
let curl_post public_key secret_key endpoint json =
|
||||
let auth_headers = build_auth_headers public_key secret_key "POST" endpoint json in
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_test_%d.json" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -X POST %s%s -H 'Content-Type: application/json'%s -d @%s"
|
||||
api_base endpoint auth_headers tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let output = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
Sys.remove tmp_file;
|
||||
output
|
||||
|
||||
let curl_post_portal public_key secret_key endpoint json =
|
||||
let auth_headers = build_auth_headers public_key secret_key "POST" endpoint json in
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_test_%d.json" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -X POST %s%s -H 'Content-Type: application/json'%s -d @%s"
|
||||
portal_base endpoint auth_headers tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let output = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
Sys.remove tmp_file;
|
||||
output
|
||||
|
||||
(* String contains helper *)
|
||||
let contains haystack needle =
|
||||
try
|
||||
let _ = Str.search_forward (Str.regexp_string needle) haystack 0 in
|
||||
true
|
||||
with Not_found -> false
|
||||
|
||||
(* Test tracking *)
|
||||
let passed = ref 0
|
||||
let failed = ref 0
|
||||
|
||||
let run_test name test_fn =
|
||||
Printf.printf " Running %s... " name;
|
||||
flush stdout;
|
||||
try
|
||||
test_fn ();
|
||||
Printf.printf "%sPASS%s\n" green reset;
|
||||
incr passed
|
||||
with e ->
|
||||
Printf.printf "%sFAIL%s\n" red reset;
|
||||
Printf.printf " %s\n" (Printexc.to_string e);
|
||||
incr failed
|
||||
|
||||
let assert_true condition message =
|
||||
if not condition then failwith message
|
||||
|
||||
(* ============================================================================
|
||||
Functional Tests
|
||||
============================================================================ *)
|
||||
|
||||
let test_health_check () =
|
||||
let cmd = Printf.sprintf "curl -s -o /dev/null -w '%%{http_code}' %s/health" api_base in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let status = try input_line ic with End_of_file -> "0" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
assert_true (String.trim status = "200") "health check should return 200"
|
||||
|
||||
let test_validate_keys public_key secret_key =
|
||||
let response = curl_post_portal public_key secret_key "/keys/validate" "{}" in
|
||||
assert_true (contains response "\"valid\"" || contains response "\"status\"") "should contain valid or status"
|
||||
|
||||
let test_execute_python public_key secret_key =
|
||||
let json = "{\"language\":\"python\",\"code\":\"print(6 * 7)\"}" in
|
||||
let response = curl_post public_key secret_key "/execute" json in
|
||||
assert_true (contains response "42") "stdout should contain 42"
|
||||
|
||||
let test_execute_with_error public_key secret_key =
|
||||
let json = "{\"language\":\"python\",\"code\":\"import sys; sys.exit(1)\"}" in
|
||||
let response = curl_post public_key secret_key "/execute" json in
|
||||
assert_true (contains response "\"exit_code\":1" || contains response "\"exit_code\": 1") "exit_code should be 1"
|
||||
|
||||
let test_session_list public_key secret_key =
|
||||
let response = curl_get public_key secret_key "/sessions" in
|
||||
let trimmed = String.trim response in
|
||||
assert_true (String.length trimmed > 0 && (trimmed.[0] = '[' || trimmed.[0] = '{')) "response should be JSON"
|
||||
|
||||
let test_service_list public_key secret_key =
|
||||
let response = curl_get public_key secret_key "/services" in
|
||||
let trimmed = String.trim response in
|
||||
assert_true (String.length trimmed > 0 && (trimmed.[0] = '[' || trimmed.[0] = '{')) "response should be JSON"
|
||||
|
||||
let test_snapshot_list public_key secret_key =
|
||||
let response = curl_get public_key secret_key "/snapshots" in
|
||||
let trimmed = String.trim response in
|
||||
assert_true (String.length trimmed > 0 && (trimmed.[0] = '[' || trimmed.[0] = '{')) "response should be JSON"
|
||||
|
||||
let test_image_list public_key secret_key =
|
||||
let response = curl_get public_key secret_key "/images" in
|
||||
let trimmed = String.trim response in
|
||||
assert_true (String.length trimmed > 0 && (trimmed.[0] = '[' || trimmed.[0] = '{')) "response should be JSON"
|
||||
|
||||
let () =
|
||||
Random.self_init ();
|
||||
Printf.printf "\n%s=== Un OCaml SDK Functional Tests ===%s\n\n" blue reset;
|
||||
|
||||
(* Check for credentials *)
|
||||
let public_key = try Some (Sys.getenv "UNSANDBOX_PUBLIC_KEY") with Not_found -> None in
|
||||
let secret_key = try Some (Sys.getenv "UNSANDBOX_SECRET_KEY") with Not_found -> None in
|
||||
|
||||
match (public_key, secret_key) with
|
||||
| (Some pk, Some sk) ->
|
||||
run_test "health_check" test_health_check;
|
||||
run_test "validate_keys" (fun () -> test_validate_keys pk sk);
|
||||
run_test "execute_python" (fun () -> test_execute_python pk sk);
|
||||
run_test "execute_with_error" (fun () -> test_execute_with_error pk sk);
|
||||
run_test "session_list" (fun () -> test_session_list pk sk);
|
||||
run_test "service_list" (fun () -> test_service_list pk sk);
|
||||
run_test "snapshot_list" (fun () -> test_snapshot_list pk sk);
|
||||
run_test "image_list" (fun () -> test_image_list pk sk);
|
||||
|
||||
let total = !passed + !failed in
|
||||
Printf.printf "\n%sResults: %d/%d passed%s\n" blue !passed total reset;
|
||||
|
||||
if !failed > 0 then begin
|
||||
Printf.printf "%s%d test(s) failed%s\n" red !failed reset;
|
||||
exit 1
|
||||
end else
|
||||
Printf.printf "%sAll functional tests passed!%s\n" green reset
|
||||
|
||||
| _ ->
|
||||
Printf.printf "%sSKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s\n" yellow reset
|
||||
160
clients/ocaml/sync/tests/test_library.ml
Executable file
160
clients/ocaml/sync/tests/test_library.ml
Executable file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env ocaml
|
||||
|
||||
(*
|
||||
* Unit Tests for Un OCaml SDK Library Functions
|
||||
*
|
||||
* Run with: ocaml test_library.ml
|
||||
* No credentials required - tests pure library functions only.
|
||||
*)
|
||||
|
||||
(* ANSI colors *)
|
||||
let blue = "\x1b[34m"
|
||||
let red = "\x1b[31m"
|
||||
let green = "\x1b[32m"
|
||||
let yellow = "\x1b[33m"
|
||||
let reset = "\x1b[0m"
|
||||
|
||||
(* Inline implementation for testing (matches un.ml) *)
|
||||
let sdk_version = "4.2.0"
|
||||
|
||||
let ext_to_lang ext =
|
||||
match ext with
|
||||
| ".hs" -> Some "haskell" | ".ml" -> Some "ocaml" | ".clj" -> Some "clojure"
|
||||
| ".scm" -> Some "scheme" | ".lisp" -> Some "commonlisp" | ".erl" -> Some "erlang"
|
||||
| ".ex" -> Some "elixir" | ".exs" -> Some "elixir" | ".py" -> Some "python"
|
||||
| ".js" -> Some "javascript" | ".ts" -> Some "typescript" | ".rb" -> Some "ruby"
|
||||
| ".go" -> Some "go" | ".rs" -> Some "rust" | ".c" -> Some "c"
|
||||
| ".cpp" -> Some "cpp" | ".cc" -> Some "cpp" | ".cxx" -> Some "cpp"
|
||||
| ".java" -> Some "java" | ".kt" -> Some "kotlin" | ".cs" -> Some "csharp"
|
||||
| ".fs" -> Some "fsharp" | ".jl" -> Some "julia" | ".r" -> Some "r"
|
||||
| ".cr" -> Some "crystal" | ".d" -> Some "d" | ".nim" -> Some "nim"
|
||||
| ".zig" -> Some "zig" | ".v" -> Some "v" | ".dart" -> Some "dart"
|
||||
| ".groovy" -> Some "groovy" | ".scala" -> Some "scala"
|
||||
| ".sh" -> Some "bash" | ".pl" -> Some "perl" | ".lua" -> Some "lua"
|
||||
| ".php" -> Some "php"
|
||||
| _ -> None
|
||||
|
||||
let get_extension filename =
|
||||
try
|
||||
let dot_pos = String.rindex filename '.' in
|
||||
String.sub filename dot_pos (String.length filename - dot_pos)
|
||||
with Not_found -> ""
|
||||
|
||||
let detect_language filename =
|
||||
let ext = get_extension filename in
|
||||
ext_to_lang ext
|
||||
|
||||
(* HMAC-SHA256 using openssl command *)
|
||||
let hmac_sha256 secret message =
|
||||
let cmd = Printf.sprintf "echo -n '%s' | openssl dgst -sha256 -hmac '%s' | awk '{print $2}'"
|
||||
(Str.global_replace (Str.regexp "'") "'\\''" message)
|
||||
(Str.global_replace (Str.regexp "'") "'\\''" secret) in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let result = input_line ic in
|
||||
let _ = Unix.close_process_in ic in
|
||||
String.trim result
|
||||
|
||||
let hmac_sign secret message = hmac_sha256 secret message
|
||||
|
||||
(* Test tracking *)
|
||||
let passed = ref 0
|
||||
let failed = ref 0
|
||||
|
||||
let run_test name test_fn =
|
||||
try
|
||||
test_fn ();
|
||||
Printf.printf "%sPASS%s: %s\n" green reset name;
|
||||
incr passed
|
||||
with e ->
|
||||
Printf.printf "%sFAIL%s: %s - %s\n" red reset name (Printexc.to_string e);
|
||||
incr failed
|
||||
|
||||
let assert_true condition message =
|
||||
if not condition then failwith message
|
||||
|
||||
let assert_equal a b message =
|
||||
if a <> b then failwith (message ^ " (got: " ^ a ^ ")")
|
||||
|
||||
let assert_some opt message =
|
||||
match opt with
|
||||
| Some _ -> ()
|
||||
| None -> failwith message
|
||||
|
||||
let assert_none opt message =
|
||||
match opt with
|
||||
| None -> ()
|
||||
| Some _ -> failwith message
|
||||
|
||||
(* ============================================================================
|
||||
Unit Tests
|
||||
============================================================================ *)
|
||||
|
||||
let test_version () =
|
||||
let version = sdk_version in
|
||||
assert_true (String.length version > 0) "version should be non-empty";
|
||||
(* Check semver format X.Y.Z *)
|
||||
let parts = String.split_on_char '.' version in
|
||||
assert_true (List.length parts = 3) "version should be semver format"
|
||||
|
||||
let test_detect_language () =
|
||||
(* Test common extensions *)
|
||||
assert_equal (match detect_language "script.py" with Some s -> s | None -> "") "python" "python";
|
||||
assert_equal (match detect_language "app.js" with Some s -> s | None -> "") "javascript" "javascript";
|
||||
assert_equal (match detect_language "main.go" with Some s -> s | None -> "") "go" "go";
|
||||
assert_equal (match detect_language "main.rs" with Some s -> s | None -> "") "rust" "rust";
|
||||
assert_equal (match detect_language "main.c" with Some s -> s | None -> "") "c" "c";
|
||||
assert_equal (match detect_language "main.cpp" with Some s -> s | None -> "") "cpp" "cpp";
|
||||
assert_equal (match detect_language "Main.java" with Some s -> s | None -> "") "java" "java";
|
||||
assert_equal (match detect_language "script.rb" with Some s -> s | None -> "") "ruby" "ruby";
|
||||
assert_equal (match detect_language "script.sh" with Some s -> s | None -> "") "bash" "bash";
|
||||
assert_equal (match detect_language "script.lua" with Some s -> s | None -> "") "lua" "lua";
|
||||
assert_equal (match detect_language "script.pl" with Some s -> s | None -> "") "perl" "perl";
|
||||
assert_equal (match detect_language "index.php" with Some s -> s | None -> "") "php" "php";
|
||||
assert_equal (match detect_language "main.hs" with Some s -> s | None -> "") "haskell" "haskell";
|
||||
assert_equal (match detect_language "main.ml" with Some s -> s | None -> "") "ocaml" "ocaml";
|
||||
assert_equal (match detect_language "main.ex" with Some s -> s | None -> "") "elixir" "elixir";
|
||||
assert_equal (match detect_language "main.erl" with Some s -> s | None -> "") "erlang" "erlang";
|
||||
|
||||
(* Test with paths *)
|
||||
assert_equal (match detect_language "/path/to/script.py" with Some s -> s | None -> "") "python" "path/python";
|
||||
|
||||
(* Test unknown extensions *)
|
||||
assert_none (detect_language "Makefile") "Makefile should be None";
|
||||
assert_none (detect_language "README") "README should be None";
|
||||
assert_none (detect_language "script.unknown") "unknown should be None"
|
||||
|
||||
let test_hmac_sign () =
|
||||
let signature = hmac_sign "my_secret" "test message" in
|
||||
assert_true (String.length signature = 64) "signature should be 64 hex characters";
|
||||
(* Should be lowercase hex *)
|
||||
let is_hex_char c = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') in
|
||||
let all_hex = String.to_seq signature |> Seq.for_all is_hex_char in
|
||||
assert_true all_hex "signature should be lowercase hex"
|
||||
|
||||
let test_hmac_sign_deterministic () =
|
||||
let sig1 = hmac_sign "test_secret" "same message" in
|
||||
let sig2 = hmac_sign "test_secret" "same message" in
|
||||
assert_equal sig1 sig2 "same inputs should produce same signature"
|
||||
|
||||
let test_hmac_sign_different_secrets () =
|
||||
let sig1 = hmac_sign "secret1" "test message" in
|
||||
let sig2 = hmac_sign "secret2" "test message" in
|
||||
assert_true (sig1 <> sig2) "different secrets should produce different signatures"
|
||||
|
||||
let () =
|
||||
Printf.printf "\n%s=== Un OCaml SDK Library Tests ===%s\n\n" blue reset;
|
||||
|
||||
run_test "version" test_version;
|
||||
run_test "detect_language" test_detect_language;
|
||||
run_test "hmac_sign" test_hmac_sign;
|
||||
run_test "hmac_sign_deterministic" test_hmac_sign_deterministic;
|
||||
run_test "hmac_sign_different_secrets" test_hmac_sign_different_secrets;
|
||||
|
||||
let total = !passed + !failed in
|
||||
Printf.printf "\n%sResults: %d/%d passed%s\n" blue !passed total reset;
|
||||
|
||||
if !failed > 0 then begin
|
||||
Printf.printf "%s%d test(s) failed%s\n" red !failed reset;
|
||||
exit 1
|
||||
end else
|
||||
Printf.printf "%sAll tests passed!%s\n" green reset
|
||||
File diff suppressed because it is too large
Load diff
239
clients/perl/tests/test_library.pl
Normal file
239
clients/perl/tests/test_library.pl
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env perl
|
||||
# Unit Tests for un.pl Library Functions
|
||||
#
|
||||
# Tests the ACTUAL exported functions from Un package.
|
||||
# NO local re-implementations. NO mocking.
|
||||
#
|
||||
# Run: perl tests/test_library.pl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use FindBin qw($Bin);
|
||||
use lib "$Bin/../sync/src";
|
||||
|
||||
# Load the Un module
|
||||
require "$Bin/../sync/src/un.pl";
|
||||
|
||||
# Test counters
|
||||
my $tests_passed = 0;
|
||||
my $tests_failed = 0;
|
||||
|
||||
# ============================================================================
|
||||
# Test: Un::version()
|
||||
# ============================================================================
|
||||
|
||||
subtest 'Un::version()' => sub {
|
||||
my $version = Un::version();
|
||||
ok(defined $version, 'version() returns defined value');
|
||||
ok(length($version) > 0, 'version() returns non-empty string');
|
||||
like($version, qr/^\d+\.\d+\.\d+$/, 'version() matches X.Y.Z format');
|
||||
diag("Version: $version");
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# Test: Un::detect_language()
|
||||
# ============================================================================
|
||||
|
||||
subtest 'Un::detect_language()' => sub {
|
||||
my @tests = (
|
||||
['test.py', 'python'],
|
||||
['app.js', 'javascript'],
|
||||
['main.go', 'go'],
|
||||
['script.rb', 'ruby'],
|
||||
['lib.rs', 'rust'],
|
||||
['main.c', 'c'],
|
||||
['app.cpp', 'cpp'],
|
||||
['Main.java', 'java'],
|
||||
['index.php', 'php'],
|
||||
['script.pl', 'perl'],
|
||||
['init.lua', 'lua'],
|
||||
['run.sh', 'bash'],
|
||||
['main.ts', 'typescript'],
|
||||
['app.kt', 'kotlin'],
|
||||
['lib.ex', 'elixir'],
|
||||
['main.hs', 'haskell'],
|
||||
);
|
||||
|
||||
for my $test (@tests) {
|
||||
my ($file, $expected) = @$test;
|
||||
my $result = Un::detect_language($file);
|
||||
is($result, $expected, "detect_language('$file') -> '$expected'");
|
||||
}
|
||||
|
||||
# Test NULL handling
|
||||
my $null_result = Un::detect_language(undef);
|
||||
ok(!defined $null_result, 'detect_language(undef) returns undef');
|
||||
|
||||
# Test unknown extension
|
||||
my $unknown = Un::detect_language('file.xyz123');
|
||||
ok(!defined $unknown, 'detect_language(unknown ext) returns undef');
|
||||
|
||||
# Test no extension
|
||||
my $noext = Un::detect_language('Makefile');
|
||||
ok(!defined $noext, 'detect_language(no ext) returns undef');
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# Test: Un::hmac_sign()
|
||||
# ============================================================================
|
||||
|
||||
subtest 'Un::hmac_sign()' => sub {
|
||||
# Test basic signature generation
|
||||
my $sig = Un::hmac_sign('secret_key', '1234567890:POST:/execute:{}');
|
||||
ok(defined $sig, 'hmac_sign() returns defined value');
|
||||
is(length($sig), 64, 'hmac_sign() returns 64-char hex string');
|
||||
|
||||
# Verify hex characters
|
||||
like($sig, qr/^[0-9a-fA-F]+$/, 'hmac_sign() returns valid hex');
|
||||
|
||||
# Test deterministic output
|
||||
my $sig1 = Un::hmac_sign('key', 'message');
|
||||
my $sig2 = Un::hmac_sign('key', 'message');
|
||||
is($sig1, $sig2, 'hmac_sign() is deterministic');
|
||||
|
||||
# Test different keys produce different signatures
|
||||
my $sig_a = Un::hmac_sign('key_a', 'message');
|
||||
my $sig_b = Un::hmac_sign('key_b', 'message');
|
||||
isnt($sig_a, $sig_b, 'Different keys produce different signatures');
|
||||
|
||||
# Test different messages produce different signatures
|
||||
my $sig_m1 = Un::hmac_sign('key', 'message1');
|
||||
my $sig_m2 = Un::hmac_sign('key', 'message2');
|
||||
isnt($sig_m1, $sig_m2, 'Different messages produce different signatures');
|
||||
|
||||
# Test NULL handling
|
||||
my $null_key = Un::hmac_sign(undef, 'message');
|
||||
ok(!defined $null_key, 'hmac_sign(undef, msg) returns undef');
|
||||
|
||||
my $null_msg = Un::hmac_sign('key', undef);
|
||||
ok(!defined $null_msg, 'hmac_sign(key, undef) returns undef');
|
||||
|
||||
# Test known HMAC value
|
||||
my $known_sig = Un::hmac_sign('key', 'message');
|
||||
like($known_sig, qr/^6e9ef29b75fffc5b7abae527d58fdadb/,
|
||||
'HMAC-SHA256("key", "message") matches expected prefix');
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# Test: Un::last_error()
|
||||
# ============================================================================
|
||||
|
||||
subtest 'Un::last_error()' => sub {
|
||||
# Initially should be empty or previous error
|
||||
my $error = Un::last_error();
|
||||
ok(defined $error, 'last_error() returns defined value');
|
||||
|
||||
# Set an error and check
|
||||
Un::set_error('test error');
|
||||
is(Un::last_error(), 'test error', 'last_error() returns set error');
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# Test: Memory stress test
|
||||
# ============================================================================
|
||||
|
||||
subtest 'Memory stress test' => sub {
|
||||
# Stress test HMAC allocation
|
||||
for my $i (0..999) {
|
||||
my $sig = Un::hmac_sign('key', 'message');
|
||||
}
|
||||
pass('1000 HMAC calls without crash');
|
||||
|
||||
# Stress test language detection
|
||||
for my $i (0..999) {
|
||||
Un::detect_language('test.py');
|
||||
}
|
||||
pass('1000 detect_language calls without crash');
|
||||
|
||||
# Stress test version
|
||||
for my $i (0..999) {
|
||||
Un::version();
|
||||
}
|
||||
pass('1000 version calls without crash');
|
||||
};
|
||||
|
||||
# ============================================================================
|
||||
# Test: Function existence
|
||||
# ============================================================================
|
||||
|
||||
subtest 'Library function existence' => sub {
|
||||
# Execution functions (8)
|
||||
can_ok('Un', 'execute');
|
||||
can_ok('Un', 'execute_async');
|
||||
can_ok('Un', 'wait_job');
|
||||
can_ok('Un', 'get_job');
|
||||
can_ok('Un', 'cancel_job');
|
||||
can_ok('Un', 'list_jobs');
|
||||
can_ok('Un', 'get_languages');
|
||||
can_ok('Un', 'detect_language');
|
||||
|
||||
# Session functions (9)
|
||||
can_ok('Un', 'session_list');
|
||||
can_ok('Un', 'session_get');
|
||||
can_ok('Un', 'session_create');
|
||||
can_ok('Un', 'session_destroy');
|
||||
can_ok('Un', 'session_freeze');
|
||||
can_ok('Un', 'session_unfreeze');
|
||||
can_ok('Un', 'session_boost');
|
||||
can_ok('Un', 'session_unboost');
|
||||
can_ok('Un', 'session_execute');
|
||||
|
||||
# Service functions (17)
|
||||
can_ok('Un', 'service_list');
|
||||
can_ok('Un', 'service_get');
|
||||
can_ok('Un', 'service_create');
|
||||
can_ok('Un', 'service_destroy');
|
||||
can_ok('Un', 'service_freeze');
|
||||
can_ok('Un', 'service_unfreeze');
|
||||
can_ok('Un', 'service_lock');
|
||||
can_ok('Un', 'service_unlock');
|
||||
can_ok('Un', 'service_set_unfreeze_on_demand');
|
||||
can_ok('Un', 'service_redeploy');
|
||||
can_ok('Un', 'service_logs');
|
||||
can_ok('Un', 'service_execute');
|
||||
can_ok('Un', 'service_env_get');
|
||||
can_ok('Un', 'service_env_set');
|
||||
can_ok('Un', 'service_env_delete');
|
||||
can_ok('Un', 'service_env_export');
|
||||
can_ok('Un', 'service_resize');
|
||||
|
||||
# Snapshot functions (9)
|
||||
can_ok('Un', 'snapshot_list');
|
||||
can_ok('Un', 'snapshot_get');
|
||||
can_ok('Un', 'snapshot_session');
|
||||
can_ok('Un', 'snapshot_service');
|
||||
can_ok('Un', 'snapshot_restore');
|
||||
can_ok('Un', 'snapshot_delete');
|
||||
can_ok('Un', 'snapshot_lock');
|
||||
can_ok('Un', 'snapshot_unlock');
|
||||
can_ok('Un', 'snapshot_clone');
|
||||
|
||||
# Image functions (13)
|
||||
can_ok('Un', 'image_list');
|
||||
can_ok('Un', 'image_get');
|
||||
can_ok('Un', 'image_publish');
|
||||
can_ok('Un', 'image_delete');
|
||||
can_ok('Un', 'image_lock');
|
||||
can_ok('Un', 'image_unlock');
|
||||
can_ok('Un', 'image_set_visibility');
|
||||
can_ok('Un', 'image_grant_access');
|
||||
can_ok('Un', 'image_revoke_access');
|
||||
can_ok('Un', 'image_list_trusted');
|
||||
can_ok('Un', 'image_transfer');
|
||||
can_ok('Un', 'image_spawn');
|
||||
can_ok('Un', 'image_clone');
|
||||
|
||||
# PaaS Logs (2)
|
||||
can_ok('Un', 'logs_fetch');
|
||||
can_ok('Un', 'logs_stream');
|
||||
|
||||
# Utilities
|
||||
can_ok('Un', 'validate_keys');
|
||||
can_ok('Un', 'hmac_sign');
|
||||
can_ok('Un', 'health_check');
|
||||
can_ok('Un', 'version');
|
||||
can_ok('Un', 'last_error');
|
||||
};
|
||||
|
||||
done_testing();
|
||||
|
|
@ -413,6 +413,29 @@ class Unsandbox {
|
|||
return $response['snapshots'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of a specific snapshot.
|
||||
*
|
||||
* @param string $snapshotId Snapshot ID to get details for
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Snapshot details array containing:
|
||||
* - id: Snapshot ID
|
||||
* - name: Snapshot name
|
||||
* - type: "session" or "service"
|
||||
* - source_id: Original resource ID
|
||||
* - hot: Whether snapshot preserves running state
|
||||
* - locked: Whether snapshot is locked
|
||||
* - created_at: Creation timestamp
|
||||
* - size_bytes: Size in bytes
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function getSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('GET', "/snapshots/{$snapshotId}", $publicKey, $secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a snapshot.
|
||||
*
|
||||
|
|
@ -1360,6 +1383,22 @@ class Unsandbox {
|
|||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a service's vCPU allocation.
|
||||
*
|
||||
* @param string $serviceId Service ID to resize
|
||||
* @param int $vcpu Number of vCPUs (1-8 typically)
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Response array with updated service info
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function resizeService(string $serviceId, int $vcpu, ?string $publicKey = null, ?string $secretKey = null): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, ['vcpu' => $vcpu]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Key Validation
|
||||
// =========================================================================
|
||||
|
|
@ -1420,6 +1459,172 @@ class Unsandbox {
|
|||
return $this->makeRequest('POST', '/image', $publicKey, $secretKey, $payload);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PaaS Logs Functions
|
||||
// =========================================================================
|
||||
|
||||
private static ?string $lastError = null;
|
||||
|
||||
/**
|
||||
* Fetch batch logs from the PaaS platform.
|
||||
*
|
||||
* @param string $source Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
* @param int $lines Number of lines to fetch (1-10000)
|
||||
* @param string $since Time window - "1m", "5m", "1h", "1d"
|
||||
* @param string|null $grep Optional filter pattern
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @return array Log entries
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function logsFetch(
|
||||
string $source = 'all',
|
||||
int $lines = 100,
|
||||
string $since = '5m',
|
||||
?string $grep = null,
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): array {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
$path = "/logs?source=" . urlencode($source) . "&lines={$lines}&since=" . urlencode($since);
|
||||
if ($grep !== null) {
|
||||
$path .= "&grep=" . urlencode($grep);
|
||||
}
|
||||
return $this->makeRequest('GET', $path, $publicKey, $secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream logs via Server-Sent Events.
|
||||
*
|
||||
* Blocks until interrupted or server closes connection.
|
||||
*
|
||||
* @param string $source Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
* @param string|null $grep Optional filter pattern
|
||||
* @param callable|null $callback Function called for each log line (signature: callback(source, line))
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @throws ApiException API request failed
|
||||
*/
|
||||
public function logsStream(
|
||||
string $source = 'all',
|
||||
?string $grep = null,
|
||||
?callable $callback = null,
|
||||
?string $publicKey = null,
|
||||
?string $secretKey = null
|
||||
): void {
|
||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||
|
||||
$path = "/logs/stream?source=" . urlencode($source);
|
||||
if ($grep !== null) {
|
||||
$path .= "&grep=" . urlencode($grep);
|
||||
}
|
||||
|
||||
$timestamp = time();
|
||||
$signature = $this->signRequest($secretKey, $timestamp, 'GET', $path);
|
||||
|
||||
$ch = curl_init(self::API_BASE . $path);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => false,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"Authorization: Bearer {$publicKey}",
|
||||
"X-Timestamp: {$timestamp}",
|
||||
"X-Signature: {$signature}",
|
||||
"Accept: text/event-stream",
|
||||
],
|
||||
CURLOPT_WRITEFUNCTION => function($ch, $data) use ($source, $callback) {
|
||||
$lines = explode("\n", $data);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos($line, 'data: ') === 0) {
|
||||
$json = substr($line, 6);
|
||||
$entry = @json_decode($json, true);
|
||||
$entrySource = $entry['source'] ?? $source;
|
||||
$entryLine = $entry['line'] ?? $json;
|
||||
|
||||
if ($callback !== null) {
|
||||
$callback($entrySource, $entryLine);
|
||||
} else {
|
||||
echo "[{$entrySource}] {$entryLine}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return strlen($data);
|
||||
},
|
||||
]);
|
||||
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Utility Functions
|
||||
// =========================================================================
|
||||
|
||||
public const SDK_VERSION = '4.2.0';
|
||||
|
||||
/**
|
||||
* Get the SDK version string.
|
||||
*
|
||||
* @return string Version string (e.g., "4.2.0")
|
||||
*/
|
||||
public static function version(): string {
|
||||
return self::SDK_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the API is healthy and responding.
|
||||
*
|
||||
* @return bool true if API is healthy, false otherwise
|
||||
*/
|
||||
public static function healthCheck(): bool {
|
||||
$ch = curl_init(self::API_BASE . '/health');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
self::$lastError = "Health check failed: {$error}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
self::$lastError = "Health check failed: HTTP {$httpCode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last error message.
|
||||
*
|
||||
* @return string|null Last error message or null
|
||||
*/
|
||||
public static function lastError(): ?string {
|
||||
return self::$lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a message using HMAC-SHA256.
|
||||
*
|
||||
* This is the underlying signing function used for request authentication.
|
||||
* Exposed for testing and debugging purposes.
|
||||
*
|
||||
* @param string $secretKey The secret key for signing
|
||||
* @param string $message The message to sign
|
||||
* @return string 64-character lowercase hex string
|
||||
*/
|
||||
public static function hmacSign(string $secretKey, string $message): string {
|
||||
return hash_hmac('sha256', $message, $secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to ~/.unsandbox directory, creating if necessary.
|
||||
*
|
||||
|
|
|
|||
298
clients/php/sync/tests/NewFunctionsTest.php
Normal file
298
clients/php/sync/tests/NewFunctionsTest.php
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
/**
|
||||
* Tests for new SDK functions (feature parity with C implementation)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsandbox\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once __DIR__ . '/../src/un.php';
|
||||
|
||||
use Unsandbox\Unsandbox;
|
||||
|
||||
class NewFunctionsTest extends TestCase
|
||||
{
|
||||
private Unsandbox $client;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->client = new Unsandbox();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Utility Functions Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testVersionReturnsString(): void
|
||||
{
|
||||
$v = Unsandbox::version();
|
||||
$this->assertIsString($v);
|
||||
$this->assertNotEmpty($v);
|
||||
}
|
||||
|
||||
public function testVersionIsSemanticFormat(): void
|
||||
{
|
||||
$v = Unsandbox::version();
|
||||
$parts = explode('.', $v);
|
||||
$this->assertGreaterThanOrEqual(2, count($parts), "Version should be semantic: $v");
|
||||
}
|
||||
|
||||
public function testHmacSignReturns64HexChars(): void
|
||||
{
|
||||
$signature = Unsandbox::hmacSign('secret_key', 'message_to_sign');
|
||||
$this->assertIsString($signature);
|
||||
$this->assertEquals(64, strlen($signature));
|
||||
$this->assertMatchesRegularExpression('/^[0-9a-f]+$/', $signature);
|
||||
}
|
||||
|
||||
public function testHmacSignIsDeterministic(): void
|
||||
{
|
||||
$sig1 = Unsandbox::hmacSign('secret', 'message');
|
||||
$sig2 = Unsandbox::hmacSign('secret', 'message');
|
||||
$this->assertEquals($sig1, $sig2);
|
||||
}
|
||||
|
||||
public function testHmacSignDifferentSecretsDifferentSignatures(): void
|
||||
{
|
||||
$sig1 = Unsandbox::hmacSign('secret1', 'message');
|
||||
$sig2 = Unsandbox::hmacSign('secret2', 'message');
|
||||
$this->assertNotEquals($sig1, $sig2);
|
||||
}
|
||||
|
||||
public function testHmacSignDifferentMessagesDifferentSignatures(): void
|
||||
{
|
||||
$sig1 = Unsandbox::hmacSign('secret', 'message1');
|
||||
$sig2 = Unsandbox::hmacSign('secret', 'message2');
|
||||
$this->assertNotEquals($sig1, $sig2);
|
||||
}
|
||||
|
||||
public function testHmacSignMatchesPhpHashHmac(): void
|
||||
{
|
||||
$secret = 'test_secret';
|
||||
$message = '1234567890:POST:/execute:';
|
||||
$signature = Unsandbox::hmacSign($secret, $message);
|
||||
$expected = hash_hmac('sha256', $message, $secret);
|
||||
$this->assertEquals($expected, $signature);
|
||||
}
|
||||
|
||||
public function testLastErrorReturnsNullOrString(): void
|
||||
{
|
||||
$error = Unsandbox::lastError();
|
||||
$this->assertTrue($error === null || is_string($error));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Method Exports Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testExecutionMethodsExist(): void
|
||||
{
|
||||
// 8 execution methods
|
||||
$this->assertTrue(method_exists($this->client, 'executeCode'));
|
||||
$this->assertTrue(method_exists($this->client, 'executeAsync'));
|
||||
$this->assertTrue(method_exists($this->client, 'getJob'));
|
||||
$this->assertTrue(method_exists($this->client, 'waitForJob'));
|
||||
$this->assertTrue(method_exists($this->client, 'cancelJob'));
|
||||
$this->assertTrue(method_exists($this->client, 'listJobs'));
|
||||
$this->assertTrue(method_exists($this->client, 'getLanguages'));
|
||||
$this->assertTrue(method_exists(Unsandbox::class, 'detectLanguage'));
|
||||
}
|
||||
|
||||
public function testSessionMethodsExist(): void
|
||||
{
|
||||
// 9 session methods
|
||||
$this->assertTrue(method_exists($this->client, 'listSessions'));
|
||||
$this->assertTrue(method_exists($this->client, 'getSession'));
|
||||
$this->assertTrue(method_exists($this->client, 'createSession'));
|
||||
$this->assertTrue(method_exists($this->client, 'deleteSession'));
|
||||
$this->assertTrue(method_exists($this->client, 'freezeSession'));
|
||||
$this->assertTrue(method_exists($this->client, 'unfreezeSession'));
|
||||
$this->assertTrue(method_exists($this->client, 'boostSession'));
|
||||
$this->assertTrue(method_exists($this->client, 'unboostSession'));
|
||||
$this->assertTrue(method_exists($this->client, 'shellSession'));
|
||||
}
|
||||
|
||||
public function testServiceMethodsExist(): void
|
||||
{
|
||||
// 17 service methods
|
||||
$this->assertTrue(method_exists($this->client, 'listServices'));
|
||||
$this->assertTrue(method_exists($this->client, 'createService'));
|
||||
$this->assertTrue(method_exists($this->client, 'getService'));
|
||||
$this->assertTrue(method_exists($this->client, 'updateService'));
|
||||
$this->assertTrue(method_exists($this->client, 'deleteService'));
|
||||
$this->assertTrue(method_exists($this->client, 'freezeService'));
|
||||
$this->assertTrue(method_exists($this->client, 'unfreezeService'));
|
||||
$this->assertTrue(method_exists($this->client, 'lockService'));
|
||||
$this->assertTrue(method_exists($this->client, 'unlockService'));
|
||||
$this->assertTrue(method_exists($this->client, 'setUnfreezeOnDemand'));
|
||||
$this->assertTrue(method_exists($this->client, 'getServiceLogs'));
|
||||
$this->assertTrue(method_exists($this->client, 'getServiceEnv'));
|
||||
$this->assertTrue(method_exists($this->client, 'setServiceEnv'));
|
||||
$this->assertTrue(method_exists($this->client, 'deleteServiceEnv'));
|
||||
$this->assertTrue(method_exists($this->client, 'exportServiceEnv'));
|
||||
$this->assertTrue(method_exists($this->client, 'redeployService'));
|
||||
$this->assertTrue(method_exists($this->client, 'executeInService'));
|
||||
$this->assertTrue(method_exists($this->client, 'resizeService'), 'resizeService should exist (NEW)');
|
||||
}
|
||||
|
||||
public function testSnapshotMethodsExist(): void
|
||||
{
|
||||
// 9 snapshot methods
|
||||
$this->assertTrue(method_exists($this->client, 'sessionSnapshot'));
|
||||
$this->assertTrue(method_exists($this->client, 'serviceSnapshot'));
|
||||
$this->assertTrue(method_exists($this->client, 'listSnapshots'));
|
||||
$this->assertTrue(method_exists($this->client, 'getSnapshot'), 'getSnapshot should exist (NEW)');
|
||||
$this->assertTrue(method_exists($this->client, 'restoreSnapshot'));
|
||||
$this->assertTrue(method_exists($this->client, 'deleteSnapshot'));
|
||||
$this->assertTrue(method_exists($this->client, 'lockSnapshot'));
|
||||
$this->assertTrue(method_exists($this->client, 'unlockSnapshot'));
|
||||
$this->assertTrue(method_exists($this->client, 'cloneSnapshot'));
|
||||
}
|
||||
|
||||
public function testImageMethodsExist(): void
|
||||
{
|
||||
// 13 image methods
|
||||
$this->assertTrue(method_exists($this->client, 'imagePublish'));
|
||||
$this->assertTrue(method_exists($this->client, 'listImages'));
|
||||
$this->assertTrue(method_exists($this->client, 'getImage'));
|
||||
$this->assertTrue(method_exists($this->client, 'deleteImage'));
|
||||
$this->assertTrue(method_exists($this->client, 'lockImage'));
|
||||
$this->assertTrue(method_exists($this->client, 'unlockImage'));
|
||||
$this->assertTrue(method_exists($this->client, 'setImageVisibility'));
|
||||
$this->assertTrue(method_exists($this->client, 'grantImageAccess'));
|
||||
$this->assertTrue(method_exists($this->client, 'revokeImageAccess'));
|
||||
$this->assertTrue(method_exists($this->client, 'listImageTrusted'));
|
||||
$this->assertTrue(method_exists($this->client, 'transferImage'));
|
||||
$this->assertTrue(method_exists($this->client, 'spawnFromImage'));
|
||||
$this->assertTrue(method_exists($this->client, 'cloneImage'));
|
||||
}
|
||||
|
||||
public function testLogsMethodsExist(): void
|
||||
{
|
||||
// 2 PaaS logs methods (NEW)
|
||||
$this->assertTrue(method_exists($this->client, 'logsFetch'), 'logsFetch should exist (NEW)');
|
||||
$this->assertTrue(method_exists($this->client, 'logsStream'), 'logsStream should exist (NEW)');
|
||||
}
|
||||
|
||||
public function testUtilityMethodsExist(): void
|
||||
{
|
||||
$this->assertTrue(method_exists($this->client, 'validateKeys'));
|
||||
$this->assertTrue(method_exists(Unsandbox::class, 'version'), 'version should exist (NEW)');
|
||||
$this->assertTrue(method_exists(Unsandbox::class, 'healthCheck'), 'healthCheck should exist (NEW)');
|
||||
$this->assertTrue(method_exists(Unsandbox::class, 'lastError'), 'lastError should exist (NEW)');
|
||||
$this->assertTrue(method_exists(Unsandbox::class, 'hmacSign'), 'hmacSign should exist (NEW)');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Language Detection Tests
|
||||
// =========================================================================
|
||||
|
||||
public function testDetectLanguagePython(): void
|
||||
{
|
||||
$this->assertEquals('python', Unsandbox::detectLanguage('test.py'));
|
||||
}
|
||||
|
||||
public function testDetectLanguageJavascript(): void
|
||||
{
|
||||
$this->assertEquals('javascript', Unsandbox::detectLanguage('test.js'));
|
||||
}
|
||||
|
||||
public function testDetectLanguageTypescript(): void
|
||||
{
|
||||
$this->assertEquals('typescript', Unsandbox::detectLanguage('test.ts'));
|
||||
}
|
||||
|
||||
public function testDetectLanguageRuby(): void
|
||||
{
|
||||
$this->assertEquals('ruby', Unsandbox::detectLanguage('test.rb'));
|
||||
}
|
||||
|
||||
public function testDetectLanguageGo(): void
|
||||
{
|
||||
$this->assertEquals('go', Unsandbox::detectLanguage('test.go'));
|
||||
}
|
||||
|
||||
public function testDetectLanguageRust(): void
|
||||
{
|
||||
$this->assertEquals('rust', Unsandbox::detectLanguage('test.rs'));
|
||||
}
|
||||
|
||||
public function testDetectLanguageUnknownReturnsNull(): void
|
||||
{
|
||||
$this->assertNull(Unsandbox::detectLanguage('test.unknown'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional tests that require API credentials
|
||||
*/
|
||||
class FunctionalAPITest extends TestCase
|
||||
{
|
||||
private ?Unsandbox $client = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
if (!$this->credentialsAvailable()) {
|
||||
$this->markTestSkipped('UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY required');
|
||||
}
|
||||
$this->client = new Unsandbox();
|
||||
}
|
||||
|
||||
private function credentialsAvailable(): bool
|
||||
{
|
||||
return getenv('UNSANDBOX_PUBLIC_KEY') && getenv('UNSANDBOX_SECRET_KEY');
|
||||
}
|
||||
|
||||
public function testHealthCheck(): void
|
||||
{
|
||||
$result = Unsandbox::healthCheck();
|
||||
$this->assertIsBool($result);
|
||||
}
|
||||
|
||||
public function testValidateKeys(): void
|
||||
{
|
||||
$result = $this->client->validateKeys();
|
||||
$this->assertIsArray($result);
|
||||
}
|
||||
|
||||
public function testGetLanguages(): void
|
||||
{
|
||||
$languages = $this->client->getLanguages();
|
||||
$this->assertIsArray($languages);
|
||||
$this->assertContains('python', $languages);
|
||||
}
|
||||
|
||||
public function testListSessions(): void
|
||||
{
|
||||
$sessions = $this->client->listSessions();
|
||||
$this->assertIsArray($sessions);
|
||||
}
|
||||
|
||||
public function testListServices(): void
|
||||
{
|
||||
$services = $this->client->listServices();
|
||||
$this->assertIsArray($services);
|
||||
}
|
||||
|
||||
public function testListSnapshots(): void
|
||||
{
|
||||
$snapshots = $this->client->listSnapshots();
|
||||
$this->assertIsArray($snapshots);
|
||||
}
|
||||
|
||||
public function testListImages(): void
|
||||
{
|
||||
$images = $this->client->listImages();
|
||||
$this->assertIsArray($images);
|
||||
}
|
||||
|
||||
public function testExecuteCode(): void
|
||||
{
|
||||
$result = $this->client->executeCode('python', 'print("hello")');
|
||||
$this->assertIsArray($result);
|
||||
$this->assertContains($result['status'] ?? '', ['completed', 'pending']);
|
||||
}
|
||||
}
|
||||
|
|
@ -757,6 +757,97 @@ function Invoke-Image {
|
|||
exit 1
|
||||
}
|
||||
|
||||
function Invoke-Snapshot {
|
||||
param($Args)
|
||||
|
||||
# Parse arguments
|
||||
$listMode = $Args -contains "--list" -or $Args -contains "-l"
|
||||
$infoId = $null
|
||||
$deleteId = $null
|
||||
$lockId = $null
|
||||
$unlockId = $null
|
||||
$restoreId = $null
|
||||
$cloneId = $null
|
||||
$cloneType = $null
|
||||
$name = $null
|
||||
$shell = $null
|
||||
$ports = $null
|
||||
|
||||
for ($i = 0; $i -lt $Args.Count; $i++) {
|
||||
switch ($Args[$i]) {
|
||||
"--info" { $infoId = $Args[$i + 1]; $i++ }
|
||||
"--delete" { $deleteId = $Args[$i + 1]; $i++ }
|
||||
"--lock" { $lockId = $Args[$i + 1]; $i++ }
|
||||
"--unlock" { $unlockId = $Args[$i + 1]; $i++ }
|
||||
"--restore" { $restoreId = $Args[$i + 1]; $i++ }
|
||||
"--clone" { $cloneId = $Args[$i + 1]; $i++ }
|
||||
"--type" { $cloneType = $Args[$i + 1]; $i++ }
|
||||
"--name" { $name = $Args[$i + 1]; $i++ }
|
||||
"--shell" { $shell = $Args[$i + 1]; $i++ }
|
||||
"-s" { $shell = $Args[$i + 1]; $i++ }
|
||||
"--ports" { $ports = $Args[$i + 1]; $i++ }
|
||||
}
|
||||
}
|
||||
|
||||
if ($listMode) {
|
||||
$result = Invoke-Api -Endpoint "/snapshots"
|
||||
$result | ConvertTo-Json -Depth 5
|
||||
return
|
||||
}
|
||||
|
||||
if ($infoId) {
|
||||
$result = Invoke-Api -Endpoint "/snapshots/$infoId"
|
||||
$result | ConvertTo-Json -Depth 5
|
||||
return
|
||||
}
|
||||
|
||||
if ($deleteId) {
|
||||
Invoke-ApiWithSudo -Endpoint "/snapshots/$deleteId" -Method "DELETE"
|
||||
Write-Host "`e[32mSnapshot deleted: $deleteId`e[0m"
|
||||
return
|
||||
}
|
||||
|
||||
if ($lockId) {
|
||||
Invoke-Api -Endpoint "/snapshots/$lockId/lock" -Method "POST" -Body "{}"
|
||||
Write-Host "`e[32mSnapshot locked: $lockId`e[0m"
|
||||
return
|
||||
}
|
||||
|
||||
if ($unlockId) {
|
||||
Invoke-ApiWithSudo -Endpoint "/snapshots/$unlockId/unlock" -Method "POST" -Body "{}"
|
||||
Write-Host "`e[32mSnapshot unlocked: $unlockId`e[0m"
|
||||
return
|
||||
}
|
||||
|
||||
if ($restoreId) {
|
||||
$result = Invoke-Api -Endpoint "/snapshots/$restoreId/restore" -Method "POST" -Body "{}"
|
||||
Write-Host "`e[32mRestored from snapshot`e[0m"
|
||||
$result | ConvertTo-Json -Depth 5
|
||||
return
|
||||
}
|
||||
|
||||
if ($cloneId) {
|
||||
if (-not $cloneType) {
|
||||
$cloneType = "session"
|
||||
}
|
||||
$payload = @{ type = $cloneType }
|
||||
if ($name) { $payload["name"] = $name }
|
||||
if ($shell) { $payload["shell"] = $shell }
|
||||
if ($ports) {
|
||||
$portList = $ports -split "," | ForEach-Object { [int]$_ }
|
||||
$payload["ports"] = $portList
|
||||
}
|
||||
$body = $payload | ConvertTo-Json
|
||||
$result = Invoke-Api -Endpoint "/snapshots/$cloneId/clone" -Method "POST" -Body $body
|
||||
Write-Host "`e[32mCloned to $cloneType`e[0m"
|
||||
$result | ConvertTo-Json -Depth 5
|
||||
return
|
||||
}
|
||||
|
||||
Write-Error "Error: Use --list, --info, --delete, --lock, --unlock, --restore, or --clone"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Invoke-Service {
|
||||
param($Args)
|
||||
|
||||
|
|
@ -1011,6 +1102,7 @@ if ($args.Count -eq 0 -or $args[0] -eq "--help" -or $args[0] -eq "-h") {
|
|||
Usage: pwsh un.ps1 [options] <source_file>
|
||||
pwsh un.ps1 session [options]
|
||||
pwsh un.ps1 service [options]
|
||||
pwsh un.ps1 snapshot [options]
|
||||
pwsh un.ps1 image [options]
|
||||
pwsh un.ps1 languages [--json]
|
||||
pwsh un.ps1 key [options]
|
||||
|
|
@ -1068,6 +1160,19 @@ Service env commands:
|
|||
env export ID Export vault contents
|
||||
env delete ID Delete vault
|
||||
|
||||
Snapshot options:
|
||||
--list, -l List all snapshots
|
||||
--info ID Get snapshot details
|
||||
--delete ID Delete snapshot
|
||||
--lock ID Lock snapshot
|
||||
--unlock ID Unlock snapshot
|
||||
--restore ID Restore from snapshot
|
||||
--clone ID Clone snapshot to session/service
|
||||
--type TYPE Clone type: session or service
|
||||
--name NAME Name for cloned resource
|
||||
--shell NAME Shell for cloned session
|
||||
--ports PORTS Ports for cloned service
|
||||
|
||||
Key options:
|
||||
--extend Open browser to extend key
|
||||
"@
|
||||
|
|
@ -1078,6 +1183,8 @@ if ($args[0] -eq "session") {
|
|||
Invoke-Session -Args $args[1..($args.Count-1)]
|
||||
} elseif ($args[0] -eq "service") {
|
||||
Invoke-Service -Args $args[1..($args.Count-1)]
|
||||
} elseif ($args[0] -eq "snapshot") {
|
||||
Invoke-Snapshot -Args $args[1..($args.Count-1)]
|
||||
} elseif ($args[0] -eq "image") {
|
||||
Invoke-Image -Args $args[1..($args.Count-1)]
|
||||
} elseif ($args[0] -eq "languages") {
|
||||
|
|
|
|||
162
clients/powershell/tests/Test-Unsandbox.ps1
Normal file
162
clients/powershell/tests/Test-Unsandbox.ps1
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
#!/usr/bin/env pwsh
|
||||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
# Unit and Functional Tests for Unsandbox PowerShell SDK
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Source the main script to get access to functions
|
||||
. "$PSScriptRoot/../sync/src/un.ps1"
|
||||
|
||||
function Write-TestResult {
|
||||
param($Name, $Passed, $Message = "")
|
||||
if ($Passed) {
|
||||
Write-Host " [PASS] $Name" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [FAIL] $Name $Message" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
function Test-Unit {
|
||||
Write-Host ""
|
||||
Write-Host "=== PowerShell SDK Unit Tests ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Test extension map
|
||||
Write-Host "Testing extension detection..."
|
||||
$tests = @{
|
||||
".py" = "python"
|
||||
".js" = "javascript"
|
||||
".go" = "go"
|
||||
".rs" = "rust"
|
||||
".ps1" = "powershell"
|
||||
}
|
||||
|
||||
$passed = 0
|
||||
foreach ($ext in $tests.Keys) {
|
||||
$expected = $tests[$ext]
|
||||
$actual = $EXT_MAP[$ext]
|
||||
if ($actual -eq $expected) {
|
||||
$passed++
|
||||
}
|
||||
}
|
||||
Write-TestResult "ExtensionMap" ($passed -eq $tests.Count) "($passed/$($tests.Count))"
|
||||
|
||||
# Test HMAC signing
|
||||
Write-Host "Testing HMAC signing..."
|
||||
$hmac = New-Object System.Security.Cryptography.HMACSHA256
|
||||
$hmac.Key = [System.Text.Encoding]::UTF8.GetBytes("key")
|
||||
$hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes("message"))
|
||||
$signature = [System.BitConverter]::ToString($hash).Replace("-", "").ToLower()
|
||||
$expected = "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a"
|
||||
Write-TestResult "HmacSign" ($signature -eq $expected)
|
||||
|
||||
# Test env content building
|
||||
Write-Host "Testing env content building..."
|
||||
$envs = @("KEY1=value1", "KEY2=value2")
|
||||
$result = Build-EnvContent -Envs $envs -EnvFile $null
|
||||
$hasKey1 = $result -match "KEY1=value1"
|
||||
$hasKey2 = $result -match "KEY2=value2"
|
||||
Write-TestResult "BuildEnvContent" ($hasKey1 -and $hasKey2)
|
||||
}
|
||||
|
||||
function Test-Functional {
|
||||
Write-Host ""
|
||||
Write-Host "=== PowerShell SDK Functional Tests ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
$publicKey = $env:UNSANDBOX_PUBLIC_KEY
|
||||
$secretKey = $env:UNSANDBOX_SECRET_KEY
|
||||
|
||||
if (-not $publicKey -or -not $secretKey) {
|
||||
Write-Host " [SKIP] No API credentials (set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY)" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
# Test key validation
|
||||
Write-Host "Testing key validation..."
|
||||
try {
|
||||
$result = Invoke-Api -Endpoint "/keys/validate" -Method "POST" -BaseUrl $PORTAL_BASE
|
||||
Write-TestResult "ValidateKeys" ($null -ne $result)
|
||||
} catch {
|
||||
Write-TestResult "ValidateKeys" $false $_.Exception.Message
|
||||
}
|
||||
|
||||
# Test languages endpoint
|
||||
Write-Host "Testing languages endpoint..."
|
||||
try {
|
||||
$result = Invoke-Api -Endpoint "/languages"
|
||||
$hasLanguages = ($result.languages -and $result.languages.Count -gt 0)
|
||||
Write-TestResult "GetLanguages" $hasLanguages "($($result.languages.Count) languages)"
|
||||
} catch {
|
||||
Write-TestResult "GetLanguages" $false $_.Exception.Message
|
||||
}
|
||||
|
||||
# Test execute
|
||||
Write-Host "Testing execute endpoint..."
|
||||
try {
|
||||
$payload = @{
|
||||
language = "python"
|
||||
code = 'print("hello from powershell test")'
|
||||
} | ConvertTo-Json
|
||||
$result = Invoke-Api -Endpoint "/execute" -Method "POST" -Body $payload
|
||||
$hasOutput = $result.stdout -match "hello"
|
||||
Write-TestResult "Execute" $hasOutput
|
||||
} catch {
|
||||
Write-TestResult "Execute" $false $_.Exception.Message
|
||||
}
|
||||
|
||||
# Test session list
|
||||
Write-Host "Testing session list..."
|
||||
try {
|
||||
$result = Invoke-Api -Endpoint "/sessions"
|
||||
Write-TestResult "SessionList" ($null -ne $result)
|
||||
} catch {
|
||||
Write-TestResult "SessionList" $false $_.Exception.Message
|
||||
}
|
||||
|
||||
# Test service list
|
||||
Write-Host "Testing service list..."
|
||||
try {
|
||||
$result = Invoke-Api -Endpoint "/services"
|
||||
Write-TestResult "ServiceList" ($null -ne $result)
|
||||
} catch {
|
||||
Write-TestResult "ServiceList" $false $_.Exception.Message
|
||||
}
|
||||
|
||||
# Test snapshot list
|
||||
Write-Host "Testing snapshot list..."
|
||||
try {
|
||||
$result = Invoke-Api -Endpoint "/snapshots"
|
||||
Write-TestResult "SnapshotList" ($null -ne $result)
|
||||
} catch {
|
||||
Write-TestResult "SnapshotList" $false $_.Exception.Message
|
||||
}
|
||||
|
||||
# Test image list
|
||||
Write-Host "Testing image list..."
|
||||
try {
|
||||
$result = Invoke-Api -Endpoint "/images"
|
||||
Write-TestResult "ImageList" ($null -ne $result)
|
||||
} catch {
|
||||
Write-TestResult "ImageList" $false $_.Exception.Message
|
||||
}
|
||||
|
||||
# Test snapshot list
|
||||
Write-Host "Testing snapshot list..."
|
||||
try {
|
||||
$result = Invoke-Api -Endpoint "/snapshots"
|
||||
Write-TestResult "SnapshotList" ($null -ne $result)
|
||||
} catch {
|
||||
Write-TestResult "SnapshotList" $false $_.Exception.Message
|
||||
}
|
||||
}
|
||||
|
||||
# Main
|
||||
Write-Host "Unsandbox PowerShell SDK Tests" -ForegroundColor Cyan
|
||||
Write-Host "==============================" -ForegroundColor Cyan
|
||||
|
||||
Test-Unit
|
||||
Test-Functional
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Tests complete." -ForegroundColor Cyan
|
||||
|
|
@ -747,10 +747,24 @@ main(Argv) :-
|
|||
-> write(user_error, 'Usage: un.pro [options] <source_file>\n'),
|
||||
write(user_error, ' un.pro session [options]\n'),
|
||||
write(user_error, ' un.pro service [options]\n'),
|
||||
write(user_error, ' un.pro snapshot [options]\n'),
|
||||
write(user_error, ' un.pro image [options]\n'),
|
||||
write(user_error, ' un.pro languages [--json]\n'),
|
||||
write(user_error, ' un.pro key [options]\n'),
|
||||
write(user_error, '\n'),
|
||||
write(user_error, 'Snapshot options:\n'),
|
||||
write(user_error, ' --list, -l List all snapshots\n'),
|
||||
write(user_error, ' --info ID Get snapshot details\n'),
|
||||
write(user_error, ' --delete ID Delete a snapshot\n'),
|
||||
write(user_error, ' --lock ID Lock snapshot\n'),
|
||||
write(user_error, ' --unlock ID Unlock snapshot\n'),
|
||||
write(user_error, ' --restore ID Restore from snapshot\n'),
|
||||
write(user_error, ' --clone ID Clone snapshot (--type required)\n'),
|
||||
write(user_error, ' --type TYPE Clone type: session or service\n'),
|
||||
write(user_error, ' --name NAME Name for cloned resource\n'),
|
||||
write(user_error, ' --shell SHELL Shell for cloned session\n'),
|
||||
write(user_error, ' --ports PORTS Ports for cloned service\n'),
|
||||
write(user_error, '\n'),
|
||||
write(user_error, 'Image options:\n'),
|
||||
write(user_error, ' --list, -l List all images\n'),
|
||||
write(user_error, ' --info ID Get image details\n'),
|
||||
|
|
@ -772,6 +786,8 @@ main(Argv) :-
|
|||
-> handle_session(Rest)
|
||||
; Argv = ['service'|Rest]
|
||||
-> handle_service(Rest)
|
||||
; Argv = ['snapshot'|Rest]
|
||||
-> handle_snapshot(Rest)
|
||||
; Argv = ['image'|Rest]
|
||||
-> handle_image(Rest)
|
||||
; Argv = ['languages'|Rest]
|
||||
|
|
@ -783,3 +799,173 @@ main(Argv) :-
|
|||
; write(user_error, 'Error: Invalid arguments\n'),
|
||||
halt(1)
|
||||
).
|
||||
|
||||
% =============================================================================
|
||||
% Snapshot handlers
|
||||
% =============================================================================
|
||||
|
||||
handle_snapshot(Args) :-
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
handle_snapshot_cmd(Args, PublicKey, SecretKey).
|
||||
|
||||
handle_snapshot_cmd(['--list'|_], PublicKey, SecretKey) :- !,
|
||||
atomic_list_concat([
|
||||
'TS=$(date +%s); ',
|
||||
'SIG=$(echo -n "$TS:GET:/snapshots:" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'curl -s -X GET "https://api.unsandbox.com/snapshots" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS" ',
|
||||
'-H "X-Signature: $SIG" | jq .'
|
||||
], Cmd),
|
||||
shell(Cmd).
|
||||
|
||||
handle_snapshot_cmd(['-l'|_], PublicKey, SecretKey) :- !,
|
||||
handle_snapshot_cmd(['--list'], PublicKey, SecretKey).
|
||||
|
||||
handle_snapshot_cmd(['--info', Id|_], PublicKey, SecretKey) :- !,
|
||||
atomic_list_concat([
|
||||
'TS=$(date +%s); ',
|
||||
'SIG=$(echo -n "$TS:GET:/snapshots/', Id, ':" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'curl -s -X GET "https://api.unsandbox.com/snapshots/', Id, '" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS" ',
|
||||
'-H "X-Signature: $SIG" | jq .'
|
||||
], Cmd),
|
||||
shell(Cmd).
|
||||
|
||||
handle_snapshot_cmd(['--delete', Id|_], PublicKey, SecretKey) :- !,
|
||||
atomic_list_concat([
|
||||
'TS=$(date +%s); ',
|
||||
'SIG=$(echo -n "$TS:DELETE:/snapshots/', Id, ':" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'RESP=$(curl -s -w "\\n%{http_code}" -X DELETE "https://api.unsandbox.com/snapshots/', Id, '" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS" ',
|
||||
'-H "X-Signature: $SIG"); ',
|
||||
'HTTP_CODE=$(echo "$RESP" | tail -1); ',
|
||||
'BODY=$(echo "$RESP" | head -n -1); ',
|
||||
'if [ "$HTTP_CODE" = "428" ]; then ',
|
||||
'OTP=$(echo "$BODY" | jq -r ".otp // empty"); ',
|
||||
'if [ -n "$OTP" ]; then ',
|
||||
'TS2=$(date +%s); ',
|
||||
'SIG2=$(echo -n "$TS2:DELETE:/snapshots/', Id, ':" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'curl -s -X DELETE "https://api.unsandbox.com/snapshots/', Id, '" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS2" ',
|
||||
'-H "X-Signature: $SIG2" ',
|
||||
'-H "X-Sudo-OTP: $OTP" | jq .; ',
|
||||
'echo -e "\\x1b[32mSnapshot deleted\\x1b[0m"; fi; ',
|
||||
'else echo "$BODY" | jq .; fi'
|
||||
], Cmd),
|
||||
shell(Cmd).
|
||||
|
||||
handle_snapshot_cmd(['--lock', Id|_], PublicKey, SecretKey) :- !,
|
||||
atomic_list_concat([
|
||||
'TS=$(date +%s); ',
|
||||
'SIG=$(echo -n "$TS:POST:/snapshots/', Id, '/lock:" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'curl -s -X POST "https://api.unsandbox.com/snapshots/', Id, '/lock" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS" ',
|
||||
'-H "X-Signature: $SIG" | jq . && ',
|
||||
'echo -e "\\x1b[32mSnapshot locked\\x1b[0m"'
|
||||
], Cmd),
|
||||
shell(Cmd).
|
||||
|
||||
handle_snapshot_cmd(['--unlock', Id|_], PublicKey, SecretKey) :- !,
|
||||
atomic_list_concat([
|
||||
'TS=$(date +%s); ',
|
||||
'BODY="{}"; ',
|
||||
'SIG=$(echo -n "$TS:POST:/snapshots/', Id, '/unlock:$BODY" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'RESP=$(curl -s -w "\\n%{http_code}" -X POST "https://api.unsandbox.com/snapshots/', Id, '/unlock" ',
|
||||
'-H "Content-Type: application/json" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS" ',
|
||||
'-H "X-Signature: $SIG" ',
|
||||
'-d "$BODY"); ',
|
||||
'HTTP_CODE=$(echo "$RESP" | tail -1); ',
|
||||
'BODY_RESP=$(echo "$RESP" | head -n -1); ',
|
||||
'if [ "$HTTP_CODE" = "428" ]; then ',
|
||||
'OTP=$(echo "$BODY_RESP" | jq -r ".otp // empty"); ',
|
||||
'if [ -n "$OTP" ]; then ',
|
||||
'TS2=$(date +%s); ',
|
||||
'SIG2=$(echo -n "$TS2:POST:/snapshots/', Id, '/unlock:$BODY" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'curl -s -X POST "https://api.unsandbox.com/snapshots/', Id, '/unlock" ',
|
||||
'-H "Content-Type: application/json" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS2" ',
|
||||
'-H "X-Signature: $SIG2" ',
|
||||
'-H "X-Sudo-OTP: $OTP" ',
|
||||
'-d "$BODY" | jq .; ',
|
||||
'echo -e "\\x1b[32mSnapshot unlocked\\x1b[0m"; fi; ',
|
||||
'else echo "$BODY_RESP" | jq .; fi'
|
||||
], Cmd),
|
||||
shell(Cmd).
|
||||
|
||||
handle_snapshot_cmd(['--restore', Id|_], PublicKey, SecretKey) :- !,
|
||||
atomic_list_concat([
|
||||
'TS=$(date +%s); ',
|
||||
'BODY="{}"; ',
|
||||
'SIG=$(echo -n "$TS:POST:/snapshots/', Id, '/restore:$BODY" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'curl -s -X POST "https://api.unsandbox.com/snapshots/', Id, '/restore" ',
|
||||
'-H "Content-Type: application/json" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS" ',
|
||||
'-H "X-Signature: $SIG" ',
|
||||
'-d "$BODY" | jq . && ',
|
||||
'echo -e "\\x1b[32mSnapshot restored\\x1b[0m"'
|
||||
], Cmd),
|
||||
shell(Cmd).
|
||||
|
||||
handle_snapshot_cmd(['--clone', Id|Rest], PublicKey, SecretKey) :- !,
|
||||
parse_snapshot_clone_args(Rest, Type, Name, Ports, Shell),
|
||||
( Type = ''
|
||||
-> write(user_error, '\x1b[31mError: --type required for --clone (session or service)\x1b[0m\n'),
|
||||
halt(1)
|
||||
; true
|
||||
),
|
||||
build_snapshot_clone_body(Type, Name, Ports, Shell, Body),
|
||||
atomic_list_concat([
|
||||
'TS=$(date +%s); ',
|
||||
'BODY=''', Body, '''; ',
|
||||
'SIG=$(echo -n "$TS:POST:/snapshots/', Id, '/clone:$BODY" | openssl dgst -sha256 -hmac "', SecretKey, '" | cut -d" " -f2); ',
|
||||
'curl -s -X POST "https://api.unsandbox.com/snapshots/', Id, '/clone" ',
|
||||
'-H "Content-Type: application/json" ',
|
||||
'-H "Authorization: Bearer ', PublicKey, '" ',
|
||||
'-H "X-Timestamp: $TS" ',
|
||||
'-H "X-Signature: $SIG" ',
|
||||
'-d "$BODY" | jq . && ',
|
||||
'echo -e "\\x1b[32mSnapshot cloned\\x1b[0m"'
|
||||
], Cmd),
|
||||
shell(Cmd).
|
||||
|
||||
handle_snapshot_cmd(_, _, _) :-
|
||||
write(user_error, '\x1b[31mError: Use --list, --info, --delete, --lock, --unlock, --restore, or --clone\x1b[0m\n'),
|
||||
halt(1).
|
||||
|
||||
parse_snapshot_clone_args([], '', '', '', '').
|
||||
parse_snapshot_clone_args(['--type', Type|Rest], Type, Name, Ports, Shell) :- !,
|
||||
parse_snapshot_clone_args(Rest, _, Name, Ports, Shell).
|
||||
parse_snapshot_clone_args(['--name', Name|Rest], Type, Name, Ports, Shell) :- !,
|
||||
parse_snapshot_clone_args(Rest, Type, _, Ports, Shell).
|
||||
parse_snapshot_clone_args(['--ports', Ports|Rest], Type, Name, Ports, Shell) :- !,
|
||||
parse_snapshot_clone_args(Rest, Type, Name, _, Shell).
|
||||
parse_snapshot_clone_args(['--shell', Shell|Rest], Type, Name, Ports, Shell) :- !,
|
||||
parse_snapshot_clone_args(Rest, Type, Name, Ports, _).
|
||||
parse_snapshot_clone_args([_|Rest], Type, Name, Ports, Shell) :-
|
||||
parse_snapshot_clone_args(Rest, Type, Name, Ports, Shell).
|
||||
|
||||
build_snapshot_clone_body(Type, Name, Ports, Shell, Body) :-
|
||||
atomic_list_concat(['{"type":"', Type, '"'], Base),
|
||||
( Name \= ''
|
||||
-> atomic_list_concat([Base, ',"name":"', Name, '"'], Base2)
|
||||
; Base2 = Base
|
||||
),
|
||||
( Ports \= ''
|
||||
-> atomic_list_concat([Base2, ',"ports":[', Ports, ']'], Base3)
|
||||
; Base3 = Base2
|
||||
),
|
||||
( Shell \= ''
|
||||
-> atomic_list_concat([Base3, ',"shell":"', Shell, '"'], Base4)
|
||||
; Base4 = Base3
|
||||
),
|
||||
atomic_list_concat([Base4, '}'], Body).
|
||||
|
|
|
|||
98
clients/prolog/tests/test_un.sh
Executable file
98
clients/prolog/tests/test_un.sh
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
#!/bin/bash
|
||||
# Test suite for Prolog Unsandbox SDK
|
||||
# Run: bash tests/test_un.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SDK_DIR="$SCRIPT_DIR/../sync/src"
|
||||
SOURCE="$SDK_DIR/un.pro"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
|
||||
# Test helper
|
||||
test_that() {
|
||||
local description="$1"
|
||||
local test_cmd="$2"
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
|
||||
if eval "$test_cmd" >/dev/null 2>&1; then
|
||||
echo -e "[${GREEN}PASS${NC}] $description"
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
return 0
|
||||
else
|
||||
echo -e "[${RED}FAIL${NC}] $description"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test source file exists
|
||||
echo ""
|
||||
echo "=== Source File ==="
|
||||
test_that "Source file exists" "[ -f '$SOURCE' ]"
|
||||
|
||||
echo ""
|
||||
echo "=== Command Handlers ==="
|
||||
test_that "Session handler defined" "grep -q 'handle_session' '$SOURCE'"
|
||||
test_that "Service handler defined" "grep -q 'handle_service' '$SOURCE'"
|
||||
test_that "Snapshot handler defined" "grep -q 'handle_snapshot' '$SOURCE'"
|
||||
test_that "Image handler defined" "grep -q 'handle_image' '$SOURCE'"
|
||||
test_that "Languages handler defined" "grep -q 'handle_languages' '$SOURCE'"
|
||||
test_that "Key handler defined" "grep -q 'handle_key' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Snapshot Operations ==="
|
||||
test_that "Snapshot list implemented" "grep -q \"handle_snapshot_cmd.*--list\" '$SOURCE'"
|
||||
test_that "Snapshot info implemented" "grep -q \"handle_snapshot_cmd.*--info\" '$SOURCE'"
|
||||
test_that "Snapshot delete implemented" "grep -q \"handle_snapshot_cmd.*--delete\" '$SOURCE'"
|
||||
test_that "Snapshot lock implemented" "grep -q \"handle_snapshot_cmd.*--lock\" '$SOURCE'"
|
||||
test_that "Snapshot unlock implemented" "grep -q \"handle_snapshot_cmd.*--unlock\" '$SOURCE'"
|
||||
test_that "Snapshot restore implemented" "grep -q \"handle_snapshot_cmd.*--restore\" '$SOURCE'"
|
||||
test_that "Snapshot clone implemented" "grep -q \"handle_snapshot_cmd.*--clone\" '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Snapshot Clone Helper ==="
|
||||
test_that "Clone args parser defined" "grep -q 'parse_snapshot_clone_args' '$SOURCE'"
|
||||
test_that "Clone body builder defined" "grep -q 'build_snapshot_clone_body' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Language Detection ==="
|
||||
test_that "Python detection" "grep -q \"ext_lang.*py.*python\" '$SOURCE'"
|
||||
test_that "JavaScript detection" "grep -q \"ext_lang.*js.*javascript\" '$SOURCE'"
|
||||
test_that "Julia detection" "grep -q \"ext_lang.*jl.*julia\" '$SOURCE'"
|
||||
test_that "R detection" "grep -q \"ext_lang.*\\.r.*r\" '$SOURCE'"
|
||||
test_that "Fortran detection" "grep -q \"ext_lang.*f90.*fortran\" '$SOURCE'"
|
||||
test_that "COBOL detection" "grep -q \"ext_lang.*cob.*cobol\" '$SOURCE'"
|
||||
test_that "Prolog detection" "grep -q \"ext_lang.*pro.*prolog\" '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== HMAC Authentication ==="
|
||||
test_that "Uses openssl for HMAC" "grep -q 'openssl dgst -sha256 -hmac' '$SOURCE'"
|
||||
test_that "Has X-Signature header" "grep -q 'X-Signature' '$SOURCE'"
|
||||
test_that "Has X-Timestamp header" "grep -q 'X-Timestamp' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Sudo OTP Handling ==="
|
||||
test_that "Handles 428 response" "grep -q '428' '$SOURCE'"
|
||||
test_that "Has X-Sudo-OTP header" "grep -q 'X-Sudo-OTP' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Constants ==="
|
||||
test_that "Portal base defined" "grep -q 'portal_base' '$SOURCE'"
|
||||
test_that "Languages cache TTL defined" "grep -q 'languages_cache_ttl' '$SOURCE'"
|
||||
|
||||
echo ""
|
||||
echo "=== Summary ==="
|
||||
echo "Tests passed: $TESTS_PASSED / $TESTS_RUN"
|
||||
|
||||
if [ $TESTS_PASSED -eq $TESTS_RUN ]; then
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -6,7 +6,7 @@ unsandbox.com Python SDK (Synchronous)
|
|||
|
||||
Library Usage:
|
||||
from un import (
|
||||
# Execution
|
||||
# Execution (8)
|
||||
execute_code,
|
||||
execute_async,
|
||||
get_job,
|
||||
|
|
@ -15,7 +15,7 @@ Library Usage:
|
|||
list_jobs,
|
||||
get_languages,
|
||||
detect_language,
|
||||
# Sessions
|
||||
# Sessions (9)
|
||||
list_sessions,
|
||||
get_session,
|
||||
create_session,
|
||||
|
|
@ -25,7 +25,7 @@ Library Usage:
|
|||
boost_session,
|
||||
unboost_session,
|
||||
shell_session,
|
||||
# Services
|
||||
# Services (17)
|
||||
list_services,
|
||||
create_service,
|
||||
get_service,
|
||||
|
|
@ -44,18 +44,42 @@ Library Usage:
|
|||
export_service_env,
|
||||
redeploy_service,
|
||||
execute_in_service,
|
||||
# Snapshots
|
||||
resize_service,
|
||||
# Snapshots (9)
|
||||
session_snapshot,
|
||||
service_snapshot,
|
||||
list_snapshots,
|
||||
get_snapshot,
|
||||
restore_snapshot,
|
||||
delete_snapshot,
|
||||
lock_snapshot,
|
||||
unlock_snapshot,
|
||||
clone_snapshot,
|
||||
# Images (13)
|
||||
image_publish,
|
||||
list_images,
|
||||
get_image,
|
||||
delete_image,
|
||||
lock_image,
|
||||
unlock_image,
|
||||
set_image_visibility,
|
||||
grant_image_access,
|
||||
revoke_image_access,
|
||||
list_image_trusted,
|
||||
transfer_image,
|
||||
spawn_from_image,
|
||||
clone_image,
|
||||
# PaaS Logs (2)
|
||||
logs_fetch,
|
||||
logs_stream,
|
||||
# Key validation
|
||||
validate_keys,
|
||||
# Image generation
|
||||
# Utilities
|
||||
version,
|
||||
health_check,
|
||||
last_error,
|
||||
hmac_sign,
|
||||
# Image generation (AI)
|
||||
image,
|
||||
)
|
||||
|
||||
|
|
@ -777,7 +801,7 @@ def list_snapshots(
|
|||
secret_key: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all snapshots (NEW).
|
||||
List all snapshots.
|
||||
|
||||
Args:
|
||||
public_key: Optional API key
|
||||
|
|
@ -796,6 +820,39 @@ def list_snapshots(
|
|||
return response.get("snapshots", [])
|
||||
|
||||
|
||||
def get_snapshot(
|
||||
snapshot_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get details of a specific snapshot.
|
||||
|
||||
Args:
|
||||
snapshot_id: Snapshot ID to get details for
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Snapshot details dict containing:
|
||||
- id: Snapshot ID
|
||||
- name: Snapshot name
|
||||
- type: "session" or "service"
|
||||
- source_id: Original resource ID
|
||||
- hot: Whether snapshot preserves running state
|
||||
- locked: Whether snapshot is locked
|
||||
- created_at: Creation timestamp
|
||||
- size_bytes: Size in bytes
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request("GET", f"/snapshots/{snapshot_id}", public_key, secret_key)
|
||||
|
||||
|
||||
def restore_snapshot(
|
||||
snapshot_id: str,
|
||||
public_key: Optional[str] = None,
|
||||
|
|
@ -1715,6 +1772,39 @@ def execute_in_service(
|
|||
)
|
||||
|
||||
|
||||
def resize_service(
|
||||
service_id: str,
|
||||
vcpu: int,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Resize a service's vCPU allocation.
|
||||
|
||||
Args:
|
||||
service_id: Service ID to resize
|
||||
vcpu: Number of vCPUs (1-8 typically)
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Response dict with updated service info
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
return _make_request(
|
||||
"PATCH",
|
||||
f"/services/{service_id}",
|
||||
public_key,
|
||||
secret_key,
|
||||
{"vcpu": vcpu},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Additional Snapshot Functions
|
||||
# =============================================================================
|
||||
|
|
@ -2269,6 +2359,177 @@ def image(
|
|||
return _make_request("POST", "/image", public_key, secret_key, payload)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PaaS Logs Functions
|
||||
# =============================================================================
|
||||
|
||||
_last_error: Optional[str] = None
|
||||
|
||||
|
||||
def logs_fetch(
|
||||
source: str = "all",
|
||||
lines: int = 100,
|
||||
since: str = "5m",
|
||||
grep: Optional[str] = None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetch batch logs from the PaaS platform.
|
||||
|
||||
Args:
|
||||
source: Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
lines: Number of lines to fetch (1-10000)
|
||||
since: Time window - "1m", "5m", "1h", "1d"
|
||||
grep: Optional filter pattern
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Returns:
|
||||
Dict with log entries
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
ValueError: Invalid response format
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
params = f"?source={source}&lines={lines}&since={since}"
|
||||
if grep:
|
||||
params += f"&grep={grep}"
|
||||
return _make_request("GET", f"/logs{params}", public_key, secret_key)
|
||||
|
||||
|
||||
def logs_stream(
|
||||
source: str = "all",
|
||||
grep: Optional[str] = None,
|
||||
callback=None,
|
||||
public_key: Optional[str] = None,
|
||||
secret_key: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Stream logs via Server-Sent Events.
|
||||
|
||||
Blocks until interrupted or server closes connection.
|
||||
|
||||
Args:
|
||||
source: Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
grep: Optional filter pattern
|
||||
callback: Function called for each log line (signature: callback(source, line))
|
||||
public_key: Optional API key
|
||||
secret_key: Optional API secret
|
||||
|
||||
Raises:
|
||||
requests.RequestException: Network errors
|
||||
CredentialsError: Missing credentials
|
||||
"""
|
||||
public_key, secret_key = _resolve_credentials(public_key, secret_key)
|
||||
|
||||
url = f"{API_BASE}/logs/stream?source={source}"
|
||||
if grep:
|
||||
url += f"&grep={grep}"
|
||||
|
||||
timestamp = int(time.time())
|
||||
path = f"/logs/stream?source={source}"
|
||||
if grep:
|
||||
path += f"&grep={grep}"
|
||||
|
||||
signature = _sign_request(secret_key, timestamp, "GET", path, None)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {public_key}",
|
||||
"X-Timestamp": str(timestamp),
|
||||
"X-Signature": signature,
|
||||
"Accept": "text/event-stream",
|
||||
}
|
||||
|
||||
with requests.get(url, headers=headers, stream=True, timeout=None) as response:
|
||||
response.raise_for_status()
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
decoded = line.decode("utf-8")
|
||||
if decoded.startswith("data: "):
|
||||
data = decoded[6:]
|
||||
try:
|
||||
entry = json.loads(data)
|
||||
if callback:
|
||||
callback(entry.get("source", source), entry.get("line", data))
|
||||
else:
|
||||
print(f"[{entry.get('source', source)}] {entry.get('line', data)}")
|
||||
except json.JSONDecodeError:
|
||||
if callback:
|
||||
callback(source, data)
|
||||
else:
|
||||
print(f"[{source}] {data}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Utility Functions
|
||||
# =============================================================================
|
||||
|
||||
SDK_VERSION = "4.2.0"
|
||||
|
||||
|
||||
def version() -> str:
|
||||
"""
|
||||
Get the SDK version string.
|
||||
|
||||
Returns:
|
||||
Version string (e.g., "4.2.0")
|
||||
"""
|
||||
return SDK_VERSION
|
||||
|
||||
|
||||
def health_check() -> bool:
|
||||
"""
|
||||
Check if the API is healthy and responding.
|
||||
|
||||
Returns:
|
||||
True if API is healthy, False otherwise
|
||||
"""
|
||||
global _last_error
|
||||
try:
|
||||
response = requests.get(f"{API_BASE}/health", timeout=10)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
_last_error = f"Health check failed: HTTP {response.status_code}"
|
||||
return False
|
||||
except Exception as e:
|
||||
_last_error = f"Health check failed: {str(e)}"
|
||||
return False
|
||||
|
||||
|
||||
def last_error() -> Optional[str]:
|
||||
"""
|
||||
Get the last error message.
|
||||
|
||||
Returns:
|
||||
Last error message or None
|
||||
"""
|
||||
return _last_error
|
||||
|
||||
|
||||
def hmac_sign(secret_key: str, message: str) -> str:
|
||||
"""
|
||||
Sign a message using HMAC-SHA256.
|
||||
|
||||
This is the underlying signing function used for request authentication.
|
||||
Exposed for testing and debugging purposes.
|
||||
|
||||
Args:
|
||||
secret_key: The secret key for signing
|
||||
message: The message to sign
|
||||
|
||||
Returns:
|
||||
64-character lowercase hex string
|
||||
"""
|
||||
return hmac.new(
|
||||
secret_key.encode(),
|
||||
message.encode(),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI Implementation
|
||||
# =============================================================================
|
||||
|
|
|
|||
294
clients/python/sync/tests/test_new_functions.py
Normal file
294
clients/python/sync/tests/test_new_functions.py
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
"""Tests for new SDK functions (feature parity with C implementation)"""
|
||||
|
||||
import pytest
|
||||
from un import (
|
||||
# Execution
|
||||
execute_code,
|
||||
execute_async,
|
||||
get_job,
|
||||
wait_for_job,
|
||||
cancel_job,
|
||||
list_jobs,
|
||||
get_languages,
|
||||
detect_language,
|
||||
# Sessions
|
||||
list_sessions,
|
||||
get_session,
|
||||
create_session,
|
||||
delete_session,
|
||||
freeze_session,
|
||||
unfreeze_session,
|
||||
boost_session,
|
||||
unboost_session,
|
||||
shell_session,
|
||||
# Services
|
||||
list_services,
|
||||
create_service,
|
||||
get_service,
|
||||
update_service,
|
||||
delete_service,
|
||||
freeze_service,
|
||||
unfreeze_service,
|
||||
lock_service,
|
||||
unlock_service,
|
||||
set_unfreeze_on_demand,
|
||||
get_service_logs,
|
||||
get_service_env,
|
||||
set_service_env,
|
||||
delete_service_env,
|
||||
export_service_env,
|
||||
redeploy_service,
|
||||
execute_in_service,
|
||||
resize_service,
|
||||
# Snapshots
|
||||
session_snapshot,
|
||||
service_snapshot,
|
||||
list_snapshots,
|
||||
get_snapshot,
|
||||
restore_snapshot,
|
||||
delete_snapshot,
|
||||
lock_snapshot,
|
||||
unlock_snapshot,
|
||||
clone_snapshot,
|
||||
# Images
|
||||
image_publish,
|
||||
list_images,
|
||||
get_image,
|
||||
delete_image,
|
||||
lock_image,
|
||||
unlock_image,
|
||||
set_image_visibility,
|
||||
grant_image_access,
|
||||
revoke_image_access,
|
||||
list_image_trusted,
|
||||
transfer_image,
|
||||
spawn_from_image,
|
||||
clone_image,
|
||||
# PaaS Logs
|
||||
logs_fetch,
|
||||
logs_stream,
|
||||
# Utilities
|
||||
version,
|
||||
health_check,
|
||||
last_error,
|
||||
hmac_sign,
|
||||
validate_keys,
|
||||
)
|
||||
|
||||
|
||||
class TestUtilityFunctions:
|
||||
"""Test utility functions that don't require API calls"""
|
||||
|
||||
def test_version_returns_string(self):
|
||||
"""Test version function returns a string"""
|
||||
v = version()
|
||||
assert isinstance(v, str)
|
||||
assert len(v) > 0
|
||||
# Should be semantic version format
|
||||
parts = v.split('.')
|
||||
assert len(parts) >= 2
|
||||
|
||||
def test_hmac_sign_basic(self):
|
||||
"""Test HMAC signing produces correct format"""
|
||||
signature = hmac_sign("secret_key", "message_to_sign")
|
||||
assert isinstance(signature, str)
|
||||
assert len(signature) == 64
|
||||
# Should be lowercase hex
|
||||
assert all(c in '0123456789abcdef' for c in signature)
|
||||
|
||||
def test_hmac_sign_deterministic(self):
|
||||
"""Test HMAC signing is deterministic"""
|
||||
sig1 = hmac_sign("secret", "message")
|
||||
sig2 = hmac_sign("secret", "message")
|
||||
assert sig1 == sig2
|
||||
|
||||
def test_hmac_sign_different_secrets(self):
|
||||
"""Test different secrets produce different signatures"""
|
||||
sig1 = hmac_sign("secret1", "message")
|
||||
sig2 = hmac_sign("secret2", "message")
|
||||
assert sig1 != sig2
|
||||
|
||||
def test_hmac_sign_different_messages(self):
|
||||
"""Test different messages produce different signatures"""
|
||||
sig1 = hmac_sign("secret", "message1")
|
||||
sig2 = hmac_sign("secret", "message2")
|
||||
assert sig1 != sig2
|
||||
|
||||
def test_hmac_sign_known_value(self):
|
||||
"""Test HMAC signing matches known value"""
|
||||
# This is a known test vector
|
||||
signature = hmac_sign("test_secret", "1234567890:POST:/execute:")
|
||||
assert len(signature) == 64
|
||||
|
||||
def test_last_error_initial_none(self):
|
||||
"""Test last_error is None initially or after success"""
|
||||
# Note: This may not be None if previous tests failed
|
||||
error = last_error()
|
||||
assert error is None or isinstance(error, str)
|
||||
|
||||
|
||||
class TestFunctionExports:
|
||||
"""Test that all required functions are exported"""
|
||||
|
||||
def test_execution_functions_exported(self):
|
||||
"""Test execution functions are exported"""
|
||||
assert callable(execute_code)
|
||||
assert callable(execute_async)
|
||||
assert callable(get_job)
|
||||
assert callable(wait_for_job)
|
||||
assert callable(cancel_job)
|
||||
assert callable(list_jobs)
|
||||
assert callable(get_languages)
|
||||
assert callable(detect_language)
|
||||
|
||||
def test_session_functions_exported(self):
|
||||
"""Test session functions are exported"""
|
||||
assert callable(list_sessions)
|
||||
assert callable(get_session)
|
||||
assert callable(create_session)
|
||||
assert callable(delete_session)
|
||||
assert callable(freeze_session)
|
||||
assert callable(unfreeze_session)
|
||||
assert callable(boost_session)
|
||||
assert callable(unboost_session)
|
||||
assert callable(shell_session)
|
||||
|
||||
def test_service_functions_exported(self):
|
||||
"""Test service functions are exported"""
|
||||
assert callable(list_services)
|
||||
assert callable(create_service)
|
||||
assert callable(get_service)
|
||||
assert callable(update_service)
|
||||
assert callable(delete_service)
|
||||
assert callable(freeze_service)
|
||||
assert callable(unfreeze_service)
|
||||
assert callable(lock_service)
|
||||
assert callable(unlock_service)
|
||||
assert callable(set_unfreeze_on_demand)
|
||||
assert callable(get_service_logs)
|
||||
assert callable(get_service_env)
|
||||
assert callable(set_service_env)
|
||||
assert callable(delete_service_env)
|
||||
assert callable(export_service_env)
|
||||
assert callable(redeploy_service)
|
||||
assert callable(execute_in_service)
|
||||
assert callable(resize_service) # New function
|
||||
|
||||
def test_snapshot_functions_exported(self):
|
||||
"""Test snapshot functions are exported"""
|
||||
assert callable(session_snapshot)
|
||||
assert callable(service_snapshot)
|
||||
assert callable(list_snapshots)
|
||||
assert callable(get_snapshot) # New function
|
||||
assert callable(restore_snapshot)
|
||||
assert callable(delete_snapshot)
|
||||
assert callable(lock_snapshot)
|
||||
assert callable(unlock_snapshot)
|
||||
assert callable(clone_snapshot)
|
||||
|
||||
def test_image_functions_exported(self):
|
||||
"""Test image functions are exported"""
|
||||
assert callable(image_publish)
|
||||
assert callable(list_images)
|
||||
assert callable(get_image)
|
||||
assert callable(delete_image)
|
||||
assert callable(lock_image)
|
||||
assert callable(unlock_image)
|
||||
assert callable(set_image_visibility)
|
||||
assert callable(grant_image_access)
|
||||
assert callable(revoke_image_access)
|
||||
assert callable(list_image_trusted)
|
||||
assert callable(transfer_image)
|
||||
assert callable(spawn_from_image)
|
||||
assert callable(clone_image)
|
||||
|
||||
def test_logs_functions_exported(self):
|
||||
"""Test PaaS logs functions are exported"""
|
||||
assert callable(logs_fetch) # New function
|
||||
assert callable(logs_stream) # New function
|
||||
|
||||
def test_utility_functions_exported(self):
|
||||
"""Test utility functions are exported"""
|
||||
assert callable(validate_keys)
|
||||
assert callable(version) # New function
|
||||
assert callable(health_check) # New function
|
||||
assert callable(last_error) # New function
|
||||
assert callable(hmac_sign) # New function
|
||||
|
||||
|
||||
class TestLanguageDetection:
|
||||
"""Test language detection (subset, full tests in test_language_detection.py)"""
|
||||
|
||||
def test_common_extensions(self):
|
||||
"""Test common file extensions"""
|
||||
assert detect_language("test.py") == "python"
|
||||
assert detect_language("test.js") == "javascript"
|
||||
assert detect_language("test.rb") == "ruby"
|
||||
assert detect_language("test.go") == "go"
|
||||
assert detect_language("test.rs") == "rust"
|
||||
|
||||
def test_unknown_returns_none(self):
|
||||
"""Test unknown extensions return None"""
|
||||
assert detect_language("test.unknown") is None
|
||||
assert detect_language("Makefile") is None
|
||||
|
||||
|
||||
# Functional tests require API credentials - mark as skip without credentials
|
||||
@pytest.mark.skipif(
|
||||
not all([
|
||||
__import__('os').environ.get('UNSANDBOX_PUBLIC_KEY'),
|
||||
__import__('os').environ.get('UNSANDBOX_SECRET_KEY'),
|
||||
]),
|
||||
reason="UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY required"
|
||||
)
|
||||
class TestFunctionalAPI:
|
||||
"""Functional tests that require real API credentials"""
|
||||
|
||||
def test_health_check(self):
|
||||
"""Test API health check"""
|
||||
result = health_check()
|
||||
assert isinstance(result, bool)
|
||||
# API should be healthy
|
||||
assert result is True
|
||||
|
||||
def test_validate_keys(self):
|
||||
"""Test key validation"""
|
||||
result = validate_keys()
|
||||
assert isinstance(result, dict)
|
||||
assert 'valid' in result or 'tier' in result
|
||||
|
||||
def test_get_languages(self):
|
||||
"""Test getting supported languages"""
|
||||
languages = get_languages()
|
||||
assert isinstance(languages, list)
|
||||
assert len(languages) > 0
|
||||
assert 'python' in languages
|
||||
assert 'javascript' in languages
|
||||
|
||||
def test_list_sessions(self):
|
||||
"""Test listing sessions"""
|
||||
sessions = list_sessions()
|
||||
assert isinstance(sessions, list)
|
||||
|
||||
def test_list_services(self):
|
||||
"""Test listing services"""
|
||||
services = list_services()
|
||||
assert isinstance(services, list)
|
||||
|
||||
def test_list_snapshots(self):
|
||||
"""Test listing snapshots"""
|
||||
snapshots = list_snapshots()
|
||||
assert isinstance(snapshots, list)
|
||||
|
||||
def test_list_images(self):
|
||||
"""Test listing images"""
|
||||
images = list_images()
|
||||
assert isinstance(images, list)
|
||||
|
||||
def test_execute_code(self):
|
||||
"""Test code execution"""
|
||||
result = execute_code("python", "print('hello world')")
|
||||
assert isinstance(result, dict)
|
||||
assert result.get('status') in ['completed', 'pending']
|
||||
if result.get('status') == 'completed':
|
||||
assert 'hello world' in result.get('stdout', '')
|
||||
|
|
@ -666,6 +666,843 @@ languages <- function(public_key = NULL, secret_key = NULL) {
|
|||
return(result)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Session Library Functions
|
||||
# =============================================================================
|
||||
|
||||
#' List Sessions
|
||||
#'
|
||||
#' Retrieves a list of all active sessions.
|
||||
#'
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return A list containing sessions
|
||||
#' @export
|
||||
session_list <- function(public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
result <- api_request("/sessions", creds$public_key, creds$secret_key)
|
||||
return(result$sessions %||% list())
|
||||
}
|
||||
|
||||
#' Get Session Details
|
||||
#'
|
||||
#' @param session_id Session ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Session details
|
||||
#' @export
|
||||
session_get <- function(session_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
return(api_request(paste0("/sessions/", session_id), creds$public_key, creds$secret_key))
|
||||
}
|
||||
|
||||
#' Create Session
|
||||
#'
|
||||
#' @param shell Shell type (default: "bash")
|
||||
#' @param network Network mode (default: "zerotrust")
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Created session details
|
||||
#' @export
|
||||
session_create <- function(shell = "bash", network = "zerotrust",
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- list(shell = shell)
|
||||
if (network != "zerotrust") payload$network <- network
|
||||
return(api_request("/sessions", creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload))
|
||||
}
|
||||
|
||||
#' Destroy Session
|
||||
#'
|
||||
#' @param session_id Session ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
session_destroy <- function(session_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/sessions/", session_id), creds$public_key, creds$secret_key,
|
||||
method = "DELETE")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Freeze Session
|
||||
#'
|
||||
#' @param session_id Session ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
session_freeze <- function(session_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/sessions/", session_id, "/freeze"), creds$public_key, creds$secret_key,
|
||||
method = "POST")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Unfreeze Session
|
||||
#'
|
||||
#' @param session_id Session ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
session_unfreeze <- function(session_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/sessions/", session_id, "/unfreeze"), creds$public_key, creds$secret_key,
|
||||
method = "POST")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Boost Session
|
||||
#'
|
||||
#' @param session_id Session ID
|
||||
#' @param vcpu vCPU count
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
session_boost <- function(session_id, vcpu, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/sessions/", session_id), creds$public_key, creds$secret_key,
|
||||
method = "PATCH", data = list(vcpu = vcpu))
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Unboost Session
|
||||
#'
|
||||
#' @param session_id Session ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
session_unboost <- function(session_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/sessions/", session_id), creds$public_key, creds$secret_key,
|
||||
method = "PATCH", data = list(vcpu = 1))
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Execute Command in Session
|
||||
#'
|
||||
#' @param session_id Session ID
|
||||
#' @param command Command to execute
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Execution result
|
||||
#' @export
|
||||
session_execute <- function(session_id, command, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
return(api_request(paste0("/sessions/", session_id, "/execute"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list(command = command)))
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Service Library Functions
|
||||
# =============================================================================
|
||||
|
||||
#' List Services
|
||||
#'
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return A list of services
|
||||
#' @export
|
||||
service_list <- function(public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
result <- api_request("/services", creds$public_key, creds$secret_key)
|
||||
return(result$services %||% list())
|
||||
}
|
||||
|
||||
#' Get Service Details
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Service details
|
||||
#' @export
|
||||
service_get <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
return(api_request(paste0("/services/", service_id), creds$public_key, creds$secret_key))
|
||||
}
|
||||
|
||||
#' Create Service
|
||||
#'
|
||||
#' @param name Service name
|
||||
#' @param ports Comma-separated ports
|
||||
#' @param domains Comma-separated domains
|
||||
#' @param bootstrap Bootstrap command
|
||||
#' @param network Network mode
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Created service details
|
||||
#' @export
|
||||
service_create <- function(name, ports = NULL, domains = NULL, bootstrap = NULL,
|
||||
network = "semitrusted", public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- list(name = name)
|
||||
if (!is.null(ports)) payload$ports <- as.integer(strsplit(ports, ",")[[1]])
|
||||
if (!is.null(domains)) payload$domains <- strsplit(domains, ",")[[1]]
|
||||
if (!is.null(bootstrap)) payload$bootstrap <- bootstrap
|
||||
if (network != "semitrusted") payload$network <- network
|
||||
return(api_request("/services", creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload))
|
||||
}
|
||||
|
||||
#' Destroy Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_destroy <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request_with_sudo(paste0("/services/", service_id), creds$public_key, creds$secret_key,
|
||||
method = "DELETE")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Freeze Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_freeze <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/services/", service_id, "/freeze"), creds$public_key, creds$secret_key,
|
||||
method = "POST")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Unfreeze Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_unfreeze <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/services/", service_id, "/unfreeze"), creds$public_key, creds$secret_key,
|
||||
method = "POST")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Lock Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_lock <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/services/", service_id, "/lock"), creds$public_key, creds$secret_key,
|
||||
method = "POST")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Unlock Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_unlock <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request_with_sudo(paste0("/services/", service_id, "/unlock"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list())
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Set Unfreeze On Demand
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param enabled TRUE to enable
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_set_unfreeze_on_demand <- function(service_id, enabled, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/services/", service_id), creds$public_key, creds$secret_key,
|
||||
method = "PATCH", data = list(unfreeze_on_demand = enabled))
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Redeploy Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param bootstrap New bootstrap command (optional)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_redeploy <- function(service_id, bootstrap = NULL, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- if (!is.null(bootstrap)) list(bootstrap = bootstrap) else list()
|
||||
api_request(paste0("/services/", service_id, "/redeploy"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload)
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Get Service Logs
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param all_logs Get all logs (default: FALSE)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Logs string
|
||||
#' @export
|
||||
service_logs <- function(service_id, all_logs = FALSE, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
endpoint <- if (all_logs) paste0("/services/", service_id, "/logs?all=true")
|
||||
else paste0("/services/", service_id, "/logs")
|
||||
result <- api_request(endpoint, creds$public_key, creds$secret_key)
|
||||
return(result$logs %||% "")
|
||||
}
|
||||
|
||||
#' Execute Command in Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param command Command to execute
|
||||
#' @param timeout_ms Timeout in milliseconds
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Execution result
|
||||
#' @export
|
||||
service_execute <- function(service_id, command, timeout_ms = 30000,
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
return(api_request(paste0("/services/", service_id, "/execute"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list(command = command, timeout_ms = timeout_ms)))
|
||||
}
|
||||
|
||||
#' Get Service Environment
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Environment content
|
||||
#' @export
|
||||
service_env_get <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
result <- api_request(paste0("/services/", service_id, "/env"), creds$public_key, creds$secret_key)
|
||||
return(result$env %||% "")
|
||||
}
|
||||
|
||||
#' Set Service Environment
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param env_content Environment content
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_env_set <- function(service_id, env_content, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/services/", service_id, "/env"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list(env = env_content))
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Delete Service Environment
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_env_delete <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/services/", service_id, "/env"), creds$public_key, creds$secret_key,
|
||||
method = "DELETE")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Export Service Environment
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Exported environment
|
||||
#' @export
|
||||
service_env_export <- function(service_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
result <- api_request(paste0("/services/", service_id, "/env/export"), creds$public_key, creds$secret_key)
|
||||
return(result$export %||% "")
|
||||
}
|
||||
|
||||
#' Resize Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param vcpu vCPU count
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
service_resize <- function(service_id, vcpu, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/services/", service_id), creds$public_key, creds$secret_key,
|
||||
method = "PATCH", data = list(vcpu = vcpu))
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Snapshot Library Functions
|
||||
# =============================================================================
|
||||
|
||||
#' List Snapshots
|
||||
#'
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return A list of snapshots
|
||||
#' @export
|
||||
snapshot_list <- function(public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
result <- api_request("/snapshots", creds$public_key, creds$secret_key)
|
||||
return(result$snapshots %||% list())
|
||||
}
|
||||
|
||||
#' Get Snapshot Details
|
||||
#'
|
||||
#' @param snapshot_id Snapshot ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Snapshot details
|
||||
#' @export
|
||||
snapshot_get <- function(snapshot_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
return(api_request(paste0("/snapshots/", snapshot_id), creds$public_key, creds$secret_key))
|
||||
}
|
||||
|
||||
#' Snapshot Session
|
||||
#'
|
||||
#' @param session_id Session ID
|
||||
#' @param name Snapshot name (optional)
|
||||
#' @param hot Hot snapshot (default: FALSE)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Snapshot ID
|
||||
#' @export
|
||||
snapshot_session <- function(session_id, name = NULL, hot = FALSE,
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- list()
|
||||
if (!is.null(name)) payload$name <- name
|
||||
if (hot) payload$hot <- TRUE
|
||||
result <- api_request(paste0("/sessions/", session_id, "/snapshot"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload)
|
||||
return(result$id %||% "")
|
||||
}
|
||||
|
||||
#' Snapshot Service
|
||||
#'
|
||||
#' @param service_id Service ID
|
||||
#' @param name Snapshot name (optional)
|
||||
#' @param hot Hot snapshot (default: FALSE)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Snapshot ID
|
||||
#' @export
|
||||
snapshot_service <- function(service_id, name = NULL, hot = FALSE,
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- list()
|
||||
if (!is.null(name)) payload$name <- name
|
||||
if (hot) payload$hot <- TRUE
|
||||
result <- api_request(paste0("/services/", service_id, "/snapshot"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload)
|
||||
return(result$id %||% "")
|
||||
}
|
||||
|
||||
#' Restore Snapshot
|
||||
#'
|
||||
#' @param snapshot_id Snapshot ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
snapshot_restore <- function(snapshot_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/snapshots/", snapshot_id, "/restore"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list())
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Delete Snapshot
|
||||
#'
|
||||
#' @param snapshot_id Snapshot ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
snapshot_delete <- function(snapshot_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request_with_sudo(paste0("/snapshots/", snapshot_id), creds$public_key, creds$secret_key,
|
||||
method = "DELETE")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Lock Snapshot
|
||||
#'
|
||||
#' @param snapshot_id Snapshot ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
snapshot_lock <- function(snapshot_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/snapshots/", snapshot_id, "/lock"), creds$public_key, creds$secret_key,
|
||||
method = "POST")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Unlock Snapshot
|
||||
#'
|
||||
#' @param snapshot_id Snapshot ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
snapshot_unlock <- function(snapshot_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request_with_sudo(paste0("/snapshots/", snapshot_id, "/unlock"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list())
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Clone Snapshot
|
||||
#'
|
||||
#' @param snapshot_id Snapshot ID
|
||||
#' @param clone_type "session" or "service"
|
||||
#' @param name Name for cloned resource (optional)
|
||||
#' @param ports Ports for service (optional)
|
||||
#' @param shell Shell for session (optional)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Cloned resource ID
|
||||
#' @export
|
||||
snapshot_clone <- function(snapshot_id, clone_type, name = NULL, ports = NULL, shell = NULL,
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- list(type = clone_type)
|
||||
if (!is.null(name)) payload$name <- name
|
||||
if (!is.null(ports)) payload$ports <- as.integer(strsplit(ports, ",")[[1]])
|
||||
if (!is.null(shell)) payload$shell <- shell
|
||||
result <- api_request(paste0("/snapshots/", snapshot_id, "/clone"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload)
|
||||
return(result$id %||% "")
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Image Library Functions
|
||||
# =============================================================================
|
||||
|
||||
#' List Images
|
||||
#'
|
||||
#' @param filter Filter type (optional: "owned", "shared", "public")
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return A list of images
|
||||
#' @export
|
||||
image_list <- function(filter = NULL, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
endpoint <- if (!is.null(filter)) paste0("/images?filter=", filter) else "/images"
|
||||
result <- api_request(endpoint, creds$public_key, creds$secret_key)
|
||||
return(result$images %||% list())
|
||||
}
|
||||
|
||||
#' Get Image Details
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Image details
|
||||
#' @export
|
||||
image_get <- function(image_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
return(api_request(paste0("/images/", image_id), creds$public_key, creds$secret_key))
|
||||
}
|
||||
|
||||
#' Publish Image
|
||||
#'
|
||||
#' @param source_type "service" or "snapshot"
|
||||
#' @param source_id Source ID
|
||||
#' @param name Image name (optional)
|
||||
#' @param description Image description (optional)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Image ID
|
||||
#' @export
|
||||
image_publish <- function(source_type, source_id, name = NULL, description = NULL,
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- list(source_type = source_type, source_id = source_id)
|
||||
if (!is.null(name)) payload$name <- name
|
||||
if (!is.null(description)) payload$description <- description
|
||||
result <- api_request("/images/publish", creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload)
|
||||
return(result$id %||% "")
|
||||
}
|
||||
|
||||
#' Delete Image
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
image_delete <- function(image_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request_with_sudo(paste0("/images/", image_id), creds$public_key, creds$secret_key,
|
||||
method = "DELETE")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Lock Image
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
image_lock <- function(image_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/images/", image_id, "/lock"), creds$public_key, creds$secret_key,
|
||||
method = "POST")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Unlock Image
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
image_unlock <- function(image_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request_with_sudo(paste0("/images/", image_id, "/unlock"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list())
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Set Image Visibility
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param visibility "private", "unlisted", or "public"
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
image_set_visibility <- function(image_id, visibility, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/images/", image_id, "/visibility"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list(visibility = visibility))
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Grant Image Access
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param trusted_api_key API key to grant access
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
image_grant_access <- function(image_id, trusted_api_key, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/images/", image_id, "/access"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list(trusted_api_key = trusted_api_key))
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Revoke Image Access
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param trusted_api_key API key to revoke access
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
image_revoke_access <- function(image_id, trusted_api_key, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/images/", image_id, "/access/", trusted_api_key), creds$public_key, creds$secret_key,
|
||||
method = "DELETE")
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' List Trusted Keys for Image
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return List of trusted API keys
|
||||
#' @export
|
||||
image_list_trusted <- function(image_id, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
result <- api_request(paste0("/images/", image_id, "/access"), creds$public_key, creds$secret_key)
|
||||
return(result$trusted_keys %||% list())
|
||||
}
|
||||
|
||||
#' Transfer Image
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param to_api_key Target API key
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return TRUE on success
|
||||
#' @export
|
||||
image_transfer <- function(image_id, to_api_key, public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
api_request(paste0("/images/", image_id, "/transfer"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = list(to_api_key = to_api_key))
|
||||
return(TRUE)
|
||||
}
|
||||
|
||||
#' Spawn Service from Image
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param name Service name (optional)
|
||||
#' @param ports Comma-separated ports (optional)
|
||||
#' @param bootstrap Bootstrap command (optional)
|
||||
#' @param network Network mode (optional)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Service ID
|
||||
#' @export
|
||||
image_spawn <- function(image_id, name = NULL, ports = NULL, bootstrap = NULL, network = NULL,
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- list()
|
||||
if (!is.null(name)) payload$name <- name
|
||||
if (!is.null(ports)) payload$ports <- as.integer(strsplit(ports, ",")[[1]])
|
||||
if (!is.null(bootstrap)) payload$bootstrap <- bootstrap
|
||||
if (!is.null(network)) payload$network <- network
|
||||
result <- api_request(paste0("/images/", image_id, "/spawn"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload)
|
||||
return(result$id %||% "")
|
||||
}
|
||||
|
||||
#' Clone Image
|
||||
#'
|
||||
#' @param image_id Image ID
|
||||
#' @param name Clone name (optional)
|
||||
#' @param description Clone description (optional)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Cloned image ID
|
||||
#' @export
|
||||
image_clone <- function(image_id, name = NULL, description = NULL,
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
payload <- list()
|
||||
if (!is.null(name)) payload$name <- name
|
||||
if (!is.null(description)) payload$description <- description
|
||||
result <- api_request(paste0("/images/", image_id, "/clone"), creds$public_key, creds$secret_key,
|
||||
method = "POST", data = payload)
|
||||
return(result$id %||% "")
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# PaaS Logs Functions
|
||||
# =============================================================================
|
||||
|
||||
#' Fetch PaaS Logs
|
||||
#'
|
||||
#' @param source Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
#' @param lines Number of lines
|
||||
#' @param since Time window ("1m", "5m", "1h", "1d")
|
||||
#' @param grep Filter pattern (optional)
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Logs string
|
||||
#' @export
|
||||
logs_fetch <- function(source = "all", lines = 100, since = "1h", grep = NULL,
|
||||
public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
endpoint <- sprintf("/logs?source=%s&lines=%d&since=%s", source, lines, since)
|
||||
if (!is.null(grep)) endpoint <- paste0(endpoint, "&grep=", grep)
|
||||
result <- api_request(endpoint, creds$public_key, creds$secret_key)
|
||||
return(result$logs %||% "")
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Utility Functions
|
||||
# =============================================================================
|
||||
|
||||
#' Validate API Keys
|
||||
#'
|
||||
#' @param public_key API public key (optional)
|
||||
#' @param secret_key API secret key (optional)
|
||||
#' @return Validation result
|
||||
#' @export
|
||||
validate_keys <- function(public_key = NULL, secret_key = NULL) {
|
||||
creds <- get_credentials(public_key, secret_key)
|
||||
timestamp <- as.integer(Sys.time())
|
||||
body <- "{}"
|
||||
message <- paste(timestamp, "POST", "/keys/validate", body, sep = ":")
|
||||
signature <- compute_signature(creds$secret_key, message)
|
||||
|
||||
response <- tryCatch({
|
||||
POST(
|
||||
paste0(PORTAL_BASE, "/keys/validate"),
|
||||
add_headers(
|
||||
"Authorization" = paste("Bearer", creds$public_key),
|
||||
"X-Timestamp" = as.character(timestamp),
|
||||
"X-Signature" = signature,
|
||||
"Content-Type" = "application/json"
|
||||
),
|
||||
body = body,
|
||||
encode = "raw"
|
||||
)
|
||||
}, error = function(e) {
|
||||
return(list(valid = FALSE, error = conditionMessage(e)))
|
||||
})
|
||||
|
||||
if (inherits(response, "list")) return(response)
|
||||
return(content(response, as = "parsed"))
|
||||
}
|
||||
|
||||
#' Health Check
|
||||
#'
|
||||
#' @return TRUE if API is healthy
|
||||
#' @export
|
||||
health_check <- function() {
|
||||
tryCatch({
|
||||
response <- GET(paste0(API_BASE, "/health"))
|
||||
return(status_code(response) == 200)
|
||||
}, error = function(e) {
|
||||
return(FALSE)
|
||||
})
|
||||
}
|
||||
|
||||
#' Get SDK Version
|
||||
#'
|
||||
#' @return Version string
|
||||
#' @export
|
||||
sdk_version <- function() {
|
||||
return("1.0.0")
|
||||
}
|
||||
|
||||
#' HMAC Sign
|
||||
#'
|
||||
#' @param secret_key Secret key
|
||||
#' @param message Message to sign
|
||||
#' @return HMAC-SHA256 signature
|
||||
#' @export
|
||||
hmac_sign <- function(secret_key, message) {
|
||||
return(digest(message, algo = "sha256", key = secret_key, serialize = FALSE, hmac = TRUE))
|
||||
}
|
||||
|
||||
# Null coalescing operator
|
||||
`%||%` <- function(a, b) if (is.null(a)) b else a
|
||||
|
||||
# =============================================================================
|
||||
# Client Class (R6)
|
||||
# =============================================================================
|
||||
|
|
|
|||
215
clients/r/tests/test_un.r
Normal file
215
clients/r/tests/test_un.r
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
#!/usr/bin/env Rscript
|
||||
# Test suite for R Unsandbox SDK
|
||||
# Run: Rscript tests/test_un.r
|
||||
|
||||
# Load the SDK
|
||||
source(file.path(dirname(sys.frame(1)$ofile), "..", "sync", "src", "un.r"))
|
||||
|
||||
# Test helper
|
||||
test_that <- function(description, test_expr) {
|
||||
result <- tryCatch({
|
||||
test_expr
|
||||
TRUE
|
||||
}, error = function(e) {
|
||||
FALSE
|
||||
})
|
||||
status <- if (result) "\033[32mPASS\033[0m" else "\033[31mFAIL\033[0m"
|
||||
cat(sprintf("[%s] %s\n", status, description))
|
||||
return(result)
|
||||
}
|
||||
|
||||
all_passed <- TRUE
|
||||
|
||||
# Test Constants
|
||||
cat("\n=== Constants ===\n")
|
||||
all_passed <- all_passed && test_that("API_BASE is correct", {
|
||||
stopifnot(API_BASE == "https://api.unsandbox.com")
|
||||
})
|
||||
|
||||
all_passed <- all_passed && test_that("PORTAL_BASE is correct", {
|
||||
stopifnot(PORTAL_BASE == "https://unsandbox.com")
|
||||
})
|
||||
|
||||
# Test Language Detection
|
||||
cat("\n=== Language Detection ===\n")
|
||||
all_passed <- all_passed && test_that("detect Python", {
|
||||
stopifnot(detect_language("test.py") == "python")
|
||||
})
|
||||
|
||||
all_passed <- all_passed && test_that("detect JavaScript", {
|
||||
stopifnot(detect_language("test.js") == "javascript")
|
||||
})
|
||||
|
||||
all_passed <- all_passed && test_that("detect Ruby", {
|
||||
stopifnot(detect_language("test.rb") == "ruby")
|
||||
})
|
||||
|
||||
all_passed <- all_passed && test_that("detect Julia", {
|
||||
stopifnot(detect_language("test.jl") == "julia")
|
||||
})
|
||||
|
||||
all_passed <- all_passed && test_that("detect R", {
|
||||
stopifnot(detect_language("test.r") == "r")
|
||||
})
|
||||
|
||||
all_passed <- all_passed && test_that("detect unknown", {
|
||||
stopifnot(detect_language("test.xyz") == "unknown")
|
||||
})
|
||||
|
||||
# Test HMAC signing
|
||||
cat("\n=== HMAC Signing ===\n")
|
||||
all_passed <- all_passed && test_that("hmac_sign returns 64 char hex", {
|
||||
sig <- hmac_sign("secret", "message")
|
||||
stopifnot(is.character(sig))
|
||||
stopifnot(nchar(sig) == 64)
|
||||
})
|
||||
|
||||
all_passed <- all_passed && test_that("hmac_sign is deterministic", {
|
||||
sig1 <- hmac_sign("secret", "message")
|
||||
sig2 <- hmac_sign("secret", "message")
|
||||
stopifnot(sig1 == sig2)
|
||||
})
|
||||
|
||||
# Test Version
|
||||
cat("\n=== Version ===\n")
|
||||
all_passed <- all_passed && test_that("sdk_version returns string", {
|
||||
v <- sdk_version()
|
||||
stopifnot(is.character(v))
|
||||
stopifnot(nchar(v) > 0)
|
||||
})
|
||||
|
||||
# Test Library Functions Exist
|
||||
cat("\n=== Library Function Existence ===\n")
|
||||
|
||||
# Execution functions
|
||||
all_passed <- all_passed && test_that("execute function exists", {
|
||||
stopifnot(exists("execute") && is.function(execute))
|
||||
})
|
||||
all_passed <- all_passed && test_that("execute_async function exists", {
|
||||
stopifnot(exists("execute_async") && is.function(execute_async))
|
||||
})
|
||||
all_passed <- all_passed && test_that("get_job function exists", {
|
||||
stopifnot(exists("get_job") && is.function(get_job))
|
||||
})
|
||||
all_passed <- all_passed && test_that("wait function exists", {
|
||||
stopifnot(exists("wait") && is.function(wait))
|
||||
})
|
||||
all_passed <- all_passed && test_that("cancel_job function exists", {
|
||||
stopifnot(exists("cancel_job") && is.function(cancel_job))
|
||||
})
|
||||
all_passed <- all_passed && test_that("list_jobs function exists", {
|
||||
stopifnot(exists("list_jobs") && is.function(list_jobs))
|
||||
})
|
||||
all_passed <- all_passed && test_that("languages function exists", {
|
||||
stopifnot(exists("languages") && is.function(languages))
|
||||
})
|
||||
|
||||
# Session functions
|
||||
all_passed <- all_passed && test_that("session_list function exists", {
|
||||
stopifnot(exists("session_list") && is.function(session_list))
|
||||
})
|
||||
all_passed <- all_passed && test_that("session_get function exists", {
|
||||
stopifnot(exists("session_get") && is.function(session_get))
|
||||
})
|
||||
all_passed <- all_passed && test_that("session_create function exists", {
|
||||
stopifnot(exists("session_create") && is.function(session_create))
|
||||
})
|
||||
all_passed <- all_passed && test_that("session_destroy function exists", {
|
||||
stopifnot(exists("session_destroy") && is.function(session_destroy))
|
||||
})
|
||||
all_passed <- all_passed && test_that("session_freeze function exists", {
|
||||
stopifnot(exists("session_freeze") && is.function(session_freeze))
|
||||
})
|
||||
all_passed <- all_passed && test_that("session_unfreeze function exists", {
|
||||
stopifnot(exists("session_unfreeze") && is.function(session_unfreeze))
|
||||
})
|
||||
all_passed <- all_passed && test_that("session_execute function exists", {
|
||||
stopifnot(exists("session_execute") && is.function(session_execute))
|
||||
})
|
||||
|
||||
# Service functions
|
||||
all_passed <- all_passed && test_that("service_list function exists", {
|
||||
stopifnot(exists("service_list") && is.function(service_list))
|
||||
})
|
||||
all_passed <- all_passed && test_that("service_get function exists", {
|
||||
stopifnot(exists("service_get") && is.function(service_get))
|
||||
})
|
||||
all_passed <- all_passed && test_that("service_create function exists", {
|
||||
stopifnot(exists("service_create") && is.function(service_create))
|
||||
})
|
||||
all_passed <- all_passed && test_that("service_destroy function exists", {
|
||||
stopifnot(exists("service_destroy") && is.function(service_destroy))
|
||||
})
|
||||
all_passed <- all_passed && test_that("service_env_get function exists", {
|
||||
stopifnot(exists("service_env_get") && is.function(service_env_get))
|
||||
})
|
||||
all_passed <- all_passed && test_that("service_env_set function exists", {
|
||||
stopifnot(exists("service_env_set") && is.function(service_env_set))
|
||||
})
|
||||
all_passed <- all_passed && test_that("service_env_delete function exists", {
|
||||
stopifnot(exists("service_env_delete") && is.function(service_env_delete))
|
||||
})
|
||||
|
||||
# Snapshot functions
|
||||
all_passed <- all_passed && test_that("snapshot_list function exists", {
|
||||
stopifnot(exists("snapshot_list") && is.function(snapshot_list))
|
||||
})
|
||||
all_passed <- all_passed && test_that("snapshot_get function exists", {
|
||||
stopifnot(exists("snapshot_get") && is.function(snapshot_get))
|
||||
})
|
||||
all_passed <- all_passed && test_that("snapshot_session function exists", {
|
||||
stopifnot(exists("snapshot_session") && is.function(snapshot_session))
|
||||
})
|
||||
all_passed <- all_passed && test_that("snapshot_service function exists", {
|
||||
stopifnot(exists("snapshot_service") && is.function(snapshot_service))
|
||||
})
|
||||
all_passed <- all_passed && test_that("snapshot_restore function exists", {
|
||||
stopifnot(exists("snapshot_restore") && is.function(snapshot_restore))
|
||||
})
|
||||
all_passed <- all_passed && test_that("snapshot_delete function exists", {
|
||||
stopifnot(exists("snapshot_delete") && is.function(snapshot_delete))
|
||||
})
|
||||
all_passed <- all_passed && test_that("snapshot_clone function exists", {
|
||||
stopifnot(exists("snapshot_clone") && is.function(snapshot_clone))
|
||||
})
|
||||
|
||||
# Image functions
|
||||
all_passed <- all_passed && test_that("image_list function exists", {
|
||||
stopifnot(exists("image_list") && is.function(image_list))
|
||||
})
|
||||
all_passed <- all_passed && test_that("image_get function exists", {
|
||||
stopifnot(exists("image_get") && is.function(image_get))
|
||||
})
|
||||
all_passed <- all_passed && test_that("image_publish function exists", {
|
||||
stopifnot(exists("image_publish") && is.function(image_publish))
|
||||
})
|
||||
all_passed <- all_passed && test_that("image_delete function exists", {
|
||||
stopifnot(exists("image_delete") && is.function(image_delete))
|
||||
})
|
||||
all_passed <- all_passed && test_that("image_spawn function exists", {
|
||||
stopifnot(exists("image_spawn") && is.function(image_spawn))
|
||||
})
|
||||
all_passed <- all_passed && test_that("image_clone function exists", {
|
||||
stopifnot(exists("image_clone") && is.function(image_clone))
|
||||
})
|
||||
|
||||
# Utility functions
|
||||
all_passed <- all_passed && test_that("validate_keys function exists", {
|
||||
stopifnot(exists("validate_keys") && is.function(validate_keys))
|
||||
})
|
||||
all_passed <- all_passed && test_that("health_check function exists", {
|
||||
stopifnot(exists("health_check") && is.function(health_check))
|
||||
})
|
||||
all_passed <- all_passed && test_that("logs_fetch function exists", {
|
||||
stopifnot(exists("logs_fetch") && is.function(logs_fetch))
|
||||
})
|
||||
|
||||
# Summary
|
||||
cat("\n=== Summary ===\n")
|
||||
if (all_passed) {
|
||||
cat("\033[32mAll tests passed!\033[0m\n")
|
||||
quit(status = 0)
|
||||
} else {
|
||||
cat("\033[31mSome tests failed!\033[0m\n")
|
||||
quit(status = 1)
|
||||
}
|
||||
|
|
@ -712,6 +712,488 @@ sub languages(Str :$public-key, Str :$secret-key) returns Hash is export {
|
|||
return %result;
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Session Functions
|
||||
# ============================================================================
|
||||
|
||||
#| List all sessions for this API key
|
||||
sub session-list(Str :$public-key, Str :$secret-key) returns Array is export {
|
||||
my %result = api-request('/sessions', 'GET', :$public-key, :$secret-key);
|
||||
return %result<sessions> // [];
|
||||
}
|
||||
|
||||
#| Get session details by ID
|
||||
sub session-get(Str $session-id, Str :$public-key, Str :$secret-key) returns Hash is export {
|
||||
return api-request("/sessions/{$session-id}", 'GET', :$public-key, :$secret-key);
|
||||
}
|
||||
|
||||
#| Create a new interactive session
|
||||
sub session-create(
|
||||
Str :$network-mode = 'zerotrust',
|
||||
Str :$shell = 'bash',
|
||||
Str :$public-key,
|
||||
Str :$secret-key
|
||||
) returns Hash is export {
|
||||
my %payload = shell => $shell, network_mode => $network-mode;
|
||||
return api-request('/sessions', 'POST', %payload, :$public-key, :$secret-key);
|
||||
}
|
||||
|
||||
#| Destroy a session
|
||||
sub session-destroy(Str $session-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/sessions/{$session-id}", 'DELETE', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Freeze a session
|
||||
sub session-freeze(Str $session-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/sessions/{$session-id}/freeze", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Unfreeze a session
|
||||
sub session-unfreeze(Str $session-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/sessions/{$session-id}/unfreeze", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Boost session CPU (1-8 vCPUs)
|
||||
sub session-boost(Str $session-id, Int $vcpu, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my %payload = vcpu => $vcpu;
|
||||
api-request("/sessions/{$session-id}/boost", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Remove CPU boost from session
|
||||
sub session-unboost(Str $session-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/sessions/{$session-id}/unboost", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Execute command in a session
|
||||
sub session-execute(Str $session-id, Str $command, Str :$public-key, Str :$secret-key) returns Hash is export {
|
||||
my %payload = command => $command;
|
||||
return api-request("/sessions/{$session-id}/execute", 'POST', %payload, :$public-key, :$secret-key);
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Service Functions
|
||||
# ============================================================================
|
||||
|
||||
#| List all services for this API key
|
||||
sub service-list(Str :$public-key, Str :$secret-key) returns Array is export {
|
||||
my %result = api-request('/services', 'GET', :$public-key, :$secret-key);
|
||||
return %result<services> // [];
|
||||
}
|
||||
|
||||
#| Get service details by ID
|
||||
sub service-get(Str $service-id, Str :$public-key, Str :$secret-key) returns Hash is export {
|
||||
return api-request("/services/{$service-id}", 'GET', :$public-key, :$secret-key);
|
||||
}
|
||||
|
||||
#| Create a new service
|
||||
sub service-create(
|
||||
Str :$name!,
|
||||
Str :$ports,
|
||||
Str :$domains,
|
||||
Str :$bootstrap,
|
||||
Str :$network-mode,
|
||||
Str :$public-key,
|
||||
Str :$secret-key
|
||||
) returns Str is export {
|
||||
my %payload = name => $name;
|
||||
%payload<ports> = $ports.split(',')>>.Int if $ports;
|
||||
%payload<domains> = $domains.split(',') if $domains;
|
||||
%payload<bootstrap> = $bootstrap if $bootstrap;
|
||||
%payload<network_mode> = $network-mode if $network-mode;
|
||||
my %result = api-request('/services', 'POST', %payload, :$public-key, :$secret-key);
|
||||
return %result<id> // '';
|
||||
}
|
||||
|
||||
#| Destroy a service
|
||||
sub service-destroy(Str $service-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/services/{$service-id}", 'DELETE', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Freeze a service
|
||||
sub service-freeze(Str $service-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/services/{$service-id}/freeze", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Unfreeze a service
|
||||
sub service-unfreeze(Str $service-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/services/{$service-id}/unfreeze", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Lock a service (prevent deletion)
|
||||
sub service-lock(Str $service-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/services/{$service-id}/lock", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Unlock a service
|
||||
sub service-unlock(Str $service-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/services/{$service-id}/unlock", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Set unfreeze-on-demand for a service
|
||||
sub service-set-unfreeze-on-demand(Str $service-id, Bool $enabled, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my %payload = unfreeze_on_demand => $enabled;
|
||||
api-request("/services/{$service-id}", 'PATCH', %payload, :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Redeploy a service with optional new bootstrap
|
||||
sub service-redeploy(Str $service-id, Str :$bootstrap, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my %payload;
|
||||
%payload<bootstrap> = $bootstrap if $bootstrap;
|
||||
api-request("/services/{$service-id}/redeploy", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Get service logs
|
||||
sub service-logs(Str $service-id, Bool :$all = False, Str :$public-key, Str :$secret-key) returns Str is export {
|
||||
my $endpoint = "/services/{$service-id}/logs";
|
||||
$endpoint ~= "?all=true" if $all;
|
||||
my %result = api-request($endpoint, 'GET', :$public-key, :$secret-key);
|
||||
return %result<logs> // '';
|
||||
}
|
||||
|
||||
#| Execute command in a service
|
||||
sub service-execute(Str $service-id, Str $command, Int :$timeout-ms, Str :$public-key, Str :$secret-key) returns Hash is export {
|
||||
my %payload = command => $command;
|
||||
%payload<timeout_ms> = $timeout-ms if $timeout-ms;
|
||||
return api-request("/services/{$service-id}/execute", 'POST', %payload, :$public-key, :$secret-key);
|
||||
}
|
||||
|
||||
#| Get service environment vault
|
||||
sub service-env-get(Str $service-id, Str :$public-key, Str :$secret-key) returns Str is export {
|
||||
my %result = api-request("/services/{$service-id}/env", 'GET', :$public-key, :$secret-key);
|
||||
return %result<content> // '';
|
||||
}
|
||||
|
||||
#| Set service environment vault
|
||||
sub service-env-set(Str $service-id, Str $content, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my ($pk, $sk) = get-credentials(:$public-key, :$secret-key);
|
||||
my $url = "{$API_BASE}/services/{$service-id}/env";
|
||||
my $timestamp = now.Int;
|
||||
my $signature = sign-request($sk, $timestamp, 'PUT', "/services/{$service-id}/env", $content);
|
||||
|
||||
my @args = 'curl', '-s', '-X', 'PUT', $url;
|
||||
@args.append: '-H', 'Content-Type: text/plain';
|
||||
@args.append: '-H', "Authorization: Bearer $pk";
|
||||
@args.append: '-H', "X-Timestamp: $timestamp";
|
||||
@args.append: '-H', "X-Signature: $signature";
|
||||
@args.append: '-d', $content;
|
||||
|
||||
my $proc = run |@args, :out, :err;
|
||||
return $proc.exitcode == 0;
|
||||
}
|
||||
|
||||
#| Delete service environment vault
|
||||
sub service-env-delete(Str $service-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/services/{$service-id}/env", 'DELETE', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Export service environment vault
|
||||
sub service-env-export(Str $service-id, Str :$public-key, Str :$secret-key) returns Str is export {
|
||||
my %result = api-request("/services/{$service-id}/env/export", 'POST', :$public-key, :$secret-key);
|
||||
return %result<content> // '';
|
||||
}
|
||||
|
||||
#| Resize service (change vCPU count)
|
||||
sub service-resize(Str $service-id, Int $vcpu, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my %payload = vcpu => $vcpu;
|
||||
api-request("/services/{$service-id}", 'PATCH', %payload, :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Snapshot Functions
|
||||
# ============================================================================
|
||||
|
||||
#| List all snapshots for this API key
|
||||
sub snapshot-list(Str :$public-key, Str :$secret-key) returns Array is export {
|
||||
my %result = api-request('/snapshots', 'GET', :$public-key, :$secret-key);
|
||||
return %result<snapshots> // [];
|
||||
}
|
||||
|
||||
#| Get snapshot details by ID
|
||||
sub snapshot-get(Str $snapshot-id, Str :$public-key, Str :$secret-key) returns Hash is export {
|
||||
return api-request("/snapshots/{$snapshot-id}", 'GET', :$public-key, :$secret-key);
|
||||
}
|
||||
|
||||
#| Create snapshot from a session
|
||||
sub snapshot-session(Str $session-id, Str :$name, Bool :$hot = False, Str :$public-key, Str :$secret-key) returns Str is export {
|
||||
my %payload = session_id => $session-id, hot => $hot;
|
||||
%payload<name> = $name if $name;
|
||||
my %result = api-request('/snapshots', 'POST', %payload, :$public-key, :$secret-key);
|
||||
return %result<id> // '';
|
||||
}
|
||||
|
||||
#| Create snapshot from a service
|
||||
sub snapshot-service(Str $service-id, Str :$name, Bool :$hot = False, Str :$public-key, Str :$secret-key) returns Str is export {
|
||||
my %payload = service_id => $service-id, hot => $hot;
|
||||
%payload<name> = $name if $name;
|
||||
my %result = api-request('/snapshots', 'POST', %payload, :$public-key, :$secret-key);
|
||||
return %result<id> // '';
|
||||
}
|
||||
|
||||
#| Restore a snapshot
|
||||
sub snapshot-restore(Str $snapshot-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/snapshots/{$snapshot-id}/restore", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Delete a snapshot
|
||||
sub snapshot-delete(Str $snapshot-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/snapshots/{$snapshot-id}", 'DELETE', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Lock a snapshot (prevent deletion)
|
||||
sub snapshot-lock(Str $snapshot-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/snapshots/{$snapshot-id}/lock", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Unlock a snapshot
|
||||
sub snapshot-unlock(Str $snapshot-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/snapshots/{$snapshot-id}/unlock", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Clone a snapshot to create a new session or service
|
||||
sub snapshot-clone(
|
||||
Str $snapshot-id,
|
||||
Str :$clone-type!, # "session" or "service"
|
||||
Str :$name, # name for cloned service
|
||||
Str :$ports, # ports for cloned service
|
||||
Str :$shell, # shell for cloned session
|
||||
Str :$public-key,
|
||||
Str :$secret-key
|
||||
) returns Str is export {
|
||||
my %payload = clone_type => $clone-type;
|
||||
%payload<name> = $name if $name;
|
||||
%payload<ports> = $ports.split(',')>>.Int if $ports;
|
||||
%payload<shell> = $shell if $shell;
|
||||
my %result = api-request("/snapshots/{$snapshot-id}/clone", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return %result<id> // '';
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Image Functions
|
||||
# ============================================================================
|
||||
|
||||
#| List images (filter: owned, shared, public, or all)
|
||||
sub image-list(Str :$filter, Str :$public-key, Str :$secret-key) returns Array is export {
|
||||
my $endpoint = '/images';
|
||||
$endpoint ~= "?filter={$filter}" if $filter;
|
||||
my %result = api-request($endpoint, 'GET', :$public-key, :$secret-key);
|
||||
return %result<images> // [];
|
||||
}
|
||||
|
||||
#| Get image details by ID
|
||||
sub image-get(Str $image-id, Str :$public-key, Str :$secret-key) returns Hash is export {
|
||||
return api-request("/images/{$image-id}", 'GET', :$public-key, :$secret-key);
|
||||
}
|
||||
|
||||
#| Publish an image from a service or snapshot
|
||||
sub image-publish(
|
||||
Str :$source-type!, # "service" or "snapshot"
|
||||
Str :$source-id!,
|
||||
Str :$name,
|
||||
Str :$description,
|
||||
Str :$public-key,
|
||||
Str :$secret-key
|
||||
) returns Str is export {
|
||||
my %payload = source_type => $source-type, source_id => $source-id;
|
||||
%payload<name> = $name if $name;
|
||||
%payload<description> = $description if $description;
|
||||
my %result = api-request('/images/publish', 'POST', %payload, :$public-key, :$secret-key);
|
||||
return %result<id> // '';
|
||||
}
|
||||
|
||||
#| Delete an image
|
||||
sub image-delete(Str $image-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/images/{$image-id}", 'DELETE', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Lock an image (prevent deletion)
|
||||
sub image-lock(Str $image-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/images/{$image-id}/lock", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Unlock an image
|
||||
sub image-unlock(Str $image-id, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
api-request("/images/{$image-id}/unlock", 'POST', :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Set image visibility (private, unlisted, public)
|
||||
sub image-set-visibility(Str $image-id, Str $visibility, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my %payload = visibility => $visibility;
|
||||
api-request("/images/{$image-id}/visibility", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Grant access to an image for another API key
|
||||
sub image-grant-access(Str $image-id, Str $trusted-api-key, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my %payload = trusted_api_key => $trusted-api-key;
|
||||
api-request("/images/{$image-id}/access/grant", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Revoke access to an image from another API key
|
||||
sub image-revoke-access(Str $image-id, Str $trusted-api-key, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my %payload = trusted_api_key => $trusted-api-key;
|
||||
api-request("/images/{$image-id}/access/revoke", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| List trusted API keys for an image
|
||||
sub image-list-trusted(Str $image-id, Str :$public-key, Str :$secret-key) returns Array is export {
|
||||
my %result = api-request("/images/{$image-id}/access", 'GET', :$public-key, :$secret-key);
|
||||
return %result<trusted_keys> // [];
|
||||
}
|
||||
|
||||
#| Transfer image ownership to another API key
|
||||
sub image-transfer(Str $image-id, Str $to-api-key, Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
my %payload = to_api_key => $to-api-key;
|
||||
api-request("/images/{$image-id}/transfer", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return True;
|
||||
}
|
||||
|
||||
#| Spawn a new service from an image
|
||||
sub image-spawn(
|
||||
Str $image-id,
|
||||
Str :$name,
|
||||
Str :$ports,
|
||||
Str :$bootstrap,
|
||||
Str :$network-mode,
|
||||
Str :$public-key,
|
||||
Str :$secret-key
|
||||
) returns Str is export {
|
||||
my %payload;
|
||||
%payload<name> = $name if $name;
|
||||
%payload<ports> = $ports.split(',')>>.Int if $ports;
|
||||
%payload<bootstrap> = $bootstrap if $bootstrap;
|
||||
%payload<network_mode> = $network-mode if $network-mode;
|
||||
my %result = api-request("/images/{$image-id}/spawn", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return %result<id> // '';
|
||||
}
|
||||
|
||||
#| Clone an image
|
||||
sub image-clone(Str $image-id, Str :$name, Str :$description, Str :$public-key, Str :$secret-key) returns Str is export {
|
||||
my %payload;
|
||||
%payload<name> = $name if $name;
|
||||
%payload<description> = $description if $description;
|
||||
my %result = api-request("/images/{$image-id}/clone", 'POST', %payload, :$public-key, :$secret-key);
|
||||
return %result<id> // '';
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# PaaS Logs Functions
|
||||
# ============================================================================
|
||||
|
||||
#| Fetch batch logs from portal
|
||||
#| source: "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
#| lines: number of lines (1-10000)
|
||||
#| since: time window ("1m", "5m", "1h", "1d")
|
||||
#| grep: optional filter pattern
|
||||
sub logs-fetch(
|
||||
Str :$source = 'all',
|
||||
Int :$lines = 100,
|
||||
Str :$since = '1h',
|
||||
Str :$grep,
|
||||
Str :$public-key,
|
||||
Str :$secret-key
|
||||
) returns Str is export {
|
||||
my $endpoint = "/logs?source={$source}&lines={$lines}&since={$since}";
|
||||
$endpoint ~= "&grep={uri-encode($grep)}" if $grep;
|
||||
my %result = api-request($endpoint, 'GET', :$public-key, :$secret-key);
|
||||
return to-json(%result);
|
||||
}
|
||||
|
||||
#| Stream logs via SSE (blocking call)
|
||||
#| Returns when interrupted or server closes connection
|
||||
sub logs-stream(
|
||||
Str :$source = 'all',
|
||||
Str :$grep,
|
||||
:&callback,
|
||||
Str :$public-key,
|
||||
Str :$secret-key
|
||||
) returns Bool is export {
|
||||
my ($pk, $sk) = get-credentials(:$public-key, :$secret-key);
|
||||
my $endpoint = "/logs/stream?source={$source}";
|
||||
$endpoint ~= "&grep={uri-encode($grep)}" if $grep;
|
||||
|
||||
my $timestamp = now.Int;
|
||||
my $signature = sign-request($sk, $timestamp, 'GET', $endpoint, '');
|
||||
|
||||
my @args = 'curl', '-s', '-N', "{$API_BASE}{$endpoint}";
|
||||
@args.append: '-H', "Authorization: Bearer $pk";
|
||||
@args.append: '-H', "X-Timestamp: $timestamp";
|
||||
@args.append: '-H', "X-Signature: $signature";
|
||||
@args.append: '-H', 'Accept: text/event-stream';
|
||||
|
||||
my $proc = run |@args, :out;
|
||||
for $proc.out.lines -> $line {
|
||||
if $line.starts-with('data: ') {
|
||||
my $data = $line.substr(6);
|
||||
&callback($source, $data) if &callback;
|
||||
}
|
||||
}
|
||||
return True;
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Key Validation
|
||||
# ============================================================================
|
||||
|
||||
#| Validate API keys and get account info
|
||||
sub validate-keys(Str :$public-key, Str :$secret-key) returns Hash is export {
|
||||
my ($pk, $sk) = get-credentials(:$public-key, :$secret-key);
|
||||
|
||||
my $url = "{$PORTAL_BASE}/keys/validate";
|
||||
my $timestamp = now.Int;
|
||||
my $signature = sign-request($sk, $timestamp, 'POST', '/keys/validate', '');
|
||||
|
||||
my @args = 'curl', '-s', '-X', 'POST', $url;
|
||||
@args.append: '-H', 'Content-Type: application/json';
|
||||
@args.append: '-H', "Authorization: Bearer $pk";
|
||||
@args.append: '-H', "X-Timestamp: $timestamp";
|
||||
@args.append: '-H', "X-Signature: $signature";
|
||||
|
||||
my $proc = run |@args, :out;
|
||||
my $resp = $proc.out.slurp;
|
||||
return from-json($resp);
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Utility Functions
|
||||
# ============================================================================
|
||||
|
||||
#| Check API health
|
||||
sub health-check(Str :$public-key, Str :$secret-key) returns Bool is export {
|
||||
try {
|
||||
my %result = api-request('/health', 'GET', :$public-key, :$secret-key);
|
||||
return %result<status> eq 'ok';
|
||||
CATCH { default { return False; } }
|
||||
}
|
||||
}
|
||||
|
||||
#| Get SDK version
|
||||
sub version() returns Str is export {
|
||||
return '0.1.0';
|
||||
}
|
||||
|
||||
#| Detect programming language from file extension or shebang
|
||||
#|
|
||||
#| Returns: Language name or Nil if undetected
|
||||
|
|
@ -814,6 +1296,66 @@ class Client is export {
|
|||
method languages() returns Hash {
|
||||
return languages(:$.public-key, :$.secret-key);
|
||||
}
|
||||
|
||||
# Session methods
|
||||
method session-list() returns Array { return session-list(:$.public-key, :$.secret-key); }
|
||||
method session-get(Str $id) returns Hash { return session-get($id, :$.public-key, :$.secret-key); }
|
||||
method session-create(*%opts) returns Hash { return session-create(:$.public-key, :$.secret-key, |%opts); }
|
||||
method session-destroy(Str $id) returns Bool { return session-destroy($id, :$.public-key, :$.secret-key); }
|
||||
method session-freeze(Str $id) returns Bool { return session-freeze($id, :$.public-key, :$.secret-key); }
|
||||
method session-unfreeze(Str $id) returns Bool { return session-unfreeze($id, :$.public-key, :$.secret-key); }
|
||||
method session-boost(Str $id, Int $vcpu) returns Bool { return session-boost($id, $vcpu, :$.public-key, :$.secret-key); }
|
||||
method session-unboost(Str $id) returns Bool { return session-unboost($id, :$.public-key, :$.secret-key); }
|
||||
method session-execute(Str $id, Str $cmd) returns Hash { return session-execute($id, $cmd, :$.public-key, :$.secret-key); }
|
||||
|
||||
# Service methods
|
||||
method service-list() returns Array { return service-list(:$.public-key, :$.secret-key); }
|
||||
method service-get(Str $id) returns Hash { return service-get($id, :$.public-key, :$.secret-key); }
|
||||
method service-create(*%opts) returns Str { return service-create(:$.public-key, :$.secret-key, |%opts); }
|
||||
method service-destroy(Str $id) returns Bool { return service-destroy($id, :$.public-key, :$.secret-key); }
|
||||
method service-freeze(Str $id) returns Bool { return service-freeze($id, :$.public-key, :$.secret-key); }
|
||||
method service-unfreeze(Str $id) returns Bool { return service-unfreeze($id, :$.public-key, :$.secret-key); }
|
||||
method service-lock(Str $id) returns Bool { return service-lock($id, :$.public-key, :$.secret-key); }
|
||||
method service-unlock(Str $id) returns Bool { return service-unlock($id, :$.public-key, :$.secret-key); }
|
||||
method service-redeploy(Str $id, *%opts) returns Bool { return service-redeploy($id, :$.public-key, :$.secret-key, |%opts); }
|
||||
method service-logs(Str $id, *%opts) returns Str { return service-logs($id, :$.public-key, :$.secret-key, |%opts); }
|
||||
method service-execute(Str $id, Str $cmd, *%opts) returns Hash { return service-execute($id, $cmd, :$.public-key, :$.secret-key, |%opts); }
|
||||
method service-resize(Str $id, Int $vcpu) returns Bool { return service-resize($id, $vcpu, :$.public-key, :$.secret-key); }
|
||||
|
||||
# Snapshot methods
|
||||
method snapshot-list() returns Array { return snapshot-list(:$.public-key, :$.secret-key); }
|
||||
method snapshot-get(Str $id) returns Hash { return snapshot-get($id, :$.public-key, :$.secret-key); }
|
||||
method snapshot-session(Str $id, *%opts) returns Str { return snapshot-session($id, :$.public-key, :$.secret-key, |%opts); }
|
||||
method snapshot-service(Str $id, *%opts) returns Str { return snapshot-service($id, :$.public-key, :$.secret-key, |%opts); }
|
||||
method snapshot-restore(Str $id) returns Bool { return snapshot-restore($id, :$.public-key, :$.secret-key); }
|
||||
method snapshot-delete(Str $id) returns Bool { return snapshot-delete($id, :$.public-key, :$.secret-key); }
|
||||
method snapshot-lock(Str $id) returns Bool { return snapshot-lock($id, :$.public-key, :$.secret-key); }
|
||||
method snapshot-unlock(Str $id) returns Bool { return snapshot-unlock($id, :$.public-key, :$.secret-key); }
|
||||
method snapshot-clone(Str $id, *%opts) returns Str { return snapshot-clone($id, :$.public-key, :$.secret-key, |%opts); }
|
||||
|
||||
# Image methods
|
||||
method image-list(*%opts) returns Array { return image-list(:$.public-key, :$.secret-key, |%opts); }
|
||||
method image-get(Str $id) returns Hash { return image-get($id, :$.public-key, :$.secret-key); }
|
||||
method image-publish(*%opts) returns Str { return image-publish(:$.public-key, :$.secret-key, |%opts); }
|
||||
method image-delete(Str $id) returns Bool { return image-delete($id, :$.public-key, :$.secret-key); }
|
||||
method image-lock(Str $id) returns Bool { return image-lock($id, :$.public-key, :$.secret-key); }
|
||||
method image-unlock(Str $id) returns Bool { return image-unlock($id, :$.public-key, :$.secret-key); }
|
||||
method image-set-visibility(Str $id, Str $v) returns Bool { return image-set-visibility($id, $v, :$.public-key, :$.secret-key); }
|
||||
method image-grant-access(Str $id, Str $key) returns Bool { return image-grant-access($id, $key, :$.public-key, :$.secret-key); }
|
||||
method image-revoke-access(Str $id, Str $key) returns Bool { return image-revoke-access($id, $key, :$.public-key, :$.secret-key); }
|
||||
method image-list-trusted(Str $id) returns Array { return image-list-trusted($id, :$.public-key, :$.secret-key); }
|
||||
method image-transfer(Str $id, Str $key) returns Bool { return image-transfer($id, $key, :$.public-key, :$.secret-key); }
|
||||
method image-spawn(Str $id, *%opts) returns Str { return image-spawn($id, :$.public-key, :$.secret-key, |%opts); }
|
||||
method image-clone(Str $id, *%opts) returns Str { return image-clone($id, :$.public-key, :$.secret-key, |%opts); }
|
||||
|
||||
# Logs methods
|
||||
method logs-fetch(*%opts) returns Str { return logs-fetch(:$.public-key, :$.secret-key, |%opts); }
|
||||
method logs-stream(*%opts) returns Bool { return logs-stream(:$.public-key, :$.secret-key, |%opts); }
|
||||
|
||||
# Utility methods
|
||||
method validate-keys() returns Hash { return validate-keys(:$.public-key, :$.secret-key); }
|
||||
method health-check() returns Bool { return health-check(:$.public-key, :$.secret-key); }
|
||||
method version() returns Str { return version(); }
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
|
|
@ -1548,11 +2090,146 @@ sub cmd-image(@args) {
|
|||
exit 1;
|
||||
}
|
||||
|
||||
sub cmd-snapshot(@args) {
|
||||
my ($public-key, $secret-key) = get-credentials();
|
||||
my $list-mode = False;
|
||||
my $info-id = '';
|
||||
my $delete-id = '';
|
||||
my $lock-id = '';
|
||||
my $unlock-id = '';
|
||||
my $restore-id = '';
|
||||
my $clone-id = '';
|
||||
my $session-id = '';
|
||||
my $service-id = '';
|
||||
my $name = '';
|
||||
my $hot = False;
|
||||
my $clone-type = '';
|
||||
my $ports = '';
|
||||
my $shell = '';
|
||||
|
||||
# Parse arguments
|
||||
my $i = 0;
|
||||
while $i < @args.elems {
|
||||
given @args[$i] {
|
||||
when '--list' | '-l' { $list-mode = True; }
|
||||
when '--info' { $i++; $info-id = @args[$i]; }
|
||||
when '--delete' { $i++; $delete-id = @args[$i]; }
|
||||
when '--lock' { $i++; $lock-id = @args[$i]; }
|
||||
when '--unlock' { $i++; $unlock-id = @args[$i]; }
|
||||
when '--restore' { $i++; $restore-id = @args[$i]; }
|
||||
when '--clone' { $i++; $clone-id = @args[$i]; }
|
||||
when '--session' { $i++; $session-id = @args[$i]; }
|
||||
when '--service' { $i++; $service-id = @args[$i]; }
|
||||
when '--name' { $i++; $name = @args[$i]; }
|
||||
when '--hot' { $hot = True; }
|
||||
when '--clone-type' { $i++; $clone-type = @args[$i]; }
|
||||
when '--ports' { $i++; $ports = @args[$i]; }
|
||||
when '--shell' { $i++; $shell = @args[$i]; }
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
if $list-mode {
|
||||
my @snapshots = snapshot-list(:$public-key, :$secret-key);
|
||||
unless @snapshots {
|
||||
say "No snapshots found";
|
||||
return;
|
||||
}
|
||||
say sprintf("%-40s %-20s %-10s %-10s %s", 'ID', 'Name', 'Type', 'Locked', 'Created');
|
||||
for @snapshots -> %s {
|
||||
say sprintf("%-40s %-20s %-10s %-10s %s",
|
||||
%s<id> // 'N/A', %s<name> // '-', %s<type> // 'N/A',
|
||||
%s<locked> ?? 'Yes' !! 'No', %s<created_at> // 'N/A');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if $info-id {
|
||||
my %result = snapshot-get($info-id, :$public-key, :$secret-key);
|
||||
say to-json(%result, :pretty);
|
||||
return;
|
||||
}
|
||||
|
||||
if $delete-id {
|
||||
my ($status, $body) = api-request-with-status("/snapshots/$delete-id", 'DELETE', :$public-key, :$secret-key);
|
||||
if $status == 428 {
|
||||
if handle-sudo-challenge($body, "/snapshots/$delete-id", 'DELETE', :$public-key, :$secret-key) {
|
||||
say "{$GREEN}Snapshot deleted: $delete-id{$RESET}";
|
||||
} else {
|
||||
note "{$RED}Error: Failed to delete snapshot (OTP verification failed){$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
} elsif $status >= 200 && $status < 300 {
|
||||
say "{$GREEN}Snapshot deleted: $delete-id{$RESET}";
|
||||
} else {
|
||||
note "{$RED}Error: Failed to delete snapshot (HTTP $status){$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if $lock-id {
|
||||
snapshot-lock($lock-id, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Snapshot locked: $lock-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
||||
if $unlock-id {
|
||||
my ($status, $body) = api-request-with-status("/snapshots/$unlock-id/unlock", 'POST', :$public-key, :$secret-key);
|
||||
if $status == 428 {
|
||||
if handle-sudo-challenge($body, "/snapshots/$unlock-id/unlock", 'POST', :$public-key, :$secret-key) {
|
||||
say "{$GREEN}Snapshot unlocked: $unlock-id{$RESET}";
|
||||
} else {
|
||||
note "{$RED}Error: Failed to unlock snapshot (OTP verification failed){$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
} elsif $status >= 200 && $status < 300 {
|
||||
say "{$GREEN}Snapshot unlocked: $unlock-id{$RESET}";
|
||||
} else {
|
||||
note "{$RED}Error: Failed to unlock snapshot (HTTP $status){$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if $restore-id {
|
||||
snapshot-restore($restore-id, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Snapshot restored: $restore-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
||||
if $clone-id {
|
||||
unless $clone-type {
|
||||
note "{$RED}Error: --clone-type required (session or service){$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
my $new-id = snapshot-clone($clone-id, :$clone-type, :$name, :$ports, :$shell, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Cloned to $clone-type: $new-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
||||
if $session-id {
|
||||
my $snap-id = snapshot-session($session-id, :$name, :$hot, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Snapshot created: $snap-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
||||
if $service-id {
|
||||
my $snap-id = snapshot-service($service-id, :$name, :$hot, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Snapshot created: $snap-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
||||
note "{$RED}Error: Specify --list, --info ID, --delete ID, --lock ID, --unlock ID, --restore ID, --clone ID, --session ID, or --service ID{$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
sub MAIN(*@args) is export {
|
||||
unless @args {
|
||||
note "Usage: un.raku [options] <source_file>";
|
||||
note " un.raku session [options]";
|
||||
note " un.raku service [options]";
|
||||
note " un.raku snapshot [options]";
|
||||
note " un.raku image [options]";
|
||||
note " un.raku key [options]";
|
||||
note " un.raku languages [--json]";
|
||||
|
|
@ -1560,6 +2237,22 @@ sub MAIN(*@args) is export {
|
|||
note "Languages options:";
|
||||
note " --json Output as JSON array";
|
||||
note "";
|
||||
note "Snapshot options:";
|
||||
note " --list List all snapshots";
|
||||
note " --info ID Get snapshot details";
|
||||
note " --delete ID Delete a snapshot";
|
||||
note " --lock ID Lock snapshot to prevent deletion";
|
||||
note " --unlock ID Unlock snapshot";
|
||||
note " --restore ID Restore a snapshot";
|
||||
note " --clone ID Clone snapshot (requires --clone-type)";
|
||||
note " --session ID Create snapshot from session";
|
||||
note " --service ID Create snapshot from service";
|
||||
note " --name NAME Name for the snapshot";
|
||||
note " --hot Create hot snapshot (while running)";
|
||||
note " --clone-type TYPE Clone type: session or service";
|
||||
note " --ports PORTS Ports for cloned service";
|
||||
note " --shell SHELL Shell for cloned session";
|
||||
note "";
|
||||
note "Image options:";
|
||||
note " --list List all images";
|
||||
note " --info ID Get image details";
|
||||
|
|
@ -1573,6 +2266,10 @@ sub MAIN(*@args) is export {
|
|||
note " --clone ID Clone an image";
|
||||
note " --name NAME Name for spawned service or cloned image";
|
||||
note " --ports PORTS Ports for spawned service";
|
||||
note " --grant ID KEY Grant image access to API key";
|
||||
note " --revoke ID KEY Revoke image access from API key";
|
||||
note " --trusted ID List trusted API keys for image";
|
||||
note " --transfer ID KEY Transfer image ownership";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
|
|
@ -1583,6 +2280,9 @@ sub MAIN(*@args) is export {
|
|||
when 'service' {
|
||||
cmd-service(@args[1..*]);
|
||||
}
|
||||
when 'snapshot' {
|
||||
cmd-snapshot(@args[1..*]);
|
||||
}
|
||||
when 'image' {
|
||||
cmd-image(@args[1..*]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ require 'json'
|
|||
require 'openssl'
|
||||
require 'fileutils'
|
||||
require 'optparse'
|
||||
require 'cgi'
|
||||
|
||||
# Unsandbox Ruby SDK module (synchronous)
|
||||
module Un
|
||||
|
|
@ -343,6 +344,31 @@ module Un
|
|||
response['snapshots'] || []
|
||||
end
|
||||
|
||||
# Get details of a specific snapshot
|
||||
#
|
||||
# @param snapshot_id [String] Snapshot ID to get details for
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Snapshot details hash containing:
|
||||
# - id: Snapshot ID
|
||||
# - name: Snapshot name
|
||||
# - type: "session" or "service"
|
||||
# - source_id: Original resource ID
|
||||
# - hot: Whether snapshot preserves running state
|
||||
# - locked: Whether snapshot is locked
|
||||
# - created_at: Creation timestamp
|
||||
# - size_bytes: Size in bytes
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# snapshot = Un.get_snapshot(snapshot_id)
|
||||
# puts "Snapshot: #{snapshot['name']} (#{snapshot['type']})"
|
||||
def get_snapshot(snapshot_id, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('GET', "/snapshots/#{snapshot_id}", pk, sk)
|
||||
end
|
||||
|
||||
# Restore a snapshot
|
||||
#
|
||||
# @param snapshot_id [String] Snapshot ID to restore
|
||||
|
|
@ -1219,6 +1245,23 @@ module Un
|
|||
wait_for_job(job_id, public_key: pk, secret_key: sk, timeout: (timeout / 1000) + 10)
|
||||
end
|
||||
|
||||
# Resize a service's vCPU allocation
|
||||
#
|
||||
# @param service_id [String] Service ID to resize
|
||||
# @param vcpu [Integer] Number of vCPUs (1-8 typically)
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Response hash with updated service info
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# Un.resize_service(service_id, 4)
|
||||
def resize_service(service_id, vcpu, public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
make_request('PATCH', "/services/#{service_id}", pk, sk, { vcpu: vcpu })
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Key Validation
|
||||
# ============================================================================
|
||||
|
|
@ -1270,6 +1313,158 @@ module Un
|
|||
make_request('POST', '/image', pk, sk, payload)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# PaaS Logs Functions
|
||||
# ============================================================================
|
||||
|
||||
# Fetch batch logs from the PaaS platform
|
||||
#
|
||||
# @param source [String] Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
# @param lines [Integer] Number of lines to fetch (1-10000)
|
||||
# @param since [String] Time window - "1m", "5m", "1h", "1d"
|
||||
# @param grep [String, nil] Optional filter pattern
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @return [Hash] Log entries
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# logs = Un.logs_fetch(source: 'api', lines: 50, since: '5m')
|
||||
def logs_fetch(source: 'all', lines: 100, since: '5m', grep: nil,
|
||||
public_key: nil, secret_key: nil)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
path = "/logs?source=#{source}&lines=#{lines}&since=#{since}"
|
||||
path += "&grep=#{CGI.escape(grep)}" if grep
|
||||
make_request('GET', path, pk, sk)
|
||||
end
|
||||
|
||||
# Stream logs via Server-Sent Events
|
||||
#
|
||||
# Blocks until interrupted or server closes connection.
|
||||
#
|
||||
# @param source [String] Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
# @param grep [String, nil] Optional filter pattern
|
||||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @yield [source, line] Called for each log line received
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# Un.logs_stream(source: 'api') do |src, line|
|
||||
# puts "[#{src}] #{line}"
|
||||
# end
|
||||
def logs_stream(source: 'all', grep: nil, public_key: nil, secret_key: nil, &block)
|
||||
pk, sk = resolve_credentials(public_key, secret_key)
|
||||
path = "/logs/stream?source=#{source}"
|
||||
path += "&grep=#{CGI.escape(grep)}" if grep
|
||||
|
||||
uri = URI("#{API_BASE}#{path}")
|
||||
timestamp = Time.now.to_i
|
||||
signature = sign_request(sk, timestamp, 'GET', path)
|
||||
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = true
|
||||
http.read_timeout = nil
|
||||
|
||||
request = Net::HTTP::Get.new(uri)
|
||||
request['Authorization'] = "Bearer #{pk}"
|
||||
request['X-Timestamp'] = timestamp.to_s
|
||||
request['X-Signature'] = signature
|
||||
request['Accept'] = 'text/event-stream'
|
||||
|
||||
http.request(request) do |response|
|
||||
response.read_body do |chunk|
|
||||
chunk.split("\n").each do |line|
|
||||
next unless line.start_with?('data: ')
|
||||
|
||||
data = line[6..]
|
||||
begin
|
||||
entry = JSON.parse(data)
|
||||
if block_given?
|
||||
yield entry['source'] || source, entry['line'] || data
|
||||
else
|
||||
puts "[#{entry['source'] || source}] #{entry['line'] || data}"
|
||||
end
|
||||
rescue JSON::ParserError
|
||||
if block_given?
|
||||
yield source, data
|
||||
else
|
||||
puts "[#{source}] #{data}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Utility Functions
|
||||
# ============================================================================
|
||||
|
||||
SDK_VERSION = '4.2.0'
|
||||
|
||||
# Thread-local storage for last error
|
||||
@last_error = nil
|
||||
|
||||
class << self
|
||||
attr_accessor :last_error_value
|
||||
end
|
||||
|
||||
# Get the SDK version string
|
||||
#
|
||||
# @return [String] Version string (e.g., "4.2.0")
|
||||
#
|
||||
# @example
|
||||
# puts Un.version # "4.2.0"
|
||||
def version
|
||||
SDK_VERSION
|
||||
end
|
||||
|
||||
# Check if the API is healthy and responding
|
||||
#
|
||||
# @return [Boolean] true if API is healthy, false otherwise
|
||||
#
|
||||
# @example
|
||||
# if Un.health_check
|
||||
# puts "API is healthy"
|
||||
# end
|
||||
def health_check
|
||||
uri = URI("#{API_BASE}/health")
|
||||
response = Net::HTTP.get_response(uri)
|
||||
response.code == '200'
|
||||
rescue StandardError => e
|
||||
Un.last_error_value = "Health check failed: #{e.message}"
|
||||
false
|
||||
end
|
||||
|
||||
# Get the last error message
|
||||
#
|
||||
# @return [String, nil] Last error message or nil
|
||||
#
|
||||
# @example
|
||||
# result = Un.health_check
|
||||
# puts Un.last_error unless result
|
||||
def last_error
|
||||
Un.last_error_value
|
||||
end
|
||||
|
||||
# Sign a message using HMAC-SHA256
|
||||
#
|
||||
# This is the underlying signing function used for request authentication.
|
||||
# Exposed for testing and debugging purposes.
|
||||
#
|
||||
# @param secret_key [String] The secret key for signing
|
||||
# @param message [String] The message to sign
|
||||
# @return [String] 64-character lowercase hex string
|
||||
#
|
||||
# @example
|
||||
# signature = Un.hmac_sign("secret", "message")
|
||||
def hmac_sign(secret_key, message)
|
||||
OpenSSL::HMAC.hexdigest('SHA256', secret_key, message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Language detection mapping (file extension -> language)
|
||||
|
|
|
|||
246
clients/ruby/sync/test/test_new_functions.rb
Normal file
246
clients/ruby/sync/test/test_new_functions.rb
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'test_helper'
|
||||
|
||||
class TestNewFunctions < Minitest::Test
|
||||
# =========================================================================
|
||||
# Utility Functions Tests
|
||||
# =========================================================================
|
||||
|
||||
def test_version_returns_string
|
||||
v = Un.version
|
||||
assert_instance_of String, v
|
||||
refute_empty v
|
||||
end
|
||||
|
||||
def test_version_is_semantic_format
|
||||
v = Un.version
|
||||
parts = v.split('.')
|
||||
assert parts.length >= 2, "Version should be semantic: #{v}"
|
||||
end
|
||||
|
||||
def test_hmac_sign_returns_64_hex_chars
|
||||
signature = Un.hmac_sign('secret_key', 'message_to_sign')
|
||||
assert_instance_of String, signature
|
||||
assert_equal 64, signature.length
|
||||
assert_match(/\A[0-9a-f]+\z/, signature)
|
||||
end
|
||||
|
||||
def test_hmac_sign_is_deterministic
|
||||
sig1 = Un.hmac_sign('secret', 'message')
|
||||
sig2 = Un.hmac_sign('secret', 'message')
|
||||
assert_equal sig1, sig2
|
||||
end
|
||||
|
||||
def test_hmac_sign_different_secrets_different_signatures
|
||||
sig1 = Un.hmac_sign('secret1', 'message')
|
||||
sig2 = Un.hmac_sign('secret2', 'message')
|
||||
refute_equal sig1, sig2
|
||||
end
|
||||
|
||||
def test_hmac_sign_different_messages_different_signatures
|
||||
sig1 = Un.hmac_sign('secret', 'message1')
|
||||
sig2 = Un.hmac_sign('secret', 'message2')
|
||||
refute_equal sig1, sig2
|
||||
end
|
||||
|
||||
def test_hmac_sign_matches_openssl
|
||||
secret = 'test_secret'
|
||||
message = '1234567890:POST:/execute:'
|
||||
signature = Un.hmac_sign(secret, message)
|
||||
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, message)
|
||||
assert_equal expected, signature
|
||||
end
|
||||
|
||||
def test_last_error_returns_nil_or_string
|
||||
error = Un.last_error
|
||||
assert error.nil? || error.is_a?(String)
|
||||
end
|
||||
|
||||
# =========================================================================
|
||||
# Function Exports Tests
|
||||
# =========================================================================
|
||||
|
||||
def test_execution_functions_defined
|
||||
# 8 execution functions
|
||||
assert Un.respond_to?(:execute_code)
|
||||
assert Un.respond_to?(:execute_async)
|
||||
assert Un.respond_to?(:get_job)
|
||||
assert Un.respond_to?(:wait_for_job)
|
||||
assert Un.respond_to?(:cancel_job)
|
||||
assert Un.respond_to?(:list_jobs)
|
||||
assert Un.respond_to?(:get_languages)
|
||||
assert Un.respond_to?(:detect_language)
|
||||
end
|
||||
|
||||
def test_session_functions_defined
|
||||
# 9 session functions
|
||||
assert Un.respond_to?(:list_sessions)
|
||||
assert Un.respond_to?(:get_session)
|
||||
assert Un.respond_to?(:create_session)
|
||||
assert Un.respond_to?(:delete_session)
|
||||
assert Un.respond_to?(:freeze_session)
|
||||
assert Un.respond_to?(:unfreeze_session)
|
||||
assert Un.respond_to?(:boost_session)
|
||||
assert Un.respond_to?(:unboost_session)
|
||||
assert Un.respond_to?(:shell_session)
|
||||
end
|
||||
|
||||
def test_service_functions_defined
|
||||
# 17 service functions
|
||||
assert Un.respond_to?(:list_services)
|
||||
assert Un.respond_to?(:create_service)
|
||||
assert Un.respond_to?(:get_service)
|
||||
assert Un.respond_to?(:update_service)
|
||||
assert Un.respond_to?(:delete_service)
|
||||
assert Un.respond_to?(:freeze_service)
|
||||
assert Un.respond_to?(:unfreeze_service)
|
||||
assert Un.respond_to?(:lock_service)
|
||||
assert Un.respond_to?(:unlock_service)
|
||||
assert Un.respond_to?(:set_unfreeze_on_demand)
|
||||
assert Un.respond_to?(:get_service_logs)
|
||||
assert Un.respond_to?(:get_service_env)
|
||||
assert Un.respond_to?(:set_service_env)
|
||||
assert Un.respond_to?(:delete_service_env)
|
||||
assert Un.respond_to?(:export_service_env)
|
||||
assert Un.respond_to?(:redeploy_service)
|
||||
assert Un.respond_to?(:execute_in_service)
|
||||
assert Un.respond_to?(:resize_service), 'resize_service should be defined (NEW)'
|
||||
end
|
||||
|
||||
def test_snapshot_functions_defined
|
||||
# 9 snapshot functions
|
||||
assert Un.respond_to?(:session_snapshot)
|
||||
assert Un.respond_to?(:service_snapshot)
|
||||
assert Un.respond_to?(:list_snapshots)
|
||||
assert Un.respond_to?(:get_snapshot), 'get_snapshot should be defined (NEW)'
|
||||
assert Un.respond_to?(:restore_snapshot)
|
||||
assert Un.respond_to?(:delete_snapshot)
|
||||
assert Un.respond_to?(:lock_snapshot)
|
||||
assert Un.respond_to?(:unlock_snapshot)
|
||||
assert Un.respond_to?(:clone_snapshot)
|
||||
end
|
||||
|
||||
def test_image_functions_defined
|
||||
# 13 image functions
|
||||
assert Un.respond_to?(:image_publish)
|
||||
assert Un.respond_to?(:list_images)
|
||||
assert Un.respond_to?(:get_image)
|
||||
assert Un.respond_to?(:delete_image)
|
||||
assert Un.respond_to?(:lock_image)
|
||||
assert Un.respond_to?(:unlock_image)
|
||||
assert Un.respond_to?(:set_image_visibility)
|
||||
assert Un.respond_to?(:grant_image_access)
|
||||
assert Un.respond_to?(:revoke_image_access)
|
||||
assert Un.respond_to?(:list_image_trusted)
|
||||
assert Un.respond_to?(:transfer_image)
|
||||
assert Un.respond_to?(:spawn_from_image)
|
||||
assert Un.respond_to?(:clone_image)
|
||||
end
|
||||
|
||||
def test_logs_functions_defined
|
||||
# 2 PaaS logs functions (NEW)
|
||||
assert Un.respond_to?(:logs_fetch), 'logs_fetch should be defined (NEW)'
|
||||
assert Un.respond_to?(:logs_stream), 'logs_stream should be defined (NEW)'
|
||||
end
|
||||
|
||||
def test_utility_functions_defined
|
||||
assert Un.respond_to?(:validate_keys)
|
||||
assert Un.respond_to?(:version), 'version should be defined (NEW)'
|
||||
assert Un.respond_to?(:health_check), 'health_check should be defined (NEW)'
|
||||
assert Un.respond_to?(:last_error), 'last_error should be defined (NEW)'
|
||||
assert Un.respond_to?(:hmac_sign), 'hmac_sign should be defined (NEW)'
|
||||
end
|
||||
|
||||
# =========================================================================
|
||||
# Language Detection Tests
|
||||
# =========================================================================
|
||||
|
||||
def test_detect_language_python
|
||||
assert_equal 'python', Un.detect_language('test.py')
|
||||
end
|
||||
|
||||
def test_detect_language_javascript
|
||||
assert_equal 'javascript', Un.detect_language('test.js')
|
||||
end
|
||||
|
||||
def test_detect_language_typescript
|
||||
assert_equal 'typescript', Un.detect_language('test.ts')
|
||||
end
|
||||
|
||||
def test_detect_language_ruby
|
||||
assert_equal 'ruby', Un.detect_language('test.rb')
|
||||
end
|
||||
|
||||
def test_detect_language_go
|
||||
assert_equal 'go', Un.detect_language('test.go')
|
||||
end
|
||||
|
||||
def test_detect_language_rust
|
||||
assert_equal 'rust', Un.detect_language('test.rs')
|
||||
end
|
||||
|
||||
def test_detect_language_unknown_returns_nil
|
||||
assert_nil Un.detect_language('test.unknown')
|
||||
end
|
||||
end
|
||||
|
||||
# Functional tests require API credentials
|
||||
class TestFunctionalAPI < Minitest::Test
|
||||
def setup
|
||||
skip 'UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY required' unless credentials_available?
|
||||
WebMock.allow_net_connect!
|
||||
end
|
||||
|
||||
def teardown
|
||||
WebMock.disable_net_connect!
|
||||
end
|
||||
|
||||
def test_health_check
|
||||
result = Un.health_check
|
||||
assert [true, false].include?(result)
|
||||
end
|
||||
|
||||
def test_validate_keys
|
||||
result = Un.validate_keys
|
||||
assert result.is_a?(Hash)
|
||||
end
|
||||
|
||||
def test_get_languages
|
||||
languages = Un.get_languages
|
||||
assert languages.is_a?(Array)
|
||||
assert languages.include?('python')
|
||||
end
|
||||
|
||||
def test_list_sessions
|
||||
sessions = Un.list_sessions
|
||||
assert sessions.is_a?(Array)
|
||||
end
|
||||
|
||||
def test_list_services
|
||||
services = Un.list_services
|
||||
assert services.is_a?(Array)
|
||||
end
|
||||
|
||||
def test_list_snapshots
|
||||
snapshots = Un.list_snapshots
|
||||
assert snapshots.is_a?(Array)
|
||||
end
|
||||
|
||||
def test_list_images
|
||||
images = Un.list_images
|
||||
assert images.is_a?(Array)
|
||||
end
|
||||
|
||||
def test_execute_code
|
||||
result = Un.execute_code('python', 'print("hello")')
|
||||
assert result.is_a?(Hash)
|
||||
assert %w[completed pending].include?(result['status'])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def credentials_available?
|
||||
ENV['UNSANDBOX_PUBLIC_KEY'] && ENV['UNSANDBOX_SECRET_KEY']
|
||||
end
|
||||
end
|
||||
|
|
@ -1302,6 +1302,19 @@ pub fn list_snapshots(creds: &Credentials) -> Result<Vec<Snapshot>> {
|
|||
Ok(response.snapshots)
|
||||
}
|
||||
|
||||
/// Get details of a specific snapshot.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `snapshot_id` - Snapshot ID to retrieve
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Snapshot information
|
||||
pub fn get_snapshot(snapshot_id: &str, creds: &Credentials) -> Result<Snapshot> {
|
||||
let path = format!("/snapshots/{}", snapshot_id);
|
||||
make_request("GET", &path, creds, None::<&()>)
|
||||
}
|
||||
|
||||
/// Restore a snapshot to create a new session or service.
|
||||
///
|
||||
/// # Arguments
|
||||
|
|
@ -2265,6 +2278,30 @@ pub fn redeploy_service(service_id: &str, creds: &Credentials) -> Result<Service
|
|||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Resize a service by changing its vCPU count.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `service_id` - Service ID to resize
|
||||
/// * `vcpu` - New vCPU count (1-8)
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Updated Service information
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let service = resize_service("service-123", 4, &creds)?;
|
||||
/// println!("Service now has {} vCPUs", service.vcpu);
|
||||
/// ```
|
||||
pub fn resize_service(service_id: &str, vcpu: u32, creds: &Credentials) -> Result<Service> {
|
||||
let path = format!("/services/{}/resize", service_id);
|
||||
let body = serde_json::json!({
|
||||
"vcpu": vcpu
|
||||
});
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Execute a command in a service container.
|
||||
///
|
||||
/// # Arguments
|
||||
|
|
@ -2397,6 +2434,278 @@ pub fn image(prompt: &str, creds: &Credentials, opts: Option<ImageOptions>) -> R
|
|||
make_request("POST", "/image", creds, Some(&payload))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PaaS Logs API Functions
|
||||
// =============================================================================
|
||||
|
||||
/// Options for fetching logs.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LogsFetchOptions {
|
||||
/// Number of lines to fetch (1-10000)
|
||||
pub lines: Option<u32>,
|
||||
/// Time window ("1m", "5m", "1h", "1d")
|
||||
pub since: Option<String>,
|
||||
/// Optional filter pattern
|
||||
pub grep: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from logs fetch.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct LogsEntry {
|
||||
/// Log source
|
||||
#[serde(default)]
|
||||
pub source: String,
|
||||
/// Log line content
|
||||
#[serde(default)]
|
||||
pub line: String,
|
||||
/// Timestamp
|
||||
#[serde(default)]
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
/// Fetch batch logs from the portal.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `source` - Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
/// * `opts` - Fetch options (can be None for defaults)
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of log entries
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
///
|
||||
/// // Fetch last 100 lines from all sources
|
||||
/// let opts = LogsFetchOptions {
|
||||
/// lines: Some(100),
|
||||
/// since: Some("1h".to_string()),
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
/// let logs = logs_fetch("all", Some(opts), &creds)?;
|
||||
/// for entry in logs {
|
||||
/// println!("[{}] {}", entry.source, entry.line);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn logs_fetch(source: &str, opts: Option<LogsFetchOptions>, creds: &Credentials) -> Result<Vec<LogsEntry>> {
|
||||
let mut path = String::from("/paas/logs");
|
||||
let mut params = vec![];
|
||||
|
||||
if !source.is_empty() {
|
||||
params.push(format!("source={}", source));
|
||||
}
|
||||
|
||||
if let Some(opts) = opts {
|
||||
if let Some(lines) = opts.lines {
|
||||
params.push(format!("lines={}", lines));
|
||||
}
|
||||
if let Some(since) = opts.since {
|
||||
params.push(format!("since={}", since));
|
||||
}
|
||||
if let Some(grep) = opts.grep {
|
||||
params.push(format!("grep={}", grep));
|
||||
}
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
path = format!("{}?{}", path, params.join("&"));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LogsResponse {
|
||||
#[serde(default)]
|
||||
logs: Vec<LogsEntry>,
|
||||
}
|
||||
|
||||
let response: LogsResponse = make_request("GET", &path, creds, None::<&()>)?;
|
||||
Ok(response.logs)
|
||||
}
|
||||
|
||||
/// Callback function for streaming logs.
|
||||
pub type LogCallback = fn(source: &str, line: &str);
|
||||
|
||||
/// Stream logs via Server-Sent Events.
|
||||
///
|
||||
/// This function blocks until the stream is closed or an error occurs.
|
||||
/// Note: This is a simplified implementation that reads until EOF.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `source` - Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
/// * `grep` - Optional filter pattern (empty string for no filter)
|
||||
/// * `callback` - Function called for each log line
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Ok(()) on clean shutdown, error on failure
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
///
|
||||
/// fn handle_log(source: &str, line: &str) {
|
||||
/// println!("[{}] {}", source, line);
|
||||
/// }
|
||||
///
|
||||
/// logs_stream("all", "", handle_log, &creds)?;
|
||||
/// ```
|
||||
pub fn logs_stream(source: &str, grep: &str, callback: LogCallback, creds: &Credentials) -> Result<()> {
|
||||
let mut path = String::from("/paas/logs/stream");
|
||||
let mut params = vec![];
|
||||
|
||||
if !source.is_empty() {
|
||||
params.push(format!("source={}", source));
|
||||
}
|
||||
if !grep.is_empty() {
|
||||
params.push(format!("grep={}", grep));
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
path = format!("{}?{}", path, params.join("&"));
|
||||
}
|
||||
|
||||
let url = format!("{}{}", API_BASE, path);
|
||||
let timestamp = get_timestamp();
|
||||
let signature = sign_request(&creds.secret_key, timestamp, "GET", &path, "");
|
||||
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(None) // No timeout for streaming
|
||||
.build()?;
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", creds.public_key))
|
||||
.header("X-Timestamp", timestamp.to_string())
|
||||
.header("X-Signature", signature)
|
||||
.header("Accept", "text/event-stream")
|
||||
.header("User-Agent", "un-rust-sync/2.0")
|
||||
.send()?;
|
||||
|
||||
let status = response.status().as_u16();
|
||||
if status < 200 || status >= 300 {
|
||||
let response_text = response.text()?;
|
||||
return Err(UnsandboxError::ApiError {
|
||||
status,
|
||||
message: response_text,
|
||||
});
|
||||
}
|
||||
|
||||
let reader = BufReader::new(response);
|
||||
let mut current_source = source.to_string();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse SSE format
|
||||
if let Some(event) = line.strip_prefix("event:") {
|
||||
current_source = event.trim().to_string();
|
||||
} else if let Some(data) = line.strip_prefix("data:") {
|
||||
let data = data.trim();
|
||||
if !data.is_empty() {
|
||||
callback(¤t_source, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utility Functions
|
||||
// =============================================================================
|
||||
|
||||
/// SDK version string
|
||||
pub const SDK_VERSION: &str = "4.2.0";
|
||||
|
||||
/// Compute HMAC-SHA256 signature for a message.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `secret_key` - Secret key for signing
|
||||
/// * `message` - Message to sign
|
||||
///
|
||||
/// # Returns
|
||||
/// Lowercase hex-encoded signature string
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let sig = hmac_sign("my-secret", "hello");
|
||||
/// println!("Signature: {}", sig);
|
||||
/// ```
|
||||
pub fn hmac_sign(secret_key: &str, message: &str) -> String {
|
||||
let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes())
|
||||
.expect("HMAC can take key of any size");
|
||||
mac.update(message.as_bytes());
|
||||
let result = mac.finalize();
|
||||
hex::encode(result.into_bytes())
|
||||
}
|
||||
|
||||
/// Check if the API is reachable and responding.
|
||||
///
|
||||
/// # Returns
|
||||
/// true if healthy, false otherwise
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// if health_check() {
|
||||
/// println!("API is healthy");
|
||||
/// } else {
|
||||
/// println!("API is unreachable");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn health_check() -> bool {
|
||||
let client = match reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
match client.get(format!("{}/health", API_BASE)).send() {
|
||||
Ok(resp) => resp.status().as_u16() == 200,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the SDK version string.
|
||||
///
|
||||
/// # Returns
|
||||
/// Version string (e.g., "4.2.0")
|
||||
pub fn version() -> &'static str {
|
||||
SDK_VERSION
|
||||
}
|
||||
|
||||
/// Thread-local storage for the last error message.
|
||||
thread_local! {
|
||||
static LAST_ERROR: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
||||
}
|
||||
|
||||
/// Set the last error message (internal use).
|
||||
pub fn set_last_error(msg: &str) {
|
||||
LAST_ERROR.with(|e| {
|
||||
*e.borrow_mut() = msg.to_string();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the last error message from the SDK.
|
||||
///
|
||||
/// # Returns
|
||||
/// Last error message, or empty string if none
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// if let Err(_) = some_operation() {
|
||||
/// println!("Error: {}", last_error());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn last_error() -> String {
|
||||
LAST_ERROR.with(|e| e.borrow().clone())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CLI Exit Codes
|
||||
// =============================================================================
|
||||
|
|
|
|||
211
clients/rust/sync/tests/lib_test.rs
Normal file
211
clients/rust/sync/tests/lib_test.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// Tests for the Rust unsandbox SDK
|
||||
// Run with: cargo test
|
||||
|
||||
use un::*;
|
||||
use std::env;
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests - Test exported library functions
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_detect_language() {
|
||||
assert_eq!(detect_language("script.py"), Some("python"));
|
||||
assert_eq!(detect_language("script.js"), Some("javascript"));
|
||||
assert_eq!(detect_language("script.ts"), Some("typescript"));
|
||||
assert_eq!(detect_language("script.rb"), Some("ruby"));
|
||||
assert_eq!(detect_language("script.go"), Some("go"));
|
||||
assert_eq!(detect_language("script.rs"), Some("rust"));
|
||||
assert_eq!(detect_language("script.c"), Some("c"));
|
||||
assert_eq!(detect_language("script.cpp"), Some("cpp"));
|
||||
assert_eq!(detect_language("script.d"), Some("d"));
|
||||
assert_eq!(detect_language("script.zig"), Some("zig"));
|
||||
assert_eq!(detect_language("script.sh"), Some("bash"));
|
||||
assert_eq!(detect_language("script.lua"), Some("lua"));
|
||||
assert_eq!(detect_language("script.php"), Some("php"));
|
||||
assert_eq!(detect_language("script.unknown"), None);
|
||||
assert_eq!(detect_language("script"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hmac_sign() {
|
||||
let secret_key = "test-secret";
|
||||
let message = "test-message";
|
||||
|
||||
let result = hmac_sign(secret_key, message);
|
||||
|
||||
// Should return a 64-character hex string
|
||||
assert_eq!(result.len(), 64, "HMAC signature should be 64 characters");
|
||||
|
||||
// Should be deterministic
|
||||
let result2 = hmac_sign(secret_key, message);
|
||||
assert_eq!(result, result2, "HMAC sign is not deterministic");
|
||||
|
||||
// Different inputs should produce different outputs
|
||||
let result3 = hmac_sign(secret_key, "different-message");
|
||||
assert_ne!(result, result3, "Different inputs should produce different signatures");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
let v = version();
|
||||
assert!(!v.is_empty(), "Version should not be empty");
|
||||
// Should be in semver format (at least "0.0.0")
|
||||
assert!(v.len() >= 5, "Version should be in semver format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_error() {
|
||||
// Set an error
|
||||
set_last_error("test error message");
|
||||
|
||||
// Retrieve it
|
||||
let err = last_error();
|
||||
assert_eq!(err, "test error message");
|
||||
|
||||
// Clear it
|
||||
set_last_error("");
|
||||
let err = last_error();
|
||||
assert!(err.is_empty(), "Error should be cleared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credentials_new() {
|
||||
let pk = "unsb-pk-test-test-test-test";
|
||||
let sk = "unsb-sk-test1-test2-test3-test4";
|
||||
|
||||
let creds = Credentials::new(pk, sk);
|
||||
|
||||
assert_eq!(creds.public_key, pk);
|
||||
assert_eq!(creds.secret_key, sk);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests - Test SDK internal consistency
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_resolve_credentials_from_env() {
|
||||
// Save original env vars
|
||||
let orig_pk = env::var("UNSANDBOX_PUBLIC_KEY").ok();
|
||||
let orig_sk = env::var("UNSANDBOX_SECRET_KEY").ok();
|
||||
|
||||
// Set test env vars
|
||||
let test_pk = "unsb-pk-test-test-test-test";
|
||||
let test_sk = "unsb-sk-test1-test2-test3-test4";
|
||||
env::set_var("UNSANDBOX_PUBLIC_KEY", test_pk);
|
||||
env::set_var("UNSANDBOX_SECRET_KEY", test_sk);
|
||||
|
||||
// Test
|
||||
let creds = resolve_credentials(None, None).expect("Should resolve from env");
|
||||
assert_eq!(creds.public_key, test_pk);
|
||||
assert_eq!(creds.secret_key, test_sk);
|
||||
|
||||
// Restore original env vars
|
||||
match orig_pk {
|
||||
Some(v) => env::set_var("UNSANDBOX_PUBLIC_KEY", v),
|
||||
None => env::remove_var("UNSANDBOX_PUBLIC_KEY"),
|
||||
}
|
||||
match orig_sk {
|
||||
Some(v) => env::set_var("UNSANDBOX_SECRET_KEY", v),
|
||||
None => env::remove_var("UNSANDBOX_SECRET_KEY"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_credentials_from_args() {
|
||||
let test_pk = "unsb-pk-arg1-arg2-arg3-arg4";
|
||||
let test_sk = "unsb-sk-arg11-arg22-arg33-arg44";
|
||||
|
||||
let creds = resolve_credentials(Some(test_pk), Some(test_sk))
|
||||
.expect("Should resolve from args");
|
||||
assert_eq!(creds.public_key, test_pk);
|
||||
assert_eq!(creds.secret_key, test_sk);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Functional Tests - Test against real API (requires credentials)
|
||||
// ============================================================================
|
||||
|
||||
fn get_test_credentials() -> Option<Credentials> {
|
||||
resolve_credentials(None, None).ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_health_check() {
|
||||
let healthy = health_check();
|
||||
if !healthy {
|
||||
eprintln!("API health check returned unhealthy (API may be unreachable)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_get_languages() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let languages = get_languages(&creds).expect("GetLanguages failed");
|
||||
|
||||
assert!(!languages.is_empty(), "Languages list should not be empty");
|
||||
|
||||
// Should include common languages
|
||||
assert!(
|
||||
languages.contains(&"python".to_string()),
|
||||
"Languages should include python"
|
||||
);
|
||||
assert!(
|
||||
languages.contains(&"javascript".to_string()),
|
||||
"Languages should include javascript"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_validate_keys() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let result = validate_keys(&creds).expect("ValidateKeys failed");
|
||||
|
||||
assert!(result.valid, "Keys should be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_execute_code() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let result = execute_code("python", "print('hello from rust test')", &creds)
|
||||
.expect("ExecuteCode failed");
|
||||
|
||||
assert!(!result.output.is_empty(), "Output should not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_list_sessions() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let _sessions = list_sessions(&creds).expect("ListSessions failed");
|
||||
// Should return a list (possibly empty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_list_services() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let _services = list_services(&creds).expect("ListServices failed");
|
||||
// Should return a list (possibly empty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_list_snapshots() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let _snapshots = list_snapshots(&creds).expect("ListSnapshots failed");
|
||||
// Should return a list (possibly empty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_list_images() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let _images = list_images(None, &creds).expect("ListImages failed");
|
||||
// Should return a list (possibly empty)
|
||||
}
|
||||
|
|
@ -764,6 +764,190 @@
|
|||
(display "Error: --name required to create service, or use env subcommand\n" (current-error-port))
|
||||
(exit 1)))))
|
||||
|
||||
;; Image access management functions
|
||||
(define (image-grant-access id trusted-key)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format #f "{\"trusted_api_key\":\"~a\"}" trusted-key)))
|
||||
(curl-post api-key (format #f "/images/~a/grant-access" id) json)
|
||||
(format #t "~aAccess granted to: ~a~a\n" green trusted-key reset)))
|
||||
|
||||
(define (image-revoke-access id trusted-key)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format #f "{\"trusted_api_key\":\"~a\"}" trusted-key)))
|
||||
(curl-post api-key (format #f "/images/~a/revoke-access" id) json)
|
||||
(format #t "~aAccess revoked from: ~a~a\n" green trusted-key reset)))
|
||||
|
||||
(define (image-list-trusted id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(display (curl-get api-key (format #f "/images/~a/trusted" id)))
|
||||
(newline)))
|
||||
|
||||
(define (image-transfer id to-key)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format #f "{\"to_api_key\":\"~a\"}" to-key)))
|
||||
(curl-post api-key (format #f "/images/~a/transfer" id) json)
|
||||
(format #t "~aImage transferred to: ~a~a\n" green to-key reset)))
|
||||
|
||||
;; Snapshot functions
|
||||
(define (snapshot-list)
|
||||
(let ((api-key (get-api-key)))
|
||||
(display (curl-get api-key "/snapshots"))
|
||||
(newline)))
|
||||
|
||||
(define (snapshot-info id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(display (curl-get api-key (format #f "/snapshots/~a" id)))
|
||||
(newline)))
|
||||
|
||||
(define (snapshot-session session-id name hot)
|
||||
(let* ((api-key (get-api-key))
|
||||
(name-json (if name (format #f ",\"name\":\"~a\"" (escape-json name)) ""))
|
||||
(hot-json (if hot ",\"hot\":true" ""))
|
||||
(json (format #f "{\"session_id\":\"~a\"~a~a}" session-id name-json hot-json)))
|
||||
(format #t "~aSnapshot created~a\n" green reset)
|
||||
(display (curl-post api-key "/snapshots" json))
|
||||
(newline)))
|
||||
|
||||
(define (snapshot-service-create service-id name hot)
|
||||
(let* ((api-key (get-api-key))
|
||||
(name-json (if name (format #f ",\"name\":\"~a\"" (escape-json name)) ""))
|
||||
(hot-json (if hot ",\"hot\":true" ""))
|
||||
(json (format #f "{\"service_id\":\"~a\"~a~a}" service-id name-json hot-json)))
|
||||
(format #t "~aSnapshot created~a\n" green reset)
|
||||
(display (curl-post api-key "/snapshots" json))
|
||||
(newline)))
|
||||
|
||||
(define (snapshot-restore id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(curl-post api-key (format #f "/snapshots/~a/restore" id) "{}")
|
||||
(format #t "~aSnapshot restored: ~a~a\n" green id reset)))
|
||||
|
||||
(define (snapshot-delete id)
|
||||
(let* ((api-key (get-api-key))
|
||||
(result (curl-delete-with-sudo api-key (format #f "/snapshots/~a" id))))
|
||||
(if (car result)
|
||||
(format #t "~aSnapshot deleted: ~a~a\n" green id reset)
|
||||
(begin
|
||||
(format (current-error-port) "~aError deleting snapshot~a\n" red reset)
|
||||
(exit 1)))))
|
||||
|
||||
(define (snapshot-lock id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(curl-post api-key (format #f "/snapshots/~a/lock" id) "{}")
|
||||
(format #t "~aSnapshot locked: ~a~a\n" green id reset)))
|
||||
|
||||
(define (snapshot-unlock id)
|
||||
(let* ((api-key (get-api-key))
|
||||
(result (curl-post-with-sudo api-key (format #f "/snapshots/~a/unlock" id) "{}")))
|
||||
(if (car result)
|
||||
(format #t "~aSnapshot unlocked: ~a~a\n" green id reset)
|
||||
(begin
|
||||
(format (current-error-port) "~aError unlocking snapshot~a\n" red reset)
|
||||
(exit 1)))))
|
||||
|
||||
(define (snapshot-clone id clone-type name ports shell)
|
||||
(let* ((api-key (get-api-key))
|
||||
(type-json (format #f "\"clone_type\":\"~a\"" clone-type))
|
||||
(name-json (if name (format #f ",\"name\":\"~a\"" (escape-json name)) ""))
|
||||
(ports-json (if ports (format #f ",\"ports\":[~a]" ports) ""))
|
||||
(shell-json (if shell (format #f ",\"shell\":\"~a\"" shell) ""))
|
||||
(json (format #f "{~a~a~a~a}" type-json name-json ports-json shell-json)))
|
||||
(format #t "~aSnapshot cloned~a\n" green reset)
|
||||
(display (curl-post api-key (format #f "/snapshots/~a/clone" id) json))
|
||||
(newline)))
|
||||
|
||||
(define (snapshot-cmd action id name ports shell hot)
|
||||
(cond
|
||||
((equal? action "list") (snapshot-list))
|
||||
((equal? action "info") (snapshot-info id))
|
||||
((equal? action "session") (snapshot-session id name hot))
|
||||
((equal? action "service") (snapshot-service-create id name hot))
|
||||
((equal? action "restore") (snapshot-restore id))
|
||||
((equal? action "delete") (snapshot-delete id))
|
||||
((equal? action "lock") (snapshot-lock id))
|
||||
((equal? action "unlock") (snapshot-unlock id))
|
||||
((equal? action "clone") (snapshot-clone id "session" name ports shell))
|
||||
(else
|
||||
(display "Error: Unknown snapshot action\n" (current-error-port))
|
||||
(exit 1))))
|
||||
|
||||
;; Session additional functions
|
||||
(define (session-info id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(display (curl-get api-key (format #f "/sessions/~a" id)))
|
||||
(newline)))
|
||||
|
||||
(define (session-boost id vcpu)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format #f "{\"vcpu\":~a}" vcpu)))
|
||||
(curl-patch api-key (format #f "/sessions/~a" id) json)
|
||||
(format #t "~aSession boosted to ~a vCPU~a\n" green vcpu reset)))
|
||||
|
||||
(define (session-unboost id)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json "{\"vcpu\":1}"))
|
||||
(curl-patch api-key (format #f "/sessions/~a" id) json)
|
||||
(format #t "~aSession unboosted to 1 vCPU~a\n" green reset)))
|
||||
|
||||
(define (session-execute id command)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (format #f "{\"command\":\"~a\"}" (escape-json command)))
|
||||
(response (curl-post api-key (format #f "/sessions/~a/execute" id) json))
|
||||
(stdout-val (json-extract-string response "stdout")))
|
||||
(when stdout-val
|
||||
(display (format #f "~a~a~a" blue stdout-val reset)))))
|
||||
|
||||
;; Service additional functions
|
||||
(define (service-lock id)
|
||||
(let ((api-key (get-api-key)))
|
||||
(curl-post api-key (format #f "/services/~a/lock" id) "{}")
|
||||
(format #t "~aService locked: ~a~a\n" green id reset)))
|
||||
|
||||
(define (service-unlock id)
|
||||
(let* ((api-key (get-api-key))
|
||||
(result (curl-post-with-sudo api-key (format #f "/services/~a/unlock" id) "{}")))
|
||||
(if (car result)
|
||||
(format #t "~aService unlocked: ~a~a\n" green id reset)
|
||||
(begin
|
||||
(format (current-error-port) "~aError unlocking service~a\n" red reset)
|
||||
(exit 1)))))
|
||||
|
||||
(define (service-redeploy id bootstrap)
|
||||
(let* ((api-key (get-api-key))
|
||||
(json (if bootstrap
|
||||
(format #f "{\"bootstrap\":\"~a\"}" (escape-json bootstrap))
|
||||
"{}")))
|
||||
(curl-post api-key (format #f "/services/~a/redeploy" id) json)
|
||||
(format #t "~aService redeploying: ~a~a\n" green id reset)))
|
||||
|
||||
;; PaaS logs functions
|
||||
(define (logs-fetch source lines since grep-pattern)
|
||||
(let* ((api-key (get-api-key))
|
||||
(params (format #f "?source=~a&lines=~a~a~a"
|
||||
(or source "all")
|
||||
(or lines 100)
|
||||
(if since (format #f "&since=~a" since) "")
|
||||
(if grep-pattern (format #f "&grep=~a" grep-pattern) ""))))
|
||||
(display (curl-get api-key (format #f "/logs~a" params)))
|
||||
(newline)))
|
||||
|
||||
;; Utility functions
|
||||
(define (health-check)
|
||||
(let* ((cmd "curl -s https://api.unsandbox.com/health")
|
||||
(port (open-input-pipe cmd))
|
||||
(result (let loop ((chars '()))
|
||||
(let ((char (read-char port)))
|
||||
(if (eof-object? char)
|
||||
(list->string (reverse chars))
|
||||
(loop (cons char chars)))))))
|
||||
(close-pipe port)
|
||||
(display result)
|
||||
(newline)
|
||||
(string-contains result "ok")))
|
||||
|
||||
(define (sdk-version)
|
||||
"4.2.0")
|
||||
|
||||
(define (image-cmd action id source-type visibility-mode name ports)
|
||||
(let ((api-key (get-api-key)))
|
||||
(cond
|
||||
|
|
@ -880,6 +1064,28 @@
|
|||
((equal? (car args) "key")
|
||||
(let ((extend (and (> (length args) 1) (equal? (cadr args) "--extend"))))
|
||||
(validate-key-cmd extend)))
|
||||
((equal? (car args) "snapshot")
|
||||
(cond
|
||||
((and (> (length args) 1) (equal? (cadr args) "--list"))
|
||||
(snapshot-cmd "list" #f #f #f #f #f))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--info"))
|
||||
(snapshot-cmd "info" (caddr args) #f #f #f #f))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--session"))
|
||||
(snapshot-cmd "session" (caddr args) #f #f #f #f))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--service"))
|
||||
(snapshot-cmd "service" (caddr args) #f #f #f #f))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--restore"))
|
||||
(snapshot-cmd "restore" (caddr args) #f #f #f #f))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--delete"))
|
||||
(snapshot-cmd "delete" (caddr args) #f #f #f #f))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--lock"))
|
||||
(snapshot-cmd "lock" (caddr args) #f #f #f #f))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--unlock"))
|
||||
(snapshot-cmd "unlock" (caddr args) #f #f #f #f))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--clone"))
|
||||
(snapshot-cmd "clone" (caddr args) #f #f #f #f))
|
||||
(else
|
||||
(snapshot-cmd "list" #f #f #f #f #f))))
|
||||
((equal? (car args) "image")
|
||||
(cond
|
||||
((and (> (length args) 1) (equal? (cadr args) "--list"))
|
||||
|
|
|
|||
|
|
@ -1197,6 +1197,166 @@ func cloneImage(_ imageId: String, name: String? = nil, description: String? = n
|
|||
return try makeRequest(method: "POST", path: "/images/\(imageId)/clone", publicKey: pk, secretKey: sk, data: data)
|
||||
}
|
||||
|
||||
/// Grant access to an image
|
||||
func grantImageAccess(_ imageId: String, trustedApiKey: String, publicKey: String? = nil, secretKey: String? = nil) throws -> [String: Any] {
|
||||
let (pk, sk) = try resolveCredentials(publicKey: publicKey, secretKey: secretKey)
|
||||
return try makeRequest(method: "POST", path: "/images/\(imageId)/grant", publicKey: pk, secretKey: sk, data: ["trusted_api_key": trustedApiKey])
|
||||
}
|
||||
|
||||
/// Revoke access to an image
|
||||
func revokeImageAccess(_ imageId: String, trustedApiKey: String, publicKey: String? = nil, secretKey: String? = nil) throws -> [String: Any] {
|
||||
let (pk, sk) = try resolveCredentials(publicKey: publicKey, secretKey: secretKey)
|
||||
return try makeRequest(method: "POST", path: "/images/\(imageId)/revoke", publicKey: pk, secretKey: sk, data: ["trusted_api_key": trustedApiKey])
|
||||
}
|
||||
|
||||
/// List trusted keys for an image
|
||||
func listImageTrusted(_ imageId: String, publicKey: String? = nil, secretKey: String? = nil) throws -> [String: Any] {
|
||||
let (pk, sk) = try resolveCredentials(publicKey: publicKey, secretKey: secretKey)
|
||||
return try makeRequest(method: "GET", path: "/images/\(imageId)/trusted", publicKey: pk, secretKey: sk)
|
||||
}
|
||||
|
||||
/// Transfer image ownership
|
||||
func transferImage(_ imageId: String, toApiKey: String, publicKey: String? = nil, secretKey: String? = nil) throws -> [String: Any] {
|
||||
let (pk, sk) = try resolveCredentials(publicKey: publicKey, secretKey: secretKey)
|
||||
return try makeRequestWithSudo(method: "POST", path: "/images/\(imageId)/transfer", publicKey: pk, secretKey: sk, data: ["to_api_key": toApiKey])
|
||||
}
|
||||
|
||||
// MARK: - PaaS Logs
|
||||
|
||||
/// Fetch batch logs from the portal
|
||||
func fetchLogs(source: String = "all", lines: Int = 100, since: String = "1h", grep: String? = nil, publicKey: String? = nil, secretKey: String? = nil) throws -> [String: Any] {
|
||||
let (pk, sk) = try resolveCredentials(publicKey: publicKey, secretKey: secretKey)
|
||||
var endpoint = "/paas/logs?source=\(source)&lines=\(lines)&since=\(since)"
|
||||
if let grep = grep {
|
||||
endpoint += "&grep=\(grep.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? grep)"
|
||||
}
|
||||
|
||||
let url = URL(string: "\(PORTAL_BASE)\(endpoint)")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.timeoutInterval = 30
|
||||
|
||||
let timestamp = Int(Date().timeIntervalSince1970)
|
||||
let signature = signRequest(secretKey: sk, timestamp: timestamp, method: "GET", path: endpoint, body: nil)
|
||||
|
||||
request.setValue("Bearer \(pk)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("\(timestamp)", forHTTPHeaderField: "X-Timestamp")
|
||||
request.setValue(signature, forHTTPHeaderField: "X-Signature")
|
||||
|
||||
var result: [String: Any]?
|
||||
var requestError: Error?
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
defer { semaphore.signal() }
|
||||
|
||||
if let error = error {
|
||||
requestError = UnsandboxError.networkError(error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
requestError = UnsandboxError.invalidResponse("No HTTP response")
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
requestError = UnsandboxError.invalidResponse("No data received")
|
||||
return
|
||||
}
|
||||
|
||||
if httpResponse.statusCode >= 400 {
|
||||
let body = String(data: data, encoding: .utf8) ?? "Unknown error"
|
||||
requestError = UnsandboxError.apiError(httpResponse.statusCode, body)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
result = json
|
||||
}
|
||||
} catch {
|
||||
requestError = UnsandboxError.invalidResponse("Failed to parse JSON")
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
semaphore.wait()
|
||||
|
||||
if let error = requestError {
|
||||
throw error
|
||||
}
|
||||
|
||||
return result ?? [:]
|
||||
}
|
||||
|
||||
// MARK: - Health Check
|
||||
|
||||
/// Check API health status
|
||||
func healthCheck() throws -> [String: Any] {
|
||||
let url = URL(string: "\(API_BASE)/health")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.timeoutInterval = 10
|
||||
|
||||
var result: [String: Any]?
|
||||
var requestError: Error?
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
defer { semaphore.signal() }
|
||||
|
||||
if let error = error {
|
||||
requestError = UnsandboxError.networkError(error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
requestError = UnsandboxError.invalidResponse("No HTTP response")
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
requestError = UnsandboxError.invalidResponse("No data received")
|
||||
return
|
||||
}
|
||||
|
||||
if httpResponse.statusCode >= 400 {
|
||||
let body = String(data: data, encoding: .utf8) ?? "Unknown error"
|
||||
requestError = UnsandboxError.apiError(httpResponse.statusCode, body)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
result = json
|
||||
}
|
||||
} catch {
|
||||
requestError = UnsandboxError.invalidResponse("Failed to parse JSON")
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
semaphore.wait()
|
||||
|
||||
if let error = requestError {
|
||||
throw error
|
||||
}
|
||||
|
||||
return result ?? [:]
|
||||
}
|
||||
|
||||
/// Get SDK version information
|
||||
func getVersion() -> [String: String] {
|
||||
return [
|
||||
"version": "1.0.0",
|
||||
"api": API_BASE,
|
||||
"portal": PORTAL_BASE
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Key Validation
|
||||
|
||||
/// Validate API keys against the portal
|
||||
|
|
@ -1538,6 +1698,19 @@ class CLIArgs {
|
|||
var visibility: String?
|
||||
var visibilityMode: String?
|
||||
var spawn: String?
|
||||
var grant: String?
|
||||
var revoke: String?
|
||||
var trusted: String?
|
||||
var trustedKey: String?
|
||||
var transfer: String?
|
||||
var toKey: String?
|
||||
|
||||
// Logs options
|
||||
var logsSource: String?
|
||||
var logsLines: Int?
|
||||
var logsSince: String?
|
||||
var logsGrep: String?
|
||||
var logsFollow: Bool = false
|
||||
|
||||
// Service-env
|
||||
var serviceEnvAction: String?
|
||||
|
|
@ -1704,7 +1877,39 @@ class CLIArgs {
|
|||
case "--spawn":
|
||||
i += 1
|
||||
if i < args.count { spawn = args[i] }
|
||||
case "session", "service", "snapshot", "image", "key", "languages":
|
||||
case "--grant":
|
||||
i += 1
|
||||
if i < args.count { grant = args[i] }
|
||||
case "--revoke":
|
||||
i += 1
|
||||
if i < args.count { revoke = args[i] }
|
||||
case "--trusted":
|
||||
i += 1
|
||||
if i < args.count { trusted = args[i] }
|
||||
case "--trusted-key":
|
||||
i += 1
|
||||
if i < args.count { trustedKey = args[i] }
|
||||
case "--transfer":
|
||||
i += 1
|
||||
if i < args.count { transfer = args[i] }
|
||||
case "--to-key":
|
||||
i += 1
|
||||
if i < args.count { toKey = args[i] }
|
||||
case "--source":
|
||||
i += 1
|
||||
if i < args.count { logsSource = args[i] }
|
||||
case "--lines":
|
||||
i += 1
|
||||
if i < args.count { logsLines = Int(args[i]) }
|
||||
case "--since":
|
||||
i += 1
|
||||
if i < args.count { logsSince = args[i] }
|
||||
case "--grep":
|
||||
i += 1
|
||||
if i < args.count { logsGrep = args[i] }
|
||||
case "--follow":
|
||||
logsFollow = true
|
||||
case "session", "service", "snapshot", "image", "key", "languages", "logs", "health", "version":
|
||||
command = arg
|
||||
case "service-env":
|
||||
command = "service-env"
|
||||
|
|
@ -2128,12 +2333,83 @@ func handleImageCommand(_ args: CLIArgs, _ pk: String, _ sk: String) throws {
|
|||
let result = try cloneImage(cloneId, name: args.name, publicKey: pk, secretKey: sk)
|
||||
let imageId = result["image_id"] as? String ?? result["id"] as? String ?? ""
|
||||
print("Image cloned: \(imageId)")
|
||||
} else if let grantId = args.grant {
|
||||
guard let trustedKey = args.trustedKey else {
|
||||
fputs("Error: --trusted-key required for --grant\n", stderr)
|
||||
exit(2)
|
||||
}
|
||||
_ = try grantImageAccess(grantId, trustedApiKey: trustedKey, publicKey: pk, secretKey: sk)
|
||||
print("Access granted to \(trustedKey)")
|
||||
} else if let revokeId = args.revoke {
|
||||
guard let trustedKey = args.trustedKey else {
|
||||
fputs("Error: --trusted-key required for --revoke\n", stderr)
|
||||
exit(2)
|
||||
}
|
||||
_ = try revokeImageAccess(revokeId, trustedApiKey: trustedKey, publicKey: pk, secretKey: sk)
|
||||
print("Access revoked from \(trustedKey)")
|
||||
} else if let trustedId = args.trusted {
|
||||
let result = try listImageTrusted(trustedId, publicKey: pk, secretKey: sk)
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: result, options: .prettyPrinted)
|
||||
print(String(data: jsonData, encoding: .utf8) ?? "{}")
|
||||
} else if let transferId = args.transfer {
|
||||
guard let toKey = args.toKey else {
|
||||
fputs("Error: --to-key required for --transfer\n", stderr)
|
||||
exit(2)
|
||||
}
|
||||
_ = try transferImage(transferId, toApiKey: toKey, publicKey: pk, secretKey: sk)
|
||||
print("Image transferred to \(toKey)")
|
||||
} else {
|
||||
fputs("Error: No action specified for image command\n", stderr)
|
||||
exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle logs command
|
||||
func handleLogsCommand(_ args: CLIArgs, _ pk: String, _ sk: String) throws {
|
||||
let source = args.logsSource ?? "all"
|
||||
let lines = args.logsLines ?? 100
|
||||
let since = args.logsSince ?? "1h"
|
||||
let grep = args.logsGrep
|
||||
|
||||
let result = try fetchLogs(source: source, lines: lines, since: since, grep: grep, publicKey: pk, secretKey: sk)
|
||||
|
||||
if let logs = result["logs"] as? [[String: Any]] {
|
||||
for log in logs {
|
||||
let src = log["source"] as? String ?? "unknown"
|
||||
let line = log["line"] as? String ?? ""
|
||||
let timestamp = log["timestamp"] as? String ?? ""
|
||||
if timestamp.isEmpty {
|
||||
print("[\(src)] \(line)")
|
||||
} else {
|
||||
print("[\(timestamp)] [\(src)] \(line)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: result, options: .prettyPrinted)
|
||||
print(String(data: jsonData, encoding: .utf8) ?? "{}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle health command
|
||||
func handleHealthCommand() throws {
|
||||
let result = try healthCheck()
|
||||
if result["status"] as? String == "healthy" || result["ok"] as? Bool == true {
|
||||
print("\u{001B}[32mAPI is healthy\u{001B}[0m")
|
||||
} else {
|
||||
print("\u{001B}[31mAPI may be unhealthy\u{001B}[0m")
|
||||
}
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: result, options: .prettyPrinted)
|
||||
print(String(data: jsonData, encoding: .utf8) ?? "{}")
|
||||
}
|
||||
|
||||
/// Handle version command
|
||||
func handleVersionCommand() {
|
||||
let info = getVersion()
|
||||
print("un.swift version \(info["version"] ?? "unknown")")
|
||||
print("API: \(info["api"] ?? "unknown")")
|
||||
print("Portal: \(info["portal"] ?? "unknown")")
|
||||
}
|
||||
|
||||
/// Format image list output
|
||||
func formatImageListOutput(_ images: [[String: Any]]) -> String {
|
||||
if images.isEmpty {
|
||||
|
|
@ -2224,6 +2500,12 @@ struct UnCLI {
|
|||
try handleKeyCommand(pk, sk)
|
||||
case "languages":
|
||||
try handleLanguagesCommand(args, pk, sk)
|
||||
case "logs":
|
||||
try handleLogsCommand(args, pk, sk)
|
||||
case "health":
|
||||
try handleHealthCommand()
|
||||
case "version":
|
||||
handleVersionCommand()
|
||||
default:
|
||||
if args.source != nil || args.shell != nil {
|
||||
try handleExecuteCommand(args, pk, sk)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
236
clients/tcl/tests/test_library.tcl
Executable file
236
clients/tcl/tests/test_library.tcl
Executable file
|
|
@ -0,0 +1,236 @@
|
|||
#!/usr/bin/env tclsh
|
||||
# Unit Tests for un.tcl Library Functions
|
||||
#
|
||||
# Tests the ACTUAL exported functions from Un module.
|
||||
# NO local re-implementations. NO mocking.
|
||||
#
|
||||
# Run: tclsh tests/test_library.tcl
|
||||
|
||||
# Adjust package path to find the module
|
||||
set script_dir [file dirname [info script]]
|
||||
source [file join $script_dir "../sync/src/un.tcl"]
|
||||
|
||||
# Test counters
|
||||
set tests_passed 0
|
||||
set tests_failed 0
|
||||
|
||||
proc PASS {msg} {
|
||||
global tests_passed
|
||||
puts " \033\[32m\[PASS\]\033\[0m $msg"
|
||||
incr tests_passed
|
||||
}
|
||||
|
||||
proc FAIL {msg} {
|
||||
global tests_failed
|
||||
puts " \033\[31m\[FAIL\]\033\[0m $msg"
|
||||
incr tests_failed
|
||||
}
|
||||
|
||||
proc assert_equal {actual expected msg} {
|
||||
if {$actual eq $expected} {
|
||||
PASS $msg
|
||||
} else {
|
||||
FAIL "$msg (expected: $expected, got: $actual)"
|
||||
}
|
||||
}
|
||||
|
||||
proc assert_not_empty {value msg} {
|
||||
if {$value ne ""} {
|
||||
PASS $msg
|
||||
} else {
|
||||
FAIL "$msg (expected non-empty)"
|
||||
}
|
||||
}
|
||||
|
||||
proc assert_match {value pattern msg} {
|
||||
if {[regexp $pattern $value]} {
|
||||
PASS $msg
|
||||
} else {
|
||||
FAIL "$msg (value: $value does not match pattern: $pattern)"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: Un::version()
|
||||
# ============================================================================
|
||||
|
||||
puts "\nTesting Un::version()..."
|
||||
|
||||
set ver [Un::version]
|
||||
assert_not_empty $ver "version() returns non-empty string"
|
||||
assert_match $ver {^\d+\.\d+\.\d+$} "version() matches X.Y.Z format"
|
||||
puts " Version: $ver"
|
||||
|
||||
# ============================================================================
|
||||
# Test: Un::detect_language()
|
||||
# ============================================================================
|
||||
|
||||
puts "\nTesting Un::detect_language()..."
|
||||
|
||||
set tests {
|
||||
{"test.py" "python"}
|
||||
{"app.js" "javascript"}
|
||||
{"main.go" "go"}
|
||||
{"script.rb" "ruby"}
|
||||
{"lib.rs" "rust"}
|
||||
{"main.c" "c"}
|
||||
{"app.cpp" "cpp"}
|
||||
{"Main.java" "java"}
|
||||
{"index.php" "php"}
|
||||
{"script.pl" "perl"}
|
||||
{"init.lua" "lua"}
|
||||
{"run.sh" "bash"}
|
||||
{"main.ts" "typescript"}
|
||||
{"app.kt" "kotlin"}
|
||||
{"lib.ex" "elixir"}
|
||||
{"main.hs" "haskell"}
|
||||
}
|
||||
|
||||
foreach test $tests {
|
||||
set file [lindex $test 0]
|
||||
set expected [lindex $test 1]
|
||||
set result [Un::detect_language $file]
|
||||
assert_equal $result $expected "detect_language('$file') -> '$expected'"
|
||||
}
|
||||
|
||||
# Test unknown extension
|
||||
set unknown [Un::detect_language "file.xyz123"]
|
||||
assert_equal $unknown "" "detect_language(unknown ext) returns empty"
|
||||
|
||||
# Test no extension
|
||||
set noext [Un::detect_language "Makefile"]
|
||||
assert_equal $noext "" "detect_language(no ext) returns empty"
|
||||
|
||||
# ============================================================================
|
||||
# Test: Un::hmac_sign()
|
||||
# ============================================================================
|
||||
|
||||
puts "\nTesting Un::hmac_sign()..."
|
||||
|
||||
# Test basic signature generation
|
||||
set sig [Un::hmac_sign "secret_key" "1234567890:POST:/execute:{}"]
|
||||
assert_not_empty $sig "hmac_sign() returns non-nil"
|
||||
assert_equal [string length $sig] 64 "hmac_sign() returns 64-char hex string"
|
||||
|
||||
# Verify hex characters
|
||||
assert_match $sig {^[0-9a-fA-F]+$} "hmac_sign() returns valid hex"
|
||||
|
||||
# Test deterministic output
|
||||
set sig1 [Un::hmac_sign "key" "message"]
|
||||
set sig2 [Un::hmac_sign "key" "message"]
|
||||
assert_equal $sig1 $sig2 "hmac_sign() is deterministic"
|
||||
|
||||
# Test different keys produce different signatures
|
||||
set sig_a [Un::hmac_sign "key_a" "message"]
|
||||
set sig_b [Un::hmac_sign "key_b" "message"]
|
||||
if {$sig_a ne $sig_b} {
|
||||
PASS "Different keys produce different signatures"
|
||||
} else {
|
||||
FAIL "Different keys produce different signatures"
|
||||
}
|
||||
|
||||
# Test different messages produce different signatures
|
||||
set sig_m1 [Un::hmac_sign "key" "message1"]
|
||||
set sig_m2 [Un::hmac_sign "key" "message2"]
|
||||
if {$sig_m1 ne $sig_m2} {
|
||||
PASS "Different messages produce different signatures"
|
||||
} else {
|
||||
FAIL "Different messages produce different signatures"
|
||||
}
|
||||
|
||||
# Test known HMAC value
|
||||
set known_sig [Un::hmac_sign "key" "message"]
|
||||
if {[string range $known_sig 0 31] eq "6e9ef29b75fffc5b7abae527d58fdadb"} {
|
||||
PASS "HMAC-SHA256('key', 'message') matches expected prefix"
|
||||
} else {
|
||||
FAIL "HMAC-SHA256('key', 'message') matches expected prefix (got: $known_sig)"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Test: Un::last_error()
|
||||
# ============================================================================
|
||||
|
||||
puts "\nTesting Un::last_error()..."
|
||||
|
||||
Un::set_error "test error"
|
||||
set err [Un::last_error]
|
||||
assert_equal $err "test error" "last_error() returns set error"
|
||||
|
||||
# ============================================================================
|
||||
# Test: Memory stress test
|
||||
# ============================================================================
|
||||
|
||||
puts "\nTesting Memory Management..."
|
||||
|
||||
# Stress test HMAC allocation
|
||||
for {set i 0} {$i < 1000} {incr i} {
|
||||
Un::hmac_sign "key" "message"
|
||||
}
|
||||
PASS "1000 HMAC calls without crash"
|
||||
|
||||
# Stress test language detection
|
||||
for {set i 0} {$i < 1000} {incr i} {
|
||||
Un::detect_language "test.py"
|
||||
}
|
||||
PASS "1000 detect_language calls without crash"
|
||||
|
||||
# Stress test version
|
||||
for {set i 0} {$i < 1000} {incr i} {
|
||||
Un::version
|
||||
}
|
||||
PASS "1000 version calls without crash"
|
||||
|
||||
# ============================================================================
|
||||
# Test: Function existence
|
||||
# ============================================================================
|
||||
|
||||
puts "\nTesting Library function existence..."
|
||||
|
||||
set functions {
|
||||
execute execute_async wait_job get_job
|
||||
cancel_job list_jobs get_languages detect_language
|
||||
|
||||
session_list session_get session_create session_destroy
|
||||
session_freeze session_unfreeze session_boost session_unboost
|
||||
session_execute
|
||||
|
||||
service_list service_get service_create service_destroy
|
||||
service_freeze service_unfreeze service_lock service_unlock
|
||||
service_set_unfreeze_on_demand service_redeploy service_logs
|
||||
service_execute service_env_get service_env_set
|
||||
service_env_delete service_env_export service_resize
|
||||
|
||||
snapshot_list snapshot_get snapshot_session snapshot_service
|
||||
snapshot_restore snapshot_delete snapshot_lock snapshot_unlock
|
||||
snapshot_clone
|
||||
|
||||
image_list image_get image_publish image_delete
|
||||
image_lock image_unlock image_set_visibility
|
||||
image_grant_access image_revoke_access image_list_trusted
|
||||
image_transfer image_spawn image_clone
|
||||
|
||||
logs_fetch logs_stream
|
||||
|
||||
validate_keys hmac_sign health_check version last_error
|
||||
}
|
||||
|
||||
foreach func $functions {
|
||||
if {[llength [info procs Un::$func]] > 0 || [llength [info commands Un::$func]] > 0} {
|
||||
PASS "Un::$func() exists"
|
||||
} else {
|
||||
FAIL "Un::$func() exists"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Summary
|
||||
# ============================================================================
|
||||
|
||||
puts "\n====================================="
|
||||
puts "Test Summary"
|
||||
puts "====================================="
|
||||
puts "Passed: \033\[32m$tests_passed\033\[0m"
|
||||
puts "Failed: \033\[31m$tests_failed\033\[0m"
|
||||
puts "====================================="
|
||||
|
||||
exit [expr {$tests_failed > 0 ? 1 : 0}]
|
||||
|
|
@ -59,6 +59,56 @@ import * as crypto from 'crypto';
|
|||
const API_BASE = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE = "https://unsandbox.com";
|
||||
const LANGUAGES_CACHE_TTL = 3600; // 1 hour in seconds
|
||||
const SDK_VERSION = "4.2.0";
|
||||
|
||||
// Thread-local error storage
|
||||
let _lastError: string | null = null;
|
||||
|
||||
// =============================================================================
|
||||
// Exported Utility Functions (for library usage)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the SDK version string.
|
||||
*/
|
||||
export function version(): string {
|
||||
return SDK_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last error message.
|
||||
*/
|
||||
export function lastError(): string | null {
|
||||
return _lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a message using HMAC-SHA256.
|
||||
* Exposed for testing and debugging purposes.
|
||||
*/
|
||||
export function hmacSign(secretKey: string, message: string): string {
|
||||
return crypto.createHmac('sha256', secretKey).update(message).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the API is healthy and responding.
|
||||
*/
|
||||
export async function healthCheck(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const url = new URL(`${API_BASE}/health`);
|
||||
const req = https.get(url, (res) => {
|
||||
resolve(res.statusCode === 200);
|
||||
});
|
||||
req.on('error', (e) => {
|
||||
_lastError = `Health check failed: ${e.message}`;
|
||||
resolve(false);
|
||||
});
|
||||
req.setTimeout(10000, () => {
|
||||
_lastError = 'Health check failed: timeout';
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
const BLUE = "\x1b[34m";
|
||||
const RED = "\x1b[31m";
|
||||
const GREEN = "\x1b[32m";
|
||||
|
|
|
|||
|
|
@ -712,7 +712,155 @@ fn get_secret_key() string {
|
|||
return ''
|
||||
}
|
||||
|
||||
fn cmd_image(list bool, info string, delete string, lock string, unlock string, publish string, source_type string, visibility_id string, visibility_mode string, spawn string, clone string, name string, ports string, api_key string) {
|
||||
fn cmd_snapshot(list bool, info string, session string, service string, restore string, delete string, lock string, unlock string, clone string, clone_type string, name string, ports string, hot bool, api_key string) {
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
|
||||
if list {
|
||||
cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:/snapshots:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/snapshots' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\""
|
||||
println(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
||||
if info != '' {
|
||||
cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:/snapshots/${info}:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/snapshots/${info}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\""
|
||||
println(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
||||
if session != '' {
|
||||
mut json := '{'
|
||||
mut has_content := false
|
||||
if name != '' {
|
||||
json += '"name":"${name}"'
|
||||
has_content = true
|
||||
}
|
||||
if hot {
|
||||
if has_content { json += ',' }
|
||||
json += '"hot":true'
|
||||
}
|
||||
json += '}'
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/sessions/${session}/snapshot:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/sessions/${session}/snapshot' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\""
|
||||
result := exec_curl(cmd)
|
||||
println('${green}Snapshot created${reset}')
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
|
||||
if service != '' {
|
||||
mut json := '{'
|
||||
mut has_content := false
|
||||
if name != '' {
|
||||
json += '"name":"${name}"'
|
||||
has_content = true
|
||||
}
|
||||
if hot {
|
||||
if has_content { json += ',' }
|
||||
json += '"hot":true'
|
||||
}
|
||||
json += '}'
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/services/${service}/snapshot:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${service}/snapshot' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\""
|
||||
result := exec_curl(cmd)
|
||||
println('${green}Snapshot created${reset}')
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
|
||||
if restore != '' {
|
||||
cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/snapshots/${restore}/restore:{}\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/snapshots/${restore}/restore' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d '{}'"
|
||||
result := exec_curl(cmd)
|
||||
println('${green}Snapshot restored${reset}')
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
|
||||
if delete != '' {
|
||||
endpoint := '/snapshots/${delete}'
|
||||
status := exec_curl_delete_with_sudo(endpoint, pub_key, secret_key)
|
||||
if status >= 200 && status < 300 {
|
||||
println('${green}Snapshot deleted: ${delete}${reset}')
|
||||
} else if status != 428 {
|
||||
eprintln('${red}Error: Failed to delete snapshot${reset}')
|
||||
exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if lock != '' {
|
||||
cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/snapshots/${lock}/lock:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/snapshots/${lock}/lock' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\""
|
||||
exec_curl(cmd)
|
||||
println('${green}Snapshot locked: ${lock}${reset}')
|
||||
return
|
||||
}
|
||||
|
||||
if unlock != '' {
|
||||
endpoint := '/snapshots/${unlock}/unlock'
|
||||
status := exec_curl_post_with_sudo(endpoint, '{}', pub_key, secret_key)
|
||||
if status >= 200 && status < 300 {
|
||||
println('${green}Snapshot unlocked: ${unlock}${reset}')
|
||||
} else if status != 428 {
|
||||
eprintln('${red}Error: Failed to unlock snapshot${reset}')
|
||||
exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if clone != '' {
|
||||
ct := if clone_type != '' { clone_type } else { 'session' }
|
||||
mut json := '{"clone_type":"${ct}"'
|
||||
if name != '' { json += ',"name":"${name}"' }
|
||||
if ports != '' { json += ',"ports":[${ports}]' }
|
||||
json += '}'
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/snapshots/${clone}/clone:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/snapshots/${clone}/clone' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\""
|
||||
result := exec_curl(cmd)
|
||||
println('${green}Snapshot cloned${reset}')
|
||||
println(result)
|
||||
return
|
||||
}
|
||||
|
||||
eprintln('${red}Error: No snapshot action specified. Use --list, --info, --session, --service, --restore, --delete, --lock, --unlock, or --clone${reset}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
fn cmd_logs(source string, lines int, since string, grep string, follow bool, api_key string) {
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
|
||||
src := if source != '' { source } else { 'all' }
|
||||
ln := if lines > 0 { lines } else { 100 }
|
||||
sn := if since != '' { since } else { '1h' }
|
||||
|
||||
if follow {
|
||||
mut endpoint := '/paas/logs/stream?source=${src}'
|
||||
if grep != '' { endpoint += '&grep=${grep}' }
|
||||
cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:${endpoint}:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -N -X GET '${portal_base}${endpoint}' -H 'Accept: text/event-stream' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\""
|
||||
println(exec_curl(cmd))
|
||||
} else {
|
||||
mut endpoint := '/paas/logs?source=${src}&lines=${ln}&since=${sn}'
|
||||
if grep != '' { endpoint += '&grep=${grep}' }
|
||||
cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:${endpoint}:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${portal_base}${endpoint}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\""
|
||||
println(exec_curl(cmd))
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_health() {
|
||||
cmd := "curl -s -X GET '${api_base}/health'"
|
||||
result := os.execute(cmd)
|
||||
if result.output.contains('"status":"healthy"') || result.output.contains('"ok":true') {
|
||||
println('${green}API is healthy${reset}')
|
||||
} else {
|
||||
println('${red}API may be unhealthy${reset}')
|
||||
}
|
||||
println(result.output)
|
||||
}
|
||||
|
||||
fn cmd_version() {
|
||||
println('un.v version 1.0.0')
|
||||
println('API: ${api_base}')
|
||||
println('Portal: ${portal_base}')
|
||||
}
|
||||
|
||||
fn cmd_image(list bool, info string, delete string, lock string, unlock string, publish string, source_type string, visibility_id string, visibility_mode string, spawn string, clone string, name string, ports string, grant string, revoke string, trusted string, trusted_key string, transfer string, to_key string, api_key string) {
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
|
||||
|
|
@ -811,7 +959,54 @@ fn cmd_image(list bool, info string, delete string, lock string, unlock string,
|
|||
return
|
||||
}
|
||||
|
||||
eprintln('${red}Error: No image action specified. Use --list, --info, --delete, --publish, etc.${reset}')
|
||||
if grant != '' {
|
||||
if trusted_key == '' {
|
||||
eprintln('${red}Error: --grant requires --trusted-key${reset}')
|
||||
exit(1)
|
||||
}
|
||||
json := '{"trusted_api_key":"${trusted_key}"}'
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/images/${grant}/grant:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/images/${grant}/grant' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\""
|
||||
exec_curl(cmd)
|
||||
println('${green}Access granted to ${trusted_key}${reset}')
|
||||
return
|
||||
}
|
||||
|
||||
if revoke != '' {
|
||||
if trusted_key == '' {
|
||||
eprintln('${red}Error: --revoke requires --trusted-key${reset}')
|
||||
exit(1)
|
||||
}
|
||||
json := '{"trusted_api_key":"${trusted_key}"}'
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/images/${revoke}/revoke:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/images/${revoke}/revoke' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\""
|
||||
exec_curl(cmd)
|
||||
println('${green}Access revoked from ${trusted_key}${reset}')
|
||||
return
|
||||
}
|
||||
|
||||
if trusted != '' {
|
||||
cmd := "TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:GET:/images/${trusted}/trusted:\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/images/${trusted}/trusted' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\""
|
||||
println(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
||||
if transfer != '' {
|
||||
if to_key == '' {
|
||||
eprintln('${red}Error: --transfer requires --to-key${reset}')
|
||||
exit(1)
|
||||
}
|
||||
json := '{"to_api_key":"${to_key}"}'
|
||||
endpoint := '/images/${transfer}/transfer'
|
||||
status := exec_curl_post_with_sudo(endpoint, json, pub_key, secret_key)
|
||||
if status >= 200 && status < 300 {
|
||||
println('${green}Image transferred to ${to_key}${reset}')
|
||||
} else if status != 428 {
|
||||
eprintln('${red}Error: Failed to transfer image${reset}')
|
||||
exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
eprintln('${red}Error: No image action specified. Use --list, --info, --delete, --publish, --grant, --revoke, --trusted, --transfer, etc.${reset}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -1277,6 +1472,12 @@ fn main() {
|
|||
mut clone := ''
|
||||
mut name := ''
|
||||
mut ports := ''
|
||||
mut grant := ''
|
||||
mut revoke := ''
|
||||
mut trusted := ''
|
||||
mut trusted_key := ''
|
||||
mut transfer := ''
|
||||
mut to_key := ''
|
||||
|
||||
mut i := 2
|
||||
for i < os.args.len {
|
||||
|
|
@ -1330,6 +1531,30 @@ fn main() {
|
|||
i++
|
||||
ports = os.args[i]
|
||||
}
|
||||
'--grant' {
|
||||
i++
|
||||
grant = os.args[i]
|
||||
}
|
||||
'--revoke' {
|
||||
i++
|
||||
revoke = os.args[i]
|
||||
}
|
||||
'--trusted' {
|
||||
i++
|
||||
trusted = os.args[i]
|
||||
}
|
||||
'--trusted-key' {
|
||||
i++
|
||||
trusted_key = os.args[i]
|
||||
}
|
||||
'--transfer' {
|
||||
i++
|
||||
transfer = os.args[i]
|
||||
}
|
||||
'--to-key' {
|
||||
i++
|
||||
to_key = os.args[i]
|
||||
}
|
||||
'-k' {
|
||||
i++
|
||||
api_key = os.args[i]
|
||||
|
|
@ -1339,7 +1564,83 @@ fn main() {
|
|||
i++
|
||||
}
|
||||
|
||||
cmd_image(list, info, delete, lock, unlock, publish, source_type, visibility_id, visibility_mode, spawn, clone, name, ports, api_key)
|
||||
cmd_image(list, info, delete, lock, unlock, publish, source_type, visibility_id, visibility_mode, spawn, clone, name, ports, grant, revoke, trusted, trusted_key, transfer, to_key, api_key)
|
||||
return
|
||||
}
|
||||
|
||||
if os.args[1] == 'snapshot' {
|
||||
mut list := false
|
||||
mut info := ''
|
||||
mut session := ''
|
||||
mut service := ''
|
||||
mut restore := ''
|
||||
mut delete := ''
|
||||
mut lock := ''
|
||||
mut unlock := ''
|
||||
mut clone := ''
|
||||
mut clone_type := ''
|
||||
mut name := ''
|
||||
mut ports := ''
|
||||
mut hot := false
|
||||
|
||||
mut i := 2
|
||||
for i < os.args.len {
|
||||
match os.args[i] {
|
||||
'--list', '-l' { list = true }
|
||||
'--info' { i++; info = os.args[i] }
|
||||
'--session' { i++; session = os.args[i] }
|
||||
'--service' { i++; service = os.args[i] }
|
||||
'--restore' { i++; restore = os.args[i] }
|
||||
'--delete' { i++; delete = os.args[i] }
|
||||
'--lock' { i++; lock = os.args[i] }
|
||||
'--unlock' { i++; unlock = os.args[i] }
|
||||
'--clone' { i++; clone = os.args[i] }
|
||||
'--clone-type' { i++; clone_type = os.args[i] }
|
||||
'--name' { i++; name = os.args[i] }
|
||||
'--ports' { i++; ports = os.args[i] }
|
||||
'--hot' { hot = true }
|
||||
'-k' { i++; api_key = os.args[i] }
|
||||
else {}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
cmd_snapshot(list, info, session, service, restore, delete, lock, unlock, clone, clone_type, name, ports, hot, api_key)
|
||||
return
|
||||
}
|
||||
|
||||
if os.args[1] == 'logs' {
|
||||
mut source := ''
|
||||
mut lines := 0
|
||||
mut since := ''
|
||||
mut grep := ''
|
||||
mut follow := false
|
||||
|
||||
mut i := 2
|
||||
for i < os.args.len {
|
||||
match os.args[i] {
|
||||
'--source' { i++; source = os.args[i] }
|
||||
'--lines' { i++; lines = os.args[i].int() }
|
||||
'--since' { i++; since = os.args[i] }
|
||||
'--grep' { i++; grep = os.args[i] }
|
||||
'--follow', '-f' { follow = true }
|
||||
'-k' { i++; api_key = os.args[i] }
|
||||
else {}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
cmd_logs(source, lines, since, grep, follow, api_key)
|
||||
return
|
||||
}
|
||||
|
||||
if os.args[1] == 'health' {
|
||||
cmd_health()
|
||||
return
|
||||
}
|
||||
|
||||
if os.args[1] == 'version' {
|
||||
cmd_version()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -319,6 +319,401 @@ fn execCurlPut(allocator: std.mem.Allocator, endpoint: []const u8, body: []const
|
|||
return ret == 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Library Functions for Zig SDK (matching C reference un.h)
|
||||
// ============================================================================
|
||||
|
||||
pub const SDK_VERSION = "4.2.0";
|
||||
|
||||
// Generic API request helper
|
||||
fn makeApiRequest(allocator: std.mem.Allocator, method: []const u8, path: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const result = try execCurlWithStatus(allocator, method, path, body, public_key, secret_key, "");
|
||||
return result.body;
|
||||
}
|
||||
|
||||
// Execute code synchronously
|
||||
pub fn execute(allocator: std.mem.Allocator, language: []const u8, code: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const body = try std.fmt.allocPrint(allocator, "{{\"language\":\"{s}\",\"code\":\"{s}\"}}", .{ language, code });
|
||||
defer allocator.free(body);
|
||||
return try makeApiRequest(allocator, "POST", "/execute", body, public_key, secret_key);
|
||||
}
|
||||
|
||||
// Execute code asynchronously (returns job_id)
|
||||
pub fn executeAsync(allocator: std.mem.Allocator, language: []const u8, code: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const body = try std.fmt.allocPrint(allocator, "{{\"language\":\"{s}\",\"code\":\"{s}\",\"async\":true}}", .{ language, code });
|
||||
defer allocator.free(body);
|
||||
return try makeApiRequest(allocator, "POST", "/execute", body, public_key, secret_key);
|
||||
}
|
||||
|
||||
// Get job status
|
||||
pub fn getJob(allocator: std.mem.Allocator, job_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/jobs/{s}", .{job_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
// Wait for job completion
|
||||
pub fn waitForJob(allocator: std.mem.Allocator, job_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const poll_delays = [_]u64{ 300, 450, 700, 900, 650, 1600, 2000 };
|
||||
var delay_idx: usize = 0;
|
||||
|
||||
while (true) {
|
||||
const result = try getJob(allocator, job_id, public_key, secret_key);
|
||||
|
||||
// Check for terminal states
|
||||
if (mem.indexOf(u8, result, "\"status\":\"completed\"") != null or
|
||||
mem.indexOf(u8, result, "\"status\":\"failed\"") != null or
|
||||
mem.indexOf(u8, result, "\"status\":\"timeout\"") != null or
|
||||
mem.indexOf(u8, result, "\"status\":\"cancelled\"") != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
allocator.free(result);
|
||||
std.time.sleep(poll_delays[delay_idx] * std.time.ns_per_ms);
|
||||
if (delay_idx < poll_delays.len - 1) delay_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel a job
|
||||
pub fn cancelJob(allocator: std.mem.Allocator, job_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/jobs/{s}/cancel", .{job_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
// List all jobs
|
||||
pub fn listJobs(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
return try makeApiRequest(allocator, "GET", "/jobs", "", public_key, secret_key);
|
||||
}
|
||||
|
||||
// Get supported languages
|
||||
pub fn getLanguages(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
return try makeApiRequest(allocator, "GET", "/languages", "", public_key, secret_key);
|
||||
}
|
||||
|
||||
// Detect language from filename
|
||||
pub fn detectLanguage(filename: []const u8) ?[]const u8 {
|
||||
const lang_map = [_]struct { ext: []const u8, lang: []const u8 }{
|
||||
.{ .ext = ".py", .lang = "python" },
|
||||
.{ .ext = ".js", .lang = "javascript" },
|
||||
.{ .ext = ".ts", .lang = "typescript" },
|
||||
.{ .ext = ".go", .lang = "go" },
|
||||
.{ .ext = ".rs", .lang = "rust" },
|
||||
.{ .ext = ".c", .lang = "c" },
|
||||
.{ .ext = ".cpp", .lang = "cpp" },
|
||||
.{ .ext = ".cc", .lang = "cpp" },
|
||||
.{ .ext = ".d", .lang = "d" },
|
||||
.{ .ext = ".zig", .lang = "zig" },
|
||||
.{ .ext = ".rb", .lang = "ruby" },
|
||||
.{ .ext = ".php", .lang = "php" },
|
||||
.{ .ext = ".sh", .lang = "bash" },
|
||||
.{ .ext = ".lua", .lang = "lua" },
|
||||
.{ .ext = ".nim", .lang = "nim" },
|
||||
.{ .ext = ".v", .lang = "v" },
|
||||
};
|
||||
|
||||
const idx = mem.lastIndexOfScalar(u8, filename, '.') orelse return null;
|
||||
const ext = filename[idx..];
|
||||
|
||||
for (lang_map) |entry| {
|
||||
if (mem.eql(u8, ext, entry.ext)) {
|
||||
return entry.lang;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Session functions
|
||||
pub fn sessionList(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
return try makeApiRequest(allocator, "GET", "/sessions", "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn sessionGet(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}", .{session_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn sessionCreate(allocator: std.mem.Allocator, shell: ?[]const u8, network: ?[]const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
var body_buf: [512]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&body_buf);
|
||||
const writer = stream.writer();
|
||||
try writer.print("{{\"shell\":\"{s}\"", .{shell orelse "bash"});
|
||||
if (network) |n| try writer.print(",\"network\":\"{s}\"", .{n});
|
||||
try writer.writeAll("}");
|
||||
const body = stream.getWritten();
|
||||
return try makeApiRequest(allocator, "POST", "/sessions", body, public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn sessionDestroy(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}", .{session_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "DELETE", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn sessionFreeze(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/freeze", .{session_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn sessionUnfreeze(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/unfreeze", .{session_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn sessionBoost(allocator: std.mem.Allocator, session_id: []const u8, vcpu: ?u32, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/boost", .{session_id});
|
||||
defer allocator.free(path);
|
||||
var body: []const u8 = "{}";
|
||||
if (vcpu) |v| {
|
||||
body = try std.fmt.allocPrint(allocator, "{{\"vcpu\":{d}}}", .{v});
|
||||
}
|
||||
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn sessionUnboost(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/unboost", .{session_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn sessionExecute(allocator: std.mem.Allocator, session_id: []const u8, command: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/shell", .{session_id});
|
||||
defer allocator.free(path);
|
||||
const body = try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\"}}", .{command});
|
||||
defer allocator.free(body);
|
||||
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
|
||||
}
|
||||
|
||||
// Service functions
|
||||
pub fn serviceList(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
return try makeApiRequest(allocator, "GET", "/services", "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceGet(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{service_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceDestroy(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{service_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "DELETE", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceFreeze(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/freeze", .{service_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceUnfreeze(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/unfreeze", .{service_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceLock(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/lock", .{service_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceUnlock(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/unlock", .{service_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceRedeploy(allocator: std.mem.Allocator, service_id: []const u8, bootstrap: ?[]const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/redeploy", .{service_id});
|
||||
defer allocator.free(path);
|
||||
const body = if (bootstrap) |b| try std.fmt.allocPrint(allocator, "{{\"bootstrap\":\"{s}\"}}", .{b}) else try allocator.dupe(u8, "{}");
|
||||
defer allocator.free(body);
|
||||
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceLogs(allocator: std.mem.Allocator, service_id: []const u8, all: bool, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = if (all)
|
||||
try std.fmt.allocPrint(allocator, "/services/{s}/logs?all=true", .{service_id})
|
||||
else
|
||||
try std.fmt.allocPrint(allocator, "/services/{s}/logs", .{service_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceExecute(allocator: std.mem.Allocator, service_id: []const u8, command: []const u8, timeout_ms: ?u32, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/execute", .{service_id});
|
||||
defer allocator.free(path);
|
||||
const body = if (timeout_ms) |t|
|
||||
try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\",\"timeout\":{d}}}", .{ command, t })
|
||||
else
|
||||
try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\"}}", .{command});
|
||||
defer allocator.free(body);
|
||||
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn serviceResize(allocator: std.mem.Allocator, service_id: []const u8, vcpu: u32, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/resize", .{service_id});
|
||||
defer allocator.free(path);
|
||||
const body = try std.fmt.allocPrint(allocator, "{{\"vcpu\":{d}}}", .{vcpu});
|
||||
defer allocator.free(body);
|
||||
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
|
||||
}
|
||||
|
||||
// Snapshot functions
|
||||
pub fn snapshotList(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
return try makeApiRequest(allocator, "GET", "/snapshots", "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn snapshotGet(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}", .{snapshot_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn snapshotRestore(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}/restore", .{snapshot_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn snapshotDelete(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}", .{snapshot_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "DELETE", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn snapshotLock(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}/lock", .{snapshot_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn snapshotUnlock(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}/unlock", .{snapshot_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
// Image functions
|
||||
pub fn imageList(allocator: std.mem.Allocator, filter: ?[]const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = if (filter) |f|
|
||||
try std.fmt.allocPrint(allocator, "/images?filter={s}", .{f})
|
||||
else
|
||||
try allocator.dupe(u8, "/images");
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn imageGet(allocator: std.mem.Allocator, image_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/images/{s}", .{image_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn imageDelete(allocator: std.mem.Allocator, image_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/images/{s}", .{image_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "DELETE", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn imageLock(allocator: std.mem.Allocator, image_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/images/{s}/lock", .{image_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
pub fn imageUnlock(allocator: std.mem.Allocator, image_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
const path = try std.fmt.allocPrint(allocator, "/images/{s}/unlock", .{image_id});
|
||||
defer allocator.free(path);
|
||||
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
// PaaS Logs functions
|
||||
pub fn logsFetch(allocator: std.mem.Allocator, source: ?[]const u8, lines: ?u32, since: ?[]const u8, grep: ?[]const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
var path_buf: [512]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&path_buf);
|
||||
const writer = stream.writer();
|
||||
try writer.writeAll("/paas/logs?");
|
||||
var has_param = false;
|
||||
if (source) |s| {
|
||||
try writer.print("source={s}", .{s});
|
||||
has_param = true;
|
||||
}
|
||||
if (lines) |l| {
|
||||
if (has_param) try writer.writeAll("&");
|
||||
try writer.print("lines={d}", .{l});
|
||||
has_param = true;
|
||||
}
|
||||
if (since) |s| {
|
||||
if (has_param) try writer.writeAll("&");
|
||||
try writer.print("since={s}", .{s});
|
||||
has_param = true;
|
||||
}
|
||||
if (grep) |g| {
|
||||
if (has_param) try writer.writeAll("&");
|
||||
try writer.print("grep={s}", .{g});
|
||||
}
|
||||
const path = stream.getWritten();
|
||||
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
|
||||
}
|
||||
|
||||
// Key validation
|
||||
pub fn validateKeys(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
return try makeApiRequest(allocator, "POST", "/keys/validate", "", public_key, secret_key);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
pub fn hmacSign(allocator: std.mem.Allocator, secret_key: []const u8, message: []const u8) ![]const u8 {
|
||||
const hmac_cmd = try computeHmacCmd(allocator, secret_key, message);
|
||||
defer allocator.free(hmac_cmd);
|
||||
|
||||
const result = try std.process.Child.run(.{
|
||||
.allocator = allocator,
|
||||
.argv = &[_][]const u8{ "sh", "-c", hmac_cmd },
|
||||
});
|
||||
defer allocator.free(result.stderr);
|
||||
|
||||
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
|
||||
const signature = try allocator.dupe(u8, trimmed);
|
||||
allocator.free(result.stdout);
|
||||
return signature;
|
||||
}
|
||||
|
||||
pub fn healthCheck(allocator: std.mem.Allocator) !bool {
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -o /dev/null -w '%{{http_code}}' '{s}/health' 2>/dev/null", .{API_BASE});
|
||||
defer allocator.free(cmd);
|
||||
|
||||
const result = try std.process.Child.run(.{
|
||||
.allocator = allocator,
|
||||
.argv = &[_][]const u8{ "sh", "-c", cmd },
|
||||
});
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
|
||||
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
|
||||
return mem.eql(u8, trimmed, "200");
|
||||
}
|
||||
|
||||
pub fn version() []const u8 {
|
||||
return SDK_VERSION;
|
||||
}
|
||||
|
||||
var last_error_msg: []const u8 = "";
|
||||
|
||||
pub fn setLastError(msg: []const u8) void {
|
||||
last_error_msg = msg;
|
||||
}
|
||||
|
||||
pub fn lastError() []const u8 {
|
||||
return last_error_msg;
|
||||
}
|
||||
|
||||
fn cmdServiceEnv(allocator: std.mem.Allocator, action: []const u8, target: []const u8, envs: std.ArrayList([]const u8), env_file: ?[]const u8, public_key: []const u8, secret_key: []const u8) !void {
|
||||
if (mem.eql(u8, action, "status")) {
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/env", .{target});
|
||||
|
|
|
|||
213
clients/zig/sync/tests/test_un.zig
Normal file
213
clients/zig/sync/tests/test_un.zig
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
// Tests for the Zig unsandbox SDK
|
||||
// Run with: zig test tests/test_un.zig
|
||||
|
||||
const std = @import("std");
|
||||
const un = @import("../src/un.zig");
|
||||
const testing = std.testing;
|
||||
const mem = std.mem;
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests - Test exported library functions
|
||||
// ============================================================================
|
||||
|
||||
test "detectLanguage" {
|
||||
try testing.expectEqualStrings("python", un.detectLanguage("script.py") orelse "");
|
||||
try testing.expectEqualStrings("javascript", un.detectLanguage("script.js") orelse "");
|
||||
try testing.expectEqualStrings("typescript", un.detectLanguage("script.ts") orelse "");
|
||||
try testing.expectEqualStrings("go", un.detectLanguage("script.go") orelse "");
|
||||
try testing.expectEqualStrings("rust", un.detectLanguage("script.rs") orelse "");
|
||||
try testing.expectEqualStrings("c", un.detectLanguage("script.c") orelse "");
|
||||
try testing.expectEqualStrings("cpp", un.detectLanguage("script.cpp") orelse "");
|
||||
try testing.expectEqualStrings("d", un.detectLanguage("script.d") orelse "");
|
||||
try testing.expectEqualStrings("zig", un.detectLanguage("script.zig") orelse "");
|
||||
try testing.expectEqualStrings("bash", un.detectLanguage("script.sh") orelse "");
|
||||
try testing.expectEqualStrings("ruby", un.detectLanguage("script.rb") orelse "");
|
||||
try testing.expectEqualStrings("php", un.detectLanguage("script.php") orelse "");
|
||||
try testing.expect(un.detectLanguage("script.unknown") == null);
|
||||
try testing.expect(un.detectLanguage("script") == null);
|
||||
}
|
||||
|
||||
test "version" {
|
||||
const v = un.version();
|
||||
try testing.expect(v.len > 0);
|
||||
// Should be in semver format (at least "0.0.0")
|
||||
try testing.expect(v.len >= 5);
|
||||
}
|
||||
|
||||
test "lastError" {
|
||||
// Set an error
|
||||
un.setLastError("test error message");
|
||||
|
||||
// Retrieve it
|
||||
const err = un.lastError();
|
||||
try testing.expectEqualStrings("test error message", err);
|
||||
|
||||
// Clear it
|
||||
un.setLastError("");
|
||||
const err2 = un.lastError();
|
||||
try testing.expectEqualStrings("", err2);
|
||||
}
|
||||
|
||||
test "SDK_VERSION constant" {
|
||||
try testing.expect(un.SDK_VERSION.len > 0);
|
||||
try testing.expect(un.SDK_VERSION.len >= 5);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests - Test SDK internal consistency
|
||||
// ============================================================================
|
||||
|
||||
test "hmacSign" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const secret_key = "test-secret";
|
||||
const message = "test-message";
|
||||
|
||||
const result = try un.hmacSign(allocator, secret_key, message);
|
||||
defer allocator.free(result);
|
||||
|
||||
// Should return a 64-character hex string
|
||||
try testing.expectEqual(@as(usize, 64), result.len);
|
||||
|
||||
// Should be deterministic
|
||||
const result2 = try un.hmacSign(allocator, secret_key, message);
|
||||
defer allocator.free(result2);
|
||||
try testing.expectEqualStrings(result, result2);
|
||||
|
||||
// Different inputs should produce different outputs
|
||||
const result3 = try un.hmacSign(allocator, secret_key, "different-message");
|
||||
defer allocator.free(result3);
|
||||
try testing.expect(!mem.eql(u8, result, result3));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Functional Tests - Test against real API (requires credentials)
|
||||
// These tests are marked as skipped by default since they require credentials
|
||||
// ============================================================================
|
||||
|
||||
fn hasCredentials() bool {
|
||||
const pk = std.posix.getenv("UNSANDBOX_PUBLIC_KEY") orelse return false;
|
||||
const sk = std.posix.getenv("UNSANDBOX_SECRET_KEY") orelse return false;
|
||||
return pk.len > 0 and sk.len > 0;
|
||||
}
|
||||
|
||||
fn getCredentials() ?struct { pk: []const u8, sk: []const u8 } {
|
||||
const pk = std.posix.getenv("UNSANDBOX_PUBLIC_KEY") orelse return null;
|
||||
const sk = std.posix.getenv("UNSANDBOX_SECRET_KEY") orelse return null;
|
||||
if (pk.len == 0 or sk.len == 0) return null;
|
||||
return .{ .pk = pk, .sk = sk };
|
||||
}
|
||||
|
||||
test "healthCheck functional" {
|
||||
if (!hasCredentials()) {
|
||||
std.debug.print("SKIP (no credentials)\n", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const healthy = try un.healthCheck(allocator);
|
||||
std.debug.print("Health check result: {}\n", .{healthy});
|
||||
}
|
||||
|
||||
test "getLanguages functional" {
|
||||
const creds = getCredentials() orelse {
|
||||
std.debug.print("SKIP (no credentials)\n", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const result = try un.getLanguages(allocator, creds.pk, creds.sk);
|
||||
defer allocator.free(result);
|
||||
|
||||
try testing.expect(result.len > 0);
|
||||
// Should contain python
|
||||
try testing.expect(mem.indexOf(u8, result, "python") != null);
|
||||
}
|
||||
|
||||
test "validateKeys functional" {
|
||||
const creds = getCredentials() orelse {
|
||||
std.debug.print("SKIP (no credentials)\n", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const result = try un.validateKeys(allocator, creds.pk, creds.sk);
|
||||
defer allocator.free(result);
|
||||
|
||||
try testing.expect(result.len > 0);
|
||||
}
|
||||
|
||||
test "sessionList functional" {
|
||||
const creds = getCredentials() orelse {
|
||||
std.debug.print("SKIP (no credentials)\n", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const result = try un.sessionList(allocator, creds.pk, creds.sk);
|
||||
defer allocator.free(result);
|
||||
|
||||
try testing.expect(result.len > 0);
|
||||
}
|
||||
|
||||
test "serviceList functional" {
|
||||
const creds = getCredentials() orelse {
|
||||
std.debug.print("SKIP (no credentials)\n", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const result = try un.serviceList(allocator, creds.pk, creds.sk);
|
||||
defer allocator.free(result);
|
||||
|
||||
try testing.expect(result.len > 0);
|
||||
}
|
||||
|
||||
test "snapshotList functional" {
|
||||
const creds = getCredentials() orelse {
|
||||
std.debug.print("SKIP (no credentials)\n", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const result = try un.snapshotList(allocator, creds.pk, creds.sk);
|
||||
defer allocator.free(result);
|
||||
|
||||
try testing.expect(result.len > 0);
|
||||
}
|
||||
|
||||
test "imageList functional" {
|
||||
const creds = getCredentials() orelse {
|
||||
std.debug.print("SKIP (no credentials)\n", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const result = try un.imageList(allocator, null, creds.pk, creds.sk);
|
||||
defer allocator.free(result);
|
||||
|
||||
try testing.expect(result.len > 0);
|
||||
}
|
||||
|
|
@ -120,6 +120,25 @@
|
|||
(catch Exception e
|
||||
(->TestResult false (str "Exception: " (.getMessage e))))))))
|
||||
|
||||
;; Test 4: Snapshot command support (feature parity test)
|
||||
(defn test-snapshot-command []
|
||||
(let [api-key (System/getenv "UNSANDBOX_API_KEY")]
|
||||
(if (nil? api-key)
|
||||
(->TestResult true "Skipped - no API key")
|
||||
(try
|
||||
;; Run the CLI with snapshot --list
|
||||
(let [result (shell/sh "./un.clj" "snapshot" "--list")
|
||||
{:keys [exit out err]} result]
|
||||
|
||||
;; Check if it executed without errors (may return empty list)
|
||||
(if (= exit 0)
|
||||
(->TestResult true nil)
|
||||
(->TestResult false (str "Snapshot list failed: exit=" exit
|
||||
", stdout=" out
|
||||
", stderr=" err))))
|
||||
(catch Exception e
|
||||
(->TestResult false (str "Exception: " (.getMessage e))))))))
|
||||
|
||||
;; Main test runner
|
||||
(defn main []
|
||||
(println "=== Clojure UN CLI Test Suite ===")
|
||||
|
|
@ -134,7 +153,8 @@
|
|||
;; Run tests
|
||||
(let [results [(print-result "Extension detection" (test-extension-detection))
|
||||
(print-result "API integration" (test-api-integration))
|
||||
(print-result "Fibonacci end-to-end test" (test-fibonacci))]
|
||||
(print-result "Fibonacci end-to-end test" (test-fibonacci))
|
||||
(print-result "Snapshot command support" (test-snapshot-command))]
|
||||
passed (count (filter true? results))
|
||||
total (count results)]
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,143 @@ print_test("Unknown extension returns 'unknown'", detect_language("file.unknown"
|
|||
print_test("Case insensitive detection", detect_language("TEST.CR") == "crystal")
|
||||
print_test("Multiple dots in filename", detect_language("my.test.py") == "python")
|
||||
|
||||
# Test 5: CLI Command Tests (new features)
|
||||
puts "\n#{BLUE}Test Suite 5: CLI Command Tests (Feature Parity)#{RESET}"
|
||||
|
||||
un_script = File.expand_path("../clients/crystal/sync/src/un.cr", File.dirname(__FILE__))
|
||||
un_script = File.expand_path("../../clients/crystal/sync/src/un.cr", File.dirname(__FILE__)) unless File.exists?(un_script)
|
||||
|
||||
if File.exists?(un_script)
|
||||
# Test: --help
|
||||
begin
|
||||
output = IO::Memory.new
|
||||
error = IO::Memory.new
|
||||
process = Process.run("crystal", args: ["run", un_script, "--", "--help"],
|
||||
output: output, error: error)
|
||||
result = output.to_s + error.to_s
|
||||
has_help = result.includes?("Usage") || result.includes?("usage")
|
||||
print_test("CLI: --help shows usage", has_help)
|
||||
rescue ex
|
||||
print_test("CLI: --help shows usage", false)
|
||||
puts " Error: #{ex.message}"
|
||||
end
|
||||
|
||||
# Test: version command
|
||||
begin
|
||||
output = IO::Memory.new
|
||||
error = IO::Memory.new
|
||||
process = Process.run("crystal", args: ["run", un_script, "--", "version"],
|
||||
output: output, error: error)
|
||||
result = output.to_s + error.to_s
|
||||
has_version = result.includes?("version") || result.includes?("Version")
|
||||
print_test("CLI: version command works", has_version)
|
||||
rescue ex
|
||||
print_test("CLI: version command works", false)
|
||||
puts " Error: #{ex.message}"
|
||||
end
|
||||
|
||||
# Test: health command (requires network)
|
||||
begin
|
||||
output = IO::Memory.new
|
||||
error = IO::Memory.new
|
||||
process = Process.run("crystal", args: ["run", un_script, "--", "health"],
|
||||
output: output, error: error)
|
||||
result = output.to_s + error.to_s
|
||||
has_health = result.includes?("health") || result.includes?("API")
|
||||
print_test("CLI: health command works", has_health)
|
||||
rescue ex
|
||||
print_test("CLI: health command works", false)
|
||||
puts " Error: #{ex.message}"
|
||||
end
|
||||
|
||||
# Test: languages command
|
||||
begin
|
||||
output = IO::Memory.new
|
||||
error = IO::Memory.new
|
||||
process = Process.run("crystal", args: ["run", un_script, "--", "languages"],
|
||||
output: output, error: error)
|
||||
result = output.to_s + error.to_s
|
||||
# Should list languages or require auth
|
||||
has_langs = result.includes?("python") || result.includes?("Error") || result.includes?("API key")
|
||||
print_test("CLI: languages command works", has_langs)
|
||||
rescue ex
|
||||
print_test("CLI: languages command works", false)
|
||||
puts " Error: #{ex.message}"
|
||||
end
|
||||
else
|
||||
puts "#{BLUE}ℹ SKIP#{RESET}: CLI tests (un.cr not found at expected location: #{un_script})"
|
||||
end
|
||||
|
||||
# Test 6: API Command Tests (require API key)
|
||||
puts "\n#{BLUE}Test Suite 6: API Command Tests (require auth)#{RESET}"
|
||||
public_key = ENV["UNSANDBOX_PUBLIC_KEY"]?
|
||||
secret_key = ENV["UNSANDBOX_SECRET_KEY"]?
|
||||
|
||||
if (public_key.nil? || public_key.empty?) && (secret_key.nil? || secret_key.empty?)
|
||||
puts "#{BLUE}ℹ SKIP#{RESET}: API command tests (UNSANDBOX_PUBLIC_KEY/SECRET_KEY not set)"
|
||||
else
|
||||
if File.exists?(un_script)
|
||||
# Test: snapshot --list
|
||||
begin
|
||||
output = IO::Memory.new
|
||||
error = IO::Memory.new
|
||||
process = Process.run("crystal", args: ["run", un_script, "--", "snapshot", "--list"],
|
||||
output: output, error: error)
|
||||
result = output.to_s + error.to_s
|
||||
# Should return JSON or error message
|
||||
has_response = result.includes?("[") || result.includes?("{") || result.includes?("Error") || result.includes?("snapshots")
|
||||
print_test("CLI: snapshot --list works", has_response)
|
||||
rescue ex
|
||||
print_test("CLI: snapshot --list works", false)
|
||||
puts " Error: #{ex.message}"
|
||||
end
|
||||
|
||||
# Test: session --list
|
||||
begin
|
||||
output = IO::Memory.new
|
||||
error = IO::Memory.new
|
||||
process = Process.run("crystal", args: ["run", un_script, "--", "session", "--list"],
|
||||
output: output, error: error)
|
||||
result = output.to_s + error.to_s
|
||||
has_response = result.includes?("[") || result.includes?("{") || result.includes?("Error") || result.includes?("sessions")
|
||||
print_test("CLI: session --list works", has_response)
|
||||
rescue ex
|
||||
print_test("CLI: session --list works", false)
|
||||
puts " Error: #{ex.message}"
|
||||
end
|
||||
|
||||
# Test: service --list
|
||||
begin
|
||||
output = IO::Memory.new
|
||||
error = IO::Memory.new
|
||||
process = Process.run("crystal", args: ["run", un_script, "--", "service", "--list"],
|
||||
output: output, error: error)
|
||||
result = output.to_s + error.to_s
|
||||
has_response = result.includes?("[") || result.includes?("{") || result.includes?("Error") || result.includes?("services")
|
||||
print_test("CLI: service --list works", has_response)
|
||||
rescue ex
|
||||
print_test("CLI: service --list works", false)
|
||||
puts " Error: #{ex.message}"
|
||||
end
|
||||
|
||||
# Test: image --list
|
||||
begin
|
||||
output = IO::Memory.new
|
||||
error = IO::Memory.new
|
||||
process = Process.run("crystal", args: ["run", un_script, "--", "image", "--list"],
|
||||
output: output, error: error)
|
||||
result = output.to_s + error.to_s
|
||||
has_response = result.includes?("[") || result.includes?("{") || result.includes?("Error") || result.includes?("images")
|
||||
print_test("CLI: image --list works", has_response)
|
||||
rescue ex
|
||||
print_test("CLI: image --list works", false)
|
||||
puts " Error: #{ex.message}"
|
||||
end
|
||||
else
|
||||
puts "#{BLUE}ℹ SKIP#{RESET}: API command tests (un.cr not found)"
|
||||
end
|
||||
end
|
||||
|
||||
# Print summary
|
||||
puts "\n#{BLUE}========================================#{RESET}"
|
||||
puts "#{BLUE}Test Summary#{RESET}"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ void main() async {
|
|||
if (apiKey != null && apiKey.isNotEmpty) {
|
||||
await testApiCall();
|
||||
await testFibExecution();
|
||||
await testSnapshotCommand();
|
||||
} else {
|
||||
print('SKIP: API integration tests (UNSANDBOX_API_KEY not set)\n');
|
||||
}
|
||||
|
|
@ -220,3 +221,28 @@ Future<void> testFibExecution() async {
|
|||
}
|
||||
print('');
|
||||
}
|
||||
|
||||
Future<void> testSnapshotCommand() async {
|
||||
print('--- Feature Parity Test: Snapshot Command ---');
|
||||
testsRun++;
|
||||
|
||||
try {
|
||||
// Execute Dart CLI with snapshot --list
|
||||
final result = await Process.run('dart', ['../un.dart', 'snapshot', '--list']);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
testsPassed++;
|
||||
print('PASS: snapshot --list command works');
|
||||
} else {
|
||||
testsFailed++;
|
||||
print('FAIL: snapshot --list command failed');
|
||||
print('Exit code: ${result.exitCode}');
|
||||
print('Output: ${result.stdout}');
|
||||
print('Error: ${result.stderr}');
|
||||
}
|
||||
} catch (e) {
|
||||
testsFailed++;
|
||||
print('FAIL: snapshot command test threw exception: $e');
|
||||
}
|
||||
print('');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,11 @@ cr
|
|||
blue ." Test Suite 3: End-to-End Functional Test" reset cr
|
||||
blue ." ℹ SKIP" reset ." : E2E test (requires runtime environment and API key)" cr
|
||||
|
||||
\ Test Suite 3b: Snapshot Command Support (feature parity test)
|
||||
cr
|
||||
blue ." Test Suite 3b: Snapshot Command Support" reset cr
|
||||
blue ." ℹ SKIP" reset ." : Snapshot test (requires runtime environment and API key)" cr
|
||||
|
||||
\ Test Suite 4: Error Handling
|
||||
cr
|
||||
blue ." Test Suite 4: Error Handling" reset cr
|
||||
|
|
|
|||
|
|
@ -147,6 +147,25 @@
|
|||
(make-test-result :passed nil
|
||||
:message (format nil "Exception: ~A" e)))))))
|
||||
|
||||
;;; Test 4: Snapshot command support (feature parity test)
|
||||
(defun test-snapshot-command ()
|
||||
(let ((api-key (uiop:getenv "UNSANDBOX_API_KEY")))
|
||||
(if (not api-key)
|
||||
(make-test-result :passed t) ; Skip test if no API key
|
||||
(handler-case
|
||||
(let* ((result (run-command "./un.lisp snapshot --list 2>&1"))
|
||||
(status (car result))
|
||||
(output (cdr result)))
|
||||
|
||||
;; Check if it executed without errors (may return empty list)
|
||||
(if (= status 0)
|
||||
(make-test-result :passed t)
|
||||
(make-test-result :passed nil
|
||||
:message (format nil "Snapshot list failed: ~A" output))))
|
||||
(error (e)
|
||||
(make-test-result :passed nil
|
||||
:message (format nil "Exception: ~A" e)))))))
|
||||
|
||||
;;; Main test runner
|
||||
(defun main ()
|
||||
(format t "=== Common Lisp UN CLI Test Suite ===~%~%")
|
||||
|
|
@ -159,7 +178,8 @@
|
|||
;; Run tests
|
||||
(let ((results (list (print-result "Extension detection" (test-extension-detection))
|
||||
(print-result "API integration" (test-api-integration))
|
||||
(print-result "Fibonacci end-to-end test" (test-fibonacci)))))
|
||||
(print-result "Fibonacci end-to-end test" (test-fibonacci))
|
||||
(print-result "Snapshot command support" (test-snapshot-command)))))
|
||||
|
||||
(format t "~%")
|
||||
|
||||
|
|
|
|||
|
|
@ -132,6 +132,139 @@ else
|
|||
test_skipped "Handles unknown file extension (could not compile test binary)"
|
||||
fi
|
||||
|
||||
# CLI Command Tests (Feature Parity)
|
||||
echo -e "\n${BLUE}=== CLI Command Tests (Feature Parity) ===${NC}"
|
||||
|
||||
# Compile for CLI tests
|
||||
if clang -x objective-c -framework Foundation "$UN_M" -o "$TEST_BINARY" 2>/dev/null; then
|
||||
# Test: --help
|
||||
if output=$("$TEST_BINARY" --help 2>&1); then
|
||||
if echo "$output" | grep -qi "usage"; then
|
||||
test_passed "CLI: --help shows usage"
|
||||
else
|
||||
test_failed "CLI: --help shows usage" "No usage indication"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qi "usage"; then
|
||||
test_passed "CLI: --help shows usage"
|
||||
else
|
||||
test_failed "CLI: --help shows usage" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test: version command
|
||||
if output=$("$TEST_BINARY" version 2>&1); then
|
||||
if echo "$output" | grep -qi "version"; then
|
||||
test_passed "CLI: version command works"
|
||||
else
|
||||
test_failed "CLI: version command works" "No version output"
|
||||
fi
|
||||
else
|
||||
test_failed "CLI: version command works" "Command failed"
|
||||
fi
|
||||
|
||||
# Test: health command
|
||||
if output=$("$TEST_BINARY" health 2>&1); then
|
||||
if echo "$output" | grep -qi "health\|api"; then
|
||||
test_passed "CLI: health command works"
|
||||
else
|
||||
test_failed "CLI: health command works" "No health output"
|
||||
fi
|
||||
else
|
||||
test_failed "CLI: health command works" "Command failed"
|
||||
fi
|
||||
|
||||
# Test: languages command
|
||||
if output=$("$TEST_BINARY" languages 2>&1); then
|
||||
if echo "$output" | grep -qi "python\|error\|api key"; then
|
||||
test_passed "CLI: languages command works"
|
||||
else
|
||||
test_failed "CLI: languages command works" "No languages output"
|
||||
fi
|
||||
else
|
||||
test_failed "CLI: languages command works" "Command failed"
|
||||
fi
|
||||
|
||||
rm -f "$TEST_BINARY"
|
||||
else
|
||||
echo -e "${YELLOW}Could not compile un.m - skipping CLI command tests${NC}"
|
||||
fi
|
||||
|
||||
# API Command Tests (require API key)
|
||||
if [ -n "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -n "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||
echo -e "\n${BLUE}=== API Command Tests (require auth) ===${NC}"
|
||||
|
||||
# Compile the binary for API tests
|
||||
if clang -x objective-c -framework Foundation "$UN_M" -o "$TEST_BINARY" 2>/dev/null; then
|
||||
|
||||
# Test: snapshot --list
|
||||
if output=$("$TEST_BINARY" snapshot --list 2>&1); then
|
||||
if echo "$output" | grep -qE '\[|\{|Error|snapshots'; then
|
||||
test_passed "CLI: snapshot --list works"
|
||||
else
|
||||
test_failed "CLI: snapshot --list works" "Unexpected output"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qE '\[|\{|Error|snapshots'; then
|
||||
test_passed "CLI: snapshot --list works"
|
||||
else
|
||||
test_failed "CLI: snapshot --list works" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test: session --list
|
||||
if output=$("$TEST_BINARY" session --list 2>&1); then
|
||||
if echo "$output" | grep -qE '\[|\{|Error|sessions'; then
|
||||
test_passed "CLI: session --list works"
|
||||
else
|
||||
test_failed "CLI: session --list works" "Unexpected output"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qE '\[|\{|Error|sessions'; then
|
||||
test_passed "CLI: session --list works"
|
||||
else
|
||||
test_failed "CLI: session --list works" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test: service --list
|
||||
if output=$("$TEST_BINARY" service --list 2>&1); then
|
||||
if echo "$output" | grep -qE '\[|\{|Error|services'; then
|
||||
test_passed "CLI: service --list works"
|
||||
else
|
||||
test_failed "CLI: service --list works" "Unexpected output"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qE '\[|\{|Error|services'; then
|
||||
test_passed "CLI: service --list works"
|
||||
else
|
||||
test_failed "CLI: service --list works" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test: image --list
|
||||
if output=$("$TEST_BINARY" image --list 2>&1); then
|
||||
if echo "$output" | grep -qE '\[|\{|Error|images'; then
|
||||
test_passed "CLI: image --list works"
|
||||
else
|
||||
test_failed "CLI: image --list works" "Unexpected output"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qE '\[|\{|Error|images'; then
|
||||
test_passed "CLI: image --list works"
|
||||
else
|
||||
test_failed "CLI: image --list works" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$TEST_BINARY"
|
||||
else
|
||||
echo -e "${YELLOW}Could not compile un.m - skipping API command tests${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "\n${YELLOW}Skipping API command tests (UNSANDBOX_PUBLIC_KEY/SECRET_KEY not set)${NC}"
|
||||
fi
|
||||
|
||||
# Integration Tests (require API key and successful compilation)
|
||||
if [ -n "${UNSANDBOX_API_KEY:-}" ]; then
|
||||
echo -e "\n${BLUE}=== Integration Tests for un.m ===${NC}"
|
||||
|
|
|
|||
|
|
@ -144,6 +144,172 @@ proc testFibExecution(): bool =
|
|||
echo "Functional Test: passed\n"
|
||||
return true
|
||||
|
||||
proc testCliCommands(): bool =
|
||||
echo "=== Test 4: CLI Commands (Feature Parity) ==="
|
||||
|
||||
var passed = 0
|
||||
var failed = 0
|
||||
|
||||
# Find un.nim script
|
||||
let scriptPaths = [
|
||||
"../clients/nim/sync/src/un.nim",
|
||||
"../../clients/nim/sync/src/un.nim",
|
||||
"../un.nim"
|
||||
]
|
||||
|
||||
var unScript = ""
|
||||
for path in scriptPaths:
|
||||
if fileExists(path):
|
||||
unScript = path
|
||||
break
|
||||
|
||||
if unScript == "":
|
||||
echo " SKIP: un.nim not found"
|
||||
echo "CLI Commands: skipped\n"
|
||||
return true
|
||||
|
||||
# Test: --help
|
||||
let (helpOutput, helpCode) = try:
|
||||
execCmdEx("nim r " & unScript & " -- --help")
|
||||
except:
|
||||
("", -1)
|
||||
|
||||
if helpOutput.contains("Usage") or helpOutput.contains("usage"):
|
||||
echo " PASS: --help shows usage"
|
||||
inc passed
|
||||
else:
|
||||
echo " FAIL: --help does not show usage"
|
||||
inc failed
|
||||
|
||||
# Test: version command
|
||||
let (versionOutput, versionCode) = try:
|
||||
execCmdEx("nim r " & unScript & " -- version")
|
||||
except:
|
||||
("", -1)
|
||||
|
||||
if versionOutput.contains("version") or versionOutput.contains("Version"):
|
||||
echo " PASS: version command works"
|
||||
inc passed
|
||||
else:
|
||||
echo " FAIL: version command does not work"
|
||||
inc failed
|
||||
|
||||
# Test: health command
|
||||
let (healthOutput, healthCode) = try:
|
||||
execCmdEx("nim r " & unScript & " -- health")
|
||||
except:
|
||||
("", -1)
|
||||
|
||||
if healthOutput.contains("health") or healthOutput.contains("API"):
|
||||
echo " PASS: health command works"
|
||||
inc passed
|
||||
else:
|
||||
echo " FAIL: health command does not work"
|
||||
inc failed
|
||||
|
||||
# Test: languages command
|
||||
let (langsOutput, langsCode) = try:
|
||||
execCmdEx("nim r " & unScript & " -- languages")
|
||||
except:
|
||||
("", -1)
|
||||
|
||||
if langsOutput.contains("python") or langsOutput.contains("Error") or langsOutput.contains("API key"):
|
||||
echo " PASS: languages command works"
|
||||
inc passed
|
||||
else:
|
||||
echo " FAIL: languages command does not work"
|
||||
inc failed
|
||||
|
||||
echo "CLI Commands: ", passed, " passed, ", failed, " failed\n"
|
||||
return failed == 0
|
||||
|
||||
proc testApiCommands(): bool =
|
||||
echo "=== Test 5: API Commands (require auth) ==="
|
||||
|
||||
let publicKey = getEnv("UNSANDBOX_PUBLIC_KEY")
|
||||
let secretKey = getEnv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if publicKey == "" and secretKey == "":
|
||||
echo " SKIP: UNSANDBOX_PUBLIC_KEY/SECRET_KEY not set"
|
||||
echo "API Commands: skipped\n"
|
||||
return true
|
||||
|
||||
var passed = 0
|
||||
var failed = 0
|
||||
|
||||
# Find un.nim script
|
||||
let scriptPaths = [
|
||||
"../clients/nim/sync/src/un.nim",
|
||||
"../../clients/nim/sync/src/un.nim",
|
||||
"../un.nim"
|
||||
]
|
||||
|
||||
var unScript = ""
|
||||
for path in scriptPaths:
|
||||
if fileExists(path):
|
||||
unScript = path
|
||||
break
|
||||
|
||||
if unScript == "":
|
||||
echo " SKIP: un.nim not found"
|
||||
echo "API Commands: skipped\n"
|
||||
return true
|
||||
|
||||
# Test: snapshot --list
|
||||
let (snapOutput, snapCode) = try:
|
||||
execCmdEx("nim r " & unScript & " -- snapshot --list")
|
||||
except:
|
||||
("", -1)
|
||||
|
||||
if snapOutput.contains("[") or snapOutput.contains("{") or snapOutput.contains("Error") or snapOutput.contains("snapshots"):
|
||||
echo " PASS: snapshot --list works"
|
||||
inc passed
|
||||
else:
|
||||
echo " FAIL: snapshot --list does not work"
|
||||
inc failed
|
||||
|
||||
# Test: session --list
|
||||
let (sessOutput, sessCode) = try:
|
||||
execCmdEx("nim r " & unScript & " -- session --list")
|
||||
except:
|
||||
("", -1)
|
||||
|
||||
if sessOutput.contains("[") or sessOutput.contains("{") or sessOutput.contains("Error") or sessOutput.contains("sessions"):
|
||||
echo " PASS: session --list works"
|
||||
inc passed
|
||||
else:
|
||||
echo " FAIL: session --list does not work"
|
||||
inc failed
|
||||
|
||||
# Test: service --list
|
||||
let (svcOutput, svcCode) = try:
|
||||
execCmdEx("nim r " & unScript & " -- service --list")
|
||||
except:
|
||||
("", -1)
|
||||
|
||||
if svcOutput.contains("[") or svcOutput.contains("{") or svcOutput.contains("Error") or svcOutput.contains("services"):
|
||||
echo " PASS: service --list works"
|
||||
inc passed
|
||||
else:
|
||||
echo " FAIL: service --list does not work"
|
||||
inc failed
|
||||
|
||||
# Test: image --list
|
||||
let (imgOutput, imgCode) = try:
|
||||
execCmdEx("nim r " & unScript & " -- image --list")
|
||||
except:
|
||||
("", -1)
|
||||
|
||||
if imgOutput.contains("[") or imgOutput.contains("{") or imgOutput.contains("Error") or imgOutput.contains("images"):
|
||||
echo " PASS: image --list works"
|
||||
inc passed
|
||||
else:
|
||||
echo " FAIL: image --list does not work"
|
||||
inc failed
|
||||
|
||||
echo "API Commands: ", passed, " passed, ", failed, " failed\n"
|
||||
return failed == 0
|
||||
|
||||
proc main() =
|
||||
echo "UN CLI Nim Implementation Test Suite"
|
||||
echo "=====================================\n"
|
||||
|
|
@ -159,6 +325,12 @@ proc main() =
|
|||
if not testFibExecution():
|
||||
allPassed = false
|
||||
|
||||
if not testCliCommands():
|
||||
allPassed = false
|
||||
|
||||
if not testApiCommands():
|
||||
allPassed = false
|
||||
|
||||
echo "====================================="
|
||||
if allPassed:
|
||||
echo "RESULT: ALL TESTS PASSED"
|
||||
|
|
|
|||
|
|
@ -138,6 +138,14 @@ if %*ENV<UNSANDBOX_API_KEY>:exists && %*ENV<UNSANDBOX_API_KEY> {
|
|||
} else {
|
||||
test-skipped("Executes Bash file successfully (fib.sh not found)");
|
||||
}
|
||||
|
||||
# Test: Snapshot command support (feature parity test)
|
||||
($exit-code, $output) = run-command([$UN_RAKU, 'snapshot', '--list']);
|
||||
if $exit-code == 0 {
|
||||
test-passed("Snapshot list command works");
|
||||
} else {
|
||||
test-failed("Snapshot list command works", "Expected exit code 0");
|
||||
}
|
||||
} else {
|
||||
say "";
|
||||
say color-yellow("Skipping integration tests (UNSANDBOX_API_KEY not set)");
|
||||
|
|
|
|||
|
|
@ -138,6 +138,26 @@
|
|||
(print-result "Fibonacci end-to-end test" #f
|
||||
(format #f "Exception: ~a" args)))))))
|
||||
|
||||
;;; Test 4: Snapshot command support (feature parity test)
|
||||
(define (test-snapshot-command)
|
||||
(let ((api-key (getenv "UNSANDBOX_API_KEY")))
|
||||
(if (not api-key)
|
||||
(print-result "Snapshot command support" #t #f) ; Skip test if no API key
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let* ((result (run-command "./un.scm snapshot --list 2>&1"))
|
||||
(status (car result))
|
||||
(output (cdr result)))
|
||||
|
||||
;; Check if it executed without errors (may return empty list)
|
||||
(if (= status 0)
|
||||
(print-result "Snapshot command support" #t #f)
|
||||
(print-result "Snapshot command support" #f
|
||||
(format #f "Snapshot list failed: ~a" output)))))
|
||||
(lambda (key . args)
|
||||
(print-result "Snapshot command support" #f
|
||||
(format #f "Exception: ~a" args)))))))
|
||||
|
||||
;;; Main test runner
|
||||
(define (main)
|
||||
(display "=== Scheme UN CLI Test Suite ===\n\n")
|
||||
|
|
@ -150,7 +170,8 @@
|
|||
;; Run tests
|
||||
(let ((results (list (test-extension-detection)
|
||||
(test-api-integration)
|
||||
(test-fibonacci))))
|
||||
(test-fibonacci)
|
||||
(test-snapshot-command))))
|
||||
|
||||
(display "\n")
|
||||
|
||||
|
|
|
|||
242
tests/test_un_swift.sh
Executable file
242
tests/test_un_swift.sh
Executable file
|
|
@ -0,0 +1,242 @@
|
|||
#!/usr/bin/env bash
|
||||
# Test suite for un.swift (Swift implementation)
|
||||
# Note: un.swift requires Swift compiler
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
UN_SWIFT="$SCRIPT_DIR/../clients/swift/sync/src/un.swift"
|
||||
TEST_DIR="$SCRIPT_DIR/../test"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test counters
|
||||
TESTS_RUN=0
|
||||
TESTS_PASSED=0
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Test result tracking
|
||||
test_passed() {
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
echo -e "${GREEN}PASS${NC}: $1"
|
||||
}
|
||||
|
||||
test_failed() {
|
||||
TESTS_FAILED=$((TESTS_FAILED + 1))
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
echo -e "${RED}FAIL${NC}: $1"
|
||||
if [ -n "${2:-}" ]; then
|
||||
echo -e "${RED} Error: $2${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
test_skipped() {
|
||||
echo -e "${YELLOW}SKIP${NC}: $1"
|
||||
}
|
||||
|
||||
# Check if Swift is available
|
||||
if ! command -v swift &> /dev/null; then
|
||||
echo -e "${YELLOW}Swift not found - skipping all Swift tests${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if un.swift exists
|
||||
if [ ! -f "$UN_SWIFT" ]; then
|
||||
# Try alternative paths
|
||||
UN_SWIFT="$SCRIPT_DIR/../un.swift"
|
||||
if [ ! -f "$UN_SWIFT" ]; then
|
||||
echo -e "${YELLOW}un.swift not found - skipping all tests${NC}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Unit Tests
|
||||
echo -e "${BLUE}=== Unit Tests for un.swift ===${NC}"
|
||||
|
||||
# Test: Script exists
|
||||
if [ -f "$UN_SWIFT" ]; then
|
||||
test_passed "Script exists"
|
||||
else
|
||||
test_failed "Script exists" "File not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test: Script is readable
|
||||
if [ -r "$UN_SWIFT" ]; then
|
||||
test_passed "Script is readable"
|
||||
else
|
||||
test_failed "Script is readable" "File not readable"
|
||||
fi
|
||||
|
||||
# CLI Command Tests (Feature Parity)
|
||||
echo -e "\n${BLUE}=== CLI Command Tests (Feature Parity) ===${NC}"
|
||||
|
||||
# Test: --help
|
||||
if output=$(swift "$UN_SWIFT" --help 2>&1); then
|
||||
if echo "$output" | grep -qi "usage"; then
|
||||
test_passed "CLI: --help shows usage"
|
||||
else
|
||||
test_failed "CLI: --help shows usage" "No usage indication"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qi "usage"; then
|
||||
test_passed "CLI: --help shows usage"
|
||||
else
|
||||
test_failed "CLI: --help shows usage" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test: version command
|
||||
if output=$(swift "$UN_SWIFT" version 2>&1); then
|
||||
if echo "$output" | grep -qi "version"; then
|
||||
test_passed "CLI: version command works"
|
||||
else
|
||||
test_failed "CLI: version command works" "No version output"
|
||||
fi
|
||||
else
|
||||
test_failed "CLI: version command works" "Command failed"
|
||||
fi
|
||||
|
||||
# Test: health command
|
||||
if output=$(swift "$UN_SWIFT" health 2>&1); then
|
||||
if echo "$output" | grep -qi "health\|api"; then
|
||||
test_passed "CLI: health command works"
|
||||
else
|
||||
test_failed "CLI: health command works" "No health output"
|
||||
fi
|
||||
else
|
||||
test_failed "CLI: health command works" "Command failed"
|
||||
fi
|
||||
|
||||
# Test: languages command
|
||||
if output=$(swift "$UN_SWIFT" languages 2>&1); then
|
||||
if echo "$output" | grep -qi "python\|error\|api key"; then
|
||||
test_passed "CLI: languages command works"
|
||||
else
|
||||
test_failed "CLI: languages command works" "No languages output"
|
||||
fi
|
||||
else
|
||||
test_failed "CLI: languages command works" "Command failed"
|
||||
fi
|
||||
|
||||
# API Command Tests (require API key)
|
||||
if [ -n "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -n "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||
echo -e "\n${BLUE}=== API Command Tests (require auth) ===${NC}"
|
||||
|
||||
# Test: snapshot --list
|
||||
if output=$(swift "$UN_SWIFT" snapshot --list 2>&1); then
|
||||
if echo "$output" | grep -qE '\[|\{|Error|snapshots'; then
|
||||
test_passed "CLI: snapshot --list works"
|
||||
else
|
||||
test_failed "CLI: snapshot --list works" "Unexpected output"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qE '\[|\{|Error|snapshots'; then
|
||||
test_passed "CLI: snapshot --list works"
|
||||
else
|
||||
test_failed "CLI: snapshot --list works" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test: session --list
|
||||
if output=$(swift "$UN_SWIFT" session --list 2>&1); then
|
||||
if echo "$output" | grep -qE '\[|\{|Error|sessions'; then
|
||||
test_passed "CLI: session --list works"
|
||||
else
|
||||
test_failed "CLI: session --list works" "Unexpected output"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qE '\[|\{|Error|sessions'; then
|
||||
test_passed "CLI: session --list works"
|
||||
else
|
||||
test_failed "CLI: session --list works" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test: service --list
|
||||
if output=$(swift "$UN_SWIFT" service --list 2>&1); then
|
||||
if echo "$output" | grep -qE '\[|\{|Error|services'; then
|
||||
test_passed "CLI: service --list works"
|
||||
else
|
||||
test_failed "CLI: service --list works" "Unexpected output"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qE '\[|\{|Error|services'; then
|
||||
test_passed "CLI: service --list works"
|
||||
else
|
||||
test_failed "CLI: service --list works" "Command failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test: image --list
|
||||
if output=$(swift "$UN_SWIFT" image --list 2>&1); then
|
||||
if echo "$output" | grep -qE '\[|\{|Error|images'; then
|
||||
test_passed "CLI: image --list works"
|
||||
else
|
||||
test_failed "CLI: image --list works" "Unexpected output"
|
||||
fi
|
||||
else
|
||||
if echo "$output" | grep -qE '\[|\{|Error|images'; then
|
||||
test_passed "CLI: image --list works"
|
||||
else
|
||||
test_failed "CLI: image --list works" "Command failed"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "\n${YELLOW}Skipping API command tests (UNSANDBOX_PUBLIC_KEY/SECRET_KEY not set)${NC}"
|
||||
fi
|
||||
|
||||
# Integration Tests (require API key)
|
||||
if [ -n "${UNSANDBOX_API_KEY:-}" ]; then
|
||||
echo -e "\n${BLUE}=== Integration Tests for un.swift ===${NC}"
|
||||
|
||||
# Test: Can execute Python file
|
||||
if [ -f "$TEST_DIR/fib.py" ]; then
|
||||
if output=$(swift "$UN_SWIFT" "$TEST_DIR/fib.py" 2>&1); then
|
||||
if echo "$output" | grep -q "fib(10)"; then
|
||||
test_passed "Executes Python file successfully"
|
||||
else
|
||||
test_failed "Executes Python file successfully" "Expected fibonacci output"
|
||||
fi
|
||||
else
|
||||
test_failed "Executes Python file successfully" "Script failed: $output"
|
||||
fi
|
||||
else
|
||||
test_skipped "Executes Python file successfully (fib.py not found)"
|
||||
fi
|
||||
|
||||
# Test: Can execute Bash file
|
||||
if [ -f "$TEST_DIR/fib.sh" ]; then
|
||||
if output=$(swift "$UN_SWIFT" "$TEST_DIR/fib.sh" 2>&1); then
|
||||
if echo "$output" | grep -q "fib(10)"; then
|
||||
test_passed "Executes Bash file successfully"
|
||||
else
|
||||
test_failed "Executes Bash file successfully" "Expected fibonacci output"
|
||||
fi
|
||||
else
|
||||
test_failed "Executes Bash file successfully" "Script failed: $output"
|
||||
fi
|
||||
else
|
||||
test_skipped "Executes Bash file successfully (fib.sh not found)"
|
||||
fi
|
||||
else
|
||||
echo -e "\n${YELLOW}Skipping integration tests (UNSANDBOX_API_KEY not set)${NC}"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo -e "\n${BLUE}=== Test Summary ===${NC}"
|
||||
echo "Total: $TESTS_RUN | Passed: $TESTS_PASSED | Failed: $TESTS_FAILED"
|
||||
|
||||
if [ $TESTS_FAILED -eq 0 ]; then
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Some tests failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -161,6 +161,161 @@ fn test_fib_execution() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
fn test_cli_commands() bool {
|
||||
println('=== Test 4: CLI Commands (Feature Parity) ===')
|
||||
|
||||
mut passed := 0
|
||||
mut failed := 0
|
||||
|
||||
// Find un.v script
|
||||
script_paths := [
|
||||
'../clients/v/sync/src/un.v',
|
||||
'../../clients/v/sync/src/un.v',
|
||||
'../un.v',
|
||||
]
|
||||
|
||||
mut un_script := ''
|
||||
for path in script_paths {
|
||||
if os.exists(path) {
|
||||
un_script = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if un_script == '' {
|
||||
println(' SKIP: un.v not found')
|
||||
println('CLI Commands: skipped\n')
|
||||
return true
|
||||
}
|
||||
|
||||
// Test: --help
|
||||
help_result := os.execute('v run ${un_script} -- --help')
|
||||
if help_result.output.contains('Usage') || help_result.output.contains('usage') {
|
||||
println(' PASS: --help shows usage')
|
||||
passed++
|
||||
} else {
|
||||
println(' FAIL: --help does not show usage')
|
||||
failed++
|
||||
}
|
||||
|
||||
// Test: version command
|
||||
version_result := os.execute('v run ${un_script} -- version')
|
||||
if version_result.output.contains('version') || version_result.output.contains('Version') {
|
||||
println(' PASS: version command works')
|
||||
passed++
|
||||
} else {
|
||||
println(' FAIL: version command does not work')
|
||||
failed++
|
||||
}
|
||||
|
||||
// Test: health command
|
||||
health_result := os.execute('v run ${un_script} -- health')
|
||||
if health_result.output.contains('health') || health_result.output.contains('API') {
|
||||
println(' PASS: health command works')
|
||||
passed++
|
||||
} else {
|
||||
println(' FAIL: health command does not work')
|
||||
failed++
|
||||
}
|
||||
|
||||
// Test: languages command
|
||||
langs_result := os.execute('v run ${un_script} -- languages')
|
||||
if langs_result.output.contains('python') || langs_result.output.contains('Error') || langs_result.output.contains('API key') {
|
||||
println(' PASS: languages command works')
|
||||
passed++
|
||||
} else {
|
||||
println(' FAIL: languages command does not work')
|
||||
failed++
|
||||
}
|
||||
|
||||
println('CLI Commands: ${passed} passed, ${failed} failed\n')
|
||||
return failed == 0
|
||||
}
|
||||
|
||||
fn test_api_commands() bool {
|
||||
println('=== Test 5: API Commands (require auth) ===')
|
||||
|
||||
public_key := os.getenv('UNSANDBOX_PUBLIC_KEY')
|
||||
secret_key := os.getenv('UNSANDBOX_SECRET_KEY')
|
||||
|
||||
if public_key == '' && secret_key == '' {
|
||||
println(' SKIP: UNSANDBOX_PUBLIC_KEY/SECRET_KEY not set')
|
||||
println('API Commands: skipped\n')
|
||||
return true
|
||||
}
|
||||
|
||||
mut passed := 0
|
||||
mut failed := 0
|
||||
|
||||
// Find un.v script
|
||||
script_paths := [
|
||||
'../clients/v/sync/src/un.v',
|
||||
'../../clients/v/sync/src/un.v',
|
||||
'../un.v',
|
||||
]
|
||||
|
||||
mut un_script := ''
|
||||
for path in script_paths {
|
||||
if os.exists(path) {
|
||||
un_script = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if un_script == '' {
|
||||
println(' SKIP: un.v not found')
|
||||
println('API Commands: skipped\n')
|
||||
return true
|
||||
}
|
||||
|
||||
// Test: snapshot --list
|
||||
snap_result := os.execute('v run ${un_script} -- snapshot --list')
|
||||
if snap_result.output.contains('[') || snap_result.output.contains('{') ||
|
||||
snap_result.output.contains('Error') || snap_result.output.contains('snapshots') {
|
||||
println(' PASS: snapshot --list works')
|
||||
passed++
|
||||
} else {
|
||||
println(' FAIL: snapshot --list does not work')
|
||||
failed++
|
||||
}
|
||||
|
||||
// Test: session --list
|
||||
sess_result := os.execute('v run ${un_script} -- session --list')
|
||||
if sess_result.output.contains('[') || sess_result.output.contains('{') ||
|
||||
sess_result.output.contains('Error') || sess_result.output.contains('sessions') {
|
||||
println(' PASS: session --list works')
|
||||
passed++
|
||||
} else {
|
||||
println(' FAIL: session --list does not work')
|
||||
failed++
|
||||
}
|
||||
|
||||
// Test: service --list
|
||||
svc_result := os.execute('v run ${un_script} -- service --list')
|
||||
if svc_result.output.contains('[') || svc_result.output.contains('{') ||
|
||||
svc_result.output.contains('Error') || svc_result.output.contains('services') {
|
||||
println(' PASS: service --list works')
|
||||
passed++
|
||||
} else {
|
||||
println(' FAIL: service --list does not work')
|
||||
failed++
|
||||
}
|
||||
|
||||
// Test: image --list
|
||||
img_result := os.execute('v run ${un_script} -- image --list')
|
||||
if img_result.output.contains('[') || img_result.output.contains('{') ||
|
||||
img_result.output.contains('Error') || img_result.output.contains('images') {
|
||||
println(' PASS: image --list works')
|
||||
passed++
|
||||
} else {
|
||||
println(' FAIL: image --list does not work')
|
||||
failed++
|
||||
}
|
||||
|
||||
println('API Commands: ${passed} passed, ${failed} failed\n')
|
||||
return failed == 0
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println('UN CLI V Implementation Test Suite')
|
||||
println('===================================\n')
|
||||
|
|
@ -179,6 +334,14 @@ fn main() {
|
|||
all_passed = false
|
||||
}
|
||||
|
||||
if !test_cli_commands() {
|
||||
all_passed = false
|
||||
}
|
||||
|
||||
if !test_api_commands() {
|
||||
all_passed = false
|
||||
}
|
||||
|
||||
println('===================================')
|
||||
if all_passed {
|
||||
println('RESULT: ALL TESTS PASSED')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue