Skip to content

Commit

Permalink
Make "install page" respect environment config (#25648) (#25799)
Browse files Browse the repository at this point in the history
Backport #25648

Replace #25580

Fix #19453

The problem was: when users set "GITEA__XXX__YYY" , the "install page"
doesn't respect it.

So, to make the result consistent and avoid surprising end users, now
the "install page" also writes the environment variables to the config
file.

And, to make things clear, there are enough messages on the UI to tell
users what will happen.

There are some necessary/related changes to `environment-to-ini.go`:

* The "--clear" flag is removed and it was incorrectly written there.
The "clear" operation should be done if INSTALL_LOCK=true
* The "--prefix" flag is removed because it's never used, never
documented and it only causes inconsistent behavior.

The only conflict during backport is "ui divider" in
templates/install.tmpl
  • Loading branch information
wxiaoguang authored Jul 10, 2023
1 parent a1bc2aa commit b4460cf
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 71 deletions.
31 changes: 1 addition & 30 deletions contrib/environment-to-ini/environment-to-ini.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,13 @@ package main

import (
"os"
"strings"

"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"

"github.com/urfave/cli"
)

// EnvironmentPrefix environment variables prefixed with this represent ini values to write
const EnvironmentPrefix = "GITEA"

func main() {
app := cli.NewApp()
app.Name = "environment-to-ini"
Expand Down Expand Up @@ -70,15 +66,6 @@ func main() {
Value: "",
Usage: "Destination file to write to",
},
cli.BoolFlag{
Name: "clear",
Usage: "Clears the matched variables from the environment",
},
cli.StringFlag{
Name: "prefix, p",
Value: EnvironmentPrefix,
Usage: "Environment prefix to look for - will be suffixed by __ (2 underscores)",
},
}
app.Action = runEnvironmentToIni
err := app.Run(os.Args)
Expand All @@ -99,9 +86,7 @@ func runEnvironmentToIni(c *cli.Context) error {
log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
}

prefixGitea := c.String("prefix") + "__"
suffixFile := "__FILE"
changed := setting.EnvironmentToConfig(cfg, prefixGitea, suffixFile, os.Environ())
changed := setting.EnvironmentToConfig(cfg, os.Environ())

// try to save the config file
destination := c.String("out")
Expand All @@ -116,19 +101,5 @@ func runEnvironmentToIni(c *cli.Context) error {
}
}

