blob: 8cfa72f0b04e32dde8d8f7b22eff68fda6da19f7 [file] [log] [blame]
gio0eaf2712024-04-14 13:08:46 +04001package welcome
2
3import (
4 "encoding/json"
gio9d66f322024-07-06 13:45:10 +04005 "errors"
gio0eaf2712024-04-14 13:08:46 +04006 "fmt"
7 "io"
gio9d66f322024-07-06 13:45:10 +04008 "io/fs"
gio0eaf2712024-04-14 13:08:46 +04009 "net/http"
10 "strings"
gio9d66f322024-07-06 13:45:10 +040011 "sync"
gio0eaf2712024-04-14 13:08:46 +040012 "time"
13
14 "github.com/giolekva/pcloud/core/installer"
15 "github.com/giolekva/pcloud/core/installer/soft"
gio33059762024-07-05 13:19:07 +040016
17 "github.com/gorilla/mux"
gio0eaf2712024-04-14 13:08:46 +040018)
19
gio9d66f322024-07-06 13:45:10 +040020const (
21 configRepoName = "config"
22 namespacesFile = "/namespaces.json"
23)
24
gio0eaf2712024-04-14 13:08:46 +040025type DodoAppServer struct {
gio9d66f322024-07-06 13:45:10 +040026 l sync.Locker
gio33059762024-07-05 13:19:07 +040027 port int
28 self string
29 sshKey string
30 gitRepoPublicKey string
31 client soft.Client
32 namespace string
33 env installer.EnvConfig
34 nsc installer.NamespaceCreator
35 jc installer.JobCreator
36 workers map[string]map[string]struct{}
37 appNs map[string]string
gio0eaf2712024-04-14 13:08:46 +040038}
39
gio33059762024-07-05 13:19:07 +040040// TODO(gio): Initialize appNs on startup
gio0eaf2712024-04-14 13:08:46 +040041func NewDodoAppServer(
42 port int,
gio33059762024-07-05 13:19:07 +040043 self string,
gio0eaf2712024-04-14 13:08:46 +040044 sshKey string,
gio33059762024-07-05 13:19:07 +040045 gitRepoPublicKey string,
gio0eaf2712024-04-14 13:08:46 +040046 client soft.Client,
47 namespace string,
gio33059762024-07-05 13:19:07 +040048 nsc installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +040049 jc installer.JobCreator,
gio0eaf2712024-04-14 13:08:46 +040050 env installer.EnvConfig,
gio9d66f322024-07-06 13:45:10 +040051) (*DodoAppServer, error) {
52 if ok, err := client.RepoExists(configRepoName); err != nil {
53 return nil, err
54 } else if !ok {
55 if err := client.AddRepository(configRepoName); err != nil {
56 return nil, err
57 }
58 }
59 s := &DodoAppServer{
60 &sync.Mutex{},
gio0eaf2712024-04-14 13:08:46 +040061 port,
gio33059762024-07-05 13:19:07 +040062 self,
gio0eaf2712024-04-14 13:08:46 +040063 sshKey,
gio33059762024-07-05 13:19:07 +040064 gitRepoPublicKey,
gio0eaf2712024-04-14 13:08:46 +040065 client,
66 namespace,
67 env,
gio33059762024-07-05 13:19:07 +040068 nsc,
giof8843412024-05-22 16:38:05 +040069 jc,
gio266c04f2024-07-03 14:18:45 +040070 map[string]map[string]struct{}{},
gio33059762024-07-05 13:19:07 +040071 map[string]string{},
gio0eaf2712024-04-14 13:08:46 +040072 }
gio9d66f322024-07-06 13:45:10 +040073 config, err := client.GetRepo(configRepoName)
74 if err != nil {
75 return nil, err
76 }
77 r, err := config.Reader(namespacesFile)
78 if err == nil {
79 defer r.Close()
80 if err := json.NewDecoder(r).Decode(&s.appNs); err != nil {
81 return nil, err
82 }
83 } else if !errors.Is(err, fs.ErrNotExist) {
84 return nil, err
85 }
86 return s, nil
gio0eaf2712024-04-14 13:08:46 +040087}
88
89func (s *DodoAppServer) Start() error {
gio33059762024-07-05 13:19:07 +040090 r := mux.NewRouter()
91 r.HandleFunc("/update", s.handleUpdate)
92 r.HandleFunc("/register-worker", s.handleRegisterWorker).Methods(http.MethodPost)
93 r.HandleFunc("/api/apps", s.handleCreateApp).Methods(http.MethodPost)
94 r.HandleFunc("/api/add-admin-key", s.handleAddAdminKey).Methods(http.MethodPost)
95 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
gio0eaf2712024-04-14 13:08:46 +040096}
97
98type updateReq struct {
gio266c04f2024-07-03 14:18:45 +040099 Ref string `json:"ref"`
100 Repository struct {
101 Name string `json:"name"`
102 } `json:"repository"`
gio0eaf2712024-04-14 13:08:46 +0400103}
104
105func (s *DodoAppServer) handleUpdate(w http.ResponseWriter, r *http.Request) {
106 fmt.Println("update")
107 var req updateReq
108 var contents strings.Builder
109 io.Copy(&contents, r.Body)
110 c := contents.String()
111 fmt.Println(c)
112 if err := json.NewDecoder(strings.NewReader(c)).Decode(&req); err != nil {
113 fmt.Println(err)
114 return
115 }
gio9d66f322024-07-06 13:45:10 +0400116 if req.Ref != "refs/heads/master" || strings.HasPrefix(req.Repository.Name, configRepoName) {
gio0eaf2712024-04-14 13:08:46 +0400117 return
118 }
119 go func() {
120 time.Sleep(20 * time.Second)
gio33059762024-07-05 13:19:07 +0400121 if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name]); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400122 fmt.Println(err)
123 }
124 }()
gio266c04f2024-07-03 14:18:45 +0400125 for addr, _ := range s.workers[req.Repository.Name] {
gio0eaf2712024-04-14 13:08:46 +0400126 go func() {
127 // TODO(gio): make port configurable
128 http.Get(fmt.Sprintf("http://%s:3000/update", addr))
129 }()
130 }
131}
132
133type registerWorkerReq struct {
gio266c04f2024-07-03 14:18:45 +0400134 AppId string `json:"appId"`
gio0eaf2712024-04-14 13:08:46 +0400135 Address string `json:"address"`
136}
137
138func (s *DodoAppServer) handleRegisterWorker(w http.ResponseWriter, r *http.Request) {
139 var req registerWorkerReq
140 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
141 http.Error(w, err.Error(), http.StatusInternalServerError)
142 return
143 }
gio266c04f2024-07-03 14:18:45 +0400144 if _, ok := s.workers[req.AppId]; !ok {
145 s.workers[req.AppId] = map[string]struct{}{}
146 }
147 s.workers[req.AppId][req.Address] = struct{}{}
gio0eaf2712024-04-14 13:08:46 +0400148}
149
gio33059762024-07-05 13:19:07 +0400150type createAppReq struct {
151 AdminPublicKey string `json:"adminPublicKey"`
152}
153
154type createAppResp struct {
155 AppName string `json:"appName"`
156}
157
158func (s *DodoAppServer) handleCreateApp(w http.ResponseWriter, r *http.Request) {
159 var req createAppReq
160 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
161 http.Error(w, err.Error(), http.StatusBadRequest)
162 return
163 }
164 g := installer.NewFixedLengthRandomNameGenerator(3)
165 appName, err := g.Generate()
166 if err != nil {
167 http.Error(w, err.Error(), http.StatusInternalServerError)
168 return
169 }
170 if err := s.CreateApp(appName, req.AdminPublicKey); err != nil {
171 http.Error(w, err.Error(), http.StatusInternalServerError)
172 return
173 }
174 resp := createAppResp{appName}
175 if err := json.NewEncoder(w).Encode(resp); err != nil {
176 http.Error(w, err.Error(), http.StatusInternalServerError)
177 return
178 }
179}
180
181func (s *DodoAppServer) CreateApp(appName, adminPublicKey string) error {
gio9d66f322024-07-06 13:45:10 +0400182 s.l.Lock()
183 defer s.l.Unlock()
gio33059762024-07-05 13:19:07 +0400184 fmt.Printf("Creating app: %s\n", appName)
185 if ok, err := s.client.RepoExists(appName); err != nil {
186 return err
187 } else if ok {
188 return nil
189 }
190 if err := s.client.AddRepository(appName); err != nil {
191 return err
192 }
193 appRepo, err := s.client.GetRepo(appName)
194 if err != nil {
195 return err
196 }
197 if err := InitRepo(appRepo); err != nil {
198 return err
199 }
200 apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
201 app, err := installer.FindEnvApp(apps, "dodo-app-instance")
202 if err != nil {
203 return err
204 }
205 suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
206 suffix, err := suffixGen.Generate()
207 if err != nil {
208 return err
209 }
210 namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
211 s.appNs[appName] = namespace
212 if err := s.updateDodoApp(appName, namespace); err != nil {
213 return err
214 }
gio9d66f322024-07-06 13:45:10 +0400215 repo, err := s.client.GetRepo(configRepoName)
gio33059762024-07-05 13:19:07 +0400216 if err != nil {
217 return err
218 }
219 hf := installer.NewGitHelmFetcher()
220 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/")
221 if err != nil {
222 return err
223 }
gio9d66f322024-07-06 13:45:10 +0400224 if err := repo.Do(func(fs soft.RepoFS) (string, error) {
225 w, err := fs.Writer(namespacesFile)
226 if err != nil {
227 return "", err
228 }
229 defer w.Close()
230 if err := json.NewEncoder(w).Encode(s.appNs); err != nil {
231 return "", err
232 }
233 if _, err := m.Install(
234 app,
235 appName,
236 "/"+appName,
237 namespace,
238 map[string]any{
239 "repoAddr": s.client.GetRepoAddress(appName),
240 "repoHost": strings.Split(s.client.Address(), ":")[0],
241 "gitRepoPublicKey": s.gitRepoPublicKey,
242 },
243 installer.WithConfig(&s.env),
244 installer.WithNoPublish(),
245 installer.WithNoLock(),
246 ); err != nil {
247 return "", err
248 }
249 return fmt.Sprintf("Installed app: %s", appName), nil
250 }); err != nil {
gio33059762024-07-05 13:19:07 +0400251 return err
252 }
253 cfg, err := m.FindInstance(appName)
254 if err != nil {
255 return err
256 }
257 fluxKeys, ok := cfg.Input["fluxKeys"]
258 if !ok {
259 return fmt.Errorf("Fluxcd keys not found")
260 }
261 fluxPublicKey, ok := fluxKeys.(map[string]any)["public"]
262 if !ok {
263 return fmt.Errorf("Fluxcd keys not found")
264 }
265 if ok, err := s.client.UserExists("fluxcd"); err != nil {
266 return err
267 } else if ok {
268 if err := s.client.AddPublicKey("fluxcd", fluxPublicKey.(string)); err != nil {
269 return err
270 }
271 } else {
272 if err := s.client.AddUser("fluxcd", fluxPublicKey.(string)); err != nil {
273 return err
274 }
275 }
276 if err := s.client.AddReadOnlyCollaborator(appName, "fluxcd"); err != nil {
277 return err
278 }
279 if err := s.client.AddWebhook(appName, fmt.Sprintf("http://%s/update", s.self), "--active=true", "--events=push", "--content-type=json"); err != nil {
280 return err
281 }
282 if user, err := s.client.FindUser(adminPublicKey); err != nil {
283 return err
284 } else if user != "" {
285 if err := s.client.AddReadWriteCollaborator(appName, user); err != nil {
286 return err
287 }
288 } else {
289 if err := s.client.AddUser(appName, adminPublicKey); err != nil {
290 return err
291 }
292 if err := s.client.AddReadWriteCollaborator(appName, appName); err != nil {
293 return err
294 }
295 }
296 return nil
297}
298
gio70be3e52024-06-26 18:27:19 +0400299type addAdminKeyReq struct {
300 Public string `json:"public"`
301}
302
303func (s *DodoAppServer) handleAddAdminKey(w http.ResponseWriter, r *http.Request) {
304 var req addAdminKeyReq
305 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
306 http.Error(w, err.Error(), http.StatusBadRequest)
307 return
308 }
309 if err := s.client.AddPublicKey("admin", req.Public); err != nil {
310 http.Error(w, err.Error(), http.StatusInternalServerError)
311 return
312 }
313}
314
gio33059762024-07-05 13:19:07 +0400315func (s *DodoAppServer) updateDodoApp(name, namespace string) error {
316 repo, err := s.client.GetRepo(name)
gio0eaf2712024-04-14 13:08:46 +0400317 if err != nil {
318 return err
319 }
giof8843412024-05-22 16:38:05 +0400320 hf := installer.NewGitHelmFetcher()
gio33059762024-07-05 13:19:07 +0400321 m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
gio0eaf2712024-04-14 13:08:46 +0400322 if err != nil {
323 return err
324 }
325 appCfg, err := soft.ReadFile(repo, "app.cue")
gio0eaf2712024-04-14 13:08:46 +0400326 if err != nil {
327 return err
328 }
329 app, err := installer.NewDodoApp(appCfg)
330 if err != nil {
331 return err
332 }
giof8843412024-05-22 16:38:05 +0400333 lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
giof71a0832024-06-27 14:45:45 +0400334 if _, err := m.Install(
335 app,
336 "app",
337 "/.dodo/app",
338 namespace,
339 map[string]any{
gio33059762024-07-05 13:19:07 +0400340 "repoAddr": repo.FullAddress(),
341 "registerWorkerAddr": fmt.Sprintf("http://%s/register-worker", s.self),
342 "appId": name,
343 "sshPrivateKey": s.sshKey,
giof71a0832024-06-27 14:45:45 +0400344 },
gio33059762024-07-05 13:19:07 +0400345 installer.WithConfig(&s.env),
giof71a0832024-06-27 14:45:45 +0400346 installer.WithLocalChartGenerator(lg),
347 installer.WithBranch("dodo"),
348 installer.WithForce(),
349 ); err != nil {
gio0eaf2712024-04-14 13:08:46 +0400350 return err
351 }
352 return nil
353}
gio33059762024-07-05 13:19:07 +0400354
355const goMod = `module dodo.app
356
357go 1.18
358`
359
360const mainGo = `package main
361
362import (
363 "flag"
364 "fmt"
365 "log"
366 "net/http"
367)
368
369var port = flag.Int("port", 8080, "Port to listen on")
370
371func handler(w http.ResponseWriter, r *http.Request) {
372 fmt.Fprintln(w, "Hello from Dodo App!")
373}
374
375func main() {
376 flag.Parse()
377 http.HandleFunc("/", handler)
378 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
379}
380`
381
382const appCue = `app: {
383 type: "golang:1.22.0"
384 run: "main.go"
385 ingress: {
386 network: "Private" // or Public
387 subdomain: "testapp"
388 auth: enabled: false
389 }
390}
391`
392
393func InitRepo(repo soft.RepoIO) error {
394 return repo.Do(func(fs soft.RepoFS) (string, error) {
395 {
396 w, err := fs.Writer("go.mod")
397 if err != nil {
398 return "", err
399 }
400 defer w.Close()
401 fmt.Fprint(w, goMod)
402 }
403 {
404 w, err := fs.Writer("main.go")
405 if err != nil {
406 return "", err
407 }
408 defer w.Close()
409 fmt.Fprintf(w, "%s", mainGo)
410 }
411 {
412 w, err := fs.Writer("app.cue")
413 if err != nil {
414 return "", err
415 }
416 defer w.Close()
417 fmt.Fprint(w, appCue)
418 }
419 return "go web app template", nil
420 })
421}