From 8fbaa640cf159e5205d8a8831b105424e32556e1 Mon Sep 17 00:00:00 2001 From: SteveMin Date: Tue, 23 Apr 2019 10:31:44 -0500 Subject: [PATCH] Statefulset functionality with tests --- config/rbac/manager_role.yaml | 38 ++ pkg/controller/add_statefulset.go | 26 ++ .../deployment/deployment_controller_test.go | 2 +- .../statefulset/statefulset_controller.go | 111 ++++++ .../statefulset_controller_suite_test.go | 89 +++++ .../statefulset_controller_test.go | 377 ++++++++++++++++++ pkg/core/children_test.go | 4 +- pkg/core/delete_test.go | 2 +- pkg/core/handler.go | 5 + pkg/core/handler_test.go | 2 +- pkg/core/owner_references.go | 6 +- pkg/core/owner_references_test.go | 2 +- pkg/core/types.go | 20 + test/utils/matchers.go | 13 +- test/utils/owner_ref.go | 18 +- test/utils/test_objects.go | 212 ++++++++++ 16 files changed, 913 insertions(+), 14 deletions(-) create mode 100644 pkg/controller/add_statefulset.go create mode 100644 pkg/controller/statefulset/statefulset_controller.go create mode 100644 pkg/controller/statefulset/statefulset_controller_suite_test.go create mode 100644 pkg/controller/statefulset/statefulset_controller_test.go diff --git a/config/rbac/manager_role.yaml b/config/rbac/manager_role.yaml index 7af0be09..071629b5 100644 --- a/config/rbac/manager_role.yaml +++ b/config/rbac/manager_role.yaml @@ -42,6 +42,44 @@ rules: - create - update - patch +- apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - update + - patch +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - update + - patch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch - apiGroups: - admissionregistration.k8s.io resources: diff --git a/pkg/controller/add_statefulset.go b/pkg/controller/add_statefulset.go new file mode 100644 index 00000000..b7c44bed --- /dev/null +++ b/pkg/controller/add_statefulset.go @@ -0,0 +1,26 @@ +/* +Copyright 2018 Pusher Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "github.com/pusher/wave/pkg/controller/statefulset" +) + +func init() { + // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. + AddToManagerFuncs = append(AddToManagerFuncs, statefulset.Add) +} diff --git a/pkg/controller/deployment/deployment_controller_test.go b/pkg/controller/deployment/deployment_controller_test.go index 6b0b32f3..0851ab6f 100644 --- a/pkg/controller/deployment/deployment_controller_test.go +++ b/pkg/controller/deployment/deployment_controller_test.go @@ -108,7 +108,7 @@ var _ = Describe("Deployment controller Suite", func() { m.Create(deployment).Should(Succeed()) waitForDeploymentReconciled(deployment) - ownerRef = utils.GetOwnerRef(deployment) + ownerRef = utils.GetOwnerRefDeployment(deployment) }) AfterEach(func() { diff --git a/pkg/controller/statefulset/statefulset_controller.go b/pkg/controller/statefulset/statefulset_controller.go new file mode 100644 index 00000000..e43a89b5 --- /dev/null +++ b/pkg/controller/statefulset/statefulset_controller.go @@ -0,0 +1,111 @@ +/* +Copyright 2018 Pusher Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statefulset + +import ( + "context" + + "github.com/pusher/wave/pkg/core" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// Add creates a new StatefulSet Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileStatefulSet{ + scheme: mgr.GetScheme(), + handler: core.NewHandler(mgr.GetClient(), mgr.GetEventRecorderFor("wave")), + } +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New("statefulset-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to StatefulSet + err = c.Watch(&source.Kind{Type: &appsv1.StatefulSet{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // Watch ConfigMaps owned by a StatefulSet + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &handler.EnqueueRequestForOwner{ + IsController: false, + OwnerType: &appsv1.StatefulSet{}, + }) + if err != nil { + return err + } + + // Watch Secrets owned by a StatefulSet + err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForOwner{ + IsController: false, + OwnerType: &appsv1.StatefulSet{}, + }) + if err != nil { + return err + } + + return nil +} + +var _ reconcile.Reconciler = &ReconcileStatefulSet{} + +// ReconcileStatefulSet reconciles a StatefulSet object +type ReconcileStatefulSet struct { + scheme *runtime.Scheme + handler *core.Handler +} + +// Reconcile reads that state of the cluster for a StatefulSet object and +// updates its PodSpec based on mounted configuration +// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=,resources=configmaps,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=,resources=secrets,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=,resources=events,verbs=create;update;patch +func (r *ReconcileStatefulSet) Reconcile(request reconcile.Request) (reconcile.Result, error) { + // Fetch the StatefulSet instance + instance := &appsv1.StatefulSet{} + err := r.handler.Get(context.TODO(), request.NamespacedName, instance) + if err != nil { + if errors.IsNotFound(err) { + // Object not found, return. Created objects are automatically garbage collected. + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + + return r.handler.HandleStatefulSet(instance) +} diff --git a/pkg/controller/statefulset/statefulset_controller_suite_test.go b/pkg/controller/statefulset/statefulset_controller_suite_test.go new file mode 100644 index 00000000..66d57dfa --- /dev/null +++ b/pkg/controller/statefulset/statefulset_controller_suite_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 Pusher Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statefulset + +import ( + "log" + "path/filepath" + "sync" + "testing" + + "github.com/pusher/wave/test/reporters" + + "github.com/go-logr/glogr" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pusher/wave/pkg/apis" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" +) + +var cfg *rest.Config + +func TestMain(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, "Wave Controller Suite", reporters.Reporters()) +} + +var t *envtest.Environment + +var _ = BeforeSuite(func() { + t = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crds")}, + } + apis.AddToScheme(scheme.Scheme) + + logf.SetLogger(glogr.New()) + + var err error + if cfg, err = t.Start(); err != nil { + log.Fatal(err) + } +}) + +var _ = AfterSuite(func() { + t.Stop() +}) + +// SetupTestReconcile returns a reconcile.Reconcile implementation that delegates to inner and +// writes the request to requests after Reconcile is finished. +func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan reconcile.Request) { + requests := make(chan reconcile.Request) + fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { + result, err := inner.Reconcile(req) + requests <- req + return result, err + }) + return fn, requests +} + +// StartTestManager adds recFn +func StartTestManager(mgr manager.Manager) (chan struct{}, *sync.WaitGroup) { + stop := make(chan struct{}) + wg := &sync.WaitGroup{} + go func() { + defer GinkgoRecover() + wg.Add(1) + Expect(mgr.Start(stop)).NotTo(HaveOccurred()) + wg.Done() + }() + return stop, wg +} diff --git a/pkg/controller/statefulset/statefulset_controller_test.go b/pkg/controller/statefulset/statefulset_controller_test.go new file mode 100644 index 00000000..1b707152 --- /dev/null +++ b/pkg/controller/statefulset/statefulset_controller_test.go @@ -0,0 +1,377 @@ +/* +Copyright 2018 Pusher Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statefulset + +import ( + "context" + "fmt" + "sync" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pusher/wave/pkg/core" + "github.com/pusher/wave/test/utils" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("StatefulSet controller Suite", func() { + var c client.Client + var m utils.Matcher + + var statefulset *appsv1.StatefulSet + var requests <-chan reconcile.Request + var mgrStopped *sync.WaitGroup + var stopMgr chan struct{} + + const timeout = time.Second * 5 + const consistentlyTimeout = time.Second + + var ownerRef metav1.OwnerReference + var cm1 *corev1.ConfigMap + var cm2 *corev1.ConfigMap + var cm3 *corev1.ConfigMap + var s1 *corev1.Secret + var s2 *corev1.Secret + var s3 *corev1.Secret + + var waitForStatefulSetReconciled = func(obj core.Object) { + request := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }, + } + // wait for reconcile for creating the StatefulSet + Eventually(requests, timeout).Should(Receive(Equal(request))) + } + + BeforeEach(func() { + mgr, err := manager.New(cfg, manager.Options{}) + Expect(err).NotTo(HaveOccurred()) + c = mgr.GetClient() + m = utils.Matcher{Client: c} + + var recFn reconcile.Reconciler + recFn, requests = SetupTestReconcile(newReconciler(mgr)) + Expect(add(mgr, recFn)).NotTo(HaveOccurred()) + + stopMgr, mgrStopped = StartTestManager(mgr) + + // Create some configmaps and secrets + cm1 = utils.ExampleConfigMap1.DeepCopy() + cm2 = utils.ExampleConfigMap2.DeepCopy() + cm3 = utils.ExampleConfigMap3.DeepCopy() + s1 = utils.ExampleSecret1.DeepCopy() + s2 = utils.ExampleSecret2.DeepCopy() + s3 = utils.ExampleSecret3.DeepCopy() + + m.Create(cm1).Should(Succeed()) + m.Create(cm2).Should(Succeed()) + m.Create(cm3).Should(Succeed()) + m.Create(s1).Should(Succeed()) + m.Create(s2).Should(Succeed()) + m.Create(s3).Should(Succeed()) + m.Get(cm1, timeout).Should(Succeed()) + m.Get(cm2, timeout).Should(Succeed()) + m.Get(cm3, timeout).Should(Succeed()) + m.Get(s1, timeout).Should(Succeed()) + m.Get(s2, timeout).Should(Succeed()) + m.Get(s3, timeout).Should(Succeed()) + + statefulset = utils.ExampleStatefulSet.DeepCopy() + + // Create a statefulset and wait for it to be reconciled + m.Create(statefulset).Should(Succeed()) + waitForStatefulSetReconciled(statefulset) + + ownerRef = utils.GetOwnerRefStatefulSet(statefulset) + }) + + AfterEach(func() { + // Make sure to delete any finalizers (if the statefulset exists) + Eventually(func() error { + key := types.NamespacedName{Namespace: statefulset.GetNamespace(), Name: statefulset.GetName()} + err := c.Get(context.TODO(), key, statefulset) + if err != nil && errors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + statefulset.SetFinalizers([]string{}) + return c.Update(context.TODO(), statefulset) + }, timeout).Should(Succeed()) + + Eventually(func() error { + key := types.NamespacedName{Namespace: statefulset.GetNamespace(), Name: statefulset.GetName()} + err := c.Get(context.TODO(), key, statefulset) + if err != nil && errors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + if len(statefulset.GetFinalizers()) > 0 { + return fmt.Errorf("Finalizers not upated") + } + return nil + }, timeout).Should(Succeed()) + + close(stopMgr) + mgrStopped.Wait() + + utils.DeleteAll(cfg, timeout, + &appsv1.StatefulSetList{}, + &corev1.ConfigMapList{}, + &corev1.SecretList{}, + &corev1.EventList{}, + ) + }) + + Context("When a StatefulSet is reconciled", func() { + Context("And it has the required annotation", func() { + BeforeEach(func() { + annotations := statefulset.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[core.RequiredAnnotation] = "true" + statefulset.SetAnnotations(annotations) + + m.Update(statefulset).Should(Succeed()) + waitForStatefulSetReconciled(statefulset) + + // Get the updated StatefulSet + m.Get(statefulset, timeout).Should(Succeed()) + }) + + It("Adds OwnerReferences to all children", func() { + for _, obj := range []core.Object{cm1, cm2, cm3, s1, s2, s3} { + m.Eventually(obj, timeout).Should(utils.WithOwnerReferences(ContainElement(ownerRef))) + } + }) + + It("Adds a finalizer to the StatefulSet", func() { + m.Eventually(statefulset, timeout).Should(utils.WithFinalizers(ContainElement(core.FinalizerString))) + }) + + It("Adds a config hash to the Pod Template", func() { + m.Eventually(statefulset, timeout).Should(utils.WithPodTemplateAnnotations(HaveKey(core.ConfigHashAnnotation))) + }) + + It("Sends an event when updating the hash", func() { + m.Eventually(statefulset, timeout).Should(utils.WithPodTemplateAnnotations(HaveKey(core.ConfigHashAnnotation))) + + events := &corev1.EventList{} + eventMessage := func(event *corev1.Event) string { + return event.Message + } + + hashMessage := "Configuration hash updated to ebabf80ef45218b27078a41ca16b35a4f91cb5672f389e520ae9da6ee3df3b1c" + m.Eventually(events, timeout).Should(utils.WithItems(ContainElement(WithTransform(eventMessage, Equal(hashMessage))))) + }) + + Context("And a child is removed", func() { + var originalHash string + BeforeEach(func() { + m.Eventually(statefulset, timeout).Should(utils.WithPodTemplateAnnotations(HaveKey(core.ConfigHashAnnotation))) + originalHash = statefulset.Spec.Template.GetAnnotations()[core.ConfigHashAnnotation] + + // Remove "container2" which references Secret example2 and ConfigMap + // example2 + containers := statefulset.Spec.Template.Spec.Containers + Expect(containers[0].Name).To(Equal("container1")) + statefulset.Spec.Template.Spec.Containers = []corev1.Container{containers[0]} + m.Update(statefulset).Should(Succeed()) + waitForStatefulSetReconciled(statefulset) + + // Get the updated StatefulSet + m.Get(statefulset, timeout).Should(Succeed()) + }) + + It("Removes the OwnerReference from the orphaned ConfigMap", func() { + m.Eventually(cm2, timeout).ShouldNot(utils.WithOwnerReferences(ContainElement(ownerRef))) + }) + + It("Removes the OwnerReference from the orphaned Secret", func() { + m.Eventually(s2, timeout).ShouldNot(utils.WithOwnerReferences(ContainElement(ownerRef))) + }) + + It("Updates the config hash in the Pod Template", func() { + m.Eventually(statefulset, timeout).ShouldNot(utils.WithPodTemplateAnnotations(HaveKeyWithValue(core.ConfigHashAnnotation, originalHash))) + }) + }) + + Context("And a child is updated", func() { + var originalHash string + + BeforeEach(func() { + m.Eventually(statefulset, timeout).Should(utils.WithPodTemplateAnnotations(HaveKey(core.ConfigHashAnnotation))) + originalHash = statefulset.Spec.Template.GetAnnotations()[core.ConfigHashAnnotation] + }) + + Context("A ConfigMap volume is updated", func() { + BeforeEach(func() { + m.Get(cm1, timeout).Should(Succeed()) + cm1.Data["key1"] = "modified" + m.Update(cm1).Should(Succeed()) + + waitForStatefulSetReconciled(statefulset) + + // Get the updated StatefulSet + m.Get(statefulset, timeout).Should(Succeed()) + }) + + It("Updates the config hash in the Pod Template", func() { + m.Eventually(statefulset, timeout).ShouldNot(utils.WithAnnotations(HaveKeyWithValue(core.ConfigHashAnnotation, originalHash))) + }) + }) + + Context("A ConfigMap EnvSource is updated", func() { + BeforeEach(func() { + m.Get(cm2, timeout).Should(Succeed()) + cm2.Data["key1"] = "modified" + m.Update(cm2).Should(Succeed()) + + waitForStatefulSetReconciled(statefulset) + + // Get the updated StatefulSet + m.Get(statefulset, timeout).Should(Succeed()) + }) + + It("Updates the config hash in the Pod Template", func() { + m.Eventually(statefulset, timeout).ShouldNot(utils.WithAnnotations(HaveKeyWithValue(core.ConfigHashAnnotation, originalHash))) + }) + }) + + Context("A Secret volume is updated", func() { + BeforeEach(func() { + m.Get(s1, timeout).Should(Succeed()) + if s1.StringData == nil { + s1.StringData = make(map[string]string) + } + s1.StringData["key1"] = "modified" + m.Update(s1).Should(Succeed()) + + waitForStatefulSetReconciled(statefulset) + + // Get the updated StatefulSet + m.Get(statefulset, timeout).Should(Succeed()) + }) + + It("Updates the config hash in the Pod Template", func() { + m.Eventually(statefulset, timeout).ShouldNot(utils.WithAnnotations(HaveKeyWithValue(core.ConfigHashAnnotation, originalHash))) + }) + }) + + Context("A Secret EnvSource is updated", func() { + BeforeEach(func() { + m.Get(s2, timeout).Should(Succeed()) + if s2.StringData == nil { + s2.StringData = make(map[string]string) + } + s2.StringData["key1"] = "modified" + m.Update(s2).Should(Succeed()) + + waitForStatefulSetReconciled(statefulset) + + // Get the updated StatefulSet + m.Get(statefulset, timeout).Should(Succeed()) + }) + + It("Updates the config hash in the Pod Template", func() { + m.Eventually(statefulset, timeout).ShouldNot(utils.WithAnnotations(HaveKeyWithValue(core.ConfigHashAnnotation, originalHash))) + }) + }) + }) + + Context("And the annotation is removed", func() { + BeforeEach(func() { + m.Get(statefulset, timeout).Should(Succeed()) + statefulset.SetAnnotations(make(map[string]string)) + m.Update(statefulset).Should(Succeed()) + waitForStatefulSetReconciled(statefulset) + + m.Eventually(statefulset, timeout).ShouldNot(utils.WithAnnotations(HaveKey(core.RequiredAnnotation))) + }) + + It("Removes the OwnerReference from the all children", func() { + for _, obj := range []core.Object{cm1, cm2, s1, s2} { + m.Eventually(obj, timeout).ShouldNot(utils.WithOwnerReferences(ContainElement(ownerRef))) + } + }) + + It("Removes the StatefulSet's finalizer", func() { + m.Eventually(statefulset, timeout).ShouldNot(utils.WithFinalizers(ContainElement(core.FinalizerString))) + }) + }) + + Context("And is deleted", func() { + BeforeEach(func() { + // Make sure the cache has synced before we run the test + m.Eventually(statefulset, timeout).Should(utils.WithPodTemplateAnnotations(HaveKey(core.ConfigHashAnnotation))) + m.Delete(statefulset).Should(Succeed()) + m.Eventually(statefulset, timeout).ShouldNot(utils.WithDeletionTimestamp(BeNil())) + waitForStatefulSetReconciled(statefulset) + + // Get the updated StatefulSet + m.Get(statefulset, timeout).Should(Succeed()) + }) + It("Removes the OwnerReference from the all children", func() { + for _, obj := range []core.Object{cm1, cm2, s1, s2} { + m.Eventually(obj, timeout).ShouldNot(utils.WithOwnerReferences(ContainElement(ownerRef))) + } + }) + + It("Removes the StatefulSet's finalizer", func() { + // Removing the finalizer causes the statefulset to be deleted + m.Get(statefulset, timeout).ShouldNot(Succeed()) + }) + }) + }) + + Context("And it does not have the required annotation", func() { + BeforeEach(func() { + // Get the updated StatefulSet + m.Get(statefulset, timeout).Should(Succeed()) + }) + + It("Doesn't add any OwnerReferences to any children", func() { + for _, obj := range []core.Object{cm1, cm2, s1, s2} { + m.Consistently(obj, consistentlyTimeout).ShouldNot(utils.WithOwnerReferences(ContainElement(ownerRef))) + } + }) + + It("Doesn't add a finalizer to the StatefulSet", func() { + m.Consistently(statefulset, consistentlyTimeout).ShouldNot(utils.WithFinalizers(ContainElement(core.FinalizerString))) + }) + + It("Doesn't add a config hash to the Pod Template", func() { + m.Consistently(statefulset, consistentlyTimeout).ShouldNot(utils.WithAnnotations(ContainElement(core.ConfigHashAnnotation))) + }) + }) + }) + +}) diff --git a/pkg/core/children_test.go b/pkg/core/children_test.go index 047ab59f..a178bf4e 100644 --- a/pkg/core/children_test.go +++ b/pkg/core/children_test.go @@ -261,7 +261,7 @@ var _ = Describe("Wave children Suite", func() { Context("getExistingChildren", func() { BeforeEach(func() { m.Get(deploymentObject, timeout).Should(Succeed()) - ownerRef := utils.GetOwnerRef(deploymentObject) + ownerRef := utils.GetOwnerRefDeployment(deploymentObject) for _, obj := range []Object{cm1, s1} { m.Get(obj, timeout).Should(Succeed()) @@ -304,7 +304,7 @@ var _ = Describe("Wave children Suite", func() { var ownerRef metav1.OwnerReference BeforeEach(func() { m.Get(deploymentObject, timeout).Should(Succeed()) - ownerRef = utils.GetOwnerRef(deploymentObject) + ownerRef = utils.GetOwnerRefDeployment(deploymentObject) }) It("returns true when the child has a single owner reference pointing to the owner", func() { diff --git a/pkg/core/delete_test.go b/pkg/core/delete_test.go index c944e9a7..c904cd31 100644 --- a/pkg/core/delete_test.go +++ b/pkg/core/delete_test.go @@ -65,7 +65,7 @@ var _ = Describe("Wave owner references Suite", func() { m.Create(deploymentObject).Should(Succeed()) - ownerRef = utils.GetOwnerRef(deploymentObject) + ownerRef = utils.GetOwnerRefDeployment(deploymentObject) stopMgr, mgrStopped = StartTestManager(mgr) m.Get(deploymentObject, timeout).Should(Succeed()) diff --git a/pkg/core/handler.go b/pkg/core/handler.go index b57814dc..17dc1c04 100644 --- a/pkg/core/handler.go +++ b/pkg/core/handler.go @@ -45,6 +45,11 @@ func (h *Handler) HandleDeployment(instance *appsv1.Deployment) (reconcile.Resul return h.handlePodController(&deployment{Deployment: instance}) } +// HandleStatefulSet is called by the StatefulSet controller to reconcile StatefulSets +func (h *Handler) HandleStatefulSet(instance *appsv1.StatefulSet) (reconcile.Result, error) { + return h.handlePodController(&statefulset{StatefulSet: instance}) +} + // handlePodController reconciles the state of a podController func (h *Handler) handlePodController(instance podController) (reconcile.Result, error) { log := logf.Log.WithName("wave") diff --git a/pkg/core/handler_test.go b/pkg/core/handler_test.go index a630a407..24ff74c9 100644 --- a/pkg/core/handler_test.go +++ b/pkg/core/handler_test.go @@ -94,7 +94,7 @@ var _ = Describe("Wave controller Suite", func() { Expect(err).NotTo(HaveOccurred()) m.Get(deployment).Should(Succeed()) - ownerRef = utils.GetOwnerRef(deployment) + ownerRef = utils.GetOwnerRefDeployment(deployment) }) AfterEach(func() { diff --git a/pkg/core/owner_references.go b/pkg/core/owner_references.go index cc7da24a..70c2d82e 100644 --- a/pkg/core/owner_references.go +++ b/pkg/core/owner_references.go @@ -125,7 +125,7 @@ func getOwnerReference(obj podController) metav1.OwnerReference { f := false return metav1.OwnerReference{ APIVersion: "apps/v1", - Kind: "Deployment", + Kind: kindOf(obj), Name: obj.GetName(), UID: obj.GetUID(), BlockOwnerDeletion: &t, @@ -150,6 +150,10 @@ func kindOf(obj Object) string { return "ConfigMap" case *corev1.Secret: return "Secret" + case *deployment: + return "Deployment" + case *statefulset: + return "StatefulSet" default: return "Unknown" } diff --git a/pkg/core/owner_references_test.go b/pkg/core/owner_references_test.go index 7ecd2214..6bcbaf95 100644 --- a/pkg/core/owner_references_test.go +++ b/pkg/core/owner_references_test.go @@ -77,7 +77,7 @@ var _ = Describe("Wave owner references Suite", func() { m.Create(deploymentObject).Should(Succeed()) - ownerRef = utils.GetOwnerRef(deploymentObject) + ownerRef = utils.GetOwnerRefDeployment(deploymentObject) stopMgr, mgrStopped = StartTestManager(mgr) diff --git a/pkg/core/types.go b/pkg/core/types.go index d96f4e77..5856684b 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -86,3 +86,23 @@ func (d *deployment) SetPodTemplate(template *corev1.PodTemplateSpec) { func (d *deployment) DeepCopy() podController { return &deployment{d.Deployment.DeepCopy()} } + +type statefulset struct { + *appsv1.StatefulSet +} + +func (d *statefulset) GetObject() runtime.Object { + return d.StatefulSet +} + +func (d *statefulset) GetPodTemplate() *corev1.PodTemplateSpec { + return &d.StatefulSet.Spec.Template +} + +func (d *statefulset) SetPodTemplate(template *corev1.PodTemplateSpec) { + d.StatefulSet.Spec.Template = *template +} + +func (d *statefulset) DeepCopy() podController { + return &statefulset{d.StatefulSet.DeepCopy()} +} diff --git a/test/utils/matchers.go b/test/utils/matchers.go index 46906472..bcaa3a64 100644 --- a/test/utils/matchers.go +++ b/test/utils/matchers.go @@ -167,14 +167,17 @@ func WithOwnerReferences(matcher gtypes.GomegaMatcher) gtypes.GomegaMatcher { }, matcher) } -// WithPodTemplateAnnotations returns the deployments PodTemplate's annotations +// WithPodTemplateAnnotations returns the PodTemplate's annotations func WithPodTemplateAnnotations(matcher gtypes.GomegaMatcher) gtypes.GomegaMatcher { return gomega.WithTransform(func(obj Object) map[string]string { - dep, ok := obj.(*appsv1.Deployment) - if !ok { - panic("Unknown Object.") + switch obj.(type) { + case *appsv1.Deployment: + return obj.(*appsv1.Deployment).Spec.Template.GetAnnotations() + case *appsv1.StatefulSet: + return obj.(*appsv1.StatefulSet).Spec.Template.GetAnnotations() + default: + panic("Unknown pod template type.") } - return dep.Spec.Template.GetAnnotations() }, matcher) } diff --git a/test/utils/owner_ref.go b/test/utils/owner_ref.go index 8ce00169..dc2cf8cc 100644 --- a/test/utils/owner_ref.go +++ b/test/utils/owner_ref.go @@ -21,8 +21,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// GetOwnerRef constructs an owner reference for the Deployment given -func GetOwnerRef(deployment *appsv1.Deployment) metav1.OwnerReference { +// GetOwnerRefDeployment constructs an owner reference for the Deployment given +func GetOwnerRefDeployment(deployment *appsv1.Deployment) metav1.OwnerReference { f := false t := true return metav1.OwnerReference{ @@ -34,3 +34,17 @@ func GetOwnerRef(deployment *appsv1.Deployment) metav1.OwnerReference { BlockOwnerDeletion: &t, } } + +// GetOwnerRefStatefulSet constructs an owner reference for the StatefulSet given +func GetOwnerRefStatefulSet(sts *appsv1.StatefulSet) metav1.OwnerReference { + f := false + t := true + return metav1.OwnerReference{ + APIVersion: "apps/v1", + Kind: "StatefulSet", + Name: sts.Name, + UID: sts.UID, + Controller: &f, + BlockOwnerDeletion: &t, + } +} diff --git a/test/utils/test_objects.go b/test/utils/test_objects.go index 8545aaac..3f1d2e34 100644 --- a/test/utils/test_objects.go +++ b/test/utils/test_objects.go @@ -240,6 +240,218 @@ var ExampleDeployment = &appsv1.Deployment{ }, } +// ExampleStatefulSet is an example StatefulSet object for use within test suites +var ExampleStatefulSet = &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "default", + Labels: labels, + }, + Spec: appsv1.StatefulSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "secret1", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "example1", + }, + }, + }, + { + Name: "configmap1", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example1", + }, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "container1", + Image: "container1", + Env: []corev1.EnvVar{ + { + Name: "example1_key1", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example1", + }, + Key: "key1", + }, + }, + }, + { + Name: "example1_key1_new_name", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example1", + }, + Key: "key1", + }, + }, + }, + { + Name: "example3_key1", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example3", + }, + Key: "key1", + }, + }, + }, + { + Name: "example3_key4", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example3", + }, + Key: "key4", + Optional: &trueValue, + }, + }, + }, + { + Name: "example4_key1", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example4", + }, + Key: "key1", + Optional: &trueValue, + }, + }, + }, + { + Name: "example1_secret_key1", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example1", + }, + Key: "key1", + }, + }, + }, + { + Name: "example3_secret_key1", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example3", + }, + Key: "key1", + }, + }, + }, + { + Name: "example3_secret_key4", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example3", + }, + Key: "key4", + Optional: &trueValue, + }, + }, + }, + { + Name: "example4_secret_key1", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example4", + }, + Key: "key1", + Optional: &trueValue, + }, + }, + }, + }, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example1", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example1", + }, + }, + }, + }, + }, + { + Name: "container2", + Image: "container2", + Env: []corev1.EnvVar{ + { + Name: "example3_key2", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example3", + }, + Key: "key2", + }, + }, + }, + { + Name: "example3_secret_key2", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example3", + }, + Key: "key2", + }, + }, + }, + }, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example2", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "example2", + }, + }, + }, + }, + }, + }, + }, + }, + }, +} + // ExampleConfigMap1 is an example ConfigMap object for use within test suites var ExampleConfigMap1 = &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{