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 ports="${2:-}"
|
||||
local bootstrap="${3:-}"
|
||||
local input_files_json="${4:-}"
|
||||
|
||||
local body
|
||||
body=$(jq -n --arg name "$name" '{name: $name}')
|
||||
|
|
@ -434,6 +435,9 @@ service_create() {
|
|||
if [ -n "$bootstrap" ]; then
|
||||
body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}')
|
||||
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"
|
||||
}
|
||||
|
|
@ -472,10 +476,14 @@ service_set_unfreeze_on_demand() {
|
|||
service_redeploy() {
|
||||
local service_id="$1"
|
||||
local bootstrap="${2:-}"
|
||||
local input_files_json="${3:-}"
|
||||
local body="{}"
|
||||
if [ -n "$bootstrap" ]; then
|
||||
body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}')
|
||||
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"
|
||||
}
|
||||
|
||||
|
|
@ -902,6 +910,9 @@ cmd_service() {
|
|||
local target=""
|
||||
local name=""
|
||||
local ports=""
|
||||
local bootstrap=""
|
||||
local bootstrap_file=""
|
||||
local -a files=()
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
|
|
@ -913,13 +924,47 @@ cmd_service() {
|
|||
--lock) action="lock"; target="$2"; shift ;;
|
||||
--unlock) action="unlock"; target="$2"; shift ;;
|
||||
--logs) action="logs"; target="$2"; shift ;;
|
||||
--redeploy) action="redeploy"; target="$2"; shift ;;
|
||||
--name) name="$2"; shift ;;
|
||||
--ports) ports="$2"; shift ;;
|
||||
--bootstrap) bootstrap="$2"; shift ;;
|
||||
--bootstrap-file) bootstrap_file="$2"; shift ;;
|
||||
-f|--file) files+=("$2"); shift ;;
|
||||
*) ;;
|
||||
esac
|
||||
shift
|
||||
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
|
||||
list)
|
||||
local result
|
||||
|
|
@ -954,14 +999,19 @@ cmd_service() {
|
|||
result=$(service_logs "$target")
|
||||
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
|
||||
local result
|
||||
result=$(service_create "$name" "$ports" "")
|
||||
result=$(service_create "$name" "$ports" "$bootstrap" "$input_files_json")
|
||||
echo -e "${GREEN}Service created${RESET}"
|
||||
echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"'
|
||||
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
|
||||
fi
|
||||
;;
|
||||
|
|
@ -1138,8 +1188,12 @@ Service options:
|
|||
--lock ID Lock service
|
||||
--unlock ID Unlock service
|
||||
--logs ID Get service logs
|
||||
--redeploy ID Re-run bootstrap (supports -f, --bootstrap)
|
||||
--name NAME Create service with name
|
||||
--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:
|
||||
--list List all snapshots
|
||||
|
|
|
|||
|
|
@ -705,9 +705,32 @@ string service_unlock(const string& service_id, const string& public_key, const
|
|||
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 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 cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||
"-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;
|
||||
}
|
||||
|
||||
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
|
||||
if (!env_action.empty()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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()) {
|
||||
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
|
||||
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
|
||||
|
|
@ -1778,7 +1857,7 @@ int main(int argc, char* argv[]) {
|
|||
if (cmd_type == "service") {
|
||||
string name, ports, type, bootstrap, bootstrap_file;
|
||||
bool list = false;
|
||||
string info, logs, tail, sleep, wake, destroy, 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;
|
||||
vector<string> files;
|
||||
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 == "--execute" && i+1 < argc) execute = 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-file" && i+1 < argc) dump_file = 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];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ class Un
|
|||
{
|
||||
CmdKey(parsedArgs);
|
||||
}
|
||||
else if (parsedArgs.Command == "languages")
|
||||
{
|
||||
CmdLanguages(parsedArgs);
|
||||
}
|
||||
else if (parsedArgs.SourceFile != null)
|
||||
{
|
||||
CmdExecute(parsedArgs);
|
||||
|
|
@ -438,6 +442,32 @@ class Un
|
|||
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)
|
||||
{
|
||||
var payload = new Dictionary<string, object>
|
||||
|
|
@ -529,6 +559,20 @@ class Un
|
|||
{
|
||||
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);
|
||||
string serviceId = result.ContainsKey("id") ? (string)result["id"] : null;
|
||||
|
|
@ -1234,10 +1278,12 @@ class Un
|
|||
public string ServiceShowFreezePage = null;
|
||||
public bool ServiceShowFreezePageEnabled = true;
|
||||
public bool ServiceCreateUnfreezeOnDemand = false;
|
||||
public string ServiceRedeploy = null;
|
||||
public string EnvFile = null;
|
||||
public string EnvAction = null;
|
||||
public string EnvTarget = null;
|
||||
public bool KeyExtend = false;
|
||||
public bool LanguagesJson = false;
|
||||
}
|
||||
|
||||
static Args ParseArgs(string[] args)
|
||||
|
|
@ -1249,6 +1295,7 @@ class Un
|
|||
if (arg == "session") result.Command = "session";
|
||||
else if (arg == "service") result.Command = "service";
|
||||
else if (arg == "key") result.Command = "key";
|
||||
else if (arg == "languages") result.Command = "languages";
|
||||
else if (arg == "env" && result.Command == "service")
|
||||
{
|
||||
// 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-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "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 == "--json") result.LanguagesJson = true;
|
||||
else if (!arg.StartsWith("-")) result.SourceFile = arg;
|
||||
}
|
||||
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()
|
||||
{
|
||||
Console.WriteLine(@"Usage: Un [options] <source_file>
|
||||
|
|
@ -1308,6 +1441,7 @@ class Un
|
|||
Un service [options]
|
||||
Un service env <action> <service_id> [options]
|
||||
Un key [options]
|
||||
Un languages [--json]
|
||||
|
||||
Execute options:
|
||||
-e KEY=VALUE Set environment variable
|
||||
|
|
@ -1340,6 +1474,7 @@ Service options:
|
|||
--show-freeze-page-enabled BOOL Enable/disable (default: true)
|
||||
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
|
||||
--destroy ID Destroy service
|
||||
--redeploy ID Re-run bootstrap (with optional --bootstrap, -f)
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
--dump-bootstrap ID Dump bootstrap script
|
||||
|
|
@ -1354,7 +1489,10 @@ Service env commands:
|
|||
env delete ID Delete vault
|
||||
|
||||
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>(); }
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// Try cache first
|
||||
var cached = LoadLanguagesCache();
|
||||
if (cached != null) return cached;
|
||||
|
||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/languages", "GET", null, pk, sk);
|
||||
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>();
|
||||
}
|
||||
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; }
|
||||
}
|
||||
|
||||
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 payload = new Dictionary<string, object> { ["name"] = name };
|
||||
|
|
@ -1645,6 +1844,7 @@ public static class Unsandbox
|
|||
if (domains != null) payload["domains"] = domains;
|
||||
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||
if (networkMode != null) payload["network"] = networkMode;
|
||||
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||
try
|
||||
{
|
||||
var result = ApiCall("/services", "POST", payload, pk, sk);
|
||||
|
|
@ -1696,10 +1896,16 @@ public static class Unsandbox
|
|||
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 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; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -392,7 +392,22 @@ void CmdService(Args args)
|
|||
|
||||
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}");
|
||||
return;
|
||||
}
|
||||
|
|
@ -452,6 +467,20 @@ void CmdService(Args args)
|
|||
if (args.Network != null) payload["network"] = args.Network;
|
||||
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
||||
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 serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
|
||||
|
|
@ -1085,7 +1114,7 @@ Service options:
|
|||
--lock ID Prevent deletion
|
||||
--unlock ID Allow deletion
|
||||
--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
|
||||
--unfreeze-on-demand ID Set unfreeze-on-demand for service
|
||||
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
|
||||
|
|
@ -1098,6 +1127,7 @@ Service options:
|
|||
--dump-bootstrap ID Dump bootstrap script
|
||||
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
||||
-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
|
||||
|
||||
Service env commands:
|
||||
|
|
@ -1406,7 +1436,7 @@ public static class Unsandbox
|
|||
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 payload = new Dictionary<string, object> { ["name"] = name };
|
||||
|
|
@ -1414,6 +1444,7 @@ public static class Unsandbox
|
|||
if (domains != null) payload["domains"] = domains;
|
||||
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||
if (networkMode != null) payload["network"] = networkMode;
|
||||
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||
try
|
||||
{
|
||||
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; }
|
||||
}
|
||||
|
||||
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 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; }
|
||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -840,12 +841,19 @@ func ShellSession(creds *Credentials, sessionID, command string) (map[string]int
|
|||
// 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.
|
||||
type ServiceOptions struct {
|
||||
NetworkMode string // "zerotrust" (default) or "semitrusted"
|
||||
Shell string // Shell to use for bootstrap
|
||||
VCPU int // Number of virtual CPUs
|
||||
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
|
||||
NetworkMode string // "zerotrust" (default) or "semitrusted"
|
||||
Shell string // Shell to use for bootstrap
|
||||
VCPU int // Number of virtual CPUs
|
||||
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.
|
||||
|
|
@ -902,6 +910,9 @@ func CreateService(creds *Credentials, name string, ports []int, bootstrap strin
|
|||
if opts.UnfreezeOnDemand {
|
||||
data["unfreeze_on_demand"] = true
|
||||
}
|
||||
if len(opts.InputFiles) > 0 {
|
||||
data["input_files"] = opts.InputFiles
|
||||
}
|
||||
}
|
||||
|
||||
return makeRequest("POST", "/services", creds, data)
|
||||
|
|
@ -1017,11 +1028,15 @@ func ExportServiceEnv(creds *Credentials, serviceID string) (map[string]interfac
|
|||
// creds: API credentials
|
||||
// serviceID: Service ID
|
||||
// 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{})
|
||||
if bootstrap != "" {
|
||||
data["bootstrap"] = bootstrap
|
||||
}
|
||||
if len(inputFiles) > 0 {
|
||||
data["input_files"] = inputFiles
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func readEnvFile(path string) (map[string]string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
|
|
@ -2353,7 +2384,15 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
|
|||
|
||||
// Redeploy service
|
||||
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 {
|
||||
return cliError(err.Error(), ExitAPIError)
|
||||
}
|
||||
|
|
@ -2412,6 +2451,13 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
|
|||
if opts.VCPU > 0 {
|
||||
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)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1493,6 +1493,31 @@ public class Un {
|
|||
String bootstrap,
|
||||
String publicKey,
|
||||
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 {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
|
||||
|
|
@ -1517,6 +1542,9 @@ public class Un {
|
|||
data.put("bootstrap", bootstrap);
|
||||
}
|
||||
}
|
||||
if (inputFiles != null && !inputFiles.isEmpty()) {
|
||||
data.put("input_files", inputFiles);
|
||||
}
|
||||
|
||||
return makeRequest("POST", "/services", creds[0], creds[1], data);
|
||||
}
|
||||
|
|
@ -1542,6 +1570,33 @@ public class Un {
|
|||
boolean unfreezeOnDemand,
|
||||
String publicKey,
|
||||
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 {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
|
||||
|
|
@ -1569,6 +1624,9 @@ public class Un {
|
|||
if (unfreezeOnDemand) {
|
||||
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);
|
||||
}
|
||||
|
|
@ -1901,9 +1959,34 @@ public class Un {
|
|||
String serviceId,
|
||||
String publicKey,
|
||||
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 {
|
||||
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);
|
||||
break;
|
||||
case "service":
|
||||
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars);
|
||||
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars, files);
|
||||
break;
|
||||
case "snapshot":
|
||||
handleSnapshot(positionalArgs, publicKey, secretKey);
|
||||
|
|
@ -3181,7 +3264,8 @@ public class Un {
|
|||
String secretKey,
|
||||
String networkMode,
|
||||
int vcpu,
|
||||
List<String> envVars
|
||||
List<String> envVars,
|
||||
List<String> files
|
||||
) throws Exception {
|
||||
// Check for "env" subcommand
|
||||
if (args.size() > 1 && args.get(1).equals("env")) {
|
||||
|
|
@ -3347,14 +3431,17 @@ public class Un {
|
|||
System.err.print(stderr);
|
||||
}
|
||||
} 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);
|
||||
} else if (snapshotId != null) {
|
||||
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
|
||||
System.out.println("Snapshot created: " + snapId);
|
||||
} else if (name != null) {
|
||||
// Create new service
|
||||
Map<String, Object> result = createService(name, ports, bootstrap, publicKey, secretKey);
|
||||
// Build input_files from -f args
|
||||
List<Map<String, String>> inputFiles = buildInputFiles(files);
|
||||
Map<String, Object> result = createService(name, ports, bootstrap, inputFiles, publicKey, secretKey);
|
||||
System.out.println("Service created:");
|
||||
printMap(result);
|
||||
} 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(
|
||||
List<String> args,
|
||||
String publicKey,
|
||||
|
|
|
|||
|
|
@ -1093,6 +1093,7 @@ async function listServices(publicKey, secretKey) {
|
|||
* - domains: Array of custom domains
|
||||
* - serviceType: Service type for SRV records (minecraft, mumble, etc.)
|
||||
* - 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)
|
||||
*/
|
||||
|
|
@ -1114,6 +1115,7 @@ async function createService(name, ports, bootstrap, opts = {}, publicKey, secre
|
|||
if (opts.domains) data.custom_domains = opts.domains;
|
||||
if (opts.serviceType) data.service_type = opts.serviceType;
|
||||
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);
|
||||
}
|
||||
|
|
@ -1331,10 +1333,11 @@ async function exportServiceEnv(serviceId, publicKey, secretKey) {
|
|||
* Args:
|
||||
* serviceId: Service ID to redeploy
|
||||
* bootstrap: Optional new bootstrap script content or URL
|
||||
* inputFiles: Optional array of {filename, content} objects (content is base64-encoded)
|
||||
*
|
||||
* 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);
|
||||
const data = {};
|
||||
if (bootstrap) {
|
||||
|
|
@ -1344,6 +1347,7 @@ async function redeployService(serviceId, bootstrap = null, publicKey, secretKey
|
|||
data.bootstrap_content = bootstrap;
|
||||
}
|
||||
}
|
||||
if (inputFiles && inputFiles.length > 0) data.input_files = inputFiles;
|
||||
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 --unlock <id> Allow deletion
|
||||
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
|
||||
|
||||
SERVICE ENV COMMANDS:
|
||||
|
|
@ -2732,7 +2736,19 @@ async function handleService(opts) {
|
|||
if (opts.bootstrapFile) {
|
||||
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.`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -2786,6 +2802,17 @@ async function handleService(opts) {
|
|||
if (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);
|
||||
console.log(`Service created: ${service.service_id}`);
|
||||
|
|
|
|||
|
|
@ -472,6 +472,7 @@ sub service_redeploy {
|
|||
my ($service_id, %opts) = @_;
|
||||
my $body = {};
|
||||
$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);
|
||||
}
|
||||
|
||||
|
|
@ -998,7 +999,12 @@ sub cmd_service {
|
|||
}
|
||||
|
||||
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";
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1339,13 +1339,18 @@ class Unsandbox {
|
|||
* @param string $serviceId Service ID
|
||||
* @param string|null $publicKey Optional API key
|
||||
* @param string|null $secretKey Optional API secret
|
||||
* @param array $opts Optional parameters: 'input_files'
|
||||
* @return array Response array with redeploy confirmation
|
||||
* @throws CredentialsException Missing credentials
|
||||
* @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);
|
||||
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']) {
|
||||
$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";
|
||||
return;
|
||||
}
|
||||
|
|
@ -2647,6 +2676,31 @@ class Unsandbox {
|
|||
$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
|
||||
$bootstrap = $serviceOpts['bootstrap'] ?? '';
|
||||
if (!empty($serviceOpts['bootstrap_file'])) {
|
||||
|
|
@ -3438,7 +3492,7 @@ SERVICE OPTIONS:
|
|||
--lock ID Prevent service deletion
|
||||
--unlock ID Allow service deletion
|
||||
--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
|
||||
--snapshot ID Create service snapshot
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ require 'openssl'
|
|||
require 'fileutils'
|
||||
require 'optparse'
|
||||
require 'cgi'
|
||||
require 'base64'
|
||||
|
||||
# Unsandbox Ruby SDK module (synchronous)
|
||||
module Un
|
||||
|
|
@ -922,6 +923,7 @@ module Un
|
|||
# @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 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
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
|
|
@ -929,7 +931,7 @@ module Un
|
|||
# @example
|
||||
# result = Un.create_service("web", [80, 443], "apt install -y nginx && nginx")
|
||||
# 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)
|
||||
data = {
|
||||
name: name,
|
||||
|
|
@ -941,6 +943,7 @@ module Un
|
|||
data[:custom_domains] = custom_domains if custom_domains
|
||||
data[:service_type] = service_type if service_type
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
@ -1202,16 +1205,18 @@ module Un
|
|||
# @param public_key [String, nil] Optional API key
|
||||
# @param secret_key [String, nil] Optional API secret
|
||||
# @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
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
# @raise [APIError] If API request fails
|
||||
#
|
||||
# @example
|
||||
# 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)
|
||||
data = {}
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
@ -2420,7 +2425,17 @@ module Un
|
|||
elsif service_opts[:bootstrap]
|
||||
bootstrap = service_opts[:bootstrap]
|
||||
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"
|
||||
elsif service_opts[:execute]
|
||||
cmd = service_opts[:execute_cmd]
|
||||
|
|
@ -2453,6 +2468,17 @@ module Un
|
|||
exit(EXIT_INVALID_ARGS)
|
||||
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(
|
||||
service_opts[:name],
|
||||
service_opts[:ports],
|
||||
|
|
@ -2461,6 +2487,7 @@ module Un
|
|||
vcpu: options[:vcpu],
|
||||
custom_domains: service_opts[:domains],
|
||||
service_type: service_opts[:type],
|
||||
input_files: service_input_files,
|
||||
**creds
|
||||
)
|
||||
puts "Service created: #{result['service_id']}"
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ reqwest = { version = "0.12", features = ["blocking", "json"] }
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# Base64 encoding (for input_files)
|
||||
base64 = "0.22"
|
||||
|
||||
# HMAC-SHA256 authentication
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
// - TTL: 1 hour
|
||||
// - Updated on successful API calls
|
||||
|
||||
use base64::Engine;
|
||||
use hmac::{Hmac, Mac};
|
||||
use reqwest::blocking::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -382,6 +383,17 @@ pub struct ServiceCreateOptions {
|
|||
pub bootstrap_url: Option<String>,
|
||||
/// Whether to enable automatic unfreezing on incoming HTTP requests
|
||||
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
|
||||
|
|
@ -1997,6 +2009,11 @@ pub fn create_service(
|
|||
if let Some(unfreeze_on_demand) = opts.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))
|
||||
|
|
@ -2269,12 +2286,22 @@ pub fn export_service_env(service_id: &str, creds: &Credentials) -> Result<Strin
|
|||
/// # Arguments
|
||||
/// * `service_id` - Service ID to redeploy
|
||||
/// * `creds` - API credentials
|
||||
/// * `input_files` - Optional files to upload (filename + base64-encoded content)
|
||||
///
|
||||
/// # Returns
|
||||
/// 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 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))
|
||||
}
|
||||
|
||||
|
|
@ -2861,7 +2888,7 @@ SESSION COMMANDS:
|
|||
|
||||
SERVICE COMMANDS:
|
||||
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 --logs ID Get all logs
|
||||
un service --tail ID Get last 9000 lines
|
||||
|
|
@ -2871,7 +2898,7 @@ SERVICE COMMANDS:
|
|||
un service --lock ID Prevent deletion
|
||||
un service --unlock ID Allow deletion
|
||||
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 env status ID Show vault status
|
||||
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 {
|
||||
let creds = match get_credentials(opts) {
|
||||
Ok(c) => c,
|
||||
|
|
@ -3736,7 +3790,9 @@ fn cmd_service(opts: &CliOptions) -> i32 {
|
|||
}
|
||||
|
||||
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) => {
|
||||
println!("Service {} redeployed.", service.service_id);
|
||||
return EXIT_SUCCESS;
|
||||
|
|
@ -3809,12 +3865,16 @@ fn cmd_service(opts: &CliOptions) -> i32 {
|
|||
String::new()
|
||||
};
|
||||
|
||||
// Build input_files from -f args
|
||||
let service_input_files = build_input_files(&opts.files);
|
||||
|
||||
let service_opts = ServiceCreateOptions {
|
||||
network_mode: opts.network.clone(),
|
||||
vcpu: opts.vcpu,
|
||||
domains: opts.service_domains.as_ref().map(|d| {
|
||||
d.split(',').map(|s| s.trim().to_string()).collect()
|
||||
}),
|
||||
input_files: service_input_files,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ interface Args {
|
|||
setUnfreezeOnDemand: string | null;
|
||||
showFreezePage: boolean | null;
|
||||
setShowFreezePage: string | null;
|
||||
redeploy: string | null;
|
||||
}
|
||||
|
||||
interface ApiKeys {
|
||||
|
|
@ -897,6 +898,37 @@ async function cmdService(args: Args): Promise<void> {
|
|||
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) {
|
||||
const payload = { command: args.command_arg };
|
||||
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
|
||||
|
|
@ -1225,6 +1257,7 @@ function parseArgs(argv: string[]): Args {
|
|||
setUnfreezeOnDemand: null,
|
||||
showFreezePage: null,
|
||||
setShowFreezePage: null,
|
||||
redeploy: null,
|
||||
};
|
||||
|
||||
let i = 2;
|
||||
|
|
@ -1397,6 +1430,9 @@ function parseArgs(argv: string[]): Args {
|
|||
} else if (arg === '--set-show-freeze-page' && i + 1 < argv.length) {
|
||||
args.setShowFreezePage = argv[++i];
|
||||
i++;
|
||||
} else if (arg === '--redeploy' && i + 1 < argv.length) {
|
||||
args.redeploy = argv[++i];
|
||||
i++;
|
||||
} else if (!arg.startsWith('-')) {
|
||||
args.sourceFile = arg;
|
||||
i++;
|
||||
|
|
@ -1474,6 +1510,7 @@ Service options:
|
|||
--unfreeze ID Unfreeze service
|
||||
--destroy ID Destroy service
|
||||
--resize ID Resize service (requires -v)
|
||||
--redeploy ID Re-run bootstrap (with optional --bootstrap, --bootstrap-file, -f)
|
||||
--execute ID Execute command in service
|
||||
--command CMD Command to execute (with --execute)
|
||||
--dump-bootstrap ID Dump bootstrap script
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue