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}