Skip to content

Commit 3609574

Browse files
author
Thomas Schuetz
committed
feat: created first version of controller, create configmap only if it doesn't exist
Signed-off-by: Thomas Schuetz <thomas.schuetz@dynatrace.com>
1 parent 55c2822 commit 3609574

File tree

3 files changed

+211
-56
lines changed

3 files changed

+211
-56
lines changed

config/samples/deployment-2.yaml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: nginx-deployment-2
5+
spec:
6+
selector:
7+
matchLabels:
8+
app: nginx
9+
replicas: 5 # tells deployment to run 2 pods matching the template
10+
template:
11+
metadata:
12+
labels:
13+
app: nginx
14+
annotations:
15+
openfeature.dev: "enabled"
16+
openfeature.dev/featureflagconfiguration: "featureflagconfiguration-sample"
17+
spec:
18+
containers:
19+
- name: nginx
20+
image: nginx:1.14.2
21+
ports:
22+
- containerPort: 80

controllers/featureflagconfiguration_controller.go

+105-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ package controllers
1818

1919
import (
2020
"context"
21+
"github.com/go-logr/logr"
22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/errors"
24+
"k8s.io/client-go/tools/record"
25+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
26+
"time"
2127

2228
"k8s.io/apimachinery/pkg/runtime"
2329
ctrl "sigs.k8s.io/controller-runtime"
@@ -30,7 +36,13 @@ import (
3036
// FeatureFlagConfigurationReconciler reconciles a FeatureFlagConfiguration object
3137
type FeatureFlagConfigurationReconciler struct {
3238
client.Client
39+
40+
// Scheme contains the scheme of this controller
3341
Scheme *runtime.Scheme
42+
// Recorder contains the Recorder of this controller
43+
Recorder record.EventRecorder
44+
// ReqLogger contains the Logger of this controller
45+
Log logr.Logger
3446
}
3547

3648
//+kubebuilder:rbac:groups=core.openfeature.dev,resources=featureflagconfigurations,verbs=get;list;watch;create;update;patch;delete
@@ -46,17 +58,107 @@ type FeatureFlagConfigurationReconciler struct {
4658
//
4759
// For more details, check Reconcile and its Result here:
4860
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile
61+
62+
const crdName = "FeatureFlagConfiguration"
63+
const reconcileErrorInterval = 10 * time.Second
64+
const reconcileSuccessInterval = 120 * time.Second
65+
const finalizerName = "featureflagconfiguration.core.openfeature.dev/finalizer"
66+
4967
func (r *FeatureFlagConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
50-
_ = log.FromContext(ctx)
68+
r.Log = log.FromContext(ctx)
69+
r.Log.Info("Reconciling" + crdName)
70+
71+
ffconf := &configv1alpha1.FeatureFlagConfiguration{}
72+
if err := r.Client.Get(ctx, req.NamespacedName, ffconf); err != nil {
73+
if errors.IsNotFound(err) {
74+
// taking down all associated K8s resources is handled by K8s
75+
r.Log.Info(crdName + " resource not found. Ignoring since object must be deleted")
76+
return r.finishReconcile(nil, false)
77+
}
78+
r.Log.Error(err, "Failed to get the "+crdName)
79+
return r.finishReconcile(err, false)
80+
}
81+
82+
if ffconf.ObjectMeta.DeletionTimestamp.IsZero() {
83+
// The object is not being deleted, so if it does not have our finalizer,
84+
// then lets add the finalizer and update the object. This is equivalent
85+
// registering our finalizer.
86+
if !ContainsString(ffconf.GetFinalizers(), finalizerName) {
87+
controllerutil.AddFinalizer(ffconf, finalizerName)
88+
if err := r.Update(ctx, ffconf); err != nil {
89+
return r.finishReconcile(err, false)
90+
}
91+
}
92+
} else {
93+
// The object is being deleted
94+
if ContainsString(ffconf.GetFinalizers(), finalizerName) {
95+
controllerutil.RemoveFinalizer(ffconf, finalizerName)
96+
if err := r.Update(ctx, ffconf); err != nil {
97+
return ctrl.Result{}, err
98+
}
99+
}
100+
// Stop reconciliation as the item is being deleted
101+
return r.finishReconcile(nil, false)
102+
}
51103

52-
// TODO(user): your logic here
104+
// Get list of configmaps
105+
configMapList := &corev1.ConfigMapList{}
106+
var ffConfigMapList []corev1.ConfigMap
107+
if err := r.List(ctx, configMapList); err != nil {
108+
return r.finishReconcile(err, false)
109+
}
53110

54-
return ctrl.Result{}, nil
111+
// Get list of configmaps with annotation
112+
for _, cm := range configMapList.Items {
113+
val, ok := cm.GetAnnotations()["openfeature.dev/featureflagconfiguration"]
114+
if ok && val == ffconf.Name {
115+
ffConfigMapList = append(ffConfigMapList, cm)
116+
}
117+
}
118+
119+
// Update ConfigMaps
120+
for _, cm := range ffConfigMapList {
121+
cm.Data = map[string]string{
122+
"config.yaml": ffconf.Spec.FeatureFlagSpec,
123+
}
124+
err := r.Client.Update(ctx, &cm)
125+
if err != nil {
126+
return r.finishReconcile(err, true)
127+
}
128+
}
129+
return r.finishReconcile(nil, false)
55130
}
56131

57132
// SetupWithManager sets up the controller with the Manager.
58133
func (r *FeatureFlagConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error {
59134
return ctrl.NewControllerManagedBy(mgr).
60135
For(&configv1alpha1.FeatureFlagConfiguration{}).
136+
Owns(&corev1.ConfigMap{}).
61137
Complete(r)
62138
}
139+
140+
func ContainsString(slice []string, s string) bool {
141+
for _, item := range slice {
142+
if item == s {
143+
return true
144+
}
145+
}
146+
return false
147+
}
148+
149+
func (r *FeatureFlagConfigurationReconciler) finishReconcile(err error, requeueImmediate bool) (ctrl.Result, error) {
150+
if err != nil {
151+
interval := reconcileErrorInterval
152+
if requeueImmediate {
153+
interval = 0
154+
}
155+
r.Log.Error(err, "Finished Reconciling "+crdName+" with error: %w")
156+
return ctrl.Result{Requeue: true, RequeueAfter: interval}, err
157+
}
158+
interval := reconcileSuccessInterval
159+
if requeueImmediate {
160+
interval = 0
161+
}
162+
r.Log.Info("Finished Reconciling " + crdName)
163+
return ctrl.Result{Requeue: true, RequeueAfter: interval}, nil
164+
}

webhooks/mutating_admission_webhook.go

+84-53
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"net/http"
8-
97
"github.com/go-logr/logr"
10-
corev1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1"
8+
configv1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1"
119
corev1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/api/errors"
1211
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"net/http"
1313
"sigs.k8s.io/controller-runtime/pkg/client"
1414
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
1515
)
@@ -42,66 +42,107 @@ func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admissio
4242
return admission.Allowed("openfeature is disabled")
4343
}
4444
}
45-
// Check if the pod is static or orphaned
46-
name := pod.Name
47-
if len(pod.GetOwnerReferences()) != 0 {
48-
name = pod.GetOwnerReferences()[0].Name
49-
} else {
50-
return admission.Denied("static or orphaned pods cannot be mutated")
51-
}
5245

53-
var featureFlagCustomResource corev1alpha1.FeatureFlagConfiguration
54-
// Check CustomResource
46+
// Check configuration
5547
val, ok = pod.GetAnnotations()["openfeature.dev/featureflagconfiguration"]
5648
if !ok {
5749
return admission.Allowed("FeatureFlagConfiguration not found")
58-
} else {
59-
// Current limitation is to use the same namespace, this is easy to fix though
60-
// e.g. namespace/name check
61-
err = m.Client.Get(context.TODO(), client.ObjectKey{Name: val,
62-
Namespace: req.Namespace},
63-
&featureFlagCustomResource)
50+
}
51+
52+
// Check if the pod is static or orphaned
53+
if len(pod.GetOwnerReferences()) == 0 {
54+
return admission.Denied("static or orphaned pods cannot be mutated")
55+
}
56+
57+
// Check for ConfigMap and create it if it doesn't exist
58+
cm := corev1.ConfigMap{}
59+
if err := m.Client.Get(ctx, client.ObjectKey{Name: val, Namespace: req.Namespace}, &cm); errors.IsNotFound(err) {
60+
err := m.CreateConfigMap(ctx, val, req.Namespace, pod)
6461
if err != nil {
65-
return admission.Denied("FeatureFlagConfiguration not found")
62+
m.Log.V(1).Info(fmt.Sprintf("failed to create config map %s error: %s", val, err.Error()))
63+
return admission.Errored(http.StatusInternalServerError, err)
6664
}
6765
}
68-
// TODO: this should be a short sha to avoid collisions
69-
configName := name
70-
// Create the agent configmap
71-
m.Client.Delete(context.TODO(), &corev1.ConfigMap{
72-
ObjectMeta: metav1.ObjectMeta{
73-
Name: configName,
74-
Namespace: req.Namespace,
75-
},
76-
}) // Delete the configmap if it exists
7766

78-
m.Log.V(1).Info(fmt.Sprintf("Creating configmap %s", configName))
79-
if err := m.Client.Create(ctx, &corev1.ConfigMap{
67+
if !CheckOwnerReference(pod, cm) {
68+
reference := pod.OwnerReferences[0]
69+
reference.Controller = m.falseVal()
70+
cm.OwnerReferences = append(cm.OwnerReferences, reference)
71+
err := m.Client.Update(ctx, &cm)
72+
if err != nil {
73+
m.Log.V(1).Info(fmt.Sprintf("failed to update owner reference for %s error: %s", val, err.Error()))
74+
}
75+
}
76+
77+
marshaledPod, err := m.InjectSidecar(pod, val)
78+
if err != nil {
79+
return admission.Errored(http.StatusInternalServerError, err)
80+
}
81+
82+
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
83+
}
84+
85+
// PodMutator implements admission.DecoderInjector.
86+
// A decoder will be automatically injected.
87+
88+
// InjectDecoder injects the decoder.
89+
func (m *PodMutator) InjectDecoder(d *admission.Decoder) error {
90+
m.decoder = d
91+
return nil
92+
}
93+
94+
func CheckOwnerReference(pod *corev1.Pod, cm corev1.ConfigMap) bool {
95+
for _, cmOwner := range cm.OwnerReferences {
96+
for _, podOwner := range pod.OwnerReferences {
97+
if cmOwner == podOwner {
98+
return true
99+
}
100+
}
101+
}
102+
return false
103+
}
104+
105+
func (m *PodMutator) CreateConfigMap(ctx context.Context, name string, namespace string, pod *corev1.Pod) error {
106+
m.Log.V(1).Info(fmt.Sprintf("Creating configmap %s", name))
107+
reference := pod.OwnerReferences[0]
108+
reference.Controller = m.falseVal()
109+
110+
spec := m.GetFeatureFlagSpec(ctx, name, namespace)
111+
cm := corev1.ConfigMap{
80112
ObjectMeta: metav1.ObjectMeta{
81-
Name: configName,
82-
Namespace: req.Namespace,
113+
Name: name,
114+
Namespace: namespace,
83115
Annotations: map[string]string{
84-
"openfeature.dev/featureflagconfiguration": featureFlagCustomResource.Name,
116+
"openfeature.dev/featureflagconfiguration": name,
117+
},
118+
OwnerReferences: []metav1.OwnerReference{
119+
reference,
85120
},
86121
},
87-
//TODO
88122
Data: map[string]string{
89-
"config.yaml": featureFlagCustomResource.Spec.FeatureFlagSpec,
123+
"config.yaml": spec.FeatureFlagSpec,
90124
},
91-
}); err != nil {
125+
}
126+
return m.Client.Create(ctx, &cm)
127+
}
92128

93-
m.Log.V(1).Info(fmt.Sprintf("failed to create config map %s error: %s", configName, err.Error()))
94-
return admission.Errored(http.StatusInternalServerError, err)
129+
func (m *PodMutator) GetFeatureFlagSpec(ctx context.Context, name string, namespace string) configv1alpha1.FeatureFlagConfigurationSpec {
130+
ffConfig := configv1alpha1.FeatureFlagConfiguration{}
131+
if err := m.Client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &ffConfig); errors.IsNotFound(err) {
132+
return configv1alpha1.FeatureFlagConfigurationSpec{}
95133
}
134+
return ffConfig.Spec
135+
}
96136

