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.Host
 55	port := cfg.Port
 56	pk := cfg.InitialAdminKey
 57
 58	if bts, err := os.ReadFile(pk); err == nil {
 59		// pk is a file, set its contents as pk
 60		pk = string(bts)
 61	}
 62	// it is a valid ssh key, nothing to do
 63	if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk)); err != nil {
 64		return nil, fmt.Errorf("invalid initial admin key: %w", err)
 65	}
 66
 67	rs := git.NewRepoSource(cfg.RepoPath)
 68	c := &Config{
 69		Cfg: cfg,
 70	}
 71	c.Host = cfg.Host
 72	c.Port = port
 73	c.Source = rs
 74	if pk == "" {
 75		anonAccess = "read-write"
 76	} else {
 77		anonAccess = "no-access"
 78	}
 79	if host == "" {
 80		displayHost = "localhost"
 81	} else {
 82		displayHost = host
 83	}
 84	yamlConfig := fmt.Sprintf(defaultConfig, displayHost, port, anonAccess)
 85	if pk != "" {
 86		pks := ""
 87		for _, key := range strings.Split(strings.TrimSpace(pk), "\n") {
 88			pks += fmt.Sprintf("      - %s\n", key)
 89		}
 90		yamlUsers = fmt.Sprintf(hasKeyUserConfig, pks)
 91	} else {
 92		yamlUsers = defaultUserConfig
 93	}
 94	yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig)
 95	err := c.createDefaultConfigRepo(yaml)
 96	if err != nil {
 97		return nil, err
 98	}
 99	return c, nil
100}
101
102// Reload reloads the configuration.
103func (cfg *Config) Reload() error {
104	err := cfg.Source.LoadRepos()
105	if err != nil {
106		return err
107	}
108	cr, err := cfg.Source.GetRepo("config")
109	if err != nil {
110		return err
111	}
112	cs, err := cr.LatestFile("config.yaml")
113	if err != nil {
114		return err
115	}
116	err = yaml.Unmarshal([]byte(cs), cfg)
117	if err != nil {
118		return fmt.Errorf("bad yaml in config.yaml: %s", err)
119	}
120	return nil
121}
122
123func createFile(path string, content string) error {
124	f, err := os.Create(path)
125	if err != nil {
126		return err
127	}
128	defer f.Close()
129	_, err = f.WriteString(content)
130	if err != nil {
131		return err
132	}
133	return f.Sync()
134}
135
136func (cfg *Config) createDefaultConfigRepo(yaml string) error {
137	cn := "config"
138	rs := cfg.Source
139	err := rs.LoadRepos()
140	if err != nil {
141		return err
142	}
143	_, err = rs.GetRepo(cn)
144	if err == git.ErrMissingRepo {
145		cr, err := rs.InitRepo(cn, true)
146		if err != nil {
147			return err
148		}
149		wt, err := cr.Repository.Worktree()
150		if err != nil {
151			return err
152		}
153		rm, err := wt.Filesystem.Create("README.md")
154		if err != nil {
155			return err
156		}
157		_, err = rm.Write([]byte(defaultReadme))
158		if err != nil {
159			return err
160		}
161		cf, err := wt.Filesystem.Create("config.yaml")
162		if err != nil {
163			return err
164		}
165		_, err = cf.Write([]byte(yaml))
166		if err != nil {
167			return err
168		}
169		_, err = wt.Add("README.md")
170		if err != nil {
171			return err
172		}
173		_, err = wt.Add("config.yaml")
174		if err != nil {
175			return err
176		}
177		_, err = wt.Commit("Default init", &gg.CommitOptions{
178			All: true,
179			Author: &object.Signature{
180				Name:  "Soft Serve Server",
181				Email: "vt100@charm.sh",
182			},
183		})
184		if err != nil {
185			return err
186		}
187		err = cr.Repository.Push(&gg.PushOptions{})
188		if err != nil {
189			return err
190		}
191		cmd := exec.Command("git", "update-server-info")
192		cmd.Dir = filepath.Join(rs.Path, cn)
193		err = cmd.Run()
194		if err != nil {
195			return err
196		}
197	} else if err != nil {
198		return err
199	}
200	return cfg.Reload()
201}
202
203func (cfg *Config) isPrivate(repo string) bool {
204	for _, r := range cfg.Repos {
205		if r.Repo == repo {
206			return r.Private
207		}
208	}
209	return false
210}