diff --git a/Un.cs b/Un.cs index ca2a741..ea908ac 100644 --- a/Un.cs +++ b/Un.cs @@ -406,6 +406,62 @@ class Un return; } + if (args.ServiceExecute != null) + { + var payload = new Dictionary + { + ["command"] = args.ServiceCommand + }; + var result = ApiRequest($"/services/{args.ServiceExecute}/execute", "POST", payload, apiKey); + if (result.ContainsKey("stdout") && !string.IsNullOrEmpty((string)result["stdout"])) + { + Console.Write($"{BLUE}{result["stdout"]}{RESET}"); + } + if (result.ContainsKey("stderr") && !string.IsNullOrEmpty((string)result["stderr"])) + { + Console.Error.Write($"{RED}{result["stderr"]}{RESET}"); + } + return; + } + + if (args.ServiceDumpBootstrap != null) + { + Console.Error.WriteLine($"Fetching bootstrap script from {args.ServiceDumpBootstrap}..."); + var payload = new Dictionary + { + ["command"] = "cat /tmp/bootstrap.sh" + }; + var result = ApiRequest($"/services/{args.ServiceDumpBootstrap}/execute", "POST", payload, apiKey); + + var bootstrap = result.ContainsKey("stdout") ? (string)result["stdout"] : null; + if (!string.IsNullOrEmpty(bootstrap)) + { + if (args.ServiceDumpFile != null) + { + try + { + File.WriteAllText(args.ServiceDumpFile, bootstrap); + Console.WriteLine($"Bootstrap saved to {args.ServiceDumpFile}"); + } + catch (Exception e) + { + Console.Error.WriteLine($"{RED}Error: Could not write to {args.ServiceDumpFile}: {e.Message}{RESET}"); + Environment.Exit(1); + } + } + else + { + Console.Write(bootstrap); + } + } + else + { + Console.Error.WriteLine($"{RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file){RESET}"); + Environment.Exit(1); + } + return; + } + if (args.ServiceName != null) { var payload = new Dictionary @@ -770,6 +826,10 @@ class Un public string ServiceWake = null; public string ServiceDestroy = null; public string ServiceType = null; + public string ServiceExecute = null; + public string ServiceCommand = null; + public string ServiceDumpBootstrap = null; + public string ServiceDumpFile = null; public bool KeyExtend = false; } @@ -803,9 +863,13 @@ class Un else if (arg == "--info") result.ServiceInfo = args[++i]; else if (arg == "--logs") result.ServiceLogs = args[++i]; else if (arg == "--tail") result.ServiceTail = args[++i]; - else if (arg == "--sleep") result.ServiceSleep = args[++i]; - else if (arg == "--wake") result.ServiceWake = args[++i]; + else if (arg == "--freeze") result.ServiceSleep = args[++i]; + else if (arg == "--unfreeze") result.ServiceWake = args[++i]; else if (arg == "--destroy") result.ServiceDestroy = args[++i]; + else if (arg == "--execute") result.ServiceExecute = args[++i]; + else if (arg == "--command") result.ServiceCommand = args[++i]; + else if (arg == "--dump-bootstrap") result.ServiceDumpBootstrap = args[++i]; + else if (arg == "--dump-file") result.ServiceDumpFile = args[++i]; else if (arg == "--extend") result.KeyExtend = true; else if (!arg.StartsWith("-")) result.SourceFile = arg; } @@ -842,9 +906,13 @@ Service options: --info ID Get service details --logs ID Get all logs --tail ID Get last 9000 lines - --sleep ID Freeze service - --wake ID Unfreeze service + --freeze ID Freeze service + --unfreeze ID Unfreeze service --destroy ID Destroy service + --execute ID Execute command in service + --command CMD Command to execute (with --execute) + --dump-bootstrap ID Dump bootstrap script + --dump-file FILE File to save bootstrap (with --dump-bootstrap) Key options: --extend Open browser to extend expired key"); diff --git a/Un.java b/Un.java index bc6837f..7d6dbc2 100644 --- a/Un.java +++ b/Un.java @@ -274,6 +274,49 @@ public class Un { return; } + if (args.serviceExecute != null) { + Map payload = new HashMap<>(); + payload.put("command", args.serviceCommand); + Map result = apiRequest("/services/" + args.serviceExecute + "/execute", "POST", payload, apiKey); + String stdout = (String) result.get("stdout"); + String stderr = (String) result.get("stderr"); + if (stdout != null && !stdout.isEmpty()) { + System.out.print(BLUE + stdout + RESET); + } + if (stderr != null && !stderr.isEmpty()) { + System.err.print(RED + stderr + RESET); + } + return; + } + + if (args.serviceDumpBootstrap != null) { + System.err.println("Fetching bootstrap script from " + args.serviceDumpBootstrap + "..."); + Map payload = new HashMap<>(); + payload.put("command", "cat /tmp/bootstrap.sh"); + Map result = apiRequest("/services/" + args.serviceDumpBootstrap + "/execute", "POST", payload, apiKey); + + String bootstrap = (String) result.get("stdout"); + if (bootstrap != null && !bootstrap.isEmpty()) { + if (args.serviceDumpFile != null) { + try { + java.nio.file.Path path = java.nio.file.Paths.get(args.serviceDumpFile); + java.nio.file.Files.write(path, bootstrap.getBytes()); + path.toFile().setExecutable(true, false); + System.out.println("Bootstrap saved to " + args.serviceDumpFile); + } catch (Exception e) { + System.err.println(RED + "Error: Could not write to " + args.serviceDumpFile + ": " + e.getMessage() + RESET); + System.exit(1); + } + } else { + System.out.print(bootstrap); + } + } else { + System.err.println(RED + "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" + RESET); + System.exit(1); + } + return; + } + if (args.serviceName != null) { Map payload = new HashMap<>(); payload.put("name", args.serviceName); @@ -667,6 +710,10 @@ public class Un { String serviceSleep = null; String serviceWake = null; String serviceDestroy = null; + String serviceExecute = null; + String serviceCommand = null; + String serviceDumpBootstrap = null; + String serviceDumpFile = null; // Key args boolean keyExtend = false; @@ -717,12 +764,20 @@ public class Un { result.serviceLogs = args[++i]; } else if (arg.equals("--tail")) { result.serviceTail = args[++i]; - } else if (arg.equals("--sleep")) { + } else if (arg.equals("--freeze")) { result.serviceSleep = args[++i]; - } else if (arg.equals("--wake")) { + } else if (arg.equals("--unfreeze")) { result.serviceWake = args[++i]; } else if (arg.equals("--destroy")) { result.serviceDestroy = args[++i]; + } else if (arg.equals("--execute")) { + result.serviceExecute = args[++i]; + } else if (arg.equals("--command")) { + result.serviceCommand = args[++i]; + } else if (arg.equals("--dump-bootstrap")) { + result.serviceDumpBootstrap = args[++i]; + } else if (arg.equals("--dump-file")) { + result.serviceDumpFile = args[++i]; } else if (arg.equals("--extend")) { result.keyExtend = true; } else if (!arg.startsWith("-")) { @@ -761,9 +816,13 @@ public class Un { System.out.println(" --info ID Get service details"); System.out.println(" --logs ID Get all logs"); System.out.println(" --tail ID Get last 9000 lines"); - System.out.println(" --sleep ID Freeze service"); - System.out.println(" --wake ID Unfreeze service"); + System.out.println(" --freeze ID Freeze service"); + System.out.println(" --unfreeze ID Unfreeze service"); System.out.println(" --destroy ID Destroy service"); + System.out.println(" --execute ID Execute command in service"); + System.out.println(" --command CMD Command to execute (with --execute)"); + System.out.println(" --dump-bootstrap ID Dump bootstrap script"); + System.out.println(" --dump-file FILE File to save bootstrap (with --dump-bootstrap)"); System.out.println(); System.out.println("Key options:"); System.out.println(" --extend Open browser to extend key"); diff --git a/un.awk b/un.awk index 1a07220..c1b220a 100644 --- a/un.awk +++ b/un.awk @@ -188,6 +188,47 @@ function service_destroy(id) { print GREEN "Service destroyed: " id RESET } +function service_dump_bootstrap(id, dump_file) { + api_key = get_api_key() + print "Fetching bootstrap script from " id "..." > "/dev/stderr" + + # Build the curl command to execute on the service + cmd = "curl -s -X POST '" API_BASE "/services/" id "/execute' " \ + "-H 'Content-Type: application/json' " \ + "-H 'Authorization: Bearer " api_key "' " \ + "-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'" + + response = "" + while ((cmd | getline line) > 0) { + response = response line + } + close(cmd) + + # Parse stdout from response + if (match(response, /"stdout":"([^"]*)"/, arr)) { + stdout = arr[1] + # Unescape JSON + gsub(/\\n/, "\n", stdout) + gsub(/\\t/, "\t", stdout) + gsub(/\\"/, "\"", stdout) + gsub(/\\\\/, "\\", stdout) + + if (dump_file != "") { + # Write to file + print stdout > dump_file + close(dump_file) + system("chmod 755 " dump_file) + print "Bootstrap saved to " dump_file + } else { + # Print to stdout + printf "%s", stdout + } + } else { + print RED "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" RESET > "/dev/stderr" + exit 1 + } +} + function service_create(name, ports, domains, service_type, bootstrap) { api_key = get_api_key() @@ -337,6 +378,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]" print " awk -f un.awk service --destroy ID" + print " awk -f un.awk service --dump-bootstrap ID [--dump-file FILE]" print "" print "Service options:" print " --name NAME Service name (required for --create)" @@ -344,6 +386,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 " --dump-bootstrap ID Dump bootstrap script from service" + print " --dump-file FILE Save bootstrap to file (with --dump-bootstrap)" print "" print "Requires: UNSANDBOX_API_KEY environment variable" } @@ -390,6 +434,12 @@ END { service_list() } else if (ARGC >= 4 && ARGV[2] == "--destroy") { service_destroy(ARGV[3]) + } else if (ARGC >= 4 && ARGV[2] == "--dump-bootstrap") { + dump_file = "" + if (ARGC >= 6 && ARGV[4] == "--dump-file") { + dump_file = ARGV[5] + } + service_dump_bootstrap(ARGV[3], dump_file) } else if (ARGV[2] == "--create") { # Parse service creation arguments name = "" diff --git a/un.clj b/un.clj index 1ef3a04..20f9726 100644 --- a/un.clj +++ b/un.clj @@ -201,6 +201,31 @@ :destroy (do (curl-delete api-key (str "/services/" sid)) (println (str green "Service destroyed: " sid reset))) + :execute (when (and sid bootstrap) + (let [json (str "{\"command\":\"" (escape-json bootstrap) "\"}") + response (curl-post api-key (str "/services/" sid "/execute") json) + stdout-val (extract-field "stdout" response)] + (when stdout-val + (print (str blue (unescape-json stdout-val) reset)) + (flush)))) + :dump-bootstrap (when sid + (binding [*out* *err*] + (println (str "Fetching bootstrap script from " sid "..."))) + (let [json "{\"command\":\"cat /tmp/bootstrap.sh\"}" + response (curl-post api-key (str "/services/" sid "/execute") json) + stdout-val (extract-field "stdout" response)] + (if stdout-val + (let [script (unescape-json stdout-val)] + (if service-type + (do + (spit service-type script) + (sh "chmod" "755" service-type) + (println (str "Bootstrap saved to " service-type))) + (print script))) + (do + (binding [*out* *err*] + (println (str red "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" reset))) + (System/exit 1))))) :create (when name (let [ports-json (if ports (str ",\"ports\":[" ports "]") "") bootstrap-json (if bootstrap (str ",\"bootstrap\":\"" (escape-json bootstrap) "\"") "") @@ -336,11 +361,11 @@ (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell :logs (second args) service-name service-ports service-bootstrap service-type key-extend mode) - (and (= mode :service) (= (first args) "--sleep")) + (and (= mode :service) (= (first args) "--freeze")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell :sleep (second args) service-name service-ports service-bootstrap service-type key-extend mode) - (and (= mode :service) (= (first args) "--wake")) + (and (= mode :service) (= (first args) "--unfreeze")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell :wake (second args) service-name service-ports service-bootstrap service-type key-extend mode) @@ -348,6 +373,18 @@ (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell :destroy (second args) service-name service-ports service-bootstrap service-type 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 + :execute (second args) service-name service-ports (nth args 2) service-type key-extend mode) + + (and (= mode :service) (= (first args) "--dump-bootstrap") (>= (count args) 3) (not (.startsWith (nth args 2) "-"))) + (recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell + :dump-bootstrap (second args) service-name service-ports service-bootstrap (nth args 2) key-extend mode) + + (and (= mode :service) (= (first args) "--dump-bootstrap")) + (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell + :dump-bootstrap (second args) service-name service-ports service-bootstrap service-type key-extend mode) + (and (= mode :service) (= (first args) "--name")) (recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell :create service-id (second args) service-ports service-bootstrap service-type key-extend mode) diff --git a/un.cob b/un.cob index 910fb05..bf102ad 100644 --- a/un.cob +++ b/un.cob @@ -197,22 +197,25 @@ ELSE IF WS-ARG2 = "--logs" ACCEPT WS-ID FROM ARGUMENT-VALUE PERFORM SERVICE-LOGS - ELSE IF WS-ARG2 = "--sleep" + ELSE IF WS-ARG2 = "--freeze" ACCEPT WS-ID FROM ARGUMENT-VALUE PERFORM SERVICE-SLEEP - ELSE IF WS-ARG2 = "--wake" + ELSE IF WS-ARG2 = "--unfreeze" ACCEPT WS-ID FROM ARGUMENT-VALUE PERFORM SERVICE-WAKE ELSE IF WS-ARG2 = "--destroy" ACCEPT WS-ID FROM ARGUMENT-VALUE PERFORM SERVICE-DESTROY + ELSE IF WS-ARG2 = "--dump-bootstrap" + ACCEPT WS-ID FROM ARGUMENT-VALUE + PERFORM SERVICE-DUMP-BOOTSTRAP ELSE IF WS-ARG2 = "--name" ACCEPT WS-NAME FROM ARGUMENT-VALUE PERFORM PARSE-SERVICE-CREATE-ARGS PERFORM SERVICE-CREATE ELSE DISPLAY "Error: Use --list, --info, --logs, " - "--sleep, --wake, --destroy, or --name" UPON SYSERR + "--freeze, --unfreeze, --destroy, --dump-bootstrap, or --name" UPON SYSERR MOVE 1 TO RETURN-CODE END-IF. @@ -375,6 +378,52 @@ CALL "SYSTEM" USING WS-CURL-CMD. + SERVICE-DUMP-BOOTSTRAP. + * Check if WS-ARG3 contains --dump-file argument + ACCEPT WS-ARG3 FROM ARGUMENT-VALUE. + MOVE SPACES TO WS-BOOTSTRAP. + IF WS-ARG3 = "--dump-file" + ACCEPT WS-BOOTSTRAP FROM ARGUMENT-VALUE + END-IF. + + STRING "echo 'Fetching bootstrap script from " + FUNCTION TRIM(WS-ID) "...' >&2; " + "RESP=$(curl -s -X POST " + "https://api.unsandbox.com/services/" + FUNCTION TRIM(WS-ID) "/execute " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY) + "' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}'); " + "STDOUT=$(echo \"$RESP\" | jq -r '.stdout // empty'); " + "if [ -n \"$STDOUT\" ]; then " + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + IF WS-BOOTSTRAP NOT = SPACES + STRING FUNCTION TRIM(WS-CURL-CMD) + "echo \"$STDOUT\" > '" + FUNCTION TRIM(WS-BOOTSTRAP) + "' && chmod 755 '" + FUNCTION TRIM(WS-BOOTSTRAP) + "' && echo 'Bootstrap saved to " + FUNCTION TRIM(WS-BOOTSTRAP) "'; " + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + ELSE + STRING FUNCTION TRIM(WS-CURL-CMD) + "echo \"$STDOUT\"; " + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING + END-IF. + + STRING FUNCTION TRIM(WS-CURL-CMD) + "else echo -e '\x1b[31mError: Failed to fetch " + "bootstrap\x1b[0m' >&2; exit 1; fi" + DELIMITED BY SIZE INTO WS-CURL-CMD + END-STRING. + + CALL "SYSTEM" USING WS-CURL-CMD. + PARSE-SERVICE-CREATE-ARGS. * Parse remaining arguments for service creation * This is a simplified parser that looks for specific flags diff --git a/un.cpp b/un.cpp index 749fc2a..4c923e9 100644 --- a/un.cpp +++ b/un.cpp @@ -218,7 +218,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, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& network, int vcpu, const string& api_key) { +void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, 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 string& api_key) { if (list) { string cmd = "curl -s -X GET '" + API_BASE + "/services' -H 'Authorization: Bearer " + api_key + "'"; cout << exec_curl(cmd) << endl; @@ -264,6 +264,93 @@ void cmd_service(const string& name, const string& ports, const string& type, co return; } + if (!execute.empty()) { + ostringstream json; + json << "{\"command\":\"" << escape_json(command) << "\"}"; + string cmd = "curl -s -X POST '" + API_BASE + "/services/" + execute + "/execute' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " + api_key + "' " + "-d '" + json.str() + "'"; + string result = exec_curl(cmd); + + size_t stdout_pos = result.find("\"stdout\":\""); + size_t stderr_pos = result.find("\"stderr\":\""); + + if (stdout_pos != string::npos) { + stdout_pos += 10; + size_t end = result.find("\"", stdout_pos); + while (end > 0 && result[end-1] == '\\') end = result.find("\"", end+1); + if (end != string::npos) { + string out = result.substr(stdout_pos, end - stdout_pos); + size_t pos = 0; + while ((pos = out.find("\\n", pos)) != string::npos) { + out.replace(pos, 2, "\n"); + } + cout << BLUE << out << RESET; + } + } + + if (stderr_pos != string::npos) { + stderr_pos += 10; + size_t end = result.find("\"", stderr_pos); + while (end > 0 && result[end-1] == '\\') end = result.find("\"", end+1); + if (end != string::npos) { + string err = result.substr(stderr_pos, end - stderr_pos); + size_t pos = 0; + while ((pos = err.find("\\n", pos)) != string::npos) { + err.replace(pos, 2, "\n"); + } + cerr << RED << err << RESET; + } + } + return; + } + + if (!dump_bootstrap.empty()) { + cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl; + string cmd = "curl -s -X POST '" + API_BASE + "/services/" + dump_bootstrap + "/execute' " + "-H 'Content-Type: application/json' " + "-H 'Authorization: Bearer " + api_key + "' " + "-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'"; + string result = exec_curl(cmd); + + size_t stdout_pos = result.find("\"stdout\":\""); + if (stdout_pos != string::npos) { + stdout_pos += 10; + size_t end = result.find("\"", stdout_pos); + while (end > 0 && result[end-1] == '\\') end = result.find("\"", end+1); + if (end != string::npos) { + string bootstrap_script = result.substr(stdout_pos, end - stdout_pos); + size_t pos = 0; + while ((pos = bootstrap_script.find("\\n", pos)) != string::npos) { + bootstrap_script.replace(pos, 2, "\n"); + } + + if (!dump_file.empty()) { + ofstream outfile(dump_file); + if (outfile) { + outfile << bootstrap_script; + outfile.close(); + chmod(dump_file.c_str(), 0755); + cout << "Bootstrap saved to " << dump_file << endl; + } else { + cerr << RED << "Error: Could not write to " << dump_file << RESET << endl; + exit(1); + } + } else { + cout << bootstrap_script; + } + } else { + cerr << RED << "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" << RESET << endl; + exit(1); + } + } else { + cerr << RED << "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" << RESET << endl; + exit(1); + } + return; + } + if (!name.empty()) { ostringstream json; json << "{\"name\":\"" << name << "\""; @@ -414,7 +501,7 @@ int main(int argc, char* argv[]) { if (cmd_type == "service") { string name, ports, type, bootstrap; bool list = false; - string info, logs, tail, sleep, wake, destroy, network; + string info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network; int vcpu = 0; for (int i = 2; i < argc; i++) { @@ -427,15 +514,19 @@ int main(int argc, char* argv[]) { else if (arg == "--info" && i+1 < argc) info = argv[++i]; else if (arg == "--logs" && i+1 < argc) logs = argv[++i]; else if (arg == "--tail" && i+1 < argc) tail = argv[++i]; - else if (arg == "--sleep" && i+1 < argc) sleep = argv[++i]; - else if (arg == "--wake" && i+1 < argc) wake = argv[++i]; + 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 == "--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]; + else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i]; else if (arg == "-n" && i+1 < argc) network = argv[++i]; else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]); else if (arg == "-k" && i+1 < argc) api_key = argv[++i]; } - cmd_service(name, ports, type, bootstrap, list, info, logs, tail, sleep, wake, destroy, network, vcpu, api_key); + cmd_service(name, ports, type, bootstrap, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network, vcpu, api_key); return 0; } diff --git a/un.cr b/un.cr index 5b5ff5f..894f47a 100644 --- a/un.cr +++ b/un.cr @@ -363,6 +363,39 @@ def cmd_service(args) return end + if execute_id = args[:execute]?.as?(String) + command = args[:command]?.as?(String) || "" + payload = JSON.parse({command: command}.to_json) + result = api_request("/services/#{execute_id}/execute", api_key, method: "POST", data: payload) + if stdout = result["stdout"]?.try(&.as_s?) + print BLUE, stdout, RESET + end + if stderr = result["stderr"]?.try(&.as_s?) + print RED, stderr, RESET + end + return + end + + if dump_id = args[:dump_bootstrap]?.as?(String) + STDERR.puts "Fetching bootstrap script from #{dump_id}..." + payload = JSON.parse({command: "cat /tmp/bootstrap.sh"}.to_json) + result = api_request("/services/#{dump_id}/execute", api_key, method: "POST", data: payload) + + if bootstrap = result["stdout"]?.try(&.as_s?) + if file_path = args[:dump_file]?.as?(String) + File.write(file_path, bootstrap) + File.chmod(file_path, 0o755) + puts "Bootstrap saved to #{file_path}" + else + print bootstrap + end + else + STDERR.puts "#{RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{RESET}" + exit 1 + end + return + end + # Create new service if name = args[:name]?.as?(String) payload = JSON.parse({name: name}.to_json) @@ -409,7 +442,7 @@ def cmd_service(args) return end - STDERR.puts "#{RED}Error: Use --list, --info, --logs, --sleep, --wake, --destroy, or --name to create#{RESET}" + STDERR.puts "#{RED}Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --name to create#{RESET}" exit 1 end @@ -430,6 +463,9 @@ def main sleep: nil, wake: nil, destroy: nil, + execute: nil, + dump_bootstrap: nil, + dump_file: nil, name: nil, ports: nil, domains: nil, @@ -451,9 +487,13 @@ def main opts.on("--kill=ID", "Kill session") { |id| args[:kill] = id } opts.on("--info=ID", "Get service info") { |id| args[:info] = id } opts.on("--logs=ID", "Get service logs") { |id| args[:logs] = id } - opts.on("--sleep=ID", "Sleep service") { |id| args[:sleep] = id } - opts.on("--wake=ID", "Wake service") { |id| args[:wake] = id } + opts.on("--freeze=ID", "Sleep service") { |id| args[:sleep] = id } + opts.on("--unfreeze=ID", "Wake service") { |id| args[:wake] = id } opts.on("--destroy=ID", "Destroy service") { |id| args[:destroy] = id } + opts.on("--execute=ID", "Execute command in service") { |id| args[:execute] = id } + opts.on("--command=CMD", "Command to execute (with --execute)") { |cmd| args[:command] = cmd } + opts.on("--dump-bootstrap=ID", "Dump bootstrap script") { |id| args[:dump_bootstrap] = id } + opts.on("--dump-file=FILE", "File to save bootstrap (with --dump-bootstrap)") { |file| args[:dump_file] = file } opts.on("--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 } diff --git a/un.d b/un.d index 7842b45..a3c2ea9 100644 --- a/un.d +++ b/un.d @@ -152,7 +152,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 type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string network, int vcpu, string apiKey) { +void cmdService(string name, string ports, string bootstrap, 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 apiKey) { if (list) { string cmd = format(`curl -s -X GET '%s/services' -H 'Authorization: Bearer %s'`, API_BASE, apiKey); writeln(execCurl(cmd)); @@ -198,6 +198,74 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l return; } + if (!execute.empty) { + string json = format(`{"command":"%s"}`, escapeJson(command)); + string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, execute, apiKey, json); + string result = execCurl(cmd); + + // Simple JSON parsing for stdout/stderr + import std.algorithm : findSplitAfter; + auto stdoutSearch = result.findSplitAfter(`"stdout":"`); + if (stdoutSearch[0].length > 0 && stdoutSearch[1].length > 0) { + auto stdoutEnd = stdoutSearch[1].findSplitAfter(`"`); + if (stdoutEnd[0].length > 1) { + string output = stdoutEnd[0][0..$-1]; + output = output.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\"); + write(output); + } + } + + auto stderrSearch = result.findSplitAfter(`"stderr":"`); + if (stderrSearch[0].length > 0 && stderrSearch[1].length > 0) { + auto stderrEnd = stderrSearch[1].findSplitAfter(`"`); + if (stderrEnd[0].length > 1) { + string errout = stderrEnd[0][0..$-1]; + errout = errout.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\"); + stderr.write(errout); + } + } + return; + } + + if (!dumpBootstrap.empty) { + stderr.writefln("Fetching bootstrap script from %s...", dumpBootstrap); + string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '{"command":"cat /tmp/bootstrap.sh"}'`, API_BASE, dumpBootstrap, apiKey); + string result = execCurl(cmd); + + import std.algorithm : findSplitAfter; + auto stdoutSearch = result.findSplitAfter(`"stdout":"`); + if (stdoutSearch[0].length > 0 && stdoutSearch[1].length > 0) { + auto stdoutEnd = stdoutSearch[1].findSplitAfter(`"`); + if (stdoutEnd[0].length > 1) { + string bootstrapScript = stdoutEnd[0][0..$-1]; + bootstrapScript = bootstrapScript.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\\"", "\"").replace("\\\\", "\\"); + + if (!dumpFile.empty) { + try { + std.file.write(dumpFile, bootstrapScript); + version(Posix) { + import core.sys.posix.sys.stat; + chmod(dumpFile.toStringz(), octal!755); + } + writefln("Bootstrap saved to %s", dumpFile); + } catch (Exception e) { + stderr.writefln("%sError: Could not write to %s: %s%s", RED, dumpFile, e.msg, RESET); + exit(1); + } + } else { + write(bootstrapScript); + } + } else { + stderr.writefln("%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s", RED, RESET); + exit(1); + } + } else { + stderr.writefln("%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s", RED, RESET); + exit(1); + } + return; + } + if (!name.empty) { string json = format(`{"name":"%s"`, name); if (!ports.empty) json ~= format(`,"ports":[%s]`, ports); @@ -379,7 +447,7 @@ int main(string[] args) { if (args[1] == "service") { string name, ports, bootstrap, type; bool list = false; - string info, logs, tail, sleep, wake, destroy, network; + string info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network; int vcpu = 0; for (size_t i = 2; i < args.length; i++) { @@ -391,15 +459,19 @@ int main(string[] args) { else if (args[i] == "--info" && i+1 < args.length) info = args[++i]; else if (args[i] == "--logs" && i+1 < args.length) logs = args[++i]; else if (args[i] == "--tail" && i+1 < args.length) tail = args[++i]; - else if (args[i] == "--sleep" && i+1 < args.length) sleep = args[++i]; - else if (args[i] == "--wake" && i+1 < args.length) wake = args[++i]; + 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] == "--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]; + else if (args[i] == "--dump-file" && i+1 < args.length) dumpFile = args[++i]; else if (args[i] == "-n" && i+1 < args.length) network = args[++i]; else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]); else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i]; } - cmdService(name, ports, bootstrap, type, list, info, logs, tail, sleep, wake, destroy, network, vcpu, apiKey); + cmdService(name, ports, bootstrap, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, apiKey); return 0; } diff --git a/un.dart b/un.dart index 1470b39..8e6831a 100644 --- a/un.dart +++ b/un.dart @@ -91,6 +91,10 @@ class Args { String? serviceSleep; String? serviceWake; String? serviceDestroy; + String? serviceExecute; + String? serviceCommand; + String? serviceDumpBootstrap; + String? serviceDumpFile; bool keyExtend = false; } @@ -318,6 +322,50 @@ Future cmdService(Args args) async { return; } + if (args.serviceExecute != null) { + final payload = { + 'command': args.serviceCommand, + }; + final result = await apiRequestCurl('/services/${args.serviceExecute}/execute', 'POST', jsonEncode(payload), apiKey); + final stdoutText = result['stdout'] as String?; + final stderrText = result['stderr'] as String?; + if (stdoutText != null && stdoutText.isNotEmpty) { + stdout.write('$blue$stdoutText$reset'); + } + if (stderrText != null && stderrText.isNotEmpty) { + stderr.write('$red$stderrText$reset'); + } + return; + } + + if (args.serviceDumpBootstrap != null) { + stderr.writeln('Fetching bootstrap script from ${args.serviceDumpBootstrap}...'); + final payload = { + 'command': 'cat /tmp/bootstrap.sh', + }; + final result = await apiRequestCurl('/services/${args.serviceDumpBootstrap}/execute', 'POST', jsonEncode(payload), apiKey); + + final bootstrap = result['stdout'] as String?; + if (bootstrap != null && bootstrap.isNotEmpty) { + if (args.serviceDumpFile != null) { + try { + await File(args.serviceDumpFile!).writeAsString(bootstrap); + await Process.run('chmod', ['755', args.serviceDumpFile!]); + print('Bootstrap saved to ${args.serviceDumpFile}'); + } catch (e) { + stderr.writeln('${red}Error: Could not write to ${args.serviceDumpFile}: $e$reset'); + exit(1); + } + } else { + stdout.write(bootstrap); + } + } else { + stderr.writeln('${red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)$reset'); + exit(1); + } + return; + } + if (args.serviceName != null) { final payload = { 'name': args.serviceName!, @@ -485,15 +533,27 @@ Args parseArgs(List argv) { case '--tail': args.serviceTail = argv[++i]; break; - case '--sleep': + case '--freeze': args.serviceSleep = argv[++i]; break; - case '--wake': + case '--unfreeze': args.serviceWake = argv[++i]; break; case '--destroy': args.serviceDestroy = argv[++i]; break; + case '--execute': + args.serviceExecute = argv[++i]; + break; + case '--command': + args.serviceCommand = argv[++i]; + break; + case '--dump-bootstrap': + args.serviceDumpBootstrap = argv[++i]; + break; + case '--dump-file': + args.serviceDumpFile = argv[++i]; + break; case '--extend': args.keyExtend = true; break; @@ -537,9 +597,13 @@ Service options: --info ID Get service details --logs ID Get all logs --tail ID Get last 9000 lines - --sleep ID Freeze service - --wake ID Unfreeze service + --freeze ID Freeze service + --unfreeze ID Unfreeze service --destroy ID Destroy service + --execute ID Execute command in service + --command CMD Command to execute (with --execute) + --dump-bootstrap ID Dump bootstrap script + --dump-file FILE File to save bootstrap (with --dump-bootstrap) Key options: --extend Open browser to extend key diff --git a/un.erl b/un.erl index 6fc0a5a..c164e47 100755 --- a/un.erl +++ b/un.erl @@ -128,14 +128,14 @@ service_command(["--logs", ServiceId | _]) -> Response = curl_get(ApiKey, "/services/" ++ ServiceId ++ "/logs"), io:format("~s~n", [Response]); -service_command(["--sleep", ServiceId | _]) -> +service_command(["--freeze", ServiceId | _]) -> ApiKey = get_api_key(), TmpFile = write_temp_file("{}"), _ = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/sleep", TmpFile), file:delete(TmpFile), io:format("\033[32mService sleeping: ~s\033[0m~n", [ServiceId]); -service_command(["--wake", ServiceId | _]) -> +service_command(["--unfreeze", ServiceId | _]) -> ApiKey = get_api_key(), TmpFile = write_temp_file("{}"), _ = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/wake", TmpFile), @@ -147,6 +147,49 @@ service_command(["--destroy", ServiceId | _]) -> _ = curl_delete(ApiKey, "/services/" ++ ServiceId), io:format("\033[32mService destroyed: ~s\033[0m~n", [ServiceId]); +service_command(["--execute", ServiceId, "--command", Command | _]) -> + ApiKey = get_api_key(), + Json = "{\"command\":\"" ++ escape_json(Command) ++ "\"}", + TmpFile = write_temp_file(Json), + Response = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/execute", TmpFile), + file:delete(TmpFile), + case extract_json_field(Response, "stdout") of + "" -> ok; + Stdout -> io:format("\033[34m~s\033[0m", [Stdout]) + end; + +service_command(["--dump-bootstrap", ServiceId, File | _]) -> + ApiKey = get_api_key(), + io:format(standard_error, "Fetching bootstrap script from ~s...~n", [ServiceId]), + Json = "{\"command\":\"cat /tmp/bootstrap.sh\"}", + TmpFile = write_temp_file(Json), + Response = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/execute", TmpFile), + file:delete(TmpFile), + case extract_json_field(Response, "stdout") of + "" -> + io:format(standard_error, "\033[31mError: Failed to fetch bootstrap (service not running or no bootstrap file)\033[0m~n"), + halt(1); + Script -> + file:write_file(File, Script), + os:cmd("chmod 755 " ++ File), + io:format("Bootstrap saved to ~s~n", [File]) + end; + +service_command(["--dump-bootstrap", ServiceId | _]) -> + ApiKey = get_api_key(), + io:format(standard_error, "Fetching bootstrap script from ~s...~n", [ServiceId]), + Json = "{\"command\":\"cat /tmp/bootstrap.sh\"}", + TmpFile = write_temp_file(Json), + Response = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/execute", TmpFile), + file:delete(TmpFile), + case extract_json_field(Response, "stdout") of + "" -> + io:format(standard_error, "\033[31mError: Failed to fetch bootstrap (service not running or no bootstrap file)\033[0m~n"), + halt(1); + Script -> + io:format("~s", [Script]) + end; + service_command(Args) -> case get_service_name(Args) of undefined -> diff --git a/un.ex b/un.ex index c103624..e63a5e2 100755 --- a/un.ex +++ b/un.ex @@ -166,13 +166,13 @@ defmodule Un do IO.puts(response) end - defp service_command(["--sleep", service_id | _]) do + defp service_command(["--freeze", service_id | _]) do api_key = get_api_key() curl_post(api_key, "/services/#{service_id}/sleep", "{}") IO.puts("#{@green}Service sleeping: #{service_id}#{@reset}") end - defp service_command(["--wake", service_id | _]) do + defp service_command(["--unfreeze", service_id | _]) do api_key = get_api_key() curl_post(api_key, "/services/#{service_id}/wake", "{}") IO.puts("#{@green}Service waking: #{service_id}#{@reset}") @@ -184,6 +184,49 @@ defmodule Un do IO.puts("#{@green}Service destroyed: #{service_id}#{@reset}") end + defp service_command(["--execute", service_id, "--command", command | _]) do + api_key = get_api_key() + json = "{\"command\":\"#{escape_json(command)}\"}" + response = curl_post(api_key, "/services/#{service_id}/execute", json) + + case extract_json_value(response, "stdout") do + nil -> :ok + stdout -> IO.write("#{@blue}#{stdout}#{@reset}") + end + end + + defp service_command(["--dump-bootstrap", service_id, file | _]) do + api_key = get_api_key() + IO.puts(:stderr, "Fetching bootstrap script from #{service_id}...") + json = "{\"command\":\"cat /tmp/bootstrap.sh\"}" + response = curl_post(api_key, "/services/#{service_id}/execute", json) + + case extract_json_value(response, "stdout") do + nil -> + IO.puts(:stderr, "#{@red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{@reset}") + System.halt(1) + script -> + File.write!(file, script) + System.cmd("chmod", ["755", file]) + IO.puts("Bootstrap saved to #{file}") + end + end + + defp service_command(["--dump-bootstrap", service_id | _]) do + api_key = get_api_key() + IO.puts(:stderr, "Fetching bootstrap script from #{service_id}...") + json = "{\"command\":\"cat /tmp/bootstrap.sh\"}" + response = curl_post(api_key, "/services/#{service_id}/execute", json) + + case extract_json_value(response, "stdout") do + nil -> + IO.puts(:stderr, "#{@red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)#{@reset}") + System.halt(1) + script -> + IO.write(script) + end + end + defp service_command(args) do name = get_opt(args, "--name", nil, nil) diff --git a/un.f90 b/un.f90 index 6a5f56f..722013e 100644 --- a/un.f90 +++ b/un.f90 @@ -248,12 +248,12 @@ contains if (i+1 <= command_argument_count()) then call get_command_argument(i+1, service_id) end if - else if (trim(arg) == '--sleep') then + else if (trim(arg) == '--freeze') then operation = 'sleep' if (i+1 <= command_argument_count()) then call get_command_argument(i+1, service_id) end if - else if (trim(arg) == '--wake') then + else if (trim(arg) == '--unfreeze') then operation = 'wake' if (i+1 <= command_argument_count()) then call get_command_argument(i+1, service_id) @@ -263,6 +263,16 @@ contains if (i+1 <= command_argument_count()) then call get_command_argument(i+1, service_id) end if + else if (trim(arg) == '--dump-bootstrap') then + operation = 'dump-bootstrap' + if (i+1 <= command_argument_count()) then + call get_command_argument(i+1, service_id) + end if + else if (trim(arg) == '--dump-file') then + operation = 'dump-file' + if (i+1 <= command_argument_count()) then + call get_command_argument(i+1, service_type) + end if end if end do @@ -314,8 +324,24 @@ contains '-H "Authorization: Bearer ', trim(api_key), '" >/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) == 'dump-bootstrap' .and. len_trim(service_id) > 0) then + write(full_cmd, '(20A)') & + 'echo "Fetching bootstrap script from ', trim(service_id), '..." >&2; ', & + 'RESP=$(curl -s -X POST https://api.unsandbox.com/services/', & + trim(service_id), '/execute ', & + '-H "Content-Type: application/json" ', & + '-H "Authorization: Bearer ', trim(api_key), '" ', & + '-d ''{"command":"cat /tmp/bootstrap.sh"}''); ', & + 'STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); ', & + 'if [ -n "$STDOUT" ]; then ', & + 'if [ -n "', trim(service_type), '" ]; then ', & + 'echo "$STDOUT" > "', trim(service_type), '" && chmod 755 "', trim(service_type), '" && ', & + 'echo "Bootstrap saved to ', trim(service_type), '"; ', & + 'else echo "$STDOUT"; fi; ', & + 'else echo -e "\x1b[31mError: Failed to fetch bootstrap\x1b[0m" >&2; exit 1; fi' + call execute_command_line(trim(full_cmd), wait=.true.) else - write(0, '(A)') 'Error: Use --list, --info, --logs, --sleep, --wake, or --destroy' + write(0, '(A)') 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --dump-bootstrap' stop 1 end if end subroutine handle_service diff --git a/un.forth b/un.forth index de7e697..0240f89 100644 --- a/un.forth +++ b/un.forth @@ -258,6 +258,43 @@ s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system ; +\ Service dump bootstrap +: service-dump-bootstrap ( service-id-addr service-id-len file-addr file-len -- ) + get-api-key + s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r + s" #!/bin/bash" r@ write-line throw + s" echo 'Fetching bootstrap script from " r@ write-file throw + 2over r@ write-file throw + s" ...' >&2" r@ write-line throw + s" RESP=$(curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw + 2over r@ write-file throw + s" /execute -H 'Content-Type: application/json' -H 'Authorization: Bearer " r@ write-file throw + get-api-key r@ write-file throw + s" ' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}')" r@ write-line throw + s" STDOUT=$(echo \"$RESP\" | jq -r '.stdout // empty')" r@ write-line throw + s" if [ -n \"$STDOUT\" ]; then" r@ write-line throw + 2dup 0 0 d= if + \ No file specified, print to stdout + 2drop + s" echo \"$STDOUT\"" r@ write-line throw + else + \ File specified, save to file + s" echo \"$STDOUT\" > '" r@ write-file throw + r@ write-file throw + s" ' && chmod 755 '" r@ write-file throw + 2dup r@ write-file throw + s" ' && echo 'Bootstrap saved to " r@ write-file throw + r@ write-file throw + s" '" r@ write-line throw + then + s" else" r@ write-line throw + s" echo -e '\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m' >&2" r@ write-line throw + s" exit 1" r@ write-line throw + s" fi" r@ write-line throw + r> close-file throw + s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system +; + \ Service create (requires --name, optional --ports, --domains, --type, --bootstrap) : service-create ( -- ) get-api-key @@ -394,7 +431,7 @@ \ Handle service subcommand : handle-service ( -- ) argc @ 3 < if - s" Error: Use --name (create), --list, --info, --logs, --sleep, --wake, or --destroy" type cr + s" Error: Use --name (create), --list, --info, --logs, --freeze, --unfreeze, or --destroy" type cr 1 (bye) then @@ -433,20 +470,20 @@ 0 (bye) then - 2dup s" --sleep" compare 0= if + 2dup s" --freeze" compare 0= if 2drop argc @ 4 < if - s" Error: --sleep requires service ID" type cr + s" Error: --freeze requires service ID" type cr 1 (bye) then 3 arg service-sleep 0 (bye) then - 2dup s" --wake" compare 0= if + 2dup s" --unfreeze" compare 0= if 2drop argc @ 4 < if - s" Error: --wake requires service ID" type cr + s" Error: --unfreeze requires service ID" type cr 1 (bye) then 3 arg service-wake @@ -463,8 +500,34 @@ 0 (bye) then + 2dup s" --dump-bootstrap" compare 0= if + 2drop + argc @ 4 < if + s" Error: --dump-bootstrap requires service ID" type cr + 1 (bye) + then + 3 arg + \ Check for --dump-file + argc @ 5 >= if + 4 arg 2dup s" --dump-file" compare 0= if + 2drop + argc @ 6 < if + s" Error: --dump-file requires filename" type cr + 1 (bye) + then + 5 arg + else + 2drop 0 0 + then + else + 0 0 + then + service-dump-bootstrap + 0 (bye) + then + 2drop - s" Error: Use --name (create), --list, --info, --logs, --sleep, --wake, or --destroy" type cr + s" Error: Use --name (create), --list, --info, --logs, --freeze, --unfreeze, --destroy, or --dump-bootstrap" type cr 1 (bye) ; diff --git a/un.fs b/un.fs index a1cbbb3..3ca8786 100644 --- a/un.fs +++ b/un.fs @@ -94,6 +94,10 @@ type Args = { mutable ServiceSleep: string option mutable ServiceWake: string option mutable ServiceDestroy: string option + mutable ServiceExecute: string option + mutable ServiceCommand: string option + mutable ServiceDumpBootstrap: string option + mutable ServiceDumpFile: string option mutable KeyExtend: bool } @@ -436,6 +440,37 @@ let cmdService (args: Args) = elif args.ServiceDestroy.IsSome then let result = apiRequest (sprintf "/services/%s" args.ServiceDestroy.Value) "DELETE" None apiKey printfn "%sService destroyed: %s%s" green args.ServiceDestroy.Value 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) apiKey + match result.TryFind "stdout" with + | Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) -> + printf "%s%s%s" blue (stdout.ToString()) reset + | _ -> () + match result.TryFind "stderr" with + | Some stderr when not (String.IsNullOrEmpty(stderr.ToString())) -> + eprintf "%s%s%s" red (stderr.ToString()) reset + | _ -> () + elif args.ServiceDumpBootstrap.IsSome then + eprintfn "Fetching bootstrap script from %s..." args.ServiceDumpBootstrap.Value + let payload = [("command", box "cat /tmp/bootstrap.sh")] + let result = apiRequest (sprintf "/services/%s/execute" args.ServiceDumpBootstrap.Value) "POST" (Some payload) apiKey + + match result.TryFind "stdout" with + | Some bootstrap when not (String.IsNullOrEmpty(bootstrap.ToString())) -> + let bootstrapText = bootstrap.ToString() + if args.ServiceDumpFile.IsSome then + try + File.WriteAllText(args.ServiceDumpFile.Value, bootstrapText) + printfn "Bootstrap saved to %s" args.ServiceDumpFile.Value + with ex -> + eprintfn "%sError: Could not write to %s: %s%s" red args.ServiceDumpFile.Value ex.Message reset + exit 1 + else + printf "%s" bootstrapText + | _ -> + eprintfn "%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s" red reset + exit 1 elif args.ServiceName.IsSome then let mutable payload = [("name", box args.ServiceName.Value)] if args.ServicePorts.IsSome then @@ -489,6 +524,10 @@ let parseArgs (argv: string[]) = ServiceSleep = None ServiceWake = None ServiceDestroy = None + ServiceExecute = None + ServiceCommand = None + ServiceDumpBootstrap = None + ServiceDumpFile = None KeyExtend = false } @@ -519,9 +558,13 @@ let parseArgs (argv: string[]) = | "--info" -> i <- i + 1; args.ServiceInfo <- Some argv.[i] | "--logs" -> i <- i + 1; args.ServiceLogs <- Some argv.[i] | "--tail" -> i <- i + 1; args.ServiceTail <- Some argv.[i] - | "--sleep" -> i <- i + 1; args.ServiceSleep <- Some argv.[i] - | "--wake" -> i <- i + 1; args.ServiceWake <- Some argv.[i] + | "--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] + | "--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] + | "--dump-file" -> i <- i + 1; args.ServiceDumpFile <- Some argv.[i] | "--extend" -> args.KeyExtend <- true | arg when not (arg.StartsWith("-")) -> args.SourceFile <- Some arg | _ -> () @@ -558,9 +601,13 @@ let printHelp () = printfn " --info ID Get service details" printfn " --logs ID Get all logs" printfn " --tail ID Get last 9000 lines" - printfn " --sleep ID Freeze service" - printfn " --wake ID Unfreeze service" + printfn " --freeze ID Freeze service" + printfn " --unfreeze ID Unfreeze service" printfn " --destroy ID Destroy service" + printfn " --execute ID Execute command in service" + printfn " --command CMD Command to execute (with --execute)" + printfn " --dump-bootstrap ID Dump bootstrap script" + printfn " --dump-file FILE File to save bootstrap (with --dump-bootstrap)" printfn "" printfn "Key options:" printfn " --extend Open browser to extend key" diff --git a/un.groovy b/un.groovy index e318b1d..6f22c42 100644 --- a/un.groovy +++ b/un.groovy @@ -85,6 +85,10 @@ class Args { String serviceSleep = null String serviceWake = null String serviceDestroy = null + String serviceExecute = null + String serviceCommand = null + String serviceDumpBootstrap = null + String serviceDumpFile = null Boolean keyExtend = false } @@ -408,6 +412,64 @@ def cmdService(args) { return } + if (args.serviceExecute) { + def json = """{"command":"${args.serviceCommand}"}""" + def output = apiRequest("/services/${args.serviceExecute}/execute", 'POST', json, apiKey) + def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/ + def stderrMatch = output =~ /"stderr":"((?:[^"\\\\]|\\\\.)*)"/ + + if (stdoutMatch.find()) { + def stdout = stdoutMatch.group(1) + .replace('\\n', '\n') + .replace('\\t', '\t') + .replace('\\"', '"') + .replace('\\\\', '\\') + print("${BLUE}${stdout}${RESET}") + } + + if (stderrMatch.find()) { + def stderr = stderrMatch.group(1) + .replace('\\n', '\n') + .replace('\\t', '\t') + .replace('\\"', '"') + .replace('\\\\', '\\') + System.err.print("${RED}${stderr}${RESET}") + } + return + } + + if (args.serviceDumpBootstrap) { + System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...") + def json = """{"command":"cat /tmp/bootstrap.sh"}""" + def output = apiRequest("/services/${args.serviceDumpBootstrap}/execute", 'POST', json, apiKey) + + def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/ + if (stdoutMatch.find()) { + def bootstrap = stdoutMatch.group(1) + .replace('\\n', '\n') + .replace('\\t', '\t') + .replace('\\"', '"') + .replace('\\\\', '\\') + + if (args.serviceDumpFile) { + try { + new File(args.serviceDumpFile).text = bootstrap + "chmod 755 ${args.serviceDumpFile}".execute().waitFor() + println("Bootstrap saved to ${args.serviceDumpFile}") + } catch (Exception e) { + System.err.println("${RED}Error: Could not write to ${args.serviceDumpFile}: ${e.message}${RESET}") + System.exit(1) + } + } else { + print(bootstrap) + } + } else { + System.err.println("${RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${RESET}") + System.exit(1) + } + return + } + if (args.serviceName) { def json = """{"name":"${args.serviceName}"""" if (args.servicePorts) { @@ -524,15 +586,27 @@ def parseArgs(argv) { case '--tail': args.serviceTail = argv[++i] break - case '--sleep': + case '--freeze': args.serviceSleep = argv[++i] break - case '--wake': + case '--unfreeze': args.serviceWake = argv[++i] break case '--destroy': args.serviceDestroy = argv[++i] break + case '--execute': + args.serviceExecute = argv[++i] + break + case '--command': + args.serviceCommand = argv[++i] + break + case '--dump-bootstrap': + args.serviceDumpBootstrap = argv[++i] + break + case '--dump-file': + args.serviceDumpFile = argv[++i] + break case '--extend': args.keyExtend = true break @@ -575,9 +649,13 @@ Service options: --info ID Get service details --logs ID Get all logs --tail ID Get last 9000 lines - --sleep ID Freeze service - --wake ID Unfreeze service + --freeze ID Freeze service + --unfreeze ID Unfreeze service --destroy ID Destroy service + --execute ID Execute command in service + --command CMD Command to execute (with --execute) + --dump-bootstrap ID Dump bootstrap script + --dump-file FILE File to save bootstrap (with --dump-bootstrap) Key options: --extend Open browser to extend key diff --git a/un.hs b/un.hs index 6b7048d..8a98d80 100644 --- a/un.hs +++ b/un.hs @@ -145,6 +145,7 @@ data ServiceOpts = ServiceOpts data ServiceAction = ServiceList | ServiceInfo String | ServiceLogs String | ServiceSleep String | ServiceWake String | ServiceDestroy String + | ServiceExecute String String | ServiceDumpBootstrap String (Maybe String) | ServiceCreate data KeyOpts = KeyOpts @@ -187,9 +188,12 @@ parseService args = return $ parseServiceArgs args defaultServiceOpts parseServiceArgs ("--list":rest) opts = parseServiceArgs rest opts { svcAction = ServiceList } parseServiceArgs ("--info":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceInfo id } parseServiceArgs ("--logs":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceLogs id } - parseServiceArgs ("--sleep":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceSleep id } - parseServiceArgs ("--wake":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceWake id } + 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 ("--execute":id:"--command":cmd:rest) opts = parseServiceArgs rest opts { svcAction = ServiceExecute id cmd } + parseServiceArgs ("--dump-bootstrap":id:file:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDumpBootstrap id (Just file) } + parseServiceArgs ("--dump-bootstrap":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDumpBootstrap id Nothing } parseServiceArgs ("--name":n:rest) opts = parseServiceArgs rest opts { svcName = Just n } parseServiceArgs ("--ports":p:rest) opts = parseServiceArgs rest opts { svcPorts = Just p } parseServiceArgs ("--type":t:rest) opts = parseServiceArgs rest opts { svcType = Just t } @@ -337,6 +341,28 @@ serviceCommand opts = do ServiceDestroy sid -> do (_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/services/" ++ sid) putStrLn $ green ++ "Service destroyed: " ++ sid ++ reset + ServiceExecute sid cmd -> do + let json = "{\"command\":\"" ++ escapeJSON cmd ++ "\"}" + (_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/execute") json + unless (null stdout) $ putStr $ blue ++ stdout ++ reset + ServiceDumpBootstrap sid maybeFile -> do + hPutStrLn stderr $ "Fetching bootstrap script from " ++ sid ++ "..." + let json = "{\"command\":\"cat /tmp/bootstrap.sh\"}" + (_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/execute") json + -- Extract stdout from JSON response + let bootstrapScript = extractJsonString stdout "stdout" + case bootstrapScript of + Just script | not (null script) -> do + case maybeFile of + Just file -> do + writeFile file script + perms <- getPermissions file + setPermissions file (setOwnerExecutable True perms) + putStrLn $ "Bootstrap saved to " ++ file + Nothing -> putStr script + _ -> do + hPutStrLn stderr $ red ++ "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" ++ reset + exitFailure ServiceCreate -> do case svcName opts of Nothing -> do diff --git a/un.jl b/un.jl index b4fc0ad..8b834e5 100755 --- a/un.jl +++ b/un.jl @@ -288,6 +288,34 @@ function cmd_service(args) return end + if args["dump-bootstrap"] !== nothing + println(stderr, "Fetching bootstrap script from $(args["dump-bootstrap"])...") + payload = Dict("command" => "cat /tmp/bootstrap.sh") + result = api_request("/services/$(args["dump-bootstrap"])/execute", api_key, method="POST", data=payload) + + if haskey(result, "stdout") && !isempty(result["stdout"]) + bootstrap = result["stdout"] + if args["dump-file"] !== nothing + # Write to file + try + write(args["dump-file"], bootstrap) + chmod(args["dump-file"], 0o755) + println("Bootstrap saved to $(args["dump-file"])") + catch e + println(stderr, "$(RED)Error: Could not write to $(args["dump-file"]): $e$(RESET)") + exit(1) + end + else + # Print to stdout + print(bootstrap) + end + else + println(stderr, "$(RED)Error: Failed to fetch bootstrap (service not running or no bootstrap file)$(RESET)") + exit(1) + end + return + end + # Create new service if args["name"] !== nothing payload = Dict("name" => args["name"]) @@ -332,7 +360,7 @@ function cmd_service(args) return end - println(stderr, "$(RED)Error: Use --name to create, or --list, --info, --logs, --sleep, --wake, --destroy$(RESET)") + println(stderr, "$(RED)Error: Use --name to create, or --list, --info, --logs, --freeze, --unfreeze, --destroy$(RESET)") exit(1) end @@ -550,12 +578,16 @@ function main() help = "Get service details" "--logs" help = "Get all logs" - "--sleep" + "--freeze" help = "Freeze service" - "--wake" + "--unfreeze" help = "Unfreeze service" "--destroy" help = "Destroy service" + "--dump-bootstrap" + help = "Dump bootstrap script from service" + "--dump-file" + help = "File to save bootstrap (with --dump-bootstrap)" "--api-key", "-k" help = "API key" end diff --git a/un.kt b/un.kt index 37d836a..ce74c39 100644 --- a/un.kt +++ b/un.kt @@ -94,6 +94,10 @@ data class Args( var serviceSleep: String? = null, var serviceWake: String? = null, var serviceDestroy: String? = null, + var serviceExecute: String? = null, + var serviceCommand: String? = null, + var serviceDumpBootstrap: String? = null, + var serviceDumpFile: String? = null, var keyExtend: Boolean = false ) @@ -301,6 +305,51 @@ fun cmdService(args: Args) { return } + if (args.serviceExecute != null) { + val payload = mutableMapOf("command" to args.serviceCommand!!) + val result = apiRequest("/services/${args.serviceExecute}/execute", "POST", payload, apiKey) + if (result.containsKey("stdout")) { + val stdout = result["stdout"] as? String + if (stdout != null && stdout.isNotEmpty()) { + print("$BLUE$stdout$RESET") + } + } + if (result.containsKey("stderr")) { + val stderr = result["stderr"] as? String + if (stderr != null && stderr.isNotEmpty()) { + System.err.print("$RED$stderr$RESET") + } + } + return + } + + if (args.serviceDumpBootstrap != null) { + System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...") + val payload = mutableMapOf("command" to "cat /tmp/bootstrap.sh") + val result = apiRequest("/services/${args.serviceDumpBootstrap}/execute", "POST", payload, apiKey) + + val bootstrap = result["stdout"] as? String + if (bootstrap != null && bootstrap.isNotEmpty()) { + if (args.serviceDumpFile != null) { + try { + val file = java.io.File(args.serviceDumpFile!!) + file.writeText(bootstrap) + file.setExecutable(true) + println("Bootstrap saved to ${args.serviceDumpFile}") + } catch (e: Exception) { + System.err.println("${RED}Error: Could not write to ${args.serviceDumpFile}: ${e.message}${RESET}") + exitProcess(1) + } + } else { + print(bootstrap) + } + } else { + System.err.println("${RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${RESET}") + exitProcess(1) + } + return + } + if (args.serviceName != null) { val payload = mutableMapOf("name" to args.serviceName!!) if (args.servicePorts != null) { @@ -597,9 +646,13 @@ fun parseArgs(args: Array): Args { "--info" -> result.serviceInfo = args[++i] "--logs" -> result.serviceLogs = args[++i] "--tail" -> result.serviceTail = args[++i] - "--sleep" -> result.serviceSleep = args[++i] - "--wake" -> result.serviceWake = args[++i] + "--freeze" -> result.serviceSleep = args[++i] + "--unfreeze" -> result.serviceWake = args[++i] "--destroy" -> result.serviceDestroy = args[++i] + "--execute" -> result.serviceExecute = args[++i] + "--command" -> result.serviceCommand = args[++i] + "--dump-bootstrap" -> result.serviceDumpBootstrap = args[++i] + "--dump-file" -> result.serviceDumpFile = args[++i] "--extend" -> result.keyExtend = true else -> if (!args[i].startsWith("-")) result.sourceFile = args[i] } @@ -638,9 +691,13 @@ Service options: --info ID Get service details --logs ID Get all logs --tail ID Get last 9000 lines - --sleep ID Freeze service - --wake ID Unfreeze service + --freeze ID Freeze service + --unfreeze ID Unfreeze service --destroy ID Destroy service + --execute ID Execute command in service + --command CMD Command to execute (with --execute) + --dump-bootstrap ID Dump bootstrap script + --dump-file FILE File to save bootstrap (with --dump-bootstrap) Key options: --extend Open browser to extend key diff --git a/un.lisp b/un.lisp index 8ae0073..c84eba7 100644 --- a/un.lisp +++ b/un.lisp @@ -183,6 +183,30 @@ ((string= action "destroy") (curl-delete api-key (format nil "/services/~a" id)) (format t "~aService destroyed: ~a~a~%" *green* id *reset*)) + ((string= action "execute") + (when (and id bootstrap) + (let* ((json (format nil "{\"command\":\"~a\"}" (escape-json bootstrap))) + (response (curl-post api-key (format nil "/services/~a/execute" id) json)) + (stdout-val (parse-json-field response "stdout"))) + (when stdout-val + (format t "~a~a~a" *blue* stdout-val *reset*))))) + ((string= action "dump-bootstrap") + (when id + (format *error-output* "Fetching bootstrap script from ~a...~%" id) + (let* ((json "{\"command\":\"cat /tmp/bootstrap.sh\"}") + (response (curl-post api-key (format nil "/services/~a/execute" id) json)) + (stdout-val (parse-json-field response "stdout"))) + (if stdout-val + (if service-type + (progn + (with-open-file (stream service-type :direction :output :if-exists :supersede) + (write-string stdout-val stream)) + (uiop:run-program (list "chmod" "755" service-type)) + (format t "Bootstrap saved to ~a~%" service-type)) + (format t "~a" stdout-val)) + (progn + (format *error-output* "~aError: Failed to fetch bootstrap (service not running or no bootstrap file)~a~%" *red* *reset*) + (uiop:quit 1)))))) ((and (string= action "create") name) (let* ((ports-json (if ports (format nil ",\"ports\":[~a]" ports) "")) (bootstrap-json (if bootstrap (format nil ",\"bootstrap\":\"~a\"" (escape-json bootstrap)) "")) @@ -307,12 +331,18 @@ (service-cmd "info" (third args) nil nil nil nil)) ((and (> (length args) 2) (string= (second args) "--logs")) (service-cmd "logs" (third args) nil nil nil nil)) - ((and (> (length args) 2) (string= (second args) "--sleep")) + ((and (> (length args) 2) (string= (second args) "--freeze")) (service-cmd "sleep" (third args) nil nil nil nil)) - ((and (> (length args) 2) (string= (second args) "--wake")) + ((and (> (length args) 2) (string= (second args) "--unfreeze")) (service-cmd "wake" (third args) nil nil nil nil)) ((and (> (length args) 2) (string= (second args) "--destroy")) (service-cmd "destroy" (third args) nil nil nil nil)) + ((and (> (length args) 3) (string= (second args) "--execute")) + (service-cmd "execute" (third args) nil nil (fourth args) nil)) + ((and (> (length args) 3) (string= (second args) "--dump-bootstrap")) + (service-cmd "dump-bootstrap" (third args) nil nil nil (fourth args))) + ((and (> (length args) 2) (string= (second args) "--dump-bootstrap")) + (service-cmd "dump-bootstrap" (third args) nil nil nil nil)) ((and (> (length args) 2) (string= (second args) "--name")) (let* ((name (third args)) (rest-args (nthcdr 3 args)) diff --git a/un.m b/un.m index a52bc83..29ed7f3 100644 --- a/un.m +++ b/un.m @@ -444,6 +444,8 @@ void cmdService(NSArray* args) { NSString* sleepId = nil; NSString* wakeId = nil; NSString* destroyId = nil; + NSString* dumpBootstrapId = nil; + NSString* dumpFile = nil; NSString* name = nil; NSString* ports = nil; NSString* type = nil; @@ -460,12 +462,16 @@ void cmdService(NSArray* args) { infoId = args[++i]; } else if ([arg isEqualToString:@"--logs"] && i + 1 < [args count]) { logsId = args[++i]; - } else if ([arg isEqualToString:@"--sleep"] && i + 1 < [args count]) { + } else if ([arg isEqualToString:@"--freeze"] && i + 1 < [args count]) { sleepId = args[++i]; - } else if ([arg isEqualToString:@"--wake"] && i + 1 < [args count]) { + } else if ([arg isEqualToString:@"--unfreeze"] && i + 1 < [args count]) { wakeId = args[++i]; } else if ([arg isEqualToString:@"--destroy"] && i + 1 < [args count]) { destroyId = args[++i]; + } else if ([arg isEqualToString:@"--dump-bootstrap"] && i + 1 < [args count]) { + dumpBootstrapId = args[++i]; + } else if ([arg isEqualToString:@"--dump-file"] && i + 1 < [args count]) { + dumpFile = args[++i]; } else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) { name = args[++i]; } else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) { @@ -541,6 +547,39 @@ void cmdService(NSArray* args) { return; } + if (dumpBootstrapId) { + fprintf(stderr, "Fetching bootstrap script from %s...\n", [dumpBootstrapId UTF8String]); + NSDictionary* payload = @{@"command": @"cat /tmp/bootstrap.sh"}; + NSString* endpoint = [NSString stringWithFormat:@"/services/%@/execute", dumpBootstrapId]; + NSDictionary* result = apiRequest(endpoint, @"POST", payload, apiKey); + + if (result[@"stdout"] && [result[@"stdout"] length] > 0) { + NSString* bootstrap = result[@"stdout"]; + if (dumpFile) { + // Write to file + NSError* error = nil; + [bootstrap writeToFile:dumpFile atomically:YES encoding:NSUTF8StringEncoding error:&error]; + if (error) { + fprintf(stderr, "%sError: Could not write to %s: %s%s\n", + [RED UTF8String], [dumpFile UTF8String], + [[error localizedDescription] UTF8String], [RESET UTF8String]); + exit(1); + } + NSFileManager* fm = [NSFileManager defaultManager]; + [fm setAttributes:@{NSFilePosixPermissions: @0755} ofItemAtPath:dumpFile error:nil]; + printf("Bootstrap saved to %s\n", [dumpFile UTF8String]); + } else { + // Print to stdout + printf("%s", [bootstrap UTF8String]); + } + } else { + fprintf(stderr, "%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s\n", + [RED UTF8String], [RESET UTF8String]); + exit(1); + } + return; + } + // Create new service if (name) { NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{@"name": name}]; diff --git a/un.ml b/un.ml index a87aae6..b0b8d8f 100755 --- a/un.ml +++ b/un.ml @@ -402,7 +402,7 @@ let service_command action name ports bootstrap service_type network vcpu = Sys.remove tmp_file; Printf.printf "%sService sleeping: %s%s\n" green sid reset | None -> - Printf.fprintf stderr "Error: --sleep requires service ID\n"; + Printf.fprintf stderr "Error: --freeze requires service ID\n"; exit 1) | "wake" -> (match name with @@ -417,7 +417,7 @@ let service_command action name ports bootstrap service_type network vcpu = Sys.remove tmp_file; Printf.printf "%sService waking: %s%s\n" green sid reset | None -> - Printf.fprintf stderr "Error: --wake requires service ID\n"; + Printf.fprintf stderr "Error: --unfreeze requires service ID\n"; exit 1) | "destroy" -> (match name with @@ -427,6 +427,72 @@ let service_command action name ports bootstrap service_type network vcpu = | None -> Printf.fprintf stderr "Error: --destroy requires service ID\n"; exit 1) + | "execute" -> + (match name with + | Some sid -> + (match bootstrap with + | Some cmd -> + let json = Printf.sprintf "{\"command\":\"%s\"}" (escape_json cmd) 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 curl_cmd = Printf.sprintf "curl -s -X POST https://api.unsandbox.com/services/%s/execute -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d @%s" + sid api_key tmp_file in + let ic = Unix.open_process_in curl_cmd in + let rec read_all acc = + try let line = input_line ic in read_all (acc ^ line ^ "\n") + with End_of_file -> acc + in + let response = read_all "" in + let _ = Unix.close_process_in ic in + Sys.remove tmp_file; + (match extract_field "stdout" response with + | Some s -> Printf.printf "%s%s%s" blue (unescape_json s) reset + | None -> ()) + | None -> + Printf.fprintf stderr "Error: --command required with --execute\n"; + exit 1) + | None -> + Printf.fprintf stderr "Error: --execute requires service ID\n"; + exit 1) + | "dump_bootstrap" -> + (match name with + | Some sid -> + Printf.fprintf stderr "Fetching bootstrap script from %s...\n" sid; + let json = "{\"command\":\"cat /tmp/bootstrap.sh\"}" 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 curl_cmd = Printf.sprintf "curl -s -X POST https://api.unsandbox.com/services/%s/execute -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d @%s" + sid api_key tmp_file in + let ic = Unix.open_process_in curl_cmd in + let rec read_all acc = + try let line = input_line ic in read_all (acc ^ line ^ "\n") + with End_of_file -> acc + in + let response = read_all "" in + let _ = Unix.close_process_in ic in + Sys.remove tmp_file; + (match extract_field "stdout" response with + | Some s -> + let script = unescape_json s in + (match service_type with + | Some file -> + let oc = open_out file in + output_string oc script; + close_out oc; + Unix.chmod file 0o755; + Printf.printf "Bootstrap saved to %s\n" file + | None -> + Printf.printf "%s" script) + | None -> + Printf.fprintf stderr "%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s\n" red reset; + exit 1) + | None -> + Printf.fprintf stderr "Error: --dump-bootstrap requires service ID\n"; + exit 1) | "create" -> (match name with | Some n -> @@ -488,9 +554,12 @@ let () = | "--list" :: rest -> parse_service "list" name ports bootstrap service_type network vcpu rest | "--info" :: id :: rest -> parse_service "info" (Some id) ports bootstrap service_type network vcpu rest | "--logs" :: id :: rest -> parse_service "logs" (Some id) ports bootstrap service_type network vcpu rest - | "--sleep" :: id :: rest -> parse_service "sleep" (Some id) ports bootstrap service_type network vcpu rest - | "--wake" :: id :: rest -> parse_service "wake" (Some id) ports bootstrap service_type network vcpu rest + | "--freeze" :: id :: rest -> parse_service "sleep" (Some id) ports bootstrap service_type network vcpu rest + | "--unfreeze" :: id :: rest -> parse_service "wake" (Some id) ports bootstrap service_type network vcpu rest | "--destroy" :: id :: rest -> parse_service "destroy" (Some id) ports bootstrap service_type network vcpu rest + | "--execute" :: id :: "--command" :: cmd :: rest -> parse_service "execute" (Some id) ports (Some cmd) service_type network vcpu rest + | "--dump-bootstrap" :: id :: file :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap (Some file) network vcpu rest + | "--dump-bootstrap" :: id :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap service_type network vcpu rest | "--name" :: n :: rest -> parse_service "create" (Some n) ports bootstrap service_type network vcpu rest | "--ports" :: p :: rest -> parse_service action name (Some p) bootstrap service_type network vcpu rest | "--bootstrap" :: b :: rest -> parse_service action name ports (Some b) service_type network vcpu rest diff --git a/un.nim b/un.nim index 426f05e..29a85d4 100644 --- a/un.nim +++ b/un.nim @@ -128,7 +128,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' -H 'Authorization: Bearer {apiKey}' -d '{json}'""" echo execCurl(cmd) -proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, network: string, vcpu: int, apiKey: string) = +proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, apiKey: string) = if list: let cmd = fmt"""curl -s -X GET '{API_BASE}/services' -H 'Authorization: Bearer {apiKey}'""" echo execCurl(cmd) @@ -167,6 +167,76 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l echo GREEN & "Service destroyed: " & destroy & RESET return + if execute != "": + let json = fmt"""{"command":"{escapeJson(command)}"}""" + let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'""" + let result = execCurl(cmd) + + # Simple parsing for stdout/stderr + let stdoutStart = result.find("\"stdout\":\"") + if stdoutStart >= 0: + let start = stdoutStart + 10 + var endPos = start + while endPos < result.len: + if result[endPos] == '"' and (endPos == 0 or result[endPos-1] != '\\'): + break + inc endPos + if endPos > start: + var output = result[start..= 0: + let start = stderrStart + 10 + var endPos = start + while endPos < result.len: + if result[endPos] == '"' and (endPos == 0 or result[endPos-1] != '\\'): + break + inc endPos + if endPos > start: + var errout = result[start..= 0: + let start = stdoutStart + 10 + var endPos = start + while endPos < result.len: + if result[endPos] == '"' and (endPos == 0 or result[endPos-1] != '\\'): + break + inc endPos + if endPos > start: + var bootstrapScript = result[start.. % No file specified, print to stdout + format(atom(Cmd), + 'echo "Fetching bootstrap script from ~w..." >&2; RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi', + [ServiceId, ServiceId, ApiKey]) + ; % File specified, save to file + format(atom(Cmd), + 'echo "Fetching bootstrap script from ~w..." >&2; RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT" > "~w" && chmod 755 "~w" && echo "Bootstrap saved to ~w"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi', + [ServiceId, ServiceId, ApiKey, DumpFile, DumpFile, DumpFile]) + ), + shell(Cmd, 0). + % Service create service_create(Name, Ports, Bootstrap, ServiceType) :- get_api_key(ApiKey), @@ -244,16 +259,21 @@ parse_service_args([], Name, Ports, Bootstrap, ServiceType, create) :- parse_service_args([], _, _, _, _, Action) :- ( Action = list -> service_list - ; write(user_error, 'Error: Use --list, --info, --logs, --sleep, --wake, --destroy, or --name\n'), + ; write(user_error, 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --name\n'), halt(1) ). parse_service_args(['--list'|_], _, _, _, _, _) :- service_list. parse_service_args(['-l'|_], _, _, _, _, _) :- service_list. parse_service_args(['--info', ServiceId|_], _, _, _, _, _) :- service_info(ServiceId). parse_service_args(['--logs', ServiceId|_], _, _, _, _, _) :- service_logs(ServiceId). -parse_service_args(['--sleep', ServiceId|_], _, _, _, _, _) :- service_sleep(ServiceId). -parse_service_args(['--wake', ServiceId|_], _, _, _, _, _) :- service_wake(ServiceId). +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(['--dump-bootstrap', ServiceId|Rest], _, _, _, _, _) :- + ( Rest = ['--dump-file', DumpFile|_] + -> service_dump_bootstrap(ServiceId, DumpFile) + ; service_dump_bootstrap(ServiceId, '') + ). parse_service_args(['--name', Name|Rest], _, Ports, Bootstrap, ServiceType, _) :- parse_service_args(Rest, Name, Ports, Bootstrap, ServiceType, create). parse_service_args(['--ports', PortsList|Rest], Name, _, Bootstrap, ServiceType, Action) :- diff --git a/un.ps1 b/un.ps1 index e1151de..3b092ec 100644 --- a/un.ps1 +++ b/un.ps1 @@ -248,16 +248,16 @@ function Invoke-Service { return } - if ($Args -contains "--sleep") { - $idx = [array]::IndexOf($Args, "--sleep") + if ($Args -contains "--freeze") { + $idx = [array]::IndexOf($Args, "--freeze") $serviceId = $Args[$idx + 1] Invoke-Api -Endpoint "/services/$serviceId/sleep" -Method "POST" -Body "{}" Write-Host "`e[32mService sleeping: $serviceId`e[0m" return } - if ($Args -contains "--wake") { - $idx = [array]::IndexOf($Args, "--wake") + if ($Args -contains "--unfreeze") { + $idx = [array]::IndexOf($Args, "--unfreeze") $serviceId = $Args[$idx + 1] Invoke-Api -Endpoint "/services/$serviceId/wake" -Method "POST" -Body "{}" Write-Host "`e[32mService waking: $serviceId`e[0m" @@ -272,6 +272,36 @@ function Invoke-Service { return } + if ($Args -contains "--dump-bootstrap") { + $idx = [array]::IndexOf($Args, "--dump-bootstrap") + $serviceId = $Args[$idx + 1] + Write-Host "Fetching bootstrap script from $serviceId..." -ForegroundColor Yellow + + $payload = @{ command = "cat /tmp/bootstrap.sh" } | ConvertTo-Json + $result = Invoke-Api -Endpoint "/services/$serviceId/execute" -Method "POST" -Body $payload + + if ($result.stdout -and $result.stdout.Length -gt 0) { + $bootstrap = $result.stdout + if ($Args -contains "--dump-file") { + $dumpIdx = [array]::IndexOf($Args, "--dump-file") + $dumpFile = $Args[$dumpIdx + 1] + # Write to file + $bootstrap | Set-Content -Path $dumpFile -NoNewline + if ($IsLinux -or $IsMacOS) { + & chmod 755 $dumpFile + } + Write-Host "Bootstrap saved to $dumpFile" + } else { + # Print to stdout + Write-Host $bootstrap -NoNewline + } + } else { + Write-Error "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" + exit 1 + } + return + } + # Create service if ($Args -contains "--name") { $idx = [array]::IndexOf($Args, "--name") @@ -324,16 +354,18 @@ Session options: --shell NAME Shell/REPL to use Service options: - --name NAME Service name - --ports PORTS Comma-separated ports - --type TYPE Service type (minecraft, mumble, teamspeak, source, tcp, udp) - --bootstrap CMD Bootstrap command - --list, -l List services - --info ID Get service info - --logs ID Get logs - --sleep ID Freeze service - --wake ID Unfreeze service - --destroy ID Destroy service + --name NAME Service name + --ports PORTS Comma-separated ports + --type TYPE Service type (minecraft, mumble, teamspeak, source, tcp, udp) + --bootstrap CMD Bootstrap command + --list, -l List services + --info ID Get service info + --logs ID Get logs + --freeze ID Freeze service + --unfreeze ID Unfreeze service + --destroy ID Destroy service + --dump-bootstrap ID Dump bootstrap script from service + --dump-file FILE Save bootstrap to file (with --dump-bootstrap) Key options: --extend Open browser to extend key diff --git a/un.r b/un.r index ff19d5e..519e42c 100644 --- a/un.r +++ b/un.r @@ -354,6 +354,34 @@ cmd_service <- function(args) { return() } + if (!is.null(args$dump_bootstrap)) { + cat(sprintf("Fetching bootstrap script from %s...\n", args$dump_bootstrap), file = stderr()) + payload <- list(command = "cat /tmp/bootstrap.sh") + result <- api_request(paste0("/services/", args$dump_bootstrap, "/execute"), api_key, method = "POST", data = payload) + + if (!is.null(result$stdout) && result$stdout != "") { + bootstrap <- result$stdout + if (!is.null(args$dump_file)) { + # Write to file + tryCatch({ + writeLines(bootstrap, args$dump_file) + Sys.chmod(args$dump_file, mode = "0755") + cat(sprintf("Bootstrap saved to %s\n", args$dump_file)) + }, error = function(e) { + cat(sprintf("%sError: Could not write to %s: %s%s\n", RED, args$dump_file, e$message, RESET), file = stderr()) + quit(status = 1) + }) + } else { + # Print to stdout + cat(bootstrap) + } + } else { + cat(sprintf("%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s\n", RED, RESET), file = stderr()) + quit(status = 1) + } + return() + } + if (!is.null(args$name)) { payload <- list(name = args$name) @@ -396,7 +424,7 @@ cmd_service <- function(args) { return() } - cat(sprintf("%sError: Use --name to create, or --list, --info, --logs, --sleep, --wake, --destroy%s\n", RED, RESET), file = stderr()) + cat(sprintf("%sError: Use --name to create, or --list, --info, --logs, --freeze, --unfreeze, --destroy%s\n", RED, RESET), file = stderr()) quit(status = 1) } @@ -419,6 +447,8 @@ parse_args <- function() { sleep = NULL, wake = NULL, destroy = NULL, + dump_bootstrap = NULL, + dump_file = NULL, name = NULL, ports = NULL, domains = NULL, @@ -479,11 +509,11 @@ parse_args <- function() { i <- i + 1 result$logs <- args[i] i <- i + 1 - } else if (arg == "--sleep") { + } else if (arg == "--freeze") { i <- i + 1 result$sleep <- args[i] i <- i + 1 - } else if (arg == "--wake") { + } else if (arg == "--unfreeze") { i <- i + 1 result$wake <- args[i] i <- i + 1 @@ -491,6 +521,14 @@ parse_args <- function() { i <- i + 1 result$destroy <- args[i] i <- i + 1 + } else if (arg == "--dump-bootstrap") { + i <- i + 1 + result$dump_bootstrap <- args[i] + i <- i + 1 + } else if (arg == "--dump-file") { + i <- i + 1 + result$dump_file <- args[i] + i <- i + 1 } else if (arg == "--name") { i <- i + 1 result$name <- args[i] diff --git a/un.raku b/un.raku index c376f0c..2b8f72b 100644 --- a/un.raku +++ b/un.raku @@ -319,6 +319,8 @@ sub cmd-service(@args) { my $sleep-id = ''; my $wake-id = ''; my $destroy-id = ''; + my $dump-bootstrap-id = ''; + my $dump-file = ''; my $name = ''; my $ports = ''; my $type = ''; @@ -341,11 +343,11 @@ sub cmd-service(@args) { $i++; $logs-id = @args[$i]; } - when '--sleep' { + when '--freeze' { $i++; $sleep-id = @args[$i]; } - when '--wake' { + when '--unfreeze' { $i++; $wake-id = @args[$i]; } @@ -353,6 +355,14 @@ sub cmd-service(@args) { $i++; $destroy-id = @args[$i]; } + when '--dump-bootstrap' { + $i++; + $dump-bootstrap-id = @args[$i]; + } + when '--dump-file' { + $i++; + $dump-file = @args[$i]; + } when '--name' { $i++; $name = @args[$i]; @@ -428,6 +438,29 @@ sub cmd-service(@args) { return; } + if $dump-bootstrap-id { + note "Fetching bootstrap script from $dump-bootstrap-id..."; + my %payload = command => "cat /tmp/bootstrap.sh"; + my %result = api-request("/services/$dump-bootstrap-id/execute", 'POST', %payload, :$api-key); + + if %result && %result ne '' { + my $bootstrap = %result; + if $dump-file { + # Write to file + $dump-file.IO.spurt($bootstrap); + run 'chmod', '755', $dump-file; + say "Bootstrap saved to $dump-file"; + } else { + # Print to stdout + print $bootstrap; + } + } else { + note "{$RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file){$RESET}"; + exit 1; + } + return; + } + # Create new service if $name { my %payload = name => $name; diff --git a/un.rs b/un.rs index e11f7e7..a342313 100644 --- a/un.rs +++ b/un.rs @@ -348,6 +348,10 @@ fn cmd_service( sleep: Option<&str>, wake: Option<&str>, destroy: Option<&str>, + execute: Option<&str>, + command: Option<&str>, + dump_bootstrap: Option<&str>, + dump_file: Option<&str>, network: Option<&str>, vcpu: Option, api_key: &str, @@ -394,6 +398,53 @@ fn cmd_service( return; } + if let Some(id) = execute { + let cmd = command.unwrap_or(""); + let json = format!(r#"{{"command":"{}"}}"#, escape_json(cmd)); + let result = api_request(&format!("/services/{}/execute", id), "POST", Some(&json), api_key); + let stdout_str = extract_json_string(&result, "stdout"); + let stderr_str = extract_json_string(&result, "stderr"); + if !stdout_str.is_empty() { + print!("{}{}{}", BLUE, stdout_str, RESET); + } + if !stderr_str.is_empty() { + eprint!("{}{}{}", RED, stderr_str, RESET); + } + return; + } + + if let Some(id) = dump_bootstrap { + eprintln!("Fetching bootstrap script from {}...", id); + let json = r#"{"command":"cat /tmp/bootstrap.sh"}"#; + let result = api_request(&format!("/services/{}/execute", id), "POST", Some(json), api_key); + let bootstrap = extract_json_string(&result, "stdout"); + + if !bootstrap.is_empty() { + if let Some(file) = dump_file { + match fs::write(file, &bootstrap) { + Ok(_) => { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = fs::set_permissions(file, fs::Permissions::from_mode(0o755)); + } + println!("Bootstrap saved to {}", file); + } + Err(e) => { + eprintln!("{}Error: Could not write to {}: {}{}", RED, file, e, RESET); + process::exit(1); + } + } + } else { + print!("{}", bootstrap); + } + } else { + eprintln!("{}Error: Failed to fetch bootstrap (service not running or no bootstrap file){}", RED, RESET); + process::exit(1); + } + return; + } + // Create service if let Some(n) = name { let mut json = format!(r#"{{"name":"{}""#, n); @@ -621,9 +672,13 @@ fn main() { args.iter().position(|x| x == "--info").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), args.iter().position(|x| x == "--logs").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), args.iter().position(|x| x == "--tail").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), - args.iter().position(|x| x == "--sleep").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), - args.iter().position(|x| x == "--wake").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), + 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 == "--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()), + args.iter().position(|x| x == "--dump-file").and_then(|p| args.get(p + 1)).map(|s| s.as_str()), network.as_deref(), vcpu, &key, diff --git a/un.scm b/un.scm index fa790f8..170966c 100644 --- a/un.scm +++ b/un.scm @@ -279,6 +279,30 @@ ((equal? action "destroy") (curl-delete api-key (format #f "/services/~a" id)) (format #t "~aService destroyed: ~a~a\n" green id reset)) + ((equal? action "execute") + (when (and id bootstrap) + (let* ((json (format #f "{\"command\":\"~a\"}" (escape-json bootstrap))) + (response (curl-post api-key (format #f "/services/~a/execute" id) json)) + (stdout-val (json-extract-string response "stdout"))) + (when stdout-val + (display (format #f "~a~a~a" blue stdout-val reset)))))) + ((equal? action "dump-bootstrap") + (when id + (format (current-error-port) "Fetching bootstrap script from ~a...\n" id) + (let* ((json "{\"command\":\"cat /tmp/bootstrap.sh\"}") + (response (curl-post api-key (format #f "/services/~a/execute" id) json)) + (stdout-val (json-extract-string response "stdout"))) + (if stdout-val + (if type + (begin + (call-with-output-file type + (lambda (port) (display stdout-val port))) + (system (format #f "chmod 755 ~a" type)) + (format #t "Bootstrap saved to ~a\n" type)) + (display stdout-val)) + (begin + (format (current-error-port) "~aError: Failed to fetch bootstrap (service not running or no bootstrap file)~a\n" red reset) + (exit 1)))))) ((and (equal? action "create") name) (let* ((ports-json (if ports (format #f ",\"ports\":[~a]" ports) "")) (bootstrap-json (if bootstrap (format #f ",\"bootstrap\":\"~a\"" (escape-json bootstrap)) "")) @@ -318,12 +342,18 @@ (service-cmd "info" (caddr args) #f #f #f #f)) ((and (> (length args) 2) (equal? (cadr args) "--logs")) (service-cmd "logs" (caddr args) #f #f #f #f)) - ((and (> (length args) 2) (equal? (cadr args) "--sleep")) + ((and (> (length args) 2) (equal? (cadr args) "--freeze")) (service-cmd "sleep" (caddr args) #f #f #f #f)) - ((and (> (length args) 2) (equal? (cadr args) "--wake")) + ((and (> (length args) 2) (equal? (cadr args) "--unfreeze")) (service-cmd "wake" (caddr args) #f #f #f #f)) ((and (> (length args) 2) (equal? (cadr args) "--destroy")) (service-cmd "destroy" (caddr args) #f #f #f #f)) + ((and (> (length args) 3) (equal? (cadr args) "--execute")) + (service-cmd "execute" (caddr args) #f #f (list-ref args 3) #f)) + ((and (> (length args) 3) (equal? (cadr args) "--dump-bootstrap")) + (service-cmd "dump-bootstrap" (caddr args) #f #f #f (list-ref args 3))) + ((and (> (length args) 2) (equal? (cadr args) "--dump-bootstrap")) + (service-cmd "dump-bootstrap" (caddr args) #f #f #f #f)) ((and (> (length args) 2) (equal? (cadr args) "--name")) (let ((name (caddr args)) (ports (if (and (> (length args) 4) (equal? (list-ref args 3) "--ports")) diff --git a/un.tcl b/un.tcl index 164b0f0..ad61533 100644 --- a/un.tcl +++ b/un.tcl @@ -445,6 +445,8 @@ proc cmd_service {args} { set sleep_id "" set wake_id "" set destroy_id "" + set dump_bootstrap_id "" + set dump_file "" set name "" set ports "" set service_type "" @@ -467,11 +469,11 @@ proc cmd_service {args} { incr i set logs_id [lindex $args $i] } - --sleep { + --freeze { incr i set sleep_id [lindex $args $i] } - --wake { + --unfreeze { incr i set wake_id [lindex $args $i] } @@ -479,6 +481,14 @@ proc cmd_service {args} { incr i set destroy_id [lindex $args $i] } + --dump-bootstrap { + incr i + set dump_bootstrap_id [lindex $args $i] + } + --dump-file { + incr i + set dump_file [lindex $args $i] + } --name { incr i set name [lindex $args $i] @@ -557,6 +567,31 @@ proc cmd_service {args} { 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"]] + set result [api_request "/services/$dump_bootstrap_id/execute" "POST" $payload $api_key] + + if {[dict exists $result stdout] && [dict get $result stdout] ne ""} { + set bootstrap [dict get $result stdout] + if {$dump_file ne ""} { + # Write to file + set fp [open $dump_file w] + puts -nonewline $fp $bootstrap + close $fp + file attributes $dump_file -permissions 0755 + puts "Bootstrap saved to $dump_file" + } else { + # Print to stdout + puts -nonewline $bootstrap + } + } else { + puts stderr "${::RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${::RESET}" + exit 1 + } + return + } + # Create new service if {$name ne ""} { set payload [list name [::json::write string $name]] diff --git a/un.v b/un.v index 1107040..31bbb91 100644 --- a/un.v +++ b/un.v @@ -268,7 +268,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, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, network string, vcpu int, api_key string) { +fn cmd_service(name string, ports string, service_type string, bootstrap 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, api_key string) { if list { cmd := "curl -s -X GET '${api_base}/services' -H 'Authorization: Bearer ${api_key}'" println(exec_curl(cmd)) @@ -314,6 +314,46 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string, return } + if execute != '' { + json := '{"command":"${escape_json(command)}"}' + cmd := "curl -s -X POST '${api_base}/services/${execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'" + result := exec_curl(cmd) + + stdout_str := extract_json_string(result, 'stdout') + stderr_str := extract_json_string(result, 'stderr') + if stdout_str != '' { + print(stdout_str) + } + if stderr_str != '' { + eprint(stderr_str) + } + return + } + + if dump_bootstrap != '' { + eprintln('Fetching bootstrap script from ${dump_bootstrap}...') + cmd := "curl -s -X POST '${api_base}/services/${dump_bootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}'" + result := exec_curl(cmd) + + bootstrap_script := extract_json_string(result, 'stdout') + if bootstrap_script != '' { + if dump_file != '' { + os.write_file(dump_file, bootstrap_script) or { + eprintln('${red}Error: Could not write to ${dump_file}: ${err}${reset}') + exit(1) + } + os.chmod(dump_file, 0o755) or {} + println('Bootstrap saved to ${dump_file}') + } else { + print(bootstrap_script) + } + } else { + eprintln('${red}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${reset}') + exit(1) + } + return + } + if name != '' { mut json := '{"name":"${name}"' if ports != '' { @@ -415,6 +455,10 @@ fn main() { mut sleep := '' mut wake := '' mut destroy := '' + mut execute := '' + mut command := '' + mut dump_bootstrap := '' + mut dump_file := '' mut network := '' mut vcpu := 0 @@ -450,11 +494,11 @@ fn main() { i++ tail = os.args[i] } - '--sleep' { + '--freeze' { i++ sleep = os.args[i] } - '--wake' { + '--unfreeze' { i++ wake = os.args[i] } @@ -462,6 +506,22 @@ fn main() { i++ destroy = os.args[i] } + '--execute' { + i++ + execute = os.args[i] + } + '--command' { + i++ + command = os.args[i] + } + '--dump-bootstrap' { + i++ + dump_bootstrap = os.args[i] + } + '--dump-file' { + i++ + dump_file = os.args[i] + } '-n' { i++ network = os.args[i] @@ -479,7 +539,7 @@ fn main() { i++ } - cmd_service(name, ports, service_type, bootstrap, list, info, logs, tail, sleep, wake, destroy, network, + cmd_service(name, ports, service_type, bootstrap, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network, vcpu, api_key) return } diff --git a/un.zig b/un.zig index 902f579..f54b3b4 100644 --- a/un.zig +++ b/un.zig @@ -121,6 +121,10 @@ pub fn main() !u8 { var ports: ?[]const u8 = null; var service_type: ?[]const u8 = null; var info: ?[]const u8 = null; + var execute: ?[]const u8 = null; + var command: ?[]const u8 = null; + var dump_bootstrap: ?[]const u8 = null; + var dump_file: ?[]const u8 = null; var i: usize = 2; while (i < args.len) : (i += 1) { if (mem.eql(u8, args[i], "--list")) { @@ -137,6 +141,18 @@ pub fn main() !u8 { } else if (mem.eql(u8, args[i], "--info") and i + 1 < args.len) { i += 1; info = args[i]; + } else if (mem.eql(u8, args[i], "--execute") and i + 1 < args.len) { + i += 1; + execute = args[i]; + } else if (mem.eql(u8, args[i], "--command") and i + 1 < args.len) { + i += 1; + command = args[i]; + } else if (mem.eql(u8, args[i], "--dump-bootstrap") and i + 1 < args.len) { + i += 1; + dump_bootstrap = args[i]; + } else if (mem.eql(u8, args[i], "--dump-file") and i + 1 < args.len) { + i += 1; + dump_file = args[i]; } } @@ -150,6 +166,57 @@ pub fn main() !u8 { defer allocator.free(cmd); _ = std.c.system(cmd.ptr); std.debug.print("\n", .{}); + } else if (execute) |exec_id| { + const cmd_text = command orelse ""; + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"command\":\"{s}\"}}'", .{ API_BASE, exec_id, api_key, cmd_text }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + std.debug.print("\n", .{}); + } else if (dump_bootstrap) |bootstrap_id| { + std.debug.print("Fetching bootstrap script from {s}...\n", .{bootstrap_id}); + const tmp_file = "/tmp/unsandbox_bootstrap_dump.txt"; + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"command\":\"cat /tmp/bootstrap.sh\"}}' -o {s}", .{ API_BASE, bootstrap_id, api_key, tmp_file }); + defer allocator.free(cmd); + _ = std.c.system(cmd.ptr); + + // Read the JSON response + const json_content = fs.cwd().readFileAlloc(allocator, tmp_file, 1024 * 1024) catch |err| { + std.debug.print("\x1b[31mError reading response: {}\x1b[0m\n", .{err}); + std.fs.cwd().deleteFile(tmp_file) catch {}; + return 1; + }; + defer allocator.free(json_content); + std.fs.cwd().deleteFile(tmp_file) catch {}; + + // Extract stdout from JSON (simple string search) + const stdout_prefix = "\"stdout\":\""; + if (mem.indexOf(u8, json_content, stdout_prefix)) |start_idx| { + const value_start = start_idx + stdout_prefix.len; + if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| { + const bootstrap_content = json_content[value_start..end_idx]; + + if (dump_file) |file_path| { + const file = try std.fs.cwd().createFile(file_path, .{}); + defer file.close(); + try file.writeAll(bootstrap_content); + // Set permissions (Unix only) + if (@import("builtin").os.tag != .windows) { + const chmod_cmd = try std.fmt.allocPrint(allocator, "chmod 755 {s}", .{file_path}); + defer allocator.free(chmod_cmd); + _ = std.c.system(chmod_cmd.ptr); + } + std.debug.print("Bootstrap saved to {s}\n", .{file_path}); + } else { + std.debug.print("{s}", .{bootstrap_content}); + } + } else { + std.debug.print("\x1b[31mError: Failed to parse bootstrap response\x1b[0m\n", .{}); + return 1; + } + } else { + std.debug.print("\x1b[31mError: Failed to fetch bootstrap (service not running or no bootstrap file)\x1b[0m\n", .{}); + return 1; + } } else if (name) |n| { var json_buf: [4096]u8 = undefined; var json_stream = std.io.fixedBufferStream(&json_buf); diff --git a/un_deno.ts b/un_deno.ts index d95a6c2..1003533 100644 --- a/un_deno.ts +++ b/un_deno.ts @@ -426,12 +426,12 @@ async function cmdService(args: string[]) { logsId = args[++i]; } break; - case "--sleep": + case "--freeze": if (i + 1 < args.length) { sleepId = args[++i]; } break; - case "--wake": + case "--unfreeze": if (i + 1 < args.length) { wakeId = args[++i]; } diff --git a/un_inception.c b/un_inception.c index 91ac8bd..a21110e 100644 --- a/un_inception.c +++ b/un_inception.c @@ -640,8 +640,8 @@ int main(int argc, char *argv[]) { else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) info = argv[++i]; else if (strcmp(argv[i], "--logs") == 0 && i + 1 < argc) logs = argv[++i]; else if (strcmp(argv[i], "--tail") == 0 && i + 1 < argc) tail = argv[++i]; - else if (strcmp(argv[i], "--sleep") == 0 && i + 1 < argc) sleep_svc = argv[++i]; - else if (strcmp(argv[i], "--wake") == 0 && i + 1 < argc) wake = argv[++i]; + else if (strcmp(argv[i], "--freeze") == 0 && i + 1 < argc) sleep_svc = argv[++i]; + else if (strcmp(argv[i], "--unfreeze") == 0 && i + 1 < argc) wake = argv[++i]; else if (strcmp(argv[i], "--destroy") == 0 && i + 1 < argc) destroy = argv[++i]; else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) network = argv[++i]; else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) vcpu = atoi(argv[++i]);