Skip to content

Commit ec00b68

Browse files
committed
wip
1 parent 86348c6 commit ec00b68

File tree

10 files changed

+275
-19
lines changed

10 files changed

+275
-19
lines changed

config.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,13 @@ tracing:
153153
opentelemetry:
154154
protocol: http/protobuf # Supported value: http/protobuf, grpc.
155155
endpoint: http://localhost:4318/v1/traces # http/protobuf(http://localhost:4318/v1/traces), grpc(localhost:4317)
156+
157+
158+
#------------------------------------------------------------------------------
159+
# Secret
160+
#------------------------------------------------------------------------------
161+
#secret:
162+
# provider: aws
163+
# aws:
164+
# region: us-west-1
165+
# url: http://localhost:4566

config/config.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,25 @@ func Init() (*Config, error) {
9898
return nil, err
9999
}
100100

101-
err := envconfig.Process("WEBHOOKX", &cfg)
102-
if err != nil {
101+
if err := envconfig.Process("WEBHOOKX_SECRET", &cfg.Secret); err != nil {
103102
return nil, err
104103
}
104+
if cfg.Secret.Provider != "" {
105+
var providerConfig map[string]interface{}
106+
switch cfg.Secret.Provider {
107+
case ProviderAWS:
108+
providerConfig = utils.Must(utils.StructToMap(cfg.Secret.Aws))
109+
}
110+
manager := secret.NewManager(secret.ProviderType(cfg.Secret.Provider), providerConfig)
111+
err := envconfig.ProcessWithReader("WEBHOOKX", &cfg,
112+
envconfig.ReaderFunc(func(key string) (string, bool, error) {
113+
return manager.Get(key)
114+
}))
115+
return &cfg, err
116+
}
105117

106-
return &cfg, nil
118+
err := envconfig.Process("WEBHOOKX", &cfg)
119+
return &cfg, err
107120
}
108121

