Skip to content

Commit

Permalink
New retriever for s3 that supports go-aws-sdk-v2 SDK (#990)
Browse files Browse the repository at this point in the history
* Update openfeature_javascript.mdx

* change doc

Signed-off-by: Thomas Poignant <[email protected]>

* Use v2 in relay proxy

Signed-off-by: Thomas Poignant <[email protected]>

---------

Signed-off-by: Thomas Poignant <[email protected]>
  • Loading branch information
thomaspoignant authored Aug 11, 2023
1 parent 0c2c622 commit dd2a703
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 19 deletions.
5 changes: 3 additions & 2 deletions cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"github.com/thomaspoignant/go-feature-flag/exporter/s3exporterv2"
"github.com/thomaspoignant/go-feature-flag/exporter/sqsexporter"
"github.com/thomaspoignant/go-feature-flag/retriever/s3retrieverv2"
"time"

ffclient "github.com/thomaspoignant/go-feature-flag"
Expand All @@ -22,7 +23,6 @@ import (
"github.com/thomaspoignant/go-feature-flag/retriever/gitlabretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/httpretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/k8sretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/s3retriever"
"go.uber.org/zap"
"golang.org/x/net/context"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -132,7 +132,8 @@ func initRetriever(c *config.RetrieverConf) (retriever.Retriever, error) {
case config.FileRetriever:
return &fileretriever.Retriever{Path: c.Path}, nil
case config.S3Retriever:
return &s3retriever.Retriever{Bucket: c.Bucket, Item: c.Item}, nil
awsConfig, err := awsConf.LoadDefaultConfig(context.Background())
return &s3retrieverv2.Retriever{Bucket: c.Bucket, Item: c.Item, AwsConfig: &awsConfig}, err
case config.HTTPRetriever:
return &httpretriever.Retriever{
URL: c.URL,
Expand Down
30 changes: 22 additions & 8 deletions cmd/relayproxy/service/gofeatureflag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/thomaspoignant/go-feature-flag/notifier"
"github.com/thomaspoignant/go-feature-flag/notifier/slacknotifier"
"github.com/thomaspoignant/go-feature-flag/notifier/webhooknotifier"
"github.com/thomaspoignant/go-feature-flag/retriever/s3retrieverv2"
"net/http"
"testing"
"time"
Expand All @@ -25,16 +26,17 @@ import (
"github.com/thomaspoignant/go-feature-flag/retriever/githubretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/gitlabretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/httpretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/s3retriever"
"github.com/xitongsys/parquet-go/parquet"
)

func Test_initRetriever(t *testing.T) {
tests := []struct {
name string
conf *config.RetrieverConf
want retriever.Retriever
wantErr assert.ErrorAssertionFunc
name string
conf *config.RetrieverConf
want retriever.Retriever
wantErr assert.ErrorAssertionFunc
wantType retriever.Retriever
skipCompleteValidation bool
}{
{
name: "Convert Github Retriever",
Expand All @@ -52,6 +54,7 @@ func Test_initRetriever(t *testing.T) {
GithubToken: "",
Timeout: 20 * time.Millisecond,
},
wantType: &githubretriever.Retriever{},
},
{
name: "Convert Github Retriever with token",
Expand All @@ -70,6 +73,7 @@ func Test_initRetriever(t *testing.T) {
GithubToken: "xxx",
Timeout: 20 * time.Millisecond,
},
wantType: &githubretriever.Retriever{},
},
{
name: "Convert Github Retriever with deprecated token",
Expand All @@ -88,6 +92,7 @@ func Test_initRetriever(t *testing.T) {
GithubToken: "xxx",
Timeout: 20 * time.Millisecond,
},
wantType: &githubretriever.Retriever{},
},
{
name: "Convert Gitlab Retriever",
Expand All @@ -107,6 +112,7 @@ func Test_initRetriever(t *testing.T) {
GitlabToken: "",
Timeout: 20 * time.Millisecond,
},
wantType: &gitlabretriever.Retriever{},
},
{
name: "Convert File Retriever",
Expand All @@ -115,7 +121,8 @@ func Test_initRetriever(t *testing.T) {
Kind: "file",
Path: "testdata/flag-config.yaml",
},
want: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"},
want: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"},
wantType: &fileretriever.Retriever{},
},
{
name: "Convert S3 Retriever",
Expand All @@ -125,10 +132,12 @@ func Test_initRetriever(t *testing.T) {
Bucket: "my-bucket-name",
Item: "testdata/flag-config.yaml",
},
want: &s3retriever.Retriever{
want: &s3retrieverv2.Retriever{
Bucket: "my-bucket-name",
Item: "testdata/flag-config.yaml",
},
wantType: &s3retrieverv2.Retriever{},
skipCompleteValidation: true,
},
{
name: "Convert HTTP Retriever",
Expand All @@ -144,6 +153,7 @@ func Test_initRetriever(t *testing.T) {
Header: nil,
Timeout: 10000000000,
},
wantType: &httpretriever.Retriever{},
}, {
name: "Convert Google storage Retriever",
wantErr: assert.NoError,
Expand All @@ -157,6 +167,7 @@ func Test_initRetriever(t *testing.T) {
Object: "testdata/flag-config.yaml",
Options: nil,
},
wantType: &gcstorageretriever.Retriever{},
},
{
name: "Convert unknown Retriever",
Expand All @@ -171,7 +182,10 @@ func Test_initRetriever(t *testing.T) {
got, err := initRetriever(tt.conf)
tt.wantErr(t, err)
if err == nil {
assert.Equal(t, tt.want, got)
assert.IsType(t, tt.wantType, got)
if !tt.skipCompleteValidation {
assert.Equal(t, tt.want, got)
}
}
})
}
Expand Down
1 change: 1 addition & 0 deletions retriever/s3retriever/retriever.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

// Retriever is a configuration struct for a S3 retriever.
// Deprecated: use s3retrieverv2.Retriever instead.
type Retriever struct {
// Bucket is the name of your S3 Bucket.
Bucket string
Expand Down
14 changes: 14 additions & 0 deletions retriever/s3retrieverv2/downloader_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package s3retrieverv2

import (
"context"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"io"
)

// DownloaderAPI provides methods to manage downloads to an S3 bucket.
type DownloaderAPI interface {
Download(ctx context.Context, w io.WriterAt, input *s3.GetObjectInput, options ...func(*manager.Downloader)) (
n int64, err error)
}
78 changes: 78 additions & 0 deletions retriever/s3retrieverv2/retriever.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package s3retrieverv2

import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"os"
"sync"
)

// Retriever is a configuration struct for a S3 retriever.
type Retriever struct {
// Bucket is the name of your S3 Bucket.
Bucket string

// Item is the path to your flag file in your bucket.
Item string

// AwsConfig is the AWS SDK configuration object we will use to
// download your feature flag configuration file.
AwsConfig *aws.Config

// downloader is an internal field, it is the downloader use by the AWS-SDK
downloader DownloaderAPI
init sync.Once
}

func (s *Retriever) Retrieve(ctx context.Context) ([]byte, error) {
if s.downloader == nil {
initErr := s.initializeDownloader(ctx)
if initErr != nil {
return nil, initErr
}
}

// Download the item from the bucket.
// If an error occurs, log it and exit.
// Otherwise, notify the user that the download succeeded.
file, err := os.CreateTemp("", "go_feature_flag")
if err != nil {
return nil, err
}

s3Req := &s3.GetObjectInput{
Bucket: aws.String(s.Bucket),
Key: aws.String(s.Item),
}
_, err = s.downloader.Download(ctx, file, s3Req)
if err != nil {
return nil, fmt.Errorf("unable to download item from S3 %q, %v", s.Item, err)
}
// Read file content
content, err := os.ReadFile(file.Name())
if err != nil {
return nil, err
}
return content, nil
}

func (s *Retriever) initializeDownloader(ctx context.Context) error {
var initErr error
s.init.Do(func() {
if s.AwsConfig == nil {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
initErr = fmt.Errorf("impossible to init S3 retriever: %v", err)
return
}
s.AwsConfig = &cfg
}
client := s3.NewFromConfig(*s.AwsConfig)
s.downloader = manager.NewDownloader(client)
})
return initErr
}
80 changes: 80 additions & 0 deletions retriever/s3retrieverv2/retriever_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package s3retrieverv2

import (
"context"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/stretchr/testify/assert"
"github.com/thomaspoignant/go-feature-flag/testutils"
"os"
"testing"
)

func Test_s3Retriever_Retrieve(t *testing.T) {
type fields struct {
downloader DownloaderAPI
bucket string
item string
context context.Context
}
tests := []struct {
name string
fields fields
want string
wantErr bool
}{
{
name: "File on S3",
fields: fields{
downloader: &testutils.S3ManagerV2Mock{
TestDataLocation: "./testdata",
},
bucket: "Bucket",
item: "valid",
},
want: "./testdata/flag-config.yaml",
wantErr: false,
},
{
name: "File not present S3",
fields: fields{
downloader: &testutils.S3ManagerV2Mock{
TestDataLocation: "./testdata",
},
bucket: "Bucket",
item: "no-file",
},
wantErr: true,
},
{
name: "File on S3 with context",
fields: fields{
downloader: &testutils.S3ManagerV2Mock{
TestDataLocation: "./testdata",
},
bucket: "Bucket",
item: "valid",
context: context.Background(),
},
want: "./testdata/flag-config.yaml",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
awsConf, _ := config.LoadDefaultConfig(context.TODO())
s := Retriever{
Bucket: tt.fields.bucket,
Item: tt.fields.item,
AwsConfig: &awsConf,
downloader: tt.fields.downloader,
}
got, err := s.Retrieve(tt.fields.context)
assert.Equal(t, tt.wantErr, err != nil, "Retrieve() error = %v, wantErr %v", err, tt.wantErr)
if err == nil {
want, err := os.ReadFile(tt.want)
assert.NoError(t, err)
assert.Equal(t, string(want), string(got), "Retrieve() got = %v, want %v", string(want), string(got))
}
})
}
}
13 changes: 13 additions & 0 deletions retriever/s3retrieverv2/testdata/flag-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
test-flag:
rule: key eq "random-key"
percentage: 100
true: true
false: false
default: false

test-flag2:
rule: key eq "not-a-key"
percentage: 100
true: true
false: false
default: false
14 changes: 14 additions & 0 deletions testutils/s3managerv2_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"io"
"os"
"strings"
)

type S3ManagerV2Mock struct {
S3ManagerMockFileSystem map[string]string
TestDataLocation string
}

func (s *S3ManagerV2Mock) Upload(ctx context.Context, uploadInput *s3.PutObjectInput, opts ...func(uploader *manager.Uploader)) (*manager.UploadOutput, error) {
Expand All @@ -34,3 +36,15 @@ func (s *S3ManagerV2Mock) Upload(ctx context.Context, uploadInput *s3.PutObjectI
Location: *uploadInput.Key,
}, nil
}

func (s *S3ManagerV2Mock) Download(ctx context.Context, w io.WriterAt, input *s3.GetObjectInput, options ...func(*manager.Downloader)) (n int64, err error) {
if *input.Key == "valid" {
res, _ := os.ReadFile(s.TestDataLocation + "/flag-config.yaml")
_, _ = w.WriteAt(res, 0)
return 1, nil
} else if *input.Key == "no-file" {
return 0, errors.New("no file")
}

return 1, nil
}
17 changes: 8 additions & 9 deletions website/docs/go_module/store_file/s3.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ The [**S3Retriever**](https://pkg.go.dev/github.com/thomaspoignant/go-feature-fl

## Example
```go linenums="1"
awsConfig, _ := config.LoadDefaultConfig(context.Background())
err := ffclient.Init(ffclient.Config{
PollingInterval: 3 * time.Second,
Retriever: &s3retriever.Retriever{
Retriever: &s3retrieverv2.Retriever{
Bucket: "tpoi-test",
Item: "flag-config.yaml",
AwsConfig: aws.Config{
Region: aws.String("eu-west-1"),
},
AwsConfig: &awsCOnfig,
},
})
defer ffclient.Close()
Expand All @@ -23,8 +22,8 @@ defer ffclient.Close()
## Configuration fields
To configure your S3 file location:

| Field | Description |
|---|---|
|**`Bucket`**| The name of your bucket.|
|**`Item`**| The location of your file in the bucket.|
|**`AwsConfig`**| An instance of `aws.Config` that configure your access to AWS <br/>*check [this documentation for more info](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html)*.|
| Field | Description |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`Bucket`** | The name of your bucket. |
| **`Item`** | The location of your file in the bucket. |
| **`AwsConfig`** | An instance of `aws.Config` that configure your access to AWS <br/>*check [this documentation for more info](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html)*. |

0 comments on commit dd2a703

Please sign in to comment.