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