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:
parent
87397949d1
commit
c219d6e550
38 changed files with 1214 additions and 45 deletions
55
un.awk
55
un.awk
|
|
@ -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
29
un.clj
|
|
@ -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
56
un.cob
|
|
@ -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
42
un.cpp
|
|
@ -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
21
un.cr
|
|
@ -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
26
un.d
|
|
@ -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
20
un.dart
|
|
@ -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
35
un.erl
|
|
@ -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
42
un.ex
|
|
@ -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
41
un.f90
|
|
@ -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; ', &
|
||||
|
|
|
|||
66
un.forth
66
un.forth
|
|
@ -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
70
un.fs
|
|
@ -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
16
un.go
|
|
@ -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":
|
||||
|
|
|
|||
76
un.groovy
76
un.groovy
|
|
@ -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
30
un.hs
|
|
@ -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
54
un.jl
|
|
@ -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
16
un.js
|
|
@ -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
53
un.kt
|
|
@ -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
32
un.lisp
|
|
@ -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
26
un.lua
|
|
@ -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
22
un.m
|
|
@ -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
29
un.ml
|
|
@ -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
27
un.nim
|
|
@ -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
17
un.php
|
|
@ -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
16
un.pl
|
|
@ -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
27
un.pro
|
|
@ -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
31
un.ps1
|
|
@ -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
14
un.py
|
|
@ -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
19
un.r
|
|
@ -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
24
un.raku
|
|
@ -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
18
un.rb
|
|
@ -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
13
un.rs
|
|
@ -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
85
un.scm
|
|
@ -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
18
un.sh
|
|
@ -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
17
un.tcl
|
|
@ -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
17
un.ts
|
|
@ -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
22
un.v
|
|
@ -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
37
un.zig
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue