Skip to content

Commit

Permalink
Kubernetes configmaps (#247)
Browse files Browse the repository at this point in the history
  • Loading branch information
mschoenlaub authored Jun 9, 2022
1 parent 1044928 commit 0a9378d
Show file tree
Hide file tree
Showing 12 changed files with 622 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Available retriever are:
- [From a S3 Bucket](https://thomaspoignant.github.io/go-feature-flag/latest/flag_file/s3/)
- [From a file](https://thomaspoignant.github.io/go-feature-flag/latest/flag_file/file/)
- [From Google Cloud Storage](https://thomaspoignant.github.io/go-feature-flag/latest/flag_file/google_cloud_storage/)
- [From Kubernetes ConfigMaps](https://thomaspoignant.github.io/go-feature-flag/latest/flag_file/kubernetes_configmaps/)

## Flags file format
`go-feature-flag` core feature is to centralize all your feature flags in a source file, and to avoid hosting and maintaining a backend server to manage them.
Expand Down
33 changes: 33 additions & 0 deletions docs/flag_file/kubernetes_configmaps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Kubernetes configmaps
The [**KubernetesRetriever**](https://pkg.go.dev/github.com/thomaspoignant/go-feature-flag#KubernetesRetriever)
will access flags in a Kubernetes ConfigMap via the [Kubernetes Go client](https://github.com/kubernetes/client-go)

## Example
```go linenums="1"
import (
restclient "k8s.io/client-go/rest"
)

config, _ := restclient.InClusterConfig()
err = ffclient.Init(ffclient.Config{
PollingInterval: 3 * time.Second,
Retriever: &ffclient.KubernetesRetriever{
Path: "file-example.yaml",
Namespace: "default"
ConfigMapName: "my-configmap"
Key: "somekey.yml"
ClientConfig: &config
},
})
defer ffclient.Close()
```

## Configuration fields
To configure your retriever:

| Field | Description |
|---------------------|----------------------------------------------------|
| **`Namespace`** | The namespace of the ConfigMap. |
| **`ConfigMapName`** | The name of the ConfigMap. |
| **`Key`** | The key within the ConfigMap storing the flags. |
| **`ClientConfig`** | The configuration object for the Kubernetes client |
12 changes: 12 additions & 0 deletions examples/retriever_configmap/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang:1.18 AS build

ARG VERSION=127.0.0.1

WORKDIR /go/src/app
COPY . /go/src/app

RUN go build -o /go/src/app/examples/retriever_configmap/goff-test-configmap /go/src/app/examples/retriever_configmap/main.go

FROM gcr.io/distroless/base-debian11:latest
COPY --from=build /go/src/app/examples/retriever_configmap/goff-test-configmap /
CMD ["/goff-test-configmap"]
70 changes: 70 additions & 0 deletions examples/retriever_configmap/README.md
Original file line number Diff line number Diff line change
@@ -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.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
```
12 changes: 12 additions & 0 deletions examples/retriever_configmap/flags.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
new-admin-access:
percentage: 30
true: true
false: false
default: false

flag-only-for-admin:
rule: admin eq true
percentage: 100
true: true
false: false
default: false
73 changes: 73 additions & 0 deletions examples/retriever_configmap/k8s-manifests.yaml
Original file line number Diff line number Diff line change
@@ -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:
- configmaps
- 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-configmap
name: goff-test-configmap
namespace: default
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: goff-test-configmap
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: goff-test-configmap
spec:
containers:
- image: goff-test-configmap:latest
imagePullPolicy: Never
name: goff-test-configmap
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
serviceAccountName: goff-sa
88 changes: 88 additions & 0 deletions examples/retriever_configmap/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"context"
"fmt"
"github.com/thomaspoignant/go-feature-flag/ffuser"
"k8s.io/client-go/rest"
"log"
"net/http"
"os"
"time"

ffclient "github.com/thomaspoignant/go-feature-flag"
)

func main() {
// Init ffclient with an http retriever.
config, err := rest.InClusterConfig()
if err != nil {
panic(err.Error())
}

err = ffclient.Init(ffclient.Config{
PollingInterval: 10 * time.Second,
Logger: log.New(os.Stdout, "", 0),
Context: context.Background(),
Retriever: &ffclient.KubernetesRetriever{
Namespace: "default",
ConfigMapName: "goff",
Key: "flags.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 := ffuser.NewAnonymousUser("aea2fdc1-b9a0-417a-b707-0c9083de68e3")
user2 := ffuser.NewUser("332460b9-a8aa-4f7a-bc5d-9cc33632df9a")
user3 := ffuser.NewUserBuilder("785a14bf-d2c5-4caa-9c70-2bbc4e3732a5").
AddCustom("email", "[email protected]").
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")
}
}
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ require (
github.com/stretchr/testify v1.7.1
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
google.golang.org/api v0.81.0
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.24.1
k8s.io/apimachinery v0.24.1
k8s.io/client-go v0.24.1
)
Loading

0 comments on commit 0a9378d

Please sign in to comment.