Documentation
¶
Index ¶
- Constants
- Variables
- func InstanceSettingIndex() map[string]SettingDef
- func IsKnown(keyName string) bool
- func Normalize(def SettingDef, value any) any
- func UserSettingIndex() map[string]SettingDef
- func Validate(def SettingDef, value any) error
- type InstanceService
- func (s *InstanceService) DefaultStorageClass(ctx context.Context) (string, error)
- func (s *InstanceService) DefaultStorageSize(ctx context.Context) (string, error)
- func (s *InstanceService) GetAll(ctx context.Context) (map[string]any, error)
- func (s *InstanceService) GetBool(ctx context.Context, key string) (bool, error)
- func (s *InstanceService) GetInt(ctx context.Context, key string) (int, error)
- func (s *InstanceService) GetString(ctx context.Context, key string) (string, error)
- func (s *InstanceService) GetStrings(ctx context.Context, key string) ([]string, error)
- func (s *InstanceService) RateLimitingBurstSize(ctx context.Context) (int, error)
- func (s *InstanceService) RateLimitingDefaultLimit(ctx context.Context) (int, error)
- func (s *InstanceService) Schema() []SettingDef
- func (s *InstanceService) Set(ctx context.Context, key string, value any) error
- func (s *InstanceService) SetHelmOverrides(overrides map[string]any)
- func (s *InstanceService) Start() error
- func (s *InstanceService) Stop() error
- type InstanceStore
- type Key
- type SeedResult
- type SeedStore
- type SettingDef
- type SettingType
- type UserService
- func (s *UserService) GetAll(ctx context.Context, userID string) (map[string]any, error)
- func (s *UserService) GetBool(ctx context.Context, userID, key string) (bool, error)
- func (s *UserService) GetInt(ctx context.Context, userID, key string) (int, error)
- func (s *UserService) GetString(ctx context.Context, userID, key string) (string, error)
- func (s *UserService) Schema() []SettingDef
- func (s *UserService) Set(ctx context.Context, userID, key string, value any) error
- func (s *UserService) Start() error
- func (s *UserService) Stop() error
- type UserStore
Constants ¶
const ( // MemoryQuantityPattern matches valid Kubernetes memory quantities // for spec.resources.memory: a positive integer with a Ki/Mi/Gi // suffix. MemoryQuantityPattern = `^[1-9][0-9]*(Ki|Mi|Gi)$` // StorageQuantityPattern matches valid storage quantities for // spec.storage.size: a positive integer with a Gi/Mi suffix. StorageQuantityPattern = `^[1-9][0-9]*(Gi|Mi)$` // CPUQuantityPattern matches valid CPU quantities for // spec.resources.cpu: positive millicores ("500m") or positive // fractional cores ("1.0", "0.5"). Zero magnitude is rejected: // "0m" and "0.0" are not useful for a workspace and would result // in unschedulable pods (kubelet rejects requests.cpu == 0). The // three alternations enumerate the positive-magnitude cases: // - [1-9][0-9]*m → "1m", "500m", "1000m" // - [1-9][0-9]*\.[0-9]+ → "1.0", "1.5", "16.0" // - 0\.[0-9]*[1-9][0-9]* → "0.5", "0.001" (zero whole part, // but a non-zero digit somewhere // in the fractional part) CPUQuantityPattern = `^([1-9][0-9]*m|[1-9][0-9]*\.[0-9]+|0\.[0-9]*[1-9][0-9]*)$` )
const SchemaVersion = 6
SchemaVersion is incremented on any schema change (add/remove/modify keys). Used by the seed job to detect orphaned keys and by the frontend to cache-bust.
Bumped to 3 (2026-06-18): added Pattern + magnitude constraints to workspace.defaultResources.{cpu,memory}; tightened the existing pattern on workspace.defaultStorageSize to reject zero-magnitude values. The schema response shape exposed to the admin UI changed, so clients caching the schema need to refresh.
Bumped to 4 (2026-06-19): tightened workspace.defaultResources.cpu Pattern to reject zero-magnitude values ("0m", "0.0", "0.00"). Closes the parallel zero-magnitude gap that the memory and storage tightening in v3 missed. Bumped to 5 (2026-06-19): added ReadOnly field to SettingDef (US-49.2 helm-precedence model — helm-managed keys are read-only in the admin UX) and added four email.* instance settings (provider/sesRegion/fromAddress/ baseUrl). The schema response shape changed (new field + new keys); clients caching the schema need to refresh. Bumped to 6 (2026-07-06): flipped user setting sendOnEnter Default from true to false and updated its Description (desktop Enter is now newline by default; Ctrl/Cmd+Enter sends; mobile is button-only). Same class of change as v3/v4 (modifying a property of an existing key) — admin UI and frontend schema caches must refresh to show the new description.
Variables ¶
var ( KeyWorkspaceDefaultStorageSize = register(Key{"workspace.defaultStorageSize", "workspace", "15Gi"}) KeyWorkspaceDefaultStorageClass = register(Key{"workspace.defaultStorageClass", "workspace", ""}) KeyWorkspaceDefaultImage = register(Key{"workspace.defaultImage", "workspace", ""}) KeyWorkspaceDefaultSecurityLevel = register(Key{"workspace.defaultSecurityLevel", "workspace", ""}) KeyWorkspaceDefaultResourcesCPU = register(Key{"workspace.defaultResources.cpu", "workspace", ""}) KeyWorkspaceDefaultResourcesMemory = register(Key{"workspace.defaultResources.memory", "workspace", ""}) KeyWorkspaceAutoSuspendEnabled = register(Key{"workspace.autoSuspend.enabled", "workspace", false}) KeyWorkspaceAutoSuspendIdleTimeout = register(Key{"workspace.autoSuspend.idleTimeoutMinutes", "workspace", 0}) KeyWorkspaceTTLDaysAfterSuspended = register(Key{"workspace.ttlDaysAfterSuspended", "workspace", 0}) KeyWorkspaceDefaultNetworkIngress = register(Key{"workspace.defaultNetworkAccess.ingress", "workspace", false}) KeyWorkspaceDefaultNetworkEgress = register(Key{"workspace.defaultNetworkAccess.egressDomains", "workspace", []string{}}) KeyWorkspaceDefaultMaxActiveSessions = register(Key{"workspace.defaultMaxActiveSessions", "workspace", 0}) KeyWorkspaceMaxActivePerUser = register(Key{"workspace.maxActiveWorkspacesPerUser", "workspace", 0}) )
Workspace settings
var ( KeyAuthLockoutEnabled = register(Key{"auth.lockoutEnabled", "auth", false}) KeyAuthLockoutAttempts = register(Key{"auth.lockoutAttempts", "auth", 0}) KeyAuthLockoutDurationMinutes = register(Key{"auth.lockoutDurationMinutes", "auth", 0}) KeyAuthRegistrationEnabled = register(Key{"auth.registrationEnabled", "auth", true}) )
Auth settings
var ( KeyInstanceName = register(Key{"instance.name", "instance", ""}) KeyInstanceMOTD = register(Key{"instance.motd", "instance", ""}) )
Instance settings
var ( KeyRateLimitingEnabled = register(Key{"rateLimiting.enabled", "rateLimiting", false}) KeyRateLimitingDefaultLimit = register(Key{"rateLimiting.defaultLimit", "rateLimiting", 100}) KeyRateLimitingDefaultWindow = register(Key{"rateLimiting.defaultWindow", "rateLimiting", "1m"}) KeyRateLimitingBurstSize = register(Key{"rateLimiting.burstSize", "rateLimiting", 20}) KeyRateLimitingStrategy = register(Key{"rateLimiting.strategy", "rateLimiting", "token_bucket"}) KeyRateLimitingWindowMinutes = register(Key{"rateLimiting.windowMinutes", "rateLimiting", 0}) )
Rate limiting settings
var ErrReadOnly = fmt.Errorf("setting is managed by Helm and cannot be changed via the API")
ErrReadOnly is returned by Set when the key is helm-managed (Tier 1). Callers (the settings handler) map this to HTTP 409 Conflict so the admin UX gets a clear "this setting is managed by Helm" signal.
var KnownKeys = map[string]Key{}
KnownKeys is the complete registry of instance settings. PUT /admin/settings validates against this set; unknown keys are rejected.
Functions ¶
func InstanceSettingIndex ¶
func InstanceSettingIndex() map[string]SettingDef
InstanceSettingIndex returns a key→SettingDef map for Tier 2 settings.
func Normalize ¶
func Normalize(def SettingDef, value any) any
Normalize canonicalizes a value before validation. The motivation is the "8gi" production failure: an admin typed "8gi" (lowercase unit) in the admin settings UI; the value passed validation (no pattern), reached the database, and broke every workspace creation when the validating webhook rejected the lowercase suffix.
Two-stage policy:
Normalize() rewrites unambiguous near-misses to canonical form ("8gi" → "8Gi", "8GB" → "8Gi", " 500m " → "500m"). The canonical form is what the Kubernetes apiserver and our validating webhook accept.
The caller then runs Validate() against the normalized value. Inputs that the normalizer can't safely correct ("banana", "8gigabyte", "8 G") pass through unchanged so Validate's pattern check rejects them with a precise error.
Only string-typed settings with a known shape are normalized. Bool, int, enum, []string, and string settings without a registered normalizer pass through untouched.
Normalize cannot fail — rejection is Validate's job. Returning (value) without an error keeps the call sites simple.
func UserSettingIndex ¶
func UserSettingIndex() map[string]SettingDef
UserSettingIndex returns a key→SettingDef map for Tier 3 settings.
func Validate ¶
func Validate(def SettingDef, value any) error
Validate checks that value is valid for the given setting definition. Returns nil if valid, or a descriptive error.
Types ¶
type InstanceService ¶
type InstanceService struct {
// contains filtered or unexported fields
}
InstanceService implements InstanceSettingsService with a full-map cache and singleflight to prevent thundering herd on TTL expiry.
func NewInstanceService ¶
func NewInstanceService(store InstanceStore, logger pkginterfaces.LoggerInterface) *InstanceService
NewInstanceService creates a new instance settings service.
func (*InstanceService) DefaultStorageClass ¶
func (s *InstanceService) DefaultStorageClass(ctx context.Context) (string, error)
func (*InstanceService) DefaultStorageSize ¶
func (s *InstanceService) DefaultStorageSize(ctx context.Context) (string, error)
func (*InstanceService) GetAll ¶
GetAll returns all instance settings merged with schema defaults and helm overrides. Helm-managed keys (Tier 1) take precedence over both DB values and schema defaults.
func (*InstanceService) GetStrings ¶
GetStrings returns a []string setting value.
func (*InstanceService) RateLimitingBurstSize ¶
func (s *InstanceService) RateLimitingBurstSize(ctx context.Context) (int, error)
func (*InstanceService) RateLimitingDefaultLimit ¶
func (s *InstanceService) RateLimitingDefaultLimit(ctx context.Context) (int, error)
func (*InstanceService) Schema ¶
func (s *InstanceService) Schema() []SettingDef
Schema returns the Tier 2 setting definitions. Helm-managed keys (those set via SetHelmOverrides) are marked ReadOnly=true so the frontend can render them as disabled with a "Managed by Helm" badge.
func (*InstanceService) Set ¶
Set validates and persists a setting value, then invalidates the cache. Returns ErrReadOnly if the key is helm-managed (Tier 1).
func (*InstanceService) SetHelmOverrides ¶
func (s *InstanceService) SetHelmOverrides(overrides map[string]any)
SetHelmOverrides marks the given keys as helm-managed (Tier 1) and pins their values. Called once at boot from app.go when email.enabled=true. Keys not in the instance schema are silently ignored (defensive). Must be called before Start() / before serving requests. After that, the map is not written again; all reads take s.mu.RLock() (the same lock that guards the data cache), so concurrent access is race-free.
func (*InstanceService) Start ¶
func (s *InstanceService) Start() error
func (*InstanceService) Stop ¶
func (s *InstanceService) Stop() error
type InstanceStore ¶
type InstanceStore interface {
GetAllInstanceSettings(ctx context.Context) (map[string]json.RawMessage, error)
SetInstanceSetting(ctx context.Context, key string, value json.RawMessage) error
}
InstanceStore is the database interface for instance settings.
type Key ¶
type Key struct {
// contains filtered or unexported fields
}
Key is a typed reference to an instance setting. It replaces raw string keys ("workspace.defaultStorageSize") with compile-time-checked constants. A typo in a Key constant is a compile error; a typo in a string literal is a silent runtime default.
type SeedResult ¶
SeedResult contains the outcome of a seed operation.
func Seed ¶
func Seed(ctx context.Context, store SeedStore, logger pkginterfaces.LoggerInterface) (*SeedResult, error)
Seed inserts schema defaults for any missing instance settings keys and detects orphaned keys (in DB but not in current schema).
type SeedStore ¶
type SeedStore interface {
GetAllInstanceSettings(ctx context.Context) (map[string]json.RawMessage, error)
InsertInstanceSettingIfMissing(ctx context.Context, key string, value json.RawMessage) (inserted bool, err error)
}
SeedStore is the database interface needed by the seed job.
type SettingDef ¶
type SettingDef struct {
Key string `json:"key"`
Tier int `json:"tier"` // 2=instance, 3=user
Type SettingType `json:"type"`
Default any `json:"default"`
Min *int `json:"min,omitempty"` // int range
Max *int `json:"max,omitempty"` // int range
Pattern string `json:"pattern,omitempty"` // string regex
Enum []string `json:"enum,omitempty"` // enum values
Category string `json:"category"` // UI grouping
Label string `json:"label"` // UI display name
Description string `json:"description"` // UI help text
// ReadOnly is set to true when the key is managed by Helm (Tier 1
// helm-precedence model, US-49.2). The admin UX must disable edits to
// these keys and show a "Managed by Helm" badge. Set() rejects writes
// to read-only keys with ErrReadOnly.
ReadOnly bool `json:"readOnly,omitempty"`
}
SettingDef defines a single mutable setting. Every setting has a default — there is no concept of a "required" setting that can be missing.
func AllSettings ¶
func AllSettings() []SettingDef
AllSettings returns all setting definitions across all tiers.
func InstanceSettings ¶
func InstanceSettings() []SettingDef
InstanceSettings returns all Tier 2 (admin-mutable) setting definitions.
func UserSettings ¶
func UserSettings() []SettingDef
UserSettings returns all Tier 3 (per-user) setting definitions.
type SettingType ¶
type SettingType string
SettingType defines the data type of a setting.
const ( TypeBool SettingType = "bool" TypeInt SettingType = "int" TypeString SettingType = "string" TypeEnum SettingType = "enum" TypeStrings SettingType = "strings" )
type UserService ¶
type UserService struct {
// contains filtered or unexported fields
}
UserService implements user settings with typed accessors. No caching — user settings are read infrequently (page load only).
func NewUserService ¶
func NewUserService(store UserStore, logger pkginterfaces.LoggerInterface) *UserService
NewUserService creates a new user settings service.
func (*UserService) Schema ¶
func (s *UserService) Schema() []SettingDef
Schema returns the Tier 3 setting definitions.
func (*UserService) Set ¶
Set validates and persists a user setting value.
As with InstanceService.Set, the value is run through Normalize() before Validate() so honest typos in any patterned string settings get auto-corrected. User-tier settings don't currently include resource quantities, but the same path is used for symmetry and future-proofing.
func (*UserService) Start ¶
func (s *UserService) Start() error
func (*UserService) Stop ¶
func (s *UserService) Stop() error