Support -f on service create and redeploy across all SDKs
Add input_files support to service redeploy for all 12 SDK implementations that have service commands. Files passed via -f are read, base64-encoded, and sent as input_files in the JSON payload. Service create also gains -f support where it was missing. Updated: bash, cpp, csharp, dotnet, go, java, javascript, perl, php, ruby, rust, typescript
This commit is contained in:
parent
ac50544ac3
commit
369f3aa39d
13 changed files with 790 additions and 46 deletions
|
|
@ -424,6 +424,7 @@ service_create() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local ports="${2:-}"
|
local ports="${2:-}"
|
||||||
local bootstrap="${3:-}"
|
local bootstrap="${3:-}"
|
||||||
|
local input_files_json="${4:-}"
|
||||||
|
|
||||||
local body
|
local body
|
||||||
body=$(jq -n --arg name "$name" '{name: $name}')
|
body=$(jq -n --arg name "$name" '{name: $name}')
|
||||||
|
|
@ -434,6 +435,9 @@ service_create() {
|
||||||
if [ -n "$bootstrap" ]; then
|
if [ -n "$bootstrap" ]; then
|
||||||
body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}')
|
body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}')
|
||||||
fi
|
fi
|
||||||
|
if [ -n "$input_files_json" ]; then
|
||||||
|
body=$(echo "$body" | jq --argjson files "$input_files_json" '. + {input_files: $files}')
|
||||||
|
fi
|
||||||
|
|
||||||
api_request "POST" "/services" "$body"
|
api_request "POST" "/services" "$body"
|
||||||
}
|
}
|
||||||
|
|
@ -472,10 +476,14 @@ service_set_unfreeze_on_demand() {
|
||||||
service_redeploy() {
|
service_redeploy() {
|
||||||
local service_id="$1"
|
local service_id="$1"
|
||||||
local bootstrap="${2:-}"
|
local bootstrap="${2:-}"
|
||||||
|
local input_files_json="${3:-}"
|
||||||
local body="{}"
|
local body="{}"
|
||||||
if [ -n "$bootstrap" ]; then
|
if [ -n "$bootstrap" ]; then
|
||||||
body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}')
|
body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}')
|
||||||
fi
|
fi
|
||||||
|
if [ -n "$input_files_json" ]; then
|
||||||
|
body=$(echo "$body" | jq --argjson files "$input_files_json" '. + {input_files: $files}')
|
||||||
|
fi
|
||||||
api_request "POST" "/services/$service_id/redeploy" "$body"
|
api_request "POST" "/services/$service_id/redeploy" "$body"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -902,6 +910,9 @@ cmd_service() {
|
||||||
local target=""
|
local target=""
|
||||||
local name=""
|
local name=""
|
||||||
local ports=""
|
local ports=""
|
||||||
|
local bootstrap=""
|
||||||
|
local bootstrap_file=""
|
||||||
|
local -a files=()
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -913,13 +924,47 @@ cmd_service() {
|
||||||
--lock) action="lock"; target="$2"; shift ;;
|
--lock) action="lock"; target="$2"; shift ;;
|
||||||
--unlock) action="unlock"; target="$2"; shift ;;
|
--unlock) action="unlock"; target="$2"; shift ;;
|
||||||
--logs) action="logs"; target="$2"; shift ;;
|
--logs) action="logs"; target="$2"; shift ;;
|
||||||
|
--redeploy) action="redeploy"; target="$2"; shift ;;
|
||||||
--name) name="$2"; shift ;;
|
--name) name="$2"; shift ;;
|
||||||
--ports) ports="$2"; shift ;;
|
--ports) ports="$2"; shift ;;
|
||||||
|
--bootstrap) bootstrap="$2"; shift ;;
|
||||||
|
--bootstrap-file) bootstrap_file="$2"; shift ;;
|
||||||
|
-f|--file) files+=("$2"); shift ;;
|
||||||
*) ;;
|
*) ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Build input_files JSON from -f args
|
||||||
|
local input_files_json=""
|
||||||
|
if [ ${#files[@]} -gt 0 ]; then
|
||||||
|
input_files_json="["
|
||||||
|
local first=1
|
||||||
|
for fpath in "${files[@]}"; do
|
||||||
|
if [ ! -f "$fpath" ]; then
|
||||||
|
echo -e "${RED}Error: File not found: $fpath${RESET}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local encoded
|
||||||
|
encoded=$(base64 -w0 "$fpath" 2>/dev/null || base64 "$fpath" 2>/dev/null)
|
||||||
|
local fname
|
||||||
|
fname=$(basename "$fpath")
|
||||||
|
[ "$first" -eq 0 ] && input_files_json="$input_files_json,"
|
||||||
|
input_files_json="$input_files_json{\"filename\":$(echo "$fname" | jq -Rs .),\"content\":$(echo "$encoded" | jq -Rs .)}"
|
||||||
|
first=0
|
||||||
|
done
|
||||||
|
input_files_json="$input_files_json]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve bootstrap from file if provided
|
||||||
|
if [ -n "$bootstrap_file" ]; then
|
||||||
|
if [ ! -f "$bootstrap_file" ]; then
|
||||||
|
echo -e "${RED}Error: Bootstrap file not found: $bootstrap_file${RESET}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
bootstrap=$(cat "$bootstrap_file")
|
||||||
|
fi
|
||||||
|
|
||||||
case "$action" in
|
case "$action" in
|
||||||
list)
|
list)
|
||||||
local result
|
local result
|
||||||
|
|
@ -954,14 +999,19 @@ cmd_service() {
|
||||||
result=$(service_logs "$target")
|
result=$(service_logs "$target")
|
||||||
echo "$result" | jq -r '.logs // empty'
|
echo "$result" | jq -r '.logs // empty'
|
||||||
;;
|
;;
|
||||||
|
redeploy)
|
||||||
|
local result
|
||||||
|
result=$(service_redeploy "$target" "$bootstrap" "$input_files_json")
|
||||||
|
echo -e "${GREEN}Service redeployed: $target${RESET}"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
if [ -n "$name" ]; then
|
if [ -n "$name" ]; then
|
||||||
local result
|
local result
|
||||||
result=$(service_create "$name" "$ports" "")
|
result=$(service_create "$name" "$ports" "$bootstrap" "$input_files_json")
|
||||||
echo -e "${GREEN}Service created${RESET}"
|
echo -e "${GREEN}Service created${RESET}"
|
||||||
echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"'
|
echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"'
|
||||||
else
|
else
|
||||||
echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--name NAME" >&2
|
echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--redeploy ID|--name NAME" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
@ -1138,8 +1188,12 @@ Service options:
|
||||||
--lock ID Lock service
|
--lock ID Lock service
|
||||||
--unlock ID Unlock service
|
--unlock ID Unlock service
|
||||||
--logs ID Get service logs
|
--logs ID Get service logs
|
||||||
|
--redeploy ID Re-run bootstrap (supports -f, --bootstrap)
|
||||||
--name NAME Create service with name
|
--name NAME Create service with name
|
||||||
--ports PORTS Service ports (comma-separated)
|
--ports PORTS Service ports (comma-separated)
|
||||||
|
--bootstrap CMD Bootstrap command
|
||||||
|
--bootstrap-file FILE Bootstrap from file
|
||||||
|
-f, --file FILE Add input file (can repeat)
|
||||||
|
|
||||||
Snapshot options:
|
Snapshot options:
|
||||||
--list List all snapshots
|
--list List all snapshots
|
||||||
|
|
|
||||||
|
|
@ -705,9 +705,32 @@ string service_unlock(const string& service_id, const string& public_key, const
|
||||||
return exec_curl(cmd);
|
return exec_curl(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
string service_redeploy(const string& service_id, const string& bootstrap, const string& public_key, const string& secret_key) {
|
string service_redeploy(const string& service_id, const string& bootstrap, const vector<string>& input_files, const string& public_key, const string& secret_key) {
|
||||||
string path = "/services/" + service_id + "/redeploy";
|
string path = "/services/" + service_id + "/redeploy";
|
||||||
string body = bootstrap.empty() ? "{}" : "{\"bootstrap\":\"" + escape_json(bootstrap) + "\"}";
|
ostringstream json;
|
||||||
|
json << "{";
|
||||||
|
bool has_field = false;
|
||||||
|
if (!bootstrap.empty()) {
|
||||||
|
json << "\"bootstrap\":\"" << escape_json(bootstrap) << "\"";
|
||||||
|
has_field = true;
|
||||||
|
}
|
||||||
|
if (!input_files.empty()) {
|
||||||
|
if (has_field) json << ",";
|
||||||
|
json << "\"input_files\":[";
|
||||||
|
for (size_t i = 0; i < input_files.size(); i++) {
|
||||||
|
if (i > 0) json << ",";
|
||||||
|
ifstream file(input_files[i], ios::binary);
|
||||||
|
if (!file) continue;
|
||||||
|
ostringstream content;
|
||||||
|
content << file.rdbuf();
|
||||||
|
string b64 = base64_encode(content.str());
|
||||||
|
string filename = input_files[i].substr(input_files[i].find_last_of("/\\") + 1);
|
||||||
|
json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}";
|
||||||
|
}
|
||||||
|
json << "]";
|
||||||
|
}
|
||||||
|
json << "}";
|
||||||
|
string body = json.str();
|
||||||
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||||
"-H 'Content-Type: application/json' "
|
"-H 'Content-Type: application/json' "
|
||||||
|
|
@ -1202,7 +1225,7 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
|
||||||
cout << exec_curl(cmd) << endl;
|
cout << exec_curl(cmd) << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const vector<string>& envs, const string& env_file, const string& env_action, const string& env_target, const string& set_unfreeze_on_demand_id, int set_unfreeze_on_demand_enabled, int unfreeze_on_demand, const string& public_key, const string& secret_key) {
|
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& redeploy, const string& network, int vcpu, const vector<string>& envs, const string& env_file, const string& env_action, const string& env_target, const string& set_unfreeze_on_demand_id, int set_unfreeze_on_demand_enabled, int unfreeze_on_demand, const string& public_key, const string& secret_key) {
|
||||||
// Handle service env subcommand
|
// Handle service env subcommand
|
||||||
if (!env_action.empty()) {
|
if (!env_action.empty()) {
|
||||||
cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key);
|
cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key);
|
||||||
|
|
@ -1328,6 +1351,62 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!redeploy.empty()) {
|
||||||
|
// Bootstrap is optional for redeploy:
|
||||||
|
// - If provided via --bootstrap or --bootstrap-file, use it
|
||||||
|
// - If omitted, API will use the stored encrypted bootstrap
|
||||||
|
string bootstrap_to_use = bootstrap;
|
||||||
|
if (!bootstrap_file.empty()) {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(bootstrap_file.c_str(), &st) == 0) {
|
||||||
|
bootstrap_to_use = read_file(bootstrap_file);
|
||||||
|
} else {
|
||||||
|
cerr << RED << "Error: Bootstrap file not found: " << bootstrap_file << RESET << endl;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cout << YELLOW << "Redeploying service " << redeploy << "..." << RESET << endl;
|
||||||
|
ostringstream json;
|
||||||
|
json << "{";
|
||||||
|
bool has_field = false;
|
||||||
|
if (!bootstrap_to_use.empty()) {
|
||||||
|
if (!bootstrap_file.empty()) {
|
||||||
|
json << "\"bootstrap_content\":\"" << escape_json(bootstrap_to_use) << "\"";
|
||||||
|
} else {
|
||||||
|
json << "\"bootstrap\":\"" << escape_json(bootstrap_to_use) << "\"";
|
||||||
|
}
|
||||||
|
has_field = true;
|
||||||
|
}
|
||||||
|
if (!files.empty()) {
|
||||||
|
if (has_field) json << ",";
|
||||||
|
json << "\"input_files\":[";
|
||||||
|
for (size_t i = 0; i < files.size(); i++) {
|
||||||
|
if (i > 0) json << ",";
|
||||||
|
ifstream file(files[i], ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
cerr << RED << "Error: Input file not found: " << files[i] << RESET << endl;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
ostringstream content;
|
||||||
|
content << file.rdbuf();
|
||||||
|
string b64 = base64_encode(content.str());
|
||||||
|
string filename = files[i].substr(files[i].find_last_of("/\\") + 1);
|
||||||
|
json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}";
|
||||||
|
}
|
||||||
|
json << "]";
|
||||||
|
}
|
||||||
|
json << "}";
|
||||||
|
string path = "/services/" + redeploy + "/redeploy";
|
||||||
|
string auth_headers = build_auth_headers("POST", path, json.str(), public_key, secret_key);
|
||||||
|
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||||
|
"-H 'Content-Type: application/json' "
|
||||||
|
+ auth_headers + " "
|
||||||
|
"-d '" + json.str() + "'";
|
||||||
|
string result = exec_curl(cmd);
|
||||||
|
cout << result << endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!dump_bootstrap.empty()) {
|
if (!dump_bootstrap.empty()) {
|
||||||
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
|
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
|
||||||
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
|
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
|
||||||
|
|
@ -1778,7 +1857,7 @@ int main(int argc, char* argv[]) {
|
||||||
if (cmd_type == "service") {
|
if (cmd_type == "service") {
|
||||||
string name, ports, type, bootstrap, bootstrap_file;
|
string name, ports, type, bootstrap, bootstrap_file;
|
||||||
bool list = false;
|
bool list = false;
|
||||||
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network;
|
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, redeploy;
|
||||||
int vcpu = 0;
|
int vcpu = 0;
|
||||||
vector<string> files;
|
vector<string> files;
|
||||||
vector<string> envs;
|
vector<string> envs;
|
||||||
|
|
@ -1816,6 +1895,7 @@ int main(int argc, char* argv[]) {
|
||||||
else if (arg == "--resize" && i+1 < argc) resize = argv[++i];
|
else if (arg == "--resize" && i+1 < argc) resize = argv[++i];
|
||||||
else if (arg == "--execute" && i+1 < argc) execute = argv[++i];
|
else if (arg == "--execute" && i+1 < argc) execute = argv[++i];
|
||||||
else if (arg == "--command" && i+1 < argc) command = argv[++i];
|
else if (arg == "--command" && i+1 < argc) command = argv[++i];
|
||||||
|
else if (arg == "--redeploy" && i+1 < argc) redeploy = argv[++i];
|
||||||
else if (arg == "--dump-bootstrap" && i+1 < argc) dump_bootstrap = 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 == "--dump-file" && i+1 < argc) dump_file = argv[++i];
|
||||||
else if (arg == "-n" && i+1 < argc) network = argv[++i];
|
else if (arg == "-n" && i+1 < argc) network = argv[++i];
|
||||||
|
|
@ -1832,7 +1912,7 @@ int main(int argc, char* argv[]) {
|
||||||
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
|
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, vcpu, envs, env_file, env_action, env_target, set_unfreeze_on_demand_id, set_unfreeze_on_demand_enabled, unfreeze_on_demand, public_key, secret_key);
|
cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, redeploy, network, vcpu, envs, env_file, env_action, env_target, set_unfreeze_on_demand_id, set_unfreeze_on_demand_enabled, unfreeze_on_demand, public_key, secret_key);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,10 @@ class Un
|
||||||
{
|
{
|
||||||
CmdKey(parsedArgs);
|
CmdKey(parsedArgs);
|
||||||
}
|
}
|
||||||
|
else if (parsedArgs.Command == "languages")
|
||||||
|
{
|
||||||
|
CmdLanguages(parsedArgs);
|
||||||
|
}
|
||||||
else if (parsedArgs.SourceFile != null)
|
else if (parsedArgs.SourceFile != null)
|
||||||
{
|
{
|
||||||
CmdExecute(parsedArgs);
|
CmdExecute(parsedArgs);
|
||||||
|
|
@ -438,6 +442,32 @@ class Un
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.ServiceRedeploy != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.ServiceBootstrap != null)
|
||||||
|
{
|
||||||
|
payload["bootstrap"] = args.ServiceBootstrap;
|
||||||
|
}
|
||||||
|
if (args.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var filepath in args.Files)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllBytes(filepath);
|
||||||
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["filename"] = Path.GetFileName(filepath),
|
||||||
|
["content"] = Convert.ToBase64String(content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
|
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", "POST", payload.Count > 0 ? payload : null, publicKey, secretKey);
|
||||||
|
Console.WriteLine($"{GREEN}Service redeployed: {args.ServiceRedeploy}{RESET}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.ServiceExecute != null)
|
if (args.ServiceExecute != null)
|
||||||
{
|
{
|
||||||
var payload = new Dictionary<string, object>
|
var payload = new Dictionary<string, object>
|
||||||
|
|
@ -529,6 +559,20 @@ class Un
|
||||||
{
|
{
|
||||||
payload["unfreeze_on_demand"] = true;
|
payload["unfreeze_on_demand"] = true;
|
||||||
}
|
}
|
||||||
|
if (args.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var filepath in args.Files)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllBytes(filepath);
|
||||||
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["filename"] = Path.GetFileName(filepath),
|
||||||
|
["content"] = Convert.ToBase64String(content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
var result = ApiRequest("/services", "POST", payload, publicKey, secretKey);
|
var result = ApiRequest("/services", "POST", payload, publicKey, secretKey);
|
||||||
string serviceId = result.ContainsKey("id") ? (string)result["id"] : null;
|
string serviceId = result.ContainsKey("id") ? (string)result["id"] : null;
|
||||||
|
|
@ -1234,10 +1278,12 @@ class Un
|
||||||
public string ServiceShowFreezePage = null;
|
public string ServiceShowFreezePage = null;
|
||||||
public bool ServiceShowFreezePageEnabled = true;
|
public bool ServiceShowFreezePageEnabled = true;
|
||||||
public bool ServiceCreateUnfreezeOnDemand = false;
|
public bool ServiceCreateUnfreezeOnDemand = false;
|
||||||
|
public string ServiceRedeploy = null;
|
||||||
public string EnvFile = null;
|
public string EnvFile = null;
|
||||||
public string EnvAction = null;
|
public string EnvAction = null;
|
||||||
public string EnvTarget = null;
|
public string EnvTarget = null;
|
||||||
public bool KeyExtend = false;
|
public bool KeyExtend = false;
|
||||||
|
public bool LanguagesJson = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Args ParseArgs(string[] args)
|
static Args ParseArgs(string[] args)
|
||||||
|
|
@ -1249,6 +1295,7 @@ class Un
|
||||||
if (arg == "session") result.Command = "session";
|
if (arg == "session") result.Command = "session";
|
||||||
else if (arg == "service") result.Command = "service";
|
else if (arg == "service") result.Command = "service";
|
||||||
else if (arg == "key") result.Command = "key";
|
else if (arg == "key") result.Command = "key";
|
||||||
|
else if (arg == "languages") result.Command = "languages";
|
||||||
else if (arg == "env" && result.Command == "service")
|
else if (arg == "env" && result.Command == "service")
|
||||||
{
|
{
|
||||||
// Parse: service env <action> <target>
|
// Parse: service env <action> <target>
|
||||||
|
|
@ -1295,12 +1342,98 @@ class Un
|
||||||
else if (arg == "--show-freeze-page") result.ServiceShowFreezePage = args[++i];
|
else if (arg == "--show-freeze-page") result.ServiceShowFreezePage = args[++i];
|
||||||
else if (arg == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true";
|
else if (arg == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true";
|
||||||
else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true;
|
else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true;
|
||||||
|
else if (arg == "--redeploy") result.ServiceRedeploy = args[++i];
|
||||||
else if (arg == "--extend") result.KeyExtend = true;
|
else if (arg == "--extend") result.KeyExtend = true;
|
||||||
|
else if (arg == "--json") result.LanguagesJson = true;
|
||||||
else if (!arg.StartsWith("-")) result.SourceFile = arg;
|
else if (!arg.StartsWith("-")) result.SourceFile = arg;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static string GetLanguagesCachePath()
|
||||||
|
{
|
||||||
|
string home = Environment.GetEnvironmentVariable("HOME")
|
||||||
|
?? Environment.GetEnvironmentVariable("USERPROFILE")
|
||||||
|
?? ".";
|
||||||
|
return Path.Combine(home, ".unsandbox", "languages.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<string> LoadLanguagesCache()
|
||||||
|
{
|
||||||
|
string cachePath = GetLanguagesCachePath();
|
||||||
|
if (!File.Exists(cachePath)) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string content = File.ReadAllText(cachePath);
|
||||||
|
double mtime = new DateTimeOffset(File.GetLastWriteTimeUtc(cachePath)).ToUnixTimeSeconds();
|
||||||
|
double now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
if (now - mtime < 3600)
|
||||||
|
{
|
||||||
|
var data = ParseJson(content);
|
||||||
|
if (data.ContainsKey("languages") && data["languages"] is List<object> langs)
|
||||||
|
return langs.ConvertAll(x => x.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SaveLanguagesCache(List<string> languages)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string cachePath = GetLanguagesCachePath();
|
||||||
|
string cacheDir = Path.GetDirectoryName(cachePath);
|
||||||
|
if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir);
|
||||||
|
|
||||||
|
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append("{\"languages\":[");
|
||||||
|
for (int i = 0; i < languages.Count; i++)
|
||||||
|
{
|
||||||
|
if (i > 0) sb.Append(",");
|
||||||
|
sb.Append("\"").Append(languages[i]).Append("\"");
|
||||||
|
}
|
||||||
|
sb.Append("],\"timestamp\":").Append(timestamp).Append("}");
|
||||||
|
File.WriteAllText(cachePath, sb.ToString());
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CmdLanguages(Args args)
|
||||||
|
{
|
||||||
|
// Try cache first
|
||||||
|
var languages = LoadLanguagesCache();
|
||||||
|
|
||||||
|
if (languages == null)
|
||||||
|
{
|
||||||
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||||
|
var result = ApiRequest("/languages", "GET", null, publicKey, secretKey);
|
||||||
|
languages = new List<string>();
|
||||||
|
if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
|
||||||
|
languages = langs.ConvertAll(x => x.ToString());
|
||||||
|
SaveLanguagesCache(languages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.LanguagesJson)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < languages.Count; i++)
|
||||||
|
{
|
||||||
|
if (i > 0) sb.Append(",");
|
||||||
|
sb.Append("\"").Append(languages[i]).Append("\"");
|
||||||
|
}
|
||||||
|
sb.Append("]");
|
||||||
|
Console.WriteLine(sb.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var lang in languages)
|
||||||
|
Console.WriteLine(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void PrintHelp()
|
static void PrintHelp()
|
||||||
{
|
{
|
||||||
Console.WriteLine(@"Usage: Un [options] <source_file>
|
Console.WriteLine(@"Usage: Un [options] <source_file>
|
||||||
|
|
@ -1308,6 +1441,7 @@ class Un
|
||||||
Un service [options]
|
Un service [options]
|
||||||
Un service env <action> <service_id> [options]
|
Un service env <action> <service_id> [options]
|
||||||
Un key [options]
|
Un key [options]
|
||||||
|
Un languages [--json]
|
||||||
|
|
||||||
Execute options:
|
Execute options:
|
||||||
-e KEY=VALUE Set environment variable
|
-e KEY=VALUE Set environment variable
|
||||||
|
|
@ -1340,6 +1474,7 @@ Service options:
|
||||||
--show-freeze-page-enabled BOOL Enable/disable (default: true)
|
--show-freeze-page-enabled BOOL Enable/disable (default: true)
|
||||||
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
|
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
|
||||||
--destroy ID Destroy service
|
--destroy ID Destroy service
|
||||||
|
--redeploy ID Re-run bootstrap (with optional --bootstrap, -f)
|
||||||
--execute ID Execute command in service
|
--execute ID Execute command in service
|
||||||
--command CMD Command to execute (with --execute)
|
--command CMD Command to execute (with --execute)
|
||||||
--dump-bootstrap ID Dump bootstrap script
|
--dump-bootstrap ID Dump bootstrap script
|
||||||
|
|
@ -1354,7 +1489,10 @@ Service env commands:
|
||||||
env delete ID Delete vault
|
env delete ID Delete vault
|
||||||
|
|
||||||
Key options:
|
Key options:
|
||||||
--extend Open browser to extend expired key");
|
--extend Open browser to extend expired key
|
||||||
|
|
||||||
|
Languages options:
|
||||||
|
--json Output as JSON array");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1486,15 +1624,76 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return new List<JobInfo>(); }
|
catch (Exception ex) { _lastError = ex.Message; return new List<JobInfo>(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get available programming languages</summary>
|
private const int LANGUAGES_CACHE_TTL = 3600; // 1 hour
|
||||||
|
|
||||||
|
private static string GetLanguagesCachePath()
|
||||||
|
{
|
||||||
|
string home = Environment.GetEnvironmentVariable("HOME")
|
||||||
|
?? Environment.GetEnvironmentVariable("USERPROFILE")
|
||||||
|
?? ".";
|
||||||
|
return Path.Combine(home, ".unsandbox", "languages.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> LoadLanguagesCache()
|
||||||
|
{
|
||||||
|
string cachePath = GetLanguagesCachePath();
|
||||||
|
if (!File.Exists(cachePath)) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string content = File.ReadAllText(cachePath);
|
||||||
|
double mtime = new DateTimeOffset(File.GetLastWriteTimeUtc(cachePath)).ToUnixTimeSeconds();
|
||||||
|
double now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
if (now - mtime < LANGUAGES_CACHE_TTL)
|
||||||
|
{
|
||||||
|
var data = ParseJson(content);
|
||||||
|
if (data.ContainsKey("languages") && data["languages"] is List<object> langs)
|
||||||
|
return langs.ConvertAll(x => x.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveLanguagesCache(List<string> languages)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string cachePath = GetLanguagesCachePath();
|
||||||
|
string cacheDir = Path.GetDirectoryName(cachePath);
|
||||||
|
if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir);
|
||||||
|
|
||||||
|
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append("{\"languages\":[");
|
||||||
|
for (int i = 0; i < languages.Count; i++)
|
||||||
|
{
|
||||||
|
if (i > 0) sb.Append(",");
|
||||||
|
sb.Append("\"").Append(languages[i]).Append("\"");
|
||||||
|
}
|
||||||
|
sb.Append("],\"timestamp\":").Append(timestamp).Append("}");
|
||||||
|
File.WriteAllText(cachePath, sb.ToString());
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get available programming languages (cached for 1 hour)</summary>
|
||||||
public static List<string> GetLanguages(string publicKey = null, string secretKey = null)
|
public static List<string> GetLanguages(string publicKey = null, string secretKey = null)
|
||||||
{
|
{
|
||||||
|
// Try cache first
|
||||||
|
var cached = LoadLanguagesCache();
|
||||||
|
if (cached != null) return cached;
|
||||||
|
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ApiCall("/languages", "GET", null, pk, sk);
|
var result = ApiCall("/languages", "GET", null, pk, sk);
|
||||||
if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
|
if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
|
||||||
return langs.ConvertAll(x => x.ToString());
|
{
|
||||||
|
var languages = langs.ConvertAll(x => x.ToString());
|
||||||
|
SaveLanguagesCache(languages);
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
}
|
}
|
||||||
catch (Exception ex) { _lastError = ex.Message; return new List<string>(); }
|
catch (Exception ex) { _lastError = ex.Message; return new List<string>(); }
|
||||||
|
|
@ -1632,7 +1831,7 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, string publicKey = null, string secretKey = null)
|
public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, List<Dictionary<string, string>> inputFiles = null, string publicKey = null, string secretKey = null)
|
||||||
{
|
{
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
var payload = new Dictionary<string, object> { ["name"] = name };
|
var payload = new Dictionary<string, object> { ["name"] = name };
|
||||||
|
|
@ -1645,6 +1844,7 @@ public static class Unsandbox
|
||||||
if (domains != null) payload["domains"] = domains;
|
if (domains != null) payload["domains"] = domains;
|
||||||
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||||
if (networkMode != null) payload["network"] = networkMode;
|
if (networkMode != null) payload["network"] = networkMode;
|
||||||
|
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ApiCall("/services", "POST", payload, pk, sk);
|
var result = ApiCall("/services", "POST", payload, pk, sk);
|
||||||
|
|
@ -1696,10 +1896,16 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ServiceRedeploy(string serviceId, string bootstrap = null, string publicKey = null, string secretKey = null)
|
public static bool ServiceRedeploy(string serviceId, string bootstrap = null, List<Dictionary<string, string>> inputFiles = null, string publicKey = null, string secretKey = null)
|
||||||
{
|
{
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null;
|
Dictionary<string, object> payload = null;
|
||||||
|
if (bootstrap != null || (inputFiles != null && inputFiles.Count > 0))
|
||||||
|
{
|
||||||
|
payload = new Dictionary<string, object>();
|
||||||
|
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||||
|
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
try { ApiCall($"/services/{serviceId}/redeploy", "POST", payload, pk, sk); return true; }
|
try { ApiCall($"/services/{serviceId}/redeploy", "POST", payload, pk, sk); return true; }
|
||||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,22 @@ void CmdService(Args args)
|
||||||
|
|
||||||
if (args.ServiceRedeploy != null)
|
if (args.ServiceRedeploy != null)
|
||||||
{
|
{
|
||||||
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, null, publicKey, secretKey);
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var filepath in args.Files)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllBytes(filepath);
|
||||||
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["filename"] = Path.GetFileName(filepath),
|
||||||
|
["content"] = Convert.ToBase64String(content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
|
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, payload.Count > 0 ? payload : null, publicKey, secretKey);
|
||||||
Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}");
|
Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -452,6 +467,20 @@ void CmdService(Args args)
|
||||||
if (args.Network != null) payload["network"] = args.Network;
|
if (args.Network != null) payload["network"] = args.Network;
|
||||||
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
||||||
if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true;
|
if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true;
|
||||||
|
if (args.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var filepath in args.Files)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllBytes(filepath);
|
||||||
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["filename"] = Path.GetFileName(filepath),
|
||||||
|
["content"] = Convert.ToBase64String(content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey);
|
var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
|
var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
|
||||||
|
|
@ -1085,7 +1114,7 @@ Service options:
|
||||||
--lock ID Prevent deletion
|
--lock ID Prevent deletion
|
||||||
--unlock ID Allow deletion
|
--unlock ID Allow deletion
|
||||||
--resize ID Resize (use with -v)
|
--resize ID Resize (use with -v)
|
||||||
--redeploy ID Re-run bootstrap
|
--redeploy ID Re-run bootstrap (use -f to include input files)
|
||||||
--snapshot ID Create snapshot from service
|
--snapshot ID Create snapshot from service
|
||||||
--unfreeze-on-demand ID Set unfreeze-on-demand for service
|
--unfreeze-on-demand ID Set unfreeze-on-demand for service
|
||||||
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
|
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
|
||||||
|
|
@ -1098,6 +1127,7 @@ Service options:
|
||||||
--dump-bootstrap ID Dump bootstrap script
|
--dump-bootstrap ID Dump bootstrap script
|
||||||
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
||||||
-e KEY=VALUE Set vault env var (with --name or env set)
|
-e KEY=VALUE Set vault env var (with --name or env set)
|
||||||
|
-f FILE Add input file (with --name or --redeploy)
|
||||||
--env-file FILE Load vault vars from file
|
--env-file FILE Load vault vars from file
|
||||||
|
|
||||||
Service env commands:
|
Service env commands:
|
||||||
|
|
@ -1406,7 +1436,7 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
catch (Exception ex) { _lastError = ex.Message; return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, string? publicKey = null, string? secretKey = null)
|
public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, List<Dictionary<string, string>>? inputFiles = null, string? publicKey = null, string? secretKey = null)
|
||||||
{
|
{
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
var payload = new Dictionary<string, object> { ["name"] = name };
|
var payload = new Dictionary<string, object> { ["name"] = name };
|
||||||
|
|
@ -1414,6 +1444,7 @@ public static class Unsandbox
|
||||||
if (domains != null) payload["domains"] = domains;
|
if (domains != null) payload["domains"] = domains;
|
||||||
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||||
if (networkMode != null) payload["network"] = networkMode;
|
if (networkMode != null) payload["network"] = networkMode;
|
||||||
|
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk);
|
var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk);
|
||||||
|
|
@ -1465,10 +1496,16 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, string? publicKey = null, string? secretKey = null)
|
public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, List<Dictionary<string, string>>? inputFiles = null, string? publicKey = null, string? secretKey = null)
|
||||||
{
|
{
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null;
|
Dictionary<string, object>? payload = null;
|
||||||
|
if (bootstrap != null || inputFiles != null)
|
||||||
|
{
|
||||||
|
payload = new Dictionary<string, object>();
|
||||||
|
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||||
|
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; }
|
try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; }
|
||||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -840,12 +841,19 @@ func ShellSession(creds *Credentials, sessionID, command string) (map[string]int
|
||||||
// Service Operations
|
// Service Operations
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// InputFile represents a file to upload with a service create or redeploy.
|
||||||
|
type InputFile struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Content string `json:"content"` // base64-encoded
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceOptions contains optional parameters for service creation.
|
// ServiceOptions contains optional parameters for service creation.
|
||||||
type ServiceOptions struct {
|
type ServiceOptions struct {
|
||||||
NetworkMode string // "zerotrust" (default) or "semitrusted"
|
NetworkMode string // "zerotrust" (default) or "semitrusted"
|
||||||
Shell string // Shell to use for bootstrap
|
Shell string // Shell to use for bootstrap
|
||||||
VCPU int // Number of virtual CPUs
|
VCPU int // Number of virtual CPUs
|
||||||
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
|
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
|
||||||
|
InputFiles []InputFile // Files to include (written to /tmp/ in container)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceUpdateOptions contains optional parameters for service updates.
|
// ServiceUpdateOptions contains optional parameters for service updates.
|
||||||
|
|
@ -902,6 +910,9 @@ func CreateService(creds *Credentials, name string, ports []int, bootstrap strin
|
||||||
if opts.UnfreezeOnDemand {
|
if opts.UnfreezeOnDemand {
|
||||||
data["unfreeze_on_demand"] = true
|
data["unfreeze_on_demand"] = true
|
||||||
}
|
}
|
||||||
|
if len(opts.InputFiles) > 0 {
|
||||||
|
data["input_files"] = opts.InputFiles
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeRequest("POST", "/services", creds, data)
|
return makeRequest("POST", "/services", creds, data)
|
||||||
|
|
@ -1017,11 +1028,15 @@ func ExportServiceEnv(creds *Credentials, serviceID string) (map[string]interfac
|
||||||
// creds: API credentials
|
// creds: API credentials
|
||||||
// serviceID: Service ID
|
// serviceID: Service ID
|
||||||
// bootstrap: New bootstrap script (empty string to keep existing)
|
// bootstrap: New bootstrap script (empty string to keep existing)
|
||||||
func RedeployService(creds *Credentials, serviceID string, bootstrap string) (map[string]interface{}, error) {
|
// inputFiles: Optional files to include (written to /tmp/ in container)
|
||||||
|
func RedeployService(creds *Credentials, serviceID string, bootstrap string, inputFiles []InputFile) (map[string]interface{}, error) {
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
if bootstrap != "" {
|
if bootstrap != "" {
|
||||||
data["bootstrap"] = bootstrap
|
data["bootstrap"] = bootstrap
|
||||||
}
|
}
|
||||||
|
if len(inputFiles) > 0 {
|
||||||
|
data["input_files"] = inputFiles
|
||||||
|
}
|
||||||
return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
|
return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1899,6 +1914,22 @@ func readFileContents(path string) (string, error) {
|
||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildInputFiles reads files from paths and returns InputFile structs with base64-encoded content.
|
||||||
|
func buildInputFiles(paths []string) ([]InputFile, error) {
|
||||||
|
var files []InputFile
|
||||||
|
for _, fpath := range paths {
|
||||||
|
data, err := os.ReadFile(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read input file %s: %w", fpath, err)
|
||||||
|
}
|
||||||
|
files = append(files, InputFile{
|
||||||
|
Filename: filepath.Base(fpath),
|
||||||
|
Content: base64.StdEncoding.EncodeToString(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
// readEnvFile reads environment variables from a .env file
|
// readEnvFile reads environment variables from a .env file
|
||||||
func readEnvFile(path string) (map[string]string, error) {
|
func readEnvFile(path string) (map[string]string, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
|
|
@ -2353,7 +2384,15 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
|
||||||
|
|
||||||
// Redeploy service
|
// Redeploy service
|
||||||
if fs.redeploy != "" {
|
if fs.redeploy != "" {
|
||||||
_, err := RedeployService(creds, fs.redeploy, fs.bootstrap)
|
var inputFiles []InputFile
|
||||||
|
if len(opts.Files) > 0 {
|
||||||
|
var err error
|
||||||
|
inputFiles, err = buildInputFiles(opts.Files)
|
||||||
|
if err != nil {
|
||||||
|
return cliError(err.Error(), ExitGeneralError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := RedeployService(creds, fs.redeploy, fs.bootstrap, inputFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliError(err.Error(), ExitAPIError)
|
return cliError(err.Error(), ExitAPIError)
|
||||||
}
|
}
|
||||||
|
|
@ -2412,6 +2451,13 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
|
||||||
if opts.VCPU > 0 {
|
if opts.VCPU > 0 {
|
||||||
serviceOpts.VCPU = opts.VCPU
|
serviceOpts.VCPU = opts.VCPU
|
||||||
}
|
}
|
||||||
|
if len(opts.Files) > 0 {
|
||||||
|
inputFiles, err := buildInputFiles(opts.Files)
|
||||||
|
if err != nil {
|
||||||
|
return cliError(err.Error(), ExitGeneralError)
|
||||||
|
}
|
||||||
|
serviceOpts.InputFiles = inputFiles
|
||||||
|
}
|
||||||
|
|
||||||
service, err := CreateService(creds, fs.name, ports, bootstrap, serviceOpts)
|
service, err := CreateService(creds, fs.name, ports, bootstrap, serviceOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1493,6 +1493,31 @@ public class Un {
|
||||||
String bootstrap,
|
String bootstrap,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
String secretKey
|
String secretKey
|
||||||
|
) throws IOException {
|
||||||
|
return createService(name, ports, bootstrap, null, publicKey, secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new service (long-running container) with optional input files.
|
||||||
|
*
|
||||||
|
* @param name Service name
|
||||||
|
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
|
||||||
|
* @param bootstrap Bootstrap script or URL to run on service creation
|
||||||
|
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
|
||||||
|
* @param publicKey Optional API key
|
||||||
|
* @param secretKey Optional API secret
|
||||||
|
* @return Response map containing service_id
|
||||||
|
* @throws IOException on network errors
|
||||||
|
* @throws CredentialsException if credentials cannot be found
|
||||||
|
* @throws ApiException if API returns an error
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> createService(
|
||||||
|
String name,
|
||||||
|
String ports,
|
||||||
|
String bootstrap,
|
||||||
|
List<Map<String, String>> inputFiles,
|
||||||
|
String publicKey,
|
||||||
|
String secretKey
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||||
|
|
||||||
|
|
@ -1517,6 +1542,9 @@ public class Un {
|
||||||
data.put("bootstrap", bootstrap);
|
data.put("bootstrap", bootstrap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (inputFiles != null && !inputFiles.isEmpty()) {
|
||||||
|
data.put("input_files", inputFiles);
|
||||||
|
}
|
||||||
|
|
||||||
return makeRequest("POST", "/services", creds[0], creds[1], data);
|
return makeRequest("POST", "/services", creds[0], creds[1], data);
|
||||||
}
|
}
|
||||||
|
|
@ -1542,6 +1570,33 @@ public class Un {
|
||||||
boolean unfreezeOnDemand,
|
boolean unfreezeOnDemand,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
String secretKey
|
String secretKey
|
||||||
|
) throws IOException {
|
||||||
|
return createService(name, ports, bootstrap, unfreezeOnDemand, null, publicKey, secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new service (long-running container) with unfreeze-on-demand option and input files.
|
||||||
|
*
|
||||||
|
* @param name Service name (used for hostname)
|
||||||
|
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
|
||||||
|
* @param bootstrap Bootstrap script or URL to run on service creation
|
||||||
|
* @param unfreezeOnDemand If true, frozen service will auto-wake on HTTP request
|
||||||
|
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
|
||||||
|
* @param publicKey Optional API key
|
||||||
|
* @param secretKey Optional API secret
|
||||||
|
* @return Response map containing service_id
|
||||||
|
* @throws IOException on network errors
|
||||||
|
* @throws CredentialsException if credentials cannot be found
|
||||||
|
* @throws ApiException if API returns an error
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> createService(
|
||||||
|
String name,
|
||||||
|
String ports,
|
||||||
|
String bootstrap,
|
||||||
|
boolean unfreezeOnDemand,
|
||||||
|
List<Map<String, String>> inputFiles,
|
||||||
|
String publicKey,
|
||||||
|
String secretKey
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||||
|
|
||||||
|
|
@ -1569,6 +1624,9 @@ public class Un {
|
||||||
if (unfreezeOnDemand) {
|
if (unfreezeOnDemand) {
|
||||||
data.put("unfreeze_on_demand", true);
|
data.put("unfreeze_on_demand", true);
|
||||||
}
|
}
|
||||||
|
if (inputFiles != null && !inputFiles.isEmpty()) {
|
||||||
|
data.put("input_files", inputFiles);
|
||||||
|
}
|
||||||
|
|
||||||
return makeRequest("POST", "/services", creds[0], creds[1], data);
|
return makeRequest("POST", "/services", creds[0], creds[1], data);
|
||||||
}
|
}
|
||||||
|
|
@ -1901,9 +1959,34 @@ public class Un {
|
||||||
String serviceId,
|
String serviceId,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
String secretKey
|
String secretKey
|
||||||
|
) throws IOException {
|
||||||
|
return redeployService(serviceId, null, publicKey, secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redeploy a service (re-run bootstrap script) with optional input files.
|
||||||
|
*
|
||||||
|
* @param serviceId Service ID to redeploy
|
||||||
|
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
|
||||||
|
* @param publicKey Optional API key
|
||||||
|
* @param secretKey Optional API secret
|
||||||
|
* @return Response map with redeploy confirmation
|
||||||
|
* @throws IOException on network errors
|
||||||
|
* @throws CredentialsException if credentials cannot be found
|
||||||
|
* @throws ApiException if API returns an error
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> redeployService(
|
||||||
|
String serviceId,
|
||||||
|
List<Map<String, String>> inputFiles,
|
||||||
|
String publicKey,
|
||||||
|
String secretKey
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||||
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>());
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
if (inputFiles != null && !inputFiles.isEmpty()) {
|
||||||
|
data.put("input_files", inputFiles);
|
||||||
|
}
|
||||||
|
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2859,7 +2942,7 @@ public class Un {
|
||||||
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
|
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
|
||||||
break;
|
break;
|
||||||
case "service":
|
case "service":
|
||||||
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars);
|
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars, files);
|
||||||
break;
|
break;
|
||||||
case "snapshot":
|
case "snapshot":
|
||||||
handleSnapshot(positionalArgs, publicKey, secretKey);
|
handleSnapshot(positionalArgs, publicKey, secretKey);
|
||||||
|
|
@ -3181,7 +3264,8 @@ public class Un {
|
||||||
String secretKey,
|
String secretKey,
|
||||||
String networkMode,
|
String networkMode,
|
||||||
int vcpu,
|
int vcpu,
|
||||||
List<String> envVars
|
List<String> envVars,
|
||||||
|
List<String> files
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
// Check for "env" subcommand
|
// Check for "env" subcommand
|
||||||
if (args.size() > 1 && args.get(1).equals("env")) {
|
if (args.size() > 1 && args.get(1).equals("env")) {
|
||||||
|
|
@ -3347,14 +3431,17 @@ public class Un {
|
||||||
System.err.print(stderr);
|
System.err.print(stderr);
|
||||||
}
|
}
|
||||||
} else if (redeployId != null) {
|
} else if (redeployId != null) {
|
||||||
redeployService(redeployId, publicKey, secretKey);
|
// Build input_files from -f args
|
||||||
|
List<Map<String, String>> inputFiles = buildInputFiles(files);
|
||||||
|
redeployService(redeployId, inputFiles, publicKey, secretKey);
|
||||||
System.out.println("Service redeployed: " + redeployId);
|
System.out.println("Service redeployed: " + redeployId);
|
||||||
} else if (snapshotId != null) {
|
} else if (snapshotId != null) {
|
||||||
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
|
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
|
||||||
System.out.println("Snapshot created: " + snapId);
|
System.out.println("Snapshot created: " + snapId);
|
||||||
} else if (name != null) {
|
} else if (name != null) {
|
||||||
// Create new service
|
// Build input_files from -f args
|
||||||
Map<String, Object> result = createService(name, ports, bootstrap, publicKey, secretKey);
|
List<Map<String, String>> inputFiles = buildInputFiles(files);
|
||||||
|
Map<String, Object> result = createService(name, ports, bootstrap, inputFiles, publicKey, secretKey);
|
||||||
System.out.println("Service created:");
|
System.out.println("Service created:");
|
||||||
printMap(result);
|
printMap(result);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3363,6 +3450,26 @@ public class Un {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build input_files list from -f file paths: read each file, base64-encode, return list of maps.
|
||||||
|
*/
|
||||||
|
private static List<Map<String, String>> buildInputFiles(List<String> filePaths) throws IOException {
|
||||||
|
if (filePaths == null || filePaths.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Map<String, String>> inputFiles = new ArrayList<>();
|
||||||
|
for (String fpath : filePaths) {
|
||||||
|
Path p = Paths.get(fpath);
|
||||||
|
byte[] content = Files.readAllBytes(p);
|
||||||
|
String encoded = Base64.getEncoder().encodeToString(content);
|
||||||
|
Map<String, String> entry = new LinkedHashMap<>();
|
||||||
|
entry.put("filename", p.getFileName().toString());
|
||||||
|
entry.put("content", encoded);
|
||||||
|
inputFiles.add(entry);
|
||||||
|
}
|
||||||
|
return inputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
private static void handleServiceEnv(
|
private static void handleServiceEnv(
|
||||||
List<String> args,
|
List<String> args,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
|
|
|
||||||
|
|
@ -1093,6 +1093,7 @@ async function listServices(publicKey, secretKey) {
|
||||||
* - domains: Array of custom domains
|
* - domains: Array of custom domains
|
||||||
* - serviceType: Service type for SRV records (minecraft, mumble, etc.)
|
* - serviceType: Service type for SRV records (minecraft, mumble, etc.)
|
||||||
* - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic
|
* - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic
|
||||||
|
* - inputFiles: Array of {filename, content} objects (content is base64-encoded)
|
||||||
*
|
*
|
||||||
* Returns: Promise<Object> (service info with service_id)
|
* Returns: Promise<Object> (service info with service_id)
|
||||||
*/
|
*/
|
||||||
|
|
@ -1114,6 +1115,7 @@ async function createService(name, ports, bootstrap, opts = {}, publicKey, secre
|
||||||
if (opts.domains) data.custom_domains = opts.domains;
|
if (opts.domains) data.custom_domains = opts.domains;
|
||||||
if (opts.serviceType) data.service_type = opts.serviceType;
|
if (opts.serviceType) data.service_type = opts.serviceType;
|
||||||
if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true;
|
if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true;
|
||||||
|
if (opts.inputFiles && opts.inputFiles.length > 0) data.input_files = opts.inputFiles;
|
||||||
|
|
||||||
return makeRequest('POST', '/services', publicKey, secretKey, data);
|
return makeRequest('POST', '/services', publicKey, secretKey, data);
|
||||||
}
|
}
|
||||||
|
|
@ -1331,10 +1333,11 @@ async function exportServiceEnv(serviceId, publicKey, secretKey) {
|
||||||
* Args:
|
* Args:
|
||||||
* serviceId: Service ID to redeploy
|
* serviceId: Service ID to redeploy
|
||||||
* bootstrap: Optional new bootstrap script content or URL
|
* bootstrap: Optional new bootstrap script content or URL
|
||||||
|
* inputFiles: Optional array of {filename, content} objects (content is base64-encoded)
|
||||||
*
|
*
|
||||||
* Returns: Promise<Object> (redeploy confirmation)
|
* Returns: Promise<Object> (redeploy confirmation)
|
||||||
*/
|
*/
|
||||||
async function redeployService(serviceId, bootstrap = null, publicKey, secretKey) {
|
async function redeployService(serviceId, bootstrap = null, inputFiles = null, publicKey, secretKey) {
|
||||||
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
|
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
|
||||||
const data = {};
|
const data = {};
|
||||||
if (bootstrap) {
|
if (bootstrap) {
|
||||||
|
|
@ -1344,6 +1347,7 @@ async function redeployService(serviceId, bootstrap = null, publicKey, secretKey
|
||||||
data.bootstrap_content = bootstrap;
|
data.bootstrap_content = bootstrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (inputFiles && inputFiles.length > 0) data.input_files = inputFiles;
|
||||||
return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data);
|
return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2146,7 +2150,7 @@ SERVICE COMMANDS:
|
||||||
node un.js service --lock <id> Prevent deletion
|
node un.js service --lock <id> Prevent deletion
|
||||||
node un.js service --unlock <id> Allow deletion
|
node un.js service --unlock <id> Allow deletion
|
||||||
node un.js service --execute <id> <cmd> Run command in service
|
node un.js service --execute <id> <cmd> Run command in service
|
||||||
node un.js service --redeploy <id> Re-run bootstrap
|
node un.js service --redeploy <id> Re-run bootstrap (supports -f)
|
||||||
node un.js service --snapshot <id> Create snapshot
|
node un.js service --snapshot <id> Create snapshot
|
||||||
|
|
||||||
SERVICE ENV COMMANDS:
|
SERVICE ENV COMMANDS:
|
||||||
|
|
@ -2732,7 +2736,19 @@ async function handleService(opts) {
|
||||||
if (opts.bootstrapFile) {
|
if (opts.bootstrapFile) {
|
||||||
bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8');
|
bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8');
|
||||||
}
|
}
|
||||||
await redeployService(opts.redeploy, bootstrap, pk, sk);
|
// Build input_files from -f args
|
||||||
|
let inputFiles = null;
|
||||||
|
if (opts.files && opts.files.length > 0) {
|
||||||
|
inputFiles = [];
|
||||||
|
for (const fpath of opts.files) {
|
||||||
|
const content = fs.readFileSync(fpath);
|
||||||
|
inputFiles.push({
|
||||||
|
filename: path.basename(fpath),
|
||||||
|
content: content.toString('base64'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await redeployService(opts.redeploy, bootstrap, inputFiles, pk, sk);
|
||||||
console.log(`Service ${opts.redeploy} redeployed.`);
|
console.log(`Service ${opts.redeploy} redeployed.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2786,6 +2802,17 @@ async function handleService(opts) {
|
||||||
if (opts.type) {
|
if (opts.type) {
|
||||||
serviceOpts.serviceType = opts.type;
|
serviceOpts.serviceType = opts.type;
|
||||||
}
|
}
|
||||||
|
// Build input_files from -f args
|
||||||
|
if (opts.files && opts.files.length > 0) {
|
||||||
|
serviceOpts.inputFiles = [];
|
||||||
|
for (const fpath of opts.files) {
|
||||||
|
const content = fs.readFileSync(fpath);
|
||||||
|
serviceOpts.inputFiles.push({
|
||||||
|
filename: path.basename(fpath),
|
||||||
|
content: content.toString('base64'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const service = await createService(opts.name, ports, bootstrap, serviceOpts, pk, sk);
|
const service = await createService(opts.name, ports, bootstrap, serviceOpts, pk, sk);
|
||||||
console.log(`Service created: ${service.service_id}`);
|
console.log(`Service created: ${service.service_id}`);
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,7 @@ sub service_redeploy {
|
||||||
my ($service_id, %opts) = @_;
|
my ($service_id, %opts) = @_;
|
||||||
my $body = {};
|
my $body = {};
|
||||||
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
|
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
|
||||||
|
$body->{input_files} = $opts{input_files} if $opts{input_files};
|
||||||
return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
|
return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -998,7 +999,12 @@ sub cmd_service {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($options->{redeploy}) {
|
if ($options->{redeploy}) {
|
||||||
Un::service_redeploy($options->{redeploy}, bootstrap => $options->{bootstrap});
|
my %opts;
|
||||||
|
$opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};
|
||||||
|
if ($options->{files} && @{$options->{files}}) {
|
||||||
|
$opts{input_files} = build_input_files(@{$options->{files}});
|
||||||
|
}
|
||||||
|
Un::service_redeploy($options->{redeploy}, %opts);
|
||||||
print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
|
print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1339,13 +1339,18 @@ class Unsandbox {
|
||||||
* @param string $serviceId Service ID
|
* @param string $serviceId Service ID
|
||||||
* @param string|null $publicKey Optional API key
|
* @param string|null $publicKey Optional API key
|
||||||
* @param string|null $secretKey Optional API secret
|
* @param string|null $secretKey Optional API secret
|
||||||
|
* @param array $opts Optional parameters: 'input_files'
|
||||||
* @return array Response array with redeploy confirmation
|
* @return array Response array with redeploy confirmation
|
||||||
* @throws CredentialsException Missing credentials
|
* @throws CredentialsException Missing credentials
|
||||||
* @throws ApiException API request failed
|
* @throws ApiException API request failed
|
||||||
*/
|
*/
|
||||||
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
|
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null, array $opts = []): array {
|
||||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||||
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, []);
|
$data = [];
|
||||||
|
if (isset($opts['input_files'])) {
|
||||||
|
$data['input_files'] = $opts['input_files'];
|
||||||
|
}
|
||||||
|
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2603,7 +2608,31 @@ class Unsandbox {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($serviceOpts['redeploy']) {
|
if ($serviceOpts['redeploy']) {
|
||||||
$result = $this->redeployService($serviceOpts['redeploy']);
|
$redeployOpts = [];
|
||||||
|
$inputFiles = [];
|
||||||
|
foreach ($opts['files'] as $filepath) {
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
$inputFiles[] = [
|
||||||
|
'name' => basename($filepath),
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($opts['files_path'] as $filepath) {
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
$inputFiles[] = [
|
||||||
|
'name' => $filepath,
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
'preserve_path' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($inputFiles)) {
|
||||||
|
$redeployOpts['input_files'] = $inputFiles;
|
||||||
|
}
|
||||||
|
$result = $this->redeployService($serviceOpts['redeploy'], null, null, $redeployOpts);
|
||||||
echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n";
|
echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2647,6 +2676,31 @@ class Unsandbox {
|
||||||
$createOpts['unfreeze_on_demand'] = true;
|
$createOpts['unfreeze_on_demand'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle input files
|
||||||
|
$inputFiles = [];
|
||||||
|
foreach ($opts['files'] as $filepath) {
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
$inputFiles[] = [
|
||||||
|
'name' => basename($filepath),
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($opts['files_path'] as $filepath) {
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
$inputFiles[] = [
|
||||||
|
'name' => $filepath,
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
'preserve_path' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($inputFiles)) {
|
||||||
|
$createOpts['input_files'] = $inputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle bootstrap from file
|
// Handle bootstrap from file
|
||||||
$bootstrap = $serviceOpts['bootstrap'] ?? '';
|
$bootstrap = $serviceOpts['bootstrap'] ?? '';
|
||||||
if (!empty($serviceOpts['bootstrap_file'])) {
|
if (!empty($serviceOpts['bootstrap_file'])) {
|
||||||
|
|
@ -3438,7 +3492,7 @@ SERVICE OPTIONS:
|
||||||
--lock ID Prevent service deletion
|
--lock ID Prevent service deletion
|
||||||
--unlock ID Allow service deletion
|
--unlock ID Allow service deletion
|
||||||
--resize ID Resize service (with --vcpu)
|
--resize ID Resize service (with --vcpu)
|
||||||
--redeploy ID Re-run bootstrap
|
--redeploy ID Re-run bootstrap (supports -f/-F for input files)
|
||||||
--execute ID 'cmd' Execute command in service
|
--execute ID 'cmd' Execute command in service
|
||||||
--snapshot ID Create service snapshot
|
--snapshot ID Create service snapshot
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ require 'openssl'
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
require 'optparse'
|
require 'optparse'
|
||||||
require 'cgi'
|
require 'cgi'
|
||||||
|
require 'base64'
|
||||||
|
|
||||||
# Unsandbox Ruby SDK module (synchronous)
|
# Unsandbox Ruby SDK module (synchronous)
|
||||||
module Un
|
module Un
|
||||||
|
|
@ -922,6 +923,7 @@ module Un
|
||||||
# @param custom_domains [Array<String>, nil] Custom domains for the service
|
# @param custom_domains [Array<String>, nil] Custom domains for the service
|
||||||
# @param service_type [String, nil] Service type for SRV records (e.g., "minecraft")
|
# @param service_type [String, nil] Service type for SRV records (e.g., "minecraft")
|
||||||
# @param unfreeze_on_demand [Boolean] If true, service will auto-wake on HTTP request (default: false)
|
# @param unfreeze_on_demand [Boolean] If true, service will auto-wake on HTTP request (default: false)
|
||||||
|
# @param input_files [Array<Hash>, nil] Optional list of hashes with "filename" and "content" (base64)
|
||||||
# @return [Hash] Response hash with service_id
|
# @return [Hash] Response hash with service_id
|
||||||
# @raise [CredentialsError] If no credentials found
|
# @raise [CredentialsError] If no credentials found
|
||||||
# @raise [APIError] If API request fails
|
# @raise [APIError] If API request fails
|
||||||
|
|
@ -929,7 +931,7 @@ module Un
|
||||||
# @example
|
# @example
|
||||||
# result = Un.create_service("web", [80, 443], "apt install -y nginx && nginx")
|
# result = Un.create_service("web", [80, 443], "apt install -y nginx && nginx")
|
||||||
# puts result["service_id"]
|
# puts result["service_id"]
|
||||||
def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil, unfreeze_on_demand: false)
|
def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil, unfreeze_on_demand: false, input_files: nil)
|
||||||
pk, sk = resolve_credentials(public_key, secret_key)
|
pk, sk = resolve_credentials(public_key, secret_key)
|
||||||
data = {
|
data = {
|
||||||
name: name,
|
name: name,
|
||||||
|
|
@ -941,6 +943,7 @@ module Un
|
||||||
data[:custom_domains] = custom_domains if custom_domains
|
data[:custom_domains] = custom_domains if custom_domains
|
||||||
data[:service_type] = service_type if service_type
|
data[:service_type] = service_type if service_type
|
||||||
data[:unfreeze_on_demand] = unfreeze_on_demand if unfreeze_on_demand
|
data[:unfreeze_on_demand] = unfreeze_on_demand if unfreeze_on_demand
|
||||||
|
data[:input_files] = input_files if input_files && !input_files.empty?
|
||||||
make_request('POST', '/services', pk, sk, data)
|
make_request('POST', '/services', pk, sk, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1202,16 +1205,18 @@ module Un
|
||||||
# @param public_key [String, nil] Optional API key
|
# @param public_key [String, nil] Optional API key
|
||||||
# @param secret_key [String, nil] Optional API secret
|
# @param secret_key [String, nil] Optional API secret
|
||||||
# @param bootstrap [String, nil] New bootstrap script (optional)
|
# @param bootstrap [String, nil] New bootstrap script (optional)
|
||||||
|
# @param input_files [Array<Hash>, nil] Optional list of hashes with "filename" and "content" (base64)
|
||||||
# @return [Hash] Response hash with redeploy confirmation
|
# @return [Hash] Response hash with redeploy confirmation
|
||||||
# @raise [CredentialsError] If no credentials found
|
# @raise [CredentialsError] If no credentials found
|
||||||
# @raise [APIError] If API request fails
|
# @raise [APIError] If API request fails
|
||||||
#
|
#
|
||||||
# @example
|
# @example
|
||||||
# Un.redeploy_service(service_id)
|
# Un.redeploy_service(service_id)
|
||||||
def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil)
|
def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil, input_files: nil)
|
||||||
pk, sk = resolve_credentials(public_key, secret_key)
|
pk, sk = resolve_credentials(public_key, secret_key)
|
||||||
data = {}
|
data = {}
|
||||||
data[:bootstrap] = bootstrap if bootstrap
|
data[:bootstrap] = bootstrap if bootstrap
|
||||||
|
data[:input_files] = input_files if input_files && !input_files.empty?
|
||||||
make_request('POST', "/services/#{service_id}/redeploy", pk, sk, data)
|
make_request('POST', "/services/#{service_id}/redeploy", pk, sk, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2420,7 +2425,17 @@ module Un
|
||||||
elsif service_opts[:bootstrap]
|
elsif service_opts[:bootstrap]
|
||||||
bootstrap = service_opts[:bootstrap]
|
bootstrap = service_opts[:bootstrap]
|
||||||
end
|
end
|
||||||
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, **creds)
|
# Build input_files from -f args
|
||||||
|
service_input_files = nil
|
||||||
|
unless options[:files].empty?
|
||||||
|
service_input_files = options[:files].map do |fpath|
|
||||||
|
{
|
||||||
|
filename: File.basename(fpath),
|
||||||
|
content: Base64.strict_encode64(File.binread(fpath))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, input_files: service_input_files, **creds)
|
||||||
puts "Service #{service_opts[:redeploy]} redeployed"
|
puts "Service #{service_opts[:redeploy]} redeployed"
|
||||||
elsif service_opts[:execute]
|
elsif service_opts[:execute]
|
||||||
cmd = service_opts[:execute_cmd]
|
cmd = service_opts[:execute_cmd]
|
||||||
|
|
@ -2453,6 +2468,17 @@ module Un
|
||||||
exit(EXIT_INVALID_ARGS)
|
exit(EXIT_INVALID_ARGS)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Build input_files from -f args
|
||||||
|
service_input_files = nil
|
||||||
|
unless options[:files].empty?
|
||||||
|
service_input_files = options[:files].map do |fpath|
|
||||||
|
{
|
||||||
|
filename: File.basename(fpath),
|
||||||
|
content: Base64.strict_encode64(File.binread(fpath))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
result = create_service(
|
result = create_service(
|
||||||
service_opts[:name],
|
service_opts[:name],
|
||||||
service_opts[:ports],
|
service_opts[:ports],
|
||||||
|
|
@ -2461,6 +2487,7 @@ module Un
|
||||||
vcpu: options[:vcpu],
|
vcpu: options[:vcpu],
|
||||||
custom_domains: service_opts[:domains],
|
custom_domains: service_opts[:domains],
|
||||||
service_type: service_opts[:type],
|
service_type: service_opts[:type],
|
||||||
|
input_files: service_input_files,
|
||||||
**creds
|
**creds
|
||||||
)
|
)
|
||||||
puts "Service created: #{result['service_id']}"
|
puts "Service created: #{result['service_id']}"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ reqwest = { version = "0.12", features = ["blocking", "json"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
# Base64 encoding (for input_files)
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
# HMAC-SHA256 authentication
|
# HMAC-SHA256 authentication
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
// - TTL: 1 hour
|
// - TTL: 1 hour
|
||||||
// - Updated on successful API calls
|
// - Updated on successful API calls
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -382,6 +383,17 @@ pub struct ServiceCreateOptions {
|
||||||
pub bootstrap_url: Option<String>,
|
pub bootstrap_url: Option<String>,
|
||||||
/// Whether to enable automatic unfreezing on incoming HTTP requests
|
/// Whether to enable automatic unfreezing on incoming HTTP requests
|
||||||
pub unfreeze_on_demand: Option<bool>,
|
pub unfreeze_on_demand: Option<bool>,
|
||||||
|
/// Input files to upload (filename + base64-encoded content)
|
||||||
|
pub input_files: Option<Vec<InputFile>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file to upload with a service create or redeploy request
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct InputFile {
|
||||||
|
/// Filename (basename only)
|
||||||
|
pub filename: String,
|
||||||
|
/// Base64-encoded file content
|
||||||
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options for updating a service
|
/// Options for updating a service
|
||||||
|
|
@ -1997,6 +2009,11 @@ pub fn create_service(
|
||||||
if let Some(unfreeze_on_demand) = opts.unfreeze_on_demand {
|
if let Some(unfreeze_on_demand) = opts.unfreeze_on_demand {
|
||||||
body["unfreeze_on_demand"] = serde_json::json!(unfreeze_on_demand);
|
body["unfreeze_on_demand"] = serde_json::json!(unfreeze_on_demand);
|
||||||
}
|
}
|
||||||
|
if let Some(input_files) = opts.input_files {
|
||||||
|
if !input_files.is_empty() {
|
||||||
|
body["input_files"] = serde_json::json!(input_files);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
make_request("POST", "/services", creds, Some(&body))
|
make_request("POST", "/services", creds, Some(&body))
|
||||||
|
|
@ -2269,12 +2286,22 @@ pub fn export_service_env(service_id: &str, creds: &Credentials) -> Result<Strin
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `service_id` - Service ID to redeploy
|
/// * `service_id` - Service ID to redeploy
|
||||||
/// * `creds` - API credentials
|
/// * `creds` - API credentials
|
||||||
|
/// * `input_files` - Optional files to upload (filename + base64-encoded content)
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// Updated Service information
|
/// Updated Service information
|
||||||
pub fn redeploy_service(service_id: &str, creds: &Credentials) -> Result<Service> {
|
pub fn redeploy_service(
|
||||||
|
service_id: &str,
|
||||||
|
creds: &Credentials,
|
||||||
|
input_files: Option<Vec<InputFile>>,
|
||||||
|
) -> Result<Service> {
|
||||||
let path = format!("/services/{}/redeploy", service_id);
|
let path = format!("/services/{}/redeploy", service_id);
|
||||||
let body = serde_json::json!({});
|
let mut body = serde_json::json!({});
|
||||||
|
if let Some(files) = input_files {
|
||||||
|
if !files.is_empty() {
|
||||||
|
body["input_files"] = serde_json::json!(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
make_request("POST", &path, creds, Some(&body))
|
make_request("POST", &path, creds, Some(&body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2861,7 +2888,7 @@ SESSION COMMANDS:
|
||||||
|
|
||||||
SERVICE COMMANDS:
|
SERVICE COMMANDS:
|
||||||
un service --list List all services
|
un service --list List all services
|
||||||
un service --name NAME --ports P Create service
|
un service --name NAME --ports P [-f FILE] Create service
|
||||||
un service --info ID Get service details
|
un service --info ID Get service details
|
||||||
un service --logs ID Get all logs
|
un service --logs ID Get all logs
|
||||||
un service --tail ID Get last 9000 lines
|
un service --tail ID Get last 9000 lines
|
||||||
|
|
@ -2871,7 +2898,7 @@ SERVICE COMMANDS:
|
||||||
un service --lock ID Prevent deletion
|
un service --lock ID Prevent deletion
|
||||||
un service --unlock ID Allow deletion
|
un service --unlock ID Allow deletion
|
||||||
un service --execute ID 'cmd' Run command
|
un service --execute ID 'cmd' Run command
|
||||||
un service --redeploy ID Re-run bootstrap
|
un service --redeploy ID [-f FILE] Re-run bootstrap
|
||||||
un service --snapshot ID Create snapshot
|
un service --snapshot ID Create snapshot
|
||||||
un service env status ID Show vault status
|
un service env status ID Show vault status
|
||||||
un service env set ID Set env vars
|
un service env set ID Set env vars
|
||||||
|
|
@ -3586,6 +3613,33 @@ fn cmd_session(opts: &CliOptions) -> i32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build input_files from -f file paths: read each file, base64 encode, return as InputFile vec.
|
||||||
|
fn build_input_files(files: &[String]) -> Option<Vec<InputFile>> {
|
||||||
|
if files.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let engine = base64::engine::general_purpose::STANDARD;
|
||||||
|
let mut input_files = Vec::new();
|
||||||
|
for fpath in files {
|
||||||
|
let path = std::path::Path::new(fpath);
|
||||||
|
match fs::read(path) {
|
||||||
|
Ok(data) => {
|
||||||
|
let filename = path.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| fpath.clone());
|
||||||
|
input_files.push(InputFile {
|
||||||
|
filename,
|
||||||
|
content: engine.encode(&data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Warning: Cannot read file '{}': {}", fpath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input_files.is_empty() { None } else { Some(input_files) }
|
||||||
|
}
|
||||||
|
|
||||||
fn cmd_service(opts: &CliOptions) -> i32 {
|
fn cmd_service(opts: &CliOptions) -> i32 {
|
||||||
let creds = match get_credentials(opts) {
|
let creds = match get_credentials(opts) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
|
|
@ -3736,7 +3790,9 @@ fn cmd_service(opts: &CliOptions) -> i32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref id) = opts.service_redeploy {
|
if let Some(ref id) = opts.service_redeploy {
|
||||||
match redeploy_service(id, &creds) {
|
// Build input_files from -f args
|
||||||
|
let service_input_files = build_input_files(&opts.files);
|
||||||
|
match redeploy_service(id, &creds, service_input_files) {
|
||||||
Ok(service) => {
|
Ok(service) => {
|
||||||
println!("Service {} redeployed.", service.service_id);
|
println!("Service {} redeployed.", service.service_id);
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
|
|
@ -3809,12 +3865,16 @@ fn cmd_service(opts: &CliOptions) -> i32 {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build input_files from -f args
|
||||||
|
let service_input_files = build_input_files(&opts.files);
|
||||||
|
|
||||||
let service_opts = ServiceCreateOptions {
|
let service_opts = ServiceCreateOptions {
|
||||||
network_mode: opts.network.clone(),
|
network_mode: opts.network.clone(),
|
||||||
vcpu: opts.vcpu,
|
vcpu: opts.vcpu,
|
||||||
domains: opts.service_domains.as_ref().map(|d| {
|
domains: opts.service_domains.as_ref().map(|d| {
|
||||||
d.split(',').map(|s| s.trim().to_string()).collect()
|
d.split(',').map(|s| s.trim().to_string()).collect()
|
||||||
}),
|
}),
|
||||||
|
input_files: service_input_files,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ interface Args {
|
||||||
setUnfreezeOnDemand: string | null;
|
setUnfreezeOnDemand: string | null;
|
||||||
showFreezePage: boolean | null;
|
showFreezePage: boolean | null;
|
||||||
setShowFreezePage: string | null;
|
setShowFreezePage: string | null;
|
||||||
|
redeploy: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiKeys {
|
interface ApiKeys {
|
||||||
|
|
@ -897,6 +898,37 @@ async function cmdService(args: Args): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.redeploy) {
|
||||||
|
const payload: any = {};
|
||||||
|
if (args.bootstrap) {
|
||||||
|
payload.bootstrap = args.bootstrap;
|
||||||
|
}
|
||||||
|
if (args.bootstrapFile) {
|
||||||
|
if (!fs.existsSync(args.bootstrapFile)) {
|
||||||
|
console.error(`${RED}Error: Bootstrap file not found: ${args.bootstrapFile}${RESET}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
payload.bootstrap_content = fs.readFileSync(args.bootstrapFile, 'utf-8');
|
||||||
|
}
|
||||||
|
if (args.files && args.files.length > 0) {
|
||||||
|
payload.input_files = args.files.map(filepath => {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filepath);
|
||||||
|
return {
|
||||||
|
filename: path.basename(filepath),
|
||||||
|
content_base64: content.toString('base64')
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await apiRequest(`/services/${args.redeploy}/redeploy`, "POST", payload, keys);
|
||||||
|
console.log(`${GREEN}Service redeployed: ${args.redeploy}${RESET}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.execute) {
|
if (args.execute) {
|
||||||
const payload = { command: args.command_arg };
|
const payload = { command: args.command_arg };
|
||||||
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
|
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
|
||||||
|
|
@ -1225,6 +1257,7 @@ function parseArgs(argv: string[]): Args {
|
||||||
setUnfreezeOnDemand: null,
|
setUnfreezeOnDemand: null,
|
||||||
showFreezePage: null,
|
showFreezePage: null,
|
||||||
setShowFreezePage: null,
|
setShowFreezePage: null,
|
||||||
|
redeploy: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
let i = 2;
|
let i = 2;
|
||||||
|
|
@ -1397,6 +1430,9 @@ function parseArgs(argv: string[]): Args {
|
||||||
} else if (arg === '--set-show-freeze-page' && i + 1 < argv.length) {
|
} else if (arg === '--set-show-freeze-page' && i + 1 < argv.length) {
|
||||||
args.setShowFreezePage = argv[++i];
|
args.setShowFreezePage = argv[++i];
|
||||||
i++;
|
i++;
|
||||||
|
} else if (arg === '--redeploy' && i + 1 < argv.length) {
|
||||||
|
args.redeploy = argv[++i];
|
||||||
|
i++;
|
||||||
} else if (!arg.startsWith('-')) {
|
} else if (!arg.startsWith('-')) {
|
||||||
args.sourceFile = arg;
|
args.sourceFile = arg;
|
||||||
i++;
|
i++;
|
||||||
|
|
@ -1474,6 +1510,7 @@ Service options:
|
||||||
--unfreeze ID Unfreeze service
|
--unfreeze ID Unfreeze service
|
||||||
--destroy ID Destroy service
|
--destroy ID Destroy service
|
||||||
--resize ID Resize service (requires -v)
|
--resize ID Resize service (requires -v)
|
||||||
|
--redeploy ID Re-run bootstrap (with optional --bootstrap, --bootstrap-file, -f)
|
||||||
--execute ID Execute command in service
|
--execute ID Execute command in service
|
||||||
--command CMD Command to execute (with --execute)
|
--command CMD Command to execute (with --execute)
|
||||||
--dump-bootstrap ID Dump bootstrap script
|
--dump-bootstrap ID Dump bootstrap script
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue