diff --git a/Dockerfile b/Dockerfile index c3d05ceb68..ef7f575fb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,8 +31,18 @@ COPY . . ARG MAKE_TARGET="controller" RUN make ${MAKE_TARGET} -FROM debian:9.4 +FROM debian:9.5-slim -COPY dist/rollouts-controller /bin/ +COPY --from=builder /go/src/github.com/argoproj/argo-rollouts/dist/rollouts-controller /bin/ -ENTRYPOINT [ "/bin/rollouts-controller" ] +RUN groupadd -g 999 rollout-controller && \ + useradd -r -u 999 -g rollout-controller rollout-controller && \ + mkdir -p /home/rollout-controller && \ + chown rollout-controller:rollout-controller /home/rollout-controller + + +USER rollout-controller + +WORKDIR /home/rollout-controller + +ENTRYPOINT [ "/bin/rollouts-controller" ] \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock index 8d5894ab2f..f0079f4647 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -17,6 +17,17 @@ revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" +[[projects]] + branch = "master" + digest = "1:7942de144228c1b62eaba44ec5153a29d25cd016e8fb23cb9533c77f6111b397" + name = "github.com/docker/distribution" + packages = [ + "digestset", + "reference", + ] + pruneopts = "" + revision = "aa985ba8897a426e5db31b36cd8cca83cf85cead" + [[projects]] digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918" name = "github.com/gogo/protobuf" @@ -74,6 +85,14 @@ pruneopts = "" revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" +[[projects]] + digest = "1:a25a2c5ae694b01713fb6cd03c3b1ac1ccc1902b9f0a922680a88ec254f968e1" + name = "github.com/google/uuid" + packages = ["."] + pruneopts = "" + revision = "9b3b1e0f5f99ae461456d768e7d301a7acdaa2d8" + version = "v1.1.0" + [[projects]] digest = "1:16b2837c8b3cf045fa2cdc82af0cf78b19582701394484ae76b2c3bc3c99ad73" name = "github.com/googleapis/gnostic" @@ -140,6 +159,22 @@ revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" version = "1.0.1" +[[projects]] + digest = "1:5d9b668b0b4581a978f07e7d2e3314af18eb27b3fb5d19b70185b7c575723d11" + name = "github.com/opencontainers/go-digest" + packages = ["."] + pruneopts = "" + revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf" + version = "v1.0.0-rc1" + +[[projects]] + digest = "1:a5484d4fa43127138ae6e7b2299a6a52ae006c7f803d98d717f60abf3e97192e" + name = "github.com/pborman/uuid" + packages = ["."] + pruneopts = "" + revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1" + version = "v1.2" + [[projects]] branch = "master" digest = "1:c24598ffeadd2762552269271b3b1510df2d83ee6696c1e543a0ff653af494bc" @@ -156,6 +191,14 @@ revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" version = "v2.0.1" +[[projects]] + digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + pruneopts = "" + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + [[projects]] digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66" name = "github.com/spf13/pflag" @@ -164,11 +207,24 @@ revision = "298182f68c66c05229eb03ac171abe6e309ee79a" version = "v1.0.3" +[[projects]] + digest = "1:c587772fb8ad29ad4db67575dad25ba17a51f072ff18a22b4f0257a4d9c24f75" + name = "github.com/stretchr/testify" + packages = ["assert"] + pruneopts = "" + revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" + version = "v1.2.2" + [[projects]] branch = "master" digest = "1:f7be435e0ca22e2cd62b2d2542081a231685837170a87a3662abb7cdf9f3f1cd" name = "golang.org/x/crypto" - packages = ["ssh/terminal"] + packages = [ + "ed25519", + "ed25519/internal/edwards25519", + "pbkdf2", + "ssh/terminal", + ] pruneopts = "" revision = "3d3f9f413869b949e48070b5bc593aa22cc2b8f2" @@ -283,6 +339,19 @@ revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf" version = "v0.9.1" +[[projects]] + digest = "1:ddc5fa8f9159bea7d1ce58143e6d8fd8054018f7bc3709940aa7f7bc92855ed9" + name = "gopkg.in/square/go-jose.v2" + packages = [ + ".", + "cipher", + "json", + "jwt", + ] + pruneopts = "" + revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8" + version = "v2.1.9" + [[projects]] digest = "1:f0620375dd1f6251d9973b5f2596228cc8042e887cd7f827e4220bc1ce8c30e2" name = "gopkg.in/yaml.v2" @@ -292,8 +361,8 @@ version = "v2.2.1" [[projects]] - branch = "master" - digest = "1:9e4fda84888dc68a9a544e896cbcd20ef2a1a3a062b5ff06628b1b4e032402a3" + branch = "release-1.12" + digest = "1:3e3e9df293bd6f9fd64effc9fa1f0edcd97e6c74145cd9ab05d35719004dc41f" name = "k8s.io/api" packages = [ "admissionregistration/v1alpha1", @@ -329,19 +398,30 @@ "storage/v1beta1", ] pruneopts = "" - revision = "46ad728b8d13de6dffe4396623b098473f626e9e" + revision = "6db15a15d2d3874a6c3ddb2140ac9f3bc7058428" + +[[projects]] + branch = "master" + digest = "1:6ca711e706e87a3031952141888be08e459557c7f56984e9aa53cc3588557cd7" + name = "k8s.io/apiextensions-apiserver" + packages = ["pkg/features"] + pruneopts = "" + revision = "e8a638592964bfb3aaacb66d283c483097d1c0a7" [[projects]] branch = "master" digest = "1:3166a472475f9904e42f93565fca74f7f20d9a5f3bc1b0ac321aedc5a211e796" name = "k8s.io/apimachinery" packages = [ + "pkg/api/equality", "pkg/api/errors", "pkg/api/meta", "pkg/api/resource", + "pkg/api/validation", "pkg/apis/meta/internalversion", "pkg/apis/meta/v1", "pkg/apis/meta/v1/unstructured", + "pkg/apis/meta/v1/validation", "pkg/apis/meta/v1beta1", "pkg/conversion", "pkg/conversion/queryparams", @@ -367,9 +447,11 @@ "pkg/util/mergepatch", "pkg/util/naming", "pkg/util/net", + "pkg/util/rand", "pkg/util/runtime", "pkg/util/sets", "pkg/util/strategicpatch", + "pkg/util/uuid", "pkg/util/validation", "pkg/util/validation/field", "pkg/util/wait", @@ -383,7 +465,23 @@ revision = "1b0702fe2927f82126da9c2ecf567f14676f6072" [[projects]] - digest = "1:5d4153d12c3aed2c90a94262520d2498d5afa4d692554af55e65a7c5af0bc399" + branch = "release-1.12" + digest = "1:5c342c7c9c7ffc6a4791e1e9e9830025edd6c8630fcd90d4957f61fca07b314d" + name = "k8s.io/apiserver" + packages = [ + "pkg/authentication/authenticator", + "pkg/authentication/serviceaccount", + "pkg/authentication/user", + "pkg/features", + "pkg/storage/names", + "pkg/util/feature", + ] + pruneopts = "" + revision = "e17da99637d8a36a35704c07be9f4d628c185f83" + +[[projects]] + branch = "release-9.0" + digest = "1:303a14c5b87098d9ebf2773aba2ebc9b3f4808ba4e6d7c547b0fed4e624d9e30" name = "k8s.io/client-go" packages = [ "discovery", @@ -545,6 +643,7 @@ "tools/pager", "tools/record", "tools/reference", + "tools/watch", "transport", "util/buffer", "util/cert", @@ -557,8 +656,7 @@ "util/workqueue", ] pruneopts = "" - revision = "1638f8970cefaa404ff3a62950f88b08292b2696" - version = "v9.0.0" + revision = "02f343fb27038ed2f570c9ecb2484fb11f2a59a9" [[projects]] branch = "master" @@ -609,26 +707,57 @@ revision = "0317810137be915b9cf888946c6e115c1bfac693" [[projects]] - branch = "master" - digest = "1:a3f2cb02cdcfe50e04c8c5cf904b6b208820be3d32b756dab0ab645167f16cfc" - name = "k8s.io/sample-controller" + digest = "1:6061aa42761235df375f20fa4a1aa6d1845cba3687575f3adb2ef3f3bc540af5" + name = "k8s.io/kubernetes" packages = [ - "pkg/apis/samplecontroller", - "pkg/apis/samplecontroller/v1alpha1", - "pkg/client/clientset/versioned", - "pkg/client/clientset/versioned/fake", - "pkg/client/clientset/versioned/scheme", - "pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1", - "pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake", - "pkg/client/informers/externalversions", - "pkg/client/informers/externalversions/internalinterfaces", - "pkg/client/informers/externalversions/samplecontroller", - "pkg/client/informers/externalversions/samplecontroller/v1alpha1", - "pkg/client/listers/samplecontroller/v1alpha1", - "pkg/signals", + "pkg/api/legacyscheme", + "pkg/api/service", + "pkg/api/v1/pod", + "pkg/apis/autoscaling", + "pkg/apis/core", + "pkg/apis/core/helper", + "pkg/apis/core/install", + "pkg/apis/core/pods", + "pkg/apis/core/v1", + "pkg/apis/core/v1/helper", + "pkg/apis/core/validation", + "pkg/apis/extensions", + "pkg/apis/networking", + "pkg/apis/policy", + "pkg/apis/scheduling", + "pkg/capabilities", + "pkg/controller", + "pkg/features", + "pkg/fieldpath", + "pkg/kubelet/apis", + "pkg/kubelet/types", + "pkg/master/ports", + "pkg/scheduler/algorithm", + "pkg/scheduler/algorithm/priorities/util", + "pkg/scheduler/api", + "pkg/scheduler/cache", + "pkg/scheduler/util", + "pkg/security/apparmor", + "pkg/serviceaccount", + "pkg/util/file", + "pkg/util/hash", + "pkg/util/labels", + "pkg/util/net/sets", + "pkg/util/node", + "pkg/util/parsers", + "pkg/util/taints", ] pruneopts = "" - revision = "e943f6752ec4a0dc71a14511d05029fa3e96ba7d" + revision = "17c77c7898218073f14c8d573582e8d2313dc740" + version = "v1.12.2" + +[[projects]] + branch = "master" + digest = "1:bea542e853f98bfcc80ecbe8fe0f32bc52c97664102aacdd7dca676354ef2faa" + name = "k8s.io/utils" + packages = ["pointer"] + pruneopts = "" + revision = "0d26856f57b32ec3398579285e5c8a2bfe8c5243" [[projects]] digest = "1:321081b4a44256715f2b68411d8eda9a17f17ebfe6f0cc61d2cc52d11c08acfa" @@ -642,9 +771,12 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/stretchr/testify/assert", "k8s.io/api/apps/v1", "k8s.io/api/core/v1", + "k8s.io/apimachinery/pkg/api/equality", "k8s.io/apimachinery/pkg/api/errors", + "k8s.io/apimachinery/pkg/api/meta", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/runtime", @@ -652,18 +784,24 @@ "k8s.io/apimachinery/pkg/runtime/serializer", "k8s.io/apimachinery/pkg/types", "k8s.io/apimachinery/pkg/util/diff", + "k8s.io/apimachinery/pkg/util/intstr", + "k8s.io/apimachinery/pkg/util/rand", "k8s.io/apimachinery/pkg/util/runtime", + "k8s.io/apimachinery/pkg/util/uuid", "k8s.io/apimachinery/pkg/util/wait", "k8s.io/apimachinery/pkg/watch", + "k8s.io/apiserver/pkg/storage/names", "k8s.io/client-go/discovery", "k8s.io/client-go/discovery/fake", "k8s.io/client-go/informers", "k8s.io/client-go/informers/apps/v1", + "k8s.io/client-go/informers/core/v1", "k8s.io/client-go/kubernetes", "k8s.io/client-go/kubernetes/fake", "k8s.io/client-go/kubernetes/scheme", "k8s.io/client-go/kubernetes/typed/core/v1", "k8s.io/client-go/listers/apps/v1", + "k8s.io/client-go/listers/core/v1", "k8s.io/client-go/plugin/pkg/client/auth/gcp", "k8s.io/client-go/plugin/pkg/client/auth/oidc", "k8s.io/client-go/rest", @@ -675,19 +813,10 @@ "k8s.io/client-go/util/workqueue", "k8s.io/code-generator/cmd/client-gen", "k8s.io/klog", - "k8s.io/sample-controller/pkg/apis/samplecontroller", - "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1", - "k8s.io/sample-controller/pkg/client/clientset/versioned", - "k8s.io/sample-controller/pkg/client/clientset/versioned/fake", - "k8s.io/sample-controller/pkg/client/clientset/versioned/scheme", - "k8s.io/sample-controller/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1", - "k8s.io/sample-controller/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake", - "k8s.io/sample-controller/pkg/client/informers/externalversions", - "k8s.io/sample-controller/pkg/client/informers/externalversions/internalinterfaces", - "k8s.io/sample-controller/pkg/client/informers/externalversions/samplecontroller", - "k8s.io/sample-controller/pkg/client/informers/externalversions/samplecontroller/v1alpha1", - "k8s.io/sample-controller/pkg/client/listers/samplecontroller/v1alpha1", - "k8s.io/sample-controller/pkg/signals", + "k8s.io/kubernetes/pkg/apis/core", + "k8s.io/kubernetes/pkg/controller", + "k8s.io/kubernetes/pkg/util/hash", + "k8s.io/kubernetes/pkg/util/labels", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 5caa3f63c5..0f85304c55 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,3 +1,23 @@ required = [ - "k8s.io/code-generator/cmd/client-gen" -] \ No newline at end of file + "k8s.io/code-generator/cmd/client-gen", +] + +[[constraint]] + name = "k8s.io/client-go" + branch = "release-9.0" + + +[[constraint]] + branch = "release-1.12" + name = "k8s.io/api" + + +# To use reference package: +# vendor/k8s.io/kubernetes/pkg/util/parsers/parsers.go:36:16: undefined: reference.ParseNormalizedNamed +[[override]] + name = "github.com/docker/distribution" + branch = "master" + +[[override]] + name = "k8s.io/apiserver" + branch = "release-1.12" diff --git a/Makefile b/Makefile index 80eef8459d..79516f239c 100644 --- a/Makefile +++ b/Makefile @@ -52,8 +52,8 @@ controller: clean-debug .PHONY: image image: - docker build -t $(IMAGE_PREFIX)argo-rollouts:$(IMAGE_TAG) . - @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argo-rollouts:$(IMAGE_TAG) ; fi + docker build -t $(IMAGE_PREFIX)rollout-controller:$(IMAGE_TAG) . + @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)rollout-controller:$(IMAGE_TAG) ; fi .PHONY: lint lint: @@ -63,6 +63,9 @@ lint: test: go test -v -covermode=count -coverprofile=coverage.out `go list ./...` +.PHONY: manifests +manifests: + ./hack/update-manifests.sh # Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes .PHONY: clean-debug diff --git a/README.md b/README.md index b4fbab7c9c..f97d4fe652 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ -# Argo-Rollouts \ No newline at end of file +# Rollout-Controller + +Kubernetes users have two deployment options when they leverage the Kubernetes resources that come out of the Box. They can manually manage the replicasSets and do their own orchestruction to achieve various deployment strageties + +## Spec + +Rollout Spec + + +Active Service: +Add notes about it just takes the service as is and only adds a selector + +## Deployment Strageties + +### Blue-Green deployment + +#### Steps + +#### Optional flags + +### Canary +TBD \ No newline at end of file diff --git a/artifacts/examples/crd-status-subresource.yaml b/artifacts/examples/crd-status-subresource.yaml deleted file mode 100644 index af74dbc5bf..0000000000 --- a/artifacts/examples/crd-status-subresource.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: foos.samplecontroller.k8s.io -spec: - group: samplecontroller.k8s.io - version: v1alpha1 - names: - kind: Foo - plural: foos - scope: Namespaced - subresources: - status: {} diff --git a/artifacts/examples/crd-validation.yaml b/artifacts/examples/crd-validation.yaml deleted file mode 100644 index 36469161c6..0000000000 --- a/artifacts/examples/crd-validation.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: foos.samplecontroller.k8s.io -spec: - group: samplecontroller.k8s.io - version: v1alpha1 - names: - kind: Foo - plural: foos - scope: Namespaced - validation: - openAPIV3Schema: - properties: - spec: - properties: - replicas: - type: integer - minimum: 1 - maximum: 10 diff --git a/artifacts/examples/example-foo.yaml b/artifacts/examples/example-foo.yaml deleted file mode 100644 index 897059c3d0..0000000000 --- a/artifacts/examples/example-foo.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: samplecontroller.k8s.io/v1alpha1 -kind: Foo -metadata: - name: example-foo -spec: - deploymentName: example-foo - replicas: 1 diff --git a/cmd/rollouts-controller/main.go b/cmd/rollouts-controller/main.go index 1ed1f131a8..db53b5cb7b 100644 --- a/cmd/rollouts-controller/main.go +++ b/cmd/rollouts-controller/main.go @@ -1,19 +1,3 @@ -/* -Copyright 2017 The Kubernetes Authors. - -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 main import ( @@ -28,11 +12,11 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // load the oidc plugin (required to authenticate with OpenID Connect). _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" - clientset "k8s.io/sample-controller/pkg/client/clientset/versioned" - informers "k8s.io/sample-controller/pkg/client/informers/externalversions" - "k8s.io/sample-controller/pkg/signals" "github.com/argoproj/argo-rollouts/controller" + clientset "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + informers "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions" + "github.com/argoproj/argo-rollouts/pkg/signals" ) var ( @@ -56,22 +40,23 @@ func main() { klog.Fatalf("Error building kubernetes clientset: %s", err.Error()) } - exampleClient, err := clientset.NewForConfig(cfg) + rolloutClient, err := clientset.NewForConfig(cfg) if err != nil { klog.Fatalf("Error building example clientset: %s", err.Error()) } kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30) - exampleInformerFactory := informers.NewSharedInformerFactory(exampleClient, time.Second*30) + rolloutInformerFactory := informers.NewSharedInformerFactory(rolloutClient, time.Second*30) - controller := controller.NewController(kubeClient, exampleClient, - kubeInformerFactory.Apps().V1().Deployments(), - exampleInformerFactory.Samplecontroller().V1alpha1().Foos()) + controller := controller.NewController(kubeClient, rolloutClient, + kubeInformerFactory.Apps().V1().ReplicaSets(), + kubeInformerFactory.Core().V1().Services(), + rolloutInformerFactory.Argoproj().V1alpha1().Rollouts()) // notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh) // Start method is non-blocking and runs all registered informers in a dedicated goroutine. kubeInformerFactory.Start(stopCh) - exampleInformerFactory.Start(stopCh) + rolloutInformerFactory.Start(stopCh) if err = controller.Run(2, stopCh); err != nil { klog.Fatalf("Error running controller: %s", err.Error()) diff --git a/controller/bluegreen.go b/controller/bluegreen.go new file mode 100644 index 0000000000..5e0348549b --- /dev/null +++ b/controller/bluegreen.go @@ -0,0 +1,207 @@ +package controller + +import ( + "fmt" + "sort" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/klog" + "k8s.io/kubernetes/pkg/controller" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/utils/annotations" + replicasetutil "github.com/argoproj/argo-rollouts/utils/replicaset" +) + +// rolloutBlueGreen implements the logic for rolling a new replica set. +func (c *Controller) rolloutBlueGreen(r *v1alpha1.Rollout, rsList []*appsv1.ReplicaSet) error { + newRS, oldRSs, err := c.getAllReplicaSetsAndSyncRevision(r, rsList, true) + if err != nil { + return err + } + previewSvc, activeSvc, err := c.getPreviewAndActiveServices(r) + if err != nil { + return err + } + allRSs := append(oldRSs, newRS) + + // Scale up, if we can. + scaledUp, err := c.reconcileNewReplicaSet(allRSs, newRS, r) + if err != nil { + return err + } + if scaledUp { + return c.syncRolloutStatus(allRSs, newRS, previewSvc, activeSvc, r) + } + + if previewSvc != nil { + switchPreviewSvc, err := c.reconcilePreviewService(r, newRS, previewSvc, activeSvc) + if err != nil { + return err + } + if switchPreviewSvc { + return c.syncRolloutStatus(allRSs, newRS, previewSvc, activeSvc, r) + } + + verfyingPreview := c.reconcileVerifyingPreview(activeSvc, r) + if verfyingPreview { + return c.syncRolloutStatus(allRSs, newRS, previewSvc, activeSvc, r) + } + } + + switchActiveSvc, err := c.reconcileActiveService(r, newRS, previewSvc, activeSvc) + if err != nil { + return err + } + if switchActiveSvc { + return c.syncRolloutStatus(allRSs, newRS, previewSvc, activeSvc, r) + } + // Scale down, if we can. + scaledDown, err := c.reconcileOldReplicaSets(allRSs, controller.FilterActiveReplicaSets(oldRSs), newRS, r) + if err != nil { + return err + } + if scaledDown { + return c.syncRolloutStatus(allRSs, newRS, previewSvc, activeSvc, r) + } + + return c.syncRolloutStatus(allRSs, newRS, previewSvc, activeSvc, r) +} + +func (c *Controller) reconcileVerifyingPreview(activeSvc *corev1.Service, rollout *v1alpha1.Rollout) bool { + if rollout.Spec.Strategy.BlueGreenStrategy.PreviewService == "" { + return false + } + if _, ok := activeSvc.Spec.Selector[v1alpha1.DefaultRolloutUniqueLabelKey]; !ok { + return false + } + + if rollout.Status.VerifyingPreview == nil { + return false + } + + return *rollout.Status.VerifyingPreview +} + +func (c *Controller) reconcileNewReplicaSet(allRSs []*appsv1.ReplicaSet, newRS *appsv1.ReplicaSet, rollout *v1alpha1.Rollout) (bool, error) { + if *(newRS.Spec.Replicas) == *(rollout.Spec.Replicas) { + // Scaling not required. + return false, nil + } + if *(newRS.Spec.Replicas) > *(rollout.Spec.Replicas) { + // Scale down. + scaled, _, err := c.scaleReplicaSetAndRecordEvent(newRS, *(rollout.Spec.Replicas), rollout) + return scaled, err + } + newReplicasCount, err := replicasetutil.NewRSNewReplicas(rollout, allRSs, newRS) + if err != nil { + return false, err + } + scaled, _, err := c.scaleReplicaSetAndRecordEvent(newRS, newReplicasCount, rollout) + return scaled, err +} + +func (c *Controller) reconcileOldReplicaSets(allRSs []*appsv1.ReplicaSet, oldRSs []*appsv1.ReplicaSet, newRS *appsv1.ReplicaSet, rollout *v1alpha1.Rollout) (bool, error) { + oldPodsCount := replicasetutil.GetReplicaCountForReplicaSets(oldRSs) + if oldPodsCount == 0 { + // Can't scale down further + return false, nil + } + + klog.V(4).Infof("New replica set %s/%s has %d available pods.", newRS.Namespace, newRS.Name, newRS.Status.AvailableReplicas) + if !annotations.IsSaturated(rollout, newRS) { + return false, nil + } + // Add check for active service + + // Clean up unhealthy replicas first, otherwise unhealthy replicas will block deployment + // and cause timeout. See https://github.com/kubernetes/kubernetes/issues/16737 + oldRSs, cleanupCount, err := c.cleanupUnhealthyReplicas(oldRSs, rollout) + if err != nil { + return false, nil + } + klog.V(4).Infof("Cleaned up unhealthy replicas from old RSes by %d", cleanupCount) + + // Scale down old replica sets, need check replicasToKeep to ensure we can scale down + allRSs = append(oldRSs, newRS) + scaledDownCount, err := c.scaleDownOldReplicaSetsForBlueGreen(allRSs, oldRSs, rollout) + if err != nil { + return false, nil + } + klog.V(4).Infof("Scaled down old RSes of deployment %s by %d", rollout.Name, scaledDownCount) + + totalScaledDown := cleanupCount + scaledDownCount + return totalScaledDown > 0, nil +} + +// cleanupUnhealthyReplicas will scale down old replica sets with unhealthy replicas, so that all unhealthy replicas will be deleted. +func (c *Controller) cleanupUnhealthyReplicas(oldRSs []*appsv1.ReplicaSet, rollout *v1alpha1.Rollout) ([]*appsv1.ReplicaSet, int32, error) { + sort.Sort(controller.ReplicaSetsByCreationTimestamp(oldRSs)) + // Safely scale down all old replica sets with unhealthy replicas. Replica set will sort the pods in the order + // such that not-ready < ready, unscheduled < scheduled, and pending < running. This ensures that unhealthy replicas will + // been deleted first and won't increase unavailability. + totalScaledDown := int32(0) + for i, targetRS := range oldRSs { + if *(targetRS.Spec.Replicas) == 0 { + // cannot scale down this replica set. + continue + } + klog.V(4).Infof("Found %d available pods in old RS %s/%s", targetRS.Status.AvailableReplicas, targetRS.Namespace, targetRS.Name) + if *(targetRS.Spec.Replicas) == targetRS.Status.AvailableReplicas { + // no unhealthy replicas found, no scaling required. + continue + } + + scaledDownCount := *(targetRS.Spec.Replicas) - targetRS.Status.AvailableReplicas + newReplicasCount := targetRS.Status.AvailableReplicas + if newReplicasCount > *(targetRS.Spec.Replicas) { + return nil, 0, fmt.Errorf("when cleaning up unhealthy replicas, got invalid request to scale down %s/%s %d -> %d", targetRS.Namespace, targetRS.Name, *(targetRS.Spec.Replicas), newReplicasCount) + } + _, updatedOldRS, err := c.scaleReplicaSetAndRecordEvent(targetRS, newReplicasCount, rollout) + if err != nil { + return nil, totalScaledDown, err + } + totalScaledDown += scaledDownCount + oldRSs[i] = updatedOldRS + } + return oldRSs, totalScaledDown, nil +} + +// scaleDownOldReplicaSetsForBlueGreen scales down old replica sets when rollout strategy is "Blue Green". +func (c *Controller) scaleDownOldReplicaSetsForBlueGreen(allRSs []*appsv1.ReplicaSet, oldRSs []*appsv1.ReplicaSet, rollout *v1alpha1.Rollout) (int32, error) { + availablePodCount := replicasetutil.GetAvailableReplicaCountForReplicaSets(allRSs) + if availablePodCount <= *(rollout.Spec.Replicas) { + // Cannot scale down. + return 0, nil + } + klog.V(4).Infof("Found %d available pods in rollout %s, scaling down old RSes", availablePodCount, rollout.Name) + + sort.Sort(controller.ReplicaSetsByCreationTimestamp(oldRSs)) + + totalScaledDown := int32(0) + for _, targetRS := range oldRSs { + if *(targetRS.Spec.Replicas) == 0 { + // cannot scale down this ReplicaSet. + continue + } + scaleDownCount := *(targetRS.Spec.Replicas) + // Scale down. + newReplicasCount := int32(0) + _, _, err := c.scaleReplicaSetAndRecordEvent(targetRS, newReplicasCount, rollout) + if err != nil { + return totalScaledDown, err + } + + totalScaledDown += scaleDownCount + } + + return totalScaledDown, nil +} + +func (c *Controller) setVerifyingPreview(r *v1alpha1.Rollout) error { + verifyPreprod := true + r.Status.VerifyingPreview = &verifyPreprod + _, err := c.rolloutsclientset.ArgoprojV1alpha1().Rollouts(r.Namespace).Update(r) + return err +} diff --git a/controller/bluegreen_test.go b/controller/bluegreen_test.go new file mode 100644 index 0000000000..02042d557c --- /dev/null +++ b/controller/bluegreen_test.go @@ -0,0 +1,242 @@ +package controller + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sfake "k8s.io/client-go/kubernetes/fake" + core "k8s.io/client-go/testing" + "k8s.io/client-go/tools/record" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake" + "github.com/argoproj/argo-rollouts/utils/annotations" +) + +var ( + noTimestamp = metav1.Time{} +) + +func TestController_reconcileVerifyingPreview(t *testing.T) { + boolPtr := func(boolean bool) *bool { return &boolean } + tests := []struct { + name string + activeSvc *corev1.Service + previewSvcName string + verifyingPreviewFlag *bool + notFinishedVerifying bool + }{ + { + name: "Continue if preview Service isn't specificed", + activeSvc: newService("active", 80, nil), + verifyingPreviewFlag: boolPtr(true), + notFinishedVerifying: false, + }, + { + name: "Continue if active service doesn't have a selector from the rollout", + previewSvcName: "previewSvc", + activeSvc: newService("active", 80, nil), + verifyingPreviewFlag: boolPtr(true), + notFinishedVerifying: false, + }, + { + name: "Do not continue if verifyingPreview flag is true", + previewSvcName: "previewSvc", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "test"}), + verifyingPreviewFlag: boolPtr(true), + notFinishedVerifying: true, + }, + { + name: "Continue if verifyingPreview flag is false", + previewSvcName: "previewSvc", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "test"}), + verifyingPreviewFlag: boolPtr(false), + notFinishedVerifying: false, + }, + { + name: "Continue if verifyingPreview flag is not set", + previewSvcName: "previewSvc", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "test"}), + notFinishedVerifying: false, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + rollout := newRollout("foo", 1, nil, map[string]string{"foo": "bar"}, "", test.previewSvcName) + rollout.Status = v1alpha1.RolloutStatus{ + VerifyingPreview: test.verifyingPreviewFlag, + } + fake := fake.Clientset{} + k8sfake := k8sfake.Clientset{} + controller := &Controller{ + rolloutsclientset: &fake, + kubeclientset: &k8sfake, + recorder: &record.FakeRecorder{}, + } + finishedVerifying := controller.reconcileVerifyingPreview(test.activeSvc, rollout) + assert.Equal(t, test.notFinishedVerifying, finishedVerifying) + }) + } +} + +func TestController_reconcileNewReplicaSet(t *testing.T) { + tests := []struct { + name string + rolloutReplicas int + newReplicas int + scaleExpected bool + expectedNewReplicas int + }{ + { + name: "New Replica Set matches rollout replica: No scale", + rolloutReplicas: 10, + newReplicas: 10, + scaleExpected: false, + }, + { + name: "New Replica Set higher than rollout replica: Scale down", + rolloutReplicas: 10, + newReplicas: 12, + scaleExpected: true, + expectedNewReplicas: 10, + }, + { + name: "New Replica Set lower than rollout replica: Scale up", + rolloutReplicas: 10, + newReplicas: 8, + scaleExpected: true, + expectedNewReplicas: 10, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + test := tests[i] + newRS := rs("foo-v2", test.newReplicas, nil, noTimestamp, nil) + allRSs := []*appsv1.ReplicaSet{newRS} + rollout := newRollout("foo", test.rolloutReplicas, nil, map[string]string{"foo": "bar"}, "", "") + fake := fake.Clientset{} + k8sfake := k8sfake.Clientset{} + controller := &Controller{ + rolloutsclientset: &fake, + kubeclientset: &k8sfake, + recorder: &record.FakeRecorder{}, + } + scaled, err := controller.reconcileNewReplicaSet(allRSs, newRS, rollout) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + if !test.scaleExpected { + if scaled || len(fake.Actions()) > 0 { + t.Errorf("unexpected scaling: %v", fake.Actions()) + } + return + } + if test.scaleExpected && !scaled { + t.Errorf("expected scaling to occur") + return + } + if len(k8sfake.Actions()) != 1 { + t.Errorf("expected 1 action during scale, got: %v", fake.Actions()) + return + } + updated := k8sfake.Actions()[0].(core.UpdateAction).GetObject().(*appsv1.ReplicaSet) + if e, a := test.expectedNewReplicas, int(*(updated.Spec.Replicas)); e != a { + t.Errorf("expected update to %d replicas, got %d", e, a) + } + }) + } +} + +func TestController_reconcileOldReplicaSet(t *testing.T) { + tests := []struct { + name string + rolloutReplicas int + oldReplicas int + newReplicas int + readyPodsFromOldRS int + readyPodsFromNewRS int + scaleExpected bool + expectedOldReplicas int + }{ + { + name: "No pods to scale down", + rolloutReplicas: 10, + oldReplicas: 0, + newReplicas: 10, + readyPodsFromOldRS: 0, + readyPodsFromNewRS: 0, + scaleExpected: false, + }, + { + name: "New ReplicaSet is not fully healthy", + rolloutReplicas: 10, + oldReplicas: 10, + newReplicas: 10, + readyPodsFromOldRS: 10, + readyPodsFromNewRS: 9, + scaleExpected: false, + }, + { + name: "Clean up unhealthy pods", + rolloutReplicas: 10, + oldReplicas: 10, + newReplicas: 10, + readyPodsFromOldRS: 8, + readyPodsFromNewRS: 10, + scaleExpected: true, + expectedOldReplicas: 0, + }, + { + name: "Normal scale down when new ReplicaSet is healthy", + rolloutReplicas: 10, + oldReplicas: 10, + newReplicas: 10, + readyPodsFromOldRS: 10, + readyPodsFromNewRS: 10, + scaleExpected: true, + expectedOldReplicas: 0, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + newSelector := map[string]string{"foo": "new"} + oldSelector := map[string]string{"foo": "old"} + newRS := rs("foo-new", test.newReplicas, newSelector, noTimestamp, nil) + newRS.Annotations = map[string]string{annotations.DesiredReplicasAnnotation: strconv.Itoa(test.newReplicas)} + newRS.Status.AvailableReplicas = int32(test.readyPodsFromNewRS) + oldRS := rs("foo-old", test.oldReplicas, oldSelector, noTimestamp, nil) + oldRS.Annotations = map[string]string{annotations.DesiredReplicasAnnotation: strconv.Itoa(test.oldReplicas)} + oldRS.Status.AvailableReplicas = int32(test.readyPodsFromOldRS) + oldRSs := []*appsv1.ReplicaSet{oldRS} + allRSs := []*appsv1.ReplicaSet{oldRS, newRS} + rollout := newRollout("foo", test.rolloutReplicas, nil, newSelector, "", "") + fake := fake.Clientset{} + k8sfake := k8sfake.Clientset{} + controller := &Controller{ + rolloutsclientset: &fake, + kubeclientset: &k8sfake, + recorder: &record.FakeRecorder{}, + } + scaled, err := controller.reconcileOldReplicaSets(allRSs, oldRSs, newRS, rollout) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + if !test.scaleExpected && scaled { + t.Errorf("unexpected scaling: %v", k8sfake.Actions()) + } + if test.scaleExpected && !scaled { + t.Errorf("expected scaling to occur") + return + } + }) + } +} diff --git a/controller/controller.go b/controller/controller.go index b971087cd2..d45c76ab25 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -1,78 +1,73 @@ -/* -Copyright 2017 The Kubernetes Authors. - -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 ( "fmt" + "reflect" "time" 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/runtime/schema" "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" appsinformers "k8s.io/client-go/informers/apps/v1" + coreinformers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" appslisters "k8s.io/client-go/listers/apps/v1" + corelisters "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" "k8s.io/klog" + "k8s.io/kubernetes/pkg/controller" - samplev1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" - clientset "k8s.io/sample-controller/pkg/client/clientset/versioned" - samplescheme "k8s.io/sample-controller/pkg/client/clientset/versioned/scheme" - informers "k8s.io/sample-controller/pkg/client/informers/externalversions/samplecontroller/v1alpha1" - listers "k8s.io/sample-controller/pkg/client/listers/samplecontroller/v1alpha1" + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + clientset "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + rolloutscheme "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/scheme" + informers "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/rollouts/v1alpha1" + listers "github.com/argoproj/argo-rollouts/pkg/client/listers/rollouts/v1alpha1" ) -const controllerAgentName = "sample-controller" +const controllerAgentName = "rollouts-controller" const ( - // SuccessSynced is used as part of the Event 'reason' when a Foo is synced + // SuccessSynced is used as part of the Event 'reason' when a Rollout is synced SuccessSynced = "Synced" - // ErrResourceExists is used as part of the Event 'reason' when a Foo fails - // to sync due to a Deployment of the same name already existing. + // ErrResourceExists is used as part of the Event 'reason' when a Rollout fails + // to sync due to a Replica of the same name already existing. ErrResourceExists = "ErrResourceExists" // MessageResourceExists is the message used for Events when a resource - // fails to sync due to a Deployment already existing - MessageResourceExists = "Resource %q already exists and is not managed by Foo" - // MessageResourceSynced is the message used for an Event fired when a Foo + // fails to sync due to a Replica already existing + MessageResourceExists = "Resource %q already exists and is not managed by Rollout" + // MessageResourceSynced is the message used for an Event fired when a Rollout // is synced successfully - MessageResourceSynced = "Foo synced successfully" + MessageResourceSynced = "Rollout synced successfully" ) -// Controller is the controller implementation for Foo resources +// Controller is the controller implementation for Rollout resources type Controller struct { + // rsControl is used for adopting/releasing replica sets. + replicaSetControl controller.RSControlInterface + // kubeclientset is a standard kubernetes clientset kubeclientset kubernetes.Interface - // sampleclientset is a clientset for our own API group - sampleclientset clientset.Interface + // rolloutsclientset is a clientset for our own API group + rolloutsclientset clientset.Interface + + replicaSetLister appslisters.ReplicaSetLister + replicaSetSynced cache.InformerSynced + serviceLister corelisters.ServiceLister + serviceSynced cache.InformerSynced + rolloutsLister listers.RolloutLister + rolloutsSynced cache.InformerSynced - deploymentsLister appslisters.DeploymentLister - deploymentsSynced cache.InformerSynced - foosLister listers.FooLister - foosSynced cache.InformerSynced + // used for unit testing + enqueueRollout func(obj interface{}) // workqueue is a rate limited work queue. This is used to queue work to be // processed instead of performing it as soon as a change happens. This @@ -85,56 +80,59 @@ type Controller struct { recorder record.EventRecorder } -// NewController returns a new sample controller +// NewController returns a new rollout controller func NewController( kubeclientset kubernetes.Interface, - sampleclientset clientset.Interface, - deploymentInformer appsinformers.DeploymentInformer, - fooInformer informers.FooInformer) *Controller { + rolloutsclientset clientset.Interface, + replicaSetInformer appsinformers.ReplicaSetInformer, + serviceInformer coreinformers.ServiceInformer, + rolloutsInformer informers.RolloutInformer) *Controller { // Create event broadcaster - // Add sample-controller types to the default Kubernetes Scheme so Events can be - // logged for sample-controller types. - utilruntime.Must(samplescheme.AddToScheme(scheme.Scheme)) + // Add rollouts-controller types to the default Kubernetes Scheme so Events can be + // logged for rollout-controller types. + utilruntime.Must(rolloutscheme.AddToScheme(scheme.Scheme)) klog.V(4).Info("Creating event broadcaster") eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(klog.Infof) eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")}) recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName}) + replicaSetControl := controller.RealRSControl{ + KubeClient: kubeclientset, + Recorder: recorder, + } controller := &Controller{ kubeclientset: kubeclientset, - sampleclientset: sampleclientset, - deploymentsLister: deploymentInformer.Lister(), - deploymentsSynced: deploymentInformer.Informer().HasSynced, - foosLister: fooInformer.Lister(), - foosSynced: fooInformer.Informer().HasSynced, - workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Foos"), + rolloutsclientset: rolloutsclientset, + replicaSetControl: replicaSetControl, + replicaSetLister: replicaSetInformer.Lister(), + replicaSetSynced: replicaSetInformer.Informer().HasSynced, + serviceLister: serviceInformer.Lister(), + serviceSynced: serviceInformer.Informer().HasSynced, + rolloutsLister: rolloutsInformer.Lister(), + rolloutsSynced: rolloutsInformer.Informer().HasSynced, + workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Rollouts"), recorder: recorder, } + controller.enqueueRollout = controller.enqueueRateLimited klog.Info("Setting up event handlers") - // Set up an event handler for when Foo resources change - fooInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: controller.enqueueFoo, + // Set up an event handler for when rollout resources change + rolloutsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: controller.enqueueRollout, UpdateFunc: func(old, new interface{}) { - controller.enqueueFoo(new) + controller.enqueueRollout(new) }, }) - // Set up an event handler for when Deployment resources change. This - // handler will lookup the owner of the given Deployment, and if it is - // owned by a Foo resource will enqueue that Foo resource for - // processing. This way, we don't need to implement custom logic for - // handling Deployment resources. More info on this pattern: - // https://github.com/kubernetes/community/blob/8cafef897a22026d42f5e5bb3f104febe7e29830/contributors/devel/controllers.md - deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + replicaSetInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: controller.handleObject, UpdateFunc: func(old, new interface{}) { - newDepl := new.(*appsv1.Deployment) - oldDepl := old.(*appsv1.Deployment) - if newDepl.ResourceVersion == oldDepl.ResourceVersion { - // Periodic resync will send update events for all known Deployments. - // Two different versions of the same Deployment will always have different RVs. + newRS := new.(*appsv1.ReplicaSet) + oldRS := old.(*appsv1.ReplicaSet) + if newRS.ResourceVersion == oldRS.ResourceVersion { + // Periodic resync will send update events for all known replicas. + // Two different versions of the same Replica will always have different RVs. return } controller.handleObject(new) @@ -142,6 +140,12 @@ func NewController( DeleteFunc: controller.handleObject, }) + serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: controller.handleService, + UpdateFunc: controller.updateService, + DeleteFunc: controller.handleService, + }) + return controller } @@ -154,16 +158,16 @@ func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error { defer c.workqueue.ShutDown() // Start the informer factories to begin populating the informer caches - klog.Info("Starting Foo controller") + klog.Info("Starting Rollout controller") // Wait for the caches to be synced before starting workers klog.Info("Waiting for informer caches to sync") - if ok := cache.WaitForCacheSync(stopCh, c.deploymentsSynced, c.foosSynced); !ok { + if ok := cache.WaitForCacheSync(stopCh, c.replicaSetSynced, c.rolloutsSynced); !ok { return fmt.Errorf("failed to wait for caches to sync") } klog.Info("Starting workers") - // Launch two workers to process Foo resources + // Launch two workers to process Rollout resources for i := 0; i < threadiness; i++ { go wait.Until(c.runWorker, time.Second, stopCh) } @@ -217,7 +221,7 @@ func (c *Controller) processNextWorkItem() bool { return nil } // Run the syncHandler, passing it the namespace/name string of the - // Foo resource to be synced. + // Rollout resource to be synced. if err := c.syncHandler(key); err != nil { // Put the item back on the workqueue to handle any transient errors. c.workqueue.AddRateLimited(key) @@ -239,104 +243,69 @@ func (c *Controller) processNextWorkItem() bool { } // syncHandler compares the actual state with the desired, and attempts to -// converge the two. It then updates the Status block of the Foo resource +// converge the two. It then updates the Status block of the Rollout resource // with the current status of the resource. func (c *Controller) syncHandler(key string) error { - // Convert the namespace/name string into a distinct namespace and name - namespace, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - runtime.HandleError(fmt.Errorf("invalid resource key: %s", key)) - return nil - } + startTime := time.Now() + klog.V(4).Infof("Started syncing rollout %q (%v)", key, startTime) + defer func() { + klog.V(4).Infof("Finished syncing rollout %q (%v)", key, time.Since(startTime)) + }() - // Get the Foo resource with this namespace/name - foo, err := c.foosLister.Foos(namespace).Get(name) + namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { - // The Foo resource may no longer exist, in which case we stop - // processing. - if errors.IsNotFound(err) { - runtime.HandleError(fmt.Errorf("foo '%s' in work queue no longer exists", key)) - return nil - } - return err } - - deploymentName := foo.Spec.DeploymentName - if deploymentName == "" { - // We choose to absorb the error here as the worker would requeue the - // resource otherwise. Instead, the next time the resource is updated - // the resource will be queued again. - runtime.HandleError(fmt.Errorf("%s: deployment name must be specified", key)) - return nil - } - - // Get the deployment with the name specified in Foo.spec - deployment, err := c.deploymentsLister.Deployments(foo.Namespace).Get(deploymentName) - // If the resource doesn't exist, we'll create it + rollout, err := c.rolloutsLister.Rollouts(namespace).Get(name) if errors.IsNotFound(err) { - deployment, err = c.kubeclientset.AppsV1().Deployments(foo.Namespace).Create(newDeployment(foo)) + klog.V(2).Infof("Rollout %v has been deleted", key) + return nil } - - // If an error occurs during Get/Create, we'll requeue the item so we can - // attempt processing again later. This could have been caused by a - // temporary network failure, or any other transient reason. if err != nil { return err } - // If the Deployment is not controlled by this Foo resource, we should log - // a warning to the event recorder and ret - if !metav1.IsControlledBy(deployment, foo) { - msg := fmt.Sprintf(MessageResourceExists, deployment.Name) - c.recorder.Event(foo, corev1.EventTypeWarning, ErrResourceExists, msg) - return fmt.Errorf(msg) - } + // Deep-copy otherwise we are mutating our cache. + r := rollout.DeepCopy() - // If this number of the replicas on the Foo resource is specified, and the - // number does not equal the current desired replicas on the Deployment, we - // should update the Deployment resource. - if foo.Spec.Replicas != nil && *foo.Spec.Replicas != *deployment.Spec.Replicas { - klog.V(4).Infof("Foo %s replicas: %d, deployment replicas: %d", name, *foo.Spec.Replicas, *deployment.Spec.Replicas) - deployment, err = c.kubeclientset.AppsV1().Deployments(foo.Namespace).Update(newDeployment(foo)) + everything := metav1.LabelSelector{} + if reflect.DeepEqual(r.Spec.Selector, &everything) { + c.recorder.Eventf(r, corev1.EventTypeWarning, "SelectingAll", "This rollout is selecting all pods. A non-empty selector is required.") + return nil } - // If an error occurs during Update, we'll requeue the item so we can - // attempt processing again later. THis could have been caused by a - // temporary network failure, or any other transient reason. + // List ReplicaSets owned by this Rollout, while reconciling ControllerRef + // through adoption/orphaning. + rsList, err := c.getReplicaSetsForRollouts(r) if err != nil { return err } - // Finally, we update the status block of the Foo resource to reflect the - // current state of the world - err = c.updateFooStatus(foo, deployment) + scalingEvent, err := c.isScalingEvent(r, rsList) if err != nil { return err } - - c.recorder.Event(foo, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced) - return nil + if scalingEvent { + return c.sync(r, rsList) + } + switch r.Spec.Strategy.Type { + case v1alpha1.BlueGreenRolloutStrategyType: + return c.rolloutBlueGreen(r, rsList) + } + return fmt.Errorf("unexpected rollout strategy type: %s", r.Spec.Strategy.Type) } -func (c *Controller) updateFooStatus(foo *samplev1alpha1.Foo, deployment *appsv1.Deployment) error { - // NEVER modify objects from the store. It's a read-only, local cache. - // You can use DeepCopy() to make a deep copy of original object and modify this copy - // Or create a copy manually for better performance - fooCopy := foo.DeepCopy() - fooCopy.Status.AvailableReplicas = deployment.Status.AvailableReplicas - // If the CustomResourceSubresources feature gate is not enabled, - // we must use Update instead of UpdateStatus to update the Status block of the Foo resource. - // UpdateStatus will not allow changes to the Spec of the resource, - // which is ideal for ensuring nothing other than resource status has been updated. - _, err := c.sampleclientset.SamplecontrollerV1alpha1().Foos(foo.Namespace).Update(fooCopy) - return err +func (c *Controller) enqueue(obj interface{}) { + var key string + var err error + if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { + runtime.HandleError(err) + return + } + c.workqueue.Add(key) } -// enqueueFoo takes a Foo resource and converts it into a namespace/name -// string which is then put onto the work queue. This method should *not* be -// passed resources of any type other than Foo. -func (c *Controller) enqueueFoo(obj interface{}) { +func (c *Controller) enqueueRateLimited(obj interface{}) { var key string var err error if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { @@ -347,9 +316,9 @@ func (c *Controller) enqueueFoo(obj interface{}) { } // handleObject will take any resource implementing metav1.Object and attempt -// to find the Foo resource that 'owns' it. It does this by looking at the +// to find the Rollout resource that 'owns' it. It does this by looking at the // objects metadata.ownerReferences field for an appropriate OwnerReference. -// It then enqueues that Foo resource to be processed. If the object does not +// It then enqueues that Rollout resource to be processed. If the object does not // have an appropriate OwnerReference, it will simply be skipped. func (c *Controller) handleObject(obj interface{}) { var object metav1.Object @@ -369,61 +338,19 @@ func (c *Controller) handleObject(obj interface{}) { } klog.V(4).Infof("Processing object: %s", object.GetName()) if ownerRef := metav1.GetControllerOf(object); ownerRef != nil { - // If this object is not owned by a Foo, we should not do anything more + // If this object is not owned by a Rollout, we should not do anything more // with it. - if ownerRef.Kind != "Foo" { + if ownerRef.Kind != "Rollout" { return } - foo, err := c.foosLister.Foos(object.GetNamespace()).Get(ownerRef.Name) + rollout, err := c.rolloutsLister.Rollouts(object.GetNamespace()).Get(ownerRef.Name) if err != nil { - klog.V(4).Infof("ignoring orphaned object '%s' of foo '%s'", object.GetSelfLink(), ownerRef.Name) + klog.V(4).Infof("ignoring orphaned object '%s' of rollout '%s'", object.GetSelfLink(), ownerRef.Name) return } - c.enqueueFoo(foo) + c.enqueueRollout(rollout) return } } - -// newDeployment creates a new Deployment for a Foo resource. It also sets -// the appropriate OwnerReferences on the resource so handleObject can discover -// the Foo resource that 'owns' it. -func newDeployment(foo *samplev1alpha1.Foo) *appsv1.Deployment { - labels := map[string]string{ - "app": "nginx", - "controller": foo.Name, - } - return &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: foo.Spec.DeploymentName, - Namespace: foo.Namespace, - OwnerReferences: []metav1.OwnerReference{ - *metav1.NewControllerRef(foo, schema.GroupVersionKind{ - Group: samplev1alpha1.SchemeGroupVersion.Group, - Version: samplev1alpha1.SchemeGroupVersion.Version, - Kind: "Foo", - }), - }, - }, - Spec: appsv1.DeploymentSpec{ - Replicas: foo.Spec.Replicas, - Selector: &metav1.LabelSelector{ - MatchLabels: labels, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "nginx", - Image: "nginx:latest", - }, - }, - }, - }, - }, - } -} diff --git a/controller/controller_test.go b/controller/controller_test.go index d2051294bb..e25f237cd3 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -1,41 +1,29 @@ -/* -Copyright 2017 The Kubernetes Authors. - -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 ( - "fmt" + "encoding/json" "reflect" + "strconv" "testing" "time" - apps "k8s.io/api/apps/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apimachinery/pkg/util/uuid" kubeinformers "k8s.io/client-go/informers" k8sfake "k8s.io/client-go/kubernetes/fake" core "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" + "k8s.io/kubernetes/pkg/controller" - samplecontroller "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" - "k8s.io/sample-controller/pkg/client/clientset/versioned/fake" - informers "k8s.io/sample-controller/pkg/client/informers/externalversions" + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake" + informers "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions" + "github.com/argoproj/argo-rollouts/utils/annotations" ) var ( @@ -49,8 +37,9 @@ type fixture struct { client *fake.Clientset kubeclient *k8sfake.Clientset // Objects to put in the store. - fooLister []*samplecontroller.Foo - deploymentLister []*apps.Deployment + rolloutLister []*v1alpha1.Rollout + replicaSetLister []*appsv1.ReplicaSet + serviceLister []*corev1.Service // Actions expected to happen on the client. kubeactions []core.Action actions []core.Action @@ -67,20 +56,93 @@ func newFixture(t *testing.T) *fixture { return f } -func newFoo(name string, replicas *int32) *samplecontroller.Foo { - return &samplecontroller.Foo{ - TypeMeta: metav1.TypeMeta{APIVersion: samplecontroller.SchemeGroupVersion.String()}, +func newRollout(name string, replicas int, revisionHistoryLimit *int32, selector map[string]string, activeSvc string, previewSvc string) *v1alpha1.Rollout { + return &v1alpha1.Rollout{ ObjectMeta: metav1.ObjectMeta{ + UID: uuid.NewUUID(), Name: name, Namespace: metav1.NamespaceDefault, + Annotations: map[string]string{ + annotations.RevisionAnnotation: "1", + }, }, - Spec: samplecontroller.FooSpec{ - DeploymentName: fmt.Sprintf("%s-deployment", name), - Replicas: replicas, + Spec: v1alpha1.RolloutSpec{ + Strategy: v1alpha1.RolloutStrategy{ + Type: v1alpha1.BlueGreenRolloutStrategyType, + BlueGreenStrategy: &v1alpha1.BlueGreenStrategy{ + ActiveService: activeSvc, + PreviewService: previewSvc, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: selector, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "foo/bar", + }, + }, + }, + }, + RevisionHistoryLimit: revisionHistoryLimit, + Replicas: func() *int32 { i := int32(replicas); return &i }(), + Selector: &metav1.LabelSelector{MatchLabels: selector}, }, } } +func newReplicaSetWithStatus(r *v1alpha1.Rollout, name string, replicas int, availableReplicas int) *appsv1.ReplicaSet { + rs := newReplicaSet(r, name, replicas) + rs.Status.AvailableReplicas = int32(availableReplicas) + return rs +} + +func newReplicaSet(r *v1alpha1.Rollout, name string, replicas int) *appsv1.ReplicaSet { + newRSTemplate := *r.Spec.Template.DeepCopy() + rsLabels := map[string]string{ + v1alpha1.DefaultRolloutUniqueLabelKey: controller.ComputeHash(&newRSTemplate, r.Status.CollisionCount), + } + for k, v := range r.Spec.Selector.MatchLabels { + rsLabels[k] = v + } + return &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + UID: uuid.NewUUID(), + Namespace: metav1.NamespaceDefault, + Labels: rsLabels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(r, controllerKind)}, + Annotations: map[string]string{ + annotations.DesiredReplicasAnnotation: strconv.Itoa(replicas), + annotations.RevisionAnnotation: r.Annotations[annotations.RevisionAnnotation], + }, + }, + Spec: appsv1.ReplicaSetSpec{ + Selector: r.Spec.Selector, + Replicas: func() *int32 { i := int32(replicas); return &i }(), + Template: r.Spec.Template, + }, + } +} + +func newImage(rs *appsv1.ReplicaSet, newImage string) *appsv1.ReplicaSet { + rsCopy := rs.DeepCopy() + rsCopy.Spec.Template.Spec.Containers[0].Image = newImage + rsCopy.ObjectMeta.Name = controller.ComputeHash(&rsCopy.Spec.Template, nil) + return rsCopy +} + +func getKey(rollout *v1alpha1.Rollout, t *testing.T) string { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(rollout) + if err != nil { + t.Errorf("Unexpected error getting key for rollout %v: %v", rollout.Name, err) + return "" + } + return key +} + func (f *fixture) newController() (*Controller, informers.SharedInformerFactory, kubeinformers.SharedInformerFactory) { f.client = fake.NewSimpleClientset(f.objects...) f.kubeclient = k8sfake.NewSimpleClientset(f.kubeobjects...) @@ -89,32 +151,39 @@ func (f *fixture) newController() (*Controller, informers.SharedInformerFactory, k8sI := kubeinformers.NewSharedInformerFactory(f.kubeclient, noResyncPeriodFunc()) c := NewController(f.kubeclient, f.client, - k8sI.Apps().V1().Deployments(), i.Samplecontroller().V1alpha1().Foos()) + k8sI.Apps().V1().ReplicaSets(), + k8sI.Core().V1().Services(), + i.Argoproj().V1alpha1().Rollouts()) - c.foosSynced = alwaysReady - c.deploymentsSynced = alwaysReady + c.rolloutsSynced = alwaysReady + c.replicaSetSynced = alwaysReady + c.serviceSynced = alwaysReady c.recorder = &record.FakeRecorder{} + c.enqueueRollout = c.enqueue - for _, f := range f.fooLister { - i.Samplecontroller().V1alpha1().Foos().Informer().GetIndexer().Add(f) + for _, r := range f.rolloutLister { + i.Argoproj().V1alpha1().Rollouts().Informer().GetIndexer().Add(r) } - for _, d := range f.deploymentLister { - k8sI.Apps().V1().Deployments().Informer().GetIndexer().Add(d) + for _, r := range f.replicaSetLister { + k8sI.Apps().V1().ReplicaSets().Informer().GetIndexer().Add(r) + } + for _, s := range f.serviceLister { + k8sI.Core().V1().Services().Informer().GetIndexer().Add(s) } return c, i, k8sI } -func (f *fixture) run(fooName string) { - f.runController(fooName, true, false) +func (f *fixture) run(rolloutName string) { + f.runController(rolloutName, true, false) } -func (f *fixture) runExpectError(fooName string) { - f.runController(fooName, true, true) +func (f *fixture) runExpectError(rolloutName string, startInformers bool) { + f.runController(rolloutName, startInformers, true) } -func (f *fixture) runController(fooName string, startInformers bool, expectError bool) { +func (f *fixture) runController(rolloutName string, startInformers bool, expectError bool) { c, i, k8sI := f.newController() if startInformers { stopCh := make(chan struct{}) @@ -123,11 +192,11 @@ func (f *fixture) runController(fooName string, startInformers bool, expectError k8sI.Start(stopCh) } - err := c.syncHandler(fooName) + err := c.syncHandler(rolloutName) if !expectError && err != nil { - f.t.Errorf("error syncing foo: %v", err) + f.t.Errorf("error syncing rollout: %v", err) } else if expectError && err == nil { - f.t.Error("expected error syncing foo, got nil") + f.t.Error("expected error syncing rollout, got nil") } actions := filterInformerActions(f.client.Actions()) @@ -161,8 +230,7 @@ func (f *fixture) runController(fooName string, startInformers bool, expectError } } -// checkAction verifies that expected and actual actions are equal and both have -// same attached resources +// checkAction verifies that expected and actual actions are equal func checkAction(expected, actual core.Action, t *testing.T) { if !(expected.Matches(actual.GetVerb(), actual.GetResource().Resource) && actual.GetSubresource() == expected.GetSubresource()) { t.Errorf("Expected\n\t%#v\ngot\n\t%#v", expected, actual) @@ -173,49 +241,20 @@ func checkAction(expected, actual core.Action, t *testing.T) { t.Errorf("Action has wrong type. Expected: %t. Got: %t", expected, actual) return } - - switch a := actual.(type) { - case core.CreateAction: - e, _ := expected.(core.CreateAction) - expObject := e.GetObject() - object := a.GetObject() - - if !reflect.DeepEqual(expObject, object) { - t.Errorf("Action %s %s has wrong object\nDiff:\n %s", - a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintDiff(expObject, object)) - } - case core.UpdateAction: - e, _ := expected.(core.UpdateAction) - expObject := e.GetObject() - object := a.GetObject() - - if !reflect.DeepEqual(expObject, object) { - t.Errorf("Action %s %s has wrong object\nDiff:\n %s", - a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintDiff(expObject, object)) - } - case core.PatchAction: - e, _ := expected.(core.PatchAction) - expPatch := e.GetPatch() - patch := a.GetPatch() - - if !reflect.DeepEqual(expPatch, patch) { - t.Errorf("Action %s %s has wrong patch\nDiff:\n %s", - a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintDiff(expPatch, patch)) - } - } } -// filterInformerActions filters list and watch actions for testing resources. -// Since list and watch don't change resource state we can filter it to lower +// filterInformerActions filters list, and watch actions for testing resources. +// Since list, and watch don't change resource state we can filter it to lower // nose level in our tests. func filterInformerActions(actions []core.Action) []core.Action { ret := []core.Action{} for _, action := range actions { - if len(action.GetNamespace()) == 0 && - (action.Matches("list", "foos") || - action.Matches("watch", "foos") || - action.Matches("list", "deployments") || - action.Matches("watch", "deployments")) { + if action.Matches("list", "rollouts") || + action.Matches("watch", "rollouts") || + action.Matches("list", "replicaSets") || + action.Matches("watch", "replicaSets") || + action.Matches("list", "services") || + action.Matches("watch", "services") { continue } ret = append(ret, action) @@ -224,90 +263,175 @@ func filterInformerActions(actions []core.Action) []core.Action { return ret } -func (f *fixture) expectCreateDeploymentAction(d *apps.Deployment) { - f.kubeactions = append(f.kubeactions, core.NewCreateAction(schema.GroupVersionResource{Resource: "deployments"}, d.Namespace, d)) +func (f *fixture) expectGetServiceAction(s *corev1.Service) { + serviceSchema := schema.GroupVersionResource{ + Resource: "services", + Version: "v1", + } + f.kubeactions = append(f.kubeactions, core.NewGetAction(serviceSchema, s.Namespace, s.Name)) +} + +func (f *fixture) expectPatchServiceAction(s *corev1.Service, rs *appsv1.ReplicaSet) { + patch := corev1.Service{ + Spec: corev1.ServiceSpec{ + Selector: map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: rs.Labels[v1alpha1.DefaultRolloutUniqueLabelKey]}, + }, + } + patchBytes, _ := json.Marshal(patch) + serviceSchema := schema.GroupVersionResource{ + Resource: "services", + Version: "v1", + } + f.kubeactions = append(f.kubeactions, core.NewPatchAction(serviceSchema, s.Namespace, s.Name, patchBytes)) +} + +func (f *fixture) expectCreateReplicaSetAction(r *appsv1.ReplicaSet) { + f.kubeactions = append(f.kubeactions, core.NewCreateAction(schema.GroupVersionResource{Resource: "replicasets"}, r.Namespace, r)) } -func (f *fixture) expectUpdateDeploymentAction(d *apps.Deployment) { - f.kubeactions = append(f.kubeactions, core.NewUpdateAction(schema.GroupVersionResource{Resource: "deployments"}, d.Namespace, d)) +func (f *fixture) expectUpdateReplicaSetAction(r *appsv1.ReplicaSet) { + f.kubeactions = append(f.kubeactions, core.NewUpdateAction(schema.GroupVersionResource{Resource: "replicasets"}, r.Namespace, r)) } -func (f *fixture) expectUpdateFooStatusAction(foo *samplecontroller.Foo) { - action := core.NewUpdateAction(schema.GroupVersionResource{Resource: "foos"}, foo.Namespace, foo) - // TODO: Until #38113 is merged, we can't use Subresource - //action.Subresource = "status" +func (f *fixture) expectGetRolloutAction(rollout *v1alpha1.Rollout) { + f.kubeactions = append(f.kubeactions, core.NewGetAction(schema.GroupVersionResource{Resource: "rollouts"}, rollout.Namespace, rollout.Name)) +} + +func (f *fixture) expectUpdateRolloutAction(rollout *v1alpha1.Rollout) { + action := core.NewUpdateAction(schema.GroupVersionResource{Resource: "rollouts"}, rollout.Namespace, rollout) f.actions = append(f.actions, action) } -func getKey(foo *samplecontroller.Foo, t *testing.T) string { - key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(foo) - if err != nil { - t.Errorf("Unexpected error getting key for foo %v: %v", foo.Name, err) - return "" - } - return key +func (f *fixture) expectUpdateRolloutStatusAction(rollout *v1alpha1.Rollout) { + action := core.NewUpdateAction(schema.GroupVersionResource{Resource: "rollouts"}, rollout.Namespace, rollout) + action.Subresource = "status" + f.actions = append(f.actions, action) } -func TestCreatesDeployment(t *testing.T) { +func TestSyncRolloutCreatesReplicaSet(t *testing.T) { f := newFixture(t) - foo := newFoo("test", int32Ptr(1)) - f.fooLister = append(f.fooLister, foo) - f.objects = append(f.objects, foo) + r := newRollout("foo", 1, nil, map[string]string{"foo": "bar"}, "bar", "") + f.rolloutLister = append(f.rolloutLister, r) + f.objects = append(f.objects, r) + s := newService("bar", 80, nil) + f.kubeobjects = append(f.kubeobjects, s) - expDeployment := newDeployment(foo) - f.expectCreateDeploymentAction(expDeployment) - f.expectUpdateFooStatusAction(foo) + rs := newReplicaSet(r, "foo-895c6c4f9", 1) - f.run(getKey(foo, t)) + f.expectCreateReplicaSetAction(rs) + f.expectGetServiceAction(s) + f.run(getKey(r, t)) } -func TestDoNothing(t *testing.T) { +func TestSyncRolloutSetPreviewService(t *testing.T) { f := newFixture(t) - foo := newFoo("test", int32Ptr(1)) - d := newDeployment(foo) - f.fooLister = append(f.fooLister, foo) - f.objects = append(f.objects, foo) - f.deploymentLister = append(f.deploymentLister, d) - f.kubeobjects = append(f.kubeobjects, d) + r := newRollout("foo", 1, nil, map[string]string{"foo": "bar"}, "active", "preview") + f.rolloutLister = append(f.rolloutLister, r) + f.objects = append(f.objects, r) + + rs := newReplicaSetWithStatus(r, "foo-895c6c4f9", 1, 1) + f.kubeobjects = append(f.kubeobjects, rs) + f.replicaSetLister = append(f.replicaSetLister, rs) + + previewSvc := newService("preview", 80, nil) + selector := map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "test"} + activeSvc := newService("active", 80, selector) + f.kubeobjects = append(f.kubeobjects, previewSvc, activeSvc) + + f.expectGetServiceAction(activeSvc) + f.expectGetServiceAction(previewSvc) + f.expectPatchServiceAction(previewSvc, rs) + f.expectUpdateRolloutAction(r) + f.expectUpdateRolloutAction(r) + f.run(getKey(r, t)) +} + +func TestSyncRolloutVerifyPreviewNoActions(t *testing.T) { + f := newFixture(t) - f.expectUpdateFooStatusAction(foo) - f.run(getKey(foo, t)) + r := newRollout("foo", 1, nil, map[string]string{"foo": "bar"}, "active", "preview") + r.Status.VerifyingPreview = func(boolean bool) *bool { return &boolean }(true) + f.rolloutLister = append(f.rolloutLister, r) + f.objects = append(f.objects, r) + + rs := newReplicaSetWithStatus(r, "foo-895c6c4f9", 1, 1) + rs2 := newImage(rs, "foo/bar2.0") + f.kubeobjects = append(f.kubeobjects, rs, rs2) + f.replicaSetLister = append(f.replicaSetLister, rs, rs2) + + previewSelector := map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "895c6c4f9"} + previewSvc := newService("preview", 80, previewSelector) + activeSelector := map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: rs2.Name} + activeSvc := newService("active", 80, activeSelector) + f.kubeobjects = append(f.kubeobjects, previewSvc, activeSvc) + + f.expectGetServiceAction(activeSvc) + f.expectGetServiceAction(previewSvc) + f.expectUpdateRolloutAction(r) + f.run(getKey(r, t)) } -func TestUpdateDeployment(t *testing.T) { +func TestSyncRolloutSkipPreviewUpdateActive(t *testing.T) { f := newFixture(t) - foo := newFoo("test", int32Ptr(1)) - d := newDeployment(foo) - // Update replicas - foo.Spec.Replicas = int32Ptr(2) - expDeployment := newDeployment(foo) + r := newRollout("foo", 1, nil, map[string]string{"foo": "bar"}, "active", "preview") + f.rolloutLister = append(f.rolloutLister, r) + f.objects = append(f.objects, r) + + rs := newReplicaSetWithStatus(r, "foo-895c6c4f9", 1, 1) + f.kubeobjects = append(f.kubeobjects, rs) + f.replicaSetLister = append(f.replicaSetLister, rs) - f.fooLister = append(f.fooLister, foo) - f.objects = append(f.objects, foo) - f.deploymentLister = append(f.deploymentLister, d) - f.kubeobjects = append(f.kubeobjects, d) + previewSvc := newService("preview", 80, nil) + activeSvc := newService("active", 80, nil) + f.kubeobjects = append(f.kubeobjects, previewSvc, activeSvc) - f.expectUpdateFooStatusAction(foo) - f.expectUpdateDeploymentAction(expDeployment) - f.run(getKey(foo, t)) + f.expectGetServiceAction(activeSvc) + f.expectGetServiceAction(previewSvc) + f.expectPatchServiceAction(activeSvc, rs) + f.run(getKey(r, t)) } -func TestNotControlledByUs(t *testing.T) { +func TestDontSyncRolloutsWithEmptyPodSelector(t *testing.T) { f := newFixture(t) - foo := newFoo("test", int32Ptr(1)) - d := newDeployment(foo) - d.ObjectMeta.OwnerReferences = []metav1.OwnerReference{} + r := newRollout("foo", 1, nil, nil, "", "") + f.rolloutLister = append(f.rolloutLister, r) + f.objects = append(f.objects, r) - f.fooLister = append(f.fooLister, foo) - f.objects = append(f.objects, foo) - f.deploymentLister = append(f.deploymentLister, d) - f.kubeobjects = append(f.kubeobjects, d) - - f.runExpectError(getKey(foo, t)) + f.run(getKey(r, t)) } -func int32Ptr(i int32) *int32 { return &i } +func TestSyncRolloutsScaleDownOldRS(t *testing.T) { + f := newFixture(t) + + r1 := newRollout("foo", 1, nil, map[string]string{"foo": "bar"}, "bar", "") + + r2 := r1.DeepCopy() + annotations.SetRolloutRevision(r2, "2") + r2.Spec.Template.Spec.Containers[0].Image = "foo/bar2.0" + f.rolloutLister = append(f.rolloutLister, r2) + f.objects = append(f.objects, r2) + + rs1 := newReplicaSetWithStatus(r1, "foo-895c6c4f9", 1, 1) + f.kubeobjects = append(f.kubeobjects, rs1) + f.replicaSetLister = append(f.replicaSetLister, rs1) + + rs2 := newReplicaSetWithStatus(r2, "foo-6479c8f85c", 1, 1) + f.kubeobjects = append(f.kubeobjects, rs2) + f.replicaSetLister = append(f.replicaSetLister, rs2) + + serviceSelector := map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "6479c8f85c"} + s := newService("bar", 80, serviceSelector) + f.kubeobjects = append(f.kubeobjects, s) + + expRS := rs2.DeepCopy() + expRS.Annotations[annotations.DesiredReplicasAnnotation] = "0" + f.expectGetServiceAction(s) + f.expectUpdateReplicaSetAction(expRS) + f.expectUpdateRolloutAction(r1) + + f.run(getKey(r2, t)) +} diff --git a/controller/replicaset.go b/controller/replicaset.go new file mode 100644 index 0000000000..9e399fdde9 --- /dev/null +++ b/controller/replicaset.go @@ -0,0 +1,41 @@ +package controller + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/kubernetes/pkg/controller" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" +) + +var controllerKind = v1alpha1.SchemeGroupVersion.WithKind("Rollouts") + +func (c *Controller) getReplicaSetsForRollouts(r *v1alpha1.Rollout) ([]*appsv1.ReplicaSet, error) { + // List all ReplicaSets to find those we own but that no longer match our + // selector. They will be orphaned by ClaimReplicaSets(). + rsList, err := c.replicaSetLister.ReplicaSets(r.Namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + replicaSetSelector, err := metav1.LabelSelectorAsSelector(r.Spec.Selector) + if err != nil { + return nil, fmt.Errorf("rollout %s/%s has invalid label selector: %v", r.Namespace, r.Name, err) + } + // If any adoptions are attempted, we should first recheck for deletion with + // an uncached quorum read sometime after listing ReplicaSets (see #42639). + canAdoptFunc := controller.RecheckDeletionTimestamp(func() (metav1.Object, error) { + fresh, err := c.rolloutsclientset.ArgoprojV1alpha1().Rollouts(r.Namespace).Get(r.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + if fresh.UID != r.UID { + return nil, fmt.Errorf("original Rollout %v/%v is gone: got uid %v, wanted %v", r.Namespace, r.Name, fresh.UID, r.UID) + } + return fresh, nil + }) + cm := controller.NewReplicaSetControllerRefManager(c.replicaSetControl, r, replicaSetSelector, controllerKind, canAdoptFunc) + return cm.ClaimReplicaSets(rsList) +} diff --git a/controller/replicaset_test.go b/controller/replicaset_test.go new file mode 100644 index 0000000000..f5e06cc9c3 --- /dev/null +++ b/controller/replicaset_test.go @@ -0,0 +1,112 @@ +package controller + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" +) + +func newRolloutControllerRef(r *v1alpha1.Rollout) *metav1.OwnerReference { + isController := true + return &metav1.OwnerReference{ + APIVersion: "argoproj.io/v1alpha1", + Kind: "Rollouts", + Name: r.GetName(), + UID: r.GetUID(), + Controller: &isController, + } +} + +func int32Ptr(i int32) *int32 { return &i } + +func TestGetReplicaSetsForRollouts(t *testing.T) { + newTimestamp := metav1.Date(2016, 5, 20, 2, 0, 0, 0, time.UTC) + selector := map[string]string{ + "app": "ngnix", + } + diffSelector := map[string]string{ + "app": "ngnix2", + } + rollout := newRollout("foo", 1, int32Ptr(1), selector, "", "") + diffRollout := newRollout("bar", 1, int32Ptr(1), selector, "", "") + tests := []struct { + name string + existingRSs []*appsv1.ReplicaSet + + expectedSelectedRSs []*appsv1.ReplicaSet + expectedError error + }{ + { + name: "Grab corrected owned replicasets", + existingRSs: []*appsv1.ReplicaSet{ + rs("foo-v2", 1, selector, newTimestamp, newRolloutControllerRef(rollout)), + rs("foo-v1", 1, selector, newTimestamp, newRolloutControllerRef(diffRollout)), + }, + expectedSelectedRSs: []*appsv1.ReplicaSet{ + rs("foo-v2", 1, selector, newTimestamp, newRolloutControllerRef(rollout)), + }, + expectedError: nil, + }, + { + name: "Adopt orphaned replica sets", + existingRSs: []*appsv1.ReplicaSet{ + rs("foo-v1", 1, selector, newTimestamp, nil), + }, + expectedSelectedRSs: []*appsv1.ReplicaSet{ + rs("foo-v1", 1, selector, newTimestamp, newRolloutControllerRef(rollout)), + }, + expectedError: nil, + }, + { + name: "No replica sets exist", + existingRSs: []*appsv1.ReplicaSet{}, + expectedSelectedRSs: []*appsv1.ReplicaSet{}, + expectedError: nil, + }, + { + name: "No selector provided so no adoption", + existingRSs: []*appsv1.ReplicaSet{ + rs("foo-v1", 1, nil, newTimestamp, newRolloutControllerRef(diffRollout)), + }, + expectedSelectedRSs: []*appsv1.ReplicaSet{}, + expectedError: nil, + }, + { + name: "Orphan RS with different selector", + existingRSs: []*appsv1.ReplicaSet{ + rs("foo-v1", 1, diffSelector, newTimestamp, newRolloutControllerRef(diffRollout)), + }, + expectedSelectedRSs: []*appsv1.ReplicaSet{}, + expectedError: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f := newFixture(t) + f.rolloutLister = append(f.rolloutLister, rollout) + f.objects = append(f.objects, rollout) + f.replicaSetLister = append(f.replicaSetLister, test.existingRSs...) + for _, rs := range test.existingRSs { + f.kubeobjects = append(f.kubeobjects, rs) + } + + c, informers, _ := f.newController() + stopCh := make(chan struct{}) + defer close(stopCh) + informers.Start(stopCh) + returnedRSs, err := c.getReplicaSetsForRollouts(rollout) + + assert.Equal(t, test.expectedError, err) + assert.Equal(t, len(test.expectedSelectedRSs), len(returnedRSs)) + for i, returnedRS := range returnedRSs { + assert.Equal(t, test.expectedSelectedRSs[i].Name, returnedRS.Name) + } + }) + } + +} diff --git a/controller/service.go b/controller/service.go new file mode 100644 index 0000000000..ca3b0fb0d6 --- /dev/null +++ b/controller/service.go @@ -0,0 +1,186 @@ +package controller + +import ( + "encoding/json" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + patchtypes "k8s.io/apimachinery/pkg/types" + "k8s.io/klog" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/utils/annotations" +) + +// switchSelector switch the selector on an existing service to a new value +func (c Controller) switchServiceSelector(service *corev1.Service, newRolloutUniqueLabelValue string) error { + patch := corev1.Service{ + Spec: corev1.ServiceSpec{ + Selector: map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: newRolloutUniqueLabelValue}, + }, + } + patchBytes, err := json.Marshal(patch) + if err != nil { + return err + } + klog.V(2).Info("Switching selector for service %s to value '%s'", service.Name, newRolloutUniqueLabelValue) + _, err = c.kubeclientset.CoreV1().Services(service.Namespace).Patch(service.Name, patchtypes.StrategicMergePatchType, patchBytes) + return err +} + +func (c *Controller) reconcilePreviewService(r *v1alpha1.Rollout, newRS *appsv1.ReplicaSet, previewSvc *corev1.Service, activeSvc *corev1.Service) (bool, error) { + if !annotations.IsSaturated(r, newRS) { + return true, nil + } + + //If the active service already points to the new RS or the active service selector does not + // point to any RS, we short-circuit changing the preview service. + if activeSvc.Spec.Selector == nil { + return false, nil + } + currentSelectorValue, ok := activeSvc.Spec.Selector[v1alpha1.DefaultRolloutUniqueLabelKey] + if !ok || currentSelectorValue == newRS.Labels[v1alpha1.DefaultRolloutUniqueLabelKey] { + return false, nil + } + + // If preview service already points to the new RS, skip the next steps + if previewSvc.Spec.Selector != nil { + currentSelectorValue, ok := previewSvc.Spec.Selector[v1alpha1.DefaultRolloutUniqueLabelKey] + if ok && currentSelectorValue == newRS.Labels[v1alpha1.DefaultRolloutUniqueLabelKey] { + return false, nil + } + } + + err := c.setVerifyingPreview(r) + if err != nil { + return false, err + } + + err = c.switchServiceSelector(previewSvc, newRS.Labels[v1alpha1.DefaultRolloutUniqueLabelKey]) + if err != nil { + return false, err + } + + return true, nil +} + +func (c *Controller) reconcileActiveService(r *v1alpha1.Rollout, newRS *appsv1.ReplicaSet, previewSvc *corev1.Service, activeSvc *corev1.Service) (bool, error) { + if !annotations.IsSaturated(r, newRS) { + return false, nil + } + + switchActiveSvc := true + if activeSvc.Spec.Selector != nil { + currentSelectorValue, ok := activeSvc.Spec.Selector[v1alpha1.DefaultRolloutUniqueLabelKey] + if ok && currentSelectorValue == newRS.Labels[v1alpha1.DefaultRolloutUniqueLabelKey] { + switchActiveSvc = false + } + } + if switchActiveSvc { + err := c.switchServiceSelector(activeSvc, newRS.Labels[v1alpha1.DefaultRolloutUniqueLabelKey]) + if err != nil { + return false, err + } + return true, nil + } + + switchPreviewSvc := false + if previewSvc != nil && previewSvc.Spec.Selector != nil { + currentSelectorValue, ok := previewSvc.Spec.Selector[v1alpha1.DefaultRolloutUniqueLabelKey] + if !ok || currentSelectorValue != "" { + switchPreviewSvc = true + } + } + + if switchPreviewSvc { + err := c.switchServiceSelector(previewSvc, "") + if err != nil { + return false, err + } + return true, nil + } + + return false, nil +} + +func (c *Controller) getRolloutsForService(service *corev1.Service) ([]*v1alpha1.Rollout, error) { + allROs, err := c.rolloutsclientset.ArgoprojV1alpha1().Rollouts(service.Namespace).List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + rollouts := []*v1alpha1.Rollout{} + for _, rollout := range allROs.Items { + if rollout.Spec.Strategy.BlueGreenStrategy.ActiveService == service.Name { + copyRO := rollout.DeepCopy() + rollouts = append(rollouts, copyRO) + } + } + if len(rollouts) > 1 { + klog.V(4).Infof("user error! more than one rollout is selecting replica set %s/%s with labels: %#v", + service.Namespace, service.Name, service.Labels, rollouts[0].Namespace, rollouts[0].Name) + } + return rollouts, nil +} + +func (c *Controller) handleService(obj interface{}) { + service := obj.(*corev1.Service) + rollouts, err := c.getRolloutsForService(service) + if err != nil { + return + } + for i := range rollouts { + c.enqueueRollout(rollouts[i]) + } + +} + +func (c *Controller) updateService(old, cur interface{}) { + curSvc := cur.(*corev1.Service) + oldSvc := old.(*corev1.Service) + if curSvc.ResourceVersion == oldSvc.ResourceVersion { + // Periodic resync will send update events for all known services. + // Two different versions of the same replica set will always have different RVs. + return + } + c.handleService(cur) +} + +func (c *Controller) getPreviewAndActiveServices(r *v1alpha1.Rollout) (*corev1.Service, *corev1.Service, error) { + var previewSvc *corev1.Service + var activeSvc *corev1.Service + var err error + if r.Spec.Strategy.BlueGreenStrategy.PreviewService != "" { + previewSvc, err = c.kubeclientset.CoreV1().Services(r.Namespace).Get(r.Spec.Strategy.BlueGreenStrategy.PreviewService, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + klog.V(2).Infof("Service %v does not exist", r.Spec.Strategy.BlueGreenStrategy.PreviewService) + } + return nil, nil, err + } + } + if r.Spec.Strategy.BlueGreenStrategy.ActiveService == "" { + return nil, nil, fmt.Errorf("Invalid Spec: Rollout missing field ActiveService") + } + activeSvc, err = c.kubeclientset.CoreV1().Services(r.Namespace).Get(r.Spec.Strategy.BlueGreenStrategy.ActiveService, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + klog.V(2).Infof("Service %v does not exist", r.Spec.Strategy.BlueGreenStrategy.PreviewService) + } + return nil, nil, err + } + return previewSvc, activeSvc, nil +} + +func (c *Controller) getRolloutSelectorLabel(svc *corev1.Service) (string, bool) { + if svc == nil { + return "", false + } + if svc.Spec.Selector == nil { + return "", false + } + currentSelectorValue, ok := svc.Spec.Selector[v1alpha1.DefaultRolloutUniqueLabelKey] + return currentSelectorValue, ok +} diff --git a/controller/service_test.go b/controller/service_test.go new file mode 100644 index 0000000000..20884ee2dd --- /dev/null +++ b/controller/service_test.go @@ -0,0 +1,262 @@ +package controller + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/kubernetes/pkg/controller" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" +) + +func newService(name string, port int, selector map[string]string) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: metav1.NamespaceDefault, + }, + Spec: corev1.ServiceSpec{ + Selector: selector, + Ports: []corev1.ServicePort{{ + Protocol: "TCP", + Port: int32(port), + TargetPort: intstr.FromInt(port), + }}, + }, + } +} + +func TestReconcilePreviewService(t *testing.T) { + tests := []struct { + name string + newRSDesiredReplicas int + newRSAvailableReplicas int + activeSvc *corev1.Service + previewSvc *corev1.Service + expectedResult bool + }{ + { + name: "Do not switch if the new RS isn't ready", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "test"}), + previewSvc: newService("preview", 80, nil), + newRSDesiredReplicas: 5, + newRSAvailableReplicas: 3, + expectedResult: true, + }, + { + name: "Continue if active service is already set to the newRS", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "57b9899597"}), + newRSDesiredReplicas: 5, + newRSAvailableReplicas: 5, + expectedResult: false, + }, + { + name: "Continue if active service doesn't have a selector", + activeSvc: newService("active", 80, map[string]string{}), + newRSDesiredReplicas: 5, + newRSAvailableReplicas: 5, + expectedResult: false, + }, + { + name: "Continue if active service selector doesn't match have DefaultRolloutUniqueLabelKey", + activeSvc: newService("active", 80, map[string]string{}), + newRSDesiredReplicas: 5, + newRSAvailableReplicas: 5, + expectedResult: false, + }, + { + name: "Continue if preview service is already set to the newRS", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "test"}), + previewSvc: newService("preview", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "57b9899597"}), + newRSDesiredReplicas: 5, + newRSAvailableReplicas: 5, + expectedResult: false, + }, + { + name: "Switch if the new RS is ready", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "test"}), + previewSvc: newService("preview", 80, nil), + newRSDesiredReplicas: 5, + newRSAvailableReplicas: 5, + expectedResult: true, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + ro := newRollout("foo", 5, nil, nil, "", "") + rs := newReplicaSetWithStatus(ro, "bar", test.newRSDesiredReplicas, test.newRSAvailableReplicas) + f := newFixture(t) + + f.rolloutLister = append(f.rolloutLister, ro) + f.objects = append(f.objects, ro) + f.kubeobjects = append(f.kubeobjects, rs) + if test.previewSvc != nil { + f.kubeobjects = append(f.kubeobjects, test.previewSvc) + } + c, _, _ := f.newController() + result, err := c.reconcilePreviewService(ro, rs, test.previewSvc, test.activeSvc) + assert.NoError(t, err) + assert.Equal(t, test.expectedResult, result) + }) + } +} + +func TestReconcileActiveService(t *testing.T) { + tests := []struct { + name string + activeSvc *corev1.Service + previewSvc *corev1.Service + expectedResult bool + }{ + { + name: "Switch active service to New RS", + activeSvc: newService("active", 80, nil), + expectedResult: true, + }, + { + name: "Switch Preview selector to empty string", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "57b9899597"}), + previewSvc: newService("preview", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "57b9899597"}), + expectedResult: true, + }, + { + name: "No switch required if the active service already points at new RS and the preview is not point at any RS", + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "57b9899597"}), + previewSvc: newService("preview", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: ""}), + expectedResult: false, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + ro := newRollout("foo", 5, nil, nil, "", "") + rs := newReplicaSetWithStatus(ro, "bar", 5, 5) + f := newFixture(t) + + f.rolloutLister = append(f.rolloutLister, ro) + f.objects = append(f.objects, ro) + f.kubeobjects = append(f.kubeobjects, rs) + if test.previewSvc != nil { + f.kubeobjects = append(f.kubeobjects, test.previewSvc) + } + c, _, _ := f.newController() + result, err := c.reconcileActiveService(ro, rs, test.previewSvc, test.activeSvc) + assert.NoError(t, err) + assert.Equal(t, test.expectedResult, result) + }) + } +} + +func TestGetRolloutsForService(t *testing.T) { + f := newFixture(t) + + s := newService("foo", 80, nil) + ro1 := newRollout("bar", 0, nil, nil, "", "") + ro1.Spec.Strategy.BlueGreenStrategy.ActiveService = "foo" + ro2 := newRollout("baz", 0, nil, nil, "", "") + ro2.Spec.Strategy.BlueGreenStrategy.ActiveService = "foo2" + f.rolloutLister = append(f.rolloutLister, ro1, ro2) + f.objects = append(f.objects, ro1, ro2) + + // Create the fixture but don't start it, + // so nothing happens in the background. + c, _, _ := f.newController() + + rollouts, err := c.getRolloutsForService(s) + assert.Nil(t, err) + + assert.Len(t, rollouts, 1) + assert.Equal(t, ro1, rollouts[0]) +} + +func TestHandleServiceEnqueueRollout(t *testing.T) { + f := newFixture(t) + + s := newService("foo", 80, nil) + ro1 := newRollout("bar", 0, nil, nil, "", "") + ro1.Spec.Strategy.BlueGreenStrategy.ActiveService = "foo" + ro2 := newRollout("baz", 0, nil, nil, "", "") + ro2.Spec.Strategy.BlueGreenStrategy.ActiveService = "foo2" + f.objects = append(f.objects, ro1, ro2) + + // Create the fixture but don't start it, + // so nothing happens in the background. + c, _, _ := f.newController() + + c.handleService(s) + assert.Equal(t, c.workqueue.Len(), 1) + + key, done := c.workqueue.Get() + assert.NotNil(t, key) + assert.False(t, done) + expectedKey, _ := controller.KeyFunc(ro1) + assert.Equal(t, key.(string), expectedKey) +} + +func TestHandleServiceNoAdditions(t *testing.T) { + f := newFixture(t) + + s := newService("foo", 80, nil) + ro1 := newRollout("bar", 0, nil, nil, "", "") + ro1.Spec.Strategy.BlueGreenStrategy.ActiveService = "notFoo" + f.objects = append(f.objects, ro1) + + // Create the fixture but don't start it, + // so nothing happens in the background. + c, _, _ := f.newController() + + c.handleService(s) + assert.Equal(t, c.workqueue.Len(), 0) +} + +func TestHandleServiceNoExistingRollouts(t *testing.T) { + f := newFixture(t) + + s := newService("foo", 80, nil) + // Create the fixture but don't start it, + // so nothing happens in the background. + c, _, _ := f.newController() + + c.handleService(s) + assert.Equal(t, c.workqueue.Len(), 0) +} + +func TestUpdateServiceEnqueueRollout(t *testing.T) { + f := newFixture(t) + + oldSvc := newService("foo", 80, nil) + newSvc := oldSvc.DeepCopy() + newSvc.ResourceVersion = "2" + ro1 := newRollout("bar", 0, nil, nil, "", "") + ro1.Spec.Strategy.BlueGreenStrategy.ActiveService = "foo" + f.objects = append(f.objects, ro1) + // Create the fixture but don't start it, + // so nothing happens in the background. + c, _, _ := f.newController() + + c.updateService(oldSvc, newSvc) + assert.Equal(t, c.workqueue.Len(), 1) + + key, done := c.workqueue.Get() + assert.NotNil(t, key) + assert.False(t, done) + expectedKey, _ := controller.KeyFunc(ro1) + assert.Equal(t, key.(string), expectedKey) +} + +func TestUpdateServiceSameService(t *testing.T) { + f := newFixture(t) + + s := newService("foo", 80, nil) + + // Create the fixture but don't start it, + // so nothing happens in the background. + c, _, _ := f.newController() + + c.updateService(s, s) + assert.Equal(t, c.workqueue.Len(), 0) +} diff --git a/controller/sync.go b/controller/sync.go new file mode 100644 index 0000000000..090bbc7c28 --- /dev/null +++ b/controller/sync.go @@ -0,0 +1,331 @@ +package controller + +import ( + "fmt" + "reflect" + "strconv" + + 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/klog" + "k8s.io/kubernetes/pkg/controller" + labelsutil "k8s.io/kubernetes/pkg/util/labels" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/utils/annotations" + "github.com/argoproj/argo-rollouts/utils/conditions" + replicasetutil "github.com/argoproj/argo-rollouts/utils/replicaset" +) + +// getAllReplicaSetsAndSyncRevision returns all the replica sets for the provided rollout (new and all old), with new RS's and rollout's revision updated. +// +// rsList should come from getReplicaSetsForRollout(r). +// +// 1. Get all old RSes this rollout targets, and calculate the max revision number among them (maxOldV). +// 2. Get new RS this rollout targets (whose pod template matches rollout's), and update new RS's revision number to (maxOldV + 1), +// only if its revision number is smaller than (maxOldV + 1). If this step failed, we'll update it in the next rollout sync loop. +// 3. Copy new RS's revision number to rollout (update rollout's revision). If this step failed, we'll update it in the next rollout sync loop. +// +// Note that currently the rollout controller is using caches to avoid querying the server for reads. +// This may lead to stale reads of replica sets, thus incorrect v status. +func (c *Controller) getAllReplicaSetsAndSyncRevision(rollout *v1alpha1.Rollout, rsList []*appsv1.ReplicaSet, createIfNotExisted bool) (*appsv1.ReplicaSet, []*appsv1.ReplicaSet, error) { + allOldRSs := replicasetutil.FindOldReplicaSets(rollout, rsList) + + // Get new replica set with the updated revision number + newRS, err := c.getNewReplicaSet(rollout, rsList, allOldRSs, createIfNotExisted) + if err != nil { + return nil, nil, err + } + + return newRS, allOldRSs, nil +} + +// Returns a replica set that matches the intent of the given rollout. Returns nil if the new replica set doesn't exist yet. +// 1. Get existing new RS (the RS that the given rollout targets, whose pod template is the same as rollout's). +// 2. If there's existing new RS, update its revision number if it's smaller than (maxOldRevision + 1), where maxOldRevision is the max revision number among all old RSes. +// 3. If there's no existing new RS and createIfNotExisted is true, create one with appropriate revision number (maxOldRevision + 1) and replicas. +// Note that the pod-template-hash will be added to adopted RSes and pods. +func (c *Controller) getNewReplicaSet(rollout *v1alpha1.Rollout, rsList, oldRSs []*appsv1.ReplicaSet, createIfNotExisted bool) (*appsv1.ReplicaSet, error) { + existingNewRS := replicasetutil.FindNewReplicaSet(rollout, rsList) + + // Calculate the max revision number among all old RSes + maxOldRevision := replicasetutil.MaxRevision(oldRSs) + // Calculate revision number for this new replica set + newRevision := strconv.FormatInt(maxOldRevision+1, 10) + + // Latest replica set exists. We need to sync its annotations (includes copying all but + // annotationsToSkip from the parent rollout, and update revision and desiredReplicas) + // and also update the revision annotation in the rollout with the + // latest revision. + if existingNewRS != nil { + rsCopy := existingNewRS.DeepCopy() + + // Set existing new replica set's annotation + annotationsUpdated := annotations.SetNewReplicaSetAnnotations(rollout, rsCopy, newRevision, true) + minReadySecondsNeedsUpdate := rsCopy.Spec.MinReadySeconds != rollout.Spec.MinReadySeconds + if annotationsUpdated || minReadySecondsNeedsUpdate { + rsCopy.Spec.MinReadySeconds = rollout.Spec.MinReadySeconds + return c.kubeclientset.AppsV1().ReplicaSets(rsCopy.ObjectMeta.Namespace).Update(rsCopy) + } + + // Should use the revision in existingNewRS's annotation, since it set by before + needsUpdate := annotations.SetRolloutRevision(rollout, rsCopy.Annotations[annotations.RevisionAnnotation]) + if needsUpdate { + var err error + if rollout, err = c.rolloutsclientset.ArgoprojV1alpha1().Rollouts(rollout.Namespace).Update(rollout); err != nil { + return nil, err + } + } + return rsCopy, nil + } + + if !createIfNotExisted { + return nil, nil + } + + // new ReplicaSet does not exist, create one. + newRSTemplate := *rollout.Spec.Template.DeepCopy() + podTemplateSpecHash := controller.ComputeHash(&newRSTemplate, rollout.Status.CollisionCount) + newRSTemplate.Labels = labelsutil.CloneAndAddLabel(rollout.Spec.Template.Labels, v1alpha1.DefaultRolloutUniqueLabelKey, podTemplateSpecHash) + // Add podTemplateHash label to selector. + newRSSelector := labelsutil.CloneSelectorAndAddLabel(rollout.Spec.Selector, v1alpha1.DefaultRolloutUniqueLabelKey, podTemplateSpecHash) + + // Create new ReplicaSet + newRS := appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: rollout.Name + "-" + podTemplateSpecHash, + Namespace: rollout.Namespace, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(rollout, controllerKind)}, + Labels: newRSTemplate.Labels, + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: new(int32), + MinReadySeconds: rollout.Spec.MinReadySeconds, + Selector: newRSSelector, + Template: newRSTemplate, + }, + } + allRSs := append(oldRSs, &newRS) + newReplicasCount, err := replicasetutil.NewRSNewReplicas(rollout, allRSs, &newRS) + if err != nil { + return nil, err + } + + *(newRS.Spec.Replicas) = newReplicasCount + // Set new replica set's annotation + annotations.SetNewReplicaSetAnnotations(rollout, &newRS, newRevision, false) + // Create the new ReplicaSet. If it already exists, then we need to check for possible + // hash collisions. If there is any other error, we need to report it in the status of + // the Rollout. + alreadyExists := false + createdRS, err := c.kubeclientset.AppsV1().ReplicaSets(rollout.Namespace).Create(&newRS) + switch { + // We may end up hitting this due to a slow cache or a fast resync of the Rollout. + case errors.IsAlreadyExists(err): + alreadyExists = true + + // Fetch a copy of the ReplicaSet. + rs, rsErr := c.replicaSetLister.ReplicaSets(newRS.Namespace).Get(newRS.Name) + if rsErr != nil { + return nil, rsErr + } + + // If the Rollout owns the ReplicaSet and the ReplicaSet's PodTemplateSpec is semantically + // deep equal to the PodTemplateSpec of the Rollout, it's the Rollout's new ReplicaSet. + // Otherwise, this is a hash collision and we need to increment the collisionCount field in + // the status of the Rollout and requeue to try the creation in the next sync. + controllerRef := metav1.GetControllerOf(rs) + replicaSetName := fmt.Sprintf("%s-%s", rollout.Name, controller.ComputeHash(&rollout.Spec.Template, rollout.Status.CollisionCount)) + if controllerRef != nil && controllerRef.UID == rollout.UID && replicaSetName == rs.Name { + createdRS = rs + err = nil + break + } + + // Matching ReplicaSet is not equal - increment the collisionCount in the RolloutStatus + // and requeue the Rollout. + if rollout.Status.CollisionCount == nil { + rollout.Status.CollisionCount = new(int32) + } + preCollisionCount := *rollout.Status.CollisionCount + *rollout.Status.CollisionCount++ + // Update the collisionCount for the Rollout and let it requeue by returning the original + // error. + _, roErr := c.rolloutsclientset.ArgoprojV1alpha1().Rollouts(rollout.Namespace).Update(rollout) + if roErr == nil { + klog.V(2).Infof("Found a hash collision for rollout %q - bumping collisionCount (%d->%d) to resolve it", rollout.Name, preCollisionCount, *rollout.Status.CollisionCount) + } + return nil, err + case err != nil: + msg := fmt.Sprintf("Failed to create new replica set %q: %v", newRS.Name, err) + c.recorder.Eventf(rollout, corev1.EventTypeWarning, conditions.FailedRSCreateReason, msg) + return nil, err + } + if !alreadyExists && newReplicasCount > 0 { + c.recorder.Eventf(rollout, corev1.EventTypeNormal, "ScalingReplicaSet", "Scaled up replica set %s to %d", createdRS.Name, newReplicasCount) + } + + needsUpdate := annotations.SetRolloutRevision(rollout, newRevision) + if needsUpdate { + _, err = c.rolloutsclientset.ArgoprojV1alpha1().Rollouts(rollout.Namespace).Update(rollout) + } + return createdRS, err +} + +// sync is responsible for reconciling rollouts on scaling events. +func (c *Controller) sync(r *v1alpha1.Rollout, rsList []*appsv1.ReplicaSet) error { + newRS, oldRSs, err := c.getAllReplicaSetsAndSyncRevision(r, rsList, false) + if err != nil { + return err + } + previewSvc, activeSvc, err := c.getPreviewAndActiveServices(r) + if err != nil { + return nil + } + if err := c.scale(r, newRS, oldRSs, previewSvc, activeSvc); err != nil { + // If we get an error while trying to scale, the rollout will be requeued + // so we can abort this resync + return err + } + allRSs := append([]*appsv1.ReplicaSet{newRS}, oldRSs...) + return c.syncRolloutStatus(allRSs, newRS, previewSvc, activeSvc, r) +} + +// Should run only on scaling events and not during the normal rollout process. +func (c *Controller) scale(rollout *v1alpha1.Rollout, newRS *appsv1.ReplicaSet, oldRSs []*appsv1.ReplicaSet, previewSvc *corev1.Service, activeSvc *corev1.Service) error { + + previewSelector, ok := c.getRolloutSelectorLabel(previewSvc) + if !ok { + previewSelector = "" + } + activeSelector, ok := c.getRolloutSelectorLabel(activeSvc) + if !ok { + activeSelector = "" + } + for _, rs := range append([]*appsv1.ReplicaSet{newRS}, oldRSs...) { + rsLabel := rs.Labels[v1alpha1.DefaultRolloutUniqueLabelKey] + if rsLabel == activeSelector || rsLabel == previewSelector { + if *(rs.Spec.Replicas) == *(rollout.Spec.Replicas) { + continue + } + _, _, err := c.scaleReplicaSetAndRecordEvent(rs, *(rollout.Spec.Replicas), rollout) + return err + } + + } + // If there is only one replica set with pods, then we should scale that up to the full count of the + // rollout. If there is no replica set with pods, then we should scale up the newest replica set. + if activeOrLatest := replicasetutil.FindActiveOrLatest(newRS, oldRSs); activeOrLatest != nil { + if *(activeOrLatest.Spec.Replicas) == *(rollout.Spec.Replicas) { + return nil + } + _, _, err := c.scaleReplicaSetAndRecordEvent(activeOrLatest, *(rollout.Spec.Replicas), rollout) + return err + } + + // Old replica sets should be fully scaled down if they aren't receiving traffic from the active or + // preview service. This case handles replica set adoption during a saturated new replica set. + for _, old := range controller.FilterActiveReplicaSets(oldRSs) { + oldLabel, ok := old.Labels[v1alpha1.DefaultRolloutUniqueLabelKey] + if !ok || (oldLabel != activeSelector && oldLabel != previewSelector) { + if _, _, err := c.scaleReplicaSetAndRecordEvent(old, 0, rollout); err != nil { + return err + } + } + } + return nil +} + +// isScalingEvent checks whether the provided rollout has been updated with a scaling event +// by looking at the desired-replicas annotation in the active replica sets of the rollout. +// +// rsList should come from getReplicaSetsForRollout(r). +func (c *Controller) isScalingEvent(rollout *v1alpha1.Rollout, rsList []*appsv1.ReplicaSet) (bool, error) { + newRS, oldRSs, err := c.getAllReplicaSetsAndSyncRevision(rollout, rsList, false) + if err != nil { + return false, err + } + allRSs := append(oldRSs, newRS) + for _, rs := range controller.FilterActiveReplicaSets(allRSs) { + desired, ok := annotations.GetDesiredReplicasAnnotation(rs) + if !ok { + continue + } + if desired != *(rollout.Spec.Replicas) { + return true, nil + } + } + return false, nil +} + +func (c *Controller) scaleReplicaSetAndRecordEvent(rs *appsv1.ReplicaSet, newScale int32, rollout *v1alpha1.Rollout) (bool, *appsv1.ReplicaSet, error) { + // No need to scale + if *(rs.Spec.Replicas) == newScale { + return false, rs, nil + } + var scalingOperation string + if *(rs.Spec.Replicas) < newScale { + scalingOperation = "up" + } else { + scalingOperation = "down" + } + scaled, newRS, err := c.scaleReplicaSet(rs, newScale, rollout, scalingOperation) + return scaled, newRS, err +} + +func (c *Controller) scaleReplicaSet(rs *appsv1.ReplicaSet, newScale int32, rollout *v1alpha1.Rollout, scalingOperation string) (bool, *appsv1.ReplicaSet, error) { + + sizeNeedsUpdate := *(rs.Spec.Replicas) != newScale + + annotationsNeedUpdate := annotations.ReplicasAnnotationsNeedUpdate(rs, *(rollout.Spec.Replicas)) + + scaled := false + var err error + if sizeNeedsUpdate || annotationsNeedUpdate { + rsCopy := rs.DeepCopy() + *(rsCopy.Spec.Replicas) = newScale + annotations.SetReplicasAnnotations(rsCopy, *(rollout.Spec.Replicas)) + rs, err = c.kubeclientset.AppsV1().ReplicaSets(rsCopy.Namespace).Update(rsCopy) + if err == nil && sizeNeedsUpdate { + scaled = true + c.recorder.Eventf(rollout, corev1.EventTypeNormal, "ScalingReplicaSet", "Scaled %s replica set %s to %d", scalingOperation, rs.Name, newScale) + } + } + return scaled, rs, err +} + +func (c *Controller) syncRolloutStatus(allRSs []*appsv1.ReplicaSet, newRS *appsv1.ReplicaSet, previewSvc *corev1.Service, activeSvc *corev1.Service, r *v1alpha1.Rollout) error { + newStatus := c.calculateStatus(allRSs, newRS, previewSvc, activeSvc, r) + + if reflect.DeepEqual(r.Status, newStatus) { + return nil + } + + r.Status = newStatus + _, err := c.rolloutsclientset.ArgoprojV1alpha1().Rollouts(r.Namespace).Update(r) + return err +} + +// calculateStatus calculates the latest status for the provided rollout by looking into the provided replica sets. +func (c *Controller) calculateStatus(allRSs []*appsv1.ReplicaSet, newRS *appsv1.ReplicaSet, previewSvc *corev1.Service, activeSvc *corev1.Service, rollout *v1alpha1.Rollout) v1alpha1.RolloutStatus { + + previewSelector, ok := c.getRolloutSelectorLabel(previewSvc) + if !ok { + previewSelector = "" + } + activeSelector, ok := c.getRolloutSelectorLabel(activeSvc) + if !ok { + activeSelector = "" + } + + status := v1alpha1.RolloutStatus{ + PreviewSelector: previewSelector, + ActiveSelector: activeSelector, + CollisionCount: rollout.Status.CollisionCount, + VerifyingPreview: rollout.Status.VerifyingPreview, + } + + return status +} diff --git a/controller/sync_test.go b/controller/sync_test.go new file mode 100644 index 0000000000..449906587f --- /dev/null +++ b/controller/sync_test.go @@ -0,0 +1,229 @@ +package controller + +import ( + "strconv" + "testing" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + k8sfake "k8s.io/client-go/kubernetes/fake" + testclient "k8s.io/client-go/testing" + "k8s.io/client-go/tools/record" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake" + "github.com/argoproj/argo-rollouts/utils/annotations" +) + +func intOrStrP(num int) *intstr.IntOrString { + intstr := intstr.FromInt(num) + return &intstr +} + +func newRolloutWithStatus(name string, replicas int, revisionHistoryLimit *int32, selector map[string]string) *v1alpha1.Rollout { + rollout := newRollout(name, replicas, revisionHistoryLimit, selector, "", "") + return rollout +} + +func rs(name string, replicas int, selector map[string]string, timestamp metav1.Time, ownerRef *metav1.OwnerReference) *appsv1.ReplicaSet { + ownerRefs := []metav1.OwnerReference{} + if ownerRef != nil { + ownerRefs = append(ownerRefs, *ownerRef) + } + + return &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + CreationTimestamp: timestamp, + Namespace: metav1.NamespaceDefault, + OwnerReferences: ownerRefs, + Labels: selector, + Annotations: map[string]string{annotations.DesiredReplicasAnnotation: strconv.Itoa(replicas)}, + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: func() *int32 { i := int32(replicas); return &i }(), + Selector: &metav1.LabelSelector{MatchLabels: selector}, + Template: corev1.PodTemplateSpec{}, + }, + } +} + +func TestScale(t *testing.T) { + newTimestamp := metav1.Date(2016, 5, 20, 3, 0, 0, 0, time.UTC) + oldTimestamp := metav1.Date(2016, 5, 20, 2, 0, 0, 0, time.UTC) + olderTimestamp := metav1.Date(2016, 5, 20, 1, 0, 0, 0, time.UTC) + oldestTimestamp := metav1.Date(2016, 5, 20, 0, 0, 0, 0, time.UTC) + + tests := []struct { + name string + rollout *v1alpha1.Rollout + oldRollout *v1alpha1.Rollout + + newRS *appsv1.ReplicaSet + oldRSs []*appsv1.ReplicaSet + + expectedNew *appsv1.ReplicaSet + expectedOld []*appsv1.ReplicaSet + wasntUpdated map[string]bool + + previewSvc *corev1.Service + activeSvc *corev1.Service + + desiredReplicasAnnotations map[string]int32 + }{ + { + name: "normal scaling event: 10 -> 12", + rollout: newRollout("foo", 12, nil, nil, "", ""), + oldRollout: newRollout("foo", 10, nil, nil, "", ""), + + newRS: rs("foo-v1", 10, nil, newTimestamp, nil), + oldRSs: []*appsv1.ReplicaSet{}, + + expectedNew: rs("foo-v1", 12, nil, newTimestamp, nil), + expectedOld: []*appsv1.ReplicaSet{}, + previewSvc: nil, + activeSvc: nil, + }, + { + name: "normal scaling event: 10 -> 5", + rollout: newRollout("foo", 5, nil, nil, "", ""), + oldRollout: newRollout("foo", 10, nil, nil, "", ""), + + newRS: rs("foo-v1", 10, nil, newTimestamp, nil), + oldRSs: []*appsv1.ReplicaSet{}, + + expectedNew: rs("foo-v1", 5, nil, newTimestamp, nil), + expectedOld: []*appsv1.ReplicaSet{}, + previewSvc: nil, + activeSvc: nil, + }, + { + name: "Scale up non-active latest Replicaset", + rollout: newRollout("foo", 5, nil, nil, "", ""), + oldRollout: newRollout("foo", 5, nil, nil, "", ""), + + newRS: rs("foo-v2", 0, nil, newTimestamp, nil), + oldRSs: []*appsv1.ReplicaSet{rs("foo-v1", 0, nil, oldTimestamp, nil)}, + + expectedNew: rs("foo-v2", 5, nil, newTimestamp, nil), + expectedOld: []*appsv1.ReplicaSet{rs("foo-v1", 0, nil, newTimestamp, nil)}, + previewSvc: nil, + activeSvc: nil, + }, + { + name: "Scale down older active replica sets", + rollout: newRolloutWithStatus("foo", 5, nil, nil), + oldRollout: newRolloutWithStatus("foo", 5, nil, nil), + + newRS: rs("foo-v2", 5, nil, newTimestamp, nil), + oldRSs: []*appsv1.ReplicaSet{ + rs("foo-v1", 5, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v1"}, oldTimestamp, nil), + rs("foo-v0", 5, nil, olderTimestamp, nil), + }, + + expectedNew: rs("foo-v2", 5, nil, newTimestamp, nil), + expectedOld: []*appsv1.ReplicaSet{ + rs("foo-v1", 5, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v1"}, oldTimestamp, nil), + rs("foo-v0", 0, nil, olderTimestamp, nil), + }, + previewSvc: nil, + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v1"}), + }, + { + name: "No updates", + rollout: newRolloutWithStatus("foo", 5, nil, nil), + oldRollout: newRolloutWithStatus("foo", 5, nil, nil), + + newRS: rs("foo-v3", 5, nil, newTimestamp, nil), + oldRSs: []*appsv1.ReplicaSet{ + rs("foo-v2", 5, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v2"}, oldTimestamp, nil), + rs("foo-v1", 5, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v1"}, olderTimestamp, nil), + rs("foo-v0", 0, nil, oldestTimestamp, nil), + }, + + expectedNew: rs("foo-v3", 5, nil, newTimestamp, nil), + expectedOld: []*appsv1.ReplicaSet{ + rs("foo-v2", 5, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v2"}, oldTimestamp, nil), + rs("foo-v1", 5, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v1"}, olderTimestamp, nil), + rs("foo-v0", 0, nil, oldestTimestamp, nil), + }, + previewSvc: newService("preview", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v2"}), + activeSvc: newService("active", 80, map[string]string{v1alpha1.DefaultRolloutUniqueLabelKey: "foo-v1"}), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _ = olderTimestamp + rolloutFake := fake.Clientset{} + k8sFake := k8sfake.Clientset{} + c := &Controller{ + rolloutsclientset: &rolloutFake, + kubeclientset: &k8sFake, + recorder: &record.FakeRecorder{}, + } + + if test.newRS != nil { + desiredReplicas := *(test.oldRollout.Spec.Replicas) + if desired, ok := test.desiredReplicasAnnotations[test.newRS.Name]; ok { + desiredReplicas = desired + } + annotations.SetReplicasAnnotations(test.newRS, desiredReplicas) + } + for i := range test.oldRSs { + rs := test.oldRSs[i] + if rs == nil { + continue + } + desiredReplicas := *(test.oldRollout.Spec.Replicas) + if desired, ok := test.desiredReplicasAnnotations[rs.Name]; ok { + desiredReplicas = desired + } + annotations.SetReplicasAnnotations(rs, desiredReplicas) + } + + if err := c.scale(test.rollout, test.newRS, test.oldRSs, test.previewSvc, test.activeSvc); err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + return + } + + // Construct the nameToSize map that will hold all the sizes we got our of tests + // Skip updating the map if the replica set wasn't updated since there will be + // no update action for it. + nameToSize := make(map[string]int32) + if test.newRS != nil { + nameToSize[test.newRS.Name] = *(test.newRS.Spec.Replicas) + } + for i := range test.oldRSs { + rs := test.oldRSs[i] + nameToSize[rs.Name] = *(rs.Spec.Replicas) + } + // Get all the UPDATE actions and update nameToSize with all the updated sizes. + for _, action := range k8sFake.Actions() { + rs := action.(testclient.UpdateAction).GetObject().(*appsv1.ReplicaSet) + if !test.wasntUpdated[rs.Name] { + nameToSize[rs.Name] = *(rs.Spec.Replicas) + } + } + + if test.expectedNew != nil && test.newRS != nil && *(test.expectedNew.Spec.Replicas) != nameToSize[test.newRS.Name] { + t.Errorf("%s: expected new replicas: %d, got: %d", test.name, *(test.expectedNew.Spec.Replicas), nameToSize[test.newRS.Name]) + return + } + if len(test.expectedOld) != len(test.oldRSs) { + t.Errorf("%s: expected %d old replica sets, got %d", test.name, len(test.expectedOld), len(test.oldRSs)) + return + } + for n := range test.oldRSs { + rs := test.oldRSs[n] + expected := test.expectedOld[n] + if *(expected.Spec.Replicas) != nameToSize[rs.Name] { + t.Errorf("%s: expected old (%s) replicas: %d, got: %d", test.name, rs.Name, *(expected.Spec.Replicas), nameToSize[rs.Name]) + } + } + }) + } +} diff --git a/examples/example-rollout.yaml b/examples/example-rollout.yaml new file mode 100644 index 0000000000..f23aaefd23 --- /dev/null +++ b/examples/example-rollout.yaml @@ -0,0 +1,26 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Rollout +metadata: + name: example-rollout +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.15.4 + ports: + - containerPort: 80 + minReadySeconds: 30 + revisionHistoryLimit: 3 + strategy: + type: BlueGreenUpdate + blueGreen: + activeService: my-service + previewService: my-service-2 diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index c4c311f5da..acc7b288dc 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -1,35 +1,12 @@ -#!/usr/bin/env bash - -# Copyright 2017 The Kubernetes Authors. -# -# 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. - set -o errexit set -o nounset set -o pipefail SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/.. -echo BASH_SOURCE: $BASH_SOURCE -echo SCRIPT_ROOT: $SCRIPT_ROOT CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${SCRIPT_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} -echo CODEGEN_PKG: $CODEGEN_PKG -# generate the code with: -# --output-base because this script should also be able to run inside the vendor dir of -# k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir -# instead of the $GOPATH directly. For normal projects this can be dropped. + ${CODEGEN_PKG}/generate-groups.sh "deepcopy,client,informer,lister" \ - k8s.io/sample-controller/pkg/client k8s.io/sample-controller/pkg/apis \ - samplecontroller:v1alpha1 \ - --output-base "$(dirname ${BASH_SOURCE})/../../.." \ + github.com/argoproj/argo-rollouts/pkg/client github.com/argoproj/argo-rollouts/pkg/apis \ + rollouts:v1alpha1 \ --go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt # To use your own boilerplate text use: diff --git a/hack/update-manifests.sh b/hack/update-manifests.sh new file mode 100755 index 0000000000..0a1fb25ffc --- /dev/null +++ b/hack/update-manifests.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +SRCROOT="$( CDPATH='' cd -- "$(dirname "$0")/.." && pwd -P )" +AUTOGENMSG="# This is an auto-generated file. DO NOT EDIT" + +update_image () { + if [ ! -z "${IMAGE_NAMESPACE}" ]; then + sed 's| image: \(.*\)/\(rollout-controller.*\)| image: '"${IMAGE_NAMESPACE}"'/\2|g' "${1}" > "${1}.bak" + mv "${1}.bak" "${1}" + fi +} + +if [ ! -z "${IMAGE_TAG}" ]; then + (cd ${SRCROOT}/manifests/base && kustomize edit set imagetag argoproj/rollout-controller:${IMAGE_TAG}) +fi + +echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/install.yaml" +kustomize build "${SRCROOT}/manifests/cluster-install" >> "${SRCROOT}/manifests/install.yaml" +update_image "${SRCROOT}/manifests/install.yaml" + +echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/namespace-install.yaml" +kustomize build "${SRCROOT}/manifests/namespace-install" >> "${SRCROOT}/manifests/namespace-install.yaml" +update_image "${SRCROOT}/manifests/namespace-install.yaml" diff --git a/hack/verify-codegen.sh b/hack/verify-codegen.sh index d02a6fa395..fb41165b59 100755 --- a/hack/verify-codegen.sh +++ b/hack/verify-codegen.sh @@ -1,19 +1,3 @@ -#!/usr/bin/env bash - -# Copyright 2017 The Kubernetes Authors. -# -# 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. - set -o errexit set -o nounset set -o pipefail diff --git a/manifests/base/kustomization.yaml b/manifests/base/kustomization.yaml new file mode 100644 index 0000000000..289fa06207 --- /dev/null +++ b/manifests/base/kustomization.yaml @@ -0,0 +1,9 @@ +resources: +- rollout-controller-sa.yaml +- rollout-controller-role.yaml +- rollout-controller-rolebinding.yaml +- rollout-controller-deployment.yaml + +imageTags: +- name: argoproj/rollout-controlller + newTag: latest diff --git a/manifests/base/rollout-controller-deployment.yaml b/manifests/base/rollout-controller-deployment.yaml new file mode 100644 index 0000000000..bb20204977 --- /dev/null +++ b/manifests/base/rollout-controller-deployment.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rollout-controller +spec: + selector: + matchLabels: + app: rollout-controller + template: + metadata: + labels: + app: rollout-controller + spec: + serviceAccountName: rollout-controller + containers: + - command: + - "/bin/rollouts-controller" + image: argoproj/rollout-controller:latest + imagePullPolicy: Always + name: rollout-controller diff --git a/manifests/base/rollout-controller-role.yaml b/manifests/base/rollout-controller-role.yaml new file mode 100644 index 0000000000..d0b2f6aa24 --- /dev/null +++ b/manifests/base/rollout-controller-role.yaml @@ -0,0 +1,37 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: rollout-controller-role +rules: +- apiGroups: + - argoproj.io + resources: + - rollouts + verbs: + - get + - list + - update +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - create + - update + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/manifests/base/rollout-controller-rolebinding.yaml b/manifests/base/rollout-controller-rolebinding.yaml new file mode 100644 index 0000000000..a4104d2a0c --- /dev/null +++ b/manifests/base/rollout-controller-rolebinding.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: rollout-controller-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: rollout-controller-role +subjects: +- kind: ServiceAccount + name: rollout-controller diff --git a/manifests/base/rollout-controller-sa.yaml b/manifests/base/rollout-controller-sa.yaml new file mode 100644 index 0000000000..f489f62902 --- /dev/null +++ b/manifests/base/rollout-controller-sa.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: rollout-controller diff --git a/manifests/cluster-install/kustomization.yaml b/manifests/cluster-install/kustomization.yaml new file mode 100644 index 0000000000..290fe16144 --- /dev/null +++ b/manifests/cluster-install/kustomization.yaml @@ -0,0 +1,6 @@ +bases: +- ../namespace-install + +resources: +- rollout-controller-clusterrole.yaml +- rollout-controller-clusterrolebinding.yaml diff --git a/manifests/cluster-install/rollout-controller-clusterrole.yaml b/manifests/cluster-install/rollout-controller-clusterrole.yaml new file mode 100644 index 0000000000..5628f4c7ab --- /dev/null +++ b/manifests/cluster-install/rollout-controller-clusterrole.yaml @@ -0,0 +1,31 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: rollout-controller-clusterrole +rules: +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - create + - update + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - patch +- apiGroups: + - argoproj.io + resources: + - rollouts + verbs: + - get + - list + - update \ No newline at end of file diff --git a/manifests/cluster-install/rollout-controller-clusterrolebinding.yaml b/manifests/cluster-install/rollout-controller-clusterrolebinding.yaml new file mode 100644 index 0000000000..212a27b726 --- /dev/null +++ b/manifests/cluster-install/rollout-controller-clusterrolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: rollout-controller-clusterrolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: rollout-controller-clusterrole +subjects: +- kind: ServiceAccount + name: rollout-controller + namespace: rollout-controller diff --git a/manifests/crds/kustomization.yaml b/manifests/crds/kustomization.yaml new file mode 100644 index 0000000000..f447aadc31 --- /dev/null +++ b/manifests/crds/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- rollout-crd.yaml diff --git a/artifacts/examples/crd.yaml b/manifests/crds/rollout-crd.yaml similarity index 57% rename from artifacts/examples/crd.yaml rename to manifests/crds/rollout-crd.yaml index 4a457068dc..58a07921f9 100644 --- a/artifacts/examples/crd.yaml +++ b/manifests/crds/rollout-crd.yaml @@ -1,11 +1,11 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: foos.samplecontroller.k8s.io + name: rollouts.argoproj.io spec: - group: samplecontroller.k8s.io + group: argoproj.io version: v1alpha1 names: - kind: Foo - plural: foos + kind: Rollout + plural: rollouts scope: Namespaced diff --git a/manifests/install.yaml b/manifests/install.yaml new file mode 100644 index 0000000000..58d240d4e2 --- /dev/null +++ b/manifests/install.yaml @@ -0,0 +1,133 @@ +# This is an auto-generated file. DO NOT EDIT +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: rollouts.argoproj.io +spec: + group: argoproj.io + names: + kind: Rollout + plural: rollouts + scope: Namespaced + version: v1alpha1 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: rollout-controller +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: rollout-controller-role +rules: +- apiGroups: + - argoproj.io + resources: + - rollouts + verbs: + - get + - list + - update +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - create + - update + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: rollout-controller-clusterrole +rules: +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - create + - update + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - patch +- apiGroups: + - argoproj.io + resources: + - rollouts + verbs: + - get + - list + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: rollout-controller-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: rollout-controller-role +subjects: +- kind: ServiceAccount + name: rollout-controller +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: rollout-controller-clusterrolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: rollout-controller-clusterrole +subjects: +- kind: ServiceAccount + name: rollout-controller + namespace: rollout-controller +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rollout-controller +spec: + selector: + matchLabels: + app: rollout-controller + template: + metadata: + labels: + app: rollout-controller + spec: + containers: + - command: + - /bin/rollouts-controller + image: argoproj/rollout-controller:latest + imagePullPolicy: Always + name: rollout-controller + serviceAccountName: rollout-controller diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml new file mode 100644 index 0000000000..2c4aa3ba92 --- /dev/null +++ b/manifests/namespace-install.yaml @@ -0,0 +1,88 @@ +# This is an auto-generated file. DO NOT EDIT +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: rollouts.argoproj.io +spec: + group: argoproj.io + names: + kind: Rollout + plural: rollouts + scope: Namespaced + version: v1alpha1 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: rollout-controller +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: rollout-controller-role +rules: +- apiGroups: + - argoproj.io + resources: + - rollouts + verbs: + - get + - list + - update +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - create + - update + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: rollout-controller-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: rollout-controller-role +subjects: +- kind: ServiceAccount + name: rollout-controller +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rollout-controller +spec: + selector: + matchLabels: + app: rollout-controller + template: + metadata: + labels: + app: rollout-controller + spec: + containers: + - command: + - /bin/rollouts-controller + image: argoproj/rollout-controller:latest + imagePullPolicy: Always + name: rollout-controller + serviceAccountName: rollout-controller diff --git a/manifests/namespace-install/kustomization.yaml b/manifests/namespace-install/kustomization.yaml new file mode 100644 index 0000000000..87447545e1 --- /dev/null +++ b/manifests/namespace-install/kustomization.yaml @@ -0,0 +1,3 @@ +bases: +- ../crds +- ../base diff --git a/pkg/apis/samplecontroller/register.go b/pkg/apis/rollouts/register.go similarity index 73% rename from pkg/apis/samplecontroller/register.go rename to pkg/apis/rollouts/register.go index 394f7967e2..b210cb04d7 100644 --- a/pkg/apis/samplecontroller/register.go +++ b/pkg/apis/rollouts/register.go @@ -14,8 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package samplecontroller +package rollouts const ( - GroupName = "samplecontroller.k8s.io" + Kind string = "Rollout" + Group string = "argoproj.io" + Singular string = "rollout" + Plural string = "rollouts" + ShortName string = "ro" + FullName string = Plural + "." + Group ) diff --git a/pkg/apis/rollouts/v1alpha1/doc.go b/pkg/apis/rollouts/v1alpha1/doc.go new file mode 100644 index 0000000000..42ce328f95 --- /dev/null +++ b/pkg/apis/rollouts/v1alpha1/doc.go @@ -0,0 +1,5 @@ +// +k8s:deepcopy-gen=package +// +groupName=argoproj.io + +// Package v1alpha1 is the v1alpha1 version of the API. +package v1alpha1 diff --git a/pkg/apis/samplecontroller/v1alpha1/register.go b/pkg/apis/rollouts/v1alpha1/register.go similarity index 54% rename from pkg/apis/samplecontroller/v1alpha1/register.go rename to pkg/apis/rollouts/v1alpha1/register.go index df5695eb09..a35774590f 100644 --- a/pkg/apis/samplecontroller/v1alpha1/register.go +++ b/pkg/apis/rollouts/v1alpha1/register.go @@ -1,19 +1,3 @@ -/* -Copyright 2017 The Kubernetes Authors. - -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 v1alpha1 import ( @@ -21,11 +5,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - samplecontroller "k8s.io/sample-controller/pkg/apis/samplecontroller" + rollouts "github.com/argoproj/argo-rollouts/pkg/apis/rollouts" ) // SchemeGroupVersion is group version used to register these objects -var SchemeGroupVersion = schema.GroupVersion{Group: samplecontroller.GroupName, Version: "v1alpha1"} +var SchemeGroupVersion = schema.GroupVersion{Group: rollouts.Group, Version: "v1alpha1"} // Kind takes an unqualified kind and returns back a Group qualified GroupKind func Kind(kind string) schema.GroupKind { @@ -45,8 +29,8 @@ var ( // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, - &Foo{}, - &FooList{}, + &Rollout{}, + &RolloutList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/rollouts/v1alpha1/types.go b/pkg/apis/rollouts/v1alpha1/types.go new file mode 100644 index 0000000000..b3ca91d3cd --- /dev/null +++ b/pkg/apis/rollouts/v1alpha1/types.go @@ -0,0 +1,107 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +genclient:skipVerbs=patch +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Rollout is a specification for a Rollout resource +type Rollout struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RolloutSpec `json:"spec"` + Status RolloutStatus `json:"status"` +} + +// RolloutSpec is the spec for a Rollout resource +type RolloutSpec struct { + // Number of desired pods. This is a pointer to distinguish between explicit + // zero and not specified. Defaults to 1. + // +optional + Replicas *int32 `json:"replicas"` + // Label selector for pods. Existing ReplicaSets whose pods are + // selected by this will be the ones affected by this rollout. + // It must match the pod template's labels. + Selector *metav1.LabelSelector `json:"selector"` + // Template describes the pods that will be created. + Template corev1.PodTemplateSpec `json:"template"` + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + // +optional + MinReadySeconds int32 `json:"minReadySeconds"` + // The deployment strategy to use to replace existing pods with new ones. + // +optional + Strategy RolloutStrategy `json:"strategy"` + // The number of old ReplicaSets to retain. + // This is a pointer to distinguish between explicit zero and not specified. + // +optional + RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"` +} + +const ( + // DefaultRolloutUniqueLabelKey is the default key of the selector that is added + // to existing ReplicaSets (and label key that is added to its pods) to prevent the existing ReplicaSets + // to select new pods (and old pods being select by new ReplicaSet). + DefaultRolloutUniqueLabelKey string = "rollouts-pod-template-hash" +) + +// RolloutStrategy defines stragtegy to apply during next rollout +type RolloutStrategy struct { + BlueGreenStrategy *BlueGreenStrategy `json:"blueGreen,omitempty"` + Type RolloutStrategyType `json:"type"` +} + +//RolloutStrategyType defines a type that holds all the different rollout straegies +type RolloutStrategyType string + +const ( + // BlueGreenRolloutStrategyType Replace the old ReplicaSets by using a blue green update + // i.e Wait until a new stack is completely health before switching the service + BlueGreenRolloutStrategyType RolloutStrategyType = "BlueGreenUpdate" +) + +// BlueGreenStrategy defines parameters for Blue Green deployment +type BlueGreenStrategy struct { + // Name of the service that the rollout modifies as the active service. + ActiveService string `json:"activeService"` + // Name of the service that the rollout modifies as the preview service. + PreviewService string `json:"previewService"` +} + +// RolloutStatus is the status for a Rollout resource +type RolloutStatus struct { + // PreviewSelector indicates which replicas set the preview service is serving traffic to + // +optional + PreviewSelector string `json:"previewSelector"` + // ActiveSelector indicates which replicas set the active service is serving traffic to + // +optional + ActiveSelector string `json:"activeSelector"` + // VerifyingPreview indicates the rollout is verifying the replicas set being served + // traffic from the preview service. User will need to edit this field to continue the rollout. + // +optional + VerifyingPreview *bool `json:"verifyingPreview"` + // Count of hash collisions for the Rollout. The Rollout controller uses this + // field as a collision avoidance mechanism when it needs to create the name for the + // newest ReplicaSet. + // +optional + CollisionCount *int32 `json:"collisionCount"` + // The generation observed by the rollout controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RolloutList is a list of Rollout resources +type RolloutList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []Rollout `json:"items"` +} diff --git a/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..ee6c5bb0f0 --- /dev/null +++ b/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,183 @@ +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BlueGreenStrategy) DeepCopyInto(out *BlueGreenStrategy) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BlueGreenStrategy. +func (in *BlueGreenStrategy) DeepCopy() *BlueGreenStrategy { + if in == nil { + return nil + } + out := new(BlueGreenStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Rollout) DeepCopyInto(out *Rollout) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rollout. +func (in *Rollout) DeepCopy() *Rollout { + if in == nil { + return nil + } + out := new(Rollout) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Rollout) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutList) DeepCopyInto(out *RolloutList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Rollout, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutList. +func (in *RolloutList) DeepCopy() *RolloutList { + if in == nil { + return nil + } + out := new(RolloutList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RolloutList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutSpec) DeepCopyInto(out *RolloutSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + in.Template.DeepCopyInto(&out.Template) + in.Strategy.DeepCopyInto(&out.Strategy) + if in.RevisionHistoryLimit != nil { + in, out := &in.RevisionHistoryLimit, &out.RevisionHistoryLimit + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutSpec. +func (in *RolloutSpec) DeepCopy() *RolloutSpec { + if in == nil { + return nil + } + out := new(RolloutSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutStatus) DeepCopyInto(out *RolloutStatus) { + *out = *in + if in.VerifyingPreview != nil { + in, out := &in.VerifyingPreview, &out.VerifyingPreview + *out = new(bool) + **out = **in + } + if in.CollisionCount != nil { + in, out := &in.CollisionCount, &out.CollisionCount + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutStatus. +func (in *RolloutStatus) DeepCopy() *RolloutStatus { + if in == nil { + return nil + } + out := new(RolloutStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutStrategy) DeepCopyInto(out *RolloutStrategy) { + *out = *in + if in.BlueGreenStrategy != nil { + in, out := &in.BlueGreenStrategy, &out.BlueGreenStrategy + *out = new(BlueGreenStrategy) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutStrategy. +func (in *RolloutStrategy) DeepCopy() *RolloutStrategy { + if in == nil { + return nil + } + out := new(RolloutStrategy) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apis/samplecontroller/v1alpha1/doc.go b/pkg/apis/samplecontroller/v1alpha1/doc.go deleted file mode 100644 index e6c135eed8..0000000000 --- a/pkg/apis/samplecontroller/v1alpha1/doc.go +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -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. -*/ - -// +k8s:deepcopy-gen=package -// +groupName=samplecontroller.k8s.io - -// Package v1alpha1 is the v1alpha1 version of the API. -package v1alpha1 diff --git a/pkg/apis/samplecontroller/v1alpha1/types.go b/pkg/apis/samplecontroller/v1alpha1/types.go deleted file mode 100644 index 74ffc67212..0000000000 --- a/pkg/apis/samplecontroller/v1alpha1/types.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -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 v1alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// +genclient -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// Foo is a specification for a Foo resource -type Foo struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec FooSpec `json:"spec"` - Status FooStatus `json:"status"` -} - -// FooSpec is the spec for a Foo resource -type FooSpec struct { - DeploymentName string `json:"deploymentName"` - Replicas *int32 `json:"replicas"` -} - -// FooStatus is the status for a Foo resource -type FooStatus struct { - AvailableReplicas int32 `json:"availableReplicas"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// FooList is a list of Foo resources -type FooList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata"` - - Items []Foo `json:"items"` -} diff --git a/pkg/apis/samplecontroller/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/samplecontroller/v1alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 4519ccf5e1..0000000000 --- a/pkg/apis/samplecontroller/v1alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,123 +0,0 @@ -// +build !ignore_autogenerated - -/* -Copyright The Kubernetes Authors. - -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. -*/ - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Foo) DeepCopyInto(out *Foo) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Foo. -func (in *Foo) DeepCopy() *Foo { - if in == nil { - return nil - } - out := new(Foo) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Foo) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FooList) DeepCopyInto(out *FooList) { - *out = *in - out.TypeMeta = in.TypeMeta - out.ListMeta = in.ListMeta - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Foo, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FooList. -func (in *FooList) DeepCopy() *FooList { - if in == nil { - return nil - } - out := new(FooList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FooList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FooSpec) DeepCopyInto(out *FooSpec) { - *out = *in - if in.Replicas != nil { - in, out := &in.Replicas, &out.Replicas - *out = new(int32) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FooSpec. -func (in *FooSpec) DeepCopy() *FooSpec { - if in == nil { - return nil - } - out := new(FooSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FooStatus) DeepCopyInto(out *FooStatus) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FooStatus. -func (in *FooStatus) DeepCopy() *FooStatus { - if in == nil { - return nil - } - out := new(FooStatus) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index 5bdd729042..d95c75fe87 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -19,35 +19,35 @@ limitations under the License. package versioned import ( + argoprojv1alpha1 "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/typed/rollouts/v1alpha1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" - samplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1" ) type Interface interface { Discovery() discovery.DiscoveryInterface - SamplecontrollerV1alpha1() samplecontrollerv1alpha1.SamplecontrollerV1alpha1Interface + ArgoprojV1alpha1() argoprojv1alpha1.ArgoprojV1alpha1Interface // Deprecated: please explicitly pick a version if possible. - Samplecontroller() samplecontrollerv1alpha1.SamplecontrollerV1alpha1Interface + Argoproj() argoprojv1alpha1.ArgoprojV1alpha1Interface } // Clientset contains the clients for groups. Each group has exactly one // version included in a Clientset. type Clientset struct { *discovery.DiscoveryClient - samplecontrollerV1alpha1 *samplecontrollerv1alpha1.SamplecontrollerV1alpha1Client + argoprojV1alpha1 *argoprojv1alpha1.ArgoprojV1alpha1Client } -// SamplecontrollerV1alpha1 retrieves the SamplecontrollerV1alpha1Client -func (c *Clientset) SamplecontrollerV1alpha1() samplecontrollerv1alpha1.SamplecontrollerV1alpha1Interface { - return c.samplecontrollerV1alpha1 +// ArgoprojV1alpha1 retrieves the ArgoprojV1alpha1Client +func (c *Clientset) ArgoprojV1alpha1() argoprojv1alpha1.ArgoprojV1alpha1Interface { + return c.argoprojV1alpha1 } -// Deprecated: Samplecontroller retrieves the default version of SamplecontrollerClient. +// Deprecated: Argoproj retrieves the default version of ArgoprojClient. // Please explicitly pick a version. -func (c *Clientset) Samplecontroller() samplecontrollerv1alpha1.SamplecontrollerV1alpha1Interface { - return c.samplecontrollerV1alpha1 +func (c *Clientset) Argoproj() argoprojv1alpha1.ArgoprojV1alpha1Interface { + return c.argoprojV1alpha1 } // Discovery retrieves the DiscoveryClient @@ -66,7 +66,7 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { } var cs Clientset var err error - cs.samplecontrollerV1alpha1, err = samplecontrollerv1alpha1.NewForConfig(&configShallowCopy) + cs.argoprojV1alpha1, err = argoprojv1alpha1.NewForConfig(&configShallowCopy) if err != nil { return nil, err } @@ -82,7 +82,7 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *Clientset { var cs Clientset - cs.samplecontrollerV1alpha1 = samplecontrollerv1alpha1.NewForConfigOrDie(c) + cs.argoprojV1alpha1 = argoprojv1alpha1.NewForConfigOrDie(c) cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) return &cs @@ -91,7 +91,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { // New creates a new Clientset for the given RESTClient. func New(c rest.Interface) *Clientset { var cs Clientset - cs.samplecontrollerV1alpha1 = samplecontrollerv1alpha1.New(c) + cs.argoprojV1alpha1 = argoprojv1alpha1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index 7a388a439c..34619232b0 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -19,14 +19,14 @@ limitations under the License. package fake import ( + clientset "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + argoprojv1alpha1 "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/typed/rollouts/v1alpha1" + fakeargoprojv1alpha1 "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/testing" - clientset "k8s.io/sample-controller/pkg/client/clientset/versioned" - samplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1" - fakesamplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake" ) // NewSimpleClientset returns a clientset that will respond with the provided objects. @@ -71,12 +71,12 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface { var _ clientset.Interface = &Clientset{} -// SamplecontrollerV1alpha1 retrieves the SamplecontrollerV1alpha1Client -func (c *Clientset) SamplecontrollerV1alpha1() samplecontrollerv1alpha1.SamplecontrollerV1alpha1Interface { - return &fakesamplecontrollerv1alpha1.FakeSamplecontrollerV1alpha1{Fake: &c.Fake} +// ArgoprojV1alpha1 retrieves the ArgoprojV1alpha1Client +func (c *Clientset) ArgoprojV1alpha1() argoprojv1alpha1.ArgoprojV1alpha1Interface { + return &fakeargoprojv1alpha1.FakeArgoprojV1alpha1{Fake: &c.Fake} } -// Samplecontroller retrieves the SamplecontrollerV1alpha1Client -func (c *Clientset) Samplecontroller() samplecontrollerv1alpha1.SamplecontrollerV1alpha1Interface { - return &fakesamplecontrollerv1alpha1.FakeSamplecontrollerV1alpha1{Fake: &c.Fake} +// Argoproj retrieves the ArgoprojV1alpha1Client +func (c *Clientset) Argoproj() argoprojv1alpha1.ArgoprojV1alpha1Interface { + return &fakeargoprojv1alpha1.FakeArgoprojV1alpha1{Fake: &c.Fake} } diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index 8916108ed1..80b3f1b050 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -19,19 +19,19 @@ limitations under the License. package fake import ( + argoprojv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - samplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" ) var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var parameterCodec = runtime.NewParameterCodec(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ - samplecontrollerv1alpha1.AddToScheme, + argoprojv1alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index 27c9b7946f..98cb91d939 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -19,19 +19,19 @@ limitations under the License. package scheme import ( + argoprojv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - samplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" ) var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ - samplecontrollerv1alpha1.AddToScheme, + argoprojv1alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/doc.go b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/doc.go similarity index 100% rename from pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/doc.go rename to pkg/client/clientset/versioned/typed/rollouts/v1alpha1/doc.go diff --git a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake/doc.go b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake/doc.go similarity index 100% rename from pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake/doc.go rename to pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake/doc.go diff --git a/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake/fake_rollout.go b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake/fake_rollout.go new file mode 100644 index 0000000000..1767260492 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake/fake_rollout.go @@ -0,0 +1,128 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeRollouts implements RolloutInterface +type FakeRollouts struct { + Fake *FakeArgoprojV1alpha1 + ns string +} + +var rolloutsResource = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "rollouts"} + +var rolloutsKind = schema.GroupVersionKind{Group: "argoproj.io", Version: "v1alpha1", Kind: "Rollout"} + +// Get takes name of the rollout, and returns the corresponding rollout object, and an error if there is any. +func (c *FakeRollouts) Get(name string, options v1.GetOptions) (result *v1alpha1.Rollout, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(rolloutsResource, c.ns, name), &v1alpha1.Rollout{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Rollout), err +} + +// List takes label and field selectors, and returns the list of Rollouts that match those selectors. +func (c *FakeRollouts) List(opts v1.ListOptions) (result *v1alpha1.RolloutList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(rolloutsResource, rolloutsKind, c.ns, opts), &v1alpha1.RolloutList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.RolloutList{ListMeta: obj.(*v1alpha1.RolloutList).ListMeta} + for _, item := range obj.(*v1alpha1.RolloutList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested rollouts. +func (c *FakeRollouts) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(rolloutsResource, c.ns, opts)) + +} + +// Create takes the representation of a rollout and creates it. Returns the server's representation of the rollout, and an error, if there is any. +func (c *FakeRollouts) Create(rollout *v1alpha1.Rollout) (result *v1alpha1.Rollout, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(rolloutsResource, c.ns, rollout), &v1alpha1.Rollout{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Rollout), err +} + +// Update takes the representation of a rollout and updates it. Returns the server's representation of the rollout, and an error, if there is any. +func (c *FakeRollouts) Update(rollout *v1alpha1.Rollout) (result *v1alpha1.Rollout, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(rolloutsResource, c.ns, rollout), &v1alpha1.Rollout{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Rollout), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeRollouts) UpdateStatus(rollout *v1alpha1.Rollout) (*v1alpha1.Rollout, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(rolloutsResource, "status", c.ns, rollout), &v1alpha1.Rollout{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Rollout), err +} + +// Delete takes name of the rollout and deletes it. Returns an error if one occurs. +func (c *FakeRollouts) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(rolloutsResource, c.ns, name), &v1alpha1.Rollout{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeRollouts) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(rolloutsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.RolloutList{}) + return err +} diff --git a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake/fake_samplecontroller_client.go b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake/fake_rollouts_client.go similarity index 72% rename from pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake/fake_samplecontroller_client.go rename to pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake/fake_rollouts_client.go index 17a28bb6e8..3b1a3423c2 100644 --- a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake/fake_samplecontroller_client.go +++ b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/fake/fake_rollouts_client.go @@ -19,22 +19,22 @@ limitations under the License. package fake import ( + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/typed/rollouts/v1alpha1" rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" - v1alpha1 "k8s.io/sample-controller/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1" ) -type FakeSamplecontrollerV1alpha1 struct { +type FakeArgoprojV1alpha1 struct { *testing.Fake } -func (c *FakeSamplecontrollerV1alpha1) Foos(namespace string) v1alpha1.FooInterface { - return &FakeFoos{c, namespace} +func (c *FakeArgoprojV1alpha1) Rollouts(namespace string) v1alpha1.RolloutInterface { + return &FakeRollouts{c, namespace} } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. -func (c *FakeSamplecontrollerV1alpha1) RESTClient() rest.Interface { +func (c *FakeArgoprojV1alpha1) RESTClient() rest.Interface { var ret *rest.RESTClient return ret } diff --git a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/generated_expansion.go similarity index 94% rename from pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/generated_expansion.go rename to pkg/client/clientset/versioned/typed/rollouts/v1alpha1/generated_expansion.go index b64ea0250e..757ef02867 100644 --- a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/generated_expansion.go @@ -18,4 +18,4 @@ limitations under the License. package v1alpha1 -type FooExpansion interface{} +type RolloutExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/rollout.go b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/rollout.go new file mode 100644 index 0000000000..af48fa1497 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/rollout.go @@ -0,0 +1,175 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "time" + + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + scheme "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// RolloutsGetter has a method to return a RolloutInterface. +// A group's client should implement this interface. +type RolloutsGetter interface { + Rollouts(namespace string) RolloutInterface +} + +// RolloutInterface has methods to work with Rollout resources. +type RolloutInterface interface { + Create(*v1alpha1.Rollout) (*v1alpha1.Rollout, error) + Update(*v1alpha1.Rollout) (*v1alpha1.Rollout, error) + UpdateStatus(*v1alpha1.Rollout) (*v1alpha1.Rollout, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.Rollout, error) + List(opts v1.ListOptions) (*v1alpha1.RolloutList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + RolloutExpansion +} + +// rollouts implements RolloutInterface +type rollouts struct { + client rest.Interface + ns string +} + +// newRollouts returns a Rollouts +func newRollouts(c *ArgoprojV1alpha1Client, namespace string) *rollouts { + return &rollouts{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the rollout, and returns the corresponding rollout object, and an error if there is any. +func (c *rollouts) Get(name string, options v1.GetOptions) (result *v1alpha1.Rollout, err error) { + result = &v1alpha1.Rollout{} + err = c.client.Get(). + Namespace(c.ns). + Resource("rollouts"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Rollouts that match those selectors. +func (c *rollouts) List(opts v1.ListOptions) (result *v1alpha1.RolloutList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.RolloutList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("rollouts"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested rollouts. +func (c *rollouts) Watch(opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("rollouts"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch() +} + +// Create takes the representation of a rollout and creates it. Returns the server's representation of the rollout, and an error, if there is any. +func (c *rollouts) Create(rollout *v1alpha1.Rollout) (result *v1alpha1.Rollout, err error) { + result = &v1alpha1.Rollout{} + err = c.client.Post(). + Namespace(c.ns). + Resource("rollouts"). + Body(rollout). + Do(). + Into(result) + return +} + +// Update takes the representation of a rollout and updates it. Returns the server's representation of the rollout, and an error, if there is any. +func (c *rollouts) Update(rollout *v1alpha1.Rollout) (result *v1alpha1.Rollout, err error) { + result = &v1alpha1.Rollout{} + err = c.client.Put(). + Namespace(c.ns). + Resource("rollouts"). + Name(rollout.Name). + Body(rollout). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *rollouts) UpdateStatus(rollout *v1alpha1.Rollout) (result *v1alpha1.Rollout, err error) { + result = &v1alpha1.Rollout{} + err = c.client.Put(). + Namespace(c.ns). + Resource("rollouts"). + Name(rollout.Name). + SubResource("status"). + Body(rollout). + Do(). + Into(result) + return +} + +// Delete takes name of the rollout and deletes it. Returns an error if one occurs. +func (c *rollouts) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("rollouts"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *rollouts) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + var timeout time.Duration + if listOptions.TimeoutSeconds != nil { + timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("rollouts"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Timeout(timeout). + Body(options). + Do(). + Error() +} diff --git a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/samplecontroller_client.go b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/rollouts_client.go similarity index 59% rename from pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/samplecontroller_client.go rename to pkg/client/clientset/versioned/typed/rollouts/v1alpha1/rollouts_client.go index c59278504e..4a52661446 100644 --- a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/samplecontroller_client.go +++ b/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/rollouts_client.go @@ -19,28 +19,28 @@ limitations under the License. package v1alpha1 import ( + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/scheme" serializer "k8s.io/apimachinery/pkg/runtime/serializer" rest "k8s.io/client-go/rest" - v1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" - "k8s.io/sample-controller/pkg/client/clientset/versioned/scheme" ) -type SamplecontrollerV1alpha1Interface interface { +type ArgoprojV1alpha1Interface interface { RESTClient() rest.Interface - FoosGetter + RolloutsGetter } -// SamplecontrollerV1alpha1Client is used to interact with features provided by the samplecontroller.k8s.io group. -type SamplecontrollerV1alpha1Client struct { +// ArgoprojV1alpha1Client is used to interact with features provided by the argoproj.io group. +type ArgoprojV1alpha1Client struct { restClient rest.Interface } -func (c *SamplecontrollerV1alpha1Client) Foos(namespace string) FooInterface { - return newFoos(c, namespace) +func (c *ArgoprojV1alpha1Client) Rollouts(namespace string) RolloutInterface { + return newRollouts(c, namespace) } -// NewForConfig creates a new SamplecontrollerV1alpha1Client for the given config. -func NewForConfig(c *rest.Config) (*SamplecontrollerV1alpha1Client, error) { +// NewForConfig creates a new ArgoprojV1alpha1Client for the given config. +func NewForConfig(c *rest.Config) (*ArgoprojV1alpha1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err @@ -49,12 +49,12 @@ func NewForConfig(c *rest.Config) (*SamplecontrollerV1alpha1Client, error) { if err != nil { return nil, err } - return &SamplecontrollerV1alpha1Client{client}, nil + return &ArgoprojV1alpha1Client{client}, nil } -// NewForConfigOrDie creates a new SamplecontrollerV1alpha1Client for the given config and +// NewForConfigOrDie creates a new ArgoprojV1alpha1Client for the given config and // panics if there is an error in the config. -func NewForConfigOrDie(c *rest.Config) *SamplecontrollerV1alpha1Client { +func NewForConfigOrDie(c *rest.Config) *ArgoprojV1alpha1Client { client, err := NewForConfig(c) if err != nil { panic(err) @@ -62,9 +62,9 @@ func NewForConfigOrDie(c *rest.Config) *SamplecontrollerV1alpha1Client { return client } -// New creates a new SamplecontrollerV1alpha1Client for the given RESTClient. -func New(c rest.Interface) *SamplecontrollerV1alpha1Client { - return &SamplecontrollerV1alpha1Client{c} +// New creates a new ArgoprojV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *ArgoprojV1alpha1Client { + return &ArgoprojV1alpha1Client{c} } func setConfigDefaults(config *rest.Config) error { @@ -82,7 +82,7 @@ func setConfigDefaults(config *rest.Config) error { // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. -func (c *SamplecontrollerV1alpha1Client) RESTClient() rest.Interface { +func (c *ArgoprojV1alpha1Client) RESTClient() rest.Interface { if c == nil { return nil } diff --git a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake/fake_foo.go b/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake/fake_foo.go deleted file mode 100644 index adef937a8e..0000000000 --- a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/fake/fake_foo.go +++ /dev/null @@ -1,140 +0,0 @@ -/* -Copyright The Kubernetes Authors. - -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. -*/ - -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - labels "k8s.io/apimachinery/pkg/labels" - schema "k8s.io/apimachinery/pkg/runtime/schema" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - testing "k8s.io/client-go/testing" - v1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" -) - -// FakeFoos implements FooInterface -type FakeFoos struct { - Fake *FakeSamplecontrollerV1alpha1 - ns string -} - -var foosResource = schema.GroupVersionResource{Group: "samplecontroller.k8s.io", Version: "v1alpha1", Resource: "foos"} - -var foosKind = schema.GroupVersionKind{Group: "samplecontroller.k8s.io", Version: "v1alpha1", Kind: "Foo"} - -// Get takes name of the foo, and returns the corresponding foo object, and an error if there is any. -func (c *FakeFoos) Get(name string, options v1.GetOptions) (result *v1alpha1.Foo, err error) { - obj, err := c.Fake. - Invokes(testing.NewGetAction(foosResource, c.ns, name), &v1alpha1.Foo{}) - - if obj == nil { - return nil, err - } - return obj.(*v1alpha1.Foo), err -} - -// List takes label and field selectors, and returns the list of Foos that match those selectors. -func (c *FakeFoos) List(opts v1.ListOptions) (result *v1alpha1.FooList, err error) { - obj, err := c.Fake. - Invokes(testing.NewListAction(foosResource, foosKind, c.ns, opts), &v1alpha1.FooList{}) - - if obj == nil { - return nil, err - } - - label, _, _ := testing.ExtractFromListOptions(opts) - if label == nil { - label = labels.Everything() - } - list := &v1alpha1.FooList{ListMeta: obj.(*v1alpha1.FooList).ListMeta} - for _, item := range obj.(*v1alpha1.FooList).Items { - if label.Matches(labels.Set(item.Labels)) { - list.Items = append(list.Items, item) - } - } - return list, err -} - -// Watch returns a watch.Interface that watches the requested foos. -func (c *FakeFoos) Watch(opts v1.ListOptions) (watch.Interface, error) { - return c.Fake. - InvokesWatch(testing.NewWatchAction(foosResource, c.ns, opts)) - -} - -// Create takes the representation of a foo and creates it. Returns the server's representation of the foo, and an error, if there is any. -func (c *FakeFoos) Create(foo *v1alpha1.Foo) (result *v1alpha1.Foo, err error) { - obj, err := c.Fake. - Invokes(testing.NewCreateAction(foosResource, c.ns, foo), &v1alpha1.Foo{}) - - if obj == nil { - return nil, err - } - return obj.(*v1alpha1.Foo), err -} - -// Update takes the representation of a foo and updates it. Returns the server's representation of the foo, and an error, if there is any. -func (c *FakeFoos) Update(foo *v1alpha1.Foo) (result *v1alpha1.Foo, err error) { - obj, err := c.Fake. - Invokes(testing.NewUpdateAction(foosResource, c.ns, foo), &v1alpha1.Foo{}) - - if obj == nil { - return nil, err - } - return obj.(*v1alpha1.Foo), err -} - -// UpdateStatus was generated because the type contains a Status member. -// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). -func (c *FakeFoos) UpdateStatus(foo *v1alpha1.Foo) (*v1alpha1.Foo, error) { - obj, err := c.Fake. - Invokes(testing.NewUpdateSubresourceAction(foosResource, "status", c.ns, foo), &v1alpha1.Foo{}) - - if obj == nil { - return nil, err - } - return obj.(*v1alpha1.Foo), err -} - -// Delete takes name of the foo and deletes it. Returns an error if one occurs. -func (c *FakeFoos) Delete(name string, options *v1.DeleteOptions) error { - _, err := c.Fake. - Invokes(testing.NewDeleteAction(foosResource, c.ns, name), &v1alpha1.Foo{}) - - return err -} - -// DeleteCollection deletes a collection of objects. -func (c *FakeFoos) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { - action := testing.NewDeleteCollectionAction(foosResource, c.ns, listOptions) - - _, err := c.Fake.Invokes(action, &v1alpha1.FooList{}) - return err -} - -// Patch applies the patch and returns the patched foo. -func (c *FakeFoos) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Foo, err error) { - obj, err := c.Fake. - Invokes(testing.NewPatchSubresourceAction(foosResource, c.ns, name, pt, data, subresources...), &v1alpha1.Foo{}) - - if obj == nil { - return nil, err - } - return obj.(*v1alpha1.Foo), err -} diff --git a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/foo.go b/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/foo.go deleted file mode 100644 index 48bc2152f1..0000000000 --- a/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1/foo.go +++ /dev/null @@ -1,191 +0,0 @@ -/* -Copyright The Kubernetes Authors. - -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. -*/ - -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - "time" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - rest "k8s.io/client-go/rest" - v1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" - scheme "k8s.io/sample-controller/pkg/client/clientset/versioned/scheme" -) - -// FoosGetter has a method to return a FooInterface. -// A group's client should implement this interface. -type FoosGetter interface { - Foos(namespace string) FooInterface -} - -// FooInterface has methods to work with Foo resources. -type FooInterface interface { - Create(*v1alpha1.Foo) (*v1alpha1.Foo, error) - Update(*v1alpha1.Foo) (*v1alpha1.Foo, error) - UpdateStatus(*v1alpha1.Foo) (*v1alpha1.Foo, error) - Delete(name string, options *v1.DeleteOptions) error - DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error - Get(name string, options v1.GetOptions) (*v1alpha1.Foo, error) - List(opts v1.ListOptions) (*v1alpha1.FooList, error) - Watch(opts v1.ListOptions) (watch.Interface, error) - Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Foo, err error) - FooExpansion -} - -// foos implements FooInterface -type foos struct { - client rest.Interface - ns string -} - -// newFoos returns a Foos -func newFoos(c *SamplecontrollerV1alpha1Client, namespace string) *foos { - return &foos{ - client: c.RESTClient(), - ns: namespace, - } -} - -// Get takes name of the foo, and returns the corresponding foo object, and an error if there is any. -func (c *foos) Get(name string, options v1.GetOptions) (result *v1alpha1.Foo, err error) { - result = &v1alpha1.Foo{} - err = c.client.Get(). - Namespace(c.ns). - Resource("foos"). - Name(name). - VersionedParams(&options, scheme.ParameterCodec). - Do(). - Into(result) - return -} - -// List takes label and field selectors, and returns the list of Foos that match those selectors. -func (c *foos) List(opts v1.ListOptions) (result *v1alpha1.FooList, err error) { - var timeout time.Duration - if opts.TimeoutSeconds != nil { - timeout = time.Duration(*opts.TimeoutSeconds) * time.Second - } - result = &v1alpha1.FooList{} - err = c.client.Get(). - Namespace(c.ns). - Resource("foos"). - VersionedParams(&opts, scheme.ParameterCodec). - Timeout(timeout). - Do(). - Into(result) - return -} - -// Watch returns a watch.Interface that watches the requested foos. -func (c *foos) Watch(opts v1.ListOptions) (watch.Interface, error) { - var timeout time.Duration - if opts.TimeoutSeconds != nil { - timeout = time.Duration(*opts.TimeoutSeconds) * time.Second - } - opts.Watch = true - return c.client.Get(). - Namespace(c.ns). - Resource("foos"). - VersionedParams(&opts, scheme.ParameterCodec). - Timeout(timeout). - Watch() -} - -// Create takes the representation of a foo and creates it. Returns the server's representation of the foo, and an error, if there is any. -func (c *foos) Create(foo *v1alpha1.Foo) (result *v1alpha1.Foo, err error) { - result = &v1alpha1.Foo{} - err = c.client.Post(). - Namespace(c.ns). - Resource("foos"). - Body(foo). - Do(). - Into(result) - return -} - -// Update takes the representation of a foo and updates it. Returns the server's representation of the foo, and an error, if there is any. -func (c *foos) Update(foo *v1alpha1.Foo) (result *v1alpha1.Foo, err error) { - result = &v1alpha1.Foo{} - err = c.client.Put(). - Namespace(c.ns). - Resource("foos"). - Name(foo.Name). - Body(foo). - Do(). - Into(result) - return -} - -// UpdateStatus was generated because the type contains a Status member. -// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - -func (c *foos) UpdateStatus(foo *v1alpha1.Foo) (result *v1alpha1.Foo, err error) { - result = &v1alpha1.Foo{} - err = c.client.Put(). - Namespace(c.ns). - Resource("foos"). - Name(foo.Name). - SubResource("status"). - Body(foo). - Do(). - Into(result) - return -} - -// Delete takes name of the foo and deletes it. Returns an error if one occurs. -func (c *foos) Delete(name string, options *v1.DeleteOptions) error { - return c.client.Delete(). - Namespace(c.ns). - Resource("foos"). - Name(name). - Body(options). - Do(). - Error() -} - -// DeleteCollection deletes a collection of objects. -func (c *foos) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { - var timeout time.Duration - if listOptions.TimeoutSeconds != nil { - timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second - } - return c.client.Delete(). - Namespace(c.ns). - Resource("foos"). - VersionedParams(&listOptions, scheme.ParameterCodec). - Timeout(timeout). - Body(options). - Do(). - Error() -} - -// Patch applies the patch and returns the patched foo. -func (c *foos) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Foo, err error) { - result = &v1alpha1.Foo{} - err = c.client.Patch(pt). - Namespace(c.ns). - Resource("foos"). - SubResource(subresources...). - Name(name). - Body(data). - Do(). - Into(result) - return -} diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go index b6cf4fe8d7..ad38e29a88 100644 --- a/pkg/client/informers/externalversions/factory.go +++ b/pkg/client/informers/externalversions/factory.go @@ -23,13 +23,13 @@ import ( sync "sync" time "time" + versioned "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + internalinterfaces "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/internalinterfaces" + rollouts "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/rollouts" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" - versioned "k8s.io/sample-controller/pkg/client/clientset/versioned" - internalinterfaces "k8s.io/sample-controller/pkg/client/informers/externalversions/internalinterfaces" - samplecontroller "k8s.io/sample-controller/pkg/client/informers/externalversions/samplecontroller" ) // SharedInformerOption defines the functional option type for SharedInformerFactory. @@ -172,9 +172,9 @@ type SharedInformerFactory interface { ForResource(resource schema.GroupVersionResource) (GenericInformer, error) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool - Samplecontroller() samplecontroller.Interface + Argoproj() rollouts.Interface } -func (f *sharedInformerFactory) Samplecontroller() samplecontroller.Interface { - return samplecontroller.New(f, f.namespace, f.tweakListOptions) +func (f *sharedInformerFactory) Argoproj() rollouts.Interface { + return rollouts.New(f, f.namespace, f.tweakListOptions) } diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index ae44b489ec..125f947c30 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -21,9 +21,9 @@ package externalversions import ( "fmt" + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" - v1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" ) // GenericInformer is type of SharedIndexInformer which will locate and delegate to other @@ -52,9 +52,9 @@ func (f *genericInformer) Lister() cache.GenericLister { // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { - // Group=samplecontroller.k8s.io, Version=v1alpha1 - case v1alpha1.SchemeGroupVersion.WithResource("foos"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Samplecontroller().V1alpha1().Foos().Informer()}, nil + // Group=argoproj.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("rollouts"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Argoproj().V1alpha1().Rollouts().Informer()}, nil } diff --git a/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go index 8ab887c1d8..174588ef01 100644 --- a/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go +++ b/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -21,10 +21,10 @@ package internalinterfaces import ( time "time" + versioned "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" cache "k8s.io/client-go/tools/cache" - versioned "k8s.io/sample-controller/pkg/client/clientset/versioned" ) // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. diff --git a/pkg/client/informers/externalversions/samplecontroller/interface.go b/pkg/client/informers/externalversions/rollouts/interface.go similarity index 85% rename from pkg/client/informers/externalversions/samplecontroller/interface.go rename to pkg/client/informers/externalversions/rollouts/interface.go index 145651cc1f..484f79b75c 100644 --- a/pkg/client/informers/externalversions/samplecontroller/interface.go +++ b/pkg/client/informers/externalversions/rollouts/interface.go @@ -16,11 +16,11 @@ limitations under the License. // Code generated by informer-gen. DO NOT EDIT. -package samplecontroller +package argoproj import ( - internalinterfaces "k8s.io/sample-controller/pkg/client/informers/externalversions/internalinterfaces" - v1alpha1 "k8s.io/sample-controller/pkg/client/informers/externalversions/samplecontroller/v1alpha1" + internalinterfaces "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/rollouts/v1alpha1" ) // Interface provides access to each of this group's versions. diff --git a/pkg/client/informers/externalversions/samplecontroller/v1alpha1/interface.go b/pkg/client/informers/externalversions/rollouts/v1alpha1/interface.go similarity index 75% rename from pkg/client/informers/externalversions/samplecontroller/v1alpha1/interface.go rename to pkg/client/informers/externalversions/rollouts/v1alpha1/interface.go index 92ab70f7da..89a46a2402 100644 --- a/pkg/client/informers/externalversions/samplecontroller/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/rollouts/v1alpha1/interface.go @@ -19,13 +19,13 @@ limitations under the License. package v1alpha1 import ( - internalinterfaces "k8s.io/sample-controller/pkg/client/informers/externalversions/internalinterfaces" + internalinterfaces "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to all the informers in this group version. type Interface interface { - // Foos returns a FooInformer. - Foos() FooInformer + // Rollouts returns a RolloutInformer. + Rollouts() RolloutInformer } type version struct { @@ -39,7 +39,7 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } -// Foos returns a FooInformer. -func (v *version) Foos() FooInformer { - return &fooInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +// Rollouts returns a RolloutInformer. +func (v *version) Rollouts() RolloutInformer { + return &rolloutInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } diff --git a/pkg/client/informers/externalversions/rollouts/v1alpha1/rollout.go b/pkg/client/informers/externalversions/rollouts/v1alpha1/rollout.go new file mode 100644 index 0000000000..e40b46166d --- /dev/null +++ b/pkg/client/informers/externalversions/rollouts/v1alpha1/rollout.go @@ -0,0 +1,89 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" + + rolloutsv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + versioned "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + internalinterfaces "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/client/listers/rollouts/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// RolloutInformer provides access to a shared informer and lister for +// Rollouts. +type RolloutInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.RolloutLister +} + +type rolloutInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewRolloutInformer constructs a new informer for Rollout type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewRolloutInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredRolloutInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredRolloutInformer constructs a new informer for Rollout type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredRolloutInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ArgoprojV1alpha1().Rollouts(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ArgoprojV1alpha1().Rollouts(namespace).Watch(options) + }, + }, + &rolloutsv1alpha1.Rollout{}, + resyncPeriod, + indexers, + ) +} + +func (f *rolloutInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredRolloutInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *rolloutInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&rolloutsv1alpha1.Rollout{}, f.defaultInformer) +} + +func (f *rolloutInformer) Lister() v1alpha1.RolloutLister { + return v1alpha1.NewRolloutLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/samplecontroller/v1alpha1/foo.go b/pkg/client/informers/externalversions/samplecontroller/v1alpha1/foo.go deleted file mode 100644 index e9319290ab..0000000000 --- a/pkg/client/informers/externalversions/samplecontroller/v1alpha1/foo.go +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright The Kubernetes Authors. - -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. -*/ - -// Code generated by informer-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - time "time" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" - watch "k8s.io/apimachinery/pkg/watch" - cache "k8s.io/client-go/tools/cache" - samplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" - versioned "k8s.io/sample-controller/pkg/client/clientset/versioned" - internalinterfaces "k8s.io/sample-controller/pkg/client/informers/externalversions/internalinterfaces" - v1alpha1 "k8s.io/sample-controller/pkg/client/listers/samplecontroller/v1alpha1" -) - -// FooInformer provides access to a shared informer and lister for -// Foos. -type FooInformer interface { - Informer() cache.SharedIndexInformer - Lister() v1alpha1.FooLister -} - -type fooInformer struct { - factory internalinterfaces.SharedInformerFactory - tweakListOptions internalinterfaces.TweakListOptionsFunc - namespace string -} - -// NewFooInformer constructs a new informer for Foo type. -// Always prefer using an informer factory to get a shared informer instead of getting an independent -// one. This reduces memory footprint and number of connections to the server. -func NewFooInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredFooInformer(client, namespace, resyncPeriod, indexers, nil) -} - -// NewFilteredFooInformer constructs a new informer for Foo type. -// Always prefer using an informer factory to get a shared informer instead of getting an independent -// one. This reduces memory footprint and number of connections to the server. -func NewFilteredFooInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options v1.ListOptions) (runtime.Object, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.SamplecontrollerV1alpha1().Foos(namespace).List(options) - }, - WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { - if tweakListOptions != nil { - tweakListOptions(&options) - } - return client.SamplecontrollerV1alpha1().Foos(namespace).Watch(options) - }, - }, - &samplecontrollerv1alpha1.Foo{}, - resyncPeriod, - indexers, - ) -} - -func (f *fooInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredFooInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) -} - -func (f *fooInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&samplecontrollerv1alpha1.Foo{}, f.defaultInformer) -} - -func (f *fooInformer) Lister() v1alpha1.FooLister { - return v1alpha1.NewFooLister(f.Informer().GetIndexer()) -} diff --git a/pkg/client/listers/samplecontroller/v1alpha1/expansion_generated.go b/pkg/client/listers/rollouts/v1alpha1/expansion_generated.go similarity index 70% rename from pkg/client/listers/samplecontroller/v1alpha1/expansion_generated.go rename to pkg/client/listers/rollouts/v1alpha1/expansion_generated.go index 9a34636b09..101bad8056 100644 --- a/pkg/client/listers/samplecontroller/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/rollouts/v1alpha1/expansion_generated.go @@ -18,10 +18,10 @@ limitations under the License. package v1alpha1 -// FooListerExpansion allows custom methods to be added to -// FooLister. -type FooListerExpansion interface{} +// RolloutListerExpansion allows custom methods to be added to +// RolloutLister. +type RolloutListerExpansion interface{} -// FooNamespaceListerExpansion allows custom methods to be added to -// FooNamespaceLister. -type FooNamespaceListerExpansion interface{} +// RolloutNamespaceListerExpansion allows custom methods to be added to +// RolloutNamespaceLister. +type RolloutNamespaceListerExpansion interface{} diff --git a/pkg/client/listers/rollouts/v1alpha1/rollout.go b/pkg/client/listers/rollouts/v1alpha1/rollout.go new file mode 100644 index 0000000000..aceca80387 --- /dev/null +++ b/pkg/client/listers/rollouts/v1alpha1/rollout.go @@ -0,0 +1,94 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// RolloutLister helps list Rollouts. +type RolloutLister interface { + // List lists all Rollouts in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.Rollout, err error) + // Rollouts returns an object that can list and get Rollouts. + Rollouts(namespace string) RolloutNamespaceLister + RolloutListerExpansion +} + +// rolloutLister implements the RolloutLister interface. +type rolloutLister struct { + indexer cache.Indexer +} + +// NewRolloutLister returns a new RolloutLister. +func NewRolloutLister(indexer cache.Indexer) RolloutLister { + return &rolloutLister{indexer: indexer} +} + +// List lists all Rollouts in the indexer. +func (s *rolloutLister) List(selector labels.Selector) (ret []*v1alpha1.Rollout, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Rollout)) + }) + return ret, err +} + +// Rollouts returns an object that can list and get Rollouts. +func (s *rolloutLister) Rollouts(namespace string) RolloutNamespaceLister { + return rolloutNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// RolloutNamespaceLister helps list and get Rollouts. +type RolloutNamespaceLister interface { + // List lists all Rollouts in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.Rollout, err error) + // Get retrieves the Rollout from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.Rollout, error) + RolloutNamespaceListerExpansion +} + +// rolloutNamespaceLister implements the RolloutNamespaceLister +// interface. +type rolloutNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Rollouts in the indexer for a given namespace. +func (s rolloutNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Rollout, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Rollout)) + }) + return ret, err +} + +// Get retrieves the Rollout from the indexer for a given namespace and name. +func (s rolloutNamespaceLister) Get(name string) (*v1alpha1.Rollout, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("rollout"), name) + } + return obj.(*v1alpha1.Rollout), nil +} diff --git a/pkg/client/listers/samplecontroller/v1alpha1/foo.go b/pkg/client/listers/samplecontroller/v1alpha1/foo.go deleted file mode 100644 index a6cbf1663c..0000000000 --- a/pkg/client/listers/samplecontroller/v1alpha1/foo.go +++ /dev/null @@ -1,94 +0,0 @@ -/* -Copyright The Kubernetes Authors. - -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. -*/ - -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/client-go/tools/cache" - v1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1" -) - -// FooLister helps list Foos. -type FooLister interface { - // List lists all Foos in the indexer. - List(selector labels.Selector) (ret []*v1alpha1.Foo, err error) - // Foos returns an object that can list and get Foos. - Foos(namespace string) FooNamespaceLister - FooListerExpansion -} - -// fooLister implements the FooLister interface. -type fooLister struct { - indexer cache.Indexer -} - -// NewFooLister returns a new FooLister. -func NewFooLister(indexer cache.Indexer) FooLister { - return &fooLister{indexer: indexer} -} - -// List lists all Foos in the indexer. -func (s *fooLister) List(selector labels.Selector) (ret []*v1alpha1.Foo, err error) { - err = cache.ListAll(s.indexer, selector, func(m interface{}) { - ret = append(ret, m.(*v1alpha1.Foo)) - }) - return ret, err -} - -// Foos returns an object that can list and get Foos. -func (s *fooLister) Foos(namespace string) FooNamespaceLister { - return fooNamespaceLister{indexer: s.indexer, namespace: namespace} -} - -// FooNamespaceLister helps list and get Foos. -type FooNamespaceLister interface { - // List lists all Foos in the indexer for a given namespace. - List(selector labels.Selector) (ret []*v1alpha1.Foo, err error) - // Get retrieves the Foo from the indexer for a given namespace and name. - Get(name string) (*v1alpha1.Foo, error) - FooNamespaceListerExpansion -} - -// fooNamespaceLister implements the FooNamespaceLister -// interface. -type fooNamespaceLister struct { - indexer cache.Indexer - namespace string -} - -// List lists all Foos in the indexer for a given namespace. -func (s fooNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Foo, err error) { - err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { - ret = append(ret, m.(*v1alpha1.Foo)) - }) - return ret, err -} - -// Get retrieves the Foo from the indexer for a given namespace and name. -func (s fooNamespaceLister) Get(name string) (*v1alpha1.Foo, error) { - obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) - if err != nil { - return nil, err - } - if !exists { - return nil, errors.NewNotFound(v1alpha1.Resource("foo"), name) - } - return obj.(*v1alpha1.Foo), nil -} diff --git a/pkg/signals/signal.go b/pkg/signals/signal.go index 6bddfddb4f..f82aafbbd2 100644 --- a/pkg/signals/signal.go +++ b/pkg/signals/signal.go @@ -1,19 +1,3 @@ -/* -Copyright 2017 The Kubernetes Authors. - -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 signals import ( diff --git a/pkg/signals/signal_posix.go b/pkg/signals/signal_posix.go index 9bdb4e7418..808c4489ee 100644 --- a/pkg/signals/signal_posix.go +++ b/pkg/signals/signal_posix.go @@ -1,21 +1,5 @@ // +build !windows -/* -Copyright 2017 The Kubernetes Authors. - -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 signals import ( diff --git a/utils/annotations/annotations.go b/utils/annotations/annotations.go new file mode 100644 index 0000000000..7857b1fe4f --- /dev/null +++ b/utils/annotations/annotations.go @@ -0,0 +1,186 @@ +package annotations + +import ( + "fmt" + "strconv" + "strings" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/klog" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" +) + +const ( + // RolloutLabel key value for the label in the annotations and selector + RolloutLabel = "rollout.argoproj.io" + // RevisionAnnotation is the revision annotation of a rollout's replica sets which records its rollout sequence + RevisionAnnotation = RolloutLabel + "/revision" + // RevisionHistoryAnnotation maintains the history of all old revisions that a replica set has served for a rollout. + RevisionHistoryAnnotation = RolloutLabel + "/revision-history" + // DesiredReplicasAnnotation is the desired replicas for a rollout recorded as an annotation + // in its replica sets. Helps in separating scaling events from the rollout process and for + // determining if the new replica set for a rollout is really saturated. + DesiredReplicasAnnotation = RolloutLabel + "/desired-replicas" +) + +// GetDesiredReplicasAnnotation returns the number of desired replicas +func GetDesiredReplicasAnnotation(rs *appsv1.ReplicaSet) (int32, bool) { + return getIntFromAnnotation(rs, DesiredReplicasAnnotation) +} + +func getIntFromAnnotation(rs *appsv1.ReplicaSet, annotationKey string) (int32, bool) { + annotationValue, ok := rs.Annotations[annotationKey] + if !ok { + return int32(0), false + } + intValue, err := strconv.Atoi(annotationValue) + if err != nil { + klog.V(2).Infof("Cannot convert the value %q with annotation key %q for the replica set %q", annotationValue, annotationKey, rs.Name) + return int32(0), false + } + return int32(intValue), true +} + +// SetRolloutRevision updates the revision for a rollout. +func SetRolloutRevision(rollout *v1alpha1.Rollout, revision string) bool { + if rollout.Annotations == nil { + rollout.Annotations = make(map[string]string) + } + if rollout.Annotations[RevisionAnnotation] != revision { + rollout.Annotations[RevisionAnnotation] = revision + return true + } + return false +} + +// SetReplicasAnnotations sets the desiredReplicas into the annotations +func SetReplicasAnnotations(rs *appsv1.ReplicaSet, desiredReplicas int32) bool { + if rs.Annotations == nil { + rs.Annotations = make(map[string]string) + } + desiredString := fmt.Sprintf("%d", desiredReplicas) + if hasString := rs.Annotations[DesiredReplicasAnnotation]; hasString != desiredString { + rs.Annotations[DesiredReplicasAnnotation] = desiredString + return true + } + return false +} + +// ReplicasAnnotationsNeedUpdate return true if ReplicasAnnotations need to be updated +func ReplicasAnnotationsNeedUpdate(rs *appsv1.ReplicaSet, desiredReplicas int32) bool { + if rs.Annotations == nil { + return true + } + desiredString := fmt.Sprintf("%d", desiredReplicas) + if hasString := rs.Annotations[DesiredReplicasAnnotation]; hasString != desiredString { + return true + } + return false +} + +// SetNewReplicaSetAnnotations sets new replica set's annotations appropriately by updating its revision and +// copying required rollout annotations to it; it returns true if replica set's annotation is changed. +func SetNewReplicaSetAnnotations(rollout *v1alpha1.Rollout, newRS *appsv1.ReplicaSet, newRevision string, exists bool) bool { + // First, copy rollout's annotations (except for apply and revision annotations) + annotationChanged := copyRolloutAnnotationsToReplicaSet(rollout, newRS) + // Then, update replica set's revision annotation + if newRS.Annotations == nil { + newRS.Annotations = make(map[string]string) + } + oldRevision, ok := newRS.Annotations[RevisionAnnotation] + // The newRS's revision should be the greatest among all RSes. Usually, its revision number is newRevision (the max revision number + // of all old RSes + 1). However, it's possible that some of the old RSes are deleted after the newRS revision being updated, and + // newRevision becomes smaller than newRS's revision. We should only update newRS revision when it's smaller than newRevision. + + oldRevisionInt, err := strconv.ParseInt(oldRevision, 10, 64) + if err != nil { + if oldRevision != "" { + klog.Warningf("Updating replica set revision OldRevision not int %s", err) + return false + } + //If the RS annotation is empty then initialise it to 0 + oldRevisionInt = 0 + } + newRevisionInt, err := strconv.ParseInt(newRevision, 10, 64) + if err != nil { + klog.Warningf("Updating replica set revision NewRevision not int %s", err) + return false + } + if oldRevisionInt < newRevisionInt { + newRS.Annotations[RevisionAnnotation] = newRevision + annotationChanged = true + klog.V(4).Infof("Updating replica set %q revision to %s", newRS.Name, newRevision) + } + // If a revision annotation already existed and this replica set was updated with a new revision + // then that means we are rolling back to this replica set. We need to preserve the old revisions + // for historical information. + if ok && annotationChanged { + revisionHistoryAnnotation := newRS.Annotations[RevisionHistoryAnnotation] + oldRevisions := strings.Split(revisionHistoryAnnotation, ",") + if len(oldRevisions[0]) == 0 { + newRS.Annotations[RevisionHistoryAnnotation] = oldRevision + } else { + oldRevisions = append(oldRevisions, oldRevision) + newRS.Annotations[RevisionHistoryAnnotation] = strings.Join(oldRevisions, ",") + } + } + // If the new replica set is about to be created, we need to add replica annotations to it. + //TODO: look at implementation due to surge + if !exists && SetReplicasAnnotations(newRS, *(rollout.Spec.Replicas)) { + annotationChanged = true + } + return annotationChanged +} + +var annotationsToSkip = map[string]bool{ + corev1.LastAppliedConfigAnnotation: true, + RevisionAnnotation: true, + RevisionHistoryAnnotation: true, + DesiredReplicasAnnotation: true, +} + +// skipCopyAnnotation returns true if we should skip copying the annotation with the given annotation key +func skipCopyAnnotation(key string) bool { + return annotationsToSkip[key] +} + +// copyRolloutAnnotationsToReplicaSet copies rollout's annotations to replica set's annotations, +// and returns true if replica set's annotation is changed. +// Note that apply and revision annotations are not copied. +func copyRolloutAnnotationsToReplicaSet(rollouts *v1alpha1.Rollout, rs *appsv1.ReplicaSet) bool { + rsAnnotationsChanged := false + if rs.Annotations == nil { + rs.Annotations = make(map[string]string) + } + for k, v := range rollouts.Annotations { + // newRS revision is updated automatically in getNewReplicaSet, and the rollout's revision number is then updated + // by copying its newRS revision number. We should not copy rollout's revision to its newRS, since the update of + // rollout revision number may fail (revision becomes stale) and the revision number in newRS is more reliable. + if skipCopyAnnotation(k) || rs.Annotations[k] == v { + continue + } + rs.Annotations[k] = v + rsAnnotationsChanged = true + } + return rsAnnotationsChanged +} + +// IsSaturated checks if the new replica set is saturated by comparing its size with its rollout size. +// Both the rollout and the replica set have to believe this replica set can own all of the desired +// replicas in the rollout and the annotation helps in achieving that. All pods of the ReplicaSet +// need to be available. +func IsSaturated(rollout *v1alpha1.Rollout, rs *appsv1.ReplicaSet) bool { + if rs == nil { + return false + } + desiredString := rs.Annotations[DesiredReplicasAnnotation] + desired, err := strconv.Atoi(desiredString) + if err != nil { + return false + } + return *(rs.Spec.Replicas) == *(rollout.Spec.Replicas) && + int32(desired) == *(rollout.Spec.Replicas) && + rs.Status.AvailableReplicas == *(rollout.Spec.Replicas) +} diff --git a/utils/annotations/annotations_test.go b/utils/annotations/annotations_test.go new file mode 100644 index 0000000000..9cd1ea2bcc --- /dev/null +++ b/utils/annotations/annotations_test.go @@ -0,0 +1,307 @@ +package annotations + +import ( + "fmt" + "math/rand" + "strconv" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/storage/names" + + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" +) + +func newROControllerRef(r *v1alpha1.Rollout) *metav1.OwnerReference { + isController := true + return &metav1.OwnerReference{ + APIVersion: "argoproj.io/v1alpha1", + Kind: "Rollouts", + Name: r.GetName(), + UID: r.GetUID(), + Controller: &isController, + } +} + +// generateRS creates a replica set, with the input rollout's template as its template +func generateRS(r *v1alpha1.Rollout) appsv1.ReplicaSet { + template := r.Spec.Template.DeepCopy() + return appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + UID: randomUID(), + Name: names.SimpleNameGenerator.GenerateName("replicaset"), + Labels: template.Labels, + OwnerReferences: []metav1.OwnerReference{*newROControllerRef(r)}, + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: new(int32), + Template: *template, + Selector: &metav1.LabelSelector{MatchLabels: template.Labels}, + }, + } +} + +func randomUID() types.UID { + return types.UID(strconv.FormatInt(rand.Int63(), 10)) +} + +// generateRollout creates a rollout, with the input image as its template +func generateRollout(image string) v1alpha1.Rollout { + podLabels := map[string]string{"name": image} + terminationSec := int64(30) + return v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: image, + Annotations: make(map[string]string), + }, + Spec: v1alpha1.RolloutSpec{ + Replicas: func(i int32) *int32 { return &i }(1), + Selector: &metav1.LabelSelector{MatchLabels: podLabels}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: podLabels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: image, + Image: image, + ImagePullPolicy: corev1.PullAlways, + TerminationMessagePath: corev1.TerminationMessagePathDefault, + }, + }, + DNSPolicy: corev1.DNSClusterFirst, + TerminationGracePeriodSeconds: &terminationSec, + RestartPolicy: corev1.RestartPolicyAlways, + SecurityContext: &corev1.PodSecurityContext{}, + }, + }, + }, + } +} + +func TestAnnotationUtils(t *testing.T) { + + //Setup + tRollout := generateRollout("nginx") + tRS := generateRS(&tRollout) + tRollout.Annotations[RevisionAnnotation] = "1" + + //Test Case 0: Check if revision anotations can be set + t.Run("SetRolloutRevision", func(t *testing.T) { + copyRollout := tRollout.DeepCopy() + updated := SetRolloutRevision(copyRollout, "2") + if !updated { + t.Errorf("SetRolloutRevision() Expected=True Obtained=False") + } + if copyRollout.Annotations[RevisionAnnotation] != "2" { + t.Errorf("Revision Expected=2 Obtained=%s", copyRollout.Annotations[RevisionAnnotation]) + } + }) + t.Run("SetRolloutRevisionNoAnnotations", func(t *testing.T) { + copyRollout := tRollout.DeepCopy() + copyRollout.Labels = nil + updated := SetRolloutRevision(copyRollout, "2") + if !updated { + t.Errorf("SetRolloutRevision() Expected=True Obtained=False") + } + if copyRollout.Annotations[RevisionAnnotation] != "2" { + t.Errorf("Revision Expected=2 Obtained=%s", copyRollout.Annotations[RevisionAnnotation]) + } + }) + + t.Run("SetRolloutRevisionAlreadySet", func(t *testing.T) { + copyRollout := tRollout.DeepCopy() + copyRollout.Labels = map[string]string{RevisionAnnotation: "2"} + updated := SetRolloutRevision(copyRollout, "2") + if !updated { + t.Errorf("SetRolloutRevision() Expected=False Obtained=True") + } + if copyRollout.Annotations[RevisionAnnotation] != "2" { + t.Errorf("Revision Expected=2 Obtained=%s", copyRollout.Annotations[RevisionAnnotation]) + } + }) + + //Test Case 1: Check if anotations are copied properly from rollout to RS + t.Run("SetNewReplicaSetAnnotations", func(t *testing.T) { + //Try to set the increment revision from 1 through 20 + for i := 0; i < 20; i++ { + + nextRevision := fmt.Sprintf("%d", i+1) + SetNewReplicaSetAnnotations(&tRollout, &tRS, nextRevision, true) + //Now the ReplicaSets Revision Annotation should be i+1 + + if tRS.Annotations[RevisionAnnotation] != nextRevision { + t.Errorf("Revision Expected=%s Obtained=%s", nextRevision, tRS.Annotations[RevisionAnnotation]) + } + } + }) + + //Test Case 2: Check if annotations are set properly + t.Run("SetReplicasAnnotations", func(t *testing.T) { + copyRS := tRS.DeepCopy() + updated := SetReplicasAnnotations(copyRS, 10) + if !updated { + t.Errorf("SetReplicasAnnotations() failed") + } + value, ok := copyRS.Annotations[DesiredReplicasAnnotation] + if !ok { + t.Errorf("SetReplicasAnnotations did not set DesiredReplicasAnnotation") + } + if value != "10" { + t.Errorf("SetReplicasAnnotations did not set DesiredReplicasAnnotation correctly value=%s", value) + } + }) + + t.Run("SetReplicasAnnotationsNoLabels", func(t *testing.T) { + copyRS := tRS.DeepCopy() + copyRS.Labels = nil + updated := SetReplicasAnnotations(copyRS, 10) + if !updated { + t.Errorf("SetReplicasAnnotations() failed") + } + value, ok := copyRS.Annotations[DesiredReplicasAnnotation] + if !ok { + t.Errorf("SetReplicasAnnotations did not set DesiredReplicasAnnotation") + } + if value != "10" { + t.Errorf("SetReplicasAnnotations did not set DesiredReplicasAnnotation correctly value=%s", value) + } + }) + + t.Run("SetReplicasAnnotationsNoChanges", func(t *testing.T) { + copyRS := tRS.DeepCopy() + copyRS.Annotations[DesiredReplicasAnnotation] = "10" + updated := SetReplicasAnnotations(copyRS, 10) + if updated { + t.Errorf("SetReplicasAnnotations() make no changes") + } + value, ok := copyRS.Annotations[DesiredReplicasAnnotation] + if !ok { + t.Errorf("SetReplicasAnnotations did not set DesiredReplicasAnnotation") + } + if value != "10" { + t.Errorf("SetReplicasAnnotations did not set DesiredReplicasAnnotation correctly value=%s", value) + } + }) + + //Test Case 3: Check if we can grab annotations from Replica Set + tRS.Annotations[DesiredReplicasAnnotation] = "1" + t.Run("GetDesiredReplicasAnnotation", func(t *testing.T) { + desired, ok := GetDesiredReplicasAnnotation(&tRS) + if !ok { + t.Errorf("GetDesiredReplicasAnnotation Expected=true Obtained=false") + } + if desired != 1 { + t.Errorf("GetDesiredReplicasAnnotation Expected=1 Obtained=%d", desired) + } + }) + + tRS.Annotations[DesiredReplicasAnnotation] = "Not a number" + t.Run("GetDesiredReplicasAnnotationInvalidLabel", func(t *testing.T) { + _, ok := GetDesiredReplicasAnnotation(&tRS) + if ok { + t.Errorf("IsSaturated Expected=false Obtained=true") + } + }) + + copyRS := tRS.DeepCopy() + copyRS.Annotations = nil + t.Run("GetDesiredReplicasAnnotationNoAnnotations", func(t *testing.T) { + _, ok := GetDesiredReplicasAnnotation(copyRS) + if ok { + t.Errorf("IsSaturated Expected=false Obtained=true") + } + }) + + //Test Case 4: Check if annotations reflect rollouts state + tRS.Annotations[DesiredReplicasAnnotation] = "1" + tRS.Status.AvailableReplicas = 1 + tRS.Spec.Replicas = new(int32) + *tRS.Spec.Replicas = 1 + + t.Run("IsSaturated", func(t *testing.T) { + saturated := IsSaturated(&tRollout, &tRS) + if !saturated { + t.Errorf("IsSaturated Expected=true Obtained=false") + } + }) + + t.Run("IsSaturatedFalseNoRS", func(t *testing.T) { + saturated := IsSaturated(&tRollout, nil) + if saturated { + t.Errorf("IsSaturated Expected=false Obtained=true") + } + }) + + t.Run("IsSaturatedFalseInvalidLabel", func(t *testing.T) { + tRS.Annotations[DesiredReplicasAnnotation] = "Not a number" + saturated := IsSaturated(&tRollout, &tRS) + if saturated { + t.Errorf("IsSaturated Expected=false Obtained=true") + } + }) + + //Tear Down +} + +func TestReplicasAnnotationsNeedUpdate(t *testing.T) { + + desiredReplicas := fmt.Sprintf("%d", int32(10)) + tests := []struct { + name string + replicaSet *appsv1.ReplicaSet + expected bool + }{ + { + name: "test Annotations nil", + replicaSet: &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: appsv1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + expected: true, + }, + { + name: "test desiredReplicas update", + replicaSet: &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hello", + Namespace: "test", + Annotations: map[string]string{DesiredReplicasAnnotation: "8"}, + }, + Spec: appsv1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + expected: true, + }, + { + name: "test needn't update", + replicaSet: &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hello", + Namespace: "test", + Annotations: map[string]string{DesiredReplicasAnnotation: desiredReplicas}, + }, + Spec: appsv1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + expected: false, + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := ReplicasAnnotationsNeedUpdate(test.replicaSet, 10) + if result != test.expected { + t.Errorf("case[%d]:%s Expected %v, Got: %v", i, test.name, test.expected, result) + } + }) + } +} diff --git a/utils/conditions/conditions.go b/utils/conditions/conditions.go new file mode 100644 index 0000000000..bb7a80a37e --- /dev/null +++ b/utils/conditions/conditions.go @@ -0,0 +1,6 @@ +package conditions + +const ( + // FailedRSCreateReason is added in a rollout when it cannot create a new replica set. + FailedRSCreateReason = "ReplicaSetCreateError" +) diff --git a/utils/replicaset/replicaset.go b/utils/replicaset/replicaset.go new file mode 100644 index 0000000000..608edd0de9 --- /dev/null +++ b/utils/replicaset/replicaset.go @@ -0,0 +1,129 @@ +package replicaset + +import ( + "fmt" + "sort" + "strconv" + + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog" + "k8s.io/kubernetes/pkg/controller" + + v1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/utils/annotations" +) + +// FindNewReplicaSet returns the new RS this given rollout targets. +func FindNewReplicaSet(rollout *v1alpha1.Rollout, rsList []*appsv1.ReplicaSet) *appsv1.ReplicaSet { + sort.Sort(controller.ReplicaSetsByCreationTimestamp(rsList)) + replicaSetName := fmt.Sprintf("%s-%s", rollout.Name, controller.ComputeHash(&rollout.Spec.Template, rollout.Status.CollisionCount)) + for i := range rsList { + if rsList[i].Name == replicaSetName { + return rsList[i] + } + } + // new ReplicaSet does not exist. + return nil +} + +// FindOldReplicaSets returns the old replica sets targeted by the given Rollout, with the given slice of RSes. +func FindOldReplicaSets(rollout *v1alpha1.Rollout, rsList []*appsv1.ReplicaSet) []*appsv1.ReplicaSet { + var allRSs []*appsv1.ReplicaSet + newRS := FindNewReplicaSet(rollout, rsList) + for _, rs := range rsList { + // Filter out new replica set + if newRS != nil && rs.UID == newRS.UID { + continue + } + allRSs = append(allRSs, rs) + } + return allRSs +} + +// NewRSNewReplicas calculates the number of replicas a Rollout's new RS should have. +// When one of the followings is true, we're rolling out the deployment; otherwise, we're scaling it. +// 1) The new RS is saturated: newRS's replicas == deployment's replicas +// 2) Max number of pods allowed is reached: deployment's replicas + maxSurge == all RSs' replicas +func NewRSNewReplicas(rollout *v1alpha1.Rollout, allRSs []*appsv1.ReplicaSet, newRS *appsv1.ReplicaSet) (int32, error) { + switch rollout.Spec.Strategy.Type { + case v1alpha1.BlueGreenRolloutStrategyType: + return *(rollout.Spec.Replicas), nil + default: + return 0, fmt.Errorf("rollout strategy type %v isn't supported", rollout.Spec.Strategy.Type) + } +} + +// MaxRevision finds the highest revision in the replica sets +func MaxRevision(allRSs []*appsv1.ReplicaSet) int64 { + max := int64(0) + for _, rs := range allRSs { + if v, err := Revision(rs); err != nil { + // Skip the replica sets when it failed to parse their revision information + klog.V(4).Infof("Error: %v. Couldn't parse revision for replica set %#v, deployment controller will skip it when reconciling revisions.", err, rs) + } else if v > max { + max = v + } + } + return max +} + +// Revision returns the revision number of the input object. +func Revision(obj runtime.Object) (int64, error) { + acc, err := meta.Accessor(obj) + if err != nil { + return 0, err + } + v, ok := acc.GetAnnotations()[annotations.RevisionAnnotation] + if !ok { + return 0, nil + } + return strconv.ParseInt(v, 10, 64) +} + +// FindActiveOrLatest returns the only active or the latest replica set in case there is at most one active +// replica set. If there are more active replica sets, then we should proportionally scale them. +func FindActiveOrLatest(newRS *appsv1.ReplicaSet, oldRSs []*appsv1.ReplicaSet) *appsv1.ReplicaSet { + if newRS == nil && len(oldRSs) == 0 { + return nil + } + + sort.Sort(sort.Reverse(controller.ReplicaSetsByCreationTimestamp(oldRSs))) + allRSs := controller.FilterActiveReplicaSets(append(oldRSs, newRS)) + + switch len(allRSs) { + case 0: + // If there is no active replica set then we should return the newest. + if newRS != nil { + return newRS + } + return oldRSs[0] + case 1: + return allRSs[0] + default: + return nil + } +} + +// GetAvailableReplicaCountForReplicaSets returns the number of available pods corresponding to the given replica sets. +func GetAvailableReplicaCountForReplicaSets(replicaSets []*appsv1.ReplicaSet) int32 { + totalAvailableReplicas := int32(0) + for _, rs := range replicaSets { + if rs != nil { + totalAvailableReplicas += rs.Status.AvailableReplicas + } + } + return totalAvailableReplicas +} + +// GetReplicaCountForReplicaSets returns the sum of Replicas of the given replica sets. +func GetReplicaCountForReplicaSets(replicaSets []*appsv1.ReplicaSet) int32 { + totalReplicas := int32(0) + for _, rs := range replicaSets { + if rs != nil { + totalReplicas += *(rs.Spec.Replicas) + } + } + return totalReplicas +} diff --git a/utils/replicaset/replicaset_test.go b/utils/replicaset/replicaset_test.go new file mode 100644 index 0000000000..56be821a78 --- /dev/null +++ b/utils/replicaset/replicaset_test.go @@ -0,0 +1,161 @@ +package replicaset + +import ( + "fmt" + "reflect" + "sort" + "testing" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/kubernetes/pkg/controller" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" +) + +// generateRollout creates a rollout, with the input image as its template +func generateRollout(image string) v1alpha1.Rollout { + podLabels := map[string]string{"name": image} + return v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: image, + Annotations: make(map[string]string), + }, + Spec: v1alpha1.RolloutSpec{ + Replicas: func(i int32) *int32 { return &i }(1), + Selector: &metav1.LabelSelector{MatchLabels: podLabels}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: podLabels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: image, + Image: image, + ImagePullPolicy: corev1.PullAlways, + TerminationMessagePath: corev1.TerminationMessagePathDefault, + }, + }, + }, + }, + }, + } +} + +// generateRS creates a replica set, with the input rollout's template as its template +func generateRS(rollout v1alpha1.Rollout) appsv1.ReplicaSet { + template := rollout.Spec.Template.DeepCopy() + return appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + UID: uuid.NewUUID(), + Name: fmt.Sprintf("%s-%s", rollout.Name, controller.ComputeHash(&rollout.Spec.Template, nil)), + Labels: template.Labels, + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: new(int32), + Template: *template, + Selector: &metav1.LabelSelector{MatchLabels: template.Labels}, + }, + } +} + +func TestFindOldReplicaSets(t *testing.T) { + now := metav1.Now() + before := metav1.Time{Time: now.Add(-time.Minute)} + + rollout := generateRollout("nginx") + newRS := generateRS(rollout) + *(newRS.Spec.Replicas) = 1 + newRS.Labels[v1alpha1.DefaultRolloutUniqueLabelKey] = "hash" + newRS.CreationTimestamp = now + + oldRollout := generateRollout("nginx") + oldRollout.Spec.Template.Spec.Containers[0].Name = "nginx-old-1" + oldRS := generateRS(oldRollout) + oldRS.Status.FullyLabeledReplicas = *(oldRS.Spec.Replicas) + oldRS.CreationTimestamp = before + + tests := []struct { + Name string + rollout v1alpha1.Rollout + rsList []*appsv1.ReplicaSet + expected []*appsv1.ReplicaSet + }{ + { + Name: "Get old ReplicaSets", + rollout: rollout, + rsList: []*appsv1.ReplicaSet{&newRS, &oldRS}, + expected: []*appsv1.ReplicaSet{&oldRS}, + }, + { + Name: "Get old ReplicaSets with no new ReplicaSet", + rollout: rollout, + rsList: []*appsv1.ReplicaSet{&oldRS}, + expected: []*appsv1.ReplicaSet{&oldRS}, + }, + { + Name: "Get empty old ReplicaSets", + rollout: rollout, + rsList: []*appsv1.ReplicaSet{&newRS}, + expected: nil, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + allRS := FindOldReplicaSets(&test.rollout, test.rsList) + sort.Sort(controller.ReplicaSetsByCreationTimestamp(allRS)) + sort.Sort(controller.ReplicaSetsByCreationTimestamp(test.expected)) + if !reflect.DeepEqual(allRS, test.expected) { + t.Errorf("In test case %q, expected %#v, got %#v", test.Name, test.expected, allRS) + } + }) + } +} + +func TestGetReplicaCountForReplicaSets(t *testing.T) { + rs1 := generateRS(generateRollout("foo")) + *(rs1.Spec.Replicas) = 1 + rs1.Status.AvailableReplicas = 2 + rs2 := generateRS(generateRollout("bar")) + *(rs2.Spec.Replicas) = 2 + rs2.Status.AvailableReplicas = 3 + + tests := []struct { + Name string + sets []*appsv1.ReplicaSet + expectedCount int32 + expectedActualAvailable int32 + }{ + { + "1:2 Replicas", + []*appsv1.ReplicaSet{&rs1}, + 1, + 2, + }, + { + "3:5 Replicas", + []*appsv1.ReplicaSet{&rs1, &rs2}, + 3, + 5, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + rs := GetReplicaCountForReplicaSets(test.sets) + if rs != test.expectedCount { + t.Errorf("In test case %s, expectedCount %+v, got %+v", test.Name, test.expectedCount, rs) + } + rs = GetAvailableReplicaCountForReplicaSets(test.sets) + if rs != test.expectedActualAvailable { + t.Errorf("In test case %s, expectedActual %+v, got %+v", test.Name, test.expectedActualAvailable, rs) + } + }) + } +} +