1package config
  2
  3import (
  4	"bytes"
  5	"log"
  6	"path/filepath"
  7	"strings"
  8	"sync"
  9	"text/template"
 10
 11	"golang.org/x/crypto/ssh"
 12	"gopkg.in/yaml.v2"
 13
 14	"fmt"
 15	"os"
 16
 17	"github.com/charmbracelet/soft-serve/config"
 18	"github.com/charmbracelet/soft-serve/internal/git"
 19	gg "github.com/gogs/git-module"
 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	mtx          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		if bts, err := os.ReadFile(k); err == nil {
 64			// pk is a file, set its contents as pk
 65			k = string(bts)
 66		}
 67		var pk = strings.TrimSpace(k)
 68		if pk == "" {
 69			continue
 70		}
 71		// it is a valid ssh key, nothing to do
 72		if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk)); err != nil {
 73			return nil, fmt.Errorf("invalid initial admin key %q: %w", k, err)
 74		}
 75		pks = append(pks, pk)
 76	}
 77
 78	rs := git.NewRepoSource(cfg.RepoPath)
 79	c := &Config{
 80		Cfg: cfg,
 81	}
 82	c.Host = cfg.Host
 83	c.Port = port
 84	c.Source = rs
 85	if len(pks) == 0 {
 86		anonAccess = "read-write"
 87	} else {
 88		anonAccess = "no-access"
 89	}
 90	if host == "" {
 91		displayHost = "localhost"
 92	} else {
 93		displayHost = host
 94	}
 95	yamlConfig := fmt.Sprintf(defaultConfig, displayHost, port, anonAccess)
 96	if len(pks) == 0 {
 97		yamlUsers = defaultUserConfig
 98	} else {
 99		var result string
100		for _, pk := range pks {
101			result += fmt.Sprintf("      - %s\n", pk)
102		}
103		yamlUsers = fmt.Sprintf(hasKeyUserConfig, result)
104	}
105	yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig)
106	err := c.createDefaultConfigRepo(yaml)
107	if err != nil {
108		return nil, err
109	}
110	err = c.Reload()
111	if err != nil {
112		return nil, err
113	}
114	return c, nil
115}
116
117// Reload reloads the configuration.
118func (cfg *Config) Reload() error {
119	cfg.mtx.Lock()
120	defer cfg.mtx.Unlock()
121	err := cfg.Source.LoadRepos()
122	if err != nil {
123		return err
124	}
125	cr, err := cfg.Source.GetRepo("config")
126	if err != nil {
127		return err
128	}
129	cs, _, err := cr.LatestFile("config.yaml")
130	if err != nil {
131		return err
132	}
133	err = yaml.Unmarshal([]byte(cs), cfg)
134	if err != nil {
135		return fmt.Errorf("bad yaml in config.yaml: %s", err)
136	}
137	for _, r := range cfg.Source.AllRepos() {
138		name := r.Name()
139		err = r.UpdateServerInfo()
140		if err != nil {
141			log.Printf("error updating server info for %s: %s", name, err)
142		}
143		pat := "README*"
144		rp := ""
145		for _, rr := range cfg.Repos {
146			if name == rr.Repo {
147				rp = rr.Readme
148				break
149			}
150		}
151		if rp != "" {
152			pat = rp
153		}
154		rm := ""
155		fc, fp, _ := r.LatestFile(pat)
156		rm = fc
157		if name == "config" {
158			md, err := templatize(rm, cfg)
159			if err != nil {
160				return err
161			}
162			rm = md
163		}
164		r.SetReadme(rm, fp)
165	}
166	return nil
167}
168
169func createFile(path string, content string) error {
170	f, err := os.Create(path)
171	if err != nil {
172		return err
173	}
174	defer f.Close()
175	_, err = f.WriteString(content)
176	if err != nil {
177		return err
178	}
179	return f.Sync()
180}
181
182func (cfg *Config) createDefaultConfigRepo(yaml string) error {
183	cn := "config"
184	rs := cfg.Source
185	err := rs.LoadRepo(cn)
186	if os.IsNotExist(err) {
187		repo, err := rs.InitRepo(cn, true)
188		if err != nil {
189			return err
190		}
191		wt := repo.Path()
192		defer os.RemoveAll(wt)
193		rm, err := os.Create(filepath.Join(wt, "README.md"))
194		if err != nil {
195			return err
196		}
197		_, err = rm.Write([]byte(defaultReadme))
198		if err != nil {
199			return err
200		}
201		cf, err := os.Create(filepath.Join(wt, "config.yaml"))
202		if err != nil {
203			return err
204		}
205		_, err = cf.Write([]byte(yaml))
206		if err != nil {
207			return err
208		}
209		err = gg.Add(wt, gg.AddOptions{All: true})
210		if err != nil {
211			return err
212		}
213		err = gg.CreateCommit(wt, &gg.Signature{
214			Name:  "Soft Serve Server",
215			Email: "vt100@charm.sh",
216		}, "Default init")
217		if err != nil {
218			return err
219		}
220		err = repo.Push("origin", "master")
221		if err != nil {
222			return err
223		}
224	} else if err != nil {
225		return err
226	}
227	return nil
228}
229
230func (cfg *Config) isPrivate(repo string) bool {
231	for _, r := range cfg.Repos {
232		if r.Repo == repo {
233			return r.Private
234		}
235	}
236	return false
237}
238
239func templatize(mdt string, tmpl interface{}) (string, error) {
240	t, err := template.New("readme").Parse(mdt)
241	if err != nil {
242		return "", err
243	}
244	buf := &bytes.Buffer{}
245	err = t.Execute(buf, tmpl)
246	if err != nil {
247		return "", err
248	}
249	return buf.String(), nil
250}