Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/hook/quotas.go

Line attribution on main · 161 line(s)

9e8cc155
Keenan Nemetz
12d ago
1 package hook
2
3 // Quotas. A repository is a place several people write to, and every one of them
4 // can choose how much: a contributor can add branches in their own namespace, any
5 // writer can add issues and comments, and a maintainer can add tags. Without a
6 // bound, one account can make every anonymous page read slowly or fill the disk,
7 // and the instance's only answer is an admin deleting things by hand.
8 //
9 // The checks run in pre-receive, where a refusal moves no refs. They are counts at
10 // the *new* tip, so they describe the repository the push would leave behind, not
11 // the one it found. The one thing that cannot be seen here is bytes still in the
12 // quarantine; that is why a push is also bounded by receive_max_bytes, and why the
13 // size check refuses a repository that is already over its cap: growth is bounded
14 // by the cap plus one permitted push.
15
16 import (
17 "fmt"
18 "strings"
19
d37d301f
Keenan Nemetz
17h ago
20 "rodent.software/vole/core/git"
21 "rodent.software/vole/internal/format"
22 "rodent.software/vole/metadata"
9e8cc155
Keenan Nemetz
12d ago
23 )
24
25 // quotaProblems reports every limit this push would break. An empty result means
26 // the push is inside every quota.
27 func quotaProblems(cfg Config, g *git.Repo, changes []change) []string {
28 var problems []string
29
30 if cfg.MaxRefs > 0 {
31 if n, err := refsAfter(g, changes); err != nil {
32 problems = append(problems, fmt.Sprintf("cannot count refs to check the repository quota: %v", err))
33 } else if n > cfg.MaxRefs {
34 problems = append(problems, fmt.Sprintf(
35 "this push would leave %d branches and tags, over the limit of %d; delete some or raise repo_max_refs",
36 n, cfg.MaxRefs))
37 }
38 }
39
40 if cfg.MaxMetadataEntries > 0 || cfg.MaxMetadataBytes > 0 {
41 if tip := metadataAfter(changes); tip != "" {
42 entries, bytes, err := metadataSize(g, tip)
43 if err != nil {
44 problems = append(problems, fmt.Sprintf("cannot measure the metadata branch to check its quota: %v", err))
45 } else {
46 if cfg.MaxMetadataEntries > 0 && entries > cfg.MaxMetadataEntries {
47 problems = append(problems, fmt.Sprintf(
48 "%s would hold %d issues, requests, runs and releases, over the limit of %d; raise repo_max_metadata_entries or remove some",
49 metadata.Ref, entries, cfg.MaxMetadataEntries))
50 }
51 if cfg.MaxMetadataBytes > 0 && bytes > cfg.MaxMetadataBytes {
52 problems = append(problems, fmt.Sprintf(
53 "%s would hold %s, over the limit of %s; raise repo_max_metadata_bytes or remove some",
54 metadata.Ref, format.Bytes(bytes), format.Bytes(cfg.MaxMetadataBytes)))
55 }
56 }
57 }
58 }
59
60 if cfg.MaxRepoBytes > 0 {
61 if used, err := g.DiskUsage(); err != nil {
62 problems = append(problems, fmt.Sprintf("cannot measure the repository to check its quota: %v", err))
63 } else if used > cfg.MaxRepoBytes {
64 problems = append(problems, fmt.Sprintf(
65 "this repository holds %s, over the limit of %s; an admin can run `vole admin gc` or raise repo_max_bytes",
66 format.Bytes(used), format.Bytes(cfg.MaxRepoBytes)))
67 }
68 }
69
70 if cfg.MaxLFSBytes > 0 && cfg.LFS != nil && cfg.RepoID != "" {
71 if used, err := lfsUsage(cfg.LFS, cfg.RepoID); err != nil {
72 problems = append(problems, fmt.Sprintf("cannot measure the object store to check its quota: %v", err))
73 } else if used > cfg.MaxLFSBytes {
74 problems = append(problems, fmt.Sprintf(
75 "this repository's LFS objects, logs and artifacts take %s, over the limit of %s; raise repo_max_lfs_bytes or remove some",
76 format.Bytes(used), format.Bytes(cfg.MaxLFSBytes)))
77 }
78 }
79
80 return problems
81 }
82
83 // refsAfter is the number of branches and tags the repository would hold if this
84 // push landed: what is there now, plus the refs the push creates, minus the ones it
85 // deletes. Pre-receive sees the refs as they were, so the change list is what makes
86 // the number describe the outcome.
87 func refsAfter(g *git.Repo, changes []change) (int, error) {
88 now, err := g.RefCount()
89 if err != nil {
90 return 0, err
91 }
92 for _, c := range changes {
93 if !strings.HasPrefix(c.ref, "refs/heads/") && !strings.HasPrefix(c.ref, "refs/tags/") {
94 continue
95 }
96 switch {
97 case c.old == "" || c.old == git.ZeroSHA:
98 if c.new != "" && c.new != git.ZeroSHA {
99 now++
100 }
101 case c.new == "" || c.new == git.ZeroSHA:
102 if now > 0 {
103 now--
104 }
105 }
106 }
107 return now, nil
108 }
109
110 // metadataAfter is the metadata tip the push would leave, or "" when it does not
111 // touch that branch (or deletes it).
112 func metadataAfter(changes []change) string {
113 for _, c := range changes {
114 if c.ref == metadata.Ref && c.new != "" && c.new != git.ZeroSHA {
115 return c.new
116 }
117 }
118 return ""
119 }
120
121 // metadataSize counts what a page load on a repository has to parse: every issue,
122 // request, comment and approval file, and their bytes.
123 func metadataSize(g *git.Repo, tip string) (entries int, bytes int64, err error) {
124 es, err := g.LsTreeSizes(tip)
125 if err != nil {
126 return 0, 0, err
127 }
128 for _, e := range es {
129 if e.Type != "blob" {
130 continue
131 }
132 // Everything the metadata branch carries as a document: issues and their
133 // comments, requests with approvals and comments, and the run and release
134 // records a runner or a maintainer adds. These are what a page load parses.
135 for _, dir := range []string{
b492b557
Keenan Nemetz
3d ago
136 metadata.IssuesDir, metadata.ChangesDir, metadata.ReleasesDir,
9e8cc155
Keenan Nemetz
12d ago
137 } {
138 if strings.HasPrefix(e.Name, dir+"/") {
139 entries++
140 bytes += e.Size
141 break
142 }
143 }
144 }
145 return entries, bytes, nil
146 }
147
148 func lfsUsage(store lfsStore, repoID string) (int64, error) {
149 var total int64
150 err := store.RepoWalk(repoID, func(_ string, size int64) error {
151 total += size
152 return nil
153 })
154 return total, err
155 }
156
157 // lfsStore is the part of the object store the quota check needs. It is an
158 // interface so a test can answer without a directory.
159 type lfsStore interface {
160 RepoWalk(repo string, fn func(oid string, size int64) error) error
161 }