From e58adcd5feaf92eb9ff2aed0be29fab830892080 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 22 Jan 2026 16:40:36 -0500 Subject: [PATCH] feat: add unfreeze_on_demand support to all SDKs Updated Python, JavaScript, Go, Rust, Ruby, Java, and TypeScript SDKs: - Add unfreeze_on_demand field to Service struct/class - Add setUnfreezeOnDemand function to toggle setting via PATCH /services/:id - Add unfreeze_on_demand parameter to createService function --- clients/go/async/src/un_async.go | 27 +++++++- clients/go/sync/src/un.go | 17 ++++- clients/java/async/src/UnsandboxAsync.java | 72 ++++++++++++++++++++ clients/java/sync/src/Un.java | 79 ++++++++++++++++++++++ clients/javascript/async/src/un_async.js | 22 +++++- clients/javascript/sync/src/un.js | 21 +++++- clients/python/async/src/un_async.py | 41 +++++++++++ clients/python/sync/src/un.py | 41 +++++++++++ clients/ruby/async/src/un_async.rb | 25 ++++++- clients/ruby/sync/src/un.rb | 23 ++++++- clients/rust/async/src/lib.rs | 28 ++++++++ clients/rust/sync/src/lib.rs | 28 ++++++++ clients/typescript/sync/src/un.ts | 21 ++++++ 13 files changed, 435 insertions(+), 10 deletions(-) diff --git a/clients/go/async/src/un_async.go b/clients/go/async/src/un_async.go index 9e08520..90ea19e 100644 --- a/clients/go/async/src/un_async.go +++ b/clients/go/async/src/un_async.go @@ -1100,9 +1100,10 @@ func ShellSession(creds *Credentials, sessionID, command string) <-chan SessionR // 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 + 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 } // ServiceUpdateOptions contains optional parameters for service updates. @@ -1184,6 +1185,9 @@ func CreateService(creds *Credentials, name string, ports []int, bootstrap strin if opts.VCPU > 0 { data["vcpu"] = opts.VCPU } + if opts.UnfreezeOnDemand { + data["unfreeze_on_demand"] = true + } } response, err := makeRequest("POST", "/services", creds, data) @@ -1304,6 +1308,23 @@ func UnlockService(creds *Credentials, serviceID string) <-chan ServiceResult { return resultChan } +// SetUnfreezeOnDemand enables or disables automatic unfreezing on HTTP request. +// Returns a channel that receives exactly one ServiceResult then closes. +func SetUnfreezeOnDemand(creds *Credentials, serviceID string, enabled bool) <-chan ServiceResult { + resultChan := make(chan ServiceResult, 1) + + go func() { + defer close(resultChan) + + response, err := makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, map[string]interface{}{ + "unfreeze_on_demand": enabled, + }) + resultChan <- ServiceResult{Data: response, Err: err} + }() + + return resultChan +} + // GetServiceLogs retrieves logs from a service. // Returns a channel that receives exactly one ServiceResult then closes. // diff --git a/clients/go/sync/src/un.go b/clients/go/sync/src/un.go index 404a247..4bf5cba 100644 --- a/clients/go/sync/src/un.go +++ b/clients/go/sync/src/un.go @@ -773,9 +773,10 @@ func ShellSession(creds *Credentials, sessionID, command string) (map[string]int // 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 + 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 } // ServiceUpdateOptions contains optional parameters for service updates. @@ -829,6 +830,9 @@ func CreateService(creds *Credentials, name string, ports []int, bootstrap strin if opts.VCPU > 0 { data["vcpu"] = opts.VCPU } + if opts.UnfreezeOnDemand { + data["unfreeze_on_demand"] = true + } } return makeRequest("POST", "/services", creds, data) @@ -875,6 +879,13 @@ func UnlockService(creds *Credentials, serviceID string) (map[string]interface{} return makeRequest("POST", fmt.Sprintf("/services/%s/unlock", serviceID), creds, map[string]interface{}{}) } +// SetUnfreezeOnDemand enables or disables automatic unfreezing on HTTP request. +func SetUnfreezeOnDemand(creds *Credentials, serviceID string, enabled bool) (map[string]interface{}, error) { + return makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, map[string]interface{}{ + "unfreeze_on_demand": enabled, + }) +} + // GetServiceLogs retrieves logs from a service. // // Args: diff --git a/clients/java/async/src/UnsandboxAsync.java b/clients/java/async/src/UnsandboxAsync.java index 103db74..e045f9a 100644 --- a/clients/java/async/src/UnsandboxAsync.java +++ b/clients/java/async/src/UnsandboxAsync.java @@ -1352,6 +1352,54 @@ public class UnsandboxAsync { return makeRequest("POST", "/services", creds[0], creds[1], data); } + /** + * Create a new service (long-running container) with unfreeze-on-demand option. + * + * @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 publicKey Optional API key + * @param secretKey Optional API secret + * @return CompletableFuture containing response map with service_id + */ + public static CompletableFuture> createService( + String name, + String ports, + String bootstrap, + boolean unfreezeOnDemand, + String publicKey, + String secretKey + ) { + String[] creds = resolveCredentials(publicKey, secretKey); + + Map data = new LinkedHashMap<>(); + data.put("name", name); + if (ports != null && !ports.isEmpty()) { + List portList = new ArrayList<>(); + for (String p : ports.split(",")) { + try { + portList.add(Integer.parseInt(p.trim())); + } catch (NumberFormatException e) { + // Skip invalid port + } + } + data.put("ports", portList); + } + if (bootstrap != null && !bootstrap.isEmpty()) { + if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) { + data.put("bootstrap_url", bootstrap); + } else { + data.put("bootstrap", bootstrap); + } + } + if (unfreezeOnDemand) { + data.put("unfreeze_on_demand", true); + } + + return makeRequest("POST", "/services", creds[0], creds[1], data); + } + /** * Get details of a specific service. * @@ -1473,6 +1521,30 @@ public class UnsandboxAsync { return makeRequest("POST", "/services/" + serviceId + "/unlock", creds[0], creds[1], new LinkedHashMap<>()); } + /** + * Set unfreeze-on-demand for a service. + * + *

