diff --git a/docker-compose.yml b/docker-compose.yml index 9412bea00..f75df2483 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -317,11 +317,13 @@ services: SPINDLE_QUOTA_USER_VCPUS: 0 SPINDLE_QUOTA_USER_MEMORY_MIB: 0 SPINDLE_QUOTA_USER_DISK_MIB: 0 + SPINDLE_QUOTA_USER_CI_MINUTES_MONTHLY: 0 SPINDLE_QUOTA_REPO_CACHE_STORAGE_MIB: 0 SPINDLE_QUOTA_REPO_WORKFLOWS: 0 SPINDLE_QUOTA_REPO_VCPUS: 0 SPINDLE_QUOTA_REPO_MEMORY_MIB: 0 SPINDLE_QUOTA_REPO_DISK_MIB: 0 + SPINDLE_QUOTA_REPO_CI_MINUTES_MONTHLY: 0 SPINDLE_MICROVM_PIPELINES_DEBUG_SSH_ENABLED: true SPINDLE_MICROVM_PIPELINES_DEBUG_SSH_LISTEN_ADDR: 0.0.0.0:2223 SPINDLE_MICROVM_PIPELINES_DEBUG_SSH_GRACE_PERIOD: 10m diff --git a/docs/src/content/docs/reference/spindle-config.md b/docs/src/content/docs/reference/spindle-config.md index 7ebd6040f..7dd607699 100644 --- a/docs/src/content/docs/reference/spindle-config.md +++ b/docs/src/content/docs/reference/spindle-config.md @@ -208,6 +208,13 @@ them per DID with `org.tangled.temp.spindle.quota.*` or `spindle quota`. | `SPINDLE_QUOTA_{USER,REPO}_DISK_MIB` | `0` | Concurrent disk. | | `SPINDLE_QUOTA_{USER,REPO}_CACHE_STORAGE_MIB` | `0` | CI cache storage. | | `SPINDLE_QUOTA_{USER,REPO}_WEBHOOKS` | `0` | Webhooks. | +| `SPINDLE_QUOTA_{USER,REPO}_CI_MINUTES_MONTHLY` | `0` | CI minutes per calendar month (UTC), see below. | + +`CI_MINUTES_MONTHLY` is consumed rather than held: each finished workflow +adds its wall-clock minutes (rounded up, from when it starts running, not +from when it queues) to the owner and the repo, and a workflow that starts +once either is used up fails with `quota_denied`. Running workflows are +never stopped. The per-DID override resource is `ci_minutes_monthly`. ## OIDC diff --git a/docs/src/content/docs/self-hosting/spindle.md b/docs/src/content/docs/self-hosting/spindle.md index 999756ac6..2c1da1b2f 100644 --- a/docs/src/content/docs/self-hosting/spindle.md +++ b/docs/src/content/docs/self-hosting/spindle.md @@ -250,6 +250,8 @@ during lookup or cleanup. `SPINDLE_QUOTA_USER_*` and `SPINDLE_QUOTA_REPO_*` cap what one owner or one repository can use at a time: `WORKFLOWS`, `VCPUS`, `MEMORY_MIB`, `DISK_MIB`, `CACHE_STORAGE_MIB`, and `WEBHOOKS`. `0` means unlimited. +`CI_MINUTES_MONTHLY` is different: it caps total workflow minutes per +calendar month (UTC) instead of what runs at once. Override them per DID at runtime: ```sh diff --git a/docs/src/content/docs/spindles/architecture.mdx b/docs/src/content/docs/spindles/architecture.mdx index b654e1864..89d2dd66e 100644 --- a/docs/src/content/docs/spindles/architecture.mdx +++ b/docs/src/content/docs/spindles/architecture.mdx @@ -141,7 +141,7 @@ There are two separate caches: ## Quotas, bans, webhooks and OIDC - **Quotas** per user and per repo (`SPINDLE_QUOTA_{USER,REPO}_*`: - workflows, vCPUs, memory, disk, cache storage, webhooks; 0 = + workflows, vCPUs, memory, disk, cache storage, webhooks, monthly CI minutes; 0 = unlimited). Override at runtime with `spindle quota` or `org.tangled.temp.spindle.quota.*`. - **Bans** with `spindle ban`, `unban` and `bans`. A ban wipes the diff --git a/localinfra/pc/services/spindle/run b/localinfra/pc/services/spindle/run index 0a4237b2e..ae01bf78c 100755 --- a/localinfra/pc/services/spindle/run +++ b/localinfra/pc/services/spindle/run @@ -23,7 +23,7 @@ export SSL_CERT_FILE=$TNGL_CERTS/root.crt # 0 is the off switch for these quotas, compose does the same for scope in USER REPO; do - for resource in CACHE_STORAGE_MIB WORKFLOWS VCPUS MEMORY_MIB DISK_MIB; do + for resource in CACHE_STORAGE_MIB WORKFLOWS VCPUS MEMORY_MIB DISK_MIB CI_MINUTES_MONTHLY; do export "SPINDLE_QUOTA_${scope}_${resource}=0" done done diff --git a/nix/modules/spindle.nix b/nix/modules/spindle.nix index 1345c4154..8261d2af0 100644 --- a/nix/modules/spindle.nix +++ b/nix/modules/spindle.nix @@ -443,6 +443,11 @@ in default = 0; description = "Default disk limit for users in MiB. Zero is unlimited."; }; + ciMinutesMonthly = mkOption { + type = types.int; + default = 0; + description = "Default CI minutes per calendar month (UTC) for users. Zero is unlimited."; + }; }; repo = { cacheStorageMiB = mkOption { @@ -470,6 +475,11 @@ in default = 0; description = "Default disk limit for repositories in MiB. Zero is unlimited."; }; + ciMinutesMonthly = mkOption { + type = types.int; + default = 0; + description = "Default CI minutes per calendar month (UTC) for repositories. Zero is unlimited."; + }; }; }; @@ -788,11 +798,13 @@ in "SPINDLE_QUOTA_USER_VCPUS=${toString instance.quota.user.vCPUs}" "SPINDLE_QUOTA_USER_MEMORY_MIB=${toString instance.quota.user.memoryMiB}" "SPINDLE_QUOTA_USER_DISK_MIB=${toString instance.quota.user.diskMiB}" + "SPINDLE_QUOTA_USER_CI_MINUTES_MONTHLY=${toString instance.quota.user.ciMinutesMonthly}" "SPINDLE_QUOTA_REPO_CACHE_STORAGE_MIB=${toString instance.quota.repo.cacheStorageMiB}" "SPINDLE_QUOTA_REPO_WORKFLOWS=${toString instance.quota.repo.workflows}" "SPINDLE_QUOTA_REPO_VCPUS=${toString instance.quota.repo.vCPUs}" "SPINDLE_QUOTA_REPO_MEMORY_MIB=${toString instance.quota.repo.memoryMiB}" "SPINDLE_QUOTA_REPO_DISK_MIB=${toString instance.quota.repo.diskMiB}" + "SPINDLE_QUOTA_REPO_CI_MINUTES_MONTHLY=${toString instance.quota.repo.ciMinutesMonthly}" "SPINDLE_ARTIFACT_STORES_DISK_DIR=${instance.artifactStores.disk.dir}" "SPINDLE_ARTIFACT_STORES_S3_BUCKET=${instance.artifactStores.s3.bucket}" "SPINDLE_ARTIFACT_STORES_S3_REGION=${instance.artifactStores.s3.region}"