diff --git a/.gitignore b/.gitignore index 234d3a6d..d54621ba 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ release out/ _gopath/ .DS_Store +.vscode vendor \ No newline at end of file diff --git a/.version b/.version index 8acdd82b..8a9ecc2e 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.0.1 +0.0.1 \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 8d76442b..10e7e595 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,5 @@ #!/usr/bin/groovy -@Library('github.com/stakater/fabric8-pipeline-library@v2.4.0') +@Library('github.com/stakater/fabric8-pipeline-library@v2.5.1') def dummy diff --git a/README.md b/README.md index 63e2d63a..bb34881c 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ## WHY NAME RELOADER -In english language, Reloader is a thing/tool that can reload certain stuff. So refereig to that meaning relaoder can reload +In english language, Reloader is a thing/tool that can reload certain stuff. So referring to that meaning reloader can reload ## Problem -We would like to watch if some change happens in `ConfigMap` and `Secret` objects and then perform certain upgrade on relavent `Deployment`, `Deamonset` and `Statefulset` +We would like to watch if some change happens in `ConfigMap` and `Secret` objects and then perform certain upgrade on relevant `Deployment`, `Deamonset` and `Statefulset` ## Solution @@ -21,10 +21,20 @@ For a `Deployment` called `foo` have a `ConfigMap` called `foo`. Then add this a ```yaml metadata: annotations: - reloader.stakater.com/update-on-change: "foo" + configmap.reloader.stakater.com/reload: "foo" ``` -Then, providing `Reloader` is running, whenever you edit the `ConfigMap` called `foo` the Reloader will update the `Deployment` by adding the environment variable: +OR + +For a `Deployment` called `foo` have a `Secret` called `foo`. Then add this annotation to your `Deployment` + +```yaml +metadata: + annotations: + 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: ``` STAKATER_FOO_REVISION=${reloaderRevision} diff --git a/deployments/kubernetes/chart/reloader/templates/rbac.yaml b/deployments/kubernetes/chart/reloader/templates/rbac.yaml index 678725d0..cc0b0d3e 100644 --- a/deployments/kubernetes/chart/reloader/templates/rbac.yaml +++ b/deployments/kubernetes/chart/reloader/templates/rbac.yaml @@ -24,6 +24,19 @@ rules: - list - get - watch + - apiGroups: + - "" + - "extensions" + - "apps" + resources: + - deployments + - daemonsets + - statefulsets + verbs: + - list + - get + - update + - patch --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: RoleBinding diff --git a/deployments/kubernetes/manifests/rbac.yaml b/deployments/kubernetes/manifests/rbac.yaml index e56b0f6a..5654b85a 100644 --- a/deployments/kubernetes/manifests/rbac.yaml +++ b/deployments/kubernetes/manifests/rbac.yaml @@ -36,6 +36,19 @@ rules: - list - get - watch + - apiGroups: + - "" + - "extensions" + - "apps" + resources: + - deployments + - daemonsets + - statefulsets + verbs: + - list + - get + - update + - patch --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: RoleBinding diff --git a/glide.lock b/glide.lock index 64d3a712..bc2e3f20 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ hash: b6fe060028bdb1249ba2413746476c2550b267eeab3c166c36a86e000a8dd354 -updated: 2018-07-17T09:08:20.493153674Z +updated: 2018-07-24T21:12:43.027181463+05:00 imports: - name: github.com/davecgh/go-spew version: 782f4967f2dc4564575ca782fe2d04090b5faca8 @@ -90,13 +90,10 @@ imports: version: 1c05540f6879653db88113bc4a2b70aec4bd491f subpackages: - context - - html - - html/atom - http2 - http2/hpack - idna - lex/httplex - - websocket - name: golang.org/x/sys version: 7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce subpackages: @@ -194,35 +191,62 @@ imports: version: 35874c597fed17ca62cd197e516d7d5ff9a2958c subpackages: - discovery + - discovery/fake - kubernetes + - kubernetes/fake - kubernetes/scheme - kubernetes/typed/admissionregistration/v1alpha1 + - kubernetes/typed/admissionregistration/v1alpha1/fake - kubernetes/typed/apps/v1beta1 + - kubernetes/typed/apps/v1beta1/fake - kubernetes/typed/apps/v1beta2 + - kubernetes/typed/apps/v1beta2/fake - kubernetes/typed/authentication/v1 + - kubernetes/typed/authentication/v1/fake - kubernetes/typed/authentication/v1beta1 + - kubernetes/typed/authentication/v1beta1/fake - kubernetes/typed/authorization/v1 + - kubernetes/typed/authorization/v1/fake - kubernetes/typed/authorization/v1beta1 + - kubernetes/typed/authorization/v1beta1/fake - kubernetes/typed/autoscaling/v1 + - kubernetes/typed/autoscaling/v1/fake - kubernetes/typed/autoscaling/v2beta1 + - kubernetes/typed/autoscaling/v2beta1/fake - kubernetes/typed/batch/v1 + - kubernetes/typed/batch/v1/fake - kubernetes/typed/batch/v1beta1 + - kubernetes/typed/batch/v1beta1/fake - kubernetes/typed/batch/v2alpha1 + - kubernetes/typed/batch/v2alpha1/fake - kubernetes/typed/certificates/v1beta1 + - kubernetes/typed/certificates/v1beta1/fake - kubernetes/typed/core/v1 + - kubernetes/typed/core/v1/fake - kubernetes/typed/extensions/v1beta1 + - kubernetes/typed/extensions/v1beta1/fake - kubernetes/typed/networking/v1 + - kubernetes/typed/networking/v1/fake - kubernetes/typed/policy/v1beta1 + - kubernetes/typed/policy/v1beta1/fake - kubernetes/typed/rbac/v1 + - kubernetes/typed/rbac/v1/fake - kubernetes/typed/rbac/v1alpha1 + - kubernetes/typed/rbac/v1alpha1/fake - kubernetes/typed/rbac/v1beta1 + - kubernetes/typed/rbac/v1beta1/fake - kubernetes/typed/scheduling/v1alpha1 + - kubernetes/typed/scheduling/v1alpha1/fake - kubernetes/typed/settings/v1alpha1 + - kubernetes/typed/settings/v1alpha1/fake - kubernetes/typed/storage/v1 + - kubernetes/typed/storage/v1/fake - kubernetes/typed/storage/v1beta1 + - kubernetes/typed/storage/v1beta1/fake - pkg/version - rest - rest/watch + - testing - tools/auth - tools/cache - tools/clientcmd diff --git a/internal/pkg/callbacks/rolling_upgrade.go b/internal/pkg/callbacks/rolling_upgrade.go new file mode 100644 index 00000000..ba395097 --- /dev/null +++ b/internal/pkg/callbacks/rolling_upgrade.go @@ -0,0 +1,91 @@ +package callbacks + +import ( + "github.com/sirupsen/logrus" + "github.com/stakater/Reloader/internal/pkg/util" + 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" +) + +//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 + ResourceType 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 +} diff --git a/internal/pkg/constants/annotations.go b/internal/pkg/constants/annotations.go new file mode 100644 index 00000000..d06cc5a4 --- /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" +) diff --git a/internal/pkg/constants/constants.go b/internal/pkg/constants/constants.go new file mode 100644 index 00000000..d01792df --- /dev/null +++ b/internal/pkg/constants/constants.go @@ -0,0 +1,10 @@ +package constants + +const ( + // ConfigmapEnvVarPostfix is a postfix for configmap envVar + ConfigmapEnvVarPostfix = "_CONFIGMAP" + // SecretEnvVarPostfix is a postfix for secret envVar + SecretEnvVarPostfix = "_SECRET" + // EnvVarPrefix is a Prefix for environment variable + EnvVarPrefix = "STAKATER_" +) diff --git a/internal/pkg/controller/controller.go b/internal/pkg/controller/controller.go index e14ead7e..8bb5ff59 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" @@ -65,15 +64,14 @@ func (c *Controller) Update(old interface{}, new interface{}) { // Delete function to add an object to the queue in case of deleting a resource func (c *Controller) Delete(old interface{}) { - // TODO Added this function for future usecase - logrus.Infof("Deleted resource has been detected but no further implementation found to take action") + logrus.Infof("Resource deletion has been detected but no further implementation found to take action") } //Run function for controller which handles the queue 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 +80,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 0035d0fb..3fdd4b86 100644 --- a/internal/pkg/controller/controller_test.go +++ b/internal/pkg/controller/controller_test.go @@ -1,254 +1,930 @@ package controller import ( - "math/rand" + "os" "testing" "time" "github.com/sirupsen/logrus" + "github.com/stakater/Reloader/internal/pkg/callbacks" + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/testutil" + "github.com/stakater/Reloader/internal/pkg/util" "github.com/stakater/Reloader/pkg/kube" - "k8s.io/api/core/v1" - "k8s.io/api/extensions/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" ) var ( + client = testutil.GetClient() + namespace = "test-reloader" configmapNamePrefix = "testconfigmap-reloader" secretNamePrefix = "testsecret-reloader" - letters = []rune("abcdefghijklmnopqrstuvwxyz") + data = "dGVzdFNlY3JldEVuY29kaW5nRm9yUmVsb2FkZXI=" + newData = "dGVzdE5ld1NlY3JldEVuY29kaW5nRm9yUmVsb2FkZXI=" + updatedData = "dGVzdFVwZGF0ZWRTZWNyZXRFbmNvZGluZ0ZvclJlbG9hZGVy" ) -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))] +func TestMain(m *testing.M) { + + logrus.Infof("Creating namespace %s", namespace) + testutil.CreateNamespace(namespace, client) + + logrus.Infof("Creating controller") + for k := range kube.ResourceMap { + c, err := NewController(client, k, namespace) + if err != nil { + logrus.Fatalf("%s", err) + } + + // Now let's start the controller + stop := make(chan struct{}) + defer close(stop) + go c.Run(1, stop) } - return string(b) + time.Sleep(5 * time.Second) + + logrus.Infof("Running Testcases") + retCode := m.Run() + + logrus.Infof("Deleting namespace %q.\n", namespace) + testutil.DeleteNamespace(namespace, client) + + os.Exit(retCode) } -// Creating a Controller to do a rolling upgrade upon updating the configmap or secret -func TestControllerForUpdatingConfigmapShouldUpdateDeployment(t *testing.T) { - client, err := kube.GetClient() +// Perform rolling upgrade on deployment and create env var upon updating the configmap +func TestControllerUpdatingConfigmapShouldCreateEnvInDeployment(t *testing.T) { + + // Creating configmap + configmapName := configmapNamePrefix + "-update-" + testutil.RandSeq(5) + configmapClient, err := testutil.CreateConfigMap(client, namespace, configmapName, "www.google.com") if err != nil { - logrus.Errorf("Unable to create Kubernetes client error = %v", err) - return + t.Errorf("Error while creating the configmap %v", err) } - namespace := "test-reloader" - createNamespace(t, namespace, client) - defer deleteNamespace(t, namespace, client) - controller, err := NewController(client, "configMaps", namespace) + // Creating deployment + _, err = testutil.CreateDeployment(client, configmapName, namespace) if err != nil { - logrus.Errorf("Unable to create NewController error = %v", err) - return + t.Errorf("Error in deployment creation: %v", err) } - stop := make(chan struct{}) - defer close(stop) - go controller.Run(1, stop) - time.Sleep(10 * time.Second) - - configmapName := configmapNamePrefix + "-update-" + randSeq(5) - configmapClient := client.CoreV1().ConfigMaps(namespace) - _, err = configmapClient.Create(initConfigmap(namespace, configmapName)) - if err != nil { - logrus.Fatalf("Fatal error in configmap creation: %v", err) - } - logrus.Infof("Created Configmap %q.\n", configmapName) - time.Sleep(10 * time.Second) - deployment := createDeployement(configmapName, namespace, client) - - logrus.Infof("Updating Configmap %q.\n", configmapName) - _, err = configmapClient.Get(configmapName, metav1.GetOptions{}) - if err != nil { - logrus.Errorf("Error while getting configmap %v", err) - } - _, updateErr := configmapClient.Update(updateConfigmap(namespace, configmapName)) - - // TODO: Add functionality to verify reloader functionality here + // Updating configmap for first time + updateErr := testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "www.stakater.com") if updateErr != nil { - err = controller.client.CoreV1().ConfigMaps(namespace).Delete(configmapName, &metav1.DeleteOptions{}) - if err != nil { - logrus.Errorf("Error while deleting the configmap %v", err) - } - logrus.Fatalf("Fatal error in configmap update: %v", updateErr) + t.Errorf("Configmap was not updated") } - time.Sleep(10 * time.Second) - logrus.Infof("Deleting Deployment %q.\n", deployment.GetObjectMeta().GetName()) - deploymentError := controller.client.ExtensionsV1beta1().Deployments(namespace).Delete(configmapName, &metav1.DeleteOptions{}) - if deploymentError != nil { - logrus.Fatalf("Error while deleting the configmap %v", deploymentError) + + // Verifying deployment update + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, } - logrus.Infof("Deleting Configmap %q.\n", configmapName) - err = controller.client.CoreV1().ConfigMaps(namespace).Delete(configmapName, &metav1.DeleteOptions{}) + deploymentFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs) + if !updated { + t.Errorf("Deployment was not updated") + } + time.Sleep(5 * time.Second) + + // Deleting deployment + err = testutil.DeleteDeployment(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the deployment %v", err) + } + + // Deleting configmap + err = testutil.DeleteConfigMap(client, namespace, configmapName) if err != nil { logrus.Errorf("Error while deleting the configmap %v", err) } - time.Sleep(15 * time.Second) + time.Sleep(5 * time.Second) } -func createDeployement(deploymentName string, namespace string, client kubernetes.Interface) *v1beta1.Deployment { - deploymentClient := client.ExtensionsV1beta1().Deployments(namespace) - deployment := initDeployment(namespace, deploymentName) - deployment, err := deploymentClient.Create(deployment) +// Perform rolling upgrade on deployment and update env var upon updating the configmap +func TestControllerForUpdatingConfigmapShouldUpdateDeployment(t *testing.T) { + // Creating secret + configmapName := configmapNamePrefix + "-update-" + testutil.RandSeq(5) + configmapClient, err := testutil.CreateConfigMap(client, namespace, configmapName, "www.google.com") if err != nil { - logrus.Fatalf("Fatal error in deployment creation: %v", err) + t.Errorf("Error while creating the configmap %v", err) } - logrus.Infof("Created Deployment %q.\n", deployment.GetObjectMeta().GetName()) - return deployment -} -func TestControllerForUpdatingSecretShouldUpdateDeployment(t *testing.T) { - client, err := kube.GetClient() + // Creating deployment + _, err = testutil.CreateDeployment(client, configmapName, namespace) if err != nil { - logrus.Errorf("Unable to create Kubernetes client error = %v", err) - return + t.Errorf("Error in deployment creation: %v", err) } - namespace := "test-reloader-secrets" - createNamespace(t, namespace, client) - defer deleteNamespace(t, namespace, client) - - controller, err := NewController(client, "secrets", namespace) - if err != nil { - logrus.Errorf("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) - - secretName := secretNamePrefix + "-update-" + randSeq(5) - secretClient := client.CoreV1().Secrets(namespace) - _, err = secretClient.Create(initSecret(namespace, secretName)) - if err != nil { - logrus.Fatalf("Fatal error in secret creation: %v", err) - } - logrus.Infof("Created Secret %q.\n", secretName) - time.Sleep(10 * time.Second) - - logrus.Infof("Updating Secret %q.\n", secretName) - _, err = secretClient.Get(secretName, metav1.GetOptions{}) - if err != nil { - logrus.Errorf("Error while getting secret %v", err) - } - _, updateErr := secretClient.Update(updateSecret(namespace, secretName)) - - // TODO: Add functionality to verify reloader functionality here + // Updating configmap for first time + updateErr := testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "www.stakater.com") if updateErr != nil { - err := controller.client.CoreV1().Secrets(namespace).Delete(secretName, &metav1.DeleteOptions{}) - if err != nil { - logrus.Errorf("Error while deleting the secret %v", err) - } - logrus.Errorf("Error while updating the secret %v", err) + t.Errorf("Configmap was not updated") } - time.Sleep(10 * time.Second) - logrus.Infof("Deleting Secret %q.\n", secretName) - err = controller.client.CoreV1().Secrets(namespace).Delete(secretName, &metav1.DeleteOptions{}) + + // Updating configmap for second time + updateErr = testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "aurorasolutions.io") + if updateErr != nil { + t.Errorf("Configmap was not updated") + } + + // Verifying deployment update + logrus.Infof("Verifying env var has been updated") + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + + deploymentFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + } + + updated := testutil.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs) + if !updated { + t.Errorf("Deployment was not updated") + } + time.Sleep(5 * time.Second) + + // Deleting deployment + err = testutil.DeleteDeployment(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the deployment %v", err) + } + + // Deleting configmap + err = testutil.DeleteConfigMap(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the configmap %v", err) + } + time.Sleep(5 * time.Second) +} + +// Do not Perform rolling upgrade on deployment and create env var upon updating the labels configmap +func TestControllerUpdatingConfigmapLabelsShouldNotCreateorUpdateEnvInDeployment(t *testing.T) { + // Creating configmap + configmapName := configmapNamePrefix + "-update-" + testutil.RandSeq(5) + configmapClient, err := testutil.CreateConfigMap(client, namespace, configmapName, "www.google.com") + if err != nil { + t.Errorf("Error while creating the configmap %v", err) + } + + // Creating deployment + _, err = testutil.CreateDeployment(client, configmapName, namespace) + if err != nil { + t.Errorf("Error in deployment creation: %v", err) + } + + // Updating configmap for first time + updateErr := testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "test", "www.google.com") + if updateErr != nil { + t.Errorf("Configmap was not updated") + } + + // Verifying deployment update + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.google.com") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + deploymentFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs) + if updated { + t.Errorf("Deployment should not be updated by changing label") + } + time.Sleep(5 * time.Second) + + // Deleting deployment + err = testutil.DeleteDeployment(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the deployment %v", err) + } + + // Deleting configmap + err = testutil.DeleteConfigMap(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the configmap %v", err) + } + time.Sleep(5 * time.Second) +} + +// Perform rolling upgrade on secret and create a env var upon updating the secret +func TestControllerUpdatingSecretShouldCreateEnvInDeployment(t *testing.T) { + // Creating secret + secretName := secretNamePrefix + "-update-" + testutil.RandSeq(5) + secretClient, err := testutil.CreateSecret(client, namespace, secretName, data) + if err != nil { + t.Errorf("Error in secret creation: %v", err) + } + + // Creating deployment + _, err = testutil.CreateDeployment(client, secretName, namespace) + if err != nil { + t.Errorf("Error in deployment creation: %v", err) + } + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", newData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Verifying Upgrade + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData) + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + deploymentFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, deploymentFuncs) + if !updated { + t.Errorf("Deployment was not updated") + } + //time.Sleep(5 * time.Second) + + // Deleting Deployment + err = testutil.DeleteDeployment(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the deployment %v", err) + } + + //Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) if err != nil { logrus.Errorf("Error while deleting the secret %v", err) } - time.Sleep(15 * time.Second) + time.Sleep(5 * 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"}, - }, - Data: map[string]string{"test.url": "www.google.com"}, - } -} - -func initDeployment(namespace string, deploymentName string) *v1beta1.Deployment { - replicaset := int32(1) - return &v1beta1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: deploymentName, - Namespace: namespace, - Labels: map[string]string{"firstLabel": "temp"}, - Annotations: map[string]string{"reloader.stakater.com/update-on-change": deploymentName}, - }, - Spec: v1beta1.DeploymentSpec{ - Replicas: &replicaset, - Strategy: v1beta1.DeploymentStrategy{ - Type: v1beta1.RollingUpdateDeploymentStrategyType, - }, - Template: v1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"secondLabel": "temp"}, - }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Image: "tutum/hello-world", - Name: deploymentName, - Env: []v1.EnvVar{ - { - Name: "BUCKET_NAME", - Value: "test", - }, - }, - }, - }, - }, - }, - }, - } -} - -func initSecret(namespace string, secretName string) *v1.Secret { - return &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - Labels: map[string]string{"firstLabel": "temp"}, - }, - Data: map[string][]byte{"test.url": []byte("dGVzdFNlY3JldEVuY29kaW5nRm9yUmVsb2FkZXI=")}, - } -} - -func createNamespace(t *testing.T, namespace string, client kubernetes.Interface) { - _, err := client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) +// Perform rolling upgrade on deployment and update env var upon updating the secret +func TestControllerUpdatingSecretShouldUpdateEnvInDeployment(t *testing.T) { + // Creating secret + secretName := secretNamePrefix + "-update-" + testutil.RandSeq(5) + secretClient, err := testutil.CreateSecret(client, namespace, secretName, data) if err != nil { - t.Error("Failed to create namespace for testing", err) - } else { - logrus.Infof("Creating namespace for testing = %s", namespace) + t.Errorf("Error in secret creation: %v", err) } -} -func deleteNamespace(t *testing.T, namespace string, client kubernetes.Interface) { - err := client.CoreV1().Namespaces().Delete(namespace, &metav1.DeleteOptions{}) + // Creating deployment + _, err = testutil.CreateDeployment(client, secretName, namespace) if err != nil { - t.Error("Failed to delete namespace that was created for testing", err) - } else { - logrus.Infof("Deleting namespace for testing = %s", namespace) + t.Errorf("Error in deployment creation: %v", err) } + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", newData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", updatedData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Verifying Upgrade + logrus.Infof("Verifying env var has been updated") + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData) + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + deploymentFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, deploymentFuncs) + if !updated { + t.Errorf("Deployment was not updated") + } + //time.Sleep(5 * time.Second) + + // Deleting Deployment + err = testutil.DeleteDeployment(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the deployment %v", err) + } + + //Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the secret %v", err) + } + time.Sleep(5 * time.Second) } -func updateConfigmap(namespace string, configmapName string) *v1.ConfigMap { - return &v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: configmapName, - Namespace: namespace, - Labels: map[string]string{"firstLabel": "temp"}, - }, - Data: map[string]string{"test.url": "www.stakater.com"}, +// Do not Perform rolling upgrade on secret and create or update a env var upon updating the label in secret +func TestControllerUpdatingSecretLabelsShouldNotCreateorUpdateEnvInDeployment(t *testing.T) { + // Creating secret + secretName := secretNamePrefix + "-update-" + testutil.RandSeq(5) + secretClient, err := testutil.CreateSecret(client, namespace, secretName, data) + if err != nil { + t.Errorf("Error in secret creation: %v", err) } + + // Creating deployment + _, err = testutil.CreateDeployment(client, secretName, namespace) + if err != nil { + t.Errorf("Error in deployment creation: %v", err) + } + + err = testutil.UpdateSecret(secretClient, namespace, secretName, "test", data) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Verifying Upgrade + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, data) + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + deploymentFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, deploymentFuncs) + if updated { + t.Errorf("Deployment should not be updated by changing label in secret") + } + //time.Sleep(5 * time.Second) + + // Deleting Deployment + err = testutil.DeleteDeployment(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the deployment %v", err) + } + + //Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the secret %v", err) + } + time.Sleep(5 * time.Second) } -func updateSecret(namespace string, secretName string) *v1.Secret { - return &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - Labels: map[string]string{"firstLabel": "temp"}, - }, - Data: map[string][]byte{"test.url": []byte("dGVzdFVwZGF0ZWRTZWNyZXRFbmNvZGluZ0ZvclJlbG9hZGVy")}, +// Perform rolling upgrade on DaemonSet and create env var upon updating the configmap +func TestControllerUpdatingConfigmapShouldCreateEnvInDaemonSet(t *testing.T) { + // Creating configmap + configmapName := configmapNamePrefix + "-update-" + testutil.RandSeq(5) + configmapClient, err := testutil.CreateConfigMap(client, namespace, configmapName, "www.google.com") + if err != nil { + t.Errorf("Error while creating the configmap %v", err) } + + // Creating DaemonSet + _, err = testutil.CreateDaemonSet(client, configmapName, namespace) + if err != nil { + t.Errorf("Error in DaemonSet creation: %v", err) + } + + // Updating configmap for first time + updateErr := testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "www.stakater.com") + if updateErr != nil { + t.Errorf("Configmap was not updated") + } + + // Verifying DaemonSet update + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + daemonSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDaemonSetItems, + ContainersFunc: callbacks.GetDaemonSetContainers, + UpdateFunc: callbacks.UpdateDaemonSet, + ResourceType: "DaemonSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, daemonSetFuncs) + if !updated { + t.Errorf("DaemonSet was not updated") + } + time.Sleep(5 * time.Second) + + // Deleting DaemonSet + err = testutil.DeleteDaemonSet(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the DaemonSet %v", err) + } + + // Deleting configmap + err = testutil.DeleteConfigMap(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the configmap %v", err) + } + time.Sleep(5 * time.Second) +} + +// Perform rolling upgrade on DaemonSet and update env var upon updating the configmap +func TestControllerForUpdatingConfigmapShouldUpdateDaemonSet(t *testing.T) { + // Creating secret + configmapName := configmapNamePrefix + "-update-" + testutil.RandSeq(5) + configmapClient, err := testutil.CreateConfigMap(client, namespace, configmapName, "www.google.com") + if err != nil { + t.Errorf("Error while creating the configmap %v", err) + } + + // Creating DaemonSet + _, err = testutil.CreateDaemonSet(client, configmapName, namespace) + if err != nil { + t.Errorf("Error in DaemonSet creation: %v", err) + } + + // Updating configmap for first time + updateErr := testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "www.stakater.com") + if updateErr != nil { + t.Errorf("Configmap was not updated") + } + + // Updating configmap for second time + updateErr = testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "aurorasolutions.io") + if updateErr != nil { + t.Errorf("Configmap was not updated") + } + + // Verifying DaemonSet update + logrus.Infof("Verifying env var has been updated") + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + daemonSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDaemonSetItems, + ContainersFunc: callbacks.GetDaemonSetContainers, + UpdateFunc: callbacks.UpdateDaemonSet, + ResourceType: "DaemonSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, daemonSetFuncs) + if !updated { + t.Errorf("DaemonSet was not updated") + } + time.Sleep(5 * time.Second) + + // Deleting DaemonSet + err = testutil.DeleteDaemonSet(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the DaemonSet %v", err) + } + + // Deleting configmap + err = testutil.DeleteConfigMap(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the configmap %v", err) + } + time.Sleep(5 * time.Second) +} + +// Perform rolling upgrade on secret and create a env var upon updating the secret +func TestControllerUpdatingSecretShouldCreateEnvInDaemonSet(t *testing.T) { + // Creating secret + secretName := secretNamePrefix + "-update-" + testutil.RandSeq(5) + secretClient, err := testutil.CreateSecret(client, namespace, secretName, data) + if err != nil { + t.Errorf("Error in secret creation: %v", err) + } + + // Creating DaemonSet + _, err = testutil.CreateDaemonSet(client, secretName, namespace) + if err != nil { + t.Errorf("Error in DaemonSet creation: %v", err) + } + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", newData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Verifying Upgrade + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData) + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + daemonSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDaemonSetItems, + ContainersFunc: callbacks.GetDaemonSetContainers, + UpdateFunc: callbacks.UpdateDaemonSet, + ResourceType: "DaemonSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, daemonSetFuncs) + if !updated { + t.Errorf("DaemonSet was not updated") + } + //time.Sleep(5 * time.Second) + + // Deleting DaemonSet + err = testutil.DeleteDaemonSet(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the DaemonSet %v", err) + } + + //Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the secret %v", err) + } + time.Sleep(5 * time.Second) +} + +// Perform rolling upgrade on DaemonSet and update env var upon updating the secret +func TestControllerUpdatingSecretShouldUpdateEnvInDaemonSet(t *testing.T) { + // Creating secret + secretName := secretNamePrefix + "-update-" + testutil.RandSeq(5) + secretClient, err := testutil.CreateSecret(client, namespace, secretName, data) + if err != nil { + t.Errorf("Error in secret creation: %v", err) + } + + // Creating DaemonSet + _, err = testutil.CreateDaemonSet(client, secretName, namespace) + if err != nil { + t.Errorf("Error in DaemonSet creation: %v", err) + } + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", newData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + time.Sleep(5 * time.Second) + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", updatedData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Verifying Upgrade + logrus.Infof("Verifying env var has been updated") + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData) + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + daemonSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDaemonSetItems, + ContainersFunc: callbacks.GetDaemonSetContainers, + UpdateFunc: callbacks.UpdateDaemonSet, + ResourceType: "DaemonSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, daemonSetFuncs) + if !updated { + t.Errorf("DaemonSet was not updated") + } + //time.Sleep(5 * time.Second) + + // Deleting DaemonSet + err = testutil.DeleteDaemonSet(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the DaemonSet %v", err) + } + + //Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the secret %v", err) + } + time.Sleep(5 * time.Second) +} + +// Do not Perform rolling upgrade on secret and create or update a env var upon updating the label in secret +func TestControllerUpdatingSecretLabelsShouldNotCreateorUpdateEnvInDaemonSet(t *testing.T) { + // Creating secret + secretName := secretNamePrefix + "-update-" + testutil.RandSeq(5) + secretClient, err := testutil.CreateSecret(client, namespace, secretName, data) + if err != nil { + t.Errorf("Error in secret creation: %v", err) + } + + // Creating DaemonSet + _, err = testutil.CreateDaemonSet(client, secretName, namespace) + if err != nil { + t.Errorf("Error in DaemonSet creation: %v", err) + } + + err = testutil.UpdateSecret(secretClient, namespace, secretName, "test", data) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Verifying Upgrade + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, data) + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + daemonSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDaemonSetItems, + ContainersFunc: callbacks.GetDaemonSetContainers, + UpdateFunc: callbacks.UpdateDaemonSet, + ResourceType: "DaemonSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, daemonSetFuncs) + if updated { + t.Errorf("DaemonSet should not be updated by changing label in secret") + } + //time.Sleep(5 * time.Second) + + // Deleting DaemonSet + err = testutil.DeleteDaemonSet(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the DaemonSet %v", err) + } + + //Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the secret %v", err) + } + time.Sleep(5 * time.Second) +} + +// Perform rolling upgrade on StatefulSet and create env var upon updating the configmap +func TestControllerUpdatingConfigmapShouldCreateEnvInStatefulSet(t *testing.T) { + // Creating configmap + configmapName := configmapNamePrefix + "-update-" + testutil.RandSeq(5) + configmapClient, err := testutil.CreateConfigMap(client, namespace, configmapName, "www.google.com") + if err != nil { + t.Errorf("Error while creating the configmap %v", err) + } + + // Creating StatefulSet + _, err = testutil.CreateStatefulSet(client, configmapName, namespace) + if err != nil { + t.Errorf("Error in StatefulSet creation: %v", err) + } + + // Updating configmap for first time + updateErr := testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "www.stakater.com") + if updateErr != nil { + t.Errorf("Configmap was not updated") + } + + // Verifying StatefulSet update + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + statefulSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetStatefulSetItems, + ContainersFunc: callbacks.GetStatefulsetContainers, + UpdateFunc: callbacks.UpdateStatefulset, + ResourceType: "StatefulSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, statefulSetFuncs) + if !updated { + t.Errorf("StatefulSet was not updated") + } + time.Sleep(5 * time.Second) + + // Deleting StatefulSet + err = testutil.DeleteStatefulSet(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the StatefulSet %v", err) + } + + // Deleting configmap + err = testutil.DeleteConfigMap(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the configmap %v", err) + } + time.Sleep(5 * time.Second) +} + +// Perform rolling upgrade on StatefulSet and update env var upon updating the configmap +func TestControllerForUpdatingConfigmapShouldUpdateStatefulSet(t *testing.T) { + // Creating secret + configmapName := configmapNamePrefix + "-update-" + testutil.RandSeq(5) + configmapClient, err := testutil.CreateConfigMap(client, namespace, configmapName, "www.google.com") + if err != nil { + t.Errorf("Error while creating the configmap %v", err) + } + + // Creating StatefulSet + _, err = testutil.CreateStatefulSet(client, configmapName, namespace) + if err != nil { + t.Errorf("Error in StatefulSet creation: %v", err) + } + + // Updating configmap for first time + updateErr := testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "www.stakater.com") + if updateErr != nil { + t.Errorf("Configmap was not updated") + } + + // Updating configmap for second time + updateErr = testutil.UpdateConfigMap(configmapClient, namespace, configmapName, "", "aurorasolutions.io") + if updateErr != nil { + t.Errorf("Configmap was not updated") + } + + // Verifying StatefulSet update + logrus.Infof("Verifying env var has been updated") + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + statefulSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetStatefulSetItems, + ContainersFunc: callbacks.GetStatefulsetContainers, + UpdateFunc: callbacks.UpdateStatefulset, + ResourceType: "StatefulSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, statefulSetFuncs) + if !updated { + t.Errorf("StatefulSet was not updated") + } + time.Sleep(5 * time.Second) + + // Deleting StatefulSet + err = testutil.DeleteStatefulSet(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the StatefulSet %v", err) + } + + // Deleting configmap + err = testutil.DeleteConfigMap(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the configmap %v", err) + } + time.Sleep(5 * time.Second) +} + +// Perform rolling upgrade on secret and create a env var upon updating the secret +func TestControllerUpdatingSecretShouldCreateEnvInStatefulSet(t *testing.T) { + // Creating secret + secretName := secretNamePrefix + "-update-" + testutil.RandSeq(5) + secretClient, err := testutil.CreateSecret(client, namespace, secretName, data) + if err != nil { + t.Errorf("Error in secret creation: %v", err) + } + + // Creating StatefulSet + _, err = testutil.CreateStatefulSet(client, secretName, namespace) + if err != nil { + t.Errorf("Error in StatefulSet creation: %v", err) + } + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", newData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Verifying Upgrade + logrus.Infof("Verifying env var has been created") + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData) + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + statefulSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetStatefulSetItems, + ContainersFunc: callbacks.GetStatefulsetContainers, + UpdateFunc: callbacks.UpdateStatefulset, + ResourceType: "StatefulSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, statefulSetFuncs) + if !updated { + t.Errorf("StatefulSet was not updated") + } + //time.Sleep(5 * time.Second) + + // Deleting StatefulSet + err = testutil.DeleteStatefulSet(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the StatefulSet %v", err) + } + + //Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the secret %v", err) + } + time.Sleep(5 * time.Second) +} + +// Perform rolling upgrade on StatefulSet and update env var upon updating the secret +func TestControllerUpdatingSecretShouldUpdateEnvInStatefulSet(t *testing.T) { + // Creating secret + secretName := secretNamePrefix + "-update-" + testutil.RandSeq(5) + secretClient, err := testutil.CreateSecret(client, namespace, secretName, data) + if err != nil { + t.Errorf("Error in secret creation: %v", err) + } + + // Creating StatefulSet + _, err = testutil.CreateStatefulSet(client, secretName, namespace) + if err != nil { + t.Errorf("Error in StatefulSet creation: %v", err) + } + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", newData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Updating Secret + err = testutil.UpdateSecret(secretClient, namespace, secretName, "", updatedData) + if err != nil { + t.Errorf("Error while updating secret %v", err) + } + + // Verifying Upgrade + logrus.Infof("Verifying env var has been updated") + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData) + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + statefulSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetStatefulSetItems, + ContainersFunc: callbacks.GetStatefulsetContainers, + UpdateFunc: callbacks.UpdateStatefulset, + ResourceType: "StatefulSet", + } + updated := testutil.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, statefulSetFuncs) + if !updated { + t.Errorf("StatefulSet was not updated") + } + //time.Sleep(5 * time.Second) + + // Deleting StatefulSet + err = testutil.DeleteStatefulSet(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the StatefulSet %v", err) + } + + //Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the secret %v", err) + } + time.Sleep(5 * time.Second) } diff --git a/internal/pkg/crypto/sha.go b/internal/pkg/crypto/sha.go new file mode 100644 index 00000000..043fc227 --- /dev/null +++ b/internal/pkg/crypto/sha.go @@ -0,0 +1,20 @@ +package crypto + +import ( + "crypto/sha1" + "fmt" + "io" + + "github.com/sirupsen/logrus" +) + +// GenerateSHA generates SHA from string +func GenerateSHA(data string) string { + hasher := sha1.New() + _, err := io.WriteString(hasher, data) + if err != nil { + logrus.Errorf("Unable to write data in hash writer %v", err) + } + sha := hasher.Sum(nil) + return fmt.Sprintf("%x", sha) +} diff --git a/internal/pkg/crypto/sha_test.go b/internal/pkg/crypto/sha_test.go new file mode 100644 index 00000000..60d5af63 --- /dev/null +++ b/internal/pkg/crypto/sha_test.go @@ -0,0 +1,15 @@ +package crypto + +import ( + "testing" +) + +// TestGenerateSHA generates the sha from given data and verifies whether it is correct or not +func TestGenerateSHA(t *testing.T) { + data := "www.stakater.com" + sha := "abd4ed82fb04548388a6cf3c339fd9dc84d275df" + result := GenerateSHA(data) + if result != sha { + t.Errorf("Failed to generate SHA") + } +} diff --git a/internal/pkg/handler/created-handler.go b/internal/pkg/handler/create.go similarity index 74% rename from internal/pkg/handler/created-handler.go rename to internal/pkg/handler/create.go index ac612c4b..67d1debc 100644 --- a/internal/pkg/handler/created-handler.go +++ b/internal/pkg/handler/create.go @@ -18,11 +18,11 @@ func (r ResourceCreatedHandler) Handle() error { logrus.Infof("Detected changes in object %s", r.Resource) // process resource based on its type if _, ok := r.Resource.(*v1.ConfigMap); ok { - logrus.Infof("Performing 'Added' action for resource of type 'configmap'") + logrus.Infof("A 'configmap' has been 'Added' but no implementation found to take action") } else if _, ok := r.Resource.(*v1.Secret); ok { - logrus.Infof("Performing 'Added' action for resource of type 'secret'") + logrus.Infof("A 'secret' has been 'Added' but no implementation found to take action") } else { - logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found %v", r.Resource) + logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource) } } return nil diff --git a/internal/pkg/handler/update.go b/internal/pkg/handler/update.go new file mode 100644 index 00000000..d8bda1b4 --- /dev/null +++ b/internal/pkg/handler/update.go @@ -0,0 +1,194 @@ +package handler + +import ( + "sort" + "strings" + + "github.com/sirupsen/logrus" + "github.com/stakater/Reloader/internal/pkg/callbacks" + "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" + "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" +) + +// ResourceUpdatedHandler contains updated objects +type ResourceUpdatedHandler struct { + Resource interface{} + OldResource interface{} +} + +// Handle processes the updated resource +func (r ResourceUpdatedHandler) Handle() error { + if r.Resource == nil || r.OldResource == nil { + logrus.Errorf("Error in Handler") + } else { + logrus.Infof("Detected changes in object %s", r.Resource) + // process resource based on its type + rollingUpgrade(r, callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + }) + rollingUpgrade(r, callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDaemonSetItems, + ContainersFunc: callbacks.GetDaemonSetContainers, + UpdateFunc: callbacks.UpdateDaemonSet, + ResourceType: "DaemonSet", + }) + rollingUpgrade(r, callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetStatefulSetItems, + ContainersFunc: callbacks.GetStatefulsetContainers, + UpdateFunc: callbacks.UpdateStatefulset, + ResourceType: "StatefulSet", + }) + } + return nil +} + +func rollingUpgrade(r ResourceUpdatedHandler, upgradeFuncs callbacks.RollingUpgradeFuncs) { + client, err := kube.GetClient() + if err != nil { + logrus.Fatalf("Unable to create Kubernetes client error = %v", err) + } + + config, envVarPostfix, oldSHAData := getConfig(r) + + if config.SHAValue != oldSHAData { + err = PerformRollingUpgrade(client, config, envVarPostfix, upgradeFuncs) + if err != nil { + logrus.Fatalf("Rolling upgrade failed with error = %v", err) + } + } else { + logrus.Infof("Rolling upgrade will not happend because no actual change in data has been detected") + } +} + +func getConfig(r ResourceUpdatedHandler) (util.Config, string, string) { + var oldSHAData, envVarPostfix string + var config util.Config + if _, ok := r.Resource.(*v1.ConfigMap); ok { + logrus.Infof("Performing 'Updated' action for resource of type 'configmap'") + oldSHAData = getSHAfromConfigmap(r.OldResource.(*v1.ConfigMap).Data) + config = getConfigmapConfig(r) + envVarPostfix = constants.ConfigmapEnvVarPostfix + } else if _, ok := r.Resource.(*v1.Secret); ok { + logrus.Infof("Performing 'Updated' action for resource of type 'secret'") + oldSHAData = getSHAfromSecret(r.OldResource.(*v1.Secret).Data) + config = getSecretConfig(r) + envVarPostfix = constants.SecretEnvVarPostfix + } else { + logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource) + } + return config, envVarPostfix, oldSHAData +} + +func getConfigmapConfig(r ResourceUpdatedHandler) util.Config { + configmap := r.Resource.(*v1.ConfigMap) + return util.Config{ + Namespace: configmap.Namespace, + ResourceName: configmap.Name, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + SHAValue: getSHAfromConfigmap(configmap.Data), + } +} + +func getSecretConfig(r ResourceUpdatedHandler) util.Config { + secret := r.Resource.(*v1.Secret) + return util.Config{ + Namespace: secret.Namespace, + ResourceName: secret.Name, + Annotation: constants.SecretUpdateOnChangeAnnotation, + SHAValue: getSHAfromSecret(secret.Data), + } +} + +// PerformRollingUpgrade upgrades the deployment if there is any change in configmap or secret data +func PerformRollingUpgrade(client kubernetes.Interface, config util.Config, envarPostfix string, upgradeFuncs callbacks.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 %s failed %v", upgradeFuncs.ResourceType, err) + } else { + logrus.Infof("Updated %s of type %s", config.ResourceName, upgradeFuncs.ResourceType) + } + break + } + } + } + } + } + return err +} + +func updateContainers(containers []v1.Container, annotationValue string, shaData string, envarPostfix string) bool { + updated := false + envar := constants.EnvVarPrefix + util.ConvertToEnvVarName(annotationValue) + envarPostfix + logrus.Infof("Generated environment variable: %s", envar) + for i := range containers { + envs := containers[i].Env + + //update if env var exists + updated = updateEnvVar(envs, envar, shaData) + + // if no existing env var exists lets create one + if !updated { + e := v1.EnvVar{ + Name: envar, + Value: shaData, + } + containers[i].Env = append(containers[i].Env, e) + updated = true + logrus.Infof("%s environment variable does not exist, creating a new envVar", envar) + } + } + return updated +} + +func updateEnvVar(envs []v1.EnvVar, envar string, shaData string) bool { + for j := range envs { + if envs[j].Name == envar { + logrus.Infof("%s environment variable found", envar) + if envs[j].Value != shaData { + logrus.Infof("Updating %s", envar) + envs[j].Value = shaData + return true + } + } + } + return false +} + +func getSHAfromConfigmap(data map[string]string) string { + values := []string{} + 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{} + 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 new file mode 100644 index 00000000..2e9c6f1c --- /dev/null +++ b/internal/pkg/handler/update_test.go @@ -0,0 +1,315 @@ +package handler + +import ( + "os" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stakater/Reloader/internal/pkg/callbacks" + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/testutil" + "github.com/stakater/Reloader/internal/pkg/util" + testclient "k8s.io/client-go/kubernetes/fake" +) + +var ( + client = testclient.NewSimpleClientset() + namespace = "test-handler" + configmapName = "testconfigmap-handler-" + testutil.RandSeq(5) + secretName = "testsecret-handler-" + testutil.RandSeq(5) +) + +func TestMain(m *testing.M) { + + logrus.Infof("Creating namespace %s", namespace) + testutil.CreateNamespace(namespace, client) + + logrus.Infof("Setting up the test resources") + setup() + + logrus.Infof("Running Testcases") + retCode := m.Run() + + logrus.Infof("tearing down the test resources") + teardown() + + os.Exit(retCode) +} + +func setup() { + // Creating configmap + _, err := testutil.CreateConfigMap(client, namespace, configmapName, "www.google.com") + if err != nil { + logrus.Errorf("Error in configmap creation: %v", err) + } + + // Creating secret + data := "dGVzdFNlY3JldEVuY29kaW5nRm9yUmVsb2FkZXI=" + _, err = testutil.CreateSecret(client, namespace, secretName, data) + if err != nil { + logrus.Errorf("Error in secret creation: %v", err) + } + + // Creating Deployment with configmap + _, err = testutil.CreateDeployment(client, configmapName, namespace) + if err != nil { + logrus.Errorf("Error in Deployment with configmap creation: %v", err) + } + + // Creating Deployment with secret + _, err = testutil.CreateDeployment(client, secretName, namespace) + if err != nil { + logrus.Errorf("Error in Deployment with secret creation: %v", err) + } + + // Creating DaemonSet with configmap + _, err = testutil.CreateDaemonSet(client, configmapName, namespace) + if err != nil { + logrus.Errorf("Error in DaemonSet with configmap creation: %v", err) + } + + // Creating DaemonSet with secret + _, err = testutil.CreateDaemonSet(client, secretName, namespace) + if err != nil { + logrus.Errorf("Error in DaemonSet with secret creation: %v", err) + } + + // Creating StatefulSet with configmap + _, err = testutil.CreateStatefulSet(client, configmapName, namespace) + if err != nil { + logrus.Errorf("Error in StatefulSet with configmap creation: %v", err) + } + + // Creating StatefulSet with secret + _, err = testutil.CreateStatefulSet(client, secretName, namespace) + if err != nil { + logrus.Errorf("Error in StatefulSet with secret creation: %v", err) + } + +} + +func teardown() { + // Deleting Deployment with configmap + deploymentError := testutil.DeleteDeployment(client, namespace, configmapName) + if deploymentError != nil { + logrus.Errorf("Error while deleting deployment with configmap %v", deploymentError) + } + + // Deleting Deployment with secret + deploymentError = testutil.DeleteDeployment(client, namespace, secretName) + if deploymentError != nil { + logrus.Errorf("Error while deleting deployment with secret %v", deploymentError) + } + + // Deleting DaemonSet with configmap + daemonSetError := testutil.DeleteDaemonSet(client, namespace, configmapName) + if daemonSetError != nil { + logrus.Errorf("Error while deleting daemonSet with configmap %v", daemonSetError) + } + + // Deleting Deployment with secret + daemonSetError = testutil.DeleteDaemonSet(client, namespace, secretName) + if daemonSetError != nil { + logrus.Errorf("Error while deleting daemonSet with secret %v", daemonSetError) + } + + // Deleting StatefulSet with configmap + statefulSetError := testutil.DeleteStatefulSet(client, namespace, configmapName) + if statefulSetError != nil { + logrus.Errorf("Error while deleting statefulSet with configmap %v", statefulSetError) + } + + // Deleting Deployment with secret + statefulSetError = testutil.DeleteStatefulSet(client, namespace, secretName) + if statefulSetError != nil { + logrus.Errorf("Error while deleting statefulSet with secret %v", statefulSetError) + } + + // Deleting Configmap + err := testutil.DeleteConfigMap(client, namespace, configmapName) + if err != nil { + logrus.Errorf("Error while deleting the configmap %v", err) + } + + // Deleting Secret + err = testutil.DeleteSecret(client, namespace, secretName) + if err != nil { + logrus.Errorf("Error while deleting the secret %v", err) + } + + // Deleting namespace + testutil.DeleteNamespace(namespace, client) + +} + +func TestRollingUpgradeForDeploymentWithConfigmap(t *testing.T) { + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, configmapName, "www.stakater.com") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + deploymentFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + } + + err := PerformRollingUpgrade(client, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs) + time.Sleep(5 * time.Second) + if err != nil { + t.Errorf("Rolling upgrade failed for Deployment with Configmap") + } + + logrus.Infof("Verifying deployment update") + updated := testutil.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs) + if !updated { + t.Errorf("Deployment was not updated") + } +} + +func TestRollingUpgradeForDeploymentWithSecret(t *testing.T) { + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, "dGVzdFVwZGF0ZWRTZWNyZXRFbmNvZGluZ0ZvclJlbG9hZGVy") + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + deploymentFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDeploymentItems, + ContainersFunc: callbacks.GetDeploymentContainers, + UpdateFunc: callbacks.UpdateDeployment, + ResourceType: "Deployment", + } + + err := PerformRollingUpgrade(client, config, constants.SecretEnvVarPostfix, 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.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, deploymentFuncs) + if !updated { + t.Errorf("Deployment was not updated") + } +} + +func TestRollingUpgradeForDaemonSetWithConfigmap(t *testing.T) { + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.facebook.com") + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + daemonSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDaemonSetItems, + ContainersFunc: callbacks.GetDaemonSetContainers, + UpdateFunc: callbacks.UpdateDaemonSet, + ResourceType: "DaemonSet", + } + + err := PerformRollingUpgrade(client, config, constants.ConfigmapEnvVarPostfix, 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.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, daemonSetFuncs) + if !updated { + t.Errorf("DaemonSet was not updated") + } +} + +func TestRollingUpgradeForDaemonSetWithSecret(t *testing.T) { + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, "d3d3LmZhY2Vib29rLmNvbQ==") + + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + daemonSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetDaemonSetItems, + ContainersFunc: callbacks.GetDaemonSetContainers, + UpdateFunc: callbacks.UpdateDaemonSet, + ResourceType: "DaemonSet", + } + + err := PerformRollingUpgrade(client, config, constants.SecretEnvVarPostfix, 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.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, daemonSetFuncs) + if !updated { + t.Errorf("DaemonSet was not updated") + } +} + +func TestRollingUpgradeForStatefulSetWithConfigmap(t *testing.T) { + shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.twitter.com") + + config := util.Config{ + Namespace: namespace, + ResourceName: configmapName, + SHAValue: shaData, + Annotation: constants.ConfigmapUpdateOnChangeAnnotation, + } + statefulSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetStatefulSetItems, + ContainersFunc: callbacks.GetStatefulsetContainers, + UpdateFunc: callbacks.UpdateStatefulset, + ResourceType: "StatefulSet", + } + + err := PerformRollingUpgrade(client, config, constants.ConfigmapEnvVarPostfix, 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.VerifyResourceUpdate(client, config, constants.ConfigmapEnvVarPostfix, statefulSetFuncs) + if !updated { + t.Errorf("StatefulSet was not updated") + } +} + +func TestRollingUpgradeForStatefulSetWithSecret(t *testing.T) { + shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, "d3d3LnR3aXR0ZXIuY29t") + + config := util.Config{ + Namespace: namespace, + ResourceName: secretName, + SHAValue: shaData, + Annotation: constants.SecretUpdateOnChangeAnnotation, + } + statefulSetFuncs := callbacks.RollingUpgradeFuncs{ + ItemsFunc: callbacks.GetStatefulSetItems, + ContainersFunc: callbacks.GetStatefulsetContainers, + UpdateFunc: callbacks.UpdateStatefulset, + ResourceType: "StatefulSet", + } + + err := PerformRollingUpgrade(client, config, constants.SecretEnvVarPostfix, 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.VerifyResourceUpdate(client, config, constants.SecretEnvVarPostfix, statefulSetFuncs) + if !updated { + t.Errorf("StatefulSet was not updated") + } +} diff --git a/internal/pkg/handler/updated-handler.go b/internal/pkg/handler/updated-handler.go deleted file mode 100644 index 97d4f7ba..00000000 --- a/internal/pkg/handler/updated-handler.go +++ /dev/null @@ -1,30 +0,0 @@ -package handler - -import ( - "github.com/sirupsen/logrus" - "k8s.io/api/core/v1" -) - -// ResourceUpdatedHandler contains updated objects -type ResourceUpdatedHandler struct { - Resource interface{} - OldResource interface{} -} - -// Handle processes the updated resource -func (r ResourceUpdatedHandler) Handle() error { - if r.Resource == nil || r.OldResource == nil { - logrus.Errorf("Error in Handler") - } else { - logrus.Infof("Detected changes in object %s", r.Resource) - // process resource based on its type - if _, ok := r.Resource.(*v1.ConfigMap); ok { - logrus.Infof("Performing 'Updated' action for resource of type 'configmap'") - } else if _, ok := r.Resource.(*v1.Secret); ok { - logrus.Infof("Performing 'Updated' action for resource of type 'secret'") - } else { - logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found %v", r.Resource) - } - } - return nil -} diff --git a/internal/pkg/testutil/kube.go b/internal/pkg/testutil/kube.go new file mode 100644 index 00000000..040f92cc --- /dev/null +++ b/internal/pkg/testutil/kube.go @@ -0,0 +1,404 @@ +package testutil + +import ( + "math/rand" + "sort" + "strings" + "time" + + "github.com/sirupsen/logrus" + "github.com/stakater/Reloader/internal/pkg/callbacks" + "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" + v1_beta1 "k8s.io/api/apps/v1beta1" + "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + core_v1 "k8s.io/client-go/kubernetes/typed/core/v1" +) + +var ( + letters = []rune("abcdefghijklmnopqrstuvwxyz") + // ConfigmapResourceType is a resource type which controller watches for changes + ConfigmapResourceType = "configMaps" + // SecretResourceType is a resource type which controller watches for changes + SecretResourceType = "secrets" +) + +func GetClient() *kubernetes.Clientset { + newClient, err := kube.GetClient() + if err != nil { + logrus.Fatalf("Unable to create Kubernetes client error = %v", err) + } + return newClient +} + +// CreateNamespace creates namespace for testing +func CreateNamespace(namespace string, client kubernetes.Interface) { + _, err := client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) + if err != nil { + logrus.Fatalf("Failed to create namespace for testing", err) + } else { + logrus.Infof("Creating namespace for testing = %s", namespace) + } +} + +// DeleteNamespace deletes namespace for testing +func DeleteNamespace(namespace string, client kubernetes.Interface) { + err := client.CoreV1().Namespaces().Delete(namespace, &metav1.DeleteOptions{}) + if err != nil { + logrus.Fatalf("Failed to delete namespace that was created for testing", err) + } else { + logrus.Infof("Deleting namespace for testing = %s", namespace) + } +} + +// GetDeployment provides deployment for testing +func GetDeployment(namespace string, deploymentName string) *v1beta1.Deployment { + replicaset := int32(1) + return &v1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": "temp"}, + Annotations: map[string]string{ + constants.ConfigmapUpdateOnChangeAnnotation: deploymentName, + constants.SecretUpdateOnChangeAnnotation: deploymentName}, + }, + Spec: v1beta1.DeploymentSpec{ + Replicas: &replicaset, + Strategy: v1beta1.DeploymentStrategy{ + Type: v1beta1.RollingUpdateDeploymentStrategyType, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"secondLabel": "temp"}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Image: "tutum/hello-world", + Name: deploymentName, + Env: []v1.EnvVar{ + { + Name: "BUCKET_NAME", + Value: "test", + }, + }, + }, + }, + }, + }, + }, + } +} + +// GetDaemonSet provides daemonset for testing +func GetDaemonSet(namespace string, daemonsetName string) *v1beta1.DaemonSet { + return &v1beta1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: daemonsetName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": "temp"}, + Annotations: map[string]string{ + constants.ConfigmapUpdateOnChangeAnnotation: daemonsetName, + constants.SecretUpdateOnChangeAnnotation: daemonsetName}, + }, + Spec: v1beta1.DaemonSetSpec{ + UpdateStrategy: v1beta1.DaemonSetUpdateStrategy{ + Type: v1beta1.RollingUpdateDaemonSetStrategyType, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"secondLabel": "temp"}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Image: "tutum/hello-world", + Name: daemonsetName, + Env: []v1.EnvVar{ + { + Name: "BUCKET_NAME", + Value: "test", + }, + }, + }, + }, + }, + }, + }, + } +} + +// GetStatefulSet provides statefulset for testing +func GetStatefulSet(namespace string, statefulsetName string) *v1_beta1.StatefulSet { + return &v1_beta1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: statefulsetName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": "temp"}, + Annotations: map[string]string{ + constants.ConfigmapUpdateOnChangeAnnotation: statefulsetName, + constants.SecretUpdateOnChangeAnnotation: statefulsetName}, + }, + Spec: v1_beta1.StatefulSetSpec{ + UpdateStrategy: v1_beta1.StatefulSetUpdateStrategy{ + Type: v1_beta1.RollingUpdateStatefulSetStrategyType, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"secondLabel": "temp"}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Image: "tutum/hello-world", + Name: statefulsetName, + Env: []v1.EnvVar{ + { + Name: "BUCKET_NAME", + Value: "test", + }, + }, + }, + }, + }, + }, + }, + } +} + +// GetConfigmap provides configmap for testing +func GetConfigmap(namespace string, configmapName string, testData string) *v1.ConfigMap { + return &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configmapName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": "temp"}, + }, + Data: map[string]string{"test.url": testData}, + } +} + +// GetConfigmapWithUpdatedLabel provides configmap for testing +func GetConfigmapWithUpdatedLabel(namespace string, configmapName string, testLabel string, testData string) *v1.ConfigMap { + return &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configmapName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": testLabel}, + }, + Data: map[string]string{"test.url": testData}, + } +} + +// GetSecret provides secret for testing +func GetSecret(namespace string, secretName string, data string) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": "temp"}, + }, + Data: map[string][]byte{"test.url": []byte(data)}, + } +} + +// GetSecretWithUpdatedLabel provides secret for testing +func GetSecretWithUpdatedLabel(namespace string, secretName string, label string, data string) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Labels: map[string]string{"firstLabel": label}, + }, + Data: map[string][]byte{"test.url": []byte(data)}, + } +} + +// GetResourceSHA returns the SHA value of given environment variable +func GetResourceSHA(containers []v1.Container, envar string) string { + for i := range containers { + envs := containers[i].Env + for j := range envs { + if envs[j].Name == envar { + return envs[j].Value + } + } + } + return "" +} + +//ConvertResourceToSHA generates SHA from secret or configmap data +func ConvertResourceToSHA(resourceType string, namespace string, resourceName string, data string) string { + values := []string{} + logrus.Infof("Generating SHA for secret data") + if resourceType == SecretResourceType { + secret := GetSecret(namespace, resourceName, data) + for k, v := range secret.Data { + values = append(values, k+"="+string(v[:])) + } + } else if resourceType == ConfigmapResourceType { + configmap := GetConfigmap(namespace, resourceName, data) + for k, v := range configmap.Data { + values = append(values, k+"="+v) + } + } + sort.Strings(values) + return crypto.GenerateSHA(strings.Join(values, ";")) +} + +// CreateConfigMap creates a configmap in given namespace and returns the ConfigMapInterface +func CreateConfigMap(client kubernetes.Interface, namespace string, configmapName string, data string) (core_v1.ConfigMapInterface, error) { + logrus.Infof("Creating configmap") + configmapClient := client.CoreV1().ConfigMaps(namespace) + _, err := configmapClient.Create(GetConfigmap(namespace, configmapName, data)) + time.Sleep(5 * time.Second) + return configmapClient, err +} + +// CreateSecret creates a secret in given namespace and returns the SecretInterface +func CreateSecret(client kubernetes.Interface, namespace string, secretName string, data string) (core_v1.SecretInterface, error) { + logrus.Infof("Creating secret") + secretClient := client.CoreV1().Secrets(namespace) + _, err := secretClient.Create(GetSecret(namespace, secretName, data)) + time.Sleep(5 * time.Second) + return secretClient, err +} + +// CreateDeployment creates a deployment in given namespace and returns the Deployment +func CreateDeployment(client kubernetes.Interface, deploymentName string, namespace string) (*v1beta1.Deployment, error) { + logrus.Infof("Creating Deployment") + deploymentClient := client.ExtensionsV1beta1().Deployments(namespace) + deployment, err := deploymentClient.Create(GetDeployment(namespace, deploymentName)) + time.Sleep(5 * time.Second) + return deployment, err +} + +// CreateDaemonSet creates a deployment in given namespace and returns the DaemonSet +func CreateDaemonSet(client kubernetes.Interface, daemonsetName string, namespace string) (*v1beta1.DaemonSet, error) { + logrus.Infof("Creating DaemonSet") + daemonsetClient := client.ExtensionsV1beta1().DaemonSets(namespace) + daemonset, err := daemonsetClient.Create(GetDaemonSet(namespace, daemonsetName)) + time.Sleep(5 * time.Second) + return daemonset, err +} + +// CreateStatefulSet creates a deployment in given namespace and returns the StatefulSet +func CreateStatefulSet(client kubernetes.Interface, statefulsetName string, namespace string) (*v1_beta1.StatefulSet, error) { + logrus.Infof("Creating StatefulSet") + statefulsetClient := client.AppsV1beta1().StatefulSets(namespace) + statefulset, err := statefulsetClient.Create(GetStatefulSet(namespace, statefulsetName)) + time.Sleep(5 * time.Second) + return statefulset, err +} + +// DeleteDeployment creates a deployment in given namespace and returns the error if any +func DeleteDeployment(client kubernetes.Interface, namespace string, deploymentName string) error { + logrus.Infof("Deleting Deployment") + deploymentError := client.ExtensionsV1beta1().Deployments(namespace).Delete(deploymentName, &metav1.DeleteOptions{}) + time.Sleep(5 * time.Second) + return deploymentError +} + +// DeleteDaemonSet creates a daemonset in given namespace and returns the error if any +func DeleteDaemonSet(client kubernetes.Interface, namespace string, daemonsetName string) error { + logrus.Infof("Deleting DaemonSet %s", daemonsetName) + daemonsetError := client.ExtensionsV1beta1().DaemonSets(namespace).Delete(daemonsetName, &metav1.DeleteOptions{}) + time.Sleep(5 * time.Second) + return daemonsetError +} + +// DeleteStatefulSet creates a statefulset in given namespace and returns the error if any +func DeleteStatefulSet(client kubernetes.Interface, namespace string, statefulsetName string) error { + logrus.Infof("Deleting StatefulSet %s", statefulsetName) + statefulsetError := client.AppsV1beta1().StatefulSets(namespace).Delete(statefulsetName, &metav1.DeleteOptions{}) + time.Sleep(5 * time.Second) + return statefulsetError +} + +// UpdateConfigMap updates a configmap in given namespace and returns the error if any +func UpdateConfigMap(configmapClient core_v1.ConfigMapInterface, namespace string, configmapName string, label string, data string) error { + logrus.Infof("Updating configmap %q.\n", configmapName) + var configmap *v1.ConfigMap + if label != "" { + configmap = GetConfigmapWithUpdatedLabel(namespace, configmapName, label, data) + } else { + configmap = GetConfigmap(namespace, configmapName, data) + } + _, updateErr := configmapClient.Update(configmap) + time.Sleep(5 * time.Second) + return updateErr +} + +// UpdateSecret updates a secret in given namespace and returns the error if any +func UpdateSecret(secretClient core_v1.SecretInterface, namespace string, secretName string, label string, data string) error { + logrus.Infof("Updating secret %q.\n", secretName) + var secret *v1.Secret + if label != "" { + secret = GetSecretWithUpdatedLabel(namespace, secretName, label, data) + } else { + secret = GetSecret(namespace, secretName, data) + } + _, updateErr := secretClient.Update(secret) + time.Sleep(5 * time.Second) + return updateErr +} + +// DeleteConfigMap deletes a configmap in given namespace and returns the error if any +func DeleteConfigMap(client kubernetes.Interface, namespace string, configmapName string) error { + logrus.Infof("Deleting configmap %q.\n", configmapName) + err := client.CoreV1().ConfigMaps(namespace).Delete(configmapName, &metav1.DeleteOptions{}) + time.Sleep(5 * time.Second) + return err +} + +// DeleteSecret deletes a secret in given namespace and returns the error if any +func DeleteSecret(client kubernetes.Interface, namespace string, secretName string) error { + logrus.Infof("Deleting secret %q.\n", secretName) + err := client.CoreV1().Secrets(namespace).Delete(secretName, &metav1.DeleteOptions{}) + time.Sleep(5 * time.Second) + return err +} + +// RandSeq generates a random sequence +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) +} + +func VerifyResourceUpdate(client kubernetes.Interface, config util.Config, envVarPostfix string, upgradeFuncs callbacks.RollingUpgradeFuncs) bool { + items := upgradeFuncs.ItemsFunc(client, config.Namespace) + for _, i := range items { + containers := upgradeFuncs.ContainersFunc(i) + // match statefulsets with the correct annotation + annotationValue := util.ToObjectMeta(i).Annotations[config.Annotation] + if annotationValue != "" { + values := strings.Split(annotationValue, ",") + matches := false + for _, value := range values { + if value == config.ResourceName { + matches = true + break + } + } + if matches { + envName := constants.EnvVarPrefix + util.ConvertToEnvVarName(annotationValue) + envVarPostfix + updated := GetResourceSHA(containers, envName) + + if updated == config.SHAValue { + return true + } + } + } + } + return false +} diff --git a/internal/pkg/util/config.go b/internal/pkg/util/config.go new file mode 100644 index 00000000..19577d35 --- /dev/null +++ b/internal/pkg/util/config.go @@ -0,0 +1,9 @@ +package util + +//Config contains rolling upgrade configuration parameters +type Config struct { + Namespace string + ResourceName string + Annotation string + SHAValue string +} 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, + } +} diff --git a/internal/pkg/util/util.go b/internal/pkg/util/util.go new file mode 100644 index 00000000..3368a24c --- /dev/null +++ b/internal/pkg/util/util.go @@ -0,0 +1,27 @@ +package util + +import ( + "bytes" + "strings" +) + +// 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 { + var buffer bytes.Buffer + upper := strings.ToUpper(text) + lastCharValid := false + for i := 0; i < len(upper); i++ { + ch := upper[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() +} diff --git a/internal/pkg/util/util_test.go b/internal/pkg/util/util_test.go new file mode 100644 index 00000000..d635fb42 --- /dev/null +++ b/internal/pkg/util/util_test.go @@ -0,0 +1,13 @@ +package util + +import ( + "testing" +) + +func TestConvertToEnvVarName(t *testing.T) { + data := "www.stakater.com" + envVar := ConvertToEnvVarName(data) + if envVar != "WWW_STAKATER_COM" { + t.Errorf("Failed to convert data into environment variable") + } +}