Skip to content

Commit 424434d

Browse files
committed
Add contrib/environment-to-ini
This contrib command provides a mechanism to allow arbitrary setting of ini values using the environment variable in a more docker standard fashion. Environment variable keys should be structured as: "GITEA__SECTION_NAME__KEY_NAME" Use of the command is explained in the README. Partial fix for go-gitea#350 Closes go-gitea#7287
1 parent 0bcf644 commit 424434d

File tree

2 files changed

+289
-0
lines changed

2 files changed

+289
-0
lines changed

contrib/environment-to-ini/README

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
Environment To Ini
2+
==================
3+
4+
Multiple docker users have requested that the Gitea docker is changed
5+
to permit arbitrary configuration via environment variables.
6+
7+
Gitea needs to use an ini file for configuration because the running
8+
environment that starts the docker may not be the same as that used
9+
by the hooks. An ini file also gives a good default and means that
10+
users do not have to completely provide a full environment.
11+
12+
With those caveats above, this command provides a generic way of
13+
converting suitably structured environment variables into any ini
14+
value.
15+
16+
To use the command is very simple just run it and the default gitea
17+
app.ini will be rewritten to take account of the variables provided,
18+
however there are various options to give slightly different
19+
behavior and these can be interrogated with the `-h` option.
20+
21+
The environment variables should be of the form:
22+
23+
GITEA__SECTION_NAME__KEY_NAME
24+
25+
Environment variables are usually restricted to a reduced character
26+
set "0-9A-Z_" - in order to allow the setting of sections with
27+
characters outside of that set, they should be escaped as following:
28+
"_0X2E_" for ".". The entire section and key names can be escaped as
29+
a UTF8 byte string if necessary. E.g. to configure:
30+
31+
"""
32+
...
33+
[log.console]
34+
COLORIZE=false
35+
STDERR=true
36+
...
37+
"""
38+
39+
You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false"
40+
and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found
41+
on the configuration cheat sheet.
42+
43+
To plug this command in to the docker, you simply compile the provided go file using:
44+
45+
go build environment-to-ini.go
46+
47+
And copy the resulting `environment-to-ini` command to /app/gitea in the docker.
48+
49+
Apply the below patch to /etc/s6/gitea.setup to wire this in.
50+
51+
If you find this useful please comment on #7287
52+
53+
54+
diff --git a/docker/root/etc/s6/gitea/setup b/docker/root/etc/s6/gitea/setup
55+
index f87ce9115..565bfcba9 100755
56+
--- a/docker/root/etc/s6/gitea/setup
57+
+++ b/docker/root/etc/s6/gitea/setup
58+
@@ -44,6 +44,8 @@ if [ ! -f ${GITEA_CUSTOM}/conf/app.ini ]; then
59+
SECRET_KEY=${SECRET_KEY:-""} \
60+
envsubst < /etc/templates/app.ini > ${GITEA_CUSTOM}/conf/app.ini
61+
62+
+ /app/gitea/environment-to-ini -c ${GITEA_CUSTOM}/conf/app.ini
63+
+
64+
chown ${USER}:git ${GITEA_CUSTOM}/conf/app.ini
65+
fi
66+
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"os"
9+
"regexp"
10+
"strconv"
11+
"strings"
12+
13+
"code.gitea.io/gitea/modules/log"
14+
"code.gitea.io/gitea/modules/setting"
15+
"github.com/unknwon/com"
16+
"github.com/urfave/cli"
17+
ini "gopkg.in/ini.v1"
18+
)
19+
20+
// EnvironmentPrefix environment variables prefixed with this represent ini values to write
21+
const EnvironmentPrefix = "GITEA"
22+
23+
func main() {
24+
app := cli.NewApp()
25+
app.Name = "environment-to-ini"
26+
app.Usage = "Use provided environment to update configuration ini"
27+
app.Description = `As a helper to allow docker users to update the gitea configuration
28+
through the environment, this command allows environment variables to
29+
be mapped to values in the ini.
30+
31+
Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME"
32+
will be mapped to the ini section "[section_name]" and the key
33+
"KEY_NAME" with the value as provided.
34+
35+
Environment variables are usually restricted to a reduced character
36+
set "0-9A-Z_" - in order to allow the setting of sections with
37+
characters outside of that set, they should be escaped as following:
38+
"_0X2E_" for ".". The entire section and key names can be escaped as
39+
a UTF8 byte string if necessary. E.g. to configure:
40+
41+
"""
42+
...
43+
[log.console]
44+
COLORIZE=false
45+
STDERR=true
46+
...
47+
"""
48+
49+
You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false"
50+
and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found
51+
on the configuration cheat sheet.`
52+
app.Flags = []cli.Flag{
53+
cli.StringFlag{
54+
Name: "custom-path, C",
55+
Value: setting.CustomPath,
56+
Usage: "Custom path file path",
57+
},
58+
cli.StringFlag{
59+
Name: "config, c",
60+
Value: setting.CustomConf,
61+
Usage: "Custom configuration file path",
62+
},
63+
cli.StringFlag{
64+
Name: "work-path, w",
65+
Value: setting.AppWorkPath,
66+
Usage: "Set the gitea working path",
67+
},
68+
cli.StringFlag{
69+
Name: "out, o",
70+
Value: "",
71+
Usage: "Destination file to write to",
72+
},
73+
cli.BoolFlag{
74+
Name: "clear",
75+
Usage: "Clears the matched variables from the environment",
76+
},
77+
cli.StringFlag{
78+
Name: "prefix, p",
79+
Value: EnvironmentPrefix,
80+
Usage: "Environment prefix to look for - will be suffixed by __ (2 underscores)",
81+
},
82+
}
83+
app.Action = runEnvironmentToIni
84+
setting.SetCustomPathAndConf("", "", "")
85+
86+
err := app.Run(os.Args)
87+
if err != nil {
88+
log.Fatal("Failed to run app with %s: %v", os.Args, err)
89+
}
90+
}
91+
92+
func runEnvironmentToIni(c *cli.Context) error {
93+
providedCustom := c.String("custom-path")
94+
providedConf := c.String("config")
95+
providedWorkPath := c.String("work-path")
96+
setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath)
97+
98+
cfg := ini.Empty()
99+
if com.IsFile(setting.CustomConf) {
100+
if err := cfg.Append(setting.CustomConf); err != nil {
101+
log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
102+
}
103+
} else {
104+
log.Warn("Custom config '%s' not found, ignore this if you're running first time", setting.CustomConf)
105+
}
106+
cfg.NameMapper = ini.AllCapsUnderscore
107+
108+
prefix := c.String("prefix") + "__"
109+
110+
for _, kv := range os.Environ() {
111+
idx := strings.IndexByte(kv, '=')
112+
if idx < 0 {
113+
continue
114+
}
115+
eKey := kv[:idx]
116+
value := kv[idx+1:]
117+
if !strings.HasPrefix(eKey, prefix) {
118+
continue
119+
}
120+
eKey = eKey[len(prefix):]
121+
sectionName, keyName := DecodeSectionKey(eKey)
122+
if len(keyName) == 0 {
123+
continue
124+
}
125+
section, err := cfg.GetSection(sectionName)
126+
if err != nil {
127+
section, err = cfg.NewSection(sectionName)
128+
if err != nil {
129+
log.Error("Error creating section: %s : %v", sectionName, err)
130+
continue
131+
}
132+
}
133+
key := section.Key(keyName)
134+
if key == nil {
135+
key, err = section.NewKey(keyName, value)
136+
if err != nil {
137+
log.Error("Error creating key: %s in section: %s with value: %s : %v", keyName, sectionName, value, err)
138+
continue
139+
}
140+
}
141+
key.SetValue(value)
142+
}
143+
destination := c.String("out")
144+
if len(destination) == 0 {
145+
destination = setting.CustomConf
146+
}
147+
err := cfg.SaveTo(destination)
148+
if err != nil {
149+
return err
150+
}
151+
if c.Bool("clear") {
152+
for _, kv := range os.Environ() {
153+
idx := strings.IndexByte(kv, '=')
154+
if idx < 0 {
155+
continue
156+
}
157+
eKey := kv[:idx]
158+
if strings.HasPrefix(eKey, prefix) {
159+
_ = os.Unsetenv(eKey)
160+
}
161+
}
162+
}
163+
return nil
164+
}
165+
166+
const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"
167+
168+
var escapeRegex = regexp.MustCompile(escapeRegexpString)
169+
170+
// DecodeSectionKey will decode a portable string encoded Section__Key pair
171+
// Portable strings are considered to be of the form [A-Z0-9_]*
172+
// We will encode a disallowed value as the UTF8 byte string preceded by _0X and
173+
// followed by _. E.g. _0X2C_ for a '-' and _0X2E_ for '.'
174+
// Section and Key are separated by a plain '__'.
175+
// The entire section can be encoded as a UTF8 byte string
176+
func DecodeSectionKey(encoded string) (string, string) {
177+
section := ""
178+
key := ""
179+
180+
inKey := false
181+
last := 0
182+
escapeStringIndices := escapeRegex.FindAllStringIndex(encoded, -1)
183+
for _, unescapeIdx := range escapeStringIndices {
184+
preceding := encoded[last:unescapeIdx[0]]
185+
if !inKey {
186+
if splitter := strings.Index(preceding, "__"); splitter > -1 {
187+
section += preceding[:splitter]
188+
inKey = true
189+
key += preceding[splitter+2:]
190+
} else {
191+
section += preceding
192+
}
193+
} else {
194+
key += preceding
195+
}
196+
toDecode := encoded[unescapeIdx[0]+3 : unescapeIdx[1]-1]
197+
decodedBytes := make([]byte, len(toDecode)/2)
198+
for i := 0; i < len(toDecode)/2; i++ {
199+
// Can ignore error here as we know these should be hexadecimal from the regexp
200+
byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 0)
201+
decodedBytes[i] = byte(byteInt)
202+
}
203+
if inKey {
204+
key += string(decodedBytes)
205+
} else {
206+
section += string(decodedBytes)
207+
}
208+
last = unescapeIdx[1]
209+
}
210+
remaining := encoded[last:]
211+
if !inKey {
212+
if splitter := strings.Index(remaining, "__"); splitter > -1 {
213+
section += remaining[:splitter]
214+
inKey = true
215+
key += remaining[splitter+2:]
216+
} else {
217+
section += remaining
218+
}
219+
} else {
220+
key += remaining
221+
}
222+
return section, key
223+
}

0 commit comments

Comments
 (0)