config.go

  1package config
  2
  3import (
  4	"os/exec"
  5	"path/filepath"
  6	"strings"
  7
  8	"golang.org/x/crypto/ssh"
  9	"gopkg.in/yaml.v2"
 10
 11	"fmt"
 12	"os"
 13
 14	"github.com/charmbracelet/soft-serve/config"
 15	"github.com/charmbracelet/soft-serve/internal/git"
 16	gg "github.com/go-git/go-git/v5"
 17	"github.com/go-git/go-git/v5/plumbing/object"
 18)
 19
 20// Config is the Soft Serve configuration.
 21type Config struct {
 22	Name         string `yaml:"name"`
 23	Host         string `yaml:"host"`
 24	Port         int    `yaml:"port"`
 25	AnonAccess   string `yaml:"anon-access"`
 26	AllowKeyless bool   `yaml:"allow-keyless"`
 27	Users        []User `yaml:"users"`
 28	Repos        []Repo `yaml:"repos"`
 29	Source       *git.RepoSource
 30	Cfg          *config.Config
 31}
 32
 33// User contains user-level configuration for a repository.
 34type User struct {
 35	Name        string   `yaml:"name"`
 36	Admin       bool     `yaml:"admin"`
 37	PublicKeys  []string `yaml:"public-keys"`
 38	CollabRepos []string `yaml:"collab-repos"`
 39}
 40
 41// Repo contains repository configuration information.
 42type Repo struct {
 43	Name    string `yaml:"name"`
 44	Repo    string `yaml:"repo"`
 45	Note    string `yaml:"note"`
 46	Private bool   `yaml:"private"`
 47}
 48
 49// NewConfig creates a new internal Config struct.
 50func NewConfig(cfg *config.Config) (*Config, error) {
 51	var anonAccess string
 52	var yamlUsers string
 53	var displayHost string
 54	host := cfg.BindAddr
 55	port := cfg.Port
 56
 57	pks := make([]string, 0, len(cfg.InitialAdminKeys))
 58	for _, k := range cfg.InitialAdminKeys {
 59		var pk = strings.TrimSpace(k)
 60		if bts, err := os.ReadFile(k); err == nil {
 61			// pk is a file, set its contents as pk
 62			pk = string(bts)
 63		}
 64		// it is a valid ssh key, nothing to do
 65		if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk)); err != nil {
 66			return nil, fmt.Errorf("invalid initial admin key %q: %w", k, err)
 67		}
 68		pks = append(pks, pk)
 69	}
 70
 71	rs := git.NewRepoSource(cfg.RepoPath)
 72	c := &Config{
 73		Cfg: cfg,
 74	}
 75	c.Host = cfg.BindAddr
 76	c.Port = port
 77	c.Source = rs
 78	if len(pks) == 0 {
 79		anonAccess = "read-write"
 80	} else {
 81		anonAccess = "no-access"
 82	}
 83	if host == "" {
 84		displayHost = "localhost"
 85	} else {
 86		displayHost = host
 87	}
 88	yamlConfig := fmt.Sprintf(defaultConfig, displayHost, port, anonAccess)
 89	if len(pks) == 0 {
 90		yamlUsers = defaultUserConfig
 91	} else {
 92		var result string
 93		for _, pk := range pks {
 94			result += fmt.Sprintf("      - %s\n", pk)
 95		}
 96		yamlUsers = fmt.Sprintf(hasKeyUserConfig, result)
 97	}
 98	yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig)
 99	err := c.createDefaultConfigRepo(yaml)
100	if err != nil {
101		return nil, err
102	}
103	return c, nil
104}
105
106// Reload reloads the configuration.
107func (cfg *Config) Reload() error {
108	err := cfg.Source.LoadRepos()
109	if err != nil {
110		return err
111	}
112	cr, err := cfg.Source.GetRepo("config")
113	if err != nil {
114		return err
115	}
116	cs, err := cr.LatestFile("config.yaml")
117	if err != nil {
118		return err
119	}
120	err = yaml.Unmarshal([]byte(cs), cfg)
121	if err != nil {
122		return fmt.Errorf("bad yaml in config.yaml: %s", err)
123	}
124	return nil
125}
126
127func createFile(path string, content string) error {
128	f, err := os.Create(path)
129	if err != nil {
130		return err
131	}
132	defer f.Close()
133	_, err = f.WriteString(content)
134	if err != nil {
135		return err
136	}
137	return f.Sync()
138}
139
140func (cfg *Config) createDefaultConfigRepo(yaml string) error {
141	cn := "config"
142	rs := cfg.Source
143	err := rs.LoadRepos()
144	if err != nil {
145		return err
146	}
147	_, err = rs.GetRepo(cn)
148	if err == git.ErrMissingRepo {
149		cr, err := rs.InitRepo(cn, true)
150		if err != nil {
151			return err
152		}
153		wt, err := cr.Repository.Worktree()
154		if err != nil {
155			return err
156		}
157		rm, err := wt.Filesystem.Create("README.md")
158		if err != nil {
159			return err
160		}
161		_, err = rm.Write([]byte(defaultReadme))
162		if err != nil {
163			return err
164		}
165		cf, err := wt.Filesystem.Create("config.yaml")
166		if err != nil {
167			return err
168		}
169		_, err = cf.Write([]byte(yaml))
170		if err != nil {
171			return err
172		}
173		_, err = wt.Add("README.md")
174		if err != nil {
175			return err
176		}
177		_, err = wt.Add("config.yaml")
178		if err != nil {
179			return err
180		}
181		_, err = wt.Commit("Default init", &gg.CommitOptions{
182			All: true,
183			Author: &object.Signature{
184				Name:  "Soft Serve Server",
185				Email: "vt100@charm.sh",
186			},
187		})
188		if err != nil {
189			return err
190		}
191		err = cr.Repository.Push(&gg.PushOptions{})
192		if err != nil {
193			return err
194		}
195		cmd := exec.Command("git", "update-server-info")
196		cmd.Dir = filepath.Join(rs.Path, cn)
197		err = cmd.Run()
198		if err != nil {
199			return err
200		}
201	} else if err != nil {
202		return err
203	}
204	return cfg.Reload()
205}
206
207func (cfg *Config) isPrivate(repo string) bool {
208	for _, r := range cfg.Repos {
209		if r.Repo == repo {
210			return r.Private
211		}
212	}
213	return false
214}