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 | } | ||