Skip to content

Commit c216059

Browse files
authored
Make environment-to-ini support loading key value from file (#24832)
Replace #19857 Close #19856 Close #10311 Close #10123 Major changes: 1. Move a lot of code from `environment-to-ini.go` to `config_env.go` to make them testable. 2. Add `__FILE` support 3. Update documents 4. Add tests
1 parent 1aa9107 commit c216059

File tree

5 files changed

+278
-111
lines changed

5 files changed

+278
-111
lines changed

contrib/environment-to-ini/environment-to-ini.go

+15-105
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@ package main
55

66
import (
77
"os"
8-
"regexp"
9-
"strconv"
108
"strings"
119

1210
"code.gitea.io/gitea/modules/log"
1311
"code.gitea.io/gitea/modules/setting"
1412
"code.gitea.io/gitea/modules/util"
1513

1614
"github.com/urfave/cli"
17-
ini "gopkg.in/ini.v1"
15+
"gopkg.in/ini.v1"
1816
)
1917

2018
// EnvironmentPrefix environment variables prefixed with this represent ini values to write
@@ -32,6 +30,10 @@ func main() {
3230
will be mapped to the ini section "[section_name]" and the key
3331
"KEY_NAME" with the value as provided.
3432
33+
Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME__FILE"
34+
will be mapped to the ini section "[section_name]" and the key
35+
"KEY_NAME" with the value loaded from the specified file.
36+
3537
Environment variables are usually restricted to a reduced character
3638
set "0-9A-Z_" - in order to allow the setting of sections with
3739
characters outside of that set, they should be escaped as following:
@@ -96,11 +98,11 @@ func runEnvironmentToIni(c *cli.Context) error {
9698
setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath)
9799

98100
cfg := ini.Empty()
99-
isFile, err := util.IsFile(setting.CustomConf)
101+
confFileExists, err := util.IsFile(setting.CustomConf)
100102
if err != nil {
101103
log.Fatal("Unable to check if %s is a file. Error: %v", setting.CustomConf, err)
102104
}
103-
if isFile {
105+
if confFileExists {
104106
if err := cfg.Append(setting.CustomConf); err != nil {
105107
log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
106108
}
@@ -109,47 +111,11 @@ func runEnvironmentToIni(c *cli.Context) error {
109111
}
110112
cfg.NameMapper = ini.SnackCase
111113

112-
changed := false
114+
prefixGitea := c.String("prefix") + "__"
115+
suffixFile := "__FILE"
116+
changed := setting.EnvironmentToConfig(cfg, prefixGitea, suffixFile, os.Environ())
113117

114-
prefix := c.String("prefix") + "__"
115-
116-
for _, kv := range os.Environ() {
117-
idx := strings.IndexByte(kv, '=')
118-
if idx < 0 {
119-
continue
120-
}
121-
eKey := kv[:idx]
122-
value := kv[idx+1:]
123-
if !strings.HasPrefix(eKey, prefix) {
124-
continue
125-
}
126-
eKey = eKey[len(prefix):]
127-
sectionName, keyName := DecodeSectionKey(eKey)
128-
if len(keyName) == 0 {
129-
continue
130-
}
131-
section, err := cfg.GetSection(sectionName)
132-
if err != nil {
133-
section, err = cfg.NewSection(sectionName)
134-
if err != nil {
135-
log.Error("Error creating section: %s : %v", sectionName, err)
136-
continue
137-
}
138-
}
139-
key := section.Key(keyName)
140-
if key == nil {
141-
key, err = section.NewKey(keyName, value)
142-
if err != nil {
143-
log.Error("Error creating key: %s in section: %s with value: %s : %v", keyName, sectionName, value, err)
144-
continue
145-
}
146-
}
147-
oldValue := key.Value()
148-
if !changed && oldValue != value {
149-
changed = true
150-
}
151-
key.SetValue(value)
152-
}
118+
// try to save the config file
153119
destination := c.String("out")
154120
if len(destination) == 0 {
155121
destination = setting.CustomConf
@@ -161,76 +127,20 @@ func runEnvironmentToIni(c *cli.Context) error {
161127
return err
162128
}
163129
}
130+
131+
// clear Gitea's specific environment variables if requested
164132
if c.Bool("clear") {
165133
for _, kv := range os.Environ() {
166134
idx := strings.IndexByte(kv, '=')
167135
if idx < 0 {
168136
continue
169137
}
170138
eKey := kv[:idx]
171-
if strings.HasPrefix(eKey, prefix) {
139+
if strings.HasPrefix(eKey, prefixGitea) {
172140
_ = os.Unsetenv(eKey)
173141
}
174142
}
175143
}
176-
return nil
177-
}
178144

179-
const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"
180-
181-
var escapeRegex = regexp.MustCompile(escapeRegexpString)
182-
183-
// DecodeSectionKey will decode a portable string encoded Section__Key pair
184-
// Portable strings are considered to be of the form [A-Z0-9_]*
185-
// We will encode a disallowed value as the UTF8 byte string preceded by _0X and
186-
// followed by _. E.g. _0X2C_ for a '-' and _0X2E_ for '.'
187-
// Section and Key are separated by a plain '__'.
188-
// The entire section can be encoded as a UTF8 byte string
189-
func DecodeSectionKey(encoded string) (string, string) {
190-
section := ""
191-
key := ""
192-
193-
inKey := false
194-
last := 0
195-
escapeStringIndices := escapeRegex.FindAllStringIndex(encoded, -1)
196-
for _, unescapeIdx := range escapeStringIndices {
197-
preceding := encoded[last:unescapeIdx[0]]
198-
if !inKey {
199-
if splitter := strings.Index(preceding, "__"); splitter > -1 {
200-
section += preceding[:splitter]
201-
inKey = true
202-
key += preceding[splitter+2:]
203-
} else {
204-
section += preceding
205-
}
206-
} else {
207-
key += preceding
208-
}
209-
toDecode := encoded[unescapeIdx[0]+3 : unescapeIdx[1]-1]
210-
decodedBytes := make([]byte, len(toDecode)/2)
211-
for i := 0; i < len(toDecode)/2; i++ {
212-
// Can ignore error here as we know these should be hexadecimal from the regexp
213-
byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 0)
214-
decodedBytes[i] = byte(byteInt)
215-
}
216-
if inKey {
217-
key += string(decodedBytes)
218-
} else {
219-
section += string(decodedBytes)
220-
}
221-
last = unescapeIdx[1]
222-
}
223-
remaining := encoded[last:]
224-
if !inKey {
225-
if splitter := strings.Index(remaining, "__"); splitter > -1 {
226-
section += remaining[:splitter]
227-
key += remaining[splitter+2:]
228-
} else {
229-
section += remaining
230-
}
231-
} else {
232-
key += remaining
233-
}
234-
section = strings.ToLower(section)
235-
return section, key
145+
return nil
236146
}

docs/content/doc/installation/with-docker-rootless.en-us.md

+12-3
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,18 @@ docker-compose up -d
286286

287287
## Managing Deployments With Environment Variables
288288

289-
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. Full information [here](https://github.com/go-gitea/gitea/tree/main/contrib/environment-to-ini).
290-
291-
These environment variables can be passed to the docker container in `docker-compose.yml`. The following example will enable an smtp mail server if the required env variables `GITEA__mailer__FROM`, `GITEA__mailer__HOST`, `GITEA__mailer__PASSWD` are set on the host or in a `.env` file in the same directory as `docker-compose.yml`:
289+
In addition to the environment variables above, any settings in `app.ini` can be set
290+
or overridden with an environment variable of the form: `GITEA__SECTION_NAME__KEY_NAME`.
291+
These settings are applied each time the docker container starts.
292+
Full information [here](https://github.com/go-gitea/gitea/tree/main/contrib/environment-to-ini).
293+
294+
These environment variables can be passed to the docker container in `docker-compose.yml`.
295+
The following example will enable a smtp mail server if the required env variables
296+
`GITEA__mailer__FROM`, `GITEA__mailer__HOST`, `GITEA__mailer__PASSWD` are set on the host
297+
or in a `.env` file in the same directory as `docker-compose.yml`.
298+
299+
The settings can be also set or overridden with the content of a file by defining an environment variable of the form:
300+
`GITEA__section_name__KEY_NAME__FILE` that points to a file.
292301

293302
```bash
294303
...

docs/content/doc/installation/with-docker.en-us.md

+12-3
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,18 @@ docker-compose up -d
287287

288288
## Managing Deployments With Environment Variables
289289

290-
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. Full information [here](https://github.com/go-gitea/gitea/tree/master/contrib/environment-to-ini).
291-
292-
These environment variables can be passed to the docker container in `docker-compose.yml`. The following example will enable an smtp mail server if the required env variables `GITEA__mailer__FROM`, `GITEA__mailer__HOST`, `GITEA__mailer__PASSWD` are set on the host or in a `.env` file in the same directory as `docker-compose.yml`:
290+
In addition to the environment variables above, any settings in `app.ini` can be set
291+
or overridden with an environment variable of the form: `GITEA__SECTION_NAME__KEY_NAME`.
292+
These settings are applied each time the docker container starts.
293+
Full information [here](https://github.com/go-gitea/gitea/tree/master/contrib/environment-to-ini).
294+
295+
These environment variables can be passed to the docker container in `docker-compose.yml`.
296+
The following example will enable an smtp mail server if the required env variables
297+
`GITEA__mailer__FROM`, `GITEA__mailer__HOST`, `GITEA__mailer__PASSWD` are set on the host
298+
or in a `.env` file in the same directory as `docker-compose.yml`.
299+
300+
The settings can be also set or overridden with the content of a file by defining an environment variable of the form:
301+
`GITEA__section_name__KEY_NAME__FILE` that points to a file.
293302

294303
```bash
295304
...

modules/setting/config_env.go

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package setting
5+
6+
import (
7+
"os"
8+
"regexp"
9+
"strconv"
10+
"strings"
11+
12+
"code.gitea.io/gitea/modules/log"
13+
14+
"gopkg.in/ini.v1"
15+
)
16+
17+
const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"
18+
19+
var escapeRegex = regexp.MustCompile(escapeRegexpString)
20+
21+
// decodeEnvSectionKey will decode a portable string encoded Section__Key pair
22+
// Portable strings are considered to be of the form [A-Z0-9_]*
23+
// We will encode a disallowed value as the UTF8 byte string preceded by _0X and
24+
// followed by _. E.g. _0X2C_ for a '-' and _0X2E_ for '.'
25+
// Section and Key are separated by a plain '__'.
26+
// The entire section can be encoded as a UTF8 byte string
27+
func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
28+
inKey := false
29+
last := 0
30+
escapeStringIndices := escapeRegex.FindAllStringIndex(encoded, -1)
31+
for _, unescapeIdx := range escapeStringIndices {
32+
preceding := encoded[last:unescapeIdx[0]]
33+
if !inKey {
34+
if splitter := strings.Index(preceding, "__"); splitter > -1 {
35+
section += preceding[:splitter]
36+
inKey = true
37+
key += preceding[splitter+2:]
38+
} else {
39+
section += preceding
40+
}
41+
} else {
42+
key += preceding
43+
}
44+
toDecode := encoded[unescapeIdx[0]+3 : unescapeIdx[1]-1]
45+
decodedBytes := make([]byte, len(toDecode)/2)
46+
for i := 0; i < len(toDecode)/2; i++ {
47+
// Can ignore error here as we know these should be hexadecimal from the regexp
48+
byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 0)
49+
decodedBytes[i] = byte(byteInt)
50+
}
51+
if inKey {
52+
key += string(decodedBytes)
53+
} else {
54+
section += string(decodedBytes)
55+
}
56+
last = unescapeIdx[1]
57+
}
58+
remaining := encoded[last:]
59+
if !inKey {
60+
if splitter := strings.Index(remaining, "__"); splitter > -1 {
61+
section += remaining[:splitter]
62+
key += remaining[splitter+2:]
63+
} else {
64+
section += remaining
65+
}
66+
} else {
67+
key += remaining
68+
}
69+
section = strings.ToLower(section)
70+
ok = section != "" && key != ""
71+
if !ok {
72+
section = ""
73+
key = ""
74+
}
75+
return ok, section, key
76+
}
77+
78+
// decodeEnvironmentKey decode the environment key to section and key
79+
// The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE
80+
func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
81+
if !strings.HasPrefix(envKey, prefixGitea) {
82+
return false, "", "", false
83+
}
84+
if strings.HasSuffix(envKey, suffixFile) {
85+
useFileValue = true
86+
envKey = envKey[:len(envKey)-len(suffixFile)]
87+
}
88+
ok, section, key = decodeEnvSectionKey(envKey[len(prefixGitea):])
89+
return ok, section, key, useFileValue
90+
}
91+
92+
func EnvironmentToConfig(cfg *ini.File, prefixGitea, suffixFile string, envs []string) (changed bool) {
93+
for _, kv := range envs {
94+
idx := strings.IndexByte(kv, '=')
95+
if idx < 0 {
96+
continue
97+
}
98+
99+
// parse the environment variable to config section name and key name
100+
envKey := kv[:idx]
101+
envValue := kv[idx+1:]
102+
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(prefixGitea, suffixFile, envKey)
103+
if !ok {
104+
continue
105+
}
106+
107+
// use environment value as config value, or read the file content as value if the key indicates a file
108+
keyValue := envValue
109+
if useFileValue {
110+
fileContent, err := os.ReadFile(envValue)
111+
if err != nil {
112+
log.Error("Error reading file for %s : %v", envKey, envValue, err)
113+
continue
114+
}
115+
keyValue = string(fileContent)
116+
}
117+
118+
// try to set the config value if necessary
119+
section, err := cfg.GetSection(sectionName)
120+
if err != nil {
121+
section, err = cfg.NewSection(sectionName)
122+
if err != nil {
123+
log.Error("Error creating section: %s : %v", sectionName, err)
124+
continue
125+
}
126+
}
127+
key := section.Key(keyName)
128+
if key == nil {
129+
key, err = section.NewKey(keyName, keyValue)
130+
if err != nil {
131+
log.Error("Error creating key: %s in section: %s with value: %s : %v", keyName, sectionName, keyValue, err)
132+
continue
133+
}
134+
}
135+
oldValue := key.Value()
136+
if !changed && oldValue != keyValue {
137+
changed = true
138+
}
139+
key.SetValue(keyValue)
140+
}
141+
return changed
142+
}

0 commit comments

Comments
 (0)