From 2cdf260f42d178d23a8db70db35664511aeab31e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 21 Jun 2023 13:50:26 +0800 Subject: [PATCH] Refactor path & config system (#25330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # The problem There were many "path tricks": * By default, Gitea uses its program directory as its work path * Gitea tries to use the "work path" to guess its "custom path" and "custom conf (app.ini)" * Users might want to use other directories as work path * The non-default work path should be passed to Gitea by GITEA_WORK_DIR or "--work-path" * But some Gitea processes are started without these values * The "serv" process started by OpenSSH server * The CLI sub-commands started by site admin * The paths are guessed by SetCustomPathAndConf again and again * The default values of "work path / custom path / custom conf" can be changed when compiling # The solution * Use `InitWorkPathAndCommonConfig` to handle these path tricks, and use test code to cover its behaviors. * When Gitea's web server runs, write the WORK_PATH to "app.ini", this value must be the most correct one, because if this value is not right, users would find that the web UI doesn't work and then they should be able to fix it. * Then all other sub-commands can use the WORK_PATH in app.ini to initialize their paths. * By the way, when Gitea starts for git protocol, it shouldn't output any log, otherwise the git protocol gets broken and client blocks forever. The "work path" priority is: WORK_PATH in app.ini > cmd arg --work-path > env var GITEA_WORK_DIR > builtin default The "app.ini" searching order is: cmd arg --config > cmd arg "work path / custom path" > env var "work path / custom path" > builtin default ## ⚠️ BREAKING If your instance's "work path / custom path / custom conf" doesn't meet the requirements (eg: work path must be absolute), Gitea will report a fatal error and exit. You need to set these values according to the error log. ---- Close #24818 Close #24222 Close #21606 Close #21498 Close #25107 Close #24981 Maybe close #24503 Replace #23301 Replace #22754 And maybe more --- .gitignore | 2 - cmd/actions.go | 2 +- cmd/cmd.go | 2 +- cmd/doctor.go | 2 +- cmd/dump.go | 2 +- cmd/embedded.go | 5 - cmd/mailer.go | 2 +- cmd/restore_repo.go | 2 +- cmd/serv.go | 2 +- cmd/web.go | 168 +++++++++------ .../environment-to-ini/environment-to-ini.go | 13 +- main.go | 171 ++++++++-------- models/migrations/base/tests.go | 2 +- models/unittest/testdb.go | 14 +- modules/doctor/doctor.go | 2 +- modules/doctor/paths.go | 2 +- modules/markup/html_test.go | 5 +- modules/markup/markdown/markdown_test.go | 5 +- modules/setting/config_provider.go | 80 ++++---- modules/setting/config_provider_test.go | 11 +- modules/setting/path.go | 191 ++++++++++++++++++ modules/setting/path_test.go | 151 ++++++++++++++ modules/setting/server.go | 2 +- modules/setting/setting.go | 133 ++---------- modules/ssh/ssh.go | 2 +- modules/testlogger/testlogger.go | 3 +- routers/init.go | 22 +- routers/install/install.go | 20 +- routers/install/setting.go | 51 ----- routers/web/admin/config.go | 15 -- templates/admin/config.tmpl | 9 - tests/test_utils.go | 20 +- 32 files changed, 652 insertions(+), 461 deletions(-) create mode 100644 modules/setting/path.go create mode 100644 modules/setting/path_test.go delete mode 100644 routers/install/setting.go diff --git a/.gitignore b/.gitignore index 581417df6..6851be742 100644 --- a/.gitignore +++ b/.gitignore @@ -53,8 +53,6 @@ cpu.out /bin /dist /custom/* -!/custom/conf -/custom/conf/* !/custom/conf/app.example.ini /data /indexers diff --git a/cmd/actions.go b/cmd/actions.go index 346de5b21..f52a91bd5 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -42,7 +42,7 @@ func runGenerateActionsRunnerToken(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() scope := c.String("scope") diff --git a/cmd/cmd.go b/cmd/cmd.go index b148007fb..8076aceca 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -58,7 +58,7 @@ func confirm() (bool, error) { } func initDB(ctx context.Context) error { - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() setting.InitSQLLoggersForCli(log.INFO) diff --git a/cmd/doctor.go b/cmd/doctor.go index b596e9ac0..b79436fc0 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -91,7 +91,7 @@ func runRecreateTable(ctx *cli.Context) error { golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) debug := ctx.Bool("debug") - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() if debug { diff --git a/cmd/dump.go b/cmd/dump.go index 7dda7fd2b..0b7c1d32c 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -182,7 +182,7 @@ func runDump(ctx *cli.Context) error { } fileName += "." + outType } - setting.Init(&setting.Options{}) + setting.MustInstalled() // make sure we are logging to the console no matter what the configuration tells us do to // FIXME: don't use CfgProvider directly diff --git a/cmd/embedded.go b/cmd/embedded.go index e51f8477b..204a623cf 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -99,11 +99,6 @@ type assetFile struct { func initEmbeddedExtractor(c *cli.Context) error { setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr) - // Read configuration file - setting.Init(&setting.Options{ - AllowEmpty: true, - }) - patterns, err := compileCollectPatterns(c.Args()) if err != nil { return err diff --git a/cmd/mailer.go b/cmd/mailer.go index 74bae1ab6..eaa5a1afe 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -16,7 +16,7 @@ func runSendMail(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() if err := argsSet(c, "title"); err != nil { return err diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 5a7ede493..c19e28f13 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -51,7 +51,7 @@ func runRestoreRepository(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setting.Init(&setting.Options{}) + setting.MustInstalled() var units []string if s := c.String("units"); s != "" { units = strings.Split(s, ",") diff --git a/cmd/serv.go b/cmd/serv.go index 87bf1cce2..01102d380 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -61,7 +61,7 @@ func setup(ctx context.Context, debug bool) { } else { setupConsoleLogger(log.FATAL, false, os.Stderr) } - setting.Init(&setting.Options{}) + setting.MustInstalled() if debug { setting.RunMode = "dev" } diff --git a/cmd/web.go b/cmd/web.go index 115024e8b..7a257a62a 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -101,6 +101,110 @@ func createPIDFile(pidPath string) { } } +func serveInstall(ctx *cli.Context) error { + log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith) + log.Info("App path: %s", setting.AppPath) + log.Info("Work path: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Config file: %s", setting.CustomConf) + log.Info("Prepare to run install page") + + routers.InitWebInstallPage(graceful.GetManager().HammerContext()) + + // Flag for port number in case first time run conflict + if ctx.IsSet("port") { + if err := setPort(ctx.String("port")); err != nil { + return err + } + } + if ctx.IsSet("install-port") { + if err := setPort(ctx.String("install-port")); err != nil { + return err + } + } + c := install.Routes() + err := listen(c, false) + if err != nil { + log.Critical("Unable to open listener for installer. Is Gitea already running?") + graceful.GetManager().DoGracefulShutdown() + } + select { + case <-graceful.GetManager().IsShutdown(): + <-graceful.GetManager().Done() + log.Info("PID: %d Gitea Web Finished", os.Getpid()) + log.GetManager().Close() + return err + default: + } + return nil +} + +func serveInstalled(ctx *cli.Context) error { + setting.InitCfgProvider(setting.CustomConf) + setting.LoadCommonSettings() + setting.MustInstalled() + + log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith) + log.Info("App path: %s", setting.AppPath) + log.Info("Work path: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Config file: %s", setting.CustomConf) + log.Info("Run mode: %s", setting.RunMode) + log.Info("Prepare to run web server") + + if setting.AppWorkPathMismatch { + log.Error("WORK_PATH from config %q doesn't match other paths from environment variables or command arguments. "+ + "Only WORK_PATH in config should be set and used. Please remove the other outdated work paths from environment variables and command arguments", setting.CustomConf) + } + + rootCfg := setting.CfgProvider + if rootCfg.Section("").Key("WORK_PATH").String() == "" { + saveCfg, err := rootCfg.PrepareSaving() + if err != nil { + log.Error("Unable to prepare saving WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err) + } else { + rootCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) + saveCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) + if err = saveCfg.Save(); err != nil { + log.Error("Unable to update WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err) + } + } + } + + routers.InitWebInstalled(graceful.GetManager().HammerContext()) + + // We check that AppDataPath exists here (it should have been created during installation) + // We can't check it in `InitWebInstalled`, because some integration tests + // use cmd -> InitWebInstalled, but the AppDataPath doesn't exist during those tests. + if _, err := os.Stat(setting.AppDataPath); err != nil { + log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath) + } + + // Override the provided port number within the configuration + if ctx.IsSet("port") { + if err := setPort(ctx.String("port")); err != nil { + return err + } + } + + // Set up Chi routes + c := routers.NormalRoutes() + err := listen(c, true) + <-graceful.GetManager().Done() + log.Info("PID: %d Gitea Web Finished", os.Getpid()) + log.GetManager().Close() + return err +} + +func servePprof() { + http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) + _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true) + // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it. + log.Info("Starting pprof server on localhost:6060") + log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) + finished() +} + func runWeb(ctx *cli.Context) error { if ctx.Bool("verbose") { setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout) @@ -128,75 +232,19 @@ func runWeb(ctx *cli.Context) error { createPIDFile(ctx.String("pid")) } - // Perform pre-initialization - needsInstall := install.PreloadSettings(graceful.GetManager().HammerContext()) - if needsInstall { - // Flag for port number in case first time run conflict - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { - return err - } - } - if ctx.IsSet("install-port") { - if err := setPort(ctx.String("install-port")); err != nil { - return err - } - } - c := install.Routes() - err := listen(c, false) - if err != nil { - log.Critical("Unable to open listener for installer. Is Gitea already running?") - graceful.GetManager().DoGracefulShutdown() - } - select { - case <-graceful.GetManager().IsShutdown(): - <-graceful.GetManager().Done() - log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() + if !setting.InstallLock { + if err := serveInstall(ctx); err != nil { return err - default: } } else { NoInstallListener() } if setting.EnablePprof { - go func() { - http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) - _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true) - // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it. - log.Info("Starting pprof server on localhost:6060") - log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) - finished() - }() + go servePprof() } - log.Info("Global init") - // Perform global initialization - setting.Init(&setting.Options{}) - routers.GlobalInitInstalled(graceful.GetManager().HammerContext()) - - // We check that AppDataPath exists here (it should have been created during installation) - // We can't check it in `GlobalInitInstalled`, because some integration tests - // use cmd -> GlobalInitInstalled, but the AppDataPath doesn't exist during those tests. - if _, err := os.Stat(setting.AppDataPath); err != nil { - log.Fatal("Can not find APP_DATA_PATH '%s'", setting.AppDataPath) - } - - // Override the provided port number within the configuration - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { - return err - } - } - - // Set up Chi routes - c := routers.NormalRoutes() - err := listen(c, true) - <-graceful.GetManager().Done() - log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() - return err + return serveInstalled(ctx) } func setPort(port string) error { diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index 3405d7d42..2cdf4e394 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -81,8 +81,6 @@ func main() { }, } app.Action = runEnvironmentToIni - setting.SetCustomPathAndConf("", "", "") - err := app.Run(os.Args) if err != nil { log.Fatal("Failed to run app with %s: %v", os.Args, err) @@ -90,12 +88,13 @@ func main() { } func runEnvironmentToIni(c *cli.Context) error { - providedCustom := c.String("custom-path") - providedConf := c.String("config") - providedWorkPath := c.String("work-path") - setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath) + setting.InitWorkPathAndCommonConfig(os.Getenv, setting.ArgWorkPathAndCustomConf{ + WorkPath: c.String("work-path"), + CustomPath: c.String("custom-path"), + CustomConf: c.String("config"), + }) - cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) } diff --git a/main.go b/main.go index 49093eb8a..1c87824c8 100644 --- a/main.go +++ b/main.go @@ -33,30 +33,58 @@ var ( Tags = "" // MakeVersion holds the current Make version if built with make MakeVersion = "" - - originalAppHelpTemplate = "" - originalCommandHelpTemplate = "" - originalSubcommandHelpTemplate = "" ) func init() { setting.AppVer = Version setting.AppBuiltWith = formatBuiltWith() setting.AppStartTime = time.Now().UTC() +} - // Grab the original help templates - originalAppHelpTemplate = cli.AppHelpTemplate - originalCommandHelpTemplate = cli.CommandHelpTemplate - originalSubcommandHelpTemplate = cli.SubcommandHelpTemplate +// cmdHelp is our own help subcommand with more information +// test cases: +// ./gitea help +// ./gitea -h +// ./gitea web help +// ./gitea web -h (due to cli lib limitation, this won't call our cmdHelp, so no extra info) +// ./gitea admin help auth +// ./gitea -c /tmp/app.ini -h +// ./gitea -c /tmp/app.ini help +// ./gitea help -c /tmp/app.ini +// GITEA_WORK_DIR=/tmp ./gitea help +// GITEA_WORK_DIR=/tmp ./gitea help --work-path /tmp/other +// GITEA_WORK_DIR=/tmp ./gitea help --config /tmp/app-other.ini +var cmdHelp = cli.Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *cli.Context) (err error) { + args := c.Args() + if args.Present() { + err = cli.ShowCommandHelp(c, args.First()) + } else { + err = cli.ShowAppHelp(c) + } + _, _ = fmt.Fprintf(c.App.Writer, ` +DEFAULT CONFIGURATION: + AppPath: %s + WorkPath: %s + CustomPath: %s + ConfigFile: %s + +`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf) + return err + }, } func main() { app := cli.NewApp() app.Name = "Gitea" app.Usage = "A painless self-hosted Git service" - app.Description = `By default, gitea will start serving using the webserver with no -arguments - which can alternatively be run by running the subcommand web.` + app.Description = `By default, Gitea will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".` app.Version = Version + formatBuiltWith() + app.EnableBashCompletion = true app.Commands = []cli.Command{ cmd.CmdWeb, cmd.CmdServ, @@ -77,118 +105,83 @@ arguments - which can alternatively be run by running the subcommand web.` cmd.CmdRestoreRepository, cmd.CmdActions, } - // Now adjust these commands to add our global configuration options - - // First calculate the default paths and set the AppHelpTemplates in this context - setting.SetCustomPathAndConf("", "", "") - setAppHelpTemplates() // default configuration flags - defaultFlags := []cli.Flag{ + globalFlags := []cli.Flag{ + cli.HelpFlag, cli.StringFlag{ Name: "custom-path, C", - Value: setting.CustomPath, - Usage: "Custom path file path", + Usage: "Set custom path (defaults to '{WorkPath}/custom')", }, cli.StringFlag{ Name: "config, c", Value: setting.CustomConf, - Usage: "Custom configuration file path", + Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')", }, - cli.VersionFlag, cli.StringFlag{ Name: "work-path, w", - Value: setting.AppWorkPath, - Usage: "Set the gitea working path", + Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)", }, } // Set the default to be equivalent to cmdWeb and add the default flags + app.Flags = append(app.Flags, globalFlags...) app.Flags = append(app.Flags, cmd.CmdWeb.Flags...) - app.Flags = append(app.Flags, defaultFlags...) - app.Action = cmd.CmdWeb.Action - - // Add functions to set these paths and these flags to the commands - app.Before = establishCustomPath + app.Action = prepareWorkPathAndCustomConf(cmd.CmdWeb.Action) + app.HideHelp = true // use our own help action to show helps (with more information like default config) + app.Commands = append(app.Commands, cmdHelp) for i := range app.Commands { - setFlagsAndBeforeOnSubcommands(&app.Commands[i], defaultFlags, establishCustomPath) + prepareSubcommands(&app.Commands[i], globalFlags) } - app.EnableBashCompletion = true - err := app.Run(os.Args) if err != nil { - log.Fatal("Failed to run app with %s: %v", os.Args, err) + _, _ = fmt.Fprintf(app.Writer, "\nFailed to run with %s: %v\n", os.Args, err) } log.GetManager().Close() } -func setFlagsAndBeforeOnSubcommands(command *cli.Command, defaultFlags []cli.Flag, before cli.BeforeFunc) { +func prepareSubcommands(command *cli.Command, defaultFlags []cli.Flag) { command.Flags = append(command.Flags, defaultFlags...) - command.Before = establishCustomPath + command.Action = prepareWorkPathAndCustomConf(command.Action) + command.HideHelp = true + if command.Name != "help" { + command.Subcommands = append(command.Subcommands, cmdHelp) + } for i := range command.Subcommands { - setFlagsAndBeforeOnSubcommands(&command.Subcommands[i], defaultFlags, before) + prepareSubcommands(&command.Subcommands[i], defaultFlags) } } -func establishCustomPath(ctx *cli.Context) error { - var providedCustom string - var providedConf string - var providedWorkPath string - - currentCtx := ctx - for { - if len(providedCustom) != 0 && len(providedConf) != 0 && len(providedWorkPath) != 0 { - break - } - if currentCtx == nil { - break - } - if currentCtx.IsSet("custom-path") && len(providedCustom) == 0 { - providedCustom = currentCtx.String("custom-path") - } - if currentCtx.IsSet("config") && len(providedConf) == 0 { - providedConf = currentCtx.String("config") - } - if currentCtx.IsSet("work-path") && len(providedWorkPath) == 0 { - providedWorkPath = currentCtx.String("work-path") - } - currentCtx = currentCtx.Parent() - +// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config +// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times +func prepareWorkPathAndCustomConf(a any) func(ctx *cli.Context) error { + if a == nil { + return nil } - setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath) - - setAppHelpTemplates() - - if ctx.IsSet("version") { - cli.ShowVersion(ctx) - os.Exit(0) + action := a.(func(*cli.Context) error) + return func(ctx *cli.Context) error { + var args setting.ArgWorkPathAndCustomConf + curCtx := ctx + for curCtx != nil { + if curCtx.IsSet("work-path") && args.WorkPath == "" { + args.WorkPath = curCtx.String("work-path") + } + if curCtx.IsSet("custom-path") && args.CustomPath == "" { + args.CustomPath = curCtx.String("custom-path") + } + if curCtx.IsSet("config") && args.CustomConf == "" { + args.CustomConf = curCtx.String("config") + } + curCtx = curCtx.Parent() + } + setting.InitWorkPathAndCommonConfig(os.Getenv, args) + if ctx.Bool("help") { + return cmdHelp.Action.(func(ctx *cli.Context) error)(ctx) + } + return action(ctx) } - - return nil -} - -func setAppHelpTemplates() { - cli.AppHelpTemplate = adjustHelpTemplate(originalAppHelpTemplate) - cli.CommandHelpTemplate = adjustHelpTemplate(originalCommandHelpTemplate) - cli.SubcommandHelpTemplate = adjustHelpTemplate(originalSubcommandHelpTemplate) -} - -func adjustHelpTemplate(originalTemplate string) string { - overridden := "" - if _, ok := os.LookupEnv("GITEA_CUSTOM"); ok { - overridden = "(GITEA_CUSTOM)" - } - - return fmt.Sprintf(`%s -DEFAULT CONFIGURATION: - CustomPath: %s %s - CustomConf: %s - AppPath: %s - AppWorkPath: %s - -`, originalTemplate, setting.CustomPath, overridden, setting.CustomConf, setting.AppPath, setting.AppWorkPath) } func formatBuiltWith() string { diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index dd99a1eda..c3100ba66 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -147,9 +147,9 @@ func MainTest(m *testing.M) { os.Exit(1) } + setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") setting.AppDataPath = tmpDataPath - setting.SetCustomPathAndConf("", "", "") unittest.InitSettings() if err = git.InitFull(context.Background()); err != nil { fmt.Printf("Unable to InitFull: %v\n", err) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 5351ff113..f926a6553 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -42,12 +42,14 @@ func fatalTestError(fmtStr string, args ...interface{}) { os.Exit(1) } -// InitSettings initializes config provider and load common setttings for tests +// InitSettings initializes config provider and load common settings for tests func InitSettings(extraConfigs ...string) { - setting.Init(&setting.Options{ - AllowEmpty: true, - ExtraConfig: strings.Join(extraConfigs, "\n"), - }) + if setting.CustomConf == "" { + setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") + _ = os.Remove(setting.CustomConf) + } + setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n")) + setting.LoadCommonSettings() if err := setting.PrepareAppDataPath(); err != nil { log.Fatalf("Can not prepare APP_DATA_PATH: %v", err) @@ -69,7 +71,7 @@ type TestOptions struct { // MainTest a reusable TestMain(..) function for unit tests that need to use a // test database. Creates the test database, and sets necessary settings. func MainTest(m *testing.M, testOpts *TestOptions) { - setting.SetCustomPathAndConf("", "", "") + setting.CustomPath = filepath.Join(testOpts.GiteaRootPath, "custom") InitSettings() var err error diff --git a/modules/doctor/doctor.go b/modules/doctor/doctor.go index 10838a751..ceee32285 100644 --- a/modules/doctor/doctor.go +++ b/modules/doctor/doctor.go @@ -28,7 +28,7 @@ type Check struct { } func initDBSkipLogger(ctx context.Context) error { - setting.Init(&setting.Options{}) + setting.MustInstalled() setting.LoadDBSetting() if err := db.InitEngine(ctx); err != nil { return fmt.Errorf("db.InitEngine: %w", err) diff --git a/modules/doctor/paths.go b/modules/doctor/paths.go index 957152349..3f62d587a 100644 --- a/modules/doctor/paths.go +++ b/modules/doctor/paths.go @@ -66,7 +66,7 @@ func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix boo return err } - setting.Init(&setting.Options{}) + setting.MustInstalled() configurationFiles := []configurationFile{ {"Configuration File Path", setting.CustomConf, false, true, false}, diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 5e5e4fecb..a8d7ba794 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -28,9 +29,7 @@ var localMetas = map[string]string{ } func TestMain(m *testing.M) { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) + unittest.InitSettings() if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 4bd2ca8d4..f2322b255 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -33,9 +34,7 @@ var localMetas = map[string]string{ } func TestMain(m *testing.M) { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) + unittest.InitSettings() if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index deec5cc58..94dd98985 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -55,15 +55,15 @@ type ConfigProvider interface { DisableSaving() PrepareSaving() (ConfigProvider, error) + IsLoadedFromEmpty() bool } type iniConfigProvider struct { - opts *Options + file string ini *ini.File - disableSaving bool - - newFile bool // whether the file has not existed previously + disableSaving bool // disable the "Save" method because the config options could be polluted + loadedFromEmpty bool // whether the file has not existed previously } type iniConfigSection struct { @@ -182,53 +182,43 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { } cfg.NameMapper = ini.SnackCase return &iniConfigProvider{ - ini: cfg, - newFile: true, + ini: cfg, + loadedFromEmpty: true, }, nil } -type Options struct { - CustomConf string // the ini file path - AllowEmpty bool // whether not finding configuration files is allowed - ExtraConfig string - - DisableLoadCommonSettings bool // only used by "Init()", not used by "NewConfigProvider()" -} - // NewConfigProviderFromFile load configuration from file. // NOTE: do not print any log except error. -func NewConfigProviderFromFile(opts *Options) (ConfigProvider, error) { +func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) { cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "}) - newFile := true + loadedFromEmpty := true - if opts.CustomConf != "" { - isFile, err := util.IsFile(opts.CustomConf) + if file != "" { + isFile, err := util.IsFile(file) if err != nil { - return nil, fmt.Errorf("unable to check if %s is a file. Error: %v", opts.CustomConf, err) + return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err) } if isFile { - if err := cfg.Append(opts.CustomConf); err != nil { - return nil, fmt.Errorf("failed to load custom conf '%s': %v", opts.CustomConf, err) + if err = cfg.Append(file); err != nil { + return nil, fmt.Errorf("failed to load config file %q: %v", file, err) } - newFile = false + loadedFromEmpty = false } } - if newFile && !opts.AllowEmpty { - return nil, fmt.Errorf("unable to find configuration file: %q, please ensure you are running in the correct environment or set the correct configuration file with -c", CustomConf) - } - - if opts.ExtraConfig != "" { - if err := cfg.Append([]byte(opts.ExtraConfig)); err != nil { - return nil, fmt.Errorf("unable to append more config: %v", err) + if len(extraConfigs) > 0 { + for _, s := range extraConfigs { + if err := cfg.Append([]byte(s)); err != nil { + return nil, fmt.Errorf("unable to append more config: %v", err) + } } } cfg.NameMapper = ini.SnackCase return &iniConfigProvider{ - opts: opts, - ini: cfg, - newFile: newFile, + file: file, + ini: cfg, + loadedFromEmpty: loadedFromEmpty, }, nil } @@ -266,20 +256,17 @@ func (p *iniConfigProvider) Save() error { if p.disableSaving { return errDisableSaving } - filename := p.opts.CustomConf + filename := p.file if filename == "" { - if !p.opts.AllowEmpty { - return fmt.Errorf("custom config path must not be empty") - } - return nil + return fmt.Errorf("config file path must not be empty") } - if p.newFile { + if p.loadedFromEmpty { if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { - return fmt.Errorf("failed to create '%s': %v", filename, err) + return fmt.Errorf("failed to create %q: %v", filename, err) } } if err := p.ini.SaveTo(filename); err != nil { - return fmt.Errorf("failed to save '%s': %v", filename, err) + return fmt.Errorf("failed to save %q: %v", filename, err) } // Change permissions to be more restrictive @@ -313,11 +300,14 @@ func (p *iniConfigProvider) DisableSaving() { // it makes the "Save" outputs a lot of garbage options // After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped. func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) { - cfgFile := p.opts.CustomConf - if cfgFile == "" { + if p.file == "" { return nil, errors.New("no config file to save") } - return NewConfigProviderFromFile(p.opts) + return NewConfigProviderFromFile(p.file) +} + +func (p *iniConfigProvider) IsLoadedFromEmpty() bool { + return p.loadedFromEmpty } func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) { @@ -356,8 +346,8 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro } iniFile.BlockMode = false return &iniConfigProvider{ - ini: iniFile, - newFile: true, + ini: iniFile, + loadedFromEmpty: true, }, nil } diff --git a/modules/setting/config_provider_test.go b/modules/setting/config_provider_test.go index c5c5196e0..7e7c6be2b 100644 --- a/modules/setting/config_provider_test.go +++ b/modules/setting/config_provider_test.go @@ -67,13 +67,14 @@ key = 123 } func TestNewConfigProviderFromFile(t *testing.T) { - _, err := NewConfigProviderFromFile(&Options{CustomConf: "no-such.ini", AllowEmpty: false}) - assert.ErrorContains(t, err, "unable to find configuration file") + cfg, err := NewConfigProviderFromFile("no-such.ini") + assert.NoError(t, err) + assert.True(t, cfg.IsLoadedFromEmpty()) // load non-existing file and save testFile := t.TempDir() + "/test.ini" testFile1 := t.TempDir() + "/test1.ini" - cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + cfg, err = NewConfigProviderFromFile(testFile) assert.NoError(t, err) sec, _ := cfg.NewSection("foo") @@ -91,7 +92,7 @@ func TestNewConfigProviderFromFile(t *testing.T) { assert.Equal(t, "[foo]\nk1 = a\nk2 = b\n", string(bs)) // load existing file and save - cfg, err = NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + cfg, err = NewConfigProviderFromFile(testFile) assert.NoError(t, err) assert.Equal(t, "a", cfg.Section("foo").Key("k1").String()) sec, _ = cfg.NewSection("bar") @@ -123,7 +124,7 @@ func TestNewConfigProviderForLocale(t *testing.T) { func TestDisableSaving(t *testing.T) { testFile := t.TempDir() + "/test.ini" _ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644) - cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + cfg, err := NewConfigProviderFromFile(testFile) assert.NoError(t, err) cfg.DisableSaving() diff --git a/modules/setting/path.go b/modules/setting/path.go new file mode 100644 index 000000000..91bb2e9bb --- /dev/null +++ b/modules/setting/path.go @@ -0,0 +1,191 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/log" +) + +var ( + // AppPath represents the path to the gitea binary + AppPath string + + // AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR. + // If that is not set it is the default set here by the linker or failing that the directory of AppPath. + // It is used as the base path for several other paths. + AppWorkPath string + CustomPath string // Custom directory path. Env: GITEA_CUSTOM + CustomConf string + + appWorkPathBuiltin string + customPathBuiltin string + customConfBuiltin string + + AppWorkPathMismatch bool +) + +func getAppPath() (string, error) { + var appPath string + var err error + if IsWindows && filepath.IsAbs(os.Args[0]) { + appPath = filepath.Clean(os.Args[0]) + } else { + appPath, err = exec.LookPath(os.Args[0]) + } + if err != nil { + if !errors.Is(err, exec.ErrDot) { + return "", err + } + appPath, err = filepath.Abs(os.Args[0]) + } + if err != nil { + return "", err + } + appPath, err = filepath.Abs(appPath) + if err != nil { + return "", err + } + // Note: (legacy code) we don't use path.Dir here because it does not handle case which path starts with two "/" in Windows: "//psf/Home/..." + return strings.ReplaceAll(appPath, "\\", "/"), err +} + +func init() { + var err error + if AppPath, err = getAppPath(); err != nil { + log.Fatal("Failed to get app path: %v", err) + } + + if AppWorkPath == "" { + AppWorkPath = filepath.Dir(AppPath) + } + + appWorkPathBuiltin = AppWorkPath + customPathBuiltin = CustomPath + customConfBuiltin = CustomConf +} + +type ArgWorkPathAndCustomConf struct { + WorkPath string + CustomPath string + CustomConf string +} + +type stringWithDefault struct { + Value string + IsSet bool +} + +func (s *stringWithDefault) Set(v string) { + s.Value = v + s.IsSet = true +} + +// InitWorkPathAndCommonConfig will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf and load common settings, +func InitWorkPathAndCommonConfig(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) { + tryAbsPath := func(paths ...string) string { + s := paths[len(paths)-1] + for i := len(paths) - 2; i >= 0; i-- { + if filepath.IsAbs(s) { + break + } + s = filepath.Join(paths[i], s) + } + return s + } + + var err error + tmpWorkPath := stringWithDefault{Value: appWorkPathBuiltin} + if tmpWorkPath.Value == "" { + tmpWorkPath.Value = filepath.Dir(AppPath) + } + tmpCustomPath := stringWithDefault{Value: customPathBuiltin} + if tmpCustomPath.Value == "" { + tmpCustomPath.Value = "custom" + } + tmpCustomConf := stringWithDefault{Value: customConfBuiltin} + if tmpCustomConf.Value == "" { + tmpCustomConf.Value = "conf/app.ini" + } + + readFromEnv := func() { + envWorkPath := getEnvFn("GITEA_WORK_DIR") + if envWorkPath != "" { + tmpWorkPath.Set(envWorkPath) + if !filepath.IsAbs(tmpWorkPath.Value) { + log.Fatal("GITEA_WORK_DIR (work path) must be absolute path") + } + } + + envCustomPath := getEnvFn("GITEA_CUSTOM") + if envCustomPath != "" { + tmpCustomPath.Set(envCustomPath) + if !filepath.IsAbs(tmpCustomPath.Value) { + log.Fatal("GITEA_CUSTOM (custom path) must be absolute path") + } + } + } + + readFromArgs := func() { + if args.WorkPath != "" { + tmpWorkPath.Set(args.WorkPath) + if !filepath.IsAbs(tmpWorkPath.Value) { + log.Fatal("--work-path must be absolute path") + } + } + if args.CustomPath != "" { + tmpCustomPath.Set(args.CustomPath) // if it is not abs, it will be based on work-path, it shouldn't happen + if !filepath.IsAbs(tmpCustomPath.Value) { + log.Error("--custom-path must be absolute path") + } + } + if args.CustomConf != "" { + tmpCustomConf.Set(args.CustomConf) + if !filepath.IsAbs(tmpCustomConf.Value) { + // the config path can be relative to the real current working path + if tmpCustomConf.Value, err = filepath.Abs(tmpCustomConf.Value); err != nil { + log.Fatal("Failed to get absolute path of config %q: %v", tmpCustomConf.Value, err) + } + } + } + } + + readFromEnv() + readFromArgs() + + if !tmpCustomConf.IsSet { + tmpCustomConf.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value, tmpCustomConf.Value)) + } + + // only read the config but do not load/init anything more, because the AppWorkPath and CustomPath are not ready + InitCfgProvider(tmpCustomConf.Value) + configWorkPath := ConfigSectionKeyString(CfgProvider.Section(""), "WORK_PATH") + if configWorkPath != "" { + if !filepath.IsAbs(configWorkPath) { + log.Fatal("WORK_PATH in %q must be absolute path", configWorkPath) + } + configWorkPath = filepath.Clean(configWorkPath) + if tmpWorkPath.Value != "" && (getEnvFn("GITEA_WORK_DIR") != "" || args.WorkPath != "") { + fi1, err1 := os.Stat(tmpWorkPath.Value) + fi2, err2 := os.Stat(configWorkPath) + if err1 != nil || err2 != nil || !os.SameFile(fi1, fi2) { + AppWorkPathMismatch = true + } + } + tmpWorkPath.Set(configWorkPath) + } + + tmpCustomPath.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value)) + + AppWorkPath = tmpWorkPath.Value + CustomPath = tmpCustomPath.Value + CustomConf = tmpCustomConf.Value + + LoadCommonSettings() +} diff --git a/modules/setting/path_test.go b/modules/setting/path_test.go new file mode 100644 index 000000000..fc6a2116d --- /dev/null +++ b/modules/setting/path_test.go @@ -0,0 +1,151 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +type envVars map[string]string + +func (e envVars) Getenv(key string) string { + return e[key] +} + +func TestInitWorkPathAndCommonConfig(t *testing.T) { + testInit := func(defaultWorkPath, defaultCustomPath, defaultCustomConf string) { + AppWorkPathMismatch = false + AppWorkPath = defaultWorkPath + appWorkPathBuiltin = defaultWorkPath + CustomPath = defaultCustomPath + customPathBuiltin = defaultCustomPath + CustomConf = defaultCustomConf + customConfBuiltin = defaultCustomConf + } + + fp := filepath.Join + + tmpDir := t.TempDir() + dirFoo := fp(tmpDir, "foo") + dirBar := fp(tmpDir, "bar") + dirXxx := fp(tmpDir, "xxx") + dirYyy := fp(tmpDir, "yyy") + + t.Run("Default", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("WorkDir(env)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirBar, AppWorkPath) + assert.Equal(t, fp(dirBar, "custom"), CustomPath) + assert.Equal(t, fp(dirBar, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("WorkDir(env,arg)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirXxx}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, fp(dirXxx, "custom/conf/app.ini"), CustomConf) + }) + + t.Run("CustomPath(env)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirBar, "custom1"), CustomPath) + assert.Equal(t, fp(dirBar, "custom1/conf/app.ini"), CustomConf) + }) + + t.Run("CustomPath(env,arg)", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{CustomPath: "custom2"}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom2"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom2/conf/app.ini"), CustomConf) + }) + + t.Run("CustomConf", func(t *testing.T) { + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: "app1.ini"}) + assert.Equal(t, dirFoo, AppWorkPath) + cwd, _ := os.Getwd() + assert.Equal(t, fp(cwd, "app1.ini"), CustomConf) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: fp(dirBar, "app1.ini")}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirBar, "app1.ini"), CustomConf) + }) + + t.Run("CustomConfOverrideWorkPath", func(t *testing.T) { + iniWorkPath := fp(tmpDir, "app-workpath.ini") + _ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.False(t, AppWorkPathMismatch) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.True(t, AppWorkPathMismatch) + + testInit(dirFoo, "", "") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirBar, CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + assert.True(t, AppWorkPathMismatch) + }) + + t.Run("Builtin", func(t *testing.T) { + testInit(dirFoo, dirBar, dirXxx) + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, dirBar, CustomPath) + assert.Equal(t, dirXxx, CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, fp(dirFoo, "custom1"), CustomPath) + assert.Equal(t, fp(dirFoo, "custom1/cfg.ini"), CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirYyy}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirYyy, AppWorkPath) + assert.Equal(t, fp(dirYyy, "custom1"), CustomPath) + assert.Equal(t, fp(dirYyy, "custom1/cfg.ini"), CustomConf) + + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": dirYyy}.Getenv, ArgWorkPathAndCustomConf{}) + assert.Equal(t, dirFoo, AppWorkPath) + assert.Equal(t, dirYyy, CustomPath) + assert.Equal(t, fp(dirYyy, "cfg.ini"), CustomConf) + + iniWorkPath := fp(tmpDir, "app-workpath.ini") + _ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644) + testInit(dirFoo, "custom1", "cfg.ini") + InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath}) + assert.Equal(t, dirXxx, AppWorkPath) + assert.Equal(t, fp(dirXxx, "custom1"), CustomPath) + assert.Equal(t, iniWorkPath, CustomConf) + }) +} diff --git a/modules/setting/server.go b/modules/setting/server.go index d937faca1..7c033bcc6 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -61,6 +61,7 @@ var ( AssetVersion string // Server settings + Protocol Scheme UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` @@ -324,7 +325,6 @@ func loadServerFrom(rootCfg ConfigProvider) { StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) if !filepath.IsAbs(AppDataPath) { - log.Info("The provided APP_DATA_PATH: %s is not absolute - it will be made absolute against the work path: %s", AppDataPath, AppWorkPath) AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath)) } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 6eaddbe2b..0d69847db 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -5,12 +5,8 @@ package setting import ( - "errors" "fmt" "os" - "os/exec" - "path" - "path/filepath" "runtime" "strings" "time" @@ -28,19 +24,9 @@ var ( // AppStartTime store time gitea has started AppStartTime time.Time - // AppPath represents the path to the gitea binary - AppPath string - // AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR. - // If that is not set it is the default set here by the linker or failing that the directory of AppPath. - // - // AppWorkPath is used as the base path for several other paths. - AppWorkPath string - // Other global setting objects CfgProvider ConfigProvider - CustomPath string // Custom directory path - CustomConf string RunMode string RunUser string IsProd bool @@ -51,62 +37,6 @@ var ( IsInTesting = false ) -func getAppPath() (string, error) { - var appPath string - var err error - if IsWindows && filepath.IsAbs(os.Args[0]) { - appPath = filepath.Clean(os.Args[0]) - } else { - appPath, err = exec.LookPath(os.Args[0]) - } - - if err != nil { - if !errors.Is(err, exec.ErrDot) { - return "", err - } - appPath, err = filepath.Abs(os.Args[0]) - } - if err != nil { - return "", err - } - appPath, err = filepath.Abs(appPath) - if err != nil { - return "", err - } - // Note: we don't use path.Dir here because it does not handle case - // which path starts with two "/" in Windows: "//psf/Home/..." - return strings.ReplaceAll(appPath, "\\", "/"), err -} - -func getWorkPath(appPath string) string { - workPath := AppWorkPath - - if giteaWorkPath, ok := os.LookupEnv("GITEA_WORK_DIR"); ok { - workPath = giteaWorkPath - } - if len(workPath) == 0 { - i := strings.LastIndex(appPath, "/") - if i == -1 { - workPath = appPath - } else { - workPath = appPath[:i] - } - } - workPath = strings.ReplaceAll(workPath, "\\", "/") - if !filepath.IsAbs(workPath) { - log.Info("Provided work path %s is not absolute - will be made absolute against the current working directory", workPath) - - absPath, err := filepath.Abs(workPath) - if err != nil { - log.Error("Unable to absolute %s against the current working directory %v. Will absolute against the AppPath %s", workPath, err, appPath) - workPath = filepath.Join(appPath, workPath) - } else { - workPath = absPath - } - } - return strings.ReplaceAll(workPath, "\\", "/") -} - func init() { IsWindows = runtime.GOOS == "windows" if AppVer == "" { @@ -116,12 +46,6 @@ func init() { // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically // By default set this logger at Info - we'll change it later, but we need to start with something. log.SetConsoleLogger(log.DEFAULT, "console", log.INFO) - - var err error - if AppPath, err = getAppPath(); err != nil { - log.Fatal("Failed to get app path: %v", err) - } - AppWorkPath = getWorkPath(AppPath) } // IsRunUserMatchCurrentUser returns false if configured run user does not match @@ -137,36 +61,6 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) { return currentUser, runUser == currentUser } -// SetCustomPathAndConf will set CustomPath and CustomConf with reference to the -// GITEA_CUSTOM environment variable and with provided overrides before stepping -// back to the default -func SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath string) { - if len(providedWorkPath) != 0 { - AppWorkPath = filepath.ToSlash(providedWorkPath) - } - if giteaCustom, ok := os.LookupEnv("GITEA_CUSTOM"); ok { - CustomPath = giteaCustom - } - if len(providedCustom) != 0 { - CustomPath = providedCustom - } - if len(CustomPath) == 0 { - CustomPath = path.Join(AppWorkPath, "custom") - } else if !filepath.IsAbs(CustomPath) { - CustomPath = path.Join(AppWorkPath, CustomPath) - } - - if len(providedConf) != 0 { - CustomConf = providedConf - } - if len(CustomConf) == 0 { - CustomConf = path.Join(CustomPath, "conf/app.ini") - } else if !filepath.IsAbs(CustomConf) { - CustomConf = path.Join(CustomPath, CustomConf) - log.Warn("Using 'custom' directory as relative origin for configuration file: '%s'", CustomConf) - } -} - // PrepareAppDataPath creates app data directory if necessary func PrepareAppDataPath() error { // FIXME: There are too many calls to MkdirAll in old code. It is incorrect. @@ -196,20 +90,23 @@ func PrepareAppDataPath() error { return nil } -func Init(opts *Options) { - if opts.CustomConf == "" { - opts.CustomConf = CustomConf - } +func InitCfgProvider(file string, extraConfigs ...string) { var err error - CfgProvider, err = NewConfigProviderFromFile(opts) - CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls - if err != nil { - log.Fatal("newConfigProviderFromFile[%v]: %v", opts, err) + if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil { + log.Fatal("Unable to init config provider from %q: %v", file, err) } - if !opts.DisableLoadCommonSettings { - if err := loadCommonSettingsFrom(CfgProvider); err != nil { - log.Fatal("loadCommonSettingsFrom[%v]: %v", opts, err) - } + CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls +} + +func MustInstalled() { + if !InstallLock { + log.Fatal(`Unable to load config file for a installed Gitea instance, you should either use "--config" to set your config file (app.ini), or run "gitea web" command to install Gitea.`) + } +} + +func LoadCommonSettings() { + if err := loadCommonSettingsFrom(CfgProvider); err != nil { + log.Fatal("Unable to load settings from config: %v", err) } } diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index 4bf57eafb..923fa51d2 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -68,7 +68,7 @@ func sessionHandler(session ssh.Session) { log.Trace("SSH: Payload: %v", command) - args := []string{"serv", "key-" + keyID, "--config=" + setting.CustomConf} + args := []string{"--config=" + setting.CustomConf, "serv", "key-" + keyID} log.Trace("SSH: Arguments: %v", args) ctx, cancel := context.WithCancel(session.Context()) diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index b4275e600..6a0cee4a2 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -90,10 +90,11 @@ func (w *testLoggerWriterCloser) Reset() { // PrintCurrentTest prints the current test to os.Stdout func PrintCurrentTest(t testing.TB, skip ...int) func() { + t.Helper() start := time.Now() actualSkip := 1 if len(skip) > 0 { - actualSkip = skip[0] + actualSkip = skip[0] + 1 } _, filename, line, _ := runtime.Caller(actualSkip) diff --git a/routers/init.go b/routers/init.go index 54e8d2b8b..ddbabcc39 100644 --- a/routers/init.go +++ b/routers/init.go @@ -28,7 +28,6 @@ import ( "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" actions_router "code.gitea.io/gitea/routers/api/actions" packages_router "code.gitea.io/gitea/routers/api/packages" @@ -101,21 +100,16 @@ func syncAppConfForGit(ctx context.Context) error { return nil } -// GlobalInitInstalled is for global installed configuration. -func GlobalInitInstalled(ctx context.Context) { - if !setting.InstallLock { - log.Fatal("Gitea is not installed") - } +func InitWebInstallPage(ctx context.Context) { + translation.InitLocales(ctx) + setting.LoadSettingsForInstall() + mustInit(svg.Init) +} +// InitWebInstalled is for global installed configuration. +func InitWebInstalled(ctx context.Context) { mustInitCtx(ctx, git.InitFull) - log.Info("Gitea Version: %s%s", setting.AppVer, setting.AppBuiltWith) - log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) - log.Info("AppPath: %s", setting.AppPath) - log.Info("AppWorkPath: %s", setting.AppWorkPath) - log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.Log.RootPath) - log.Info("Configuration file: %s", setting.CustomConf) - log.Info("Run Mode: %s", util.ToTitleCase(setting.RunMode)) + log.Info("Git version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) // Setup i18n translation.InitLocales(ctx) diff --git a/routers/install/install.go b/routers/install/install.go index 16bb55b68..c94a30b89 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/forms" "gitea.com/go-chi/session" @@ -370,11 +371,16 @@ func SubmitInstall(ctx *context.Context) { } // Save settings. - cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err) } + cfg.Section("").Key("APP_NAME").SetValue(form.AppName) + cfg.Section("").Key("RUN_USER").SetValue(form.RunUser) + cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) + cfg.Section("").Key("RUN_MODE").SetValue("prod") + cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String()) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) @@ -386,9 +392,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("database").Key("PATH").SetValue(setting.Database.Path) cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful - cfg.Section("").Key("APP_NAME").SetValue(form.AppName) cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath) - cfg.Section("").Key("RUN_USER").SetValue(form.RunUser) cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain) cfg.Section("server").Key("DOMAIN").SetValue(form.Domain) cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort) @@ -450,8 +454,6 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress)) cfg.Section("cron.update_checker").Key("ENABLED").SetValue(fmt.Sprint(form.EnableUpdateChecker)) - cfg.Section("").Key("RUN_MODE").SetValue("prod") - cfg.Section("session").Key("PROVIDER").SetValue("file") cfg.Section("log").Key("MODE").MustString("console") @@ -514,7 +516,13 @@ func SubmitInstall(ctx *context.Context) { // ---- All checks are passed // Reload settings (and re-initialize database connection) - reloadSettings(ctx) + setting.InitCfgProvider(setting.CustomConf) + setting.LoadCommonSettings() + setting.MustInstalled() + setting.LoadDBSetting() + if err := common.InitDBEngine(ctx); err != nil { + log.Fatal("ORM engine initialization failed: %v", err) + } // Create admin account if len(form.AdminName) > 0 { diff --git a/routers/install/setting.go b/routers/install/setting.go deleted file mode 100644 index c14843d8e..000000000 --- a/routers/install/setting.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package install - -import ( - "context" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/svg" - "code.gitea.io/gitea/modules/translation" - "code.gitea.io/gitea/routers/common" -) - -// PreloadSettings preloads the configuration to check if we need to run install -func PreloadSettings(ctx context.Context) bool { - setting.Init(&setting.Options{ - AllowEmpty: true, - }) - if !setting.InstallLock { - log.Info("AppPath: %s", setting.AppPath) - log.Info("AppWorkPath: %s", setting.AppWorkPath) - log.Info("Custom path: %s", setting.CustomPath) - log.Info("Log path: %s", setting.Log.RootPath) - log.Info("Configuration file: %s", setting.CustomConf) - log.Info("Prepare to run install page") - translation.InitLocales(ctx) - if setting.EnableSQLite3 { - log.Info("SQLite3 is supported") - } - - setting.LoadSettingsForInstall() - _ = svg.Init() - } - - return !setting.InstallLock -} - -// reloadSettings reloads the existing settings and starts up the database -func reloadSettings(ctx context.Context) { - setting.Init(&setting.Options{}) - setting.LoadDBSetting() - if setting.InstallLock { - if err := common.InitDBEngine(ctx); err == nil { - log.Info("ORM engine initialization successful!") - } else { - log.Fatal("ORM engine initialization failed: %v", err) - } - } -} diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index be662c22e..2c6989a71 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "net/url" - "os" "strconv" "strings" @@ -167,20 +166,6 @@ func Config(ctx *context.Context) { ctx.Data["SessionConfig"] = sessionCfg ctx.Data["Git"] = setting.Git - - type envVar struct { - Name, Value string - } - - envVars := map[string]*envVar{} - if len(os.Getenv("GITEA_WORK_DIR")) > 0 { - envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")} - } - if len(os.Getenv("GITEA_CUSTOM")) > 0 { - envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")} - } - - ctx.Data["EnvVars"] = envVars ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate ctx.Data["LogSQL"] = setting.Database.LogSQL diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 2850cc8d3..2ddc0c1ac 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -46,15 +46,6 @@
{{.ScriptType}}
{{.locale.Tr "admin.config.reverse_auth_user"}}
{{.ReverseProxyAuthUser}}
- - {{if .EnvVars}} -
- {{range .EnvVars}} -
{{.Name}}
-
{{.Value}}
- {{end}} - {{end}} - diff --git a/tests/test_utils.go b/tests/test_utils.go index 5540d92e9..bf7d1b3fb 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -42,7 +42,10 @@ func InitTest(requireGitea bool) { if giteaRoot == "" { exitf("Environment variable $GITEA_ROOT not set") } + + setting.IsInTesting = true setting.AppWorkPath = giteaRoot + setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") if requireGitea { giteaBinary := "gitea" if setting.IsWindows { @@ -53,7 +56,6 @@ func InitTest(requireGitea bool) { exitf("Could not find gitea binary at %s", setting.AppPath) } } - giteaConf := os.Getenv("GITEA_CONF") if giteaConf == "" { // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. @@ -66,16 +68,12 @@ func InitTest(requireGitea bool) { exitf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify`) } } - - setting.IsInTesting = true - if !path.IsAbs(giteaConf) { - setting.CustomConf = path.Join(giteaRoot, giteaConf) + setting.CustomConf = filepath.Join(giteaRoot, giteaConf) } else { setting.CustomConf = giteaConf } - setting.SetCustomPathAndConf("", "", "") unittest.InitSettings() setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" _ = util.RemoveAll(repo_module.LocalCopyPath()) @@ -175,7 +173,7 @@ func InitTest(requireGitea bool) { defer db.Close() } - routers.GlobalInitInstalled(graceful.GetManager().HammerContext()) + routers.InitWebInstalled(graceful.GetManager().HammerContext()) } func PrepareTestEnv(t testing.TB, skip ...int) func() { @@ -240,10 +238,12 @@ func PrepareTestEnv(t testing.TB, skip ...int) func() { } func PrintCurrentTest(t testing.TB, skip ...int) func() { - if len(skip) == 1 { - skip = []int{skip[0] + 1} + t.Helper() + actualSkip := 1 + if len(skip) > 0 { + actualSkip = skip[0] + 1 } - return testlogger.PrintCurrentTest(t, skip...) + return testlogger.PrintCurrentTest(t, actualSkip) } // Printf takes a format and args and prints the string to os.Stdout