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.

refactor

c5902451905acdaa478f8476844ccd4a510d9ca9 · Keenan Nemetz · 16d ago · parent ec2aba4b

forge/instance.go +235

@@ -0,0 +1,235 @@
1 // Package forge opens and owns the state of a Vole instance.
2 //
3 // An instance is one directory of durable state: the user roster and access
4 // rules (data/users.db), the derived index, the CI queue, the object store, and
5 // the bare repositories. This package is the one place that knows how those fit
6 // together, so the server, the CLI's administrative commands and an embedding
7 // application all open the same set in the same order.
8 package forge
9
10 import (
11 "errors"
12 "fmt"
13 "os"
14 "path/filepath"
15 "strings"
16
17 "github.com/rodent-software/vole/ci/runs"
18 "github.com/rodent-software/vole/core/lfs"
19 "github.com/rodent-software/vole/forge/access"
20 "github.com/rodent-software/vole/forge/config"
21 "github.com/rodent-software/vole/forge/identity"
22 "github.com/rodent-software/vole/forge/index"
23 "github.com/rodent-software/vole/forge/repo"
24 )
25
26 // Instance is the state a running or administered Vole instance owns.
27 type Instance struct {
28 Cfg *config.Config
29 Repos *repo.Store
30 Identity *identity.Store
31 Index *index.Store
32 Runs *runs.Store
33 ACLs *access.Store
34 Locks *lfs.LockStore
35 LFS *lfs.Store
36
37 // ImportedACLs is how many legacy refs/vole/acl rule sets were imported by
38 // Open, so a caller that logs startup can say so.
39 ImportedACLs int
40 }
41
42 // Open opens every store an instance needs, in the order their dependencies
43 // require, and releases what it opened if one of them fails.
44 func Open(cfg *config.Config, binPath string) (*Instance, error) {
45 if err := os.MkdirAll(cfg.ReposDir(), 0o755); err != nil {
46 return nil, err
47 }
48 ident, err := identity.Open(cfg.UsersDB())
49 if err != nil {
50 return nil, fmt.Errorf("%w\nrun `vole init` first", err)
51 }
52 ident.AuditPath = cfg.AuditLog()
53
54 ix, err := index.Open(cfg.IndexDB())
55 if err != nil {
56 ident.Close()
57 return nil, fmt.Errorf("open index: %w", err)
58 }
59 rn, err := runs.Open(cfg.RunsDB())
60 if err != nil {
61 ix.Close()
62 ident.Close()
63 return nil, fmt.Errorf("open run queue: %w", err)
64 }
65 acls, err := access.Open(cfg.UsersDB())
66 if err != nil {
67 rn.Close()
68 ix.Close()
69 ident.Close()
70 return nil, fmt.Errorf("open access rules: %w", err)
71 }
72 acls.AuditPath = cfg.AuditLog()
73
74 lk, err := lfs.OpenLocks(cfg.UsersDB())
75 if err != nil {
76 acls.Close()
77 rn.Close()
78 ix.Close()
79 ident.Close()
80 return nil, fmt.Errorf("open locks: %w", err)
81 }
82 lk.AuditPath = cfg.AuditLog()
83
84 rs := repo.New(cfg.ReposDir(), binPath, acls)
85 rs.MaxInputBytes = cfg.ReceiveMaxBytes
86 imported, err := rs.ImportACLs()
87 if err != nil {
88 lk.Close()
89 acls.Close()
90 rn.Close()
91 ix.Close()
92 ident.Close()
93 return nil, err
94 }
95
96 return &Instance{
97 Cfg: cfg,
98 Repos: rs,
99 Identity: ident,
100 Index: ix,
101 Runs: rn,
102 ACLs: acls,
103 Locks: lk,
104 LFS: lfs.New(cfg.LFSDir(), cfg.LFSObjectMaxBytes),
105 ImportedACLs: imported,
106 }, nil
107 }
108
109 // OpenPath loads the configuration at cfgPath and opens the instance it names.
110 func OpenPath(cfgPath string) (*Instance, error) {
111 cfg, err := config.Load(cfgPath)
112 if err != nil {
113 return nil, err
114 }
115 bin, err := os.Executable()
116 if err != nil {
117 return nil, err
118 }
119 return Open(cfg, bin)
120 }
121
122 // Close releases every store, reporting the first error but closing the rest.
123 func (i *Instance) Close() error {
124 if i == nil {
125 return nil
126 }
127 var err error
128 for _, closeFn := range []func() error{
129 i.Locks.Close,
130 i.ACLs.Close,
131 i.Runs.Close,
132 i.Index.Close,
133 i.Identity.Close,
134 } {
135 if closeFn == nil {
136 continue
137 }
138 if cerr := closeFn(); cerr != nil && err == nil {
139 err = cerr
140 }
141 }
142 return err
143 }
144
145 // Init creates the three things an instance lives in — the config file, the
146 // repository directory and the roster database — and leaves existing state
147 // alone. It returns the absolute instance directory.
148 func Init(dir string) (string, error) {
149 if dir == "" {
150 dir = "."
151 }
152 abs, err := filepath.Abs(dir)
153 if err != nil {
154 return "", err
155 }
156 if err := os.MkdirAll(abs, 0o755); err != nil {
157 return "", err
158 }
159 path := filepath.Join(abs, "vole.toml")
160 if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
161 // Write an absolute data_dir so `serve` works from any working directory.
162 body := strings.Replace(config.Sample(),
163 `data_dir = "./data"`, fmt.Sprintf("data_dir = %q", filepath.Join(abs, "data")), 1)
164 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
165 return "", err
166 }
167 }
168 cfg, err := config.Load(path)
169 if err != nil {
170 return "", err
171 }
172 if err := os.MkdirAll(cfg.ReposDir(), 0o755); err != nil {
173 return "", err
174 }
175 if err := identity.Init(cfg.UsersDB()); err != nil {
176 return "", err
177 }
178 return abs, nil
179 }
180
181 // RenameRepo moves a repository and everything keyed by its old name: the
182 // derived index rows, the CI queue's rows and cursors, and the object store's
183 // per-repository namespace.
184 func (i *Instance) RenameRepo(old, next string) error {
185 m, err := i.Repos.Open(old)
186 if err != nil {
187 return err
188 }
189 if err := i.Repos.Rename(old, next); err != nil {
190 return err
191 }
192 _ = i.Index.Remove(m.ID)
193 if renamed, err := i.Repos.Open(next); err == nil {
194 _ = i.Index.Upsert(renamed)
195 }
196 if err := i.Runs.RenameRepo(old, next); err != nil {
197 return err
198 }
199 if err := i.LFS.RenameRepo(old, next); err != nil {
200 return fmt.Errorf("renamed in the database, but the object namespace did not move: %w", err)
201 }
202 return nil
203 }
204
205 // DeleteRepo removes a repository and the state that would otherwise point at
206 // something nobody can clone: index rows, queue rows and cursors, and locks.
207 func (i *Instance) DeleteRepo(name string) error {
208 e, _ := i.Index.Get(name)
209 if err := i.Repos.Delete(name); err != nil {
210 return err
211 }
212 if e != nil {
213 _ = i.Index.Remove(e.ID)
214 }
215 _ = i.Runs.ForgetRepo(name)
216 if e != nil {
217 _, _ = i.Locks.ForgetRepo(e.ID)
218 }
219 return nil
220 }
221
222 // RebuildIndex reconstructs the derived repository and issue rows, then the
223 // finished half of the run queue from the records on the metadata branch.
224 func (i *Instance) RebuildIndex() (repos int, runRecords int, err error) {
225 repos, err = i.Index.Rebuild(i.Repos)
226 if err != nil {
227 return 0, 0, err
228 }
229 list, err := i.Repos.List()
230 if err != nil {
231 return repos, 0, err
232 }
233 runRecords, err = i.Runs.RestoreFinished(list, "refs/heads/vole")
234 return repos, runRecords, err
235 }