Skip to content

Commit f95e8a9

Browse files
committed
package config expands env like trpc_go.yaml
1 parent 2b0a118 commit f95e8a9

7 files changed

Lines changed: 159 additions & 60 deletions

File tree

config.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"time"
2626

2727
yaml "gopkg.in/yaml.v3"
28+
"trpc.group/trpc-go/trpc-go/internal/expandenv"
2829
trpcpb "trpc.group/trpc/trpc-protocol/pb/go/trpc"
2930

3031
"trpc.group/trpc-go/trpc-go/client"
@@ -608,11 +609,8 @@ func parseConfigFromFile(configPath string) (*Config, error) {
608609
if err != nil {
609610
return nil, err
610611
}
611-
// expand environment variables
612-
buf = []byte(expandEnv(string(buf)))
613-
614612
cfg := defaultConfig()
615-
if err := yaml.Unmarshal(buf, cfg); err != nil {
613+
if err := yaml.Unmarshal(expandenv.ExpandEnv(buf), cfg); err != nil {
616614
return nil, err
617615
}
618616
return cfg, nil

config/options.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
package config
1515

16+
import "trpc.group/trpc-go/trpc-go/internal/expandenv"
17+
1618
// WithCodec returns an option which sets the codec's name.
1719
func WithCodec(name string) LoadOption {
1820
return func(c *TrpcConfig) {
@@ -27,6 +29,14 @@ func WithProvider(name string) LoadOption {
2729
}
2830
}
2931

32+
// WithExpandEnv replaces ${var} in raw bytes with environment value of var.
33+
// Note, method TrpcConfig.Bytes will return the replaced bytes.
34+
func WithExpandEnv() LoadOption {
35+
return func(c *TrpcConfig) {
36+
c.expandEnv = expandenv.ExpandEnv
37+
}
38+
}
39+
3040
// options is config option.
3141
type options struct{}
3242

config/trpc_config.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ type TrpcConfig struct {
162162
path string
163163
decoder Codec
164164
rawData []byte
165+
expandEnv func([]byte) []byte
165166
}
166167

167168
func newTrpcConfig(path string) *TrpcConfig {
@@ -170,6 +171,7 @@ func newTrpcConfig(path string) *TrpcConfig {
170171
unmarshalledData: make(map[string]interface{}),
171172
path: path,
172173
decoder: &YamlCodec{},
174+
expandEnv: func(bytes []byte) []byte { return bytes },
173175
}
174176
}
175177

@@ -189,7 +191,7 @@ func (c *TrpcConfig) Load() error {
189191
return fmt.Errorf("trpc/config: failed to load %s: %s", c.path, err.Error())
190192
}
191193

192-
c.rawData = data
194+
c.rawData = c.expandEnv(data)
193195
if err := c.decoder.Unmarshal(c.rawData, &c.unmarshalledData); err != nil {
194196
return fmt.Errorf("trpc/config: failed to parse %s: %s", c.path, err.Error())
195197
}
@@ -208,8 +210,8 @@ func (c *TrpcConfig) Reload() {
208210
return
209211
}
210212

211-
c.rawData = data
212-
if err := c.decoder.Unmarshal(data, &c.unmarshalledData); err != nil {
213+
c.rawData = c.expandEnv(data)
214+
if err := c.decoder.Unmarshal(c.rawData, &c.unmarshalledData); err != nil {
213215
log.Tracef("trpc/config: failed to parse %s: %v", c.path, err)
214216
return
215217
}

config/trpc_config_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,43 @@ func TestYamlCodec_Unmarshal(t *testing.T) {
126126
require.NotNil(t, GetCodec("yaml").Unmarshal([]byte("[1, 2]"), &tt))
127127
})
128128
}
129+
130+
func TestEnvExpanded(t *testing.T) {
131+
RegisterProvider(NewEnvProvider(t.Name(), []byte(`
132+
password: ${pwd}
133+
`)))
134+
135+
t.Setenv("pwd", t.Name())
136+
cfg, err := DefaultConfigLoader.Load(
137+
t.Name(),
138+
WithProvider(t.Name()),
139+
WithExpandEnv())
140+
require.Nil(t, err)
141+
142+
require.Equal(t, t.Name(), cfg.GetString("password", ""))
143+
require.Contains(t, string(cfg.Bytes()), fmt.Sprintf("password: %s", t.Name()))
144+
}
145+
146+
func NewEnvProvider(name string, data []byte) *EnvProvider {
147+
return &EnvProvider{
148+
name: name,
149+
data: data,
150+
}
151+
}
152+
153+
type EnvProvider struct {
154+
name string
155+
data []byte
156+
}
157+
158+
func (ep *EnvProvider) Name() string {
159+
return ep.name
160+
}
161+
162+
func (ep *EnvProvider) Read(string) ([]byte, error) {
163+
return ep.data, nil
164+
}
165+
166+
func (ep *EnvProvider) Watch(cb ProviderCallback) {
167+
cb("", ep.data)
168+
}

internal/expandenv/expand_env.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Package expandenv replaces ${key} in byte slices with the env value of key.
2+
package expandenv
3+
4+
import (
5+
"os"
6+
)
7+
8+
// ExpandEnv looks for ${var} in s and replaces them with value of the corresponding environment variable.
9+
// It's not like os.ExpandEnv which handles both ${var} and $var.
10+
// $var is considered invalid, since configurations like password for redis/mysql may contain $.
11+
func ExpandEnv(s []byte) []byte {
12+
var buf []byte
13+
i := 0
14+
for j := 0; j < len(s); j++ {
15+
if s[j] == '$' && j+2 < len(s) && s[j+1] == '{' { // only ${var} instead of $var is valid
16+
if buf == nil {
17+
buf = make([]byte, 0, 2*len(s))
18+
}
19+
buf = append(buf, s[i:j]...)
20+
name, w := getEnvName(s[j+1:])
21+
if name == nil && w > 0 {
22+
// invalid matching, remove the $
23+
} else if name == nil {
24+
buf = append(buf, s[j]) // keep the $
25+
} else {
26+
buf = append(buf, os.Getenv(string(name))...)
27+
}
28+
j += w
29+
i = j + 1
30+
}
31+
}
32+
if buf == nil {
33+
return s
34+
}
35+
return append(buf, s[i:]...)
36+
}
37+
38+
// getEnvName gets env name, that is, var from ${var}.
39+
// The env name and its len will be returned.
40+
func getEnvName(s []byte) ([]byte, int) {
41+
// look for right curly bracket '}'
42+
// it's guaranteed that the first char is '{' and the string has at least two char
43+
for i := 1; i < len(s); i++ {
44+
if s[i] == ' ' || s[i] == '\n' || s[i] == '"' { // "xx${xxx"
45+
return nil, 0 // encounter invalid char, keep the $
46+
}
47+
if s[i] == '}' {
48+
if i == 1 { // ${}
49+
return nil, 2 // remove ${}
50+
}
51+
return s[1:i], i + 1
52+
}
53+
}
54+
return nil, 0 // no },keep the $
55+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package expandenv_test
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
. "trpc.group/trpc-go/trpc-go/internal/expandenv"
10+
)
11+
12+
func TestExpandEnv(t *testing.T) {
13+
key := "env_key"
14+
t.Run("no env", func(t *testing.T) {
15+
require.Equal(t, []byte("abc"), ExpandEnv([]byte("abc")))
16+
})
17+
t.Run("${..} is expanded", func(t *testing.T) {
18+
t.Setenv(key, t.Name())
19+
require.Equal(t, fmt.Sprintf("head_%s_tail", t.Name()),
20+
string(ExpandEnv([]byte(fmt.Sprintf("head_${%s}_tail", key)))))
21+
})
22+
t.Run("${ is not expanded", func(t *testing.T) {
23+
require.Equal(t, "head_${_tail",
24+
string(ExpandEnv([]byte(fmt.Sprintf("head_${_tail")))))
25+
})
26+
t.Run("${} is expanded as empty", func(t *testing.T) {
27+
require.Equal(t, "head__tail",
28+
string(ExpandEnv([]byte("head_${}_tail"))))
29+
})
30+
t.Run("${..} is not expanded if .. contains any space", func(t *testing.T) {
31+
t.Setenv("key key", t.Name())
32+
require.Equal(t, "head_${key key}_tail",
33+
string(ExpandEnv([]byte("head_${key key}_tail"))))
34+
})
35+
t.Run("${..} is not expanded if .. contains any new line", func(t *testing.T) {
36+
t.Setenv("key\nkey", t.Name())
37+
require.Equal(t, t.Name(), os.Getenv("key\nkey"))
38+
require.Equal(t, "head_${key\nkey}_tail",
39+
string(ExpandEnv([]byte("head_${key\nkey}_tail"))))
40+
})
41+
t.Run(`${..} is not expanded if .. contains any "`, func(t *testing.T) {
42+
t.Setenv(`key"key`, t.Name())
43+
require.Equal(t, t.Name(), os.Getenv(`key"key`))
44+
require.Equal(t, `head_${key"key}_tail`,
45+
string(ExpandEnv([]byte(`head_${key"key}_tail`))))
46+
})
47+
}

trpc_util.go

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package trpc
1616
import (
1717
"context"
1818
"net"
19-
"os"
2019
"runtime"
2120
"sync"
2221
"time"
@@ -275,58 +274,6 @@ func Go(ctx context.Context, timeout time.Duration, handler func(context.Context
275274
return DefaultGoer.Go(ctx, timeout, handler)
276275
}
277276

278-
// expandEnv looks for ${var} in s and replaces them with value of the
279-
// corresponding environment variable.
280-
// $var is considered invalid.
281-
// It's not like os.ExpandEnv which will handle both ${var} and $var.
282-
// Since configurations like password for redis/mysql may contain $, this
283-
// method is needed.
284-
func expandEnv(s string) string {
285-
var buf []byte
286-
i := 0
287-
for j := 0; j < len(s); j++ {
288-
if s[j] == '$' && j+2 < len(s) && s[j+1] == '{' { // only ${var} instead of $var is valid
289-
if buf == nil {
290-
buf = make([]byte, 0, 2*len(s))
291-
}
292-
buf = append(buf, s[i:j]...)
293-
name, w := getEnvName(s[j+1:])
294-
if name == "" && w > 0 {
295-
// invalid matching, remove the $
296-
} else if name == "" {
297-
buf = append(buf, s[j]) // keep the $
298-
} else {
299-
buf = append(buf, os.Getenv(name)...)
300-
}
301-
j += w
302-
i = j + 1
303-
}
304-
}
305-
if buf == nil {
306-
return s
307-
}
308-
return string(buf) + s[i:]
309-
}
310-
311-
// getEnvName gets env name, that is, var from ${var}.
312-
// And content of var and its len will be returned.
313-
func getEnvName(s string) (string, int) {
314-
// look for right curly bracket '}'
315-
// it's guaranteed that the first char is '{' and the string has at least two char
316-
for i := 1; i < len(s); i++ {
317-
if s[i] == ' ' || s[i] == '\n' || s[i] == '"' { // "xx${xxx"
318-
return "", 0 // encounter invalid char, keep the $
319-
}
320-
if s[i] == '}' {
321-
if i == 1 { // ${}
322-
return "", 2 // remove ${}
323-
}
324-
return s[1:i], i + 1
325-
}
326-
}
327-
return "", 0 // no },keep the $
328-
}
329-
330277
// --------------- the following code is IP Config related -----------------//
331278

332279
// nicIP defines the parameters used to record the ip address (ipv4 & ipv6) of the nic.

0 commit comments

Comments
 (0)