From 3609574a8e05daf89f0989030ecce35f55f3f080 Mon Sep 17 00:00:00 2001 From: Thomas Schuetz Date: Mon, 30 May 2022 06:48:58 +0200 Subject: [PATCH] feat: created first version of controller, create configmap only if it doesn't exist Signed-off-by: Thomas Schuetz --- config/samples/deployment-2.yaml | 22 +++ .../featureflagconfiguration_controller.go | 108 +++++++++++++- webhooks/mutating_admission_webhook.go | 137 +++++++++++------- 3 files changed, 211 insertions(+), 56 deletions(-) create mode 100644 config/samples/deployment-2.yaml diff --git a/config/samples/deployment-2.yaml b/config/samples/deployment-2.yaml new file mode 100644 index 000000000..fa9f4a811 --- /dev/null +++ b/config/samples/deployment-2.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-2 +spec: + selector: + matchLabels: + app: nginx + replicas: 5 # tells deployment to run 2 pods matching the template + template: + metadata: + labels: + app: nginx + annotations: + openfeature.dev: "enabled" + openfeature.dev/featureflagconfiguration: "featureflagconfiguration-sample" + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/controllers/featureflagconfiguration_controller.go b/controllers/featureflagconfiguration_controller.go index 60da125e2..6f0ef0f5e 100644 --- a/controllers/featureflagconfiguration_controller.go +++ b/controllers/featureflagconfiguration_controller.go @@ -18,6 +18,12 @@ package controllers import ( "context" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "time" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -30,7 +36,13 @@ import ( // FeatureFlagConfigurationReconciler reconciles a FeatureFlagConfiguration object type FeatureFlagConfigurationReconciler struct { client.Client + + // Scheme contains the scheme of this controller Scheme *runtime.Scheme + // Recorder contains the Recorder of this controller + Recorder record.EventRecorder + // ReqLogger contains the Logger of this controller + Log logr.Logger } //+kubebuilder:rbac:groups=core.openfeature.dev,resources=featureflagconfigurations,verbs=get;list;watch;create;update;patch;delete @@ -46,17 +58,107 @@ type FeatureFlagConfigurationReconciler struct { // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile + +const crdName = "FeatureFlagConfiguration" +const reconcileErrorInterval = 10 * time.Second +const reconcileSuccessInterval = 120 * time.Second +const finalizerName = "featureflagconfiguration.core.openfeature.dev/finalizer" + func (r *FeatureFlagConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + r.Log = log.FromContext(ctx) + r.Log.Info("Reconciling" + crdName) + + ffconf := &configv1alpha1.FeatureFlagConfiguration{} + if err := r.Client.Get(ctx, req.NamespacedName, ffconf); err != nil { + if errors.IsNotFound(err) { + // taking down all associated K8s resources is handled by K8s + r.Log.Info(crdName + " resource not found. Ignoring since object must be deleted") + return r.finishReconcile(nil, false) + } + r.Log.Error(err, "Failed to get the "+crdName) + return r.finishReconcile(err, false) + } + + if ffconf.ObjectMeta.DeletionTimestamp.IsZero() { + // The object is not being deleted, so if it does not have our finalizer, + // then lets add the finalizer and update the object. This is equivalent + // registering our finalizer. + if !ContainsString(ffconf.GetFinalizers(), finalizerName) { + controllerutil.AddFinalizer(ffconf, finalizerName) + if err := r.Update(ctx, ffconf); err != nil { + return r.finishReconcile(err, false) + } + } + } else { + // The object is being deleted + if ContainsString(ffconf.GetFinalizers(), finalizerName) { + controllerutil.RemoveFinalizer(ffconf, finalizerName) + if err := r.Update(ctx, ffconf); err != nil { + return ctrl.Result{}, err + } + } + // Stop reconciliation as the item is being deleted + return r.finishReconcile(nil, false) + } - // TODO(user): your logic here + // Get list of configmaps + configMapList := &corev1.ConfigMapList{} + var ffConfigMapList []corev1.ConfigMap + if err := r.List(ctx, configMapList); err != nil { + return r.finishReconcile(err, false) + } - return ctrl.Result{}, nil + // Get list of configmaps with annotation + for _, cm := range configMapList.Items { + val, ok := cm.GetAnnotations()["openfeature.dev/featureflagconfiguration"] + if ok && val == ffconf.Name { + ffConfigMapList = append(ffConfigMapList, cm) + } + } + + // Update ConfigMaps + for _, cm := range ffConfigMapList { + cm.Data = map[string]string{ + "config.yaml": ffconf.Spec.FeatureFlagSpec, + } + err := r.Client.Update(ctx, &cm) + if err != nil { + return r.finishReconcile(err, true) + } + } + return r.finishReconcile(nil, false) } // SetupWithManager sets up the controller with the Manager. func (r *FeatureFlagConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&configv1alpha1.FeatureFlagConfiguration{}). + Owns(&corev1.ConfigMap{}). Complete(r) } + +func ContainsString(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +func (r *FeatureFlagConfigurationReconciler) finishReconcile(err error, requeueImmediate bool) (ctrl.Result, error) { + if err != nil { + interval := reconcileErrorInterval + if requeueImmediate { + interval = 0 + } + r.Log.Error(err, "Finished Reconciling "+crdName+" with error: %w") + return ctrl.Result{Requeue: true, RequeueAfter: interval}, err + } + interval := reconcileSuccessInterval + if requeueImmediate { + interval = 0 + } + r.Log.Info("Finished Reconciling " + crdName) + return ctrl.Result{Requeue: true, RequeueAfter: interval}, nil +} diff --git a/webhooks/mutating_admission_webhook.go b/webhooks/mutating_admission_webhook.go index 0a57c37de..d737c7896 100644 --- a/webhooks/mutating_admission_webhook.go +++ b/webhooks/mutating_admission_webhook.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" "fmt" - "net/http" - "github.com/go-logr/logr" - corev1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" + configv1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "net/http" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -42,58 +42,99 @@ func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admissio return admission.Allowed("openfeature is disabled") } } - // Check if the pod is static or orphaned - name := pod.Name - if len(pod.GetOwnerReferences()) != 0 { - name = pod.GetOwnerReferences()[0].Name - } else { - return admission.Denied("static or orphaned pods cannot be mutated") - } - var featureFlagCustomResource corev1alpha1.FeatureFlagConfiguration - // Check CustomResource + // Check configuration val, ok = pod.GetAnnotations()["openfeature.dev/featureflagconfiguration"] if !ok { return admission.Allowed("FeatureFlagConfiguration not found") - } else { - // Current limitation is to use the same namespace, this is easy to fix though - // e.g. namespace/name check - err = m.Client.Get(context.TODO(), client.ObjectKey{Name: val, - Namespace: req.Namespace}, - &featureFlagCustomResource) + } + + // Check if the pod is static or orphaned + if len(pod.GetOwnerReferences()) == 0 { + return admission.Denied("static or orphaned pods cannot be mutated") + } + + // Check for ConfigMap and create it if it doesn't exist + cm := corev1.ConfigMap{} + if err := m.Client.Get(ctx, client.ObjectKey{Name: val, Namespace: req.Namespace}, &cm); errors.IsNotFound(err) { + err := m.CreateConfigMap(ctx, val, req.Namespace, pod) if err != nil { - return admission.Denied("FeatureFlagConfiguration not found") + m.Log.V(1).Info(fmt.Sprintf("failed to create config map %s error: %s", val, err.Error())) + return admission.Errored(http.StatusInternalServerError, err) } } - // TODO: this should be a short sha to avoid collisions - configName := name - // Create the agent configmap - m.Client.Delete(context.TODO(), &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: configName, - Namespace: req.Namespace, - }, - }) // Delete the configmap if it exists - m.Log.V(1).Info(fmt.Sprintf("Creating configmap %s", configName)) - if err := m.Client.Create(ctx, &corev1.ConfigMap{ + if !CheckOwnerReference(pod, cm) { + reference := pod.OwnerReferences[0] + reference.Controller = m.falseVal() + cm.OwnerReferences = append(cm.OwnerReferences, reference) + err := m.Client.Update(ctx, &cm) + if err != nil { + m.Log.V(1).Info(fmt.Sprintf("failed to update owner reference for %s error: %s", val, err.Error())) + } + } + + marshaledPod, err := m.InjectSidecar(pod, val) + if err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + + return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) +} + +// PodMutator implements admission.DecoderInjector. +// A decoder will be automatically injected. + +// InjectDecoder injects the decoder. +func (m *PodMutator) InjectDecoder(d *admission.Decoder) error { + m.decoder = d + return nil +} + +func CheckOwnerReference(pod *corev1.Pod, cm corev1.ConfigMap) bool { + for _, cmOwner := range cm.OwnerReferences { + for _, podOwner := range pod.OwnerReferences { + if cmOwner == podOwner { + return true + } + } + } + return false +} + +func (m *PodMutator) CreateConfigMap(ctx context.Context, name string, namespace string, pod *corev1.Pod) error { + m.Log.V(1).Info(fmt.Sprintf("Creating configmap %s", name)) + reference := pod.OwnerReferences[0] + reference.Controller = m.falseVal() + + spec := m.GetFeatureFlagSpec(ctx, name, namespace) + cm := corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: configName, - Namespace: req.Namespace, + Name: name, + Namespace: namespace, Annotations: map[string]string{ - "openfeature.dev/featureflagconfiguration": featureFlagCustomResource.Name, + "openfeature.dev/featureflagconfiguration": name, + }, + OwnerReferences: []metav1.OwnerReference{ + reference, }, }, - //TODO Data: map[string]string{ - "config.yaml": featureFlagCustomResource.Spec.FeatureFlagSpec, + "config.yaml": spec.FeatureFlagSpec, }, - }); err != nil { + } + return m.Client.Create(ctx, &cm) +} - m.Log.V(1).Info(fmt.Sprintf("failed to create config map %s error: %s", configName, err.Error())) - return admission.Errored(http.StatusInternalServerError, err) +func (m *PodMutator) GetFeatureFlagSpec(ctx context.Context, name string, namespace string) configv1alpha1.FeatureFlagConfigurationSpec { + ffConfig := configv1alpha1.FeatureFlagConfiguration{} + if err := m.Client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &ffConfig); errors.IsNotFound(err) { + return configv1alpha1.FeatureFlagConfigurationSpec{} } + return ffConfig.Spec +} +func (m *PodMutator) InjectSidecar(pod *corev1.Pod, configMap string) ([]byte, error) { m.Log.V(1).Info(fmt.Sprintf("Creating sidecar for pod %s/%s", pod.Namespace, pod.Name)) // Inject the agent pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ @@ -101,7 +142,7 @@ func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admissio VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: configName, + Name: configMap, }, }, }, @@ -119,20 +160,10 @@ func (m *PodMutator) Handle(ctx context.Context, req admission.Request) admissio }, }, }) - - marshaledPod, err := json.Marshal(pod) - if err != nil { - return admission.Errored(http.StatusInternalServerError, err) - } - - return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) + return json.Marshal(pod) } -// PodMutator implements admission.DecoderInjector. -// A decoder will be automatically injected. - -// InjectDecoder injects the decoder. -func (m *PodMutator) InjectDecoder(d *admission.Decoder) error { - m.decoder = d - return nil +func (m *PodMutator) falseVal() *bool { + b := false + return &b }