├── docs ├── install │ ├── olm.md │ └── kustomize.md ├── assets │ └── logo.png ├── requirements.txt ├── index.md ├── e2e-tests │ └── usage.md ├── usage │ └── getting_started.md ├── developer-guide │ └── developer_guide.md └── crd_reference.md ├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── scorecard │ ├── bases │ │ └── config.yaml │ ├── patches │ │ ├── basic.config.yaml │ │ └── olm.config.yaml │ └── kustomization.yaml ├── samples │ ├── kustomization.yaml │ └── argoproj.io_v1alpha1_rolloutmanager.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_argorollouts.yaml │ │ └── webhook_in_argorollouts.yaml │ ├── kustomizeconfig.yaml │ └── kustomization.yaml ├── rbac │ ├── service_account.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── argorollouts_viewer_role.yaml │ ├── argorollouts_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml └── manifests │ ├── kustomization.yaml │ └── bases │ └── argo-rollouts-manager.clusterserviceversion.yaml ├── hack ├── upgrade-rollouts-script │ ├── .gitignore │ ├── settings_template.env │ ├── go.mod │ ├── go-run.sh │ ├── go.sum │ ├── init-repo.sh │ ├── README.md │ └── main.go ├── verify-rollouts-e2e-tests │ ├── go.mod │ ├── verify-e2e-test-results.sh │ └── main.go ├── boilerplate.go.txt ├── start-rollouts-manager-for-e2e-tests.sh ├── run-upstream-argo-rollouts-e2e-tests.sh └── run-rollouts-manager-e2e-tests.sh ├── .dockerignore ├── examples ├── basic_rolloutmanager.yaml ├── custom_resources_rolloutmanager.yaml ├── custom_image_rolloutmanager.yaml ├── custom_metadata_rolloutmanager.yaml ├── nodeplacement_rolloutmanager.yaml └── catalog_source.yaml ├── .readthedocs.yml ├── .gitignore ├── .github ├── pull_request_template.md └── workflows │ ├── lint.yaml │ ├── go.yml │ ├── e2e_tests.yml │ ├── rollouts_e2e_tests.yml │ └── codegen.yaml ├── bundle ├── manifests │ ├── argo-rollouts-manager-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml │ └── argo-rollouts-manager-controller-manager-metrics-service_v1_service.yaml ├── metadata │ └── annotations.yaml └── tests │ └── scorecard │ └── config.yaml ├── tests └── e2e │ ├── argorollouts_suite_test.go │ ├── cluster-scoped │ └── cluster_scoped_suite_test.go │ ├── namespace-scoped │ └── namespace_scoped_suite_test.go │ ├── fixture │ ├── k8s │ │ └── fixture.go │ ├── rolloutmanager │ │ └── fixture.go │ ├── rollouts │ │ └── fixture.go │ └── fixture.go │ └── rollouts_imagepullpolicy_test.go ├── PROJECT ├── mkdocs.yml ├── bundle.Dockerfile ├── controllers ├── suite_test.go ├── status.go ├── status_test.go ├── default.go ├── reconcile.go ├── configmap.go └── configmap_test.go ├── Dockerfile ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── argorollouts_types.go │ └── zz_generated.deepcopy.go ├── README.md ├── go.mod ├── cmd └── main.go └── LICENSE /docs/install/olm.md: -------------------------------------------------------------------------------- 1 | # Install Operator through OLM 2 | 3 | [WIP] -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /hack/upgrade-rollouts-script/.gitignore: -------------------------------------------------------------------------------- 1 | settings.env 2 | argo-rollouts-manager 3 | -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/argoproj-labs/argo-rollouts-manager/HEAD/docs/assets/logo.png -------------------------------------------------------------------------------- /hack/verify-rollouts-e2e-tests/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/argoproj-labs/argo-rollouts-manager/verify-rollouts-e2e-tests 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /examples/basic_rolloutmanager.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: RolloutManager 4 | metadata: 5 | name: rollout-manager 6 | labels: 7 | example: basic 8 | spec: {} -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /hack/verify-rollouts-e2e-tests/verify-e2e-test-results.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | cd $SCRIPT_DIR 6 | 7 | go run . $* 8 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - argoproj.io_v1alpha1_rolloutmanager.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /hack/upgrade-rollouts-script/settings_template.env: -------------------------------------------------------------------------------- 1 | export GITHUB_FORK_USERNAME="(your username here)" 2 | export GH_TOKEN="(a GitHub personal access token that can clone/push/open PRs against argo-rollouts-manager repo)" -------------------------------------------------------------------------------- /hack/upgrade-rollouts-script/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jgwest/argo-rollouts-release-job 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/go-github/v58 v58.0.0 7 | github.com/google/go-querystring v1.1.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: quay.io/argoprojlabs/argo-rollouts-manager 8 | newTag: v0.0.1 9 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.28.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.3.0 2 | # Strict mode has been disabled in latest versions of mkdocs-material. 3 | # Thus pointing to the older version of mkdocs-material. 4 | mkdocs-material==7.1.8 5 | markdown_include==0.6.0 6 | pygments==2.15.0 7 | jinja2==3.1.6 8 | markdown==3.3.7 9 | pymdown-extensions==10.2.1 10 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_argorollouts.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: rolloutmanagers.argoproj.io 8 | -------------------------------------------------------------------------------- /examples/custom_resources_rolloutmanager.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: RolloutManager 4 | metadata: 5 | name: argo-rollout 6 | labels: 7 | example: with-resources-example 8 | spec: 9 | controllerResources: 10 | requests: 11 | memory: "64Mi" 12 | cpu: "250m" 13 | limits: 14 | memory: "128Mi" 15 | cpu: "500m" -------------------------------------------------------------------------------- /hack/upgrade-rollouts-script/go-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | cd $SCRIPT_DIR 6 | 7 | # Read Github Token and Username from settings.env, if it exists 8 | vars_file="$SCRIPT_DIR/settings.env" 9 | if [[ -f "$vars_file" ]]; then 10 | source "$vars_file" 11 | fi 12 | 13 | # Run the upgrade code 14 | go run . 15 | -------------------------------------------------------------------------------- /examples/custom_image_rolloutmanager.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: RolloutManager 4 | metadata: 5 | name: argo-rollout 6 | labels: 7 | example: with-properties 8 | spec: 9 | env: 10 | - name: "foo" 11 | value: "bar" 12 | extraCommandArgs: 13 | - --foo 14 | - bar 15 | image: "quay.io/random/my-rollout-image" 16 | version: "sha256:...." 17 | imagePullPolicy: "IfNotPresent" -------------------------------------------------------------------------------- /config/samples/argoproj.io_v1alpha1_rolloutmanager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: RolloutManager 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolloutmanagers 6 | app.kubernetes.io/instance: rolloutmanager-sample 7 | app.kubernetes.io/part-of: argo-rollouts-manager 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: argo-rollouts-manager 10 | name: rolloutmanager-sample 11 | spec: {} 12 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: argo-rollouts-manager 9 | app.kubernetes.io/part-of: argo-rollouts-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /examples/custom_metadata_rolloutmanager.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: RolloutManager 4 | metadata: 5 | name: rollout-manager 6 | labels: 7 | example: withLabelsAndAnnotations 8 | spec: 9 | additionalMetadata: 10 | labels: 11 | exampleLabel: example-label 12 | secondExampleLabel: second-example-label 13 | annotations: 14 | exampleAnnotation: example-annotation 15 | secondExampleAnnotation: second-example-annotation 16 | -------------------------------------------------------------------------------- /examples/nodeplacement_rolloutmanager.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: RolloutManager 4 | metadata: 5 | name: argo-rollout 6 | labels: 7 | example: nodeplacement-example 8 | spec: 9 | nodePlacement: 10 | nodeSelector: 11 | key1: value1 12 | tolerations: 13 | - key: key1 14 | operator: Equal 15 | value: value1 16 | effect: NoSchedule 17 | - key: key1 18 | operator: Equal 19 | value: value1 20 | effect: NoExecute -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_argorollouts.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: rolloutmanagers.argoproj.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /examples/catalog_source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: CatalogSource 3 | metadata: 4 | name: argo-rollouts-manager-catalog 5 | namespace: openshift-marketplace # for kubernetes choose - `operators` namespace 6 | spec: 7 | sourceType: grpc 8 | image: quay.io/argoproj-labs/argo-rollouts-manager-catalog@sha256:00d94e2b82490a98a06025c48b2f4781f85eaf626a4a0f6d8b6da1a9b8a4ff19 # replace with your catalog image 9 | displayName: Argo Rollouts Manager 10 | publisher: Argo Rollouts 11 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | 17 | # Optionally declare the Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: argo-rollouts-manager 9 | app.kubernetes.io/part-of: argo-rollouts-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | Dockerfile.cross 11 | docs-website 12 | e2e.json 13 | kubeconfig 14 | 15 | # Test binary, build with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Kubernetes Generated files - skip generated files, except for vendored files 22 | 23 | !vendor/**/zz_generated.* 24 | 25 | # editor and IDE paraphernalia 26 | .idea 27 | *.swp 28 | *.swo 29 | *~ 30 | vendor/* -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **What does this PR do / why we need it**: 2 | - (Provide a list of the changes you have made, and why you made those changes) 3 | - (You don't need to re-mention any changes/reasons which are already documented in the issue) 4 | 5 | **Have you updated the necessary documentation?** 6 | 7 | * [ ] Documentation update is required by this PR, and has been updated. 8 | 9 | **Which issue(s) this PR fixes**: 10 | Fixes #? 11 | 12 | 13 | 14 | **How to test changes / Special notes to the reviewer**: 15 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /bundle/manifests/argo-rollouts-manager-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: kube-rbac-proxy 7 | app.kubernetes.io/created-by: argo-rollouts-manager 8 | app.kubernetes.io/instance: metrics-reader 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: clusterrole 11 | app.kubernetes.io/part-of: argo-rollouts-manager 12 | name: argo-rollouts-manager-metrics-reader 13 | rules: 14 | - nonResourceURLs: 15 | - /metrics 16 | verbs: 17 | - get 18 | -------------------------------------------------------------------------------- /tests/e2e/argorollouts_suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | "go.uber.org/zap/zapcore" 9 | logf "sigs.k8s.io/controller-runtime/pkg/log" 10 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 11 | ) 12 | 13 | var _ = BeforeSuite(func() { 14 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) 15 | }) 16 | 17 | func TestArgoRolloutsManager(t *testing.T) { 18 | suiteConfig, _ := GinkgoConfiguration() 19 | 20 | RegisterFailHandler(Fail) 21 | 22 | RunSpecs(t, "argo-rollouts-manager suite", suiteConfig) 23 | } 24 | -------------------------------------------------------------------------------- /tests/e2e/cluster-scoped/cluster_scoped_suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | "go.uber.org/zap/zapcore" 9 | 10 | logf "sigs.k8s.io/controller-runtime/pkg/log" 11 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 12 | ) 13 | 14 | var _ = BeforeSuite(func() { 15 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) 16 | }) 17 | 18 | func TestClusterScoped(t *testing.T) { 19 | suiteConfig, _ := GinkgoConfiguration() 20 | 21 | RegisterFailHandler(Fail) 22 | 23 | RunSpecs(t, "Cluster-scoped Suite", suiteConfig) 24 | } 25 | -------------------------------------------------------------------------------- /tests/e2e/namespace-scoped/namespace_scoped_suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | "go.uber.org/zap/zapcore" 9 | logf "sigs.k8s.io/controller-runtime/pkg/log" 10 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 11 | ) 12 | 13 | var _ = BeforeSuite(func() { 14 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) 15 | }) 16 | 17 | func TestNamespaceScoped(t *testing.T) { 18 | suiteConfig, _ := GinkgoConfiguration() 19 | 20 | RegisterFailHandler(Fail) 21 | 22 | RunSpecs(t, "Namespace-scoped Suite", suiteConfig) 23 | } 24 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: argo-rollouts-manager 9 | app.kubernetes.io/part-of: argo-rollouts-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /hack/upgrade-rollouts-script/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 2 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 3 | github.com/google/go-github/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw= 4 | github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4= 5 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 6 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 7 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 8 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: argo-rollouts-manager 9 | app.kubernetes.io/part-of: argo-rollouts-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: argo-rollouts-manager 9 | app.kubernetes.io/part-of: argo-rollouts-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: argo-rollouts-manager 9 | app.kubernetes.io/part-of: argo-rollouts-manager 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: argo-rollouts-manager 10 | app.kubernetes.io/part-of: argo-rollouts-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /hack/start-rollouts-manager-for-e2e-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPTPATH="$( 4 | cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit 5 | pwd -P 6 | )" 7 | 8 | cd "$SCRIPTPATH/.." 9 | 10 | killall main 11 | 12 | sleep 5s 13 | 14 | rm -f /tmp/e2e-operator-run.log || true 15 | 16 | set -o pipefail 17 | set -ex 18 | 19 | make install generate fmt vet 20 | 21 | # Set namespaces used for cluster-scoped e2e tests 22 | export CLUSTER_SCOPED_ARGO_ROLLOUTS_NAMESPACES="argo-rollouts,test-rom-ns-1,rom-ns-1" 23 | 24 | if [ "$RUN_IN_BACKGROUND" == "true" ]; then 25 | go run ./cmd/main.go 2>&1 | tee /tmp/e2e-operator-run.log & 26 | else 27 | go run ./cmd/main.go 2>&1 | tee /tmp/e2e-operator-run.log 28 | fi 29 | -------------------------------------------------------------------------------- /hack/upgrade-rollouts-script/init-repo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | cd $SCRIPT_DIR 6 | 7 | # Read Github Token and Username from settings.env, if it exists 8 | vars_file="$SCRIPT_DIR/settings.env" 9 | if [[ -f "$vars_file" ]]; then 10 | source "$vars_file" 11 | fi 12 | 13 | # Clone fork of argo-rollouts-manager repo 14 | 15 | rm -rf "$SCRIPT_DIR/argo-rollouts-manager" || true 16 | 17 | git clone "git@github.com:$GITHUB_FORK_USERNAME/argo-rollouts-manager" 18 | cd argo-rollouts-manager 19 | 20 | # Add a remote back to the original repo 21 | 22 | git remote add parent "git@github.com:argoproj-labs/argo-rollouts-manager" 23 | git fetch parent 24 | 25 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | layout: 6 | - go.kubebuilder.io/v4 7 | plugins: 8 | manifests.sdk.operatorframework.io/v2: {} 9 | scorecard.sdk.operatorframework.io/v2: {} 10 | projectName: argo-rollouts-manager 11 | repo: github.com/argoproj-labs/argo-rollouts-manager 12 | resources: 13 | - api: 14 | crdVersion: v1 15 | namespaced: true 16 | controller: true 17 | group: argoproj.io 18 | kind: RolloutManager 19 | path: github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1 20 | version: v1alpha1 21 | version: "3" 22 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Argo Rollouts Manager 2 | site_url: "" 3 | repo_url: https://github.com/argoproj-labs/argo-rollouts-manager 4 | strict: true 5 | theme: 6 | name: material 7 | palette: 8 | primary: teal 9 | font: 10 | text: 'Work Sans' 11 | logo: 'assets/logo.png' 12 | extra_css: 13 | - 'assets/extra.css' 14 | markdown_extensions: 15 | - codehilite 16 | - admonition 17 | - toc: 18 | permalink: true 19 | nav: 20 | - CRD Reference: crd_reference.md 21 | - Developer Guide: developer-guide/developer_guide.md 22 | - E2E Tests: 23 | - Usage: e2e-tests/usage.md 24 | - Install: 25 | - Kustomize: install/kustomize.md 26 | - OLM: install/olm.md 27 | - Usage: 28 | - Getting Started: usage/getting_started.md 29 | - Overview: index.md 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint code 2 | on: 3 | 4 | push: 5 | branches: 6 | - 'main' 7 | - 'release-*' 8 | 9 | pull_request: 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | lint_code: 15 | name: Run golangci-lint and gosec 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup Golang 23 | uses: actions/setup-go@v5.0.0 24 | with: 25 | go-version-file: './go.mod' 26 | 27 | - name: "run gosec" 28 | run: | 29 | cd $GITHUB_WORKSPACE 30 | make gosec 31 | - name: "run golangci-lint" 32 | run: | 33 | cd $GITHUB_WORKSPACE 34 | make lint 35 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/argorollouts_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view rolloutmanagers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: rolloutmanagers-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: argo-rollouts-manager 10 | app.kubernetes.io/part-of: argo-rollouts-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: rolloutmanagers-viewer-role 13 | rules: 14 | - apiGroups: 15 | - argoproj.io 16 | resources: 17 | - rolloutmanagers 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - argoproj.io 24 | resources: 25 | - rolloutmanagers/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /bundle/metadata/annotations.yaml: -------------------------------------------------------------------------------- 1 | annotations: 2 | # Core bundle annotations. 3 | operators.operatorframework.io.bundle.mediatype.v1: registry+v1 4 | operators.operatorframework.io.bundle.manifests.v1: manifests/ 5 | operators.operatorframework.io.bundle.metadata.v1: metadata/ 6 | operators.operatorframework.io.bundle.package.v1: argo-rollouts-manager 7 | operators.operatorframework.io.bundle.channels.v1: alpha 8 | operators.operatorframework.io.metrics.builder: operator-sdk-v1.35.0 9 | operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 10 | operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 11 | 12 | # Annotations for testing. 13 | operators.operatorframework.io.test.mediatype.v1: scorecard+v1 14 | operators.operatorframework.io.test.config.v1: tests/scorecard/ 15 | -------------------------------------------------------------------------------- /bundle/manifests/argo-rollouts-manager-controller-manager-metrics-service_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app.kubernetes.io/component: kube-rbac-proxy 7 | app.kubernetes.io/created-by: argo-rollouts-manager 8 | app.kubernetes.io/instance: controller-manager-metrics-service 9 | app.kubernetes.io/managed-by: kustomize 10 | app.kubernetes.io/name: service 11 | app.kubernetes.io/part-of: argo-rollouts-manager 12 | control-plane: controller-manager 13 | name: argo-rollouts-manager-controller-manager-metrics-service 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | status: 23 | loadBalancer: {} 24 | -------------------------------------------------------------------------------- /config/rbac/argorollouts_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit rolloutmanagers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: rolloutmanagers-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: argo-rollouts-manager 10 | app.kubernetes.io/part-of: argo-rollouts-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: rolloutmanagers-editor-role 13 | rules: 14 | - apiGroups: 15 | - argoproj.io 16 | resources: 17 | - rolloutmanagers 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - argoproj.io 28 | resources: 29 | - rolloutmanagers/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: argo-rollouts-manager 12 | app.kubernetes.io/part-of: argo-rollouts-manager 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: argo-rollouts-manager 10 | app.kubernetes.io/part-of: argo-rollouts-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=argo-rollouts-manager 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.35.0 10 | LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 11 | LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4 12 | 13 | # Labels for testing. 14 | LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 15 | LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ 16 | 17 | # Copy files to locations specified by labels. 18 | COPY bundle/manifests /manifests/ 19 | COPY bundle/metadata /metadata/ 20 | COPY bundle/tests/scorecard /tests/scorecard/ 21 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Argo Rollouts Manager 2 | 3 | **Project Status: BETA** 4 | 5 | Not all planned features are completed. The API, spec, status and other user facing objects may change. 6 | 7 | ## Summary 8 | 9 | A Kubernetes operator for managing [Argo Rollouts](https://github.com/argoproj/argo-rollouts/). This operator provides an easy way to install, upgrade and manage the lifecycle of Argo Rollouts. 10 | 11 | This operator is built using ``, version - `v1.35.0`. 12 | 13 | ## What exactly the operator does ? 14 | 15 | When Installed, this operator creates a Custom Resource Definition called RolloutManager. 16 | 17 | Operator will then wait for the users to deploy the corresponding Custom Resource to create the rollout controller and other resources according to the provided spec. 18 | 19 | Read more about the Argo Rollout CRD specification here. 20 | 21 | ## Where to start ? 22 | 23 | We have a getting started [guide](usage/getting_started.md) which provides information on how to start using the operator. -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | package rollouts 2 | 3 | import ( 4 | "flag" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | "go.uber.org/zap/zapcore" 11 | logf "sigs.k8s.io/controller-runtime/pkg/log" 12 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 13 | ) 14 | 15 | var _ = BeforeSuite(func() { 16 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) 17 | }) 18 | 19 | func TestRollouts(t *testing.T) { 20 | 21 | suiteConfig, _ := GinkgoConfiguration() 22 | 23 | // Define a flag for the poll progress after interval 24 | var pollProgressAfter time.Duration 25 | // A test is "slow" if it takes longer than a few minutes 26 | flag.DurationVar(&pollProgressAfter, "poll-progress-after", 3*time.Minute, "Interval for polling progress after") 27 | 28 | // Parse the flags 29 | flag.Parse() 30 | 31 | // Set the poll progress after interval in the suite configuration 32 | suiteConfig.PollProgressAfter = pollProgressAfter 33 | 34 | RegisterFailHandler(Fail) 35 | RunSpecs(t, "Rollouts Suite") 36 | } 37 | -------------------------------------------------------------------------------- /docs/e2e-tests/usage.md: -------------------------------------------------------------------------------- 1 | # E2E Tests Guide 2 | 3 | E2E tests are written using [Ginkgo](https://github.com/onsi/ginkgo). 4 | 5 | ## Requirements 6 | 7 | This test suite assumes that an Argo Rollouts Manager is installed on the cluster or running locally. 8 | 9 | The system executing the tests must have following tools installed: 10 | 11 | * `kubectl` client 12 | 13 | There should be a `kubeconfig` pointing to your cluster, user should have full admin privileges (i.e. `kubeadm`). 14 | 15 | ### Run e2e tests 16 | 17 | Run the controller: 18 | ```sh 19 | make install run 20 | ``` 21 | 22 | In a separate window/terminal, run the tests against the controller: 23 | ```sh 24 | make test-e2e 25 | ``` 26 | 27 | ### Running single tests 28 | 29 | Sometimes (e.g. when initially writing a test or troubleshooting an existing 30 | one), you may want to run single test cases isolated. To do so, you can use the 31 | ginkgo CLI utility. It is also possible to use 'go test'. 32 | 33 | ```sh 34 | ginkgo -r -focus "(name of test)" tests/e2e 35 | 36 | # Example: 37 | ginkgo -r -focus "Reconcile is called on a new, basic, namespaced-scoped RolloutManager" tests/e2e 38 | ``` 39 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/analysis-run-crd.yaml 6 | - bases/analysis-template-crd.yaml 7 | - bases/argoproj.io_rolloutmanagers.yaml 8 | - bases/cluster-analysis-template-crd.yaml 9 | - bases/experiment-crd.yaml 10 | - bases/rollout-crd.yaml 11 | #+kubebuilder:scaffold:crdkustomizeresource 12 | 13 | patchesStrategicMerge: 14 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 15 | # patches here are for enabling the conversion webhook for each CRD 16 | #- patches/webhook_in_rolloutmanagers.yaml 17 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 18 | 19 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 20 | # patches here are for enabling the CA injection for each CRD 21 | #- patches/cainjection_in_rolloutmanagers.yaml 22 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 23 | 24 | # the following config is for teaching kustomize how to do kustomization for CRDs. 25 | configurations: 26 | - kustomizeconfig.yaml 27 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'main' 10 | - 'release-*' 11 | pull_request: 12 | branches: 13 | - '*' 14 | 15 | jobs: 16 | 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Setup Golang 23 | uses: actions/setup-go@v5.0.0 24 | with: 25 | go-version-file: './go.mod' 26 | 27 | - name: Build 28 | run: make build 29 | 30 | - name: Test 31 | run: make test 32 | 33 | gofmt: 34 | name: "Ensure that code is gofmt-ed" 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@master 38 | - name: Setup Golang 39 | uses: actions/setup-go@v5.0.0 40 | with: 41 | go-version-file: './go.mod' 42 | - name: "Run make fmt and then 'git diff' to see if anything changed: to fix this check, run make fmt and then commit the changes." 43 | run: | 44 | make fmt 45 | git diff --exit-code -- . 46 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/argo-rollouts-manager.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | # path: /spec/template/spec/containers/1/volumeMounts/0 24 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 25 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 26 | # - op: remove 27 | # path: /spec/template/spec/volumes/0 28 | -------------------------------------------------------------------------------- /config/manifests/bases/argo-rollouts-manager.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | alm-examples: '[]' 6 | capabilities: Basic Install 7 | name: argo-rollouts-manager.v0.0.0 8 | namespace: placeholder 9 | spec: 10 | apiservicedefinitions: {} 11 | customresourcedefinitions: 12 | owned: 13 | - description: RolloutManager is the Schema for the RolloutManagers API 14 | displayName: Rollout Manager 15 | kind: RolloutManager 16 | name: rolloutmanagers.argoproj.io 17 | version: v1alpha1 18 | description: Kubernetes Operator for managing argo-rollouts. 19 | displayName: argo-rollouts-manager 20 | icon: 21 | - base64data: "" 22 | mediatype: "" 23 | install: 24 | spec: 25 | deployments: null 26 | strategy: "" 27 | installModes: 28 | - supported: false 29 | type: OwnNamespace 30 | - supported: false 31 | type: SingleNamespace 32 | - supported: false 33 | type: MultiNamespace 34 | - supported: true 35 | type: AllNamespaces 36 | keywords: 37 | - argo-rollouts 38 | - progressive delivery 39 | - argoproj 40 | links: 41 | - name: Argo Rollouts Manager 42 | url: https://argo-rollouts-manager.domain 43 | maturity: alpha 44 | provider: 45 | name: Argo Community 46 | version: 0.0.0 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.24 AS builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY controllers/ controllers/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 | 26 | # Use distroless as minimal base image to package the manager binary 27 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 28 | FROM gcr.io/distroless/static:nonroot 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | USER 65532:65532 32 | 33 | ENTRYPOINT ["/manager"] 34 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the argoproj.io v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=argoproj.io 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "argoproj.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.28.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.28.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.28.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.28.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.28.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /controllers/status.go: -------------------------------------------------------------------------------- 1 | package rollouts 2 | 3 | import ( 4 | "context" 5 | 6 | rolloutsmanagerv1alpha1 "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 7 | appsv1 "k8s.io/api/apps/v1" 8 | apierrors "k8s.io/apimachinery/pkg/api/errors" 9 | ) 10 | 11 | // determineStatusPhase calculates and returns RolloutManager's current .status.phase and .status.rolloutcontroller, both based on Deployment status. 12 | func (r *RolloutManagerReconciler) determineStatusPhase(ctx context.Context, cr rolloutsmanagerv1alpha1.RolloutManager) (reconcileStatusResult, error) { 13 | 14 | status := rolloutsmanagerv1alpha1.PhaseUnknown 15 | 16 | deploy := &appsv1.Deployment{} 17 | if err := fetchObject(ctx, r.Client, cr.Namespace, DefaultArgoRolloutsResourceName, deploy); err != nil { 18 | if apierrors.IsNotFound(err) { 19 | status = rolloutsmanagerv1alpha1.PhaseFailure 20 | } else { 21 | log.Error(err, "error retrieving Deployment") 22 | return reconcileStatusResult{}, err 23 | } 24 | } else { 25 | 26 | // Deployment exists 27 | 28 | if deploy.Spec.Replicas != nil { 29 | status = rolloutsmanagerv1alpha1.PhasePending 30 | if deploy.Status.ReadyReplicas == *deploy.Spec.Replicas { 31 | status = rolloutsmanagerv1alpha1.PhaseAvailable 32 | } 33 | } 34 | } 35 | 36 | var res reconcileStatusResult 37 | 38 | if cr.Status.RolloutController != status { 39 | res.rolloutController = &status 40 | } 41 | 42 | if cr.Status.Phase != status { 43 | res.phase = &status 44 | } 45 | 46 | return res, nil 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/e2e_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run E2E tests 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | - 'release-*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | 13 | test-e2e: 14 | name: Run end-to-end tests 15 | timeout-minutes: 90 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | k3s-version: [ v1.28.2 ] 20 | # k3s-version: [ v1.28.2, v1.27.6, v1.26.9 ] 21 | steps: 22 | - name: Install K3S 23 | run: | 24 | set -x 25 | curl -sfL https://get.k3s.io | sh - 26 | sudo chmod -R a+rw /etc/rancher/k3s 27 | sudo mkdir -p $HOME/.kube && sudo chown -R runner $HOME/.kube 28 | sudo k3s kubectl config view --raw > $HOME/.kube/config 29 | sudo chown runner $HOME/.kube/config 30 | sudo chmod go-r $HOME/.kube/config 31 | kubectl version 32 | - name: Checkout code 33 | uses: actions/checkout@v2 34 | - name: Setup Golang 35 | uses: actions/setup-go@v5.0.0 36 | with: 37 | go-version-file: './go.mod' 38 | - name: GH actions workaround - Kill XSP4 process 39 | run: | 40 | sudo pkill mono || true 41 | - name: Add /usr/local/bin to PATH 42 | run: | 43 | echo "/usr/local/bin" >> $GITHUB_PATH 44 | - name: Download Go dependencies 45 | run: | 46 | go mod download 47 | - name: Run tests 48 | run: | 49 | set -o pipefail 50 | make start-test-e2e-all 2>&1 | tee /tmp/e2e-test.log 51 | -------------------------------------------------------------------------------- /hack/upgrade-rollouts-script/README.md: -------------------------------------------------------------------------------- 1 | # Update argo-rollouts-manager to latest release of Argo Rollouts 2 | 3 | The Go code and script this in this directory will automatically open a pull request to update the argo-rollouts-manager to the latest official argo-rollouts release: 4 | - Update container image version in `default.go` 5 | - Update `go.mod` to point to latest module version 6 | - Update CRDs to latest 7 | - Update target Rollouts version in `hack/run-upstream-argo-rollouts-e2e-tests.sh` 8 | - Open Pull Request using 'gh' CLI 9 | 10 | ## Instructions 11 | 12 | ### Prerequisites 13 | - GitHub CLI (_gh_) installed and on PATH 14 | - Go installed an on PATH 15 | - [Operator-sdk v1.35.0](https://github.com/operator-framework/operator-sdk/releases/tag/v1.35.0) installed (as of January 2024), and on PATH 16 | - You must have your own fork of the [argo-rollouts-manager](https://github.com/argoproj-labs/argo-rollouts-manager) repository (example: `jgwest/argo-rollouts-manager`) 17 | - Your local SSH key registered (e.g. `~/.ssh/id_rsa.pub`) with GitHub to allow git clone via SSH 18 | 19 | ### Configure and run the tool 20 | 21 | ```bash 22 | 23 | # Set required Environment Variables 24 | export GITHUB_FORK_USERNAME="(your username here)" 25 | export GH_TOKEN="(a GitHub personal access token that can clone/push/open PRs against argo-rollouts-manager repo)" 26 | 27 | # or, you can set these values in the settings.env file: 28 | # cp settings_template.env settings.env 29 | # Then set env vars in settings.env (which is excluded in the .gitignore) 30 | 31 | ./init-repo.sh 32 | ./go-run.sh 33 | ``` 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Argo Rollouts Manager 2 | 3 | **Project Status: BETA** 4 | 5 | Not all planned features are completed. The API, spec, status and other user facing objects may change. 6 | 7 | ## Summary 8 | 9 | A Kubernetes operator for managing [Argo Rollouts](https://github.com/argoproj/argo-rollouts/). This operator provides an easy way to install, upgrade and manage the lifecycle of Argo Rollouts. 10 | 11 | This operator is built using `operator-sdk`, version - `v1.35.0`. 12 | 13 | ## Documentation 14 | 15 | The complete documentation for the operator can be found [here](https://argo-rollouts-manager.readthedocs.io/en/latest/). 16 | 17 | ## What exactly the operator does ? 18 | 19 | When Installed, this operator creates a Custom Resource Definition called RolloutManager. 20 | 21 | Operator will then wait for the users to deploy the corresponding Custom Resource to create the [rollout](https://argo-rollouts.readthedocs.io/en/stable/) controller and other resources according to the provided spec. 22 | 23 | Read more about the Argo Rollout CRD specification [here](https://argo-rollouts-manager.readthedocs.io/en/latest/crd_reference/). 24 | 25 | ## Where to start ? 26 | 27 | We have a getting started [guide](docs/usage/getting_started.md) which provides information on how to start using the operator. 28 | 29 | ### Development 30 | 31 | Instructions to run the operator locally or create your own version of the operator Image are provided in the development [section](docs/developer-guide/developer_guide.md) of the docs. 32 | 33 | ### Contributions 34 | 35 | [WIP] 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/install/kustomize.md: -------------------------------------------------------------------------------- 1 | # Manual Installation using kustomize 2 | 3 | The following steps can be used to manually install the operator on any Kubernetes environment with minimal overhead. 4 | 5 | !!! info 6 | Several of the steps in this process require the `cluster-admin` ClusterRole or equivalent. 7 | 8 | ## Namespace 9 | 10 | By default, the operator is installed into the `argo-rollouts-manager-system` namespace. To modify this, update the 11 | value of the `namespace` specified in the `config/default/kustomization.yaml` file. 12 | 13 | ## Deploy Operator 14 | 15 | ```bash 16 | make deploy 17 | ``` 18 | 19 | If you want to use your own custom operator container image, you can specify the image name using the `IMG` variable. 20 | 21 | ```bash 22 | make deploy IMG=quay.io/my-org/rollouts-manager:latest 23 | ``` 24 | 25 | The operator pod should start and enter a `Running` state after a few seconds. 26 | 27 | ```bash 28 | kubectl get pods -n argo-rollouts-manager-system 29 | ``` 30 | 31 | ```bash 32 | NAME READY STATUS RESTARTS AGE 33 | argo-rollouts-manager-controller-manager-65777cf998-pr9fg 2/2 Running 0 69s 34 | ``` 35 | 36 | ## Usage 37 | 38 | Once the operator is installed and running, new RolloutManager resources can be created. See the getting started [guide](../usage/getting_started.md) to learn how to create new `RolloutManager` resources. 39 | 40 | ## Cleanup 41 | 42 | To remove the operator from the cluster, run the following comand. This will remove all resources that were created, 43 | including the namespace. 44 | 45 | ```bash 46 | make undeploy 47 | ``` -------------------------------------------------------------------------------- /.github/workflows/rollouts_e2e_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run upstream Argo-Rollouts E2E tests 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | - 'release-*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | 13 | test-e2e: 14 | name: Run end-to-end tests from upstream Argo Rollouts repo 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 90 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | kubernetes: 21 | - version: '1.28' 22 | latest: false 23 | steps: 24 | - name: Install K3S 25 | env: 26 | INSTALL_K3S_CHANNEL: v${{ matrix.kubernetes.version }} 27 | run: | 28 | set -x 29 | curl -sfL https://get.k3s.io | sh - 30 | sudo chmod -R a+rw /etc/rancher/k3s 31 | sudo mkdir -p $HOME/.kube && sudo chown -R runner $HOME/.kube 32 | sudo k3s kubectl config view --raw > $HOME/.kube/config 33 | sudo chown runner $HOME/.kube/config 34 | sudo chmod go-r $HOME/.kube/config 35 | kubectl version 36 | - name: Checkout code 37 | uses: actions/checkout@v2 38 | - name: Setup Golang 39 | uses: actions/setup-go@v5.0.0 40 | with: 41 | go-version-file: './go.mod' 42 | - name: GH actions workaround - Kill XSP4 process 43 | run: | 44 | sudo pkill mono || true 45 | - name: Add /usr/local/bin to PATH 46 | run: | 47 | echo "/usr/local/bin" >> $GITHUB_PATH 48 | - name: Download Go dependencies 49 | run: | 50 | go mod download 51 | - name: Run the Argo Rollouts E2E tests 52 | run: | 53 | set -o pipefail 54 | ./hack/run-upstream-argo-rollouts-e2e-tests.sh 55 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | affinity: 12 | nodeAffinity: 13 | requiredDuringSchedulingIgnoredDuringExecution: 14 | nodeSelectorTerms: 15 | - matchExpressions: 16 | - key: kubernetes.io/arch 17 | operator: In 18 | values: 19 | - amd64 20 | - arm64 21 | - ppc64le 22 | - s390x 23 | - key: kubernetes.io/os 24 | operator: In 25 | values: 26 | - linux 27 | containers: 28 | - name: kube-rbac-proxy 29 | securityContext: 30 | allowPrivilegeEscalation: false 31 | capabilities: 32 | drop: 33 | - "ALL" 34 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 35 | args: 36 | - "--secure-listen-address=0.0.0.0:8443" 37 | - "--upstream=http://127.0.0.1:8080/" 38 | - "--logtostderr=true" 39 | - "--v=0" 40 | ports: 41 | - containerPort: 8443 42 | protocol: TCP 43 | name: https 44 | resources: 45 | limits: 46 | cpu: 500m 47 | memory: 128Mi 48 | requests: 49 | cpu: 5m 50 | memory: 64Mi 51 | - name: manager 52 | args: 53 | - "--health-probe-bind-address=:8081" 54 | - "--metrics-bind-address=127.0.0.1:8080" 55 | - "--leader-elect" 56 | -------------------------------------------------------------------------------- /bundle/tests/scorecard/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: 8 | - entrypoint: 9 | - scorecard-test 10 | - basic-check-spec 11 | image: quay.io/operator-framework/scorecard-test:v1.28.0 12 | labels: 13 | suite: basic 14 | test: basic-check-spec-test 15 | storage: 16 | spec: 17 | mountPath: {} 18 | - entrypoint: 19 | - scorecard-test 20 | - olm-bundle-validation 21 | image: quay.io/operator-framework/scorecard-test:v1.28.0 22 | labels: 23 | suite: olm 24 | test: olm-bundle-validation-test 25 | storage: 26 | spec: 27 | mountPath: {} 28 | - entrypoint: 29 | - scorecard-test 30 | - olm-crds-have-validation 31 | image: quay.io/operator-framework/scorecard-test:v1.28.0 32 | labels: 33 | suite: olm 34 | test: olm-crds-have-validation-test 35 | storage: 36 | spec: 37 | mountPath: {} 38 | - entrypoint: 39 | - scorecard-test 40 | - olm-crds-have-resources 41 | image: quay.io/operator-framework/scorecard-test:v1.28.0 42 | labels: 43 | suite: olm 44 | test: olm-crds-have-resources-test 45 | storage: 46 | spec: 47 | mountPath: {} 48 | - entrypoint: 49 | - scorecard-test 50 | - olm-spec-descriptors 51 | image: quay.io/operator-framework/scorecard-test:v1.28.0 52 | labels: 53 | suite: olm 54 | test: olm-spec-descriptors-test 55 | storage: 56 | spec: 57 | mountPath: {} 58 | - entrypoint: 59 | - scorecard-test 60 | - olm-status-descriptors 61 | image: quay.io/operator-framework/scorecard-test:v1.28.0 62 | labels: 63 | suite: olm 64 | test: olm-status-descriptors-test 65 | storage: 66 | spec: 67 | mountPath: {} 68 | storage: 69 | spec: 70 | mountPath: {} 71 | -------------------------------------------------------------------------------- /.github/workflows/codegen.yaml: -------------------------------------------------------------------------------- 1 | name: Generated code 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | pull_request: 7 | paths-ignore: 8 | - "docs/**" 9 | branches: 10 | - 'main' 11 | 12 | jobs: 13 | check-go-modules: 14 | name: "Check for go.mod/go.sum synchronicity" 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | - name: Setup Golang 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: 'go.mod' 23 | - name: Download all Go modules 24 | run: | 25 | go mod download 26 | - name: Check for tidyness of go.mod and go.sum 27 | run: | 28 | go mod tidy 29 | git diff --exit-code -- . 30 | 31 | check-sdk-codegen: 32 | name: "Check for changes from make bundle" 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | - name: Setup Golang 38 | uses: actions/setup-go@v5 39 | with: 40 | go-version-file: 'go.mod' 41 | - name: Run make bundle 42 | run: | 43 | make bundle 44 | - name: Ensure there is no diff in bundle 45 | # --ignore-matching-lines='.*createdAt:.*' added to ignore bundle differences of timestamp that is generated by upgraded operator-sdk 46 | run: | 47 | git diff --ignore-matching-lines='.*createdAt:.*' --exit-code -- . 48 | - name: Run make generate 49 | run: | 50 | make generate 51 | - name: Ensure there is no diff in generated assets 52 | run: | 53 | git diff --ignore-matching-lines='.*createdAt:.*' --exit-code -- . 54 | - name: Run make manifests 55 | run: | 56 | make manifests 57 | - name: Ensure there is no diff in manifests 58 | run: | 59 | git diff --ignore-matching-lines='.*createdAt:.*' --exit-code -- . 60 | -------------------------------------------------------------------------------- /controllers/status_test.go: -------------------------------------------------------------------------------- 1 | package rollouts 2 | 3 | import ( 4 | "context" 5 | 6 | rolloutsmanagerv1alpha1 "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | appsv1 "k8s.io/api/apps/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | var _ = Describe("RolloutManager Test", func() { 14 | It("RolloutManagerStatus Test", func() { 15 | ctx := context.Background() 16 | a := makeTestRolloutManager() 17 | 18 | r := makeTestReconciler(a) 19 | Expect(createNamespace(r, a.Namespace)).To(Succeed()) 20 | 21 | rr, err := r.determineStatusPhase(ctx, *a) 22 | Expect(err).ToNot(HaveOccurred()) 23 | 24 | By("When deployment for rollout controller does not exist") 25 | Expect(*rr.rolloutController).To(Equal(rolloutsmanagerv1alpha1.PhaseFailure)) 26 | Expect(*rr.phase).To(Equal(rolloutsmanagerv1alpha1.PhaseFailure)) 27 | 28 | By("When deployment exists but with an unknown status") 29 | deploy := &appsv1.Deployment{ 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: DefaultArgoRolloutsResourceName, 32 | Namespace: a.Namespace, 33 | }, 34 | } 35 | 36 | Expect(r.Client.Create(ctx, deploy)).To(Succeed()) 37 | 38 | rr, err = r.determineStatusPhase(ctx, *a) 39 | Expect(err).ToNot(HaveOccurred()) 40 | 41 | Expect(*rr.rolloutController).To(Equal(rolloutsmanagerv1alpha1.PhaseUnknown)) 42 | Expect(*rr.phase).To(Equal(rolloutsmanagerv1alpha1.PhaseUnknown)) 43 | 44 | By("When deployment exists and replicas are in pending state.") 45 | var requiredReplicas int32 = 1 46 | deploy.Status.ReadyReplicas = 0 47 | deploy.Spec.Replicas = &requiredReplicas 48 | 49 | Expect(r.Client.Update(ctx, deploy)).To(Succeed()) 50 | 51 | rr, err = r.determineStatusPhase(ctx, *a) 52 | Expect(err).ToNot(HaveOccurred()) 53 | 54 | Expect(*rr.rolloutController).To(Equal(rolloutsmanagerv1alpha1.PhasePending)) 55 | Expect(*rr.phase).To(Equal(rolloutsmanagerv1alpha1.PhasePending)) 56 | 57 | By("When deployment exists and required number of replicas are up and running.") 58 | deploy.Status.ReadyReplicas = 1 59 | deploy.Spec.Replicas = &requiredReplicas 60 | 61 | Expect(r.Client.Status().Update(ctx, deploy)).To(Succeed()) 62 | 63 | rr, err = r.determineStatusPhase(ctx, *a) 64 | Expect(err).ToNot(HaveOccurred()) 65 | 66 | Expect(*rr.rolloutController).To(Equal(rolloutsmanagerv1alpha1.PhaseAvailable)) 67 | Expect(*rr.phase).To(Equal(rolloutsmanagerv1alpha1.PhaseAvailable)) 68 | 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /controllers/default.go: -------------------------------------------------------------------------------- 1 | package rollouts 2 | 3 | const ( 4 | // ArgoRolloutsImageEnvName is an environment variable that can be used to deploy a 5 | // Custom Image of rollouts controller. 6 | ArgoRolloutsImageEnvName = "ARGO_ROLLOUTS_IMAGE" 7 | 8 | // DefaultArgoRolloutsMetricsServiceName is the default name for rollouts metrics Service. 9 | DefaultArgoRolloutsMetricsServiceName = "argo-rollouts-metrics" 10 | 11 | // ArgoRolloutsDefaultImage is the default image for rollouts controller. 12 | DefaultArgoRolloutsImage = "quay.io/argoproj/argo-rollouts" 13 | 14 | // ArgoRolloutsDefaultVersion is the default version for the Rollouts controller. 15 | DefaultArgoRolloutsVersion = "v1.8.3" // v1.8.3 16 | 17 | // DefaultArgoRolloutsResourceName is the default name for Rollouts controller resources such as 18 | // deployment, service, role, rolebinding and serviceaccount. 19 | DefaultArgoRolloutsResourceName = "argo-rollouts" 20 | 21 | // DefaultRolloutsNotificationSecretName is the default name for rollout controller secret resource. 22 | DefaultRolloutsNotificationSecretName = "argo-rollouts-notification-secret" // #nosec G101 23 | 24 | // DefaultRolloutsServiceSelectorKey is key used by selector 25 | DefaultRolloutsSelectorKey = "app.kubernetes.io/name" 26 | 27 | // OpenShiftRolloutPluginName is the plugin name for Openshift Route Plugin 28 | OpenShiftRolloutPluginName = "argoproj-labs/openshift" 29 | 30 | // DefaultRolloutsConfigMapName is the default name of the ConfigMap that contains the Rollouts controller configuration 31 | DefaultRolloutsConfigMapName = "argo-rollouts-config" 32 | 33 | DefaultOpenShiftRoutePluginURL = "https://github.com/argoproj-labs/rollouts-plugin-trafficrouter-openshift/releases/download/commit-8d0b3c6c5c18341f9f019cf1015b56b0d0c6085b/rollouts-plugin-trafficrouter-openshift-linux-amd64" 34 | 35 | // NamespaceScopedArgoRolloutsController is an environment variable that can be used to configure scope of Argo Rollouts controller 36 | // Set true to allow only namespace-scoped Argo Rollouts controller deployment and false for cluster-scoped 37 | NamespaceScopedArgoRolloutsController = "NAMESPACE_SCOPED_ARGO_ROLLOUTS" 38 | 39 | // ClusterScopedArgoRolloutsNamespaces is an environment variable that can be used to configure namespaces that are allowed to host cluster-scoped Argo Rollouts 40 | ClusterScopedArgoRolloutsNamespaces = "CLUSTER_SCOPED_ARGO_ROLLOUTS_NAMESPACES" 41 | 42 | KubernetesHostnameLabel = "kubernetes.io/hostname" 43 | 44 | TopologyKubernetesZoneLabel = "topology.kubernetes.io/zone" 45 | 46 | ImagePullPolicy = "IMAGE_PULL_POLICY" 47 | ) 48 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: argo-rollouts-manager-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: argo-rollouts-manager- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | 34 | 35 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 36 | # crd/kustomization.yaml 37 | #- manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | #- webhookcainjection_patch.yaml 43 | 44 | # the following config is for teaching kustomize how to do var substitution 45 | vars: 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 48 | # objref: 49 | # kind: Certificate 50 | # group: cert-manager.io 51 | # version: v1 52 | # name: serving-cert # this name should match the one in certificate.yaml 53 | # fieldref: 54 | # fieldpath: metadata.namespace 55 | #- name: CERTIFICATE_NAME 56 | # objref: 57 | # kind: Certificate 58 | # group: cert-manager.io 59 | # version: v1 60 | # name: serving-cert # this name should match the one in certificate.yaml 61 | #- name: SERVICE_NAMESPACE # namespace of the service 62 | # objref: 63 | # kind: Service 64 | # version: v1 65 | # name: webhook-service 66 | # fieldref: 67 | # fieldpath: metadata.namespace 68 | #- name: SERVICE_NAME 69 | # objref: 70 | # kind: Service 71 | # version: v1 72 | # name: webhook-service 73 | -------------------------------------------------------------------------------- /docs/usage/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Install the Operator 4 | 5 | Install the operator using one of the two steps mentioned below. 6 | 7 | - [Install using kustomize](../install/kustomize.md) 8 | - [Install using OLM](../install/olm.md) 9 | 10 | Alternatively, if you are a developer looking to run operator locally or build a new version of operator for your changes, please follow the steps mentioned in developer [guide](../developer-guide/developer_guide.md). 11 | 12 | ## Deploy RolloutManager 13 | 14 | It is recommended to start with [basic](../crd_reference.md/#basic-rolloutmanager-example) RolloutManager configuration. 15 | 16 | ### Apply 17 | 18 | ```bash 19 | kubectl apply -f examples/basic_rolloutmanager.yaml 20 | ``` 21 | 22 | This will create the rollout controller and related resources such as serviceaccount, roles, rolebinding, deployment, service, secret and others. 23 | 24 | You can check if the above mentioned resources are created by running the below command. 25 | 26 | ```bash 27 | kubectl get all 28 | ``` 29 | 30 | If you would like to understand the siginificance of each rollout controller resource created by the operator, please go through the official rollouts controller [docs](https://argo-rollouts.readthedocs.io/en/stable/). 31 | 32 | 33 | 34 | 35 | ## Namespace Scoped Rollouts Instance 36 | 37 | A namespace-scoped Rollouts instance can manage Rollouts resources of same namespace it is deployed into. To deploy a namespace-scoped Rollouts instance set `spec.namespaceScoped` field to `true`. 38 | 39 | ```yml 40 | apiVersion: argoproj.io/v1alpha1 41 | kind: RolloutManager 42 | metadata: 43 | name: argo-rollout 44 | spec: 45 | namespaceScoped: true 46 | ``` 47 | 48 | 49 | ## Cluster Scoped Rollouts Instance 50 | 51 | A cluster-scoped Rollouts instance can manage Rollouts resources from other namespaces as well. To install a cluster-scoped Rollouts instance first you need to add `NAMESPACE_SCOPED_ARGO_ROLLOUTS` and `CLUSTER_SCOPED_ARGO_ROLLOUTS_NAMESPACES` environment variables in subscription resource. If `NAMESPACE_SCOPED_ARGO_ROLLOUTS` is set to `false` then only you are allowed to create a cluster-scoped instance and then you need to provide list of namespaces that are allowed host a cluster-scoped Rollouts instance via `CLUSTER_SCOPED_ARGO_ROLLOUTS_NAMESPACES` environment variable. 52 | 53 | ```yml 54 | apiVersion: operators.coreos.com/v1alpha1 55 | kind: Subscription 56 | metadata: 57 | name: argo-operator 58 | spec: 59 | config: 60 | env: 61 | - name: NAMESPACE_SCOPED_ARGO_ROLLOUTS 62 | value: 'false' 63 | - name: CLUSTER_SCOPED_ARGO_ROLLOUTS_NAMESPACES 64 | value: 65 | (...) 66 | ``` 67 | 68 | Now set `spec.namespaceScoped` field to `false` to create a Rollouts instance. 69 | 70 | ```yml 71 | apiVersion: argoproj.io/v1alpha1 72 | kind: RolloutManager 73 | metadata: 74 | name: argo-rollout 75 | spec: 76 | namespaceScoped: false 77 | ``` 78 | -------------------------------------------------------------------------------- /tests/e2e/fixture/k8s/fixture.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | . "github.com/onsi/gomega" 8 | matcher "github.com/onsi/gomega/types" 9 | "k8s.io/client-go/util/retry" 10 | 11 | apierrors "k8s.io/apimachinery/pkg/api/errors" 12 | 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | // ExistByName checks if the given resource exists, when retrieving it by name/namespace. 17 | // Does NOT check if the resource content matches. 18 | func ExistByName(k8sClient client.Client) matcher.GomegaMatcher { 19 | 20 | return WithTransform(func(k8sObject client.Object) bool { 21 | 22 | err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) 23 | if err != nil { 24 | fmt.Println("Object does not exists in ExistByName:", k8sObject.GetName(), err) 25 | } else { 26 | fmt.Println("Object exists in ExistByName:", k8sObject.GetName()) 27 | } 28 | return err == nil 29 | }, BeTrue()) 30 | } 31 | 32 | // NotExistByName checks if the given resource does not exist, when retrieving it by name/namespace. 33 | // Does NOT check if the resource content matches. 34 | func NotExistByName(k8sClient client.Client) matcher.GomegaMatcher { 35 | 36 | return WithTransform(func(k8sObject client.Object) bool { 37 | 38 | err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) 39 | return apierrors.IsNotFound(err) 40 | }, BeTrue()) 41 | } 42 | 43 | // UpdateWithoutConflict will keep trying to update object until it succeeds, or times out. 44 | func UpdateWithoutConflict(ctx context.Context, obj client.Object, k8sClient client.Client, modify func(client.Object)) error { 45 | err := retry.RetryOnConflict(retry.DefaultRetry, func() error { 46 | // Retrieve the latest version of the object 47 | err := k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | modify(obj) 53 | 54 | // Attempt to update the object 55 | return k8sClient.Update(ctx, obj) 56 | }) 57 | 58 | return err 59 | } 60 | 61 | func HaveLabel(keyParam, valueParam string, k8sClient client.Client) matcher.GomegaMatcher { 62 | 63 | return WithTransform(func(k8sObject client.Object) bool { 64 | 65 | err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) 66 | Expect(err).ToNot(HaveOccurred()) 67 | 68 | for key, value := range k8sObject.GetLabels() { 69 | if key == keyParam && value == valueParam { 70 | return true 71 | } 72 | } 73 | 74 | return false 75 | 76 | }, BeTrue()) 77 | } 78 | 79 | func HaveAnnotation(keyParam, valueParam string, k8sClient client.Client) matcher.GomegaMatcher { 80 | 81 | return WithTransform(func(k8sObject client.Object) bool { 82 | 83 | err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) 84 | Expect(err).ToNot(HaveOccurred()) 85 | 86 | for key, value := range k8sObject.GetAnnotations() { 87 | if key == keyParam && value == valueParam { 88 | return true 89 | } 90 | } 91 | 92 | return false 93 | 94 | }, BeTrue()) 95 | } 96 | -------------------------------------------------------------------------------- /tests/e2e/fixture/rolloutmanager/fixture.go: -------------------------------------------------------------------------------- 1 | package rolloutmanager 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | . "github.com/onsi/gomega" 8 | matcher "github.com/onsi/gomega/types" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | 11 | rolloutsmanagerv1alpha1 "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | 14 | "github.com/argoproj-labs/argo-rollouts-manager/tests/e2e/fixture" 15 | ) 16 | 17 | // This is intentionally NOT exported, for now. Create another function in this file/package that calls this function, and export that. 18 | func fetchRolloutManager(f func(app rolloutsmanagerv1alpha1.RolloutManager) bool) matcher.GomegaMatcher { 19 | 20 | return WithTransform(func(app rolloutsmanagerv1alpha1.RolloutManager) bool { 21 | 22 | k8sClient, _, err := fixture.GetE2ETestKubeClient() 23 | if err != nil { 24 | fmt.Println(err) 25 | return false 26 | } 27 | 28 | err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(&app), &app) 29 | if err != nil { 30 | fmt.Println(err) 31 | return false 32 | } 33 | 34 | return f(app) 35 | 36 | }, BeTrue()) 37 | 38 | } 39 | 40 | func HavePhase(phase rolloutsmanagerv1alpha1.RolloutControllerPhase) matcher.GomegaMatcher { 41 | return fetchRolloutManager(func(app rolloutsmanagerv1alpha1.RolloutManager) bool { 42 | fmt.Println("HavePhase:", "expected: ", phase, "actual: ", app.Status.Phase) 43 | return app.Status.Phase == phase 44 | }) 45 | } 46 | 47 | func HaveCondition(expected metav1.Condition) matcher.GomegaMatcher { 48 | return fetchRolloutManager(func(app rolloutsmanagerv1alpha1.RolloutManager) bool { 49 | 50 | if len(app.Status.Conditions) == 0 { 51 | fmt.Println("HaveCondition: Conditions is nil") 52 | return false 53 | } 54 | 55 | for _, condition := range app.Status.Conditions { 56 | if condition.Type == expected.Type { 57 | fmt.Println("HaveCondition:", "expected: ", expected, "actual: ", condition) 58 | return condition.Type == expected.Type && 59 | condition.Status == expected.Status && 60 | condition.Reason == expected.Reason && 61 | condition.Message == expected.Message 62 | } 63 | } 64 | return false 65 | }) 66 | } 67 | 68 | func HaveSuccessCondition() matcher.GomegaMatcher { 69 | return fetchRolloutManager(func(app rolloutsmanagerv1alpha1.RolloutManager) bool { 70 | 71 | if len(app.Status.Conditions) == 0 { 72 | fmt.Println("HaveSuccessCondition: Conditions is nil") 73 | return false 74 | } 75 | 76 | expected := metav1.Condition{ 77 | Type: rolloutsmanagerv1alpha1.RolloutManagerConditionType, 78 | Status: metav1.ConditionTrue, 79 | Reason: rolloutsmanagerv1alpha1.RolloutManagerReasonSuccess, 80 | Message: "", 81 | } 82 | 83 | for _, condition := range app.Status.Conditions { 84 | if condition.Type == rolloutsmanagerv1alpha1.RolloutManagerConditionType { 85 | fmt.Println("HaveSuccessCondition:", "expected: ", expected, "actual: ", condition) 86 | return condition.Type == expected.Type && 87 | condition.Status == expected.Status && 88 | condition.Reason == expected.Reason && 89 | condition.Message == expected.Message 90 | } 91 | } 92 | return false 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /docs/developer-guide/developer_guide.md: -------------------------------------------------------------------------------- 1 | 2 | # Development 3 | 4 | ## Prerequisites 5 | 6 | * Go 1.19+ 7 | * Operator SDK `v1.28.0` 8 | * Bash or equivalent 9 | * Docker 10 | 11 | ### Run and test operator locally 12 | 13 | To run the operator locally on your machine (outside a container), invoke the following make target: 14 | 15 | ``` bash 16 | make install run 17 | ``` 18 | 19 | This will install the CRDs into your cluster, then run the operator on your machine. 20 | 21 | To run the unit tests, invoke the following make target: 22 | 23 | ``` bash 24 | make test 25 | ``` 26 | 27 | ### Build operator 28 | 29 | Use the following make target to build the operator. A container image wil be created locally. 30 | 31 | The name of the image is specified by the `IMG` variable defined in the `Makefile`. 32 | 33 | ``` bash 34 | make docker-build 35 | ``` 36 | 37 | ### Push the development container image. 38 | 39 | Override the name of the image to push by specifying the `IMG` variable. 40 | 41 | ``` bash 42 | make docker-push IMG=quay.io//argo-rollouts-manager:latest 43 | ``` 44 | 45 | ### Generate the bundle artifacts. 46 | 47 | Override the name of the development image by specifying the `IMG` variable. 48 | 49 | ``` bash 50 | rm -fr bundle/ 51 | make bundle IMG=quay.io//argo-rollouts-manager:latest 52 | ``` 53 | 54 | ### Build and push the development bundle image. 55 | 56 | Override the name of the bundle image by specifying the `BUNDLE_IMG` variable. 57 | 58 | ``` bash 59 | make bundle-build BUNDLE_IMG=quay.io//argo-rollouts-manager-bundle:latest 60 | make bundle-push BUNDLE_IMG=quay.io//argo-rollouts-manager-bundle:latest 61 | ``` 62 | 63 | ### Build and push the development catalog image. 64 | 65 | Override the name of the catalog image by specifying the `CATALOG_IMG` variable. 66 | Specify the bundle image to include using the `BUNDLE_IMG` variable 67 | 68 | ``` bash 69 | make catalog-build BUNDLE_IMG=quay.io//argo-rollouts-manager-bundle:latest CATALOG_IMG=quay.io//argo-rollouts-manager-catalog:latest 70 | make catalog-push CATALOG_IMG=quay.io//argo-rollouts-manager-catalog:latest 71 | ``` 72 | 73 | ### Deploy the CatalogSource 74 | 75 | # Note: Make sure all the images created above(operator, bundle, catalog) are public. 76 | 77 | ``` 78 | apiVersion: operators.coreos.com/v1alpha1 79 | kind: CatalogSource 80 | metadata: 81 | name: argo-rollouts-manager-catalog 82 | spec: 83 | sourceType: grpc 84 | image: quay.io//argo-rollouts-manager-catalog@sha256:dc3aaf1ae4148accac61c2d03abf6784a239f5350e244e931a0b8d414031adc4 # replace with your catalog image 85 | displayName: Argo Rollouts Manager 86 | publisher: Abhishek Veeramalla 87 | ``` 88 | 89 | ### Build and Verify RolloutManager Operator Docs 90 | 91 | #### Prerequisites 92 | 93 | - `Python3` 94 | 95 | Create a Python Virtual Environment. This is not mandatory, you can continue without creating a Virtual Environment as well. 96 | 97 | ```bash 98 | python3 -m venv doc 99 | ``` 100 | 101 | Get into the virtual environment, if you have created one using the above step. 102 | 103 | ```bash 104 | source doc/bin/activate 105 | ``` 106 | 107 | Install the required Python libraries 108 | 109 | ```bash 110 | pip3 install mkdocs 111 | pip3 install mkdocs-material 112 | ``` 113 | 114 | Start the `mkdocs` server locally to verify the UI changes. 115 | 116 | ```bash 117 | mkdocs serve 118 | ``` 119 | 120 | ### Run the e2e tests. 121 | 122 | Please refer the e2e tests [usage](../e2e-tests/usage.md) guide. -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: argo-rollouts-manager 10 | app.kubernetes.io/part-of: argo-rollouts-manager 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: argo-rollouts-manager 25 | app.kubernetes.io/part-of: argo-rollouts-manager 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /manager 71 | args: 72 | - --leader-elect 73 | image: controller:latest 74 | name: manager 75 | securityContext: 76 | allowPrivilegeEscalation: false 77 | capabilities: 78 | drop: 79 | - "ALL" 80 | livenessProbe: 81 | httpGet: 82 | path: /healthz 83 | port: 8081 84 | initialDelaySeconds: 15 85 | periodSeconds: 20 86 | readinessProbe: 87 | httpGet: 88 | path: /readyz 89 | port: 8081 90 | initialDelaySeconds: 5 91 | periodSeconds: 10 92 | # TODO(user): Configure the resources accordingly based on the project requirements. 93 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 94 | resources: 95 | limits: 96 | cpu: 500m 97 | memory: 128Mi 98 | requests: 99 | cpu: 10m 100 | memory: 64Mi 101 | serviceAccountName: controller-manager 102 | terminationGracePeriodSeconds: 10 103 | -------------------------------------------------------------------------------- /tests/e2e/fixture/rollouts/fixture.go: -------------------------------------------------------------------------------- 1 | package rollouts 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/argoproj-labs/argo-rollouts-manager/tests/e2e/fixture" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "sigs.k8s.io/yaml" 11 | ) 12 | 13 | var rolloutGVR = schema.GroupVersionResource{ 14 | Group: "argoproj.io", 15 | Version: "v1alpha1", 16 | Resource: "rollouts", 17 | } 18 | 19 | func buildGenericRolloutResource(name, namespace, activeService, previewService string) string { 20 | 21 | rolloutStr := ` 22 | apiVersion: argoproj.io/v1alpha1 23 | kind: Rollout 24 | metadata: 25 | name: ` + name + ` 26 | namespace: ` + namespace + ` 27 | spec: 28 | replicas: 2 29 | revisionHistoryLimit: 2 30 | selector: 31 | matchLabels: 32 | app: test-argo-app 33 | strategy: 34 | blueGreen: 35 | activeService: ` + activeService + ` 36 | autoPromotionEnabled: false 37 | previewService: ` + previewService + ` 38 | 39 | template: 40 | metadata: 41 | labels: 42 | app: test-argo-app 43 | spec: 44 | containers: 45 | - image: "quay.io/nginx/nginx-unprivileged@sha256:6d51e4a8e10dfe334f8e2d15bb81b1ed2580ea9cb874b644acc720eda7022b54" 46 | # From: https://quay.io/repository/nginx/nginx-unprivileged 1.27.3 47 | name: webserver-simple 48 | ports: 49 | - containerPort: 8080 50 | name: http 51 | protocol: TCP 52 | resources: {}` 53 | 54 | return rolloutStr 55 | } 56 | 57 | func CreateArgoRollout(ctx context.Context, name, namespace, activeService, previewService string) (string, error) { 58 | 59 | dynclient, err := fixture.GetDynamicClient() 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | rolloutStr := buildGenericRolloutResource(name, namespace, activeService, previewService) 65 | 66 | var un unstructured.Unstructured 67 | if err := yaml.UnmarshalStrict([]byte(rolloutStr), &un, yaml.DisallowUnknownFields); err != nil { 68 | return "", err 69 | } 70 | 71 | if _, err := dynclient.Resource(rolloutGVR).Namespace(namespace).Create(ctx, &un, metav1.CreateOptions{}); err != nil { 72 | return "", err 73 | } 74 | 75 | return rolloutStr, nil 76 | } 77 | 78 | func GetArgoRollout(ctx context.Context, name, namespace string) (*unstructured.Unstructured, error) { 79 | 80 | dynclient, err := fixture.GetDynamicClient() 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return dynclient.Resource(rolloutGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) 86 | 87 | } 88 | 89 | func DeleteArgoRollout(ctx context.Context, name, namespace string) error { 90 | 91 | dynclient, err := fixture.GetDynamicClient() 92 | if err != nil { 93 | return err 94 | } 95 | 96 | return dynclient.Resource(rolloutGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) 97 | } 98 | 99 | func HasEmptyStatus(ctx context.Context, name, namespace string) (bool, error) { 100 | 101 | rollout, err := GetArgoRollout(ctx, name, namespace) 102 | if err != nil { 103 | return false, err 104 | } 105 | 106 | mapVal := rollout.UnstructuredContent() 107 | 108 | if mapVal["status"] == nil { 109 | return true, nil 110 | } 111 | 112 | statusMapVal := (mapVal["status"]).(map[string]interface{}) 113 | 114 | return len(statusMapVal) == 0, nil 115 | } 116 | 117 | func HasStatusPhase(ctx context.Context, name, namespace string, expectedPhase string) (bool, error) { 118 | 119 | rollout, err := GetArgoRollout(ctx, name, namespace) 120 | if err != nil { 121 | return false, err 122 | } 123 | 124 | mapVal := rollout.UnstructuredContent() 125 | 126 | if mapVal["status"] == nil { 127 | return false, nil 128 | } 129 | 130 | statusMapVal := (mapVal["status"]).(map[string]interface{}) 131 | 132 | if statusMapVal["phase"] == nil { 133 | return false, nil 134 | } 135 | 136 | return (statusMapVal["phase"]).(string) == expectedPhase, nil 137 | 138 | } 139 | -------------------------------------------------------------------------------- /docs/crd_reference.md: -------------------------------------------------------------------------------- 1 | # RolloutManager Custom Resource 2 | 3 | This page provides the information about Argo Rollout Custom Resource specification. 4 | 5 | Name | Default | Description 6 | --- | --- | --- 7 | Env | [Empty] | Adds environment variables to the Rollouts controller. 8 | ExtraCommandArgs | [Empty] | Extra Command arguments allows user to pass command line arguments to rollouts controller. 9 | Image | `quay.io/argoproj/argo-rollouts` | The container image for the rollouts controller. This overrides the `ARGO_ROLLOUTS_IMAGE` environment variable. 10 | NodePlacement | [Empty] | Refer NodePlacement [Section](#nodeplacement) 11 | Version | *(recent rollouts version)* | The tag to use with the rollouts container image. 12 | 13 | ## NodePlacement 14 | 15 | The following properties are available for configuring the NodePlacement component. 16 | 17 | Name | Default | Description 18 | --- | --- | --- 19 | NodeSelector | [Empty] | A map of key value pairs for node selection. 20 | Tolerations | [Empty] | Tolerations allow pods to schedule on nodes with matching taints. 21 | 22 | ### Basic RolloutManager example 23 | 24 | ``` yaml 25 | apiVersion: argoproj.io/v1alpha1 26 | kind: RolloutManager 27 | metadata: 28 | name: argo-rollout 29 | labels: 30 | example: basic 31 | spec: {} 32 | ``` 33 | 34 | ### RolloutManager example with properties 35 | 36 | ``` yaml 37 | apiVersion: argoproj.io/v1alpha1 38 | kind: RolloutManager 39 | metadata: 40 | name: argo-rollout 41 | labels: 42 | example: with-properties 43 | spec: 44 | env: 45 | - name: "foo" 46 | value: "bar" 47 | extraCommandArgs: 48 | - --foo 49 | - bar 50 | image: "quay.io/random/my-rollout-image" 51 | version: "sha256:...." 52 | ``` 53 | 54 | ### RolloutManager with NodePlacement Example 55 | 56 | The following example sets a NodeSelector and tolerations using NodePlacement property in the RolloutManager CR. 57 | 58 | ``` yaml 59 | apiVersion: argoproj.io/v1alpha1 60 | kind: RolloutManager 61 | metadata: 62 | name: argo-rollout 63 | labels: 64 | example: nodeplacement-example 65 | spec: 66 | nodePlacement: 67 | nodeSelector: 68 | key1: value1 69 | tolerations: 70 | - key: key1 71 | operator: Equal 72 | value: value1 73 | effect: NoSchedule 74 | - key: key1 75 | operator: Equal 76 | value: value1 77 | effect: NoExecute 78 | ``` 79 | 80 | 81 | ### RolloutManager example with metadata for the resources generated 82 | 83 | You can provide labels and annotation for all the resources generated (Argo Rollouts controller, ConfigMap, etc.). 84 | 85 | ``` yaml 86 | apiVersion: argoproj.io/v1alpha1 87 | kind: RolloutManager 88 | metadata: 89 | name: argo-rollout 90 | labels: 91 | example: with-metadata-example 92 | spec: 93 | additionalMetadata: 94 | labels: 95 | mylabel: "true" 96 | annotations: 97 | myannotation: "myvalue" 98 | ``` 99 | 100 | 101 | ### RolloutManager example with resources requests/limits for the Argo Rollouts controller 102 | 103 | You can provide resources requests and limits for the Argo Rollouts controller. 104 | 105 | ``` yaml 106 | apiVersion: argoproj.io/v1alpha1 107 | kind: RolloutManager 108 | metadata: 109 | name: argo-rollout 110 | labels: 111 | example: with-resources-example 112 | spec: 113 | controllerResources: 114 | requests: 115 | memory: "64Mi" 116 | cpu: "250m" 117 | limits: 118 | memory: "128Mi" 119 | cpu: "500m" 120 | ``` 121 | 122 | 123 | ### RolloutManager example with an option to skip the argo rollouts notification secret deployment 124 | 125 | ``` yaml 126 | apiVersion: argoproj.io/v1alpha1 127 | kind: RolloutManager 128 | metadata: 129 | name: argo-rollout 130 | labels: 131 | example: with-metadata-example 132 | spec: 133 | skipNotificationSecretDeployment: true 134 | ``` 135 | 136 | 137 | ### RolloutManager example with metric and trafficManagement Plugins 138 | 139 | ``` yaml 140 | apiVersion: argoproj.io/v1alpha1 141 | kind: RolloutManager 142 | metadata: 143 | name: argo-rollout 144 | labels: 145 | example: with-plugins 146 | spec: 147 | plugins: 148 | trafficManagement: 149 | - name: argoproj-labs/gatewayAPI 150 | location: https://github.com/argoproj-labs/rollouts-plugin-trafficrouter-gatewayapi/releases/download/v0.4.0/gatewayapi-plugin-linux-amd64 151 | metric: 152 | - name: "argoproj-labs/sample-prometheus" 153 | location: https://github.com/argoproj-labs/sample-rollouts-metric-plugin/releases/download/v0.0.3/metric-plugin-linux-amd64 154 | sha256: a597a017a9a1394a31b3cbc33e08a071c88f0bd8 155 | ``` 156 | 157 | 158 | ### RolloutManager example with HA enabled 159 | 160 | ``` yaml 161 | apiVersion: argoproj.io/v1alpha1 162 | kind: RolloutManager 163 | metadata: 164 | name: argo-rollout 165 | labels: 166 | example: with-ha 167 | spec: 168 | ha: 169 | enabled: true 170 | ``` -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - endpoints 12 | - events 13 | - namespaces 14 | - pods 15 | - secrets 16 | - serviceaccounts 17 | - services 18 | - services/finalizers 19 | verbs: 20 | - create 21 | - delete 22 | - get 23 | - list 24 | - patch 25 | - update 26 | - watch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - deployments 31 | - podtemplates 32 | verbs: 33 | - get 34 | - list 35 | - watch 36 | - apiGroups: 37 | - "" 38 | resources: 39 | - pods/eviction 40 | verbs: 41 | - create 42 | - apiGroups: 43 | - apiextensions.k8s.io 44 | resources: 45 | - customresourcedefinitions 46 | verbs: 47 | - get 48 | - list 49 | - watch 50 | - apiGroups: 51 | - apisix.apache.org 52 | resources: 53 | - apisixroutes 54 | verbs: 55 | - get 56 | - update 57 | - watch 58 | - apiGroups: 59 | - appmesh.k8s.aws 60 | resources: 61 | - virtualnodes 62 | - virtualrouters 63 | verbs: 64 | - get 65 | - list 66 | - patch 67 | - update 68 | - watch 69 | - apiGroups: 70 | - appmesh.k8s.aws 71 | resources: 72 | - virtualservices 73 | verbs: 74 | - get 75 | - list 76 | - watch 77 | - apiGroups: 78 | - apps 79 | resources: 80 | - deployments 81 | - podtemplates 82 | - replicasets 83 | verbs: 84 | - create 85 | - delete 86 | - get 87 | - list 88 | - patch 89 | - update 90 | - watch 91 | - apiGroups: 92 | - apps 93 | resources: 94 | - deployments/finalizers 95 | verbs: 96 | - update 97 | - apiGroups: 98 | - argoproj.io 99 | resources: 100 | - analysisruns 101 | - analysisruns/finalizers 102 | - analysistemplates 103 | - clusteranalysistemplates 104 | - experiments 105 | - experiments/finalizers 106 | - rollouts 107 | - rollouts/finalizers 108 | - rollouts/scale 109 | - rollouts/status 110 | verbs: 111 | - create 112 | - delete 113 | - deletecollection 114 | - get 115 | - list 116 | - patch 117 | - update 118 | - watch 119 | - apiGroups: 120 | - argoproj.io 121 | resources: 122 | - rolloutmanagers 123 | verbs: 124 | - create 125 | - delete 126 | - get 127 | - list 128 | - patch 129 | - update 130 | - watch 131 | - apiGroups: 132 | - argoproj.io 133 | resources: 134 | - rolloutmanagers/finalizers 135 | verbs: 136 | - update 137 | - apiGroups: 138 | - argoproj.io 139 | resources: 140 | - rolloutmanagers/status 141 | verbs: 142 | - get 143 | - patch 144 | - update 145 | - apiGroups: 146 | - batch 147 | resources: 148 | - jobs 149 | verbs: 150 | - create 151 | - delete 152 | - get 153 | - list 154 | - patch 155 | - update 156 | - watch 157 | - apiGroups: 158 | - coordination.k8s.io 159 | resources: 160 | - leases 161 | verbs: 162 | - create 163 | - get 164 | - update 165 | - apiGroups: 166 | - elbv2.k8s.aws 167 | resources: 168 | - targetgroupbindings 169 | verbs: 170 | - get 171 | - list 172 | - apiGroups: 173 | - extensions 174 | resources: 175 | - ingresses 176 | verbs: 177 | - create 178 | - get 179 | - list 180 | - patch 181 | - watch 182 | - apiGroups: 183 | - getambassador.io 184 | - x.getambassador.io 185 | resources: 186 | - ambassadormappings 187 | - mappings 188 | verbs: 189 | - create 190 | - delete 191 | - get 192 | - list 193 | - update 194 | - watch 195 | - apiGroups: 196 | - monitoring.coreos.com 197 | resources: 198 | - servicemonitors 199 | verbs: 200 | - create 201 | - get 202 | - list 203 | - patch 204 | - update 205 | - watch 206 | - apiGroups: 207 | - networking.istio.io 208 | resources: 209 | - destinationrules 210 | - virtualservices 211 | verbs: 212 | - get 213 | - list 214 | - patch 215 | - update 216 | - watch 217 | - apiGroups: 218 | - networking.k8s.io 219 | resources: 220 | - ingresses 221 | verbs: 222 | - create 223 | - get 224 | - list 225 | - patch 226 | - update 227 | - watch 228 | - apiGroups: 229 | - rbac.authorization.k8s.io 230 | resources: 231 | - clusterrolebindings 232 | - clusterroles 233 | - rolebindings 234 | - roles 235 | verbs: 236 | - create 237 | - delete 238 | - get 239 | - list 240 | - patch 241 | - update 242 | - watch 243 | - apiGroups: 244 | - route.openshift.io 245 | resources: 246 | - routes 247 | verbs: 248 | - create 249 | - get 250 | - list 251 | - patch 252 | - update 253 | - watch 254 | - apiGroups: 255 | - split.smi-spec.io 256 | resources: 257 | - trafficsplits 258 | verbs: 259 | - create 260 | - get 261 | - patch 262 | - update 263 | - watch 264 | - apiGroups: 265 | - traefik.containo.us 266 | resources: 267 | - traefikservices 268 | verbs: 269 | - get 270 | - update 271 | - watch 272 | -------------------------------------------------------------------------------- /hack/run-upstream-argo-rollouts-e2e-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CURRENT_ROLLOUTS_VERSION=v1.8.3 4 | 5 | function cleanup { 6 | echo "* Cleaning up" 7 | killall main || true 8 | killall go || true 9 | } 10 | 11 | set -x 12 | set -e 13 | 14 | trap cleanup EXIT 15 | 16 | # Directory of bash script 17 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 18 | 19 | # 1) Clone a specific version of argo-rollouts into a temporary directory 20 | TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 21 | cd $TMP_DIR 22 | 23 | git clone https://github.com/argoproj/argo-rollouts 24 | cd argo-rollouts 25 | git checkout $CURRENT_ROLLOUTS_VERSION 26 | go mod tidy 27 | 28 | # 2a) Replace 'argoproj/rollouts-demo' image with 'quay.io/jgwest-redhat/rollouts-demo' in upstream E2E tests 29 | # - The original 'argoproj/rollouts-demo' repository only has amd64 images, thus some of the E2E tests will not work on Power/Z. 30 | # - 'quay.io/jgwest-redhat/rollouts-demo' is based on the same code, but built for other archs 31 | find "$TMP_DIR/argo-rollouts/test/e2e" -type f | xargs sed -i.bak 's/argoproj\/rollouts-demo/quay.io\/jgwest-redhat\/rollouts-demo/g' 32 | 33 | # 2b) Replace nginx images used by E2E tests with images from quay.io (thus no rate limiting) 34 | 35 | # quay.io/jgwest-redhat/nginx@sha256:07ab71a2c8e4ecb19a5a5abcfb3a4f175946c001c8af288b1aa766d67b0d05d2 is a copy of nginx:1.19-alpine 36 | 37 | find "$TMP_DIR/argo-rollouts/test/e2e" -type f | xargs sed -i.bak 's/nginx:1.19-alpine/quay.io\/jgwest-redhat\/nginx@sha256:07ab71a2c8e4ecb19a5a5abcfb3a4f175946c001c8af288b1aa766d67b0d05d2/g' 38 | 39 | find "$TMP_DIR/argo-rollouts/test/e2e" -type f | xargs sed -i.bak 's/nginx:1.14.2/quay.io\/jgwest-redhat\/nginx@sha256:07ab71a2c8e4ecb19a5a5abcfb3a4f175946c001c8af288b1aa766d67b0d05d2/g' 40 | 41 | # 2c) replace the rollouts-pod-template-hash of 'TestCanaryDynamicStableScale', since we have updated the image above 42 | find "$TMP_DIR/argo-rollouts/test/e2e" -type f | xargs sed -i.bak 's/868d98995b/5496d694d6/g' 43 | 44 | # replace the TestCanaryScaleDownOnAbort and TestCanaryScaleDownOnAbortNoTrafficRouting, for same reason 45 | find "$TMP_DIR/argo-rollouts/test/e2e" -type f | xargs sed -i.bak 's/66597877b7/6fcb5674b5/g' 46 | 47 | 48 | find "$TMP_DIR/argo-rollouts/test/e2e" -type f -name "*.bak" -delete 49 | 50 | # 3) Setup the Namespace 51 | 52 | kubectl delete ns argo-rollouts || true 53 | 54 | kubectl wait --timeout=5m --for=delete namespace/argo-rollouts 55 | 56 | kubectl create ns argo-rollouts 57 | kubectl config set-context --current --namespace=argo-rollouts 58 | 59 | 60 | # 4) Build, install, and start the argo-rollouts-manager controller 61 | cd $SCRIPT_DIR/.. 62 | 63 | 64 | # Only start the controller if SKIP_RUN_STEP is empty 65 | # - Otherwise, we assume that Argo Rollouts operator is already installed and running (for example, via OpenShift GitOps) 66 | if [ -z "$SKIP_RUN_STEP" ]; then 67 | make generate fmt vet install 68 | 69 | set +e 70 | 71 | rm -f /tmp/e2e-operator-run.log || true 72 | 73 | # Set namespaces used for cluster-scoped e2e tests 74 | export CLUSTER_SCOPED_ARGO_ROLLOUTS_NAMESPACES="argo-rollouts" 75 | 76 | go run ./cmd/main.go 2>&1 | tee /tmp/e2e-operator-run.log & 77 | 78 | set -e 79 | fi 80 | 81 | # 5) Install Argo Rollouts into the Namespace via RolloutManager CR 82 | 83 | cd $TMP_DIR/argo-rollouts 84 | 85 | cat << EOF > $TMP_DIR/rollout-manager.yaml 86 | apiVersion: argoproj.io/v1alpha1 87 | kind: RolloutManager 88 | metadata: 89 | name: argo-rollout 90 | spec: 91 | extraCommandArgs: 92 | - "--loglevel" 93 | - "debug" 94 | - "--kloglevel" 95 | - "6" 96 | - "--instance-id" 97 | - "argo-rollouts-e2e" 98 | EOF 99 | 100 | kubectl apply -f $TMP_DIR/rollout-manager.yaml 101 | 102 | echo "* Waiting for Argo Rollouts Deployment to exist" 103 | 104 | until kubectl get -n argo-rollouts deployment/argo-rollouts 105 | do 106 | sleep 1s 107 | done 108 | 109 | kubectl wait --for=condition=Available --timeout=10m -n argo-rollouts deployment/argo-rollouts 110 | 111 | kubectl apply -f test/e2e/crds 112 | 113 | # Required because the rollouts containers run as root, and OpenShift's default security policy doesn't like that 114 | oc adm policy add-scc-to-user anyuid -z argo-rollouts -n argo-rollouts || true 115 | oc adm policy add-scc-to-user anyuid -z default -n argo-rollouts || true 116 | 117 | 118 | # 6) Run the E2E tests 119 | rm -f /tmp/test-e2e.log 120 | 121 | set +e 122 | 123 | make test-e2e | tee /tmp/test-e2e.log 124 | 125 | set +x 126 | 127 | # 7) Check and report the results for unexpected failures 128 | 129 | echo "-----------------------------------------------------------------" 130 | echo 131 | echo "These were the tests that succeeded:" 132 | echo 133 | cat /tmp/test-e2e.log | grep "PASS" | sort 134 | echo 135 | echo "These were the tests that failed:" 136 | echo 137 | cat /tmp/test-e2e.log | grep " --- FAIL:" | grep -v "re-run" | sort -u 138 | echo 139 | echo 140 | 141 | set -e 142 | 143 | # Call a small Go script to verify expected test failures. See Go file for details. 144 | "$SCRIPT_DIR/verify-rollouts-e2e-tests/verify-e2e-test-results.sh" /tmp/test-e2e.log 145 | 146 | echo "* SUCCESS: No unexpected errors occurred." 147 | 148 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/argoproj-labs/argo-rollouts-manager 2 | 3 | go 1.24.5 4 | 5 | require ( 6 | github.com/distribution/reference v0.6.0 7 | github.com/go-logr/logr v1.4.2 8 | github.com/onsi/ginkgo/v2 v2.22.0 9 | github.com/onsi/gomega v1.36.1 10 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.73.2 11 | go.uber.org/zap v1.27.0 12 | gopkg.in/yaml.v2 v2.4.0 13 | k8s.io/api v0.33.1 14 | k8s.io/apiextensions-apiserver v0.33.1 15 | k8s.io/apimachinery v0.33.1 16 | k8s.io/client-go v0.33.1 17 | sigs.k8s.io/controller-runtime v0.21.0 18 | sigs.k8s.io/yaml v1.4.0 19 | ) 20 | 21 | require ( 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 25 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 26 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 27 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 28 | github.com/fsnotify/fsnotify v1.7.0 // indirect 29 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 30 | github.com/go-logr/zapr v1.3.0 // indirect 31 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 32 | github.com/go-openapi/jsonreference v0.20.2 // indirect 33 | github.com/go-openapi/swag v0.23.0 // indirect 34 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 35 | github.com/gogo/protobuf v1.3.2 // indirect 36 | github.com/google/btree v1.1.3 // indirect 37 | github.com/google/gnostic-models v0.6.9 // indirect 38 | github.com/google/go-cmp v0.7.0 // indirect 39 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect 40 | github.com/google/uuid v1.6.0 // indirect 41 | github.com/josharian/intern v1.0.0 // indirect 42 | github.com/json-iterator/go v1.1.12 // indirect 43 | github.com/mailru/easyjson v0.7.7 // indirect 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 | github.com/modern-go/reflect2 v1.0.2 // indirect 46 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 47 | github.com/opencontainers/go-digest v1.0.0 // indirect 48 | github.com/pkg/errors v0.9.1 // indirect 49 | github.com/prometheus/client_golang v1.22.0 // indirect 50 | github.com/prometheus/client_model v0.6.1 // indirect 51 | github.com/prometheus/common v0.62.0 // indirect 52 | github.com/prometheus/procfs v0.15.1 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/x448/float16 v0.8.4 // indirect 55 | go.uber.org/multierr v1.11.0 // indirect 56 | golang.org/x/net v0.38.0 // indirect 57 | golang.org/x/oauth2 v0.27.0 // indirect 58 | golang.org/x/sync v0.12.0 // indirect 59 | golang.org/x/sys v0.31.0 // indirect 60 | golang.org/x/term v0.30.0 // indirect 61 | golang.org/x/text v0.23.0 // indirect 62 | golang.org/x/time v0.9.0 // indirect 63 | golang.org/x/tools v0.26.0 // indirect 64 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 65 | google.golang.org/protobuf v1.36.5 // indirect 66 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 67 | gopkg.in/inf.v0 v0.9.1 // indirect 68 | gopkg.in/yaml.v3 v3.0.1 // indirect 69 | k8s.io/klog/v2 v2.130.1 // indirect 70 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 71 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 72 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 73 | sigs.k8s.io/randfill v1.0.0 // indirect 74 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 75 | ) 76 | 77 | replace ( 78 | github.com/golang/protobuf => github.com/golang/protobuf v1.5.4 79 | github.com/grpc-ecosystem/grpc-gateway => github.com/grpc-ecosystem/grpc-gateway v1.16.0 80 | 81 | // Avoid CVE-2022-3064 82 | gopkg.in/yaml.v2 => gopkg.in/yaml.v2 v2.4.0 83 | 84 | // Avoid CVE-2022-28948 85 | gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 86 | 87 | k8s.io/api => k8s.io/api v0.33.1 88 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.33.1 89 | k8s.io/apimachinery => k8s.io/apimachinery v0.33.1 90 | k8s.io/apiserver => k8s.io/apiserver v0.33.1 91 | k8s.io/cli-runtime => k8s.io/cli-runtime v0.33.1 92 | k8s.io/client-go => k8s.io/client-go v0.33.1 93 | k8s.io/cloud-provider => k8s.io/cloud-provider v0.33.1 94 | k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.33.1 95 | k8s.io/code-generator => k8s.io/code-generator v0.33.1 96 | k8s.io/component-base => k8s.io/component-base v0.33.1 97 | k8s.io/component-helpers => k8s.io/component-helpers v0.33.1 98 | k8s.io/controller-manager => k8s.io/controller-manager v0.33.1 99 | k8s.io/cri-api => k8s.io/cri-api v0.33.1 100 | k8s.io/cri-client => k8s.io/cri-client v0.33.1 101 | k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.33.1 102 | k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.33.1 103 | k8s.io/endpointslice => k8s.io/endpointslice v0.33.1 104 | k8s.io/externaljwt => k8s.io/externaljwt v0.33.1 105 | k8s.io/kms => k8s.io/kms v0.33.1 106 | k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.33.1 107 | k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.33.1 108 | k8s.io/kube-proxy => k8s.io/kube-proxy v0.33.1 109 | k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.33.1 110 | k8s.io/kubectl => k8s.io/kubectl v0.33.1 111 | k8s.io/kubelet => k8s.io/kubelet v0.33.1 112 | k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.33.1 113 | k8s.io/metrics => k8s.io/metrics v0.33.1 114 | k8s.io/mount-utils => k8s.io/mount-utils v0.33.1 115 | k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.33.1 116 | k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.33.1 117 | k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.33.1 118 | k8s.io/sample-controller => k8s.io/sample-controller v0.33.1 119 | ) 120 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | "strings" 23 | 24 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 25 | // to ensure that exec-entrypoint and run can make use of them. 26 | _ "k8s.io/client-go/plugin/pkg/client/auth" 27 | 28 | "k8s.io/apimachinery/pkg/runtime" 29 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 30 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/healthz" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | "sigs.k8s.io/controller-runtime/pkg/metrics/server" 35 | "sigs.k8s.io/controller-runtime/pkg/webhook" 36 | 37 | rolloutsmanagerv1alpha1 "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 38 | 39 | controllers "github.com/argoproj-labs/argo-rollouts-manager/controllers" 40 | monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 41 | crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 42 | //+kubebuilder:scaffold:imports 43 | ) 44 | 45 | var ( 46 | scheme = runtime.NewScheme() 47 | setupLog = ctrl.Log.WithName("setup") 48 | ) 49 | 50 | func init() { 51 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 52 | 53 | utilruntime.Must(rolloutsmanagerv1alpha1.AddToScheme(scheme)) 54 | //+kubebuilder:scaffold:scheme 55 | } 56 | 57 | func main() { 58 | var metricsAddr string 59 | var enableLeaderElection bool 60 | var probeAddr string 61 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 62 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 63 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 64 | "Enable leader election for controller manager. "+ 65 | "Enabling this will ensure there is only one active controller manager.") 66 | opts := zap.Options{ 67 | Development: true, 68 | } 69 | opts.BindFlags(flag.CommandLine) 70 | flag.Parse() 71 | 72 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 73 | 74 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 75 | Scheme: scheme, 76 | Metrics: server.Options{ 77 | BindAddress: metricsAddr, 78 | }, 79 | WebhookServer: webhook.NewServer(webhook.Options{ 80 | Port: 9443, 81 | }), 82 | HealthProbeBindAddress: probeAddr, 83 | LeaderElection: enableLeaderElection, 84 | LeaderElectionID: "rolloutsmanager.argoproj.io", 85 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 86 | // when the Manager ends. This requires the binary to immediately end when the 87 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 88 | // speeds up voluntary leader transitions as the new leader don't have to wait 89 | // LeaseDuration time first. 90 | // 91 | // In the default scaffold provided, the program ends immediately after 92 | // the manager stops, so would be fine to enable this option. However, 93 | // if you are doing or is intended to do any operation such as perform cleanups 94 | // after the manager stops then its usage might be unsafe. 95 | // LeaderElectionReleaseOnCancel: true, 96 | }) 97 | if err != nil { 98 | setupLog.Error(err, "unable to start manager") 99 | os.Exit(1) 100 | } 101 | 102 | openShiftRoutePluginLocation := os.Getenv("OPENSHIFT_ROUTE_PLUGIN_LOCATION") 103 | 104 | if openShiftRoutePluginLocation == "" { 105 | openShiftRoutePluginLocation = controllers.DefaultOpenShiftRoutePluginURL 106 | } 107 | 108 | isNamespaceScoped := strings.ToLower(os.Getenv(controllers.NamespaceScopedArgoRolloutsController)) == "true" 109 | 110 | if isNamespaceScoped { 111 | setupLog.Info("Running in namespaced-scoped mode") 112 | } else { 113 | setupLog.Info("Running in cluster-scoped mode") 114 | } 115 | 116 | if err := monitoringv1.AddToScheme(mgr.GetScheme()); err != nil { 117 | setupLog.Error(err, "") 118 | os.Exit(1) 119 | } 120 | 121 | if err := crdv1.AddToScheme(mgr.GetScheme()); err != nil { 122 | setupLog.Error(err, "") 123 | os.Exit(1) 124 | } 125 | 126 | if err = (&controllers.RolloutManagerReconciler{ 127 | Client: mgr.GetClient(), 128 | Scheme: mgr.GetScheme(), 129 | OpenShiftRoutePluginLocation: openShiftRoutePluginLocation, 130 | NamespaceScopedArgoRolloutsController: isNamespaceScoped, 131 | }).SetupWithManager(mgr); err != nil { 132 | setupLog.Error(err, "unable to create controller", "controller", "RolloutManager") 133 | os.Exit(1) 134 | } 135 | //+kubebuilder:scaffold:builder 136 | 137 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 138 | setupLog.Error(err, "unable to set up health check") 139 | os.Exit(1) 140 | } 141 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 142 | setupLog.Error(err, "unable to set up ready check") 143 | os.Exit(1) 144 | } 145 | 146 | setupLog.Info("starting manager") 147 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 148 | setupLog.Error(err, "problem running manager") 149 | os.Exit(1) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /controllers/reconcile.go: -------------------------------------------------------------------------------- 1 | package rollouts 2 | 3 | import ( 4 | "context" 5 | 6 | rolloutsmanagerv1alpha1 "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 7 | rbacv1 "k8s.io/api/rbac/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // reconcileStatusResult is returned by 'reconcileRolloutsManager', and related functions, to control what values to set on the .status field of RolloutManager, after reconciliation. Values set in reconcileStatusResult will be set on RolloutManager's .status field. 12 | type reconcileStatusResult struct { 13 | 14 | // condition to be set on RolloutManager's .status.condition 15 | condition metav1.Condition 16 | 17 | // rolloutController: if non-nil, .status.rolloutController will be set to this value, after call to reconcileRolloutsManager 18 | rolloutController *rolloutsmanagerv1alpha1.RolloutControllerPhase 19 | 20 | // phase: if non-nil, .status.phase will be set to this value, after call to reconcileRolloutsManager 21 | phase *rolloutsmanagerv1alpha1.RolloutControllerPhase 22 | } 23 | 24 | func (r *RolloutManagerReconciler) reconcileRolloutsManager(ctx context.Context, cr rolloutsmanagerv1alpha1.RolloutManager) (reconcileStatusResult, error) { 25 | 26 | log.Info("validating RolloutManager's scope") 27 | if rr, err := validateRolloutsScope(cr, r.NamespaceScopedArgoRolloutsController); err != nil { 28 | if invalidRolloutScope(err) { 29 | rr.condition = createCondition(err.Error(), rolloutsmanagerv1alpha1.RolloutManagerReasonInvalidScoped) 30 | return *rr, nil 31 | } 32 | 33 | if invalidRolloutNamespace(err) { 34 | rr.condition = createCondition(err.Error(), rolloutsmanagerv1alpha1.RolloutManagerReasonInvalidNamespace) 35 | return *rr, nil 36 | } 37 | 38 | log.Error(err, "failed to validate RolloutManager's scope.") 39 | return wrapCondition(createCondition(err.Error())), err 40 | } 41 | 42 | log.Info("searching for existing RolloutManagers") 43 | if res, err := checkForExistingRolloutManager(ctx, r.Client, cr); err != nil { 44 | if multipleRolloutManagersExist(err) { 45 | 46 | res.condition = createCondition(err.Error(), rolloutsmanagerv1alpha1.RolloutManagerReasonMultipleClusterScopedRolloutManager) 47 | 48 | return *res, nil 49 | } 50 | log.Error(err, "failed to validate multiple RolloutManagers.") 51 | return wrapCondition(createCondition(err.Error())), err 52 | } 53 | 54 | log.Info("reconciling Rollouts ServiceAccount") 55 | sa, err := r.reconcileRolloutsServiceAccount(ctx, cr) 56 | if err != nil { 57 | log.Error(err, "failed to reconcile Rollout's ServiceAccount.") 58 | return wrapCondition(createCondition(err.Error())), err 59 | } 60 | 61 | var role *rbacv1.Role 62 | var clusterRole *rbacv1.ClusterRole 63 | 64 | if cr.Spec.NamespaceScoped { 65 | log.Info("reconciling Rollouts Roles") 66 | role, err = r.reconcileRolloutsRole(ctx, cr) 67 | if err != nil { 68 | log.Error(err, "failed to reconcile Rollout's Role.") 69 | return wrapCondition(createCondition(err.Error())), err 70 | } 71 | } else { 72 | log.Info("reconciling Rollouts ClusterRoles") 73 | clusterRole, err = r.reconcileRolloutsClusterRole(ctx, cr) 74 | if err != nil { 75 | log.Error(err, "failed to reconcile Rollout's ClusterRoles.") 76 | return wrapCondition(createCondition(err.Error())), err 77 | } 78 | } 79 | 80 | log.Info("reconciling aggregate-to-admin ClusterRole") 81 | if err := r.reconcileRolloutsAggregateToAdminClusterRole(ctx, cr); err != nil { 82 | log.Error(err, "failed to reconcile Rollout's aggregate-to-admin ClusterRoles.") 83 | return wrapCondition(createCondition(err.Error())), err 84 | } 85 | 86 | log.Info("reconciling aggregate-to-edit ClusterRole") 87 | if err := r.reconcileRolloutsAggregateToEditClusterRole(ctx, cr); err != nil { 88 | log.Error(err, "failed to reconcile Rollout's aggregate-to-edit ClusterRoles.") 89 | return wrapCondition(createCondition(err.Error())), err 90 | } 91 | 92 | log.Info("reconciling aggregate-to-view ClusterRole") 93 | if err := r.reconcileRolloutsAggregateToViewClusterRole(ctx, cr); err != nil { 94 | log.Error(err, "failed to reconcile Rollout's aggregate-to-view ClusterRoles.") 95 | return wrapCondition(createCondition(err.Error())), err 96 | } 97 | 98 | if cr.Spec.NamespaceScoped { 99 | log.Info("reconciling Rollouts RoleBindings") 100 | if err := r.reconcileRolloutsRoleBinding(ctx, cr, role, sa); err != nil { 101 | log.Error(err, "failed to reconcile Rollout's RoleBindings.") 102 | return wrapCondition(createCondition(err.Error())), err 103 | } 104 | } else { 105 | log.Info("reconciling Rollouts ClusterRoleBinding") 106 | if err := r.reconcileRolloutsClusterRoleBinding(ctx, clusterRole, sa, cr); err != nil { 107 | log.Error(err, "failed to reconcile Rollout's ClusterRoleBinding.") 108 | return wrapCondition(createCondition(err.Error())), err 109 | } 110 | } 111 | 112 | log.Info("reconciling Rollouts Secret") 113 | if err := r.reconcileRolloutsSecrets(ctx, cr); err != nil { 114 | log.Error(err, "failed to reconcile Rollout's Secret.") 115 | return wrapCondition(createCondition(err.Error())), err 116 | } 117 | 118 | log.Info("reconciling ConfigMap for plugins") 119 | if err := r.reconcileConfigMap(ctx, cr); err != nil { 120 | log.Error(err, "failed to reconcile Rollout's ConfigMap.") 121 | return wrapCondition(createCondition(err.Error())), err 122 | } 123 | 124 | log.Info("reconciling Rollouts Deployment") 125 | if err := r.reconcileRolloutsDeployment(ctx, cr, *sa); err != nil { 126 | log.Error(err, "failed to reconcile Rollout's Deployment.") 127 | return wrapCondition(createCondition(err.Error())), err 128 | } 129 | 130 | log.Info("reconciling Rollouts Metrics Service") 131 | if err := r.reconcileRolloutsMetricsServiceAndMonitor(ctx, cr); err != nil { 132 | log.Error(err, "failed to reconcile Rollout's Metrics Service.") 133 | return wrapCondition(createCondition(err.Error())), err 134 | } 135 | 136 | log.Info("reconciling status of workloads") 137 | rr, err := r.determineStatusPhase(ctx, cr) 138 | if err != nil { 139 | log.Error(err, "failed to reconcile status of workloads.") 140 | return wrapCondition(createCondition(err.Error())), err 141 | } 142 | 143 | rr.condition = createCondition("") // success 144 | 145 | return rr, nil 146 | } 147 | -------------------------------------------------------------------------------- /api/v1alpha1/argorollouts_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // RolloutManagerSpec defines the desired state of Argo Rollouts 25 | type RolloutManagerSpec struct { 26 | 27 | // Env lets you specify environment for Rollouts pods 28 | Env []corev1.EnvVar `json:"env,omitempty"` 29 | 30 | // Extra Command arguments that would append to the Rollouts 31 | // ExtraCommandArgs will not be added, if one of these commands is already part of the Rollouts command 32 | // with same or different value. 33 | ExtraCommandArgs []string `json:"extraCommandArgs,omitempty"` 34 | 35 | // Image defines Argo Rollouts controller image (optional) 36 | Image string `json:"image,omitempty"` 37 | 38 | // NodePlacement defines NodeSelectors and Taints for Rollouts workloads 39 | NodePlacement *RolloutsNodePlacementSpec `json:"nodePlacement,omitempty"` 40 | 41 | // Version defines Argo Rollouts controller tag (optional) 42 | Version string `json:"version,omitempty"` 43 | 44 | // NamespaceScoped lets you specify if RolloutManager has to watch a namespace or the whole cluster 45 | NamespaceScoped bool `json:"namespaceScoped,omitempty"` 46 | 47 | // Metadata to apply to the generated resources 48 | AdditionalMetadata *ResourceMetadata `json:"additionalMetadata,omitempty"` 49 | 50 | // Resources requests/limits for Argo Rollout controller 51 | ControllerResources *corev1.ResourceRequirements `json:"controllerResources,omitempty"` 52 | 53 | // SkipNotificationSecretDeployment lets you specify if the argo notification secret should be deployed 54 | SkipNotificationSecretDeployment bool `json:"skipNotificationSecretDeployment,omitempty"` 55 | 56 | // Plugins specify the traffic and metric plugins in Argo Rollout 57 | Plugins Plugins `json:"plugins,omitempty"` 58 | 59 | // HA options for High Availability support for Rollouts. 60 | HA *RolloutManagerHASpec `json:"ha,omitempty"` 61 | 62 | // ImagePullPolicy specifies the image pull policy for the Rollouts controller. 63 | // Valid values are: Always, IfNotPresent, Never 64 | // +kubebuilder:validation:Enum=Always;IfNotPresent;Never 65 | // +optional 66 | ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` 67 | } 68 | 69 | // Plugin is used to integrate traffic management and metric plugins into the Argo Rollouts controller. For more information on these plugins, see the upstream Argo Rollouts documentation. 70 | type Plugin struct { 71 | // Name of the plugin, it must match the name required by the plugin so it can find its configuration 72 | Name string `json:"name"` 73 | // Location supports http(s):// urls and file://, though file:// requires the plugin be available on the filesystem 74 | Location string `json:"location"` 75 | // SHA256 is an optional sha256 checksum of the plugin executable 76 | SHA256 string `json:"sha256,omitempty"` 77 | } 78 | 79 | type Plugins struct { 80 | // TrafficManagement holds a list of traffic management plugins used to control traffic routing during rollouts. 81 | TrafficManagement []Plugin `json:"trafficManagement,omitempty"` 82 | // Metric holds a list of metric plugins used to gather and report metrics during rollouts. 83 | Metric []Plugin `json:"metric,omitempty"` 84 | } 85 | 86 | // RolloutManagerHASpec specifies HA options for High Availability support for Rollouts. 87 | type RolloutManagerHASpec struct { 88 | // Enabled will toggle HA support globally for RolloutManager. 89 | Enabled bool `json:"enabled"` 90 | } 91 | 92 | // ArgoRolloutsNodePlacementSpec is used to specify NodeSelector and Tolerations for Rollouts workloads 93 | type RolloutsNodePlacementSpec struct { 94 | // NodeSelector is a field of PodSpec, it is a map of key value pairs used for node selection 95 | NodeSelector map[string]string `json:"nodeSelector,omitempty"` 96 | // Tolerations allow the pods to schedule onto nodes with matching taints 97 | Tolerations []corev1.Toleration `json:"tolerations,omitempty"` 98 | } 99 | 100 | // RolloutManagerStatus defines the observed state of RolloutManager 101 | type RolloutManagerStatus struct { 102 | // RolloutController is a simple, high-level summary of where the RolloutController component is in its lifecycle. 103 | // There are three possible RolloutController values: 104 | // Pending: The RolloutController component has been accepted by the Kubernetes system, but one or more of the required resources have not been created. 105 | // Running: All of the required Pods for the RolloutController component are in a Ready state. 106 | // Unknown: The state of the RolloutController component could not be obtained. 107 | RolloutController RolloutControllerPhase `json:"rolloutController,omitempty"` 108 | // Phase is a simple, high-level summary of where the RolloutManager is in its lifecycle. 109 | // There are three possible phase values: 110 | // Pending: The RolloutManager has been accepted by the Kubernetes system, but one or more of the required resources have not been created. 111 | // Available: All of the resources for the RolloutManager are ready. 112 | // Unknown: The state of the RolloutManager phase could not be obtained. 113 | Phase RolloutControllerPhase `json:"phase,omitempty"` 114 | 115 | // Conditions is an array of the RolloutManager's status conditions 116 | Conditions []metav1.Condition `json:"conditions,omitempty"` 117 | } 118 | 119 | type RolloutControllerPhase string 120 | 121 | const ( 122 | PhaseAvailable RolloutControllerPhase = "Available" 123 | PhasePending RolloutControllerPhase = "Pending" 124 | PhaseUnknown RolloutControllerPhase = "Unknown" 125 | PhaseFailure RolloutControllerPhase = "Failure" 126 | ) 127 | 128 | const ( 129 | RolloutManagerConditionType = "Reconciled" 130 | ) 131 | 132 | const ( 133 | RolloutManagerReasonSuccess = "Success" 134 | RolloutManagerReasonErrorOccurred = "ErrorOccurred" 135 | RolloutManagerReasonMultipleClusterScopedRolloutManager = "MultipleClusterScopedRolloutManager" 136 | RolloutManagerReasonInvalidScoped = "InvalidRolloutManagerScope" 137 | RolloutManagerReasonInvalidNamespace = "InvalidRolloutManagerNamespace" 138 | ) 139 | 140 | type ResourceMetadata struct { 141 | // Annotations to add to the resources during its creation. 142 | // +optional 143 | Annotations map[string]string `json:"annotations,omitempty"` 144 | // Labels to add to the resources during its creation. 145 | // +optional 146 | Labels map[string]string `json:"labels,omitempty"` 147 | } 148 | 149 | //+kubebuilder:object:root=true 150 | //+kubebuilder:subresource:status 151 | 152 | // RolloutManager is the Schema for the RolloutManagers API 153 | type RolloutManager struct { 154 | metav1.TypeMeta `json:",inline"` 155 | metav1.ObjectMeta `json:"metadata,omitempty"` 156 | 157 | Spec RolloutManagerSpec `json:"spec,omitempty"` 158 | Status RolloutManagerStatus `json:"status,omitempty"` 159 | } 160 | 161 | //+kubebuilder:object:root=true 162 | 163 | // RolloutManagerList contains a list of RolloutManagers 164 | type RolloutManagerList struct { 165 | metav1.TypeMeta `json:",inline"` 166 | metav1.ListMeta `json:"metadata,omitempty"` 167 | Items []RolloutManager `json:"items"` 168 | } 169 | 170 | func init() { 171 | SchemeBuilder.Register(&RolloutManager{}, &RolloutManagerList{}) 172 | } 173 | -------------------------------------------------------------------------------- /controllers/configmap.go: -------------------------------------------------------------------------------- 1 | package rollouts 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | 7 | "fmt" 8 | "reflect" 9 | 10 | rolloutsmanagerv1alpha1 "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 11 | "gopkg.in/yaml.v2" 12 | appsv1 "k8s.io/api/apps/v1" 13 | corev1 "k8s.io/api/core/v1" 14 | 15 | "k8s.io/apimachinery/pkg/api/errors" 16 | 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/apimachinery/pkg/types" 19 | "sigs.k8s.io/controller-runtime/pkg/client" 20 | ) 21 | 22 | // From https://argo-rollouts.readthedocs.io/en/stable/features/traffic-management/plugins/ 23 | const TrafficRouterPluginConfigMapKey = "trafficRouterPlugins" 24 | const MetricPluginConfigMapKey = "metricPlugins" 25 | 26 | // Reconcile the Rollouts Default Config Map. 27 | func (r *RolloutManagerReconciler) reconcileConfigMap(ctx context.Context, cr rolloutsmanagerv1alpha1.RolloutManager) error { 28 | 29 | if r.OpenShiftRoutePluginLocation == "" { // sanity test the plugin value 30 | return fmt.Errorf("OpenShift Route Plugin location is not set") 31 | } 32 | 33 | desiredConfigMap := &corev1.ConfigMap{ 34 | ObjectMeta: metav1.ObjectMeta{ 35 | Name: DefaultRolloutsConfigMapName, 36 | Namespace: cr.Namespace, 37 | Labels: map[string]string{ 38 | "app.kubernetes.io/name": DefaultRolloutsConfigMapName, 39 | }, 40 | }, 41 | } 42 | 43 | setRolloutsLabelsAndAnnotationsToObject(&desiredConfigMap.ObjectMeta, cr) 44 | if r.ResourceLabels != nil { 45 | setCustomLabels(&desiredConfigMap.ObjectMeta, r.ResourceLabels) 46 | } 47 | 48 | trafficRouterPluginsMap := map[string]pluginItem{ 49 | OpenShiftRolloutPluginName: { 50 | Name: OpenShiftRolloutPluginName, 51 | Location: r.OpenShiftRoutePluginLocation, 52 | }, 53 | } 54 | 55 | // Append traffic management plugins specified in RolloutManager CR 56 | for _, plugin := range cr.Spec.Plugins.TrafficManagement { 57 | // Prevent adding or modifying the OpenShiftRoutePluginName through the CR 58 | if plugin.Name == OpenShiftRolloutPluginName { 59 | return fmt.Errorf("the plugin %s cannot be modified or added through the RolloutManager CR", OpenShiftRolloutPluginName) 60 | } 61 | // Check for duplicate traffic plugins 62 | if _, exists := trafficRouterPluginsMap[plugin.Name]; !exists { 63 | trafficRouterPluginsMap[plugin.Name] = pluginItem{ 64 | Name: plugin.Name, 65 | Location: plugin.Location, 66 | } 67 | } 68 | } 69 | 70 | // Sort trafficRouterPluginsMap keys for deterministic ordering 71 | trafficRouterPluginKeys := make([]string, 0, len(trafficRouterPluginsMap)) 72 | for key := range trafficRouterPluginsMap { 73 | trafficRouterPluginKeys = append(trafficRouterPluginKeys, key) 74 | } 75 | sort.Strings(trafficRouterPluginKeys) 76 | 77 | // Convert trafficRouterPluginsMap to sorted slice 78 | trafficRouterPlugins := make([]pluginItem, 0, len(trafficRouterPluginsMap)) 79 | for _, key := range trafficRouterPluginKeys { 80 | trafficRouterPlugins = append(trafficRouterPlugins, trafficRouterPluginsMap[key]) 81 | } 82 | 83 | // Append metric plugins specified in RolloutManager CR 84 | metricPluginsMap := map[string]pluginItem{} 85 | for _, plugin := range cr.Spec.Plugins.Metric { 86 | // Check for duplicate metric plugins 87 | if _, exists := metricPluginsMap[plugin.Name]; !exists { 88 | metricPluginsMap[plugin.Name] = pluginItem{ 89 | Name: plugin.Name, 90 | Location: plugin.Location, 91 | Sha256: plugin.SHA256, 92 | } 93 | } 94 | } 95 | 96 | // Sort metricPluginsMap keys for deterministic ordering 97 | metricPluginKeys := make([]string, 0, len(metricPluginsMap)) 98 | for key := range metricPluginsMap { 99 | metricPluginKeys = append(metricPluginKeys, key) 100 | } 101 | sort.Strings(metricPluginKeys) 102 | 103 | // Convert metricPluginsMap to sorted slice 104 | metricPlugins := make([]pluginItem, 0, len(metricPluginsMap)) 105 | for _, key := range metricPluginKeys { 106 | metricPlugins = append(metricPlugins, metricPluginsMap[key]) 107 | } 108 | 109 | desiredTrafficRouterPluginString, err := yaml.Marshal(trafficRouterPlugins) 110 | if err != nil { 111 | return fmt.Errorf("error marshalling trafficRouterPlugin to string %s", err) 112 | } 113 | 114 | desiredMetricPluginString, err := yaml.Marshal(metricPlugins) 115 | if err != nil { 116 | return fmt.Errorf("error marshalling metricPlugins to string %s", err) 117 | } 118 | 119 | desiredConfigMap.Data = map[string]string{ 120 | TrafficRouterPluginConfigMapKey: string(desiredTrafficRouterPluginString), 121 | MetricPluginConfigMapKey: string(desiredMetricPluginString), 122 | } 123 | 124 | actualConfigMap := &corev1.ConfigMap{} 125 | 126 | if err := fetchObject(ctx, r.Client, cr.Namespace, desiredConfigMap.Name, actualConfigMap); err != nil { 127 | if errors.IsNotFound(err) { 128 | // ConfigMap is not present, create default config map 129 | log.Info("configMap not found, creating default configmap with openshift route plugin information") 130 | return r.Client.Create(ctx, desiredConfigMap) 131 | } 132 | return fmt.Errorf("failed to get the serviceAccount associated with %s: %w", desiredConfigMap.Name, err) 133 | } 134 | 135 | // Unmarshal the existing plugin data from the actual ConfigMap 136 | var actualTrafficRouterPlugins, actualMetricPlugins []pluginItem 137 | if err = yaml.Unmarshal([]byte(actualConfigMap.Data[TrafficRouterPluginConfigMapKey]), &actualTrafficRouterPlugins); err != nil { 138 | return fmt.Errorf("failed to unmarshal traffic router plugins: %s", err) 139 | } 140 | if err = yaml.Unmarshal([]byte(actualConfigMap.Data[MetricPluginConfigMapKey]), &actualMetricPlugins); err != nil { 141 | return fmt.Errorf("failed to unmarshal metric plugins: %s", err) 142 | } 143 | 144 | // Check if an update is needed by comparing desired and actual plugin configurations 145 | updateNeeded := !reflect.DeepEqual(actualTrafficRouterPlugins, trafficRouterPlugins) || !reflect.DeepEqual(actualMetricPlugins, metricPlugins) 146 | 147 | if updateNeeded { 148 | // Update the ConfigMap's plugin data with the new values 149 | actualConfigMap.Data[TrafficRouterPluginConfigMapKey] = string(desiredTrafficRouterPluginString) 150 | actualConfigMap.Data[MetricPluginConfigMapKey] = string(desiredMetricPluginString) 151 | 152 | log.Info("Updating Rollouts ConfigMap due to detected difference") 153 | 154 | // Update the ConfigMap in the cluster 155 | if err := r.Client.Update(ctx, actualConfigMap); err != nil { 156 | return fmt.Errorf("failed to update ConfigMap: %v", err) 157 | } 158 | // Restarting rollouts pod only if configMap is updated 159 | if err := r.restartRolloutsPod(ctx, cr.Namespace); err != nil { 160 | return err 161 | } 162 | } 163 | log.Info("No changes detected in ConfigMap, skipping update and pod restart") 164 | return nil 165 | } 166 | 167 | // restartRolloutsPod deletes the Rollouts Pod to trigger a restart 168 | func (r *RolloutManagerReconciler) restartRolloutsPod(ctx context.Context, namespace string) error { 169 | deployment := &appsv1.Deployment{} 170 | if err := r.Client.Get(ctx, types.NamespacedName{Name: DefaultArgoRolloutsResourceName, Namespace: namespace}, deployment); err != nil { 171 | if errors.IsNotFound(err) { 172 | // If Deployment isn't found, return nil as there is no child pod to restart 173 | return nil 174 | } 175 | return fmt.Errorf("failed to get deployment: %w", err) 176 | } 177 | 178 | podList := &corev1.PodList{} 179 | listOpts := []client.ListOption{ 180 | client.InNamespace(namespace), 181 | client.MatchingLabels(deployment.Spec.Selector.MatchLabels), 182 | } 183 | if err := r.Client.List(ctx, podList, listOpts...); err != nil { 184 | return fmt.Errorf("failed to list Rollouts Pods: %w", err) 185 | } 186 | 187 | for i := range podList.Items { 188 | pod := podList.Items[i] 189 | if pod.ObjectMeta.DeletionTimestamp == nil { 190 | log.Info("Deleting Rollouts Pod", "podName", pod.Name) 191 | if err := r.Client.Delete(ctx, &pod); err != nil { 192 | if errors.IsNotFound(err) { 193 | log.Info(fmt.Sprintf("Pod %s already deleted", pod.Name)) 194 | continue 195 | } 196 | return fmt.Errorf("failed to delete Rollouts Pod %s: %w", pod.Name, err) 197 | } 198 | log.Info("Rollouts Pod deleted successfully", "podName", pod.Name) 199 | } 200 | } 201 | 202 | return nil 203 | } 204 | -------------------------------------------------------------------------------- /tests/e2e/fixture/fixture.go: -------------------------------------------------------------------------------- 1 | package fixture 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/selection" 15 | "k8s.io/apimachinery/pkg/util/wait" 16 | "k8s.io/client-go/dynamic" 17 | "k8s.io/client-go/rest" 18 | "k8s.io/client-go/tools/clientcmd" 19 | "sigs.k8s.io/controller-runtime/pkg/client" 20 | 21 | rolloutsmanagerv1alpha1 "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 22 | monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 23 | admissionv1 "k8s.io/api/admissionregistration/v1" 24 | apps "k8s.io/api/apps/v1" 25 | corev1 "k8s.io/api/core/v1" 26 | rbacv1 "k8s.io/api/rbac/v1" 27 | crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 28 | apierr "k8s.io/apimachinery/pkg/api/errors" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | ) 31 | 32 | const ( 33 | TestE2ENamespace = "argo-rollouts" 34 | LabelsKey = "app" 35 | LabelsValue = "test-argo-app" 36 | ) 37 | 38 | var NamespaceLabels = map[string]string{LabelsKey: LabelsValue} 39 | 40 | type Cleaner struct { 41 | cxt context.Context 42 | k8sClient client.Client 43 | } 44 | 45 | func newCleaner() (*Cleaner, error) { 46 | k8sClient, _, err := GetE2ETestKubeClient() 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return &Cleaner{ 52 | cxt: context.Background(), 53 | k8sClient: k8sClient, 54 | }, nil 55 | } 56 | 57 | func EnsureCleanSlate() error { 58 | cleaner, err := newCleaner() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | // ensure namespaces created during test are deleted 64 | err = cleaner.ensureTestNamespaceDeleted() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // create default namespace used for Rollouts controller 70 | err = cleaner.ensureRolloutNamespaceExists(TestE2ENamespace) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | err = cleaner.deleteRolloutsClusterRoles() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (cleaner *Cleaner) ensureRolloutNamespaceExists(namespaceParam string) error { 84 | if err := cleaner.deleteNamespace(namespaceParam); err != nil { 85 | return fmt.Errorf("unable to delete namespace '%s': %w", namespaceParam, err) 86 | } 87 | 88 | namespaceToCreate := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ 89 | Name: namespaceParam, 90 | }} 91 | 92 | if err := cleaner.k8sClient.Create(cleaner.cxt, &namespaceToCreate); err != nil { 93 | return fmt.Errorf("unable to create namespace '%s': %w", namespaceParam, err) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (cleaner *Cleaner) deleteRolloutsClusterRoles() error { 100 | crList := rbacv1.ClusterRoleList{} 101 | if err := cleaner.k8sClient.List(cleaner.cxt, &crList, &client.ListOptions{}); err != nil { 102 | return err 103 | } 104 | for idx := range crList.Items { 105 | sa := crList.Items[idx] 106 | // Skip any CRs that DON'T contain argo-rollouts 107 | if !strings.Contains(sa.Name, "argo-rollouts") { 108 | continue 109 | } 110 | if err := cleaner.k8sClient.Delete(cleaner.cxt, &sa); err != nil { 111 | return err 112 | } 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // deleteNamespace deletes a namespace, and waits for it to be reported as deleted. 119 | func (cleaner *Cleaner) deleteNamespace(namespaceParam string) error { 120 | 121 | // Delete the namespace: 122 | // - Issue a request to Delete the namespace 123 | // - Finally, we check if it has been deleted. 124 | if err := wait.PollUntilContextTimeout(cleaner.cxt, time.Second*5, time.Minute*6, true, func(ctx context.Context) (done bool, err error) { 125 | // Delete the namespace, if it exists 126 | namespace := corev1.Namespace{ 127 | ObjectMeta: metav1.ObjectMeta{ 128 | Name: namespaceParam, 129 | }, 130 | } 131 | if err := cleaner.k8sClient.Delete(cleaner.cxt, &namespace); err != nil { 132 | if !apierr.IsNotFound(err) { 133 | GinkgoWriter.Printf("unable to delete namespace '%s': %v\n", namespaceParam, err) 134 | return false, nil 135 | } 136 | } 137 | 138 | if err := cleaner.k8sClient.Get(cleaner.cxt, client.ObjectKeyFromObject(&namespace), &namespace); err != nil { 139 | if apierr.IsNotFound(err) { 140 | return true, nil 141 | } else { 142 | GinkgoWriter.Printf("unable to Get namespace '%s': %v\n", namespaceParam, err) 143 | return false, nil 144 | } 145 | } 146 | 147 | return false, nil 148 | }); err != nil { 149 | return fmt.Errorf("namespace was never deleted, after delete was issued. '%s':%v", namespaceParam, err) 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func GetDynamicClient() (*dynamic.DynamicClient, error) { 156 | config, err := getSystemKubeConfig() 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | return dynamic.NewForConfig(config) 162 | } 163 | 164 | func GetE2ETestKubeClient() (client.Client, *runtime.Scheme, error) { 165 | config, err := getSystemKubeConfig() 166 | if err != nil { 167 | return nil, nil, err 168 | } 169 | 170 | k8sClient, scheme, err := getKubeClient(config) 171 | if err != nil { 172 | return nil, nil, err 173 | } 174 | 175 | return k8sClient, scheme, nil 176 | } 177 | 178 | // getKubeClient returns a controller-runtime Client for accessing K8s API resources used by the controller. 179 | func getKubeClient(config *rest.Config) (client.Client, *runtime.Scheme, error) { 180 | 181 | scheme := runtime.NewScheme() 182 | 183 | if err := rolloutsmanagerv1alpha1.AddToScheme(scheme); err != nil { 184 | return nil, nil, err 185 | } 186 | 187 | if err := corev1.AddToScheme(scheme); err != nil { 188 | return nil, nil, err 189 | } 190 | 191 | if err := apps.AddToScheme(scheme); err != nil { 192 | return nil, nil, err 193 | } 194 | if err := rbacv1.AddToScheme(scheme); err != nil { 195 | return nil, nil, err 196 | } 197 | 198 | if err := admissionv1.AddToScheme(scheme); err != nil { 199 | return nil, nil, err 200 | } 201 | 202 | if err := monitoringv1.AddToScheme(scheme); err != nil { 203 | return nil, nil, err 204 | } 205 | 206 | if err := crdv1.AddToScheme(scheme); err != nil { 207 | return nil, nil, err 208 | } 209 | 210 | k8sClient, err := client.New(config, client.Options{Scheme: scheme}) 211 | if err != nil { 212 | return nil, nil, err 213 | } 214 | 215 | return k8sClient, scheme, nil 216 | 217 | } 218 | 219 | // Retrieve the system-level Kubernetes config (e.g. ~/.kube/config or service account config from volume) 220 | func getSystemKubeConfig() (*rest.Config, error) { 221 | 222 | overrides := clientcmd.ConfigOverrides{} 223 | 224 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 225 | clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin) 226 | 227 | restConfig, err := clientConfig.ClientConfig() 228 | if err != nil { 229 | return nil, err 230 | } 231 | return restConfig, nil 232 | } 233 | 234 | // Delete all namespaces having a specific label used to identify namespaces that are created by e2e tests. 235 | func (cleaner *Cleaner) ensureTestNamespaceDeleted() error { 236 | 237 | // fetch all namespaces having given label 238 | nsList, err := listE2ETestNamespaces(cleaner.cxt, cleaner.k8sClient) 239 | if err != nil { 240 | return fmt.Errorf("unable to delete test namespace: %w", err) 241 | } 242 | 243 | // delete selected namespaces 244 | for _, namespace := range nsList.Items { 245 | if err := cleaner.deleteNamespace(namespace.Name); err != nil { 246 | return fmt.Errorf("unable to delete namespace '%s': %w", namespace.Name, err) 247 | } 248 | } 249 | return nil 250 | } 251 | 252 | // Retrieve list of namespaces having a specific label used to identify namespaces that are created by e2e tests. 253 | func listE2ETestNamespaces(ctx context.Context, k8sClient client.Client) (corev1.NamespaceList, error) { 254 | nsList := corev1.NamespaceList{} 255 | 256 | // set e2e label 257 | req, err := labels.NewRequirement(LabelsKey, selection.Equals, []string{LabelsValue}) 258 | if err != nil { 259 | return nsList, fmt.Errorf("unable to set labels while fetching list of test namespace: %w", err) 260 | } 261 | 262 | // fetch all namespaces having given label 263 | err = k8sClient.List(ctx, &nsList, &client.ListOptions{LabelSelector: labels.NewSelector().Add(*req)}) 264 | if err != nil { 265 | return nsList, fmt.Errorf("unable to fetch list of test namespace: %w", err) 266 | } 267 | return nsList, nil 268 | } 269 | 270 | func EnvLocalRun() bool { 271 | _, exists := os.LookupEnv("LOCAL_RUN") 272 | return exists 273 | } 274 | -------------------------------------------------------------------------------- /tests/e2e/rollouts_imagepullpolicy_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 8 | controllers "github.com/argoproj-labs/argo-rollouts-manager/controllers" 9 | "github.com/argoproj-labs/argo-rollouts-manager/tests/e2e/fixture" 10 | "github.com/argoproj-labs/argo-rollouts-manager/tests/e2e/fixture/k8s" 11 | rolloutManagerFixture "github.com/argoproj-labs/argo-rollouts-manager/tests/e2e/fixture/rolloutmanager" 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | appsv1 "k8s.io/api/apps/v1" 15 | corev1 "k8s.io/api/core/v1" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | ) 19 | 20 | var _ = Describe("Argo RolloutManager ImagePullPolicy E2E tests", func() { 21 | var ( 22 | k8sClient client.Client 23 | ctx context.Context 24 | rolloutManager v1alpha1.RolloutManager 25 | ) 26 | 27 | BeforeEach(func() { 28 | Expect(fixture.EnsureCleanSlate()).To(Succeed()) 29 | 30 | var err error 31 | k8sClient, _, err = fixture.GetE2ETestKubeClient() 32 | Expect(err).ToNot(HaveOccurred()) 33 | ctx = context.Background() 34 | }) 35 | 36 | Context("ImagePullPolicy tests", func() { 37 | It("Verify imagePullPolicy is used from CR spec when specified", func() { 38 | By("creating a RolloutManager with imagePullPolicy set to Always") 39 | rolloutManager = v1alpha1.RolloutManager{ 40 | ObjectMeta: metav1.ObjectMeta{ 41 | Name: "imagepullpolicy-rollouts-manager", 42 | Namespace: fixture.TestE2ENamespace, 43 | }, 44 | Spec: v1alpha1.RolloutManagerSpec{ 45 | NamespaceScoped: true, 46 | ImagePullPolicy: corev1.PullAlways, 47 | }, 48 | } 49 | 50 | Expect(k8sClient.Create(ctx, &rolloutManager)).To(Succeed()) 51 | Eventually(rolloutManager, "60s", "1s").Should(rolloutManagerFixture.HavePhase(v1alpha1.PhaseAvailable)) 52 | 53 | By("verifying the deployment exists") 54 | deployment := appsv1.Deployment{ 55 | ObjectMeta: metav1.ObjectMeta{ 56 | Name: controllers.DefaultArgoRolloutsResourceName, 57 | Namespace: rolloutManager.Namespace, 58 | }, 59 | } 60 | Eventually(&deployment, "10s", "1s").Should(k8s.ExistByName(k8sClient)) 61 | Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&deployment), &deployment)).To(Succeed()) 62 | By("verifying the deployment has the correct imagePullPolicy Always") 63 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) 64 | 65 | By("changing the RolloutManager imagePullPolicy to Never") 66 | rolloutManager.Spec.ImagePullPolicy = corev1.PullNever 67 | 68 | Expect(k8sClient.Create(ctx, &rolloutManager)).To(Succeed()) 69 | Eventually(rolloutManager, "60s", "1s").Should(rolloutManagerFixture.HavePhase(v1alpha1.PhaseAvailable)) 70 | 71 | By("verifying the deployment has the correct imagePullPolicy Never") 72 | Eventually(&deployment, "10s", "1s").Should(k8s.ExistByName(k8sClient)) 73 | Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&deployment), &deployment)).To(Succeed()) 74 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullNever)) 75 | 76 | By("changing the RolloutManager imagePullPolicy to IfNotPresent") 77 | rolloutManager.Spec.ImagePullPolicy = corev1.PullIfNotPresent 78 | 79 | Expect(k8sClient.Create(ctx, &rolloutManager)).To(Succeed()) 80 | Eventually(rolloutManager, "60s", "1s").Should(rolloutManagerFixture.HavePhase(v1alpha1.PhaseAvailable)) 81 | 82 | By("verifying the deployment has the correct imagePullPolicy IfNotPresent") 83 | Eventually(&deployment, "10s", "1s").Should(k8s.ExistByName(k8sClient)) 84 | Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&deployment), &deployment)).To(Succeed()) 85 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullIfNotPresent)) 86 | }) 87 | 88 | It("should use the environment variable IMAGE_PULL_POLICY when specified and check the precedence", func() { 89 | if fixture.EnvLocalRun() { 90 | Skip("This test does not support local run, as when the controller is running locally there is no env var to modify") 91 | return 92 | } 93 | By("adding image pull policy env variable to IMAGE_PULL_POLICY in Subscription") 94 | Expect(os.Setenv("IMAGE_PULL_POLICY", "Always")).To(Succeed()) 95 | defer os.Unsetenv("IMAGE_PULL_POLICY") 96 | 97 | By("creating a RolloutManager without imagePullPolicy") 98 | rolloutManager = v1alpha1.RolloutManager{ 99 | ObjectMeta: metav1.ObjectMeta{ 100 | Name: "imagepullpolicy-rollouts-manager", 101 | Namespace: fixture.TestE2ENamespace, 102 | }, 103 | Spec: v1alpha1.RolloutManagerSpec{ 104 | NamespaceScoped: true, 105 | }, 106 | } 107 | 108 | Expect(k8sClient.Create(ctx, &rolloutManager)).To(Succeed()) 109 | Eventually(rolloutManager, "60s", "1s").Should(rolloutManagerFixture.HavePhase(v1alpha1.PhaseAvailable)) 110 | 111 | By("verifying the deployment exists") 112 | deployment := appsv1.Deployment{ 113 | ObjectMeta: metav1.ObjectMeta{ 114 | Name: controllers.DefaultArgoRolloutsResourceName, 115 | Namespace: rolloutManager.Namespace, 116 | }, 117 | } 118 | Eventually(&deployment, "10s", "1s").Should(k8s.ExistByName(k8sClient)) 119 | Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&deployment), &deployment)).To(Succeed()) 120 | By("verifying the deployment has the correct imagePullPolicy Always") 121 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) 122 | 123 | By("updating the env var IMAGE_PULL_POLICY to Never") 124 | Expect(os.Setenv("IMAGE_PULL_POLICY", "Never")).To(Succeed()) 125 | 126 | By("verifying the deployment has the correct imagePullPolicy Never") 127 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullNever)) 128 | 129 | By("updating the env var IMAGE_PULL_POLICY to IfNotPresent") 130 | Expect(os.Setenv("IMAGE_PULL_POLICY", "IfNotPresent")).To(Succeed()) 131 | 132 | By("verifying the deployment has the correct imagePullPolicy IfNotPresent") 133 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullIfNotPresent)) 134 | 135 | By("setting the imagePullPolicy to Always in the RolloutManager CR") 136 | rolloutManager.Spec.ImagePullPolicy = corev1.PullAlways 137 | 138 | Expect(k8sClient.Update(ctx, &rolloutManager)).To(Succeed()) 139 | Eventually(rolloutManager, "60s", "1s").Should(rolloutManagerFixture.HavePhase(v1alpha1.PhaseAvailable)) 140 | 141 | By("verifying the deployment has the correct imagePullPolicy Always") 142 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullAlways)) 143 | 144 | By("changing the imagePullPolicy to Never in the RolloutManager CR") 145 | rolloutManager.Spec.ImagePullPolicy = corev1.PullNever 146 | 147 | Expect(k8sClient.Update(ctx, &rolloutManager)).To(Succeed()) 148 | Eventually(rolloutManager, "60s", "1s").Should(rolloutManagerFixture.HavePhase(v1alpha1.PhaseAvailable)) 149 | 150 | By("verifying the deployment has the correct imagePullPolicy Never") 151 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullNever)) 152 | }) 153 | 154 | It("should default to IfNotPresent when imagePullPolicy if not specified in either the CR spec or the environment variable", func() { 155 | By("creating a RolloutManager without imagePullPolicy") 156 | rolloutManager = v1alpha1.RolloutManager{ 157 | ObjectMeta: metav1.ObjectMeta{ 158 | Name: "imagepullpolicy-rollouts-manager", 159 | Namespace: fixture.TestE2ENamespace, 160 | }, 161 | Spec: v1alpha1.RolloutManagerSpec{ 162 | NamespaceScoped: true, 163 | }, 164 | } 165 | 166 | Expect(k8sClient.Create(ctx, &rolloutManager)).To(Succeed()) 167 | Eventually(rolloutManager, "60s", "1s").Should(rolloutManagerFixture.HavePhase(v1alpha1.PhaseAvailable)) 168 | 169 | By("verifying the deployment exists") 170 | deployment := appsv1.Deployment{ 171 | ObjectMeta: metav1.ObjectMeta{ 172 | Name: controllers.DefaultArgoRolloutsResourceName, 173 | Namespace: rolloutManager.Namespace, 174 | }, 175 | } 176 | Eventually(&deployment, "10s", "1s").Should(k8s.ExistByName(k8sClient)) 177 | Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&deployment), &deployment)).To(Succeed()) 178 | By("verifying the deployment has the correct imagePullPolicy IfNotPresent") 179 | Expect(deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy).To(Equal(corev1.PullIfNotPresent)) 180 | }) 181 | 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /hack/verify-rollouts-e2e-tests/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | 13 | // As of March 2024 (Rollouts v1.6.6): 14 | // 15 | // Intermittently fail: 16 | // - TestCanaryDynamicStableScale 17 | // - TestCanaryScaleDownOnAbort 18 | // - TestALBExperimentStepNoSetWeight 19 | // - TestALBExperimentStep 20 | // - TestALBExperimentStepNoSetWeightMultiIngress 21 | 22 | // - Most of these tests fail 100% of the time, because they were not designed to run against Argo Rollouts running on a cluster. (These are safe to ignore.) 23 | // - The rollouts tests are written to assume they are running locally: not within a container, and not on a K8s cluster. 24 | // - Some are intermittently failing, implying a race condition in the test/product. 25 | 26 | testsExpectedToFailList := []string{ 27 | "TestAPISIXSuite/TestAPISIXCanarySetHeaderStep", 28 | "TestExperimentSuite/TestExperimentWithDryRunMetrics", 29 | "TestFunctionalSuite/TestControllerMetrics", 30 | "TestFunctionalSuite/TestBlueGreenPromoteFull", 31 | "TestAnalysisSuite/TestCanaryInconclusiveBackgroundAnalysis", 32 | } 33 | 34 | // 1) Read E2E test log, output by run-upstream-argo-rollouts-e2e-tests.sh 35 | if len(os.Args) != 2 { 36 | reportErrorAndExit(fmt.Errorf("expected args: path to E2E test log")) 37 | return 38 | } 39 | 40 | testsE2EResultsLogPath := os.Args[1] 41 | 42 | fileContents, err := waitAndGetE2EFileContents(testsE2EResultsLogPath) 43 | if err != nil { 44 | reportErrorAndExit(err) 45 | return 46 | } 47 | fmt.Println() 48 | 49 | // 2) Parse E2E test log, skipping any tests that we expect to fail 50 | // - map: name of test -> list of test run lines 51 | testResults, err := parseTestResultsFromFile(fileContents, testsExpectedToFailList) 52 | if err != nil { 53 | reportErrorAndExit(err) 54 | return 55 | } 56 | 57 | // 3a) Report unexpected failed tests, in alphabetical order 58 | mapKeys := []string{} 59 | for key := range testResults { 60 | mapKeys = append(mapKeys, key) 61 | } 62 | 63 | sort.Strings(mapKeys) 64 | 65 | if len(mapKeys) == 0 { // sanity test 66 | reportErrorAndExit(fmt.Errorf("no test results found")) 67 | return 68 | } 69 | 70 | var testFailureOutput []string 71 | var testSuccessOutput []string 72 | 73 | atLeastOneTestPermFail := false 74 | 75 | for _, testName := range mapKeys { 76 | 77 | testRuns := testResults[testName] 78 | 79 | // gotestsum will rerun Argo Rollouts tests up to 5 times (6 runs total) 80 | // - Here we check if there exists AT LEAST ONE 'PASS' 81 | // - If no 'PASS' results exist, it implies the test never passed after all retries, so it's a permfail 82 | 83 | passFound := false 84 | for _, testRunLine := range testRuns { 85 | if strings.Contains(testRunLine, "PASS") { 86 | passFound = true 87 | testSuccessOutput = append(testSuccessOutput, "Test passed - "+testName) 88 | break 89 | } 90 | } 91 | 92 | if !passFound { 93 | 94 | testFailureOutput = append(testFailureOutput, "Unexpected test failure, test failed too many times - "+testName+":") 95 | 96 | testFailureOutput = append(testFailureOutput, testRuns...) 97 | 98 | testFailureOutput = append(testFailureOutput, "") 99 | atLeastOneTestPermFail = true 100 | 101 | } 102 | 103 | } 104 | 105 | fmt.Println() 106 | 107 | // 3b) First print successes 108 | for _, line := range testSuccessOutput { 109 | fmt.Println(line) 110 | } 111 | 112 | fmt.Println() 113 | 114 | // 3c) Then print failures 115 | for _, line := range testFailureOutput { 116 | fmt.Println(line) 117 | } 118 | 119 | // 4) Exit with error code 1 if there was at least one unexpected test failure. 120 | if atLeastOneTestPermFail { 121 | reportErrorAndExit(fmt.Errorf("at least one test failure occurred")) 122 | return 123 | } 124 | 125 | } 126 | 127 | // parseTestResultsFromFile parses the E2E test log, skipping any tests that we expect to fail, and storing the results in a map 128 | func parseTestResultsFromFile(fileContents []string, testsExpectedToFailList []string) (map[string][]string, error) { 129 | 130 | atLeastOnePassSeen := false 131 | atLeastOneFailSeen := false 132 | 133 | testResults := map[string][]string{} 134 | 135 | testsExpectedToFailMap := map[string]any{} 136 | 137 | for _, testExpectedToFail := range testsExpectedToFailList { 138 | testsExpectedToFailMap[testExpectedToFail] = "" 139 | } 140 | 141 | for _, line := range fileContents { 142 | 143 | testSlashE2EText := "test/e2e" 144 | 145 | if !strings.Contains(line, testSlashE2EText) { 146 | continue 147 | } 148 | 149 | var testName string 150 | 151 | if strings.HasPrefix(line, "===") && strings.Contains(line, "FAIL") { 152 | // example: === FAIL: test/e2e TestFunctionalSuite/TestBlueGreenPromoteFull (unknown) 153 | // 154 | // Unfortunately, the FAIL line is surrounded by invisible ANSI colour whitespace, so we can't scan for it directly. 155 | // - we instead check for ===, FAIL, and the test/e2e string. 156 | 157 | testName = line[strings.Index(line, testSlashE2EText)+len(testSlashE2EText)+1 : strings.Index(line, "(")-1] 158 | testName = strings.TrimSpace(testName) 159 | 160 | if !strings.Contains(testName, "/") { 161 | // Ignore the fail reported for the suite: we only care about individual test fails 162 | continue 163 | } 164 | 165 | atLeastOneFailSeen = true 166 | 167 | } else if strings.Contains(line, "PASS") && strings.Contains(line, ".") { 168 | // example: PASS test/e2e.TestCanarySuite/TestCanaryDynamicStableScale (20.91s) 169 | 170 | roundBraceIndex := strings.Index(line, "(") 171 | 172 | if roundBraceIndex == -1 { 173 | continue 174 | } 175 | 176 | dotIndex := strings.Index(line, ".") 177 | if dotIndex == -1 { 178 | continue 179 | } 180 | 181 | if dotIndex+1 >= roundBraceIndex-1 { 182 | fmt.Println("Unexpected line format: [", line, "]") 183 | continue 184 | } 185 | 186 | testName = line[dotIndex+1 : roundBraceIndex-1] 187 | atLeastOnePassSeen = true 188 | 189 | } else { 190 | continue 191 | } 192 | 193 | if _, exists := testsExpectedToFailMap[testName]; exists { 194 | // Ignore tests that are expected to fail 195 | continue 196 | } 197 | 198 | if !strings.Contains(testName, "/") { 199 | // Skip suite-only results 200 | continue 201 | } 202 | 203 | testResults[testName] = append(testResults[testName], line) 204 | } 205 | 206 | if !atLeastOneFailSeen || !atLeastOnePassSeen { // sanity test: BOTH a pass and fail should have occurred in the log. 207 | return nil, fmt.Errorf("there may be something wrong with the parser: we expect to see both at least one pass, and at least one fail, in the parsed output: %v %v", atLeastOneFailSeen, atLeastOnePassSeen) 208 | } 209 | 210 | return testResults, nil 211 | } 212 | 213 | // waitAndGetE2EFileContents waits for one of the last 50 lines of the file to start with 'DONE' before returning the contents. 214 | // This allows us to avoid a race condition with tee where the file contents of the file may not have been fully written. 215 | func waitAndGetE2EFileContents(testsE2EResultsLogPath string) ([]string, error) { 216 | 217 | // Return whatever we have after 1 minute has elapsed 218 | expireTime := time.Now().Add(1 * time.Minute) 219 | 220 | for { 221 | 222 | fileLines, err := readFileIntoListOfLines(testsE2EResultsLogPath, true) 223 | if err != nil { 224 | return []string{}, err 225 | } 226 | 227 | lineIndexStart := len(fileLines) - 50 228 | if lineIndexStart < 0 { 229 | lineIndexStart = 0 230 | } 231 | 232 | for _, line := range fileLines[lineIndexStart:] { 233 | // example line: DONE 6 runs, 144 tests, 6 skipped, 47 failures in 2279.668s 234 | if strings.HasPrefix(line, "DONE ") { 235 | fmt.Println("E2E tests file is complete:", line) 236 | return fileLines, nil 237 | } 238 | } 239 | 240 | if time.Now().After(expireTime) { 241 | fmt.Println("E2E tests file timed out waiting. Previous X lines were:", fileLines[lineIndexStart:]) 242 | return fileLines, nil 243 | } 244 | 245 | // Otherwise, wait a second then check again. 246 | fmt.Println("* Waiting for E2E test file to be complete") 247 | time.Sleep(time.Second) 248 | } 249 | 250 | } 251 | 252 | func reportErrorAndExit(err error) { 253 | fmt.Println("ERROR:", err) 254 | os.Exit(1) 255 | } 256 | 257 | func readFileIntoListOfLines(path string, initialTrimSpace bool) ([]string, error) { 258 | fileContentsBytes, err := os.ReadFile(path) 259 | if err != nil { 260 | return []string{}, err 261 | } 262 | 263 | fileContents := string(fileContentsBytes) 264 | 265 | if initialTrimSpace { 266 | fileContents = strings.TrimSpace(fileContents) 267 | } 268 | 269 | return strings.Split(string(fileContents), "\n"), nil 270 | 271 | } 272 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2023. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 | func (in *Plugin) DeepCopyInto(out *Plugin) { 31 | *out = *in 32 | } 33 | 34 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plugin. 35 | func (in *Plugin) DeepCopy() *Plugin { 36 | if in == nil { 37 | return nil 38 | } 39 | out := new(Plugin) 40 | in.DeepCopyInto(out) 41 | return out 42 | } 43 | 44 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 45 | func (in *Plugins) DeepCopyInto(out *Plugins) { 46 | *out = *in 47 | if in.TrafficManagement != nil { 48 | in, out := &in.TrafficManagement, &out.TrafficManagement 49 | *out = make([]Plugin, len(*in)) 50 | copy(*out, *in) 51 | } 52 | if in.Metric != nil { 53 | in, out := &in.Metric, &out.Metric 54 | *out = make([]Plugin, len(*in)) 55 | copy(*out, *in) 56 | } 57 | } 58 | 59 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plugins. 60 | func (in *Plugins) DeepCopy() *Plugins { 61 | if in == nil { 62 | return nil 63 | } 64 | out := new(Plugins) 65 | in.DeepCopyInto(out) 66 | return out 67 | } 68 | 69 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 70 | func (in *ResourceMetadata) DeepCopyInto(out *ResourceMetadata) { 71 | *out = *in 72 | if in.Annotations != nil { 73 | in, out := &in.Annotations, &out.Annotations 74 | *out = make(map[string]string, len(*in)) 75 | for key, val := range *in { 76 | (*out)[key] = val 77 | } 78 | } 79 | if in.Labels != nil { 80 | in, out := &in.Labels, &out.Labels 81 | *out = make(map[string]string, len(*in)) 82 | for key, val := range *in { 83 | (*out)[key] = val 84 | } 85 | } 86 | } 87 | 88 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceMetadata. 89 | func (in *ResourceMetadata) DeepCopy() *ResourceMetadata { 90 | if in == nil { 91 | return nil 92 | } 93 | out := new(ResourceMetadata) 94 | in.DeepCopyInto(out) 95 | return out 96 | } 97 | 98 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 99 | func (in *RolloutManager) DeepCopyInto(out *RolloutManager) { 100 | *out = *in 101 | out.TypeMeta = in.TypeMeta 102 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 103 | in.Spec.DeepCopyInto(&out.Spec) 104 | in.Status.DeepCopyInto(&out.Status) 105 | } 106 | 107 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutManager. 108 | func (in *RolloutManager) DeepCopy() *RolloutManager { 109 | if in == nil { 110 | return nil 111 | } 112 | out := new(RolloutManager) 113 | in.DeepCopyInto(out) 114 | return out 115 | } 116 | 117 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 118 | func (in *RolloutManager) DeepCopyObject() runtime.Object { 119 | if c := in.DeepCopy(); c != nil { 120 | return c 121 | } 122 | return nil 123 | } 124 | 125 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 126 | func (in *RolloutManagerHASpec) DeepCopyInto(out *RolloutManagerHASpec) { 127 | *out = *in 128 | } 129 | 130 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutManagerHASpec. 131 | func (in *RolloutManagerHASpec) DeepCopy() *RolloutManagerHASpec { 132 | if in == nil { 133 | return nil 134 | } 135 | out := new(RolloutManagerHASpec) 136 | in.DeepCopyInto(out) 137 | return out 138 | } 139 | 140 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 141 | func (in *RolloutManagerList) DeepCopyInto(out *RolloutManagerList) { 142 | *out = *in 143 | out.TypeMeta = in.TypeMeta 144 | in.ListMeta.DeepCopyInto(&out.ListMeta) 145 | if in.Items != nil { 146 | in, out := &in.Items, &out.Items 147 | *out = make([]RolloutManager, len(*in)) 148 | for i := range *in { 149 | (*in)[i].DeepCopyInto(&(*out)[i]) 150 | } 151 | } 152 | } 153 | 154 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutManagerList. 155 | func (in *RolloutManagerList) DeepCopy() *RolloutManagerList { 156 | if in == nil { 157 | return nil 158 | } 159 | out := new(RolloutManagerList) 160 | in.DeepCopyInto(out) 161 | return out 162 | } 163 | 164 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 165 | func (in *RolloutManagerList) DeepCopyObject() runtime.Object { 166 | if c := in.DeepCopy(); c != nil { 167 | return c 168 | } 169 | return nil 170 | } 171 | 172 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 173 | func (in *RolloutManagerSpec) DeepCopyInto(out *RolloutManagerSpec) { 174 | *out = *in 175 | if in.Env != nil { 176 | in, out := &in.Env, &out.Env 177 | *out = make([]v1.EnvVar, len(*in)) 178 | for i := range *in { 179 | (*in)[i].DeepCopyInto(&(*out)[i]) 180 | } 181 | } 182 | if in.ExtraCommandArgs != nil { 183 | in, out := &in.ExtraCommandArgs, &out.ExtraCommandArgs 184 | *out = make([]string, len(*in)) 185 | copy(*out, *in) 186 | } 187 | if in.NodePlacement != nil { 188 | in, out := &in.NodePlacement, &out.NodePlacement 189 | *out = new(RolloutsNodePlacementSpec) 190 | (*in).DeepCopyInto(*out) 191 | } 192 | if in.AdditionalMetadata != nil { 193 | in, out := &in.AdditionalMetadata, &out.AdditionalMetadata 194 | *out = new(ResourceMetadata) 195 | (*in).DeepCopyInto(*out) 196 | } 197 | if in.ControllerResources != nil { 198 | in, out := &in.ControllerResources, &out.ControllerResources 199 | *out = new(v1.ResourceRequirements) 200 | (*in).DeepCopyInto(*out) 201 | } 202 | in.Plugins.DeepCopyInto(&out.Plugins) 203 | if in.HA != nil { 204 | in, out := &in.HA, &out.HA 205 | *out = new(RolloutManagerHASpec) 206 | **out = **in 207 | } 208 | } 209 | 210 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutManagerSpec. 211 | func (in *RolloutManagerSpec) DeepCopy() *RolloutManagerSpec { 212 | if in == nil { 213 | return nil 214 | } 215 | out := new(RolloutManagerSpec) 216 | in.DeepCopyInto(out) 217 | return out 218 | } 219 | 220 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 221 | func (in *RolloutManagerStatus) DeepCopyInto(out *RolloutManagerStatus) { 222 | *out = *in 223 | if in.Conditions != nil { 224 | in, out := &in.Conditions, &out.Conditions 225 | *out = make([]metav1.Condition, len(*in)) 226 | for i := range *in { 227 | (*in)[i].DeepCopyInto(&(*out)[i]) 228 | } 229 | } 230 | } 231 | 232 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutManagerStatus. 233 | func (in *RolloutManagerStatus) DeepCopy() *RolloutManagerStatus { 234 | if in == nil { 235 | return nil 236 | } 237 | out := new(RolloutManagerStatus) 238 | in.DeepCopyInto(out) 239 | return out 240 | } 241 | 242 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 243 | func (in *RolloutsNodePlacementSpec) DeepCopyInto(out *RolloutsNodePlacementSpec) { 244 | *out = *in 245 | if in.NodeSelector != nil { 246 | in, out := &in.NodeSelector, &out.NodeSelector 247 | *out = make(map[string]string, len(*in)) 248 | for key, val := range *in { 249 | (*out)[key] = val 250 | } 251 | } 252 | if in.Tolerations != nil { 253 | in, out := &in.Tolerations, &out.Tolerations 254 | *out = make([]v1.Toleration, len(*in)) 255 | for i := range *in { 256 | (*in)[i].DeepCopyInto(&(*out)[i]) 257 | } 258 | } 259 | } 260 | 261 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutsNodePlacementSpec. 262 | func (in *RolloutsNodePlacementSpec) DeepCopy() *RolloutsNodePlacementSpec { 263 | if in == nil { 264 | return nil 265 | } 266 | out := new(RolloutsNodePlacementSpec) 267 | in.DeepCopyInto(out) 268 | return out 269 | } 270 | -------------------------------------------------------------------------------- /hack/run-rollouts-manager-e2e-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPTPATH="$( 4 | cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit 5 | pwd -P 6 | )" 7 | 8 | function cleanup { 9 | echo "* Cleaning up" 10 | killall main || true 11 | killall go || true 12 | } 13 | 14 | trap cleanup EXIT 15 | 16 | extract_metrics_data() { 17 | 18 | set +e 19 | 20 | TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 21 | 22 | while true; do 23 | curl http://localhost:8080/metrics -o "$TMP_DIR/rollouts-metric-endpoint-output.txt" 24 | if [ "$?" == "0" ]; then 25 | break 26 | fi 27 | echo "* Waiting for Metrics endpoint to become available" 28 | sleep 3 29 | done 30 | 31 | # 1) Extract REST client get/put/post metrics 32 | 33 | # Example: the metrics from /metric endpoint look like this: 34 | # rest_client_requests_total{code="200",host="api.pgqqd-novoo-oqu.pa43.p3.openshiftapps.com:443",method="GET"} 42 35 | # rest_client_requests_total{code="200",host="api.pgqqd-novoo-oqu.pa43.p3.openshiftapps.com:443",method="PUT"} 88 36 | # rest_client_requests_total{code="201",host="api.pgqqd-novoo-oqu.pa43.p3.openshiftapps.com:443",method="POST"} 110 37 | 38 | curl http://localhost:8080/metrics -o "$TMP_DIR/rollouts-metric-endpoint-output.txt" 39 | 40 | echo "* Metrics gathered raw output ---------------------------------------------------------------" 41 | cat "$TMP_DIR/rollouts-metric-endpoint-output.txt" 42 | echo "---------------------------------------------------------------------------------------------" 43 | 44 | GET_REQUESTS=`cat "$TMP_DIR/rollouts-metric-endpoint-output.txt" | grep "rest_client_requests_total" | grep "GET" | grep "200" | rev | cut -d' ' -f1 | rev` 45 | 46 | PUT_REQUESTS_200=`cat "$TMP_DIR/rollouts-metric-endpoint-output.txt" | grep "rest_client_requests_total" | grep "PUT" | grep "200" | rev | cut -d' ' -f1 | rev` 47 | 48 | # 409 Conflict error code 49 | PUT_REQUESTS_409=`cat "$TMP_DIR/rollouts-metric-endpoint-output.txt" | grep "rest_client_requests_total" | grep "PUT" | grep "409" | rev | cut -d' ' -f1 | rev` 50 | 51 | # 201 Created error code 52 | POST_REQUESTS_201=`cat "$TMP_DIR/rollouts-metric-endpoint-output.txt" | grep "rest_client_requests_total" | grep "POST" | grep "201" | rev | cut -d' ' -f1 | rev` 53 | 54 | # 409 Conflict error code 55 | POST_REQUESTS_409=`cat "$TMP_DIR/rollouts-metric-endpoint-output.txt" | grep "rest_client_requests_total" | grep "POST" | grep "409" | rev | cut -d' ' -f1 | rev` 56 | 57 | if [[ "$GET_REQUESTS" == "" ]]; then 58 | GET_REQUESTS=0 59 | fi 60 | 61 | if [[ "$POST_REQUESTS_201" == "" ]]; then 62 | POST_REQUESTS_201=0 63 | fi 64 | if [[ "$POST_REQUESTS_409" == "" ]]; then 65 | POST_REQUESTS_409=0 66 | fi 67 | 68 | if [[ "$PUT_REQUESTS_200" == "" ]]; then 69 | PUT_REQUESTS_200=0 70 | fi 71 | if [[ "$PUT_REQUESTS_409" == "" ]]; then 72 | PUT_REQUESTS_409=0 73 | fi 74 | 75 | PUT_REQUESTS=`expr $PUT_REQUESTS_200 + $PUT_REQUESTS_409` 76 | POST_REQUESTS=`expr $POST_REQUESTS_201 + $POST_REQUESTS_409` 77 | 78 | # 2) Extract the # of RolloutManager reconciles 79 | 80 | # Example: the metrics from /metric endpoint look like this: 81 | # controller_runtime_reconcile_total{controller="rolloutmanager",result="error"} 0 82 | # controller_runtime_reconcile_total{controller="rolloutmanager",result="requeue"} 0 83 | # controller_runtime_reconcile_total{controller="rolloutmanager",result="requeue_after"} 0 84 | # controller_runtime_reconcile_total{controller="rolloutmanager",result="success"} 135 85 | ERROR_RECONCILES=`cat "$TMP_DIR/rollouts-metric-endpoint-output.txt" | grep "controller_runtime_reconcile_total" | grep "error" | rev | cut -d' ' -f1` 86 | SUCCESS_RECONCILES=`cat "$TMP_DIR/rollouts-metric-endpoint-output.txt" | grep "controller_runtime_reconcile_total" | grep "success" | rev | cut -d' ' -f1` 87 | 88 | if [[ "$ERROR_RECONCILES" == "" ]]; then 89 | ERROR_RECONCILES=0 90 | fi 91 | 92 | if [[ "$SUCCESS_RECONCILES" == "" ]]; then 93 | SUCCESS_RECONCILES=0 94 | fi 95 | 96 | } 97 | 98 | cd "$SCRIPTPATH/.." 99 | 100 | set -o pipefail 101 | 102 | # Check if the CRD exists 103 | kubectl get crd/servicemonitors.monitoring.coreos.com &> /dev/null 104 | retVal=$? 105 | if [ $retVal -ne 0 ]; then 106 | # If the CRD is not found, apply the CRD YAML 107 | kubectl apply -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/release-0.52/example/prometheus-operator-crd/monitoring.coreos.com_servicemonitors.yaml 108 | fi 109 | 110 | if [[ "$DISABLE_METRICS" == "" ]]; then 111 | 112 | # Before the test starts, extract initial metrics values from the /metrics endpoint of the operator 113 | extract_metrics_data 114 | 115 | fi 116 | 117 | set -e 118 | 119 | if [[ "$DISABLE_METRICS" == "" ]]; then 120 | 121 | set -u 122 | 123 | INITIAL_GET_REQUESTS=$GET_REQUESTS 124 | INITIAL_PUT_REQUESTS=$PUT_REQUESTS 125 | INITIAL_POST_REQUESTS=$POST_REQUESTS 126 | INITIAL_ERROR_RECONCILES=$ERROR_RECONCILES 127 | INITIAL_SUCCESS_RECONCILES=$SUCCESS_RECONCILES 128 | 129 | set +u 130 | fi 131 | 132 | 133 | # Run the tests 134 | 135 | set -ex 136 | 137 | if [ "$NAMESPACE_SCOPED_ARGO_ROLLOUTS" == "true" ]; then 138 | 139 | go test -v -p=1 -timeout=30m -race -count=1 -coverprofile=coverage.out ./tests/e2e/namespace-scoped 140 | 141 | else 142 | 143 | go test -v -p=1 -timeout=30m -race -count=1 -coverprofile=coverage.out ./tests/e2e/cluster-scoped 144 | 145 | fi 146 | 147 | # Sanity test the behaviour of the operator during the tests: 148 | # - We check the (prometheus) metrics coming from the 'localhost:8080/metrics' endpoint of the operator. 149 | # - For example, if the reported # of Reconcile calls was unusually high, this might mean that the operator was stuck in a Reconcile loop 150 | # - Or, if the number of REST client POST requests (e.g. k8s objection creation) was equal to the number of PUT request (e.g. k8s object update), this may imply we are updating .status or .spec of an object too frequently. 151 | sanity_test_metrics_data() { 152 | 153 | extract_metrics_data 154 | 155 | set -eux 156 | 157 | FINAL_GET_REQUESTS=$GET_REQUESTS 158 | FINAL_PUT_REQUESTS=$PUT_REQUESTS 159 | FINAL_POST_REQUESTS=$POST_REQUESTS 160 | FINAL_ERROR_RECONCILES=$ERROR_RECONCILES 161 | FINAL_SUCCESS_RECONCILES=$SUCCESS_RECONCILES 162 | 163 | DELTA_GET_REQUESTS=`expr $FINAL_GET_REQUESTS - $INITIAL_GET_REQUESTS` 164 | DELTA_PUT_REQUESTS=`expr $FINAL_PUT_REQUESTS - $INITIAL_PUT_REQUESTS` 165 | DELTA_POST_REQUESTS=`expr $FINAL_POST_REQUESTS - $INITIAL_POST_REQUESTS` 166 | 167 | DELTA_ERROR_RECONCILES=`expr $FINAL_ERROR_RECONCILES - $INITIAL_ERROR_RECONCILES` 168 | DELTA_SUCCESS_RECONCILES=`expr $FINAL_SUCCESS_RECONCILES - $INITIAL_SUCCESS_RECONCILES` 169 | 170 | if [[ "$DELTA_POST_REQUESTS" == "0" ]]; then 171 | echo "Unexpected number of REST client post requests: should be at least 1" 172 | exit 1 173 | fi 174 | 175 | # The # of PUT requests should be less than 40% of the # of POST requests 176 | # - If the number is higher, this implies we are updating the .status or .spec fields of resources more than is necessary. 177 | PUT_REQUEST_PERCENT=`expr "$DELTA_PUT_REQUESTS"00 / $DELTA_POST_REQUESTS` 178 | 179 | if [[ "$PUT_REQUEST_PERCENT" -gt 45 ]]; then 180 | # This value is arbitrary, and should be updated if at any point it becomes inaccurate (but first audit the test/code to make sure it is not an actual product/test issue, before increasing) 181 | 182 | echo "Put request was %$PUT_REQUEST_PERCENT greater than the expected value" 183 | exit 1 184 | 185 | fi 186 | 187 | if [[ "$DELTA_ERROR_RECONCILES" -gt 90 ]]; then 188 | # This value is arbitrary, and should be updated if at any point it becomes inaccurate (but first audit the test/code to make sure it is not an actual product/test issue, before increasing) 189 | echo "Number of Reconcile calls that returned an error '$DELTA_ERROR_RECONCILES' was greater than the expected value" 190 | exit 1 191 | 192 | fi 193 | 194 | if [[ "$DELTA_SUCCESS_RECONCILES" -gt 1200 ]]; then 195 | # This value is arbitrary, and should be updated if at any point it becomes inaccurate (but first audit the test/code to make sure it is not an actual product/test issue, before increasing) 196 | 197 | echo "Number of Reconcile calls that returned success '$DELTA_SUCCESS_RECONCILES' was greater than the expected value" 198 | exit 1 199 | 200 | fi 201 | 202 | set +u 203 | } 204 | 205 | if [[ "$DISABLE_METRICS" == "" ]]; then 206 | sanity_test_metrics_data 207 | fi 208 | 209 | set +e 210 | 211 | # If the output from the E2E operator is available, then check it for errors 212 | if [ -f "/tmp/e2e-operator-run.log" ]; then 213 | 214 | # Wait for the controller to flush to the file, before killing the controller 215 | sleep 10 216 | killall main 217 | sleep 5 218 | 219 | # Grep the log for unexpected errors 220 | # - Ignore errors that are expected to occur 221 | 222 | UNEXPECTED_ERRORS_FOUND_TEXT=`cat /tmp/e2e-operator-run.log | grep "ERROR" | grep -v "because it is being terminated" | grep -v "the object has been modified; please apply your changes to the latest version and try again" | grep -v "unable to fetch" | grep -v "StorageError" | grep -v "client rate limiter Wait returned an error: context canceled" | grep -v "failed to reconcile Rollout's ClusterRoleBinding" | grep -v "clusterrolebindings.rbac.authorization.k8s.io.*argo-rollouts.*already.*exists" | grep -v "servicemonitors.monitoring.coreos.com.*argo-rollouts.*already.*exists"` 223 | 224 | if [ "$UNEXPECTED_ERRORS_FOUND_TEXT" != "" ]; then 225 | 226 | UNEXPECTED_ERRORS_COUNT=`echo $UNEXPECTED_ERRORS_FOUND_TEXT | grep "ERROR" | wc -l` 227 | 228 | if [ "$UNEXPECTED_ERRORS_COUNT" != "0" ]; then 229 | echo "Unexpected errors found: $UNEXPECTED_ERRORS_FOUND_TEXT" 230 | exit 1 231 | fi 232 | fi 233 | fi 234 | -------------------------------------------------------------------------------- /controllers/configmap_test.go: -------------------------------------------------------------------------------- 1 | package rollouts 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/argoproj-labs/argo-rollouts-manager/api/v1alpha1" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | appsv1 "k8s.io/api/apps/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | ) 15 | 16 | var _ = Describe("ConfigMap Test", func() { 17 | var ctx context.Context 18 | var a v1alpha1.RolloutManager 19 | var r *RolloutManagerReconciler 20 | var sa *corev1.ServiceAccount 21 | var existingDeployment *appsv1.Deployment 22 | const trafficrouterPluginLocation = "https://custom-traffic-plugin-location" 23 | const metricPluginLocation = "https://custom-metric-plugin-location" 24 | 25 | BeforeEach(func() { 26 | ctx = context.Background() 27 | a = *makeTestRolloutManager() 28 | 29 | r = makeTestReconciler(&a) 30 | Expect(createNamespace(r, a.Namespace)).To(Succeed()) 31 | 32 | sa = &corev1.ServiceAccount{ 33 | ObjectMeta: metav1.ObjectMeta{ 34 | Name: DefaultArgoRolloutsResourceName, 35 | Namespace: a.Namespace, 36 | }, 37 | } 38 | Expect(r.Client.Create(ctx, sa)).To(Succeed()) 39 | 40 | var err error 41 | 42 | existingDeployment, err = deploymentCR(DefaultArgoRolloutsResourceName, a.Namespace, DefaultArgoRolloutsResourceName, []string{"plugin-bin-test", "tmp-test"}, "linux-test", sa.Name, a) 43 | Expect(err).ToNot(HaveOccurred()) 44 | 45 | Expect(r.Client.Create(ctx, existingDeployment)).To(Succeed()) 46 | 47 | }) 48 | 49 | It("verifies that the default ConfigMap is created if it is not present", func() { 50 | expectedConfigMap := &corev1.ConfigMap{ 51 | ObjectMeta: metav1.ObjectMeta{ 52 | Name: DefaultRolloutsConfigMapName, 53 | }, 54 | } 55 | 56 | By("Call reconcileConfigMap") 57 | Expect(r.reconcileConfigMap(ctx, a)).To(Succeed()) 58 | 59 | By("Verify that the fetched ConfigMap matches the desired one") 60 | 61 | fetchedConfigMap := &corev1.ConfigMap{} 62 | Expect(fetchObject(ctx, r.Client, a.Namespace, expectedConfigMap.Name, fetchedConfigMap)).To(Succeed()) 63 | 64 | Expect(fetchedConfigMap.Name).To(Equal(expectedConfigMap.Name)) 65 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(OpenShiftRolloutPluginName)) 66 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(r.OpenShiftRoutePluginLocation)) 67 | 68 | By("Call reconcileConfigMap again") 69 | Expect(r.reconcileConfigMap(ctx, a)).To(Succeed()) 70 | 71 | By("verifying that the data is still present") 72 | Expect(fetchedConfigMap.Name).To(Equal(expectedConfigMap.Name)) 73 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(OpenShiftRolloutPluginName)) 74 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(r.OpenShiftRoutePluginLocation)) 75 | 76 | }) 77 | 78 | It("verifies that the custom labels are added to the ConfigMap", func() { 79 | r = makeTestReconcilerWithCustomLabels(map[string]string{ 80 | "custom1": "value", 81 | }, &a) 82 | expectedConfigMap := &corev1.ConfigMap{ 83 | ObjectMeta: metav1.ObjectMeta{ 84 | Name: DefaultRolloutsConfigMapName, 85 | Labels: map[string]string{ 86 | "custom1": "value", 87 | "app.kubernetes.io/component": DefaultArgoRolloutsResourceName, 88 | "app.kubernetes.io/name": DefaultArgoRolloutsResourceName, 89 | "app.kubernetes.io/part-of": DefaultArgoRolloutsResourceName, 90 | }, 91 | }, 92 | } 93 | 94 | By("Call reconcileConfigMap") 95 | Expect(r.reconcileConfigMap(ctx, a)).To(Succeed()) 96 | 97 | By("Verify that the fetched ConfigMap matches the desired one") 98 | 99 | fetchedConfigMap := &corev1.ConfigMap{} 100 | Expect(fetchObject(ctx, r.Client, a.Namespace, expectedConfigMap.Name, fetchedConfigMap)).To(Succeed()) 101 | 102 | Expect(fetchedConfigMap.Labels).To(Equal(expectedConfigMap.Labels)) 103 | 104 | By("Call reconcileConfigMap again") 105 | Expect(r.reconcileConfigMap(ctx, a)).To(Succeed()) 106 | 107 | By("verifying that the data is still present") 108 | Expect(fetchedConfigMap.Labels).To(Equal(expectedConfigMap.Labels)) 109 | 110 | }) 111 | 112 | It("verifies traffic and metric plugin creation/modification and ensures OpenShiftRolloutPlugin existence", func() { 113 | By("Add a pod that matches the deployment's selector") 114 | addTestPodToFakeClient(r, a.Namespace, existingDeployment) 115 | 116 | expectedConfigMap := &corev1.ConfigMap{ 117 | ObjectMeta: metav1.ObjectMeta{ 118 | Name: DefaultRolloutsConfigMapName, 119 | }, 120 | } 121 | 122 | By("Adding traffic and metric plugins through the CR") 123 | a.Spec.Plugins.TrafficManagement = []v1alpha1.Plugin{ 124 | {Name: "custom-traffic-plugin", Location: trafficrouterPluginLocation}, 125 | } 126 | a.Spec.Plugins.Metric = []v1alpha1.Plugin{ 127 | {Name: "custom-metric-plugin", Location: metricPluginLocation, SHA256: "sha256-test"}, 128 | } 129 | 130 | Expect(r.Client.Update(ctx, &a)).To(Succeed()) 131 | 132 | By("Call reconcileConfigMap") 133 | Expect(r.reconcileConfigMap(ctx, a)).To(Succeed()) 134 | 135 | By("Fetched ConfigMap") 136 | fetchedConfigMap := &corev1.ConfigMap{} 137 | Expect(fetchObject(ctx, r.Client, a.Namespace, expectedConfigMap.Name, fetchedConfigMap)).To(Succeed()) 138 | 139 | By("Verify that the fetched ConfigMap contains OpenShiftRolloutPlugin") 140 | Expect(fetchedConfigMap.Name).To(Equal(expectedConfigMap.Name)) 141 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(OpenShiftRolloutPluginName)) 142 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(r.OpenShiftRoutePluginLocation)) 143 | 144 | By("Verify that the fetched ConfigMap contains plugins added by CR") 145 | Expect(fetchedConfigMap.Data[MetricPluginConfigMapKey]).To(ContainSubstring(a.Spec.Plugins.Metric[0].Name)) 146 | Expect(fetchedConfigMap.Data[MetricPluginConfigMapKey]).To(ContainSubstring(a.Spec.Plugins.Metric[0].Location)) 147 | 148 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(a.Spec.Plugins.TrafficManagement[0].Name)) 149 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(a.Spec.Plugins.TrafficManagement[0].Location)) 150 | 151 | By("Update metric and traffic plugins through RolloutManager CR") 152 | updatedPluginLocation := "https://test-updated-plugin-location" 153 | 154 | a.Spec.Plugins.TrafficManagement = []v1alpha1.Plugin{ 155 | {Name: "custom-traffic-plugin", Location: updatedPluginLocation}, 156 | } 157 | a.Spec.Plugins.Metric = []v1alpha1.Plugin{ 158 | {Name: "custom-metric-plugin", Location: updatedPluginLocation, SHA256: "sha256-test"}, 159 | } 160 | 161 | Expect(r.Client.Update(ctx, &a)).To(Succeed()) 162 | 163 | By("Call reconcileConfigMap again after update") 164 | Expect(r.reconcileConfigMap(ctx, a)).To(Succeed()) 165 | 166 | By("Fetched ConfigMap") 167 | Expect(fetchObject(ctx, r.Client, a.Namespace, expectedConfigMap.Name, fetchedConfigMap)).To(Succeed()) 168 | 169 | By("Verify that the fetched ConfigMap contains OpenShiftRolloutPlugin") 170 | Expect(fetchedConfigMap.Name).To(Equal(expectedConfigMap.Name)) 171 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(OpenShiftRolloutPluginName)) 172 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(r.OpenShiftRoutePluginLocation)) 173 | 174 | By("Verify that ConfigMap is updated with the plugins modified by RolloutManger CR") 175 | Expect(fetchedConfigMap.Data[MetricPluginConfigMapKey]).To(ContainSubstring(a.Spec.Plugins.Metric[0].Name)) 176 | Expect(fetchedConfigMap.Data[MetricPluginConfigMapKey]).To(ContainSubstring(updatedPluginLocation)) 177 | 178 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(a.Spec.Plugins.TrafficManagement[0].Name)) 179 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(updatedPluginLocation)) 180 | 181 | By("Verify that OpenShiftRolloutPlugin is not updated through RolloutManager CR") 182 | a.Spec.Plugins = v1alpha1.Plugins{ 183 | TrafficManagement: []v1alpha1.Plugin{ 184 | { 185 | Name: OpenShiftRolloutPluginName, 186 | Location: r.OpenShiftRoutePluginLocation, 187 | }, 188 | }, 189 | } 190 | 191 | Expect(r.Client.Update(ctx, &a)).To(Succeed()) 192 | 193 | By("Calling reconcileConfigMap again after the attempt to update OpenShiftRolloutPlugin") 194 | Expect(r.reconcileConfigMap(ctx, a)).ToNot(Succeed(), "the plugin %s cannot be modified or added through the RolloutManager CR", OpenShiftRolloutPluginName) 195 | 196 | By("Remove plugins from RolloutManager spec should remove plugins from ConfigMap") 197 | a.Spec.Plugins.TrafficManagement = nil 198 | a.Spec.Plugins.Metric = nil 199 | 200 | Expect(r.Client.Update(ctx, &a)).To(Succeed()) 201 | 202 | By("Call reconcileConfigMap after plugins are removed") 203 | Expect(r.reconcileConfigMap(ctx, a)).To(Succeed()) 204 | 205 | By("Fetched ConfigMap after removing plugins") 206 | Expect(fetchObject(ctx, r.Client, a.Namespace, expectedConfigMap.Name, fetchedConfigMap)).To(Succeed()) 207 | 208 | By("Verify that the fetched ConfigMap contains OpenShiftRolloutPlugin after removing plugins from CR") 209 | Expect(fetchedConfigMap.Name).To(Equal(expectedConfigMap.Name)) 210 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(OpenShiftRolloutPluginName)) 211 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).To(ContainSubstring(r.OpenShiftRoutePluginLocation)) 212 | 213 | By("Verify that the ConfigMap no longer contains removed plugins") 214 | Expect(fetchedConfigMap.Data[TrafficRouterPluginConfigMapKey]).NotTo(ContainSubstring("custom-traffic-plugin")) 215 | Expect(fetchedConfigMap.Data[MetricPluginConfigMapKey]).NotTo(ContainSubstring("custom-metric-plugin")) 216 | 217 | By("Verify that the pod has been deleted after the above update.") 218 | rolloutsPodList := &corev1.PodList{} 219 | err := r.Client.List(ctx, rolloutsPodList, client.InNamespace(a.Namespace), client.MatchingLabels(existingDeployment.Spec.Selector.MatchLabels)) 220 | Expect(err).NotTo(HaveOccurred()) 221 | Expect(len(rolloutsPodList.Items)).To(BeNumerically("==", 0)) 222 | }) 223 | }) 224 | 225 | func addTestPodToFakeClient(r *RolloutManagerReconciler, namespace string, deployment *appsv1.Deployment) { 226 | // Create a test pod with labels that match the deployment's selector 227 | testPod := &corev1.Pod{ 228 | ObjectMeta: metav1.ObjectMeta{ 229 | Name: "test-rollouts-pod", 230 | Namespace: namespace, 231 | Labels: deployment.Spec.Selector.MatchLabels, 232 | }, 233 | Spec: corev1.PodSpec{ 234 | Containers: []corev1.Container{ 235 | { 236 | Name: DefaultArgoRolloutsResourceName, 237 | Image: "argoproj/argo-rollouts:latest", 238 | }, 239 | }, 240 | }, 241 | } 242 | 243 | // Add the pod to the fake client 244 | err := r.Client.Create(context.TODO(), testPod) 245 | Expect(err).ToNot(HaveOccurred()) 246 | } 247 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /hack/upgrade-rollouts-script/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "sort" 12 | "strings" 13 | 14 | "os/exec" 15 | 16 | "github.com/google/go-github/v58/github" 17 | ) 18 | 19 | // These can be set while debugging 20 | const ( 21 | skipInitialPRCheck = false // default to false 22 | 23 | // if readOnly is true: 24 | // - PRs will not be opened 25 | // - Git commits will not be pushed to fork 26 | // This is roughly equivalent to a dry run 27 | readOnly = false // default to false 28 | 29 | ) 30 | 31 | const ( 32 | PRTitle = "Upgrade to Argo Rollouts " 33 | argoRolloutsRepoOrg = "argoproj" 34 | argoRolloutsRepoName = "argo-rollouts" 35 | 36 | argoprojlabsRepoOrg = "argoproj-labs" 37 | argoRolloutsManagerRepoName = "argo-rollouts-manager" 38 | 39 | controllersDefaultGo = "controllers/default.go" 40 | ) 41 | 42 | func main() { 43 | 44 | pathToGitHubRepo := "argo-rollouts-manager" 45 | 46 | gitHubToken := os.Getenv("GH_TOKEN") 47 | if gitHubToken == "" { 48 | exitWithError(fmt.Errorf("missing GH_TOKEN")) 49 | return 50 | } 51 | 52 | client := github.NewClient(nil).WithAuthToken(gitHubToken) 53 | 54 | // 1) Check for existing version update PRs on the repo 55 | 56 | if !skipInitialPRCheck { 57 | prList, _, err := client.PullRequests.List(context.Background(), argoprojlabsRepoOrg, argoRolloutsManagerRepoName, &github.PullRequestListOptions{}) 58 | if err != nil { 59 | exitWithError(err) 60 | return 61 | } 62 | for _, pr := range prList { 63 | if strings.HasPrefix(*pr.Title, PRTitle) { 64 | exitWithError(fmt.Errorf("PR already exists")) 65 | return 66 | } 67 | } 68 | } 69 | 70 | // 2) Pull the latest releases from rollouts repo 71 | 72 | releases, _, err := client.Repositories.ListReleases(context.Background(), argoRolloutsRepoOrg, argoRolloutsRepoName, &github.ListOptions{}) 73 | if err != nil { 74 | exitWithError(err) 75 | return 76 | } 77 | 78 | var firstProperRelease *github.RepositoryRelease 79 | 80 | for _, release := range releases { 81 | 82 | if strings.Contains(*release.TagName, "rc") { 83 | continue 84 | } 85 | firstProperRelease = release 86 | break 87 | } 88 | 89 | if firstProperRelease == nil { 90 | exitWithError(fmt.Errorf("no release found")) 91 | return 92 | } 93 | 94 | newBranchName := "upgrade-to-rollouts-" + *firstProperRelease.TagName 95 | 96 | // 3) Create, commit, and push a new branch 97 | if repoAlreadyUpToDate, err := createNewCommitAndBranch(*firstProperRelease.TagName, "quay.io/argoproj/argo-rollouts", newBranchName, pathToGitHubRepo); err != nil { 98 | 99 | if repoAlreadyUpToDate { 100 | fmt.Println("* Exiting as target repository is already up to date.") 101 | return 102 | } 103 | 104 | exitWithError(err) 105 | return 106 | } 107 | 108 | if !readOnly { 109 | 110 | bodyText := "Update to latest release of Argo Rollouts" 111 | 112 | if firstProperRelease != nil && firstProperRelease.HTMLURL != nil && *firstProperRelease.HTMLURL != "" { 113 | bodyText += ": " + *firstProperRelease.HTMLURL 114 | } 115 | 116 | bodyText += ` 117 | Before merging this PR, ensure you check the Argo Rollouts change logs and release notes: 118 | - Ensure there are no changes to the Argo Rollouts install YAML that we need to respond to with changes in the operator 119 | - You can do this by downloading the 'install.yaml' from both the previous version (for example, v1.7.1) and new version (for example, v1.7.2), and then comparing them using a tool like [Meld](https://gitlab.gnome.org/GNOME/meld) or diff. 120 | - If there are changes to resources like Deployments and Roles in the install.yaml between the two versions, this will likely require a corresponding code change within the operator. e.g. a new permission added to a Role would require a change to the Role creation code in the operator. 121 | - Ensure there are no backwards incompatible API/behaviour changes in the change logs` 122 | 123 | // 4) Create PR if it doesn't exist 124 | if stdout, stderr, err := runCommandWithWorkDir(pathToGitHubRepo, "gh", "pr", "create", 125 | "-R", argoprojlabsRepoOrg+"/"+argoRolloutsManagerRepoName, 126 | "--title", PRTitle+(*firstProperRelease.TagName), "--body", bodyText); err != nil { 127 | fmt.Println(stdout, stderr) 128 | exitWithError(err) 129 | return 130 | } 131 | } 132 | 133 | } 134 | 135 | // return true if the argo-rollouts-manager repo is already up to date 136 | func createNewCommitAndBranch(latestReleaseVersionTag string, latestReleaseVersionImage, newBranchName, pathToGitRepo string) (bool, error) { 137 | 138 | commands := [][]string{ 139 | {"git", "stash"}, 140 | {"git", "fetch", "parent"}, 141 | {"git", "checkout", "main"}, 142 | {"git", "reset", "--hard", "parent/main"}, 143 | {"git", "checkout", "-b", newBranchName}, 144 | } 145 | 146 | if err := runCommandListWithWorkDir(pathToGitRepo, commands); err != nil { 147 | return false, err 148 | } 149 | 150 | if repoTargetVersion, err := extractCurrentTargetVersionFromRepo(pathToGitRepo); err != nil { 151 | return false, fmt.Errorf("unable to extract current target version from repo") 152 | } else if repoTargetVersion == latestReleaseVersionTag { 153 | return true, fmt.Errorf("target repository is already on the most recent version") 154 | } 155 | 156 | if err := regenerateControllersDefaultGo(latestReleaseVersionTag, latestReleaseVersionImage, pathToGitRepo); err != nil { 157 | return false, err 158 | } 159 | 160 | if err := regenerateGoMod(latestReleaseVersionTag, pathToGitRepo); err != nil { 161 | return false, err 162 | } 163 | 164 | if err := regenerateArgoRolloutsE2ETestScriptMod(latestReleaseVersionTag, pathToGitRepo); err != nil { 165 | return false, err 166 | } 167 | 168 | if err := copyCRDsFromRolloutsRepo(latestReleaseVersionTag, pathToGitRepo); err != nil { 169 | return false, fmt.Errorf("unable to copy rollouts CRDs: %w", err) 170 | } 171 | 172 | commands = [][]string{ 173 | {"go", "mod", "tidy"}, 174 | {"make", "generate", "manifests"}, 175 | {"make", "bundle"}, 176 | {"make", "fmt"}, 177 | {"git", "add", "--all"}, 178 | {"git", "commit", "-s", "-m", PRTitle + latestReleaseVersionTag}, 179 | } 180 | if err := runCommandListWithWorkDir(pathToGitRepo, commands); err != nil { 181 | return false, err 182 | } 183 | 184 | if !readOnly { 185 | commands = [][]string{ 186 | {"git", "push", "-f", "--set-upstream", "origin", newBranchName}, 187 | } 188 | if err := runCommandListWithWorkDir(pathToGitRepo, commands); err != nil { 189 | return false, err 190 | } 191 | } 192 | 193 | return false, nil 194 | 195 | } 196 | 197 | func copyCRDsFromRolloutsRepo(latestReleaseVersionTag string, pathToGitRepo string) error { 198 | 199 | rolloutsRepoPath, err := checkoutRolloutsRepoIntoTempDir(latestReleaseVersionTag) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | crdPath := filepath.Join(rolloutsRepoPath, "manifests/crds") 205 | crdYamlDirEntries, err := os.ReadDir(crdPath) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | var crdYAMLs []string 211 | for _, crdYamlDirEntry := range crdYamlDirEntries { 212 | 213 | if crdYamlDirEntry.Name() == "kustomization.yaml" { 214 | continue 215 | } 216 | 217 | if !crdYamlDirEntry.IsDir() { 218 | crdYAMLs = append(crdYAMLs, crdYamlDirEntry.Name()) 219 | } 220 | } 221 | 222 | sort.Strings(crdYAMLs) 223 | 224 | // NOTE: If this line fails, check if any new CRDs have been added to Rollouts, and/or if they have changed the filenames. 225 | // - If so, this will require verifying the changes, then updating this list 226 | if !reflect.DeepEqual(crdYAMLs, []string{ 227 | "analysis-run-crd.yaml", 228 | "analysis-template-crd.yaml", 229 | "cluster-analysis-template-crd.yaml", 230 | "experiment-crd.yaml", 231 | "rollout-crd.yaml"}) { 232 | return fmt.Errorf("unexpected CRDs found: %v", crdYAMLs) 233 | } 234 | 235 | destinationPath := filepath.Join(pathToGitRepo, "config/crd/bases") 236 | for _, crdYAML := range crdYAMLs { 237 | 238 | destFile, err := os.Create(filepath.Join(destinationPath, crdYAML)) 239 | if err != nil { 240 | return fmt.Errorf("unable to create file for '%s': %w", crdYAML, err) 241 | } 242 | defer destFile.Close() 243 | 244 | srcFile, err := os.Open(filepath.Join(crdPath, crdYAML)) 245 | if err != nil { 246 | return fmt.Errorf("unable to open source file for '%s': %w", crdYAML, err) 247 | } 248 | defer srcFile.Close() 249 | 250 | _, err = io.Copy(destFile, srcFile) 251 | if err != nil { 252 | return fmt.Errorf("unable to copy file for '%s': %w", crdYAML, err) 253 | } 254 | 255 | } 256 | 257 | return nil 258 | } 259 | 260 | func checkoutRolloutsRepoIntoTempDir(latestReleaseVersionTag string) (string, error) { 261 | 262 | tmpDir, err := os.MkdirTemp("", "argo-rollouts-src") 263 | if err != nil { 264 | return "", err 265 | } 266 | 267 | if _, _, err := runCommandWithWorkDir(tmpDir, "git", "clone", "https://github.com/argoproj/argo-rollouts"); err != nil { 268 | return "", err 269 | } 270 | 271 | newWorkDir := filepath.Join(tmpDir, "argo-rollouts") 272 | 273 | commands := [][]string{ 274 | {"git", "checkout", latestReleaseVersionTag}, 275 | } 276 | 277 | if err := runCommandListWithWorkDir(newWorkDir, commands); err != nil { 278 | return "", err 279 | } 280 | 281 | return newWorkDir, nil 282 | } 283 | 284 | func runCommandListWithWorkDir(workingDir string, commands [][]string) error { 285 | 286 | for _, command := range commands { 287 | 288 | _, _, err := runCommandWithWorkDir(workingDir, command...) 289 | if err != nil { 290 | return err 291 | } 292 | } 293 | return nil 294 | } 295 | 296 | func regenerateGoMod(latestReleaseVersionTag string, pathToGitRepo string) error { 297 | 298 | // Format of string to modify: 299 | // github.com/argoproj/argo-rollouts v1.6.3 300 | 301 | path := filepath.Join(pathToGitRepo, "go.mod") 302 | 303 | fileBytes, err := os.ReadFile(path) 304 | if err != nil { 305 | return err 306 | } 307 | 308 | var res string 309 | 310 | for _, line := range strings.Split(string(fileBytes), "\n") { 311 | 312 | if strings.Contains(line, "\tgithub.com/argoproj/argo-rollouts v") { 313 | 314 | res += "\tgithub.com/argoproj/argo-rollouts " + latestReleaseVersionTag + "\n" 315 | 316 | } else { 317 | res += line + "\n" 318 | } 319 | 320 | } 321 | 322 | if err := os.WriteFile(path, []byte(res), 0600); err != nil { 323 | return err 324 | } 325 | 326 | return nil 327 | 328 | } 329 | 330 | func regenerateArgoRolloutsE2ETestScriptMod(latestReleaseVersionTag string, pathToGitRepo string) error { 331 | 332 | // Format of string to modify: 333 | // CURRENT_ROLLOUTS_VERSION=v1.6.4 334 | 335 | path := filepath.Join(pathToGitRepo, "hack/run-upstream-argo-rollouts-e2e-tests.sh") 336 | 337 | fileBytes, err := os.ReadFile(path) 338 | if err != nil { 339 | return err 340 | } 341 | 342 | var res string 343 | 344 | for _, line := range strings.Split(string(fileBytes), "\n") { 345 | 346 | if strings.Contains(line, "CURRENT_ROLLOUTS_VERSION=") { 347 | 348 | res += "CURRENT_ROLLOUTS_VERSION=" + latestReleaseVersionTag + "\n" 349 | 350 | } else { 351 | res += line + "\n" 352 | } 353 | 354 | } 355 | 356 | if err := os.WriteFile(path, []byte(res), 0600); err != nil { 357 | return err 358 | } 359 | 360 | return nil 361 | 362 | } 363 | 364 | // extractCurrentTargetVersionFromRepo read the contents of the argo-rollouts-manager repo and determine which argo-rollouts version is being targeted. 365 | func extractCurrentTargetVersionFromRepo(pathToGitRepo string) (string, error) { 366 | 367 | // Style of text string to parse: 368 | // DefaultArgoRolloutsVersion = "sha256:995450a0a7f7843d68e96d1a7f63422fa29b245c58f7b57dd0cf9cad72b8308f" //v1.4.1 369 | 370 | path := filepath.Join(pathToGitRepo, controllersDefaultGo) 371 | 372 | fileBytes, err := os.ReadFile(path) 373 | if err != nil { 374 | return "", err 375 | } 376 | 377 | for _, line := range strings.Split(string(fileBytes), "\n") { 378 | if strings.Contains(line, "DefaultArgoRolloutsVersion") { 379 | 380 | indexOfForwardSlash := strings.LastIndex(line, "/") 381 | if indexOfForwardSlash != -1 { 382 | return strings.TrimSpace(line[indexOfForwardSlash+1:]), nil 383 | } 384 | 385 | } 386 | } 387 | 388 | return "", fmt.Errorf("no version found in '" + controllersDefaultGo + "'") 389 | } 390 | 391 | func regenerateControllersDefaultGo(latestReleaseVersionTag string, latestReleaseVersionImage, pathToGitRepo string) error { 392 | 393 | // Style of text string to replace: 394 | // DefaultArgoRolloutsVersion = "sha256:995450a0a7f7843d68e96d1a7f63422fa29b245c58f7b57dd0cf9cad72b8308f" //v1.4.1 395 | 396 | path := filepath.Join(pathToGitRepo, controllersDefaultGo) 397 | 398 | fileBytes, err := os.ReadFile(path) 399 | if err != nil { 400 | return err 401 | } 402 | 403 | var res string 404 | 405 | for _, line := range strings.Split(string(fileBytes), "\n") { 406 | 407 | if strings.Contains(line, "DefaultArgoRolloutsVersion") { 408 | 409 | res += " DefaultArgoRolloutsVersion = \"" + latestReleaseVersionTag + "\" // " + latestReleaseVersionTag + "\n" 410 | 411 | } else { 412 | res += line + "\n" 413 | } 414 | 415 | } 416 | 417 | if err := os.WriteFile(path, []byte(res), 0600); err != nil { 418 | return err 419 | } 420 | 421 | return nil 422 | 423 | } 424 | 425 | func runCommandWithWorkDir(workingDir string, cmdList ...string) (string, string, error) { 426 | 427 | fmt.Println(cmdList) 428 | 429 | cmd := exec.Command(cmdList[0], cmdList[1:]...) 430 | var stdout bytes.Buffer 431 | var stderr bytes.Buffer 432 | cmd.Dir = workingDir 433 | cmd.Stdout = &stdout 434 | cmd.Stderr = &stderr 435 | 436 | err := cmd.Run() 437 | stdoutStr := stdout.String() 438 | stderrStr := stderr.String() 439 | 440 | fmt.Println(stdoutStr, stderrStr) 441 | 442 | return stdoutStr, stderrStr, err 443 | 444 | } 445 | 446 | func exitWithError(err error) { 447 | fmt.Println("ERROR:", err) 448 | os.Exit(1) 449 | } 450 | --------------------------------------------------------------------------------