109122
func InitWithFile(filename string) (*Config, error) {

config/secret.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package config
2+
3+
type Provider string
4+
5+
const (
6+
ProviderAWS Provider = "aws"
7+
)
8+
9+
type SecretConfig struct {
10+
Provider Provider `json:"provider" yaml:"provider"`
11+
Aws AwsProviderConfig `json:"aws" yaml:"aws"`
12+
}
13+
14+
func (cfg *SecretConfig) Validate() error {
15+
return nil
16+
}
17+
18+
type AwsProviderConfig struct {
19+
Region string `json:"region" yaml:"region"`
20+
URL string `json:"url" yaml:"url"`
21+
}

pkg/envconfig/envconfig.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,30 +180,46 @@ func CheckDisallowed(prefix string, spec interface{}) error {
180180
return nil
181181
}
182182

183+
type Reader interface {
184+
Read(string) (string, bool, error)
185+
}
186+
187+
type ReaderFunc func(string) (string, bool, error)
188+
189+
func (f ReaderFunc) Read(key string) (string, bool, error) {
190+
return f(key)
191+
}
192+
193+
var (
194+
EnvironmentReader = ReaderFunc(func(key string) (string, bool, error) {
195+
v, ok := os.LookupEnv(key)
196+
return v, ok, nil
197+
})
198+
)
199+
183200
// Process populates the specified struct based on environment variables
184201
func Process(prefix string, spec interface{}) error {
202+
return ProcessWithReader(prefix, spec, EnvironmentReader)
203+
}
204+
205+
func ProcessWithReader(prefix string, spec interface{}, reader Reader) error {
185206
infos, err := gatherInfo(prefix, spec)
186207

187208
for _, info := range infos {
188-
189-
// `os.Getenv` cannot differentiate between an explicitly set empty value
190-
// and an unset value. `os.LookupEnv` is preferred to `syscall.Getenv`,
191-
// but it is only available in go1.5 or newer. We're using Go build tags
192-
// here to use os.LookupEnv for >=go1.5
193-
value, ok := lookupEnv(info.Key)
194-
if !ok && info.Alt != "" {
195-
value, ok = lookupEnv(info.Alt)
209+
value, ok, err := reader.Read(info.Key)
210+
if err != nil {
211+
return err // todo warp error?
196212
}
197213

198-
//patch: does not handle 'default' tag
199-
def := ""
214+
//patch: do not handle 'default' tag
215+
//def := ""
200216
//def := info.Tags.Get("default")
201217
//if def != "" && !ok {
202218
// value = def
203219
//}
204220

205221
req := info.Tags.Get("required")
206-
if !ok && def == "" {
222+
if !ok {
207223
if isTrue(req) {
208224
key := info.Key
209225
if info.Alt != "" {

pkg/secret/aws.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package secret
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"github.com/aws/aws-sdk-go-v2/aws"
8+
"github.com/aws/aws-sdk-go-v2/config"
9+
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
10+
"github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
11+
"github.com/aws/smithy-go/logging"
12+
"log"
13+
)
14+
15+
type AwsProvider struct {
16+
cfg interface{}
17+
client *secretsmanager.Client
18+
}
19+
20+
func NewAwsProvider(cfg map[string]interface{}) *AwsProvider {
21+
opts := []func(*config.LoadOptions) error{
22+
config.WithClientLogMode(aws.LogRequestWithBody | aws.LogResponseWithBody),
23+
config.WithLogger(logging.LoggerFunc(func(classification logging.Classification, format string, v ...interface{}) {
24+
log.Printf("[AWS %s] %s", classification, fmt.Sprintf(format, v...))
25+
})),
26+
}
27+
url := cfg["url"].(string)
28+
if url != "" {
29+
opts = append(opts, config.WithBaseEndpoint(url))
30+
}
31+
region := cfg["region"].(string)
32+
if region != "" {
33+
opts = append(opts, config.WithRegion(region))
34+
}
35+
36+
config, err := config.LoadDefaultConfig(context.TODO(), opts...)
37+
if err != nil {
38+
panic(err) // todo
39+
}
40+
41+
provider := &AwsProvider{}
42+
provider.cfg = cfg
43+
provider.client = secretsmanager.NewFromConfig(config, func(options *secretsmanager.Options) {})
44+
45+
return provider
46+
}
47+
48+
func (p *AwsProvider) GetValue(ctx context.Context, key string, properties map[string]string) (string, error) {
49+
result, err := p.client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{SecretId: aws.String(key)})
50+
if err != nil {
51+
var awsErr *types.ResourceNotFoundException
52+
if errors.As(err, &awsErr) {
53+
return "", fmt.Errorf("%w: %s", ErrSecretNotFound, key)
54+
}
55+
return "", err
56+
}
57+
return *result.SecretString, nil
58+
}

pkg/secret/manager.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package secret
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"strings"
8+
)
9+
10+
var (
11+
ErrSecretNotFound = errors.New("secret not found")
12+
)
13+
14+
type ProviderType string
15+
16+
const (
17+
AwsPrivider ProviderType = "aws"
18+
)
19+
20+
type Provider interface {
21+
GetValue(ctx context.Context, key string, properties map[string]string) (string, error)
22+
}
23+
24+
type Manager struct {
25+
provider Provider
26+
}
27+
28+
func NewManager(typ ProviderType, cfg map[string]interface{}) *Manager {
29+
var p Provider
30+
switch typ {
31+
case AwsPrivider:
32+
p = NewAwsProvider(cfg)
33+
}
34+
return &Manager{
35+
provider: p,
36+
}
37+
}
38+
39+
func (p *Manager) Get(key string) (string, bool, error) {
40+
value, ok := os.LookupEnv(key)
41+
if ok && strings.HasPrefix(value, "{secret://") && strings.HasSuffix(value, "}") {
42+
ref, err := Parse(value)
43+
if err != nil {
44+
return "", false, err
45+
}
46+
value, err = p.provider.GetValue(context.TODO(), ref.Name, ref.Properties)
47+
if err != nil {
48+
return "", false, err
49+
}
50+
return value, true, nil
51+
}
52+
return value, ok, nil
53+
}

pkg/secret/reference.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func Parse(s string) (*Reference, error) {
4242

4343
ref := &Reference{
4444
Type: u.Host,
45-
Name: u.Path,
45+
Name: u.Path[1:],
4646
Properties: make(map[string]string),
4747
}
4848
for k := range values {

test/anonymous/anonymous_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/webhookx-io/webhookx/test/helper"
99
"github.com/webhookx-io/webhookx/utils"
1010
"testing"
11+
"time"
1112
)
1213

1314
var _ = Describe("anonymous reports", Ordered, func() {
@@ -18,7 +19,7 @@ var _ = Describe("anonymous reports", Ordered, func() {
1819
BeforeAll(func() {
1920
helper.InitDB(true, nil)
2021
app = utils.Must(helper.Start(map[string]string{
21-
"ANONYMOUS_REPORTS": "false",
22+
"WEBHOOKX_ANONYMOUS_REPORTS": "false",
2223
}))
2324
})
2425

@@ -27,9 +28,11 @@ var _ = Describe("anonymous reports", Ordered, func() {
2728
})
2829

2930
It("should display log when anonymous_reports is disabled", func() {
30-
matched, err := helper.FileHasLine("webhookx.log", "^.*anonymous reports is disabled$")
31-
assert.Nil(GinkgoT(), err)
32-
assert.Equal(GinkgoT(), true, matched)
31+
assert.Eventually(GinkgoT(), func() bool {
32+
matched, err := helper.FileHasLine("webhookx.log", "^.*anonymous reports is disabled$")
33+
assert.Nil(GinkgoT(), err)
34+
return matched
35+
}, time.Second, time.Millisecond*100)
3336
})
3437
})
3538

test/docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,14 @@ services:
4343
ports:
4444
- 4317:4317
4545
- 4318:4318
46+
47+
localstack:
48+
container_name: "localstack"
49+
image: localstack/localstack
50+
ports:
51+
- "4566:4566"
52+
environment:
53+
DEBUG: "1"
54+
# DOCKER_HOST: unix:///var/run/docker.sock
55+
# volumes:
56+
# - "/var/run/docker.sock:/var/run/docker.sock"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package secret_reference
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-sdk-go-v2/aws"
6+
"github.com/aws/aws-sdk-go-v2/config"
7+
"github.com/aws/aws-sdk-go-v2/credentials"
8+
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
9+
. "github.com/onsi/ginkgo/v2"
10+
"github.com/onsi/gomega"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/webhookx-io/webhookx/app"
13+
"github.com/webhookx-io/webhookx/test/helper"
14+
"testing"
15+
)
16+
17+
var _ = Describe("SecretReference", Ordered, func() {
18+
19+
Context("test", func() {
20+
var app *app.Application
21+
22+
AfterEach(func() {
23+
app.Stop()
24+
})
25+
26+
BeforeAll(func() {
27+
helper.InitDB(true, nil)
28+
// mock aws config
29+
config, err := config.LoadDefaultConfig(context.TODO(),
30+
config.WithBaseEndpoint("http://localhost:4566"),
31+
config.WithRegion("us-east-1"),
32+
config.WithCredentialsProvider(aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(
33+
"test",
34+
"test",
35+
"",
36+
))),
37+
)
38+
assert.NoError(GinkgoT(), err)
39+
client := secretsmanager.NewFromConfig(config, func(options *secretsmanager.Options) {})
40+
_, err = client.CreateSecret(context.TODO(), &secretsmanager.CreateSecretInput{
41+
Name: aws.String("path/to/value"),
42+
SecretString: aws.String("this is value"),
43+
})
44+
assert.NoError(GinkgoT(), err)
45+
})
46+
47+
AfterAll(func() {
48+
app.Stop()
49+
})
50+
51+
It("reference should be de-reference", func() {
52+
var err error
53+
app, err = helper.Start(map[string]string{
54+
"AWS_ACCESS_KEY_ID": "test",
55+
"AWS_SECRET_ACCESS_KEY": "test",
56+
"WEBHOOKX_SECRET_AWS_REGION": "us-east-1",
57+
"WEBHOOKX_SECRET_AWS_URL": "http://localhost:4566",
58+
"WEBHOOKX_SECRET_PROVIDER": "aws",
59+
"WEBHOOKX_REDIS_PASSWORD": "{secret://aws/path/to/value}",
60+
})
61+
assert.Nil(GinkgoT(), err)
62+
assert.Equal(GinkgoT(), "this is value", string(app.Config().Redis.Password))
63+
})
64+
})
65+
66+
})
67+
68+
func TestAdmin(t *testing.T) {
69+
gomega.RegisterFailHandler(Fail)
70+
RunSpecs(t, "SecretReference Suite")
71+
}

0 commit comments

Comments
 (0)