settings

package
v0.4.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

Documentation

Index

Constants

View Source
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]*)$`
)
View Source
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

View Source
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

View Source
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

View Source
var (
	KeyInstanceName = register(Key{"instance.name", "instance", ""})
	KeyInstanceMOTD = register(Key{"instance.motd", "instance", ""})
)

Instance settings

View Source
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

View Source
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.

View Source
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 IsKnown

func IsKnown(keyName string) bool

IsKnown reports whether keyName is a registered setting.

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:

  1. 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.

  2. 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

func (s *InstanceService) GetAll(ctx context.Context) (map[string]any, error)

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

func (s *InstanceService) GetBool(ctx context.Context, key string) (bool, error)

GetBool returns a bool setting value.

func (*InstanceService) GetInt

func (s *InstanceService) GetInt(ctx context.Context, key string) (int, error)

GetInt returns an int setting value.

func (*InstanceService) GetString

func (s *InstanceService) GetString(ctx context.Context, key string) (string, error)

GetString returns a string setting value.

func (*InstanceService) GetStrings

func (s *InstanceService) GetStrings(ctx context.Context, key string) ([]string, error)

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

func (s *InstanceService) Set(ctx context.Context, key string, value any) error

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.

func (Key) Default

func (k Key) Default() any

Default returns the default value for this setting.

func (Key) Name

func (k Key) Name() string

Name returns the dotted setting name (e.g. "workspace.defaultStorageSize").

func (Key) Section

func (k Key) Section() string

Section returns the top-level section (e.g. "workspace", "auth").

type SeedResult

type SeedResult struct {
	Inserted int
	Skipped  int
	Orphaned []string
}

SeedResult contains the outcome of a seed operation.

func Seed

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

func (s *UserService) GetAll(ctx context.Context, userID string) (map[string]any, error)

GetAll returns all user settings merged with schema defaults.

func (*UserService) GetBool

func (s *UserService) GetBool(ctx context.Context, userID, key string) (bool, error)

GetBool returns a bool user setting value.

func (*UserService) GetInt

func (s *UserService) GetInt(ctx context.Context, userID, key string) (int, error)

GetInt returns an int user setting value.

func (*UserService) GetString

func (s *UserService) GetString(ctx context.Context, userID, key string) (string, error)

GetString returns a string user setting value.

func (*UserService) Schema

func (s *UserService) Schema() []SettingDef

Schema returns the Tier 3 setting definitions.

func (*UserService) Set

func (s *UserService) Set(ctx context.Context, userID, key string, value any) error

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

type UserStore

type UserStore interface {
	GetAllUserSettings(ctx context.Context, userID string) (map[string]json.RawMessage, error)
	SetUserSetting(ctx context.Context, userID, key string, value json.RawMessage) error
}

UserStore is the database interface for user settings.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL