Skip to content

Commit 15589a1

Browse files
authored
merge: PR #8 from feature/env-interpolation
feature/env-interpolation
2 parents f1cf05d + 906afbb commit 15589a1

File tree

10 files changed

+77
-59
lines changed

10 files changed

+77
-59
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ You can configure periodic probes using a simple `curl` command in a cron job or
178178
- [x] Whitebox YAML configuration w/ auto-reload by SIGHUP ([#7](https://github.com/quyxishi/whitebox/pull/7)).
179179
- [x] Response status/body validation.
180180
- [x] Custom HTTP-headers qualify support.
181+
- [x] Configuration environment variables interpolation support.
181182
- [ ] Authorization/OAuth 2.0 support.
182183
- [ ] Configuration for TLS protocol of HTTP probe support.
183184
- [ ] More advanced metrics.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.1
1+
1.1.0

cmd/api/cli.go

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
package main
22

33
import (
4-
"log/slog"
5-
"os"
6-
74
"github.com/alecthomas/kong"
8-
"github.com/goccy/go-yaml"
95
"github.com/quyxishi/whitebox/internal/config"
106
)
117

@@ -22,20 +18,5 @@ func (h *CLI) LoadConfig() (*config.WhiteboxConfig, error) {
2218
}
2319

2420
// if path provided, load from file
25-
file, err := os.Open(h.ConfigPath)
26-
if err != nil {
27-
return nil, err
28-
}
29-
defer func() {
30-
if err := file.Close(); err != nil {
31-
slog.Error("failed to close file instance", "err", err)
32-
}
33-
}()
34-
35-
var config config.WhiteboxConfig
36-
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
37-
return nil, err
38-
}
39-
40-
return &config, nil
21+
return config.Load(h.ConfigPath)
4122
}

cmd/api/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ func main() {
7474

7575
_, err := parser.Parse(os.Args[1:])
7676
if err != nil {
77-
parser.FatalIfErrorf(fmt.Errorf("unable to parse cli args due: %w", err))
77+
parser.FatalIfErrorf(fmt.Errorf("unable to parse cli args due: %v", err))
7878
}
7979

8080
cfg, err := cli.LoadConfig()
8181
if err != nil {
82-
parser.FatalIfErrorf(fmt.Errorf("unable to load config file due: %w", err))
82+
parser.FatalIfErrorf(fmt.Errorf("unable to load config file due: %v", err))
8383
}
8484

8585
wrapper := config.NewConfigWrapper(cfg)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ require (
4444
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
4545
github.com/fsnotify/fsnotify v1.9.0 // indirect
4646
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
47-
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
47+
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect
4848
github.com/gin-contrib/sse v1.1.0 // indirect
4949
github.com/go-playground/locales v0.14.1 // indirect
5050
github.com/go-playground/universal-translator v0.18.1 // indirect

internal/api/v1/probe/handler.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import (
4343
func matchRegularExpression(body []byte, predicate string) (bool, error) {
4444
regex, err := regexp.Compile(predicate)
4545
if err != nil {
46-
return false, fmt.Errorf("failed to compile regex:%s due: %w", predicate, err)
46+
return false, fmt.Errorf("failed to compile regex:%s due: %v", predicate, err)
4747
}
4848

4949
return regex.Match(body), nil
@@ -54,17 +54,17 @@ func newCELExpression(predicate string) (*cel.Program, error) {
5454
cel.Variable("body", cel.DynType),
5555
)
5656
if err != nil {
57-
return nil, fmt.Errorf("failed to construct CEL environment due: %w", err)
57+
return nil, fmt.Errorf("failed to construct CEL environment due: %v", err)
5858
}
5959

6060
ast, issues := env.Compile(predicate)
6161
if issues != nil && issues.Err() != nil {
62-
return nil, fmt.Errorf("failed to compile CEL:%s due: %w", predicate, issues.Err())
62+
return nil, fmt.Errorf("failed to compile CEL:%s due: %v", predicate, issues.Err())
6363
}
6464

6565
celExpr, err := env.Program(ast, cel.InterruptCheckFrequency(100))
6666
if err != nil {
67-
return nil, fmt.Errorf("failed to construct CEL:%s due: %w", predicate, err)
67+
return nil, fmt.Errorf("failed to construct CEL:%s due: %v", predicate, err)
6868
}
6969

7070
return &celExpr, nil
@@ -73,7 +73,7 @@ func newCELExpression(predicate string) (*cel.Program, error) {
7373
func matchCELExpression(ctx context.Context, body []byte, predicate string) (bool, error) {
7474
var bodyJSON any
7575
if err := json.Unmarshal(body, &bodyJSON); err != nil {
76-
return false, fmt.Errorf("failed to unmarshall http body to json due: %w", err)
76+
return false, fmt.Errorf("failed to unmarshall http body to json due: %v", err)
7777
}
7878

7979
evalPayload := map[string]any{
@@ -82,12 +82,12 @@ func matchCELExpression(ctx context.Context, body []byte, predicate string) (boo
8282

8383
celExpr, err := newCELExpression(predicate)
8484
if err != nil {
85-
return false, fmt.Errorf("unable to perform CEL validation due: %w", err)
85+
return false, fmt.Errorf("unable to perform CEL validation due: %v", err)
8686
}
8787

8888
result, details, err := (*celExpr).ContextEval(ctx, evalPayload)
8989
if err != nil {
90-
return false, fmt.Errorf("failed to evaluate CEL:%s due: %w", predicate, err)
90+
return false, fmt.Errorf("failed to evaluate CEL:%s due: %v", predicate, err)
9191
}
9292
if result.Type() != cel.BoolType {
9393
return false, fmt.Errorf("on CEL:%s evaluation result is not a boolean, details: %v", predicate, details)
@@ -105,7 +105,7 @@ func matchRegularExpressionsOnHeaders(headers http.Header, key string, predicate
105105

106106
regex, err := regexp.Compile(predicate)
107107
if err != nil {
108-
return false, fmt.Errorf("failed to compile regex:%s due: %w", predicate, err)
108+
return false, fmt.Errorf("failed to compile regex:%s due: %v", predicate, err)
109109
}
110110

111111
for _, v := range values {

internal/config/config.go

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"os"
77
"time"
88

9-
"github.com/ghodss/yaml"
9+
"github.com/goccy/go-yaml"
1010
)
1111

1212
const DefaultScopeName string = "default"
@@ -45,21 +45,21 @@ func Load(path string) (*WhiteboxConfig, error) {
4545
return nil, err
4646
}
4747

48+
expandedData := ExpandEnvironment(data)
49+
4850
var config WhiteboxConfig
49-
if err := yaml.Unmarshal(data, &config); err != nil {
51+
if err := yaml.Unmarshal(expandedData, &config); err != nil {
5052
slog.Error("unable to parse whitebox config file", "err", err)
5153
return nil, err
5254
}
5355

5456
for name, scope := range config.Scopes {
55-
if err := scope.Validate(); err != nil {
57+
if err := scope.Http.Validate(); err != nil {
5658
slog.Error("whitebox scope configuration is invalid", "name", name, "err", err)
57-
return nil, fmt.Errorf("invalid scope configuration: %w", err)
59+
return nil, fmt.Errorf("invalid scope configuration: %v", err)
5860
}
5961
}
6062

61-
// todo! cover case when scope doesn't have http declared
62-
6363
if _, ok := config.Scopes[DefaultScopeName]; !ok {
6464
config.Scopes[DefaultScopeName] = NewScopeRecord()
6565
}
@@ -80,25 +80,6 @@ func NewScopeRecord() ScopeRecord {
8080
}
8181
}
8282

83-
// Validate ensures the scope configuration semantic correctness
84-
func (h *ScopeRecord) Validate() error {
85-
for i, rule := range h.Http.FailIf {
86-
switch rule.Mod {
87-
case FailIf_BodyMatchesRegexp, FailIf_BodyJsonMatchesCEL:
88-
// Valid
89-
case "":
90-
return fmt.Errorf("http.fail_if[%d]: mod is required", i)
91-
default:
92-
return fmt.Errorf("http.fail_if[%d]: unknown module '%s'", i, rule.Mod)
93-
}
94-
95-
if rule.Val == "" {
96-
return fmt.Errorf("http.fail_if[%d]: val (pattern/expression) cannot be empty", i)
97-
}
98-
}
99-
return nil
100-
}
101-
10283
type HttpRecord struct {
10384
// Fallbacks to 5 by default
10485
MaxRedirects int `yaml:"max_redirects,omitempty"`
@@ -121,6 +102,25 @@ func NewHttpRecord() HttpRecord {
121102
}
122103
}
123104

105+
// Validate ensures the http configuration semantic correctness
106+
func (h *HttpRecord) Validate() error {
107+
for i, rule := range h.FailIf {
108+
switch rule.Mod {
109+
case FailIf_SSL, FailIf_BodyMatchesRegexp, FailIf_BodyJsonMatchesCEL, FailIf_HeaderMatchesRegexp, FailIf_StatusCodeMatches:
110+
// Valid
111+
case "":
112+
return fmt.Errorf("http.fail_if[%d]: mod is required", i)
113+
default:
114+
return fmt.Errorf("http.fail_if[%d]: unknown module '%s'", i, rule.Mod)
115+
}
116+
117+
if rule.Val == "" && rule.Mod != FailIf_SSL {
118+
return fmt.Errorf("http.fail_if[%d]: val (pattern/expression) cannot be empty", i)
119+
}
120+
}
121+
return nil
122+
}
123+
124124
type FailIfRecord struct {
125125
// Predicate, see FailIf_* constants modules
126126
Mod FailIfModule `yaml:"mod,omitempty"`

internal/config/env.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package config
2+
3+
import (
4+
"os"
5+
"regexp"
6+
)
7+
8+
// ${VAR} or ${VAR:-default}
9+
var envVarRegex = regexp.MustCompile(`\${([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?}`)
10+
11+
// Replaces ${VAR} and ${VAR:-default} with actual environment values.
12+
func ExpandEnvironment(data []byte) []byte {
13+
return envVarRegex.ReplaceAllFunc(data, func(match []byte) []byte {
14+
s := string(match)
15+
16+
// ReplaceAllFunc gives the whole match, not subgroups directly
17+
submatches := envVarRegex.FindStringSubmatch(s)
18+
if len(submatches) < 2 {
19+
return match
20+
}
21+
22+
var defaultValue string
23+
if len(submatches) > 2 {
24+
defaultValue = submatches[2]
25+
}
26+
27+
// Lookup environment variable
28+
val, exists := os.LookupEnv(submatches[1])
29+
if !exists {
30+
return []byte(defaultValue)
31+
}
32+
33+
return []byte(val)
34+
})
35+
}

internal/serial/parse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func ParseSubscriptionURI(json_sub_uri string, params *ParseSubParams) (out stri
9696

9797
for _, s := range debugFields {
9898
if err := j.Set(s.key, s.val); err != nil {
99-
return "", fmt.Errorf("failed to patch config with key '%s': %w", s.key, err)
99+
return "", fmt.Errorf("failed to patch config with key '%s': %v", s.key, err)
100100
}
101101
}
102102

whitebox.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ scopes:
3333
method: POST
3434

3535
headers:
36-
Authorization: "Bearer dXNlcjpwYXNz"
36+
# Environment variables interpolation
37+
Authorization: "Bearer ${BEARER_AUTH:-dXNlcjpwYXNz}"
3738
Content-Type: "application/json"
3839

3940
body: '{"ping": true}'

0 commit comments

Comments
 (0)