// clear Gitea's specific environment variables if requested
if c.Bool("clear") {
for _, kv := range os.Environ() {
idx := strings.IndexByte(kv, '=')
if idx < 0 {
continue
}
eKey := kv[:idx]
if strings.HasPrefix(eKey, prefixGitea) {
_ = os.Unsetenv(eKey)
}
}
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ docker-compose up -d

In addition to the environment variables above, any settings in `app.ini` can be set
or overridden with an environment variable of the form: `GITEA__SECTION_NAME__KEY_NAME`.
These settings are applied each time the docker container starts.
These settings are applied each time the docker container starts, and won't be passed into Gitea's sub-processes.
Full information [here](https://github.com/go-gitea/gitea/tree/main/contrib/environment-to-ini).

These environment variables can be passed to the docker container in `docker-compose.yml`.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/doc/installation/with-docker.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ docker-compose up -d

In addition to the environment variables above, any settings in `app.ini` can be set
or overridden with an environment variable of the form: `GITEA__SECTION_NAME__KEY_NAME`.
These settings are applied each time the docker container starts.
These settings are applied each time the docker container starts, and won't be passed into Gitea's sub-processes.
Full information [here](https://github.com/go-gitea/gitea/tree/master/contrib/environment-to-ini).

These environment variables can be passed to the docker container in `docker-compose.yml`.
Expand Down
1 change: 1 addition & 0 deletions modules/assetfs/layered.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ func (l *LayeredFS) WatchLocalChanges(ctx context.Context, callback func()) {
log.Error("Unable to list directories for asset local file-system %q: %v", layer.localPath, err)
continue
}
layerDirs = append(layerDirs, ".")
for _, dir := range layerDirs {
if err = watcher.Add(util.FilePathJoinAbs(layer.localPath, dir)); err != nil {
log.Error("Unable to watch directory %s: %v", dir, err)
Expand Down
25 changes: 23 additions & 2 deletions modules/setting/config_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,31 @@ import (
"code.gitea.io/gitea/modules/log"
)

const (
EnvConfigKeyPrefixGitea = "GITEA__"
EnvConfigKeySuffixFile = "__FILE"
)

const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"

var escapeRegex = regexp.MustCompile(escapeRegexpString)

func CollectEnvConfigKeys() (keys []string) {
for _, env := range os.Environ() {
if strings.HasPrefix(env, EnvConfigKeyPrefixGitea) {
k, _, _ := strings.Cut(env, "=")
keys = append(keys, k)
}
}
return keys
}

func ClearEnvConfigKeys() {
for _, k := range CollectEnvConfigKeys() {
_ = os.Unsetenv(k)
}
}

// decodeEnvSectionKey will decode a portable string encoded Section__Key pair
// Portable strings are considered to be of the form [A-Z0-9_]*
// We will encode a disallowed value as the UTF8 byte string preceded by _0X and
Expand Down Expand Up @@ -87,7 +108,7 @@ func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, sect
return ok, section, key, useFileValue
}

func EnvironmentToConfig(cfg ConfigProvider, prefixGitea, suffixFile string, envs []string) (changed bool) {
func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
for _, kv := range envs {
idx := strings.IndexByte(kv, '=')
if idx < 0 {
Expand All @@ -97,7 +118,7 @@ func EnvironmentToConfig(cfg ConfigProvider, prefixGitea, suffixFile string, env
// parse the environment variable to config section name and key name
envKey := kv[:idx]
envValue := kv[idx+1:]
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(prefixGitea, suffixFile, envKey)
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
if !ok {
continue
}
Expand Down
8 changes: 4 additions & 4 deletions modules/setting/config_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestDecodeEnvironmentKey(t *testing.T) {
func TestEnvironmentToConfig(t *testing.T) {
cfg, _ := NewConfigProviderFromData("")

changed := EnvironmentToConfig(cfg, "GITEA__", "__FILE", nil)
changed := EnvironmentToConfig(cfg, nil)
assert.False(t, changed)

cfg, err := NewConfigProviderFromData(`
Expand All @@ -81,16 +81,16 @@ key = old
`)
assert.NoError(t, err)

changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key=new"})
changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key=new"})
assert.True(t, changed)
assert.Equal(t, "new", cfg.Section("sec").Key("key").String())

changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key=new"})
changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key=new"})
assert.False(t, changed)

tmpFile := t.TempDir() + "/the-file"
_ = os.WriteFile(tmpFile, []byte("value-from-file"), 0o644)
changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key__FILE=" + tmpFile})
changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key__FILE=" + tmpFile})
assert.True(t, changed)
assert.Equal(t, "value-from-file", cfg.Section("sec").Key("key").String())
}
3 changes: 3 additions & 0 deletions modules/setting/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkP

// only read the config but do not load/init anything more, because the AppWorkPath and CustomPath are not ready
InitCfgProvider(tmpCustomConf.Value)
if HasInstallLock(CfgProvider) {
ClearEnvConfigKeys() // if the instance has been installed, do not pass the environment variables to sub-processes
}
configWorkPath := ConfigSectionKeyString(CfgProvider.Section(""), "WORK_PATH")
if configWorkPath != "" {
if !filepath.IsAbs(configWorkPath) {
Expand Down
2 changes: 1 addition & 1 deletion modules/setting/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func generateSaveInternalToken(rootCfg ConfigProvider) {

func loadSecurityFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("security")
InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
InstallLock = HasInstallLock(rootCfg)
LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
Expand Down
8 changes: 6 additions & 2 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,14 @@ func loadRunModeFrom(rootCfg ConfigProvider) {
}
}

// HasInstallLock checks the install-lock in ConfigProvider directly, because sometimes the config file is not loaded into setting variables yet.
func HasInstallLock(rootCfg ConfigProvider) bool {
return rootCfg.Section("security").Key("INSTALL_LOCK").MustBool(false)
}

func mustCurrentRunUserMatch(rootCfg ConfigProvider) {
// Does not check run user when the "InstallLock" is off.
installLock := rootCfg.Section("security").Key("INSTALL_LOCK").MustBool(false)
if installLock {
if HasInstallLock(rootCfg) {
currentUser, match := IsRunUserMatchCurrentUser(RunUser)
if !match {
log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser)
Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@ invalid_password_algorithm = Invalid password hash algorithm
password_algorithm_helper = Set the password hashing algorithm. Algorithms have differing requirements and strength. The argon2 algorithm is rather secure but uses a lot of memory and may be inappropriate for small systems.
enable_update_checker = Enable Update Checker
enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io.
env_config_keys = Environment Configuration
env_config_keys_prompt = The following environment variables will also be applied to your configuration file:

[home]
uname_holder = Username or Email Address
Expand Down
18 changes: 12 additions & 6 deletions routers/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
dbTypeNames := getSupportedDbTypeNames()
envConfigKeys := setting.CollectEnvConfigKeys()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := context.NewBaseContext(resp, req)
Expand All @@ -70,11 +71,13 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.AppendContextValue(context.WebContextKey, ctx)
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data.MergeFrom(middleware.ContextData{
"locale": ctx.Locale,
"Title": ctx.Locale.Tr("install.install"),
"PageIsInstall": true,
"DbTypeNames": dbTypeNames,
"AllLangs": translation.AllLangs(),
"locale": ctx.Locale,
"Title": ctx.Locale.Tr("install.install"),
"PageIsInstall": true,
"DbTypeNames": dbTypeNames,
"EnvConfigKeys": envConfigKeys,
"CustomConfFile": setting.CustomConf,
"AllLangs": translation.AllLangs(),

"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
})
Expand Down Expand Up @@ -218,7 +221,7 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool {
return false
}

log.Info("User confirmed reinstallation of Gitea into a pre-existing database")
log.Info("User confirmed re-installation of Gitea into a pre-existing database")
}

if hasPostInstallationUser || dbMigrationVersion > 0 {
Expand Down Expand Up @@ -502,6 +505,8 @@ func SubmitInstall(ctx *context.Context) {
return
}

setting.EnvironmentToConfig(cfg, os.Environ())

if err = cfg.SaveTo(setting.CustomConf); err != nil {
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return
Expand Down Expand Up @@ -568,6 +573,7 @@ func SubmitInstall(ctx *context.Context) {
}
}

setting.ClearEnvConfigKeys()
log.Info("First-time run install finished!")
InstallDone(ctx)

Expand Down
32 changes: 26 additions & 6 deletions templates/install.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content install">
<div class="ui middle very relaxed page grid">
<div class="ui grid install-config-container">
<div class="sixteen wide center aligned centered column">
<h3 class="ui top attached header">
{{.locale.Tr "install.title"}}
Expand Down Expand Up @@ -149,7 +149,7 @@
</div>
<div class="inline field">
<div class="ui checkbox">
<label for="enable_update_checker">{{.locale.Tr "install.enable_update_checker"}}</label>
<label>{{.locale.Tr "install.enable_update_checker"}}</label>
<input name="enable_update_checker" type="checkbox">
</div>
<span class="help">{{.locale.Tr "install.enable_update_checker_helper"}}</span>
Expand All @@ -161,7 +161,7 @@

<!-- Email -->
<details class="optional field">
<summary class="title gt-py-3{{if .Err_SMTP}} text red{{end}}">
<summary class="right-content gt-py-3{{if .Err_SMTP}} text red{{end}}">
{{.locale.Tr "install.email_title"}}
</summary>
<div class="inline field">
Expand Down Expand Up @@ -201,7 +201,7 @@

<!-- Server and other services -->
<details class="optional field">
<summary class="title gt-py-3{{if .Err_Services}} text red{{end}}">
<summary class="right-content gt-py-3{{if .Err_Services}} text red{{end}}">
{{.locale.Tr "install.server_service_title"}}
</summary>
<div class="inline field">
Expand Down Expand Up @@ -299,7 +299,7 @@

<!-- Admin -->
<details class="optional field">
<summary class="title gt-py-3{{if .Err_Admin}} text red{{end}}">
<summary class="right-content gt-py-3{{if .Err_Admin}} text red{{end}}">
{{.locale.Tr "install.admin_title"}}
</summary>
<p class="center">{{.locale.Tr "install.admin_setting_desc"}}</p>
Expand All @@ -321,10 +321,30 @@
</div>
</details>

<div class="ui divider"></div>
{{if .EnvConfigKeys}}
<!-- Environment Config -->
<h4 class="ui dividing header">{{.locale.Tr "install.env_config_keys"}}</h4>
<div class="inline field">
<label></label>
<button class="ui primary button">{{.locale.Tr "install.install_btn_confirm"}}</button>
<div class="right-content">
{{.locale.Tr "install.env_config_keys_prompt"}}
</div>
<div class="right-content gt-mt-3">
{{range .EnvConfigKeys}}<span class="ui label">{{.}}</span>{{end}}
</div>
</div>
{{end}}

<div class="ui divider"></div>

<div class="inline field">
<div class="right-content">
These configuration options will be written into: {{.CustomConfFile}}
</div>
<div class="right-content gt-mt-3">
<button class="ui primary button">{{.locale.Tr "install.install_btn_confirm"}}</button>
</div>
</div>
</form>
</div>
Expand Down
25 changes: 7 additions & 18 deletions web_src/css/install.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.page-content.install {
padding-top: 45px;
.page-content.install .install-config-container {
max-width: 900px;
margin: auto;
}

.page-content.install form.ui.form .inline.field > label {
Expand All @@ -9,26 +10,20 @@
margin-right: 0;
}

.page-content.install form.ui.form .inline.field > .ui.checkbox:first-child {
.page-content.install .ui.form .field > .help,
.page-content.install .ui.form .field > .ui.checkbox:first-child,
.page-content.install .ui.form .field > .right-content {
margin-left: 30%;
padding-left: 5px;
}

.page-content.install form.ui.form .inline.field > .ui.checkbox:first-child label {
width: auto;
}

.page-content.install form.ui.form .title {
margin-left: 30%;
padding-left: 5px;
}

.page-content.install form.ui.form input {
width: 60%;
}

.page-content.install form.ui.form details.optional.field[open] {
border-bottom: 1px solid var(--color-secondary);
border-bottom: 1px dashed var(--color-secondary);
padding-bottom: 10px;
}

Expand All @@ -44,12 +39,6 @@
text-align: left;
}

.page-content.install form.ui.form .field .help {
margin-left: 30%;
padding-left: 5px;
width: 60%;
}

.page-content.install .ui .reinstall-message {
width: 70%;
margin: 20px auto;
Expand Down

0 comments on commit b4460cf

Please sign in to comment.