Skip to content

Commit 9749c4f

Browse files
committed
feat(secret): secret reference
1 parent 9111f92 commit 9749c4f

File tree

22 files changed

+1330
-52
lines changed

22 files changed

+1330
-52
lines changed

config/config.go

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package config
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"slices"
78

89
"github.com/creasty/defaults"
910
"github.com/webhookx-io/webhookx/pkg/envconfig"
11+
"github.com/webhookx-io/webhookx/pkg/secret"
12+
"github.com/webhookx-io/webhookx/pkg/secret/reference"
13+
"github.com/webhookx-io/webhookx/utils"
14+
"go.uber.org/zap"
1015
"gopkg.in/yaml.v3"
1116
)
1217

@@ -37,6 +42,7 @@ type Config struct {
3742
Tracing TracingConfig `yaml:"tracing" json:"tracing" envconfig:"TRACING"`
3843
Role Role `yaml:"role" json:"role" envconfig:"ROLE" default:"standalone"`
3944
AnonymousReports bool `yaml:"anonymous_reports" json:"anonymous_reports" envconfig:"ANONYMOUS_REPORTS" default:"true"`
45+
Secret SecretConfig `yaml:"secret" json:"secret" envconfig:"SECRET"`
4046
}
4147

4248
func (cfg Config) String() string {
@@ -81,6 +87,9 @@ func (cfg Config) Validate() error {
8187
if !slices.Contains([]Role{RoleStandalone, RoleCP, RoleDPWorker, RoleDPProxy}, cfg.Role) {
8288
return fmt.Errorf("invalid role: '%s'", cfg.Role)
8389
}
90+
if err := cfg.Secret.Validate(); err != nil {
91+
return err
92+
}
8493

8594
return nil
8695
}
@@ -89,27 +98,126 @@ type Options struct {
8998
YAML []byte
9099
}
91100

101+
func resolveReferences(n *yaml.Node, manager *secret.Manager) error {
102+
switch n.Kind {
103+
case yaml.ScalarNode:
104+
if reference.IsReference(n.Value) {
105+
ref, err := reference.Parse(n.Value)
106+
if err != nil {
107+
return err
108+
}
109+
val, err := manager.ResolveReference(context.TODO(), ref)
110+
if err != nil {
111+
return err
112+
}
113+
n.Value = val
114+
}
115+
case yaml.MappingNode:
116+
for i := 0; i < len(n.Content); i += 2 {
117+
if err := resolveReferences(n.Content[i+1], manager); err != nil {
118+
return err
119+
}
120+
}
121+
case yaml.AliasNode:
122+
if n.Alias != nil {
123+
if err := resolveReferences(n.Alias, manager); err != nil {
124+
return err
125+
}
126+
}
127+
default:
128+
for _, c := range n.Content {
129+
if err := resolveReferences(c, manager); err != nil {
130+
return err
131+
}
132+
}
133+
}
134+
return nil
135+
}
136+
92137
func New(opts *Options) (*Config, error) {
93138
var cfg Config
94139
err := defaults.Set(&cfg)
95140
if err != nil {
96141
return nil, err
97142
}
98143

99-
if opts != nil {
100-
if len(opts.YAML) > 0 {
101-
if err := yaml.Unmarshal(opts.YAML, &cfg); err != nil {
102-
return nil, err
144+
if opts == nil {
145+
opts = &Options{}
146+
}
147+
148+
var doc *yaml.Node
149+
if len(opts.YAML) > 0 {
150+
doc = new(yaml.Node)
151+
if err := yaml.Unmarshal(opts.YAML, doc); err != nil {
152+
return nil, err
153+
}
154+
}
155+
156+
// todo: only if secret feature is allowed
157+
if doc != nil {
158+
// fixme
159+
if len(doc.Content) > 0 {
160+
if node := utils.FindYaml(doc.Content[0], "secret"); node != nil {
161+
if err := node.Decode(&cfg.Secret); err != nil {
162+
return nil, err
163+
}
103164
}
104165
}
105166
}
167+
if err := envconfig.Process("WEBHOOKX_SECRET", &cfg.Secret); err != nil {
168+
return nil, err
169+
}
106170

107-
err = envconfig.Process("WEBHOOKX", &cfg)
108-
if err != nil {
171+
var reader = envconfig.EnvironmentReader
172+
173+
if err := cfg.Secret.Validate(); err != nil {
109174
return nil, err
110175
}
111176

112-
return &cfg, nil
177+
if cfg.Secret.Enabled() {
178+
providers := make(map[string]map[string]interface{})
179+
for _, p := range cfg.Secret.Providers {
180+
name := string(p)
181+
providers[name] = cfg.Secret.GetProviderConfiguration(name)
182+
}
183+
manager := secret.NewManager(zap.S(), providers)
184+
reader = func(key string) (string, bool, error) {
185+
value, ok, _ := envconfig.EnvironmentReader.Read(key)
186+
if ok && reference.IsReference(value) {
187+
ref, err := reference.Parse(value)
188+
if err != nil {
189+
return "", false, err
190+
}
191+
resolved, err := manager.ResolveReference(context.TODO(), ref)
192+
if err != nil {
193+
return "", false, err
194+
}
195+
value = resolved
196+
}
197+
return value, ok, nil
198+
}
199+
200+
if doc != nil {
201+
if len(doc.Content) > 0 {
202+
if err := resolveReferences(doc.Content[0], manager); err != nil {
203+
return nil, err
204+
}
205+
}
206+
resolvedYaml := utils.Must(yaml.Marshal(doc))
207+
fmt.Println(string(resolvedYaml))
208+
doc = new(yaml.Node)
209+
_ = yaml.Unmarshal(resolvedYaml, doc)
210+
}
211+
}
212+
213+
if doc != nil {
214+
if err := doc.Decode(&cfg); err != nil {
215+
return nil, err
216+
}
217+
}
218+
219+
err = envconfig.ProcessWithReader("WEBHOOKX", &cfg, reader)
220+
return &cfg, err
113221
}
114222

115223
func (cfg *Config) OverrideByRole(role Role) {

config/config_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,47 @@ func TestWorkerProxyConfig(t *testing.T) {
481481
}
482482
}
483483

484+
func TestSecretConfig(t *testing.T) {
485+
tests := []struct {
486+
desc string
487+
cfg SecretConfig
488+
validateErr error
489+
}{
490+
{
491+
desc: "sanity",
492+
cfg: SecretConfig{
493+
Vault: VaultProviderConfig{
494+
AuthMethod: "token",
495+
},
496+
},
497+
validateErr: nil,
498+
},
499+
{
500+
desc: "invalid provider",
501+
cfg: SecretConfig{
502+
Providers: []Provider{"aws", "vault", "unknown"},
503+
Vault: VaultProviderConfig{
504+
AuthMethod: "token",
505+
},
506+
},
507+
validateErr: errors.New("invalid provider: unknown"),
508+
},
509+
{
510+
desc: "invalid vault.auth_method",
511+
cfg: SecretConfig{
512+
Vault: VaultProviderConfig{
513+
AuthMethod: "unknown",
514+
},
515+
},
516+
validateErr: errors.New("invalid auth_method: unknown"),
517+
},
518+
}
519+
for _, test := range tests {
520+
actual := test.cfg.Validate()
521+
assert.Equal(t, test.validateErr, actual, "expected %v got %v", test.validateErr, actual)
522+
}
523+
}
524+
484525
func TestConfig(t *testing.T) {
485526
cfg, err := New(nil)
486527
assert.Nil(t, err)

config/secret.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
7+
"github.com/webhookx-io/webhookx/pkg/secret/provider/vault"
8+
"github.com/webhookx-io/webhookx/utils"
9+
)
10+
11+
type Provider string
12+
13+
const (
14+
ProviderAWS Provider = "aws"
15+
ProviderVault Provider = "vault"
16+
)
17+
18+
type SecretConfig struct {
19+
Providers []Provider `json:"providers" yaml:"providers" default:"[\"aws\", \"vault\"]"`
20+
Aws AwsProviderConfig `json:"aws" yaml:"aws"`
21+
Vault VaultProviderConfig `json:"vault" yaml:"vault"`
22+
}
23+
24+
func (cfg *SecretConfig) Validate() error {
25+
for _, name := range cfg.Providers {
26+
if !slices.Contains([]Provider{ProviderAWS, ProviderVault}, name) {
27+
return fmt.Errorf("invalid provider: %s", name)
28+
}
29+
}
30+
if err := cfg.Aws.Validate(); err != nil {
31+
return err
32+
}
33+
if err := cfg.Vault.Validate(); err != nil {
34+
return err
35+
}
36+
return nil
37+
}
38+
39+
func (cfg *SecretConfig) Enabled() bool {
40+
return len(cfg.Providers) > 0
41+
}
42+
43+
func (cfg *SecretConfig) GetProviderConfiguration(name string) map[string]interface{} {
44+
switch Provider(name) {
45+
case ProviderAWS:
46+
return utils.Must(utils.StructToMap(cfg.Aws))
47+
case ProviderVault:
48+
return utils.Must(utils.StructToMap(cfg.Vault))
49+
default:
50+
return nil
51+
}
52+
}
53+
54+
type AwsProviderConfig struct {
55+
Region string `json:"region" yaml:"region"`
56+
URL string `json:"url" yaml:"url"`
57+
}
58+
59+
func (cfg *AwsProviderConfig) Validate() error {
60+
return nil
61+
}
62+
63+
type VaultProviderConfig struct {
64+
Address string `json:"address" yaml:"address" default:"http://localhost:8200"`
65+
MountPath string `json:"mount_path" yaml:"mount_path" default:"secret" split_words:"true"`
66+
Namespace string `json:"namespace" yaml:"namespace"`
67+
AuthMethod string `json:"auth_method" yaml:"auth_method" default:"token" split_words:"true"`
68+
AuthN vault.AuthN `json:"authn" yaml:"authn"`
69+
}
70+
71+
func (cfg *VaultProviderConfig) Validate() error {
72+
if !slices.Contains([]string{"token", "approle", "kubernetes"}, cfg.AuthMethod) {
73+
return fmt.Errorf("invalid auth_method: %s", cfg.AuthMethod)
74+
}
75+
return nil
76+
}

go.mod

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ go 1.25.4
55
require (
66
github.com/Masterminds/squirrel v1.5.4
77
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
8+
github.com/aws/aws-sdk-go-v2 v1.39.6
9+
github.com/aws/aws-sdk-go-v2/config v1.31.20
10+
github.com/aws/aws-sdk-go-v2/credentials v1.18.24
11+
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.13
12+
github.com/aws/smithy-go v1.23.2
813
github.com/creasty/defaults v1.8.0
914
github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7
1015
github.com/elazarl/goproxy v1.7.2
@@ -17,9 +22,13 @@ require (
1722
github.com/golang-migrate/migrate/v4 v4.19.0
1823
github.com/gorilla/mux v1.8.1
1924
github.com/hashicorp/golang-lru/v2 v2.0.7
25+
github.com/hashicorp/vault/api v1.22.0
26+
github.com/hashicorp/vault/api/auth/approle v0.11.0
27+
github.com/hashicorp/vault/api/auth/kubernetes v0.10.0
2028
github.com/jackc/pgx/v5 v5.7.6
2129
github.com/jmoiron/sqlx v1.4.0
2230
github.com/lib/pq v1.10.9
31+
github.com/mitchellh/mapstructure v1.5.0
2332
github.com/onsi/ginkgo/v2 v2.27.2
2433
github.com/onsi/gomega v1.38.2
2534
github.com/pkg/errors v0.9.1
@@ -30,6 +39,7 @@ require (
3039
github.com/spf13/cobra v1.10.1
3140
github.com/stretchr/testify v1.11.1
3241
github.com/tetratelabs/wazero v1.10.0
42+
github.com/tidwall/gjson v1.18.0
3343
github.com/vmihailenco/msgpack/v5 v5.4.1
3444
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0
3545
go.opentelemetry.io/otel v1.38.0
@@ -52,23 +62,44 @@ require github.com/felixge/httpsnoop v1.0.4 // indirect
5262

5363
require (
5464
github.com/Masterminds/semver/v3 v3.4.0 // indirect
65+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
66+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
67+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
68+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
69+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
70+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
71+
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect
72+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
73+
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect
74+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
5575
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
5676
github.com/dlclark/regexp2 v1.11.4 // indirect
77+
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
5778
github.com/go-openapi/jsonpointer v0.21.0 // indirect
5879
github.com/go-openapi/swag v0.23.0 // indirect
5980
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
81+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
82+
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
83+
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
84+
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
85+
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
86+
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
87+
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
6088
github.com/jackc/pgpassfile v1.0.0 // indirect
6189
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
6290
github.com/jackc/puddle/v2 v2.2.2 // indirect
6391
github.com/josharian/intern v1.0.0 // indirect
6492
github.com/mailru/easyjson v0.7.7 // indirect
65-
github.com/mattn/go-colorable v0.1.13 // indirect
66-
github.com/mattn/go-isatty v0.0.19 // indirect
67-
github.com/mitchellh/mapstructure v1.5.0 // indirect
93+
github.com/mattn/go-colorable v0.1.14 // indirect
94+
github.com/mattn/go-isatty v0.0.20 // indirect
95+
github.com/mitchellh/go-homedir v1.1.0 // indirect
6896
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
6997
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
7098
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
7199
github.com/perimeterx/marshmallow v1.1.5 // indirect
100+
github.com/ryanuber/go-glob v1.0.0 // indirect
101+
github.com/tidwall/match v1.2.0 // indirect
102+
github.com/tidwall/pretty v1.2.1 // indirect
72103
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
73104
github.com/woodsbury/decimal128 v1.3.0 // indirect
74105
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@@ -78,6 +109,7 @@ require (
78109
go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect
79110
go.yaml.in/yaml/v3 v3.0.4 // indirect
80111
golang.org/x/mod v0.27.0 // indirect
112+
golang.org/x/time v0.12.0 // indirect
81113
)
82114

83115
require (

0 commit comments

Comments
 (0)