diff --git a/Dockerfile b/Dockerfile index 8f693b77..55734152 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,17 @@ FROM alpine:3.11 + +RUN echo 'https://repository.walmart.com/content/repositories/alpine-v38/community' > /etc/apk/repositories \ + && echo 'https://repository.walmart.com/content/repositories/alpine-v38/main' >> /etc/apk/repositories \ + && apk update && apk upgrade && apk --no-cache add \ + ca-certificates + RUN addgroup -S flagger \ - && adduser -S -g flagger flagger \ - && apk --no-cache add ca-certificates + && adduser -S -g flagger flagger + +#RUN addgroup -S flagger \ +# && adduser -S -g flagger flagger \ +# && apk --no-cache add ca-certificates WORKDIR /home/flagger diff --git a/go.mod b/go.mod index 75f58ddb..ed7aaf92 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( k8s.io/api v0.17.2 k8s.io/apimachinery v0.17.2 k8s.io/client-go v0.17.2 - k8s.io/code-generator v0.17.2 + k8s.io/code-generator v0.17.3 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f ) diff --git a/go.sum b/go.sum index 92348998..b4359eb6 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,10 @@ github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3s github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -34,6 +36,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -46,14 +49,18 @@ github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2i github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -123,6 +130,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -297,6 +305,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc= @@ -307,6 +317,8 @@ k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc= k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= k8s.io/code-generator v0.17.2 h1:pTwl3rLB1fUyxmvEzmVPMM0tBSdUehd7z+bDzpj4lPE= k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/code-generator v0.17.3 h1:q/hDMk2cvFzSxol7k/VA1qCssR7VSMXHQHhzuX29VJ8= +k8s.io/code-generator v0.17.3/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505 h1:ZY6yclUKVbZ+SdWnkfY+Je5vrMpKOxmGeKRbsXVmqYM= diff --git a/pkg/apis/flagger/v1beta1/status.go b/pkg/apis/flagger/v1beta1/status.go index b44341e4..0c83f5a5 100644 --- a/pkg/apis/flagger/v1beta1/status.go +++ b/pkg/apis/flagger/v1beta1/status.go @@ -57,6 +57,12 @@ const ( // CanaryPhaseFailed means the canary analysis failed // and the canary deployment has been scaled to zero CanaryPhaseFailed CanaryPhase = "Failed" + // CanaryPhaseTerminating means the canary has been marked + // for deletion and in the finalizing state + CanaryPhaseTerminating CanaryPhase = "Terminating" + // CanaryPhaseTerminated means the canary has been finalized + // and successfully deleted + CanaryPhaseTerminated CanaryPhase = "Terminated" ) // CanaryStatus is used for state persistence (read-only) diff --git a/pkg/canary/controller.go b/pkg/canary/controller.go index d54c1a20..635f1af1 100644 --- a/pkg/canary/controller.go +++ b/pkg/canary/controller.go @@ -19,4 +19,5 @@ type Controller interface { HaveDependenciesChanged(canary *flaggerv1.Canary) (bool, error) ScaleToZero(canary *flaggerv1.Canary) error ScaleFromZero(canary *flaggerv1.Canary) error + Finalize(canary *flaggerv1.Canary) error } diff --git a/pkg/canary/daemonset_controller.go b/pkg/canary/daemonset_controller.go index 0642f6b8..e0e80d7b 100644 --- a/pkg/canary/daemonset_controller.go +++ b/pkg/canary/daemonset_controller.go @@ -296,3 +296,7 @@ func (c *DaemonSetController) getSelectorLabel(daemonSet *appsv1.DaemonSet) (str func (c *DaemonSetController) HaveDependenciesChanged(cd *flaggerv1.Canary) (bool, error) { return c.configTracker.HasConfigChanged(cd) } + +func (c *DaemonSetController) Finalize(cd *flaggerv1.Canary) error { + return nil +} diff --git a/pkg/canary/deployment_controller.go b/pkg/canary/deployment_controller.go index 3a89526c..19bc874e 100644 --- a/pkg/canary/deployment_controller.go +++ b/pkg/canary/deployment_controller.go @@ -376,3 +376,40 @@ func (c *DeploymentController) getSelectorLabel(deployment *appsv1.Deployment) ( func (c *DeploymentController) HaveDependenciesChanged(cd *flaggerv1.Canary) (bool, error) { return c.configTracker.HasConfigChanged(cd) } + + +// revertDeployment will set the replica count from the primary to the reference instance. This method is used +// during a delete to attempt to revert the deployment back to the original state. Error is returned if unable +// update the reference deployment replicas to the primary replicas +func (c *DeploymentController) Finalize(cd *flaggerv1.Canary) error { + + //1. Get the Primary deployment if possible + primaryName := fmt.Sprintf("%s-primary", cd.Spec.TargetRef.Name) + //2. Get the replicas value + primaryDep, err := c.kubeClient.AppsV1().Deployments(cd.Namespace).Get(primaryName, metav1.GetOptions{}) + if err != nil { + /*if errors.IsNotFound(err) { + c.logger.Warnf("deployment %s.%s not found while finalizing", primaryName, cd.Namespace) + return nil + }*/ + return err + } + refDep, err := c.kubeClient.AppsV1().Deployments(cd.Namespace).Get(cd.Spec.TargetRef.Name, metav1.GetOptions{}) + if err != nil { + /*if errors.IsNotFound(err) { + c.logger.Warnf("deployment %s.%s not found while finalizing", cd.Spec.TargetRef.Name, cd.Namespace) + return nil + }*/ + return err + } + + if refDep.Spec.Replicas != primaryDep.Spec.Replicas { + //3. Set the replicas value on the original reference deployment + desiredReplicas := primaryDep.Spec.Replicas + if err := c.Scale(cd, int32Default(desiredReplicas)); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/canary/service_controller.go b/pkg/canary/service_controller.go index c62949a2..fa7ff8ef 100644 --- a/pkg/canary/service_controller.go +++ b/pkg/canary/service_controller.go @@ -221,3 +221,7 @@ func (c *ServiceController) IsPrimaryReady(_ *flaggerv1.Canary) error { func (c *ServiceController) IsCanaryReady(_ *flaggerv1.Canary) (bool, error) { return true, nil } + +func (c *ServiceController) Finalize(cd *flaggerv1.Canary) error { + return nil +} diff --git a/pkg/controller/finalizer.go b/pkg/controller/finalizer.go new file mode 100644 index 00000000..3f7f136b --- /dev/null +++ b/pkg/controller/finalizer.go @@ -0,0 +1,221 @@ +package controller + +import ( + "fmt" + ex "github.com/pkg/errors" + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" + "github.com/weaveworks/flagger/pkg/canary" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +const finalizer = "finalizer.flagger.com" + +func (c *Controller) finalize(old interface{}) error { + var r *flaggerv1.Canary + var ok bool + + //Ensure interface is a canary + if r, ok = old.(*flaggerv1.Canary); !ok { + c.logger.Warnf("Received unexpected object: %v", old) + return nil + } + + //Retrieve a controller + canaryController := c.canaryFactory.Controller(r.Spec.TargetRef.Kind) + + //Set the status to terminating if not already in that state + if r.Status.Phase != flaggerv1.CanaryPhaseTerminating { + if err := canaryController.SetStatusPhase(r, flaggerv1.CanaryPhaseTerminating); err != nil { + c.logger.Infof("Failed to update status to finalizing %s", err) + return err + } + //record event + c.recordEventInfof(r, "Terminating canary %s.%s", r.Name, r.Namespace) + } + + err := c.revertTargetRef(canaryController, r) + if err != nil { + if errors.IsNotFound(err) { + //No reason to wait not found + c.logger.Warnf("%s.%s failed due to %s not found", r.Name, r.Namespace, r.Kind) + return nil + } + c.logger.Errorf("%s.%s failed due to %s", r.Name, r.Namespace, err) + return err + } else { + //Ensure that targetRef has met a ready state + c.logger.Infof("Checking is canary is ready %s.%s", r.Name, r.Namespace) + ready, err := canaryController.IsCanaryReady(r) + if err != nil && ready { + return fmt.Errorf("%s.%s has not reached ready state during finalizing", r.Name, r.Namespace) + } + + } + + c.logger.Infof("%s.%s moving forward with router finalizing", r.Name, r.Namespace) + //TODO if I can't revert continue on? + labelSelector, ports, err := canaryController.GetMetadata(r) + if err != nil { + c.logger.Errorf("%s.%s failed to get metadata for router finalizing", r.Name, r.Namespace) + return err + } + //Revert the router + if err := c.revertRouter(r, labelSelector, ports); err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + + c.logger.Infof("%s.%s moving forward with mesh finalizing", r.Name, r.Namespace) + //TODO if I can't revert the mesh continue on? + //Revert the Mesh + if err := c.revertMesh(r); err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + + c.logger.Infof("Finalization complete for %s.%s", r.Name, r.Namespace) + + return nil + + +} + +func (c *Controller) revertTargetRef(ctrl canary.Controller, r *flaggerv1.Canary) error { + if err := ctrl.Finalize(r); err != nil { + return err + } + /*if err != nil { + if errors.IsNotFound(err) { + return false, err + } + return true, fmt.Errorf("%s.%s failed to revert deployment to original replicas", r.Name, r.Namespace) + }*/ + c.logger.Infof("%s.%s kind %s reverted", r.Name, r.Namespace, r.Spec.TargetRef.Kind) + return nil +} + +//revertRouter +func (c *Controller) revertRouter(r *flaggerv1.Canary, labelSelector string, ports map[string]int32) error { + router := c.routerFactory.KubernetesRouter(r.Spec.TargetRef.Kind, labelSelector, map[string]string{}, ports) + if err := router.Finalize(r); err != nil { + c.logger.Errorf("%s.%s router failed with error %s", r.Name, r.Namespace, err) + return err + } + /*if err != nil { + return fmt.Errorf("%s.%s failed to revert service to original state", r.Name, r.Namespace) + }*/ + c.logger.Infof("Service %s.%s reverted", r.Name, r.Namespace) + return nil +} + +//revertMesh reverts defined mesh provider based upon the implementation's respective Finalize method. +//If the Finalize method encounters and error that is returned, else revert is considered successful. +func (c *Controller) revertMesh(r *flaggerv1.Canary) error { + provider := c.meshProvider + if r.Spec.Provider != "" { + provider = r.Spec.Provider + } + + //Establish provider + meshRouter := c.routerFactory.MeshRouter(provider) + + //Finalize mesh + err := meshRouter.Finalize(r) + if err != nil { + c.logger.Errorf("%s.%s mesh failed with error %s", r.Name, r.Namespace, err) + return err + } + + c.logger.Infof("%s.%s mesh provider %s reverted", r.Name, r.Namespace, provider) + return nil +} + +//hasFinalizer evaluates the finalizers of a given canary for for existence of a provide finalizer string. +//It returns a boolean, true if the finalizer is found false otherwise. +func hasFinalizer(canary *flaggerv1.Canary, finalizerString string) bool { + currentFinalizers := canary.ObjectMeta.Finalizers + + for _, f := range currentFinalizers { + if f == finalizerString { + return true + } + } + return false +} + +//addFinalizer adds a provided finalizer to the specified canary resource. +//If failures occur the error will be returned otherwise the action is deemed successful +//and error will be nil. +func (c *Controller) addFinalizer(canary *flaggerv1.Canary, finalizerString string) error { + firstTry := true + err := retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { + + var selErr error + if !firstTry { + canary, selErr = c.flaggerClient.FlaggerV1beta1().Canaries(canary.Namespace).Get(canary.GetName(),metav1.GetOptions{}) + if selErr != nil { + return selErr + } + } + + copy := canary.DeepCopy() + copy.ObjectMeta.Finalizers = append(copy.ObjectMeta.Finalizers, finalizerString) + + _, err = c.flaggerClient.FlaggerV1beta1().Canaries(canary.Namespace).Update(copy) + + firstTry = false + return + }) + + if err != nil { + return ex.Wrap(err, "Remove finalizer failed") + } + return nil +} + +//removeFinalizer removes a provided finalizer to the specified canary resource. +//If failures occur the error will be returned otherwise the action is deemed successful +//and error will be nil. +func (c *Controller) removeFinalizer(canary *flaggerv1.Canary, finalizerString string) error { + firstTry := true + err := retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { + + var selErr error + if !firstTry { + canary, selErr = c.flaggerClient.FlaggerV1beta1().Canaries(canary.Namespace).Get(canary.GetName(),metav1.GetOptions{}) + if selErr != nil { + return selErr + } + } + copy := canary.DeepCopy() + + newSlice := make([]string, 0) + for _, item := range copy.ObjectMeta.Finalizers { + if item == finalizerString { + continue + } + newSlice = append(newSlice, item) + } + if len(newSlice) == 0 { + newSlice = nil + } + copy.ObjectMeta.Finalizers = newSlice + + _, err = c.flaggerClient.FlaggerV1beta1().Canaries(canary.Namespace).Update(copy) + + firstTry = false + return + }) + + if err != nil { + return ex.Wrap(err, "Remove finalizer failed") + } + return nil + +} \ No newline at end of file diff --git a/pkg/controller/finalizer_test.go b/pkg/controller/finalizer_test.go new file mode 100644 index 00000000..b5ed4d3c --- /dev/null +++ b/pkg/controller/finalizer_test.go @@ -0,0 +1,107 @@ +package controller + +import ( + "fmt" + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" + fakeFlagger "github.com/weaveworks/flagger/pkg/client/clientset/versioned/fake" + "k8s.io/apimachinery/pkg/runtime" + k8sTesting "k8s.io/client-go/testing" + + "testing" +) + +//Test has finalizers +func TestFinalizer_hasFinalizer(t *testing.T) { + + withFinalizer := newTestCanary() + withFinalizer.Finalizers = append(withFinalizer.Finalizers, finalizer) + + tables := []struct{ + canary *flaggerv1.Canary + result bool + }{ + {newTestCanary(), false}, + { withFinalizer, true}, + } + + for _, table := range tables { + isPresent := hasFinalizer(table.canary, finalizer) + if isPresent != table.result { + t.Errorf("Result of hasFinalizer returned [%t], but expected [%t]", isPresent, table.result) + } + } +} + +func TestFinalizer_addFinalizer(t *testing.T) { + + mockError := fmt.Errorf("failed to add finalizer to canary %s", "testCanary") + cs := fakeFlagger.NewSimpleClientset(newTestCanary()) + //prepend so it is evaluated over the catch all * + cs.PrependReactor("update", "canaries", func(action k8sTesting.Action) (handled bool, ret runtime.Object, err error){ + return true, nil, mockError + }) + m := Mocks{ + canary:newTestCanary(), + flaggerClient: cs, + ctrl:&Controller{flaggerClient:cs}, + } + + + tables := []struct{ + mock Mocks + canary *flaggerv1.Canary + error error + }{ + {SetupMocks(nil), newTestCanary(), nil}, + {m, m.canary, mockError}, + } + + for _, table := range tables { + response := table.mock.ctrl.addFinalizer(table.canary, finalizer) + + if table.error != nil && response == nil { + t.Errorf("Expected an error from addFinalizer, but wasn't present") + } else if table.error == nil && response != nil { + t.Errorf("Expected no error from addFinalizer, but returned error %s", response) + } + } + +} + +func TestFinalizer_removeFinalizer(t *testing.T) { + + withFinalizer := newTestCanary() + withFinalizer.Finalizers = append(withFinalizer.Finalizers, finalizer) + + mockError := fmt.Errorf("failed to add finalizer to canary %s", "testCanary") + cs := fakeFlagger.NewSimpleClientset(newTestCanary()) + //prepend so it is evaluated over the catch all * + cs.PrependReactor("update", "canaries", func(action k8sTesting.Action) (handled bool, ret runtime.Object, err error){ + return true, nil, mockError + }) + m := Mocks{ + canary:withFinalizer, + flaggerClient: cs, + ctrl:&Controller{flaggerClient:cs}, + } + + tables := []struct{ + mock Mocks + canary *flaggerv1.Canary + error error + }{ + {SetupMocks(nil), withFinalizer, nil}, + {m, m.canary, mockError}, + } + + for _, table := range tables { + response := table.mock.ctrl.removeFinalizer(table.canary, finalizer) + + if table.error != nil && response == nil { + t.Errorf("Expected an error from addFinalizer, but wasn't present") + } else if table.error == nil && response != nil { + t.Errorf("Expected no error from addFinalizer, but returned error %s", response) + } + + } +} \ No newline at end of file diff --git a/pkg/router/kubernetes.go b/pkg/router/kubernetes.go index a81a3f68..663d4302 100644 --- a/pkg/router/kubernetes.go +++ b/pkg/router/kubernetes.go @@ -10,4 +10,6 @@ type KubernetesRouter interface { Initialize(canary *flaggerv1.Canary) error // Reconcile creates or updates the main service Reconcile(canary *flaggerv1.Canary) error + // Revert router + Finalize(canary *flaggerv1.Canary) error } diff --git a/pkg/router/kubernetes_default.go b/pkg/router/kubernetes_default.go index 4511ecb9..2b5f6bdf 100644 --- a/pkg/router/kubernetes_default.go +++ b/pkg/router/kubernetes_default.go @@ -162,3 +162,41 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam return nil } + +func (c *KubernetesDeploymentRouter) Finalize(canary *flaggerv1.Canary) error { + apexName, _, _ := canary.GetServiceNames() + + svc, err := c.kubeClient.CoreV1().Services(canary.Namespace).Get(apexName, metav1.GetOptions{}) + if err != nil { + return err + } + + if hasCanaryOwnerRef, isOwned := c.isOwnedByCanary(svc, canary.Name); !hasCanaryOwnerRef && !isOwned { + err = c.reconcileService(canary, apexName, canary.Spec.TargetRef.Name) + if err != nil { + return err + } + } + + return nil +} + +//isOwnedByCanary evaluates if an object contains an OwnerReference declaration, that is of kind Canary and +//has the same ref name as the Canary under evaluation. It returns two bool the first returns true if +//an OwnerReference is present and the second, returns if it is owned by the supplied name. +func (c KubernetesDeploymentRouter) isOwnedByCanary(obj interface{}, name string) (bool, bool) { + var object metav1.Object + var ok bool + if object, ok = obj.(metav1.Object); ok { + if ownerRef := metav1.GetControllerOf(object); ownerRef != nil { + if ownerRef.Kind == flaggerv1.CanaryKind { + //And the name exists return true + if name == ownerRef.Name { + return true, true + } + return true, false + } + } + } + return false, false +} \ No newline at end of file diff --git a/pkg/router/router.go b/pkg/router/router.go index bd72e583..5a3a414a 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -6,4 +6,5 @@ type Interface interface { Reconcile(canary *flaggerv1.Canary) error SetRoutes(canary *flaggerv1.Canary, primaryWeight int, canaryWeight int, mirrored bool) error GetRoutes(canary *flaggerv1.Canary) (primaryWeight int, canaryWeight int, mirrored bool, err error) + Finalize(canary *flaggerv1.Canary) error }