From c4e1f39d28b3c77d0fa626bb29b4fcbf1adee392 Mon Sep 17 00:00:00 2001 From: wyike Date: Fri, 24 Jun 2022 18:03:04 +0800 Subject: [PATCH] Feat: Support kruise rollout (#4243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: support kruise rollout Signed-off-by: Somefive resolve roll back fix add tests Signed-off-by: 楚岳 small fix * fix rollback Signed-off-by: 楚岳 topology filter by owner reference Signed-off-by: 楚岳 fix ci Signed-off-by: 楚岳 add comments Signed-off-by: 楚岳 fix imports Signed-off-by: 楚岳 fix lint * rollback related tests Signed-off-by: 楚岳 rename the operator Signed-off-by: 楚岳 fix bugs Signed-off-by: 楚岳 fix test Signed-off-by: 楚岳 fix test * clean args before start Signed-off-by: 楚岳 fix test * remove replace go mod Signed-off-by: 楚岳 * fix operation tests Signed-off-by: 楚岳 Co-authored-by: Somefive --- go.mod | 20 +- go.sum | 33 ++ .../v1alpha2/application/apply.go | 3 + pkg/rollout/rollout.go | 200 ++++++++++++ pkg/rollout/rollout_test.go | 155 +++++++++ pkg/rollout/suit_test.go | 119 +++++++ pkg/rollout/testdata/rollouts.yaml | 290 +++++++++++++++++ pkg/utils/common/common.go | 2 + pkg/velaql/providers/query/tree.go | 23 +- pkg/velaql/providers/query/tree_test.go | 6 +- pkg/workflow/operation/operation.go | 289 +++++++++++++++++ pkg/workflow/operation/operation_test.go | 215 +++++++++++++ pkg/workflow/operation/suit_test.go | 119 +++++++ pkg/workflow/operation/testdata/rollouts.yaml | 290 +++++++++++++++++ references/cli/sample-1.0.1.tgz | Bin 0 -> 474 bytes .../cli/test-data/addon/sample/Chart.yaml | 8 + references/cli/workflow.go | 293 ++++-------------- references/cli/workflow_test.go | 8 + 18 files changed, 1820 insertions(+), 253 deletions(-) create mode 100644 pkg/rollout/rollout.go create mode 100644 pkg/rollout/rollout_test.go create mode 100644 pkg/rollout/suit_test.go create mode 100644 pkg/rollout/testdata/rollouts.yaml create mode 100644 pkg/workflow/operation/operation.go create mode 100644 pkg/workflow/operation/operation_test.go create mode 100644 pkg/workflow/operation/suit_test.go create mode 100644 pkg/workflow/operation/testdata/rollouts.yaml create mode 100644 references/cli/sample-1.0.1.tgz create mode 100644 references/cli/test-data/addon/sample/Chart.yaml diff --git a/go.mod b/go.mod index 0949f4979..eaf770ad9 100644 --- a/go.mod +++ b/go.mod @@ -105,6 +105,18 @@ require ( sigs.k8s.io/yaml v1.3.0 ) +require ( + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.0 // indirect + github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect + github.com/openkruise/rollouts v0.1.1-0.20220622054609-149e5a48da5e + github.com/xanzy/ssh-agent v0.3.0 // indirect + golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect + golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect + google.golang.org/protobuf v1.28.0 // indirect +) + require ( cloud.google.com/go/compute v1.6.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -148,7 +160,6 @@ require ( github.com/creack/pty v1.1.11 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/docker/cli v20.10.16+incompatible // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/docker v20.10.16+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/go-connections v0.4.0 // indirect @@ -190,8 +201,6 @@ require ( github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -201,7 +210,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/klauspost/compress v1.15.4 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/kr/pty v1.1.8 // indirect @@ -254,7 +262,6 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.3.2 // indirect - github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect @@ -281,16 +288,13 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 // indirect google.golang.org/grpc v1.45.0 // indirect - google.golang.org/protobuf v1.28.0 // indirect gopkg.in/gorp.v1 v1.7.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.63.2 // indirect diff --git a/go.sum b/go.sum index a9f6ef7b3..2a616c1ef 100644 --- a/go.sum +++ b/go.sum @@ -1608,6 +1608,8 @@ github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvw github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= @@ -1627,6 +1629,7 @@ github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDs github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= @@ -1662,8 +1665,11 @@ github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqi github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/openkruise/kruise-api v1.0.0/go.mod h1:kxV/UA/vrf/hz3z+kL21c0NOawC6K1ZjaKcJFgiOwsE= github.com/openkruise/kruise-api v1.1.0 h1:ZRhV0FnxUp4XHc60YPkUqj2LJD4GRFB92qhtdgU6Zhc= github.com/openkruise/kruise-api v1.1.0/go.mod h1:kxV/UA/vrf/hz3z+kL21c0NOawC6K1ZjaKcJFgiOwsE= +github.com/openkruise/rollouts v0.1.1-0.20220622054609-149e5a48da5e h1:jUMEDsA0OOpp0262pK8MV8M2glac+jIjx+q5Aydn6G0= +github.com/openkruise/rollouts v0.1.1-0.20220622054609-149e5a48da5e/go.mod h1:SORsT96ssCqMJYSVA90v6Z52utlV2jxPlyGh4czRfHA= github.com/openshift/api v0.0.0-20210915110300-3cd8091317c4/go.mod h1:RsQCVJu4qhUawxxDP7pGlwU3IA4F01wYm3qKEu29Su8= github.com/openshift/api v0.0.0-20211209135129-c58d9f695577/go.mod h1:DoslCwtqUpr3d/gsbq4ZlkaMEdYqKxuypsDjorcHhME= github.com/openshift/build-machinery-go v0.0.0-20210115170933-e575b44a7a94/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= @@ -2199,6 +2205,7 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8= @@ -2375,6 +2382,7 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -2547,6 +2555,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -3073,7 +3082,10 @@ k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= k8s.io/api v0.21.2/go.mod h1:Lv6UGJZ1rlMI1qusN8ruAp9PUBFyBwpEHAdG24vIsiU= k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg= k8s.io/api v0.22.1/go.mod h1:bh13rkTp3F1XEaLGykbyRD2QaTTzPm0e/BMd8ptFONY= +k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= k8s.io/api v0.22.4/go.mod h1:Rgs+9gIGYC5laXQSZZ9JqT5NevNgoGiOdVWi1BAB3qk= +k8s.io/api v0.22.4/go.mod h1:Rgs+9gIGYC5laXQSZZ9JqT5NevNgoGiOdVWi1BAB3qk= +k8s.io/api v0.22.6/go.mod h1:q1F7IfaNrbi/83ebLy3YFQYLjPSNyunZ/IXQxMmbwCg= k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= k8s.io/api v0.23.1/go.mod h1:WfXnOnwSqNtG62Y1CdjoMxh7r7u9QXGCkA1u0na2jgo= k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= @@ -3089,7 +3101,9 @@ k8s.io/apiextensions-apiserver v0.18.6/go.mod h1:lv89S7fUysXjLZO7ke783xOwVTm6lKi k8s.io/apiextensions-apiserver v0.21.2/go.mod h1:+Axoz5/l3AYpGLlhJDfcVQzCerVYq3K3CvDMvw6X1RA= k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8LGDkprcd+KbsE= k8s.io/apiextensions-apiserver v0.22.1/go.mod h1:HeGmorjtRmRLE+Q8dJu6AYRoZccvCMsghwS8XTUYb2c= +k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA= k8s.io/apiextensions-apiserver v0.22.4/go.mod h1:kH9lxD8dbJ+k0ZizGET55lFgdGjO8t45fgZnCVdZEpw= +k8s.io/apiextensions-apiserver v0.22.6/go.mod h1:wNsLwy8mfIkGThiv4Qq/Hy4qRazViKXqmH5pfYiRKyY= k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ= k8s.io/apiextensions-apiserver v0.23.6 h1:v58cQ6Z0/GK1IXYr+oW0fnYl52o9LTY0WgoWvI8uv5Q= @@ -3119,7 +3133,9 @@ k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswP k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apimachinery v0.22.4/go.mod h1:yU6oA6Gnax9RrxGzVvPFFJ+mpnW6PBSqp0sx0I0HHW0= +k8s.io/apimachinery v0.22.6/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno= k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= @@ -3139,7 +3155,10 @@ k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= k8s.io/apiserver v0.21.2/go.mod h1:lN4yBoGyiNT7SC1dmNk0ue6a5Wi6O3SWOIw91TsucQw= k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU= k8s.io/apiserver v0.22.1/go.mod h1:2mcM6dzSt+XndzVQJX21Gx0/Klo7Aen7i0Ai6tIa400= +k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= k8s.io/apiserver v0.22.4/go.mod h1:38WmcUZiiy41A7Aty8/VorWRa8vDGqoUzDf2XYlku0E= +k8s.io/apiserver v0.22.4/go.mod h1:38WmcUZiiy41A7Aty8/VorWRa8vDGqoUzDf2XYlku0E= +k8s.io/apiserver v0.22.6/go.mod h1:OlL1rGa2kKWGj2JEXnwBcul/BwC9Twe95gm4ohtiIIs= k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= k8s.io/apiserver v0.23.1/go.mod h1:Bqt0gWbeM2NefS8CjWswwd2VNAKN6lUKR85Ft4gippY= k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw= @@ -3173,7 +3192,9 @@ k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= k8s.io/client-go v0.21.2/go.mod h1:HdJ9iknWpbl3vMGtib6T2PyI/VYxiZfq936WNVHBRrA= k8s.io/client-go v0.21.3/go.mod h1:+VPhCgTsaFmGILxR/7E1N0S+ryO010QBeNCv5JwRGYU= k8s.io/client-go v0.22.1/go.mod h1:BquC5A4UOo4qVDUtoc04/+Nxp1MeHcVc1HJm1KmG8kk= +k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= k8s.io/client-go v0.22.4/go.mod h1:Yzw4e5e7h1LNHA4uqnMVrpEpUs1hJOiuBsJKIlRCHDA= +k8s.io/client-go v0.22.6/go.mod h1:TffU4AV2idZGeP+g3kdFZP+oHVHWPL1JYFySOALriw0= k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= k8s.io/client-go v0.23.1/go.mod h1:6QSI8fEuqD4zgFK0xbdwfB/PthBsIxCJMa3s17WlcO0= k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= @@ -3189,10 +3210,13 @@ k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRV k8s.io/code-generator v0.18.6/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= k8s.io/code-generator v0.20.0/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= k8s.io/code-generator v0.20.10/go.mod h1:i6FmG+QxaLxvJsezvZp0q/gAEzzOz3U53KFibghWToU= +k8s.io/code-generator v0.20.10/go.mod h1:i6FmG+QxaLxvJsezvZp0q/gAEzzOz3U53KFibghWToU= k8s.io/code-generator v0.21.2/go.mod h1:8mXJDCB7HcRo1xiEQstcguZkbxZaqeUOrO9SsicWs3U= k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= k8s.io/code-generator v0.22.1/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= +k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= k8s.io/code-generator v0.22.4/go.mod h1:qjYl54pQ/emhkT0UxbufbREYJMWsHNNV/jSVwhYZQGw= +k8s.io/code-generator v0.22.6/go.mod h1:iOZwYADSgFPNGWfqHFfg1V0TNJnl1t0WyZluQp4baqU= k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= k8s.io/code-generator v0.23.1/go.mod h1:V7yn6VNTCWW8GqodYCESVo95fuiEg713S8B7WacWZDA= k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= @@ -3213,7 +3237,9 @@ k8s.io/component-base v0.20.10/go.mod h1:ZKOEin1xu68aJzxgzl5DZSp5J1IrjAOPlPN90/t k8s.io/component-base v0.21.2/go.mod h1:9lvmIThzdlrJj5Hp8Z/TOgIkdfsNARQ1pT+3PByuiuc= k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ= k8s.io/component-base v0.22.1/go.mod h1:0D+Bl8rrnsPN9v0dyYvkqFfBeAd4u7n77ze+p8CMiPo= +k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllunGug= k8s.io/component-base v0.22.4/go.mod h1:MrSaQy4a3tFVViff8TZL6JHYSewNCLshZCwHYM58v5A= +k8s.io/component-base v0.22.6/go.mod h1:ngHLefY4J5fq2fApNdbWyj4yh0lvw36do4aAjNN8rc8= k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= k8s.io/component-base v0.23.1/go.mod h1:6llmap8QtJIXGDd4uIWJhAq0Op8AtQo6bDW2RrNMTeo= k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= @@ -3262,6 +3288,7 @@ k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAG k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kubectl v0.0.0-20191219154910-1528d4eea6dd/go.mod h1:9ehGcuUGjXVZh0qbYSB0vvofQw2JQe6c6cO0k4wu/Oo= @@ -3287,6 +3314,7 @@ k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211208161948-7d6a63dca704/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= @@ -3330,6 +3358,7 @@ sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2 sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= sigs.k8s.io/controller-runtime v0.9.2/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk= sigs.k8s.io/controller-runtime v0.9.5/go.mod h1:q6PpkM5vqQubEKUKOM6qr06oXGzOBcCby1DA9FbyZeA= +sigs.k8s.io/controller-runtime v0.10.3/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= sigs.k8s.io/controller-runtime v0.11.1/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= sigs.k8s.io/controller-runtime v0.11.2 h1:H5GTxQl0Mc9UjRJhORusqfJCIjBO8UtUxGggCwL1rLA= @@ -3370,7 +3399,11 @@ sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w= sigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go index 604f39b64..61628c497 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go @@ -280,6 +280,9 @@ func (h *AppHandler) collectHealthStatus(ctx context.Context, wl *appfile.Worklo if err != nil { return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, evaluate status message error", appName, wl.Name, tr.Name) } + if status.Message == "" && traitStatus.Message != "" { + status.Message = traitStatus.Message + } traitStatusList = append(traitStatusList, traitStatus) namespace = appRev.GetNamespace() wl.Ctx.SetCtx(context.WithValue(wl.Ctx.GetCtx(), multicluster.ClusterContextKey, status.Cluster)) diff --git a/pkg/rollout/rollout.go b/pkg/rollout/rollout.go new file mode 100644 index 000000000..7c240f996 --- /dev/null +++ b/pkg/rollout/rollout.go @@ -0,0 +1,200 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rollout + +import ( + "context" + "fmt" + "io" + + "github.com/pkg/errors" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + + kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/resourcetracker" + velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors" +) + +// ClusterRollout rollout in specified cluster +type ClusterRollout struct { + *kruisev1alpha1.Rollout + Cluster string +} + +func getAssociatedRollouts(ctx context.Context, cli client.Client, app *v1beta1.Application, withHistoryRTs bool) ([]*ClusterRollout, error) { + rootRT, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, cli, app) + if err != nil { + return nil, errors.Wrapf(err, "failed to list resource trackers") + } + if !withHistoryRTs { + historyRTs = []*v1beta1.ResourceTracker{} + } + var rollouts []*ClusterRollout + for _, rt := range append(historyRTs, rootRT, currentRT) { + if rt == nil { + continue + } + for _, mr := range rt.Spec.ManagedResources { + if mr.APIVersion == kruisev1alpha1.SchemeGroupVersion.String() && mr.Kind == "Rollout" { + rollout := &kruisev1alpha1.Rollout{} + if err = cli.Get(multicluster.ContextWithClusterName(ctx, mr.Cluster), k8stypes.NamespacedName{Namespace: mr.Namespace, Name: mr.Name}, rollout); err != nil { + if multicluster.IsNotFoundOrClusterNotExists(err) || velaerrors.IsCRDNotExists(err) { + continue + } + return nil, errors.Wrapf(err, "failed to get kruise rollout %s/%s in cluster %s", mr.Namespace, mr.Name, mr.Cluster) + } + rollouts = append(rollouts, &ClusterRollout{Rollout: rollout, Cluster: mr.Cluster}) + } + } + } + return rollouts, nil +} + +// SuspendRollout find all rollouts associated with the application (including history RTs) and resume them +func SuspendRollout(ctx context.Context, cli client.Client, app *v1beta1.Application, writer io.Writer) error { + rollouts, err := getAssociatedRollouts(ctx, cli, app, true) + if err != nil { + return err + } + for i := range rollouts { + rollout := rollouts[i] + if rollout.Status.Phase == kruisev1alpha1.RolloutPhaseProgressing && !rollout.Spec.Strategy.Paused { + _ctx := multicluster.ContextWithClusterName(ctx, rollout.Cluster) + rolloutKey := client.ObjectKeyFromObject(rollout.Rollout) + if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err = cli.Get(_ctx, rolloutKey, rollout.Rollout); err != nil { + return err + } + if rollout.Status.Phase == kruisev1alpha1.RolloutPhaseProgressing && !rollout.Spec.Strategy.Paused { + rollout.Spec.Strategy.Paused = true + if err = cli.Update(_ctx, rollout.Rollout); err != nil { + return err + } + if writer != nil { + _, _ = writer.Write([]byte(fmt.Sprintf("Rollout %s/%s in cluster %s suspended.\n", rollout.Namespace, rollout.Name, rollout.Cluster))) + } + return nil + } + return nil + }); err != nil { + return errors.Wrapf(err, "failed to suspend rollout %s/%s in cluster %s", rollout.Namespace, rollout.Name, rollout.Cluster) + } + } + } + return nil +} + +// ResumeRollout find all rollouts associated with the application (in the current RT) and resume them +func ResumeRollout(ctx context.Context, cli client.Client, app *v1beta1.Application, writer io.Writer) (bool, error) { + rollouts, err := getAssociatedRollouts(ctx, cli, app, false) + if err != nil { + return false, err + } + modified := false + for i := range rollouts { + rollout := rollouts[i] + if rollout.Spec.Strategy.Paused || (rollout.Status.CanaryStatus != nil && rollout.Status.CanaryStatus.CurrentStepState == kruisev1alpha1.CanaryStepStatePaused) { + _ctx := multicluster.ContextWithClusterName(ctx, rollout.Cluster) + rolloutKey := client.ObjectKeyFromObject(rollout.Rollout) + resumed := false + if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err = cli.Get(_ctx, rolloutKey, rollout.Rollout); err != nil { + return err + } + if rollout.Spec.Strategy.Paused { + rollout.Spec.Strategy.Paused = false + if err = cli.Update(_ctx, rollout.Rollout); err != nil { + return err + } + resumed = true + return nil + } + return nil + }); err != nil { + return false, errors.Wrapf(err, "failed to resume rollout %s/%s in cluster %s", rollout.Namespace, rollout.Name, rollout.Cluster) + } + if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err = cli.Get(_ctx, rolloutKey, rollout.Rollout); err != nil { + return err + } + if rollout.Status.CanaryStatus != nil && rollout.Status.CanaryStatus.CurrentStepState == kruisev1alpha1.CanaryStepStatePaused { + rollout.Status.CanaryStatus.CurrentStepState = kruisev1alpha1.CanaryStepStateReady + if err = cli.Status().Update(_ctx, rollout.Rollout); err != nil { + return err + } + resumed = true + return nil + } + return nil + }); err != nil { + return false, errors.Wrapf(err, "failed to resume rollout %s/%s in cluster %s", rollout.Namespace, rollout.Name, rollout.Cluster) + } + if resumed { + modified = true + if writer != nil { + _, _ = writer.Write([]byte(fmt.Sprintf("Rollout %s/%s in cluster %s resumed.\n", rollout.Namespace, rollout.Name, rollout.Cluster))) + } + } + } + } + return modified, nil +} + +// RollbackRollout find all rollouts associated with the application (in the current RT) and disable the pause field. +func RollbackRollout(ctx context.Context, cli client.Client, app *v1beta1.Application, writer io.Writer) (bool, error) { + rollouts, err := getAssociatedRollouts(ctx, cli, app, false) + if err != nil { + return false, err + } + modified := false + for i := range rollouts { + rollout := rollouts[i] + if rollout.Spec.Strategy.Paused || (rollout.Status.CanaryStatus != nil && rollout.Status.CanaryStatus.CurrentStepState == kruisev1alpha1.CanaryStepStatePaused) { + _ctx := multicluster.ContextWithClusterName(ctx, rollout.Cluster) + rolloutKey := client.ObjectKeyFromObject(rollout.Rollout) + resumed := false + if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err = cli.Get(_ctx, rolloutKey, rollout.Rollout); err != nil { + return err + } + if rollout.Spec.Strategy.Paused { + rollout.Spec.Strategy.Paused = false + if err = cli.Update(_ctx, rollout.Rollout); err != nil { + return err + } + resumed = true + return nil + } + return nil + }); err != nil { + return false, errors.Wrapf(err, "failed to rollback rollout %s/%s in cluster %s", rollout.Namespace, rollout.Name, rollout.Cluster) + } + if resumed { + modified = true + if writer != nil { + _, _ = writer.Write([]byte(fmt.Sprintf("Rollout %s/%s in cluster %s rollback.\n", rollout.Namespace, rollout.Name, rollout.Cluster))) + } + } + } + } + return modified, nil +} diff --git a/pkg/rollout/rollout_test.go b/pkg/rollout/rollout_test.go new file mode 100644 index 000000000..69db27018 --- /dev/null +++ b/pkg/rollout/rollout_test.go @@ -0,0 +1,155 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rollout + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/oam/util" +) + +var _ = Describe("Kruise rollout test", func() { + ctx := context.Background() + BeforeEach(func() { + Expect(k8sClient.Create(ctx, rollout.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) + Expect(k8sClient.Create(ctx, rt.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) + Expect(k8sClient.Create(ctx, app.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) + }) + + It("test get associated rollout func", func() { + rollouts, err := getAssociatedRollouts(ctx, k8sClient, &app, false) + Expect(err).Should(BeNil()) + Expect(len(rollouts)).Should(BeEquivalentTo(1)) + }) + + It("Suspend rollout", func() { + r := kruisev1alpha1.Rollout{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)).Should(BeNil()) + r.Status.Phase = kruisev1alpha1.RolloutPhaseProgressing + Expect(k8sClient.Status().Update(ctx, &r)).Should(BeNil()) + Expect(SuspendRollout(ctx, k8sClient, &app, nil)) + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)) + Expect(r.Spec.Strategy.Paused).Should(BeEquivalentTo(true)) + }) + + It("Resume rollout", func() { + r := kruisev1alpha1.Rollout{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)).Should(BeNil()) + Expect(r.Spec.Strategy.Paused).Should(BeEquivalentTo(true)) + Expect(ResumeRollout(ctx, k8sClient, &app, nil)) + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)) + Expect(r.Spec.Strategy.Paused).Should(BeEquivalentTo(false)) + }) + + It("Rollback rollout", func() { + r := kruisev1alpha1.Rollout{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)).Should(BeNil()) + r.Spec.Strategy.Paused = true + Expect(k8sClient.Update(ctx, &r)).Should(BeNil()) + Expect(RollbackRollout(ctx, k8sClient, &app, nil)) + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)) + Expect(r.Spec.Strategy.Paused).Should(BeEquivalentTo(false)) + }) +}) + +var app = v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "core.oam.dev/v1beta1", + Kind: "Application", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rollout-app", + Namespace: "default", + Generation: 1, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{}, + }, +} + +var rt = v1beta1.ResourceTracker{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "core.oam.dev/v1beta1", + Kind: "ResourceTracker", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rollout-app", + Labels: map[string]string{ + "app.oam.dev/appRevision": "rollout-app-v1", + "app.oam.dev/name": "rollout-app", + "app.oam.dev/namespace": "default", + }, + }, + Spec: v1beta1.ResourceTrackerSpec{ + ApplicationGeneration: 1, + Type: v1beta1.ResourceTrackerTypeVersioned, + ManagedResources: []v1beta1.ManagedResource{ + { + ClusterObjectReference: common.ClusterObjectReference{ + ObjectReference: v1.ObjectReference{ + APIVersion: "rollouts.kruise.io/v1alpha1", + Kind: "Rollout", + Name: "my-rollout", + Namespace: "default", + }, + }, + OAMObjectReference: common.OAMObjectReference{ + Component: "my-rollout", + }, + }, + }, + }, +} + +var rollout = kruisev1alpha1.Rollout{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rollouts.kruise.io/v1alpha1", + Kind: "Rollout", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-rollout", + Namespace: "default", + }, + Spec: kruisev1alpha1.RolloutSpec{ + ObjectRef: kruisev1alpha1.ObjectRef{ + WorkloadRef: &kruisev1alpha1.WorkloadRef{ + APIVersion: "appsv1", + Kind: "Deployment", + Name: "canary-demo", + }, + }, + Strategy: kruisev1alpha1.RolloutStrategy{ + Canary: &kruisev1alpha1.CanaryStrategy{ + Steps: []kruisev1alpha1.CanaryStep{ + { + Weight: 30, + }, + }, + }, + Paused: false, + }, + }, +} diff --git a/pkg/rollout/suit_test.go b/pkg/rollout/suit_test.go new file mode 100644 index 000000000..933be1921 --- /dev/null +++ b/pkg/rollout/suit_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rollout + +import ( + "context" + "path/filepath" + "testing" + "time" + + "k8s.io/client-go/discovery" + ocmclusterv1 "open-cluster-management.io/api/cluster/v1" + ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1" + ocmworkv1 "open-cluster-management.io/api/work/v1" + + v12 "k8s.io/api/core/v1" + crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1" + + coreoam "github.com/oam-dev/kubevela/apis/core.oam.dev" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + // +kubebuilder:scaffold:imports +) + +var cfg *rest.Config +var scheme *runtime.Scheme +var k8sClient client.Client +var testEnv *envtest.Environment +var dm discoverymapper.DiscoveryMapper +var pd *packages.PackageDiscover +var testns string +var dc *discovery.DiscoveryClient + +func TestAddon(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, + "Kruise rollout Suite test", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func(done Done) { + logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter))) + By("bootstrapping test environment") + useExistCluster := false + testEnv = &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute, + ControlPlaneStopTimeout: time.Minute, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "charts", "vela-core", "crds"), filepath.Join("", "testdata")}, + UseExistingCluster: &useExistCluster, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + scheme = runtime.NewScheme() + Expect(coreoam.AddToScheme(scheme)).NotTo(HaveOccurred()) + Expect(clientgoscheme.AddToScheme(scheme)).NotTo(HaveOccurred()) + Expect(crdv1.AddToScheme(scheme)).NotTo(HaveOccurred()) + _ = ocmclusterv1alpha1.Install(scheme) + _ = ocmclusterv1.Install(scheme) + _ = ocmworkv1.Install(scheme) + _ = kruisev1alpha1.AddToScheme(scheme) + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + + dc, err = discovery.NewDiscoveryClientForConfig(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(dc).ShouldNot(BeNil()) + + dm, err = discoverymapper.New(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(dm).ToNot(BeNil()) + pd, err = packages.NewPackageDiscover(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(pd).ToNot(BeNil()) + testns = "vela-system" + Expect(k8sClient.Create(context.Background(), + &v12.Namespace{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Namespace"}, ObjectMeta: metav1.ObjectMeta{ + Name: testns, + }})) + + close(done) +}, 120) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) diff --git a/pkg/rollout/testdata/rollouts.yaml b/pkg/rollout/testdata/rollouts.yaml new file mode 100644 index 000000000..43ab7546a --- /dev/null +++ b/pkg/rollout/testdata/rollouts.yaml @@ -0,0 +1,290 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: rollouts.rollouts.kruise.io +spec: + group: rollouts.kruise.io + names: + kind: Rollout + listKind: RolloutList + plural: rollouts + singular: rollout + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The rollout status phase + jsonPath: .status.phase + name: STATUS + type: string + - description: The rollout canary status step + jsonPath: .status.canaryStatus.currentStepIndex + name: CANARY_STEP + type: integer + - description: The rollout canary status step state + jsonPath: .status.canaryStatus.currentStepState + name: CANARY_STATE + type: string + - description: The rollout canary status message + jsonPath: .status.message + name: MESSAGE + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Rollout is the Schema for the rollouts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: RolloutSpec defines the desired state of Rollout + properties: + objectRef: + description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + ObjectRef indicates workload' + properties: + workloadRef: + description: WorkloadRef contains enough information to let you + identify a workload for Rollout Batch release of the bypass + properties: + apiVersion: + description: API Version of the referent + type: string + kind: + description: Kind of the referent + type: string + name: + description: Name of the referent + type: string + required: + - apiVersion + - kind + - name + type: object + type: object + strategy: + description: rollout strategy + properties: + canary: + description: CanaryStrategy defines parameters for a Replica Based + Canary + properties: + steps: + description: Steps define the order of phases to execute release + in batches(20%, 40%, 60%, 80%, 100%) + items: + description: CanaryStep defines a step of a canary workload. + properties: + pause: + description: Pause defines a pause stage for a rollout, + manual or auto + properties: + duration: + description: Duration the amount of time to wait + before moving to the next step. + format: int32 + type: integer + type: object + replicas: + anyOf: + - type: integer + - type: string + description: 'Replicas is the number of expected canary + pods in this batch it can be an absolute number (ex: + 5) or a percentage of total pods.' + x-kubernetes-int-or-string: true + weight: + description: SetWeight sets what percentage of the canary + pods should receive + format: int32 + type: integer + type: object + type: array + trafficRoutings: + description: TrafficRoutings hosts all the supported service + meshes supported to enable more fine-grained traffic routing + todo current only support one + items: + description: TrafficRouting hosts all the different configuration + for supported service meshes to enable more fine-grained + traffic routing + properties: + gracePeriodSeconds: + description: Optional duration in seconds the traffic + provider(e.g. nginx ingress controller) consumes the + service, ingress configuration changes gracefully. + format: int32 + type: integer + ingress: + description: Ingress holds Ingress specific configuration + to route traffic, e.g. Nginx, Alb. + properties: + name: + description: Name refers to the name of an `Ingress` + resource in the same namespace as the `Rollout` + type: string + required: + - name + type: object + service: + description: Service holds the name of a service which + selects pods with stable version and don't select + any pods with canary version. + type: string + type: + description: nginx, alb, istio etc. + type: string + required: + - service + - type + type: object + type: array + type: object + paused: + description: Paused indicates that the Rollout is paused. Default + value is false + type: boolean + type: object + required: + - objectRef + - strategy + type: object + status: + description: RolloutStatus defines the observed state of Rollout + properties: + canaryStatus: + description: Canary describes the state of the canary rollout + properties: + canaryReadyReplicas: + description: CanaryReadyReplicas the numbers of ready canary revision + pods + format: int32 + type: integer + canaryReplicas: + description: CanaryReplicas the numbers of canary revision pods + format: int32 + type: integer + canaryRevision: + description: CanaryRevision is calculated by rollout based on + podTemplateHash, and the internal logic flow uses It may be + different from rs podTemplateHash in different k8s versions, + so it cannot be used as service selector label + type: string + canaryService: + description: CanaryService holds the name of a service which selects + pods with canary version and don't select any pods with stable + version. + type: string + currentStepIndex: + description: CurrentStepIndex defines the current step of the + rollout is on. If the current step index is null, the controller + will execute the rollout. + format: int32 + type: integer + currentStepState: + type: string + lastReadyTime: + description: The last time this step pods is ready. + format: date-time + type: string + message: + type: string + observedWorkloadGeneration: + description: observedWorkloadGeneration is the most recent generation + observed for this Rollout ref workload generation. + format: int64 + type: integer + podTemplateHash: + description: pod template hash is used as service selector label + type: string + rolloutHash: + description: RolloutHash from rollout.spec object + type: string + required: + - canaryReadyReplicas + - canaryReplicas + - canaryService + - currentStepState + - podTemplateHash + type: object + conditions: + description: Conditions a list of conditions a rollout can have. + items: + description: RolloutCondition describes the state of a rollout at + a certain point. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Phase of the condition, one of True, False, Unknown. + type: string + type: + description: Type of rollout condition. + type: string + required: + - message + - reason + - status + - type + type: object + type: array + message: + description: Message provides details on why the rollout is in its + current phase + type: string + observedGeneration: + description: observedGeneration is the most recent generation observed + for this Rollout. + format: int64 + type: integer + phase: + description: BlueGreenStatus *BlueGreenStatus `json:"blueGreenStatus,omitempty"` + Phase is the rollout phase. + type: string + stableRevision: + description: CanaryRevision the hash of the canary pod template CanaryRevision + string `json:"canaryRevision,omitempty"` StableRevision indicates + the revision pods that has successfully rolled out + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] \ No newline at end of file diff --git a/pkg/utils/common/common.go b/pkg/utils/common/common.go index 4dbc5ce5e..6fa3f31a9 100644 --- a/pkg/utils/common/common.go +++ b/pkg/utils/common/common.go @@ -37,6 +37,7 @@ import ( "github.com/hashicorp/hcl/v2/hclparse" "github.com/oam-dev/terraform-config-inspect/tfconfig" kruise "github.com/openkruise/kruise-api/apps/v1alpha1" + kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1" errors2 "github.com/pkg/errors" certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1" istioclientv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" @@ -102,6 +103,7 @@ func init() { _ = ocmworkv1.Install(Scheme) _ = clustergatewayapi.AddToScheme(Scheme) _ = metricsV1beta1api.AddToScheme(Scheme) + _ = kruisev1alpha1.AddToScheme(Scheme) _ = prismclusterv1alpha1.AddToScheme(Scheme) // +kubebuilder:scaffold:scheme } diff --git a/pkg/velaql/providers/query/tree.go b/pkg/velaql/providers/query/tree.go index b17c2f6f7..11e78b178 100644 --- a/pkg/velaql/providers/query/tree.go +++ b/pkg/velaql/providers/query/tree.go @@ -89,7 +89,8 @@ func init() { {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}: nil, {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"}: nil, }, - DefaultGenListOptionFunc: helmRelease2AnyListOption, + DefaultGenListOptionFunc: helmRelease2AnyListOption, + DisableFilterByOwnerReference: true, } } @@ -117,6 +118,8 @@ type ChildrenResourcesRule struct { CareResource map[ResourceType]genListOptionFunc // if specified genListOptionFunc is nil will use use default genListOptionFunc to generate listOption. DefaultGenListOptionFunc genListOptionFunc + // DisableFilterByOwnerReference means don't use parent resource's UID filter the result. + DisableFilterByOwnerReference bool } type genListOptionFunc func(unstructured.Unstructured) (client.ListOptions, error) @@ -618,7 +621,7 @@ func fetchObjectWithResourceTreeNode(ctx context.Context, cluster string, k8sCli } func listItemByRule(clusterCTX context.Context, k8sClient client.Client, resource ResourceType, - parentObject unstructured.Unstructured, specifiedFunc genListOptionFunc, defaultFunc genListOptionFunc) ([]unstructured.Unstructured, error) { + parentObject unstructured.Unstructured, specifiedFunc genListOptionFunc, defaultFunc genListOptionFunc, disableFilterByOwner bool) ([]unstructured.Unstructured, error) { itemList := unstructured.UnstructuredList{} itemList.SetAPIVersion(resource.APIVersion) @@ -657,6 +660,20 @@ func listItemByRule(clusterCTX context.Context, k8sClient client.Client, resourc if err != nil { return nil, err } + if !disableFilterByOwner { + var res []unstructured.Unstructured + for _, item := range itemList.Items { + if len(item.GetOwnerReferences()) == 0 { + res = append(res, item) + } + for _, reference := range item.GetOwnerReferences() { + if reference.UID == parentObject.GetUID() { + res = append(res, item) + } + } + } + return res, nil + } return itemList.Items, nil } @@ -676,7 +693,7 @@ func iteratorChildResources(ctx context.Context, cluster string, k8sClient clien var resList []*types.ResourceTreeNode for resource, specifiedFunc := range rules.CareResource { clusterCTX := multicluster.ContextWithClusterName(ctx, cluster) - items, err := listItemByRule(clusterCTX, k8sClient, resource, *parentObject, specifiedFunc, rules.DefaultGenListOptionFunc) + items, err := listItemByRule(clusterCTX, k8sClient, resource, *parentObject, specifiedFunc, rules.DefaultGenListOptionFunc, rules.DisableFilterByOwnerReference) if err != nil { if meta.IsNoMatchError(err) || runtime.IsNotRegisteredError(err) { log.Logger.Errorf("error to list subresources: %s err: %v", resource.Kind, err) diff --git a/pkg/velaql/providers/query/tree_test.go b/pkg/velaql/providers/query/tree_test.go index b841c6c13..4e363875b 100644 --- a/pkg/velaql/providers/query/tree_test.go +++ b/pkg/velaql/providers/query/tree_test.go @@ -1266,14 +1266,14 @@ var _ = Describe("unit-test to e2e test", func() { u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(deploy1.DeepCopy()) Expect(err).Should(BeNil()) items, err := listItemByRule(ctx, k8sClient, ResourceType{APIVersion: "apps/v1", Kind: "ReplicaSet"}, unstructured.Unstructured{Object: u}, - deploy2RsLabelListOption, nil) + deploy2RsLabelListOption, nil, true) Expect(err).Should(BeNil()) Expect(len(items)).Should(BeEquivalentTo(2)) u2, err := runtime.DefaultUnstructuredConverter.ToUnstructured(deploy2.DeepCopy()) Expect(err).Should(BeNil()) items2, err := listItemByRule(ctx, k8sClient, ResourceType{APIVersion: "apps/v1", Kind: "ReplicaSet"}, unstructured.Unstructured{Object: u2}, - nil, deploy2RsLabelListOption) + nil, deploy2RsLabelListOption, true) Expect(len(items2)).Should(BeEquivalentTo(1)) // test use ownerReference UId to filter @@ -1285,7 +1285,7 @@ var _ = Describe("unit-test to e2e test", func() { Expect(k8sClient.Get(ctx, types2.NamespacedName{Namespace: u3.GetNamespace(), Name: u3.GetName()}, &u3)) Expect(err).Should(BeNil()) items3, err := listItemByRule(ctx, k8sClient, ResourceType{APIVersion: "v1", Kind: "Pod"}, u3, - nil, nil) + nil, nil, true) Expect(err).Should(BeNil()) Expect(len(items3)).Should(BeEquivalentTo(1)) }) diff --git a/pkg/workflow/operation/operation.go b/pkg/workflow/operation/operation.go new file mode 100644 index 000000000..521fefa6f --- /dev/null +++ b/pkg/workflow/operation/operation.go @@ -0,0 +1,289 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package operation + +import ( + "context" + "fmt" + "io" + + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/domain/service" + "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application" + "github.com/oam-dev/kubevela/pkg/controller/utils" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/resourcetracker" + "github.com/oam-dev/kubevela/pkg/rollout" + errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" + + "github.com/pkg/errors" +) + +// WorkflowOperator is opratior handler for workflow's resume/rollback/restart +type WorkflowOperator interface { + Suspend(ctx context.Context, app *v1beta1.Application) error + Resume(ctx context.Context, app *v1beta1.Application) error + Rollback(ctx context.Context, app *v1beta1.Application) error + Restart(ctx context.Context, app *v1beta1.Application) error + Terminate(ctx context.Context, app *v1beta1.Application) error +} + +// NewWorkflowOperator get an workflow operator with k8sClient and ioWriter(optional, useful for cli) +func NewWorkflowOperator(cli client.Client, w io.Writer) WorkflowOperator { + return wfOperator{cli: cli, outputWriter: w} +} + +type wfOperator struct { + cli client.Client + outputWriter io.Writer +} + +// Suspend a running workflow +func (wo wfOperator) Suspend(ctx context.Context, app *v1beta1.Application) error { + if app.Status.Workflow == nil { + return fmt.Errorf("the workflow in application is not running") + } + var err error + if err = rollout.SuspendRollout(context.Background(), wo.cli, app, wo.outputWriter); err != nil { + return err + } + appKey := client.ObjectKeyFromObject(app) + if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err := wo.cli.Get(ctx, appKey, app); err != nil { + return err + } + // set the workflow suspend to true + app.Status.Workflow.Suspend = true + return wo.cli.Status().Patch(ctx, app, client.Merge) + }); err != nil { + return err + } + + return wo.writeOutputF("Successfully suspend workflow: %s\n", app.Name) +} + +// Resume a suspending workflow +func (wo wfOperator) Resume(ctx context.Context, app *v1beta1.Application) error { + if app.Status.Workflow == nil { + return fmt.Errorf("the workflow in application is not running") + } + if app.Status.Workflow.Terminated { + return fmt.Errorf("can not resume a terminated workflow") + } + + var rolloutResumed bool + var err error + + if rolloutResumed, err = rollout.ResumeRollout(context.Background(), wo.cli, app, wo.outputWriter); err != nil { + return err + } + if !rolloutResumed && !app.Status.Workflow.Suspend { + return wo.writeOutput("the workflow is not suspending") + } + + if app.Status.Workflow.Suspend { + if err = service.ResumeWorkflow(ctx, wo.cli, app); err != nil { + return err + } + } + return nil +} + +// Rollback a running in middle state workflow. +//nolint +func (wo wfOperator) Rollback(ctx context.Context, app *v1beta1.Application) error { + if oam.GetPublishVersion(app) == "" { + return fmt.Errorf("app without public version cannot rollback") + } + + appRevs, err := application.GetSortedAppRevisions(ctx, wo.cli, app.Name, app.Namespace) + if err != nil { + return errors.Wrapf(err, "failed to list revisions for application %s/%s", app.Namespace, app.Name) + } + + // find succeeded revision to rollback + var rev *v1beta1.ApplicationRevision + var outdatedRev []*v1beta1.ApplicationRevision + for i := range appRevs { + candidate := appRevs[len(appRevs)-i-1] + _rev := candidate.DeepCopy() + if !candidate.Status.Succeeded || oam.GetPublishVersion(_rev) == "" { + outdatedRev = append(outdatedRev, _rev) + continue + } + rev = _rev + break + } + if rev == nil { + return errors.Errorf("failed to find previous succeeded revision for application %s/%s", app.Namespace, app.Name) + } + publishVersion := oam.GetPublishVersion(rev) + revisionNumber, err := utils.ExtractRevision(rev.Name) + if err != nil { + return errors.Wrapf(err, "failed to extract revision number from revision %s", rev.Name) + } + _, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, wo.cli, app) + if err != nil { + return errors.Wrapf(err, "failed to list resource trackers for application %s/%s", app.Namespace, app.Name) + } + var matchRT *v1beta1.ResourceTracker + for _, rt := range append(historyRTs, currentRT) { + if rt == nil { + continue + } + labels := rt.GetLabels() + if labels != nil && labels[oam.LabelAppRevision] == rev.Name { + matchRT = rt.DeepCopy() + } + } + if matchRT == nil { + return errors.Errorf("cannot find resource tracker for previous revision %s, unable to rollback", rev.Name) + } + if matchRT.DeletionTimestamp != nil { + return errors.Errorf("previous revision %s is being recycled, unable to rollback", rev.Name) + } + err = wo.writeOutput("Find succeeded application revision %s (PublishVersion: %s) to rollback.\n") + if err != nil { + return err + } + appKey := client.ObjectKeyFromObject(app) + // rollback application spec and freeze + controllerRequirement, err := utils.FreezeApplication(ctx, wo.cli, app, func() { + app.Spec = rev.Spec.Application.Spec + oam.SetPublishVersion(app, publishVersion) + }) + if err != nil { + return errors.Wrapf(err, "failed to rollback application spec to revision %s (PublishVersion: %s)", rev.Name, publishVersion) + } + err = wo.writeOutput("Application spec rollback successfully.\n") + if err != nil { + return err + } + // rollback application status + if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err = wo.cli.Get(ctx, appKey, app); err != nil { + return err + } + app.Status.Workflow = rev.Status.Workflow + app.Status.Services = []common.ApplicationComponentStatus{} + app.Status.AppliedResources = []common.ClusterObjectReference{} + for _, rsc := range matchRT.Spec.ManagedResources { + app.Status.AppliedResources = append(app.Status.AppliedResources, rsc.ClusterObjectReference) + } + app.Status.LatestRevision = &common.Revision{ + Name: rev.Name, + Revision: int64(revisionNumber), + RevisionHash: rev.GetLabels()[oam.LabelAppRevisionHash], + } + return wo.cli.Status().Update(ctx, app) + }); err != nil { + return errors.Wrapf(err, "failed to rollback application status to revision %s (PublishVersion: %s)", rev.Name, publishVersion) + } + + err = wo.writeOutput("Application status rollback successfully.\n") + if err != nil { + return err + } + // update resource tracker generation + matchRTKey := client.ObjectKeyFromObject(matchRT) + if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err = wo.cli.Get(ctx, matchRTKey, matchRT); err != nil { + return err + } + matchRT.Spec.ApplicationGeneration = app.Generation + return wo.cli.Update(ctx, matchRT) + }); err != nil { + return errors.Wrapf(err, "failed to update application generation in resource tracker") + } + + // unfreeze application + if err = utils.UnfreezeApplication(ctx, wo.cli, app, nil, controllerRequirement); err != nil { + return errors.Wrapf(err, "failed to resume application to restart") + } + + rollback, err := rollout.RollbackRollout(ctx, wo.cli, app, wo.outputWriter) + if err != nil { + return err + } + + if rollback { + err = wo.writeOutput("Successfully rollback rollout") + if err != nil { + return err + } + } + + // clean up outdated revisions + var errs errors3.ErrorList + for _, _rev := range outdatedRev { + if err = wo.cli.Delete(ctx, _rev); err != nil { + errs = append(errs, err) + } + } + if errs.HasError() { + return errors.Wrapf(errs, "failed to clean up outdated revisions") + } + + err = wo.writeOutput("Application outdated revision cleaned up.\n") + if err != nil { + return err + } + return nil +} + +// Restart a terminated or finished workflow. +func (wo wfOperator) Restart(ctx context.Context, app *v1beta1.Application) error { + if app.Status.Workflow == nil { + return fmt.Errorf("the workflow in application is not running") + } + // reset the workflow status to restart the workflow + app.Status.Workflow = nil + + if err := wo.cli.Status().Update(context.TODO(), app); err != nil { + return err + } + + return wo.writeOutputF("Successfully restart workflow: %s\n", app.Name) +} + +func (wo wfOperator) Terminate(ctx context.Context, app *v1beta1.Application) error { + if err := service.TerminateWorkflow(context.TODO(), wo.cli, app); err != nil { + return err + } + + return nil +} + +func (wo wfOperator) writeOutput(str string) error { + if wo.outputWriter == nil { + return nil + } + _, err := wo.outputWriter.Write([]byte(str)) + return err +} + +func (wo wfOperator) writeOutputF(format string, a ...interface{}) error { + if wo.outputWriter == nil { + return nil + } + _, err := wo.outputWriter.Write([]byte(fmt.Sprintf(format, a...))) + return err +} diff --git a/pkg/workflow/operation/operation_test.go b/pkg/workflow/operation/operation_test.go new file mode 100644 index 000000000..1311999e3 --- /dev/null +++ b/pkg/workflow/operation/operation_test.go @@ -0,0 +1,215 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package operation + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" +) + +var _ = Describe("Kruise rollout test", func() { + ctx := context.Background() + BeforeEach(func() { + Expect(k8sClient.Create(ctx, myRollout.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) + Expect(k8sClient.Create(ctx, rt.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) + Expect(k8sClient.Create(ctx, app.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) + }) + + It("Suspend workflow", func() { + checkApp := v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + checkApp.Status.Workflow = &common.WorkflowStatus{Suspend: false, StartTime: metav1.Now()} + Expect(k8sClient.Status().Update(ctx, &checkApp)).Should(BeNil()) + operator := NewWorkflowOperator(k8sClient, nil) + checkApp = v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + Expect(operator.Suspend(ctx, checkApp.DeepCopy())).Should(BeNil()) + checkApp = v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + Expect(checkApp.Status.Workflow.Suspend).Should(BeEquivalentTo(true)) + }) + + It("Resume workflow", func() { + checkApp := v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + operator := NewWorkflowOperator(k8sClient, nil) + Expect(operator.Resume(ctx, &checkApp)).Should(BeNil()) + checkApp = v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + Expect(checkApp.Status.Workflow.Suspend).Should(BeEquivalentTo(false)) + }) + + It("Terminate workflow", func() { + checkApp := v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + operator := NewWorkflowOperator(k8sClient, nil) + Expect(operator.Terminate(ctx, &checkApp)).Should(BeNil()) + checkApp = v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + Expect(checkApp.Status.Workflow.Terminated).Should(BeEquivalentTo(true)) + }) + + It("Restart workflow", func() { + checkApp := v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + operator := NewWorkflowOperator(k8sClient, nil) + Expect(operator.Restart(ctx, &checkApp)).Should(BeNil()) + checkApp = v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + Expect(checkApp.Status.Workflow).Should(BeNil()) + }) + + It("Rollback workflow", func() { + Expect(k8sClient.Create(ctx, &appRev)).Should(BeNil()) + checkAppRev := v1beta1.ApplicationRevision{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app-v1"}, &checkAppRev)).Should(BeNil()) + checkAppRev.Status.Succeeded = true + Expect(k8sClient.Status().Update(ctx, checkAppRev.DeepCopy())).Should(BeNil()) + + checkApp := v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + checkApp.Annotations = map[string]string{ + oam.AnnotationPublishVersion: "v2", + } + operator := NewWorkflowOperator(k8sClient, nil) + Expect(operator.Rollback(ctx, checkApp.DeepCopy())).Should(BeNil()) + + checkApp = v1beta1.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil()) + // must rollback to v1 + Expect(oam.GetPublishVersion(&checkApp)).Should(BeEquivalentTo("v1")) + Expect(checkApp.Status.LatestRevision.Name).Should(BeEquivalentTo("opt-app-v1")) + Expect(checkApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1)) + }) +}) + +var app = v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "core.oam.dev/v1beta1", + Kind: "Application", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "opt-app", + Namespace: "default", + Generation: 1, + Labels: map[string]string{ + oam.AnnotationPublishVersion: "v2", + }, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{}, + }, +} + +var rt = v1beta1.ResourceTracker{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "core.oam.dev/v1beta1", + Kind: "ResourceTracker", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rollout-app", + Labels: map[string]string{ + "app.oam.dev/appRevision": "opt-app-v1", + "app.oam.dev/name": "opt-app", + "app.oam.dev/namespace": "default", + }, + }, + Spec: v1beta1.ResourceTrackerSpec{ + ApplicationGeneration: 1, + Type: v1beta1.ResourceTrackerTypeVersioned, + ManagedResources: []v1beta1.ManagedResource{ + { + ClusterObjectReference: common.ClusterObjectReference{ + ObjectReference: v1.ObjectReference{ + APIVersion: "rollouts.kruise.io/v1alpha1", + Kind: "Rollout", + Name: "my-rollout", + Namespace: "default", + }, + }, + OAMObjectReference: common.OAMObjectReference{ + Component: "my-rollout", + }, + }, + }, + }, +} + +var appRev = v1beta1.ApplicationRevision{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "core.oam.dev/v1beta1", + Kind: "ApplicationRevision", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "opt-app-v1", + Namespace: "default", + Labels: map[string]string{ + "app.oam.dev/name": "opt-app", + }, + Annotations: map[string]string{ + oam.AnnotationPublishVersion: "v1", + }, + }, + Spec: v1beta1.ApplicationRevisionSpec{ + Application: v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{}, + }, + }, + }, +} + +var myRollout = kruisev1alpha1.Rollout{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rollouts.kruise.io/v1alpha1", + Kind: "Rollout", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-rollout", + Namespace: "default", + }, + Spec: kruisev1alpha1.RolloutSpec{ + ObjectRef: kruisev1alpha1.ObjectRef{ + WorkloadRef: &kruisev1alpha1.WorkloadRef{ + APIVersion: "appsv1", + Kind: "Deployment", + Name: "canary-demo", + }, + }, + Strategy: kruisev1alpha1.RolloutStrategy{ + Canary: &kruisev1alpha1.CanaryStrategy{ + Steps: []kruisev1alpha1.CanaryStep{ + { + Weight: 30, + }, + }, + }, + Paused: false, + }, + }, +} diff --git a/pkg/workflow/operation/suit_test.go b/pkg/workflow/operation/suit_test.go new file mode 100644 index 000000000..ba4c74110 --- /dev/null +++ b/pkg/workflow/operation/suit_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package operation + +import ( + "context" + "path/filepath" + "testing" + "time" + + "k8s.io/client-go/discovery" + ocmclusterv1 "open-cluster-management.io/api/cluster/v1" + ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1" + ocmworkv1 "open-cluster-management.io/api/work/v1" + + v12 "k8s.io/api/core/v1" + crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1" + + coreoam "github.com/oam-dev/kubevela/apis/core.oam.dev" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + // +kubebuilder:scaffold:imports +) + +var cfg *rest.Config +var scheme *runtime.Scheme +var k8sClient client.Client +var testEnv *envtest.Environment +var dm discoverymapper.DiscoveryMapper +var pd *packages.PackageDiscover +var testns string +var dc *discovery.DiscoveryClient + +func TestAddon(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, + "Kruise rollout Suite test", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func(done Done) { + logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter))) + By("bootstrapping test environment") + useExistCluster := false + testEnv = &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute, + ControlPlaneStopTimeout: time.Minute, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "charts", "vela-core", "crds"), filepath.Join("", "testdata")}, + UseExistingCluster: &useExistCluster, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + scheme = runtime.NewScheme() + Expect(coreoam.AddToScheme(scheme)).NotTo(HaveOccurred()) + Expect(clientgoscheme.AddToScheme(scheme)).NotTo(HaveOccurred()) + Expect(crdv1.AddToScheme(scheme)).NotTo(HaveOccurred()) + _ = ocmclusterv1alpha1.Install(scheme) + _ = ocmclusterv1.Install(scheme) + _ = ocmworkv1.Install(scheme) + _ = kruisev1alpha1.AddToScheme(scheme) + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + + dc, err = discovery.NewDiscoveryClientForConfig(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(dc).ShouldNot(BeNil()) + + dm, err = discoverymapper.New(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(dm).ToNot(BeNil()) + pd, err = packages.NewPackageDiscover(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(pd).ToNot(BeNil()) + testns = "vela-system" + Expect(k8sClient.Create(context.Background(), + &v12.Namespace{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Namespace"}, ObjectMeta: metav1.ObjectMeta{ + Name: testns, + }})) + + close(done) +}, 120) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) diff --git a/pkg/workflow/operation/testdata/rollouts.yaml b/pkg/workflow/operation/testdata/rollouts.yaml new file mode 100644 index 000000000..43ab7546a --- /dev/null +++ b/pkg/workflow/operation/testdata/rollouts.yaml @@ -0,0 +1,290 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: rollouts.rollouts.kruise.io +spec: + group: rollouts.kruise.io + names: + kind: Rollout + listKind: RolloutList + plural: rollouts + singular: rollout + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The rollout status phase + jsonPath: .status.phase + name: STATUS + type: string + - description: The rollout canary status step + jsonPath: .status.canaryStatus.currentStepIndex + name: CANARY_STEP + type: integer + - description: The rollout canary status step state + jsonPath: .status.canaryStatus.currentStepState + name: CANARY_STATE + type: string + - description: The rollout canary status message + jsonPath: .status.message + name: MESSAGE + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Rollout is the Schema for the rollouts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: RolloutSpec defines the desired state of Rollout + properties: + objectRef: + description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + Important: Run "make" to regenerate code after modifying this file + ObjectRef indicates workload' + properties: + workloadRef: + description: WorkloadRef contains enough information to let you + identify a workload for Rollout Batch release of the bypass + properties: + apiVersion: + description: API Version of the referent + type: string + kind: + description: Kind of the referent + type: string + name: + description: Name of the referent + type: string + required: + - apiVersion + - kind + - name + type: object + type: object + strategy: + description: rollout strategy + properties: + canary: + description: CanaryStrategy defines parameters for a Replica Based + Canary + properties: + steps: + description: Steps define the order of phases to execute release + in batches(20%, 40%, 60%, 80%, 100%) + items: + description: CanaryStep defines a step of a canary workload. + properties: + pause: + description: Pause defines a pause stage for a rollout, + manual or auto + properties: + duration: + description: Duration the amount of time to wait + before moving to the next step. + format: int32 + type: integer + type: object + replicas: + anyOf: + - type: integer + - type: string + description: 'Replicas is the number of expected canary + pods in this batch it can be an absolute number (ex: + 5) or a percentage of total pods.' + x-kubernetes-int-or-string: true + weight: + description: SetWeight sets what percentage of the canary + pods should receive + format: int32 + type: integer + type: object + type: array + trafficRoutings: + description: TrafficRoutings hosts all the supported service + meshes supported to enable more fine-grained traffic routing + todo current only support one + items: + description: TrafficRouting hosts all the different configuration + for supported service meshes to enable more fine-grained + traffic routing + properties: + gracePeriodSeconds: + description: Optional duration in seconds the traffic + provider(e.g. nginx ingress controller) consumes the + service, ingress configuration changes gracefully. + format: int32 + type: integer + ingress: + description: Ingress holds Ingress specific configuration + to route traffic, e.g. Nginx, Alb. + properties: + name: + description: Name refers to the name of an `Ingress` + resource in the same namespace as the `Rollout` + type: string + required: + - name + type: object + service: + description: Service holds the name of a service which + selects pods with stable version and don't select + any pods with canary version. + type: string + type: + description: nginx, alb, istio etc. + type: string + required: + - service + - type + type: object + type: array + type: object + paused: + description: Paused indicates that the Rollout is paused. Default + value is false + type: boolean + type: object + required: + - objectRef + - strategy + type: object + status: + description: RolloutStatus defines the observed state of Rollout + properties: + canaryStatus: + description: Canary describes the state of the canary rollout + properties: + canaryReadyReplicas: + description: CanaryReadyReplicas the numbers of ready canary revision + pods + format: int32 + type: integer + canaryReplicas: + description: CanaryReplicas the numbers of canary revision pods + format: int32 + type: integer + canaryRevision: + description: CanaryRevision is calculated by rollout based on + podTemplateHash, and the internal logic flow uses It may be + different from rs podTemplateHash in different k8s versions, + so it cannot be used as service selector label + type: string + canaryService: + description: CanaryService holds the name of a service which selects + pods with canary version and don't select any pods with stable + version. + type: string + currentStepIndex: + description: CurrentStepIndex defines the current step of the + rollout is on. If the current step index is null, the controller + will execute the rollout. + format: int32 + type: integer + currentStepState: + type: string + lastReadyTime: + description: The last time this step pods is ready. + format: date-time + type: string + message: + type: string + observedWorkloadGeneration: + description: observedWorkloadGeneration is the most recent generation + observed for this Rollout ref workload generation. + format: int64 + type: integer + podTemplateHash: + description: pod template hash is used as service selector label + type: string + rolloutHash: + description: RolloutHash from rollout.spec object + type: string + required: + - canaryReadyReplicas + - canaryReplicas + - canaryService + - currentStepState + - podTemplateHash + type: object + conditions: + description: Conditions a list of conditions a rollout can have. + items: + description: RolloutCondition describes the state of a rollout at + a certain point. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Phase of the condition, one of True, False, Unknown. + type: string + type: + description: Type of rollout condition. + type: string + required: + - message + - reason + - status + - type + type: object + type: array + message: + description: Message provides details on why the rollout is in its + current phase + type: string + observedGeneration: + description: observedGeneration is the most recent generation observed + for this Rollout. + format: int64 + type: integer + phase: + description: BlueGreenStatus *BlueGreenStatus `json:"blueGreenStatus,omitempty"` + Phase is the rollout phase. + type: string + stableRevision: + description: CanaryRevision the hash of the canary pod template CanaryRevision + string `json:"canaryRevision,omitempty"` StableRevision indicates + the revision pods that has successfully rolled out + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] \ No newline at end of file diff --git a/references/cli/sample-1.0.1.tgz b/references/cli/sample-1.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..f7cd11ae7fa92aac100efe0be7db91d7d3f22980 GIT binary patch literal 474 zcmV<00VVz)iwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PK}tYuhjo!2LRXildK%+OlJ(MX&n`WiMlti+ynfvLtk8r(xT7 zA5F7DGDcxb8#?@Z5w@&9*4^*KI7COqrwt~_cR0B0{AjK9vaa{7*7~&7i>l7bx@wy0 zM(e81bXnC+m4SYRz5K?=f+^Ggyw>T@y%R9n&y?5(Z(ynv#`yeL<~lD`N8BbGW&i4K zV;L+1f)EQlB@bZN1+O+?AOjnb$fhWSQo?meLvBN%Z2LI+SKoHK&5!z#nS={FU{74Q z(1&ZG2f4m&7c0EQWzJKtJU-6T_5~iJ+j<3`iPD z0i@B39q7JwBMT)1YjjNek>oq_t);_U)=!r0%F*GQONZhj9Q;qjVN5M?5i#LFLMd;@ zGaboG{_CbW@xQ6cV<<#X2)HGxPuO QGXMbp|NXyArvMND0GD;-WB>pF literal 0 HcmV?d00001 diff --git a/references/cli/test-data/addon/sample/Chart.yaml b/references/cli/test-data/addon/sample/Chart.yaml new file mode 100644 index 000000000..e6ef11512 --- /dev/null +++ b/references/cli/test-data/addon/sample/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +appVersion: 1.0.1 +description: This is a test sample addon +home: https://terraform.io/ +icon: https://www.terraform.io/assets/images/logo-text-8c3ba8a6.svg +name: sample +type: library +version: 1.0.1 diff --git a/references/cli/workflow.go b/references/cli/workflow.go index 0b7874b87..86fe62cc6 100644 --- a/references/cli/workflow.go +++ b/references/cli/workflow.go @@ -20,23 +20,16 @@ import ( "context" "fmt" - "github.com/pkg/errors" "github.com/spf13/cobra" k8stypes "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" - "sigs.k8s.io/controller-runtime/pkg/client" - oamcommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/apiserver/domain/service" - "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application" - "github.com/oam-dev/kubevela/pkg/controller/utils" + "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" - "github.com/oam-dev/kubevela/pkg/resourcetracker" "github.com/oam-dev/kubevela/pkg/utils/common" - velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors" cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" + "github.com/oam-dev/kubevela/pkg/workflow/operation" "github.com/oam-dev/kubevela/references/appfile" ) @@ -79,18 +72,19 @@ func NewWorkflowSuspendCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra if err != nil { return err } - if app.Status.Workflow == nil { - return fmt.Errorf("the workflow in application is not running") - } - client, err := c.GetClient() + + config, err := c.GetConfig() if err != nil { return err } - err = suspendWorkflow(client, app) + config.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper) + cli, err := c.GetClient() if err != nil { return err } - return nil + + wo := operation.NewWorkflowOperator(cli, cmd.OutOrStdout()) + return wo.Suspend(context.Background(), app) }, } addNamespaceAndEnvArg(cmd) @@ -116,29 +110,19 @@ func NewWorkflowResumeCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra. if err != nil { return err } - if app.Status.Workflow == nil { - return fmt.Errorf("the workflow in application is not running") + + config, err := c.GetConfig() + if err != nil { + return err } - if app.Status.Workflow.Terminated { - return fmt.Errorf("can not resume a terminated workflow") - } - if !app.Status.Workflow.Suspend { - _, err := ioStream.Out.Write([]byte("the workflow is not suspending\n")) - if err != nil { - return err - } - return nil - } - client, err := c.GetClient() + config.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper) + cli, err := c.GetClient() if err != nil { return err } - err = resumeWorkflow(client, app) - if err != nil { - return err - } - return nil + wo := operation.NewWorkflowOperator(cli, cmd.OutOrStdout()) + return wo.Resume(context.Background(), app) }, } addNamespaceAndEnvArg(cmd) @@ -167,15 +151,12 @@ func NewWorkflowTerminateCommand(c common.Args, ioStream cmdutil.IOStreams) *cob if app.Status.Workflow == nil { return fmt.Errorf("the workflow in application is not running") } - client, err := c.GetClient() + cli, err := c.GetClient() if err != nil { return err } - err = terminateWorkflow(client, app) - if err != nil { - return err - } - return nil + wo := operation.NewWorkflowOperator(cli, cmd.OutOrStdout()) + return wo.Terminate(context.Background(), app) }, } addNamespaceAndEnvArg(cmd) @@ -201,19 +182,18 @@ func NewWorkflowRestartCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra if err != nil { return err } - if app.Status.Workflow == nil { - return fmt.Errorf("the workflow in application is not running") + config, err := c.GetConfig() + if err != nil { + return err } - client, err := c.GetClient() + config.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper) + cli, err := c.GetClient() if err != nil { return err } - err = restartWorkflow(client, app) - if err != nil { - return err - } - return nil + wo := operation.NewWorkflowOperator(cli, cmd.OutOrStdout()) + return wo.Restart(context.Background(), app) }, } addNamespaceAndEnvArg(cmd) @@ -239,205 +219,40 @@ func NewWorkflowRollbackCommand(c common.Args, ioStream cmdutil.IOStreams) *cobr if err != nil { return err } + config, err := c.GetConfig() + if err != nil { + return err + } + config.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper) + cli, err := c.GetClient() + if err != nil { + return err + } if app.Status.Workflow != nil && !app.Status.Workflow.Terminated && !app.Status.Workflow.Suspend && !app.Status.Workflow.Finished { return fmt.Errorf("can not rollback a running workflow") } - client, err := c.GetClient() - if err != nil { - return err - } + if oam.GetPublishVersion(app) == "" { + if app.Status.LatestRevision == nil || app.Status.LatestRevision.Name == "" { + return fmt.Errorf("the latest revision is not set: %s", app.Name) + } + // get the last revision + revision := &v1beta1.ApplicationRevision{} + if err := cli.Get(context.TODO(), k8stypes.NamespacedName{Name: app.Status.LatestRevision.Name, Namespace: app.Namespace}, revision); err != nil { + return fmt.Errorf("failed to get the latest revision: %w", err) + } - err = rollbackWorkflow(cmd, client, app) - if err != nil { - return err + app.Spec = revision.Spec.Application.Spec + if err := cli.Status().Update(context.TODO(), app); err != nil { + return err + } + + fmt.Printf("Successfully rollback workflow to the latest revision: %s\n", app.Name) + return nil } - return nil + wo := operation.NewWorkflowOperator(cli, cmd.OutOrStdout()) + return wo.Rollback(context.Background(), app) }, } addNamespaceAndEnvArg(cmd) return cmd } - -func suspendWorkflow(kubecli client.Client, app *v1beta1.Application) error { - appKey := client.ObjectKeyFromObject(app) - ctx := context.Background() - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - if err := kubecli.Get(ctx, appKey, app); err != nil { - return err - } - // set the workflow suspend to true - app.Status.Workflow.Suspend = true - return kubecli.Status().Patch(ctx, app, client.Merge) - }); err != nil { - return err - } - fmt.Printf("Successfully suspend workflow: %s\n", app.Name) - return nil -} - -func resumeWorkflow(kubecli client.Client, app *v1beta1.Application) error { - if err := service.ResumeWorkflow(context.TODO(), kubecli, app); err != nil { - return err - } - - fmt.Printf("Successfully resume workflow: %s\n", app.Name) - return nil -} - -func terminateWorkflow(kubecli client.Client, app *v1beta1.Application) error { - if err := service.TerminateWorkflow(context.TODO(), kubecli, app); err != nil { - return err - } - - fmt.Printf("Successfully terminate workflow: %s\n", app.Name) - return nil -} - -func restartWorkflow(kubecli client.Client, app *v1beta1.Application) error { - // reset the workflow status to restart the workflow - app.Status.Workflow = nil - - if err := kubecli.Status().Update(context.TODO(), app); err != nil { - return err - } - - fmt.Printf("Successfully restart workflow: %s\n", app.Name) - return nil -} - -func rollbackWorkflow(cmd *cobra.Command, kubecli client.Client, app *v1beta1.Application) error { - if oam.GetPublishVersion(app) != "" { - return rollbackApplicationWithPublishVersion(cmd, kubecli, app) - } - if app.Status.LatestRevision == nil || app.Status.LatestRevision.Name == "" { - return fmt.Errorf("the latest revision is not set: %s", app.Name) - } - // get the last revision - revision := &v1beta1.ApplicationRevision{} - if err := kubecli.Get(context.TODO(), k8stypes.NamespacedName{Name: app.Status.LatestRevision.Name, Namespace: app.Namespace}, revision); err != nil { - return fmt.Errorf("failed to get the latest revision: %w", err) - } - - app.Spec = revision.Spec.Application.Spec - if err := kubecli.Status().Update(context.TODO(), app); err != nil { - return err - } - - fmt.Printf("Successfully rollback workflow to the latest revision: %s\n", app.Name) - return nil -} - -func rollbackApplicationWithPublishVersion(cmd *cobra.Command, cli client.Client, app *v1beta1.Application) error { - ctx := context.Background() - appRevs, err := application.GetSortedAppRevisions(ctx, cli, app.Name, app.Namespace) - if err != nil { - return errors.Wrapf(err, "failed to list revisions for application %s/%s", app.Namespace, app.Name) - } - - // find succeeded revision to rollback - var rev *v1beta1.ApplicationRevision - var outdatedRev []*v1beta1.ApplicationRevision - for i := range appRevs { - candidate := appRevs[len(appRevs)-i-1] - _rev := candidate.DeepCopy() - if !candidate.Status.Succeeded || oam.GetPublishVersion(_rev) == "" { - outdatedRev = append(outdatedRev, _rev) - continue - } - rev = _rev - break - } - if rev == nil { - return errors.Errorf("failed to find previous succeeded revision for application %s/%s", app.Namespace, app.Name) - } - publishVersion := oam.GetPublishVersion(rev) - revisionNumber, err := utils.ExtractRevision(rev.Name) - if err != nil { - return errors.Wrapf(err, "failed to extract revision number from revision %s", rev.Name) - } - _, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, cli, app) - if err != nil { - return errors.Wrapf(err, "failed to list resource trackers for application %s/%s", app.Namespace, app.Name) - } - var matchRT *v1beta1.ResourceTracker - for _, rt := range append(historyRTs, currentRT) { - if rt == nil { - continue - } - labels := rt.GetLabels() - if labels != nil && labels[oam.LabelAppRevision] == rev.Name { - matchRT = rt.DeepCopy() - } - } - if matchRT == nil { - return errors.Errorf("cannot find resource tracker for previous revision %s, unable to rollback", rev.Name) - } - if matchRT.DeletionTimestamp != nil { - return errors.Errorf("previous revision %s is being recycled, unable to rollback", rev.Name) - } - cmd.Printf("Find succeeded application revision %s (PublishVersion: %s) to rollback.\n", rev.Name, publishVersion) - - appKey := client.ObjectKeyFromObject(app) - // rollback application spec and freeze - controllerRequirement, err := utils.FreezeApplication(ctx, cli, app, func() { - app.Spec = rev.Spec.Application.Spec - oam.SetPublishVersion(app, publishVersion) - }) - if err != nil { - return errors.Wrapf(err, "failed to rollback application spec to revision %s (PublishVersion: %s)", rev.Name, publishVersion) - } - cmd.Printf("Application spec rollback successfully.\n") - - // rollback application status - if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { - if err = cli.Get(ctx, appKey, app); err != nil { - return err - } - app.Status.Workflow = rev.Status.Workflow - app.Status.Services = []oamcommon.ApplicationComponentStatus{} - app.Status.AppliedResources = []oamcommon.ClusterObjectReference{} - for _, rsc := range matchRT.Spec.ManagedResources { - app.Status.AppliedResources = append(app.Status.AppliedResources, rsc.ClusterObjectReference) - } - app.Status.LatestRevision = &oamcommon.Revision{ - Name: rev.Name, - Revision: int64(revisionNumber), - RevisionHash: rev.GetLabels()[oam.LabelAppRevisionHash], - } - return cli.Status().Update(ctx, app) - }); err != nil { - return errors.Wrapf(err, "failed to rollback application status to revision %s (PublishVersion: %s)", rev.Name, publishVersion) - } - cmd.Printf("Application status rollback successfully.\n") - - // update resource tracker generation - matchRTKey := client.ObjectKeyFromObject(matchRT) - if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { - if err = cli.Get(ctx, matchRTKey, matchRT); err != nil { - return err - } - matchRT.Spec.ApplicationGeneration = app.Generation - return cli.Update(ctx, matchRT) - }); err != nil { - return errors.Wrapf(err, "failed to update application generation in resource tracker") - } - - // unfreeze application - if err = utils.UnfreezeApplication(ctx, cli, app, nil, controllerRequirement); err != nil { - return errors.Wrapf(err, "failed to resume application to restart") - } - cmd.Printf("Application rollback completed.\n") - - // clean up outdated revisions - var errs velaerrors.ErrorList - for _, _rev := range outdatedRev { - if err = cli.Delete(ctx, _rev); err != nil { - errs = append(errs, err) - } - } - if errs.HasError() { - return errors.Wrapf(errs, "failed to clean up outdated revisions") - } - cmd.Printf("Application outdated revision cleaned up.\n") - return nil -} diff --git a/references/cli/workflow_test.go b/references/cli/workflow_test.go index 147564f4a..0446ccc94 100644 --- a/references/cli/workflow_test.go +++ b/references/cli/workflow_test.go @@ -225,6 +225,8 @@ func TestWorkflowResume(t *testing.T) { r := require.New(t) cmd := NewWorkflowResumeCommand(c, ioStream) initCommand(cmd) + // clean up the arguments before start + cmd.SetArgs([]string{}) client, err := c.GetClient() r.NoError(err) if tc.app != nil { @@ -352,6 +354,8 @@ func TestWorkflowTerminate(t *testing.T) { r := require.New(t) cmd := NewWorkflowTerminateCommand(c, ioStream) initCommand(cmd) + // clean up the arguments before start + cmd.SetArgs([]string{}) client, err := c.GetClient() r.NoError(err) if tc.app != nil { @@ -444,6 +448,8 @@ func TestWorkflowRestart(t *testing.T) { r := require.New(t) cmd := NewWorkflowRestartCommand(c, ioStream) initCommand(cmd) + // clean up the arguments before start + cmd.SetArgs([]string{}) client, err := c.GetClient() r.NoError(err) if tc.app != nil { @@ -567,6 +573,8 @@ func TestWorkflowRollback(t *testing.T) { r := require.New(t) cmd := NewWorkflowRollbackCommand(c, ioStream) initCommand(cmd) + // clean up the arguments before start + cmd.SetArgs([]string{}) client, err := c.GetClient() r.NoError(err) if tc.app != nil {