// Package config reads barerepo.toml, which holds deployment facts only. Chapter 14. package config import ( "fmt" "net/url" "os" "path/filepath" "strings" "github.com/BurntSushi/toml" ) const DefaultPath = "/etc/barerepo/barerepo.toml" type Config struct { Server Server `toml:"server"` Database Database `toml:"database"` Paths Paths `toml:"paths"` Limits Limits `toml:"limits"` Behavior Behavior `toml:"behavior"` // Path is where this configuration was read from. Not a config key. Path string `toml:"-"` } type Server struct { Listen string `toml:"listen"` ExternalURL string `toml:"external_url"` // RawURL is a separate host for raw content, and empty means a download from the main one. RawURL string `toml:"raw_url"` SSHHost string `toml:"ssh_host"` SSHPort int `toml:"ssh_port"` // SSHUser is the account in a clone url, which another barerepo on the same host may already own. SSHUser string `toml:"ssh_user"` // GitIdentity signs commits the server makes, and empty derives it from the external url. GitIdentity string `toml:"git_identity"` } // Database picks SQLite or PostgreSQL by URL scheme, and no feature exists on only one. type Database struct { URL string `toml:"url"` } type Paths struct { Repos string `toml:"repos"` Cache string `toml:"cache"` Artifacts string `toml:"artifacts"` // SSHDir holds the authorized_keys the server writes, and empty means it writes none. SSHDir string `toml:"ssh_dir"` } type Limits struct { // MaxBlobMB bounds one file, because turning LFS off without it does not hold. MaxBlobMB int `toml:"max_blob_mb"` // MaxPushMB is loose, because the push most likely to hit it is somebody's first import. MaxPushMB int `toml:"max_push_mb"` MaxOpenProposals int `toml:"max_open_proposals"` SignupPerHourPerIP int `toml:"signup_per_hour_per_ip"` // CommentPerHourPerThread is per account as well, since chapter 27 says comments are cheap. CommentPerHourPerThread int `toml:"comment_per_hour_per_thread"` ArtifactRetainDays int `toml:"artifact_retain_days"` } type Behavior struct { AllowPushToCreate bool `toml:"allow_push_to_create"` AllowLFS bool `toml:"allow_lfs"` } // Default is a working single-host install on SQLite with no config file. Chapter 41.7. func Default() Config { return Config{ Server: Server{ Listen: "127.0.0.1:3000", ExternalURL: "http://127.0.0.1:3000", RawURL: "", SSHHost: "localhost", SSHPort: 22, SSHUser: "git", }, Database: Database{URL: "sqlite:///var/lib/barerepo/forge.db"}, Paths: Paths{ Repos: "/var/lib/barerepo/repos", Cache: "/var/lib/barerepo/cache", Artifacts: "/var/lib/barerepo/artifacts", }, Limits: Limits{ MaxBlobMB: 100, MaxPushMB: 2048, MaxOpenProposals: 10, SignupPerHourPerIP: 5, // Generous, because a reviewer working through a diff leaves a lot of comments at once. CommentPerHourPerThread: 30, ArtifactRetainDays: 90, }, Behavior: Behavior{ AllowPushToCreate: true, AllowLFS: false, }, } } // Identity is who the server commits as, from config or from the host it answers on. func (c Config) Identity() string { if c.Server.GitIdentity != "" { return c.Server.GitIdentity } user := c.Server.SSHUser if user == "" { user = "git" } host := c.Server.SSHHost if host == "" { if u, err := url.Parse(c.Server.ExternalURL); err == nil && u.Hostname() != "" { host = u.Hostname() } } if host == "" { host = "localhost" } return user + "@" + host } // Load reads over the defaults, and a missing file is an operator who has decided nothing yet. func Load(path string) (Config, error) { if path == "" { path = DefaultPath } cfg := Default() cfg.Path = path raw, err := os.ReadFile(path) if os.IsNotExist(err) { return cfg, cfg.check() } if err != nil { return cfg, err } var md toml.MetaData if md, err = toml.Decode(string(raw), &cfg); err != nil { return cfg, fmt.Errorf("%s: %w", path, err) } // A silently ignored misspelling is how a limit turns out never to have applied. Say so. if un := md.Undecoded(); len(un) > 0 { keys := make([]string, len(un)) for i, k := range un { keys[i] = k.String() } return cfg, fmt.Errorf("%s: unknown setting %s", path, strings.Join(keys, ", ")) } return cfg, cfg.check() } // check refuses a configuration that would fail later in a confusing place. func (c Config) check() error { for name, p := range map[string]string{ "paths.repos": c.Paths.Repos, "paths.cache": c.Paths.Cache, "paths.artifacts": c.Paths.Artifacts, } { if p == "" { return fmt.Errorf("%s: %s is empty", c.Path, name) } if !filepath.IsAbs(p) { return fmt.Errorf("%s: %s must be an absolute path, got %q", c.Path, name, p) } } if c.Server.Listen == "" { return fmt.Errorf("%s: server.listen is empty", c.Path) } if c.Behavior.AllowLFS { return fmt.Errorf("%s: behavior.allow_lfs is on and chapter 20.3 is not built, "+ "so nothing would serve it. leave it off", c.Path) } if _, err := c.DatabaseKind(); err != nil { return fmt.Errorf("%s: %w", c.Path, err) } if c.Server.RawURL != "" { u, err := url.Parse(c.Server.RawURL) if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") { return fmt.Errorf("%s: server.raw_url must be a full http or https url", c.Path) } // A shared domain shares cookies, which is the whole thing chapter 42.3 avoids. if ext, err := url.Parse(c.Server.ExternalURL); err == nil && ext.Host == u.Host { return fmt.Errorf("%s: server.raw_url must be a different host from server.external_url", c.Path) } } return nil } // Kind names a supported database. type Kind string const ( SQLite Kind = "sqlite" Postgres Kind = "postgres" ) // DatabaseKind reports which database database.url selects. func (c Config) DatabaseKind() (Kind, error) { switch { case c.Database.URL == "": return "", fmt.Errorf("database.url is empty") case strings.HasPrefix(c.Database.URL, "sqlite:"): return SQLite, nil case strings.HasPrefix(c.Database.URL, "postgres:"), strings.HasPrefix(c.Database.URL, "postgresql:"): return Postgres, nil default: return "", fmt.Errorf("database.url must start with sqlite: or postgres:, got %q", c.Database.URL) } }