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}