Documentation
¶
Index ¶
- func ContextWithSessionID(ctx context.Context, sessionID string) context.Context
- type Config
- type CredentialProvisioner
- type OrgMembershipChecker
- type PhaseChangeCallback
- type PolicyChecker
- type SecretAutoProvisioner
- type Service
- func (s *Service) ActivateWorkspace(ctx context.Context, userID, workspaceID string) (*types.ActivateWorkspaceResponse, error)
- func (s *Service) CheckOwnership(ctx context.Context, userID string, meta *types.WorkspaceMetadata) error
- func (s *Service) CreateWorkspace(ctx context.Context, userID string, req types.CreateWorkspaceRequest) (*types.Workspace, error)
- func (s *Service) DeleteWorkspace(ctx context.Context, userID, workspaceID string) error
- func (s *Service) EnsureSession(ctx context.Context, userID, workspaceID string) (*types.EnsureSessionResponse, error)
- func (s *Service) GetWorkspace(ctx context.Context, userID, workspaceID string) (*types.Workspace, error)
- func (s *Service) GetWorkspaceStatus(ctx context.Context, userID, workspaceID string) (*types.WorkspaceStatusResult, error)
- func (s *Service) ListWorkspaceSessions(ctx context.Context, userID, workspaceID string) ([]types.SessionListItem, error)
- func (s *Service) ListWorkspaces(ctx context.Context, userID string, opts types.ListOptions) (*types.WorkspaceListResult, error)
- func (s *Service) MarkSessionSeen(ctx context.Context, userID, workspaceID, sessionID string) error
- func (s *Service) NeutralizeUserWorkspaces(ctx context.Context, userID string) error
- func (s *Service) RefreshWorkspaceCompute(ctx context.Context, userID, workspaceID string) (*types.RefreshWorkspaceResult, error)
- func (s *Service) RenameSession(ctx context.Context, userID, workspaceID, sessionID, title string) error
- func (s *Service) RenameWorkspace(ctx context.Context, userID, workspaceID, name string) error
- func (s *Service) ResolveWorkspace(ctx context.Context, workspaceID string) (*types.WorkspaceMetadata, error)
- func (s *Service) RestartWorkspace(ctx context.Context, userID, workspaceID string) error
- func (s *Service) SetCredentialProvisioner(cp CredentialProvisioner)
- func (s *Service) SetInstanceSettings(svc *settings.InstanceService)
- func (s *Service) SetOrgStore(store OrgMembershipChecker)
- func (s *Service) SetPolicyChecker(checker PolicyChecker)
- func (s *Service) SetSecretAutoProvisioner(sp SecretAutoProvisioner)
- func (s *Service) SetSessionIndex(si apiinterfaces.SessionIndexService)
- func (s *Service) Start() error
- func (s *Service) Stop() error
- func (s *Service) SuspendWorkspace(ctx context.Context, userID, workspaceID string) error
- type VersionSyncCallback
- type Watcher
- func (w *Watcher) GetAllKnownPhases() map[string]string
- func (w *Watcher) GetKnownPhase(name string) (string, bool)
- func (w *Watcher) SetKnownPhase(name, phase string)
- func (w *Watcher) SetUserBroker(broker WorkspaceOwnerTracker)
- func (w *Watcher) SetVersionSyncCallback(cb VersionSyncCallback)
- func (w *Watcher) SetWorkspaceUpdateCallback(cb WorkspaceUpdateCallback)
- func (w *Watcher) Start() error
- func (w *Watcher) Stop()
- type WorkspaceConfig
- type WorkspaceOwnerTracker
- type WorkspaceUpdateCallback
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Config ¶
type Config struct {
Namespace string
OpencodePort int // Port for opencode on sandbox pods. Default: 4096.
}
Config holds workspace service configuration.
type CredentialProvisioner ¶
type CredentialProvisioner interface {
SeedWorkspaceCredentials(ctx context.Context, workspaceID, userID string, orgID *string) error
// SeedWorkspaceMCPServers seeds mcp_server_bindings from auto-apply rules.
// Epic 53. Best-effort: a failure does not block workspace creation.
SeedWorkspaceMCPServers(ctx context.Context, workspaceID, userID string, orgID *string) error
}
CredentialProvisioner seeds workspace_credential_bindings from credential_auto_apply.
type OrgMembershipChecker ¶
type OrgMembershipChecker interface {
IsOrgMember(ctx context.Context, orgID, userID string) (bool, error)
IsOrgAdmin(ctx context.Context, orgID, userID string) (bool, error)
// GetUserOrgID returns the user's single org ID (or "" if not in any org).
// Used by CreateWorkspace for D4 auto-attribution.
GetUserOrgID(ctx context.Context, userID string) (string, error)
}
type PhaseChangeCallback ¶
type PolicyChecker ¶
type PolicyChecker interface {
GetEffectivePolicy(ctx context.Context, orgID string) (*types.OrgPolicyValues, error)
}
PolicyChecker reads the effective org policy for enforcement. The policy service implements this; nil means no policy enforcement (dev/test).
type SecretAutoProvisioner ¶
type SecretAutoProvisioner interface {
SeedGlobalDefaultSecrets(ctx context.Context, workspaceID, userID string) error
}
SecretAutoProvisioner seeds user_secret_bindings from user_secrets where global_default=true. Called on workspace creation as a best-effort operation.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service implements apiinterfaces.WorkspaceService.
func New ¶
func New( logger pkginterfaces.LoggerInterface, k8sClient pkginterfaces.KubernetesClient, dbService apiinterfaces.DatabaseService, cacheService apiinterfaces.CacheService, metricsService apiinterfaces.MetricsService, config *Config, ) (*Service, error)
New creates a validated workspace service. config may be nil to use defaults.
func (*Service) ActivateWorkspace ¶
func (s *Service) ActivateWorkspace(ctx context.Context, userID, workspaceID string) (*types.ActivateWorkspaceResponse, error)
ActivateWorkspace resumes a workspace, suspending the stalest active one if at cap.
func (*Service) CheckOwnership ¶
func (s *Service) CheckOwnership(ctx context.Context, userID string, meta *types.WorkspaceMetadata) error
CheckOwnership is the pure-authorisation half of verifyOwner, operating on a previously-resolved *types.WorkspaceMetadata so callers can avoid a second DB hit. Behavior is identical to the post-fetch portion of verifyOwner:
- nil meta → fail-closed Forbidden (defense-in-depth; ResolveWorkspace already returns NotFound for missing rows).
- creator match → for org-attributed workspaces the creator must still be a CURRENT org member (D5 offboarding); personal workspaces pass.
- non-creator on an org workspace → allowed only as org admin (D6).
- anything else → Forbidden.
Org-store failures propagate as wrapped errors (NOT *APIError) so callers can distinguish infrastructure failure from authorisation denial — this preserves the exact return-shape contract verifyOwner had before the split.
func (*Service) CreateWorkspace ¶
func (s *Service) CreateWorkspace(ctx context.Context, userID string, req types.CreateWorkspaceRequest) (*types.Workspace, error)
CreateWorkspace validates the request, creates a Workspace CRD, and persists metadata to the database. On database failure the CRD is deleted.
func (*Service) DeleteWorkspace ¶
DeleteWorkspace marks a workspace as terminating and deletes the CRD.
func (*Service) EnsureSession ¶
func (s *Service) EnsureSession(ctx context.Context, userID, workspaceID string) (*types.EnsureSessionResponse, error)
EnsureSession guarantees the workspace has a Running sandbox and creates a new session on it. If the workspace is suspended it resumes it; if no sandbox exists it creates one. Blocks until the sandbox reaches Running, then creates the session via opencode's POST /session endpoint.
func (*Service) GetWorkspace ¶
func (s *Service) GetWorkspace(ctx context.Context, userID, workspaceID string) (*types.Workspace, error)
GetWorkspace retrieves a workspace by ID, verifying owner.
func (*Service) GetWorkspaceStatus ¶
func (s *Service) GetWorkspaceStatus(ctx context.Context, userID, workspaceID string) (*types.WorkspaceStatusResult, error)
GetWorkspaceStatus returns infrastructure state from the Workspace CRD.
func (*Service) ListWorkspaceSessions ¶
func (s *Service) ListWorkspaceSessions(ctx context.Context, userID, workspaceID string) ([]types.SessionListItem, error)
ListWorkspaceSessions returns session index entries for a workspace.
func (*Service) ListWorkspaces ¶
func (s *Service) ListWorkspaces(ctx context.Context, userID string, opts types.ListOptions) (*types.WorkspaceListResult, error)
ListWorkspaces returns workspace metadata for a user with pagination.
func (*Service) MarkSessionSeen ¶
func (*Service) NeutralizeUserWorkspaces ¶
NeutralizeUserWorkspaces suspends every Active workspace owned by userID and best-effort deletes any legacy workspace-secrets-<id> K8s Secret for each. Post-Epic-35, workspaces no longer create this Secret (secretless injection), so this is an upgrade-path cleanup no-op for post-Epic-35 workspaces. For pre-Epic-35 workspaces with a leftover Secret, it ensures the plaintext is scrubbed.
Suspend is best-effort: workspaces not in the Active phase (Creating, Resuming, already Suspended, ...) are skipped without aborting the loop, and individual failures are logged. The Secret scrub runs for every workspace regardless of phase and ignores NotFound. A nil k8sClient (dev/test) makes the scrub a no-op.
func (*Service) RefreshWorkspaceCompute ¶
func (s *Service) RefreshWorkspaceCompute(ctx context.Context, userID, workspaceID string) (*types.RefreshWorkspaceResult, error)
RefreshWorkspaceCompute re-syncs a workspace CRD with the platform's current defaults, then bumps spec.restartGeneration so the controller rebuilds the pod. The rebuild re-resolves spec.runtime to the latest RuntimeEnvironment image (picking up new image versions) and applies the refreshed resource requests.
Use cases:
- A new runtime image version is published; the user wants the workspace to pick it up without a full recreate.
- The platform's default CPU/memory (workspace.defaultResources) increased and the user wants their long-lived workspace to adopt the new values.
Fields re-applied from instance settings (overwritten to current platform defaults when configured): Resources.CPU, Resources.Memory, SecurityLevel, Storage.StorageClassName, MaxActiveSessions. Fields the platform has no opinion on (empty/zero default) are left untouched, so refresh never clobbers a deliberate user setting with a schema default.
Like RestartWorkspace, refresh is REJECTED for Terminating/Terminated phases (they race with finalizer cleanup) and idempotent at the spec layer.
Suspended workspaces have no pod, and handleSuspended only observes spec.suspend (NOT restartGeneration) — so a generation bump alone would be a no-op. When the workspace is Suspended, refresh also requests a resume via ActivateWorkspace (which enforces the active-workspace cap and writes spec.suspend=false), so the controller builds a fresh pod carrying the refreshed spec. If the resume fails after the spec refresh has persisted, the error is returned but the config update remains in effect for the next manual activate.
func (*Service) RenameSession ¶
func (s *Service) RenameSession(ctx context.Context, userID, workspaceID, sessionID, title string) error
RenameSession updates the title of a session in the session index.
func (*Service) RenameWorkspace ¶
RenameWorkspace updates the name of a workspace.
func (*Service) ResolveWorkspace ¶
func (s *Service) ResolveWorkspace(ctx context.Context, workspaceID string) (*types.WorkspaceMetadata, error)
ResolveWorkspace fetches workspace metadata by ID. It is the pure-fetch half of verifyOwner and the entry point for WorkspaceAccessMiddleware. Returns a NotFound APIError when the workspace does not exist (or the id is empty, mirroring GetWorkspace's empty-input contract) and an Internal APIError when the underlying lookup fails.
func (*Service) RestartWorkspace ¶
RestartWorkspace bumps spec.restartGeneration so the controller's handleFailed (Epic 21 Change A) or handleActive recovery paths walk the workspace back through Pending and rebuild the pod from scratch.
Use cases:
- Recover a Failed workspace (the original motivation; previously required `kubectl patch --subresource=status`).
- Force-restart a stuck Active workspace whose agent is hung but the controller hasn't yet exhausted its transient-failure budget.
Restart is REJECTED for Terminating/Terminated phases — those are genuinely terminal and would race with finalizer cleanup. For all other phases the call is idempotent at the spec layer (each call bumps the field by 1; the controller responds to each bump exactly once via the strict-greater-than check on observedRestartGeneration).
Epic 35: the pod's init container fetches credentials via the bootstrap endpoint at boot — no pre-writing of a K8s Secret is needed.
func (*Service) SetCredentialProvisioner ¶
func (s *Service) SetCredentialProvisioner(cp CredentialProvisioner)
SetCredentialProvisioner installs the credential auto-apply seeder.
func (*Service) SetInstanceSettings ¶
func (s *Service) SetInstanceSettings(svc *settings.InstanceService)
SetInstanceSettings injects the instance settings service for enforcement.
func (*Service) SetOrgStore ¶
func (s *Service) SetOrgStore(store OrgMembershipChecker)
func (*Service) SetPolicyChecker ¶
func (s *Service) SetPolicyChecker(checker PolicyChecker)
SetPolicyChecker installs the org policy checker for workspace quota enforcement.
func (*Service) SetSecretAutoProvisioner ¶
func (s *Service) SetSecretAutoProvisioner(sp SecretAutoProvisioner)
SetSecretAutoProvisioner installs the global-default secret seeder.
func (*Service) SetSessionIndex ¶
func (s *Service) SetSessionIndex(si apiinterfaces.SessionIndexService)
SetSessionIndex injects the session index service. Optional — nil disables session tracking.
type VersionSyncCallback ¶
type VersionSyncCallback func(workspaceID, imageTag, agentVersion string)
VersionSyncCallback is called with (workspaceID, imageTag, agentVersion) when the CRD's imageTag changes so the API can persist the new tag to its DB without a K8s Get. Optional — nil-safe on the watcher.
type Watcher ¶
type Watcher struct {
// contains filtered or unexported fields
}
func NewWatcher ¶
func NewWatcher( k8sClient pkginterfaces.KubernetesClient, logger pkginterfaces.LoggerInterface, namespace string, onPhaseChange PhaseChangeCallback, ) (*Watcher, error)
func (*Watcher) GetAllKnownPhases ¶
func (*Watcher) SetKnownPhase ¶
func (*Watcher) SetUserBroker ¶
func (w *Watcher) SetUserBroker(broker WorkspaceOwnerTracker)
func (*Watcher) SetVersionSyncCallback ¶
func (w *Watcher) SetVersionSyncCallback(cb VersionSyncCallback)
SetVersionSyncCallback sets the callback invoked when a workspace becomes Active with a non-empty imageTag. Must be called before Start(); calling after Start() is a data race (the watch goroutine reads onVersionSync without a lock). Follows the same contract as SetUserBroker.
func (*Watcher) SetWorkspaceUpdateCallback ¶
func (w *Watcher) SetWorkspaceUpdateCallback(cb WorkspaceUpdateCallback)
SetWorkspaceUpdateCallback installs a callback invoked on every Added/Modified event for any Workspace CRD. Optional; nil-safe. Must be called before Start() (same data-race constraint as SetVersionSyncCallback). See WorkspaceUpdateCallback docstring for the callback contract.
type WorkspaceConfig ¶
type WorkspaceConfig = types.WorkspaceConfig
WorkspaceConfig is non-sensitive workspace metadata persisted for pod boot. Re-exported from pkg/types to avoid requiring callers to import both.
type WorkspaceOwnerTracker ¶
type WorkspaceUpdateCallback ¶
WorkspaceUpdateCallback is called on every Added/Modified event (NOT Deleted) for a Workspace, regardless of what changed. Used by the watcher-driven auto-push (worklog 0591): the callback filters on the exact state it cares about (Phase==Active + UserCredsPresent==false + bindings-exist) and no-ops otherwise.
Distinct from PhaseChangeCallback because a pod-recreation may not change phase (e.g. `kubectl delete pod` with immediate controller respawn keeps status.Phase=Active) but does change UserCredsPresent (controller clears it on unreachable, then reports the new agentd's state on the next scrape). Filtering by phase alone misses those.
Callback contract:
- MUST be fast + non-blocking. Slow callbacks stall the watch goroutine, which delays the SSE stream and phase events for all workspaces on this API replica.
- MUST tolerate repeat calls for the same workspace — the watcher emits many Modified events per workspace lifetime, and the callback's own state is the source of truth for "already handled this update."
- MUST NOT panic. Panics propagate to the watch goroutine.