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