From cf07b22a725e8822073ce33b87cce2f2a2e39aec Mon Sep 17 00:00:00 2001 From: timehexon Date: Mon, 2 Feb 2026 15:24:08 +0000 Subject: [PATCH] Add --add-domain, --remove-domain, --set-domains flags to C and Python CLIs Post-creation custom domain management for services. All three flags require --domains with a comma-separated list. Calls PUT /services/{id}/domains with add, remove, or custom_domains action. CLI_SPEC.md updated. --- clients/CLI_SPEC.md | 6 ++ clients/c/src/un.c | 148 ++++++++++++++++++++++++++++++++++ clients/python/sync/src/un.py | 54 +++++++++++++ 3 files changed, 208 insertions(+) diff --git a/clients/CLI_SPEC.md b/clients/CLI_SPEC.md index b272e93..cff44ed 100644 --- a/clients/CLI_SPEC.md +++ b/clients/CLI_SPEC.md @@ -140,6 +140,9 @@ un service --unfreeze ID # Resume service un service --destroy ID # Delete service un service --lock ID # Prevent deletion un service --unlock ID # Allow deletion +un service --add-domain ID --domains example.com # Add domains +un service --remove-domain ID --domains example.com # Remove domains +un service --set-domains ID --domains a.com,b.com # Replace all domains un service --execute ID 'cmd' # Run command in service un service --redeploy ID # Re-run bootstrap un service --snapshot ID # Create snapshot @@ -165,6 +168,9 @@ un service --snapshot ID # Create snapshot | `--destroy ID` | Delete service | | `--lock ID` | Prevent deletion | | `--unlock ID` | Allow deletion | +| `--add-domain ID` | Add domains (requires --domains) | +| `--remove-domain ID` | Remove domains (requires --domains) | +| `--set-domains ID` | Replace all domains (requires --domains) | | `--auto-unfreeze ID` | Enable auto-unfreeze on HTTP request | | `--no-auto-unfreeze ID` | Disable auto-unfreeze | | `--show-freeze-page ID` | Show payment page when frozen (default) | diff --git a/clients/c/src/un.c b/clients/c/src/un.c index 94ce0ba..bc803d7 100644 --- a/clients/c/src/un.c +++ b/clients/c/src/un.c @@ -3766,6 +3766,115 @@ static int unlock_service(const UnsandboxCredentials *creds, const char *service return 0; } +// Update custom domains for a service +// action: "custom_domains" (full replace), "add", or "remove" +// domains: comma-separated domain list +static int update_service_domains(const UnsandboxCredentials *creds, const char *service_id, const char *action, const char *domains) { + CURL *curl = curl_easy_init(); + if (!curl) return 1; + + struct ResponseBuffer response = {0}; + response.data = malloc(1); + response.size = 0; + + char url[512]; + snprintf(url, sizeof(url), "%s/services/%s/domains", API_BASE, service_id); + + char path[256]; + snprintf(path, sizeof(path), "/services/%s/domains", service_id); + + // Build JSON payload: {"action": ["domain1", "domain2"]} + size_t payload_size = strlen(domains) * 2 + 256; + char *payload = malloc(payload_size); + if (!payload) { + curl_easy_cleanup(curl); + return 1; + } + char *p = payload; + p += sprintf(p, "{\"%s\":[", action); + + char *domains_copy = strdup(domains); + char *token = strtok(domains_copy, ","); + int first = 1; + while (token) { + while (*token == ' ') token++; + char *end = token + strlen(token) - 1; + while (end > token && *end == ' ') *end-- = '\0'; + if (!first) p += sprintf(p, ","); + char *esc = escape_json_string(token); + p += sprintf(p, "\"%s\"", esc); + free(esc); + first = 0; + token = strtok(NULL, ","); + } + free(domains_copy); + p += sprintf(p, "]}"); + + struct curl_slist *headers = NULL; + headers = add_hmac_auth_headers(headers, creds, "PUT", path, payload); + headers = curl_slist_append(headers, "Content-Type: application/json"); + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + 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); + free(payload); + + if (res != CURLE_OK) { + fprintf(stderr, "Error: %s\n", curl_easy_strerror(res)); + free(response.data); + return 1; + } + + if (http_code == 404) { + fprintf(stderr, "Error: Service not found\n"); + free(response.data); + return 1; + } + + if (http_code == 403) { + fprintf(stderr, "Error: Not authorized to modify this service\n"); + free(response.data); + return 1; + } + + 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; + } + + printf("\033[32mCustom domains updated successfully\033[0m\n"); + // Print resulting domains from response + if (response.data) { + // Quick parse for custom_domains array + char *cd = strstr(response.data, "\"custom_domains\""); + if (cd) { + char *start = strchr(cd, '['); + char *end = start ? strchr(start, ']') : NULL; + if (start && end) { + char tmp = *(end + 1); + *(end + 1) = '\0'; + printf("Domains: %s\n", start); + *(end + 1) = tmp; + } + } + } + free(response.data); + return 0; +} + // Set unfreeze_on_demand for a service static int set_unfreeze_on_demand(const UnsandboxCredentials *creds, const char *service_id, int enabled) { CURL *curl = curl_easy_init(); @@ -9735,6 +9844,9 @@ int main(int argc, char *argv[]) { const char *restore_snapshot_id = NULL; const char *snapshot_name = NULL; int hot_snapshot = 0; + int do_add_domain = 0; + int do_remove_domain = 0; + int do_set_domains = 0; int create_unfreeze_on_demand = 0; // For --unfreeze-on-demand flag on create // Environment variables for service create @@ -9877,6 +9989,18 @@ int main(int argc, char *argv[]) { do_unlock = 1; i++; service_id = argv[i]; + } else if (strcmp(argv[i], "--add-domain") == 0 && i + 1 < argc) { + do_add_domain = 1; + i++; + service_id = argv[i]; + } else if (strcmp(argv[i], "--remove-domain") == 0 && i + 1 < argc) { + do_remove_domain = 1; + i++; + service_id = argv[i]; + } else if (strcmp(argv[i], "--set-domains") == 0 && i + 1 < argc) { + do_set_domains = 1; + i++; + service_id = argv[i]; } else if (strcmp(argv[i], "--auto-unfreeze") == 0 && i + 1 < argc) { do_auto_unfreeze = 1; i++; @@ -10086,6 +10210,30 @@ int main(int argc, char *argv[]) { ret = lock_service(creds, service_id); } else if (do_unlock) { ret = unlock_service(creds, service_id); + } else if (do_add_domain) { + if (!service_domains) { + fprintf(stderr, "Error: --domains required with --add-domain\n"); + curl_global_cleanup(); + free_credentials(creds); + return 2; + } + ret = update_service_domains(creds, service_id, "add", service_domains); + } else if (do_remove_domain) { + if (!service_domains) { + fprintf(stderr, "Error: --domains required with --remove-domain\n"); + curl_global_cleanup(); + free_credentials(creds); + return 2; + } + ret = update_service_domains(creds, service_id, "remove", service_domains); + } else if (do_set_domains) { + if (!service_domains) { + fprintf(stderr, "Error: --domains required with --set-domains\n"); + curl_global_cleanup(); + free_credentials(creds); + return 2; + } + ret = update_service_domains(creds, service_id, "custom_domains", service_domains); } else if (do_auto_unfreeze) { ret = set_unfreeze_on_demand(creds, service_id, 1); } else if (do_no_auto_unfreeze) { diff --git a/clients/python/sync/src/un.py b/clients/python/sync/src/un.py index c8b5e0e..6e93b1f 100644 --- a/clients/python/sync/src/un.py +++ b/clients/python/sync/src/un.py @@ -1305,6 +1305,30 @@ def unlock_service( return _make_request("POST", f"/services/{service_id}/unlock", public_key, secret_key, {}) +def update_service_domains( + service_id: str, + domains: list, + action: str = "custom_domains", + public_key: Optional[str] = None, + secret_key: Optional[str] = None, +) -> Dict[str, Any]: + """ + Update custom domains for a service. + + Args: + service_id: Service ID + domains: List of domain strings + action: "custom_domains" (replace), "add" (merge), or "remove" (subtract) + public_key: Optional API key + secret_key: Optional API secret + + Returns: + Response dict with updated domains + """ + public_key, secret_key = _resolve_credentials(public_key, secret_key) + return _make_request("PUT", f"/services/{service_id}/domains", public_key, secret_key, {action: domains}) + + def set_unfreeze_on_demand( service_id: str, enabled: bool, @@ -2362,6 +2386,12 @@ Examples: help="Prevent deletion") service_group.add_argument("--unlock", metavar="ID", help="Allow deletion") + service_group.add_argument("--add-domain", metavar="ID", + help="Add domains (with --domains)") + service_group.add_argument("--remove-domain", metavar="ID", + help="Remove domains (with --domains)") + service_group.add_argument("--set-domains", metavar="ID", + help="Replace all domains (with --domains)") service_group.add_argument("--resize", metavar="ID", help="Resize service (with --vcpu)") service_group.add_argument("--redeploy", metavar="ID", @@ -2667,6 +2697,30 @@ def _handle_service_command(args, public_key: str, secret_key: str): elif args.unlock: result = unlock_service(args.unlock, public_key, secret_key) print(f"Service {args.unlock} unlocked") + elif getattr(args, 'add_domain', None): + if not args.domains: + print("Error: --domains required with --add-domain", file=sys.stderr) + sys.exit(2) + domains = [d.strip() for d in args.domains.split(",")] + result = update_service_domains(args.add_domain, domains, "add", public_key, secret_key) + print(f"Domains added to {args.add_domain}") + print(f"Domains: {result.get('custom_domains', [])}") + elif getattr(args, 'remove_domain', None): + if not args.domains: + print("Error: --domains required with --remove-domain", file=sys.stderr) + sys.exit(2) + domains = [d.strip() for d in args.domains.split(",")] + result = update_service_domains(args.remove_domain, domains, "remove", public_key, secret_key) + print(f"Domains removed from {args.remove_domain}") + print(f"Domains: {result.get('custom_domains', [])}") + elif getattr(args, 'set_domains', None): + if not args.domains: + print("Error: --domains required with --set-domains", file=sys.stderr) + sys.exit(2) + domains = [d.strip() for d in args.domains.split(",")] + result = update_service_domains(args.set_domains, domains, "custom_domains", public_key, secret_key) + print(f"Domains set for {args.set_domains}") + print(f"Domains: {result.get('custom_domains', [])}") elif args.resize: vcpu = getattr(args, 'vcpu', 1) or 1 result = update_service(args.resize, public_key, secret_key, vcpu=vcpu)