config.go

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