Add service resize command to all 38 language implementations

Adds --resize ID --vcpu N flag to resize running services live.
PATCH /services/{id} with {"vcpu": 1-8} applies CPU/memory limits
without restart. Memory formula: 2GB per vCPU.
This commit is contained in:
Russell Ballestrini 2026-01-11 17:19:50 -05:00
parent 87397949d1
commit c219d6e550
38 changed files with 1214 additions and 45 deletions

55
un.awk
View file

@ -272,6 +272,40 @@ function service_destroy(id , timestamp, sig_headers, signature, sig_input, s
print GREEN "Service destroyed: " id RESET
}
function service_resize(id, vcpu , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, ram) {
get_api_keys()
endpoint = "/services/" id
json = "{\"vcpu\":" vcpu "}"
# Write to temp file
tmp = "/tmp/un_awk_resize_" PROCINFO["pid"] ".json"
print json > tmp
close(tmp)
timestamp = systime()
sig_headers = ""
if (GLOBAL_SECRET_KEY != "") {
sig_input = timestamp ":PATCH:" 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 PATCH '" API_BASE endpoint "' " \
"-H 'Content-Type: application/json' " \
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
sig_headers \
"-d '@" tmp "'"
system(cmd " > /dev/null")
# Clean up
system("rm -f " tmp)
ram = vcpu * 2
print GREEN "Service resized to " vcpu " vCPU, " ram " GB RAM" RESET
}
function service_dump_bootstrap(id, dump_file , endpoint, json_body, timestamp, sig_headers, signature, sig_input, sig_cmd) {
get_api_keys()
print "Fetching bootstrap script from " id "..." > "/dev/stderr"
@ -985,6 +1019,7 @@ function show_help() {
print " awk -f un.awk service --list"
print " awk -f un.awk service --create --name NAME [--ports PORTS] [--domains DOMAINS] [--type TYPE] [--bootstrap CMD] [-e KEY=VAL] [--env-file FILE] [-f FILE]..."
print " awk -f un.awk service --destroy ID"
print " awk -f un.awk service --resize ID -v VCPU"
print " awk -f un.awk service --dump-bootstrap ID [--dump-file FILE]"
print " awk -f un.awk service --snapshot SERVICE_ID [--snapshot-name NAME] [--hot]"
print " awk -f un.awk service --restore SNAPSHOT_ID"
@ -1010,6 +1045,8 @@ function show_help() {
print " --domains DOMAINS Comma-separated domain names"
print " --type TYPE Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)"
print " --bootstrap CMD Bootstrap command or script"
print " --destroy ID Destroy service"
print " --resize ID Resize service (requires -v)"
print " --dump-bootstrap ID Dump bootstrap script from service"
print " --dump-file FILE Save bootstrap to file (with --dump-bootstrap)"
print " -e KEY=VAL Environment variable for vault (can be repeated)"
@ -1140,6 +1177,24 @@ END {
service_list()
} else if (ARGC >= 4 && ARGV[2] == "--destroy") {
service_destroy(ARGV[3])
} else if (ARGC >= 4 && ARGV[2] == "--resize") {
# Parse -v for vcpu
resize_id = ARGV[3]
resize_vcpu = ""
i = 4
while (i < ARGC) {
if (ARGV[i] == "-v" && i + 1 < ARGC) {
resize_vcpu = ARGV[i + 1]
i += 2
} else {
i++
}
}
if (resize_vcpu == "") {
print RED "Error: --vcpu (-v) is required with --resize" RESET > "/dev/stderr"
exit 1
}
service_resize(resize_id, resize_vcpu)
} else if (ARGC >= 4 && ARGV[2] == "--dump-bootstrap") {
dump_file = ""
if (ARGC >= 6 && ARGV[4] == "--dump-file") {

29
un.clj
View file

@ -217,6 +217,21 @@
(let [status (Integer/parseInt (str/trim out))]
(and (>= status 200) (< status 300))))))
(defn curl-patch [api-key endpoint json-data]
(let [tmp-file (str "/tmp/un_clj_" (rand-int 999999) ".json")
[public-key secret-key] (get-api-keys)
auth-headers (build-auth-headers public-key secret-key "PATCH" endpoint json-data)]
(spit tmp-file json-data)
(let [args (concat ["curl" "-s" "-X" "PATCH"
(str "https://api.unsandbox.com" endpoint)
"-H" "Content-Type: application/json"]
auth-headers
["-d" (str "@" tmp-file)])
{:keys [out]} (apply sh args)]
(io/delete-file tmp-file true)
(check-clock-drift-error out)
out)))
(def max-env-content-size 65536)
(defn read-env-file [path]
@ -396,6 +411,16 @@
:destroy (do
(curl-delete api-key (str "/services/" sid))
(println (str green "Service destroyed: " sid reset)))
:resize (when sid
(if (or (nil? vcpu) (< vcpu 1) (> vcpu 8))
(do
(binding [*out* *err*]
(println (str red "Error: --resize requires -v N (1-8)" reset)))
(System/exit 1))
(let [json (str "{\"vcpu\":" vcpu "}")
_ (curl-patch api-key (str "/services/" sid) json)
ram (* vcpu 2)]
(println (str green "Service resized to " vcpu " vCPU, " ram " GB RAM" reset)))))
:execute (when (and sid bootstrap)
(let [json (str "{\"command\":\"" (escape-json bootstrap) "\"}")
response (curl-post api-key (str "/services/" sid "/execute") json)
@ -591,6 +616,10 @@
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
:destroy (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode)
(and (= mode :service) (= (first args) "--resize"))
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
:resize (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode)
(and (= mode :service) (= (first args) "--execute"))
(recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
:execute (second args) service-name service-ports (nth args 2) service-bootstrap-file service-type service-input-files service-envs service-env-file key-extend mode)

56
un.cob
View file

@ -84,6 +84,9 @@
01 WS-SVC-ENV-FILE PIC X(256).
01 WS-ENV-ACTION PIC X(32).
01 WS-ENV-TARGET PIC X(256).
01 WS-VCPU PIC 9(2) VALUE 0.
01 WS-VCPU-STR PIC X(8).
01 WS-RAM PIC 9(4) VALUE 0.
PROCEDURE DIVISION.
MAIN-PROCEDURE.
@ -244,6 +247,10 @@
ELSE IF WS-ARG2 = "--dump-bootstrap"
ACCEPT WS-ID FROM ARGUMENT-VALUE
PERFORM SERVICE-DUMP-BOOTSTRAP
ELSE IF WS-ARG2 = "--resize"
ACCEPT WS-ID FROM ARGUMENT-VALUE
PERFORM PARSE-SERVICE-RESIZE-ARGS
PERFORM SERVICE-RESIZE
ELSE IF WS-ARG2 = "--name"
ACCEPT WS-NAME FROM ARGUMENT-VALUE
PERFORM PARSE-SERVICE-CREATE-ARGS
@ -251,7 +258,7 @@
ELSE
DISPLAY "Error: Use --list, --info, --logs, "
"--freeze, --unfreeze, --destroy, --dump-bootstrap, "
"--name, or env" UPON SYSERR
"--resize, --name, or env" UPON SYSERR
MOVE 1 TO RETURN-CODE
END-IF.
@ -934,3 +941,50 @@
END-STRING.
CALL "SYSTEM" USING WS-CURL-CMD.
PARSE-SERVICE-RESIZE-ARGS.
* Parse -v argument for vcpu
MOVE 0 TO WS-VCPU.
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE.
PERFORM UNTIL WS-ARG3 = SPACES
IF WS-ARG3 = "-v"
ACCEPT WS-VCPU-STR FROM ARGUMENT-VALUE
MOVE FUNCTION NUMVAL(WS-VCPU-STR) TO WS-VCPU
END-IF
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE
END-PERFORM.
SERVICE-RESIZE.
* Validate vcpu
IF WS-VCPU < 1 OR WS-VCPU > 8
DISPLAY "Error: --resize requires -v N (1-8)"
UPON SYSERR
MOVE 1 TO RETURN-CODE
STOP RUN
END-IF.
* Calculate RAM
COMPUTE WS-RAM = WS-VCPU * 2.
* Build and execute resize request with HMAC auth
STRING "TS=$(date +%s); "
"BODY='{\"vcpu\":" WS-VCPU "}'; "
"SIG=$(echo -n \"$TS:PATCH:/services/"
FUNCTION TRIM(WS-ID)
":$BODY\" | openssl dgst -sha256 -hmac '"
FUNCTION TRIM(WS-SECRET-KEY)
"' | cut -d' ' -f2); "
"curl -s -X PATCH 'https://api.unsandbox.com/services/"
FUNCTION TRIM(WS-ID)
"' "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer " FUNCTION TRIM(WS-PUBLIC-KEY) "' "
"-H 'X-Timestamp: '$TS "
"-H 'X-Signature: '$SIG "
"-d \"$BODY\" >/dev/null && "
"echo -e '\x1b[32mService resized to " WS-VCPU
" vCPU, " WS-RAM " GB RAM\x1b[0m'"
DELIMITED BY SIZE INTO WS-CURL-CMD
END-STRING.
CALL "SYSTEM" USING WS-CURL-CMD.

42
un.cpp
View file

@ -439,7 +439,7 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
cout << exec_curl(cmd) << endl;
}
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const vector<string>& envs, const string& env_file, const string& env_action, const string& env_target, const string& public_key, const string& secret_key) {
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const vector<string>& envs, const string& env_file, const string& env_action, const string& env_target, const string& public_key, const string& secret_key) {
// Handle service env subcommand
if (!env_action.empty()) {
cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key);
@ -498,6 +498,23 @@ void cmd_service(const string& name, const string& ports, const string& type, co
return;
}
if (!resize.empty()) {
if (vcpu <= 0) {
cerr << RED << "Error: --resize requires -v <vcpu>" << RESET << endl;
exit(1);
}
ostringstream json;
json << "{\"vcpu\":" << vcpu << "}";
string auth_headers = build_auth_headers("PATCH", "/services/" + resize, json.str(), public_key, secret_key);
string cmd = "curl -s -X PATCH '" + API_BASE + "/services/" + resize + "' "
"-H 'Content-Type: application/json' "
+ auth_headers + " "
"-d '" + json.str() + "'";
exec_curl(cmd);
cout << GREEN << "Service resized to " << vcpu << " vCPU, " << (vcpu * 2) << " GB RAM" << RESET << endl;
return;
}
if (!execute.empty()) {
ostringstream json;
json << "{\"command\":\"" << escape_json(command) << "\"}";
@ -685,12 +702,12 @@ void cmd_validate_key(bool extend, const string& public_key, const string& secre
size_t status_end = result.find("\"", status_pos);
string status = result.substr(status_pos, status_end - status_pos);
// Extract public_key
string public_key;
// Extract public_key from response
string resp_public_key;
if (public_key_pos != string::npos) {
public_key_pos += 14;
size_t pk_end = result.find("\"", public_key_pos);
public_key = result.substr(public_key_pos, pk_end - public_key_pos);
resp_public_key = result.substr(public_key_pos, pk_end - public_key_pos);
}
// Extract tier
@ -711,8 +728,8 @@ void cmd_validate_key(bool extend, const string& public_key, const string& secre
if (status == "valid") {
cout << GREEN << "Valid" << RESET << endl;
if (!public_key.empty()) {
cout << "Public Key: " << public_key << endl;
if (!resp_public_key.empty()) {
cout << "Public Key: " << resp_public_key << endl;
}
if (!tier.empty()) {
cout << "Tier: " << tier << endl;
@ -722,8 +739,8 @@ void cmd_validate_key(bool extend, const string& public_key, const string& secre
}
} else if (status == "expired") {
cout << RED << "Expired" << RESET << endl;
if (!public_key.empty()) {
cout << "Public Key: " << public_key << endl;
if (!resp_public_key.empty()) {
cout << "Public Key: " << resp_public_key << endl;
}
if (!tier.empty()) {
cout << "Tier: " << tier << endl;
@ -733,8 +750,8 @@ void cmd_validate_key(bool extend, const string& public_key, const string& secre
}
cout << YELLOW << "To renew: Visit " << PORTAL_BASE << "/keys/extend" << RESET << endl;
if (extend && !public_key.empty()) {
string url = PORTAL_BASE + "/keys/extend?pk=" + public_key;
if (extend && !resp_public_key.empty()) {
string url = PORTAL_BASE + "/keys/extend?pk=" + resp_public_key;
string browser_cmd = "xdg-open '" + url + "' 2>/dev/null || open '" + url + "' 2>/dev/null";
system(browser_cmd.c_str());
}
@ -789,7 +806,7 @@ int main(int argc, char* argv[]) {
if (cmd_type == "service") {
string name, ports, type, bootstrap, bootstrap_file;
bool list = false;
string info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network;
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network;
int vcpu = 0;
vector<string> files;
vector<string> envs;
@ -821,6 +838,7 @@ int main(int argc, char* argv[]) {
else if (arg == "--freeze" && i+1 < argc) sleep = argv[++i];
else if (arg == "--unfreeze" && i+1 < argc) wake = argv[++i];
else if (arg == "--destroy" && i+1 < argc) destroy = argv[++i];
else if (arg == "--resize" && i+1 < argc) resize = argv[++i];
else if (arg == "--execute" && i+1 < argc) execute = argv[++i];
else if (arg == "--command" && i+1 < argc) command = argv[++i];
else if (arg == "--dump-bootstrap" && i+1 < argc) dump_bootstrap = argv[++i];
@ -830,7 +848,7 @@ int main(int argc, char* argv[]) {
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
}
cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network, vcpu, envs, env_file, env_action, env_target, public_key, secret_key);
cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, vcpu, envs, env_file, env_action, env_target, public_key, secret_key);
return 0;
}

21
un.cr
View file

@ -122,6 +122,8 @@ def api_request(endpoint : String, public_key : String, secret_key : String?, me
HTTP::Client.get(url, headers: headers)
when "POST"
HTTP::Client.post(url, headers: headers, body: body)
when "PATCH"
HTTP::Client.patch(url, headers: headers, body: body)
when "DELETE"
HTTP::Client.delete(url, headers: headers)
else
@ -600,6 +602,19 @@ def cmd_service(args)
return
end
if resize_id = args[:resize]?.as?(String)
vcpu = args[:vcpu]?.as?(Int32)
if vcpu.nil? || vcpu < 1 || vcpu > 8
STDERR.puts "#{RED}Error: --resize requires -v N (1-8)#{RESET}"
exit 1
end
payload = JSON.parse({vcpu: vcpu}.to_json)
api_request("/services/#{resize_id}", public_key, secret_key, method: "PATCH", data: payload)
ram = vcpu * 2
puts "#{GREEN}Service resized to #{vcpu} vCPU, #{ram} GB RAM#{RESET}"
return
end
# Create new service
if name = args[:name]?.as?(String)
payload = JSON.parse({name: name}.to_json)
@ -684,7 +699,7 @@ def cmd_service(args)
return
end
STDERR.puts "#{RED}Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --name to create#{RESET}"
STDERR.puts "#{RED}Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, --resize, or --name to create#{RESET}"
exit 1
end
@ -708,6 +723,8 @@ def main
execute: nil,
dump_bootstrap: nil,
dump_file: nil,
resize: nil,
vcpu: nil,
name: nil,
ports: nil,
domains: nil,
@ -744,6 +761,8 @@ def main
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("--ports=PORTS", "Comma-separated ports") { |p| args[:ports] = p }
opts.on("--domains=DOMAINS", "Comma-separated domains") { |d| args[:domains] = d }

26
un.d
View file

@ -375,7 +375,7 @@ void cmdSession(bool list, string kill, string shell, string network, int vcpu,
writeln(execCurl(cmd));
}
void cmdService(string name, string ports, string bootstrap, string bootstrapFile, string type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string execute, string command, string dumpBootstrap, string dumpFile, string network, int vcpu, string[] inputFiles, string[] svcEnvs, string svcEnvFile, string envAction, string envTarget, string publicKey, string secretKey) {
void cmdService(string name, string ports, string bootstrap, string bootstrapFile, string type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string resize, int resizeVcpu, string execute, string command, string dumpBootstrap, string dumpFile, string network, int vcpu, string[] inputFiles, string[] svcEnvs, string svcEnvFile, string envAction, string envTarget, string publicKey, string secretKey) {
// Handle env subcommand
if (!envAction.empty) {
cmdServiceEnv(envAction, envTarget, svcEnvs, svcEnvFile, publicKey, secretKey);
@ -440,6 +440,21 @@ void cmdService(string name, string ports, string bootstrap, string bootstrapFil
return;
}
if (!resize.empty) {
if (resizeVcpu < 1 || resizeVcpu > 8) {
stderr.writefln("%sError: --vcpu must be between 1 and 8%s", RED, RESET);
exit(1);
}
string json = format(`{"vcpu":%d}`, resizeVcpu);
string path = format("/services/%s", resize);
string authHeaders = buildAuthHeaders("PATCH", path, json, publicKey, secretKey);
string cmd = format(`curl -s -X PATCH '%s/services/%s' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, resize, authHeaders, json);
execCurl(cmd);
int ram = resizeVcpu * 2;
writefln("%sService resized to %d vCPU, %d GB RAM%s", GREEN, resizeVcpu, ram, RESET);
return;
}
if (!execute.empty) {
string json = format(`{"command":"%s"}`, escapeJson(command));
string path = format("/services/%s/execute", execute);
@ -731,8 +746,9 @@ int main(string[] args) {
if (args[1] == "service") {
string name, ports, bootstrap, bootstrapFile, type;
bool list = false;
string info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network;
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dumpBootstrap, dumpFile, network;
int vcpu = 0;
int resizeVcpu = 0;
string[] inputFiles;
string[] svcEnvs;
string svcEnvFile;
@ -747,7 +763,7 @@ int main(string[] args) {
else if (args[i] == "--env-file" && i+1 < args.length) svcEnvFile = args[++i];
else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
}
cmdService(name, ports, bootstrap, bootstrapFile, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey);
cmdService(name, ports, bootstrap, bootstrapFile, type, list, info, logs, tail, sleep, wake, destroy, resize, resizeVcpu, execute, command, dumpBootstrap, dumpFile, network, vcpu, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey);
return 0;
}
@ -764,6 +780,8 @@ int main(string[] args) {
else if (args[i] == "--freeze" && i+1 < args.length) sleep = args[++i];
else if (args[i] == "--unfreeze" && i+1 < args.length) wake = args[++i];
else if (args[i] == "--destroy" && i+1 < args.length) destroy = args[++i];
else if (args[i] == "--resize" && i+1 < args.length) resize = args[++i];
else if (args[i] == "--vcpu" && i+1 < args.length) resizeVcpu = to!int(args[++i]);
else if (args[i] == "--execute" && i+1 < args.length) execute = args[++i];
else if (args[i] == "--command" && i+1 < args.length) command = args[++i];
else if (args[i] == "--dump-bootstrap" && i+1 < args.length) dumpBootstrap = args[++i];
@ -776,7 +794,7 @@ int main(string[] args) {
else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
}
cmdService(name, ports, bootstrap, bootstrapFile, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey);
cmdService(name, ports, bootstrap, bootstrapFile, type, list, info, logs, tail, sleep, wake, destroy, resize, resizeVcpu, execute, command, dumpBootstrap, dumpFile, network, vcpu, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey);
return 0;
}

20
un.dart
View file

@ -93,6 +93,8 @@ class Args {
String? serviceSleep;
String? serviceWake;
String? serviceDestroy;
String? serviceResize;
int serviceResizeVcpu = 0;
String? serviceExecute;
String? serviceCommand;
String? serviceDumpBootstrap;
@ -571,6 +573,18 @@ Future<void> cmdService(Args args) async {
return;
}
if (args.serviceResize != null) {
if (args.serviceResizeVcpu < 1 || args.serviceResizeVcpu > 8) {
stderr.writeln('${red}Error: --vcpu must be between 1 and 8$reset');
exit(1);
}
final payload = {'vcpu': args.serviceResizeVcpu};
await apiRequestCurl('/services/${args.serviceResize}', 'PATCH', jsonEncode(payload), publicKey, secretKey);
final ram = args.serviceResizeVcpu * 2;
print('${green}Service resized to ${args.serviceResizeVcpu} vCPU, $ram GB RAM$reset');
return;
}
if (args.serviceExecute != null) {
final payload = <String, dynamic>{
'command': args.serviceCommand,
@ -836,6 +850,12 @@ Args parseArgs(List<String> argv) {
case '--destroy':
args.serviceDestroy = argv[++i];
break;
case '--resize':
args.serviceResize = argv[++i];
break;
case '--vcpu':
args.serviceResizeVcpu = int.parse(argv[++i]);
break;
case '--execute':
args.serviceExecute = argv[++i];
break;

35
un.erl
View file

@ -213,6 +213,12 @@ service_command(["--destroy", ServiceId | _]) ->
_ = curl_delete(ApiKey, "/services/" ++ ServiceId),
io:format("\033[32mService destroyed: ~s\033[0m~n", [ServiceId]);
service_command(["--resize", ServiceId, "--vcpu", VcpuStr | _]) ->
service_resize(ServiceId, VcpuStr);
service_command(["--resize", ServiceId, "-v", VcpuStr | _]) ->
service_resize(ServiceId, VcpuStr);
service_command(["--execute", ServiceId, "--command", Command | _]) ->
ApiKey = get_api_key(),
Json = "{\"command\":\"" ++ escape_json(Command) ++ "\"}",
@ -688,6 +694,19 @@ curl_delete(ApiKey, Endpoint) ->
check_clock_drift_error(Result),
Result.
curl_patch(ApiKey, Endpoint, TmpFile) ->
{ok, Body} = file:read_file(TmpFile),
BodyStr = binary_to_list(Body),
{PublicKey, SecretKey} = get_api_keys(),
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "PATCH", Endpoint, BodyStr),
Cmd = "curl -s -X PATCH https://api.unsandbox.com" ++ Endpoint ++
" -H 'Content-Type: application/json'" ++
AuthHeaders ++
" -d @" ++ TmpFile,
Result = os:cmd(Cmd),
check_clock_drift_error(Result),
Result.
curl_put_text(Endpoint, Content) ->
{PublicKey, SecretKey} = get_api_keys(),
TmpFile = write_temp_file(Content),
@ -746,6 +765,22 @@ service_env_delete(ServiceId) ->
_ = curl_delete(ApiKey, "/services/" ++ ServiceId ++ "/env"),
io:format("\033[32mVault deleted: ~s\033[0m~n", [ServiceId]).
service_resize(ServiceId, VcpuStr) ->
ApiKey = get_api_key(),
Vcpu = list_to_integer(VcpuStr),
if
Vcpu < 1 orelse Vcpu > 8 ->
io:format(standard_error, "\033[31mError: --vcpu must be between 1 and 8\033[0m~n", []),
halt(1);
true -> ok
end,
Json = "{\"vcpu\":" ++ integer_to_list(Vcpu) ++ "}",
TmpFile = write_temp_file(Json),
_ = curl_patch(ApiKey, "/services/" ++ ServiceId, TmpFile),
file:delete(TmpFile),
Ram = Vcpu * 2,
io:format("\033[32mService resized to ~B vCPU, ~B GB RAM\033[0m~n", [Vcpu, Ram]).
%% Argument parsing
parse_exec_args([], Opts) ->
{maps:get(file, Opts), Opts};

42
un.ex
View file

@ -234,6 +234,28 @@ defmodule Un do
IO.puts("#{@green}Service destroyed: #{service_id}#{@reset}")
end
defp service_command(["--resize", service_id | rest]) do
vcpu = get_opt(rest, "--vcpu", "-v", nil)
if is_nil(vcpu) do
IO.puts(:stderr, "#{@red}Error: --resize requires --vcpu N#{@reset}")
System.halt(1)
end
vcpu_int = String.to_integer(vcpu)
if vcpu_int < 1 or vcpu_int > 8 do
IO.puts(:stderr, "#{@red}Error: --vcpu must be between 1 and 8#{@reset}")
System.halt(1)
end
api_key = get_api_key()
json = "{\"vcpu\":#{vcpu_int}}"
curl_patch(api_key, "/services/#{service_id}", json)
ram = vcpu_int * 2
IO.puts("#{@green}Service resized to #{vcpu_int} vCPU, #{ram} GB RAM#{@reset}")
end
defp service_command(["--snapshot", service_id | rest]) do
api_key = get_api_key()
name = get_opt(rest, "--snapshot-name", nil, nil)
@ -750,6 +772,26 @@ defmodule Un do
output
end
defp curl_patch(api_key, endpoint, json) do
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
File.write!(tmp_file, json)
{public_key, secret_key} = get_api_keys()
headers = build_auth_headers(public_key, secret_key, "PATCH", endpoint, json)
args = [
"-s", "-X", "PATCH",
"https://api.unsandbox.com#{endpoint}",
"-H", "Content-Type: application/json"
] ++ headers ++ ["-d", "@#{tmp_file}"]
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
File.rm(tmp_file)
check_clock_drift(output)
output
end
defp curl_put_text(endpoint, body) do
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.txt"
File.write!(tmp_file, body)

41
un.f90
View file

@ -319,7 +319,7 @@ contains
character(len=256) :: arg, service_id, operation, service_type, service_name
character(len=1024) :: input_files, public_key, secret_key
character(len=2048) :: svc_envs, svc_env_file, env_action, env_target
integer :: i, stat
integer :: i, stat, resize_vcpu
logical :: list_mode
list_mode = .false.
@ -332,6 +332,7 @@ contains
svc_env_file = ''
env_action = ''
env_target = ''
resize_vcpu = 0
! Parse service arguments
i = 2
@ -412,6 +413,24 @@ contains
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--resize') then
operation = 'resize'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--vcpu') then
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, arg)
read(arg, *) resize_vcpu
i = i + 1
end if
else if (trim(arg) == '-v' .and. operation == 'resize') then
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, arg)
read(arg, *) resize_vcpu
i = i + 1
end if
else if (trim(arg) == '--dump-bootstrap') then
operation = 'dump-bootstrap'
if (i+1 <= command_argument_count()) then
@ -582,6 +601,26 @@ contains
'-H "X-Signature: $SIG" >/dev/null && ', &
'echo -e "\x1b[32mService destroyed: ', trim(service_id), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'resize' .and. len_trim(service_id) > 0) then
if (resize_vcpu < 1 .or. resize_vcpu > 8) then
write(0, '(A)') char(27)//'[31mError: --vcpu must be between 1 and 8'//char(27)//'[0m'
stop 1
end if
write(full_cmd, '(30A,I0,A,I0,A,I0,A)') &
'VCPU=', resize_vcpu, '; ', &
'RAM=$((VCPU * 2)); ', &
'BODY=''{"vcpu":''$VCPU''}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:PATCH:/services/', trim(service_id), ':$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X PATCH https://api.unsandbox.com/services/', &
trim(service_id), ' ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-d "$BODY" >/dev/null && ', &
'echo -e "\x1b[32mService resized to ', resize_vcpu, ' vCPU, ', resize_vcpu * 2, ' GB RAM\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'dump-bootstrap' .and. len_trim(service_id) > 0) then
write(full_cmd, '(30A)') &
'echo "Fetching bootstrap script from ', trim(service_id), '..." >&2; ', &

View file

@ -356,6 +356,38 @@
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
;
\ Service resize
: service-resize ( service-id-addr service-id-len vcpu-addr vcpu-len -- )
get-api-key
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
s" #!/bin/bash" r@ write-line throw
s" SERVICE_ID='" r@ write-file throw
2over r@ write-file throw
s" '" r@ write-line throw
s" VCPU='" 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" if [ \"$VCPU\" -lt 1 ] || [ \"$VCPU\" -gt 8 ]; then" r@ write-line throw
s" echo -e '\\x1b[31mError: --vcpu must be between 1 and 8\\x1b[0m' >&2" r@ write-line throw
s" exit 1" r@ write-line throw
s" fi" r@ write-line throw
s" RAM=$((VCPU * 2))" r@ write-line throw
s" BODY='{\"vcpu\":'$VCPU'}'" r@ write-line throw
s" TIMESTAMP=$(date +%s)" r@ write-line throw
s" MESSAGE=\"$TIMESTAMP:PATCH:/services/$SERVICE_ID:$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 PATCH https://api.unsandbox.com/services/$SERVICE_ID -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[32mService resized to $VCPU vCPU, $RAM GB RAM\\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
;
\ Service env status
: service-env-status ( addr len -- )
get-api-key
@ -841,6 +873,40 @@
0 (bye)
then
2dup s" --resize" compare 0= if
2drop
argc @ 4 < if
s" Error: --resize requires service ID" type cr
1 (bye)
then
\ Look for --vcpu or -v in remaining args
argc @ 5 < if
s" Error: --resize requires --vcpu N" type cr
1 (bye)
then
4 arg 2dup s" --vcpu" compare 0= if
2drop
argc @ 6 < if
s" Error: --vcpu requires a value" type cr
1 (bye)
then
3 arg 5 arg service-resize
0 (bye)
then
2dup s" -v" compare 0= if
2drop
argc @ 6 < if
s" Error: -v requires a value" type cr
1 (bye)
then
3 arg 5 arg service-resize
0 (bye)
then
2drop
s" Error: --resize requires --vcpu N" type cr
1 (bye)
then
2dup s" --dump-bootstrap" compare 0= if
2drop
argc @ 4 < if

70
un.fs
View file

@ -105,6 +105,7 @@ type Args = {
mutable ServiceCommand: string option
mutable ServiceDumpBootstrap: string option
mutable ServiceDumpFile: string option
mutable ServiceResize: string option
mutable ServiceSnapshot: string option
mutable ServiceRestore: string option
mutable ServiceFrom: string option
@ -314,6 +315,64 @@ let apiRequest (endpoint: string) (method: string) (data: (string * obj) list op
failwithf "HTTP error - %s" errorMsg
let apiRequestPatch (endpoint: string) (data: (string * obj) list) (publicKey: string) (secretKey: string) =
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest
request.Method <- "PATCH"
request.ContentType <- "application/json"
request.Timeout <- 300000
let body = toJson (box data)
// Add HMAC authentication headers if secretKey is provided
if not (String.IsNullOrEmpty(secretKey)) then
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
let message = sprintf "%d:%s:%s:%s" timestamp "PATCH" 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)
let bytes = Encoding.UTF8.GetBytes(body)
request.ContentLength <- int64 bytes.Length
use stream = request.GetRequestStream()
stream.Write(bytes, 0, bytes.Length)
try
use response = request.GetResponse() :?> HttpWebResponse
if response.StatusCode <> HttpStatusCode.OK then
failwithf "HTTP %A" response.StatusCode
use reader = new StreamReader(response.GetResponseStream())
let responseText = reader.ReadToEnd()
parseJson responseText
with
| :? WebException as ex ->
let errorMsg =
if ex.Response <> null then
use reader = new StreamReader(ex.Response.GetResponseStream())
reader.ReadToEnd()
else
ex.Message
// Check for clock drift error
if errorMsg.Contains("timestamp") && (errorMsg.Contains("401") || errorMsg.Contains("expired") || errorMsg.Contains("invalid")) then
eprintfn "%sError: Request timestamp expired (must be within 5 minutes of server time)%s" red reset
eprintfn "%sYour computer's clock may have drifted.%s" yellow reset
eprintfn "Check your system time and sync with NTP if needed:"
eprintfn " Linux: sudo ntpdate -s time.nist.gov"
eprintfn " macOS: sudo sntp -sS time.apple.com"
eprintfn " Windows: w32tm /resync%s" reset
exit 1
failwithf "HTTP error - %s" errorMsg
let apiRequestText (endpoint: string) (method: string) (body: string) (publicKey: string) (secretKey: string) =
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
@ -735,6 +794,14 @@ let cmdService (args: Args) =
elif args.ServiceDestroy.IsSome then
let result = apiRequest (sprintf "/services/%s" args.ServiceDestroy.Value) "DELETE" None publicKey secretKey
printfn "%sService destroyed: %s%s" green args.ServiceDestroy.Value reset
elif args.ServiceResize.IsSome then
if args.Vcpu <= 0 then
eprintfn "%sError: --resize requires --vcpu N (1-8)%s" red reset
exit 1
let payload = [("vcpu", box args.Vcpu)]
let result = apiRequestPatch (sprintf "/services/%s" args.ServiceResize.Value) payload publicKey secretKey
let ram = args.Vcpu * 2
printfn "%sService resized to %d vCPU, %d GB RAM%s" green args.Vcpu ram reset
elif args.ServiceExecute.IsSome then
let payload = [("command", box args.ServiceCommand.Value)]
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceExecute.Value) "POST" (Some payload) publicKey secretKey
@ -854,6 +921,7 @@ let parseArgs (argv: string[]) =
ServiceCommand = None
ServiceDumpBootstrap = None
ServiceDumpFile = None
ServiceResize = None
ServiceSnapshot = None
ServiceRestore = None
ServiceFrom = None
@ -969,6 +1037,7 @@ let parseArgs (argv: string[]) =
| "--freeze" -> i <- i + 1; args.ServiceSleep <- Some argv.[i]
| "--unfreeze" -> i <- i + 1; args.ServiceWake <- Some argv.[i]
| "--destroy" -> i <- i + 1; args.ServiceDestroy <- Some argv.[i]
| "--resize" -> i <- i + 1; args.ServiceResize <- Some argv.[i]
| "--execute" -> i <- i + 1; args.ServiceExecute <- Some argv.[i]
| "--command" -> i <- i + 1; args.ServiceCommand <- Some argv.[i]
| "--dump-bootstrap" -> i <- i + 1; args.ServiceDumpBootstrap <- Some argv.[i]
@ -1017,6 +1086,7 @@ let printHelp () =
printfn " --freeze ID Freeze service"
printfn " --unfreeze ID Unfreeze service"
printfn " --destroy ID Destroy service"
printfn " --resize ID Resize service (requires --vcpu N)"
printfn " --execute ID Execute command in service"
printfn " --command CMD Command to execute (with --execute)"
printfn " --dump-bootstrap ID Dump bootstrap script"

16
un.go
View file

@ -596,7 +596,7 @@ func cmdSession(sessionList, sessionKill, sessionShell, sessionSnapshot, session
fmt.Printf("%sSession created: %s%s\n", Green, result["id"], Reset)
}
func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceBootstrap, serviceBootstrapFile, serviceList, serviceInfo, serviceLogs, serviceTail, serviceSleep, serviceWake, serviceDestroy, serviceExecute, serviceCommand, serviceDumpBootstrap, serviceDumpFile, serviceSnapshot, serviceRestore, serviceSnapshotName string, serviceHot bool, network string, vcpu int, files inputFiles, envs envVars, envFile, publicKey, secretKey string) {
func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceBootstrap, serviceBootstrapFile, serviceList, serviceInfo, serviceLogs, serviceTail, serviceSleep, serviceWake, serviceDestroy, serviceResize, serviceExecute, serviceCommand, serviceDumpBootstrap, serviceDumpFile, serviceSnapshot, serviceRestore, serviceSnapshotName string, serviceHot bool, network string, vcpu int, files inputFiles, envs envVars, envFile, publicKey, secretKey string) {
if serviceSnapshot != "" {
payload := map[string]interface{}{}
if serviceSnapshotName != "" {
@ -690,6 +690,17 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
return
}
if serviceResize != "" {
if vcpu <= 0 {
fmt.Fprintf(os.Stderr, "%sError: --resize requires -v <vcpu>%s\n", Red, Reset)
os.Exit(1)
}
payload := map[string]interface{}{"vcpu": vcpu}
apiRequest("/services/"+serviceResize, "PATCH", payload, publicKey, secretKey)
fmt.Printf("%sService resized to %d vCPU, %d GB RAM%s\n", Green, vcpu, vcpu*2, Reset)
return
}
if serviceExecute != "" {
payload := map[string]interface{}{"command": serviceCommand}
result := apiRequest("/services/"+serviceExecute+"/execute", "POST", payload, publicKey, secretKey)
@ -1057,6 +1068,7 @@ func main() {
serviceSleep := serviceCmd.String("sleep", "", "Freeze service")
serviceWake := serviceCmd.String("wake", "", "Unfreeze service")
serviceDestroy := serviceCmd.String("destroy", "", "Destroy service")
serviceResize := serviceCmd.String("resize", "", "Resize service vCPU")
serviceExecute := serviceCmd.String("execute", "", "Execute command in service")
serviceCommand := serviceCmd.String("command", "", "Command to execute (with -execute)")
serviceDumpBootstrap := serviceCmd.String("dump-bootstrap", "", "Dump bootstrap script")
@ -1143,7 +1155,7 @@ func main() {
if vc == 0 {
vc = *vcpu
}
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceBootstrapFile, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, *serviceSnapshot, *serviceRestore, *serviceSnapshotName, *serviceHot, net, vc, serviceFiles, serviceEnvs, *serviceEnvFile, publicKey, secretKey)
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceBootstrapFile, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceResize, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, *serviceSnapshot, *serviceRestore, *serviceSnapshotName, *serviceHot, net, vc, serviceFiles, serviceEnvs, *serviceEnvFile, publicKey, secretKey)
return
case "snapshot":

View file

@ -96,6 +96,7 @@ class Args {
String serviceCommand = null
String serviceDumpBootstrap = null
String serviceDumpFile = null
String serviceResize = null
String serviceSnapshot = null
String serviceRestore = null
String serviceFrom = null
@ -210,6 +211,65 @@ def apiRequest(endpoint, method, data, publicKey, secretKey) {
}
}
def apiRequestPatch(endpoint, data, publicKey, secretKey) {
def tempFile = File.createTempFile('un_request_', '.json')
try {
def body = data ?: ""
if (data) {
tempFile.text = data
}
def curlCmd = ['curl', '-s', '-X', 'PATCH', "${API_BASE}${endpoint}",
'-H', 'Content-Type: application/json']
// Add HMAC authentication headers if secretKey is provided
if (secretKey) {
def timestamp = (System.currentTimeMillis() / 1000) as long
def message = "${timestamp}:PATCH:${endpoint}:${body}"
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
def signature = mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
curlCmd += ['-H', "X-Timestamp: ${timestamp}"]
curlCmd += ['-H', "X-Signature: ${signature}"]
} else {
// Legacy API key authentication
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
}
if (data) {
curlCmd += ['-d', "@${tempFile.absolutePath}"]
}
def proc = curlCmd.execute()
def output = proc.text
proc.waitFor()
if (proc.exitValue() != 0) {
System.err.println("${RED}Error: curl failed${RESET}")
System.exit(1)
}
// Check for timestamp authentication errors
if (output.toLowerCase().contains('timestamp') &&
(output.contains('401') || output.toLowerCase().contains('expired') || output.toLowerCase().contains('invalid'))) {
System.err.println("${RED}Error: Request timestamp expired (must be within 5 minutes of server time)${RESET}")
System.err.println("${YELLOW}Your computer's clock may have drifted.${RESET}")
System.err.println("Check your system time and sync with NTP if needed:")
System.err.println(" Linux: sudo ntpdate -s time.nist.gov")
System.err.println(" macOS: sudo sntp -sS time.apple.com")
System.err.println(" Windows: w32tm /resync")
System.exit(1)
}
return output
} finally {
tempFile.delete()
}
}
def readEnvFile(filename) {
def file = new File(filename)
if (!file.exists()) {
@ -713,6 +773,18 @@ def cmdService(args) {
return
}
if (args.serviceResize) {
if (args.vcpu <= 0) {
System.err.println("${RED}Error: --resize requires --vcpu N (1-8)${RESET}")
System.exit(1)
}
def json = """{"vcpu":${args.vcpu}}"""
apiRequestPatch("/services/${args.serviceResize}", json, publicKey, secretKey)
def ram = args.vcpu * 2
println("${GREEN}Service resized to ${args.vcpu} vCPU, ${ram} GB RAM${RESET}")
return
}
if (args.serviceExecute) {
def json = """{"command":"${args.serviceCommand}"}"""
def output = apiRequest("/services/${args.serviceExecute}/execute", 'POST', json, publicKey, secretKey)
@ -987,6 +1059,9 @@ def parseArgs(argv) {
case '--destroy':
args.serviceDestroy = argv[++i]
break
case '--resize':
args.serviceResize = argv[++i]
break
case '--execute':
args.serviceExecute = argv[++i]
break
@ -1050,6 +1125,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires --vcpu N)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

30
un.hs
View file

@ -161,7 +161,7 @@ data ServiceOpts = ServiceOpts
data ServiceAction = ServiceList | ServiceInfo String | ServiceLogs String
| ServiceSleep String | ServiceWake String | ServiceDestroy String
| ServiceExecute String String | ServiceDumpBootstrap String (Maybe String)
| ServiceResize String | ServiceExecute String String | ServiceDumpBootstrap String (Maybe String)
| ServiceCreate | ServiceSnapshot String | ServiceRestore String
| ServiceEnv String (Maybe String) -- action, target
@ -240,6 +240,7 @@ parseService args = return $ parseServiceArgs args defaultServiceOpts
parseServiceArgs ("--freeze":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceSleep id }
parseServiceArgs ("--unfreeze":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceWake id }
parseServiceArgs ("--destroy":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDestroy id }
parseServiceArgs ("--resize":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceResize id }
parseServiceArgs ("--snapshot":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceSnapshot id }
parseServiceArgs ("--restore":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceRestore id }
parseServiceArgs ("--execute":id:"--command":cmd:rest) opts = parseServiceArgs rest opts { svcAction = ServiceExecute id cmd }
@ -322,6 +323,10 @@ printHelp = do
putStrLn "Service options:"
putStrLn " -e KEY=VALUE Set vault env var (with --name or env set)"
putStrLn " --env-file FILE Load vault vars from file"
putStrLn " --freeze ID Freeze service"
putStrLn " --unfreeze ID Unfreeze service"
putStrLn " --destroy ID Destroy service"
putStrLn " --resize ID Resize service (requires -v N)"
putStrLn ""
putStrLn "Service env commands:"
putStrLn " env status ID Check vault status"
@ -467,6 +472,16 @@ serviceCommand opts = do
ServiceDestroy sid -> do
(_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/services/" ++ sid)
putStrLn $ green ++ "Service destroyed: " ++ sid ++ reset
ServiceResize sid -> do
case svcVcpu opts of
Nothing -> do
hPutStrLn stderr $ red ++ "Error: --resize requires -v N (1-8)" ++ reset
exitFailure
Just vcpu -> do
let json = "{\"vcpu\":" ++ show vcpu ++ "}"
let ram = vcpu * 2
(_, stdout, _) <- curlPatch apiKey ("https://api.unsandbox.com/services/" ++ sid) json
putStrLn $ green ++ "Service resized to " ++ show vcpu ++ " vCPU, " ++ show ram ++ " GB RAM" ++ reset
ServiceExecute sid cmd -> do
let json = "{\"command\":\"" ++ escapeJSON cmd ++ "\"}"
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/execute") json
@ -667,6 +682,19 @@ curlDelete apiKey url = do
checkClockDriftError stdout
return (exitCode, stdout, stderr)
curlPatch :: String -> String -> String -> IO (ExitCode, String, String)
curlPatch apiKey url body = do
(publicKey, secretKey) <- getApiKeys
let path = drop (length "https://api.unsandbox.com") url
authHeaders <- buildAuthHeaders publicKey secretKey "PATCH" path body
(exitCode, stdout, stderr) <- readProcessWithExitCode "curl"
([ "-s", "-X", "PATCH"
, url
, "-H", "Content-Type: application/json"
] ++ authHeaders ++ ["-d", body]) ""
checkClockDriftError stdout
return (exitCode, stdout, stderr)
curlPut :: String -> String -> String -> IO (ExitCode, String, String)
curlPut apiKey url body = do
(publicKey, secretKey) <- getApiKeys

54
un.jl
View file

@ -159,6 +159,46 @@ function api_request(endpoint::String, public_key::String, secret_key::String; m
end
end
function api_request_patch(endpoint::String, public_key::String, secret_key::String; data=nothing)
url = API_BASE * endpoint
# Prepare body
body = data !== nothing ? JSON.json(data) : ""
# Generate timestamp and signature
timestamp = Int64(floor(time()))
signature = compute_signature(secret_key, timestamp, "PATCH", endpoint, body)
headers = [
"Authorization" => "Bearer $public_key",
"X-Timestamp" => string(timestamp),
"X-Signature" => signature,
"Content-Type" => "application/json"
]
try
response = HTTP.request("PATCH", url, headers, body, readtimeout=300)
return JSON.parse(String(response.body))
catch e
if isa(e, HTTP.ExceptionRequest.StatusError)
error_body = String(e.response.body)
if e.status == 401 && occursin("timestamp", lowercase(error_body))
println(stderr, "$(RED)Error: Request timestamp expired (must be within 5 minutes of server time)$(RESET)")
println(stderr, "$(YELLOW)Your computer's clock may have drifted.$(RESET)")
println(stderr, "Check your system time and sync with NTP if needed:")
println(stderr, " Linux: sudo ntpdate -s time.nist.gov")
println(stderr, " macOS: sudo sntp -sS time.apple.com")
println(stderr, " Windows: w32tm /resync")
else
println(stderr, "$(RED)Error: HTTP $(e.status) - $(error_body)$(RESET)")
end
else
println(stderr, "$(RED)Error: Request failed: $e$(RESET)")
end
exit(1)
end
end
function api_request_text(endpoint::String, public_key::String, secret_key::String, body::String)::Bool
url = API_BASE * endpoint
timestamp = Int64(floor(time()))
@ -508,6 +548,18 @@ function cmd_service(args)
return
end
if args["resize"] !== nothing
vcpu = args["vcpu"]
if vcpu === nothing || vcpu <= 0
println(stderr, "$(RED)Error: --resize requires --vcpu N (1-8)$(RESET)")
exit(1)
end
api_request_patch("/services/$(args["resize"])", public_key, secret_key, data=Dict("vcpu" => vcpu))
ram = vcpu * 2
println("$(GREEN)Service resized to $(vcpu) vCPU, $(ram) GB RAM$(RESET)")
return
end
if args["dump-bootstrap"] !== nothing
println(stderr, "Fetching bootstrap script from $(args["dump-bootstrap"])...")
payload = Dict("command" => "cat /tmp/bootstrap.sh")
@ -862,6 +914,8 @@ function main()
help = "Unfreeze service"
"--destroy"
help = "Destroy service"
"--resize"
help = "Resize service (requires --vcpu N)"
"--dump-bootstrap"
help = "Dump bootstrap script from service"
"--dump-file"

16
un.js
View file

@ -696,6 +696,17 @@ async function cmdService(args) {
return;
}
if (args.resize) {
if (!args.vcpu) {
console.error(`${RED}Error: --vcpu required with --resize${RESET}`);
process.exit(1);
}
const payload = { vcpu: args.vcpu };
await apiRequest(`/services/${args.resize}`, "PATCH", payload, publicKey, secretKey);
console.log(`${GREEN}Service resized to ${args.vcpu} vCPU, ${args.vcpu * 2}GB RAM${RESET}`);
return;
}
if (args.execute) {
const payload = { command: args.command };
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, publicKey, secretKey);
@ -816,6 +827,7 @@ function parseArgs(argv) {
sleep: null,
wake: null,
destroy: null,
resize: null,
execute: null,
command_arg: null,
dumpBootstrap: null,
@ -931,6 +943,9 @@ function parseArgs(argv) {
} else if (arg === '--destroy' && i + 1 < argv.length) {
args.destroy = argv[++i];
i++;
} else if (arg === '--resize' && i + 1 < argv.length) {
args.resize = argv[++i];
i++;
} else if (arg === '--execute' && i + 1 < argv.length) {
args.execute = argv[++i];
i++;
@ -1013,6 +1028,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires -v)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

53
un.kt
View file

@ -101,6 +101,7 @@ data class Args(
var serviceCommand: String? = null,
var serviceDumpBootstrap: String? = null,
var serviceDumpFile: String? = null,
var serviceResize: String? = null,
var keyExtend: Boolean = false,
var envFile: String? = null,
var envAction: String? = null,
@ -333,6 +334,18 @@ fun cmdService(args: Args) {
return
}
if (args.serviceResize != null) {
if (args.vcpu <= 0) {
System.err.println("${RED}Error: --resize requires --vcpu N (1-8)${RESET}")
exitProcess(1)
}
val payload = mapOf("vcpu" to args.vcpu)
apiRequestPatch("/services/${args.serviceResize}", payload, publicKey, secretKey)
val ram = args.vcpu * 2
println("${GREEN}Service resized to ${args.vcpu} vCPU, ${ram} GB RAM${RESET}")
return
}
if (args.serviceExecute != null) {
val payload = mutableMapOf<String, Any>("command" to args.serviceCommand!!)
val result = apiRequest("/services/${args.serviceExecute}/execute", "POST", payload, publicKey, secretKey)
@ -609,6 +622,44 @@ fun apiRequest(endpoint: String, method: String, data: Map<String, Any>?, public
return parseJson(response)
}
fun apiRequestPatch(endpoint: String, data: Map<String, Any>, publicKey: String?, secretKey: String): Map<String, Any> {
val timestamp = System.currentTimeMillis() / 1000
val body = toJson(data)
val signatureData = "$timestamp:PATCH:$endpoint:$body"
val signature = hmacSha256(secretKey, signatureData)
val url = URL(API_BASE + endpoint)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "PATCH"
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "application/json")
connection.connectTimeout = 30000
connection.readTimeout = 300000
connection.doOutput = true
connection.outputStream.use { it.write(body.toByteArray()) }
if (connection.responseCode !in 200..299) {
val error = connection.errorStream?.bufferedReader()?.readText() ?: ""
if (connection.responseCode == 401 && error.lowercase().contains("timestamp")) {
System.err.println("${RED}Error: Request timestamp expired (must be within 5 minutes of server time)${RESET}")
System.err.println("${YELLOW}Your computer's clock may have drifted.${RESET}")
System.err.println("Check your system time and sync with NTP if needed:")
System.err.println(" Linux: sudo ntpdate -s time.nist.gov")
System.err.println(" macOS: sudo sntp -sS time.apple.com")
System.err.println(" Windows: w32tm /resync")
exitProcess(1)
}
throw RuntimeException("HTTP ${connection.responseCode} - $error")
}
val response = connection.inputStream.bufferedReader().readText()
return parseJson(response)
}
fun apiRequestText(endpoint: String, method: String, body: String, publicKey: String?, secretKey: String): Pair<Boolean, String> {
val timestamp = System.currentTimeMillis() / 1000
val signatureData = "$timestamp:$method:$endpoint:$body"
@ -912,6 +963,7 @@ fun parseArgs(args: Array<String>): Args {
"--freeze" -> result.serviceSleep = args[++i]
"--unfreeze" -> result.serviceWake = args[++i]
"--destroy" -> result.serviceDestroy = args[++i]
"--resize" -> result.serviceResize = args[++i]
"--execute" -> result.serviceExecute = args[++i]
"--command" -> result.serviceCommand = args[++i]
"--dump-bootstrap" -> result.serviceDumpBootstrap = args[++i]
@ -975,6 +1027,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires --vcpu N)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

32
un.lisp
View file

@ -186,6 +186,19 @@
response))
(delete-file tmp-file))))
(defun curl-patch (api-key endpoint json-data)
(let ((tmp-file (write-temp-file json-data)))
(unwind-protect
(destructuring-bind (public-key secret-key) (get-api-keys)
(let* ((auth-headers (build-auth-headers public-key secret-key "PATCH" endpoint json-data))
(base-args (list "curl" "-s" "-X" "PATCH"
(format nil "https://api.unsandbox.com~a" endpoint)
"-H" "Content-Type: application/json"))
(response (run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
(check-clock-drift response)
response))
(delete-file tmp-file))))
(defun curl-put-text (api-key endpoint content)
"PUT request with text/plain content type (for vault)"
(let ((tmp-file (write-temp-file content)))
@ -316,6 +329,16 @@
((string= action "destroy")
(curl-delete api-key (format nil "/services/~a" id))
(format t "~aService destroyed: ~a~a~%" *green* id *reset*))
((string= action "resize")
(if (or (null service-type) (string= service-type ""))
(progn
(format *error-output* "~aError: --resize requires --vcpu N (1-8)~a~%" *red* *reset*)
(uiop:quit 1))
(let* ((vcpu (parse-integer service-type))
(ram (* vcpu 2))
(json (format nil "{\"vcpu\":~a}" vcpu)))
(curl-patch api-key (format nil "/services/~a" id) json)
(format t "~aService resized to ~a vCPU, ~a GB RAM~a~%" *green* vcpu ram *reset*))))
((string= action "execute")
(when (and id bootstrap)
(let* ((json (format nil "{\"command\":\"~a\"}" (escape-json bootstrap)))
@ -522,6 +545,15 @@
(service-cmd "wake" (third args) nil nil nil nil nil nil nil nil))
((and (> (length args) 2) (string= (second args) "--destroy"))
(service-cmd "destroy" (third args) nil nil nil nil nil nil nil nil))
((and (> (length args) 3) (string= (second args) "--resize"))
;; --resize ID --vcpu N: id is third, vcpu is fourth (after -v flag)
(let ((id (third args))
(vcpu (if (and (> (length args) 4)
(or (string= (fourth args) "-v")
(string= (fourth args) "--vcpu")))
(fifth args)
nil)))
(service-cmd "resize" id nil nil nil nil vcpu nil nil nil)))
((and (> (length args) 3) (string= (second args) "--execute"))
(service-cmd "execute" (third args) nil nil (fourth args) nil nil nil nil nil))
((and (> (length args) 3) (string= (second args) "--dump-bootstrap"))

26
un.lua
View file

@ -736,6 +736,23 @@ local function cmd_service(options)
return
end
if options.resize then
local vcpu = options.resize_vcpu or options.vcpu
if not vcpu then
io.stderr:write(RED .. "Error: --resize requires --vcpu or -v" .. RESET .. "\n")
os.exit(1)
end
if vcpu < 1 or vcpu > 8 then
io.stderr:write(RED .. "Error: vCPU must be between 1 and 8" .. RESET .. "\n")
os.exit(1)
end
local payload = { vcpu = vcpu }
api_request("/services/" .. options.resize, "PATCH", payload, keys)
local ram = vcpu * 2
print(GREEN .. "Service resized to " .. vcpu .. " vCPU, " .. ram .. " GB RAM" .. RESET)
return
end
if options.execute then
local payload = { command = options.command }
local result = api_request("/services/" .. options.execute .. "/execute", "POST", payload, keys)
@ -867,6 +884,8 @@ local function main()
sleep = nil,
wake = nil,
destroy = nil,
resize = nil,
resize_vcpu = nil,
execute = nil,
command = nil,
dump_bootstrap = nil,
@ -979,6 +998,12 @@ local function main()
elseif a == "--destroy" then
i = i + 1
options.destroy = arg[i]
elseif a == "--resize" then
i = i + 1
options.resize = arg[i]
elseif a == "--vcpu" then
i = i + 1
options.resize_vcpu = tonumber(arg[i])
elseif a == "--execute" then
i = i + 1
options.execute = arg[i]
@ -1059,6 +1084,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires --vcpu)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

22
un.m
View file

@ -663,6 +663,7 @@ void cmdService(NSArray* args) {
NSString* sleepId = nil;
NSString* wakeId = nil;
NSString* destroyId = nil;
NSString* resizeId = nil;
NSString* dumpBootstrapId = nil;
NSString* dumpFile = nil;
NSString* name = nil;
@ -736,6 +737,10 @@ void cmdService(NSArray* args) {
wakeId = args[++i];
} else if ([arg isEqualToString:@"--destroy"] && i + 1 < [args count]) {
destroyId = args[++i];
} else if ([arg isEqualToString:@"--resize"] && i + 1 < [args count]) {
resizeId = args[++i];
} else if ([arg isEqualToString:@"--vcpu"] && i + 1 < [args count]) {
vcpu = [args[++i] intValue];
} else if ([arg isEqualToString:@"--dump-bootstrap"] && i + 1 < [args count]) {
dumpBootstrapId = args[++i];
} else if ([arg isEqualToString:@"--dump-file"] && i + 1 < [args count]) {
@ -833,6 +838,23 @@ void cmdService(NSArray* args) {
return;
}
if (resizeId) {
if (vcpu <= 0) {
fprintf(stderr, "%sError: --resize requires --vcpu or -v%s\n", [RED UTF8String], [RESET UTF8String]);
exit(1);
}
if (vcpu < 1 || vcpu > 8) {
fprintf(stderr, "%sError: vCPU must be between 1 and 8%s\n", [RED UTF8String], [RESET UTF8String]);
exit(1);
}
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", resizeId];
NSDictionary* payload = @{@"vcpu": @(vcpu)};
apiRequest(endpoint, @"PATCH", payload, publicKey, secretKey);
int ram = vcpu * 2;
printf("%sService resized to %d vCPU, %d GB RAM%s\n", [GREEN UTF8String], vcpu, ram, [RESET UTF8String]);
return;
}
if (dumpBootstrapId) {
fprintf(stderr, "Fetching bootstrap script from %s...\n", [dumpBootstrapId UTF8String]);
NSDictionary* payload = @{@"command": @"cat /tmp/bootstrap.sh"};

29
un.ml
View file

@ -678,6 +678,33 @@ let service_command action name ports bootstrap bootstrap_file service_type netw
| None ->
Printf.fprintf stderr "Error: --destroy requires service ID\n";
exit 1)
| "resize" ->
(match (name, vcpu) with
| (Some sid, Some v) ->
if v < 1 || v > 8 then begin
Printf.fprintf stderr "%sError: vCPU must be between 1 and 8%s\n" red reset;
exit 1
end;
let json = Printf.sprintf "{\"vcpu\":%d}" v in
let endpoint = Printf.sprintf "/services/%s" sid in
let (public_key, secret_key) = get_api_keys () in
let auth_headers = build_auth_headers public_key secret_key "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 https://api.unsandbox.com%s -H 'Content-Type: application/json'%s -d @%s"
endpoint auth_headers tmp_file in
let _ = Sys.command cmd in
Sys.remove tmp_file;
let ram = v * 2 in
Printf.printf "%sService resized to %d vCPU, %d GB RAM%s\n" green v ram reset
| (Some _, None) ->
Printf.fprintf stderr "%sError: --resize requires --vcpu or -v%s\n" red reset;
exit 1
| (None, _) ->
Printf.fprintf stderr "Error: --resize requires service ID\n";
exit 1)
| "execute" ->
(match name with
| Some sid ->
@ -859,6 +886,8 @@ let () =
| "--freeze" :: id :: rest -> parse_service "sleep" (Some id) ports bootstrap bootstrap_file service_type network vcpu env_file rest
| "--unfreeze" :: id :: rest -> parse_service "wake" (Some id) ports bootstrap bootstrap_file service_type network vcpu env_file rest
| "--destroy" :: id :: rest -> parse_service "destroy" (Some id) ports bootstrap bootstrap_file service_type network vcpu env_file rest
| "--resize" :: id :: rest -> parse_service "resize" (Some id) ports bootstrap bootstrap_file service_type network vcpu env_file rest
| "--vcpu" :: v :: rest -> parse_service action name ports bootstrap bootstrap_file service_type network (Some (int_of_string v)) env_file rest
| "--execute" :: id :: "--command" :: cmd :: rest -> parse_service "execute" (Some id) ports (Some cmd) bootstrap_file service_type network vcpu env_file rest
| "--dump-bootstrap" :: id :: file :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap (Some file) service_type network vcpu env_file rest
| "--dump-bootstrap" :: id :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap bootstrap_file service_type network vcpu env_file rest

27
un.nim
View file

@ -302,7 +302,7 @@ proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, scree
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
echo execCurl(cmd)
proc cmdService(name, ports, bootstrap, bootstrapFile, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, 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, 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)
@ -359,6 +359,22 @@ proc cmdService(name, ports, bootstrap, bootstrapFile, serviceType: string, list
echo GREEN & "Service destroyed: " & destroy & RESET
return
if resize != "":
if resizeVcpu <= 0:
stderr.writeLine(RED & "Error: --resize requires --vcpu or -v" & RESET)
quit(1)
if resizeVcpu < 1 or resizeVcpu > 8:
stderr.writeLine(RED & "Error: vCPU must be between 1 and 8" & RESET)
quit(1)
let json = fmt"""{{"vcpu":{resizeVcpu}}}"""
let path = fmt"/services/{resize}"
let authHeaders = buildAuthHeaders("PATCH", path, json, publicKey, secretKey)
let cmd = fmt"""curl -s -X PATCH '{API_BASE}/services/{resize}' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
discard execCurl(cmd)
let ram = resizeVcpu * 2
echo GREEN & "Service resized to " & $resizeVcpu & " vCPU, " & $ram & " GB RAM" & RESET
return
if execute != "":
let json = fmt"""{"command":"{escapeJson(command)}"}"""
let path = fmt"/services/{execute}/execute"
@ -620,8 +636,9 @@ proc main() =
if args[0] == "service":
var name, ports, bootstrap, bootstrapFile, serviceType = ""
var list = false
var info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network = ""
var info, logs, tail, sleep, wake, destroy, resize, execute, command, dumpBootstrap, dumpFile, network = ""
var vcpu = 0
var resizeVcpu = 0
var inputFiles: seq[string] = @[]
var svcEnvs: seq[string] = @[]
var svcEnvFile = ""
@ -642,7 +659,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, execute, command, dumpBootstrap, dumpFile, network, vcpu, 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, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey)
return
while i < args.len:
@ -659,6 +676,8 @@ proc main() =
of "--freeze": sleep = args[i+1]; inc i
of "--unfreeze": wake = args[i+1]; inc i
of "--destroy": destroy = args[i+1]; inc i
of "--resize": resize = args[i+1]; inc i
of "--vcpu": resizeVcpu = parseInt(args[i+1]); inc i
of "--execute": execute = args[i+1]; inc i
of "--command": command = args[i+1]; inc i
of "--dump-bootstrap": dumpBootstrap = args[i+1]; inc i
@ -678,7 +697,7 @@ proc main() =
inc i
else: discard
inc i
cmdService(name, ports, bootstrap, bootstrapFile, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, 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, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey)
return
# Execute mode

17
un.php
View file

@ -660,6 +660,18 @@ function cmd_service($options) {
return;
}
if ($options['resize']) {
if (!$options['vcpu']) {
fwrite(STDERR, RED . "Error: --vcpu is required with --resize" . RESET . "\n");
exit(1);
}
$payload = ['vcpu' => $options['vcpu']];
api_request("/services/{$options['resize']}", 'PATCH', $payload, $keys);
$ram = $options['vcpu'] * 2;
echo GREEN . "Service resized to {$options['vcpu']} vCPU, {$ram} GB RAM" . RESET . "\n";
return;
}
if ($options['execute']) {
$payload = ['command' => $options['command']];
$result = api_request("/services/{$options['execute']}/execute", 'POST', $payload, $keys);
@ -783,6 +795,7 @@ function main() {
'sleep' => null,
'wake' => null,
'destroy' => null,
'resize' => null,
'execute' => null,
'command' => null,
'dump_bootstrap' => null,
@ -896,6 +909,9 @@ function main() {
case '--destroy':
$options['destroy'] = $argv[++$i];
break;
case '--resize':
$options['resize'] = $argv[++$i];
break;
case '--execute':
$options['execute'] = $argv[++$i];
break;
@ -977,6 +993,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires -v)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

16
un.pl
View file

@ -524,6 +524,18 @@ sub cmd_service {
return;
}
if ($options->{resize}) {
unless ($options->{vcpu}) {
print STDERR "${RED}Error: --vcpu is required with --resize${RESET}\n";
exit 1;
}
my $payload = { vcpu => $options->{vcpu} };
api_request("/services/$options->{resize}", 'PATCH', $payload, $public_key, $secret_key);
my $ram = $options->{vcpu} * 2;
print "${GREEN}Service resized to $options->{vcpu} vCPU, $ram GB RAM${RESET}\n";
return;
}
if ($options->{execute}) {
my $payload = { command => $options->{command} };
my $result = api_request("/services/$options->{execute}/execute", 'POST', $payload, $public_key, $secret_key);
@ -734,6 +746,7 @@ sub main {
sleep => undef,
wake => undef,
destroy => undef,
resize => undef,
execute => undef,
command => undef,
dump_bootstrap => undef,
@ -811,6 +824,8 @@ sub main {
$options{wake} = $ARGV[++$i];
} elsif ($arg eq '--destroy') {
$options{destroy} = $ARGV[++$i];
} elsif ($arg eq '--resize') {
$options{resize} = $ARGV[++$i];
} elsif ($arg eq '--execute') {
$options{execute} = $ARGV[++$i];
} elsif ($arg eq '--command') {
@ -885,6 +900,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires -v)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

27
un.pro
View file

@ -230,6 +230,16 @@ service_destroy(ServiceId) :-
[ServiceId, SecretKey, ServiceId, PublicKey, ServiceId]),
shell(Cmd, 0).
% Service resize
service_resize(ServiceId, Vcpu) :-
get_public_key(PublicKey),
get_secret_key(SecretKey),
Ram is Vcpu * 2,
format(atom(Cmd),
'BODY=\'\'{\"vcpu\":~w}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:PATCH:/services/~w:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X PATCH https://api.unsandbox.com/services/~w -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" >/dev/null && echo -e "\\x1b[32mService resized to ~w vCPU, ~w GB RAM\\x1b[0m"',
[Vcpu, ServiceId, SecretKey, ServiceId, PublicKey, Vcpu, Ram]),
shell(Cmd, 0).
% Service env status
service_env_status(ServiceId) :-
get_public_key(PublicKey),
@ -417,6 +427,23 @@ parse_service_args(['--logs', ServiceId|_], _, _, _, _, _, _, _, _, _, _) :- ser
parse_service_args(['--freeze', ServiceId|_], _, _, _, _, _, _, _, _, _, _) :- service_sleep(ServiceId).
parse_service_args(['--unfreeze', ServiceId|_], _, _, _, _, _, _, _, _, _, _) :- service_wake(ServiceId).
parse_service_args(['--destroy', ServiceId|_], _, _, _, _, _, _, _, _, _, _) :- service_destroy(ServiceId).
parse_service_args(['--resize', ServiceId, '--vcpu', VcpuAtom|_], _, _, _, _, _, _, _, _, _, _) :-
atom_number(VcpuAtom, Vcpu),
( Vcpu >= 1, Vcpu =< 8
-> service_resize(ServiceId, Vcpu)
; write(user_error, '\x1b[31mError: vCPU must be between 1 and 8\x1b[0m\n'),
halt(1)
).
parse_service_args(['--resize', ServiceId, '-v', VcpuAtom|_], _, _, _, _, _, _, _, _, _, _) :-
atom_number(VcpuAtom, Vcpu),
( Vcpu >= 1, Vcpu =< 8
-> service_resize(ServiceId, Vcpu)
; write(user_error, '\x1b[31mError: vCPU must be between 1 and 8\x1b[0m\n'),
halt(1)
).
parse_service_args(['--resize', _|_], _, _, _, _, _, _, _, _, _, _) :-
write(user_error, '\x1b[31mError: --resize requires --vcpu or -v\x1b[0m\n'),
halt(1).
parse_service_args(['--dump-bootstrap', ServiceId|Rest], _, _, _, _, _, _, _, _, _, _) :-
( Rest = ['--dump-file', DumpFile|_]
-> service_dump_bootstrap(ServiceId, DumpFile)

31
un.ps1
View file

@ -539,6 +539,36 @@ function Invoke-Service {
return
}
if ($Args -contains "--resize") {
$idx = [array]::IndexOf($Args, "--resize")
$serviceId = $Args[$idx + 1]
# Get vcpu value from --vcpu or -v
$vcpuValue = 0
if ($Args -contains "--vcpu") {
$vIdx = [array]::IndexOf($Args, "--vcpu")
$vcpuValue = [int]$Args[$vIdx + 1]
} elseif ($Args -contains "-v") {
$vIdx = [array]::IndexOf($Args, "-v")
$vcpuValue = [int]$Args[$vIdx + 1]
}
if ($vcpuValue -le 0) {
Write-Error "Error: --resize requires --vcpu or -v"
exit 1
}
if ($vcpuValue -lt 1 -or $vcpuValue -gt 8) {
Write-Error "Error: vCPU must be between 1 and 8"
exit 1
}
$payload = @{ vcpu = $vcpuValue } | ConvertTo-Json
Invoke-Api -Endpoint "/services/$serviceId" -Method "PATCH" -Body $payload
$ram = $vcpuValue * 2
Write-Host "`e[32mService resized to $vcpuValue vCPU, $ram GB RAM`e[0m"
return
}
if ($Args -contains "--dump-bootstrap") {
$idx = [array]::IndexOf($Args, "--dump-bootstrap")
$serviceId = $Args[$idx + 1]
@ -681,6 +711,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires --vcpu or -v)
--dump-bootstrap ID Dump bootstrap script from service
--dump-file FILE Save bootstrap to file (with --dump-bootstrap)

14
un.py
View file

@ -744,6 +744,15 @@ def cmd_service(args):
print(f"{GREEN}Service destroyed: {args.destroy}{RESET}")
return
if args.resize:
if not args.vcpu:
print(f"{RED}Error: --vcpu required with --resize{RESET}", file=sys.stderr)
sys.exit(1)
payload = {"vcpu": args.vcpu}
result = api_request(f"/services/{args.resize}", method="PATCH", data=payload, public_key=public_key, secret_key=secret_key)
print(f"{GREEN}Service resized to {args.vcpu} vCPU, {args.vcpu * 2}GB RAM{RESET}")
return
if args.snapshot:
payload = {}
if args.snapshot_name:
@ -925,9 +934,10 @@ Examples:
service_parser.add_argument("--info", metavar="ID", help="Get service details")
service_parser.add_argument("--tail", metavar="ID", help="Get last 9000 lines of logs")
service_parser.add_argument("--logs", metavar="ID", help="Get all logs")
service_parser.add_argument("--freeze", metavar="ID", help="Freeze service")
service_parser.add_argument("--unfreeze", metavar="ID", help="Unfreeze service")
service_parser.add_argument("--freeze", "--sleep", dest="sleep", metavar="ID", help="Freeze service")
service_parser.add_argument("--unfreeze", "--wake", dest="wake", metavar="ID", help="Unfreeze service")
service_parser.add_argument("--destroy", metavar="ID", help="Destroy service")
service_parser.add_argument("--resize", metavar="ID", help="Resize service vCPU/memory")
service_parser.add_argument("--snapshot", metavar="SERVICE_ID", help="Create snapshot of service")
service_parser.add_argument("--restore", metavar="SNAPSHOT_ID", help="Restore from snapshot ID")
service_parser.add_argument("--snapshot-name", metavar="NAME", help="Name for snapshot")

19
un.r
View file

@ -145,6 +145,8 @@ api_request <- function(endpoint, public_key, secret_key, method = "GET", data =
response <- POST(url, headers, body = body_content, encode = "raw", timeout(300))
} else if (method == "DELETE") {
response <- DELETE(url, headers, timeout(300))
} else if (method == "PATCH") {
response <- PATCH(url, headers, body = body_content, encode = "raw", timeout(300))
} else {
stop(paste("Unsupported method:", method))
}
@ -710,6 +712,18 @@ cmd_service <- function(args) {
return()
}
if (!is.null(args$resize)) {
if (is.null(args$vcpu) || args$vcpu < 1 || args$vcpu > 8) {
cat(sprintf("%sError: --resize requires --vcpu N (1-8)%s\n", RED, RESET), file = stderr())
quit(status = 1)
}
payload <- list(vcpu = args$vcpu)
result <- api_request(paste0("/services/", args$resize), public_key, secret_key, method = "PATCH", data = payload)
ram <- args$vcpu * 2
cat(sprintf("%sService resized to %d vCPU, %d GB RAM%s\n", GREEN, args$vcpu, ram, RESET))
return()
}
if (!is.null(args$snapshot_svc)) {
payload <- list()
if (!is.null(args$snapshot_name)) {
@ -871,6 +885,7 @@ parse_args <- function() {
sleep = NULL,
wake = NULL,
destroy = NULL,
resize = NULL,
delete = NULL,
clone = NULL,
clone_name = NULL,
@ -977,6 +992,10 @@ parse_args <- function() {
i <- i + 1
result$destroy <- args[i]
i <- i + 1
} else if (arg == "--resize") {
i <- i + 1
result$resize <- args[i]
i <- i + 1
} else if (arg == "--dump-bootstrap") {
i <- i + 1
result$dump_bootstrap <- args[i]

24
un.raku
View file

@ -123,6 +123,13 @@ sub api-request(Str $endpoint, Str $method, %data?, Str :$public-key!, Str :$sec
$body = to-json(%data);
@args.append: '-d', $body;
}
} elsif $method eq 'PATCH' {
@args.append: '-X', 'PATCH';
@args.append: '-H', 'Content-Type: application/json';
if %data {
$body = to-json(%data);
@args.append: '-d', $body;
}
}
@args.append: '-H', "Authorization: Bearer $public-key";
@ -438,6 +445,7 @@ sub cmd-service(@args) {
my $sleep-id = '';
my $wake-id = '';
my $destroy-id = '';
my $resize-id = '';
my $dump-bootstrap-id = '';
my $dump-file = '';
my $name = '';
@ -531,6 +539,10 @@ sub cmd-service(@args) {
$i++;
$destroy-id = @args[$i];
}
when '--resize' {
$i++;
$resize-id = @args[$i];
}
when '--dump-bootstrap' {
$i++;
$dump-bootstrap-id = @args[$i];
@ -630,6 +642,18 @@ sub cmd-service(@args) {
return;
}
if $resize-id {
unless $vcpu >= 1 && $vcpu <= 8 {
note "{$RED}Error: --resize requires --vcpu N (1-8){$RESET}";
exit 1;
}
my %payload = vcpu => $vcpu;
api-request("/services/$resize-id", 'PATCH', %payload, :$public-key, :$secret-key);
my $ram = $vcpu * 2;
say "{$GREEN}Service resized to $vcpu vCPU, $ram GB RAM{$RESET}";
return;
}
if $dump-bootstrap-id {
note "Fetching bootstrap script from $dump-bootstrap-id...";
my %payload = command => "cat /tmp/bootstrap.sh";

18
un.rb
View file

@ -139,6 +139,7 @@ def api_request(endpoint, method: 'GET', data: nil, keys:)
when 'GET' then Net::HTTP::Get.new(uri)
when 'POST' then Net::HTTP::Post.new(uri)
when 'DELETE' then Net::HTTP::Delete.new(uri)
when 'PATCH' then Net::HTTP::Patch.new(uri)
else raise "Unknown method: #{method}"
end
@ -678,6 +679,18 @@ def cmd_service(options)
return
end
if options[:resize]
unless options[:vcpu]
warn "#{RED}Error: --vcpu is required with --resize#{RESET}"
exit 1
end
payload = { vcpu: options[:vcpu] }
api_request("/services/#{options[:resize]}", method: 'PATCH', data: payload, keys: keys)
ram = options[:vcpu] * 2
puts "#{GREEN}Service resized to #{options[:vcpu]} vCPU, #{ram} GB RAM#{RESET}"
return
end
if options[:snapshot_service]
payload = {}
payload[:name] = options[:snapshot_name] if options[:snapshot_name]
@ -826,6 +839,7 @@ def main
sleep: nil,
wake: nil,
destroy: nil,
resize: nil,
execute: nil,
dump_bootstrap: nil,
dump_file: nil,
@ -938,6 +952,9 @@ def main
when '--destroy'
i += 1
options[:destroy] = ARGV[i]
when '--resize'
i += 1
options[:resize] = ARGV[i]
when '--execute'
i += 1
options[:execute] = ARGV[i]
@ -1073,6 +1090,7 @@ def main
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires -v)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

13
un.rs
View file

@ -571,6 +571,7 @@ fn cmd_service(
sleep: Option<&str>,
wake: Option<&str>,
destroy: Option<&str>,
resize: Option<&str>,
execute: Option<&str>,
command: Option<&str>,
dump_bootstrap: Option<&str>,
@ -632,6 +633,17 @@ fn cmd_service(
return;
}
if let Some(id) = resize {
let v = vcpu.unwrap_or_else(|| {
eprintln!("{}Error: --resize requires -v <vcpu>{}", RED, RESET);
process::exit(1);
});
let json = format!(r#"{{"vcpu":{}}}"#, v);
api_request(&format!("/services/{}", id), "PATCH", Some(&json), public_key, secret_key);
println!("{}Service resized to {} vCPU, {} GB RAM{}", GREEN, v, v * 2, RESET);
return;
}
if let Some(id) = execute {
let cmd = command.unwrap_or("");
let json = format!(r#"{{"command":"{}"}}"#, escape_json(cmd));
@ -994,6 +1006,7 @@ fn main() {
args.iter().position(|x| x == "--freeze").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--unfreeze").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--destroy").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--resize").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--execute").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--command").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
args.iter().position(|x| x == "--dump-bootstrap").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),

85
un.scm
View file

@ -211,6 +211,38 @@
(exit 1))
output))
(define (curl-patch api-key endpoint json-data)
(let* ((tmp-file (write-temp-file json-data))
(keys (get-api-keys))
(public-key (car keys))
(secret-key (cadr keys))
(auth-headers (build-auth-headers public-key secret-key "PATCH" endpoint json-data))
(cmd (string-append "curl -s -X PATCH https://api.unsandbox.com" endpoint
" -H 'Content-Type: application/json' "
(string-join auth-headers " ")
" -d @" tmp-file))
(port (open-input-pipe cmd))
(output (let loop ((lines '()))
(let ((line (read-line port)))
(if (eof-object? line)
(string-join (reverse lines) "\n")
(loop (cons line lines)))))))
(close-pipe port)
(delete-file tmp-file)
;; Check for clock drift errors
(when (and (string-contains output "timestamp")
(or (string-contains output "401")
(string-contains output "expired")
(string-contains output "invalid")))
(format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset)
(format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset)
(format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset)
(format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset)
(format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset)
(format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset)
(exit 1))
output))
(define (curl-post-portal api-key endpoint json-data)
(let* ((tmp-file (write-temp-file json-data))
(keys (get-api-keys))
@ -434,7 +466,7 @@
(display response)
(newline))))))
(define (service-cmd action id name ports bootstrap bootstrap-file type input-files env-vars env-file)
(define (service-cmd action id name ports bootstrap bootstrap-file type input-files env-vars env-file vcpu)
(let ((api-key (get-api-key)))
(cond
((equal? action "list")
@ -455,6 +487,15 @@
((equal? action "destroy")
(curl-delete api-key (format #f "/services/~a" id))
(format #t "~aService destroyed: ~a~a\n" green id reset))
((equal? action "resize")
(if (and vcpu (>= vcpu 1) (<= vcpu 8))
(let* ((json (format #f "{\"vcpu\":~a}" vcpu))
(ram (* vcpu 2)))
(curl-patch api-key (format #f "/services/~a" id) json)
(format #t "~aService resized to ~a vCPU, ~a GB RAM~a\n" green vcpu ram reset))
(begin
(format (current-error-port) "~aError: --resize requires --vcpu N (1-8)~a\n" red reset)
(exit 1))))
((equal? action "env-status")
(service-env-status api-key id))
((equal? action "env-set")
@ -568,23 +609,37 @@
((equal? (car args) "service")
(cond
((and (> (length args) 1) (equal? (cadr args) "--list"))
(service-cmd "list" #f #f #f #f #f #f '() '() #f))
(service-cmd "list" #f #f #f #f #f #f '() '() #f #f))
((and (> (length args) 2) (equal? (cadr args) "--info"))
(service-cmd "info" (caddr args) #f #f #f #f #f '() '() #f))
(service-cmd "info" (caddr args) #f #f #f #f #f '() '() #f #f))
((and (> (length args) 2) (equal? (cadr args) "--logs"))
(service-cmd "logs" (caddr args) #f #f #f #f #f '() '() #f))
(service-cmd "logs" (caddr args) #f #f #f #f #f '() '() #f #f))
((and (> (length args) 2) (equal? (cadr args) "--freeze"))
(service-cmd "sleep" (caddr args) #f #f #f #f #f '() '() #f))
(service-cmd "sleep" (caddr args) #f #f #f #f #f '() '() #f #f))
((and (> (length args) 2) (equal? (cadr args) "--unfreeze"))
(service-cmd "wake" (caddr args) #f #f #f #f #f '() '() #f))
(service-cmd "wake" (caddr args) #f #f #f #f #f '() '() #f #f))
((and (> (length args) 2) (equal? (cadr args) "--destroy"))
(service-cmd "destroy" (caddr args) #f #f #f #f #f '() '() #f))
(service-cmd "destroy" (caddr args) #f #f #f #f #f '() '() #f #f))
((and (> (length args) 2) (equal? (cadr args) "--resize"))
;; Parse --resize ID -v N
(let* ((resize-id (caddr args))
(rest-args (cdddr args))
(vcpu-val #f))
;; Look for -v or --vcpu
(let loop ((args rest-args))
(when (pair? args)
(cond
((and (or (equal? (car args) "-v") (equal? (car args) "--vcpu")) (pair? (cdr args)))
(set! vcpu-val (string->number (cadr args)))
(loop (cddr args)))
(else (loop (cdr args))))))
(service-cmd "resize" resize-id #f #f #f #f #f '() '() #f vcpu-val)))
((and (> (length args) 3) (equal? (cadr args) "--execute"))
(service-cmd "execute" (caddr args) #f #f (list-ref args 3) #f #f '() '() #f))
(service-cmd "execute" (caddr args) #f #f (list-ref args 3) #f #f '() '() #f #f))
((and (> (length args) 3) (equal? (cadr args) "--dump-bootstrap"))
(service-cmd "dump-bootstrap" (caddr args) #f #f #f #f (list-ref args 3) '() '() #f))
(service-cmd "dump-bootstrap" (caddr args) #f #f #f #f (list-ref args 3) '() '() #f #f))
((and (> (length args) 2) (equal? (cadr args) "--dump-bootstrap"))
(service-cmd "dump-bootstrap" (caddr args) #f #f #f #f #f '() '() #f))
(service-cmd "dump-bootstrap" (caddr args) #f #f #f #f #f '() '() #f #f))
;; Service env subcommand: service env <action> <id> [options]
((and (> (length args) 1) (equal? (cadr args) "env"))
(if (< (length args) 4)
@ -596,12 +651,12 @@
(rest-args (if (> (length args) 4) (list-tail args 4) '())))
(cond
((equal? env-action "status")
(service-cmd "env-status" service-id #f #f #f #f #f '() '() #f))
(service-cmd "env-status" service-id #f #f #f #f #f '() '() #f #f))
((equal? env-action "set")
;; Parse -e and --env-file from rest-args
(let loop ((args rest-args) (env-vars '()) (env-file #f))
(if (null? args)
(service-cmd "env-set" service-id #f #f #f #f #f '() env-vars env-file)
(service-cmd "env-set" service-id #f #f #f #f #f '() env-vars env-file #f)
(cond
((and (equal? (car args) "-e") (pair? (cdr args)))
(loop (cddr args) (cons (cadr args) env-vars) env-file))
@ -609,9 +664,9 @@
(loop (cddr args) env-vars (cadr args)))
(else (loop (cdr args) env-vars env-file))))))
((equal? env-action "export")
(service-cmd "env-export" service-id #f #f #f #f #f '() '() #f))
(service-cmd "env-export" service-id #f #f #f #f #f '() '() #f #f))
((equal? env-action "delete")
(service-cmd "env-delete" service-id #f #f #f #f #f '() '() #f))
(service-cmd "env-delete" service-id #f #f #f #f #f '() '() #f #f))
(else
(format (current-error-port) "~aUnknown env action: ~a~a\n" red env-action reset)
(exit 1))))))
@ -650,7 +705,7 @@
((equal? (car args) "-f")
(loop (cddr args))) ; skip -f, already parsed
(else (loop (cdr args))))))
(service-cmd "create" #f name ports bootstrap bootstrap-file type input-files env-vars env-file)))
(service-cmd "create" #f name ports bootstrap bootstrap-file type input-files env-vars env-file #f)))
(else
(display "Error: Invalid service command\n" (current-error-port))
(exit 1))))

18
un.sh
View file

@ -773,6 +773,7 @@ cmd_service() {
local sleep=""
local wake=""
local destroy=""
local resize=""
local execute=""
local command=""
local network=""
@ -853,6 +854,10 @@ cmd_service() {
destroy="$2"
shift 2
;;
--resize)
resize="$2"
shift 2
;;
--execute)
execute="$2"
shift 2
@ -982,6 +987,18 @@ cmd_service() {
return
fi
if [[ -n "$resize" ]]; then
if [[ -z "$vcpu" ]]; then
echo -e "${RED}Error: --vcpu is required with --resize${RESET}" >&2
exit 1
fi
local payload=$(jq -n --argjson v "$vcpu" '{vcpu: $v}')
api_request "/services/$resize" "PATCH" "$payload" "$api_key" > /dev/null
local ram=$((vcpu * 2))
echo -e "${GREEN}Service resized to $vcpu vCPU, $ram GB RAM${RESET}"
return
fi
if [[ -n "$execute" ]]; then
local payload=$(jq -n --arg cmd "$command" '{command: $cmd}')
local result=$(api_request "/services/$execute/execute" "POST" "$payload" "$api_key")
@ -1418,6 +1435,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires -v)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

17
un.tcl
View file

@ -675,6 +675,7 @@ proc cmd_service {args} {
set sleep_id ""
set wake_id ""
set destroy_id ""
set resize_id ""
set dump_bootstrap_id ""
set dump_file ""
set name ""
@ -734,6 +735,10 @@ proc cmd_service {args} {
incr i
set destroy_id [lindex $args $i]
}
--resize {
incr i
set resize_id [lindex $args $i]
}
--dump-bootstrap {
incr i
set dump_bootstrap_id [lindex $args $i]
@ -842,6 +847,18 @@ proc cmd_service {args} {
return
}
if {$resize_id ne ""} {
if {$vcpu < 1 || $vcpu > 8} {
puts stderr "${::RED}Error: --resize requires --vcpu N (1-8)${::RESET}"
exit 1
}
set payload [list vcpu $vcpu]
api_request "/services/$resize_id" "PATCH" $payload $public_key $secret_key
set ram [expr {$vcpu * 2}]
puts "${::GREEN}Service resized to $vcpu vCPU, $ram GB RAM${::RESET}"
return
}
if {$dump_bootstrap_id ne ""} {
puts stderr "Fetching bootstrap script from $dump_bootstrap_id..."
set payload [list command [::json::write string "cat /tmp/bootstrap.sh"]]

17
un.ts
View file

@ -109,6 +109,7 @@ interface Args {
sleep: string | null;
wake: string | null;
destroy: string | null;
resize: string | null;
execute: string | null;
command_arg: string | null;
extend: boolean;
@ -626,6 +627,17 @@ async function cmdService(args: Args): Promise<void> {
return;
}
if (args.resize) {
if (!args.vcpu) {
console.error(`${RED}Error: --vcpu required with --resize${RESET}`);
process.exit(1);
}
const payload = { vcpu: args.vcpu };
await apiRequest(`/services/${args.resize}`, "PATCH", payload, keys);
console.log(`${GREEN}Service resized to ${args.vcpu} vCPU, ${args.vcpu * 2}GB RAM${RESET}`);
return;
}
if (args.execute) {
const payload = { command: args.command_arg };
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
@ -813,6 +825,7 @@ function parseArgs(argv: string[]): Args {
sleep: null,
wake: null,
destroy: null,
resize: null,
execute: null,
command_arg: null,
dumpBootstrap: null,
@ -922,6 +935,9 @@ function parseArgs(argv: string[]): Args {
} else if (arg === '--destroy' && i + 1 < argv.length) {
args.destroy = argv[++i];
i++;
} else if (arg === '--resize' && i + 1 < argv.length) {
args.resize = argv[++i];
i++;
} else if (arg === '--execute' && i + 1 < argv.length) {
args.execute = argv[++i];
i++;
@ -1007,6 +1023,7 @@ Service options:
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires -v)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script

22
un.v
View file

@ -410,7 +410,7 @@ fn cmd_session(list bool, kill string, shell string, network string, vcpu int, t
println(exec_curl(cmd))
}
fn cmd_service(name string, ports string, service_type string, bootstrap string, bootstrap_file string, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, execute string, command string, dump_bootstrap string, dump_file string, network string, vcpu int, input_files []string, svc_envs []string, svc_env_file string, api_key string) {
fn cmd_service(name string, ports string, service_type string, bootstrap string, bootstrap_file string, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, resize string, execute string, command string, dump_bootstrap string, dump_file string, network string, vcpu int, input_files []string, svc_envs []string, svc_env_file string, api_key string) {
pub_key := get_public_key()
secret_key := get_secret_key()
@ -459,6 +459,19 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
return
}
if resize != '' {
if vcpu < 1 || vcpu > 8 {
eprintln('${red}Error: --resize requires --vcpu N (1-8)${reset}')
exit(1)
}
json := '{"vcpu":${vcpu}}'
cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:PATCH:/services/${resize}:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X PATCH '${api_base}/services/${resize}' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\""
exec_curl(cmd)
ram := vcpu * 2
println('${green}Service resized to ${vcpu} vCPU, ${ram} GB RAM${reset}')
return
}
if execute != '' {
json := '{"command":"${escape_json(command)}"}'
cmd := "BODY='${json}'; TIMESTAMP=\$(date +%s); MESSAGE=\"\$TIMESTAMP:POST:/services/${execute}/execute:\$BODY\"; SIGNATURE=\$(echo -n \"\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \$TIMESTAMP\" -H \"X-Signature: \$SIGNATURE\" -d \"\$BODY\""
@ -667,6 +680,7 @@ fn main() {
mut sleep := ''
mut wake := ''
mut destroy := ''
mut resize := ''
mut execute := ''
mut command := ''
mut dump_bootstrap := ''
@ -736,6 +750,10 @@ fn main() {
i++
destroy = os.args[i]
}
'--resize' {
i++
resize = os.args[i]
}
'--execute' {
i++
execute = os.args[i]
@ -793,7 +811,7 @@ fn main() {
return
}
cmd_service(name, ports, service_type, bootstrap, bootstrap_file, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network,
cmd_service(name, ports, service_type, bootstrap, bootstrap_file, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network,
vcpu, input_files, svc_envs, svc_env_file, api_key)
return
}

37
un.zig
View file

@ -397,6 +397,8 @@ pub fn main() !u8 {
var command: ?[]const u8 = null;
var dump_bootstrap: ?[]const u8 = null;
var dump_file: ?[]const u8 = null;
var resize: ?[]const u8 = null;
var vcpu: i32 = 0;
var input_files = std.ArrayList([]const u8).init(allocator);
defer input_files.deinit();
var svc_envs = std.ArrayList([]const u8).init(allocator);
@ -444,6 +446,12 @@ pub fn main() !u8 {
} else if (mem.eql(u8, args[i], "--dump-file") and i + 1 < args.len) {
i += 1;
dump_file = args[i];
} else if (mem.eql(u8, args[i], "--resize") and i + 1 < args.len) {
i += 1;
resize = args[i];
} else if (mem.eql(u8, args[i], "-v") and i + 1 < args.len) {
i += 1;
vcpu = std.fmt.parseInt(i32, args[i], 10) catch 0;
} else if (mem.eql(u8, args[i], "-e") and i + 1 < args.len) {
i += 1;
try svc_envs.append(args[i]);
@ -552,6 +560,35 @@ pub fn main() !u8 {
std.debug.print("\x1b[31mError: Failed to fetch bootstrap (service not running or no bootstrap file)\x1b[0m\n", .{});
return 1;
}
} else if (resize) |resize_id| {
// Validate vcpu
if (vcpu < 1 or vcpu > 8) {
std.debug.print("{s}Error: --resize requires -v N (1-8){s}\n", .{ RED, RESET });
return 1;
}
// Build JSON body
var vcpu_buf: [16]u8 = undefined;
const vcpu_str = std.fmt.bufPrint(&vcpu_buf, "{d}", .{vcpu}) catch "0";
const json = try std.fmt.allocPrint(allocator, "{{\"vcpu\":{s}}}", .{vcpu_str});
defer allocator.free(json);
// Build path
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{resize_id});
defer allocator.free(path);
// Build auth headers
const auth_headers = try buildAuthCmd(allocator, "PATCH", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
// Execute PATCH request
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X PATCH '{s}/services/{s}' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, resize_id, auth_headers, json });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
// Calculate RAM
const ram = vcpu * 2;
std.debug.print("\n{s}Service resized to {d} vCPU, {d} GB RAM{s}\n", .{ GREEN, vcpu, ram, RESET });
} else if (name) |n| {
var json_buf: [65536]u8 = undefined;
var json_stream = std.io.fixedBufferStream(&json_buf);