From a2af49c89d2e027594ded7e623ad7d4a479bc0a5 Mon Sep 17 00:00:00 2001 From: faizanahmad055 Date: Tue, 10 Jul 2018 20:08:45 +0500 Subject: [PATCH] Implement actions for configmap upgrade --- internal/pkg/actions/action.go | 160 +++++++++++++++++++++ internal/pkg/controller/controller.go | 15 +- internal/pkg/controller/controller_test.go | 91 ++++++++++++ 3 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 internal/pkg/actions/action.go create mode 100644 internal/pkg/controller/controller_test.go diff --git a/internal/pkg/actions/action.go b/internal/pkg/actions/action.go new file mode 100644 index 00000000..66b82211 --- /dev/null +++ b/internal/pkg/actions/action.go @@ -0,0 +1,160 @@ +package actions + +import ( + "bytes" + "sort" + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/api/core/v1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + updateOnChangeAnnotation = "reloader.stakater.com/update-on-change" +) + +// Action interface so that other actions like slack can implement this +type Action interface { + ObjectCreated(obj interface{}, client kubernetes.Interface) + ObjectDeleted(obj interface{}) + ObjectUpdated(oldObj interface{}, client kubernetes.Interface) +} + +// Default class with empty implementations for any action that we dont support currently +type Default struct { +} + +// ObjectCreated Do nothing for default handler +func (d *Default) ObjectCreated(obj interface{}, client kubernetes.Interface) { + message := "Configmap: `" + obj.(*v1.ConfigMap).Name + "`has been created in Namespace: `" + obj.(*v1.ConfigMap).Namespace + "`" + logrus.Infof(message) + err := rollingUpgradeDeployments(obj, client) + if err != nil { + logrus.Errorf("failed to update Deployment: %v", err) + } +} + +// ObjectDeleted Do nothing for default handler +func (d *Default) ObjectDeleted(obj interface{}) { + +} + +// ObjectUpdated Do nothing for default handler +func (d *Default) ObjectUpdated(oldObj interface{}, client kubernetes.Interface) { + message := "Configmap: `" + oldObj.(*v1.ConfigMap).Name + "`has been updated in Namespace: `" + oldObj.(*v1.ConfigMap).Namespace + "`" + logrus.Infof(message) + err := rollingUpgradeDeployments(oldObj, client) + if err != nil { + logrus.Errorf("failed to update Deployment: %v", err) + } +} + +// Implementation has been borrowed from fabric8io/configmapcontroller +// Method has been modified a little to use updated liberaries. +func rollingUpgradeDeployments(oldObj interface{}, client kubernetes.Interface) error { + ns := oldObj.(*v1.ConfigMap).Namespace + configMapName := oldObj.(*v1.ConfigMap).Name + configMapVersion := convertConfigMapToToken(oldObj.(*v1.ConfigMap)) + + deployments, err := client.Apps().Deployments(ns).List(meta_v1.ListOptions{}) + if err != nil { + return errors.Wrap(err, "failed to list deployments") + } + for _, d := range deployments.Items { + containers := d.Spec.Template.Spec.Containers + // match deployments with the correct annotation + annotationValue, _ := d.ObjectMeta.Annotations[updateOnChangeAnnotation] + if annotationValue != "" { + values := strings.Split(annotationValue, ",") + matches := false + for _, value := range values { + if value == configMapName { + matches = true + break + } + } + if matches { + updateContainers(containers, annotationValue, configMapVersion) + + // update the deployment + _, err := client.Apps().Deployments(ns).Update(&d) + if err != nil { + return errors.Wrap(err, "update deployment failed") + } + logrus.Infof("Updated Deployment %s", d.Name) + } + } + } + return nil +} + +func updateContainers(containers []v1.Container, annotationValue, configMapVersion string) bool { + // we can have multiple configmaps to update + answer := false + configmaps := strings.Split(annotationValue, ",") + for _, cmNameToUpdate := range configmaps { + configmapEnvar := "STAKATER_" + convertToEnvVarName(cmNameToUpdate) + "_CONFIGMAP" + + for i := range containers { + envs := containers[i].Env + matched := false + for j := range envs { + if envs[j].Name == configmapEnvar { + matched = true + if envs[j].Value != configMapVersion { + logrus.Infof("Updating %s to %s", configmapEnvar, configMapVersion) + envs[j].Value = configMapVersion + answer = true + } + } + } + // if no existing env var exists lets create one + if !matched { + e := v1.EnvVar{ + Name: configmapEnvar, + Value: configMapVersion, + } + containers[i].Env = append(containers[i].Env, e) + answer = true + } + } + } + return answer +} + +// convertToEnvVarName converts the given text into a usable env var +// removing any special chars with '_' +func convertToEnvVarName(text string) string { + var buffer bytes.Buffer + lower := strings.ToUpper(text) + lastCharValid := false + for i := 0; i < len(lower); i++ { + ch := lower[i] + if (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') { + buffer.WriteString(string(ch)) + lastCharValid = true + } else { + if lastCharValid { + buffer.WriteString("_") + } + lastCharValid = false + } + } + return buffer.String() +} + +// lets convert the configmap into a unique token based on the data values +func convertConfigMapToToken(cm *v1.ConfigMap) string { + values := []string{} + for k, v := range cm.Data { + values = append(values, k+"="+v) + } + sort.Strings(values) + text := strings.Join(values, ";") + // we could zip and base64 encode + // but for now we could leave this easy to read so that its easier to diagnose when & why things changed + return text +} diff --git a/internal/pkg/controller/controller.go b/internal/pkg/controller/controller.go index f1f2b687..272d0f37 100644 --- a/internal/pkg/controller/controller.go +++ b/internal/pkg/controller/controller.go @@ -13,10 +13,10 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" "github.com/sirupsen/logrus" + "github.com/stakater/Reloader/internal/pkg/actions" ) const ( - updateOnChangeAnnotation = "reloader.stakater.com.io/update-on-change" // AllNamespaces as our controller will be looking for events in all namespaces AllNamespaces = "temp-reloader" ) @@ -36,6 +36,7 @@ type Controller struct { queue workqueue.RateLimitingInterface informer cache.Controller resource string + Actions []actions.Action stopCh chan struct{} } @@ -153,20 +154,18 @@ func (c *Controller) takeAction(event Event) error { logrus.Infof("Error in Action") } else { logrus.Infof("Detected changes in object %s", obj) - /*logrus.Infof("Resource block not found, performing actions") // process events based on its type - for index, action := range c.Actions { - gllogrusog.Infof("Performing '%s' action for controller of type '%s'", c.controllerConfig.Actions[index].Name, c.controllerConfig.Type) + for _, action := range c.Actions { + logrus.Infof("Performing '%s' action for controller of type '%s'", event.eventType, c.resource) switch event.eventType { case "create": - action.ObjectCreated(obj) + action.ObjectCreated(obj, c.client) case "update": - //TODO: Figure how to pass old and new object - action.ObjectUpdated(obj, nil) + action.ObjectUpdated(obj, c.client) case "delete": action.ObjectDeleted(obj) } - }*/ + } } return nil } diff --git a/internal/pkg/controller/controller_test.go b/internal/pkg/controller/controller_test.go new file mode 100644 index 00000000..4dd54ded --- /dev/null +++ b/internal/pkg/controller/controller_test.go @@ -0,0 +1,91 @@ +package controller + +import ( + "time" + "math/rand" + + "github.com/stakater/Reloader/pkg/kube" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + client, _ = kube.GetClient() + configmapNamePrefix = "testconfigmap-reloader" + letters = []rune("abcdefghijklmnopqrstuvwxyz") +) + +func randSeq(n int) string { + rand.Seed(time.Now().UnixNano()) + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +// Creating a Controller for Updating Pod with Default Action without Resources so messages printed +/*func TestControllerForUpdatePodShouldUpdateDefaultAction(t *testing.T) { + controller, err := NewController(client, "configMaps", &v1.ConfigMap{}) + if err != nil { + logrus.Infof("Unable to create NewController error = %v", err) + return + } + stop := make(chan struct{}) + defer close(stop) + go controller.Run(1, stop) + time.Sleep(10 * time.Second) + namespace := "test" + configmapName := configmapNamePrefix + "-withoutresources-update-" + randSeq(5) + configmapClient := client.CoreV1().ConfigMaps(namespace) + configmap := initConfigmap(namespace, configmapName) + configmap, err = configmapClient.Create(configmap) + if err != nil { + panic(err) + } + logrus.Infof("Created Configmap %q.\n", configmap.GetObjectMeta().GetName()) + time.Sleep(10 * time.Second) + + logrus.Infof("Updating Configmap %q.\n", configmap.GetObjectMeta().GetName()) + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + configmap, err = configmapClient.Get(configmapName, metav1.GetOptions{}) + if err != nil { + + } + configmap = updateConfigmap(namespace, configmapName) + _, updateErr := configmapClient.Update(configmap) + return updateErr + }) + + + // TODO: Add functionality to verify reloader functionality here + + if retryErr != nil { + controller.client.CoreV1().ConfigMaps(namespace).Delete(configmapName, &metav1.DeleteOptions{}) + panic(retryErr) + } + time.Sleep(10 * time.Second) + logrus.Infof("Deleting Pod %q.\n", configmap.GetObjectMeta().GetName()) + controller.client.CoreV1().ConfigMaps(namespace).Delete(configmapName, &metav1.DeleteOptions{}) + time.Sleep(15 * time.Second) +}*/ + +func initConfigmap(namespace string, configmapName string) *v1.ConfigMap { + return &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configmapName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": "temp"}, + }, + } +} + +func updateConfigmap(namespace string, configmapName string) *v1.ConfigMap { + return &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configmapName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": "updated"}, + }, + } +}