1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6 "context"
7 "errors"
8 "os"
9 "os/signal"
10 "strings"
11 "syscall"
12
13 "github.com/bnema/zerowrap"
14 "github.com/spf13/cobra"
15
16 "bnema.dev/ermite/internal/config"
17 "bnema.dev/ermite/internal/domain"
18)
19
20func newServeCmd() *cobra.Command {
21 return &cobra.Command{
22 Use: "serve",
23 Short: "Run the server",
24 Long: "Run the server. It is configured by the environment only:\n" +
25 config.EnvAddr + ", " + config.EnvDataDir + ", " + config.EnvPublicURL + ", " + config.EnvAdminToken + ",\n" +
26 config.EnvTrustedProxies + ", " + config.EnvGit + ", " + config.EnvSourceURL + ", " +
27 config.EnvVideoHosts + ", " + config.EnvLogLevel + ", " + config.EnvLogFormat + ",\n" +
28 config.EnvCommitterName + ", " + config.EnvCommitterEmail + ".",
29 Args: cobra.NoArgs,
30 RunE: func(cmd *cobra.Command, _ []string) error {
31 return serve(cmd.Context())
32 },
33 }
34}
35
36// serve wires the adapters and runs the server until SIGINT or SIGTERM.
37func serve(ctx context.Context) error {
38 cfg, err := loadConfig(os.Getenv)
39 if err != nil {
40 return err
41 }
42 // The logger is built from the validated settings, which carry the
43 // defaults of ERMITE_LOG_LEVEL and ERMITE_LOG_FORMAT.
44 log := zerowrap.New(zerowrap.Config{Level: cfg.LogLevel, Format: cfg.LogFormat})
45 ctx = zerowrap.WithCtx(ctx, log)
46 ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
47 defer stop()
48 // Restore the default signal handling as soon as the first signal
49 // arrives, so a second one kills the process during the shutdown.
50 go func() {
51 <-ctx.Done()
52 stop()
53 }()
54
55 log.Info().Str("version", version()).Str("addr", cfg.Addr).Str("data_dir", cfg.DataDir).Msg("starting")
56
57 if err := prepareDataDir(cfg.DataDir); err != nil {
58 return err
59 }
60 f, err := newForge(ctx, cfg, options{})
61 if err != nil {
62 return err
63 }
64 // The server has stopped by the time this runs; close even when ctx ended.
65 defer f.Close(context.WithoutCancel(ctx))
66
67 if err := f.Run(ctx); err != nil {
68 return err
69 }
70 log.Info().Msg("stopped")
71 return nil
72}
73
74// loadConfig reads the configuration and adds the checks that need the domain,
75// which config does not import: ERMITE_OWNER follows the rules of repository
76// names. Its problem joins the others in one report.
77func loadConfig(get func(string) string) (config.Config, error) {
78 cfg, err := config.Load(get)
79 var problems []string
80 var ce *config.Error
81 switch {
82 case errors.As(err, &ce):
83 problems = ce.Problems
84 case err != nil:
85 return config.Config{}, err
86 }
87 if owner := strings.TrimSpace(get(config.EnvOwner)); owner != "" {
88 if _, perr := domain.ParseRepoName(owner); perr != nil {
89 problems = append(problems, config.EnvOwner+": must follow the rules of repository names ("+perr.Error()+")")
90 }
91 }
92 if len(problems) > 0 {
93 return config.Config{}, &config.Error{Problems: problems}
94 }
95 return cfg, nil
96}