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