main.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "path/filepath"
  6. "strings"
  7. log "github.com/sirupsen/logrus"
  8. "github.com/OliveTin/OliveTin/internal/api"
  9. "github.com/OliveTin/OliveTin/internal/auth"
  10. "github.com/OliveTin/OliveTin/internal/entities"
  11. "github.com/OliveTin/OliveTin/internal/executor"
  12. "github.com/OliveTin/OliveTin/internal/httpservers"
  13. "github.com/OliveTin/OliveTin/internal/installationinfo"
  14. "github.com/OliveTin/OliveTin/internal/oncalendarfile"
  15. "github.com/OliveTin/OliveTin/internal/oncron"
  16. "github.com/OliveTin/OliveTin/internal/onfileindir"
  17. "github.com/OliveTin/OliveTin/internal/onstartup"
  18. "github.com/OliveTin/OliveTin/internal/servicehost"
  19. updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
  20. "os"
  21. "strconv"
  22. config "github.com/OliveTin/OliveTin/internal/config"
  23. "github.com/knadh/koanf/parsers/yaml"
  24. "github.com/knadh/koanf/providers/env"
  25. "github.com/knadh/koanf/providers/file"
  26. "github.com/knadh/koanf/v2"
  27. )
  28. var (
  29. cfg *config.Config
  30. cli cliOptions
  31. version = "dev"
  32. commit = "nocommit"
  33. date = "nodate"
  34. syntaxCheckSearchResults []string
  35. syntaxCheckLoadError error
  36. )
  37. type cliOptions struct {
  38. configDir string
  39. syntaxCheck bool
  40. startupTrace bool
  41. printVersion bool
  42. }
  43. func init() {
  44. // Parse flags before initLog so -startuptrace can enable TRACE before config loads.
  45. cli = parseCliFlags()
  46. initLog()
  47. handleEarlyCliFlags()
  48. initConfig(cli.configDir, !cli.syntaxCheck)
  49. quietSyntaxCheckLogs()
  50. initCheckEnvironment()
  51. initInstallationInfo()
  52. log.Info("OliveTin initialization complete")
  53. }
  54. func initLog() {
  55. logFormat := os.Getenv("OLIVETIN_LOG_FORMAT")
  56. if logFormat == "json" {
  57. log.SetFormatter(&log.JSONFormatter{})
  58. } else {
  59. log.SetFormatter(&log.TextFormatter{
  60. ForceQuote: true,
  61. DisableTimestamp: true,
  62. })
  63. }
  64. if cli.startupTrace {
  65. log.SetLevel(log.TraceLevel)
  66. return
  67. }
  68. log.SetLevel(log.InfoLevel)
  69. }
  70. func parseCliFlags() cliOptions {
  71. options := cliOptions{}
  72. flag.StringVar(&options.configDir, "configdir", ".", "Config directory path")
  73. flag.BoolVar(&options.printVersion, "version", false, "Prints the version number and exits")
  74. flag.BoolVar(&options.syntaxCheck, "syntaxcheck", false, "Check configuration for issues and exit")
  75. flag.BoolVar(&options.startupTrace, "startuptrace", false, "Enable TRACE logging before configuration is loaded")
  76. flag.Parse()
  77. return options
  78. }
  79. func handleEarlyCliFlags() {
  80. // This log message should be the first log message OliveTin prints.
  81. if cli.printVersion {
  82. logStartupMessage("OliveTin is just printing the startup message")
  83. os.Exit(1)
  84. }
  85. if cli.syntaxCheck {
  86. // Suppress routine logs before any further startup output. Re-applied
  87. // after config load because sanitize may raise the level again.
  88. log.SetLevel(log.FatalLevel)
  89. return
  90. }
  91. logStartupMessage("OliveTin initializing")
  92. log.WithFields(log.Fields{
  93. "value": cli.configDir,
  94. }).Debugf("Value of -configdir flag")
  95. }
  96. // quietSyntaxCheckLogs keeps -syntaxcheck output limited to the report on
  97. // stdout. Load failures are returned to the syntax-check report instead of
  98. // exiting immediately via Fatalf.
  99. func quietSyntaxCheckLogs() {
  100. if cli.syntaxCheck {
  101. log.SetLevel(log.FatalLevel)
  102. }
  103. }
  104. func failConfigLoad(format string, args ...any) {
  105. err := fmt.Errorf(format, args...)
  106. if cli.syntaxCheck {
  107. if syntaxCheckLoadError == nil {
  108. syntaxCheckLoadError = err
  109. }
  110. return
  111. }
  112. log.Fatal(err)
  113. }
  114. func getBasePort() int {
  115. var err error
  116. defaultPort := 1337
  117. basePort := defaultPort
  118. envPort := os.Getenv("PORT")
  119. if envPort != "" {
  120. basePort, err = strconv.Atoi(os.Getenv("PORT"))
  121. if err != nil {
  122. log.Errorf("Error converting port to int. %s", err)
  123. os.Exit(1)
  124. }
  125. }
  126. if defaultPort != basePort {
  127. log.WithFields(log.Fields{
  128. "basePort": basePort,
  129. }).Debug("Base port")
  130. }
  131. return basePort
  132. }
  133. func getConfigPath(directory string) string {
  134. joinedPath := filepath.Join(directory, "config.yaml")
  135. configPath, err := filepath.Abs(joinedPath)
  136. if err != nil {
  137. log.WithError(err).Warnf("Error getting absolute path for %s", joinedPath)
  138. return joinedPath
  139. }
  140. return configPath
  141. }
  142. func configSearchDirectories(configDir string) []string {
  143. directories := []string{configDir}
  144. // Only load additional configs if not in integration test mode
  145. absConfigDir, _ := filepath.Abs(configDir)
  146. if strings.Contains(absConfigDir, "integration-tests") {
  147. return directories
  148. }
  149. return append(directories,
  150. servicehost.GetConfigFilePath(),
  151. "/config", // For containers.
  152. "/etc/OliveTin/",
  153. )
  154. }
  155. func configPathExists(configPath string) bool {
  156. _, err := os.Stat(configPath)
  157. found := err == nil
  158. log.WithFields(log.Fields{
  159. "configPath": configPath,
  160. "found": found,
  161. }).Debug("Checking base config path")
  162. return found
  163. }
  164. func watchConfigFile(k *koanf.Koanf, f *file.File, configPath string) {
  165. err := f.Watch(func(evt any, err error) {
  166. log.Infof("config file changed: %v", evt)
  167. errLoad := k.Load(f, yaml.Parser())
  168. if errLoad != nil {
  169. log.WithFields(log.Fields{
  170. "error": errLoad,
  171. }).Fatalf("Error loading config file")
  172. }
  173. config.AppendSource(cfg, k, configPath)
  174. })
  175. if err != nil {
  176. log.WithFields(log.Fields{
  177. "error": err,
  178. }).Fatalf("Error watching config file")
  179. }
  180. }
  181. func loadConfigFromPath(k *koanf.Koanf, configPath string, watch bool) bool {
  182. log.WithFields(log.Fields{
  183. "configPath": configPath,
  184. }).Info("Loading config from path")
  185. f := file.Provider(configPath)
  186. if err := k.Load(f, yaml.Parser()); err != nil {
  187. failConfigLoad("error loading config from %s: %v", configPath, err)
  188. return false
  189. }
  190. if watch {
  191. watchConfigFile(k, f, configPath)
  192. }
  193. return true
  194. }
  195. func findAndLoadBaseConfig(k *koanf.Koanf, directories []string, watch bool) string {
  196. for _, directory := range directories {
  197. configPath := getConfigPath(directory)
  198. found := configPathExists(configPath)
  199. printConfigSearchResult(configPath, found)
  200. if !found {
  201. continue
  202. }
  203. if !loadConfigFromPath(k, configPath, watch) {
  204. return ""
  205. }
  206. return configPath
  207. }
  208. return ""
  209. }
  210. func printConfigSearchResult(configPath string, found bool) {
  211. if !cli.syntaxCheck {
  212. return
  213. }
  214. if found {
  215. syntaxCheckSearchResults = append(syntaxCheckSearchResults, "Found config file: "+configPath)
  216. return
  217. }
  218. syntaxCheckSearchResults = append(syntaxCheckSearchResults, "Config file not found: "+configPath)
  219. }
  220. func initConfig(configDir string, watch bool) {
  221. k := koanf.New(".")
  222. err := k.Load(env.Provider(".", ".", nil), nil)
  223. if err != nil {
  224. failConfigLoad("Error loading environment variables: %v", err)
  225. return
  226. }
  227. baseConfigPath := findAndLoadBaseConfig(k, configSearchDirectories(configDir), watch)
  228. cfg = config.DefaultConfigWithBasePort(getBasePort())
  229. if baseConfigPath == "" {
  230. if syntaxCheckLoadError == nil {
  231. failConfigLoad("No base config file found")
  232. }
  233. return
  234. }
  235. config.AppendSource(cfg, k, baseConfigPath)
  236. }
  237. func initInstallationInfo() {
  238. installationinfo.Config = cfg
  239. installationinfo.Build.Version = version
  240. installationinfo.Build.Commit = commit
  241. installationinfo.Build.Date = date
  242. }
  243. func logStartupMessage(message string) {
  244. log.WithFields(log.Fields{
  245. "version": version,
  246. "commit": commit,
  247. "date": date,
  248. }).Info(message)
  249. }
  250. func initCheckEnvironment() {
  251. warnIfPuidGuid()
  252. }
  253. func warnIfPuidGuid() {
  254. if os.Getenv("PUID") != "" || os.Getenv("PGID") != "" {
  255. log.Warnf("PUID or PGID seem to be set to something, but they are ignored by OliveTin. Please check https://docs.olivetin.app/no-puid-pgid.html")
  256. }
  257. }
  258. func main() {
  259. if cli.syntaxCheck {
  260. runSyntaxCheck()
  261. return
  262. }
  263. servicehost.Start(cfg.ServiceHostMode, cfg.ServiceLogs.Directory)
  264. log.WithFields(log.Fields{
  265. "configDir": cfg.GetDir(),
  266. }).Infof("OliveTin started")
  267. log.Debugf("Config: %+v", cfg)
  268. executor := executor.DefaultExecutor(cfg)
  269. executor.RebuildActionMap()
  270. config.AddListener(executor.RebuildActionMap)
  271. config.AddListener(func() {
  272. entities.SyncEntityFileWatchers(cfg)
  273. })
  274. executor.LoadLogsFromDisk()
  275. api.RegisterExecutorListener(executor)
  276. entities.AddListener(executor.RebuildActionMap)
  277. go onstartup.Execute(cfg, executor)
  278. go oncron.Schedule(cfg, executor)
  279. go onfileindir.WatchFilesInDirectory(cfg, executor)
  280. go oncalendarfile.Schedule(cfg, executor)
  281. go entities.SyncEntityFileWatchers(cfg)
  282. go updatecheck.StartUpdateChecker(cfg)
  283. // Load persistent sessions from disk
  284. auth.LoadUserSessions(cfg)
  285. httpservers.StartFrontendMux(cfg, executor)
  286. }