2023-06-03 08:41:16 +00:00
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgejo_migrations //nolint:revive
import (
"context"
"fmt"
"os"
2023-08-08 21:55:25 +00:00
"code.gitea.io/gitea/models/forgejo/semver"
2023-08-08 21:52:37 +00:00
forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20"
2023-12-20 20:44:55 +00:00
forgejo_v1_22 "code.gitea.io/gitea/models/forgejo_migrations/v1_22"
2023-06-03 08:41:16 +00:00
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm"
"xorm.io/xorm/names"
)
// ForgejoVersion describes the Forgejo version table. Should have only one row with id = 1.
type ForgejoVersion struct {
ID int64 ` xorm:"pk autoincr" `
Version int64
}
type Migration struct {
description string
migrate func ( * xorm . Engine ) error
}
// NewMigration creates a new migration.
func NewMigration ( desc string , fn func ( * xorm . Engine ) error ) * Migration {
return & Migration { desc , fn }
}
// This is a sequence of additional Forgejo migrations.
// Add new migrations to the bottom of the list.
2023-08-08 21:52:37 +00:00
var migrations = [ ] * Migration {
2023-08-14 23:06:13 +00:00
// v0 -> v1
2024-03-27 12:55:28 +00:00
NewMigration ( "Create the `forgejo_blocked_user` table" , forgejo_v1_20 . AddForgejoBlockedUser ) ,
2023-08-08 21:52:37 +00:00
// v1 -> v2
2024-03-27 12:55:28 +00:00
NewMigration ( "Create the `forgejo_sem_ver` table" , forgejo_v1_20 . CreateSemVerTable ) ,
2023-11-22 16:26:21 +00:00
// v2 -> v3
2024-03-27 12:55:28 +00:00
NewMigration ( "Create the `forgejo_auth_token` table" , forgejo_v1_20 . CreateAuthorizationTokenTable ) ,
2023-12-20 20:44:55 +00:00
// v3 -> v4
2024-03-27 12:55:28 +00:00
NewMigration ( "Add the `default_permissions` column to the `repo_unit` table" , forgejo_v1_22 . AddDefaultPermissionsToRepoUnit ) ,
[FEAT] Repository flags
This implements "repository flags", a way for instance administrators to
assign custom flags to repositories. The idea is that custom templates
can look at these flags, and display banners based on them, Forgejo does
not provide anything built on top of it, just the foundation. The
feature is optional, and disabled by default. To enable it, set
`[repository].ENABLE_FLAGS = true`.
On the UI side, instance administrators will see a new "Manage flags"
tab on repositories, and a list of enabled tags (if any) on the
repository home page. The "Manage flags" page allows them to remove
existing flags, or add any new ones that are listed in
`[repository].SETTABLE_FLAGS`.
The model does not enforce that only the `SETTABLE_FLAGS` are present.
If the setting is changed, old flags may remain present in the database,
and anything that uses them, will still work. The repository flag
management page will allow an instance administrator to remove them, but
not set them, once removed.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit ba735ce2228f8dd7ca105e94b9baa1be058ebe37)
(cherry picked from commit f09f6e029b4fb2714b86cd32dc19255078ecc0ee)
(cherry picked from commit 2f8b0414892f6099f519bda63a9e0fbc8ba6cfc7)
(cherry picked from commit d3186ee5f41fac896c7d2341402fcd39dd250bf1)
2024-01-04 13:28:19 +00:00
// v4 -> v5
2024-03-27 12:55:28 +00:00
NewMigration ( "Create the `forgejo_repo_flag` table" , forgejo_v1_22 . CreateRepoFlagTable ) ,
[GITEA] Allow changing the repo Wiki branch to main
Previously, the repo wiki was hardcoded to use `master` as its branch,
this change makes it possible to use `main` (or something else, governed
by `[repository].DEFAULT_BRANCH`, a setting that already exists and
defaults to `main`).
The way it is done is that a new column is added to the `repository`
table: `wiki_branch`. The migration will make existing repositories
default to `master`, for compatibility's sake, even if they don't have a
Wiki (because it's easier to do that). Newly created repositories will
default to `[repository].DEFAULT_BRANCH` instead.
The Wiki service was updated to use the branch name stored in the
database, and fall back to the default if it is empty.
Old repositories with Wikis using the older `master` branch will have
the option to do a one-time transition to `main`, available via the
repository settings in the "Danger Zone". This option will only be
available for repositories that have the internal wiki enabled, it is
not empty, and the wiki branch is not `[repository].DEFAULT_BRANCH`.
When migrating a repository with a Wiki, Forgejo will use the same
branch name for the wiki as the source repository did. If that's not the
same as the default, the option to normalize it will be available after
the migration's done.
Additionally, the `/api/v1/{owner}/{repo}` endpoint was updated: it will
now include the wiki branch name in `GET` requests, and allow changing
the wiki branch via `PATCH`.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit d87c526d2a313fa45093ab49b78bb30322b33298)
2024-01-30 11:18:53 +00:00
// v5 -> v6
2024-03-27 12:55:28 +00:00
NewMigration ( "Add the `wiki_branch` column to the `repository` table" , forgejo_v1_22 . AddWikiBranchToRepository ) ,
2024-03-01 12:22:40 +00:00
// v6 -> v7
2024-03-27 12:55:28 +00:00
NewMigration ( "Add the `enable_repo_unit_hints` column to the `user` table" , forgejo_v1_22 . AddUserRepoUnitHintsSetting ) ,
2024-02-29 08:14:50 +00:00
// v7 -> v8
2024-03-27 12:55:28 +00:00
NewMigration ( "Modify the `release`.`note` content to remove SSH signatures" , forgejo_v1_22 . RemoveSSHSignaturesFromReleaseNotes ) ,
2024-03-28 20:41:52 +00:00
// v8 -> v9
NewMigration ( "Add the `apply_to_admins` column to the `protected_branch` table" , forgejo_v1_22 . AddApplyToAdminsSetting ) ,
2024-02-23 22:33:02 +00:00
// v9 -> v10
NewMigration ( "Add pronouns to user" , forgejo_v1_22 . AddPronounsToUser ) ,
2024-04-04 08:40:25 +00:00
// v11 -> v12
2024-03-31 13:27:59 +00:00
NewMigration ( "Add the `created` column to the `issue` table" , forgejo_v1_22 . AddCreatedToIssue ) ,
2024-04-11 09:13:28 +00:00
// v12 -> v13
2024-04-04 08:40:25 +00:00
NewMigration ( "Add repo_archive_download_count table" , forgejo_v1_22 . AddRepoArchiveDownloadCount ) ,
2024-04-24 15:15:55 +00:00
// v13 -> v14
NewMigration ( "Add `hide_archive_links` column to `release` table" , AddHideArchiveLinksToRelease ) ,
Drop Gitea-specific columns from two tables
Gitea and Forgejo chose to implement wiki branch naming differently, but
Forgejo picked the Gitea migration anyway, resulting in an unused column
in the database, which wasn't part of the `Repository` struct either -
something warned about during startup, too.
Similarly, Forgejo chose not to implement User badges at all - but kept
the existing code for it -, and the `badge` table ended up with an
unused `slug` column due to a Gitea migration, and resulted in another
warning at startup.
To keep the database consistent with the code, and to get rid of these
warnings, lets introduce a new migration, which simply drops these
Gitea-specific columns from the database.
Fixes #3463.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
2024-04-26 07:53:00 +00:00
// v14 -> v15
NewMigration ( "Remove Gitea-specific columns from the repository and badge tables" , RemoveGiteaSpecificColumnsFromRepositoryAndBadge ) ,
2024-05-14 06:24:31 +00:00
// v15 -> v16
NewMigration ( "Create the `federation_host` table" , CreateFederationHostTable ) ,
2024-05-16 16:25:16 +00:00
// v16 -> v17
NewMigration ( "Create the `federated_user` table" , CreateFederatedUserTable ) ,
// v17 -> v18
NewMigration ( "Add `normalized_federated_uri` column to `user` table" , AddNormalizedFederatedURIToUser ) ,
2024-05-29 16:31:06 +00:00
// v18 -> v19
NewMigration ( "Create the `following_repo` table" , CreateFollowingRepoTable ) ,
2023-09-15 16:20:16 +00:00
// v19 -> v20
NewMigration ( "Add external_url to attachment table" , AddExternalURLColumnToAttachmentTable ) ,
feat(quota): Humble beginnings of a quota engine
This is an implementation of a quota engine, and the API routes to
manage its settings. This does *not* contain any enforcement code: this
is just the bedrock, the engine itself.
The goal of the engine is to be flexible and future proof: to be nimble
enough to build on it further, without having to rewrite large parts of
it.
It might feel a little more complicated than necessary, because the goal
was to be able to support scenarios only very few Forgejo instances
need, scenarios the vast majority of mostly smaller instances simply do
not care about. The goal is to support both big and small, and for that,
we need a solid, flexible foundation.
There are thee big parts to the engine: counting quota use, setting
limits, and evaluating whether the usage is within the limits. Sounds
simple on paper, less so in practice!
Quota counting
==============
Quota is counted based on repo ownership, whenever possible, because
repo owners are in ultimate control over the resources they use: they
can delete repos, attachments, everything, even if they don't *own*
those themselves. They can clean up, and will always have the permission
and access required to do so. Would we count quota based on the owning
user, that could lead to situations where a user is unable to free up
space, because they uploaded a big attachment to a repo that has been
taken private since. It's both more fair, and much safer to count quota
against repo owners.
This means that if user A uploads an attachment to an issue opened
against organization O, that will count towards the quota of
organization O, rather than user A.
One's quota usage stats can be queried using the `/user/quota` API
endpoint. To figure out what's eating into it, the
`/user/repos?order_by=size`, `/user/quota/attachments`,
`/user/quota/artifacts`, and `/user/quota/packages` endpoints should be
consulted. There's also `/user/quota/check?subject=<...>` to check
whether the signed-in user is within a particular quota limit.
Quotas are counted based on sizes stored in the database.
Setting quota limits
====================
There are different "subjects" one can limit usage for. At this time,
only size-based limits are implemented, which are:
- `size:all`: As the name would imply, the total size of everything
Forgejo tracks.
- `size:repos:all`: The total size of all repositories (not including
LFS).
- `size:repos:public`: The total size of all public repositories (not
including LFS).
- `size:repos:private`: The total size of all private repositories (not
including LFS).
- `size:git:all`: The total size of all git data (including all
repositories, and LFS).
- `size:git:lfs`: The size of all git LFS data (either in private or
public repos).
- `size:assets:all`: The size of all assets tracked by Forgejo.
- `size:assets:attachments:all`: The size of all kinds of attachments
tracked by Forgejo.
- `size:assets:attachments:issues`: Size of all attachments attached to
issues, including issue comments.
- `size:assets:attachments:releases`: Size of all attachments attached
to releases. This does *not* include automatically generated archives.
- `size:assets:artifacts`: Size of all Action artifacts.
- `size:assets:packages:all`: Size of all Packages.
- `size:wiki`: Wiki size
Wiki size is currently not tracked, and the engine will always deem it
within quota.
These subjects are built into Rules, which set a limit on *all* subjects
within a rule. Thus, we can create a rule that says: "1Gb limit on all
release assets, all packages, and git LFS, combined". For a rule to
stand, the total sum of all subjects must be below the rule's limit.
Rules are in turn collected into groups. A group is just a name, and a
list of rules. For a group to stand, all of its rules must stand. Thus,
if we have a group with two rules, one that sets a combined 1Gb limit on
release assets, all packages, and git LFS, and another rule that sets a
256Mb limit on packages, if the user has 512Mb of packages, the group
will not stand, because the second rule deems it over quota. Similarly,
if the user has only 128Mb of packages, but 900Mb of release assets, the
group will not stand, because the combined size of packages and release
assets is over the 1Gb limit of the first rule.
Groups themselves are collected into Group Lists. A group list stands
when *any* of the groups within stand. This allows an administrator to
set conservative defaults, but then place select users into additional
groups that increase some aspect of their limits.
To top it off, it is possible to set the default quota groups a user
belongs to in `app.ini`. If there's no explicit assignment, the engine
will use the default groups. This makes it possible to avoid having to
assign each and every user a list of quota groups, and only those need
to be explicitly assigned who need a different set of groups than the
defaults.
If a user has any quota groups assigned to them, the default list will
not be considered for them.
The management APIs
===================
This commit contains the engine itself, its unit tests, and the quota
management APIs. It does not contain any enforcement.
The APIs are documented in-code, and in the swagger docs, and the
integration tests can serve as an example on how to use them.
Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
2024-07-06 08:25:41 +00:00
// v20 -> v21
NewMigration ( "Creating Quota-related tables" , CreateQuotaTables ) ,
2023-08-08 21:52:37 +00:00
}
2023-06-03 08:41:16 +00:00
// GetCurrentDBVersion returns the current Forgejo database version.
func GetCurrentDBVersion ( x * xorm . Engine ) ( int64 , error ) {
if err := x . Sync ( new ( ForgejoVersion ) ) ; err != nil {
return - 1 , fmt . Errorf ( "sync: %w" , err )
}
currentVersion := & ForgejoVersion { ID : 1 }
has , err := x . Get ( currentVersion )
if err != nil {
return - 1 , fmt . Errorf ( "get: %w" , err )
}
if ! has {
return - 1 , nil
}
return currentVersion . Version , nil
}
// ExpectedVersion returns the expected Forgejo database version.
func ExpectedVersion ( ) int64 {
return int64 ( len ( migrations ) )
}
// EnsureUpToDate will check if the Forgejo database is at the correct version.
func EnsureUpToDate ( x * xorm . Engine ) error {
currentDB , err := GetCurrentDBVersion ( x )
if err != nil {
return err
}
if currentDB < 0 {
return fmt . Errorf ( "database has not been initialized" )
}
expected := ExpectedVersion ( )
if currentDB != expected {
return fmt . Errorf ( ` current Forgejo database version %d is not equal to the expected version %d. Please run "forgejo [--config /path/to/app.ini] migrate" to update the database version ` , currentDB , expected )
}
return nil
}
// Migrate Forgejo database to current version.
func Migrate ( x * xorm . Engine ) error {
// Set a new clean the default mapper to GonicMapper as that is the default for .
x . SetMapper ( names . GonicMapper { } )
if err := x . Sync ( new ( ForgejoVersion ) ) ; err != nil {
return fmt . Errorf ( "sync: %w" , err )
}
currentVersion := & ForgejoVersion { ID : 1 }
has , err := x . Get ( currentVersion )
if err != nil {
return fmt . Errorf ( "get: %w" , err )
} else if ! has {
// If the version record does not exist we think
// it is a fresh installation and we can skip all migrations.
currentVersion . ID = 0
currentVersion . Version = ExpectedVersion ( )
if _ , err = x . InsertOne ( currentVersion ) ; err != nil {
return fmt . Errorf ( "insert: %w" , err )
}
}
v := currentVersion . Version
// Downgrading Forgejo's database version not supported
if v > ExpectedVersion ( ) {
msg := fmt . Sprintf ( "Your Forgejo database (migration version: %d) is for a newer version of Forgejo, you cannot use the newer database for this old Forgejo release (%d)." , v , ExpectedVersion ( ) )
msg += "\nForgejo will exit to keep your database safe and unchanged. Please use the correct Forgejo release, do not change the migration version manually (incorrect manual operation may cause data loss)."
if ! setting . IsProd {
msg += fmt . Sprintf ( "\nIf you are in development and really know what you're doing, you can force changing the migration version by executing: UPDATE forgejo_version SET version=%d WHERE id=1;" , ExpectedVersion ( ) )
}
_ , _ = fmt . Fprintln ( os . Stderr , msg )
log . Fatal ( msg )
return nil
}
// Some migration tasks depend on the git command
if git . DefaultContext == nil {
if err = git . InitSimple ( context . Background ( ) ) ; err != nil {
return err
}
}
// Migrate
for i , m := range migrations [ v : ] {
log . Info ( "Migration[%d]: %s" , v + int64 ( i ) , m . description )
// Reset the mapper between each migration - migrations are not supposed to depend on each other
x . SetMapper ( names . GonicMapper { } )
if err = m . migrate ( x ) ; err != nil {
return fmt . Errorf ( "migration[%d]: %s failed: %w" , v + int64 ( i ) , m . description , err )
}
currentVersion . Version = v + int64 ( i ) + 1
if _ , err = x . ID ( 1 ) . Update ( currentVersion ) ; err != nil {
return err
}
}
2023-08-08 21:55:25 +00:00
if err := x . Sync ( new ( semver . ForgejoSemVer ) ) ; err != nil {
return fmt . Errorf ( "sync: %w" , err )
}
return semver . SetVersionStringWithEngine ( x , setting . ForgejoVersion )
2023-06-03 08:41:16 +00:00
}