When enabled, a frozen service will automatically wake up when it receives + * an HTTP request, without requiring an explicit unfreeze API call. + * + * @param serviceId Service ID to configure + * @param enabled True to enable unfreeze-on-demand, false to disable + * @param publicKey Optional API key + * @param secretKey Optional API secret + * @return CompletableFuture containing response map with update confirmation + */ + public static CompletableFuture> setUnfreezeOnDemand( + String serviceId, + boolean enabled, + String publicKey, + String secretKey + ) { + String[] creds = resolveCredentials(publicKey, secretKey); + Map data = new LinkedHashMap<>(); + data.put("unfreeze_on_demand", enabled); + return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], data); + } + /** * Get bootstrap logs for a service. * diff --git a/clients/java/sync/src/Un.java b/clients/java/sync/src/Un.java index 46820bb..bc67dac 100644 --- a/clients/java/sync/src/Un.java +++ b/clients/java/sync/src/Un.java @@ -1345,6 +1345,58 @@ public class Un { return makeRequest("POST", "/services", creds[0], creds[1], data); } + /** + * Create a new service (long-running container) with unfreeze-on-demand option. + * + * @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 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 createService( + String name, + String ports, + String bootstrap, + boolean unfreezeOnDemand, + String publicKey, + String secretKey + ) throws IOException { + String[] creds = resolveCredentials(publicKey, secretKey); + + Map data = new LinkedHashMap<>(); + data.put("name", name); + if (ports != null && !ports.isEmpty()) { + // Parse ports string into list + List portList = new ArrayList<>(); + for (String p : ports.split(",")) { + try { + portList.add(Integer.parseInt(p.trim())); + } catch (NumberFormatException e) { + // Skip invalid port + } + } + data.put("ports", portList); + } + if (bootstrap != null && !bootstrap.isEmpty()) { + if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) { + data.put("bootstrap_url", bootstrap); + } else { + data.put("bootstrap", bootstrap); + } + } + if (unfreezeOnDemand) { + data.put("unfreeze_on_demand", true); + } + + return makeRequest("POST", "/services", creds[0], creds[1], data); + } + /** * Get details of a specific service. * @@ -1487,6 +1539,33 @@ public class Un { return makeRequest("POST", "/services/" + serviceId + "/unlock", creds[0], creds[1], new LinkedHashMap<>()); } + /** + * Set unfreeze-on-demand for a service. + * + *

When enabled, a frozen service will automatically wake up when it receives + * an HTTP request, without requiring an explicit unfreeze API call. + * + * @param serviceId Service ID to configure + * @param enabled True to enable unfreeze-on-demand, false to disable + * @param publicKey Optional API key + * @param secretKey Optional API secret + * @return Response map with update confirmation + * @throws IOException on network errors + * @throws CredentialsException if credentials cannot be found + * @throws ApiException if API returns an error + */ + public static Map setUnfreezeOnDemand( + String serviceId, + boolean enabled, + String publicKey, + String secretKey + ) throws IOException { + String[] creds = resolveCredentials(publicKey, secretKey); + Map data = new LinkedHashMap<>(); + data.put("unfreeze_on_demand", enabled); + return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], data); + } + /** * Get bootstrap logs for a service. * diff --git a/clients/javascript/async/src/un_async.js b/clients/javascript/async/src/un_async.js index 8f59483..40e0993 100644 --- a/clients/javascript/async/src/un_async.js +++ b/clients/javascript/async/src/un_async.js @@ -13,7 +13,7 @@ * freezeSession, unfreezeSession, boostSession, unboostSession, shellSession, * // Service management * listServices, createService, getService, updateService, deleteService, - * freezeService, unfreezeService, lockService, unlockService, + * freezeService, unfreezeService, lockService, unlockService, setUnfreezeOnDemand, * getServiceLogs, getServiceEnv, setServiceEnv, deleteServiceEnv, * exportServiceEnv, redeployService, executeInService, * // Snapshot management @@ -751,6 +751,7 @@ async function listServices(publicKey, secretKey) { * - vcpu: Number of vCPUs (1-8) * - domains: Array of custom domains * - serviceType: Service type for SRV records (minecraft, mumble, etc.) + * - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic * * Returns: Promise (service info with service_id) */ @@ -771,6 +772,7 @@ async function createService(name, ports, bootstrap, opts = {}, publicKey, secre if (opts.vcpu && opts.vcpu > 1) data.vcpu = opts.vcpu; if (opts.domains) data.custom_domains = opts.domains; if (opts.serviceType) data.service_type = opts.serviceType; + if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true; return makeRequest('POST', '/services', publicKey, secretKey, data); } @@ -870,6 +872,22 @@ async function unlockService(serviceId, publicKey, secretKey) { return makeRequest('POST', `/services/${serviceId}/unlock`, publicKey, secretKey, {}); } +/** + * Set unfreeze-on-demand for a service. + * + * When enabled, frozen services will automatically wake when HTTP traffic arrives. + * + * Args: + * serviceId: Service ID to update + * enabled: true to enable, false to disable + * + * Returns: Promise (update confirmation) + */ +async function setUnfreezeOnDemand(serviceId, enabled, publicKey, secretKey) { + [publicKey, secretKey] = resolveCredentials(publicKey, secretKey); + return makeRequest('PATCH', `/services/${serviceId}`, publicKey, secretKey, { unfreeze_on_demand: enabled }); +} + /** * Get service logs. * @@ -1127,6 +1145,7 @@ export { unfreezeService, lockService, unlockService, + setUnfreezeOnDemand, getServiceLogs, getServiceEnv, setServiceEnv, @@ -1185,6 +1204,7 @@ export default { unfreezeService, lockService, unlockService, + setUnfreezeOnDemand, getServiceLogs, getServiceEnv, setServiceEnv, diff --git a/clients/javascript/sync/src/un.js b/clients/javascript/sync/src/un.js index 7a1c4f8..85f1976 100644 --- a/clients/javascript/sync/src/un.js +++ b/clients/javascript/sync/src/un.js @@ -13,7 +13,7 @@ * freezeSession, unfreezeSession, boostSession, unboostSession, shellSession, * // Service management * listServices, createService, getService, updateService, deleteService, - * freezeService, unfreezeService, lockService, unlockService, + * freezeService, unfreezeService, lockService, unlockService, setUnfreezeOnDemand, * getServiceLogs, getServiceEnv, setServiceEnv, deleteServiceEnv, * exportServiceEnv, redeployService, executeInService, * // Snapshot management @@ -735,6 +735,7 @@ async function listServices(publicKey, secretKey) { * - vcpu: Number of vCPUs (1-8) * - domains: Array of custom domains * - serviceType: Service type for SRV records (minecraft, mumble, etc.) + * - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic * * Returns: Promise (service info with service_id) */ @@ -755,6 +756,7 @@ async function createService(name, ports, bootstrap, opts = {}, publicKey, secre if (opts.vcpu && opts.vcpu > 1) data.vcpu = opts.vcpu; if (opts.domains) data.custom_domains = opts.domains; if (opts.serviceType) data.service_type = opts.serviceType; + if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true; return makeRequest('POST', '/services', publicKey, secretKey, data); } @@ -854,6 +856,22 @@ async function unlockService(serviceId, publicKey, secretKey) { return makeRequest('POST', `/services/${serviceId}/unlock`, publicKey, secretKey, {}); } +/** + * Set unfreeze-on-demand for a service. + * + * When enabled, frozen services will automatically wake when HTTP traffic arrives. + * + * Args: + * serviceId: Service ID to update + * enabled: true to enable, false to disable + * + * Returns: Promise (update confirmation) + */ +async function setUnfreezeOnDemand(serviceId, enabled, publicKey, secretKey) { + [publicKey, secretKey] = resolveCredentials(publicKey, secretKey); + return makeRequest('PATCH', `/services/${serviceId}`, publicKey, secretKey, { unfreeze_on_demand: enabled }); +} + /** * Get service logs. * @@ -1356,6 +1374,7 @@ module.exports = { unfreezeService, lockService, unlockService, + setUnfreezeOnDemand, getServiceLogs, getServiceEnv, setServiceEnv, diff --git a/clients/python/async/src/un_async.py b/clients/python/async/src/un_async.py index 4671646..fe3dd2e 100644 --- a/clients/python/async/src/un_async.py +++ b/clients/python/async/src/un_async.py @@ -35,6 +35,7 @@ Library Usage: unfreeze_service, lock_service, unlock_service, + set_unfreeze_on_demand, get_service_logs, get_service_env, set_service_env, @@ -1046,6 +1047,7 @@ async def create_service( custom_domains: Optional[List[str]] = None, vcpu: int = 1, service_type: Optional[str] = None, + unfreeze_on_demand: bool = False, ) -> Dict[str, Any]: """ Create a new persistent service. @@ -1060,6 +1062,7 @@ async def create_service( custom_domains: Optional list of custom domain names vcpu: Number of vCPUs (1-8, default 1) service_type: Optional service type for SRV records (e.g., "minecraft") + unfreeze_on_demand: If True, automatically unfreeze service on incoming requests Returns: Response dict containing service_id, etc. @@ -1087,6 +1090,8 @@ async def create_service( data["vcpu"] = vcpu if service_type: data["service_type"] = service_type + if unfreeze_on_demand: + data["unfreeze_on_demand"] = unfreeze_on_demand return await _make_request("POST", "/services", public_key, secret_key, data) @@ -1275,6 +1280,42 @@ async def unlock_service( return await _make_request("POST", f"/services/{service_id}/unlock", public_key, secret_key, {}) +async def set_unfreeze_on_demand( + service_id: str, + enabled: bool, + public_key: Optional[str] = None, + secret_key: Optional[str] = None, +) -> Dict[str, Any]: + """ + Enable or disable automatic unfreezing on incoming requests. + + When enabled, a frozen service will automatically wake up when it + receives an incoming HTTP request. + + Args: + service_id: Service ID to configure + enabled: True to enable auto-unfreeze, False to disable + public_key: Optional API key + secret_key: Optional API secret + + Returns: + Response dict with update confirmation + + Raises: + aiohttp.ClientError: Network errors + ValueError: Invalid response format + CredentialsError: Missing credentials + """ + public_key, secret_key = _resolve_credentials(public_key, secret_key) + return await _make_request( + "PATCH", + f"/services/{service_id}", + public_key, + secret_key, + {"unfreeze_on_demand": enabled}, + ) + + async def get_service_logs( service_id: str, all_logs: bool = False, diff --git a/clients/python/sync/src/un.py b/clients/python/sync/src/un.py index d7b051e..1d271f1 100644 --- a/clients/python/sync/src/un.py +++ b/clients/python/sync/src/un.py @@ -34,6 +34,7 @@ Library Usage: unfreeze_service, lock_service, unlock_service, + set_unfreeze_on_demand, get_service_logs, get_service_env, set_service_env, @@ -1060,6 +1061,7 @@ def create_service( custom_domains: Optional[List[str]] = None, vcpu: int = 1, service_type: Optional[str] = None, + unfreeze_on_demand: bool = False, ) -> Dict[str, Any]: """ Create a new persistent service. @@ -1074,6 +1076,7 @@ def create_service( custom_domains: Optional list of custom domain names vcpu: Number of vCPUs (1-8, default 1) service_type: Optional service type for SRV records (e.g., "minecraft") + unfreeze_on_demand: If True, automatically unfreeze service on incoming requests Returns: Response dict containing service_id, etc. @@ -1101,6 +1104,8 @@ def create_service( data["vcpu"] = vcpu if service_type: data["service_type"] = service_type + if unfreeze_on_demand: + data["unfreeze_on_demand"] = unfreeze_on_demand return _make_request("POST", "/services", public_key, secret_key, data) @@ -1289,6 +1294,42 @@ def unlock_service( return _make_request("POST", f"/services/{service_id}/unlock", public_key, secret_key, {}) +def set_unfreeze_on_demand( + service_id: str, + enabled: bool, + public_key: Optional[str] = None, + secret_key: Optional[str] = None, +) -> Dict[str, Any]: + """ + Enable or disable automatic unfreezing on incoming requests. + + When enabled, a frozen service will automatically wake up when it + receives an incoming HTTP request. + + Args: + service_id: Service ID to configure + enabled: True to enable auto-unfreeze, False to disable + public_key: Optional API key + secret_key: Optional API secret + + Returns: + Response dict with update confirmation + + Raises: + requests.RequestException: Network errors + ValueError: Invalid response format + CredentialsError: Missing credentials + """ + public_key, secret_key = _resolve_credentials(public_key, secret_key) + return _make_request( + "PATCH", + f"/services/{service_id}", + public_key, + secret_key, + {"unfreeze_on_demand": enabled}, + ) + + def get_service_logs( service_id: str, all_logs: bool = False, diff --git a/clients/ruby/async/src/un_async.rb b/clients/ruby/async/src/un_async.rb index d531996..94740ed 100644 --- a/clients/ruby/async/src/un_async.rb +++ b/clients/ruby/async/src/un_async.rb @@ -765,6 +765,7 @@ module UnAsync # @param vcpu [Integer] Number of vCPUs (1-8) # @param custom_domains [Array, nil] Custom domains for the service # @param service_type [String, nil] Service type for SRV records + # @param unfreeze_on_demand [Boolean] If true, service will auto-wake on HTTP request (default: false) # @return [Future] Future resolving to response hash with service_id # @raise [CredentialsError] If no credentials found (on .value) # @raise [APIError] If API request fails (on .value) @@ -772,7 +773,7 @@ module UnAsync # @example # result = UnAsync.create_service("web", [80, 443], "apt install -y nginx && nginx").value # 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) + 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) Future.new do pk, sk = resolve_credentials(public_key, secret_key) data = { @@ -784,6 +785,7 @@ module UnAsync data[:vcpu] = vcpu if vcpu > 1 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 make_request_sync('POST', '/services', pk, sk, data) end end @@ -920,6 +922,27 @@ module UnAsync end end + # Set unfreeze-on-demand for a service + # + # When enabled, a frozen service will automatically wake when it receives an HTTP request. + # + # @param service_id [String] Service ID to update + # @param enabled [Boolean] Whether to enable unfreeze-on-demand + # @param public_key [String, nil] Optional API key + # @param secret_key [String, nil] Optional API secret + # @return [Future] Future resolving to response hash with update confirmation + # @raise [CredentialsError] If no credentials found (on .value) + # @raise [APIError] If API request fails (on .value) + # + # @example + # UnAsync.set_unfreeze_on_demand(service_id, true).value + def set_unfreeze_on_demand(service_id, enabled, public_key: nil, secret_key: nil) + Future.new do + pk, sk = resolve_credentials(public_key, secret_key) + make_request_sync('PATCH', "/services/#{service_id}", pk, sk, { unfreeze_on_demand: enabled }) + end + end + # Get service logs (bootstrap output) # # @param service_id [String] Service ID to get logs for diff --git a/clients/ruby/sync/src/un.rb b/clients/ruby/sync/src/un.rb index 5ec687f..4bd583d 100644 --- a/clients/ruby/sync/src/un.rb +++ b/clients/ruby/sync/src/un.rb @@ -894,6 +894,7 @@ module Un # @param vcpu [Integer] Number of vCPUs (1-8) # @param custom_domains [Array, 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) # @return [Hash] Response hash with service_id # @raise [CredentialsError] If no credentials found # @raise [APIError] If API request fails @@ -901,7 +902,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) + 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) pk, sk = resolve_credentials(public_key, secret_key) data = { name: name, @@ -912,6 +913,7 @@ module Un data[:vcpu] = vcpu if vcpu > 1 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 make_request('POST', '/services', pk, sk, data) end @@ -1033,6 +1035,25 @@ module Un make_request('POST', "/services/#{service_id}/unlock", pk, sk, {}) end + # Set unfreeze-on-demand for a service + # + # When enabled, a frozen service will automatically wake when it receives an HTTP request. + # + # @param service_id [String] Service ID to update + # @param enabled [Boolean] Whether to enable unfreeze-on-demand + # @param public_key [String, nil] Optional API key + # @param secret_key [String, nil] Optional API secret + # @return [Hash] Response hash with update confirmation + # @raise [CredentialsError] If no credentials found + # @raise [APIError] If API request fails + # + # @example + # Un.set_unfreeze_on_demand(service_id, true) + def set_unfreeze_on_demand(service_id, enabled, public_key: nil, secret_key: nil) + pk, sk = resolve_credentials(public_key, secret_key) + make_request('PATCH', "/services/#{service_id}", pk, sk, { unfreeze_on_demand: enabled }) + end + # Get service logs (bootstrap output) # # @param service_id [String] Service ID to get logs for diff --git a/clients/rust/async/src/lib.rs b/clients/rust/async/src/lib.rs index c2b4cab..d9a801c 100644 --- a/clients/rust/async/src/lib.rs +++ b/clients/rust/async/src/lib.rs @@ -316,6 +316,9 @@ pub struct Service { /// Whether the service is locked (cannot be modified) #[serde(default)] pub locked: bool, + /// Whether the service automatically unfreezes on incoming HTTP requests + #[serde(default)] + pub unfreeze_on_demand: bool, /// Public URL for the service #[serde(default)] pub url: String, @@ -382,6 +385,8 @@ pub struct ServiceCreateOptions { pub bootstrap: Option, /// Bootstrap script URL pub bootstrap_url: Option, + /// Whether to enable automatic unfreezing on incoming HTTP requests + pub unfreeze_on_demand: Option, } /// Options for updating a service @@ -1500,6 +1505,9 @@ pub async fn create_service( if let Some(bootstrap_url) = opts.bootstrap_url { body["bootstrap_url"] = serde_json::json!(bootstrap_url); } + if let Some(unfreeze_on_demand) = opts.unfreeze_on_demand { + body["unfreeze_on_demand"] = serde_json::json!(unfreeze_on_demand); + } } make_request("POST", "/services", creds, Some(&body)).await @@ -1618,6 +1626,26 @@ pub async fn unlock_service(service_id: &str, creds: &Credentials) -> Result Result { + let path = format!("/services/{}", service_id); + let body = serde_json::json!({ + "unfreeze_on_demand": enabled + }); + make_request("PATCH", &path, creds, Some(&body)).await +} + /// Get bootstrap logs for a service. /// /// # Arguments diff --git a/clients/rust/sync/src/lib.rs b/clients/rust/sync/src/lib.rs index 0e29223..4d567ea 100644 --- a/clients/rust/sync/src/lib.rs +++ b/clients/rust/sync/src/lib.rs @@ -311,6 +311,9 @@ pub struct Service { /// Whether the service is locked (cannot be modified) #[serde(default)] pub locked: bool, + /// Whether the service automatically unfreezes on incoming HTTP requests + #[serde(default)] + pub unfreeze_on_demand: bool, /// Public URL for the service #[serde(default)] pub url: String, @@ -377,6 +380,8 @@ pub struct ServiceCreateOptions { pub bootstrap: Option, /// Bootstrap script URL pub bootstrap_url: Option, + /// Whether to enable automatic unfreezing on incoming HTTP requests + pub unfreeze_on_demand: Option, } /// Options for updating a service @@ -1807,6 +1812,9 @@ pub fn create_service( if let Some(bootstrap_url) = opts.bootstrap_url { body["bootstrap_url"] = serde_json::json!(bootstrap_url); } + if let Some(unfreeze_on_demand) = opts.unfreeze_on_demand { + body["unfreeze_on_demand"] = serde_json::json!(unfreeze_on_demand); + } } make_request("POST", "/services", creds, Some(&body)) @@ -1925,6 +1933,26 @@ pub fn unlock_service(service_id: &str, creds: &Credentials) -> Result make_request("POST", &path, creds, Some(&body)) } +/// Set the unfreeze_on_demand flag for a service. +/// +/// When enabled, the service will automatically unfreeze when it receives +/// an incoming HTTP request while frozen. +/// +/// # Arguments +/// * `service_id` - Service ID to update +/// * `enabled` - Whether to enable automatic unfreezing on demand +/// * `creds` - API credentials +/// +/// # Returns +/// Updated Service information +pub fn set_unfreeze_on_demand(service_id: &str, enabled: bool, creds: &Credentials) -> Result { + let path = format!("/services/{}", service_id); + let body = serde_json::json!({ + "unfreeze_on_demand": enabled + }); + make_request("PATCH", &path, creds, Some(&body)) +} + /// Get bootstrap logs for a service. /// /// # Arguments diff --git a/clients/typescript/sync/src/un.ts b/clients/typescript/sync/src/un.ts index a6a3c98..97d5b78 100644 --- a/clients/typescript/sync/src/un.ts +++ b/clients/typescript/sync/src/un.ts @@ -141,6 +141,8 @@ interface Args { visibilityMode: string | null; imageSpawn: string | null; imageClone: string | null; + unfreezeOnDemand: boolean | null; + setUnfreezeOnDemand: string | null; } interface ApiKeys { @@ -687,6 +689,14 @@ async function cmdService(args: Args): Promise { return; } + if (args.setUnfreezeOnDemand) { + const enabled = args.unfreezeOnDemand === true; + const payload = { unfreeze_on_demand: enabled }; + await apiRequest(`/services/${args.setUnfreezeOnDemand}`, "PATCH", payload, keys); + console.log(`${GREEN}Unfreeze on demand ${enabled ? 'enabled' : 'disabled'} for service ${args.setUnfreezeOnDemand}${RESET}`); + return; + } + if (args.execute) { const payload = { command: args.command_arg }; const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys); @@ -755,6 +765,7 @@ async function cmdService(args: Args): Promise { } if (args.network) payload.network = args.network; if (args.vcpu) payload.vcpu = args.vcpu; + if (args.unfreezeOnDemand) payload.unfreeze_on_demand = true; const result = await apiRequest("/services", "POST", payload, keys); const serviceId = result.id; @@ -1010,6 +1021,8 @@ function parseArgs(argv: string[]): Args { visibilityMode: null, imageSpawn: null, imageClone: null, + unfreezeOnDemand: null, + setUnfreezeOnDemand: null, }; let i = 2; @@ -1170,6 +1183,12 @@ function parseArgs(argv: string[]): Args { args.clone = argv[++i]; } i++; + } else if (arg === '--unfreeze-on-demand') { + args.unfreezeOnDemand = true; + i++; + } else if (arg === '--set-unfreeze-on-demand' && i + 1 < argv.length) { + args.setUnfreezeOnDemand = argv[++i]; + i++; } else if (!arg.startsWith('-')) { args.sourceFile = arg; i++; @@ -1251,6 +1270,8 @@ Service options: --command CMD Command to execute (with --execute) --dump-bootstrap ID Dump bootstrap script --dump-file FILE File to save bootstrap (with --dump-bootstrap) + --unfreeze-on-demand Enable unfreeze on demand (with --name or --set-unfreeze-on-demand) + --set-unfreeze-on-demand ID Set unfreeze on demand for service Image options: --list List all images