From df89437b466a10de953f7d619c2788670304a33b Mon Sep 17 00:00:00 2001 From: Abeeujah Date: Mon, 18 Nov 2024 18:18:30 +0100 Subject: [PATCH] refac: Implemented requested changes --- README.md | 58 ++++----- cmd/relayproxy/config/retriever.go | 23 ++-- cmd/relayproxy/config/retriever_test.go | 9 +- cmd/relayproxy/service/gofeatureflag.go | 2 +- retriever/azblobstorageretriever/README.md | 115 ++++++++++++++++-- retriever/azblobstorageretriever/retriever.go | 74 ++++++++--- .../azblobstorageretriever/retriever_test.go | 2 +- 7 files changed, 216 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index ad0485ca570d..a96f4f690db0 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ Sponsords

-> :pray: If you are using **GO Feature Flag** please consider to add yourself in the [adopters](./ADOPTERS.md) list. +> :pray: If you are using **GO Feature Flag** please consider to add yourself in the [adopters](./ADOPTERS.md) list. > This simple act significantly boosts the project's visibility and credibility, making a substantial contribution to its advancement. -> +> > If you want to support me and GO Feature Flag, you can also [become a sponsor](https://github.com/sponsors/thomaspoignant). ## Table of Contents @@ -82,11 +82,11 @@ _The code of this demo is available in [`examples/demo`](examples/demo) reposito > [!IMPORTANT] > Before starting to use **GO Feature Flag** you should decide > if you want to use Open Feature SDKs or if you want to use GO Feature Flag as a GO Module. -> +> > We recommend using the relay-proxy for a central flag management and evaluation solution, -> it enables the multi-languages support, and it integrates seamlessly with the Open Feature SDKs. +> it enables the multi-languages support, and it integrates seamlessly with the Open Feature SDKs. > This is the best way to get full potential of GO Feature Flag. -> +> > If your project is exclusively in GO, the GO module is an option. It will perform the flag evaluation directly in your GO code. @@ -128,7 +128,7 @@ exporter: ### Install the relay proxy -And we will run the **relay proxy** locally to make the API available. +And we will run the **relay proxy** locally to make the API available. The default port will be `1031`. ```shell @@ -239,8 +239,8 @@ defer ffclient.Close() *This example will load a file from your local computer and will refresh the flags every 3 seconds (if you omit the PollingInterval, the default value is 60 seconds).* -> ℹ info -This is a basic configuration to test locally, in production it is better to use a remote place to store your feature flag configuration file. +> ℹ info +This is a basic configuration to test locally, in production it is better to use a remote place to store your feature flag configuration file. Look at the list of available options in the [**Store your feature flag file** page](https://gofeatureflag.org/docs/go_module/store_file/). ### Evaluate your flags @@ -255,18 +255,18 @@ if hasFlag { // flag "test-flag" is false for the user } ``` -The full documentation is available on https://docs.gofeatureflag.org +The full documentation is available on https://docs.gofeatureflag.org You can find more examples in the [examples/](https://github.com/thomaspoignant/go-feature-flag/tree/main/examples) directory. ## Can I use GO Feature Flag with any language? -Originally GO Feature Flag was built to be a GOlang only library, but it limits the ecosystem too much. +Originally GO Feature Flag was built to be a GOlang only library, but it limits the ecosystem too much. To be compatible with more languages we have implemented the [GO Feature Flag Relay Proxy](cmd/relayproxy/). It is a service you can host that provides an API to evaluate your flags, you can call it using HTTP to get your variation. -Since we believe in standardization we are also implementing [OpenFeature](https://github.com/open-feature) providers to interact with this API in the language of your choice. +Since we believe in standardization we are also implementing [OpenFeature](https://github.com/open-feature) providers to interact with this API in the language of your choice. _(OpenFeature is still at an early stage, so not all languages are supported and expect some changes in the future)_ For now, we have providers for: @@ -275,7 +275,7 @@ For now, we have providers for: |--------------------------------|-------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Go | [Go Provider](https://github.com/open-feature/go-sdk-contrib/tree/main/providers/go-feature-flag) | [![version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fproxy.golang.org%2Fgithub.com%2Fopen-feature%2Fgo-sdk-contrib%2Fproviders%2Fgo-feature-flag%2F%40latest&query=%24.Version&label=GO&color=blue&style=flat-square&logo=golang)](https://github.com/open-feature/go-sdk-contrib/tree/main/providers/go-feature-flag) | | Java / Kotlin (server) | [Java Provider](https://github.com/open-feature/java-sdk-contrib/tree/main/providers/go-feature-flag) | [![version](https://img.shields.io/maven-central/v/dev.openfeature.contrib.providers/go-feature-flag?color=blue&style=flat-square&logo=java)](https://central.sonatype.com/artifact/dev.openfeature.contrib.providers/go-feature-flag) | -| Android / Kotlin (client) | [Kotlin Provider](openfeature/providers/kotlin-provider) | [![version](https://img.shields.io/maven-central/v/org.gofeatureflag.openfeature/gofeatureflag-kotlin-provider?color=blue&style=flat-square&logo=android)](https://central.sonatype.com/artifact/org.gofeatureflag.openfeature/gofeatureflag-kotlin-provider) | +| Android / Kotlin (client) | [Kotlin Provider](openfeature/providers/kotlin-provider) | [![version](https://img.shields.io/maven-central/v/org.gofeatureflag.openfeature/gofeatureflag-kotlin-provider?color=blue&style=flat-square&logo=android)](https://central.sonatype.com/artifact/org.gofeatureflag.openfeature/gofeatureflag-kotlin-provider) | | Javascript/Typescript (server) | [Server Provider](https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/go-feature-flag) | [![version](https://img.shields.io/npm/v/%40openfeature%2Fgo-feature-flag-provider?color=blue&style=flat-square&logo=npm)](https://www.npmjs.com/package/@openfeature/go-feature-flag-provider) | | Javascript/Typescript (client) | [Client Provider](https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/go-feature-flag-web) | [![version](https://img.shields.io/npm/v/%40openfeature%2Fgo-feature-flag-web-provider?color=blue&style=flat-square&logo=npm)](https://www.npmjs.com/package/@openfeature/go-feature-flag-web-provider) | | Python | [Python Provider](openfeature/providers/python-provider) | [![version](https://img.shields.io/pypi/v/gofeatureflag-python-provider?color=blue&style=flat-square&logo=pypi)](https://pypi.org/project/gofeatureflag-python-provider/) | @@ -283,11 +283,11 @@ For now, we have providers for: | Ruby | [Ruby Provider](https://github.com/open-feature/ruby-sdk-contrib/tree/main/providers/openfeature-go-feature-flag-provider) | [![version](https://img.shields.io/gem/v/openfeature-go-feature-flag-provider?color=blue&style=flat-square&logo=ruby)](https://rubygems.org/gems/openfeature-go-feature-flag-provider) | | Swift | [Swift Provider](https://github.com/go-feature-flag/openfeature-swift-provider) | [![version](https://img.shields.io/github/v/release/go-feature-flag/openfeature-swift-provider?label=Swift&display_name=tag&style=flat-square&logo=Swift)](https://github.com/go-feature-flag/openfeature-swift-provider) | | PHP | [PHP Provider](https://github.com/open-feature/php-sdk-contrib/tree/main/providers/GoFeatureFlag) | [![version](https://img.shields.io/packagist/v/open-feature/go-feature-flag-provider?logo=php&color=blue&style=flat-square)](https://packagist.org/packages/open-feature/go-feature-flag-provider) | - + ## Where do I store my flags file? -The module supports different ways of retrieving the flag file. +The module supports different ways of retrieving the flag file. The available retrievers are: - **GitHub** - **GitLab** @@ -309,7 +309,7 @@ _[See the full list and more information.](https://gofeatureflag.org/docs/config Your file should be a `YAML`, `JSON` or `TOML` file with a list of flags *(examples: [`YAML`](testdata/flag-config.yaml), [`JSON`](testdata/flag-config.json), [`TOML`](testdata/flag-config.toml))*. -The easiest way to create your configuration file is to use **GO Feature Flag Editor** available at https://editor.gofeatureflag.org. +The easiest way to create your configuration file is to use **GO Feature Flag Editor** available at https://editor.gofeatureflag.org. If you prefer to do it manually please follow the instruction below. **A flag configuration looks like this:** @@ -455,20 +455,20 @@ For detailed information on the fields required to create a flag, please refer t The query format is based on the [`nikunjy/rules`](https://github.com/nikunjy/rules) library. -All the operations can be written in capitalized or lowercase (ex: `eq` or `EQ` can be used). +All the operations can be written in capitalized or lowercase (ex: `eq` or `EQ` can be used). Logical Operations supported are `AND` `OR`. Compare Expression and their definitions (`a|b` means you can use either one of the two `a` or `b`): ``` -eq|==: equals to +eq|==: equals to ne|!=: not equals to -lt|<: less than +lt|<: less than gt|>: greater than le|<=: less than equal to -ge|>=: greater than equal to -co: contains -sw: starts with +ge|>=: greater than equal to +co: contains +sw: starts with ew: ends with in: in a list pr: present @@ -487,8 +487,8 @@ When using GO Feature Flag, it's often necessary to personalize the experience f For instance, GO Feature Flag ensures that in cases where a feature is being rolled out to a percentage of users, based on the targeting key, they will see the same variation each time they encounter the feature flag. -The targeting key is a fundamental part of the evaluation context because it directly affects the determination of which feature variant is served to a particular user, and it maintains that continuity over time. To do so GO Feature Flag to do a hash to define if the flag can apply to this evaluation context or not. -**We recommend using a hash if possible.** +The targeting key is a fundamental part of the evaluation context because it directly affects the determination of which feature variant is served to a particular user, and it maintains that continuity over time. To do so GO Feature Flag to do a hash to define if the flag can apply to this evaluation context or not. +**We recommend using a hash if possible.** Feature flag targeting and rollouts are all determined by the user you pass to your evaluation calls. @@ -499,7 +499,7 @@ In some cases, you might need to _bucket_ users based on a different key, e.g. a This can be achieved by defining the `bucketingKey` field in the flag configuration. When present, the value corresponding to the `bucketingKey` will be extracted from the attributes, and that value used for hashing and determining the outcome in place of the `targetingKey`. ## Variations -Variations are the different values possible for a feature flag. +Variations are the different values possible for a feature flag. GO Feature Flag can manage more than just `boolean` values; the value of your flag can be any of the following types: - `bool` - `int` @@ -519,7 +519,7 @@ Variation methods take the feature **flag key**, an **evaluation context**, and **Why do we need a default value?** If we have any error during the evaluation of the flag, we will return the default value, you will always get a value return from the function and we will never throw an error. -In the example, if the flag `your.feature.key` does not exist, the result will be `false`. +In the example, if the flag `your.feature.key` does not exist, the result will be `false`. Note that the result will always provide a usable value. ## Rollout @@ -551,7 +551,7 @@ Available notifiers are: - **Microsoft Teams** ## Export data -**GO Feature Flag** allows you to export data about the usage of your flags. +**GO Feature Flag** allows you to export data about the usage of your flags. It collects all variation events and can save these events in several locations: - **Local file** *- create local files with the variation usages.* @@ -563,7 +563,7 @@ It collects all variation events and can save these events in several locations: - **AWS SQS** *- export your variation usages by sending events to SQS.* - **Google PubSub** *- export your variation usages by publishing events to PubSub topic.* -Currently, we are supporting only feature events. +Currently, we are supporting only feature events. It represents individual flag evaluations and is considered "full fidelity" events. **An example feature event below:** @@ -591,7 +591,7 @@ A command line tool is available to help you lint your configuration file: [go-f This project welcomes contributions from the community. If you're interested in contributing, see the [contributors' guide](CONTRIBUTING.md) for some helpful tips. ## Community meetings -Since everyone's voice is important we want to hear back from the community. +Since everyone's voice is important we want to hear back from the community. For this reason, we are launching a community meeting every 2 weeks and it is the perfect place to discuss the future of GO Feature Flag and help you use it at full potential. | Name | Meeting Time | Meeting Notes | Discussions | @@ -621,4 +621,4 @@ These are our really cool sponsors! If you are using `go-feature-flag`, we encourage you to include your company's name in this list. This simple act significantly boosts the project's visibility and credibility, making a substantial contribution to its advancement. To do so, kindly add yourself to [adopters](./ADOPTERS.md). -Here is the list of [adopters](./ADOPTERS.md). +Here is the list of [adopters](./ADOPTERS.md). \ No newline at end of file diff --git a/cmd/relayproxy/config/retriever.go b/cmd/relayproxy/config/retriever.go index 24c86694df5c..c494be0474a7 100644 --- a/cmd/relayproxy/config/retriever.go +++ b/cmd/relayproxy/config/retriever.go @@ -70,14 +70,8 @@ func (c *RetrieverConf) IsValid() error { if c.Kind == RedisRetriever { return c.validateRedisRetriever() } - if c.Kind == AzBlobStorageRetriever && c.AccountName == "" { - return fmt.Errorf("invalid retriever: no \"accountName\" property found for kind \"%s\"", c.Kind) - } - if c.Kind == AzBlobStorageRetriever && c.Container == "" { - return fmt.Errorf("invalid retriever: no \"container\" property found for kind \"%s\"", c.Kind) - } - if c.Kind == AzBlobStorageRetriever && c.Object == "" { - return fmt.Errorf("invalid retriever: no \"object\" property found for kind \"%s\"", c.Kind) + if c.Kind == AzBlobStorageRetriever { + return c.validateAzBlobStorageRetriever() } return nil } @@ -125,6 +119,19 @@ func (c *RetrieverConf) validateRedisRetriever() error { return nil } +func (c *RetrieverConf) validateAzBlobStorageRetriever() error { + if c.AccountName == "" { + return fmt.Errorf("invalid retriever: no \"accountName\" property found for kind \"%s\"", c.Kind) + } + if c.Container == "" { + return fmt.Errorf("invalid retriever: no \"container\" property found for kind \"%s\"", c.Kind) + } + if c.Object == "" { + return fmt.Errorf("invalid retriever: no \"object\" property found for kind \"%s\"", c.Kind) + } + return nil +} + // RetrieverKind is an enum containing all accepted Retriever kind type RetrieverKind string diff --git a/cmd/relayproxy/config/retriever_test.go b/cmd/relayproxy/config/retriever_test.go index 231af2ef5040..d831abedf8ca 100644 --- a/cmd/relayproxy/config/retriever_test.go +++ b/cmd/relayproxy/config/retriever_test.go @@ -123,14 +123,15 @@ func TestRetrieverConf_IsValid(t *testing.T) { errValue: "invalid retriever: no \"bucket\" property found for kind \"googleStorage\"", }, { - name: "valid azureBlobStorage", + name: "kind azureBlobStorage without object", fields: config.RetrieverConf{ Kind: "azureBlobStorage", Container: "testcontainer", - Object: "flag-config.yaml", AccountName: "devstoreaccount1", AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq", }, + wantErr: true, + errValue: "invalid retriever: no \"object\" property found for kind \"azureBlobStorage\"", }, { name: "kind azureBlobStorage without accountName", @@ -140,6 +141,8 @@ func TestRetrieverConf_IsValid(t *testing.T) { Object: "flag-config.yaml", AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq", }, + wantErr: true, + errValue: "invalid retriever: no \"accountName\" property found for kind \"azureBlobStorage\"", }, { name: "kind azureBlobStorage without container", @@ -149,6 +152,8 @@ func TestRetrieverConf_IsValid(t *testing.T) { AccountName: "devstoreaccount1", AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq", }, + wantErr: true, + errValue: "invalid retriever: no \"container\" property found for kind \"azureBlobStorage\"", }, { name: "valid s3", diff --git a/cmd/relayproxy/service/gofeatureflag.go b/cmd/relayproxy/service/gofeatureflag.go index b9582bf0039e..4cf2ba69eadb 100644 --- a/cmd/relayproxy/service/gofeatureflag.go +++ b/cmd/relayproxy/service/gofeatureflag.go @@ -26,7 +26,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/notifier/slacknotifier" "github.com/thomaspoignant/go-feature-flag/notifier/webhooknotifier" "github.com/thomaspoignant/go-feature-flag/retriever" - azblobretriever "github.com/thomaspoignant/go-feature-flag/retriever/azblobstorageretriever" + "github.com/thomaspoignant/go-feature-flag/retriever/azblobstorageretriever" "github.com/thomaspoignant/go-feature-flag/retriever/bitbucketretriever" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" "github.com/thomaspoignant/go-feature-flag/retriever/gcstorageretriever" diff --git a/retriever/azblobstorageretriever/README.md b/retriever/azblobstorageretriever/README.md index 4302a2e621d3..213c0e137b7a 100644 --- a/retriever/azblobstorageretriever/README.md +++ b/retriever/azblobstorageretriever/README.md @@ -1,9 +1,106 @@ -# Redis retriever -This retriever is used to retrieve data from your Azure Blob Storage Container. - -## How to use? -Provide the following fields: -- Azure Account Name -- Azure Account Key (Optional if you're authenticating with Microsoft Entra ID) -- Azure Container Name -- Azure Blob Name +# Azure Blob Storage Feature Flag Retriever + +This retriever is used to retrieve data from a Container on Azure Blob Storage. + +## Installation + +```bash +go get github.com/thomaspoignant/go-feature-flag/retriever/azblobstorageretriever +``` + +## Usage + +### Configuration + +Create a `Retriever` struct with the following fields: + +- `Container`: Name of the Azure Blob Storage container +- `AccountName`: Azure Storage Account Name +- `AccountKey`: (Optional) Storage Account Key +- `ServiceURL`: (Optional) Custom service URL +- `Object`: Name of the feature flag file in the container + +### Authentication Methods + +#### 1. Shared Key Authentication + +```go +retriever := &azblobretriever.Retriever{ + Container: "your-container", + AccountName: "your-account-name", + AccountKey: "your-account-key", + Object: "feature-flags.json", +} +``` + +#### 2. Microsoft Entra ID (Recommended) + +```go +retriever := &azblobretriever.Retriever{ + Container: "your-container", + AccountName: "your-account-name", + Object: "feature-flags.json", +} +``` + +### Retrieving Feature Flags + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/thomaspoignant/go-feature-flag/retriever/azblobstorageretriever" +) + +func main() { + retriever := &azblobretriever.Retriever{ + Container: "feature-flags", + AccountName: "mystorageaccount", + AccountKey: "your-account-key", + Object: "flags.json", + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + err := retriever.Init(context.Background(), nil) + if err != nil { + log.Fatalf("Failed to initialize retriever:", err) + } + + data, err := retriever.Retrieve(ctx) + if err != nil { + log.Fatalf("Failed to retrieve feature flags: %v", err) + } + + retriever.Shutdown() + + fmt.Println("Retrieved feature flags:") + fmt.Println(string(data)) +} +``` + +## Key Features + +- Supports both shared key and default Azure credential authentication +- Automatic retry mechanism for blob downloads +- Flexible configuration with optional custom service URL + +## Error Handling + +The `Retrieve` method returns an error if: +- `AccountName` is empty +- `Container` or `Object` is not specified +- There's an issue initializing the Azure client. +- There's a problem downloading or reading the file from Azure Blob Storage. + +## Best Practices + +- **Security** Never hard-code your `AccountKey` in your source code. Use environment variables or a secure secret management system. +- **Error Handling**: Always check for errors returned by the `Retrieve` method. +- **Context**: Use a context with timeout for better control over the retrieval process. diff --git a/retriever/azblobstorageretriever/retriever.go b/retriever/azblobstorageretriever/retriever.go index 31e94cc7e9d2..26ceb64f4501 100644 --- a/retriever/azblobstorageretriever/retriever.go +++ b/retriever/azblobstorageretriever/retriever.go @@ -7,6 +7,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" + "github.com/thomaspoignant/go-feature-flag/retriever" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" ) type Retriever struct { @@ -24,38 +26,76 @@ type Retriever struct { // Object is the name of your file in your container. Object string + + // client is a pointer to an Azure Blob Storage client. + // It provides access to Azure Blob Storage services for operations like + // creating, reading, updating, and deleting blobs. + client *azblob.Client + status retriever.Status } -func (f *Retriever) initializeAzureClient() (*azblob.Client, error) { - url := fmt.Sprintf("https://%s.blob.core.windows.net/", f.AccountName) - if f.ServiceURL != "" { - url = f.ServiceURL +func (r *Retriever) Init(_ context.Context, _ *fflog.FFLogger) error { + if r.AccountName == "" { + return fmt.Errorf("unable to connect to Azure Blob Storage, \"AccountName\" cannot be empty") + } + + url := r.ServiceURL + if url == "" { + url = fmt.Sprintf("https://%s.blob.core.windows.net/", r.AccountName) } - if f.AccountKey == "" { - cred, err := azidentity.NewDefaultAzureCredential(nil) + + var client *azblob.Client + var err error + + if r.AccountKey == "" { + var cred *azidentity.DefaultAzureCredential + cred, err = azidentity.NewDefaultAzureCredential(nil) if err != nil { - return nil, err + r.status = retriever.RetrieverError + return err } - return azblob.NewClient(url, cred, nil) + client, err = azblob.NewClient(url, cred, nil) + } else { + var cred *azblob.SharedKeyCredential + cred, err = azblob.NewSharedKeyCredential(r.AccountName, r.AccountKey) + if err != nil { + r.status = retriever.RetrieverError + return err + } + client, err = azblob.NewClientWithSharedKeyCredential(url, cred, nil) } - cred, err := azblob.NewSharedKeyCredential(f.AccountName, f.AccountKey) + if err != nil { - return nil, err + r.status = retriever.RetrieverError + return err } - return azblob.NewClientWithSharedKeyCredential(url, cred, nil) + + r.client = client + r.status = retriever.RetrieverReady + return nil +} + +func (r *Retriever) Shutdown(_ context.Context) error { + r.status = retriever.RetrieverNotReady + r.client = nil + return nil +} + +func (r *Retriever) Status() retriever.Status { + return r.status } func (r *Retriever) Retrieve(ctx context.Context) ([]byte, error) { - if r.Object == "" || r.Container == "" { - return nil, fmt.Errorf("missing mandatory information filePath=%s, repositorySlug=%s", r.Object, r.Container) + if r.client == nil { + r.status = retriever.RetrieverError + return nil, fmt.Errorf("client is not initialized") } - client, err := r.initializeAzureClient() - if err != nil { - return nil, err + if r.Object == "" || r.Container == "" { + return nil, fmt.Errorf("missing mandatory information filePath=%s, repositorySlug=%s", r.Object, r.Container) } - fileStream, err := client.DownloadStream(ctx, r.Container, r.Object, nil) + fileStream, err := r.client.DownloadStream(ctx, r.Container, r.Object, nil) if err != nil { return nil, err } diff --git a/retriever/azblobstorageretriever/retriever_test.go b/retriever/azblobstorageretriever/retriever_test.go index 7ec55b5c963b..ac500bef76d5 100644 --- a/retriever/azblobstorageretriever/retriever_test.go +++ b/retriever/azblobstorageretriever/retriever_test.go @@ -1,7 +1,7 @@ //go:build docker // +build docker -package azblobretriever +package azblobretriever_test import ( "context"