diff --git a/README.md b/README.md
index 9387b818ce0..78b757619c4 100644
--- a/README.md
+++ b/README.md
@@ -69,8 +69,18 @@ Example:
```go
ffclient.Init(ffclient.Config{
PollInterval: 3,
- Logger: log.New(file, "/tmp/log", 0)
- Context context.Background(),
+ Logger: log.New(file, "/tmp/log", 0),
+ Context: context.Background(),
+ Retriever: &ffclient.FileRetriever{Path: "testdata/test.yaml"},
+ Webhooks: []ffclient.WebhookConfig{
+ {
+ PayloadURL: " https://example.com/hook",
+ Secret: "Secret",
+ Meta: map[string]string{
+ "app.name": "my app",
+ },
+ },
+ },
})
```
@@ -80,7 +90,7 @@ ffclient.Init(ffclient.Config{
|`Logger` | Logger used to log what `go-feature-flag` is doing.
If no logger is provided the module will not log anything.|
|`Context` | The context used by the retriever.
The default value is `context.Background()`.|
|`Retriever` | The configuration retriever you want to use to get your flag file *(see [Where do I store my flags file](#where-do-i-store-my-flags-file) for the configuration details)*.|
-
+|`Webhooks` | List of webhooks to call when your flag file has changed *(see [webhook section](#webhook) for more details)*.|
## Where do I store my flags file
`go-feature-flags` support different ways of retrieving the flag file.
We can have only one source for the file, if you set multiple sources in your configuration, only one will be take in
@@ -256,6 +266,111 @@ The default value is return when an error is encountered _(`ffclient` not initia
In the example, if the flag `your.feature.key` does not exists, result will be `false`.
Not that you will always have a usable value in the result.
+## Webhook
+If you want to be informed when a flag has changed outside of your app, you can configure a webhook.
+
+```go
+ffclient.Config{
+ // ...
+ Webhooks: []ffclient.WebhookConfig{
+ {
+ PayloadURL: " https://example.com/hook",
+ Secret: "Secret",
+ Meta: map[string]string{
+ "app.name": "my app",
+ },
+ },
+ },
+}
+```
+
+| | | |
+|---|---|---|
+|`PayloadURL` |![mandatory](https://img.shields.io/badge/-mandatory-red) | The complete URL of your API *(we will send a POST request to this URL, [see format](#format))* |
+|`Secret` |![optional](https://img.shields.io/badge/-optional-green) | A secret key you can share with your webhook. We will use this key to sign the request *(see [signature section](#signature) for more details)*. |
+|`Meta` |![optional](https://img.shields.io/badge/-optional-green) | A list of key value that will be add in your request, this is super usefull if you to add information on the current running instance of your app.
*By default the hostname is always added in the meta informations.*|
+
+
+
+### Format
+If you have configured a webhook, a POST request will be sent to the `PayloadURL` with a body in this format:
+
+```json
+{
+ "meta": {
+ "hostname": "server01",
+ // ...
+ },
+ "flags": {
+ "deleted": {}, // map of your deleted flags
+ "added": {}, // map of your added flags
+ "updated": {
+ "flag-name": { // an object that contains old and new value
+ "old_value": {},
+ "new_value": {}
+ }
+ }
+ }
+}
+```
+
+
+Example
+
+ ```json
+{
+ "meta":{
+ "hostname": "server01"
+ },
+ "flags":{
+ "deleted": {
+ "test-flag": {
+ "rule": "key eq \"random-key\"",
+ "percentage": 100,
+ "true": true,
+ "false": false,
+ "default": false
+ }
+ },
+ "added": {
+ "test-flag3": {
+ "percentage": 5,
+ "true": "test",
+ "false": "false",
+ "default": "default"
+ }
+ },
+ "updated": {
+ "test-flag2": {
+ "old_value": {
+ "rule": "key eq \"not-a-key\"",
+ "percentage": 100,
+ "true": true,
+ "false": false,
+ "default": false
+ },
+ "new_value": {
+ "disable": true,
+ "rule": "key eq \"not-a-key\"",
+ "percentage": 100,
+ "true": true,
+ "false": false,
+ "default": false
+ }
+ }
+ }
+ }
+}
+```
+
+
+### Signature
+This header **`X-Hub-Signature-256`** is sent if the webhook is configured with a secret. This is the HMAC hex digest of the request body, and is generated using the SHA-256 hash function and the secret as the HMAC key.
+
+:warning: **The recommendation is to always use the `Secret` and on your API/webook always verify the signature key to be sure that you don't have a man in the middle attack.**
+
+---
+
## Multiple flag configurations
`go-feature-flag` comes ready to use out of the box by calling the `Init` function and after that it will be available everywhere.
Since most applications will want to use a single central flag configuration, the `go-feature-flag` package provides this. It is similar to a singleton.
diff --git a/config.go b/config.go
index 160d421b3ed..27209229d62 100644
--- a/config.go
+++ b/config.go
@@ -18,10 +18,11 @@ import (
// PollInterval is the interval in seconds where we gonna read the file to update the cache.
// You should also have a retriever to specify where to read the flags file.
type Config struct {
- PollInterval int // Poll every X seconds
- Logger *log.Logger
- Context context.Context // default is context.Background()
- Retriever Retriever
+ PollInterval int // Poll every X seconds
+ Logger *log.Logger
+ Context context.Context // default is context.Background()
+ Retriever Retriever
+ Webhooks []WebhookConfig // webhooks we should call when a flag create/update/delete
}
// GetRetriever returns a retriever.FlagRetriever configure with the retriever available in the config.
@@ -131,3 +132,55 @@ func (r *GithubRetriever) getFlagRetriever() (retriever.FlagRetriever, error) {
return httpRetriever.getFlagRetriever()
}
+
+// WebhookConfig is the configuration of your webhook.
+// we will call this URL with a POST request with the following format
+//
+// {
+// "meta":{
+// "hostname": "server01"
+// },
+// "flags":{
+// "deleted": {
+// "test-flag": {
+// "rule": "key eq \"random-key\"",
+// "percentage": 100,
+// "true": true,
+// "false": false,
+// "default": false
+// }
+// },
+// "added": {
+// "test-flag3": {
+// "percentage": 5,
+// "true": "test",
+// "false": "false",
+// "default": "default"
+// }
+// },
+// "updated": {
+// "test-flag2": {
+// "old_value": {
+// "rule": "key eq \"not-a-key\"",
+// "percentage": 100,
+// "true": true,
+// "false": false,
+// "default": false
+// },
+// "new_value": {
+// "disable": true,
+// "rule": "key eq \"not-a-key\"",
+// "percentage": 100,
+// "true": true,
+// "false": false,
+// "default": false
+// }
+// }
+// }
+// }
+// }
+type WebhookConfig struct {
+ PayloadURL string // PayloadURL of your webhook
+ Secret string // Secret used to sign your request body.
+ Meta map[string]string // Meta information that you want to send to your webhook (not mandatory)
+}
diff --git a/feature_flag.go b/feature_flag.go
index 8f95e6041c9..cce46b69518 100644
--- a/feature_flag.go
+++ b/feature_flag.go
@@ -56,14 +56,20 @@ func New(config Config) (*GoFeatureFlag, error) {
return nil, fmt.Errorf("%d is not a valid PollInterval value, it need to be > 0", config.PollInterval)
}
+ notifiers, err := getNotifiers(config)
+ if err != nil {
+ return nil, fmt.Errorf("wrong configuration in your webhook: %v", err)
+ }
+ notificationService := cache.NewNotificationService(notifiers)
+
goFF := &GoFeatureFlag{
config: config,
bgUpdater: newBackgroundUpdater(config.PollInterval),
+ cache: cache.New(notificationService),
}
- goFF.cache = cache.New(cache.NewService(goFF.getNotifiers()))
// fail if we cannot retrieve the flags the 1st time
- err := retrieveFlagsAndUpdateCache(goFF.config, goFF.cache)
+ err = retrieveFlagsAndUpdateCache(goFF.config, goFF.cache)
if err != nil {
return nil, fmt.Errorf("impossible to retrieve the flags, please check your configuration: %v", err)
}
@@ -74,6 +80,7 @@ func New(config Config) (*GoFeatureFlag, error) {
return goFF, nil
}
+// Close wait until thread are done
func (g *GoFeatureFlag) Close() {
// clear the cache
g.cache.Close()
@@ -97,15 +104,6 @@ func (g *GoFeatureFlag) startFlagUpdaterDaemon() {
}
}
-// getNotifiers is creating Notifier from the config
-func (g *GoFeatureFlag) getNotifiers() []cache.Notifier {
- var notifiers []cache.Notifier
- if g.config.Logger != nil {
- notifiers = append(notifiers, &cache.LogNotifier{Logger: g.config.Logger})
- }
- return notifiers
-}
-
// retrieveFlagsAndUpdateCache is called every X seconds to refresh the cache flag.
func retrieveFlagsAndUpdateCache(config Config, cache cache.Cache) error {
retriever, err := config.GetRetriever()
diff --git a/feature_flag_test.go b/feature_flag_test.go
index ef20c48922c..d15342fb155 100644
--- a/feature_flag_test.go
+++ b/feature_flag_test.go
@@ -138,7 +138,7 @@ func TestImpossibleToLoadfile(t *testing.T) {
false: false
default: false`
- flagFile, _ := ioutil.TempFile("", "")
+ flagFile, _ := ioutil.TempFile("", "impossible")
_ = ioutil.WriteFile(flagFile.Name(), []byte(initialFileContent), 0600)
gffClient1, _ := New(Config{
@@ -161,3 +161,23 @@ func TestImpossibleToLoadfile(t *testing.T) {
flagValue, _ = gffClient1.BoolVariation("test-flag", ffuser.NewUser("random-key"), false)
assert.True(t, flagValue)
}
+
+func TestWrongWebhookConfig(t *testing.T) {
+ _, err := New(Config{
+ PollInterval: 5,
+ Retriever: &FileRetriever{Path: "testdata/test.yaml"},
+ Webhooks: []WebhookConfig{
+ {
+ PayloadURL: " https://example.com/hook",
+ Secret: "Secret",
+ Meta: map[string]string{
+ "my-app": "go-ff-test",
+ },
+ },
+ },
+ })
+
+ assert.Errorf(t, err, "wrong url should return an error")
+ assert.Equal(t, err.Error(), "wrong configuration in your webhook: parse \" https://example.com/hook\": "+
+ "first path segment in URL cannot contain colon")
+}
diff --git a/internal/HTTPClient.go b/internal/HTTPClient.go
new file mode 100644
index 00000000000..fe987a57c25
--- /dev/null
+++ b/internal/HTTPClient.go
@@ -0,0 +1,8 @@
+package internal
+
+import "net/http"
+
+// HTTPClient is an interface over http.Client to make mock easier.
+type HTTPClient interface {
+ Do(req *http.Request) (*http.Response, error)
+}
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index 92d22853379..ea7e1123fcd 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -6,13 +6,13 @@ import (
"gopkg.in/yaml.v3"
"sync"
- "github.com/thomaspoignant/go-feature-flag/internal/flags"
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
)
type Cache interface {
UpdateCache(loadedFlags []byte) error
Close()
- GetFlag(key string) (flags.Flag, error)
+ GetFlag(key string) (model.Flag, error)
}
type cacheImpl struct {
@@ -23,7 +23,7 @@ type cacheImpl struct {
func New(notificationService Service) Cache {
return &cacheImpl{
- flagsCache: make(map[string]flags.Flag),
+ flagsCache: make(map[string]model.Flag),
mutex: sync.RWMutex{},
notificationService: notificationService,
}
@@ -56,17 +56,17 @@ func (c *cacheImpl) Close() {
c.notificationService.Close()
}
-func (c *cacheImpl) GetFlag(key string) (flags.Flag, error) {
+func (c *cacheImpl) GetFlag(key string) (model.Flag, error) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if c.flagsCache == nil {
- return flags.Flag{}, errors.New("impossible to read the toggle before the initialisation")
+ return model.Flag{}, errors.New("impossible to read the toggle before the initialisation")
}
flag, ok := c.flagsCache[key]
if !ok {
- return flags.Flag{}, fmt.Errorf("flag [%v] does not exists", key)
+ return model.Flag{}, fmt.Errorf("flag [%v] does not exists", key)
}
return flag, nil
}
diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go
index 561b78f32ac..58e1870e475 100644
--- a/internal/cache/cache_test.go
+++ b/internal/cache/cache_test.go
@@ -4,7 +4,8 @@ import (
"github.com/stretchr/testify/assert"
"testing"
- "github.com/thomaspoignant/go-feature-flag/internal/flags"
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
+ "github.com/thomaspoignant/go-feature-flag/internal/notifier"
)
func Test_FlagCacheNotInit(t *testing.T) {
@@ -34,7 +35,7 @@ func Test_FlagCache(t *testing.T) {
tests := []struct {
name string
args args
- expected map[string]flags.Flag
+ expected map[string]model.Flag
wantErr bool
}{
{
@@ -42,7 +43,7 @@ func Test_FlagCache(t *testing.T) {
args: args{
loadedFlags: exampleFile,
},
- expected: map[string]flags.Flag{
+ expected: map[string]model.Flag{
"test-flag": {
Disable: false,
Rule: "key eq \"random-key\"",
@@ -70,7 +71,7 @@ func Test_FlagCache(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- fCache := New(NewService([]Notifier{}))
+ fCache := New(NewNotificationService([]notifier.Notifier{}))
err := fCache.UpdateCache(tt.args.loadedFlags)
if (err != nil) != tt.wantErr {
t.Errorf("UpdateCache() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/cache/diff_cache.go b/internal/cache/diff_cache.go
deleted file mode 100644
index 1f338dab745..00000000000
--- a/internal/cache/diff_cache.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package cache
-
-import "github.com/thomaspoignant/go-feature-flag/internal/flags"
-
-// diffCache contains the changes made in the cache, to be able
-// to notify the user that something has changed (logs, webhook ...)
-type diffCache struct {
- Deleted map[string]flags.Flag
- Added map[string]flags.Flag
- Updated map[string]diffUpdated
-}
-
-// hasDiff check if we have differences
-func (d diffCache) hasDiff() bool {
- return len(d.Deleted) > 0 || len(d.Added) > 0 || len(d.Updated) > 0
-}
-
-type diffUpdated struct {
- Before flags.Flag
- After flags.Flag
-}
diff --git a/internal/cache/flag_cache.go b/internal/cache/flag_cache.go
index 2db9d425582..59612970530 100644
--- a/internal/cache/flag_cache.go
+++ b/internal/cache/flag_cache.go
@@ -1,8 +1,8 @@
package cache
-import "github.com/thomaspoignant/go-feature-flag/internal/flags"
+import "github.com/thomaspoignant/go-feature-flag/internal/model"
-type FlagsCache map[string]flags.Flag
+type FlagsCache map[string]model.Flag
func (fc FlagsCache) Copy() FlagsCache {
copyCache := make(FlagsCache)
diff --git a/internal/cache/notification_service.go b/internal/cache/notification_service.go
index 9ca601993c3..acecfb03d3b 100644
--- a/internal/cache/notification_service.go
+++ b/internal/cache/notification_service.go
@@ -4,7 +4,8 @@ import (
"github.com/google/go-cmp/cmp"
"sync"
- "github.com/thomaspoignant/go-feature-flag/internal/flags"
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
+ "github.com/thomaspoignant/go-feature-flag/internal/notifier"
)
type Service interface {
@@ -12,7 +13,7 @@ type Service interface {
Notify(oldCache FlagsCache, newCache FlagsCache)
}
-func NewService(notifiers []Notifier) Service {
+func NewNotificationService(notifiers []notifier.Notifier) Service {
return ¬ificationService{
Notifiers: notifiers,
waitGroup: &sync.WaitGroup{},
@@ -20,13 +21,13 @@ func NewService(notifiers []Notifier) Service {
}
type notificationService struct {
- Notifiers []Notifier
+ Notifiers []notifier.Notifier
waitGroup *sync.WaitGroup
}
func (c *notificationService) Notify(oldCache FlagsCache, newCache FlagsCache) {
diff := c.getDifferences(oldCache, newCache)
- if diff.hasDiff() {
+ if diff.HasDiff() {
for _, notifier := range c.Notifiers {
c.waitGroup.Add(1)
go notifier.Notify(diff, c.waitGroup)
@@ -40,11 +41,11 @@ func (c *notificationService) Close() {
// getDifferences is checking what are the difference in the updated cache.
func (c *notificationService) getDifferences(
- oldCache FlagsCache, newCache FlagsCache) diffCache {
- diff := diffCache{
- Deleted: map[string]flags.Flag{},
- Added: map[string]flags.Flag{},
- Updated: map[string]diffUpdated{},
+ oldCache FlagsCache, newCache FlagsCache) model.DiffCache {
+ diff := model.DiffCache{
+ Deleted: map[string]model.Flag{},
+ Added: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{},
}
for key := range oldCache {
_, inNewCache := newCache[key]
@@ -54,7 +55,7 @@ func (c *notificationService) getDifferences(
}
if !cmp.Equal(oldCache[key], newCache[key]) {
- diff.Updated[key] = diffUpdated{
+ diff.Updated[key] = model.DiffUpdated{
Before: oldCache[key],
After: newCache[key],
}
diff --git a/internal/cache/notification_service_test.go b/internal/cache/notification_service_test.go
index 8803aeb7017..5cc7ec14223 100644
--- a/internal/cache/notification_service_test.go
+++ b/internal/cache/notification_service_test.go
@@ -5,12 +5,13 @@ import (
"sync"
"testing"
- "github.com/thomaspoignant/go-feature-flag/internal/flags"
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
+ "github.com/thomaspoignant/go-feature-flag/internal/notifier"
)
func Test_notificationService_getDifferences(t *testing.T) {
type fields struct {
- Notifiers []Notifier
+ Notifiers []notifier.Notifier
}
type args struct {
oldCache FlagsCache
@@ -20,19 +21,19 @@ func Test_notificationService_getDifferences(t *testing.T) {
name string
fields fields
args args
- want diffCache
+ want model.DiffCache
}{
{
name: "Delete flag",
args: args{
oldCache: FlagsCache{
- "test-flag": flags.Flag{
+ "test-flag": model.Flag{
Percentage: 100,
True: true,
False: false,
Default: false,
},
- "test-flag2": flags.Flag{
+ "test-flag2": model.Flag{
Percentage: 100,
True: true,
False: false,
@@ -40,7 +41,7 @@ func Test_notificationService_getDifferences(t *testing.T) {
},
},
newCache: FlagsCache{
- "test-flag": flags.Flag{
+ "test-flag": model.Flag{
Percentage: 100,
True: true,
False: false,
@@ -48,8 +49,8 @@ func Test_notificationService_getDifferences(t *testing.T) {
},
},
},
- want: diffCache{
- Deleted: map[string]flags.Flag{
+ want: model.DiffCache{
+ Deleted: map[string]model.Flag{
"test-flag2": {
Percentage: 100,
True: true,
@@ -57,15 +58,15 @@ func Test_notificationService_getDifferences(t *testing.T) {
Default: false,
},
},
- Added: map[string]flags.Flag{},
- Updated: map[string]diffUpdated{},
+ Added: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{},
},
},
{
name: "Added flag",
args: args{
oldCache: FlagsCache{
- "test-flag": flags.Flag{
+ "test-flag": model.Flag{
Percentage: 100,
True: true,
False: false,
@@ -73,13 +74,13 @@ func Test_notificationService_getDifferences(t *testing.T) {
},
},
newCache: FlagsCache{
- "test-flag": flags.Flag{
+ "test-flag": model.Flag{
Percentage: 100,
True: true,
False: false,
Default: false,
},
- "test-flag2": flags.Flag{
+ "test-flag2": model.Flag{
Percentage: 100,
True: true,
False: false,
@@ -87,8 +88,8 @@ func Test_notificationService_getDifferences(t *testing.T) {
},
},
},
- want: diffCache{
- Added: map[string]flags.Flag{
+ want: model.DiffCache{
+ Added: map[string]model.Flag{
"test-flag2": {
Percentage: 100,
True: true,
@@ -96,15 +97,15 @@ func Test_notificationService_getDifferences(t *testing.T) {
Default: false,
},
},
- Deleted: map[string]flags.Flag{},
- Updated: map[string]diffUpdated{},
+ Deleted: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{},
},
},
{
name: "Updated flag",
args: args{
oldCache: FlagsCache{
- "test-flag": flags.Flag{
+ "test-flag": model.Flag{
Percentage: 100,
True: true,
False: false,
@@ -112,7 +113,7 @@ func Test_notificationService_getDifferences(t *testing.T) {
},
},
newCache: FlagsCache{
- "test-flag": flags.Flag{
+ "test-flag": model.Flag{
Percentage: 100,
True: true,
False: false,
@@ -120,18 +121,18 @@ func Test_notificationService_getDifferences(t *testing.T) {
},
},
},
- want: diffCache{
- Added: map[string]flags.Flag{},
- Deleted: map[string]flags.Flag{},
- Updated: map[string]diffUpdated{
+ want: model.DiffCache{
+ Added: map[string]model.Flag{},
+ Deleted: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{
"test-flag": {
- Before: flags.Flag{
+ Before: model.Flag{
Percentage: 100,
True: true,
False: false,
Default: false,
},
- After: flags.Flag{
+ After: model.Flag{
Percentage: 100,
True: true,
False: false,
diff --git a/internal/cache/notifier.go b/internal/cache/notifier.go
deleted file mode 100644
index 0fe087676b7..00000000000
--- a/internal/cache/notifier.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package cache
-
-import "sync"
-
-type Notifier interface {
- Notify(cache diffCache, waitGroup *sync.WaitGroup)
-}
diff --git a/internal/model/diff_cache.go b/internal/model/diff_cache.go
new file mode 100644
index 00000000000..1dd0d58f9a9
--- /dev/null
+++ b/internal/model/diff_cache.go
@@ -0,0 +1,19 @@
+package model
+
+// DiffCache contains the changes made in the cache, to be able
+// to notify the user that something has changed (logs, webhook ...)
+type DiffCache struct {
+ Deleted map[string]Flag `json:"deleted"`
+ Added map[string]Flag `json:"added"`
+ Updated map[string]DiffUpdated `json:"updated"`
+}
+
+// HasDiff check if we have differences
+func (d DiffCache) HasDiff() bool {
+ return len(d.Deleted) > 0 || len(d.Added) > 0 || len(d.Updated) > 0
+}
+
+type DiffUpdated struct {
+ Before Flag `json:"old_value"`
+ After Flag `json:"new_value"`
+}
diff --git a/internal/model/diff_cache_test.go b/internal/model/diff_cache_test.go
new file mode 100644
index 00000000000..2d9272b34a1
--- /dev/null
+++ b/internal/model/diff_cache_test.go
@@ -0,0 +1,135 @@
+package model
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestDiffCache_HasDiff(t *testing.T) {
+ type fields struct {
+ Deleted map[string]Flag
+ Added map[string]Flag
+ Updated map[string]DiffUpdated
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want bool
+ }{
+ {
+ name: "null fields",
+ fields: fields{},
+ want: false,
+ },
+ {
+ name: "empty fields",
+ fields: fields{
+ Deleted: map[string]Flag{},
+ Added: map[string]Flag{},
+ Updated: map[string]DiffUpdated{},
+ },
+ want: false,
+ },
+ {
+ name: "only Deleted",
+ fields: fields{
+ Deleted: map[string]Flag{
+ "flag": {
+ Percentage: 100,
+ True: true,
+ False: true,
+ Default: true,
+ }},
+ Added: map[string]Flag{},
+ Updated: map[string]DiffUpdated{},
+ },
+ want: true,
+ },
+ {
+ name: "only Added",
+ fields: fields{
+ Added: map[string]Flag{
+ "flag": {
+ Percentage: 100,
+ True: true,
+ False: true,
+ Default: true,
+ }},
+ Deleted: map[string]Flag{},
+ Updated: map[string]DiffUpdated{},
+ },
+ want: true,
+ },
+ {
+ name: "only Updated",
+ fields: fields{
+ Added: map[string]Flag{},
+ Deleted: map[string]Flag{},
+ Updated: map[string]DiffUpdated{
+ "flag": {
+ Before: Flag{
+ Percentage: 100,
+ True: true,
+ False: true,
+ Default: true,
+ },
+ After: Flag{
+ Percentage: 100,
+ True: true,
+ False: true,
+ Default: false,
+ },
+ },
+ },
+ },
+ want: true,
+ },
+ {
+ name: "all fields",
+ fields: fields{
+ Added: map[string]Flag{
+ "flag": {
+ Percentage: 100,
+ True: true,
+ False: true,
+ Default: true,
+ },
+ },
+ Deleted: map[string]Flag{
+ "flag": {
+ Percentage: 100,
+ True: true,
+ False: true,
+ Default: true,
+ }},
+ Updated: map[string]DiffUpdated{
+ "flag": {
+ Before: Flag{
+ Percentage: 100,
+ True: true,
+ False: true,
+ Default: true,
+ },
+ After: Flag{
+ Percentage: 100,
+ True: true,
+ False: true,
+ Default: false,
+ },
+ },
+ },
+ },
+ want: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ d := DiffCache{
+ Deleted: tt.fields.Deleted,
+ Added: tt.fields.Added,
+ Updated: tt.fields.Updated,
+ }
+ assert.Equal(t, tt.want, d.HasDiff())
+ })
+ }
+}
diff --git a/internal/flags/flag.go b/internal/model/flag.go
similarity index 85%
rename from internal/flags/flag.go
rename to internal/model/flag.go
index 295232f30bc..5679a7b9f9a 100644
--- a/internal/flags/flag.go
+++ b/internal/model/flag.go
@@ -1,4 +1,4 @@
-package flags
+package model
import (
"fmt"
@@ -11,12 +11,12 @@ import (
// Flag describe the fields of a flag.
type Flag struct {
- Disable bool
- Rule string
- Percentage int
- True interface{} // Value if Rule applied, and in percentage
- False interface{} // Value if Rule applied and not in percentage
- Default interface{} // Value if Rule does not applied
+ Disable bool `json:"disable,omitempty"`
+ Rule string `json:"rule,omitempty"`
+ Percentage int `json:"percentage,omitempty"`
+ True interface{} `json:"true,omitempty"` // Value if Rule applied, and in percentage
+ False interface{} `json:"false,omitempty"` // Value if Rule applied and not in percentage
+ Default interface{} `json:"default,omitempty"` // Value if Rule does not applied
}
// Value is returning the Value associate to the flag (True or False) based
diff --git a/internal/flags/flag_test.go b/internal/model/flag_test.go
similarity index 99%
rename from internal/flags/flag_test.go
rename to internal/model/flag_test.go
index 2c558d216f6..01ca2c78e4a 100644
--- a/internal/flags/flag_test.go
+++ b/internal/model/flag_test.go
@@ -1,4 +1,4 @@
-package flags
+package model
import (
"github.com/stretchr/testify/assert"
diff --git a/internal/notifier/notifier.go b/internal/notifier/notifier.go
new file mode 100644
index 00000000000..7493d0775f0
--- /dev/null
+++ b/internal/notifier/notifier.go
@@ -0,0 +1,11 @@
+package notifier
+
+import (
+ "sync"
+
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
+)
+
+type Notifier interface {
+ Notify(cache model.DiffCache, waitGroup *sync.WaitGroup)
+}
diff --git a/internal/cache/notifier_log.go b/internal/notifier/notifier_log.go
similarity index 84%
rename from internal/cache/notifier_log.go
rename to internal/notifier/notifier_log.go
index 98b8de21410..f39fc6ca767 100644
--- a/internal/cache/notifier_log.go
+++ b/internal/notifier/notifier_log.go
@@ -1,16 +1,18 @@
-package cache
+package notifier
import (
"log"
"sync"
"time"
+
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
)
type LogNotifier struct {
Logger *log.Logger
}
-func (c *LogNotifier) Notify(diff diffCache, wg *sync.WaitGroup) {
+func (c *LogNotifier) Notify(diff model.DiffCache, wg *sync.WaitGroup) {
defer wg.Done()
date := time.Now().Format(time.RFC3339)
for key := range diff.Deleted {
diff --git a/internal/cache/notifier_log_test.go b/internal/notifier/notifier_log_test.go
similarity index 76%
rename from internal/cache/notifier_log_test.go
rename to internal/notifier/notifier_log_test.go
index 48560937266..3e3c2f7b379 100644
--- a/internal/cache/notifier_log_test.go
+++ b/internal/notifier/notifier_log_test.go
@@ -1,4 +1,4 @@
-package cache
+package notifier
import (
"fmt"
@@ -9,13 +9,13 @@ import (
"sync"
"testing"
- "github.com/thomaspoignant/go-feature-flag/internal/flags"
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
"github.com/thomaspoignant/go-feature-flag/testutil"
)
func TestLogNotifier_Notify(t *testing.T) {
type args struct {
- diff diffCache
+ diff model.DiffCache
wg *sync.WaitGroup
}
tests := []struct {
@@ -26,8 +26,8 @@ func TestLogNotifier_Notify(t *testing.T) {
{
name: "Flag deleted",
args: args{
- diff: diffCache{
- Deleted: map[string]flags.Flag{
+ diff: model.DiffCache{
+ Deleted: map[string]model.Flag{
"test-flag": {
Percentage: 100,
True: true,
@@ -35,8 +35,8 @@ func TestLogNotifier_Notify(t *testing.T) {
Default: false,
},
},
- Updated: map[string]diffUpdated{},
- Added: map[string]flags.Flag{},
+ Updated: map[string]model.DiffUpdated{},
+ Added: map[string]model.Flag{},
},
wg: &sync.WaitGroup{},
},
@@ -45,18 +45,18 @@ func TestLogNotifier_Notify(t *testing.T) {
{
name: "Update flag",
args: args{
- diff: diffCache{
- Deleted: map[string]flags.Flag{},
- Updated: map[string]diffUpdated{
+ diff: model.DiffCache{
+ Deleted: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{
"test-flag": {
- Before: flags.Flag{
+ Before: model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
True: true,
False: false,
Default: false,
},
- After: flags.Flag{
+ After: model.Flag{
Percentage: 100,
True: true,
False: false,
@@ -64,7 +64,7 @@ func TestLogNotifier_Notify(t *testing.T) {
},
},
},
- Added: map[string]flags.Flag{},
+ Added: map[string]model.Flag{},
},
wg: &sync.WaitGroup{},
},
@@ -73,18 +73,18 @@ func TestLogNotifier_Notify(t *testing.T) {
{
name: "Disable flag",
args: args{
- diff: diffCache{
- Deleted: map[string]flags.Flag{},
- Updated: map[string]diffUpdated{
+ diff: model.DiffCache{
+ Deleted: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{
"test-flag": {
- Before: flags.Flag{
+ Before: model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
True: true,
False: false,
Default: false,
},
- After: flags.Flag{
+ After: model.Flag{
Rule: "key eq \"random-key\"",
Disable: true,
Percentage: 100,
@@ -94,7 +94,7 @@ func TestLogNotifier_Notify(t *testing.T) {
},
},
},
- Added: map[string]flags.Flag{},
+ Added: map[string]model.Flag{},
},
wg: &sync.WaitGroup{},
},
@@ -103,10 +103,10 @@ func TestLogNotifier_Notify(t *testing.T) {
{
name: "Add flag",
args: args{
- diff: diffCache{
- Deleted: map[string]flags.Flag{},
- Updated: map[string]diffUpdated{},
- Added: map[string]flags.Flag{
+ diff: model.DiffCache{
+ Deleted: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{},
+ Added: map[string]model.Flag{
"add-test-flag": {
Rule: "key eq \"random-key\"",
Percentage: 100,
@@ -123,18 +123,18 @@ func TestLogNotifier_Notify(t *testing.T) {
{
name: "Enable flag",
args: args{
- diff: diffCache{
- Deleted: map[string]flags.Flag{},
- Updated: map[string]diffUpdated{
+ diff: model.DiffCache{
+ Deleted: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{
"test-flag": {
- After: flags.Flag{
+ After: model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
True: true,
False: false,
Default: false,
},
- Before: flags.Flag{
+ Before: model.Flag{
Rule: "key eq \"random-key\"",
Disable: true,
Percentage: 100,
@@ -144,7 +144,7 @@ func TestLogNotifier_Notify(t *testing.T) {
},
},
},
- Added: map[string]flags.Flag{},
+ Added: map[string]model.Flag{},
},
wg: &sync.WaitGroup{},
},
diff --git a/internal/notifier/notifier_webhook.go b/internal/notifier/notifier_webhook.go
new file mode 100644
index 00000000000..5a5b47dcdef
--- /dev/null
+++ b/internal/notifier/notifier_webhook.go
@@ -0,0 +1,84 @@
+package notifier
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "sync"
+ "time"
+
+ "github.com/thomaspoignant/go-feature-flag/internal"
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
+)
+
+type webhookReqBody struct {
+ Meta map[string]string `json:"meta"`
+ Flags model.DiffCache `json:"flags"`
+}
+
+type WebhookNotifier struct {
+ Logger *log.Logger
+ HTTPClient internal.HTTPClient
+ PayloadURL url.URL
+ Secret string
+ Meta map[string]string
+}
+
+func (c *WebhookNotifier) Notify(diff model.DiffCache, wg *sync.WaitGroup) {
+ defer wg.Done()
+ date := time.Now().Format(time.RFC3339)
+
+ // Create request body
+ reqBody := webhookReqBody{
+ Meta: c.Meta,
+ Flags: diff,
+ }
+
+ payload, err := json.Marshal(reqBody)
+ if err != nil && c.Logger != nil {
+ c.Logger.Printf("[%v] error: (WebhookNotifier) impossible to read differences; %v\n", date, err)
+ return
+ }
+
+ headers := http.Header{
+ "Content-Type": []string{"application/json"},
+ }
+
+ // if a secret is provided we sign the body and add this signature as a header.
+ if c.Secret != "" {
+ headers["X-Hub-Signature-256"] = []string{signPayload(payload, []byte(c.Secret))}
+ }
+
+ request := http.Request{
+ Method: "POST",
+ URL: &c.PayloadURL,
+ Header: headers,
+ Body: ioutil.NopCloser(bytes.NewReader(payload)),
+ }
+ response, err := c.HTTPClient.Do(&request)
+ // Log if something went wrong while calling the webhook.
+ if err != nil && c.Logger != nil {
+ c.Logger.Printf("[%v] error: while calling webhook: %v\n", date, err)
+ return
+ }
+ defer response.Body.Close()
+ if response.StatusCode > 399 && c.Logger != nil {
+ c.Logger.Printf("[%v] error: while calling webhook, statusCode = %d", date, response.StatusCode)
+ return
+ }
+}
+
+// signPayload is using the data and the secret to compute a HMAC(SHA256) to sign the body of the request.
+// so the webhook can use this signature to verify that no data have been compromised.
+func signPayload(payloadBody []byte, secretToken []byte) string {
+ mac := hmac.New(sha256.New, secretToken)
+ _, _ = mac.Write(payloadBody)
+ expectedMAC := mac.Sum(nil)
+ return "sha256=" + hex.EncodeToString(expectedMAC)
+}
diff --git a/internal/notifier/notifier_webhook_test.go b/internal/notifier/notifier_webhook_test.go
new file mode 100644
index 00000000000..6ce584e3fc5
--- /dev/null
+++ b/internal/notifier/notifier_webhook_test.go
@@ -0,0 +1,192 @@
+package notifier
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "github.com/stretchr/testify/assert"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "sync"
+ "testing"
+
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
+ "github.com/thomaspoignant/go-feature-flag/testutil"
+)
+
+type httpClientMock struct {
+ forceError bool
+ statusCode int
+ body string
+ signature string
+}
+
+func (h *httpClientMock) Do(req *http.Request) (*http.Response, error) {
+ if h.forceError {
+ return nil, errors.New("random error")
+ }
+
+ b, _ := ioutil.ReadAll(req.Body)
+ h.body = string(b)
+ h.signature = req.Header.Get("X-Hub-Signature-256")
+ resp := &http.Response{
+ Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
+ }
+ resp.StatusCode = h.statusCode
+ return resp, nil
+}
+
+func Test_webhookNotifier_Notify(t *testing.T) {
+ type fields struct {
+ Secret string
+ }
+ type expected struct {
+ err bool
+ errLog string
+ bodyPath string
+ signature string
+ }
+ type args struct {
+ diff model.DiffCache
+ statusCode int
+ forceError bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ expected expected
+ }{
+ {
+ name: "should call webhook and have valid results",
+ fields: fields{
+ Secret: "test-secret",
+ },
+ expected: expected{
+ bodyPath: "../../testdata/internal/cache/notifier/webhook/should_call_webhook_and_have_valid_results.json",
+ signature: "sha256=9859701f2e692d33b0cf7ed4546c56dc0d0df8d587e95472f36592c482cd835d",
+ },
+ args: args{
+ statusCode: http.StatusOK,
+ diff: model.DiffCache{
+ Added: map[string]model.Flag{
+ "test-flag3": {
+ Percentage: 5,
+ True: "test",
+ False: "false",
+ Default: "default",
+ },
+ },
+ Deleted: map[string]model.Flag{
+ "test-flag": {
+ Rule: "key eq \"random-key\"",
+ Percentage: 100,
+ True: true,
+ False: false,
+ Default: false,
+ },
+ },
+ Updated: map[string]model.DiffUpdated{
+ "test-flag2": {
+ Before: model.Flag{
+ Rule: "key eq \"not-a-key\"",
+ Percentage: 100,
+ True: true,
+ False: false,
+ Default: false,
+ },
+ After: model.Flag{
+ Rule: "key eq \"not-a-key\"",
+ Percentage: 100,
+ True: true,
+ False: false,
+ Default: false,
+ Disable: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "should not be signed if no secret",
+ expected: expected{
+ bodyPath: "../../testdata/internal/cache/notifier/webhook/should_not_be_signed_if_no_secret.json",
+ signature: "",
+ },
+ args: args{
+ statusCode: http.StatusOK,
+ diff: model.DiffCache{
+ Added: map[string]model.Flag{
+ "test-flag3": {
+ Percentage: 5,
+ True: "test",
+ False: "false",
+ Default: "default",
+ },
+ },
+ Deleted: map[string]model.Flag{},
+ Updated: map[string]model.DiffUpdated{},
+ },
+ },
+ },
+ {
+ name: "should log if http code is superior to 399",
+ expected: expected{
+ err: true,
+ errLog: "\\[" + testutil.RFC3339Regex + "\\] error: while calling webhook, statusCode = 400",
+ },
+ args: args{
+ statusCode: http.StatusBadRequest,
+ diff: model.DiffCache{},
+ },
+ },
+ {
+ name: "should log if error while calling webhook",
+ expected: expected{
+ err: true,
+ errLog: "\\[" + testutil.RFC3339Regex + "\\] error: while calling webhook: random error",
+ },
+ args: args{
+ statusCode: http.StatusOK,
+ diff: model.DiffCache{},
+ forceError: true,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ logFile, _ := ioutil.TempFile("", "")
+ defer logFile.Close()
+ defer os.Remove(logFile.Name())
+
+ webhookURL, _ := url.Parse("http://webhook.example/hook")
+ mockHTTPClient := &httpClientMock{statusCode: tt.args.statusCode, forceError: tt.args.forceError}
+
+ c := WebhookNotifier{
+ Logger: log.New(logFile, "", 0),
+ HTTPClient: mockHTTPClient,
+ PayloadURL: *webhookURL,
+ Secret: tt.fields.Secret,
+ Meta: map[string]string{"hostname": "toto"},
+ }
+
+ w := sync.WaitGroup{}
+ w.Add(1)
+ c.Notify(tt.args.diff, &w)
+
+ if tt.expected.err {
+ log, _ := ioutil.ReadFile(logFile.Name())
+ fmt.Println(string(log))
+ assert.Regexp(t, tt.expected.errLog, string(log))
+ } else {
+ content, _ := ioutil.ReadFile(tt.expected.bodyPath)
+ assert.JSONEq(t, string(content), mockHTTPClient.body)
+ assert.Equal(t, tt.expected.signature, mockHTTPClient.signature)
+ }
+ })
+ }
+}
diff --git a/internal/retriever/retriever_http.go b/internal/retriever/retriever_http.go
index 935dc071978..2d1931924e6 100644
--- a/internal/retriever/retriever_http.go
+++ b/internal/retriever/retriever_http.go
@@ -7,15 +7,13 @@ import (
"io/ioutil"
"net/http"
"strings"
-)
-// HTTPClient is an interface over http.Client to make mock easier.
-type HTTPClient interface {
- Do(req *http.Request) (*http.Response, error)
-}
+ "github.com/thomaspoignant/go-feature-flag/internal"
+)
// NewHTTPRetriever return a new HTTPRetriever to get the file from an HTTP endpoint.
-func NewHTTPRetriever(httpClient HTTPClient, url string, method string, body string, header http.Header) FlagRetriever {
+func NewHTTPRetriever(httpClient internal.HTTPClient, url string, method string,
+ body string, header http.Header) FlagRetriever {
return &httpRetriever{
httpClient,
url,
@@ -26,7 +24,7 @@ func NewHTTPRetriever(httpClient HTTPClient, url string, method string, body str
}
type httpRetriever struct {
- httpClient HTTPClient
+ httpClient internal.HTTPClient
url string
method string
body string
diff --git a/notifier.go b/notifier.go
new file mode 100644
index 00000000000..92fd303e284
--- /dev/null
+++ b/notifier.go
@@ -0,0 +1,58 @@
+package ffclient
+
+import (
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+
+ "github.com/thomaspoignant/go-feature-flag/internal/notifier"
+)
+
+// getNotifiers is creating Notifier from the config
+func getNotifiers(config Config) ([]notifier.Notifier, error) {
+ var notifiers []notifier.Notifier
+ if config.Logger != nil {
+ notifiers = append(notifiers, ¬ifier.LogNotifier{Logger: config.Logger})
+ }
+
+ wh, err := getWebhooks(config)
+ if err != nil {
+ return nil, err
+ }
+
+ notifiers = append(notifiers, wh...)
+ return notifiers, nil
+}
+
+func getWebhooks(config Config) ([]notifier.Notifier, error) {
+ res := make([]notifier.Notifier, len(config.Webhooks))
+ for index, whConf := range config.Webhooks {
+ // httpClient used to call the webhook
+ httpClient := http.Client{
+ Timeout: 10 * time.Second,
+ }
+
+ // Deal with meta informations
+ if whConf.Meta == nil {
+ whConf.Meta = make(map[string]string)
+ }
+ hostname, _ := os.Hostname()
+ whConf.Meta["hostname"] = hostname
+
+ payloadURL, err := url.Parse(whConf.PayloadURL)
+ if err != nil {
+ return nil, err
+ }
+
+ w := notifier.WebhookNotifier{
+ Logger: config.Logger,
+ PayloadURL: *payloadURL,
+ Secret: whConf.Secret,
+ Meta: whConf.Meta,
+ HTTPClient: &httpClient,
+ }
+ res[index] = &w
+ }
+ return res, nil
+}
diff --git a/notifier_test.go b/notifier_test.go
new file mode 100644
index 00000000000..e11d96baffb
--- /dev/null
+++ b/notifier_test.go
@@ -0,0 +1,92 @@
+package ffclient
+
+import (
+ "github.com/stretchr/testify/assert"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/thomaspoignant/go-feature-flag/internal/notifier"
+)
+
+func TestGoFeatureFlag_getNotifiers(t *testing.T) {
+ urlStr := "http://webhook.com/hook"
+ parsedURL, _ := url.Parse(urlStr)
+ hostname, _ := os.Hostname()
+
+ type fields struct {
+ config Config
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want []notifier.Notifier
+ wantErr bool
+ }{
+ {
+ name: "log + webhook notifier",
+ fields: fields{
+ config: Config{
+ Logger: log.New(os.Stdout, "", 0),
+ Webhooks: []WebhookConfig{
+ {
+ PayloadURL: urlStr,
+ Secret: "Secret",
+ Meta: map[string]string{
+ "my-app": "go-ff-test",
+ "hostname": hostname,
+ },
+ },
+ },
+ },
+ },
+ want: []notifier.Notifier{
+ ¬ifier.LogNotifier{Logger: log.New(os.Stdout, "", 0)},
+ ¬ifier.WebhookNotifier{
+ Logger: log.New(os.Stdout, "", 0),
+ HTTPClient: &http.Client{
+ Timeout: 10 * time.Second,
+ },
+ PayloadURL: *parsedURL,
+ Secret: "Secret",
+ Meta: map[string]string{
+ "my-app": "go-ff-test",
+ "hostname": hostname,
+ },
+ },
+ },
+ },
+ {
+ name: "error in DNS",
+ fields: fields{
+ config: Config{
+ Logger: log.New(os.Stdout, "", 0),
+ Webhooks: []WebhookConfig{
+ {
+ PayloadURL: " https://example.com/hook",
+ Secret: "Secret",
+ Meta: map[string]string{
+ "my-app": "go-ff-test",
+ "hostname": hostname,
+ },
+ },
+ },
+ },
+ },
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := getNotifiers(tt.fields.config)
+ if tt.wantErr {
+ assert.Error(t, err)
+ return
+ }
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/testdata/internal/cache/notifier/webhook/should_call_webhook_and_have_valid_results.json b/testdata/internal/cache/notifier/webhook/should_call_webhook_and_have_valid_results.json
new file mode 100644
index 00000000000..e0cc07d2581
--- /dev/null
+++ b/testdata/internal/cache/notifier/webhook/should_call_webhook_and_have_valid_results.json
@@ -0,0 +1,43 @@
+{
+ "flags": {
+ "deleted": {
+ "test-flag": {
+ "rule": "key eq \"random-key\"",
+ "percentage": 100,
+ "true": true,
+ "false": false,
+ "default": false
+ }
+ },
+ "added": {
+ "test-flag3": {
+ "percentage": 5,
+ "true": "test",
+ "false": "false",
+ "default": "default"
+ }
+ },
+ "updated": {
+ "test-flag2": {
+ "old_value": {
+ "rule": "key eq \"not-a-key\"",
+ "percentage": 100,
+ "true": true,
+ "false": false,
+ "default": false
+ },
+ "new_value": {
+ "disable": true,
+ "rule": "key eq \"not-a-key\"",
+ "percentage": 100,
+ "true": true,
+ "false": false,
+ "default": false
+ }
+ }
+ }
+ },
+ "meta": {
+ "hostname": "toto"
+ }
+}
diff --git a/testdata/internal/cache/notifier/webhook/should_not_be_signed_if_no_secret.json b/testdata/internal/cache/notifier/webhook/should_not_be_signed_if_no_secret.json
new file mode 100644
index 00000000000..ec20d2cb32a
--- /dev/null
+++ b/testdata/internal/cache/notifier/webhook/should_not_be_signed_if_no_secret.json
@@ -0,0 +1,17 @@
+{
+ "flags": {
+ "deleted": {},
+ "added": {
+ "test-flag3": {
+ "percentage": 5,
+ "true": "test",
+ "false": "false",
+ "default": "default"
+ }
+ },
+ "updated": {}
+ },
+ "meta": {
+ "hostname": "toto"
+ }
+}
diff --git a/variation.go b/variation.go
index c9c564daab6..e5e1779ea62 100644
--- a/variation.go
+++ b/variation.go
@@ -5,7 +5,7 @@ import (
"time"
"github.com/thomaspoignant/go-feature-flag/ffuser"
- "github.com/thomaspoignant/go-feature-flag/internal/flags"
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
)
const errorFlagNotAvailable = "flag %v is not present or disabled"
@@ -188,7 +188,7 @@ func (g *GoFeatureFlag) notifyVariation(flagKey string, userKey string, value in
// getFlagFromCache try to get the flag from the cache.
// It returns an error if the cache is not init or if the flag is not present or disabled.
-func (g *GoFeatureFlag) getFlagFromCache(flagKey string) (flags.Flag, error) {
+func (g *GoFeatureFlag) getFlagFromCache(flagKey string) (model.Flag, error) {
flag, err := g.cache.GetFlag(flagKey)
if err != nil || flag.Disable {
return flag, fmt.Errorf(errorFlagNotAvailable, flagKey)
diff --git a/variation_test.go b/variation_test.go
index 9f7021469e1..a89aa5908ff 100644
--- a/variation_test.go
+++ b/variation_test.go
@@ -10,16 +10,16 @@ import (
"github.com/thomaspoignant/go-feature-flag/ffuser"
"github.com/thomaspoignant/go-feature-flag/internal/cache"
- "github.com/thomaspoignant/go-feature-flag/internal/flags"
+ "github.com/thomaspoignant/go-feature-flag/internal/model"
"github.com/thomaspoignant/go-feature-flag/testutil"
)
type cacheMock struct {
- flag flags.Flag
+ flag model.Flag
err error
}
-func NewCacheMock(flag flags.Flag, err error) cache.Cache {
+func NewCacheMock(flag model.Flag, err error) cache.Cache {
return &cacheMock{
flag: flag,
err: err,
@@ -29,7 +29,7 @@ func (c *cacheMock) UpdateCache(loadedFlags []byte) error {
return nil
}
func (c *cacheMock) Close() {}
-func (c *cacheMock) GetFlag(key string) (flags.Flag, error) {
+func (c *cacheMock) GetFlag(key string) (model.Flag, error) {
return c.flag, c.err
}
@@ -53,7 +53,7 @@ func TestBoolVariation(t *testing.T) {
flagKey: "disable-flag",
user: ffuser.NewUser("random-key"),
defaultValue: true,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Disable: true,
}, nil),
},
@@ -68,7 +68,7 @@ func TestBoolVariation(t *testing.T) {
user: ffuser.NewUser("random-key"),
defaultValue: true,
cacheMock: NewCacheMock(
- flags.Flag{},
+ model.Flag{},
errors.New("impossible to read the toggle before the initialisation")),
},
want: true,
@@ -81,7 +81,7 @@ func TestBoolVariation(t *testing.T) {
flagKey: "key-not-exist",
user: ffuser.NewUser("random-key"),
defaultValue: true,
- cacheMock: NewCacheMock(flags.Flag{}, errors.New("flag [key-not-exist] does not exists")),
+ cacheMock: NewCacheMock(model.Flag{}, errors.New("flag [key-not-exist] does not exists")),
},
want: true,
wantErr: true,
@@ -93,7 +93,7 @@ func TestBoolVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key"),
defaultValue: true,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"key\"",
Percentage: 100,
Default: true,
@@ -111,7 +111,7 @@ func TestBoolVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key"),
defaultValue: true,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
Default: false,
@@ -129,7 +129,7 @@ func TestBoolVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key-ssss1"),
defaultValue: true,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "anonymous eq true",
Percentage: 50,
Default: true,
@@ -147,7 +147,7 @@ func TestBoolVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key-ssss1"),
defaultValue: true,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Percentage: 100,
Default: "xxx",
True: "xxx",
@@ -215,7 +215,7 @@ func TestFloat64Variation(t *testing.T) {
flagKey: "disable-flag",
user: ffuser.NewUser("random-key"),
defaultValue: 120.12,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Disable: true,
}, nil),
},
@@ -230,7 +230,7 @@ func TestFloat64Variation(t *testing.T) {
user: ffuser.NewUser("random-key"),
defaultValue: 118.12,
cacheMock: NewCacheMock(
- flags.Flag{},
+ model.Flag{},
errors.New("impossible to read the toggle before the initialisation")),
},
want: 118.12,
@@ -243,7 +243,7 @@ func TestFloat64Variation(t *testing.T) {
flagKey: "key-not-exist",
user: ffuser.NewUser("random-key"),
defaultValue: 118.12,
- cacheMock: NewCacheMock(flags.Flag{}, errors.New("flag [key-not-exist] does not exists")),
+ cacheMock: NewCacheMock(model.Flag{}, errors.New("flag [key-not-exist] does not exists")),
},
want: 118.12,
wantErr: true,
@@ -255,7 +255,7 @@ func TestFloat64Variation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key"),
defaultValue: 118.12,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"key\"",
Percentage: 100,
Default: 119.12,
@@ -273,7 +273,7 @@ func TestFloat64Variation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key"),
defaultValue: 118.12,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
Default: 119.12,
@@ -291,7 +291,7 @@ func TestFloat64Variation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key-ssss1"),
defaultValue: 118.12,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "anonymous eq true",
Percentage: 50,
Default: 119.12,
@@ -309,7 +309,7 @@ func TestFloat64Variation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key-ssss1"),
defaultValue: 118.12,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Percentage: 100,
Default: "xxx",
True: "xxx",
@@ -377,7 +377,7 @@ func TestJSONArrayVariation(t *testing.T) {
flagKey: "disable-flag",
user: ffuser.NewUser("random-key"),
defaultValue: []interface{}{"toto"},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Disable: true,
}, nil),
},
@@ -392,7 +392,7 @@ func TestJSONArrayVariation(t *testing.T) {
user: ffuser.NewUser("random-key"),
defaultValue: []interface{}{"toto"},
cacheMock: NewCacheMock(
- flags.Flag{},
+ model.Flag{},
errors.New("impossible to read the toggle before the initialisation")),
},
want: []interface{}{"toto"},
@@ -405,7 +405,7 @@ func TestJSONArrayVariation(t *testing.T) {
flagKey: "key-not-exist",
user: ffuser.NewUser("random-key"),
defaultValue: []interface{}{"toto"},
- cacheMock: NewCacheMock(flags.Flag{}, errors.New("flag [key-not-exist] does not exists")),
+ cacheMock: NewCacheMock(model.Flag{}, errors.New("flag [key-not-exist] does not exists")),
},
want: []interface{}{"toto"},
wantErr: true,
@@ -417,7 +417,7 @@ func TestJSONArrayVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key"),
defaultValue: []interface{}{"toto"},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"key\"",
Percentage: 100,
Default: []interface{}{"default"},
@@ -435,7 +435,7 @@ func TestJSONArrayVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key"),
defaultValue: []interface{}{"toto"},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
Default: []interface{}{"default"},
@@ -453,7 +453,7 @@ func TestJSONArrayVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key-ssss1"),
defaultValue: []interface{}{"toto"},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "anonymous eq true",
Percentage: 50,
Default: []interface{}{"default"},
@@ -471,7 +471,7 @@ func TestJSONArrayVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key-ssss1"),
defaultValue: []interface{}{"toto"},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Percentage: 100,
Default: "xxx",
True: "xxx",
@@ -539,7 +539,7 @@ func TestJSONVariation(t *testing.T) {
flagKey: "disable-flag",
user: ffuser.NewUser("random-key"),
defaultValue: map[string]interface{}{"default-notkey": true},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Disable: true,
}, nil),
},
@@ -554,7 +554,7 @@ func TestJSONVariation(t *testing.T) {
user: ffuser.NewUser("random-key"),
defaultValue: map[string]interface{}{"default-notkey": true},
cacheMock: NewCacheMock(
- flags.Flag{},
+ model.Flag{},
errors.New("impossible to read the toggle before the initialisation")),
},
want: map[string]interface{}{"default-notkey": true},
@@ -567,7 +567,7 @@ func TestJSONVariation(t *testing.T) {
flagKey: "key-not-exist",
user: ffuser.NewUser("random-key"),
defaultValue: map[string]interface{}{"default-notkey": true},
- cacheMock: NewCacheMock(flags.Flag{}, errors.New("flag [key-not-exist] does not exists")),
+ cacheMock: NewCacheMock(model.Flag{}, errors.New("flag [key-not-exist] does not exists")),
},
want: map[string]interface{}{"default-notkey": true},
wantErr: true,
@@ -579,7 +579,7 @@ func TestJSONVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key"),
defaultValue: map[string]interface{}{"default-notkey": true},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"key\"",
Percentage: 100,
Default: map[string]interface{}{"default": true},
@@ -597,7 +597,7 @@ func TestJSONVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key"),
defaultValue: map[string]interface{}{"default-notkey": true},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
Default: map[string]interface{}{"default": true},
@@ -615,7 +615,7 @@ func TestJSONVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key-ssss1"),
defaultValue: map[string]interface{}{"default-notkey": true},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "anonymous eq true",
Percentage: 50,
Default: map[string]interface{}{"default": true},
@@ -633,7 +633,7 @@ func TestJSONVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key-ssss1"),
defaultValue: map[string]interface{}{"default-notkey": true},
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Percentage: 100,
Default: "xxx",
True: "xxx",
@@ -701,7 +701,7 @@ func TestStringVariation(t *testing.T) {
flagKey: "disable-flag",
user: ffuser.NewUser("random-key"),
defaultValue: "default-notkey",
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Disable: true,
}, nil),
},
@@ -716,7 +716,7 @@ func TestStringVariation(t *testing.T) {
user: ffuser.NewUser("random-key"),
defaultValue: "default-notkey",
cacheMock: NewCacheMock(
- flags.Flag{},
+ model.Flag{},
errors.New("impossible to read the toggle before the initialisation")),
},
want: "default-notkey",
@@ -729,7 +729,7 @@ func TestStringVariation(t *testing.T) {
flagKey: "key-not-exist",
user: ffuser.NewUser("random-key"),
defaultValue: "default-notkey",
- cacheMock: NewCacheMock(flags.Flag{}, errors.New("flag [key-not-exist] does not exists")),
+ cacheMock: NewCacheMock(model.Flag{}, errors.New("flag [key-not-exist] does not exists")),
},
want: "default-notkey",
wantErr: true,
@@ -742,7 +742,7 @@ func TestStringVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key"),
defaultValue: "default-notkey",
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"key\"",
Percentage: 100,
Default: "default",
@@ -760,7 +760,7 @@ func TestStringVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key"),
defaultValue: "default-notkey",
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
Default: "default",
@@ -778,7 +778,7 @@ func TestStringVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key-ssss1"),
defaultValue: "default-notkey",
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "anonymous eq true",
Percentage: 50,
Default: "default",
@@ -796,7 +796,7 @@ func TestStringVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key-ssss1"),
defaultValue: "default-notkey",
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "anonymous eq true",
Percentage: 50,
Default: 111,
@@ -864,7 +864,7 @@ func TestIntVariation(t *testing.T) {
flagKey: "disable-flag",
user: ffuser.NewUser("random-key"),
defaultValue: 125,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Disable: true,
}, nil),
},
@@ -879,7 +879,7 @@ func TestIntVariation(t *testing.T) {
user: ffuser.NewUser("random-key"),
defaultValue: 118,
cacheMock: NewCacheMock(
- flags.Flag{},
+ model.Flag{},
errors.New("impossible to read the toggle before the initialisation")),
},
want: 118,
@@ -892,7 +892,7 @@ func TestIntVariation(t *testing.T) {
flagKey: "key-not-exist",
user: ffuser.NewUser("random-key"),
defaultValue: 118,
- cacheMock: NewCacheMock(flags.Flag{}, errors.New("flag [key-not-exist] does not exists")),
+ cacheMock: NewCacheMock(model.Flag{}, errors.New("flag [key-not-exist] does not exists")),
},
want: 118,
wantErr: true,
@@ -904,7 +904,7 @@ func TestIntVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key"),
defaultValue: 118,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"key\"",
Percentage: 100,
Default: 119,
@@ -922,7 +922,7 @@ func TestIntVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key"),
defaultValue: 118,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "key eq \"random-key\"",
Percentage: 100,
Default: 119,
@@ -940,7 +940,7 @@ func TestIntVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewAnonymousUser("random-key-ssss1"),
defaultValue: 118,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "anonymous eq true",
Percentage: 50,
Default: 119,
@@ -958,7 +958,7 @@ func TestIntVariation(t *testing.T) {
flagKey: "test-flag",
user: ffuser.NewUser("random-key-ssss1"),
defaultValue: 118,
- cacheMock: NewCacheMock(flags.Flag{
+ cacheMock: NewCacheMock(model.Flag{
Rule: "anonymous eq true",
Percentage: 50,
Default: "default",