From 3d01df56e5c0f8295d36cb4044025612954ab65f Mon Sep 17 00:00:00 2001 From: "shawn.wu" Date: Fri, 9 Oct 2026 14:55:02 -0700 Subject: [PATCH 1/2] fix(sandboxes): align CLI with hosting-2 lifetime contract --- docs/project-configuration.md | 10 +- internal/apiclient/client.gen.go | 266 ++++++++++++++---- internal/cmd/config/config_test.go | 3 +- internal/projectconfig/manifest.go | 11 +- .../projectconfig/manifest_sandboxes_test.go | 10 +- openapi/openapi.yaml | 172 +++++++---- tests/e2e/localmode/sandbox_config_test.go | 16 +- 7 files changed, 356 insertions(+), 132 deletions(-) diff --git a/docs/project-configuration.md b/docs/project-configuration.md index 2262ece..b64803c 100644 --- a/docs/project-configuration.md +++ b/docs/project-configuration.md @@ -378,7 +378,7 @@ entry. Rebuild the frontend to change values embedded in browser assets. Declare an existing template by name. `memory_mb` and `ports` assert the deployed image settings; changing them requires a new source deployment. Configuration -apply updates `idle_timeout_seconds` and `ttl_seconds` for new sessions. +apply updates `ttl_seconds`, the default VM lifetime for new sessions. ```yaml version: 1 @@ -386,13 +386,13 @@ sandboxes: - name: local-custom memory_mb: 1024 ports: [8080] - idle_timeout_seconds: 90 ttl_seconds: 600 ``` Preview with `volcano config deploy --dry-run`, apply with `volcano config deploy`, and export with `volcano config pull --force`. Add `cloud` after `volcano` for a -cloud project. Omitted fields preserve current settings; an explicit idle timeout -of `0` disables idle expiration. TTL must be 30–28800 seconds, and a nonzero idle -timeout must not exceed it. Config apply does not build images or delete templates +cloud project. Omitted fields preserve current settings. Cloud TTL must be +30–28800 seconds. Local mode accepts `0` for unlimited lifetime or a positive TTL. +`idle_timeout_seconds` is no longer supported; remove it from existing manifests. +Config apply does not build images or delete templates omitted from the manifest. See [Sandbox source deployment](sandboxes.md). diff --git a/internal/apiclient/client.gen.go b/internal/apiclient/client.gen.go index 1cf36fc..8d91ff5 100644 --- a/internal/apiclient/client.gen.go +++ b/internal/apiclient/client.gen.go @@ -300,6 +300,24 @@ func (e BatchFunctionDeployFailureOperation) Valid() bool { } } +// Defines values for CapabilityStatus. +const ( + CapabilityStatusAvailable CapabilityStatus = "available" + CapabilityStatusUnavailable CapabilityStatus = "unavailable" +) + +// Valid indicates whether the value is a known member of the CapabilityStatus enum. +func (e CapabilityStatus) Valid() bool { + switch e { + case CapabilityStatusAvailable: + return true + case CapabilityStatusUnavailable: + return true + default: + return false + } +} + // Defines values for CreateDatabaseRequestDatabaseType. const ( CreateDatabaseRequestDatabaseTypeVolcanoDb2xl CreateDatabaseRequestDatabaseType = "volcano-db-2xl" @@ -3998,25 +4016,25 @@ func (e GetProjectConfigParamsFormat) Valid() bool { // Defines values for ListDatabasesParamsStatus. const ( - Active ListDatabasesParamsStatus = "active" - Deleting ListDatabasesParamsStatus = "deleting" - Failed ListDatabasesParamsStatus = "failed" - Provisioning ListDatabasesParamsStatus = "provisioning" - Restoring ListDatabasesParamsStatus = "restoring" + ListDatabasesParamsStatusActive ListDatabasesParamsStatus = "active" + ListDatabasesParamsStatusDeleting ListDatabasesParamsStatus = "deleting" + ListDatabasesParamsStatusFailed ListDatabasesParamsStatus = "failed" + ListDatabasesParamsStatusProvisioning ListDatabasesParamsStatus = "provisioning" + ListDatabasesParamsStatusRestoring ListDatabasesParamsStatus = "restoring" ) // Valid indicates whether the value is a known member of the ListDatabasesParamsStatus enum. func (e ListDatabasesParamsStatus) Valid() bool { switch e { - case Active: + case ListDatabasesParamsStatusActive: return true - case Deleting: + case ListDatabasesParamsStatusDeleting: return true - case Failed: + case ListDatabasesParamsStatusFailed: return true - case Provisioning: + case ListDatabasesParamsStatusProvisioning: return true - case Restoring: + case ListDatabasesParamsStatusRestoring: return true default: return false @@ -4982,6 +5000,23 @@ type BatchFunctionDeployResponse struct { Failed *[]BatchFunctionDeployFailure `json:"failed,omitempty"` } +// Capability A product capability and whether it accepts new work in this environment. +type Capability struct { + // Id Stable capability ID, such as `sandboxes`, `sandboxes.sessions`, or `sandboxes.custom_templates`. New IDs may appear at any time. + Id string `json:"id"` + + // Status `available` means the capability accepts new work; a temporary outage still answers `503`. `unavailable` means requests that start new work answer `404` or `400` with code `feature_unavailable`. Treat any other value as `unavailable`. + Status CapabilityStatus `json:"status"` +} + +// CapabilityList defines model for CapabilityList. +type CapabilityList struct { + Data []Capability `json:"data"` +} + +// CapabilityStatus `available` means the capability accepts new work; a temporary outage still answers `503`. `unavailable` means requests that start new work answer `404` or `400` with code `feature_unavailable`. Treat any other value as `unavailable`. +type CapabilityStatus string + // CompleteUploadSessionResponse Response when completing an upload type CompleteUploadSessionResponse struct { Object *StorageObject `json:"object,omitempty"` @@ -5202,10 +5237,7 @@ type CreateProjectRequestTemplateId string // CreateSandboxSessionRequest defines model for CreateSandboxSessionRequest. type CreateSandboxSessionRequest struct { - // IdleTimeoutSeconds Inherits the template idle timeout when omitted, capped at the session duration. Set zero to disable idle timeout. - IdleTimeoutSeconds *int `json:"idle_timeout_seconds,omitempty"` - - // MaxDurationSeconds Inherits the template TTL when omitted (3600 seconds for a new template). + // MaxDurationSeconds Inherits the template TTL when omitted (cloud default 3600 seconds; local default 0, unlimited). Cloud accepts 30–28800 seconds; local accepts 0 for unlimited or a positive lifetime. MaxDurationSeconds *int `json:"max_duration_seconds,omitempty"` MemoryMb *CreateSandboxSessionRequestMemoryMb `json:"memory_mb,omitempty"` @@ -6361,7 +6393,9 @@ type FrontendCustomDomainResponse struct { RoutingTargetHostname *string `json:"routing_target_hostname,omitempty"` TlsMode FrontendCustomDomainResponseTlsMode `json:"tls_mode"` UpdatedAt time.Time `json:"updated_at"` - VerificationRecords *[]FrontendDomainVerificationRecord `json:"verification_records,omitempty"` + + // VerificationRecords DNS records to publish now. For a managed domain whose hostname the account already owns, the create response names the `_acme-challenge` CNAME that authorizes certificate issuance and renewal. Otherwise it names the `_volcano` TXT record that proves ownership of the hostname's registrable domain, and reads return the CNAME once Volcano sees that record. Usually empty for BYOC. + VerificationRecords *[]FrontendDomainVerificationRecord `json:"verification_records,omitempty"` // VerificationStatus `verified`: the domain is served by a validated certificate. `pending`: it is not served yet, is being re-validated after its certificate material was withdrawn, or Volcano is retrying after a failure. `failed`: a failure left the domain unserved, alongside `domain_status: failed`; managed domains report the cause in `failure_reason`. VerificationStatus FrontendCustomDomainResponseVerificationStatus `json:"verification_status"` @@ -8093,7 +8127,7 @@ type ProjectConfigBucketPolicy struct { // ProjectConfigBucketPolicyOperation defines model for ProjectConfigBucketPolicy.Operation. type ProjectConfigBucketPolicyOperation string -// ProjectConfigCustomDomain Custom domain with managed or BYOC TLS (SUPERAGENT plan). `tls` is required +// ProjectConfigCustomDomain Custom domain with managed or BYOC TLS. `tls` is required // when the domain is first created and optional afterwards. For an existing // domain, omitting `tls` or sending only `tls.mode` keeps the stored // certificate; new BYOC material for the same domain rotates the @@ -8162,7 +8196,7 @@ type ProjectConfigEmailTemplates struct { // created or deleted through the manifest. A declared frontend entry // without `custom_domain` deletes an existing custom domain. type ProjectConfigFrontend struct { - // CustomDomain Custom domain with managed or BYOC TLS (SUPERAGENT plan). `tls` is required + // CustomDomain Custom domain with managed or BYOC TLS. `tls` is required // when the domain is first created and optional afterwards. For an existing // domain, omitting `tls` or sending only `tls.mode` keeps the stored // certificate; new BYOC material for the same domain rotates the @@ -8336,9 +8370,6 @@ type ProjectConfigRealtime struct { // ProjectConfigSandbox defines model for ProjectConfigSandbox. type ProjectConfigSandbox struct { - // IdleTimeoutSeconds Default idle timeout for new sessions when the caller omits it. - IdleTimeoutSeconds *int `json:"idle_timeout_seconds,omitempty"` - // MemoryMb Immutable deployed memory profile; config apply asserts it and Git deploy builds it. MemoryMb *ProjectConfigSandboxMemoryMb `json:"memory_mb,omitempty"` @@ -8348,7 +8379,7 @@ type ProjectConfigSandbox struct { // Ports Service readiness ports; config apply asserts them and Git deploy builds them. Ports *[]int `json:"ports,omitempty"` - // TtlSeconds Default absolute lifetime for new sessions when the caller omits it. + // TtlSeconds Default absolute lifetime for new sessions when the caller omits it. Cloud accepts 30–28800 seconds; local accepts 0 for unlimited or a positive lifetime. TtlSeconds *int `json:"ttl_seconds,omitempty"` } @@ -8490,10 +8521,12 @@ type ProjectFrontendCustomDomain struct { RequiredRoutingRecord *FrontendDomainRoutingRecord `json:"required_routing_record,omitempty"` // RoutingTargetHostname DNS routing target hostname for this frontend. The DNS record type depends on whether the custom domain is a zone apex. - RoutingTargetHostname *string `json:"routing_target_hostname,omitempty"` - TlsMode ProjectFrontendCustomDomainTlsMode `json:"tls_mode"` - UpdatedAt time.Time `json:"updated_at"` - VerificationRecords *[]FrontendDomainVerificationRecord `json:"verification_records,omitempty"` + RoutingTargetHostname *string `json:"routing_target_hostname,omitempty"` + TlsMode ProjectFrontendCustomDomainTlsMode `json:"tls_mode"` + UpdatedAt time.Time `json:"updated_at"` + + // VerificationRecords DNS records to publish now. For a managed domain whose hostname the account already owns, the create response names the `_acme-challenge` CNAME that authorizes certificate issuance and renewal. Otherwise it names the `_volcano` TXT record that proves ownership of the hostname's registrable domain, and reads return the CNAME once Volcano sees that record. Usually empty for BYOC. + VerificationRecords *[]FrontendDomainVerificationRecord `json:"verification_records,omitempty"` // VerificationStatus `verified`: the domain is served by a validated certificate. `pending`: it is not served yet, is being re-validated after its certificate material was withdrawn, or Volcano is retrying after a failure. `failed`: a failure left the domain unserved, alongside `domain_status: failed`; managed domains report the cause in `failure_reason`. VerificationStatus ProjectFrontendCustomDomainVerificationStatus `json:"verification_status"` @@ -9060,9 +9093,11 @@ type SandboxCapacityList struct { // SandboxCommandRequest defines model for SandboxCommandRequest. type SandboxCommandRequest struct { - Command string `json:"command"` - Environment *map[string]string `json:"environment,omitempty"` - TimeoutSeconds *int `json:"timeout_seconds,omitempty"` + Command string `json:"command"` + Environment *map[string]string `json:"environment,omitempty"` + + // TimeoutSeconds Command execution time from process start. Cloud defaults to 60 seconds and accepts 1–28800; local defaults to 0 (unlimited) and accepts nonnegative values. VM expiry always takes precedence. Cloud synchronous requests must return within the public connection idle limit (1000 seconds); use a session with a background process and short polling requests for longer work. + TimeoutSeconds *int `json:"timeout_seconds,omitempty"` } // SandboxCommandResult defines model for SandboxCommandResult. @@ -9091,17 +9126,22 @@ type SandboxDeploymentPage struct { // SandboxExecutionRequest defines model for SandboxExecutionRequest. type SandboxExecutionRequest struct { - Command string `json:"command"` - Environment *map[string]string `json:"environment,omitempty"` - MemoryMb *SandboxExecutionRequestMemoryMb `json:"memory_mb,omitempty"` + Command string `json:"command"` + Environment *map[string]string `json:"environment,omitempty"` + + // MaxDurationSeconds Absolute VM lifetime including startup, bounded by environment capacity policy. Inherits the template TTL when omitted (cloud default 3600 seconds; local default 0, unlimited). Cloud accepts 30–28800 seconds; local accepts 0 for unlimited or a positive lifetime. The VM is reclaimed early when the command finishes. + MaxDurationSeconds *int `json:"max_duration_seconds,omitempty"` + MemoryMb *SandboxExecutionRequestMemoryMb `json:"memory_mb,omitempty"` // Preset Preset ID from the available Sandbox preset catalog. Preset *string `json:"preset,omitempty"` // Region Region such as `us-east-1`. Region IDs issued by earlier versions of the API are still accepted. - Region string `json:"region"` - SandboxId *openapi_types.UUID `json:"sandbox_id,omitempty"` - TimeoutSeconds *int `json:"timeout_seconds,omitempty"` + Region string `json:"region"` + SandboxId *openapi_types.UUID `json:"sandbox_id,omitempty"` + + // TimeoutSeconds Command execution time from process start. Cloud defaults to 60 seconds and accepts 1–28800; local defaults to 0 (unlimited) and accepts nonnegative values. VM expiry always takes precedence. Cloud synchronous requests must return within the public connection idle limit (1000 seconds); use a session with a background process and short polling requests for longer work. + TimeoutSeconds *int `json:"timeout_seconds,omitempty"` union json.RawMessage } @@ -9171,14 +9211,18 @@ type SandboxPresetList struct { type SandboxSession struct { CreatedAt time.Time `json:"created_at"` DesiredState SandboxSessionDesiredState `json:"desired_state"` - ExpiresAt time.Time `json:"expires_at"` - Id openapi_types.UUID `json:"id"` - MemoryMb int `json:"memory_mb"` - ProjectId openapi_types.UUID `json:"project_id"` - Region string `json:"region"` - SandboxId openapi_types.UUID `json:"sandbox_id"` - StartedAt *time.Time `json:"started_at,omitempty"` - State SandboxSessionState `json:"state"` + + // ExpiresAt Absolute VM expiry. Null means unlimited in local mode. + ExpiresAt *time.Time `json:"expires_at"` + Id openapi_types.UUID `json:"id"` + MemoryMb int `json:"memory_mb"` + ProjectId openapi_types.UUID `json:"project_id"` + Region string `json:"region"` + + // SandboxId Explicit template used to create the session. Null when created directly from a preset. + SandboxId *openapi_types.UUID `json:"sandbox_id"` + StartedAt *time.Time `json:"started_at,omitempty"` + State SandboxSessionState `json:"state"` } // SandboxSessionDesiredState defines model for SandboxSession.DesiredState. @@ -12069,13 +12113,6 @@ func (t CreateSandboxSessionRequest) MarshalJSON() ([]byte, error) { } } - if t.IdleTimeoutSeconds != nil { - object["idle_timeout_seconds"], err = json.Marshal(t.IdleTimeoutSeconds) - if err != nil { - return nil, fmt.Errorf("error marshaling 'idle_timeout_seconds': %w", err) - } - } - if t.MaxDurationSeconds != nil { object["max_duration_seconds"], err = json.Marshal(t.MaxDurationSeconds) if err != nil { @@ -12123,13 +12160,6 @@ func (t *CreateSandboxSessionRequest) UnmarshalJSON(b []byte) error { return err } - if raw, found := object["idle_timeout_seconds"]; found { - err = json.Unmarshal(raw, &t.IdleTimeoutSeconds) - if err != nil { - return fmt.Errorf("error reading 'idle_timeout_seconds': %w", err) - } - } - if raw, found := object["max_duration_seconds"]; found { err = json.Unmarshal(raw, &t.MaxDurationSeconds) if err != nil { @@ -12908,6 +12938,13 @@ func (t SandboxExecutionRequest) MarshalJSON() ([]byte, error) { } } + if t.MaxDurationSeconds != nil { + object["max_duration_seconds"], err = json.Marshal(t.MaxDurationSeconds) + if err != nil { + return nil, fmt.Errorf("error marshaling 'max_duration_seconds': %w", err) + } + } + if t.MemoryMb != nil { object["memory_mb"], err = json.Marshal(t.MemoryMb) if err != nil { @@ -12969,6 +13006,13 @@ func (t *SandboxExecutionRequest) UnmarshalJSON(b []byte) error { } } + if raw, found := object["max_duration_seconds"]; found { + err = json.Unmarshal(raw, &t.MaxDurationSeconds) + if err != nil { + return fmt.Errorf("error reading 'max_duration_seconds': %w", err) + } + } + if raw, found := object["memory_mb"]; found { err = json.Unmarshal(raw, &t.MemoryMb) if err != nil { @@ -13474,6 +13518,9 @@ type ClientInterface interface { // AuthDeleteMySession request AuthDeleteMySession(ctx context.Context, sessionId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListCapabilities request + ListCapabilities(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListPostgresVersions request ListPostgresVersions(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -15045,6 +15092,18 @@ func (c *Client) AuthDeleteMySession(ctx context.Context, sessionId openapi_type return c.Client.Do(req) } +func (c *Client) ListCapabilities(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListCapabilitiesRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ListPostgresVersions(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewListPostgresVersionsRequest(c.Server) if err != nil { @@ -20546,6 +20605,33 @@ func NewAuthDeleteMySessionRequest(server string, sessionId openapi_types.UUID) return req, nil } +// NewListCapabilitiesRequest generates requests for ListCapabilities +func NewListCapabilitiesRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/capabilities") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewListPostgresVersionsRequest generates requests for ListPostgresVersions func NewListPostgresVersionsRequest(server string) (*http.Request, error) { var err error @@ -34306,6 +34392,9 @@ type ClientWithResponsesInterface interface { // AuthDeleteMySessionWithResponse request AuthDeleteMySessionWithResponse(ctx context.Context, sessionId openapi_types.UUID, reqEditors ...RequestEditorFn) (*AuthDeleteMySessionClientResponse, error) + // ListCapabilitiesWithResponse request + ListCapabilitiesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListCapabilitiesClientResponse, error) + // ListPostgresVersionsWithResponse request ListPostgresVersionsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListPostgresVersionsClientResponse, error) @@ -36426,6 +36515,36 @@ func (r AuthDeleteMySessionClientResponse) ContentType() string { return "" } +type ListCapabilitiesClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *CapabilityList +} + +// Status returns HTTPResponse.Status +func (r ListCapabilitiesClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListCapabilitiesClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r ListCapabilitiesClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type ListPostgresVersionsClientResponse struct { Body []byte HTTPResponse *http.Response @@ -45144,6 +45263,15 @@ func (c *ClientWithResponses) AuthDeleteMySessionWithResponse(ctx context.Contex return ParseAuthDeleteMySessionClientResponse(rsp) } +// ListCapabilitiesWithResponse request returning *ListCapabilitiesClientResponse +func (c *ClientWithResponses) ListCapabilitiesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListCapabilitiesClientResponse, error) { + rsp, err := c.ListCapabilities(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseListCapabilitiesClientResponse(rsp) +} + // ListPostgresVersionsWithResponse request returning *ListPostgresVersionsClientResponse func (c *ClientWithResponses) ListPostgresVersionsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListPostgresVersionsClientResponse, error) { rsp, err := c.ListPostgresVersions(ctx, reqEditors...) @@ -49435,6 +49563,32 @@ func ParseAuthDeleteMySessionClientResponse(rsp *http.Response) (*AuthDeleteMySe return response, nil } +// ParseListCapabilitiesClientResponse parses an HTTP response from a ListCapabilitiesWithResponse call +func ParseListCapabilitiesClientResponse(rsp *http.Response) (*ListCapabilitiesClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListCapabilitiesClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest CapabilityList + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseListPostgresVersionsClientResponse parses an HTTP response from a ListPostgresVersionsWithResponse call func ParseListPostgresVersionsClientResponse(rsp *http.Response) (*ListPostgresVersionsClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 6589687..db825a2 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -114,7 +114,6 @@ sandboxes: - name: local-custom memory_mb: 1024 ports: [8080] - idle_timeout_seconds: 0 ttl_seconds: 600 `), 0o644)) @@ -148,7 +147,7 @@ sandboxes: assert.Equal(t, false, realtime["enabled"]) sandboxes, err := json.Marshal(uploaded["sandboxes"]) require.NoError(t, err) - assert.JSONEq(t, `[{"name":"local-custom","memory_mb":1024,"ports":[8080],"idle_timeout_seconds":0,"ttl_seconds":600}]`, string(sandboxes)) + assert.JSONEq(t, `[{"name":"local-custom","memory_mb":1024,"ports":[8080],"ttl_seconds":600}]`, string(sandboxes)) assert.Contains(t, out, "Configuration deployed from volcano-config.yaml") assert.Contains(t, out, "variables: 1 created, 1 deleted") diff --git a/internal/projectconfig/manifest.go b/internal/projectconfig/manifest.go index 09c91f4..80bc574 100644 --- a/internal/projectconfig/manifest.go +++ b/internal/projectconfig/manifest.go @@ -76,13 +76,12 @@ type Manifest struct { // SandboxManifest declares image assertions and default session settings for an // existing Sandbox template. Nil fields preserve existing values; explicit -// zero idle timeout and empty ports remain present during round trips. +// zero TTL and empty ports remain present during round trips. type SandboxManifest struct { - Name string `yaml:"name" json:"name"` - MemoryMB *int `yaml:"memory_mb,omitempty" json:"memory_mb,omitempty"` - Ports *[]int `yaml:"ports,omitempty" json:"ports,omitempty"` - IdleTimeoutSeconds *int `yaml:"idle_timeout_seconds,omitempty" json:"idle_timeout_seconds,omitempty"` - TTLSeconds *int `yaml:"ttl_seconds,omitempty" json:"ttl_seconds,omitempty"` + Name string `yaml:"name" json:"name"` + MemoryMB *int `yaml:"memory_mb,omitempty" json:"memory_mb,omitempty"` + Ports *[]int `yaml:"ports,omitempty" json:"ports,omitempty"` + TTLSeconds *int `yaml:"ttl_seconds,omitempty" json:"ttl_seconds,omitempty"` } // ProjectManifest declares project-level settings. diff --git a/internal/projectconfig/manifest_sandboxes_test.go b/internal/projectconfig/manifest_sandboxes_test.go index decc7de..6d5ef2a 100644 --- a/internal/projectconfig/manifest_sandboxes_test.go +++ b/internal/projectconfig/manifest_sandboxes_test.go @@ -16,13 +16,12 @@ sandboxes: - name: local-custom memory_mb: 1024 ports: [8080] - idle_timeout_seconds: 90 ttl_seconds: 600 `), noEnv) require.NoError(t, err) body, err := manifest.uploadBody() require.NoError(t, err) - require.JSONEq(t, `{"version":1,"sandboxes":[{"name":"local-custom","memory_mb":1024,"ports":[8080],"idle_timeout_seconds":90,"ttl_seconds":600}]}`, string(body)) + require.JSONEq(t, `{"version":1,"sandboxes":[{"name":"local-custom","memory_mb":1024,"ports":[8080],"ttl_seconds":600}]}`, string(body)) var wire apiclient.ProjectConfig require.NoError(t, json.Unmarshal(body, &wire)) require.NotNil(t, wire.Sandboxes) @@ -46,7 +45,7 @@ func TestSandboxManifestPreservesOmittedAndExplicitValues(t *testing.T) { "omitted section": {"version: 1\n", `{"version":1}`}, "empty section": {"version: 1\nsandboxes: []\n", `{"version":1,"sandboxes":[]}`}, "omitted settings": {"version: 1\nsandboxes:\n - name: custom\n", `{"version":1,"sandboxes":[{"name":"custom"}]}`}, - "zero and empty settings": {"version: 1\nsandboxes:\n - name: custom\n ports: []\n idle_timeout_seconds: 0\n", `{"version":1,"sandboxes":[{"name":"custom","ports":[],"idle_timeout_seconds":0}]}`}, + "zero and empty settings": {"version: 1\nsandboxes:\n - name: custom\n ports: []\n ttl_seconds: 0\n", `{"version":1,"sandboxes":[{"name":"custom","ports":[],"ttl_seconds":0}]}`}, } { t.Run(name, func(t *testing.T) { manifest, err := Parse([]byte(example.yaml), noEnv) @@ -69,3 +68,8 @@ func TestSandboxManifestRejectsUnknownFields(t *testing.T) { _, err := Parse([]byte("version: 1\nsandboxes:\n - name: custom\n ttl_second: 600\n"), noEnv) require.ErrorContains(t, err, "field ttl_second not found") } + +func TestSandboxManifestRejectsRemovedIdleTimeout(t *testing.T) { + _, err := Parse([]byte("version: 1\nsandboxes:\n - name: custom\n idle_timeout_seconds: 90\n"), noEnv) + require.ErrorContains(t, err, "field idle_timeout_seconds not found") +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 231110b..2eafc31 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -16,7 +16,7 @@ servers: description: Development API server (use VOLCANO_API_URL env var) tags: - name: Sandboxes - description: Isolated Linux sessions and reusable templates. + description: 'Preview, not generally available: isolated Linux sessions and reusable templates. Where Sandboxes are not available, every endpoint answers `404` with code `feature_unavailable`. Where they are turned off after use, only creating or starting a session, template, execution, or deployment, and listing presets, answer that `404`, so existing ones can still be read and deleted. A `503` means the Sandbox service is temporarily unavailable.' - name: Projects description: Project management operations - name: Logs @@ -78,7 +78,7 @@ tags: - name: Realtime description: WebSocket realtime configuration and monitoring - name: System - description: System health and monitoring endpoints + description: System health, capability discovery, and monitoring endpoints - name: CLI Authentication description: | Browser-based CLI login flow for automatic token provisioning. @@ -101,7 +101,7 @@ paths: schema: $ref: '#/components/schemas/SandboxPresetList' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -132,7 +132,7 @@ paths: schema: $ref: '#/components/schemas/SandboxTemplatePage' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -172,7 +172,7 @@ paths: schema: $ref: '#/components/schemas/SandboxTemplate' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -207,7 +207,7 @@ paths: schema: $ref: '#/components/schemas/SandboxTemplate' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -247,7 +247,7 @@ paths: schema: $ref: '#/components/schemas/SandboxTemplate' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -277,7 +277,7 @@ paths: '202': description: Retire a template and terminate its sessions default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -345,7 +345,7 @@ paths: schema: $ref: '#/components/schemas/SandboxDeployment' default: - description: Invalid source, conflicting retry, denied access, or unavailable deployment service + description: Invalid source (400), denied access (401, 403), conflicting retry (409), or a temporarily unavailable deployment service (503). Code `feature_unavailable` with 404 means sandboxes or custom templates are not available in this environment. content: application/json: schema: @@ -381,7 +381,7 @@ paths: schema: $ref: '#/components/schemas/SandboxDeploymentPage' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -422,7 +422,7 @@ paths: schema: $ref: '#/components/schemas/SandboxDeployment' default: - description: Denied access, missing resource, or unavailable deployment service + description: Denied access (401, 403), missing resource (404), or a temporarily unavailable deployment service (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -481,7 +481,7 @@ paths: schema: $ref: '#/components/schemas/SandboxBuildLogPage' default: - description: Denied access, missing resource, or unavailable deployment service + description: Denied access (401, 403), missing resource (404), or a temporarily unavailable deployment service (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -523,7 +523,7 @@ paths: type: string format: binary default: - description: Denied access, missing resource, or unavailable deployment service + description: Denied access (401, 403), missing resource (404), or a temporarily unavailable deployment service (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -554,7 +554,7 @@ paths: schema: $ref: '#/components/schemas/SandboxSessionPage' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -594,7 +594,7 @@ paths: schema: $ref: '#/components/schemas/SandboxSession' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -635,7 +635,7 @@ paths: schema: $ref: '#/components/schemas/SandboxExecutionResult' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -665,7 +665,7 @@ paths: schema: $ref: '#/components/schemas/SandboxSession' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -693,7 +693,7 @@ paths: schema: $ref: '#/components/schemas/SandboxSession' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -722,7 +722,7 @@ paths: schema: $ref: '#/components/schemas/SandboxSession' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -751,7 +751,7 @@ paths: schema: $ref: '#/components/schemas/SandboxSession' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -793,7 +793,7 @@ paths: schema: $ref: '#/components/schemas/SandboxCommandResult' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -829,7 +829,7 @@ paths: schema: $ref: '#/components/schemas/SandboxFileResult' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -861,7 +861,7 @@ paths: '204': description: Write a workspace file default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -898,7 +898,7 @@ paths: '204': description: Authorize an authenticated project user for this session default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -928,7 +928,7 @@ paths: '204': description: Revoke a project user session grant default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -964,7 +964,7 @@ paths: schema: $ref: '#/components/schemas/SandboxAccess' default: - description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and disabled or unavailable (503). + description: Request refused or unavailable. Errors include invalid input (400), unauthenticated (401), forbidden (403), not found (404), conflicting retry (409), capacity exhausted (429), and temporarily unavailable (503). Code `feature_unavailable` with 404 means sandboxes are not available in this environment. content: application/json: schema: @@ -6222,7 +6222,7 @@ paths: post: tags: - Frontends - summary: Configure frontend custom domain (SUPERAGENT) + summary: Configure frontend custom domain description: | Configures one custom domain for a frontend. The default Volcano-generated frontend URL remains active. @@ -6270,7 +6270,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Forbidden - custom domains require SUPERAGENT plan, or a managed TLS request would exceed the account's managed TLS certificate limit + description: Forbidden - the plan does not include custom domains, or a managed TLS request would exceed the account's managed TLS certificate limit content: application/json: schema: @@ -10719,7 +10719,8 @@ paths: `subject` field is ignored. Sending `subject` alone (no bodies) is rejected with 400 to avoid a silently-dropped subject or a blank message. Also rejects with 400 if - `email_enabled=false` or `smtp_host` is empty. + `email_enabled=false`, `smtp_host` is empty, or the saved + `smtp_password` can't be read and must be set again. operationId: testEmailConfig security: - UserToken: [] @@ -14277,6 +14278,45 @@ paths: schema: type: string example: OK + /capabilities: + get: + tags: + - System + summary: List capabilities + description: | + Lists which product capabilities accept new work in this environment. Check it + to decide what to offer, instead of calling a route to see whether it fails. + + | ID | Covers | + | --- | --- | + | `sandboxes` | The Sandbox API: presets, templates, sessions, and executions | + | `sandboxes.sessions` | Starting and resuming sessions, and one-shot executions | + | `sandboxes.custom_templates` | Deploying a custom template; its validation waits while `sandboxes.sessions` is unavailable | + + A capability missing from the list is unavailable, and a capability under + another, such as `sandboxes.sessions`, is unavailable whenever its parent is. + While a capability is unavailable, requests that start new work answer `404` + or `400` with code `feature_unavailable`; refresh this list when you receive + that code. Reading, deleting, and terminating what already exists keep + working unless the whole feature is absent from the environment. Every caller + in an environment gets the same answer. + operationId: listCapabilities + security: [] + responses: + '200': + description: Capabilities in this environment + content: + application/json: + schema: + $ref: '#/components/schemas/CapabilityList' + example: + data: + - id: sandboxes + status: available + - id: sandboxes.sessions + status: available + - id: sandboxes.custom_templates + status: unavailable /openapi.json: get: tags: @@ -15046,6 +15086,8 @@ components: sandbox_id: type: string format: uuid + nullable: true + description: Explicit template used to create the session. Null when created directly from a preset. state: type: string enum: @@ -15076,6 +15118,8 @@ components: expires_at: type: string format: date-time + nullable: true + description: Absolute VM expiry. Null means unlimited in local mode. required: - id - project_id @@ -15106,15 +15150,10 @@ components: pattern: ^(aws-)?[a-z]{2}(-[a-z]+)+-[0-9]+$ description: Region such as `us-east-1`. Region IDs issued by earlier versions of the API are still accepted. max_duration_seconds: - type: integer - minimum: 30 - maximum: 28800 - description: Inherits the template TTL when omitted (3600 seconds for a new template). - idle_timeout_seconds: type: integer minimum: 0 - maximum: 28800 - description: Inherits the template idle timeout when omitted, capped at the session duration. Set zero to disable idle timeout. + maximum: 2147483647 + description: Inherits the template TTL when omitted (cloud default 3600 seconds; local default 0, unlimited). Cloud accepts 30–28800 seconds; local accepts 0 for unlimited or a positive lifetime. required: - region oneOf: @@ -15138,9 +15177,9 @@ components: maxLength: 65536 timeout_seconds: type: integer - minimum: 1 - maximum: 3600 - default: 60 + minimum: 0 + maximum: 2147483647 + description: Command execution time from process start. Cloud defaults to 60 seconds and accepts 1–28800; local defaults to 0 (unlimited) and accepts nonnegative values. VM expiry always takes precedence. Cloud synchronous requests must return within the public connection idle limit (1000 seconds); use a session with a background process and short polling requests for longer work. environment: type: object additionalProperties: @@ -15167,15 +15206,20 @@ components: type: string pattern: ^(aws-)?[a-z]{2}(-[a-z]+)+-[0-9]+$ description: Region such as `us-east-1`. Region IDs issued by earlier versions of the API are still accepted. + max_duration_seconds: + type: integer + minimum: 0 + maximum: 2147483647 + description: Absolute VM lifetime including startup, bounded by environment capacity policy. Inherits the template TTL when omitted (cloud default 3600 seconds; local default 0, unlimited). Cloud accepts 30–28800 seconds; local accepts 0 for unlimited or a positive lifetime. The VM is reclaimed early when the command finishes. command: type: string minLength: 1 maxLength: 65536 timeout_seconds: type: integer - minimum: 1 - maximum: 60 - default: 60 + minimum: 0 + maximum: 2147483647 + description: Command execution time from process start. Cloud defaults to 60 seconds and accepts 1–28800; local defaults to 0 (unlimited) and accepts nonnegative values. VM expiry always takes precedence. Cloud synchronous requests must return within the public connection idle limit (1000 seconds); use a session with a background process and short polling requests for longer work. environment: type: object additionalProperties: @@ -15430,6 +15474,36 @@ components: $ref: '#/components/schemas/SandboxPreset' required: - data + Capability: + type: object + additionalProperties: false + description: A product capability and whether it accepts new work in this environment. + properties: + id: + type: string + description: Stable capability ID, such as `sandboxes`, `sandboxes.sessions`, or `sandboxes.custom_templates`. New IDs may appear at any time. + example: sandboxes.sessions + status: + $ref: '#/components/schemas/CapabilityStatus' + required: + - id + - status + CapabilityStatus: + type: string + enum: + - available + - unavailable + description: '`available` means the capability accepts new work; a temporary outage still answers `503`. `unavailable` means requests that start new work answer `404` or `400` with code `feature_unavailable`. Treat any other value as `unavailable`.' + CapabilityList: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/Capability' + required: + - data SandboxCapacity: type: object additionalProperties: false @@ -18511,6 +18585,7 @@ components: description: Failure category, present only when managed TLS setup has failed. Current values are provider, certificate, ownership, and internal; ownership means another account has already claimed the hostname through ownership verification. Treat unrecognized values as internal. verification_records: type: array + description: DNS records to publish now. For a managed domain whose hostname the account already owns, the create response names the `_acme-challenge` CNAME that authorizes certificate issuance and renewal. Otherwise it names the `_volcano` TXT record that proves ownership of the hostname's registrable domain, and reads return the CNAME once Volcano sees that record. Usually empty for BYOC. items: $ref: '#/components/schemas/FrontendDomainVerificationRecord' required_routing_record: @@ -21060,7 +21135,7 @@ components: type: object additionalProperties: false description: | - Custom domain with managed or BYOC TLS (SUPERAGENT plan). `tls` is required + Custom domain with managed or BYOC TLS. `tls` is required when the domain is first created and optional afterwards. For an existing domain, omitting `tls` or sending only `tls.mode` keeps the stored certificate; new BYOC material for the same domain rotates the @@ -23097,16 +23172,11 @@ components: type: integer minimum: 1 maximum: 65532 - idle_timeout_seconds: - type: integer - minimum: 0 - maximum: 28800 - description: Default idle timeout for new sessions when the caller omits it. ttl_seconds: type: integer - minimum: 30 - maximum: 28800 - description: Default absolute lifetime for new sessions when the caller omits it. + minimum: 0 + maximum: 2147483647 + description: Default absolute lifetime for new sessions when the caller omits it. Cloud accepts 30–28800 seconds; local accepts 0 for unlimited or a positive lifetime. DatabaseQueryPerformanceDatabase: type: object properties: diff --git a/tests/e2e/localmode/sandbox_config_test.go b/tests/e2e/localmode/sandbox_config_test.go index e3c0179..fc24cf3 100644 --- a/tests/e2e/localmode/sandbox_config_test.go +++ b/tests/e2e/localmode/sandbox_config_test.go @@ -18,11 +18,10 @@ import ( ) type localSandboxSettings struct { - Name string `yaml:"name"` - MemoryMB int `yaml:"memory_mb"` - Ports []int `yaml:"ports"` - IdleTimeout int `yaml:"idle_timeout_seconds"` - TTL int `yaml:"ttl_seconds"` + Name string `yaml:"name"` + MemoryMB int `yaml:"memory_mb"` + Ports []int `yaml:"ports"` + TTL int `yaml:"ttl_seconds"` } func sandboxSettingsFromManifest(data []byte, name string) (localSandboxSettings, error) { @@ -60,7 +59,6 @@ sandboxes: - name: local-custom memory_mb: 1024 ports: [8080] - idle_timeout_seconds: 90 ttl_seconds: 600 `), 0o600)) plan := runVolcanoLocalModeE2EStdout(t, binary, env, dir, "config", "deploy", "--file", manifest, "--dry-run") @@ -69,7 +67,7 @@ sandboxes: require.Equal(t, before, pull(), "dry run must not change template settings") applied := runVolcanoLocalModeE2EStdout(t, binary, env, dir, "config", "deploy", "--file", manifest) require.Contains(t, applied, "Configuration deployed") - require.Equal(t, localSandboxSettings{Name: "local-custom", MemoryMB: 1024, Ports: []int{8080}, IdleTimeout: 90, TTL: 600}, pull()) + require.Equal(t, localSandboxSettings{Name: "local-custom", MemoryMB: 1024, Ports: []int{8080}, TTL: 600}, pull()) var saved apiclient.SandboxTemplate require.NoError(t, json.Unmarshal([]byte(runVolcanoLocalModeE2EStdout(t, binary, env, dir, "sandboxes", "templates", "get", template, "--json")), &saved)) @@ -118,10 +116,10 @@ func requireLocalSandboxDeploymentArtifacts(t *testing.T, run func(...string) st } func TestSandboxSettingsFromManifest(t *testing.T) { - data := []byte("version: 1\nvariables:\n - name: retained\nsandboxes:\n - name: other\n - name: local-custom\n memory_mb: 1024\n ports: [8080]\n idle_timeout_seconds: 90\n ttl_seconds: 600\n") + data := []byte("version: 1\nvariables:\n - name: retained\nsandboxes:\n - name: other\n - name: local-custom\n memory_mb: 1024\n ports: [8080]\n ttl_seconds: 600\n") settings, err := sandboxSettingsFromManifest(data, "local-custom") require.NoError(t, err) - require.Equal(t, localSandboxSettings{Name: "local-custom", MemoryMB: 1024, Ports: []int{8080}, IdleTimeout: 90, TTL: 600}, settings) + require.Equal(t, localSandboxSettings{Name: "local-custom", MemoryMB: 1024, Ports: []int{8080}, TTL: 600}, settings) _, err = sandboxSettingsFromManifest(data, "missing") require.ErrorContains(t, err, "omitted sandbox") _, err = sandboxSettingsFromManifest([]byte("invalid: ["), "local-custom") From 8b2703beaa3ae6c9aa9a2460dc83de15b7a8d0a0 Mon Sep 17 00:00:00 2001 From: "shawn.wu" Date: Fri, 9 Oct 2026 15:16:22 -0700 Subject: [PATCH 2/2] fix(sandboxes): inherit template session lifetime --- docs/sandboxes.md | 7 +- internal/cmd/sandboxes/exec.go | 16 +++-- internal/cmd/sandboxes/run_test.go | 77 ++++++++++++++++++++++ internal/cmd/sandboxes/sandboxes.go | 9 +-- tests/e2e/localmode/sandbox_config_test.go | 25 +++++++ 5 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 internal/cmd/sandboxes/run_test.go diff --git a/docs/sandboxes.md b/docs/sandboxes.md index 7bc6564..40d1ec2 100644 --- a/docs/sandboxes.md +++ b/docs/sandboxes.md @@ -47,6 +47,11 @@ shell variables and `cd` changes do not. `exit` or EOF detaches without terminat the session. Full-screen programs such as `vim` require a PTY, which this prompt does not provide. Always terminate sessions you no longer need. +Without `--duration`, `run` inherits the template's `ttl_seconds`. The default +is 3600 seconds in cloud mode and unlimited in local mode. To override a finite +template with an unlimited local session, use +`volcano sandboxes run --template TEMPLATE_ID --duration 0`. + `exec` requires `--` before the command. Each argument is shell-quoted. For shell syntax such as pipes or background processes, explicitly invoke `sh -c`: ```bash @@ -84,7 +89,7 @@ A template saves a preset and memory size. Custom image builds are not supported | `--memory` | `exec`, `run` | Override memory with 1024 or 2048 MB; otherwise inherit the preset or template | | `--region` | `exec`, `run` | Select a region; defaults to `us-east-1` | | `--timeout` | `exec` | Command deadline: 1–60 seconds for one-shot execution, 1–3600 for session execution; defaults to 60 | -| `--duration` | `run` | Maximum session lifetime: 30–28800 seconds; defaults to 3600 | +| `--duration` | `run` | Override session lifetime in seconds: cloud 30–28800; local 0 (unlimited) or positive. Omitted inherits the template | | `--request-id` | `exec`, `run` | UUID used to retry the same request safely | | `--json` | All except `deployments source` | Print compact JSON; `exec`, `shell`, and `files read` return structured results | diff --git a/internal/cmd/sandboxes/exec.go b/internal/cmd/sandboxes/exec.go index bd1b934..9eeb503 100644 --- a/internal/cmd/sandboxes/exec.go +++ b/internal/cmd/sandboxes/exec.go @@ -155,14 +155,20 @@ func (c *commands) outputCommand(cmd *cobra.Command, result apiclient.SandboxCom func (c *commands) run() *cobra.Command { var opts launchOptions cmd := &cobra.Command{Use: "run", Short: "Start a persistent session", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { - if opts.duration < 30 || opts.duration > 28800 { - return errors.New("--duration must be between 30 and 28800 seconds") - } request, key, err := opts.request() if err != nil { return err } - request.MaxDurationSeconds = &opts.duration + if cmd.Flags().Changed("duration") { + if c.local { + if opts.duration < 0 { + return errors.New("--duration must be zero (unlimited) or positive for local sessions") + } + } else if opts.duration < 30 || opts.duration > 28800 { + return errors.New("--duration must be between 30 and 28800 seconds for cloud sessions") + } + request.MaxDurationSeconds = &opts.duration + } project, err := c.project() if err != nil { return err @@ -174,7 +180,7 @@ func (c *commands) run() *cobra.Command { return c.write(cmd, value) }} opts.flags(cmd) - cmd.Flags().IntVar(&opts.duration, "duration", 3600, "Maximum session lifetime in seconds") + cmd.Flags().IntVar(&opts.duration, "duration", 0, "Maximum session lifetime in seconds; omitted inherits the template (cloud: 30–28800; local: 0 for unlimited or positive)") return cmd } diff --git a/internal/cmd/sandboxes/run_test.go b/internal/cmd/sandboxes/run_test.go new file mode 100644 index 0000000..5e51bec --- /dev/null +++ b/internal/cmd/sandboxes/run_test.go @@ -0,0 +1,77 @@ +package sandboxes + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunSessionLifetime(t *testing.T) { + t.Parallel() + for _, local := range []bool{false, true} { + mode := map[bool]string{false: "cloud", true: "local"}[local] + for _, selector := range []string{"preset", "template"} { + for _, duration := range []string{"", "-1", "0", "1", "29", "30", "300", "28800", "28801"} { + t.Run(mode+"/"+selector+"/duration="+duration, func(t *testing.T) { + t.Parallel() + seconds, _ := strconv.Atoi(duration) + valid := duration == "" || (local && seconds >= 0) || (!local && seconds >= 30 && seconds <= 28800) + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + assert.True(t, valid, "invalid duration must not reach transport") + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/projects/"+testProject+"/sandbox-sessions", r.URL.Path) + var body map[string]any + decoder := json.NewDecoder(r.Body) + decoder.UseNumber() + if err := decoder.Decode(&body); !assert.NoError(t, err) { + w.WriteHeader(http.StatusBadRequest) + return + } + if duration == "" { + assert.NotContains(t, body, "max_duration_seconds", "inherit the template lifetime") + } else { + assert.Equal(t, json.Number(duration), body["max_duration_seconds"]) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(`{"id":"` + testSession + `","expires_at":null}`)) + })) + defer server.Close() + deps := testDeps(server) + cmd := New(deps) + if local { + // Local Sandbox APIs authenticate, so deps.LocalMode remains false. + cmd = NewLocal(deps, deps) + } + value := "node22" + if selector == "template" { + value = testSession + } + args := []string{"run", "--" + selector, value} + if duration != "" { + args = append(args, "--duration", duration) + } + cmd.SetArgs(args) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + err := cmd.Execute() + if valid { + require.NoError(t, err) + assert.Equal(t, 1, requests) + } else { + require.ErrorContains(t, err, "--duration") + assert.Zero(t, requests) + } + }) + } + } + } +} diff --git a/internal/cmd/sandboxes/sandboxes.go b/internal/cmd/sandboxes/sandboxes.go index 3a4890e..0c1f6ff 100644 --- a/internal/cmd/sandboxes/sandboxes.go +++ b/internal/cmd/sandboxes/sandboxes.go @@ -35,21 +35,22 @@ type commands struct { deps cliruntime.Deps usageDeps cliruntime.Deps json bool + local bool } // New returns the Sandbox command tree. func New(deps cliruntime.Deps) *cobra.Command { - return newCommands(deps, deps) + return newCommands(deps, deps, false) } // NewLocal uses authenticated local user credentials for project usage and // service-role credentials for the Sandbox session APIs. func NewLocal(deps, usageDeps cliruntime.Deps) *cobra.Command { - return newCommands(deps, usageDeps) + return newCommands(deps, usageDeps, true) } -func newCommands(deps, usageDeps cliruntime.Deps) *cobra.Command { - c := &commands{deps: deps, usageDeps: usageDeps} +func newCommands(deps, usageDeps cliruntime.Deps, local bool) *cobra.Command { + c := &commands{deps: deps, usageDeps: usageDeps, local: local} cmd := &cobra.Command{Use: "sandboxes", Short: "Run isolated commands and manage temporary sessions"} cmd.PersistentFlags().BoolVar(&c.json, "json", false, "Print compact JSON; use structured results for exec, shell, and files read") cmd.AddCommand(c.presets(), c.usage(), c.execute(), c.shell(), c.run(), c.sessions(), c.get(), c.control("suspend"), c.control("resume"), c.control("terminate"), c.files(), c.templates(), c.deployments()) diff --git a/tests/e2e/localmode/sandbox_config_test.go b/tests/e2e/localmode/sandbox_config_test.go index fc24cf3..fc2d3f9 100644 --- a/tests/e2e/localmode/sandbox_config_test.go +++ b/tests/e2e/localmode/sandbox_config_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" @@ -75,6 +76,30 @@ sandboxes: require.Equal(t, "local-custom", saved.Name) require.NotNil(t, saved.MemoryMb) require.Equal(t, 1024, *saved.MemoryMb) + + checkLifetime := func(want time.Duration, extraArgs ...string) { + t.Helper() + args := append([]string{"sandboxes", "run", "--template", template, "--json"}, extraArgs...) + var session apiclient.SandboxSession + require.NoError(t, json.Unmarshal([]byte(runVolcanoLocalModeE2EStdout(t, binary, env, dir, args...)), &session)) + defer runVolcanoLocalModeE2EStdout(t, binary, env, dir, "sandboxes", "terminate", session.Id.String()) + if want == 0 { + require.Nil(t, session.ExpiresAt, "unlimited local sessions must not expire") + } else { + require.NotNil(t, session.ExpiresAt) + require.WithinDuration(t, session.CreatedAt.Add(want), *session.ExpiresAt, time.Second) + } + } + checkLifetime(600 * time.Second) + checkLifetime(0, "--duration", "0") + require.NoError(t, os.WriteFile(manifest, []byte(`version: 1 +sandboxes: + - name: local-custom + ttl_seconds: 0 +`), 0o600)) + runVolcanoLocalModeE2EStdout(t, binary, env, dir, "config", "deploy", "--file", manifest) + require.Zero(t, pull().TTL) + checkLifetime(0) } func requireLocalSandboxDeploymentArtifacts(t *testing.T, run func(...string) string, template, deployment string) {