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	Port           int `env:"PORT" envDefault:"9418"`
 41	MaxTimeout     int `env:"MAX_TIMEOUT" envDefault:"0"`
 42	IdleTimeout    int `env:"IDLE_TIMEOUT" envDefault:"3"`
 43	MaxConnections int `env:"SOFT_SERVE_GIT_MAX_CONNECTIONS" envDefault:"32"`
 44}
 45
 46// DBConfig is the database configuration for the server.
 47type DBConfig struct {
 48	Driver   string `env:"DRIVER" envDefault:"sqlite"`
 49	User     string `env:"USER"`
 50	Password string `env:"PASSWORD"`
 51	Host     string `env:"HOST"`
 52	Port     string `env:"PORT"`
 53	Name     string `env:"NAME"`
 54	SSLMode  bool   `env:"SSL_MODE" envDefault:"false"`
 55}
 56
 57// URL returns a database URL for the configuration.
 58func (d *DBConfig) URL() *url.URL {
 59	switch d.Driver {
 60	case "sqlite":
 61		return &url.URL{
 62			Scheme: "sqlite",
 63			Path:   filepath.Join(d.Name),
 64		}
 65	default:
 66		ssl := "disable"
 67		if d.SSLMode {
 68			ssl = "require"
 69		}
 70		var user *url.Userinfo
 71		if d.User != "" && d.Password != "" {
 72			user = url.UserPassword(d.User, d.Password)
 73		} else if d.User != "" {
 74			user = url.User(d.User)
 75		}
 76		return &url.URL{
 77			Scheme:   d.Driver,
 78			Host:     net.JoinHostPort(d.Host, d.Port),
 79			User:     user,
 80			Path:     d.Name,
 81			RawQuery: fmt.Sprintf("sslmode=%s", ssl),
 82		}
 83	}
 84}
 85
 86// Config is the configuration for Soft Serve.
 87type Config struct {
 88	Host string `env:"HOST" envDefault:"localhost"`
 89
 90	SSH SSHConfig `env:"SSH" envPrefix:"SSH_"`
 91	Git GitConfig `env:"GIT" envPrefix:"GIT_"`
 92	Db  DBConfig  `env:"DB" envPrefix:"DB_"`
 93
 94	ServerName string            `env:"SERVER_NAME" envDefault:"Soft Serve"`
 95	AnonAccess proto.AccessLevel `env:"ANON_ACCESS" envDefault:"read-only"`
 96	DataPath   string            `env:"DATA_PATH" envDefault:"data"`
 97
 98	// Deprecated: use SOFT_SERVE_SSH_PORT instead.
 99	Port int `env:"PORT"`
100	// Deprecated: use DataPath instead.
101	KeyPath string `env:"KEY_PATH"`
102	// Deprecated: use DataPath instead.
103	ReposPath string `env:"REPO_PATH"`
104
105	InitialAdminKeys []string `env:"INITIAL_ADMIN_KEY" envSeparator:"\n"`
106	Callbacks        Callbacks
107
108	db db.Store
109}
110
111// RepoPath returns the path to the repositories.
112func (c *Config) RepoPath() string {
113	return filepath.Join(c.DataPath, "repos")
114}
115
116// SSHPath returns the path to the SSH directory.
117func (c *Config) SSHPath() string {
118	return filepath.Join(c.DataPath, "ssh")
119}
120
121// PrivateKeyPath returns the path to the SSH key.
122func (c *Config) PrivateKeyPath() string {
123	return filepath.Join(c.SSHPath(), c.SSH.KeyPath)
124}
125
126// DBPath returns the path to the database.
127func (c *Config) DBPath() string {
128	return filepath.Join(c.DataPath, "db", "soft-serve.db")
129}
130
131// DefaultConfig returns a Config with the values populated with the defaults
132// or specified environment variables.
133func DefaultConfig() *Config {
134	var err error
135	var migrateWarn bool
136	var cfg Config
137	if err = env.Parse(&cfg, env.Options{
138		Prefix: "SOFT_SERVE_",
139	}); err != nil {
140		log.Fatalln(err)
141	}
142	if cfg.Port != 0 {
143		log.Printf("warning: SOFT_SERVE_PORT is deprecated, use SOFT_SERVE_SSH_PORT instead.")
144		migrateWarn = true
145	}
146	if cfg.KeyPath != "" {
147		log.Printf("warning: SOFT_SERVE_KEY_PATH is deprecated, use SOFT_SERVE_DATA_PATH instead.")
148		migrateWarn = true
149	}
150	if cfg.ReposPath != "" {
151		log.Printf("warning: SOFT_SERVE_REPO_PATH is deprecated, use SOFT_SERVE_DATA_PATH instead.")
152		migrateWarn = true
153	}
154	if migrateWarn {
155		log.Printf("warning: please run `soft serve migrate` to migrate your server and configuration.")
156	}
157	// initialize admin keys
158	for i, k := range cfg.InitialAdminKeys {
159		if bts, err := os.ReadFile(k); err == nil {
160			// k is a file path, read the file
161			k = string(bts)
162		}
163		pk := strings.TrimSpace(k)
164		if pk == "" {
165			// ignore empty keys
166			continue
167		}
168		if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)); err != nil {
169			// Fatal if the key is invalid
170			log.Fatalf("invalid initial admin key %q: %v", k, err)
171		}
172		// store the key in the config
173		cfg.InitialAdminKeys[i] = pk
174	}
175	log.Printf("initial admin keys are: %v", cfg.InitialAdminKeys)
176	// init data path and db
177	if err := os.MkdirAll(cfg.RepoPath(), 0755); err != nil {
178		log.Fatalln(err)
179	}
180	if err := cfg.createDefaultConfigRepoAndUsers(); err != nil {
181		log.Fatalln(err)
182	}
183	var db db.Store
184	switch cfg.Db.Driver {
185	case "sqlite":
186		if err := os.MkdirAll(filepath.Dir(cfg.DBPath()), 0755); err != nil {
187			log.Fatalln(err)
188		}
189		db, err = sqlite.New(cfg.DBPath())
190		if err != nil {
191			log.Fatalln(err)
192		}
193	}
194	return cfg.WithDB(db).WithDataPath(cfg.DataPath)
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}