Skip to content

Commit

Permalink
Merge pull request #18 from thschue/main
Browse files Browse the repository at this point in the history
feat: change configmap creation behavior
  • Loading branch information
thschue authored May 30, 2022
2 parents 55c2822 + 3609574 commit 81c689b
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 56 deletions.
22 changes: 22 additions & 0 deletions config/samples/deployment-2.yaml
Original file line number Diff line number Diff line change
@@ -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
108 changes: 105 additions & 3 deletions controllers/featureflagconfiguration_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -46,17 +58,107 @@ type FeatureFlagConfigurationReconciler struct {
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/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
}
137 changes: 84 additions & 53 deletions webhooks/mutating_admission_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -42,66 +42,107 @@ 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{
Name: "flagd-config",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: configName,
Name: configMap,
},
},
},
Expand All @@ -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
}

0 comments on commit 81c689b

Please sign in to comment.