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}