Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: aligned environment variables application with flagd provider spec #119

Merged
merged 2 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions providers/flagd/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# Flagd Provider

![Experimental](https://img.shields.io/badge/experimental-breaking%20changes%20allowed-yellow)
![Alpha](https://img.shields.io/badge/alpha-release-red)

[Flagd](https://github.com/open-feature/flagd) is a simple command line tool for fetching and presenting feature flags to services. It is designed to conform to OpenFeature schema for flag definitions. This repository and package provides the client side code for interacting with it via the [OpenFeature SDK](https://github.com/open-feature/go-sdk).

## Setup
Expand Down Expand Up @@ -44,16 +41,18 @@ func main() {
```

### Using flagd.FromEnv()
By default the flagd provider will not read environment variables to set its own configuration, however, if the `flagd.FromEnv()` option is set as an argument for the `flagd.NewProvider()` method, then the following table of environment variables are applicable.

| Option name | Environment variable name | Type | Default |
|-----------------|---------------------------| ------- |-----------|
| host | FLAGD_HOST | string | localhost |
| port | FLAGD_PORT | number | 8013 |
| tls | FLAGD_TLS | boolean | false |
| socketPath | FLAGD_SOCKET_PATH | string | |
| certPath | FLAGD_SERVER_CERT_PATH | string | |
| cachingDisabled | FLAGD_CACHING_DISABLED | boolean | false |
By default the flagd provider will read non-empty environment variables to set its own configuration with the lowest priority. Use the `flagd.FromEnv()` option as an argument for the `flagd.NewProvider()` method to give environment variables a higher priority.

| Option name | Environment variable name | Type | Options | Default |
|-----------------------|--------------------------------|-----------|--------------|-----------|
| host | FLAGD_HOST | string | | localhost |
| port | FLAGD_PORT | number | | 8013 |
| tls | FLAGD_TLS | boolean | | false |
| socketPath | FLAGD_SOCKET_PATH | string | | |
| certPath | FLAGD_SERVER_CERT_PATH | string | | |
| cache | FLAGD_CACHE | string | lru,disabled | lru |
| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | | 1000 |
| maxEventStreamRetries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | | 5 |

In the event that another configuration option is passed to the `flagd.NewProvider()` method, such as `flagd.WithPort(8013)` then priority is decided by the order in which the options are passed to the constructor from lowest to highest priority.

Expand Down
117 changes: 89 additions & 28 deletions providers/flagd/pkg/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
schemaV1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/schema/v1"
"context"
"errors"
"fmt"
"github.com/go-logr/logr"
lru "github.com/hashicorp/golang-lru/v2"
flagdModels "github.com/open-feature/flagd/pkg/model"
Expand All @@ -17,21 +18,32 @@ import (
"strconv"
)

// naming and defaults follow https://github.com/open-feature/flagd/blob/main/docs/other_resources/creating_providers.md?plain=1#L117
const (
defaultLRUCacheSize int = 1000
flagdHostEnvironmentVariableName = "FLAGD_HOST"
flagdPortEnvironmentVariableName = "FLAGD_PORT"
flagdTLSEnvironmentVariableName = "FLAGD_TLS"
flagdSocketPathEnvironmentVariableName = "FLAGD_SOCKET_PATH"
flagdServerCertPathEnvironmentVariableName = "FLAGD_SERVER_CERT_PATH"
flagdCachingDisabledEnvironmentVariableName = "FLAGD_CACHING_DISABLED"
defaultMaxCacheSize int = 1000
defaultPort = 8013
defaultMaxEventStreamRetries = 5
defaultTLS bool = false
defaultCache string = "lru"
defaultHost = "localhost"
flagdHostEnvironmentVariableName = "FLAGD_HOST"
flagdPortEnvironmentVariableName = "FLAGD_PORT"
flagdTLSEnvironmentVariableName = "FLAGD_TLS"
flagdSocketPathEnvironmentVariableName = "FLAGD_SOCKET_PATH"
flagdServerCertPathEnvironmentVariableName = "FLAGD_SERVER_CERT_PATH"
flagdCacheEnvironmentVariableName = "FLAGD_CACHE"
flagdMaxCacheSizeEnvironmentVariableName = "FLAGD_MAX_CACHE_SIZE"
flagdMaxEventStreamRetriesEnvironmentVariableName = "FLAGD_MAX_EVENT_STREAM_RETRIES"
cacheDisabledValue = "disabled"
cacheLRUValue = "lru"
)

type Provider struct {
ctx context.Context
service service.IService
cacheEnabled bool
cache Cache[string, interface{}]
maxCacheSize int
providerConfiguration *ProviderConfiguration
eventStreamConnectionMaxAttempts int
isReady chan struct{}
Expand All @@ -52,16 +64,15 @@ func NewProvider(opts ...ProviderOption) *Provider {
ctx: context.Background(),
// providerConfiguration maintains its default values, to ensure that the FromEnv option does not overwrite any explicitly set
// values (default values are then set after the options are run via applyDefaults())
providerConfiguration: &ProviderConfiguration{},
eventStreamConnectionMaxAttempts: 5,
isReady: make(chan struct{}),
logger: logr.New(logger.Logger{}),
providerConfiguration: &ProviderConfiguration{},
isReady: make(chan struct{}),
logger: logr.New(logger.Logger{}),
}
WithLRUCache(defaultLRUCacheSize)(provider)
for _, opt := range opts {
provider.applyDefaults() // defaults have the lowest priority
FromEnv()(provider) // env variables have higher priority than defaults
for _, opt := range opts { // explicitly declared options have the highest priority
opt(provider)
}
provider.applyDefaults()
provider.service = service.NewService(&service.Client{
ServiceConfiguration: &service.ServiceConfiguration{
Host: provider.providerConfiguration.Host,
Expand All @@ -82,12 +93,12 @@ func NewProvider(opts ...ProviderOption) *Provider {
}

func (p *Provider) applyDefaults() {
if p.providerConfiguration.Host == "" {
p.providerConfiguration.Host = "localhost"
}
if p.providerConfiguration.Port == 0 {
p.providerConfiguration.Port = 8013
}
p.providerConfiguration.Host = defaultHost
p.providerConfiguration.Port = defaultPort
p.providerConfiguration.TLSEnabled = defaultTLS
p.eventStreamConnectionMaxAttempts = defaultMaxEventStreamRetries
p.maxCacheSize = defaultMaxCacheSize
p.withCache(defaultCache)
}

// WithSocketPath overrides the default hostname and port, a unix socket connection is made to flagd instead
Expand All @@ -97,14 +108,18 @@ func WithSocketPath(socketPath string) ProviderOption {
}
}

// FromEnv sets the provider configuration from environment variables: FLAGD_HOST, FLAGD_PORT, FLAGD_SERVICE_PROVIDER, FLAGD_SERVER_CERT_PATH & FLAGD_CACHING_DISABLED
// FromEnv sets the provider configuration from environment variables (if set) as defined https://github.com/open-feature/flagd/blob/main/docs/other_resources/creating_providers.md?plain=1#L117
func FromEnv() ProviderOption {
return func(p *Provider) {
portS := os.Getenv(flagdPortEnvironmentVariableName)
if portS != "" {
port, err := strconv.Atoi(portS)
if err != nil {
p.logger.Error(err, "invalid env config for FLAGD_PORT provided, using default value")
p.logger.Error(err,
fmt.Sprintf(
"invalid env config for %s provided, using default value: %d",
flagdPortEnvironmentVariableName, defaultPort,
))
} else {
p.providerConfiguration.Port = uint16(port)
}
Expand All @@ -120,22 +135,65 @@ func FromEnv() ProviderOption {
p.providerConfiguration.SocketPath = socketPath
}

cachingDisabled := os.Getenv(flagdCachingDisabledEnvironmentVariableName)
if cachingDisabled == "true" {
WithoutCache()(p)
}

certificatePath := os.Getenv(flagdServerCertPathEnvironmentVariableName)
if certificatePath != "" || os.Getenv(flagdTLSEnvironmentVariableName) == "true" {
WithTLS(certificatePath)(p)
}

maxCacheSizeS := os.Getenv(flagdMaxCacheSizeEnvironmentVariableName)
if maxCacheSizeS != "" {
maxCacheSizeFromEnv, err := strconv.Atoi(maxCacheSizeS)
if err != nil {
p.logger.Error(err,
fmt.Sprintf("invalid env config for %s provided, using default value: %d",
flagdMaxCacheSizeEnvironmentVariableName, defaultMaxCacheSize,
))
} else {
p.maxCacheSize = maxCacheSizeFromEnv
}
}

if cacheValue := os.Getenv(flagdCacheEnvironmentVariableName); cacheValue != "" {
if ok := p.withCache(cacheValue); !ok {
p.logger.Error(fmt.Errorf("%s is invalid", cacheValue),
fmt.Sprintf("invalid env config for %s provided, using default value: %s",
flagdCacheEnvironmentVariableName, defaultCache,
))
}
}

maxEventStreamRetriesS := os.Getenv(flagdMaxEventStreamRetriesEnvironmentVariableName)
if maxEventStreamRetriesS != "" {
maxEventStreamRetries, err := strconv.Atoi(maxEventStreamRetriesS)
if err != nil {
p.logger.Error(err,
fmt.Sprintf("invalid env config for %s provided, using default value: %d",
flagdMaxEventStreamRetriesEnvironmentVariableName, defaultMaxEventStreamRetries))
} else {
p.eventStreamConnectionMaxAttempts = maxEventStreamRetries
}
}
}
}

func (p *Provider) withCache(cache string) bool {
switch cache {
case cacheDisabledValue:
WithoutCache()(p)
case cacheLRUValue:
WithLRUCache(p.maxCacheSize)
default:
return false
}

return true
}

// WithCertificatePath specifies the location of the certificate to be used in the gRPC dial credentials. If certificate loading fails insecure credentials will be used instead
func WithCertificatePath(path string) ProviderOption {
return func(p *Provider) {
p.providerConfiguration.CertificatePath = path
p.providerConfiguration.TLSEnabled = true
}
}

Expand Down Expand Up @@ -175,7 +233,10 @@ func WithBasicInMemoryCache() ProviderOption {
// least recently used entry.
func WithLRUCache(size int) ProviderOption {
return func(p *Provider) {
c, err := lru.New[string, interface{}](size)
if size != 0 {
p.maxCacheSize = size
}
c, err := lru.New[string, interface{}](p.maxCacheSize)
if err != nil {
p.logger.Error(err, "init lru cache")
return
Expand Down