diff --git a/README.md b/README.md
index 2c4f660b055..0df6441b650 100644
--- a/README.md
+++ b/README.md
@@ -299,6 +299,7 @@ The available retrievers are:
- **Local file**
- **Google Cloud Storage**
- **Kubernetes ConfigMaps**
+- **Kubernetes Secrets**
- **MongoDB**
- **Redis**
- **BitBucket**
diff --git a/cmdhelpers/retrieverconf/init/retriever_init.go b/cmdhelpers/retrieverconf/init/retriever_init.go
index bd97c137b82..1b1d8f66a34 100644
--- a/cmdhelpers/retrieverconf/init/retriever_init.go
+++ b/cmdhelpers/retrieverconf/init/retriever_init.go
@@ -6,6 +6,8 @@ import (
"time"
awsConf "github.com/aws/aws-sdk-go-v2/config"
+ "k8s.io/client-go/rest"
+
"github.com/thomaspoignant/go-feature-flag/cmdhelpers/retrieverconf"
"github.com/thomaspoignant/go-feature-flag/retriever"
azblobretriever "github.com/thomaspoignant/go-feature-flag/retriever/azblobstorageretriever"
@@ -20,7 +22,6 @@ import (
"github.com/thomaspoignant/go-feature-flag/retriever/postgresqlretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/redisretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/s3retrieverv2"
- "k8s.io/client-go/rest"
)
// retrieverFactory defines the signature for retriever factory functions
@@ -28,18 +29,19 @@ type retrieverFactory func(*retrieverconf.RetrieverConf, time.Duration) (retriev
// retrieverFactories maps retriever kinds to their factory functions
var retrieverFactories = map[retrieverconf.RetrieverKind]retrieverFactory{
- retrieverconf.GitHubRetriever: createGitHubRetriever,
- retrieverconf.GitlabRetriever: createGitlabRetriever,
- retrieverconf.BitbucketRetriever: createBitbucketRetriever,
- retrieverconf.FileRetriever: createFileRetriever,
- retrieverconf.S3Retriever: createS3Retriever,
- retrieverconf.HTTPRetriever: createHTTPRetriever,
- retrieverconf.GoogleStorageRetriever: createGoogleStorageRetriever,
- retrieverconf.KubernetesRetriever: createKubernetesRetriever,
- retrieverconf.MongoDBRetriever: createMongoDBRetriever,
- retrieverconf.RedisRetriever: createRedisRetriever,
- retrieverconf.AzBlobStorageRetriever: createAzBlobStorageRetriever,
- retrieverconf.PostgreSQLRetriever: createPostgreSQLRetriever,
+ retrieverconf.GitHubRetriever: createGitHubRetriever,
+ retrieverconf.GitlabRetriever: createGitlabRetriever,
+ retrieverconf.BitbucketRetriever: createBitbucketRetriever,
+ retrieverconf.FileRetriever: createFileRetriever,
+ retrieverconf.S3Retriever: createS3Retriever,
+ retrieverconf.HTTPRetriever: createHTTPRetriever,
+ retrieverconf.GoogleStorageRetriever: createGoogleStorageRetriever,
+ retrieverconf.KubernetesRetriever: createKubernetesRetriever,
+ retrieverconf.KubernetesSecretRetriever: createKubernetesSecretRetriever,
+ retrieverconf.MongoDBRetriever: createMongoDBRetriever,
+ retrieverconf.RedisRetriever: createRedisRetriever,
+ retrieverconf.AzBlobStorageRetriever: createAzBlobStorageRetriever,
+ retrieverconf.PostgreSQLRetriever: createPostgreSQLRetriever,
}
// InitRetriever initialize the retriever based on the configuration
@@ -151,6 +153,20 @@ func createKubernetesRetriever(
}, nil
}
+func createKubernetesSecretRetriever(
+ c *retrieverconf.RetrieverConf, _ time.Duration) (retriever.Retriever, error) {
+ client, err := rest.InClusterConfig()
+ if err != nil {
+ return nil, err
+ }
+ return &k8sretriever.SecretRetriever{
+ Namespace: c.Namespace,
+ SecretName: c.Secret,
+ SecretKey: c.Key,
+ ClientConfig: *client,
+ }, nil
+}
+
func createMongoDBRetriever(c *retrieverconf.RetrieverConf, _ time.Duration) (retriever.Retriever, error) {
return &mongodbretriever.Retriever{Database: c.Database, URI: c.URI, Collection: c.Collection}, nil
}
diff --git a/cmdhelpers/retrieverconf/retriever_conf.go b/cmdhelpers/retrieverconf/retriever_conf.go
index cb86661d108..0133824b8e1 100644
--- a/cmdhelpers/retrieverconf/retriever_conf.go
+++ b/cmdhelpers/retrieverconf/retriever_conf.go
@@ -36,6 +36,7 @@ type RetrieverConf struct {
Item string `mapstructure:"item" koanf:"item"`
Namespace string `mapstructure:"namespace" koanf:"namespace"`
ConfigMap string `mapstructure:"configmap" koanf:"configmap"`
+ Secret string `mapstructure:"secret" koanf:"secret"`
Key string `mapstructure:"key" koanf:"key"`
BaseURL string `mapstructure:"baseUrl" koanf:"baseurl"`
AuthToken string `mapstructure:"token" koanf:"token"`
@@ -199,26 +200,27 @@ func (c *RetrieverConf) validateAzBlobStorageRetriever() error {
type RetrieverKind string
const (
- HTTPRetriever RetrieverKind = "http"
- GitHubRetriever RetrieverKind = "github"
- GitlabRetriever RetrieverKind = "gitlab"
- S3Retriever RetrieverKind = "s3"
- FileRetriever RetrieverKind = "file"
- GoogleStorageRetriever RetrieverKind = "googleStorage"
- KubernetesRetriever RetrieverKind = "configmap"
- MongoDBRetriever RetrieverKind = "mongodb"
- RedisRetriever RetrieverKind = "redis"
- BitbucketRetriever RetrieverKind = "bitbucket"
- AzBlobStorageRetriever RetrieverKind = "azureBlobStorage"
- PostgreSQLRetriever RetrieverKind = "postgresql"
+ HTTPRetriever RetrieverKind = "http"
+ GitHubRetriever RetrieverKind = "github"
+ GitlabRetriever RetrieverKind = "gitlab"
+ S3Retriever RetrieverKind = "s3"
+ FileRetriever RetrieverKind = "file"
+ GoogleStorageRetriever RetrieverKind = "googleStorage"
+ KubernetesRetriever RetrieverKind = "configmap"
+ KubernetesSecretRetriever RetrieverKind = "secret"
+ MongoDBRetriever RetrieverKind = "mongodb"
+ RedisRetriever RetrieverKind = "redis"
+ BitbucketRetriever RetrieverKind = "bitbucket"
+ AzBlobStorageRetriever RetrieverKind = "azureBlobStorage"
+ PostgreSQLRetriever RetrieverKind = "postgresql"
)
// IsValid is checking if the value is part of the enum
func (r RetrieverKind) IsValid() error {
switch r {
case HTTPRetriever, GitHubRetriever, GitlabRetriever, S3Retriever, RedisRetriever,
- FileRetriever, GoogleStorageRetriever, KubernetesRetriever, MongoDBRetriever,
- BitbucketRetriever, AzBlobStorageRetriever, PostgreSQLRetriever:
+ FileRetriever, GoogleStorageRetriever, KubernetesRetriever, KubernetesSecretRetriever,
+ MongoDBRetriever, BitbucketRetriever, AzBlobStorageRetriever, PostgreSQLRetriever:
return nil
}
return fmt.Errorf("invalid retriever: kind \"%s\" is not supported", r)
diff --git a/examples/retriever_k8s_secret/Dockerfile b/examples/retriever_k8s_secret/Dockerfile
new file mode 100644
index 00000000000..326032c254f
--- /dev/null
+++ b/examples/retriever_k8s_secret/Dockerfile
@@ -0,0 +1,9 @@
+FROM golang:1.21
+
+ARG VERSION=127.0.0.1
+
+WORKDIR /go/src/app
+COPY . /go/src/app
+
+RUN go build -o /goff-test-k8s-secret /go/src/app/examples/retriever_k8s_secret/main.go
+CMD ["/goff-test-k8s-secret"]
diff --git a/examples/retriever_k8s_secret/README.md b/examples/retriever_k8s_secret/README.md
new file mode 100644
index 00000000000..9b89ebaabec
--- /dev/null
+++ b/examples/retriever_k8s_secret/README.md
@@ -0,0 +1,70 @@
+# Kubernetes config map example
+
+This example contains everything you need to use a **`configmap`** as the source for your flags.
+We will use minikube to test the solution, but it works the same in your cluster.
+
+As you can see the `main.go` file contains a basic HTTP server that expose an API that use your flags.
+For this example we are using a `InClusterConfig` because we will run the service inside kubernetes.
+
+## How to setup the example
+_All commands should be run in the root level of the repository._
+
+1. Load all dependencies
+
+```shell
+make vendor
+```
+
+2. Create a minikube environment in your machine:
+
+```shell
+minikube start --vm
+```
+
+3. Use the minikube docker cli in your shell
+
+```shell
+eval $(minikube docker-env)
+```
+
+4. Build the docker image of the service
+
+```shell
+docker build -f examples/retriever_configmap/Dockerfile -t goff-test-configmap .
+```
+
+5. Create a `configmap` based on your `go-feature-flag` config file
+
+```shell
+kubectl create configmap goff --from-file=examples/retriever_configmap/flags.goff.yaml
+```
+
+6. Deploy your service to your kubernetes instance
+
+```shell
+kubectl apply -f examples/retriever_configmap/k8s-manifests.yaml
+```
+
+7. Forward the port to the service
+
+```shell
+kubectl port-forward $(kubectl get pod | grep "goff-test-configmap" | cut -d ' ' -f1) 9090:8080
+```
+
+8. Access to the service and check the values for different users
+
+```shell
+curl http://localhost:9090/
+```
+
+9. Play with the values in the `go-feature-flag` config file
+
+```shell
+kubectl edit configmap goff
+```
+
+10. Delete your minikube instance
+
+```shell
+minikube delete
+```
diff --git a/examples/retriever_k8s_secret/flags.goff.yaml b/examples/retriever_k8s_secret/flags.goff.yaml
new file mode 100644
index 00000000000..b0783257a9d
--- /dev/null
+++ b/examples/retriever_k8s_secret/flags.goff.yaml
@@ -0,0 +1,22 @@
+new-admin-access:
+ variations:
+ default_var: false
+ false_var: false
+ true_var: true
+ defaultRule:
+ percentage:
+ false_var: 70
+ true_var: 30
+
+flag-only-for-admin:
+ variations:
+ default_var: false
+ false_var: false
+ true_var: true
+ targeting:
+ - query: admin eq true
+ percentage:
+ false_var: 0
+ true_var: 100
+ defaultRule:
+ variation: default_var
diff --git a/examples/retriever_k8s_secret/k8s-manifests.yaml b/examples/retriever_k8s_secret/k8s-manifests.yaml
new file mode 100644
index 00000000000..2c39687ac19
--- /dev/null
+++ b/examples/retriever_k8s_secret/k8s-manifests.yaml
@@ -0,0 +1,73 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: goff-sa
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: goff-sa
+ namespace: default
+rules:
+ - apiGroups: [""]
+ resources:
+ - secrets
+ - namespaces
+ verbs:
+ - get
+ - list
+ - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: goff-sa
+subjects:
+ - kind: ServiceAccount
+ name: goff-sa
+roleRef:
+ kind: ClusterRole
+ name: goff-sa
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations:
+ deployment.kubernetes.io/revision: "2"
+ labels:
+ app: goff-test-k8s-secret
+ name: goff-test-k8s-secret
+ namespace: default
+spec:
+ progressDeadlineSeconds: 600
+ replicas: 1
+ revisionHistoryLimit: 10
+ selector:
+ matchLabels:
+ app: goff-test-k8s-secret
+ strategy:
+ rollingUpdate:
+ maxSurge: 25%
+ maxUnavailable: 25%
+ type: RollingUpdate
+ template:
+ metadata:
+ labels:
+ app: goff-test-k8s-secret
+ spec:
+ containers:
+ - image: goff-test-k8s-secret:latest
+ imagePullPolicy: Never
+ name: goff-test-k8s-secret
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ terminationMessagePolicy: File
+ dnsPolicy: ClusterFirst
+ restartPolicy: Always
+ schedulerName: default-scheduler
+ securityContext: {}
+ terminationGracePeriodSeconds: 30
+ serviceAccountName: goff-sa
diff --git a/examples/retriever_k8s_secret/main.go b/examples/retriever_k8s_secret/main.go
new file mode 100644
index 00000000000..c54b78280a9
--- /dev/null
+++ b/examples/retriever_k8s_secret/main.go
@@ -0,0 +1,95 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "log/slog"
+ "net/http"
+ "time"
+
+ "github.com/thomaspoignant/go-feature-flag/ffcontext"
+
+ "k8s.io/client-go/rest"
+
+ "github.com/thomaspoignant/go-feature-flag/retriever/k8sretriever"
+
+ ffclient "github.com/thomaspoignant/go-feature-flag"
+)
+
+func main() {
+ // Init ffclient with a kubernetes secret retriever.
+ config, err := rest.InClusterConfig()
+ if err != nil {
+ panic(err.Error())
+ }
+
+ err = ffclient.Init(ffclient.Config{
+ PollingInterval: 10 * time.Second,
+ LeveledLogger: slog.Default(),
+ Context: context.Background(),
+ Retriever: &k8sretriever.SecretRetriever{
+ Namespace: "default",
+ SecretName: "goff",
+ SecretKey: "flags.goff.yaml",
+ ClientConfig: *config,
+ },
+ })
+
+ // Check init errors.
+ if err != nil {
+ log.Fatal(err)
+ }
+ // defer closing ffclient
+ defer ffclient.Close()
+
+ http.HandleFunc("/", handler)
+ http.ListenAndServe(":8080", nil)
+}
+
+func handler(w http.ResponseWriter, req *http.Request) {
+ user1 := ffcontext.
+ NewEvaluationContextBuilder("aea2fdc1-b9a0-417a-b707-0c9083de68e3").
+ AddCustom("anonymous", true).
+ Build()
+ user2 := ffcontext.NewEvaluationContext("332460b9-a8aa-4f7a-bc5d-9cc33632df9a")
+ user3 := ffcontext.NewEvaluationContextBuilder("785a14bf-d2c5-4caa-9c70-2bbc4e3732a5").
+ AddCustom("email", "user2@email.com").
+ AddCustom("firstname", "John").
+ AddCustom("lastname", "Doe").
+ AddCustom("admin", true).
+ Build()
+
+ // --- test flag with no rule
+ // user1
+ user1HasAccessToNewAdmin, err := ffclient.BoolVariation("new-admin-access", user1, false)
+ if err != nil {
+ // we log the error, but we still have a meaningful value in user1HasAccessToNewAdmin (the default value).
+ log.Printf("something went wrong when getting the flag: %v", err)
+ }
+ if user1HasAccessToNewAdmin {
+ fmt.Fprintf(w, "user1 has access to the new admin\n")
+ }
+
+ // user2
+ user2HasAccessToNewAdmin, err := ffclient.BoolVariation("new-admin-access", user2, false)
+ if err != nil {
+ // we log the error, but we still have a meaningful value in hasAccessToNewAdmin (the default value).
+ fmt.Fprintf(w, "something went wrong when getting the flag: %v\n", err)
+ }
+ if !user2HasAccessToNewAdmin {
+ fmt.Fprintf(w, "user2 has not access to the new admin\n")
+ }
+
+ // --- test flag with rule only for admins
+ // user 1 is not admin so should not access to the flag
+ user1HasAccess, _ := ffclient.BoolVariation("flag-only-for-admin", user1, false)
+ if !user1HasAccess {
+ fmt.Fprintf(w, "user1 is not admin so no access to the flag\n")
+ }
+
+ // user 3 is admin and the flag apply to this key.
+ if user3HasAccess, _ := ffclient.BoolVariation("flag-only-for-admin", user3, false); user3HasAccess {
+ fmt.Fprintf(w, "user 3 is admin and the flag apply to this key.\n")
+ }
+}
diff --git a/retriever/k8sretriever/secret_retriever.go b/retriever/k8sretriever/secret_retriever.go
new file mode 100644
index 00000000000..e46100ab3da
--- /dev/null
+++ b/retriever/k8sretriever/secret_retriever.go
@@ -0,0 +1,59 @@
+package k8sretriever
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ restclient "k8s.io/client-go/rest"
+)
+
+// SecretRetriever is a configuration struct for a Kubernetes Secret retriever.
+type SecretRetriever struct {
+ Namespace string
+ SecretName string
+ SecretKey string
+ ClientConfig restclient.Config
+ client kubernetes.Interface
+}
+
+// Retrieve is the function in charge of fetching the flag configuration.
+func (s *SecretRetriever) Retrieve(ctx context.Context) ([]byte, error) {
+ if s.client == nil {
+ client, clientErr := kubeClientProvider(&s.ClientConfig)
+ if clientErr != nil {
+ return nil, fmt.Errorf("unable to create client, error: %s", clientErr)
+ }
+ s.client = client
+ }
+
+ if s.client == nil {
+ return nil, fmt.Errorf("k8s client is nil after initialization")
+ }
+
+ secret, err := s.client.CoreV1().Secrets(s.Namespace).Get(ctx, s.SecretName, v1.GetOptions{})
+ if err != nil {
+ return nil, fmt.Errorf(
+ "unable to read from secret %s.%s, error: %s", s.SecretName, s.Namespace, err,
+ )
+ }
+
+ encodedContent, ok := secret.StringData[s.SecretKey]
+ if !ok {
+ return nil, fmt.Errorf(
+ "key %s not existing in secret %s.%s",
+ s.SecretKey,
+ s.SecretName,
+ s.Namespace,
+ )
+ }
+
+ decodedContent, err := base64.StdEncoding.DecodeString(encodedContent)
+ if err != nil {
+ return nil, fmt.Errorf("unable to decode secret %s.%s, error: %s", s.SecretName, s.Namespace, err)
+ }
+
+ return decodedContent, nil
+}
diff --git a/retriever/k8sretriever/secret_retriever_test.go b/retriever/k8sretriever/secret_retriever_test.go
new file mode 100644
index 00000000000..1bb26a37be8
--- /dev/null
+++ b/retriever/k8sretriever/secret_retriever_test.go
@@ -0,0 +1,192 @@
+package k8sretriever
+
+import (
+ "context"
+ "encoding/base64"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ api "k8s.io/api/core/v1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/kubernetes/fake"
+ restclient "k8s.io/client-go/rest"
+)
+
+var expectedDecodedContent = `
+test-flag:
+ variations:
+ true_var: true
+ false_var: false
+ targeting:
+ - query: key eq "random-key"
+ percentage:
+ true_var: 0
+ false_var: 100
+ defaultRule:
+ variation: false_var
+ trackEvents: false
+
+test-flag2:
+ variations:
+ true_var: true
+ false_var: false
+ targeting:
+ - query: key eq "not-a-key"
+ percentage:
+ true_var: 0
+ false_var: 100
+ defaultRule:
+ variation: false_var
+ trackEvents: false
+`
+
+var expectedEncodedContent = base64.StdEncoding.EncodeToString([]byte(expectedDecodedContent))
+
+func Test_kubernetesSecretRetriever_Retrieve(t *testing.T) {
+ originalKubeClientProvider := kubeClientProvider
+ defer func() {
+ kubeClientProvider = originalKubeClientProvider
+ }()
+
+ kubeClientProviderFactory := func(object ...runtime.Object) func(*restclient.Config) (kubernetes.Interface, error) {
+ return func(config *restclient.Config) (kubernetes.Interface, error) {
+ return fake.NewClientset(object...), nil
+ }
+ }
+
+ type fields struct {
+ object runtime.Object
+ namespace string
+ secretName string
+ secretKey string
+ context context.Context
+ setClient bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want string
+ wantErr error
+ }{
+ {
+ name: "Secret existing",
+ fields: fields{
+ object: &api.Secret{
+ ObjectMeta: v1.ObjectMeta{Name: "Secret1", Namespace: "Namespace"},
+ StringData: map[string]string{"FEATURE_FLAGS": expectedEncodedContent},
+ },
+ namespace: "Namespace",
+ secretName: "Secret1",
+ secretKey: "FEATURE_FLAGS",
+ },
+ want: expectedDecodedContent,
+ wantErr: nil,
+ },
+ {
+ name: "Key not existing",
+ fields: fields{
+ object: &api.Secret{
+ ObjectMeta: v1.ObjectMeta{Name: "Secret1", Namespace: "Namespace"},
+ StringData: map[string]string{"FEATURE_FLAGS": expectedEncodedContent},
+ },
+ namespace: "Namespace",
+ secretName: "Secret1",
+ secretKey: "INVALID",
+ },
+ wantErr: errors.New("key INVALID not existing in secret Secret1.Namespace"),
+ },
+ {
+ name: "Config Map not existing",
+ fields: fields{
+ object: &api.Secret{
+ ObjectMeta: v1.ObjectMeta{Name: "Secret1", Namespace: "Namespace"},
+ Data: map[string][]byte{"FEATURE_FLAGS": []byte(expectedEncodedContent)},
+ },
+ namespace: "WrongNamespace",
+ secretName: "NotExisting",
+ },
+ wantErr: errors.New(
+ "unable to read from secret NotExisting.WrongNamespace, error: secrets \"NotExisting\" not found",
+ ),
+ },
+ {
+ name: "Client already there",
+ fields: fields{
+ object: &api.Secret{
+ ObjectMeta: v1.ObjectMeta{Name: "Secret1", Namespace: "Namespace"},
+ StringData: map[string]string{"FEATURE_FLAGS": expectedEncodedContent},
+ },
+ namespace: "Namespace",
+ secretName: "Secret1",
+ secretKey: "FEATURE_FLAGS",
+ setClient: true,
+ },
+ wantErr: errors.New(
+ "unable to read from secret Secret1.Namespace, error: secrets \"Secret1\" not found",
+ ),
+ },
+ {
+ name: "Secret decoding fails",
+ fields: fields{
+ object: &api.Secret{
+ ObjectMeta: v1.ObjectMeta{Name: "Secret1", Namespace: "Namespace"},
+ StringData: map[string]string{"FEATURE_FLAGS": "_INVALID"},
+ },
+ namespace: "Namespace",
+ secretName: "Secret1",
+ secretKey: "FEATURE_FLAGS",
+ },
+ wantErr: errors.New(
+ "unable to decode secret Secret1.Namespace, error: illegal base64 data at input byte 0",
+ ),
+ },
+ {
+ name: "k8s client is nil",
+ fields: fields{
+ object: &api.Secret{
+ ObjectMeta: v1.ObjectMeta{Name: "Secret1", Namespace: "Namespace"},
+ StringData: map[string]string{"FEATURE_FLAGS": expectedEncodedContent},
+ },
+ namespace: "Namespace",
+ secretName: "Secret1",
+ secretKey: "FEATURE_FLAGS",
+ },
+ wantErr: errors.New("k8s client is nil after initialization"),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ kubeClientProvider = kubeClientProviderFactory(tt.fields.object)
+ if tt.name == "k8s client is nil" {
+ // mocking the kubeClientProvider function
+ kubeClientProvider = func(config *restclient.Config) (kubernetes.Interface, error) {
+ return nil, nil
+ }
+ }
+ s := SecretRetriever{
+ SecretName: tt.fields.secretName,
+ SecretKey: tt.fields.secretKey,
+ Namespace: tt.fields.namespace,
+ }
+ if tt.fields.setClient {
+ s.client = fake.NewClientset()
+ }
+ got, err := s.Retrieve(tt.fields.context)
+
+ assert.Equal(t, tt.wantErr, err, "retrieve() error = %v, wantErr %v", err, tt.wantErr)
+ if err == nil {
+ assert.Equal(
+ t,
+ tt.want,
+ string(got),
+ "retrieve() got = %v, want %v",
+ string(got),
+ tt.want,
+ )
+ }
+ })
+ }
+}
diff --git a/website/data/integrations.js b/website/data/integrations.js
index 85d5a0984f9..a8c397bb0c1 100644
--- a/website/data/integrations.js
+++ b/website/data/integrations.js
@@ -48,6 +48,15 @@ export const integrations = {
logo: k8slogo,
docLink: 'kubernetes-configmap',
},
+ {
+ name: 'Kubernetes Secret',
+ description: 'Loads the configuration from a Kubernetes Secret.',
+ longDescription: `Loads the configuration from a Kubernetes Secret. This retriever is useful when you are using Kubernetes and want to use Secrets to store your configuration files.`,
+ bgColor: 'cornflowerblue',
+ faLogo: 'devicon-kubernetes-plain',
+ logo: k8slogo,
+ docLink: 'kubernetes-secret',
+ },
{
name: 'AWS S3',
description: 'Retrieves the configuration from an AWS S3 bucket.',
diff --git a/website/docs/integrations/store-flags-configuration/kubernetes-secret.mdx b/website/docs/integrations/store-flags-configuration/kubernetes-secret.mdx
new file mode 100644
index 00000000000..08f0039e3a4
--- /dev/null
+++ b/website/docs/integrations/store-flags-configuration/kubernetes-secret.mdx
@@ -0,0 +1,77 @@
+---
+sidebar_position: 31
+description: How to configure a kubernetes secret retriever.
+---
+import { integrations } from "@site/data/integrations";
+import {Mandatory, NotMandatory} from "@site/src/components/checks/checks";
+export const retrieverName = 'Kubernetes Secret'
+export const info = integrations.retrievers.find((r) => r.name === retrieverName)
+
+# Kubernetes Secret
+
+## Overview
+{info.longDescription ?? info.description}
+
+The **Kubernetes Secret Retriever** will access flags in a Kubernetes Secret via the [Kubernetes Go Client](https://github.com/kubernetes/client-go).
+
+## Add a flag configuration file as Secret
+If you have a flag configuration file, you can create a Secret with the content of the file.
+The following command will create a Secret with the content of the `examples/retriever_k8s_secret/flags.goff.yaml` file:
+```shell
+kubectl create secret generic goff --from-file=examples/retriever_k8s_secret/flags.goff.yaml
+```
+## Configure the relay proxy
+
+To configure your relay proxy to use the {retrieverName} retriever, you need to add the following
+configuration to your relay proxy configuration file:
+
+:::note
+Relay proxy is only supporting the secrets while running inside the kubernetes cluster.
+:::
+
+```yaml title="goff-proxy.yaml"
+# ...
+retrievers:
+ - kind: secret
+ namespace: default
+ secret: goff
+ key: flags.goff.yaml
+# ...
+```
+
+| Field name | Mandatory | Type | Default | Description |
+|-------------|:-------------:|--------|----------|-------------------------------------------------------------------------------------------------------------|
+| `kind` | | string | **none** | Value should be **`secret`**.
_This field is mandatory and describes which retriever you are using._ |
+| `namespace` | | string | **none** | This is the name of the namespace where your **secret** is located _(ex: `default`)_. |
+| `secret` | | string | **none** | Name of the **secret** we should read _(ex: `feature-flag`)_. |
+| `key` | | string | **none** | Name of the `key` in the **secret** which contains the flag. |
+
+## Configure the GO Module
+To configure your GO module to use the {retrieverName} retriever, you need to add the following
+configuration to your `ffclient.Config{}` object:
+
+```go title="example.go"
+import (
+ restclient "k8s.io/client-go/rest"
+)
+// ...
+config, _ := restclient.InClusterConfig()
+err = ffclient.Init(ffclient.Config{
+ PollingInterval: 3 * time.Second,
+ Retriever: &k8sretriever.SecretRetriever{
+ Path: "flags.goff.yaml",
+ Namespace: "default"
+ SecretName: "secret"
+ SecretKey: "flags.yml"
+ ClientConfig: &config
+ },
+})
+defer ffclient.Close()
+```
+
+| Field | Mandatory | Description |
+|---------------------|:-------------:|----------------------------------------------------|
+| **`Namespace`** | | The namespace of the Secret. |
+| **`SecretName`** | | The name of the Secret. |
+| **`SecretKey`** | | The key within the Secret storing the flags. |
+| **`ClientConfig`** | | The configuration object for the Kubernetes client |
\ No newline at end of file