diff --git a/common/admins.nix b/common/admins.nix index 5aa2df9..1b55cc8 100644 --- a/common/admins.nix +++ b/common/admins.nix @@ -9,5 +9,6 @@ in { keys.users.jade ++ keys.users.janik ++ keys.users.lukegb ++ + keys.users.emilylange ++ keys.users.yuka; } diff --git a/common/ssh-keys.nix b/common/ssh-keys.nix index a151aea..eb1afb8 100644 --- a/common/ssh-keys.nix +++ b/common/ssh-keys.nix @@ -4,6 +4,7 @@ meta01 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM5t9gYorOWgpCFDJgb24pyCKIabGpeI2H/UfdvXODcT"; gerrit01 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA+eSZu+u9sCynrMlsmFzQHLIELQAuVg0Cs1pBvwb4+A"; fodwatch = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFRyTNfvKl5FcSyzGzw+h+bNFNOxdhvI67WdUZ2iIJ1L"; + git = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEQJcpkCUOx8+5oukMX6lxrYcIX8FyHu8Mc/3+ieKMUn"; builder-0 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBHSNcDGctvlG6BHcJuYIzW9WsBJsts2vpwSketsbXoL"; builder-1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQOGUjERK7Mx8UPM/rbOdMqVyn1sbWqYOG6CbOzH2wm"; builder-2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMKzXIqCoYElEKIYgjbSpqEcDeOvV+Wo3Agq3jba83cB"; @@ -40,6 +41,7 @@ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBLZxVITpJ8xbiCa/u2gjSSIupeiqOnRh+8tFIoVhCON" ]; lukegb = [ ''cert-authority,principals="lukegb" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEqNOwlR7Qa8cbGpDfSCOweDPbAGQOZIcoRgh6s/J8DR'' ]; + emilylange = [ "no-touch-required sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIL7jgq3i+N3gVJhs4shm7Kmw6dIocs2OuR0GBMG1RxfKAAAABHNzaDo=" ]; yuka = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKath4/fDnlv/4fzxkPrQN1ttmoPRNu/m9bEtdPJBDfY cardno:16_933_242" ]; }; } diff --git a/flake.nix b/flake.nix index 5d7281e..84f7cff 100644 --- a/flake.nix +++ b/flake.nix @@ -99,6 +99,7 @@ meta01.imports = commonModules ++ [ ./hosts/meta01 ]; gerrit01.imports = commonModules ++ [ ./hosts/gerrit01 ]; fodwatch.imports = commonModules ++ [ ./hosts/fodwatch ]; + git.imports = commonModules ++ [ ./hosts/git ]; wob-vpn-gw.imports = commonModules ++ [ ./hosts/wob-vpn-gw ]; } // builders; diff --git a/hosts/git/default.nix b/hosts/git/default.nix new file mode 100644 index 0000000..7d3383c --- /dev/null +++ b/hosts/git/default.nix @@ -0,0 +1,43 @@ +let + ipv6 = { + openssh ="2001:bc8:38ee:100:1000::41"; + forgejo = "2001:bc8:38ee:100:1000::40"; + }; +in +{ + networking.hostName = "git"; + networking.domain = "infra.forkos.org"; + + time.timeZone = "Europe/Paris"; + + bagel.sysadmin.enable = true; + # Forgejo will be proxied. + bagel.raito.v6-proxy-awareness.enable = true; + bagel.hardware.raito-vm = { + enable = true; + networking = { + nat-lan-mac = "BC:24:11:83:71:56"; + wan = { + address = "${ipv6.forgejo}/64"; + mac = "BC:24:11:0B:8A:81"; + }; + }; + }; + + # Add one additional IPv6, so we can have both OpenSSH and + # Forgejo's built-in server bind on port :22. + systemd.network.networks."10-wan".networkConfig.Address = [ "${ipv6.openssh}/64" ]; + services.openssh.listenAddresses = [{ + addr = "[${ipv6.openssh}]"; + }]; + + bagel.services.forgejo = { + enable = true; + sshBindAddr = ipv6.forgejo; + }; + + i18n.defaultLocale = "en_US.UTF-8"; + + system.stateVersion = "24.05"; + deployment.targetHost = "git.infra.forkos.org"; +} diff --git a/pkgs/forgejo/branch-view_remove-expensive-commit-divergence-metric.patch b/pkgs/forgejo/branch-view_remove-expensive-commit-divergence-metric.patch new file mode 100644 index 0000000..c4bb4c2 --- /dev/null +++ b/pkgs/forgejo/branch-view_remove-expensive-commit-divergence-metric.patch @@ -0,0 +1,59 @@ +diff --git a/services/repository/branch.go b/services/repository/branch.go +index e1a313749f..5a8d823eef 100644 +--- a/services/repository/branch.go ++++ b/services/repository/branch.go +@@ -26,7 +26,6 @@ import ( + "code.gitea.io/gitea/modules/timeutil" + webhook_module "code.gitea.io/gitea/modules/webhook" + notify_service "code.gitea.io/gitea/services/notify" +- files_service "code.gitea.io/gitea/services/repository/files" + + "xorm.io/builder" + ) +@@ -129,21 +128,7 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g + p := protectedBranches.GetFirstMatched(branchName) + isProtected := p != nil + +- var divergence *git.DivergeObject +- +- // it's not default branch +- if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted { +- var err error +- divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName) +- if err != nil { +- return nil, fmt.Errorf("CountDivergingCommits: %v", err) +- } +- } +- +- if divergence == nil { +- // tolerate the error that we cannot get divergence +- divergence = &git.DivergeObject{Ahead: -1, Behind: -1} +- } ++ divergence := &git.DivergeObject{Ahead: -1, Behind: -1} + + pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx, repo.ID, branchName) + if err != nil { +diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl +index a577fed450..e102796315 100644 +--- a/templates/repo/branch/list.tmpl ++++ b/templates/repo/branch/list.tmpl +@@ -102,19 +102,6 @@ + {{end}} + + +- {{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}} +-
+-
+-
{{.CommitsBehind}}
+- {{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}} +-
+-
+-
+-
{{.CommitsAhead}}
+-
+-
+-
+- {{end}} + + + {{if not .LatestPullRequest}} diff --git a/pkgs/forgejo/commit-view_fix-broken-and-expensive-cherry-pick-default-branch-selection.patch b/pkgs/forgejo/commit-view_fix-broken-and-expensive-cherry-pick-default-branch-selection.patch new file mode 100644 index 0000000..ae7e60f --- /dev/null +++ b/pkgs/forgejo/commit-view_fix-broken-and-expensive-cherry-pick-default-branch-selection.patch @@ -0,0 +1,32 @@ +diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go +index 718454e063..8fa299710c 100644 +--- a/routers/web/repo/commit.go ++++ b/routers/web/repo/commit.go +@@ -408,12 +408,6 @@ func Diff(ctx *context.Context) { + } + } + +- ctx.Data["BranchName"], err = commit.GetBranchName() +- if err != nil { +- ctx.ServerError("commit.GetBranchName", err) +- return +- } +- + ctx.HTML(http.StatusOK, tplCommitPage) + } + +diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl +index c37fb46975..18c9cf18f8 100644 +--- a/templates/repo/commit_page.tmpl ++++ b/templates/repo/commit_page.tmpl +@@ -71,8 +71,8 @@ + "branchForm" "branch-dropdown-form" + "branchURLPrefix" (printf "%s/_cherrypick/%s/" $.RepoLink .CommitID) "branchURLSuffix" "" + "setAction" true "submitForm" true}} +-
+- ++ ++ + +
+ diff --git a/pkgs/forgejo/default.nix b/pkgs/forgejo/default.nix new file mode 100644 index 0000000..bb87f3e --- /dev/null +++ b/pkgs/forgejo/default.nix @@ -0,0 +1,40 @@ +{ forgejo }: + +forgejo.overrideAttrs (prev: { + patches = [ + # Branch divergence calculations for a single branch may take 100-200ms on something as big + # as nixpkgs. The branch view defaults to 20 branches for each page, taking roughtly 3s to + # calculate each branch sequentially and render, while consuming a single core at 100%. + # The idea is to look into making this less expensive or async. + # But for now, to get this going, we will simply drop that metric. + ./branch-view_remove-expensive-commit-divergence-metric.patch + + # This is literally broken and eats resources for nothing of value. + # We should upstream this. + # The tl;dr is: It calculates the nearest branch for the requested commit at + # /:owner/:repo/commit/:commit to use it as the default cherry-pick target branch + # selection in a drop-down only users with commit perms can actually view and use. + # It's expensive to calculate and happens on every request to /commit/:commit. + # To add insult to injury, it's hardly of any use: The nearest branch of a commit + # will almost always be a branch that already carries the commit. The branch you + # most likely don't want to cherry-pick to. + ./commit-view_fix-broken-and-expensive-cherry-pick-default-branch-selection.patch + + # Disable various /:owner/:repo/activity/ sub-views. They are expensive, which is + # totally fine and expected. There is even proper caching in place. + # However, on a scale of nixpkgs, those calculations take ages, while, of course, + # pinning a single CPU core at 100%. + # For now, we will simply disable this feature. + # Due to the 501 status code it returns, the frontend prints a "Not implemented" + # error, saving us from patching the frontend while still providing a helpful + # user-facing error text. + # It should be noted that this particular status code has the downside of being + # in the 5xx range, meaning it will show up as such in our prometheus metrics. + ./disable-expensive-repository-activity-stats.patch + + # Migrations and pull-mirrors are something easily abused to bring a public instance to a complete halt. + # Both features can be disabled via repository.DISABLE_MIGRATIONS and mirror.ENABLE, but we want to keep + # this functionality for admins. + ./limit-migrations-and-pull-mirrors-to-admins.patch + ]; +}) diff --git a/pkgs/forgejo/disable-expensive-repository-activity-stats.patch b/pkgs/forgejo/disable-expensive-repository-activity-stats.patch new file mode 100644 index 0000000..555007c --- /dev/null +++ b/pkgs/forgejo/disable-expensive-repository-activity-stats.patch @@ -0,0 +1,34 @@ +diff --git a/routers/web/web.go b/routers/web/web.go +index ee9694f41c..f55b8d6f62 100644 +--- a/routers/web/web.go ++++ b/routers/web/web.go +@@ -57,6 +57,10 @@ import ( + "github.com/prometheus/client_golang/prometheus" + ) + ++func endpointNotImplemented(ctx *context.Context) { ++ ctx.JSON(http.StatusNotImplemented, "This endpoint has been removed due to performance issues with it and as such is not longer implemented.") ++} ++ + // optionsCorsHandler return a http handler which sets CORS options if enabled by config, it blocks non-CORS OPTIONS requests. + func optionsCorsHandler() func(next http.Handler) http.Handler { + var corsHandler func(next http.Handler) http.Handler +@@ -1425,15 +1429,15 @@ func registerRoutes(m *web.Route) { + m.Get("/{period}", repo.Activity) + m.Group("/contributors", func() { + m.Get("", repo.Contributors) +- m.Get("/data", repo.ContributorsData) ++ m.Get("/data", endpointNotImplemented) + }, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode)) + m.Group("/code-frequency", func() { + m.Get("", repo.CodeFrequency) +- m.Get("/data", repo.CodeFrequencyData) ++ m.Get("/data", endpointNotImplemented) + }, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode)) + m.Group("/recent-commits", func() { + m.Get("", repo.RecentCommits) +- m.Get("/data", repo.RecentCommitsData) ++ m.Get("/data", endpointNotImplemented) + }, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode)) + }, context.RepoRef(), context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases)) + diff --git a/pkgs/forgejo/limit-migrations-and-pull-mirrors-to-admins.patch b/pkgs/forgejo/limit-migrations-and-pull-mirrors-to-admins.patch new file mode 100644 index 0000000..9221e69 --- /dev/null +++ b/pkgs/forgejo/limit-migrations-and-pull-mirrors-to-admins.patch @@ -0,0 +1,53 @@ +diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go +index 2caaa130e8..455e89e93e 100644 +--- a/routers/api/v1/repo/migrate.go ++++ b/routers/api/v1/repo/migrate.go +@@ -12,7 +12,6 @@ import ( + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" +- "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" +@@ -86,22 +85,7 @@ func Migrate(ctx *context.APIContext) { + } + + if !ctx.Doer.IsAdmin { +- if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID { +- ctx.Error(http.StatusForbidden, "", "Given user is not an organization.") +- return +- } +- +- if repoOwner.IsOrganization() { +- // Check ownership of organization. +- isOwner, err := organization.OrgFromUser(repoOwner).IsOwnedBy(ctx, ctx.Doer.ID) +- if err != nil { +- ctx.Error(http.StatusInternalServerError, "IsOwnedBy", err) +- return +- } else if !isOwner { +- ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.") +- return +- } +- } ++ ctx.Error(http.StatusForbidden, "", "You need to be administrator of this Forgejo instance to be able to create mirrors.") + } + + remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword) +diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go +index 97b0c425ea..554a470eab 100644 +--- a/routers/web/repo/migrate.go ++++ b/routers/web/repo/migrate.go +@@ -150,6 +150,12 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplN + // MigratePost response for migrating from external git repository + func MigratePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.MigrateRepoForm) ++ ++ if !ctx.Doer.IsAdmin { ++ ctx.Error(http.StatusForbidden, "MigratePost: you need to be site administrator to use migrations and mirrors") ++ return ++ } ++ + if setting.Repository.DisableMigrations { + ctx.Error(http.StatusForbidden, "MigratePost: the site administrator has disabled migrations") + return diff --git a/services/default.nix b/services/default.nix index cd25088..0599eb5 100644 --- a/services/default.nix +++ b/services/default.nix @@ -6,6 +6,7 @@ ./netbox ./ofborg ./postgres + ./forgejo ./baremetal-builder ]; } diff --git a/services/forgejo/default.nix b/services/forgejo/default.nix new file mode 100644 index 0000000..dedda99 --- /dev/null +++ b/services/forgejo/default.nix @@ -0,0 +1,139 @@ +{ pkgs, lib, config, ... }: + + +let + cfg = config.bagel.services.forgejo; + inherit (lib) mkIf mkEnableOption mkOption types; + + domain = "git.forkos.org"; +in +{ + options.bagel.services.forgejo = { + enable = mkEnableOption "Forgejo"; + sshBindAddr = mkOption { + type = types.str; + }; + }; + + config = mkIf cfg.enable { + services.forgejo = { + enable = true; + + package = pkgs.callPackage ../../pkgs/forgejo { }; + + database = { + type = "postgres"; + createDatabase = true; + }; + + lfs.enable = true; + + settings = { + DEFAULT = { + APP_NAME = "ForkOS"; + }; + + server = { + PROTOCOL = "http+unix"; + ROOT_URL = "https://${domain}/"; + DOMAIN = "${domain}"; + + BUILTIN_SSH_SERVER_USER = "git"; + SSH_PORT = 22; + SSH_LISTEN_HOST = cfg.sshBindAddr; + START_SSH_SERVER = true; + }; + + session = { + PROVIDER = "redis"; + PROVIDER_CONFIG = "network=unix,addr=${config.services.redis.servers.forgejo.unixSocket},db=0"; + COOKIE_NAME = "session"; + }; + + service = { + DISABLE_REGISTRATION = true; + DEFAULT_KEEP_EMAIL_PRIVATE = true; + }; + + oauth2_client = { + REGISTER_EMAIL_CONFIRM = false; + ENABLE_AUTO_REGISTRATION = true; + }; + + # TODO: transactional mails + + cache = { + ADAPTER = "redis"; + HOST = "network=unix,addr=${config.services.redis.servers.forgejo.unixSocket},db=1"; + ITEM_TTL = "72h"; # increased from default 16h + }; + + ui = { + SHOW_USER_EMAIL = false; + }; + + repository = { + # Forks in forgejo are suprisingly expensive because they are full git clones. + # If we do want to enable forks, we can write a small patch that disables + # only for repositories that are as large as nixpkgs. + DISABLE_FORKS = true; + }; + + packages = { + # Forgejo's various package registries can easily take up a lot of space. + # We could either store the blobs on some slower disks but larger, or even + # better, use an s3 bucket for it. But until we actually have a use-case for + # this feature, we will simply keep it disabled for now. + ENABLED = false; + }; + + indexer = { + REPO_INDEXER_REPO_TYPES = "sources,mirrors,templates"; # skip forks + REPO_INDEXER_ENABLED = true; + ISSUE_INDEXER_TYPE = "bleve"; + }; + + "git.timeout" = { + MIGRATE = 3600; # increase from default 600 (seconds) for something as large as nixpkgs on a slow uplink + }; + + log = { + LEVEL = "Warn"; + }; + }; + }; + + systemd.services.forgejo = { + serviceConfig = lib.optionalAttrs (config.services.forgejo.settings.server.SSH_PORT < 1024) { + AmbientCapabilities = lib.mkForce "CAP_NET_BIND_SERVICE"; + CapabilityBoundingSet = lib.mkForce "CAP_NET_BIND_SERVICE"; + PrivateUsers = lib.mkForce false; + }; + + # start Forgejo *after* sshd.service, so in case Forgejo tries to wildcard bind :22 due to + # a bug or whatever, we don't lose OpenSSH in a race. + wants = [ "sshd.service" "redis-forgejo.service" ]; + requires = [ "sshd.service" "redis-forgejo.service" ]; + }; + + services.redis.servers.forgejo = { + enable = true; + user = "forgejo"; + }; + + services.nginx = { + enable = true; + virtualHosts.${domain} = { + enableACME = true; + forceSSL = true; + locations."/".proxyPass = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}"; + }; + }; + + networking.firewall.allowedTCPPorts = [ + 80 + 443 + config.services.forgejo.settings.server.SSH_PORT + ]; + }; +}