137+
func (m *PodMutator) InjectSidecar(pod *corev1.Pod, configMap string) ([]byte, error) {
97138
m.Log.V(1).Info(fmt.Sprintf("Creating sidecar for pod %s/%s", pod.Namespace, pod.Name))
98139
// Inject the agent
99140
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
100141
Name: "flagd-config",
101142
VolumeSource: corev1.VolumeSource{
102143
ConfigMap: &corev1.ConfigMapVolumeSource{
103144
LocalObjectReference: corev1.LocalObjectReference{
104-
Name: configName,
145+
Name: configMap,
105146
},
106147
},
107148
},
@@ -119,20 +160,10 @@ func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admissio
119160
},
120161
},
121162
})
122-
123-
marshaledPod, err := json.Marshal(pod)
124-
if err != nil {
125-
return admission.Errored(http.StatusInternalServerError, err)
126-
}
127-
128-
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
163+
return json.Marshal(pod)
129164
}
130165

131-
// PodMutator implements admission.DecoderInjector.
132-
// A decoder will be automatically injected.
133-
134-
// InjectDecoder injects the decoder.
135-
func (m *PodMutator) InjectDecoder(d *admission.Decoder) error {
136-
m.decoder = d
137-
return nil
166+
func (m *PodMutator) falseVal() *bool {
167+
b := false
168+
return &b
138169
}

0 commit comments

Comments
 (0)