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.
This commit is contained in:
timehexon 2026-02-02 15:24:08 +00:00
parent 2ae90fce12
commit cf07b22a72
3 changed files with 208 additions and 0 deletions

View file

@ -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) |

View file

@ -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) {

View file

@ -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)