diff --git a/api/core/v1beta1/common/common.go b/api/core/v1beta1/common/common.go index b07d953fa..ca9ad0d77 100644 --- a/api/core/v1beta1/common/common.go +++ b/api/core/v1beta1/common/common.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "strings" corev1 "k8s.io/api/core/v1" ) @@ -11,6 +12,7 @@ type SyncProviderType string const ( SyncProviderKubernetes SyncProviderType = "kubernetes" SyncProviderFilepath SyncProviderType = "file" + SyncProviderAzureBlob SyncProviderType = "azblob" SyncProviderGcs SyncProviderType = "gcs" SyncProviderHttp SyncProviderType = "http" SyncProviderGrpc SyncProviderType = "grpc" @@ -54,6 +56,10 @@ func (s SyncProviderType) IsKubernetes() bool { return s == SyncProviderKubernetes } +func (s SyncProviderType) IsAzureBlob() bool { + return s == SyncProviderAzureBlob +} + func (s SyncProviderType) IsHttp() bool { return s == SyncProviderHttp } @@ -85,6 +91,10 @@ func FalseVal() *bool { } func EnvVarKey(prefix string, suffix string) string { + // If prefix is blank (empty or whitespace), return just the suffix as the env var key + if strings.TrimSpace(prefix) == "" { + return suffix + } return fmt.Sprintf("%s_%s", prefix, suffix) } diff --git a/api/core/v1beta1/common/common_test.go b/api/core/v1beta1/common/common_test.go index 5f6d0b557..d9e4ffcc0 100644 --- a/api/core/v1beta1/common/common_test.go +++ b/api/core/v1beta1/common/common_test.go @@ -13,24 +13,35 @@ func Test_FeatureFlagSource_SyncProvider(t *testing.T) { h := SyncProviderHttp g := SyncProviderGrpc gcs := SyncProviderGcs + azureBlob := SyncProviderAzureBlob require.True(t, k.IsKubernetes()) require.True(t, f.IsFilepath()) require.True(t, h.IsHttp()) require.True(t, g.IsGrpc()) require.True(t, gcs.IsGcs()) + require.True(t, azureBlob.IsAzureBlob()) require.False(t, f.IsKubernetes()) require.False(t, h.IsFilepath()) require.False(t, k.IsGrpc()) require.False(t, g.IsHttp()) require.False(t, g.IsGcs()) + require.False(t, gcs.IsAzureBlob()) } func Test_FLagSourceConfiguration_EnvVarKey(t *testing.T) { require.Equal(t, "pre_suf", EnvVarKey("pre", "suf")) } +func Test_FLagSourceConfiguration_EnvVarKey_EmptyPre(t *testing.T) { + require.Equal(t, "suf", EnvVarKey(" ", "suf")) +} + +func Test_FLagSourceConfiguration_EnvVarKey_NoPre(t *testing.T) { + require.Equal(t, "suf", EnvVarKey("", "suf")) +} + func Test_FLagSourceConfiguration_FeatureFlagConfigurationId(t *testing.T) { require.Equal(t, "pre_suf", FeatureFlagConfigurationId("pre", "suf")) } diff --git a/api/core/v1beta1/featureflagsource_types.go b/api/core/v1beta1/featureflagsource_types.go index 5519a6fbc..c0647a863 100644 --- a/api/core/v1beta1/featureflagsource_types.go +++ b/api/core/v1beta1/featureflagsource_types.go @@ -18,6 +18,7 @@ package v1beta1 import ( "fmt" + "strings" "github.com/open-feature/open-feature-operator/apis/core/v1beta1/common" corev1 "k8s.io/api/core/v1" @@ -209,12 +210,19 @@ func (fc *FeatureFlagSourceSpec) Merge(new *FeatureFlagSourceSpec) { } } +func (fc *FeatureFlagSourceSpec) decorateEnvVarName(original string) string { + if strings.HasPrefix(original, "AZURE_STORAGE") { + return original + } + return common.EnvVarKey(fc.EnvVarPrefix, original) +} + func (fc *FeatureFlagSourceSpec) ToEnvVars() []corev1.EnvVar { envs := []corev1.EnvVar{} for _, envVar := range fc.EnvVars { envs = append(envs, corev1.EnvVar{ - Name: common.EnvVarKey(fc.EnvVarPrefix, envVar.Name), + Name: fc.decorateEnvVarName(envVar.Name), Value: envVar.Value, }) } diff --git a/api/core/v1beta1/featureflagsource_types_test.go b/api/core/v1beta1/featureflagsource_types_test.go index dc66ace72..2c4e2228b 100644 --- a/api/core/v1beta1/featureflagsource_types_test.go +++ b/api/core/v1beta1/featureflagsource_types_test.go @@ -180,6 +180,14 @@ func Test_FLagSourceConfiguration_ToEnvVars(t *testing.T) { Name: "env2", Value: "val2", }, + { + Name: "AZURE_STORAGE_ACCOUNT", + Value: "account123", + }, + { + Name: "AZURE_STORAGE_KEY", + Value: "key456", + }, }, EnvVarPrefix: "PRE", ManagementPort: 22, @@ -198,6 +206,14 @@ func Test_FLagSourceConfiguration_ToEnvVars(t *testing.T) { Name: "PRE_env2", Value: "val2", }, + { + Name: "AZURE_STORAGE_ACCOUNT", + Value: "account123", + }, + { + Name: "AZURE_STORAGE_KEY", + Value: "key456", + }, { Name: "PRE_MANAGEMENT_PORT", Value: "22", diff --git a/docs/feature_flag_source.md b/docs/feature_flag_source.md index 486e23b65..db2f95cd1 100644 --- a/docs/feature_flag_source.md +++ b/docs/feature_flag_source.md @@ -83,6 +83,22 @@ sources: selector: 'source=database,app=weatherapp' # flag filtering options ``` +### Azure Blob Storage + +Given below is an example configuration with provider type `azblob` and supported options, + +```yaml +sources: + - source: azblob://my-bucket/test.json # my-bucket - container name + provider: azblob + envVars: + - name: AZURE_STORAGE_ACCOUNT + value: + - name: AZURE_STORAGE_SAS_TOKEN + value: +``` +Other type of credentials for Azure Blob Storage are supported, for details (see [AZ credentials config](https://pkg.go.dev/gocloud.dev/blob/azureblob#hdr-URLs)) + ## Sidecar configurations `FeatureFlagSource` provides configurations to the injected flagd sidecar. diff --git a/internal/common/flagdinjector/flagdinjector.go b/internal/common/flagdinjector/flagdinjector.go index 8ac6ea239..67d3302c9 100644 --- a/internal/common/flagdinjector/flagdinjector.go +++ b/internal/common/flagdinjector/flagdinjector.go @@ -236,6 +236,8 @@ func (fi *FlagdContainerInjector) newSourceConfig(ctx context.Context, source ap sourceCfg = fi.toGrpcProviderConfig(source) case source.Provider.IsFlagdProxy(): sourceCfg, err = fi.toFlagdProxyConfig(ctx, objectMeta, source) + case source.Provider.IsAzureBlob(): + sourceCfg = fi.toAzureBlobConfig(source) default: err = fmt.Errorf("could not add provider %s: %w", source.Provider, common.ErrUnrecognizedSyncProvider) } @@ -327,6 +329,14 @@ func (fi *FlagdContainerInjector) toGrpcProviderConfig(source api.Source) types. } } +func (fi *FlagdContainerInjector) toAzureBlobConfig(source api.Source) types.SourceConfig { + return types.SourceConfig{ + URI: source.Source, + Provider: string(apicommon.SyncProviderAzureBlob), + Interval: source.Interval, + } +} + func (fi *FlagdContainerInjector) toFlagdProxyConfig(ctx context.Context, objectMeta *metav1.ObjectMeta, source api.Source) (types.SourceConfig, error) { // does the proxy exist exists, ready, err := fi.isFlagdProxyReady(ctx)