config.go

  1package config
  2
  3import (
  4	"fmt"
  5	"log"
  6	"net"
  7	"net/url"
  8	"os"
  9	"path/filepath"
 10	"strings"
 11
 12	"github.com/caarlos0/env/v6"
 13	"github.com/charmbracelet/soft-serve/proto"
 14	"github.com/charmbracelet/soft-serve/server/db"
 15	"github.com/charmbracelet/soft-serve/server/db/sqlite"
 16	"github.com/gliderlabs/ssh"
 17)
 18
 19// Callbacks provides an interface that can be used to run callbacks on different events.
 20type Callbacks interface {
 21	Tui(action string)
 22	Push(repo string)
 23	Fetch(repo string)
 24}
 25
 26// SSHConfig is the SSH configuration for the server.
 27type SSHConfig struct {
 28	Key           string `env:"KEY"`
 29	KeyPath       string `env:"KEY_PATH" envDefault:"soft_serve"`
 30	Port          int    `env:"PORT" envDefault:"23231"`
 31	AllowKeyless  bool   `env:"ALLOW_KEYLESS" envDefault:"true"`
 32	AllowPassword bool   `env:"ALLOW_PASSWORD" envDefault:"false"`
 33	Password      string `env:"PASSWORD"`
 34	MaxTimeout    int    `env:"MAX_TIMEOUT" envDefault:"0"`
 35	IdleTimeout   int    `env:"IDLE_TIMEOUT" envDefault:"300"`
 36}
 37
 38// GitConfig is the Git daemon configuration for the server.
 39type GitConfig struct {
 40	Enabled        bool `env:"ENABLED" envDefault:"true"`
 41	Port           int  `env:"PORT" envDefault:"9418"`
 42	MaxTimeout     int  `env:"MAX_TIMEOUT" envDefault:"0"`
 43	IdleTimeout    int  `env:"IDLE_TIMEOUT" envDefault:"3"`
 44	MaxConnections int  `env:"SOFT_SERVE_GIT_MAX_CONNECTIONS" envDefault:"32"`
 45}
 46
 47// DBConfig is the database configuration for the server.
 48type DBConfig struct {
 49	Driver   string `env:"DRIVER" envDefault:"sqlite"`
 50	User     string `env:"USER"`
 51	Password string `env:"PASSWORD"`
 52	Host     string `env:"HOST"`
 53	Port     string `env:"PORT"`
 54	Name     string `env:"NAME"`
 55	SSLMode  bool   `env:"SSL_MODE" envDefault:"false"`
 56}
 57
 58// URL returns a database URL for the configuration.
 59func (d *DBConfig) URL() *url.URL {
 60	switch d.Driver {
 61	case "sqlite":
 62		return &url.URL{
 63			Scheme: "sqlite",
 64			Path:   filepath.Join(d.Name),
 65		}
 66	default:
 67		ssl := "disable"
 68		if d.SSLMode {
 69			ssl = "require"
 70		}
 71		var user *url.Userinfo
 72		if d.User != "" && d.Password != "" {
 73			user = url.UserPassword(d.User, d.Password)
 74		} else if d.User != "" {
 75			user = url.User(d.User)
 76		}
 77		return &url.URL{
 78			Scheme:   d.Driver,
 79			Host:     net.JoinHostPort(d.Host, d.Port),
 80			User:     user,
 81			Path:     d.Name,
 82			RawQuery: fmt.Sprintf("sslmode=%s", ssl),
 83		}
 84	}
 85}
 86
 87// Config is the configuration for Soft Serve.
 88type Config struct {
 89	Host string `env:"HOST" envDefault:"localhost"`
 90
 91	SSH SSHConfig `env:"SSH" envPrefix:"SSH_"`
 92	Git GitConfig `env:"GIT" envPrefix:"GIT_"`
 93	Db  DBConfig  `env:"DB" envPrefix:"DB_"`
 94
 95	ServerName string            `env:"SERVER_NAME" envDefault:"Soft Serve"`
 96	AnonAccess proto.AccessLevel `env:"ANON_ACCESS" envDefault:"read-only"`
 97	DataPath   string            `env:"DATA_PATH" envDefault:"data"`
 98
 99	// Deprecated: use SOFT_SERVE_SSH_PORT instead.
100	Port int `env:"PORT"`
101	// Deprecated: use DataPath instead.
102	KeyPath string `env:"KEY_PATH"`
103	// Deprecated: use DataPath instead.
104	ReposPath string `env:"REPO_PATH"`
105
106	InitialAdminKeys []string `env:"INITIAL_ADMIN_KEY" envSeparator:"\n"`
107	Callbacks        Callbacks
108
109	db db.Store
110}
111
112// RepoPath returns the path to the repositories.
113func (c *Config) RepoPath() string {
114	return filepath.Join(c.DataPath, "repos")
115}
116
117// SSHPath returns the path to the SSH directory.
118func (c *Config) SSHPath() string {
119	return filepath.Join(c.DataPath, "ssh")
120}
121
122// PrivateKeyPath returns the path to the SSH key.
123func (c *Config) PrivateKeyPath() string {
124	return filepath.Join(c.SSHPath(), c.SSH.KeyPath)
125}
126
127// DBPath returns the path to the database.
128func (c *Config) DBPath() string {
129	return filepath.Join(c.DataPath, "db", "soft-serve.db")
130}
131
132// DefaultConfig returns a Config with the values populated with the defaults
133// or specified environment variables.
134func DefaultConfig() *Config {
135	var err error
136	var migrateWarn bool
137	var cfg Config
138	if err = env.Parse(&cfg, env.Options{
139		Prefix: "SOFT_SERVE_",
140	}); err != nil {
141		log.Fatalln(err)
142	}
143	if cfg.Port != 0 {
144		log.Printf("warning: SOFT_SERVE_PORT is deprecated, use SOFT_SERVE_SSH_PORT instead.")
145		migrateWarn = true
146	}
147	if cfg.KeyPath != "" {
148		log.Printf("warning: SOFT_SERVE_KEY_PATH is deprecated, use SOFT_SERVE_DATA_PATH instead.")
149		migrateWarn = true
150	}
151	if cfg.ReposPath != "" {
152		log.Printf("warning: SOFT_SERVE_REPO_PATH is deprecated, use SOFT_SERVE_DATA_PATH instead.")
153		migrateWarn = true
154	}
155	if migrateWarn {
156		log.Printf("warning: please run `soft serve migrate` to migrate your server and configuration.")
157	}
158	// initialize admin keys
159	for i, k := range cfg.InitialAdminKeys {
160		if bts, err := os.ReadFile(k); err == nil {
161			// k is a file path, read the file
162			k = string(bts)
163		}
164		pk := strings.TrimSpace(k)
165		if pk == "" {
166			// ignore empty keys
167			continue
168		}
169		if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)); err != nil {
170			// Fatal if the key is invalid
171			log.Fatalf("invalid initial admin key %q: %v", k, err)
172		}
173		// store the key in the config
174		cfg.InitialAdminKeys[i] = pk
175	}
176	// init data path and db
177	if err := os.MkdirAll(cfg.RepoPath(), 0755); err != nil {
178		log.Fatalln(err)
179	}
180	switch cfg.Db.Driver {
181	case "sqlite":
182		if err := os.MkdirAll(filepath.Dir(cfg.DBPath()), 0755); err != nil {
183			log.Fatalln(err)
184		}
185		db, err := sqlite.New(cfg.DBPath())
186		if err != nil {
187			log.Fatalln(err)
188		}
189		cfg.WithDB(db)
190	}
191	if err := cfg.createDefaultConfigRepoAndUsers(); err != nil {
192		log.Fatalln(err)
193	}
194	return &cfg
195}
196
197// DB returns the database for the configuration.
198func (c *Config) DB() db.Store {
199	return c.db
200}
201
202// WithCallbacks applies the given Callbacks to the configuration.
203func (c *Config) WithCallbacks(callbacks Callbacks) *Config {
204	c.Callbacks = callbacks
205	return c
206}
207
208// WithDB sets the database for the configuration.
209func (c *Config) WithDB(db db.Store) *Config {
210	c.db = db
211	return c
212}
213
214// WithDataPath sets the data path for the configuration.
215func (c *Config) WithDataPath(path string) *Config {
216	c.DataPath = path
217	return c
218}