package model import ( "fmt" "time" "k8s.io/apimachinery/pkg/api/resource" ) // User represents a tenant who can create and manage dev pods. type User struct { ID string `json:"id"` CreatedAt time.Time `json:"created_at"` Quota Quota `json:"quota"` } // Quota defines resource limits for a user. type Quota struct { MaxConcurrentPods int `json:"max_concurrent_pods"` MaxCPUPerPod int `json:"max_cpu_per_pod"` MaxRAMGBPerPod int `json:"max_ram_gb_per_pod"` MonthlyPodHours int `json:"monthly_pod_hours"` MonthlyAIRequests int `json:"monthly_ai_requests"` } // DefaultQuota returns the default quota for new users. func DefaultQuota() Quota { return Quota{ MaxConcurrentPods: 3, MaxCPUPerPod: 8, MaxRAMGBPerPod: 16, MonthlyPodHours: 500, MonthlyAIRequests: 10000, } } // ApplyOverrides applies non-nil fields from an UpdateQuotasRequest onto q. func (q *Quota) ApplyOverrides(o UpdateQuotasRequest) { if o.MaxConcurrentPods != nil { q.MaxConcurrentPods = *o.MaxConcurrentPods } if o.MaxCPUPerPod != nil { q.MaxCPUPerPod = *o.MaxCPUPerPod } if o.MaxRAMGBPerPod != nil { q.MaxRAMGBPerPod = *o.MaxRAMGBPerPod } if o.MonthlyPodHours != nil { q.MonthlyPodHours = *o.MonthlyPodHours } if o.MonthlyAIRequests != nil { q.MonthlyAIRequests = *o.MonthlyAIRequests } } // Validate checks that all quota values are positive. func (q *Quota) Validate() error { if q.MaxConcurrentPods < 1 { return fmt.Errorf("max_concurrent_pods must be at least 1") } if q.MaxCPUPerPod < 1 { return fmt.Errorf("max_cpu_per_pod must be at least 1") } if q.MaxRAMGBPerPod < 1 { return fmt.Errorf("max_ram_gb_per_pod must be at least 1") } if q.MonthlyPodHours < 1 { return fmt.Errorf("monthly_pod_hours must be at least 1") } if q.MonthlyAIRequests < 0 { return fmt.Errorf("monthly_ai_requests must be non-negative") } return nil } // APIKey represents an authentication key for API access. type APIKey struct { KeyHash string `json:"-"` UserID string `json:"user_id"` Role string `json:"role"` CreatedAt time.Time `json:"created_at"` LastUsedAt time.Time `json:"last_used_at,omitempty"` } // Role constants for API keys. const ( RoleAdmin = "admin" RoleUser = "user" ) // APIKeyPrefix is the prefix for generated API keys. const APIKeyPrefix = "dpk_" // Pod represents a running dev pod. type Pod struct { User string `json:"user"` Name string `json:"name"` Tools string `json:"tools"` CPUReq string `json:"cpu_req"` CPULimit string `json:"cpu_limit"` MemReq string `json:"mem_req"` MemLimit string `json:"mem_limit"` Task string `json:"task,omitempty"` Status string `json:"status,omitempty"` Age string `json:"age,omitempty"` CPUUsage string `json:"cpu_usage,omitempty"` MemUsage string `json:"mem_usage,omitempty"` URL string `json:"url,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` Labels map[string]string `json:"labels,omitempty"` } // UsageRecord tracks resource usage events. type UsageRecord struct { ID int64 `json:"id"` UserID string `json:"user_id"` PodName string `json:"pod_name"` EventType string `json:"event_type"` Value float64 `json:"value"` RecordedAt time.Time `json:"recorded_at"` } // Event type constants for usage records. const ( EventPodStart = "pod_start" EventPodStop = "pod_stop" EventCPUSample = "cpu_sample" EventMemSample = "mem_sample" EventAIRequest = "ai_request" ) // UsageSummary holds aggregated usage for a billing period. type UsageSummary struct { PodHours float64 `json:"pod_hours"` CPUHours float64 `json:"cpu_hours"` AIRequests int64 `json:"ai_requests"` BudgetUsedPct float64 `json:"budget_used_pct"` } // DailyUsage holds usage data for a single day. type DailyUsage struct { Date string `json:"date"` PodHours float64 `json:"pod_hours"` CPUHours float64 `json:"cpu_hours"` AIRequests int64 `json:"ai_requests"` } // UserUsageSummary holds usage data for a single user. type UserUsageSummary struct { UserID string `json:"user_id"` Usage UsageSummary `json:"usage"` } // CreatePodRequest is the request body for creating a pod. type CreatePodRequest struct { User string `json:"user"` Pod string `json:"pod"` Tools string `json:"tools"` CPUReq string `json:"cpu_req"` CPULimit string `json:"cpu_limit"` MemReq string `json:"mem_req"` MemLimit string `json:"mem_limit"` Task string `json:"task"` TailscaleKey string `json:"tailscale_key,omitempty"` } // Validate checks that a CreatePodRequest has required fields and valid values. func (r *CreatePodRequest) Validate() error { if r.User == "" { return fmt.Errorf("user is required") } if r.Pod == "" { return fmt.Errorf("pod name is required") } if !isValidName(r.User) { return fmt.Errorf("user must be lowercase alphanumeric with hyphens, 1-63 chars") } if len(NamespaceName(r.User)) > 63 { return fmt.Errorf("user name too long: derived namespace %q exceeds 63-char k8s limit (max user name: 59 chars)", NamespaceName(r.User)) } if !isValidName(r.Pod) { return fmt.Errorf("pod name must be lowercase alphanumeric with hyphens, 1-63 chars") } if len(ServiceName(r.Pod)) > 63 { return fmt.Errorf("pod name too long: derived service name %q exceeds 63-char k8s limit (max pod name: 51 chars)", ServiceName(r.Pod)) } for _, pair := range []struct{ field, value string }{ {"cpu_req", r.CPUReq}, {"cpu_limit", r.CPULimit}, {"mem_req", r.MemReq}, {"mem_limit", r.MemLimit}, } { if pair.value != "" { if _, err := resource.ParseQuantity(pair.value); err != nil { return fmt.Errorf("%s is not a valid resource quantity: %s", pair.field, pair.value) } } } return nil } // CreateUserRequest is the request body for creating a user. // Quotas uses pointer fields so that zero values (e.g. monthly_ai_requests: 0) // can be distinguished from omitted fields. type CreateUserRequest struct { User string `json:"user"` Quotas *UpdateQuotasRequest `json:"quotas,omitempty"` } // Validate checks that a CreateUserRequest has required fields. func (r *CreateUserRequest) Validate() error { if r.User == "" { return fmt.Errorf("user is required") } if !isValidName(r.User) { return fmt.Errorf("user must be lowercase alphanumeric with hyphens, 1-63 chars") } if len(NamespaceName(r.User)) > 63 { return fmt.Errorf("user name too long: derived namespace %q exceeds 63-char k8s limit (max user name: 59 chars)", NamespaceName(r.User)) } return nil } // UpdateQuotasRequest is the request body for updating user quotas. type UpdateQuotasRequest struct { MaxConcurrentPods *int `json:"max_concurrent_pods,omitempty"` MaxCPUPerPod *int `json:"max_cpu_per_pod,omitempty"` MaxRAMGBPerPod *int `json:"max_ram_gb_per_pod,omitempty"` MonthlyPodHours *int `json:"monthly_pod_hours,omitempty"` MonthlyAIRequests *int `json:"monthly_ai_requests,omitempty"` } // UpdatePodRequest is the request body for updating pod resources (delete + recreate). type UpdatePodRequest struct { Tools *string `json:"tools,omitempty"` CPUReq *string `json:"cpu_req,omitempty"` CPULimit *string `json:"cpu_limit,omitempty"` MemReq *string `json:"mem_req,omitempty"` MemLimit *string `json:"mem_limit,omitempty"` Task *string `json:"task,omitempty"` } // Validate checks that resource quantities in UpdatePodRequest are valid. func (r *UpdatePodRequest) Validate() error { for _, pair := range []struct{ field string; value *string }{ {"cpu_req", r.CPUReq}, {"cpu_limit", r.CPULimit}, {"mem_req", r.MemReq}, {"mem_limit", r.MemLimit}, } { if pair.value != nil { if _, err := resource.ParseQuantity(*pair.value); err != nil { return fmt.Errorf("%s is not a valid resource quantity: %s", pair.field, *pair.value) } } } return nil } // Validate checks that quota values are positive when provided. func (r *UpdateQuotasRequest) Validate() error { if r.MaxConcurrentPods != nil && *r.MaxConcurrentPods < 1 { return fmt.Errorf("max_concurrent_pods must be at least 1") } if r.MaxCPUPerPod != nil && *r.MaxCPUPerPod < 1 { return fmt.Errorf("max_cpu_per_pod must be at least 1") } if r.MaxRAMGBPerPod != nil && *r.MaxRAMGBPerPod < 1 { return fmt.Errorf("max_ram_gb_per_pod must be at least 1") } if r.MonthlyPodHours != nil && *r.MonthlyPodHours < 1 { return fmt.Errorf("monthly_pod_hours must be at least 1") } if r.MonthlyAIRequests != nil && *r.MonthlyAIRequests < 0 { return fmt.Errorf("monthly_ai_requests must be non-negative") } return nil } // isValidName checks if a string is a valid k8s-compatible name: // lowercase alphanumeric and hyphens, 1-63 chars, must start and end with alphanumeric. func isValidName(name string) bool { if len(name) == 0 || len(name) > 63 { return false } for i, c := range name { isAlphaNum := (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') isHyphen := c == '-' if !isAlphaNum && !isHyphen { return false } if (i == 0 || i == len(name)-1) && isHyphen { return false } } return true } // NamespaceName returns the k8s namespace for a user. func NamespaceName(user string) string { return "dev-" + user } // PodFullName returns the full k8s pod name. func PodFullName(podName string) string { return "dev-pod-" + podName } // ServiceName returns the k8s service name for a pod. func ServiceName(podName string) string { return "dev-pod-" + podName + "-svc" } // PodURL returns the external URL for a dev pod. func PodURL(domain, user, podName string) string { return fmt.Sprintf("https://%s/@%s/%s/", domain, user, podName) } // ClusterStatus holds overall cluster status including per-node details. type ClusterStatus struct { Nodes []NodeStatus `json:"nodes"` Total ResourceSummary `json:"total"` } // NodeStatus describes a single cluster node's capacity and usage. type NodeStatus struct { Name string `json:"name"` Status string `json:"status"` CPUCapacity string `json:"cpu_capacity"` CPUAllocatable string `json:"cpu_allocatable"` MemCapacity string `json:"mem_capacity"` MemAllocatable string `json:"mem_allocatable"` CPUUsage string `json:"cpu_usage,omitempty"` MemUsage string `json:"mem_usage,omitempty"` } // ResourceSummary holds aggregated resource totals for the cluster. type ResourceSummary struct { CPUCapacity string `json:"cpu_capacity"` CPUAllocatable string `json:"cpu_allocatable"` MemCapacity string `json:"mem_capacity"` MemAllocatable string `json:"mem_allocatable"` } // CacheStat describes a cache service PVC in the dev-infra namespace. type CacheStat struct { Name string `json:"name"` PVCName string `json:"pvc_name"` Capacity string `json:"capacity"` Status string `json:"status"` } // RunnerStatus represents the lifecycle state of a runner. type RunnerStatus string const ( RunnerStatusReceived RunnerStatus = "received" RunnerStatusPodCreating RunnerStatus = "pod_creating" RunnerStatusRunnerRegistered RunnerStatus = "runner_registered" RunnerStatusJobClaimed RunnerStatus = "job_claimed" RunnerStatusCompleted RunnerStatus = "completed" RunnerStatusFailed RunnerStatus = "failed" RunnerStatusCleanupPending RunnerStatus = "cleanup_pending" ) var validRunnerTransitions = map[RunnerStatus][]RunnerStatus{ RunnerStatusReceived: {RunnerStatusPodCreating, RunnerStatusFailed}, RunnerStatusPodCreating: {RunnerStatusRunnerRegistered, RunnerStatusFailed}, RunnerStatusRunnerRegistered: {RunnerStatusJobClaimed, RunnerStatusFailed}, RunnerStatusJobClaimed: {RunnerStatusCompleted, RunnerStatusFailed}, RunnerStatusCompleted: {RunnerStatusCleanupPending}, RunnerStatusFailed: {RunnerStatusCleanupPending}, } // CanTransitionTo checks if a status transition is valid. func (s RunnerStatus) CanTransitionTo(next RunnerStatus) bool { for _, valid := range validRunnerTransitions[s] { if valid == next { return true } } return false } // IsTerminal returns true if the runner is in a terminal state. func (s RunnerStatus) IsTerminal() bool { return s == RunnerStatusCompleted || s == RunnerStatusFailed } // Runner represents an ephemeral builder pod managed by the API. type Runner struct { ID string `json:"id"` User string `json:"user"` RepoURL string `json:"repo_url"` Branch string `json:"branch"` Tools string `json:"tools"` Task string `json:"task"` Status RunnerStatus `json:"status"` ForgejoRunnerID string `json:"forgejo_runner_id,omitempty"` WebhookDeliveryID string `json:"webhook_delivery_id,omitempty"` PodName string `json:"pod_name,omitempty"` CPUReq string `json:"cpu_req"` MemReq string `json:"mem_req"` CreatedAt time.Time `json:"created_at"` ClaimedAt *time.Time `json:"claimed_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"` } // CreateRunnerRequest is the request body for creating a runner. type CreateRunnerRequest struct { User string `json:"user"` Repo string `json:"repo"` Branch string `json:"branch"` Tools string `json:"tools"` Task string `json:"task"` CPUReq string `json:"cpu_req"` MemReq string `json:"mem_req"` WebhookDeliveryID string `json:"webhook_delivery_id"` } // Validate checks that a CreateRunnerRequest has required fields. func (r *CreateRunnerRequest) Validate() error { if r.User == "" { return fmt.Errorf("user is required") } if r.Repo == "" { return fmt.Errorf("repo is required") } if !isValidName(r.User) { return fmt.Errorf("user must be lowercase alphanumeric with hyphens, 1-63 chars") } return nil } // UpdateRunnerStatusRequest is the request body for updating runner status. type UpdateRunnerStatusRequest struct { Status RunnerStatus `json:"status"` ForgejoRunnerID string `json:"forgejo_runner_id,omitempty"` } // Validate checks the status update request. func (r *UpdateRunnerStatusRequest) Validate() error { switch r.Status { case RunnerStatusPodCreating, RunnerStatusRunnerRegistered, RunnerStatusJobClaimed, RunnerStatusCompleted, RunnerStatusFailed, RunnerStatusCleanupPending: return nil default: return fmt.Errorf("invalid status: %s", r.Status) } } // RunnerPodName returns the k8s pod name for a runner. func RunnerPodName(runnerID string) string { return "dev-pod-" + runnerID }