From 2ee025ca7c8d8ec945d54c510fb9d953be913a81 Mon Sep 17 00:00:00 2001 From: dawn Date: Fri, 23 Oct 2026 02:36:08 +0300 Subject: [PATCH] spindle/config: add default monthly ci minutes limits for users and repos Signed-off-by: dawn --- cmd/spindle/main.go | 2 ++ spindle/config/config.go | 37 +++++++++++++++++++++++------------ spindle/config/config_test.go | 34 +++++++++++++++++++++++--------- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/cmd/spindle/main.go b/cmd/spindle/main.go index 15f79fe46..8a517b8db 100644 --- a/cmd/spindle/main.go +++ b/cmd/spindle/main.go @@ -543,6 +543,8 @@ func formatLimit(resource string, limit int64) string { return fmt.Sprintf("%d vCPUs", limit) case quota.ResourceWebhooks: return fmt.Sprintf("%d webhooks", limit) + case quota.ResourceCIMinutesMonthly: + return fmt.Sprintf("%d minutes/month", limit) default: return fmt.Sprintf("%d", limit) } diff --git a/spindle/config/config.go b/spindle/config/config.go index ea85afeaf..279d05865 100644 --- a/spindle/config/config.go +++ b/spindle/config/config.go @@ -179,20 +179,22 @@ type NixCache struct { type Quota struct { User struct { - CacheStorageMiB int64 `env:"CACHE_STORAGE_MIB, default=0"` - Workflows int64 `env:"WORKFLOWS, default=0"` - VCPUs int64 `env:"VCPUS, default=0"` - MemoryMiB int64 `env:"MEMORY_MIB, default=0"` - DiskMiB int64 `env:"DISK_MIB, default=0"` - Webhooks int64 `env:"WEBHOOKS, default=0"` + CacheStorageMiB int64 `env:"CACHE_STORAGE_MIB, default=0"` + Workflows int64 `env:"WORKFLOWS, default=0"` + VCPUs int64 `env:"VCPUS, default=0"` + MemoryMiB int64 `env:"MEMORY_MIB, default=0"` + DiskMiB int64 `env:"DISK_MIB, default=0"` + Webhooks int64 `env:"WEBHOOKS, default=0"` + CIMinutesMonthly int64 `env:"CI_MINUTES_MONTHLY, default=0"` } `env:",prefix=USER_"` Repo struct { - CacheStorageMiB int64 `env:"CACHE_STORAGE_MIB, default=0"` - Workflows int64 `env:"WORKFLOWS, default=0"` - VCPUs int64 `env:"VCPUS, default=0"` - MemoryMiB int64 `env:"MEMORY_MIB, default=0"` - DiskMiB int64 `env:"DISK_MIB, default=0"` - Webhooks int64 `env:"WEBHOOKS, default=0"` + CacheStorageMiB int64 `env:"CACHE_STORAGE_MIB, default=0"` + Workflows int64 `env:"WORKFLOWS, default=0"` + VCPUs int64 `env:"VCPUS, default=0"` + MemoryMiB int64 `env:"MEMORY_MIB, default=0"` + DiskMiB int64 `env:"DISK_MIB, default=0"` + Webhooks int64 `env:"WEBHOOKS, default=0"` + CIMinutesMonthly int64 `env:"CI_MINUTES_MONTHLY, default=0"` } `env:",prefix=REPO_"` } @@ -384,6 +386,13 @@ func (c *Config) validate() error { return fmt.Errorf("repo disk limit cannot be negative, got %d", c.Quota.Repo.DiskMiB) } + if c.Quota.User.CIMinutesMonthly < 0 { + return fmt.Errorf("user monthly ci minutes limit cannot be negative, got %d", c.Quota.User.CIMinutesMonthly) + } + if c.Quota.Repo.CIMinutesMonthly < 0 { + return fmt.Errorf("repo monthly ci minutes limit cannot be negative, got %d", c.Quota.Repo.CIMinutesMonthly) + } + if c.Quota.User.CacheStorageMiB > 8796093022207 { return fmt.Errorf("user cache storage limit %d MiB would overflow bytes", c.Quota.User.CacheStorageMiB) } @@ -422,6 +431,8 @@ func (c *Config) ToDefaults() (quota.Defaults, error) { {scope: "repo", resource: quota.ResourceVCPUs, limit: c.Quota.Repo.VCPUs}, {scope: "repo", resource: quota.ResourceMemoryMiB, limit: c.Quota.Repo.MemoryMiB}, {scope: "repo", resource: quota.ResourceDiskMiB, limit: c.Quota.Repo.DiskMiB}, + {scope: "user", resource: quota.ResourceCIMinutesMonthly, limit: c.Quota.User.CIMinutesMonthly}, + {scope: "repo", resource: quota.ResourceCIMinutesMonthly, limit: c.Quota.Repo.CIMinutesMonthly}, } for _, item := range limits { if item.limit < 0 { @@ -446,6 +457,7 @@ func (c *Config) ToDefaults() (quota.Defaults, error) { quota.ResourceMemoryMiB: c.Quota.User.MemoryMiB, quota.ResourceDiskMiB: c.Quota.User.DiskMiB, quota.ResourceWebhooks: c.Quota.User.Webhooks, + quota.ResourceCIMinutesMonthly: c.Quota.User.CIMinutesMonthly, }, quota.ScopeRepo: quota.Resources{ quota.ResourceCacheStorageBytes: repoCacheBytes, @@ -454,6 +466,7 @@ func (c *Config) ToDefaults() (quota.Defaults, error) { quota.ResourceMemoryMiB: c.Quota.Repo.MemoryMiB, quota.ResourceDiskMiB: c.Quota.Repo.DiskMiB, quota.ResourceWebhooks: c.Quota.Repo.Webhooks, + quota.ResourceCIMinutesMonthly: c.Quota.Repo.CIMinutesMonthly, }, }, nil } diff --git a/spindle/config/config_test.go b/spindle/config/config_test.go index 968d25732..2e2b93bb8 100644 --- a/spindle/config/config_test.go +++ b/spindle/config/config_test.go @@ -215,8 +215,8 @@ func TestLoadTracingConfigValidation(t *testing.T) { func TestLoadQuotaLimitsValidation(t *testing.T) { vars := []string{ - "SPINDLE_QUOTA_USER_CACHE_STORAGE_MIB", "SPINDLE_QUOTA_USER_WORKFLOWS", "SPINDLE_QUOTA_USER_VCPUS", "SPINDLE_QUOTA_USER_MEMORY_MIB", "SPINDLE_QUOTA_USER_DISK_MIB", - "SPINDLE_QUOTA_REPO_CACHE_STORAGE_MIB", "SPINDLE_QUOTA_REPO_WORKFLOWS", "SPINDLE_QUOTA_REPO_VCPUS", "SPINDLE_QUOTA_REPO_MEMORY_MIB", "SPINDLE_QUOTA_REPO_DISK_MIB", + "SPINDLE_QUOTA_USER_CACHE_STORAGE_MIB", "SPINDLE_QUOTA_USER_WORKFLOWS", "SPINDLE_QUOTA_USER_VCPUS", "SPINDLE_QUOTA_USER_MEMORY_MIB", "SPINDLE_QUOTA_USER_DISK_MIB", "SPINDLE_QUOTA_USER_CI_MINUTES_MONTHLY", + "SPINDLE_QUOTA_REPO_CACHE_STORAGE_MIB", "SPINDLE_QUOTA_REPO_WORKFLOWS", "SPINDLE_QUOTA_REPO_VCPUS", "SPINDLE_QUOTA_REPO_MEMORY_MIB", "SPINDLE_QUOTA_REPO_DISK_MIB", "SPINDLE_QUOTA_REPO_CI_MINUTES_MONTHLY", } const overflow = "9223372036854775807" zeros := map[string]string{} @@ -229,14 +229,15 @@ func TestLoadQuotaLimitsValidation(t *testing.T) { }{ "valid defaults": {zeros, false}, "valid positive values": {map[string]string{ - "SPINDLE_QUOTA_USER_CACHE_STORAGE_MIB": "100", - "SPINDLE_QUOTA_USER_WORKFLOWS": "5", - "SPINDLE_QUOTA_USER_VCPUS": "4", - "SPINDLE_QUOTA_USER_MEMORY_MIB": "4096", - "SPINDLE_QUOTA_USER_DISK_MIB": "10240", + "SPINDLE_QUOTA_USER_CACHE_STORAGE_MIB": "100", + "SPINDLE_QUOTA_USER_WORKFLOWS": "5", + "SPINDLE_QUOTA_USER_VCPUS": "4", + "SPINDLE_QUOTA_USER_MEMORY_MIB": "4096", + "SPINDLE_QUOTA_USER_DISK_MIB": "10240", + "SPINDLE_QUOTA_USER_CI_MINUTES_MONTHLY": "3000", }, false}, - "user cache storage overflow": {map[string]string{vars[0]: overflow}, true}, - "repo cache storage overflow": {map[string]string{vars[5]: overflow}, true}, + "user cache storage overflow": {map[string]string{"SPINDLE_QUOTA_USER_CACHE_STORAGE_MIB": overflow}, true}, + "repo cache storage overflow": {map[string]string{"SPINDLE_QUOTA_REPO_CACHE_STORAGE_MIB": overflow}, true}, } for _, v := range vars { cases["negative "+v] = struct { @@ -302,6 +303,21 @@ func TestLoadQuotaDefaultsDoesNotRequireServerConfiguration(t *testing.T) { } } +func TestLoadQuotaDefaultsCarriesMonthlyCIMinutes(t *testing.T) { + t.Setenv("SPINDLE_QUOTA_USER_CI_MINUTES_MONTHLY", "3000") + t.Setenv("SPINDLE_QUOTA_REPO_CI_MINUTES_MONTHLY", "500") + defaults, err := LoadQuotaDefaults(context.Background()) + if err != nil { + t.Fatal(err) + } + if got := defaults[quota.ScopeUser][quota.ResourceCIMinutesMonthly]; got != 3000 { + t.Fatalf("user monthly ci minutes = %d, want 3000", got) + } + if got := defaults[quota.ScopeRepo][quota.ResourceCIMinutesMonthly]; got != 500 { + t.Fatalf("repo monthly ci minutes = %d, want 500", got) + } +} + func TestLoadQuotaDefaultsRejectsNegativeLimits(t *testing.T) { t.Setenv("SPINDLE_QUOTA_REPO_VCPUS", "-1") if _, err := LoadQuotaDefaults(context.Background()); err == nil { -- 2.54.0