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