diff --git a/cmd/environment_to_ini.go b/cmd/environment_to_ini.go new file mode 100644 index 0000000000000..baf9118f4e290 --- /dev/null +++ b/cmd/environment_to_ini.go @@ -0,0 +1,146 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "os" + "regexp" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "github.com/unknwon/com" + "github.com/urfave/cli" + ini "gopkg.in/ini.v1" +) + +// EnvironmentPrefix environment variables prefixed with this represent ini values to write +const EnvironmentPrefix = "GITEA__" + +// CmdEnvironmentToIni represents the command to use a provided environment to update the configuration ini +var CmdEnvironmentToIni = cli.Command{ + Name: "environment-to-ini", + Usage: "Use provided environment to update configuration ini", + Action: runEnvironmentToIni, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "out, o", + Value: "", + Usage: "Destination file to write to", + }, + }, +} + +func runEnvironmentToIni(c *cli.Context) error { + cfg := ini.Empty() + if com.IsFile(setting.CustomConf) { + if err := cfg.Append(setting.CustomConf); err != nil { + log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) + } + } else { + log.Warn("Custom config '%s' not found, ignore this if you're running first time", setting.CustomConf) + } + cfg.NameMapper = ini.AllCapsUnderscore + + for _, kv := range os.Environ() { + idx := strings.IndexByte(kv, '=') + if idx < 0 { + continue + } + eKey := kv[:idx] + value := kv[idx+1:] + if !strings.HasPrefix(eKey, EnvironmentPrefix) { + continue + } + eKey = eKey[len(EnvironmentPrefix):] + sectionName, keyName := DecodeSectionKey(eKey) + if len(keyName) == 0 { + continue + } + section, err := cfg.GetSection(sectionName) + if err != nil { + section, err = cfg.NewSection(sectionName) + if err != nil { + log.Error("Error creating section: %s : %v", sectionName, err) + continue + } + } + key := section.Key(keyName) + if key == nil { + key, err = section.NewKey(keyName, value) + if err != nil { + log.Error("Error creating key: %s in section: %s with value: %s : %v", keyName, sectionName, value, err) + continue + } + } + key.SetValue(value) + } + destination := c.String("out") + if len(destination) == 0 { + destination = setting.CustomConf + } + err := cfg.SaveTo(destination) + return err +} + +const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_" + +var escapeRegex = regexp.MustCompile(escapeRegexpString) + +// DecodeSectionKey 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 +// followed by _. E.g. _0X2C_ for a '-' and _0X2E_ for '.' +// Section and Key are separated by a plain '__'. +// The entire section can be encoded as a UTF8 byte string +func DecodeSectionKey(encoded string) (string, string) { + section := "" + key := "" + + inKey := false + last := 0 + escapeStringIndices := escapeRegex.FindAllStringIndex(encoded, -1) + for _, unescapeIdx := range escapeStringIndices { + preceding := encoded[last:unescapeIdx[0]] + if !inKey { + if splitter := strings.Index(preceding, "__"); splitter > -1 { + section += preceding[:splitter] + inKey = true + key += preceding[splitter+2:] + } else { + section += preceding + } + } else { + key += preceding + } + toDecode := encoded[unescapeIdx[0]+3 : unescapeIdx[1]-1] + decodedBytes := make([]byte, len(toDecode)/2) + for i := 0; i < len(toDecode)/2; i++ { + // Can ignore error here as we know these should be hexadecimal from the regexp + byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 0) + decodedBytes[i] = byte(byteInt) + } + if inKey { + key += string(decodedBytes) + } else { + section += string(decodedBytes) + } + last = unescapeIdx[1] + } + remaining := encoded[last:] + if !inKey { + if splitter := strings.Index(remaining, "__"); splitter > -1 { + section += remaining[:splitter] + inKey = true + key += remaining[splitter+2:] + } else { + section += remaining + } + } else { + key += remaining + } + return section, key +} diff --git a/cmd/environment_to_ini_test.go b/cmd/environment_to_ini_test.go new file mode 100644 index 0000000000000..1baf411990f02 --- /dev/null +++ b/cmd/environment_to_ini_test.go @@ -0,0 +1,64 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cmd + +import "testing" + +func TestDecodeSectionKey(t *testing.T) { + tests := []struct { + name string + encoded string + section string + key string + }{ + { + name: "Simple", + encoded: "Section__Key", + section: "Section", + key: "Key", + }, + { + name: "LessSimple", + encoded: "Section_SubSection__Key_SubKey", + section: "Section_SubSection", + key: "Key_SubKey", + }, + { + name: "OneDotOneDash", + encoded: "Section_0X2E_SubSection__Key_0X2D_SubKey", + section: "Section.SubSection", + key: "Key-SubKey", + }, + { + name: "OneDotOneEncodedOneDash", + encoded: "Section_0X2E_0X2E_Sub_0X2D_Section__Key_0X2D_SubKey", + section: "Section.0X2E_Sub-Section", + key: "Key-SubKey", + }, + { + name: "EncodedUnderscore", + encoded: "Section__0X5F_0X2E_Sub_0X2D_Section__Key_0X2D__0X2D_SubKey", + section: "Section__0X2E_Sub-Section", + key: "Key--SubKey", + }, + { + name: "EncodedUtf8", + encoded: "Section__0XE280A6_Sub_0X2D_Section__Key_0X2D__0X2D_SubKey", + section: "Section_…Sub-Section", + key: "Key--SubKey", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotSection, gotKey := DecodeSectionKey(tt.encoded) + if gotSection != tt.section { + t.Errorf("DecodeSectionKey() gotSection = %v, want %v", gotSection, tt.section) + } + if gotKey != tt.key { + t.Errorf("DecodeSectionKey() gotKey = %v, want %v", gotKey, tt.key) + } + }) + } +} diff --git a/docker/root/etc/s6/gitea/setup b/docker/root/etc/s6/gitea/setup index f87ce9115e5b3..565bfcba98bbf 100755 --- a/docker/root/etc/s6/gitea/setup +++ b/docker/root/etc/s6/gitea/setup @@ -44,6 +44,8 @@ if [ ! -f ${GITEA_CUSTOM}/conf/app.ini ]; then SECRET_KEY=${SECRET_KEY:-""} \ envsubst < /etc/templates/app.ini > ${GITEA_CUSTOM}/conf/app.ini + /app/gitea/gitea environment-to-ini -c ${GITEA_CUSTOM}/conf/app.ini + chown ${USER}:git ${GITEA_CUSTOM}/conf/app.ini fi diff --git a/docs/content/doc/installation/with-docker.en-us.md b/docs/content/doc/installation/with-docker.en-us.md index 01914a9a2e1a3..f33982201bceb 100644 --- a/docs/content/doc/installation/with-docker.en-us.md +++ b/docs/content/doc/installation/with-docker.en-us.md @@ -272,6 +272,10 @@ You can configure some of Gitea's settings via environment variables: * `USER_UID`: **1000**: The UID (Unix user ID) of the user that runs Gitea within the container. Match this to the UID of the owner of the `/data` volume if using host volumes (this is not necessary with named volumes). * `USER_GID`: **1000**: The GID (Unix group ID) of the user that runs Gitea within the container. Match this to the GID of the owner of the `/data` volume if using host volumes (this is not necessary with named volumes). +Any other configuration value can be set using environment variables +of the form: `GITEA__SECTION_NAME__KEY_NAME`. See the +`environment-to-ini` command for more information. + # Customization Customization files described [here](https://docs.gitea.io/en-us/customizing-gitea/) should diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md index 0f7b4f61a20d7..18b30b28307ec 100644 --- a/docs/content/doc/usage/command-line.en-us.md +++ b/docs/content/doc/usage/command-line.en-us.md @@ -289,3 +289,37 @@ This command is idempotent. #### convert Converts an existing MySQL database from utf8 to utf8mb4. + +#### environment-to-ini + +As a helper to allow docker users to update the gitea configuration +through the environment, this command allows environment variables to +be mapped to values in the ini. + +Environment variables of the form `GITEA__SECTION_NAME__KEY_NAME` +will be mapped to the ini section `[section_name]` and the key +`KEY_NAME` with the value as provided. + +Environment variables are usually restricted to a reduced character +set `0-9A-Z_` - in order to allow the setting of sections with +characters outside of that set, they should be escaped as following: +`_0X2E_` for `.`. The entire section and key names can be escaped as +a UTF8 byte string if necessary. E.g. to configure: + +```ini +... + +[log.console] +COLORIZE=false +STDERR=true + +... +``` + +You would set the environment variables: `GITEA__LOG_0x2E_CONSOLE__COLORIZE=false` +and `GITEA__LOG_0x2E_CONSOLE__STDERR=false`. Other examples can be found +on the configuration cheat sheet. + + +- Options: + - `--out name`, `-o name`: Name of the adjusted ini file to be created. Optional. (default: The gitea conf file will be changed in place). diff --git a/main.go b/main.go index 30dbf2766224c..1220a8e6c14ec 100644 --- a/main.go +++ b/main.go @@ -68,6 +68,7 @@ arguments - which can alternatively be run by running the subcommand web.` cmd.CmdMigrate, cmd.CmdKeys, cmd.CmdConvert, + cmd.CmdEnvironmentToIni, } // Now adjust these commands to add our global configuration options