feat: add -f file upload support to service --execute
Files can now be uploaded before executing a command: un service -f data.txt --execute myapp 'cat /tmp/input/data.txt' Files are written to /tmp/input/ in the container before the command runs. Matches the API's new input_files parameter. Signed: Agent Blackops
This commit is contained in:
parent
7cdefaa0d7
commit
43b862111f
1 changed files with 230 additions and 7 deletions
|
|
@ -1391,6 +1391,12 @@ static char* find_session_by_container(const UnsandboxCredentials *creds, const
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Forward declaration for sudo challenge handler (defined after destroy_service)
|
||||
static int handle_sudo_challenge(const char *response_data,
|
||||
const UnsandboxCredentials *creds,
|
||||
const char *method, const char *url,
|
||||
const char *path, const char *body);
|
||||
|
||||
// Kill a session by ID or container name
|
||||
static int kill_session(const UnsandboxCredentials *creds, const char *session_id_or_container) {
|
||||
char *session_id = NULL;
|
||||
|
|
@ -1463,6 +1469,12 @@ static int kill_session(const UnsandboxCredentials *creds, const char *session_i
|
|||
free(response.data);
|
||||
free(session_id);
|
||||
return 1;
|
||||
} else if (http_code == 428) {
|
||||
fprintf(stderr, "\n");
|
||||
int result = handle_sudo_challenge(response.data, creds, "DELETE", url, path, NULL);
|
||||
free(response.data);
|
||||
free(session_id);
|
||||
return result;
|
||||
} else {
|
||||
fprintf(stderr, " failed\nError: HTTP %ld\n", http_code);
|
||||
if (response.data) fprintf(stderr, "%s\n", response.data);
|
||||
|
|
@ -3592,6 +3604,110 @@ static int unfreeze_service(const UnsandboxCredentials *creds, const char *servi
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Handle 428 sudo OTP challenge - prompts user for OTP and retries the request
|
||||
static int handle_sudo_challenge(const char *response_data,
|
||||
const UnsandboxCredentials *creds,
|
||||
const char *method, const char *url,
|
||||
const char *path, const char *body) {
|
||||
// Extract challenge_id from response
|
||||
char *challenge_id = extract_json_string(response_data, "challenge_id");
|
||||
|
||||
fprintf(stderr, "\033[33mConfirmation required. Check your email for a one-time code.\033[0m\n");
|
||||
fprintf(stderr, "Enter OTP: ");
|
||||
|
||||
char otp[32];
|
||||
if (!fgets(otp, sizeof(otp), stdin)) {
|
||||
fprintf(stderr, "Error: Failed to read OTP\n");
|
||||
if (challenge_id) free(challenge_id);
|
||||
return 1;
|
||||
}
|
||||
// Strip newline
|
||||
size_t otp_len = strlen(otp);
|
||||
if (otp_len > 0 && otp[otp_len - 1] == '\n') otp[otp_len - 1] = '\0';
|
||||
otp_len = strlen(otp);
|
||||
if (otp_len > 0 && otp[otp_len - 1] == '\r') otp[otp_len - 1] = '\0';
|
||||
|
||||
if (strlen(otp) == 0) {
|
||||
fprintf(stderr, "Error: Operation cancelled\n");
|
||||
if (challenge_id) free(challenge_id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Retry the request with sudo headers
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
if (challenge_id) free(challenge_id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct ResponseBuffer retry_response = {0};
|
||||
retry_response.data = malloc(1);
|
||||
retry_response.size = 0;
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = add_hmac_auth_headers(headers, creds, method, path, body);
|
||||
|
||||
// Add sudo headers
|
||||
char otp_header[64];
|
||||
snprintf(otp_header, sizeof(otp_header), "X-Sudo-OTP: %s", otp);
|
||||
headers = curl_slist_append(headers, otp_header);
|
||||
|
||||
if (challenge_id) {
|
||||
char challenge_header[256];
|
||||
snprintf(challenge_header, sizeof(challenge_header), "X-Sudo-Challenge: %s", challenge_id);
|
||||
headers = curl_slist_append(headers, challenge_header);
|
||||
free(challenge_id);
|
||||
}
|
||||
|
||||
if (body) {
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
if (strcmp(method, "DELETE") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
|
||||
} else if (strcmp(method, "POST") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body ? body : "");
|
||||
}
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &retry_response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
|
||||
free(retry_response.data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (http_code >= 200 && http_code < 300) {
|
||||
printf("\033[32mOperation completed successfully\033[0m\n");
|
||||
free(retry_response.data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Error: HTTP %ld\n", http_code);
|
||||
if (retry_response.data) {
|
||||
char *error = extract_json_string(retry_response.data, "error");
|
||||
if (error) {
|
||||
fprintf(stderr, "%s\n", error);
|
||||
free(error);
|
||||
} else {
|
||||
fprintf(stderr, "%s\n", retry_response.data);
|
||||
}
|
||||
}
|
||||
free(retry_response.data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Destroy a service
|
||||
static int destroy_service(const UnsandboxCredentials *creds, const char *service_id) {
|
||||
CURL *curl = curl_easy_init();
|
||||
|
|
@ -3636,6 +3752,12 @@ static int destroy_service(const UnsandboxCredentials *creds, const char *servic
|
|||
return 1;
|
||||
}
|
||||
|
||||
if (http_code == 428) {
|
||||
int result = handle_sudo_challenge(response.data, creds, "DELETE", url, path, NULL);
|
||||
free(response.data);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (http_code != 200) {
|
||||
fprintf(stderr, "Error: HTTP %ld\n", http_code);
|
||||
if (response.data) fprintf(stderr, "%s\n", response.data);
|
||||
|
|
@ -3754,6 +3876,12 @@ static int unlock_service(const UnsandboxCredentials *creds, const char *service
|
|||
return 1;
|
||||
}
|
||||
|
||||
if (http_code == 428) {
|
||||
int result = handle_sudo_challenge(response.data, creds, "POST", url, path, body);
|
||||
free(response.data);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (http_code != 200) {
|
||||
fprintf(stderr, "Error: HTTP %ld\n", http_code);
|
||||
if (response.data) fprintf(stderr, "%s\n", response.data);
|
||||
|
|
@ -4595,7 +4723,8 @@ static int redeploy_service(const UnsandboxCredentials *creds, const char *servi
|
|||
|
||||
// Execute a command in a running service container
|
||||
// Uses async job polling for long-running commands
|
||||
static int execute_service(const UnsandboxCredentials *creds, const char *service_id, const char *command, int timeout_ms) {
|
||||
// Optional input_files: files to upload before executing (written to /tmp/input/)
|
||||
static int execute_service(const UnsandboxCredentials *creds, const char *service_id, const char *command, int timeout_ms, struct InputFile *input_files, int input_file_count) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return 1;
|
||||
|
||||
|
|
@ -4617,10 +4746,38 @@ static int execute_service(const UnsandboxCredentials *creds, const char *servic
|
|||
return 1;
|
||||
}
|
||||
|
||||
char payload[8192];
|
||||
snprintf(payload, sizeof(payload), "{\"command\":\"%s\",\"timeout\":%d}", esc_command, timeout_ms);
|
||||
// Calculate payload size (base + files)
|
||||
size_t payload_size = 8192;
|
||||
for (int i = 0; i < input_file_count; i++) {
|
||||
payload_size += strlen(input_files[i].content_base64) + 256;
|
||||
}
|
||||
|
||||
char *payload = malloc(payload_size);
|
||||
if (!payload) {
|
||||
free(esc_command);
|
||||
curl_easy_cleanup(curl);
|
||||
free(response.data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char *p = payload;
|
||||
p += sprintf(p, "{\"command\":\"%s\",\"timeout\":%d", esc_command, timeout_ms);
|
||||
free(esc_command);
|
||||
|
||||
// Add input files if provided
|
||||
if (input_file_count > 0) {
|
||||
p += sprintf(p, ",\"input_files\":[");
|
||||
for (int i = 0; i < input_file_count; i++) {
|
||||
if (i > 0) *p++ = ',';
|
||||
char *esc_filename = escape_json_string(input_files[i].filename);
|
||||
p += sprintf(p, "{\"filename\":\"%s\",\"content\":\"%s\"}",
|
||||
esc_filename, input_files[i].content_base64);
|
||||
free(esc_filename);
|
||||
}
|
||||
p += sprintf(p, "]");
|
||||
}
|
||||
p += sprintf(p, "}");
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);
|
||||
|
|
@ -4638,6 +4795,7 @@ static int execute_service(const UnsandboxCredentials *creds, const char *servic
|
|||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(payload);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
|
||||
|
|
@ -5130,6 +5288,12 @@ static int delete_snapshot(const UnsandboxCredentials *creds, const char *snapsh
|
|||
return 1;
|
||||
}
|
||||
|
||||
if (http_code == 428) {
|
||||
int result = handle_sudo_challenge(response.data, creds, "DELETE", url, path, NULL);
|
||||
free(response.data);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (http_code != 200) {
|
||||
fprintf(stderr, "Error: HTTP %ld\n", http_code);
|
||||
if (response.data) fprintf(stderr, "%s\n", response.data);
|
||||
|
|
@ -5248,6 +5412,12 @@ static int unlock_snapshot(const UnsandboxCredentials *creds, const char *snapsh
|
|||
return 1;
|
||||
}
|
||||
|
||||
if (http_code == 428) {
|
||||
int result = handle_sudo_challenge(response.data, creds, "POST", url, path, body);
|
||||
free(response.data);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (http_code != 200) {
|
||||
fprintf(stderr, "Error: HTTP %ld\n", http_code);
|
||||
if (response.data) fprintf(stderr, "%s\n", response.data);
|
||||
|
|
@ -5799,11 +5969,34 @@ static int delete_image(const UnsandboxCredentials *creds, const char *image_id)
|
|||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
|
||||
free(response.data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (http_code == 428) {
|
||||
int result = handle_sudo_challenge(response.data, creds, "DELETE", url, path, NULL);
|
||||
free(response.data);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (http_code != 200) {
|
||||
fprintf(stderr, "Error: HTTP %ld\n", http_code);
|
||||
if (response.data) fprintf(stderr, "%s\n", response.data);
|
||||
free(response.data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Lock an image
|
||||
|
|
@ -5871,11 +6064,34 @@ static int unlock_image(const UnsandboxCredentials *creds, const char *image_id)
|
|||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
|
||||
free(response.data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (http_code == 428) {
|
||||
int result = handle_sudo_challenge(response.data, creds, "POST", url, path, "{}");
|
||||
free(response.data);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (http_code != 200) {
|
||||
fprintf(stderr, "Error: HTTP %ld\n", http_code);
|
||||
if (response.data) fprintf(stderr, "%s\n", response.data);
|
||||
free(response.data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
free(response.data);
|
||||
return (res == CURLE_OK) ? 0 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Set image visibility (private, unlisted, public)
|
||||
|
|
@ -6489,7 +6705,7 @@ void print_usage(const char *prog) {
|
|||
fprintf(stderr, " --no-show-freeze-page ID Disable freeze page (return JSON error when frozen)\n");
|
||||
fprintf(stderr, " --resize ID Resize service vCPU/memory (requires --vcpu)\n");
|
||||
fprintf(stderr, " --redeploy ID Re-run bootstrap script (optional: --bootstrap or --bootstrap-file)\n");
|
||||
fprintf(stderr, " --execute ID CMD Run a command in a running service\n");
|
||||
fprintf(stderr, " --execute ID CMD Run a command in a running service (use -f to upload files first)\n");
|
||||
fprintf(stderr, " --dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)\n");
|
||||
fprintf(stderr, " --snapshot ID Create snapshot of service (paid tiers only)\n");
|
||||
fprintf(stderr, " --restore SNAPSHOT Restore service from snapshot\n");
|
||||
|
|
@ -6568,6 +6784,7 @@ void print_usage(const char *prog) {
|
|||
fprintf(stderr, " %s service --redeploy abc123 --bootstrap-file ./script.sh\n", prog);
|
||||
fprintf(stderr, " %s service --redeploy abc123 # uses stored encrypted bootstrap\n", prog);
|
||||
fprintf(stderr, " %s service --execute maldoror 'journalctl -u myapp -n 50'\n", prog);
|
||||
fprintf(stderr, " %s service -f data.txt --execute myapp 'cat /tmp/input/data.txt' # upload then run\n", prog);
|
||||
fprintf(stderr, " %s service --dump-bootstrap maldoror # print bootstrap to stdout\n", prog);
|
||||
fprintf(stderr, " %s service --dump-bootstrap maldoror backup.sh # save to file\n", prog);
|
||||
fprintf(stderr, " %s service --name app -e API_KEY=secret -e DEBUG=1 # with env vars\n", prog);
|
||||
|
|
@ -10258,7 +10475,13 @@ int main(int argc, char *argv[]) {
|
|||
ret = redeploy_service(creds, service_id, bootstrap_to_use);
|
||||
} else if (do_execute) {
|
||||
// Default timeout 30 seconds (30000ms)
|
||||
ret = execute_service(creds, service_id, execute_command, 30000);
|
||||
// Pass input files if provided (written to /tmp/input/ before command runs)
|
||||
ret = execute_service(creds, service_id, execute_command, 30000, service_input_files, service_input_file_count);
|
||||
// Free input file memory
|
||||
for (int i = 0; i < service_input_file_count; i++) {
|
||||
free(service_input_files[i].filename);
|
||||
free(service_input_files[i].content_base64);
|
||||
}
|
||||
} else if (do_dump_bootstrap) {
|
||||
// Dump bootstrap script from /tmp/bootstrap.sh inside the service
|
||||
// This is useful for migrations - the bootstrap is stored at the same path on all instances
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue