From 5b467b731c5d51c01d4cc3402a64b87dadcdebe6 Mon Sep 17 00:00:00 2001 From: faizanahmad055 Date: Tue, 24 Jul 2018 17:45:48 +0500 Subject: [PATCH] Revamp perform rolling method --- README.md | 4 +- .../chart/reloader/templates/rbac.yaml | 1 - internal/pkg/common/common.go | 13 - internal/pkg/constants/annotations.go | 8 + internal/pkg/constants/constants.go | 10 + internal/pkg/controller/controller.go | 5 +- internal/pkg/controller/controller_test.go | 33 +- internal/pkg/handler/update.go | 311 ++++++++++-------- internal/pkg/handler/update_test.go | 102 +++++- internal/pkg/testutil/kube.go | 19 +- internal/pkg/util/interface.go | 38 +++ 11 files changed, 356 insertions(+), 188 deletions(-) create mode 100644 internal/pkg/constants/annotations.go create mode 100644 internal/pkg/constants/constants.go create mode 100644 internal/pkg/util/interface.go diff --git a/README.md b/README.md index 725a6ba1..bb34881c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ For a `Deployment` called `foo` have a `ConfigMap` called `foo`. Then add this a ```yaml metadata: annotations: - reloader.stakater.com/configmap.update-on-change: "foo" + configmap.reloader.stakater.com/reload: "foo" ``` OR @@ -31,7 +31,7 @@ For a `Deployment` called `foo` have a `Secret` called `foo`. Then add this anno ```yaml metadata: annotations: - reloader.stakater.com/secret.update-on-change: "foo" + secret.reloader.stakater.com/reload: "foo" ``` Then, providing `Reloader` is running, whenever you edit the `ConfigMap` or `Secret` called `foo` the Reloader will update the `Deployment` by adding the environment variable: diff --git a/deployments/kubernetes/chart/reloader/templates/rbac.yaml b/deployments/kubernetes/chart/reloader/templates/rbac.yaml index 88c2cbb5..cc0b0d3e 100644 --- a/deployments/kubernetes/chart/reloader/templates/rbac.yaml +++ b/deployments/kubernetes/chart/reloader/templates/rbac.yaml @@ -37,7 +37,6 @@ rules: - get - update - patch - - watch --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: RoleBinding diff --git a/internal/pkg/common/common.go b/internal/pkg/common/common.go index 2030d233..3f52f1d8 100644 --- a/internal/pkg/common/common.go +++ b/internal/pkg/common/common.go @@ -11,19 +11,6 @@ var ( letters = []rune("abcdefghijklmnopqrstuvwxyz") ) -const ( - // ConfigmapUpdateOnChangeAnnotation is an annotation to detect changes in configmaps - ConfigmapUpdateOnChangeAnnotation = "configmap.reloader.stakater.com/reload" - // SecretUpdateOnChangeAnnotation is an annotation to detect changes in secrets - SecretUpdateOnChangeAnnotation = "secret.reloader.stakater.com/reload" - // ConfigmapEnvarPostfix is a postfix for configmap envVar - ConfigmapEnvarPostfix = "_CONFIGMAP" - // SecretEnvarPostfix is a postfix for secret envVar - SecretEnvarPostfix = "_SECRET" - // EnvVarPrefix is a Prefix for environment variable - EnvVarPrefix = "STAKATER_" -) - // ConvertToEnvVarName converts the given text into a usable env var // removing any special chars with '_' and transforming text to upper case func ConvertToEnvVarName(text string) string { diff --git a/internal/pkg/constants/annotations.go b/internal/pkg/constants/annotations.go new file mode 100644 index 00000000..df6eb8dd --- /dev/null +++ b/internal/pkg/constants/annotations.go @@ -0,0 +1,8 @@ +package constants + +const ( + // ConfigmapUpdateOnChangeAnnotation is an annotation to detect changes in configmaps + ConfigmapUpdateOnChangeAnnotation = "configmap.reloader.stakater.com/reload" + // SecretUpdateOnChangeAnnotation is an annotation to detect changes in secrets + SecretUpdateOnChangeAnnotation = "secret.reloader.stakater.com/reload" +) \ No newline at end of file diff --git a/internal/pkg/constants/constants.go b/internal/pkg/constants/constants.go new file mode 100644 index 00000000..efb53927 --- /dev/null +++ b/internal/pkg/constants/constants.go @@ -0,0 +1,10 @@ +package constants + +const ( + // ConfigmapEnvarPostfix is a postfix for configmap envVar + ConfigmapEnvarPostfix = "_CONFIGMAP" + // SecretEnvarPostfix is a postfix for secret envVar + SecretEnvarPostfix = "_SECRET" + // EnvVarPrefix is a Prefix for environment variable + EnvVarPrefix = "STAKATER_" +) \ No newline at end of file diff --git a/internal/pkg/controller/controller.go b/internal/pkg/controller/controller.go index 2a1f72ae..0a84011d 100644 --- a/internal/pkg/controller/controller.go +++ b/internal/pkg/controller/controller.go @@ -9,7 +9,6 @@ import ( "github.com/stakater/Reloader/pkg/kube" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/runtime" - errorHandler "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" @@ -73,7 +72,7 @@ func (c *Controller) Delete(old interface{}) { func (c *Controller) Run(threadiness int, stopCh chan struct{}) { logrus.Infof("Starting Controller") - defer errorHandler.HandleCrash() + defer runtime.HandleCrash() // Let the workers stop when we are done defer c.queue.ShutDown() @@ -82,7 +81,7 @@ func (c *Controller) Run(threadiness int, stopCh chan struct{}) { // Wait for all involved caches to be synced, before processing items from the queue is started if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) { - errorHandler.HandleError(fmt.Errorf("Timed out waiting for caches to sync")) + runtime.HandleError(fmt.Errorf("Timed out waiting for caches to sync")) return } diff --git a/internal/pkg/controller/controller_test.go b/internal/pkg/controller/controller_test.go index 55a1ba57..3903b360 100644 --- a/internal/pkg/controller/controller_test.go +++ b/internal/pkg/controller/controller_test.go @@ -7,6 +7,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stakater/Reloader/internal/pkg/common" + "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/testutil" "github.com/stakater/Reloader/pkg/kube" "k8s.io/client-go/kubernetes" @@ -83,7 +84,7 @@ func TestControllerUpdatingConfigmapShouldCreateEnvInDeployment(t *testing.T) { // Verifying deployment update logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com") - updated := testutil.VerifyDeploymentUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyDeploymentUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("Deployment was not updated") } @@ -133,7 +134,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateDeployment(t *testing.T) { // Verifying deployment update logrus.Infof("Verifying env var has been updated") shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io") - updated := testutil.VerifyDeploymentUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyDeploymentUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("Deployment was not updated") } @@ -177,7 +178,7 @@ func TestControllerUpdatingConfigmapLabelsShouldNotCreateorUpdateEnvInDeployment // Verifying deployment update logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.google.com") - updated := testutil.VerifyDeploymentUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyDeploymentUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if updated { t.Errorf("Deployment should not be updated by changing label") } @@ -221,7 +222,7 @@ func TestControllerUpdatingSecretShouldCreateEnvInDeployment(t *testing.T) { // Verifying Upgrade logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData) - updated := testutil.VerifyDeploymentUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyDeploymentUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("Deployment was not updated") } @@ -271,7 +272,7 @@ func TestControllerUpdatingSecretShouldUpdateEnvInDeployment(t *testing.T) { // Verifying Upgrade logrus.Infof("Verifying env var has been updated") shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData) - updated := testutil.VerifyDeploymentUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyDeploymentUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("Deployment was not updated") } @@ -314,7 +315,7 @@ func TestControllerUpdatingSecretLabelsShouldNotCreateorUpdateEnvInDeployment(t // Verifying Upgrade logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, data) - updated := testutil.VerifyDeploymentUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyDeploymentUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if updated { t.Errorf("Deployment should not be updated by changing label in secret") } @@ -358,7 +359,7 @@ func TestControllerUpdatingConfigmapShouldCreateEnvInDaemonSet(t *testing.T) { // Verifying DaemonSet update logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com") - updated := testutil.VerifyDaemonSetUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyDaemonSetUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("DaemonSet was not updated") } @@ -408,7 +409,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateDaemonSet(t *testing.T) { // Verifying DaemonSet update logrus.Infof("Verifying env var has been updated") shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io") - updated := testutil.VerifyDaemonSetUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyDaemonSetUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("DaemonSet was not updated") } @@ -452,7 +453,7 @@ func TestControllerUpdatingSecretShouldCreateEnvInDaemonSet(t *testing.T) { // Verifying Upgrade logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData) - updated := testutil.VerifyDaemonSetUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyDaemonSetUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("DaemonSet was not updated") } @@ -502,7 +503,7 @@ func TestControllerUpdatingSecretShouldUpdateEnvInDaemonSet(t *testing.T) { // Verifying Upgrade logrus.Infof("Verifying env var has been updated") shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData) - updated := testutil.VerifyDaemonSetUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyDaemonSetUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("DaemonSet was not updated") } @@ -545,7 +546,7 @@ func TestControllerUpdatingSecretLabelsShouldNotCreateorUpdateEnvInDaemonSet(t * // Verifying Upgrade logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, data) - updated := testutil.VerifyDaemonSetUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyDaemonSetUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if updated { t.Errorf("DaemonSet should not be updated by changing label in secret") } @@ -589,7 +590,7 @@ func TestControllerUpdatingConfigmapShouldCreateEnvInStatefulSet(t *testing.T) { // Verifying StatefulSet update logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com") - updated := testutil.VerifyStatefulSetUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyStatefulSetUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("StatefulSet was not updated") } @@ -639,7 +640,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateStatefulSet(t *testing.T) { // Verifying StatefulSet update logrus.Infof("Verifying env var has been updated") shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io") - updated := testutil.VerifyStatefulSetUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyStatefulSetUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("StatefulSet was not updated") } @@ -683,7 +684,7 @@ func TestControllerUpdatingConfigmapLabelsShouldNotCreateorUpdateEnvInStatefulSe // Verifying StatefulSet update logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.google.com") - updated := testutil.VerifyStatefulSetUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyStatefulSetUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if updated { t.Errorf("StatefulSet should not be updated by changing label") } @@ -727,7 +728,7 @@ func TestControllerUpdatingSecretShouldCreateEnvInStatefulSet(t *testing.T) { // Verifying Upgrade logrus.Infof("Verifying env var has been created") shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData) - updated := testutil.VerifyStatefulSetUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyStatefulSetUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("StatefulSet was not updated") } @@ -777,7 +778,7 @@ func TestControllerUpdatingSecretShouldUpdateEnvInStatefulSet(t *testing.T) { // Verifying Upgrade logrus.Infof("Verifying env var has been updated") shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData) - updated := testutil.VerifyStatefulSetUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyStatefulSetUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("StatefulSet was not updated") } diff --git a/internal/pkg/handler/update.go b/internal/pkg/handler/update.go index 51f16fe2..78fb3c7b 100644 --- a/internal/pkg/handler/update.go +++ b/internal/pkg/handler/update.go @@ -6,9 +6,13 @@ import ( "github.com/sirupsen/logrus" "github.com/stakater/Reloader/internal/pkg/common" + "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/crypto" + "github.com/stakater/Reloader/internal/pkg/util" "github.com/stakater/Reloader/pkg/kube" + apps_v1beta1 "k8s.io/api/apps/v1beta1" "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) @@ -19,6 +23,30 @@ type ResourceUpdatedHandler struct { OldResource interface{} } +//Config contains rolling upgrade configuration parameters +type Config struct { + namespace string + resourceName string + annotation string + shaValue string +} + +//ItemsFunc is a generic function to return a specific resource array in given namespace +type ItemsFunc func(kubernetes.Interface, string) []interface{} + +//ContainersFunc is a generic func to return containers +type ContainersFunc func(interface{}) []v1.Container + +//UpdateFunc performs the resource update +type UpdateFunc func(kubernetes.Interface, string, interface{}) error + +//RollingUpgradeFuncs contains generic functions to perform rolling upgrade +type RollingUpgradeFuncs struct { + ItemsFunc ItemsFunc + ContainersFunc ContainersFunc + UpdateFunc UpdateFunc +} + // Handle processes the updated resource func (r ResourceUpdatedHandler) Handle() error { if r.Resource == nil || r.OldResource == nil { @@ -26,154 +54,173 @@ func (r ResourceUpdatedHandler) Handle() error { } else { logrus.Infof("Detected changes in object %s", r.Resource) // process resource based on its type - rollingUpgrade(r, "deployments") - rollingUpgrade(r, "daemonsets") - rollingUpgrade(r, "statefulSets") + rollingUpgrade(r, RollingUpgradeFuncs{ + ItemsFunc: GetDeploymentItems, + ContainersFunc: GetDeploymentContainers, + UpdateFunc: UpdateDeployment, + }) + rollingUpgrade(r, RollingUpgradeFuncs{ + ItemsFunc: GetDaemonSetItems, + ContainersFunc: GetDaemonSetContainers, + UpdateFunc: UpdateDaemonSet, + }) + rollingUpgrade(r, RollingUpgradeFuncs{ + ItemsFunc: GetStatefulSetItems, + ContainersFunc: GetStatefulsetContainers, + UpdateFunc: UpdateStatefulset, + }) } return nil } -func rollingUpgrade(r ResourceUpdatedHandler, rollingUpgradeType string) { +// GetDeploymentItems returns the deployments in given namespace +func GetDeploymentItems(client kubernetes.Interface, namespace string) []interface{} { + deployments, err := client.ExtensionsV1beta1().Deployments(namespace).List(meta_v1.ListOptions{}) + if err != nil { + logrus.Errorf("Failed to list deployments %v", err) + } + return util.InterfaceSlice(deployments.Items) +} + +// GetDaemonSetItems returns the daemonSet in given namespace +func GetDaemonSetItems(client kubernetes.Interface, namespace string) []interface{} { + daemonSets, err := client.ExtensionsV1beta1().DaemonSets(namespace).List(meta_v1.ListOptions{}) + if err != nil { + logrus.Errorf("Failed to list daemonSets %v", err) + } + return util.InterfaceSlice(daemonSets.Items) +} + +// GetStatefulSetItems returns the statefulSet in given namespace +func GetStatefulSetItems(client kubernetes.Interface, namespace string) []interface{} { + statefulSets, err := client.AppsV1beta1().StatefulSets(namespace).List(meta_v1.ListOptions{}) + if err != nil { + logrus.Errorf("Failed to list statefulSets %v", err) + } + return util.InterfaceSlice(statefulSets.Items) +} + +// GetDeploymentContainers returns the containers of given deployment +func GetDeploymentContainers(item interface{}) []v1.Container { + return item.(v1beta1.Deployment).Spec.Template.Spec.Containers +} + +// GetDaemonSetContainers returns the containers of given daemonset +func GetDaemonSetContainers(item interface{}) []v1.Container { + return item.(v1beta1.DaemonSet).Spec.Template.Spec.Containers +} + +// GetStatefulsetContainers returns the containers of given statefulSet +func GetStatefulsetContainers(item interface{}) []v1.Container { + return item.(apps_v1beta1.StatefulSet).Spec.Template.Spec.Containers +} + +// UpdateDeployment performs rolling upgrade on deployment +func UpdateDeployment(client kubernetes.Interface, namespace string, resource interface{}) error { + deployment := resource.(v1beta1.Deployment) + _, err := client.ExtensionsV1beta1().Deployments(namespace).Update(&deployment) + return err +} + +// UpdateDaemonSet performs rolling upgrade on daemonSet +func UpdateDaemonSet(client kubernetes.Interface, namespace string, resource interface{}) error { + daemonSet := resource.(v1beta1.DaemonSet) + _, err := client.ExtensionsV1beta1().DaemonSets(namespace).Update(&daemonSet) + return err +} + +// UpdateStatefulset performs rolling upgrade on statefulSet +func UpdateStatefulset(client kubernetes.Interface, namespace string, resource interface{}) error { + statefulSet := resource.(apps_v1beta1.StatefulSet) + _, err := client.AppsV1beta1().StatefulSets(namespace).Update(&statefulSet) + return err +} + +func rollingUpgrade(r ResourceUpdatedHandler, upgradeFuncs RollingUpgradeFuncs) { client, err := kube.GetClient() if err != nil { logrus.Fatalf("Unable to create Kubernetes client error = %v", err) } - var shaData, oldSHAdata string - shaData = getSHAfromData(r.Resource) - oldSHAdata = getSHAfromData(r.OldResource) - if shaData != oldSHAdata { - var namespace, resourceName, envarPostfix, annotation string - if _, ok := r.Resource.(*v1.ConfigMap); ok { - logrus.Infof("Performing 'Updated' action for resource of type 'configmap'") - namespace = r.Resource.(*v1.ConfigMap).Namespace - resourceName = r.Resource.(*v1.ConfigMap).Name - envarPostfix = common.ConfigmapEnvarPostfix - annotation = common.ConfigmapUpdateOnChangeAnnotation - } else if _, ok := r.Resource.(*v1.Secret); ok { - logrus.Infof("Performing 'Updated' action for resource of type 'secret'") - namespace = r.Resource.(*v1.Secret).Namespace - resourceName = r.Resource.(*v1.Secret).Name - envarPostfix = common.SecretEnvarPostfix - annotation = common.SecretUpdateOnChangeAnnotation - } else { - logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource) - } - - if rollingUpgradeType == "deployments" { - err = RollingUpgradeDeployment(client, namespace, resourceName, shaData, envarPostfix, annotation) - } else if rollingUpgradeType == "daemonsets" { - err = RollingUpgradeDaemonSets(client, namespace, resourceName, shaData, envarPostfix, annotation) - } else if rollingUpgradeType == "statefulSets" { - err = RollingUpgradeStatefulSets(client, namespace, resourceName, shaData, envarPostfix, annotation) - } + config, envVarPostfix, oldSHAData := getConfig(r) + if config.shaValue != oldSHAData { + err = PerformRollingUpgrade(client, config, envVarPostfix, upgradeFuncs) if err != nil { - logrus.Errorf("Rolling upgrade failed for %s of resource type %s", resourceName, rollingUpgradeType) + logrus.Fatalf("Rolling upgrade failed with error = %v", err) } } else { - logrus.Infof("Resource update will not happen because no data change detected") + logrus.Infof("Rolling upgrade will not happend because no actual change in data has been detected") } } -// RollingUpgradeDeployment upgrades the deployment if there is any change in configmap or secret data -func RollingUpgradeDeployment(client kubernetes.Interface, namespace string, resourceName string, shaData string, envarPostfix string, annotation string) error { - deployments, err := client.ExtensionsV1beta1().Deployments(namespace).List(meta_v1.ListOptions{}) - if err != nil { - logrus.Errorf("Failed to list deployments %v", err) - } - - for _, d := range deployments.Items { - containers := d.Spec.Template.Spec.Containers - // match deployments with the correct annotation - annotationValue := d.ObjectMeta.Annotations[annotation] - updated := performRollingUpgrade(containers, resourceName, annotationValue, shaData, envarPostfix) - if !updated { - logrus.Warnf("Rolling upgrade did not happen") - } else { - // update the deployment - _, err := client.ExtensionsV1beta1().Deployments(namespace).Update(&d) - if err != nil { - logrus.Errorf("Update deployment failed %v", err) - } else { - logrus.Infof("Updated Deployment %s", d.Name) - } +func getConfig(r ResourceUpdatedHandler) (Config, string, string) { + var shaData, oldSHAData, envVarPostfix string + var config Config + if _, ok := r.Resource.(*v1.ConfigMap); ok { + logrus.Infof("Performing 'Updated' action for resource of type 'configmap'") + configmap := r.Resource.(*v1.ConfigMap) + shaData = getSHAfromConfigmap(configmap.Data) + oldSHAData = getSHAfromConfigmap(r.OldResource.(*v1.ConfigMap).Data) + config = Config{ + namespace: configmap.Namespace, + resourceName: configmap.Name, + annotation: constants.ConfigmapUpdateOnChangeAnnotation, + shaValue: shaData, } + envVarPostfix = constants.ConfigmapEnvarPostfix + } else if _, ok := r.Resource.(*v1.Secret); ok { + logrus.Infof("Performing 'Updated' action for resource of type 'secret'") + secret := r.Resource.(*v1.Secret) + shaData = getSHAfromSecret(secret.Data) + oldSHAData = getSHAfromSecret(r.OldResource.(*v1.Secret).Data) + config = Config{ + namespace: secret.Namespace, + resourceName: secret.Name, + annotation: constants.SecretUpdateOnChangeAnnotation, + shaValue: shaData, + } + envVarPostfix = constants.SecretEnvarPostfix + } else { + logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource) } - return nil + return config, envVarPostfix, oldSHAData } -// RollingUpgradeDaemonSets upgrades the daemonset if there is any change in configmap or secret data -func RollingUpgradeDaemonSets(client kubernetes.Interface, namespace string, resourceName string, shaData string, envarPostfix string, annotation string) error { - daemonSets, err := client.ExtensionsV1beta1().DaemonSets(namespace).List(meta_v1.ListOptions{}) - if err != nil { - logrus.Errorf("Failed to list daemonSets %v", err) - } - - for _, d := range daemonSets.Items { - containers := d.Spec.Template.Spec.Containers - // match daemonSets with the correct annotation - annotationValue := d.ObjectMeta.Annotations[annotation] - updated := performRollingUpgrade(containers, resourceName, annotationValue, shaData, envarPostfix) - if !updated { - logrus.Warnf("Rolling upgrade did not happen") - } else { - // update the daemonSet - _, err := client.ExtensionsV1beta1().DaemonSets(namespace).Update(&d) - if err != nil { - logrus.Errorf("Update daemonSet failed %v", err) - } else { - logrus.Infof("Updated daemonSet %s", d.Name) +// PerformRollingUpgrade upgrades the deployment if there is any change in configmap or secret data +func PerformRollingUpgrade(client kubernetes.Interface, config Config, envarPostfix string, upgradeFuncs RollingUpgradeFuncs) error { + items := upgradeFuncs.ItemsFunc(client, config.namespace) + var err error + for _, i := range items { + containers := upgradeFuncs.ContainersFunc(i) + // find correct annotation and update the resource + annotationValue := util.ToObjectMeta(i).Annotations[config.annotation] + if annotationValue != "" { + values := strings.Split(annotationValue, ",") + for _, value := range values { + if value == config.resourceName { + updated := updateContainers(containers, value, config.shaValue, envarPostfix) + if !updated { + logrus.Warnf("Rolling upgrade did not happen") + } else { + err = upgradeFuncs.UpdateFunc(client, config.namespace, i) + if err != nil { + logrus.Errorf("Update deployment failed %v", err) + } else { + logrus.Infof("Updated Deployment %s", config.resourceName) + } + break + } + } } } } return err } -// RollingUpgradeStatefulSets upgrades the statefulset if there is any change in configmap or secret data -func RollingUpgradeStatefulSets(client kubernetes.Interface, namespace string, resourceName string, shaData string, envarPostfix string, annotation string) error { - statefulSets, err := client.AppsV1beta1().StatefulSets(namespace).List(meta_v1.ListOptions{}) - if err != nil { - logrus.Errorf("Failed to list statefulSets %v", err) - } - - for _, s := range statefulSets.Items { - containers := s.Spec.Template.Spec.Containers - // match statefulSets with the correct annotation - annotationValue := s.ObjectMeta.Annotations[annotation] - updated := performRollingUpgrade(containers, resourceName, annotationValue, shaData, envarPostfix) - if !updated { - logrus.Warnf("Rolling upgrade did not happen") - } else { - // update the statefulSet - _, err := client.AppsV1beta1().StatefulSets(namespace).Update(&s) - if err != nil { - logrus.Errorf("Update statefulSet failed %v", err) - } else { - logrus.Infof("Updated statefulSet %s", s.Name) - } - } - } - return err -} - -func performRollingUpgrade(containers []v1.Container, resourceName string, annotationValue string, shaData string, envarPostfix string) bool { - updated := false - if annotationValue != "" { - values := strings.Split(annotationValue, ",") - for _, value := range values { - if value == resourceName { - updated = updateContainers(containers, value, shaData, envarPostfix) - break - } - } - } - return updated -} - func updateContainers(containers []v1.Container, annotationValue string, shaData string, envarPostfix string) bool { updated := false - envar := common.EnvVarPrefix + common.ConvertToEnvVarName(annotationValue) + envarPostfix + envar := constants.EnvVarPrefix + common.ConvertToEnvVarName(annotationValue) + envarPostfix logrus.Infof("Generated environment variable: %s", envar) for i := range containers { envs := containers[i].Env @@ -209,18 +256,20 @@ func updateEnvVar(envs []v1.EnvVar, envar string, shaData string) bool { return false } -func getSHAfromData(resource interface{}) string { +func getSHAfromConfigmap(data map[string]string) string { values := []string{} - if _, ok := resource.(*v1.ConfigMap); ok { - logrus.Infof("Generating SHA for configmap data") - for k, v := range resource.(*v1.ConfigMap).Data { - values = append(values, k+"="+v) - } - } else if _, ok := resource.(*v1.Secret); ok { - logrus.Infof("Generating SHA for secret data") - for k, v := range resource.(*v1.Secret).Data { - values = append(values, k+"="+string(v[:])) - } + for k, v := range data { + values = append(values, k+"="+v) + } + sort.Strings(values) + return crypto.GenerateSHA(strings.Join(values, ";")) +} + +func getSHAfromSecret(data map[string][]byte) string { + values := []string{} + logrus.Infof("Generating SHA for secret data") + for k, v := range data { + values = append(values, k+"="+string(v[:])) } sort.Strings(values) return crypto.GenerateSHA(strings.Join(values, ";")) diff --git a/internal/pkg/handler/update_test.go b/internal/pkg/handler/update_test.go index 7cc5461f..7f54fa09 100644 --- a/internal/pkg/handler/update_test.go +++ b/internal/pkg/handler/update_test.go @@ -7,6 +7,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stakater/Reloader/internal/pkg/common" + "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/testutil" "github.com/stakater/Reloader/pkg/kube" "k8s.io/client-go/kubernetes" @@ -152,14 +153,26 @@ func teardown() { func TestRollingUpgradeForDeploymentWithConfigmap(t *testing.T) { shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, configmapName, "www.stakater.com") - err := RollingUpgradeDeployment(client, namespace, configmapName, shaData, common.ConfigmapEnvarPostfix, common.ConfigmapUpdateOnChangeAnnotation) + config := Config{ + namespace: namespace, + resourceName: configmapName, + shaValue: shaData, + annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + deploymentFuncs := RollingUpgradeFuncs{ + ItemsFunc: GetDeploymentItems, + ContainersFunc: GetDeploymentContainers, + UpdateFunc: UpdateDeployment, + } + + err := PerformRollingUpgrade(client, config, constants.ConfigmapEnvarPostfix, deploymentFuncs) time.Sleep(5 * time.Second) if err != nil { - t.Errorf("Rolling upgrade failed for Deployment with configmap") + t.Errorf("Rolling upgrade failed for Deployment with Configmap") } logrus.Infof("Verifying deployment update") - updated := testutil.VerifyDeploymentUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyDeploymentUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("Deployment was not updated") } @@ -167,14 +180,26 @@ func TestRollingUpgradeForDeploymentWithConfigmap(t *testing.T) { func TestRollingUpgradeForDeploymentWithSecret(t *testing.T) { shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, "dGVzdFVwZGF0ZWRTZWNyZXRFbmNvZGluZ0ZvclJlbG9hZGVy") - err := RollingUpgradeDeployment(client, namespace, secretName, shaData, common.SecretEnvarPostfix, common.SecretUpdateOnChangeAnnotation) + config := Config{ + namespace: namespace, + resourceName: secretName, + shaValue: shaData, + annotation: constants.SecretUpdateOnChangeAnnotation, + } + deploymentFuncs := RollingUpgradeFuncs{ + ItemsFunc: GetDeploymentItems, + ContainersFunc: GetDeploymentContainers, + UpdateFunc: UpdateDeployment, + } + + err := PerformRollingUpgrade(client, config, constants.SecretEnvarPostfix, deploymentFuncs) time.Sleep(5 * time.Second) if err != nil { t.Errorf("Rolling upgrade failed for Deployment with Secret") } logrus.Infof("Verifying deployment update") - updated := testutil.VerifyDeploymentUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyDeploymentUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("Deployment was not updated") } @@ -182,14 +207,26 @@ func TestRollingUpgradeForDeploymentWithSecret(t *testing.T) { func TestRollingUpgradeForDaemonSetWithConfigmap(t *testing.T) { shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.facebook.com") - err := RollingUpgradeDaemonSets(client, namespace, configmapName, shaData, common.ConfigmapEnvarPostfix, common.ConfigmapUpdateOnChangeAnnotation) + config := Config{ + namespace: namespace, + resourceName: configmapName, + shaValue: shaData, + annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + daemonSetFuncs := RollingUpgradeFuncs{ + ItemsFunc: GetDaemonSetItems, + ContainersFunc: GetDaemonSetContainers, + UpdateFunc: UpdateDaemonSet, + } + + err := PerformRollingUpgrade(client, config, constants.ConfigmapEnvarPostfix, daemonSetFuncs) time.Sleep(5 * time.Second) if err != nil { t.Errorf("Rolling upgrade failed for DaemonSet with configmap") } logrus.Infof("Verifying daemonSet update") - updated := testutil.VerifyDaemonSetUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyDaemonSetUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("DaemonSet was not updated") } @@ -197,14 +234,27 @@ func TestRollingUpgradeForDaemonSetWithConfigmap(t *testing.T) { func TestRollingUpgradeForDaemonSetWithSecret(t *testing.T) { shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, "d3d3LmZhY2Vib29rLmNvbQ==") - err := RollingUpgradeDaemonSets(client, namespace, secretName, shaData, common.SecretEnvarPostfix, common.SecretUpdateOnChangeAnnotation) + + config := Config{ + namespace: namespace, + resourceName: secretName, + shaValue: shaData, + annotation: constants.SecretUpdateOnChangeAnnotation, + } + daemonSetFuncs := RollingUpgradeFuncs{ + ItemsFunc: GetDaemonSetItems, + ContainersFunc: GetDaemonSetContainers, + UpdateFunc: UpdateDaemonSet, + } + + err := PerformRollingUpgrade(client, config, constants.SecretEnvarPostfix, daemonSetFuncs) time.Sleep(5 * time.Second) if err != nil { t.Errorf("Rolling upgrade failed for DaemonSet with secret") } logrus.Infof("Verifying daemonSet update") - updated := testutil.VerifyDaemonSetUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyDaemonSetUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("DaemonSet was not updated") } @@ -212,14 +262,27 @@ func TestRollingUpgradeForDaemonSetWithSecret(t *testing.T) { func TestRollingUpgradeForStatefulSetWithConfigmap(t *testing.T) { shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.twitter.com") - err := RollingUpgradeStatefulSets(client, namespace, configmapName, shaData, common.ConfigmapEnvarPostfix, common.ConfigmapUpdateOnChangeAnnotation) + + config := Config{ + namespace: namespace, + resourceName: configmapName, + shaValue: shaData, + annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + statefulSetFuncs := RollingUpgradeFuncs{ + ItemsFunc: GetStatefulSetItems, + ContainersFunc: GetStatefulsetContainers, + UpdateFunc: UpdateStatefulset, + } + + err := PerformRollingUpgrade(client, config, constants.ConfigmapEnvarPostfix, statefulSetFuncs) time.Sleep(5 * time.Second) if err != nil { t.Errorf("Rolling upgrade failed for StatefulSet with configmap") } logrus.Infof("Verifying statefulSet update") - updated := testutil.VerifyStatefulSetUpdate(client, namespace, configmapName, common.ConfigmapEnvarPostfix, shaData, common.ConfigmapUpdateOnChangeAnnotation) + updated := testutil.VerifyStatefulSetUpdate(client, namespace, configmapName, constants.ConfigmapEnvarPostfix, shaData, constants.ConfigmapUpdateOnChangeAnnotation) if !updated { t.Errorf("StatefulSet was not updated") } @@ -227,14 +290,27 @@ func TestRollingUpgradeForStatefulSetWithConfigmap(t *testing.T) { func TestRollingUpgradeForStatefulSetWithSecret(t *testing.T) { shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, "d3d3LnR3aXR0ZXIuY29t") - err := RollingUpgradeStatefulSets(client, namespace, secretName, shaData, common.SecretEnvarPostfix, common.SecretUpdateOnChangeAnnotation) + + config := Config{ + namespace: namespace, + resourceName: secretName, + shaValue: shaData, + annotation: constants.SecretUpdateOnChangeAnnotation, + } + statefulSetFuncs := RollingUpgradeFuncs{ + ItemsFunc: GetStatefulSetItems, + ContainersFunc: GetStatefulsetContainers, + UpdateFunc: UpdateStatefulset, + } + + err := PerformRollingUpgrade(client, config, constants.SecretEnvarPostfix, statefulSetFuncs) time.Sleep(5 * time.Second) if err != nil { t.Errorf("Rolling upgrade failed for StatefulSet with secret") } logrus.Infof("Verifying statefulSet update") - updated := testutil.VerifyStatefulSetUpdate(client, namespace, secretName, common.SecretEnvarPostfix, shaData, common.SecretUpdateOnChangeAnnotation) + updated := testutil.VerifyStatefulSetUpdate(client, namespace, secretName, constants.SecretEnvarPostfix, shaData, constants.SecretUpdateOnChangeAnnotation) if !updated { t.Errorf("StatefulSet was not updated") } diff --git a/internal/pkg/testutil/kube.go b/internal/pkg/testutil/kube.go index b17a88ea..4fea00f7 100644 --- a/internal/pkg/testutil/kube.go +++ b/internal/pkg/testutil/kube.go @@ -7,6 +7,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stakater/Reloader/internal/pkg/common" + "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/crypto" v1_beta1 "k8s.io/api/apps/v1beta1" "k8s.io/api/core/v1" @@ -52,8 +53,8 @@ func GetDeployment(namespace string, deploymentName string) *v1beta1.Deployment Namespace: namespace, Labels: map[string]string{"firstLabel": "temp"}, Annotations: map[string]string{ - common.ConfigmapUpdateOnChangeAnnotation: deploymentName, - common.SecretUpdateOnChangeAnnotation: deploymentName}, + constants.ConfigmapUpdateOnChangeAnnotation: deploymentName, + constants.SecretUpdateOnChangeAnnotation: deploymentName}, }, Spec: v1beta1.DeploymentSpec{ Replicas: &replicaset, @@ -91,8 +92,8 @@ func GetDaemonSet(namespace string, daemonsetName string) *v1beta1.DaemonSet { Namespace: namespace, Labels: map[string]string{"firstLabel": "temp"}, Annotations: map[string]string{ - common.ConfigmapUpdateOnChangeAnnotation: daemonsetName, - common.SecretUpdateOnChangeAnnotation: daemonsetName}, + constants.ConfigmapUpdateOnChangeAnnotation: daemonsetName, + constants.SecretUpdateOnChangeAnnotation: daemonsetName}, }, Spec: v1beta1.DaemonSetSpec{ UpdateStrategy: v1beta1.DaemonSetUpdateStrategy{ @@ -129,8 +130,8 @@ func GetStatefulSet(namespace string, statefulsetName string) *v1_beta1.Stateful Namespace: namespace, Labels: map[string]string{"firstLabel": "temp"}, Annotations: map[string]string{ - common.ConfigmapUpdateOnChangeAnnotation: statefulsetName, - common.SecretUpdateOnChangeAnnotation: statefulsetName}, + constants.ConfigmapUpdateOnChangeAnnotation: statefulsetName, + constants.SecretUpdateOnChangeAnnotation: statefulsetName}, }, Spec: v1_beta1.StatefulSetSpec{ UpdateStrategy: v1_beta1.StatefulSetUpdateStrategy{ @@ -227,7 +228,7 @@ func VerifyDeploymentUpdate(client kubernetes.Interface, namespace string, name } } if matches { - envName := common.EnvVarPrefix + common.ConvertToEnvVarName(annotationValue) + envarPostfix + envName := constants.EnvVarPrefix + common.ConvertToEnvVarName(annotationValue) + envarPostfix updated := getResourceSHA(containers, envName) if updated == shaData { return true @@ -258,7 +259,7 @@ func VerifyDaemonSetUpdate(client kubernetes.Interface, namespace string, name s } } if matches { - envName := common.EnvVarPrefix + common.ConvertToEnvVarName(annotationValue) + resourceType + envName := constants.EnvVarPrefix + common.ConvertToEnvVarName(annotationValue) + resourceType updated := getResourceSHA(containers, envName) if updated == shaData { @@ -290,7 +291,7 @@ func VerifyStatefulSetUpdate(client kubernetes.Interface, namespace string, name } } if matches { - envName := common.EnvVarPrefix + common.ConvertToEnvVarName(annotationValue) + resourceType + envName := constants.EnvVarPrefix + common.ConvertToEnvVarName(annotationValue) + resourceType updated := getResourceSHA(containers, envName) if updated == shaData { diff --git a/internal/pkg/util/interface.go b/internal/pkg/util/interface.go new file mode 100644 index 00000000..e4f318a9 --- /dev/null +++ b/internal/pkg/util/interface.go @@ -0,0 +1,38 @@ +package util + +import ( + "reflect" + + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InterfaceSlice converts an interface to an interface array +func InterfaceSlice(slice interface{}) []interface{} { + s := reflect.ValueOf(slice) + if s.Kind() != reflect.Slice { + logrus.Errorf("InterfaceSlice() given a non-slice type") + } + + ret := make([]interface{}, s.Len()) + + for i := 0; i < s.Len(); i++ { + ret[i] = s.Index(i).Interface() + } + + return ret +} + +type ObjectMeta struct { + metav1.ObjectMeta +} + +func ToObjectMeta(kubernetesObject interface{}) ObjectMeta { + objectValue := reflect.ValueOf(kubernetesObject) + fieldName := reflect.TypeOf((*metav1.ObjectMeta)(nil)).Elem().Name() + field := objectValue.FieldByName(fieldName).Interface().(metav1.ObjectMeta) + + return ObjectMeta{ + ObjectMeta: field, + } +}