rewrite batchRelease controller (#90)
Signed-off-by: mingzhou.swx <mingzhou.swx@alibaba-inc.com> Signed-off-by: mingzhou.swx <mingzhou.swx@alibaba-inc.com> Co-authored-by: mingzhou.swx <mingzhou.swx@alibaba-inc.com>
This commit is contained in:
parent
0c54037c60
commit
c0b1fea7f8
|
|
@ -21,9 +21,6 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
// ReleaseStrategyType defines strategies for pods rollout
|
||||
type ReleaseStrategyType string
|
||||
|
||||
// ReleasePlan fines the details of the release plan
|
||||
type ReleasePlan struct {
|
||||
// Batches is the details on each batch of the ReleasePlan.
|
||||
|
|
@ -37,7 +34,7 @@ type ReleasePlan struct {
|
|||
Batches []ReleaseBatch `json:"batches"`
|
||||
// All pods in the batches up to the batchPartition (included) will have
|
||||
// the target resource specification while the rest still is the stable revision.
|
||||
// This is designed for the operators to manually rollout
|
||||
// This is designed for the operators to manually rollout.
|
||||
// Default is nil, which means no partition and will release all batches.
|
||||
// BatchPartition start from 0.
|
||||
// +optional
|
||||
|
|
@ -50,8 +47,22 @@ type ReleasePlan struct {
|
|||
// FailureThreshold.
|
||||
// Defaults to nil.
|
||||
FailureThreshold *intstr.IntOrString `json:"failureThreshold,omitempty"`
|
||||
// FinalizingPolicy define the behavior of controller when phase enter Finalizing
|
||||
// Defaults to "Immediate"
|
||||
FinalizingPolicy FinalizingPolicyType `json:"finalizingPolicy,omitempty"`
|
||||
}
|
||||
|
||||
type FinalizingPolicyType string
|
||||
|
||||
const (
|
||||
// WaitResumeFinalizingPolicyType will wait workload to be resumed, which means
|
||||
// controller will be hold at Finalizing phase util all pods of workload is upgraded.
|
||||
// WaitResumeFinalizingPolicyType only works in canary-style BatchRelease controller.
|
||||
WaitResumeFinalizingPolicyType FinalizingPolicyType = "WaitResume"
|
||||
// ImmediateFinalizingPolicyType will not to wait workload to be resumed.
|
||||
ImmediateFinalizingPolicyType FinalizingPolicyType = "Immediate"
|
||||
)
|
||||
|
||||
// ReleaseBatch is used to describe how each batch release should be
|
||||
type ReleaseBatch struct {
|
||||
// CanaryReplicas is the number of upgraded pods that should have in this batch.
|
||||
|
|
@ -123,38 +134,10 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
// RolloutPhaseCancelled indicates a rollout is cancelled
|
||||
RolloutPhaseCancelled RolloutPhase = "Cancelled"
|
||||
// RolloutPhaseFinalizing indicates a rollout is finalizing
|
||||
RolloutPhaseFinalizing RolloutPhase = "Finalizing"
|
||||
// RolloutPhaseCompleted indicates a rollout is completed
|
||||
RolloutPhaseCompleted RolloutPhase = "Completed"
|
||||
// RolloutPhasePreparing indicates a rollout is preparing for next progress.
|
||||
RolloutPhasePreparing RolloutPhase = "Preparing"
|
||||
)
|
||||
|
||||
const (
|
||||
// VerifyingBatchReleaseCondition indicates the controller is verifying whether workload
|
||||
// is ready to do rollout.
|
||||
VerifyingBatchReleaseCondition RolloutConditionType = "Verifying"
|
||||
// PreparingBatchReleaseCondition indicates the controller is preparing something before executing
|
||||
// release plan, such as create canary deployment and record stable & canary revisions.
|
||||
PreparingBatchReleaseCondition RolloutConditionType = "Preparing"
|
||||
// ProgressingBatchReleaseCondition indicates the controller is executing release plan.
|
||||
ProgressingBatchReleaseCondition RolloutConditionType = "Progressing"
|
||||
// FinalizingBatchReleaseCondition indicates the canary state is completed,
|
||||
// and the controller is doing something, such as cleaning up canary deployment.
|
||||
FinalizingBatchReleaseCondition RolloutConditionType = "Finalizing"
|
||||
// TerminatingBatchReleaseCondition indicates the rollout is terminating when the
|
||||
// BatchRelease cr is being deleted or cancelled.
|
||||
TerminatingBatchReleaseCondition RolloutConditionType = "Terminating"
|
||||
// TerminatedBatchReleaseCondition indicates the BatchRelease cr can be deleted.
|
||||
TerminatedBatchReleaseCondition RolloutConditionType = "Terminated"
|
||||
// CancelledBatchReleaseCondition indicates the release plan is cancelled during rollout.
|
||||
CancelledBatchReleaseCondition RolloutConditionType = "Cancelled"
|
||||
// CompletedBatchReleaseCondition indicates the release plan is completed successfully.
|
||||
CompletedBatchReleaseCondition RolloutConditionType = "Completed"
|
||||
|
||||
SucceededBatchReleaseConditionReason = "Succeeded"
|
||||
FailedBatchReleaseConditionReason = "Failed"
|
||||
// RolloutPhaseFinalizing indicates a rollout is finalizing
|
||||
RolloutPhaseFinalizing RolloutPhase = "Finalizing"
|
||||
// RolloutPhaseCompleted indicates a rollout is completed/cancelled/terminated
|
||||
RolloutPhaseCompleted RolloutPhase = "Completed"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -44,14 +44,8 @@ type BatchReleaseSpec struct {
|
|||
TargetRef ObjectRef `json:"targetReference"`
|
||||
// ReleasePlan is the details on how to rollout the resources
|
||||
ReleasePlan ReleasePlan `json:"releasePlan"`
|
||||
// Paused the rollout, the release progress will be paused util paused is false.
|
||||
// default is false
|
||||
// +optional
|
||||
Paused bool `json:"paused,omitempty"`
|
||||
}
|
||||
|
||||
type DeploymentReleaseStrategyType string
|
||||
|
||||
// BatchReleaseList contains a list of BatchRelease
|
||||
// +kubebuilder:object:root=true
|
||||
type BatchReleaseList struct {
|
||||
|
|
|
|||
|
|
@ -52,10 +52,6 @@ spec:
|
|||
description: BatchReleaseSpec defines how to describe an update between
|
||||
different compRevision
|
||||
properties:
|
||||
paused:
|
||||
description: Paused the rollout, the release progress will be paused
|
||||
util paused is false. default is false
|
||||
type: boolean
|
||||
releasePlan:
|
||||
description: ReleasePlan is the details on how to rollout the resources
|
||||
properties:
|
||||
|
|
@ -63,7 +59,7 @@ spec:
|
|||
description: All pods in the batches up to the batchPartition
|
||||
(included) will have the target resource specification while
|
||||
the rest still is the stable revision. This is designed for
|
||||
the operators to manually rollout Default is nil, which means
|
||||
the operators to manually rollout. Default is nil, which means
|
||||
no partition and will release all batches. BatchPartition start
|
||||
from 0.
|
||||
format: int32
|
||||
|
|
@ -102,6 +98,10 @@ spec:
|
|||
is nil, Rollout will use the MaxUnavailable of workload as its
|
||||
FailureThreshold. Defaults to nil.
|
||||
x-kubernetes-int-or-string: true
|
||||
finalizingPolicy:
|
||||
description: FinalizingPolicy define the behavior of controller
|
||||
when phase enter Finalizing Defaults to "Immediate"
|
||||
type: string
|
||||
rolloutID:
|
||||
description: RolloutID indicates an id for each rollout progress
|
||||
type: string
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ spec:
|
|||
spec:
|
||||
containers:
|
||||
- name: manager
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- "--config=controller_manager_config.yaml"
|
||||
volumeMounts:
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -7,7 +7,7 @@ require (
|
|||
github.com/evanphx/json-patch v4.11.0+incompatible
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/onsi/gomega v1.17.0
|
||||
github.com/openkruise/kruise-api v1.0.0
|
||||
github.com/openkruise/kruise-api v1.3.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
|
|
|||
18
go.sum
18
go.sum
|
|
@ -43,17 +43,14 @@ github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX
|
|||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
|
||||
github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
|
||||
github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM=
|
||||
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
|
|
@ -127,7 +124,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
|
|
@ -155,7 +151,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
|||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
|
|
@ -402,8 +397,8 @@ github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+t
|
|||
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
|
||||
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/openkruise/kruise-api v1.0.0 h1:ScA0LxRRNBsgbcyLhTzR9B+KpGNWsIMptzzmjTqfYQo=
|
||||
github.com/openkruise/kruise-api v1.0.0/go.mod h1:kxV/UA/vrf/hz3z+kL21c0NOawC6K1ZjaKcJFgiOwsE=
|
||||
github.com/openkruise/kruise-api v1.3.0 h1:yfEy64uXgSuX/5RwePLbwUK/uX8RRM8fHJkccel5ZIQ=
|
||||
github.com/openkruise/kruise-api v1.3.0/go.mod h1:9ZX+ycdHKNzcA5ezAf35xOa2Mwfa2BYagWr0lKgi5dU=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
|
|
@ -718,7 +713,6 @@ golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -756,7 +750,6 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
|
|||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
|
@ -976,7 +969,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.20.10/go.mod h1:0kei3F6biGjtRQBo5dUeujq6Ji3UCh9aOSfp/THYd7I=
|
||||
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=
|
||||
|
|
@ -986,7 +978,6 @@ k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8L
|
|||
k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA=
|
||||
k8s.io/apiextensions-apiserver v0.22.6 h1:TH+9+EGtoVzzbrlfSDnObzFTnyXKqw1NBfT5XFATeJI=
|
||||
k8s.io/apiextensions-apiserver v0.22.6/go.mod h1:wNsLwy8mfIkGThiv4Qq/Hy4qRazViKXqmH5pfYiRKyY=
|
||||
k8s.io/apimachinery v0.20.10/go.mod h1:kQa//VOAwyVwJ2+L9kOREbsnryfsGSkSM1przND4+mw=
|
||||
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=
|
||||
|
|
@ -995,13 +986,11 @@ k8s.io/apimachinery v0.22.6/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1E
|
|||
k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU=
|
||||
k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI=
|
||||
k8s.io/apiserver v0.22.6/go.mod h1:OlL1rGa2kKWGj2JEXnwBcul/BwC9Twe95gm4ohtiIIs=
|
||||
k8s.io/client-go v0.20.10/go.mod h1:fFg+aLoasv/R+xiVaWjxeqGFYltzgQcOQzkFaSRfnJ0=
|
||||
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.6 h1:ugAXeC312xeGXsn7zTRz+btgtLBnW3qYhtUUpVQL7YE=
|
||||
k8s.io/client-go v0.22.6/go.mod h1:TffU4AV2idZGeP+g3kdFZP+oHVHWPL1JYFySOALriw0=
|
||||
k8s.io/code-generator v0.20.10/go.mod h1:i6FmG+QxaLxvJsezvZp0q/gAEzzOz3U53KFibghWToU=
|
||||
k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo=
|
||||
k8s.io/code-generator v0.22.0/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o=
|
||||
k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o=
|
||||
|
|
@ -1011,19 +1000,16 @@ k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllun
|
|||
k8s.io/component-base v0.22.6 h1:YgGMDVnr97rhn0eljuYIU/9XFyz8JVDM30slMYrDgPc=
|
||||
k8s.io/component-base v0.22.6/go.mod h1:ngHLefY4J5fq2fApNdbWyj4yh0lvw36do4aAjNN8rc8=
|
||||
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
|
||||
k8s.io/gengo v0.0.0-20201203183100-97869a43a9d9/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
|
||||
k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
|
||||
k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
|
||||
k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
|
||||
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
|
||||
k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
|
||||
k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
|
||||
k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
|
||||
k8s.io/klog/v2 v2.10.0 h1:R2HDMDJsHVTHA2n4RjwbeYXdOcBymXdX/JRb1v0VGhE=
|
||||
k8s.io/klog/v2 v2.10.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
|
||||
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
|
||||
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 h1:jvamsI1tn9V0S8jicyX82qaFC0H/NKxv2e5mbqsgR80=
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/klog/v2"
|
||||
|
|
@ -186,11 +187,13 @@ func (r *BatchReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request
|
|||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
errList := field.ErrorList{}
|
||||
|
||||
// executor start to execute the batch release plan.
|
||||
startTimestamp := time.Now()
|
||||
result, currentStatus, err := r.executor.Do(release)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
errList = append(errList, field.InternalError(field.NewPath("do-release"), err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
|
@ -202,9 +205,14 @@ func (r *BatchReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request
|
|||
"reconcile-result ", result, "time-cost", time.Since(startTimestamp))
|
||||
}()
|
||||
|
||||
return result, r.updateStatus(release, currentStatus)
|
||||
err = r.updateStatus(release, currentStatus)
|
||||
if err != nil {
|
||||
errList = append(errList, field.InternalError(field.NewPath("update-status"), err))
|
||||
}
|
||||
return result, errList.ToAggregate()
|
||||
}
|
||||
|
||||
// updateStatus update BatchRelease status to newStatus
|
||||
func (r *BatchReleaseReconciler) updateStatus(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus) error {
|
||||
var err error
|
||||
defer func() {
|
||||
|
|
@ -235,6 +243,7 @@ func (r *BatchReleaseReconciler) updateStatus(release *v1alpha1.BatchRelease, ne
|
|||
return err
|
||||
}
|
||||
|
||||
// handleFinalizer will remove finalizer in finalized phase and add finalizer in the other phases.
|
||||
func (r *BatchReleaseReconciler) handleFinalizer(release *v1alpha1.BatchRelease) (bool, error) {
|
||||
var err error
|
||||
defer func() {
|
||||
|
|
@ -245,7 +254,7 @@ func (r *BatchReleaseReconciler) handleFinalizer(release *v1alpha1.BatchRelease)
|
|||
|
||||
// remove the release finalizer if it needs
|
||||
if !release.DeletionTimestamp.IsZero() &&
|
||||
HasTerminatingCondition(release.Status) &&
|
||||
release.Status.Phase == v1alpha1.RolloutPhaseCompleted &&
|
||||
controllerutil.ContainsFinalizer(release, ReleaseFinalizer) {
|
||||
err = util.UpdateFinalizer(r.Client, release, util.RemoveFinalizerOpType, ReleaseFinalizer)
|
||||
if client.IgnoreNotFound(err) != nil {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ var (
|
|||
},
|
||||
},
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
BatchPartition: pointer.Int32(0),
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("10%"),
|
||||
|
|
@ -147,6 +148,7 @@ var (
|
|||
},
|
||||
},
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
BatchPartition: pointer.Int32Ptr(0),
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("10%"),
|
||||
|
|
@ -233,47 +235,6 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
ExpectedPhase v1alpha1.RolloutPhase
|
||||
ExpectedState v1alpha1.BatchReleaseBatchStateType
|
||||
}{
|
||||
// Following cases of Linear Transaction on State Machine
|
||||
{
|
||||
Name: "IfNeedProgress=false, Input-Phase=Initial, Output-Phase=Healthy",
|
||||
GetRelease: func() client.Object {
|
||||
return setPhase(releaseClone, v1alpha1.RolloutPhaseInitial)
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
clone.Annotations = nil
|
||||
return []client.Object{
|
||||
clone,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseHealthy,
|
||||
},
|
||||
{
|
||||
Name: "IfNeedProgress=false, Input-Phase=Healthy, Output-Phase=Healthy",
|
||||
GetRelease: func() client.Object {
|
||||
return setPhase(releaseClone, v1alpha1.RolloutPhaseHealthy)
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
return []client.Object{
|
||||
stableClone.DeepCopy(),
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseHealthy,
|
||||
},
|
||||
{
|
||||
Name: "IfNeedProgress=true, Input-Phase=Healthy, Output-Phase=Preparing",
|
||||
GetRelease: func() client.Object {
|
||||
return setPhase(releaseClone, v1alpha1.RolloutPhaseHealthy)
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
stable := getStableWithReady(stableClone, "v2")
|
||||
canary := getCanaryWithStage(stable, "v2", -1, true)
|
||||
return []client.Object{
|
||||
canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhasePreparing,
|
||||
},
|
||||
{
|
||||
Name: "Preparing, Input-Phase=Preparing, Output-Phase=Progressing",
|
||||
GetRelease: func() client.Object {
|
||||
|
|
@ -349,6 +310,8 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canaryTemplate.Spec.Containers = containers("v2")
|
||||
release.Status.StableRevision = util.ComputeHash(stableTemplate, nil)
|
||||
release.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
return release
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
|
|
@ -372,6 +335,9 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canaryTemplate.Spec.Containers = containers("v2")
|
||||
release.Status.StableRevision = util.ComputeHash(stableTemplate, nil)
|
||||
release.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
release.Spec.ReleasePlan.BatchPartition = pointer.Int32(1)
|
||||
return release
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
|
|
@ -397,6 +363,8 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canaryTemplate.Spec.Containers = containers("v2")
|
||||
release.Status.StableRevision = util.ComputeHash(stableTemplate, nil)
|
||||
release.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
return release
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
|
|
@ -421,6 +389,8 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canaryTemplate.Spec.Containers = containers("v2")
|
||||
release.Status.StableRevision = util.ComputeHash(stableTemplate, nil)
|
||||
release.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
return release
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
|
|
@ -435,7 +405,7 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
ExpectedState: v1alpha1.UpgradingBatchState,
|
||||
},
|
||||
{
|
||||
Name: `Special Case: RollBack, Input-Phase=Progressing, Output-Phase=Abort`,
|
||||
Name: `Special Case: RollBack, Input-Phase=Progressing, Output-Phase=Progressing`,
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseClone, v1alpha1.ReadyBatchState)
|
||||
now := metav1.Now()
|
||||
|
|
@ -446,6 +416,8 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canaryTemplate.Spec.Containers = containers("v2")
|
||||
release.Status.StableRevision = util.ComputeHash(stableTemplate, nil)
|
||||
release.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
return release
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
|
|
@ -459,11 +431,11 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseFinalizing,
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseProgressing,
|
||||
ExpectedState: v1alpha1.ReadyBatchState,
|
||||
},
|
||||
{
|
||||
Name: `Special Case: Deletion, Input-Phase=Progressing, Output-Phase=Terminating`,
|
||||
Name: `Special Case: Deletion, Input-Phase=Progressing, Output-Phase=Finalizing`,
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseClone, v1alpha1.ReadyBatchState)
|
||||
now := metav1.Now()
|
||||
|
|
@ -476,6 +448,8 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
release.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
release.DeletionTimestamp = &metav1.Time{Time: time.Now()}
|
||||
release.Finalizers = append(release.Finalizers, ReleaseFinalizer)
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
return release
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
|
|
@ -485,11 +459,11 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseTerminating,
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseFinalizing,
|
||||
ExpectedState: v1alpha1.ReadyBatchState,
|
||||
},
|
||||
{
|
||||
Name: `Special Case: Continuous Release, Input-Phase=Progressing, Output-Phase=Initial`,
|
||||
Name: `Special Case: Continuous Release, Input-Phase=Progressing, Output-Phase=Progressing`,
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseClone, v1alpha1.ReadyBatchState)
|
||||
now := metav1.Now()
|
||||
|
|
@ -500,6 +474,10 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canaryTemplate.Spec.Containers = containers("v2")
|
||||
release.Status.StableRevision = util.ComputeHash(stableTemplate, nil)
|
||||
release.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
release.Spec.ReleasePlan.BatchPartition = pointer.Int32Ptr(1)
|
||||
release.Status.ObservedReleasePlanHash = util.HashReleasePlanBatches(&release.Spec.ReleasePlan)
|
||||
return release
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
|
|
@ -515,13 +493,41 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseInitial,
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseProgressing,
|
||||
ExpectedState: v1alpha1.ReadyBatchState,
|
||||
},
|
||||
{
|
||||
Name: `Special Case: BatchPartition=nil, Input-Phase=Progressing, Output-Phase=Finalizing`,
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseClone, v1alpha1.ReadyBatchState)
|
||||
now := metav1.Now()
|
||||
release.Status.CanaryStatus.BatchReadyTime = &now
|
||||
stableTemplate := stableClone.Spec.Template.DeepCopy()
|
||||
canaryTemplate := stableClone.Spec.Template.DeepCopy()
|
||||
stableTemplate.Spec.Containers = containers("v1")
|
||||
canaryTemplate.Spec.Containers = containers("v2")
|
||||
release.Status.StableRevision = util.ComputeHash(stableTemplate, nil)
|
||||
release.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
release.Finalizers = append(release.Finalizers, ReleaseFinalizer)
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
release.Spec.ReleasePlan.BatchPartition = nil
|
||||
return release
|
||||
},
|
||||
GetCloneSet: func() []client.Object {
|
||||
stable := getStableWithReady(stableClone, "v2")
|
||||
canary := getCanaryWithStage(stable, "v2", 0, true)
|
||||
return []client.Object{
|
||||
canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseFinalizing,
|
||||
ExpectedState: v1alpha1.ReadyBatchState,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
defer GinkgoRecover()
|
||||
release := cs.GetRelease()
|
||||
clonesets := cs.GetCloneSet()
|
||||
rec := record.NewFakeRecorder(100)
|
||||
|
|
@ -530,6 +536,7 @@ func TestReconcile_CloneSet(t *testing.T) {
|
|||
Client: cli,
|
||||
recorder: rec,
|
||||
Scheme: scheme,
|
||||
executor: NewReleasePlanExecutor(cli, rec),
|
||||
}
|
||||
|
||||
key := client.ObjectKeyFromObject(release)
|
||||
|
|
@ -561,31 +568,7 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
}{
|
||||
// Following cases of Linear Transaction on State Machine
|
||||
{
|
||||
Name: "IfNeedProgress=false, Input-Phase=Initial, Output-Phase=Healthy",
|
||||
GetRelease: func() client.Object {
|
||||
return setPhase(releaseDeploy, v1alpha1.RolloutPhaseInitial)
|
||||
},
|
||||
GetDeployments: func() []client.Object {
|
||||
return []client.Object{
|
||||
stableDeploy.DeepCopy(),
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseHealthy,
|
||||
},
|
||||
{
|
||||
Name: "IfNeedProgress=false, Input-Phase=Healthy, Output-Phase=Healthy",
|
||||
GetRelease: func() client.Object {
|
||||
return setPhase(releaseDeploy, v1alpha1.RolloutPhaseHealthy)
|
||||
},
|
||||
GetDeployments: func() []client.Object {
|
||||
return []client.Object{
|
||||
stableDeploy.DeepCopy(),
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseHealthy,
|
||||
},
|
||||
{
|
||||
Name: "IfNeedProgress=true, Input-Phase=Healthy, Output-Phase=Preparing",
|
||||
Name: "IfNeedProgress=true, Input-Phase=Healthy, Output-Phase=Progressing",
|
||||
GetRelease: func() client.Object {
|
||||
return setPhase(releaseDeploy, v1alpha1.RolloutPhaseHealthy)
|
||||
},
|
||||
|
|
@ -596,7 +579,7 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
stable, canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhasePreparing,
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseProgressing,
|
||||
},
|
||||
{
|
||||
Name: "Preparing, Input-Phase=Preparing, Output-Phase=Progressing",
|
||||
|
|
@ -628,24 +611,30 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
ExpectedState: v1alpha1.VerifyingBatchState,
|
||||
},
|
||||
{
|
||||
Name: "Progressing, stage=0, Input-State=Upgrade, Output-State=Verify",
|
||||
Name: "Progressing, stage=0, Input-State=Verify, Output-State=Upgrade",
|
||||
GetRelease: func() client.Object {
|
||||
return setState(releaseDeploy, v1alpha1.UpgradingBatchState)
|
||||
release := releaseDeploy.DeepCopy()
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 5
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 5
|
||||
return setState(release, v1alpha1.VerifyingBatchState)
|
||||
},
|
||||
GetDeployments: func() []client.Object {
|
||||
stable := getStableWithReady(stableDeploy, "v2")
|
||||
canary := getCanaryWithStage(stable, "v2", -1, true)
|
||||
canary := getCanaryWithStage(stable, "v2", 0, false)
|
||||
return []client.Object{
|
||||
stable, canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseProgressing,
|
||||
ExpectedState: v1alpha1.VerifyingBatchState,
|
||||
ExpectedState: v1alpha1.UpgradingBatchState,
|
||||
},
|
||||
{
|
||||
Name: "Progressing, stage=0, Input-State=Verify, Output-State=BatchReady",
|
||||
GetRelease: func() client.Object {
|
||||
return setState(releaseDeploy, v1alpha1.VerifyingBatchState)
|
||||
release := releaseDeploy.DeepCopy()
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
return setState(release, v1alpha1.VerifyingBatchState)
|
||||
},
|
||||
GetDeployments: func() []client.Object {
|
||||
stable := getStableWithReady(stableDeploy, "v2")
|
||||
|
|
@ -660,9 +649,11 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
{
|
||||
Name: "Progressing, stage=0->1, Input-State=BatchReady, Output-State=Upgrade",
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseDeploy, v1alpha1.ReadyBatchState)
|
||||
release.Status.CanaryStatus.BatchReadyTime = getOldTime()
|
||||
return release
|
||||
release := releaseDeploy.DeepCopy()
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
release.Spec.ReleasePlan.BatchPartition = pointer.Int32Ptr(1)
|
||||
return setState(release, v1alpha1.ReadyBatchState)
|
||||
},
|
||||
GetDeployments: func() []client.Object {
|
||||
stable := getStableWithReady(stableDeploy, "v2")
|
||||
|
|
@ -678,9 +669,10 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
{
|
||||
Name: "Progressing, stage=0->1, Input-State=BatchReady, Output-State=BatchReady",
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseDeploy, v1alpha1.ReadyBatchState)
|
||||
now := metav1.Now()
|
||||
release.Status.CanaryStatus.BatchReadyTime = &now
|
||||
release := releaseDeploy.DeepCopy()
|
||||
release.Status.CanaryStatus.UpdatedReplicas = 10
|
||||
release.Status.CanaryStatus.UpdatedReadyReplicas = 10
|
||||
release = setState(release, v1alpha1.ReadyBatchState)
|
||||
return release
|
||||
},
|
||||
GetDeployments: func() []client.Object {
|
||||
|
|
@ -713,7 +705,7 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
ExpectedState: v1alpha1.UpgradingBatchState,
|
||||
},
|
||||
{
|
||||
Name: `Special Case: RollBack, Input-Phase=Progressing, Output-Phase=Abort`,
|
||||
Name: `Special Case: RollBack, Input-Phase=Progressing, Output-Phase=Progressing`,
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseDeploy, v1alpha1.ReadyBatchState)
|
||||
now := metav1.Now()
|
||||
|
|
@ -733,11 +725,11 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
stable, canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseFinalizing,
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseProgressing,
|
||||
ExpectedState: v1alpha1.ReadyBatchState,
|
||||
},
|
||||
{
|
||||
Name: `Special Case: Deletion, Input-Phase=Progressing, Output-Phase=Terminating`,
|
||||
Name: `Special Case: Deletion, Input-Phase=Progressing, Output-Phase=Finalizing`,
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseDeploy, v1alpha1.ReadyBatchState)
|
||||
now := metav1.Now()
|
||||
|
|
@ -759,11 +751,11 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
stable, canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseTerminating,
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseFinalizing,
|
||||
ExpectedState: v1alpha1.ReadyBatchState,
|
||||
},
|
||||
{
|
||||
Name: `Special Case: Continuous Release, Input-Phase=Progressing, Output-Phase=Initial`,
|
||||
Name: `Special Case: Continuous Release, Input-Phase=Progressing, Output-Phase=Progressing`,
|
||||
GetRelease: func() client.Object {
|
||||
release := setState(releaseDeploy, v1alpha1.ReadyBatchState)
|
||||
now := metav1.Now()
|
||||
|
|
@ -783,13 +775,13 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
stable, canary,
|
||||
}
|
||||
},
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseInitial,
|
||||
ExpectedState: v1alpha1.ReadyBatchState,
|
||||
ExpectedPhase: v1alpha1.RolloutPhaseProgressing,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
defer GinkgoRecover()
|
||||
release := cs.GetRelease()
|
||||
deployments := cs.GetDeployments()
|
||||
rec := record.NewFakeRecorder(100)
|
||||
|
|
@ -808,16 +800,16 @@ func TestReconcile_Deployment(t *testing.T) {
|
|||
Client: cli,
|
||||
recorder: rec,
|
||||
Scheme: scheme,
|
||||
executor: NewReleasePlanExecutor(cli, rec),
|
||||
}
|
||||
|
||||
key := client.ObjectKeyFromObject(release)
|
||||
request := reconcile.Request{NamespacedName: key}
|
||||
result, err := reconciler.Reconcile(context.TODO(), request)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
result, _ := reconciler.Reconcile(context.TODO(), request)
|
||||
Expect(result.RequeueAfter).Should(BeNumerically(">=", int64(0)))
|
||||
|
||||
newRelease := v1alpha1.BatchRelease{}
|
||||
err = cli.Get(context.TODO(), key, &newRelease)
|
||||
err := cli.Get(context.TODO(), key, &newRelease)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(newRelease.Status.Phase).Should(Equal(cs.ExpectedPhase))
|
||||
Expect(newRelease.Status.CanaryStatus.CurrentBatch).Should(Equal(cs.ExpectedBatch))
|
||||
|
|
@ -838,12 +830,8 @@ func containers(version string) []corev1.Container {
|
|||
func setPhase(release *v1alpha1.BatchRelease, phase v1alpha1.RolloutPhase) *v1alpha1.BatchRelease {
|
||||
r := release.DeepCopy()
|
||||
r.Status.Phase = phase
|
||||
switch phase {
|
||||
case v1alpha1.RolloutPhaseInitial, v1alpha1.RolloutPhaseHealthy:
|
||||
default:
|
||||
r.Status.ObservedWorkloadReplicas = 100
|
||||
r.Status.ObservedReleasePlanHash = util.HashReleasePlanBatches(&release.Spec.ReleasePlan)
|
||||
}
|
||||
r.Status.ObservedWorkloadReplicas = 100
|
||||
r.Status.ObservedReleasePlanHash = util.HashReleasePlanBatches(&release.Spec.ReleasePlan)
|
||||
return r
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
utilclient "github.com/openkruise/rollouts/pkg/util/client"
|
||||
expectations "github.com/openkruise/rollouts/pkg/util/expectation"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
|
@ -61,10 +62,13 @@ func (p podEventHandler) Create(evt event.CreateEvent, q workqueue.RateLimitingI
|
|||
}
|
||||
p.enqueue(pod, q)
|
||||
}
|
||||
|
||||
func (p podEventHandler) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
|
||||
}
|
||||
|
||||
func (p podEventHandler) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
|
||||
}
|
||||
|
||||
func (p podEventHandler) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
|
||||
oldPod, oldOK := evt.ObjectOld.(*corev1.Pod)
|
||||
newPod, newOK := evt.ObjectNew.(*corev1.Pod)
|
||||
|
|
@ -118,6 +122,7 @@ type workloadEventHandler struct {
|
|||
}
|
||||
|
||||
func (w workloadEventHandler) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
|
||||
expectationObserved(evt.Object)
|
||||
w.handleWorkload(q, evt.Object, CreateEventAction)
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +145,7 @@ func (w workloadEventHandler) Update(evt event.UpdateEvent, q workqueue.RateLimi
|
|||
|
||||
oldObject := evt.ObjectNew
|
||||
newObject := evt.ObjectOld
|
||||
expectationObserved(newObject)
|
||||
if newObject.GetResourceVersion() == oldObject.GetResourceVersion() {
|
||||
return
|
||||
}
|
||||
|
|
@ -244,3 +250,24 @@ func getBatchRelease(c client.Reader, workloadNamespaceName types.NamespacedName
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
func expectationObserved(object client.Object) {
|
||||
controllerKey := getControllerKey(object)
|
||||
if controllerKey != nil {
|
||||
klog.V(3).Infof("observed %v, remove from expectation %s: %s",
|
||||
klog.KObj(object), *controllerKey, string(object.GetUID()))
|
||||
expectations.ResourceExpectations.Observe(*controllerKey, expectations.Create, string(object.GetUID()))
|
||||
}
|
||||
}
|
||||
|
||||
func getControllerKey(object client.Object) *string {
|
||||
owner := metav1.GetControllerOfNoCopy(object)
|
||||
if owner == nil {
|
||||
return nil
|
||||
}
|
||||
if owner.APIVersion == v1alpha1.GroupVersion.String() {
|
||||
key := types.NamespacedName{Namespace: object.GetNamespace(), Name: owner.Name}.String()
|
||||
return &key
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ import (
|
|||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
|
@ -34,7 +36,7 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
)
|
||||
|
||||
func TestEventHandler_Update(t *testing.T) {
|
||||
func TestWorkloadEventHandler_Update(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
|
|
@ -141,7 +143,7 @@ func TestEventHandler_Update(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEventHandler_Create(t *testing.T) {
|
||||
func TestWorkloadEventHandler_Create(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
|
|
@ -197,7 +199,7 @@ func TestEventHandler_Create(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEventHandler_Delete(t *testing.T) {
|
||||
func TestWorkloadEventHandler_Delete(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
|
|
@ -252,3 +254,245 @@ func TestEventHandler_Delete(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodEventHandler_Update(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
GetOldPod func() client.Object
|
||||
GetNewPod func() client.Object
|
||||
GetWorkload func() client.Object
|
||||
ExpectedQueueLen int
|
||||
}{
|
||||
{
|
||||
Name: "CloneSet Pod NotReady -> Ready",
|
||||
GetOldPod: func() client.Object {
|
||||
return generatePod(false, true, "version-1")
|
||||
},
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(true, true, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(releaseClone, releaseClone.GetObjectKind().GroupVersionKind()))
|
||||
clone.Annotations = map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(owner),
|
||||
}
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 1,
|
||||
},
|
||||
{
|
||||
Name: "CloneSet Pod Ready -> Ready",
|
||||
GetOldPod: func() client.Object {
|
||||
return generatePod(true, true, "version-1")
|
||||
},
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(true, true, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(releaseClone, releaseClone.GetObjectKind().GroupVersionKind()))
|
||||
clone.Annotations = map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(owner),
|
||||
}
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 0,
|
||||
},
|
||||
{
|
||||
Name: "Orphan Pod NotReady -> Ready",
|
||||
GetOldPod: func() client.Object {
|
||||
return generatePod(false, false, "version-1")
|
||||
},
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(true, false, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(releaseClone, releaseClone.GetObjectKind().GroupVersionKind()))
|
||||
clone.Annotations = map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(owner),
|
||||
}
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 0,
|
||||
},
|
||||
{
|
||||
Name: "Orphan Pod Ready -> Ready",
|
||||
GetOldPod: func() client.Object {
|
||||
return generatePod(true, false, "version-1")
|
||||
},
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(true, false, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(releaseClone, releaseClone.GetObjectKind().GroupVersionKind()))
|
||||
clone.Annotations = map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(owner),
|
||||
}
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 0,
|
||||
},
|
||||
{
|
||||
Name: "Free CloneSet Pod NotReady -> Ready",
|
||||
GetOldPod: func() client.Object {
|
||||
return generatePod(false, false, "version-1")
|
||||
},
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(true, false, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 0,
|
||||
},
|
||||
{
|
||||
Name: "Free CloneSet Pod Ready -> Ready",
|
||||
GetOldPod: func() client.Object {
|
||||
return generatePod(true, false, "version-1")
|
||||
},
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(true, false, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 0,
|
||||
},
|
||||
{
|
||||
Name: "CloneSet Pod V1 -> V2",
|
||||
GetOldPod: func() client.Object {
|
||||
return generatePod(true, true, "version-1")
|
||||
},
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(true, true, "version-2")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(releaseClone, releaseClone.GetObjectKind().GroupVersionKind()))
|
||||
clone.Annotations = map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(owner),
|
||||
}
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
oldObject := cs.GetOldPod()
|
||||
newObject := cs.GetNewPod()
|
||||
workload := cs.GetWorkload()
|
||||
newSJk := scheme
|
||||
fmt.Println(newSJk)
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(releaseDeploy.DeepCopy(), workload).Build()
|
||||
handler := podEventHandler{Reader: cli}
|
||||
updateQ := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
updateEvt := event.UpdateEvent{
|
||||
ObjectOld: oldObject,
|
||||
ObjectNew: newObject,
|
||||
}
|
||||
handler.Update(updateEvt, updateQ)
|
||||
Expect(updateQ.Len()).Should(Equal(cs.ExpectedQueueLen))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodEventHandler_Create(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
GetNewPod func() client.Object
|
||||
GetWorkload func() client.Object
|
||||
ExpectedQueueLen int
|
||||
}{
|
||||
{
|
||||
Name: "CloneSet Pod",
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(false, true, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(releaseClone, releaseClone.GetObjectKind().GroupVersionKind()))
|
||||
clone.Annotations = map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(owner),
|
||||
}
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 1,
|
||||
},
|
||||
{
|
||||
Name: "Orphan Pod",
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(false, false, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
clone := stableClone.DeepCopy()
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(releaseClone, releaseClone.GetObjectKind().GroupVersionKind()))
|
||||
clone.Annotations = map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(owner),
|
||||
}
|
||||
return clone
|
||||
},
|
||||
ExpectedQueueLen: 0,
|
||||
},
|
||||
{
|
||||
Name: "Free CloneSet Pod",
|
||||
GetNewPod: func() client.Object {
|
||||
return generatePod(false, true, "version-1")
|
||||
},
|
||||
GetWorkload: func() client.Object {
|
||||
return stableClone.DeepCopy()
|
||||
},
|
||||
ExpectedQueueLen: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
newObject := cs.GetNewPod()
|
||||
workload := cs.GetWorkload()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(releaseDeploy.DeepCopy(), workload).Build()
|
||||
handler := podEventHandler{Reader: cli}
|
||||
createQ := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
createEvt := event.CreateEvent{
|
||||
Object: newObject,
|
||||
}
|
||||
handler.Create(createEvt, createQ)
|
||||
Expect(createQ.Len()).Should(Equal(cs.ExpectedQueueLen))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generatePod(ready, owned bool, version string) *corev1.Pod {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pod",
|
||||
Namespace: stableClone.Namespace,
|
||||
ResourceVersion: string(uuid.NewUUID()),
|
||||
Labels: map[string]string{
|
||||
apps.ControllerRevisionHashLabelKey: version,
|
||||
},
|
||||
},
|
||||
}
|
||||
if ready {
|
||||
pod.Status.Phase = corev1.PodRunning
|
||||
pod.Status.Conditions = append(pod.Status.Conditions, corev1.PodCondition{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionTrue,
|
||||
})
|
||||
}
|
||||
if owned {
|
||||
pod.OwnerReferences = append(pod.OwnerReferences,
|
||||
*metav1.NewControllerRef(stableClone, stableClone.GroupVersionKind()))
|
||||
}
|
||||
return pod
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 batchrelease
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
appsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control/canarystyle"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control/canarystyle/deployment"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control/partitionstyle"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control/partitionstyle/cloneset"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control/partitionstyle/statefulset"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDuration = 2 * time.Second
|
||||
)
|
||||
|
||||
// Executor is the controller that controls the release plan resource
|
||||
type Executor struct {
|
||||
client client.Client
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
// NewReleasePlanExecutor creates a RolloutPlanController
|
||||
func NewReleasePlanExecutor(cli client.Client, recorder record.EventRecorder) *Executor {
|
||||
return &Executor{
|
||||
client: cli,
|
||||
recorder: recorder,
|
||||
}
|
||||
}
|
||||
|
||||
// Do execute the release plan
|
||||
func (r *Executor) Do(release *v1alpha1.BatchRelease) (reconcile.Result, *v1alpha1.BatchReleaseStatus, error) {
|
||||
klog.InfoS("Starting one round of reconciling release plan",
|
||||
"BatchRelease", client.ObjectKeyFromObject(release),
|
||||
"phase", release.Status.Phase,
|
||||
"current-batch", release.Status.CanaryStatus.CurrentBatch,
|
||||
"current-batch-state", release.Status.CanaryStatus.CurrentBatchState)
|
||||
|
||||
newStatus := getInitializedStatus(&release.Status)
|
||||
workloadController, err := r.getReleaseController(release, newStatus)
|
||||
if err != nil || workloadController == nil {
|
||||
return reconcile.Result{}, nil, nil
|
||||
}
|
||||
|
||||
stop, result, err := r.syncStatusBeforeExecuting(release, newStatus, workloadController)
|
||||
if stop || err != nil {
|
||||
return result, newStatus, err
|
||||
}
|
||||
|
||||
return r.executeBatchReleasePlan(release, newStatus, workloadController)
|
||||
}
|
||||
|
||||
func (r *Executor) executeBatchReleasePlan(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, workloadController control.Interface) (reconcile.Result, *v1alpha1.BatchReleaseStatus, error) {
|
||||
var err error
|
||||
result := reconcile.Result{}
|
||||
|
||||
klog.V(3).Infof("BatchRelease(%v) State Machine into '%s' state", klog.KObj(release), newStatus.Phase)
|
||||
|
||||
switch newStatus.Phase {
|
||||
default:
|
||||
// for compatibility. if it is an unknown phase, should start from beginning.
|
||||
newStatus.Phase = v1alpha1.RolloutPhasePreparing
|
||||
fallthrough
|
||||
|
||||
case v1alpha1.RolloutPhasePreparing:
|
||||
// prepare and initialize something before progressing in this state.
|
||||
err = workloadController.Initialize()
|
||||
switch {
|
||||
case err == nil:
|
||||
newStatus.Phase = v1alpha1.RolloutPhaseProgressing
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
}
|
||||
|
||||
case v1alpha1.RolloutPhaseProgressing:
|
||||
// progress the release plan in this state.
|
||||
result, err = r.progressBatches(release, newStatus, workloadController)
|
||||
|
||||
case v1alpha1.RolloutPhaseFinalizing:
|
||||
err = workloadController.Finalize()
|
||||
switch {
|
||||
case err == nil:
|
||||
newStatus.Phase = v1alpha1.RolloutPhaseCompleted
|
||||
}
|
||||
|
||||
case v1alpha1.RolloutPhaseCompleted:
|
||||
// this state indicates that the plan is executed/cancelled successfully, should do nothing in these states.
|
||||
}
|
||||
|
||||
return result, newStatus, err
|
||||
}
|
||||
|
||||
// reconcile logic when we are in the middle of release, we have to go through finalizing state before succeed or fail
|
||||
func (r *Executor) progressBatches(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, workloadController control.Interface) (reconcile.Result, error) {
|
||||
var err error
|
||||
result := reconcile.Result{}
|
||||
|
||||
klog.V(3).Infof("BatchRelease(%v) Canary Batch State Machine into '%s' state", klog.KObj(release), newStatus.CanaryStatus.CurrentBatchState)
|
||||
|
||||
switch newStatus.CanaryStatus.CurrentBatchState {
|
||||
default:
|
||||
// for compatibility. if it is an unknown state, should start from beginning.
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
fallthrough
|
||||
|
||||
case v1alpha1.UpgradingBatchState:
|
||||
// modify workload replicas/partition based on release plan in this state.
|
||||
err = workloadController.UpgradeBatch()
|
||||
switch {
|
||||
case err == nil:
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.VerifyingBatchState
|
||||
}
|
||||
|
||||
case v1alpha1.VerifyingBatchState:
|
||||
// replicas/partition has been modified, should wait pod ready in this state.
|
||||
err = workloadController.CheckBatchReady()
|
||||
switch {
|
||||
case err != nil:
|
||||
// should go to upgrade state to do again to avoid dead wait.
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
default:
|
||||
now := metav1.Now()
|
||||
newStatus.CanaryStatus.BatchReadyTime = &now
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.ReadyBatchState
|
||||
}
|
||||
|
||||
case v1alpha1.ReadyBatchState:
|
||||
// replicas/partition may be modified even though ready, should recheck in this state.
|
||||
err = workloadController.CheckBatchReady()
|
||||
switch {
|
||||
case err != nil:
|
||||
// if the batch ready condition changed due to some reasons, just recalculate the current batch.
|
||||
newStatus.CanaryStatus.BatchReadyTime = nil
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
case !isPartitioned(release):
|
||||
r.moveToNextBatch(release, newStatus)
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
}
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetWorkloadController pick the right workload controller to work on the workload
|
||||
func (r *Executor) getReleaseController(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus) (control.Interface, error) {
|
||||
targetRef := release.Spec.TargetRef.WorkloadRef
|
||||
if targetRef == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gvk := schema.FromAPIVersionAndKind(targetRef.APIVersion, targetRef.Kind)
|
||||
if !util.IsSupportedWorkload(gvk) {
|
||||
message := fmt.Sprintf("the workload type '%v' is not supported", gvk)
|
||||
r.recorder.Event(release, v1.EventTypeWarning, "UnsupportedWorkload", message)
|
||||
return nil, fmt.Errorf(message)
|
||||
}
|
||||
|
||||
targetKey := types.NamespacedName{
|
||||
Namespace: release.Namespace,
|
||||
Name: targetRef.Name,
|
||||
}
|
||||
|
||||
switch targetRef.APIVersion {
|
||||
case appsv1alpha1.GroupVersion.String():
|
||||
if targetRef.Kind == reflect.TypeOf(appsv1alpha1.CloneSet{}).Name() {
|
||||
klog.InfoS("Using CloneSet batch release controller for this batch release", "workload name", targetKey.Name, "namespace", targetKey.Namespace)
|
||||
return partitionstyle.NewControlPlane(cloneset.NewController, r.client, r.recorder, release, newStatus, targetKey, gvk), nil
|
||||
}
|
||||
|
||||
case apps.SchemeGroupVersion.String():
|
||||
if targetRef.Kind == reflect.TypeOf(apps.Deployment{}).Name() {
|
||||
klog.InfoS("Using Deployment batch release controller for this batch release", "workload name", targetKey.Name, "namespace", targetKey.Namespace)
|
||||
return canarystyle.NewControlPlane(deployment.NewController, r.client, r.recorder, release, newStatus, targetKey), nil
|
||||
}
|
||||
}
|
||||
|
||||
// try to use StatefulSet-like rollout controller by default
|
||||
klog.InfoS("Using StatefulSet-like batch release controller for this batch release", "workload name", targetKey.Name, "namespace", targetKey.Namespace)
|
||||
return partitionstyle.NewControlPlane(statefulset.NewController, r.client, r.recorder, release, newStatus, targetKey, gvk), nil
|
||||
}
|
||||
|
||||
func (r *Executor) moveToNextBatch(release *v1alpha1.BatchRelease, status *v1alpha1.BatchReleaseStatus) {
|
||||
currentBatch := int(status.CanaryStatus.CurrentBatch)
|
||||
if currentBatch >= len(release.Spec.ReleasePlan.Batches)-1 {
|
||||
klog.V(3).Infof("BatchRelease(%v) finished all batch, release current batch: %v", klog.KObj(release), status.CanaryStatus.CurrentBatch)
|
||||
}
|
||||
if release.Spec.ReleasePlan.BatchPartition == nil || *release.Spec.ReleasePlan.BatchPartition > status.CanaryStatus.CurrentBatch {
|
||||
status.CanaryStatus.CurrentBatch++
|
||||
}
|
||||
status.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
klog.V(3).Infof("BatchRelease(%v) finished one batch, release current batch: %v", klog.KObj(release), status.CanaryStatus.CurrentBatch)
|
||||
}
|
||||
|
||||
func isPartitioned(release *v1alpha1.BatchRelease) bool {
|
||||
return release.Spec.ReleasePlan.BatchPartition != nil &&
|
||||
*release.Spec.ReleasePlan.BatchPartition <= release.Status.CanaryStatus.CurrentBatch
|
||||
}
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 batchrelease
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
appsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/workloads"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDuration = 2 * time.Second
|
||||
)
|
||||
|
||||
// Executor is the controller that controls the release plan resource
|
||||
type Executor struct {
|
||||
client client.Client
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
// NewReleasePlanExecutor creates a RolloutPlanController
|
||||
func NewReleasePlanExecutor(cli client.Client, recorder record.EventRecorder) *Executor {
|
||||
return &Executor{
|
||||
client: cli,
|
||||
recorder: recorder,
|
||||
}
|
||||
}
|
||||
|
||||
// Do execute the release plan
|
||||
func (r *Executor) Do(release *v1alpha1.BatchRelease) (reconcile.Result, *v1alpha1.BatchReleaseStatus, error) {
|
||||
klog.InfoS("Starting one round of reconciling release plan",
|
||||
"BatchRelease", client.ObjectKeyFromObject(release),
|
||||
"phase", release.Status.Phase,
|
||||
"current-batch", release.Status.CanaryStatus.CurrentBatch,
|
||||
"current-batch-state", release.Status.CanaryStatus.CurrentBatchState)
|
||||
|
||||
newStatus := getInitializedStatus(&release.Status)
|
||||
workloadController, err := r.getWorkloadController(release, newStatus)
|
||||
if err != nil || workloadController == nil {
|
||||
return reconcile.Result{}, nil, nil
|
||||
}
|
||||
|
||||
stop, result, err := r.syncStatusBeforeExecuting(release, newStatus, workloadController)
|
||||
if stop || err != nil {
|
||||
return result, newStatus, err
|
||||
}
|
||||
|
||||
return r.executeBatchReleasePlan(release, newStatus, workloadController)
|
||||
}
|
||||
|
||||
func (r *Executor) executeBatchReleasePlan(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, workloadController workloads.WorkloadController) (reconcile.Result, *v1alpha1.BatchReleaseStatus, error) {
|
||||
var err error
|
||||
result := reconcile.Result{}
|
||||
|
||||
klog.V(3).Infof("BatchRelease(%v) State Machine into '%s' state", klog.KObj(release), newStatus.Phase)
|
||||
|
||||
switch newStatus.Phase {
|
||||
case v1alpha1.RolloutPhaseInitial:
|
||||
// if this batchRelease was created but workload doest not exist,
|
||||
// should keep this phase and do nothing util workload is created.
|
||||
|
||||
case v1alpha1.RolloutPhaseHealthy:
|
||||
// verify whether the workload is ready to execute the release plan in this state.
|
||||
var verifiedDone bool
|
||||
verifiedDone, err = workloadController.VerifyWorkload()
|
||||
switch {
|
||||
case err != nil:
|
||||
setCondition(newStatus, v1alpha1.VerifyingBatchReleaseCondition, v1.ConditionFalse, v1alpha1.FailedBatchReleaseConditionReason, err.Error())
|
||||
case verifiedDone:
|
||||
newStatus.Phase = v1alpha1.RolloutPhasePreparing
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
setCondition(newStatus, v1alpha1.PreparingBatchReleaseCondition, v1.ConditionTrue, "", "BatchRelease is preparing for progress")
|
||||
}
|
||||
|
||||
case v1alpha1.RolloutPhasePreparing:
|
||||
// prepare and initialize something before progressing in this state.
|
||||
var preparedDone bool
|
||||
var replicasNoNeedToRollback *int32
|
||||
preparedDone, replicasNoNeedToRollback, err = workloadController.PrepareBeforeProgress()
|
||||
switch {
|
||||
case err != nil:
|
||||
setCondition(newStatus, v1alpha1.PreparingBatchReleaseCondition, v1.ConditionFalse, v1alpha1.FailedBatchReleaseConditionReason, err.Error())
|
||||
case preparedDone:
|
||||
newStatus.Phase = v1alpha1.RolloutPhaseProgressing
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
newStatus.CanaryStatus.NoNeedUpdateReplicas = replicasNoNeedToRollback
|
||||
setCondition(newStatus, v1alpha1.ProgressingBatchReleaseCondition, v1.ConditionTrue, "", "BatchRelease is progressing")
|
||||
default:
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
}
|
||||
|
||||
case v1alpha1.RolloutPhaseProgressing:
|
||||
// progress the release plan in this state.
|
||||
var progressDone bool
|
||||
progressDone, result, err = r.progressBatches(release, newStatus, workloadController)
|
||||
switch {
|
||||
case progressDone:
|
||||
newStatus.Phase = v1alpha1.RolloutPhaseFinalizing
|
||||
setCondition(newStatus, v1alpha1.FinalizingBatchReleaseCondition, v1.ConditionTrue, "", "BatchRelease is finalizing")
|
||||
}
|
||||
|
||||
case v1alpha1.RolloutPhaseFinalizing:
|
||||
// finalize canary the resources when progressing done.
|
||||
// Do not clean the canary resources, because rollout
|
||||
// controller should set the traffic routing first.
|
||||
var finalizedDone bool
|
||||
finalizedDone, err = workloadController.FinalizeProgress(false)
|
||||
switch {
|
||||
case err != nil:
|
||||
setCondition(newStatus, v1alpha1.CompletedBatchReleaseCondition, v1.ConditionFalse, v1alpha1.FailedBatchReleaseConditionReason, err.Error())
|
||||
case finalizedDone:
|
||||
if IsAllBatchReady(release) {
|
||||
newStatus.Phase = v1alpha1.RolloutPhaseCompleted
|
||||
setCondition(newStatus, v1alpha1.CompletedBatchReleaseCondition, v1.ConditionTrue, v1alpha1.SucceededBatchReleaseConditionReason, "BatchRelease is completed")
|
||||
} else {
|
||||
newStatus.Phase = v1alpha1.RolloutPhaseCancelled
|
||||
setCondition(newStatus, v1alpha1.CancelledBatchReleaseCondition, v1.ConditionTrue, v1alpha1.SucceededBatchReleaseConditionReason, "BatchRelease is cancelled")
|
||||
}
|
||||
default:
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
}
|
||||
|
||||
case v1alpha1.RolloutPhaseTerminating:
|
||||
var finalizedDone bool
|
||||
finalizedDone, err = workloadController.FinalizeProgress(true)
|
||||
switch {
|
||||
case err != nil:
|
||||
setCondition(newStatus, v1alpha1.CompletedBatchReleaseCondition, v1.ConditionFalse, v1alpha1.FailedBatchReleaseConditionReason, err.Error())
|
||||
case finalizedDone:
|
||||
setCondition(newStatus, v1alpha1.TerminatedBatchReleaseCondition, v1.ConditionTrue, v1alpha1.SucceededBatchReleaseConditionReason, "BatchRelease is terminated")
|
||||
default:
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
}
|
||||
|
||||
case v1alpha1.RolloutPhaseCompleted, v1alpha1.RolloutPhaseCancelled:
|
||||
// this state indicates that the plan is executed/cancelled successfully, should do nothing in these states.
|
||||
|
||||
default:
|
||||
klog.V(3).Infof("BatchRelease(%v) State Machine into %s state", klog.KObj(release), "Unknown")
|
||||
panic(fmt.Sprintf("illegal release status %+v", newStatus))
|
||||
}
|
||||
|
||||
return result, newStatus, err
|
||||
}
|
||||
|
||||
// reconcile logic when we are in the middle of release, we have to go through finalizing state before succeed or fail
|
||||
func (r *Executor) progressBatches(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, workloadController workloads.WorkloadController) (bool, reconcile.Result, error) {
|
||||
var err error
|
||||
progressDone := false
|
||||
result := reconcile.Result{}
|
||||
|
||||
klog.V(3).Infof("BatchRelease(%v) Canary Batch State Machine into '%s' state", klog.KObj(release), newStatus.CanaryStatus.CurrentBatchState)
|
||||
|
||||
switch newStatus.CanaryStatus.CurrentBatchState {
|
||||
case "", v1alpha1.UpgradingBatchState:
|
||||
// modify workload replicas/partition based on release plan in this state.
|
||||
upgradeDone, upgradeErr := workloadController.UpgradeOneBatch()
|
||||
switch {
|
||||
case upgradeErr != nil:
|
||||
err = upgradeErr
|
||||
setCondition(newStatus, "Progressing", v1.ConditionFalse, "UpgradeBatchFailed", err.Error())
|
||||
case upgradeDone:
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.VerifyingBatchState
|
||||
}
|
||||
|
||||
case v1alpha1.VerifyingBatchState:
|
||||
// replicas/partition has been modified, should wait pod ready in this state.
|
||||
verified, verifiedErr := workloadController.CheckOneBatchReady()
|
||||
switch {
|
||||
case verifiedErr != nil:
|
||||
err = verifiedErr
|
||||
setCondition(newStatus, "Progressing", v1.ConditionFalse, "VerifyBatchFailed", err.Error())
|
||||
case verified:
|
||||
now := metav1.Now()
|
||||
newStatus.CanaryStatus.BatchReadyTime = &now
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.ReadyBatchState
|
||||
default:
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
}
|
||||
|
||||
case v1alpha1.ReadyBatchState:
|
||||
if !IsPartitioned(release) {
|
||||
// expected pods in the batch are upgraded and the state is ready, then try to move to the next batch
|
||||
progressDone = r.moveToNextBatch(release, newStatus)
|
||||
result = reconcile.Result{RequeueAfter: DefaultDuration}
|
||||
setCondition(newStatus, v1alpha1.ProgressingBatchReleaseCondition, v1.ConditionTrue, "", "BatchRelease is progressing")
|
||||
} else {
|
||||
setCondition(newStatus, "Progressing", v1.ConditionFalse, "Paused", fmt.Sprintf("BatchRelease is partitioned in %v-th batch", newStatus.CanaryStatus.CurrentBatch))
|
||||
}
|
||||
|
||||
default:
|
||||
klog.V(3).Infof("ReleasePlan(%v) Batch State Machine into %s state", "Unknown")
|
||||
panic(fmt.Sprintf("illegal status %+v", newStatus))
|
||||
}
|
||||
|
||||
return progressDone, result, err
|
||||
}
|
||||
|
||||
// GetWorkloadController pick the right workload controller to work on the workload
|
||||
func (r *Executor) getWorkloadController(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus) (workloads.WorkloadController, error) {
|
||||
targetRef := release.Spec.TargetRef.WorkloadRef
|
||||
if targetRef == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gvk := schema.FromAPIVersionAndKind(targetRef.APIVersion, targetRef.Kind)
|
||||
if !util.IsSupportedWorkload(gvk) {
|
||||
message := fmt.Sprintf("the workload type '%v' is not supported", gvk)
|
||||
r.recorder.Event(release, v1.EventTypeWarning, "UnsupportedWorkload", message)
|
||||
return nil, fmt.Errorf(message)
|
||||
}
|
||||
|
||||
targetKey := types.NamespacedName{
|
||||
Namespace: release.Namespace,
|
||||
Name: targetRef.Name,
|
||||
}
|
||||
|
||||
switch targetRef.APIVersion {
|
||||
case appsv1alpha1.GroupVersion.String():
|
||||
if targetRef.Kind == reflect.TypeOf(appsv1alpha1.CloneSet{}).Name() {
|
||||
klog.InfoS("using cloneset batch release controller for this batch release", "workload name", targetKey.Name, "namespace", targetKey.Namespace)
|
||||
return workloads.NewCloneSetRolloutController(r.client, r.recorder, release, newStatus, targetKey), nil
|
||||
}
|
||||
|
||||
case apps.SchemeGroupVersion.String():
|
||||
if targetRef.Kind == reflect.TypeOf(apps.Deployment{}).Name() {
|
||||
klog.InfoS("using deployment batch release controller for this batch release", "workload name", targetKey.Name, "namespace", targetKey.Namespace)
|
||||
return workloads.NewDeploymentRolloutController(r.client, r.recorder, release, newStatus, targetKey), nil
|
||||
}
|
||||
}
|
||||
|
||||
klog.InfoS("using statefulset-like batch release controller for this batch release", "workload name", targetKey.Name, "namespace", targetKey.Namespace)
|
||||
return workloads.NewUnifiedWorkloadRolloutControlPlane(workloads.NewStatefulSetLikeController, r.client, r.recorder, release, newStatus, targetKey, gvk), nil
|
||||
}
|
||||
|
||||
func (r *Executor) moveToNextBatch(release *v1alpha1.BatchRelease, status *v1alpha1.BatchReleaseStatus) bool {
|
||||
currentBatch := int(status.CanaryStatus.CurrentBatch)
|
||||
if currentBatch >= len(release.Spec.ReleasePlan.Batches)-1 {
|
||||
klog.V(3).Infof("BatchRelease(%v) finished all batch, release current batch: %v", klog.KObj(release), status.CanaryStatus.CurrentBatch)
|
||||
return true
|
||||
} else {
|
||||
if release.Spec.ReleasePlan.BatchPartition == nil || *release.Spec.ReleasePlan.BatchPartition > status.CanaryStatus.CurrentBatch {
|
||||
status.CanaryStatus.CurrentBatch++
|
||||
}
|
||||
status.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
klog.V(3).Infof("BatchRelease(%v) finished one batch, release current batch: %v", klog.KObj(release), status.CanaryStatus.CurrentBatch)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -20,24 +20,23 @@ import (
|
|||
"reflect"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/workloads"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/integer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
func (r *Executor) syncStatusBeforeExecuting(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, controller workloads.WorkloadController) (bool, reconcile.Result, error) {
|
||||
func (r *Executor) syncStatusBeforeExecuting(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, controller control.Interface) (bool, reconcile.Result, error) {
|
||||
var err error
|
||||
var reason string
|
||||
var message string
|
||||
var needRetry bool
|
||||
needStopThisRound := false
|
||||
result := reconcile.Result{}
|
||||
// sync the workload info and watch the workload change event
|
||||
workloadEvent, workloadInfo, err := controller.SyncWorkloadInfo()
|
||||
workloadEvent, workloadInfo, err := controller.SyncWorkloadInformation()
|
||||
|
||||
// Note: must keep the order of the following cases:
|
||||
switch {
|
||||
|
|
@ -45,40 +44,35 @@ func (r *Executor) syncStatusBeforeExecuting(release *v1alpha1.BatchRelease, new
|
|||
SPECIAL CASES ABOUT THE BATCH RELEASE PLAN
|
||||
*************************************************************************/
|
||||
//The following special cases are about the **batch release plan**, include:
|
||||
// (1). Plan is deleted or cancelled
|
||||
// (2). Plan is paused during rollout
|
||||
// (1). Plan has been terminated
|
||||
// (2). Plan is deleted or cancelled
|
||||
// (3). Plan is changed during rollout
|
||||
// (4). Plan status is unexpected/unhealthy
|
||||
case isPlanTerminating(release):
|
||||
// handle the case that the plan is deleted or is terminating
|
||||
reason = "PlanTerminating"
|
||||
message = "Release plan is deleted or cancelled, then terminate"
|
||||
signalTerminating(newStatus)
|
||||
|
||||
case isPlanPaused(workloadEvent, release):
|
||||
// handle the case that releasePlan.paused = true
|
||||
reason = "PlanPaused"
|
||||
message = "release plan is paused, then stop reconcile"
|
||||
case isPlanCompleted(release):
|
||||
message = "release plan has been terminated, will do nothing"
|
||||
needStopThisRound = true
|
||||
|
||||
case isPlanFinalizing(release):
|
||||
// handle the case that the plan is deleted or is terminating
|
||||
message = "release plan is deleted or cancelled, then finalize"
|
||||
signalFinalizing(newStatus)
|
||||
|
||||
case isPlanChanged(release):
|
||||
// handle the case that release plan is changed during progressing
|
||||
reason = "PlanChanged"
|
||||
message = "release plan is changed, then recalculate status"
|
||||
signalRecalculate(release, newStatus)
|
||||
|
||||
case isPlanUnhealthy(release):
|
||||
// handle the case that release status is chaos which may lead to panic
|
||||
reason = "PlanStatusUnhealthy"
|
||||
message = "release plan is unhealthy, then restart"
|
||||
needStopThisRound = true
|
||||
signalRestartAll(newStatus)
|
||||
|
||||
/**************************************************************************
|
||||
SPECIAL CASES ABOUT THE WORKLOAD
|
||||
*************************************************************************/
|
||||
// The following special cases are about the **workload**, include:
|
||||
// (1). Get workload info err
|
||||
// (2). Workload was deleted
|
||||
// (2). Workload is deleted
|
||||
// (3). Workload is created
|
||||
// (4). Workload scale when rollout
|
||||
// (5). Workload rollback when rollout
|
||||
|
|
@ -87,57 +81,39 @@ func (r *Executor) syncStatusBeforeExecuting(release *v1alpha1.BatchRelease, new
|
|||
// (8). Workload is at unstable state, its workload info is untrustworthy
|
||||
case isGetWorkloadInfoError(err):
|
||||
// handle the case of IgnoreNotFound(err) != nil
|
||||
reason = "GetWorkloadError"
|
||||
message = err.Error()
|
||||
|
||||
case isWorkloadGone(workloadEvent, release):
|
||||
// handle the case that the workload is deleted
|
||||
reason = "WorkloadGone"
|
||||
message = "target workload has gone, then terminate"
|
||||
signalTerminating(newStatus)
|
||||
|
||||
case isWorkloadLocated(err, release):
|
||||
// handle the case that workload is newly created
|
||||
reason = "WorkloadLocated"
|
||||
message = "workload is located, then start"
|
||||
signalLocated(newStatus)
|
||||
message = "target workload has gone, then finalize"
|
||||
signalFinalizing(newStatus)
|
||||
|
||||
case isWorkloadScaling(workloadEvent, release):
|
||||
// handle the case that workload is scaling during progressing
|
||||
reason = "ReplicasChanged"
|
||||
message = "workload is scaling, then reinitialize batch status"
|
||||
signalReinitializeBatch(newStatus)
|
||||
signalRestartBatch(newStatus)
|
||||
// we must ensure that this field is updated only when we have observed
|
||||
// the workload scaling event, otherwise this event may be lost.
|
||||
newStatus.ObservedWorkloadReplicas = *workloadInfo.Replicas
|
||||
newStatus.ObservedWorkloadReplicas = workloadInfo.Replicas
|
||||
|
||||
case isWorkloadRevisionChanged(workloadEvent, release):
|
||||
// handle the case of continuous release
|
||||
reason = "TargetRevisionChanged"
|
||||
message = "workload revision was changed, then abort"
|
||||
signalFinalize(newStatus)
|
||||
|
||||
case isWorkloadUnhealthy(workloadEvent, release):
|
||||
// handle the case that workload is unhealthy, and rollout plan cannot go on
|
||||
reason = "WorkloadUnHealthy"
|
||||
message = "workload is UnHealthy, then stop"
|
||||
newStatus.UpdateRevision = workloadInfo.Status.UpdateRevision
|
||||
needStopThisRound = true
|
||||
|
||||
case isWorkloadUnstable(workloadEvent, release):
|
||||
// handle the case that workload.Generation != workload.Status.ObservedGeneration
|
||||
reason = "WorkloadNotStable"
|
||||
message = "workload status is not stable, then wait"
|
||||
needStopThisRound = true
|
||||
|
||||
case isWorkloadRollbackInBatch(workloadEvent, release):
|
||||
// handle the case of rollback in batches
|
||||
if isRollbackInBatchSatisfied(workloadInfo, release) {
|
||||
reason = "RollbackInBatch"
|
||||
message = "workload is rollback in batch"
|
||||
signalRePrepareRollback(newStatus)
|
||||
newStatus.UpdateRevision = workloadInfo.Status.UpdateRevision
|
||||
} else {
|
||||
reason = "Rollback"
|
||||
message = "workload is preparing rollback, wait condition to be satisfied"
|
||||
needStopThisRound = true
|
||||
}
|
||||
|
|
@ -145,7 +121,6 @@ func (r *Executor) syncStatusBeforeExecuting(release *v1alpha1.BatchRelease, new
|
|||
|
||||
// log the special event info
|
||||
if len(message) > 0 {
|
||||
r.recorder.Eventf(release, v1.EventTypeWarning, reason, message)
|
||||
klog.Warningf("Special case occurred in BatchRelease(%v), message: %v", klog.KObj(release), message)
|
||||
}
|
||||
|
||||
|
|
@ -173,18 +148,23 @@ func (r *Executor) syncStatusBeforeExecuting(release *v1alpha1.BatchRelease, new
|
|||
func refreshStatus(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, workloadInfo *util.WorkloadInfo) {
|
||||
// refresh workload info for status
|
||||
if workloadInfo != nil {
|
||||
if workloadInfo.Status != nil {
|
||||
newStatus.CanaryStatus.UpdatedReplicas = workloadInfo.Status.UpdatedReplicas
|
||||
newStatus.CanaryStatus.UpdatedReadyReplicas = workloadInfo.Status.UpdatedReadyReplicas
|
||||
}
|
||||
newStatus.CanaryStatus.UpdatedReplicas = workloadInfo.Status.UpdatedReplicas
|
||||
newStatus.CanaryStatus.UpdatedReadyReplicas = workloadInfo.Status.UpdatedReadyReplicas
|
||||
}
|
||||
if len(newStatus.ObservedReleasePlanHash) == 0 {
|
||||
newStatus.ObservedReleasePlanHash = util.HashReleasePlanBatches(&release.Spec.ReleasePlan)
|
||||
}
|
||||
}
|
||||
|
||||
func isPlanTerminating(release *v1alpha1.BatchRelease) bool {
|
||||
return release.DeletionTimestamp != nil || release.Status.Phase == v1alpha1.RolloutPhaseTerminating
|
||||
func isPlanFinalizing(release *v1alpha1.BatchRelease) bool {
|
||||
if release.DeletionTimestamp != nil || release.Status.Phase == v1alpha1.RolloutPhaseFinalizing {
|
||||
return true
|
||||
}
|
||||
return release.Spec.ReleasePlan.BatchPartition == nil
|
||||
}
|
||||
|
||||
func isPlanCompleted(release *v1alpha1.BatchRelease) bool {
|
||||
return release.Status.Phase == v1alpha1.RolloutPhaseCompleted
|
||||
}
|
||||
|
||||
func isPlanChanged(release *v1alpha1.BatchRelease) bool {
|
||||
|
|
@ -195,46 +175,90 @@ func isPlanUnhealthy(release *v1alpha1.BatchRelease) bool {
|
|||
return int(release.Status.CanaryStatus.CurrentBatch) >= len(release.Spec.ReleasePlan.Batches) && release.Status.Phase == v1alpha1.RolloutPhaseProgressing
|
||||
}
|
||||
|
||||
func isPlanPaused(event workloads.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return release.Spec.Paused && release.Status.Phase == v1alpha1.RolloutPhaseProgressing && !isWorkloadGone(event, release)
|
||||
}
|
||||
|
||||
func isGetWorkloadInfoError(err error) bool {
|
||||
return err != nil && !errors.IsNotFound(err)
|
||||
}
|
||||
|
||||
func isWorkloadLocated(err error, release *v1alpha1.BatchRelease) bool {
|
||||
return err == nil && (release.Status.Phase == v1alpha1.RolloutPhaseInitial || release.Status.Phase == "")
|
||||
func isWorkloadGone(event control.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return event == control.WorkloadHasGone && release.Status.Phase != v1alpha1.RolloutPhaseInitial && release.Status.Phase != ""
|
||||
}
|
||||
|
||||
func isWorkloadGone(event workloads.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return event == workloads.WorkloadHasGone && release.Status.Phase != v1alpha1.RolloutPhaseInitial && release.Status.Phase != ""
|
||||
func isWorkloadScaling(event control.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return event == control.WorkloadReplicasChanged && release.Status.Phase == v1alpha1.RolloutPhaseProgressing
|
||||
}
|
||||
|
||||
func isWorkloadScaling(event workloads.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return event == workloads.WorkloadReplicasChanged && release.Status.Phase == v1alpha1.RolloutPhaseProgressing
|
||||
func isWorkloadRevisionChanged(event control.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return event == control.WorkloadPodTemplateChanged && release.Status.Phase == v1alpha1.RolloutPhaseProgressing
|
||||
}
|
||||
|
||||
func isWorkloadRevisionChanged(event workloads.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return event == workloads.WorkloadPodTemplateChanged && release.Status.Phase == v1alpha1.RolloutPhaseProgressing
|
||||
}
|
||||
|
||||
func isWorkloadRollbackInBatch(event workloads.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return (event == workloads.WorkloadRollbackInBatch || release.Annotations[util.RollbackInBatchAnnotation] != "") &&
|
||||
func isWorkloadRollbackInBatch(event control.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return (event == control.WorkloadRollbackInBatch || release.Annotations[util.RollbackInBatchAnnotation] != "") &&
|
||||
release.Status.CanaryStatus.NoNeedUpdateReplicas == nil && release.Status.Phase == v1alpha1.RolloutPhaseProgressing
|
||||
}
|
||||
|
||||
func isWorkloadUnhealthy(event workloads.WorkloadEventType, release *v1alpha1.BatchRelease) bool {
|
||||
return event == workloads.WorkloadUnHealthy && release.Status.Phase == v1alpha1.RolloutPhaseProgressing
|
||||
}
|
||||
|
||||
func isWorkloadUnstable(event workloads.WorkloadEventType, _ *v1alpha1.BatchRelease) bool {
|
||||
return event == workloads.WorkloadStillReconciling
|
||||
func isWorkloadUnstable(event control.WorkloadEventType, _ *v1alpha1.BatchRelease) bool {
|
||||
return event == control.WorkloadStillReconciling
|
||||
}
|
||||
|
||||
func isRollbackInBatchSatisfied(workloadInfo *util.WorkloadInfo, release *v1alpha1.BatchRelease) bool {
|
||||
if workloadInfo.Status == nil {
|
||||
return false
|
||||
}
|
||||
return workloadInfo.Status.StableRevision == workloadInfo.Status.UpdateRevision && release.Annotations[util.RollbackInBatchAnnotation] != ""
|
||||
}
|
||||
|
||||
func signalRePrepareRollback(newStatus *v1alpha1.BatchReleaseStatus) {
|
||||
newStatus.Phase = v1alpha1.RolloutPhasePreparing
|
||||
newStatus.CanaryStatus.BatchReadyTime = nil
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
}
|
||||
|
||||
func signalRestartBatch(status *v1alpha1.BatchReleaseStatus) {
|
||||
status.CanaryStatus.BatchReadyTime = nil
|
||||
status.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
}
|
||||
|
||||
func signalRestartAll(status *v1alpha1.BatchReleaseStatus) {
|
||||
emptyStatus := v1alpha1.BatchReleaseStatus{}
|
||||
resetStatus(&emptyStatus)
|
||||
*status = emptyStatus
|
||||
}
|
||||
|
||||
func signalFinalizing(status *v1alpha1.BatchReleaseStatus) {
|
||||
status.Phase = v1alpha1.RolloutPhaseFinalizing
|
||||
}
|
||||
|
||||
func signalRecalculate(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus) {
|
||||
// When BatchRelease plan was changed, rollout controller will update this batchRelease cr,
|
||||
// and rollout controller will set BatchPartition as its expected current batch index.
|
||||
currentBatch := int32(0)
|
||||
// if rollout-id is not changed, just use batchPartition;
|
||||
// if rollout-id is changed, we should patch pod batch id from batch 0.
|
||||
observedRolloutID := release.Status.ObservedRolloutID
|
||||
if release.Spec.ReleasePlan.BatchPartition != nil && release.Spec.ReleasePlan.RolloutID == observedRolloutID {
|
||||
// ensure current batch upper bound
|
||||
currentBatch = integer.Int32Min(*release.Spec.ReleasePlan.BatchPartition, int32(len(release.Spec.ReleasePlan.Batches)-1))
|
||||
}
|
||||
|
||||
klog.Infof("BatchRelease(%v) canary batch changed from %v to %v when the release plan changed, observed-rollout-id: %s, current-rollout-id: %s",
|
||||
client.ObjectKeyFromObject(release), newStatus.CanaryStatus.CurrentBatch, currentBatch, observedRolloutID, release.Spec.ReleasePlan.RolloutID)
|
||||
newStatus.CanaryStatus.BatchReadyTime = nil
|
||||
newStatus.CanaryStatus.CurrentBatch = currentBatch
|
||||
newStatus.ObservedRolloutID = release.Spec.ReleasePlan.RolloutID
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
newStatus.ObservedReleasePlanHash = util.HashReleasePlanBatches(&release.Spec.ReleasePlan)
|
||||
}
|
||||
|
||||
func getInitializedStatus(status *v1alpha1.BatchReleaseStatus) *v1alpha1.BatchReleaseStatus {
|
||||
newStatus := status.DeepCopy()
|
||||
if len(status.Phase) == 0 {
|
||||
resetStatus(newStatus)
|
||||
}
|
||||
return newStatus
|
||||
}
|
||||
|
||||
func resetStatus(status *v1alpha1.BatchReleaseStatus) {
|
||||
status.Phase = v1alpha1.RolloutPhasePreparing
|
||||
status.StableRevision = ""
|
||||
status.UpdateRevision = ""
|
||||
status.ObservedReleasePlanHash = ""
|
||||
status.ObservedWorkloadReplicas = -1
|
||||
status.CanaryStatus = v1alpha1.BatchReleaseCanaryStatus{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 batchrelease
|
||||
|
||||
import (
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/integer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func HasTerminatingCondition(status v1alpha1.BatchReleaseStatus) bool {
|
||||
for i := range status.Conditions {
|
||||
c := status.Conditions[i]
|
||||
if c.Type == v1alpha1.TerminatedBatchReleaseCondition && c.Status == v1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getInitializedStatus(status *v1alpha1.BatchReleaseStatus) *v1alpha1.BatchReleaseStatus {
|
||||
newStatus := status.DeepCopy()
|
||||
if len(status.Phase) == 0 {
|
||||
resetStatus(newStatus)
|
||||
}
|
||||
return newStatus
|
||||
}
|
||||
|
||||
func signalRePrepareRollback(newStatus *v1alpha1.BatchReleaseStatus) {
|
||||
newStatus.Phase = v1alpha1.RolloutPhasePreparing
|
||||
newStatus.CanaryStatus.BatchReadyTime = nil
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
}
|
||||
|
||||
func signalReinitializeBatch(status *v1alpha1.BatchReleaseStatus) {
|
||||
status.CanaryStatus.BatchReadyTime = nil
|
||||
status.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
}
|
||||
|
||||
func signalLocated(status *v1alpha1.BatchReleaseStatus) {
|
||||
status.Phase = v1alpha1.RolloutPhaseHealthy
|
||||
setCondition(status, v1alpha1.VerifyingBatchReleaseCondition, v1.ConditionTrue, "", "BatchRelease is verifying the workload")
|
||||
}
|
||||
|
||||
func signalTerminating(status *v1alpha1.BatchReleaseStatus) {
|
||||
status.Phase = v1alpha1.RolloutPhaseTerminating
|
||||
setCondition(status, v1alpha1.TerminatingBatchReleaseCondition, v1.ConditionTrue, "", "BatchRelease is terminating")
|
||||
}
|
||||
|
||||
func signalFinalize(status *v1alpha1.BatchReleaseStatus) {
|
||||
status.Phase = v1alpha1.RolloutPhaseFinalizing
|
||||
setCondition(status, v1alpha1.FinalizingBatchReleaseCondition, v1.ConditionTrue, "", "BatchRelease is finalizing")
|
||||
}
|
||||
|
||||
func signalRecalculate(release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus) {
|
||||
// When BatchRelease plan was changed, rollout controller will update this batchRelease cr,
|
||||
// and rollout controller will set BatchPartition as its expected current batch index.
|
||||
currentBatch := int32(0)
|
||||
// if rollout-id is not changed, just use batchPartition;
|
||||
// if rollout-id is changed, we should patch pod batch id from batch 0.
|
||||
observedRolloutID := release.Status.ObservedRolloutID
|
||||
if release.Spec.ReleasePlan.BatchPartition != nil && release.Spec.ReleasePlan.RolloutID == observedRolloutID {
|
||||
// ensure current batch upper bound
|
||||
currentBatch = integer.Int32Min(*release.Spec.ReleasePlan.BatchPartition, int32(len(release.Spec.ReleasePlan.Batches)-1))
|
||||
}
|
||||
|
||||
klog.Infof("BatchRelease(%v) canary batch changed from %v to %v when the release plan changed, observed-rollout-id: %s, current-rollout-id: %s",
|
||||
client.ObjectKeyFromObject(release), newStatus.CanaryStatus.CurrentBatch, currentBatch, observedRolloutID, release.Spec.ReleasePlan.RolloutID)
|
||||
newStatus.CanaryStatus.BatchReadyTime = nil
|
||||
newStatus.CanaryStatus.CurrentBatch = currentBatch
|
||||
newStatus.ObservedRolloutID = release.Spec.ReleasePlan.RolloutID
|
||||
newStatus.CanaryStatus.CurrentBatchState = v1alpha1.UpgradingBatchState
|
||||
newStatus.ObservedReleasePlanHash = util.HashReleasePlanBatches(&release.Spec.ReleasePlan)
|
||||
}
|
||||
|
||||
func resetStatus(status *v1alpha1.BatchReleaseStatus) {
|
||||
status.Phase = v1alpha1.RolloutPhaseInitial
|
||||
status.StableRevision = ""
|
||||
status.UpdateRevision = ""
|
||||
status.ObservedReleasePlanHash = ""
|
||||
status.ObservedWorkloadReplicas = -1
|
||||
status.CanaryStatus = v1alpha1.BatchReleaseCanaryStatus{}
|
||||
}
|
||||
|
||||
func setCondition(status *v1alpha1.BatchReleaseStatus, condType v1alpha1.RolloutConditionType, condStatus v1.ConditionStatus, reason, message string) {
|
||||
if status == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(status.Conditions) == 0 {
|
||||
status.Conditions = append(status.Conditions, v1alpha1.RolloutCondition{
|
||||
Type: condType,
|
||||
Status: condStatus,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
LastUpdateTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
condition := &status.Conditions[0]
|
||||
isConditionChanged := func() bool {
|
||||
return condition.Type != condType || condition.Status != condStatus || condition.Reason != reason || condition.Message != message
|
||||
}
|
||||
|
||||
if isConditionChanged() {
|
||||
condition.Type = condType
|
||||
condition.Reason = reason
|
||||
condition.Message = message
|
||||
condition.LastUpdateTime = metav1.Now()
|
||||
if condition.Status != condStatus {
|
||||
condition.LastTransitionTime = metav1.Now()
|
||||
}
|
||||
condition.Status = condStatus
|
||||
}
|
||||
}
|
||||
|
||||
func IsPartitioned(release *v1alpha1.BatchRelease) bool {
|
||||
return release.Spec.ReleasePlan.BatchPartition != nil && *release.Spec.ReleasePlan.BatchPartition <= release.Status.CanaryStatus.CurrentBatch
|
||||
}
|
||||
|
||||
func IsAllBatchReady(release *v1alpha1.BatchRelease) bool {
|
||||
return len(release.Spec.ReleasePlan.Batches)-1 == int(release.Status.CanaryStatus.CurrentBatch) && release.Status.CanaryStatus.CurrentBatchState == v1alpha1.ReadyBatchState
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
type BatchContext struct {
|
||||
RolloutID string `json:"rolloutID,omitempty"`
|
||||
// current batch index, start from 0
|
||||
CurrentBatch int32 `json:"currentBatchIndex"`
|
||||
// workload update revision
|
||||
UpdateRevision string `json:"updateRevision,omitempty"`
|
||||
|
||||
// workload replicas
|
||||
Replicas int32 `json:"replicas"`
|
||||
// Updated replicas
|
||||
UpdatedReplicas int32 `json:"updatedReplicas"`
|
||||
// Updated ready replicas
|
||||
UpdatedReadyReplicas int32 `json:"updatedReadyReplicas"`
|
||||
// no need update replicas that marked before rollout
|
||||
NoNeedUpdatedReplicas *int32 `json:"noNeedUpdatedReplicas,omitempty"`
|
||||
// the planned number of Pods should be upgrade in current batch
|
||||
// this field corresponds to releasePlan.Batches[currentBatch]
|
||||
PlannedUpdatedReplicas int32 `json:"plannedUpdatedReplicas,omitempty"`
|
||||
// the total number of the really updated pods you desired in current batch.
|
||||
// In most normal cases, this field will equal to PlannedUpdatedReplicas,
|
||||
// but in some scene, e.g., rolling back in batches, the really desired updated
|
||||
// replicas will not equal to planned update replicas, because we just roll the
|
||||
// pods that really need update back in batches.
|
||||
DesiredUpdatedReplicas int32 `json:"desiredUpdatedReplicas,omitempty"`
|
||||
// workload current partition
|
||||
CurrentPartition intstr.IntOrString `json:"currentPartition,omitempty"`
|
||||
// desired partition replicas in current batch
|
||||
DesiredPartition intstr.IntOrString `json:"desiredPartition,omitempty"`
|
||||
// failureThreshold to tolerate unready updated replicas;
|
||||
FailureThreshold *intstr.IntOrString `json:"failureThreshold,omitempty"`
|
||||
|
||||
// the pods owned by workload
|
||||
Pods []*corev1.Pod `json:"-"`
|
||||
// filter or sort pods before patch label
|
||||
FilterFunc FilterFuncType `json:"-"`
|
||||
}
|
||||
|
||||
type FilterFuncType func(pods []*corev1.Pod, ctx *BatchContext) []*corev1.Pod
|
||||
|
||||
func (bc *BatchContext) Log() string {
|
||||
marshal, _ := json.Marshal(bc)
|
||||
return fmt.Sprintf("%s with %d pods", string(marshal), len(bc.Pods))
|
||||
}
|
||||
|
||||
// IsBatchReady return nil if the batch is ready
|
||||
func (bc *BatchContext) IsBatchReady() error {
|
||||
if bc.UpdatedReplicas < bc.DesiredUpdatedReplicas {
|
||||
return fmt.Errorf("current batch not ready: updated replicas not satified")
|
||||
}
|
||||
|
||||
unavailableToleration := allowedUnavailable(bc.FailureThreshold, bc.UpdatedReplicas)
|
||||
if unavailableToleration+bc.UpdatedReadyReplicas < bc.DesiredUpdatedReplicas {
|
||||
return fmt.Errorf("current batch not ready: updated ready replicas not satified")
|
||||
}
|
||||
|
||||
if bc.DesiredUpdatedReplicas > 0 && bc.UpdatedReadyReplicas == 0 {
|
||||
return fmt.Errorf("current batch not ready: no updated ready replicas")
|
||||
}
|
||||
|
||||
if !batchLabelSatisfied(bc.Pods, bc.RolloutID, bc.PlannedUpdatedReplicas) {
|
||||
return fmt.Errorf("current batch not ready: pods with batch label not satified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// batchLabelSatisfied return true if the expected batch label has been patched
|
||||
func batchLabelSatisfied(pods []*corev1.Pod, rolloutID string, targetCount int32) bool {
|
||||
if rolloutID == "" || len(pods) == 0 {
|
||||
return true
|
||||
}
|
||||
patchedCount := util.WrappedPodCount(pods, func(pod *corev1.Pod) bool {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
return false
|
||||
}
|
||||
return pod.Labels[util.RolloutIDLabel] == rolloutID
|
||||
})
|
||||
return patchedCount >= int(targetCount)
|
||||
}
|
||||
|
||||
// allowedUnavailable return absolute number of failure threshold
|
||||
func allowedUnavailable(threshold *intstr.IntOrString, replicas int32) int32 {
|
||||
failureThreshold := 0
|
||||
if threshold != nil {
|
||||
failureThreshold, _ = intstr.GetScaledValueFromIntOrPercent(threshold, int(replicas), true)
|
||||
}
|
||||
return int32(failureThreshold)
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func TestIsBatchReady(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
p := func(f intstr.IntOrString) *intstr.IntOrString {
|
||||
return &f
|
||||
}
|
||||
r := func(f *intstr.IntOrString, id, revision string) *v1alpha1.BatchRelease {
|
||||
return &v1alpha1.BatchRelease{
|
||||
Spec: v1alpha1.BatchReleaseSpec{ReleasePlan: v1alpha1.ReleasePlan{RolloutID: id, FailureThreshold: f}},
|
||||
Status: v1alpha1.BatchReleaseStatus{UpdateRevision: revision},
|
||||
}
|
||||
}
|
||||
cases := map[string]struct {
|
||||
release *v1alpha1.BatchRelease
|
||||
pods []*corev1.Pod
|
||||
maxUnavailable *intstr.IntOrString
|
||||
labelDesired int32
|
||||
desired int32
|
||||
updated int32
|
||||
updatedReady int32
|
||||
isReady bool
|
||||
}{
|
||||
"ready: no-rollout-id, all pod ready": {
|
||||
release: r(p(intstr.FromInt(1)), "", "v2"),
|
||||
pods: nil,
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 5,
|
||||
isReady: true,
|
||||
},
|
||||
"ready: no-rollout-id, tolerated failed pods": {
|
||||
release: r(p(intstr.FromInt(1)), "", "v2"),
|
||||
pods: nil,
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 4,
|
||||
isReady: true,
|
||||
},
|
||||
"false: no-rollout-id, un-tolerated failed pods": {
|
||||
release: r(p(intstr.FromInt(1)), "", "v2"),
|
||||
pods: nil,
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 3,
|
||||
isReady: false,
|
||||
},
|
||||
"false: no-rollout-id, tolerated failed pods, but 1 pod isn't updated": {
|
||||
release: r(p(intstr.FromString("60%")), "", "v2"),
|
||||
pods: nil,
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 4,
|
||||
updatedReady: 4,
|
||||
isReady: false,
|
||||
},
|
||||
"false: no-rollout-id, tolerated, but no-pod-ready": {
|
||||
release: r(p(intstr.FromInt(100)), "", "v2"),
|
||||
pods: nil,
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 0,
|
||||
isReady: false,
|
||||
},
|
||||
"false: no-rollout-id, un-tolerated failed pods, failureThreshold=nil": {
|
||||
release: r(nil, "", "v2"),
|
||||
pods: nil,
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 3,
|
||||
isReady: false,
|
||||
},
|
||||
"true: rollout-id, labeled pods satisfied": {
|
||||
release: r(p(intstr.FromInt(1)), "1", "version-1"),
|
||||
pods: generatePods(5, 0),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 5,
|
||||
isReady: true,
|
||||
},
|
||||
"false: rollout-id, labeled pods not satisfied": {
|
||||
release: r(p(intstr.FromInt(1)), "1", "version-1"),
|
||||
pods: generatePods(3, 0),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 5,
|
||||
isReady: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := BatchContext{
|
||||
Pods: cs.pods,
|
||||
PlannedUpdatedReplicas: cs.labelDesired,
|
||||
DesiredUpdatedReplicas: cs.desired,
|
||||
UpdatedReplicas: cs.updated,
|
||||
UpdatedReadyReplicas: cs.updatedReady,
|
||||
UpdateRevision: cs.release.Status.UpdateRevision,
|
||||
RolloutID: cs.release.Spec.ReleasePlan.RolloutID,
|
||||
FailureThreshold: cs.release.Spec.ReleasePlan.FailureThreshold,
|
||||
}
|
||||
err := ctx.IsBatchReady()
|
||||
if cs.isReady {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
} else {
|
||||
Expect(err).To(HaveOccurred())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generatePods(updatedReplicas, noNeedRollbackReplicas int) []*corev1.Pod {
|
||||
podsNoNeed := generatePodsWith(map[string]string{
|
||||
util.NoNeedUpdatePodLabel: "0x1",
|
||||
util.RolloutIDLabel: "1",
|
||||
apps.ControllerRevisionHashLabelKey: "version-1",
|
||||
}, noNeedRollbackReplicas, 0)
|
||||
return append(generatePodsWith(map[string]string{
|
||||
util.RolloutIDLabel: "1",
|
||||
apps.ControllerRevisionHashLabelKey: "version-1",
|
||||
}, updatedReplicas-noNeedRollbackReplicas, noNeedRollbackReplicas), podsNoNeed...)
|
||||
}
|
||||
|
||||
func generatePodsWith(labels map[string]string, replicas int, beginOrder int) []*corev1.Pod {
|
||||
pods := make([]*corev1.Pod, replicas)
|
||||
for i := 0; i < replicas; i++ {
|
||||
pods[i] = &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("pod-name-%d", beginOrder+i),
|
||||
Labels: labels,
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Conditions: []corev1.PodCondition{
|
||||
{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionTrue,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return pods
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 canarystyle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/labelpatch"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// CloneSetRolloutController is responsible for handling rollout CloneSet type of workloads
|
||||
type realCanaryController struct {
|
||||
Interface
|
||||
client.Client
|
||||
record.EventRecorder
|
||||
patcher labelpatch.LabelPatcher
|
||||
release *v1alpha1.BatchRelease
|
||||
newStatus *v1alpha1.BatchReleaseStatus
|
||||
}
|
||||
|
||||
type NewInterfaceFunc func(cli client.Client, key types.NamespacedName) Interface
|
||||
|
||||
// NewControlPlane creates a new release controller to drive batch release state machine
|
||||
func NewControlPlane(f NewInterfaceFunc, cli client.Client, recorder record.EventRecorder, release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, key types.NamespacedName) *realCanaryController {
|
||||
return &realCanaryController{
|
||||
Client: cli,
|
||||
EventRecorder: recorder,
|
||||
newStatus: newStatus,
|
||||
Interface: f(cli, key),
|
||||
release: release.DeepCopy(),
|
||||
patcher: labelpatch.NewLabelPatcher(cli, klog.KObj(release)),
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *realCanaryController) Initialize() error {
|
||||
stable, err := rc.BuildStableController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = stable.Initialize(rc.release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
canary, err := rc.BuildCanaryController(rc.release)
|
||||
if client.IgnoreNotFound(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = canary.Create(rc.release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// record revision and replicas
|
||||
stableInfo := stable.GetStableInfo()
|
||||
canaryInfo := canary.GetCanaryInfo()
|
||||
rc.newStatus.ObservedWorkloadReplicas = stableInfo.Replicas
|
||||
rc.newStatus.StableRevision = stableInfo.Status.StableRevision
|
||||
rc.newStatus.UpdateRevision = canaryInfo.Status.UpdateRevision
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *realCanaryController) UpgradeBatch() error {
|
||||
stable, err := rc.BuildStableController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stable.GetStableInfo().Replicas == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
canary, err := rc.BuildCanaryController(rc.release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !canary.GetCanaryInfo().IsStable() {
|
||||
return fmt.Errorf("wait canary workload %v reconcile", canary.GetCanaryInfo().LogKey)
|
||||
}
|
||||
|
||||
batchContext := rc.CalculateBatchContext(rc.release)
|
||||
klog.Infof("BatchRelease %v upgrade batch: %s", klog.KObj(rc.release), batchContext.Log())
|
||||
|
||||
return canary.UpgradeBatch(batchContext)
|
||||
}
|
||||
|
||||
func (rc *realCanaryController) CheckBatchReady() error {
|
||||
stable, err := rc.BuildStableController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stable.GetStableInfo().Replicas == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
canary, err := rc.BuildCanaryController(rc.release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !canary.GetCanaryInfo().IsStable() {
|
||||
return fmt.Errorf("wait canary workload %v reconcile", canary.GetCanaryInfo().LogKey)
|
||||
}
|
||||
|
||||
batchContext := rc.CalculateBatchContext(rc.release)
|
||||
klog.Infof("BatchRelease %v check batch: %s", klog.KObj(rc.release), batchContext.Log())
|
||||
|
||||
return batchContext.IsBatchReady()
|
||||
}
|
||||
|
||||
func (rc *realCanaryController) Finalize() error {
|
||||
stable, err := rc.BuildStableController()
|
||||
if client.IgnoreNotFound(err) != nil {
|
||||
klog.Errorf("BatchRelease %v build stable controller err: %v", klog.KObj(rc.release), err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = stable.Finalize(rc.release)
|
||||
if err != nil {
|
||||
klog.Errorf("BatchRelease %v finalize stable err: %v", klog.KObj(rc.release), err)
|
||||
return err
|
||||
}
|
||||
|
||||
canary, err := rc.BuildCanaryController(rc.release)
|
||||
if client.IgnoreNotFound(err) != nil {
|
||||
klog.Errorf("BatchRelease %v build canary controller err: %v", klog.KObj(rc.release), err)
|
||||
return err
|
||||
}
|
||||
err = canary.Delete(rc.release)
|
||||
if err != nil {
|
||||
klog.Errorf("BatchRelease %v delete canary workload err: %v", klog.KObj(rc.release), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (rc *realCanaryController) SyncWorkloadInformation() (control.WorkloadEventType, *util.WorkloadInfo, error) {
|
||||
// ignore the sync if the release plan is deleted
|
||||
if rc.release.DeletionTimestamp != nil {
|
||||
return control.WorkloadNormalState, nil, nil
|
||||
}
|
||||
|
||||
stable, err := rc.BuildStableController()
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return control.WorkloadHasGone, nil, err
|
||||
}
|
||||
return control.WorkloadUnknownState, nil, err
|
||||
}
|
||||
|
||||
canary, err := rc.BuildCanaryController(rc.release)
|
||||
if client.IgnoreNotFound(err) != nil {
|
||||
return control.WorkloadUnknownState, nil, err
|
||||
}
|
||||
|
||||
syncInfo := &util.WorkloadInfo{}
|
||||
stableInfo, canaryInfo := stable.GetStableInfo(), canary.GetCanaryInfo()
|
||||
if canaryInfo != nil {
|
||||
syncInfo.Status.UpdatedReplicas = canaryInfo.Status.Replicas
|
||||
syncInfo.Status.UpdatedReadyReplicas = canaryInfo.Status.AvailableReplicas
|
||||
}
|
||||
|
||||
if !stableInfo.IsStable() {
|
||||
klog.Warningf("Workload(%v) is still reconciling, generation: %v, observed: %v",
|
||||
stableInfo.LogKey, stableInfo.Generation, stableInfo.Status.ObservedGeneration)
|
||||
return control.WorkloadStillReconciling, syncInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload has been promoted
|
||||
if stableInfo.IsPromoted() {
|
||||
return control.WorkloadNormalState, syncInfo, nil
|
||||
}
|
||||
|
||||
if stableInfo.IsScaling(rc.newStatus.ObservedWorkloadReplicas) {
|
||||
syncInfo.Replicas = stableInfo.Replicas
|
||||
klog.Warningf("Workload(%v) replicas is modified, replicas from: %v to -> %v",
|
||||
stableInfo.LogKey, rc.newStatus.ObservedWorkloadReplicas, stableInfo.Replicas)
|
||||
return control.WorkloadReplicasChanged, syncInfo, nil
|
||||
}
|
||||
|
||||
if stableInfo.IsRevisionNotEqual(rc.newStatus.UpdateRevision) {
|
||||
syncInfo.Status.UpdateRevision = stableInfo.Status.UpdateRevision
|
||||
klog.Warningf("Workload(%v) updateRevision is modified, updateRevision from: %v to -> %v",
|
||||
stableInfo.LogKey, rc.newStatus.UpdateRevision, stableInfo.Status.UpdateRevision)
|
||||
return control.WorkloadPodTemplateChanged, syncInfo, nil
|
||||
}
|
||||
return control.WorkloadUnknownState, syncInfo, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
utilclient "github.com/openkruise/rollouts/pkg/util/client"
|
||||
expectations "github.com/openkruise/rollouts/pkg/util/expectation"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
type realCanaryController struct {
|
||||
canaryInfo *util.WorkloadInfo
|
||||
canaryObject *apps.Deployment
|
||||
canaryClient client.Client
|
||||
objectKey types.NamespacedName
|
||||
}
|
||||
|
||||
func newCanary(cli client.Client, key types.NamespacedName) realCanaryController {
|
||||
return realCanaryController{canaryClient: cli, objectKey: key}
|
||||
}
|
||||
|
||||
func (r *realCanaryController) GetCanaryInfo() *util.WorkloadInfo {
|
||||
return r.canaryInfo
|
||||
}
|
||||
|
||||
// Delete do not delete canary deployments actually, it only removes the finalizers of
|
||||
// Deployments. These deployments will be cascaded deleted when BatchRelease is deleted.
|
||||
func (r *realCanaryController) Delete(release *v1alpha1.BatchRelease) error {
|
||||
deployments, err := r.listDeployment(release, client.InNamespace(r.objectKey.Namespace), utilclient.DisableDeepCopy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range deployments {
|
||||
if !controllerutil.ContainsFinalizer(d, util.CanaryDeploymentFinalizer) {
|
||||
continue
|
||||
}
|
||||
err = util.UpdateFinalizer(r.canaryClient, d, util.RemoveFinalizerOpType, util.CanaryDeploymentFinalizer)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
klog.Infof("Successfully remove finalizers for Deplot %v", klog.KObj(d))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *realCanaryController) UpgradeBatch(ctx *batchcontext.BatchContext) error {
|
||||
// desired replicas for canary deployment
|
||||
desired := ctx.DesiredUpdatedReplicas
|
||||
deployment := util.GetEmptyObjectWithKey(r.canaryObject)
|
||||
|
||||
if r.canaryInfo.Replicas >= desired {
|
||||
return nil
|
||||
}
|
||||
|
||||
body := fmt.Sprintf(`{"spec":{"replicas":%d}}`, desired)
|
||||
if err := r.canaryClient.Patch(context.TODO(), deployment, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return err
|
||||
}
|
||||
klog.Infof("Successfully submit rolling replicas %d to Deployment %v", desired, klog.KObj(deployment))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *realCanaryController) Create(release *v1alpha1.BatchRelease) error {
|
||||
if r.canaryObject != nil {
|
||||
return nil // Don't re-create if exists
|
||||
}
|
||||
|
||||
// check expectation before creating canary deployment to avoid
|
||||
// repeatedly create multiple canary deployment incorrectly.
|
||||
controllerKey := client.ObjectKeyFromObject(release).String()
|
||||
satisfied, timeoutDuration, rest := expectations.ResourceExpectations.SatisfiedExpectations(controllerKey)
|
||||
if !satisfied {
|
||||
if timeoutDuration >= expectations.ExpectationTimeout {
|
||||
klog.Warningf("Unsatisfied time of expectation exceeds %v, delete key and continue, key: %v, rest: %v",
|
||||
expectations.ExpectationTimeout, klog.KObj(release), rest)
|
||||
expectations.ResourceExpectations.DeleteExpectations(controllerKey)
|
||||
} else {
|
||||
return fmt.Errorf("expectation is not satisfied, key: %v, rest: %v", klog.KObj(release), rest)
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the stable deployment as template to create canary deployment.
|
||||
stable := &apps.Deployment{}
|
||||
if err := r.canaryClient.Get(context.TODO(), r.objectKey, stable); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.create(release, stable)
|
||||
}
|
||||
func (r *realCanaryController) create(release *v1alpha1.BatchRelease, template *apps.Deployment) error {
|
||||
canary := &apps.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: fmt.Sprintf("%v-", r.objectKey.Name),
|
||||
Namespace: r.objectKey.Namespace,
|
||||
Labels: map[string]string{},
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
// metadata
|
||||
canary.Finalizers = append(canary.Finalizers, util.CanaryDeploymentFinalizer)
|
||||
canary.OwnerReferences = append(canary.OwnerReferences, *metav1.NewControllerRef(release, release.GroupVersionKind()))
|
||||
canary.Labels[util.CanaryDeploymentLabel] = template.Name
|
||||
ownerInfo, _ := json.Marshal(metav1.NewControllerRef(release, release.GroupVersionKind()))
|
||||
canary.Annotations[util.BatchReleaseControlAnnotation] = string(ownerInfo)
|
||||
|
||||
// spec
|
||||
canary.Spec = *template.Spec.DeepCopy()
|
||||
canary.Spec.Replicas = pointer.Int32Ptr(0)
|
||||
canary.Spec.Paused = false
|
||||
|
||||
if err := r.canaryClient.Create(context.TODO(), canary); err != nil {
|
||||
klog.Errorf("Failed to create canary Deployment(%v), error: %v", klog.KObj(canary), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// add expect to avoid to create repeatedly
|
||||
controllerKey := client.ObjectKeyFromObject(release).String()
|
||||
expectations.ResourceExpectations.Expect(controllerKey, expectations.Create, string(canary.UID))
|
||||
|
||||
canaryInfo, _ := json.Marshal(canary)
|
||||
klog.Infof("Create canary Deployment(%v) successfully, details: %s", klog.KObj(canary), string(canaryInfo))
|
||||
return fmt.Errorf("created canary deployment %v succeeded, but waiting informer synced", klog.KObj(canary))
|
||||
}
|
||||
|
||||
func (r *realCanaryController) listDeployment(release *v1alpha1.BatchRelease, options ...client.ListOption) ([]*apps.Deployment, error) {
|
||||
dList := &apps.DeploymentList{}
|
||||
if err := r.canaryClient.List(context.TODO(), dList, options...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ds []*apps.Deployment
|
||||
for i := range dList.Items {
|
||||
d := &dList.Items[i]
|
||||
o := metav1.GetControllerOf(d)
|
||||
if o == nil || o.UID != release.UID {
|
||||
continue
|
||||
}
|
||||
ds = append(ds, d)
|
||||
}
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// return the latest deployment with the newer creation time
|
||||
func filterCanaryDeployment(ds []*apps.Deployment, template *corev1.PodTemplateSpec) *apps.Deployment {
|
||||
if len(ds) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Slice(ds, func(i, j int) bool {
|
||||
return ds[i].CreationTimestamp.After(ds[j].CreationTimestamp.Time)
|
||||
})
|
||||
if template == nil {
|
||||
return ds[0]
|
||||
}
|
||||
for _, d := range ds {
|
||||
if util.EqualIgnoreHash(template, &d.Spec.Template) {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control/canarystyle"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
utilclient "github.com/openkruise/rollouts/pkg/util/client"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type realController struct {
|
||||
realStableController
|
||||
realCanaryController
|
||||
}
|
||||
|
||||
func NewController(cli client.Client, key types.NamespacedName) canarystyle.Interface {
|
||||
return &realController{
|
||||
realStableController: newStable(cli, key),
|
||||
realCanaryController: newCanary(cli, key),
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *realController) BuildStableController() (canarystyle.StableInterface, error) {
|
||||
if rc.stableObject != nil {
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
object := &apps.Deployment{}
|
||||
err := rc.stableClient.Get(context.TODO(), rc.stableKey, object)
|
||||
if err != nil {
|
||||
return rc, err
|
||||
}
|
||||
rc.stableObject = object
|
||||
rc.stableInfo = util.ParseWorkload(object)
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (rc *realController) BuildCanaryController(release *v1alpha1.BatchRelease) (canarystyle.CanaryInterface, error) {
|
||||
if rc.canaryObject != nil {
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
ds, err := rc.listDeployment(release, client.InNamespace(rc.stableKey.Namespace), utilclient.DisableDeepCopy)
|
||||
if err != nil {
|
||||
return rc, err
|
||||
}
|
||||
|
||||
template, err := rc.getLatestTemplate()
|
||||
if client.IgnoreNotFound(err) != nil {
|
||||
return rc, err
|
||||
}
|
||||
|
||||
rc.canaryObject = filterCanaryDeployment(util.FilterActiveDeployment(ds), template)
|
||||
if rc.canaryObject == nil {
|
||||
return rc, control.GenerateNotFoundError(fmt.Sprintf("%v-canary", rc.stableKey), "Deployment")
|
||||
}
|
||||
|
||||
rc.canaryInfo = util.ParseWorkload(rc.canaryObject)
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (rc *realController) CalculateBatchContext(release *v1alpha1.BatchRelease) *batchcontext.BatchContext {
|
||||
replicas := *rc.stableObject.Spec.Replicas
|
||||
currentBatch := release.Status.CanaryStatus.CurrentBatch
|
||||
desiredUpdate := int32(control.CalculateBatchReplicas(release, int(replicas), int(currentBatch)))
|
||||
|
||||
return &batchcontext.BatchContext{
|
||||
Replicas: replicas,
|
||||
CurrentBatch: currentBatch,
|
||||
DesiredUpdatedReplicas: desiredUpdate,
|
||||
FailureThreshold: release.Spec.ReleasePlan.FailureThreshold,
|
||||
UpdatedReplicas: rc.canaryObject.Status.Replicas,
|
||||
UpdatedReadyReplicas: rc.canaryObject.Status.AvailableReplicas,
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *realController) getLatestTemplate() (*v1.PodTemplateSpec, error) {
|
||||
_, err := rc.BuildStableController()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rc.stableObject.Spec.Template, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
expectations "github.com/openkruise/rollouts/pkg/util/expectation"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
|
||||
deploymentKey = types.NamespacedName{
|
||||
Name: "deployment",
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
deploymentDemo = &apps.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: deploymentKey.Name,
|
||||
Namespace: deploymentKey.Namespace,
|
||||
Generation: 1,
|
||||
Labels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"type": "unit-test",
|
||||
},
|
||||
},
|
||||
Spec: apps.DeploymentSpec{
|
||||
Paused: true,
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
},
|
||||
Replicas: pointer.Int32(10),
|
||||
Strategy: apps.DeploymentStrategy{
|
||||
Type: apps.RollingUpdateDeploymentStrategyType,
|
||||
RollingUpdate: &apps.RollingUpdateDeployment{
|
||||
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
|
||||
},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: apps.DeploymentStatus{
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 10,
|
||||
ReadyReplicas: 10,
|
||||
AvailableReplicas: 10,
|
||||
CollisionCount: pointer.Int32Ptr(1),
|
||||
ObservedGeneration: 1,
|
||||
},
|
||||
}
|
||||
|
||||
releaseDemo = &v1alpha1.BatchRelease{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rollouts.kruise.io/v1alpha1",
|
||||
Kind: "BatchRelease",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "release",
|
||||
Namespace: deploymentKey.Namespace,
|
||||
UID: uuid.NewUUID(),
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
FinalizingPolicy: v1alpha1.WaitResumeFinalizingPolicyType,
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("10%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("50%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("100%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
TargetRef: v1alpha1.ObjectRef{
|
||||
WorkloadRef: &v1alpha1.WorkloadRef{
|
||||
APIVersion: deploymentDemo.APIVersion,
|
||||
Kind: deploymentDemo.Kind,
|
||||
Name: deploymentDemo.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
apps.AddToScheme(scheme)
|
||||
v1alpha1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
func TestCalculateBatchContext(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
percent := intstr.FromString("20%")
|
||||
cases := map[string]struct {
|
||||
workload func() (*apps.Deployment, *apps.Deployment)
|
||||
release func() *v1alpha1.BatchRelease
|
||||
result *batchcontext.BatchContext
|
||||
}{
|
||||
"normal case": {
|
||||
workload: func() (*apps.Deployment, *apps.Deployment) {
|
||||
stable := &apps.Deployment{
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: pointer.Int32Ptr(10),
|
||||
},
|
||||
Status: apps.DeploymentStatus{
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 0,
|
||||
AvailableReplicas: 10,
|
||||
},
|
||||
}
|
||||
canary := &apps.Deployment{
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: pointer.Int32Ptr(5),
|
||||
},
|
||||
Status: apps.DeploymentStatus{
|
||||
Replicas: 5,
|
||||
UpdatedReplicas: 5,
|
||||
AvailableReplicas: 5,
|
||||
},
|
||||
}
|
||||
return stable, canary
|
||||
},
|
||||
release: func() *v1alpha1.BatchRelease {
|
||||
r := &v1alpha1.BatchRelease{
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
FailureThreshold: &percent,
|
||||
FinalizingPolicy: v1alpha1.WaitResumeFinalizingPolicyType,
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: percent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
return r
|
||||
},
|
||||
result: &batchcontext.BatchContext{
|
||||
FailureThreshold: &percent,
|
||||
CurrentBatch: 0,
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 5,
|
||||
UpdatedReadyReplicas: 5,
|
||||
DesiredUpdatedReplicas: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
stable, canary := cs.workload()
|
||||
control := realController{
|
||||
realStableController: realStableController{
|
||||
stableObject: stable,
|
||||
},
|
||||
realCanaryController: realCanaryController{
|
||||
canaryObject: canary,
|
||||
},
|
||||
}
|
||||
got := control.CalculateBatchContext(cs.release())
|
||||
Expect(reflect.DeepEqual(got, cs.result)).Should(BeTrue())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealStableController(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
release := releaseDemo.DeepCopy()
|
||||
deployment := deploymentDemo.DeepCopy()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(release, deployment).Build()
|
||||
c := NewController(cli, deploymentKey).(*realController)
|
||||
controller, err := c.BuildStableController()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = controller.Initialize(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
fetch := &apps.Deployment{}
|
||||
Expect(cli.Get(context.TODO(), deploymentKey, fetch)).NotTo(HaveOccurred())
|
||||
Expect(fetch.Annotations[util.BatchReleaseControlAnnotation]).Should(Equal(getControlInfo(release)))
|
||||
c.stableObject = fetch // mock
|
||||
|
||||
err = controller.Finalize(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
fetch = &apps.Deployment{}
|
||||
Expect(cli.Get(context.TODO(), deploymentKey, fetch)).NotTo(HaveOccurred())
|
||||
Expect(fetch.Annotations[util.BatchReleaseControlAnnotation]).Should(Equal(""))
|
||||
|
||||
stableInfo := controller.GetStableInfo()
|
||||
Expect(stableInfo).ShouldNot(BeNil())
|
||||
checkWorkloadInfo(stableInfo, deployment)
|
||||
}
|
||||
|
||||
func TestRealCanaryController(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
release := releaseDemo.DeepCopy()
|
||||
deployment := deploymentDemo.DeepCopy()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(release, deployment).Build()
|
||||
c := NewController(cli, deploymentKey).(*realController)
|
||||
controller, err := c.BuildCanaryController(release)
|
||||
Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred())
|
||||
|
||||
// check creation
|
||||
for {
|
||||
err = controller.Create(release)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
// mock create event handler
|
||||
controller, err = c.BuildCanaryController(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
if c.canaryObject != nil {
|
||||
controllerKey := client.ObjectKeyFromObject(release).String()
|
||||
resourceKey := client.ObjectKeyFromObject(c.canaryObject).String()
|
||||
expectations.ResourceExpectations.Observe(controllerKey, expectations.Create, resourceKey)
|
||||
}
|
||||
}
|
||||
Expect(metav1.IsControlledBy(c.canaryObject, release)).Should(BeTrue())
|
||||
Expect(controllerutil.ContainsFinalizer(c.canaryObject, util.CanaryDeploymentFinalizer)).Should(BeTrue())
|
||||
Expect(*c.canaryObject.Spec.Replicas).Should(BeNumerically("==", 0))
|
||||
Expect(util.EqualIgnoreHash(&c.canaryObject.Spec.Template, &deployment.Spec.Template)).Should(BeTrue())
|
||||
|
||||
// check rolling
|
||||
batchContext := c.CalculateBatchContext(release)
|
||||
err = controller.UpgradeBatch(batchContext)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
canary := getCanaryDeployment(release, deployment, c)
|
||||
Expect(canary).ShouldNot(BeNil())
|
||||
Expect(*canary.Spec.Replicas).Should(BeNumerically("==", batchContext.DesiredUpdatedReplicas))
|
||||
|
||||
// check deletion
|
||||
for {
|
||||
err = controller.Delete(release)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
d := getCanaryDeployment(release, deployment, c)
|
||||
Expect(d).NotTo(BeNil())
|
||||
Expect(len(d.Finalizers)).Should(Equal(0))
|
||||
}
|
||||
|
||||
func getCanaryDeployment(release *v1alpha1.BatchRelease, stable *apps.Deployment, c *realController) *apps.Deployment {
|
||||
ds, err := c.listDeployment(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
if len(ds) == 0 {
|
||||
return nil
|
||||
}
|
||||
return filterCanaryDeployment(ds, &stable.Spec.Template)
|
||||
}
|
||||
|
||||
func checkWorkloadInfo(stableInfo *util.WorkloadInfo, deployment *apps.Deployment) {
|
||||
Expect(stableInfo.Replicas).Should(Equal(*deployment.Spec.Replicas))
|
||||
Expect(stableInfo.Status.Replicas).Should(Equal(deployment.Status.Replicas))
|
||||
Expect(stableInfo.Status.ReadyReplicas).Should(Equal(deployment.Status.ReadyReplicas))
|
||||
Expect(stableInfo.Status.UpdatedReplicas).Should(Equal(deployment.Status.UpdatedReplicas))
|
||||
Expect(stableInfo.Status.AvailableReplicas).Should(Equal(deployment.Status.AvailableReplicas))
|
||||
Expect(stableInfo.Status.ObservedGeneration).Should(Equal(deployment.Status.ObservedGeneration))
|
||||
}
|
||||
|
||||
func getControlInfo(release *v1alpha1.BatchRelease) string {
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(release, release.GetObjectKind().GroupVersionKind()))
|
||||
return string(owner)
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type realStableController struct {
|
||||
stableInfo *util.WorkloadInfo
|
||||
stableObject *apps.Deployment
|
||||
stableClient client.Client
|
||||
stableKey types.NamespacedName
|
||||
}
|
||||
|
||||
func newStable(cli client.Client, key types.NamespacedName) realStableController {
|
||||
return realStableController{stableClient: cli, stableKey: key}
|
||||
}
|
||||
|
||||
func (rc *realStableController) GetStableInfo() *util.WorkloadInfo {
|
||||
return rc.stableInfo
|
||||
}
|
||||
|
||||
func (rc *realStableController) Initialize(release *v1alpha1.BatchRelease) error {
|
||||
if control.IsControlledByBatchRelease(release, rc.stableObject) {
|
||||
return nil
|
||||
}
|
||||
|
||||
d := util.GetEmptyObjectWithKey(rc.stableObject)
|
||||
owner := control.BuildReleaseControlInfo(release)
|
||||
|
||||
body := fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"}}}`, util.BatchReleaseControlAnnotation, owner)
|
||||
if err := rc.stableClient.Patch(context.TODO(), d, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return err
|
||||
}
|
||||
klog.Infof("Successfully claim Deployment %v", klog.KObj(rc.stableObject))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *realStableController) Finalize(release *v1alpha1.BatchRelease) (err error) {
|
||||
if rc.stableObject == nil {
|
||||
return nil // no need to process deleted object
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
klog.Infof("Successfully finalize Deployment %v", klog.KObj(rc.stableObject))
|
||||
}
|
||||
}()
|
||||
|
||||
// if batchPartition == nil, workload should be promoted;
|
||||
pause := release.Spec.ReleasePlan.BatchPartition != nil
|
||||
body := fmt.Sprintf(`{"metadata":{"annotations":{"%s":null}},"spec":{"paused":%v}}`,
|
||||
util.BatchReleaseControlAnnotation, pause)
|
||||
|
||||
d := util.GetEmptyObjectWithKey(rc.stableObject)
|
||||
if err = rc.stableClient.Patch(context.TODO(), d, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return
|
||||
}
|
||||
if control.ShouldWaitResume(release) {
|
||||
err = waitAllUpdatedAndReady(d.(*apps.Deployment))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func waitAllUpdatedAndReady(deployment *apps.Deployment) error {
|
||||
if deployment.Spec.Paused {
|
||||
return fmt.Errorf("promote error: deployment should not be paused")
|
||||
}
|
||||
|
||||
createdReplicas := deployment.Status.Replicas
|
||||
updatedReplicas := deployment.Status.UpdatedReplicas
|
||||
if createdReplicas != updatedReplicas {
|
||||
return fmt.Errorf("promote error: all replicas should be upgraded")
|
||||
}
|
||||
|
||||
availableReplicas := deployment.Status.AvailableReplicas
|
||||
allowedUnavailable := util.DeploymentMaxUnavailable(deployment)
|
||||
if allowedUnavailable+availableReplicas < createdReplicas {
|
||||
return fmt.Errorf("promote error: ready replicas should satisfy maxUnavailable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 canarystyle
|
||||
|
||||
import (
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
CanaryInterface
|
||||
StableInterface
|
||||
// BuildStableController will get stable workload object and parse
|
||||
// stable workload info, and return a controller for stable workload.
|
||||
BuildStableController() (StableInterface, error)
|
||||
// BuildCanaryController will get canary workload object and parse
|
||||
// canary workload info, and return a controller for canary workload.
|
||||
BuildCanaryController(release *v1alpha1.BatchRelease) (CanaryInterface, error)
|
||||
// CalculateBatchContext calculate the current batch context according to
|
||||
// our release plan and the statues of stable workload and canary workload.
|
||||
CalculateBatchContext(release *v1alpha1.BatchRelease) *batchcontext.BatchContext
|
||||
}
|
||||
|
||||
// CanaryInterface contains the methods about canary workload
|
||||
type CanaryInterface interface {
|
||||
// GetCanaryInfo return the information about canary workload
|
||||
GetCanaryInfo() *util.WorkloadInfo
|
||||
// UpgradeBatch upgrade canary workload according to current batch context
|
||||
UpgradeBatch(*batchcontext.BatchContext) error
|
||||
// Create creates canary workload before rolling out
|
||||
Create(controller *v1alpha1.BatchRelease) error
|
||||
// Delete deletes canary workload after rolling out
|
||||
Delete(controller *v1alpha1.BatchRelease) error
|
||||
}
|
||||
|
||||
// StableInterface contains the methods about stable workload
|
||||
type StableInterface interface {
|
||||
// GetStableInfo return the information about stable workload
|
||||
GetStableInfo() *util.WorkloadInfo
|
||||
// Initialize claim the stable workload is under rollout control
|
||||
Initialize(controller *v1alpha1.BatchRelease) error
|
||||
// Finalize do something after rolling out, for example:
|
||||
// - free the stable workload from rollout control;
|
||||
// - resume stable workload and wait all pods updated if we need.
|
||||
Finalize(controller *v1alpha1.BatchRelease) error
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 control
|
||||
|
||||
import (
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
)
|
||||
|
||||
type WorkloadEventType string
|
||||
|
||||
const (
|
||||
// WorkloadNormalState means workload is normal and event should be ignored.
|
||||
WorkloadNormalState WorkloadEventType = "workload-is-at-normal-state"
|
||||
// WorkloadUnknownState means workload state is unknown and should retry.
|
||||
WorkloadUnknownState WorkloadEventType = "workload-is-at-unknown-state"
|
||||
// WorkloadPodTemplateChanged means workload revision changed, should be stopped to execute batch release plan.
|
||||
WorkloadPodTemplateChanged WorkloadEventType = "workload-pod-template-changed"
|
||||
// WorkloadReplicasChanged means workload is scaling during rollout, should recalculate upgraded pods in current batch.
|
||||
WorkloadReplicasChanged WorkloadEventType = "workload-replicas-changed"
|
||||
// WorkloadStillReconciling means workload status is untrusted Untrustworthy, we should wait workload controller to reconcile.
|
||||
WorkloadStillReconciling WorkloadEventType = "workload-is-reconciling"
|
||||
// WorkloadHasGone means workload is deleted during rollout, we should do something finalizing works if this event occurs.
|
||||
WorkloadHasGone WorkloadEventType = "workload-has-gone"
|
||||
// WorkloadRollbackInBatch means workload is rollback according to BatchRelease batch plan.
|
||||
WorkloadRollbackInBatch WorkloadEventType = "workload-rollback-in-batch"
|
||||
)
|
||||
|
||||
// Interface is the interface that all type of control plane implements for rollout.
|
||||
type Interface interface {
|
||||
// Initialize make sure that the resource is ready to be progressed.
|
||||
// this function is tasked to do any initialization work on the resources.
|
||||
// it returns nil if the preparation is succeeded, else the preparation should retry.
|
||||
Initialize() error
|
||||
|
||||
// UpgradeBatch tries to upgrade old replicas according to the release plan.
|
||||
// it will upgrade the old replicas as the release plan allows in the current batch.
|
||||
// this function is tasked to do any initialization work on the resources.
|
||||
// it returns nil if the preparation is succeeded, else the preparation should retry.
|
||||
UpgradeBatch() error
|
||||
|
||||
// CheckBatchReady checks how many replicas are ready to serve requests in the current batch.
|
||||
// this function is tasked to do any initialization work on the resources.
|
||||
// it returns nil if the preparation is succeeded, else the preparation should retry.
|
||||
CheckBatchReady() error
|
||||
|
||||
// Finalize makes sure the resources are in a good final state.
|
||||
// this function is tasked to do any initialization work on the resources.
|
||||
// it returns nil if the preparation is succeeded, else the preparation should retry.
|
||||
Finalize() error
|
||||
|
||||
// SyncWorkloadInformation will watch and compare the status recorded in Status of BatchRelease
|
||||
// and the real-time workload information. If workload status is inconsistent with that recorded
|
||||
// in BatchRelease, it will return the corresponding WorkloadEventType and info.
|
||||
SyncWorkloadInformation() (WorkloadEventType, *util.WorkloadInfo, error)
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 cloneset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control/partitionstyle"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/labelpatch"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type realController struct {
|
||||
*util.WorkloadInfo
|
||||
client client.Client
|
||||
pods []*corev1.Pod
|
||||
key types.NamespacedName
|
||||
object *kruiseappsv1alpha1.CloneSet
|
||||
}
|
||||
|
||||
func NewController(cli client.Client, key types.NamespacedName, _ schema.GroupVersionKind) partitionstyle.Interface {
|
||||
return &realController{
|
||||
key: key,
|
||||
client: cli,
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *realController) GetInfo() *util.WorkloadInfo {
|
||||
return rc.WorkloadInfo
|
||||
}
|
||||
|
||||
func (rc *realController) BuildController() (partitionstyle.Interface, error) {
|
||||
if rc.object != nil {
|
||||
return rc, nil
|
||||
}
|
||||
object := &kruiseappsv1alpha1.CloneSet{}
|
||||
if err := rc.client.Get(context.TODO(), rc.key, object); err != nil {
|
||||
return rc, err
|
||||
}
|
||||
rc.object = object
|
||||
rc.WorkloadInfo = util.ParseWorkload(object)
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (rc *realController) ListOwnedPods() ([]*corev1.Pod, error) {
|
||||
if rc.pods != nil {
|
||||
return rc.pods, nil
|
||||
}
|
||||
var err error
|
||||
rc.pods, err = util.ListOwnedPods(rc.client, rc.object)
|
||||
return rc.pods, err
|
||||
}
|
||||
|
||||
func (rc *realController) Initialize(release *v1alpha1.BatchRelease) error {
|
||||
if control.IsControlledByBatchRelease(release, rc.object) {
|
||||
return nil
|
||||
}
|
||||
|
||||
clone := util.GetEmptyObjectWithKey(rc.object)
|
||||
owner := control.BuildReleaseControlInfo(release)
|
||||
body := fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"}},"spec":{"updateStrategy":{"paused":%v,"partition":"%s"}}}`,
|
||||
util.BatchReleaseControlAnnotation, owner, false, "100%")
|
||||
if err := rc.client.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return err
|
||||
}
|
||||
klog.Infof("Successfully initialized CloneSet %v", klog.KObj(clone))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *realController) UpgradeBatch(ctx *batchcontext.BatchContext) error {
|
||||
var body string
|
||||
var desired int
|
||||
switch partition := ctx.DesiredPartition; partition.Type {
|
||||
case intstr.Int:
|
||||
desired = int(partition.IntVal)
|
||||
body = fmt.Sprintf(`{"spec":{"updateStrategy":{"partition": %d }}}`, partition.IntValue())
|
||||
case intstr.String:
|
||||
desired, _ = intstr.GetScaledValueFromIntOrPercent(&partition, int(ctx.Replicas), true)
|
||||
body = fmt.Sprintf(`{"spec":{"updateStrategy":{"partition":"%s"}}}`, partition.String())
|
||||
}
|
||||
current, _ := intstr.GetScaledValueFromIntOrPercent(&ctx.CurrentPartition, int(ctx.Replicas), true)
|
||||
|
||||
// current less than desired, which means current revision replicas will be less than desired,
|
||||
// in other word, update revision replicas will be more than desired, no need to update again.
|
||||
if current <= desired {
|
||||
return nil
|
||||
}
|
||||
|
||||
clone := util.GetEmptyObjectWithKey(rc.object)
|
||||
if err := rc.client.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return err
|
||||
}
|
||||
klog.Infof("Successfully submit partition %v for CloneSet %v", ctx.DesiredPartition, klog.KObj(clone))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *realController) Finalize(release *v1alpha1.BatchRelease) error {
|
||||
if rc.object == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var specBody string
|
||||
// if batchPartition == nil, workload should be promoted.
|
||||
if release.Spec.ReleasePlan.BatchPartition == nil {
|
||||
specBody = `,"spec":{"updateStrategy":{"partition":null,"paused":false}}`
|
||||
}
|
||||
|
||||
body := fmt.Sprintf(`{"metadata":{"annotations":{"%s":null}}%s}`, util.BatchReleaseControlAnnotation, specBody)
|
||||
|
||||
clone := util.GetEmptyObjectWithKey(rc.object)
|
||||
if err := rc.client.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return err
|
||||
}
|
||||
klog.Infof("Successfully finalize StatefulSet %v", klog.KObj(rc.object))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *realController) CalculateBatchContext(release *v1alpha1.BatchRelease) (*batchcontext.BatchContext, error) {
|
||||
rolloutID := release.Spec.ReleasePlan.RolloutID
|
||||
if rolloutID != "" {
|
||||
// if rollout-id is set, the pod will be patched batch label,
|
||||
// so we have to list pod here.
|
||||
if _, err := rc.ListOwnedPods(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// current batch index
|
||||
currentBatch := release.Status.CanaryStatus.CurrentBatch
|
||||
// the number of no need update pods that marked before rollout
|
||||
noNeedUpdate := release.Status.CanaryStatus.NoNeedUpdateReplicas
|
||||
// the number of upgraded pods according to release plan in current batch.
|
||||
plannedUpdate := int32(control.CalculateBatchReplicas(release, int(rc.Replicas), int(currentBatch)))
|
||||
// the number of pods that should be upgraded in real
|
||||
desiredUpdate := plannedUpdate
|
||||
// the number of pods that should not be upgraded in real
|
||||
desiredStable := rc.Replicas - desiredUpdate
|
||||
// if we should consider the no-need-update pods that were marked before progressing
|
||||
if noNeedUpdate != nil && *noNeedUpdate > 0 {
|
||||
// specially, we should ignore the pods that were marked as no-need-update, this logic is for Rollback scene
|
||||
desiredUpdateNew := int32(control.CalculateBatchReplicas(release, int(rc.Replicas-*noNeedUpdate), int(currentBatch)))
|
||||
desiredStable = rc.Replicas - *noNeedUpdate - desiredUpdateNew
|
||||
desiredUpdate = rc.Replicas - desiredStable
|
||||
}
|
||||
|
||||
// make sure at least one pod is upgrade is canaryReplicas is not "0%"
|
||||
desiredPartition := intstr.FromInt(int(desiredStable))
|
||||
batchPlan := release.Spec.ReleasePlan.Batches[currentBatch].CanaryReplicas
|
||||
if batchPlan.Type == intstr.String {
|
||||
desiredPartition = control.ParseIntegerAsPercentageIfPossible(desiredStable, rc.Replicas, &batchPlan)
|
||||
}
|
||||
|
||||
currentPartition := intstr.FromInt(0)
|
||||
if rc.object.Spec.UpdateStrategy.Partition != nil {
|
||||
currentPartition = *rc.object.Spec.UpdateStrategy.Partition
|
||||
}
|
||||
|
||||
batchContext := &batchcontext.BatchContext{
|
||||
Pods: rc.pods,
|
||||
RolloutID: rolloutID,
|
||||
CurrentBatch: currentBatch,
|
||||
UpdateRevision: release.Status.UpdateRevision,
|
||||
DesiredPartition: desiredPartition,
|
||||
CurrentPartition: currentPartition,
|
||||
FailureThreshold: release.Spec.ReleasePlan.FailureThreshold,
|
||||
|
||||
Replicas: rc.Replicas,
|
||||
UpdatedReplicas: rc.Status.UpdatedReplicas,
|
||||
UpdatedReadyReplicas: rc.Status.UpdatedReadyReplicas,
|
||||
NoNeedUpdatedReplicas: noNeedUpdate,
|
||||
PlannedUpdatedReplicas: plannedUpdate,
|
||||
DesiredUpdatedReplicas: desiredUpdate,
|
||||
}
|
||||
|
||||
if noNeedUpdate != nil {
|
||||
batchContext.FilterFunc = labelpatch.FilterPodsForUnorderedUpdate
|
||||
}
|
||||
return batchContext, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 cloneset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/labelpatch"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
|
||||
cloneKey = types.NamespacedName{
|
||||
Namespace: "default",
|
||||
Name: "cloneset",
|
||||
}
|
||||
cloneDemo = &kruiseappsv1alpha1.CloneSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps.kruise.io/v1alpha1",
|
||||
Kind: "CloneSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cloneKey.Name,
|
||||
Namespace: cloneKey.Namespace,
|
||||
Generation: 1,
|
||||
Labels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"type": "unit-test",
|
||||
},
|
||||
},
|
||||
Spec: kruiseappsv1alpha1.CloneSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
},
|
||||
Replicas: pointer.Int32(10),
|
||||
UpdateStrategy: kruiseappsv1alpha1.CloneSetUpdateStrategy{
|
||||
Paused: true,
|
||||
Partition: &intstr.IntOrString{Type: intstr.String, StrVal: "100%"},
|
||||
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: kruiseappsv1alpha1.CloneSetStatus{
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 0,
|
||||
ReadyReplicas: 10,
|
||||
AvailableReplicas: 10,
|
||||
UpdatedReadyReplicas: 0,
|
||||
UpdateRevision: "version-2",
|
||||
CurrentRevision: "version-1",
|
||||
ObservedGeneration: 1,
|
||||
CollisionCount: pointer.Int32Ptr(1),
|
||||
},
|
||||
}
|
||||
|
||||
releaseDemo = &v1alpha1.BatchRelease{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rollouts.kruise.io/v1alpha1",
|
||||
Kind: "BatchRelease",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "release",
|
||||
Namespace: cloneKey.Namespace,
|
||||
UID: uuid.NewUUID(),
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("10%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("50%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("100%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
TargetRef: v1alpha1.ObjectRef{
|
||||
WorkloadRef: &v1alpha1.WorkloadRef{
|
||||
APIVersion: cloneDemo.APIVersion,
|
||||
Kind: cloneDemo.Kind,
|
||||
Name: cloneDemo.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
apps.AddToScheme(scheme)
|
||||
v1alpha1.AddToScheme(scheme)
|
||||
kruiseappsv1alpha1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
func TestCalculateBatchContext(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
percent := intstr.FromString("20%")
|
||||
cases := map[string]struct {
|
||||
workload func() *kruiseappsv1alpha1.CloneSet
|
||||
release func() *v1alpha1.BatchRelease
|
||||
result *batchcontext.BatchContext
|
||||
}{
|
||||
"without NoNeedUpdate": {
|
||||
workload: func() *kruiseappsv1alpha1.CloneSet {
|
||||
return &kruiseappsv1alpha1.CloneSet{
|
||||
Spec: kruiseappsv1alpha1.CloneSetSpec{
|
||||
Replicas: pointer.Int32Ptr(10),
|
||||
UpdateStrategy: kruiseappsv1alpha1.CloneSetUpdateStrategy{
|
||||
Partition: func() *intstr.IntOrString { p := intstr.FromString("100%"); return &p }(),
|
||||
},
|
||||
},
|
||||
Status: kruiseappsv1alpha1.CloneSetStatus{
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 5,
|
||||
UpdatedReadyReplicas: 5,
|
||||
AvailableReplicas: 10,
|
||||
},
|
||||
}
|
||||
},
|
||||
release: func() *v1alpha1.BatchRelease {
|
||||
r := &v1alpha1.BatchRelease{
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
FailureThreshold: &percent,
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: percent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
return r
|
||||
},
|
||||
result: &batchcontext.BatchContext{
|
||||
FailureThreshold: &percent,
|
||||
CurrentBatch: 0,
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 5,
|
||||
UpdatedReadyReplicas: 5,
|
||||
PlannedUpdatedReplicas: 2,
|
||||
DesiredUpdatedReplicas: 2,
|
||||
CurrentPartition: intstr.FromString("100%"),
|
||||
DesiredPartition: intstr.FromString("80%"),
|
||||
},
|
||||
},
|
||||
"with NoNeedUpdate": {
|
||||
workload: func() *kruiseappsv1alpha1.CloneSet {
|
||||
return &kruiseappsv1alpha1.CloneSet{
|
||||
Spec: kruiseappsv1alpha1.CloneSetSpec{
|
||||
Replicas: pointer.Int32Ptr(20),
|
||||
UpdateStrategy: kruiseappsv1alpha1.CloneSetUpdateStrategy{
|
||||
Partition: func() *intstr.IntOrString { p := intstr.FromString("100%"); return &p }(),
|
||||
},
|
||||
},
|
||||
Status: kruiseappsv1alpha1.CloneSetStatus{
|
||||
Replicas: 20,
|
||||
UpdatedReplicas: 10,
|
||||
UpdatedReadyReplicas: 10,
|
||||
AvailableReplicas: 20,
|
||||
ReadyReplicas: 20,
|
||||
},
|
||||
}
|
||||
},
|
||||
release: func() *v1alpha1.BatchRelease {
|
||||
r := &v1alpha1.BatchRelease{
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
FailureThreshold: &percent,
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: percent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
NoNeedUpdateReplicas: pointer.Int32(10),
|
||||
},
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
return r
|
||||
},
|
||||
result: &batchcontext.BatchContext{
|
||||
CurrentBatch: 0,
|
||||
UpdateRevision: "update-version",
|
||||
Replicas: 20,
|
||||
UpdatedReplicas: 10,
|
||||
UpdatedReadyReplicas: 10,
|
||||
NoNeedUpdatedReplicas: pointer.Int32Ptr(10),
|
||||
PlannedUpdatedReplicas: 4,
|
||||
DesiredUpdatedReplicas: 12,
|
||||
CurrentPartition: intstr.FromString("100%"),
|
||||
DesiredPartition: intstr.FromString("40%"),
|
||||
FailureThreshold: &percent,
|
||||
FilterFunc: labelpatch.FilterPodsForUnorderedUpdate,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
control := realController{
|
||||
object: cs.workload(),
|
||||
WorkloadInfo: util.ParseWorkload(cs.workload()),
|
||||
}
|
||||
got, err := control.CalculateBatchContext(cs.release())
|
||||
fmt.Println(got)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.Log()).Should(Equal(cs.result.Log()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealController(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
release := releaseDemo.DeepCopy()
|
||||
clone := cloneDemo.DeepCopy()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(release, clone).Build()
|
||||
c := NewController(cli, cloneKey, clone.GroupVersionKind()).(*realController)
|
||||
controller, err := c.BuildController()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = controller.Initialize(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
fetch := &kruiseappsv1alpha1.CloneSet{}
|
||||
Expect(cli.Get(context.TODO(), cloneKey, fetch)).NotTo(HaveOccurred())
|
||||
Expect(fetch.Spec.UpdateStrategy.Paused).Should(BeFalse())
|
||||
Expect(fetch.Annotations[util.BatchReleaseControlAnnotation]).Should(Equal(getControlInfo(release)))
|
||||
c.object = fetch // mock
|
||||
|
||||
for {
|
||||
batchContext, err := controller.CalculateBatchContext(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = controller.UpgradeBatch(batchContext)
|
||||
fetch = &kruiseappsv1alpha1.CloneSet{}
|
||||
// mock
|
||||
Expect(cli.Get(context.TODO(), cloneKey, fetch)).NotTo(HaveOccurred())
|
||||
c.object = fetch
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
fetch = &kruiseappsv1alpha1.CloneSet{}
|
||||
Expect(cli.Get(context.TODO(), cloneKey, fetch)).NotTo(HaveOccurred())
|
||||
Expect(fetch.Spec.UpdateStrategy.Partition.StrVal).Should(Equal("90%"))
|
||||
|
||||
err = controller.Finalize(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
fetch = &kruiseappsv1alpha1.CloneSet{}
|
||||
Expect(cli.Get(context.TODO(), cloneKey, fetch)).NotTo(HaveOccurred())
|
||||
Expect(fetch.Annotations[util.BatchReleaseControlAnnotation]).Should(Equal(""))
|
||||
|
||||
stableInfo := controller.GetInfo()
|
||||
Expect(stableInfo).ShouldNot(BeNil())
|
||||
checkWorkloadInfo(stableInfo, clone)
|
||||
}
|
||||
|
||||
func checkWorkloadInfo(stableInfo *util.WorkloadInfo, clone *kruiseappsv1alpha1.CloneSet) {
|
||||
Expect(stableInfo.Replicas).Should(Equal(*clone.Spec.Replicas))
|
||||
Expect(stableInfo.Status.Replicas).Should(Equal(clone.Status.Replicas))
|
||||
Expect(stableInfo.Status.ReadyReplicas).Should(Equal(clone.Status.ReadyReplicas))
|
||||
Expect(stableInfo.Status.UpdatedReplicas).Should(Equal(clone.Status.UpdatedReplicas))
|
||||
Expect(stableInfo.Status.UpdatedReadyReplicas).Should(Equal(clone.Status.UpdatedReadyReplicas))
|
||||
Expect(stableInfo.Status.UpdateRevision).Should(Equal(clone.Status.UpdateRevision))
|
||||
Expect(stableInfo.Status.StableRevision).Should(Equal(clone.Status.CurrentRevision))
|
||||
Expect(stableInfo.Status.AvailableReplicas).Should(Equal(clone.Status.AvailableReplicas))
|
||||
Expect(stableInfo.Status.ObservedGeneration).Should(Equal(clone.Status.ObservedGeneration))
|
||||
}
|
||||
|
||||
func getControlInfo(release *v1alpha1.BatchRelease) string {
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(release, release.GetObjectKind().GroupVersionKind()))
|
||||
return string(owner)
|
||||
}
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 partitionstyle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/labelpatch"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type realBatchControlPlane struct {
|
||||
Interface
|
||||
client.Client
|
||||
record.EventRecorder
|
||||
patcher labelpatch.LabelPatcher
|
||||
release *v1alpha1.BatchRelease
|
||||
newStatus *v1alpha1.BatchReleaseStatus
|
||||
}
|
||||
|
||||
type NewInterfaceFunc func(cli client.Client, key types.NamespacedName, gvk schema.GroupVersionKind) Interface
|
||||
|
||||
// NewControlPlane creates a new release controller with partitioned-style to drive batch release state machine
|
||||
func NewControlPlane(f NewInterfaceFunc, cli client.Client, recorder record.EventRecorder, release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, key types.NamespacedName, gvk schema.GroupVersionKind) *realBatchControlPlane {
|
||||
return &realBatchControlPlane{
|
||||
Client: cli,
|
||||
EventRecorder: recorder,
|
||||
newStatus: newStatus,
|
||||
Interface: f(cli, key, gvk),
|
||||
release: release.DeepCopy(),
|
||||
patcher: labelpatch.NewLabelPatcher(cli, klog.KObj(release)),
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *realBatchControlPlane) Initialize() error {
|
||||
controller, err := rc.BuildController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// claim workload under our control
|
||||
err = controller.Initialize(rc.release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// record revision and replicas
|
||||
workloadInfo := controller.GetInfo()
|
||||
rc.newStatus.StableRevision = workloadInfo.Status.StableRevision
|
||||
rc.newStatus.UpdateRevision = workloadInfo.Status.UpdateRevision
|
||||
rc.newStatus.ObservedWorkloadReplicas = workloadInfo.Replicas
|
||||
|
||||
// mark the pods that no need to update if it needs
|
||||
noNeedUpdateReplicas, err := rc.markNoNeedUpdatePodsIfNeeds()
|
||||
if noNeedUpdateReplicas != nil && err == nil {
|
||||
rc.newStatus.CanaryStatus.NoNeedUpdateReplicas = noNeedUpdateReplicas
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (rc *realBatchControlPlane) UpgradeBatch() error {
|
||||
controller, err := rc.BuildController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if controller.GetInfo().Replicas == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = rc.countAndUpdateNoNeedUpdateReplicas()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
batchContext, err := controller.CalculateBatchContext(rc.release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.Infof("BatchRelease %v upgrade batch: %s", klog.KObj(rc.release), batchContext.Log())
|
||||
|
||||
err = controller.UpgradeBatch(batchContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rc.patcher.PatchPodBatchLabel(batchContext)
|
||||
}
|
||||
|
||||
func (rc *realBatchControlPlane) CheckBatchReady() error {
|
||||
controller, err := rc.BuildController()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if controller.GetInfo().Replicas == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// do not countAndUpdateNoNeedUpdateReplicas when checking,
|
||||
// the target calculated should be consistent with UpgradeBatch.
|
||||
batchContext, err := controller.CalculateBatchContext(rc.release)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.Infof("BatchRelease %v check batch: %s", klog.KObj(rc.release), batchContext.Log())
|
||||
|
||||
return batchContext.IsBatchReady()
|
||||
}
|
||||
|
||||
func (rc *realBatchControlPlane) Finalize() error {
|
||||
controller, err := rc.BuildController()
|
||||
if err != nil {
|
||||
return client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// release workload control info and clean up resources if it needs
|
||||
return controller.Finalize(rc.release)
|
||||
}
|
||||
|
||||
func (rc *realBatchControlPlane) SyncWorkloadInformation() (control.WorkloadEventType, *util.WorkloadInfo, error) {
|
||||
// ignore the sync if the release plan is deleted
|
||||
if rc.release.DeletionTimestamp != nil {
|
||||
return control.WorkloadNormalState, nil, nil
|
||||
}
|
||||
|
||||
controller, err := rc.BuildController()
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return control.WorkloadHasGone, nil, err
|
||||
}
|
||||
return control.WorkloadUnknownState, nil, err
|
||||
}
|
||||
|
||||
workloadInfo := controller.GetInfo()
|
||||
if !workloadInfo.IsStable() {
|
||||
klog.Info("Workload(%v) still reconciling, waiting for it to complete, generation: %v, observed: %v",
|
||||
workloadInfo.LogKey, workloadInfo.Generation, workloadInfo.Status.ObservedGeneration)
|
||||
return control.WorkloadStillReconciling, workloadInfo, nil
|
||||
}
|
||||
|
||||
if workloadInfo.IsPromoted() {
|
||||
klog.Info("Workload(%v) has been promoted, no need to rollout again actually, replicas: %v, updated: %v",
|
||||
workloadInfo.LogKey, workloadInfo.Replicas, workloadInfo.Status.UpdatedReadyReplicas)
|
||||
return control.WorkloadNormalState, workloadInfo, nil
|
||||
}
|
||||
|
||||
if workloadInfo.IsScaling(rc.newStatus.ObservedWorkloadReplicas) {
|
||||
klog.Warningf("Workload(%v) replicas is modified, replicas from: %v to -> %v",
|
||||
workloadInfo.LogKey, rc.newStatus.ObservedWorkloadReplicas, workloadInfo.Replicas)
|
||||
return control.WorkloadReplicasChanged, workloadInfo, nil
|
||||
}
|
||||
|
||||
if workloadInfo.IsRollback(rc.newStatus.StableRevision, rc.newStatus.UpdateRevision) {
|
||||
klog.Warningf("Workload(%v) is rolling back", workloadInfo.LogKey)
|
||||
return control.WorkloadRollbackInBatch, workloadInfo, nil
|
||||
}
|
||||
|
||||
if workloadInfo.IsRevisionNotEqual(rc.newStatus.UpdateRevision) {
|
||||
klog.Warningf("Workload(%v) updateRevision is modified, updateRevision from: %v to -> %v",
|
||||
workloadInfo.LogKey, rc.newStatus.UpdateRevision, workloadInfo.Status.UpdateRevision)
|
||||
return control.WorkloadPodTemplateChanged, workloadInfo, nil
|
||||
}
|
||||
|
||||
return control.WorkloadNormalState, workloadInfo, nil
|
||||
}
|
||||
|
||||
/* --------------------------------------------
|
||||
The functions below are helper functions
|
||||
----------------------------------------------- */
|
||||
|
||||
// MarkNoNeedUpdatePods makes sure that the updated pods have been patched no-need-update label.
|
||||
// return values:
|
||||
// - *int32: how many pods have been patched;
|
||||
// - err: whether error occurs.
|
||||
func (rc *realBatchControlPlane) markNoNeedUpdatePodsIfNeeds() (*int32, error) {
|
||||
// currently, we only support rollback scene, in the future, we may support more scenes.
|
||||
if rc.release.Annotations[util.RollbackInBatchAnnotation] == "" {
|
||||
return nil, nil
|
||||
}
|
||||
// currently, if rollout-id is not set, it is no scene which require patch this label
|
||||
// we only return the current updated replicas.
|
||||
if rc.release.Spec.ReleasePlan.RolloutID == "" {
|
||||
return pointer.Int32(rc.newStatus.CanaryStatus.UpdatedReplicas), nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var pods []*v1.Pod
|
||||
var filterPods []*v1.Pod
|
||||
noNeedUpdateReplicas := int32(0)
|
||||
rolloutID := rc.release.Spec.ReleasePlan.RolloutID
|
||||
if pods, err = rc.ListOwnedPods(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range pods {
|
||||
if !pods[i].DeletionTimestamp.IsZero() {
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pods[i], rc.newStatus.UpdateRevision) {
|
||||
continue
|
||||
}
|
||||
if pods[i].Labels[util.NoNeedUpdatePodLabel] == rolloutID {
|
||||
noNeedUpdateReplicas++
|
||||
continue
|
||||
}
|
||||
filterPods = append(filterPods, pods[i])
|
||||
}
|
||||
|
||||
if len(filterPods) == 0 {
|
||||
return &noNeedUpdateReplicas, nil
|
||||
}
|
||||
|
||||
for _, pod := range filterPods {
|
||||
clone := util.GetEmptyObjectWithKey(pod)
|
||||
body := fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s"}}}`, util.NoNeedUpdatePodLabel, rolloutID)
|
||||
err = rc.Patch(context.TODO(), clone, client.RawPatch(types.StrategicMergePatchType, []byte(body)))
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to patch no-need-update label(%v) to pod %v, err: %v", rolloutID, klog.KObj(pod), err)
|
||||
return &noNeedUpdateReplicas, err
|
||||
} else {
|
||||
klog.Info("Succeeded to patch no-need-update label(%v) to pod %v", rolloutID, klog.KObj(pod))
|
||||
}
|
||||
noNeedUpdateReplicas++
|
||||
}
|
||||
|
||||
return &noNeedUpdateReplicas, fmt.Errorf("initilization not yet: patch and find %d pods with no-need-update-label", noNeedUpdateReplicas)
|
||||
}
|
||||
|
||||
// countAndUpdateNoNeedUpdateReplicas will count the pods with no-need-update
|
||||
// label and update corresponding field for BatchRelease
|
||||
func (rc *realBatchControlPlane) countAndUpdateNoNeedUpdateReplicas() error {
|
||||
if rc.release.Spec.ReleasePlan.RolloutID == "" || rc.release.Status.CanaryStatus.NoNeedUpdateReplicas == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pods, err := rc.ListOwnedPods()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
noNeedUpdateReplicas := int32(0)
|
||||
for _, pod := range pods {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pod, rc.release.Status.UpdateRevision) {
|
||||
continue
|
||||
}
|
||||
id, ok := pod.Labels[util.NoNeedUpdatePodLabel]
|
||||
if ok && id == rc.release.Spec.ReleasePlan.RolloutID {
|
||||
noNeedUpdateReplicas++
|
||||
}
|
||||
}
|
||||
|
||||
// refresh newStatus for updating
|
||||
rc.newStatus.CanaryStatus.NoNeedUpdateReplicas = &noNeedUpdateReplicas
|
||||
// refresh release.Status for calculation of BatchContext
|
||||
rc.release.Status.CanaryStatus.NoNeedUpdateReplicas = &noNeedUpdateReplicas
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 partitionstyle
|
||||
|
||||
import (
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
// BuildController will get workload object and parse workload info,
|
||||
// and return a controller for workload
|
||||
BuildController() (Interface, error)
|
||||
// GetInfo return workload information
|
||||
GetInfo() *util.WorkloadInfo
|
||||
// ListOwnedPods fetch the pods owned by the workload.
|
||||
// Note that we should list pod only if we really need it.
|
||||
ListOwnedPods() ([]*corev1.Pod, error)
|
||||
// CalculateBatchContext calculate current batch context
|
||||
// according to release plan and current status of workload.
|
||||
CalculateBatchContext(release *v1alpha1.BatchRelease) (*batchcontext.BatchContext, error)
|
||||
|
||||
// Initialize do something before rolling out, for example
|
||||
// - claim the workload is under our control;
|
||||
// - other things related with specific type of workload, such as 100% partition settings.
|
||||
Initialize(release *v1alpha1.BatchRelease) error
|
||||
// UpgradeBatch upgrade workload according current batch context.
|
||||
UpgradeBatch(ctx *batchcontext.BatchContext) error
|
||||
// Finalize do something after rolling out, for example:
|
||||
// - free the stable workload from rollout control;
|
||||
// - resume workload if we need.
|
||||
Finalize(release *v1alpha1.BatchRelease) error
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 statefulset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/control/partitionstyle"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/labelpatch"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type realController struct {
|
||||
*util.WorkloadInfo
|
||||
client client.Client
|
||||
pods []*corev1.Pod
|
||||
key types.NamespacedName
|
||||
gvk schema.GroupVersionKind
|
||||
object client.Object
|
||||
}
|
||||
|
||||
func NewController(cli client.Client, key types.NamespacedName, gvk schema.GroupVersionKind) partitionstyle.Interface {
|
||||
return &realController{
|
||||
key: key,
|
||||
gvk: gvk,
|
||||
client: cli,
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *realController) GetInfo() *util.WorkloadInfo {
|
||||
return rc.WorkloadInfo
|
||||
}
|
||||
|
||||
func (rc *realController) BuildController() (partitionstyle.Interface, error) {
|
||||
if rc.object != nil {
|
||||
return rc, nil
|
||||
}
|
||||
object := util.GetEmptyWorkloadObject(rc.gvk)
|
||||
if err := rc.client.Get(context.TODO(), rc.key, object); err != nil {
|
||||
return rc, err
|
||||
}
|
||||
rc.object = object
|
||||
rc.WorkloadInfo = util.ParseWorkload(object)
|
||||
|
||||
// for native StatefulSet which has no updatedReadyReplicas field, we should
|
||||
// list and count its owned Pods one by one.
|
||||
if rc.WorkloadInfo != nil && rc.WorkloadInfo.Status.UpdatedReadyReplicas <= 0 {
|
||||
pods, err := rc.ListOwnedPods()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updatedReadyReplicas := util.WrappedPodCount(pods, func(pod *corev1.Pod) bool {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
return false
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pod, rc.WorkloadInfo.Status.UpdateRevision) {
|
||||
return false
|
||||
}
|
||||
return util.IsPodReady(pod)
|
||||
})
|
||||
rc.WorkloadInfo.Status.UpdatedReadyReplicas = int32(updatedReadyReplicas)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (rc *realController) ListOwnedPods() ([]*corev1.Pod, error) {
|
||||
if rc.pods != nil {
|
||||
return rc.pods, nil
|
||||
}
|
||||
var err error
|
||||
rc.pods, err = util.ListOwnedPods(rc.client, rc.object)
|
||||
return rc.pods, err
|
||||
}
|
||||
|
||||
func (rc *realController) Initialize(release *v1alpha1.BatchRelease) error {
|
||||
if control.IsControlledByBatchRelease(release, rc.object) {
|
||||
return nil
|
||||
}
|
||||
|
||||
owner := control.BuildReleaseControlInfo(release)
|
||||
metaBody := fmt.Sprintf(`"metadata":{"annotations":{"%s":"%s"}}`, util.BatchReleaseControlAnnotation, owner)
|
||||
specBody := fmt.Sprintf(`"spec":{"updateStrategy":{"rollingUpdate":{"partition":%d,"paused":false}}}`, math.MaxInt16)
|
||||
body := fmt.Sprintf(`{%s,%s}`, metaBody, specBody)
|
||||
|
||||
clone := util.GetEmptyObjectWithKey(rc.object)
|
||||
if err := rc.client.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.Infof("Successfully initialize StatefulSet %v", klog.KObj(clone))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *realController) UpgradeBatch(ctx *batchcontext.BatchContext) error {
|
||||
desired := ctx.DesiredPartition.IntVal
|
||||
current := ctx.CurrentPartition.IntVal
|
||||
// current less than desired, which means current revision replicas will be less than desired,
|
||||
// in other word, update revision replicas will be more than desired, no need to update again.
|
||||
if current <= desired {
|
||||
return nil
|
||||
}
|
||||
|
||||
body := fmt.Sprintf(`{"spec":{"updateStrategy":{"rollingUpdate":{"partition":%d}}}}`, desired)
|
||||
|
||||
clone := rc.object.DeepCopyObject().(client.Object)
|
||||
if err := rc.client.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.Infof("Successfully patch partition from %d to %d for StatefulSet %v", current, desired, klog.KObj(clone))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *realController) Finalize(release *v1alpha1.BatchRelease) error {
|
||||
if rc.object == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var specBody string
|
||||
// If batchPartition == nil, workload should be promoted;
|
||||
if release.Spec.ReleasePlan.BatchPartition == nil {
|
||||
specBody = `,"spec":{"updateStrategy":{"rollingUpdate":{"partition":null}}}`
|
||||
}
|
||||
|
||||
body := fmt.Sprintf(`{"metadata":{"annotations":{"%s":null}}%s}`, util.BatchReleaseControlAnnotation, specBody)
|
||||
|
||||
clone := util.GetEmptyObjectWithKey(rc.object)
|
||||
if err := rc.client.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.Infof("Successfully finalize StatefulSet %v", klog.KObj(clone))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *realController) CalculateBatchContext(release *v1alpha1.BatchRelease) (*batchcontext.BatchContext, error) {
|
||||
rolloutID := release.Spec.ReleasePlan.RolloutID
|
||||
if rolloutID != "" {
|
||||
// if rollout-id is set, the pod will be patched batch label,
|
||||
// so we have to list pod here.
|
||||
if _, err := rc.ListOwnedPods(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// current batch index
|
||||
currentBatch := release.Status.CanaryStatus.CurrentBatch
|
||||
// the number of no need update pods that marked before rollout
|
||||
noNeedUpdate := release.Status.CanaryStatus.NoNeedUpdateReplicas
|
||||
// the number of upgraded pods according to release plan in current batch.
|
||||
plannedUpdate := int32(control.CalculateBatchReplicas(release, int(rc.Replicas), int(currentBatch)))
|
||||
// the number of pods that should be upgraded in real
|
||||
desiredUpdate := plannedUpdate
|
||||
// the number of pods that should not be upgraded in real
|
||||
desiredStable := rc.Replicas - desiredUpdate
|
||||
// if we should consider the no-need-update pods that were marked before rolling, the desired will change
|
||||
if noNeedUpdate != nil && *noNeedUpdate > 0 {
|
||||
// specially, we should ignore the pods that were marked as no-need-update, this logic is for Rollback scene
|
||||
desiredUpdateNew := int32(control.CalculateBatchReplicas(release, int(rc.Replicas-*noNeedUpdate), int(currentBatch)))
|
||||
desiredStable = rc.Replicas - *noNeedUpdate - desiredUpdateNew
|
||||
desiredUpdate = rc.Replicas - desiredStable
|
||||
}
|
||||
|
||||
// Note that:
|
||||
// * if ordered update, partition is related with pod ordinals;
|
||||
// * if unordered update, partition just like cloneSet partition.
|
||||
unorderedUpdate := util.IsStatefulSetUnorderedUpdate(rc.object)
|
||||
if !unorderedUpdate && noNeedUpdate != nil {
|
||||
desiredStable += *noNeedUpdate
|
||||
desiredUpdate = rc.Replicas - desiredStable + *noNeedUpdate
|
||||
}
|
||||
|
||||
// if canaryReplicas is percentage, we should calculate its real
|
||||
batchContext := &batchcontext.BatchContext{
|
||||
Pods: rc.pods,
|
||||
RolloutID: rolloutID,
|
||||
CurrentBatch: currentBatch,
|
||||
UpdateRevision: release.Status.UpdateRevision,
|
||||
DesiredPartition: intstr.FromInt(int(desiredStable)),
|
||||
CurrentPartition: intstr.FromInt(int(util.GetStatefulSetPartition(rc.object))),
|
||||
FailureThreshold: release.Spec.ReleasePlan.FailureThreshold,
|
||||
|
||||
Replicas: rc.Replicas,
|
||||
UpdatedReplicas: rc.Status.UpdatedReplicas,
|
||||
UpdatedReadyReplicas: rc.Status.UpdatedReadyReplicas,
|
||||
NoNeedUpdatedReplicas: noNeedUpdate,
|
||||
PlannedUpdatedReplicas: plannedUpdate,
|
||||
DesiredUpdatedReplicas: desiredUpdate,
|
||||
}
|
||||
|
||||
if noNeedUpdate != nil {
|
||||
if unorderedUpdate {
|
||||
batchContext.FilterFunc = labelpatch.FilterPodsForUnorderedUpdate
|
||||
} else {
|
||||
batchContext.FilterFunc = labelpatch.FilterPodsForOrderedUpdate
|
||||
}
|
||||
}
|
||||
return batchContext, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,663 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 statefulset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
appsv1pub "github.com/openkruise/kruise-api/apps/pub"
|
||||
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
kruiseappsv1beta1 "github.com/openkruise/kruise-api/apps/v1beta1"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/controller/batchrelease/labelpatch"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/rand"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
|
||||
stsKey = types.NamespacedName{
|
||||
Namespace: "default",
|
||||
Name: "statefulset",
|
||||
}
|
||||
stsDemo = &kruiseappsv1beta1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps.kruise.io/v1alpha1",
|
||||
Kind: "StatefulSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: stsKey.Name,
|
||||
Namespace: stsKey.Namespace,
|
||||
Generation: 1,
|
||||
Labels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"type": "unit-test",
|
||||
},
|
||||
},
|
||||
Spec: kruiseappsv1beta1.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
},
|
||||
Replicas: pointer.Int32(10),
|
||||
UpdateStrategy: kruiseappsv1beta1.StatefulSetUpdateStrategy{
|
||||
Type: apps.RollingUpdateStatefulSetStrategyType,
|
||||
RollingUpdate: &kruiseappsv1beta1.RollingUpdateStatefulSetStrategy{
|
||||
Paused: true,
|
||||
Partition: pointer.Int32(10),
|
||||
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
|
||||
},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: "busybox:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: kruiseappsv1beta1.StatefulSetStatus{
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 0,
|
||||
ReadyReplicas: 10,
|
||||
AvailableReplicas: 10,
|
||||
UpdateRevision: "version-2",
|
||||
CurrentRevision: "version-1",
|
||||
ObservedGeneration: 1,
|
||||
UpdatedReadyReplicas: 0,
|
||||
CollisionCount: pointer.Int32Ptr(1),
|
||||
},
|
||||
}
|
||||
|
||||
releaseDemo = &v1alpha1.BatchRelease{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rollouts.kruise.io/v1alpha1",
|
||||
Kind: "BatchRelease",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "release",
|
||||
Namespace: stsKey.Namespace,
|
||||
UID: uuid.NewUUID(),
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("10%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("50%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("100%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
TargetRef: v1alpha1.ObjectRef{
|
||||
WorkloadRef: &v1alpha1.WorkloadRef{
|
||||
APIVersion: stsDemo.APIVersion,
|
||||
Kind: stsDemo.Kind,
|
||||
Name: stsDemo.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(87076677)
|
||||
apps.AddToScheme(scheme)
|
||||
corev1.AddToScheme(scheme)
|
||||
v1alpha1.AddToScheme(scheme)
|
||||
kruiseappsv1alpha1.AddToScheme(scheme)
|
||||
kruiseappsv1beta1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
func TestCalculateBatchContextForNativeStatefulSet(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
percent := intstr.FromString("20%")
|
||||
cases := map[string]struct {
|
||||
workload func() *apps.StatefulSet
|
||||
release func() *v1alpha1.BatchRelease
|
||||
pods func() []*corev1.Pod
|
||||
result *batchcontext.BatchContext
|
||||
}{
|
||||
"without NoNeedUpdate": {
|
||||
workload: func() *apps.StatefulSet {
|
||||
return &apps.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "StatefulSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-sts",
|
||||
Namespace: "test",
|
||||
UID: "test",
|
||||
},
|
||||
Spec: apps.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}},
|
||||
Replicas: pointer.Int32Ptr(10),
|
||||
UpdateStrategy: apps.StatefulSetUpdateStrategy{
|
||||
Type: apps.RollingUpdateStatefulSetStrategyType,
|
||||
RollingUpdate: &apps.RollingUpdateStatefulSetStrategy{
|
||||
Partition: pointer.Int32Ptr(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: apps.StatefulSetStatus{
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 5,
|
||||
AvailableReplicas: 10,
|
||||
CurrentRevision: "stable-version",
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
},
|
||||
pods: func() []*corev1.Pod {
|
||||
stablePods := generatePods(5, "stable-version", "True")
|
||||
updatedReadyPods := generatePods(5, "update-version", "True")
|
||||
return append(stablePods, updatedReadyPods...)
|
||||
},
|
||||
release: func() *v1alpha1.BatchRelease {
|
||||
r := &v1alpha1.BatchRelease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-br",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
FailureThreshold: &percent,
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: percent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
},
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
return r
|
||||
},
|
||||
result: &batchcontext.BatchContext{
|
||||
FailureThreshold: &percent,
|
||||
CurrentBatch: 0,
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 5,
|
||||
UpdatedReadyReplicas: 5,
|
||||
PlannedUpdatedReplicas: 2,
|
||||
DesiredUpdatedReplicas: 2,
|
||||
UpdateRevision: "update-version",
|
||||
CurrentPartition: intstr.FromInt(100),
|
||||
DesiredPartition: intstr.FromInt(8),
|
||||
Pods: generatePods(10, "", ""),
|
||||
},
|
||||
},
|
||||
"with NoNeedUpdate": {
|
||||
workload: func() *apps.StatefulSet {
|
||||
return &apps.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "StatefulSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-sts",
|
||||
Namespace: "test",
|
||||
UID: "test",
|
||||
},
|
||||
Spec: apps.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}},
|
||||
Replicas: pointer.Int32Ptr(20),
|
||||
UpdateStrategy: apps.StatefulSetUpdateStrategy{
|
||||
Type: apps.RollingUpdateStatefulSetStrategyType,
|
||||
RollingUpdate: &apps.RollingUpdateStatefulSetStrategy{
|
||||
Partition: pointer.Int32Ptr(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: apps.StatefulSetStatus{
|
||||
Replicas: 20,
|
||||
UpdatedReplicas: 10,
|
||||
AvailableReplicas: 20,
|
||||
CurrentRevision: "stable-version",
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
},
|
||||
pods: func() []*corev1.Pod {
|
||||
stablePods := generatePods(10, "stable-version", "True")
|
||||
updatedReadyPods := generatePods(10, "update-version", "True")
|
||||
return append(stablePods, updatedReadyPods...)
|
||||
},
|
||||
release: func() *v1alpha1.BatchRelease {
|
||||
r := &v1alpha1.BatchRelease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-br",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
FailureThreshold: &percent,
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: percent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
NoNeedUpdateReplicas: pointer.Int32(10),
|
||||
},
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
return r
|
||||
},
|
||||
result: &batchcontext.BatchContext{
|
||||
CurrentBatch: 0,
|
||||
UpdateRevision: "update-version",
|
||||
Replicas: 20,
|
||||
UpdatedReplicas: 10,
|
||||
UpdatedReadyReplicas: 10,
|
||||
NoNeedUpdatedReplicas: pointer.Int32Ptr(10),
|
||||
PlannedUpdatedReplicas: 4,
|
||||
DesiredUpdatedReplicas: 12,
|
||||
CurrentPartition: intstr.FromInt(100),
|
||||
DesiredPartition: intstr.FromInt(18),
|
||||
FailureThreshold: &percent,
|
||||
FilterFunc: labelpatch.FilterPodsForUnorderedUpdate,
|
||||
Pods: generatePods(20, "", ""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
pods := func() []client.Object {
|
||||
var objects []client.Object
|
||||
pods := cs.pods()
|
||||
for _, pod := range pods {
|
||||
objects = append(objects, pod)
|
||||
}
|
||||
return objects
|
||||
}()
|
||||
br := cs.release()
|
||||
sts := cs.workload()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(br, sts).WithObjects(pods...).Build()
|
||||
control := realController{
|
||||
client: cli,
|
||||
gvk: sts.GetObjectKind().GroupVersionKind(),
|
||||
key: types.NamespacedName{Namespace: "test", Name: "test-sts"},
|
||||
}
|
||||
c, err := control.BuildController()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
got, err := c.CalculateBatchContext(cs.release())
|
||||
fmt.Println(got.Log())
|
||||
fmt.Println(cs.result.Log())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.Log()).Should(Equal(cs.result.Log()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateBatchContextForAdvancedStatefulSet(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
percent := intstr.FromString("20%")
|
||||
cases := map[string]struct {
|
||||
workload func() *kruiseappsv1beta1.StatefulSet
|
||||
release func() *v1alpha1.BatchRelease
|
||||
pods func() []*corev1.Pod
|
||||
result *batchcontext.BatchContext
|
||||
}{
|
||||
"without NoNeedUpdate": {
|
||||
workload: func() *kruiseappsv1beta1.StatefulSet {
|
||||
return &kruiseappsv1beta1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps.kruise.io/v1beta1",
|
||||
Kind: "StatefulSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-sts",
|
||||
Namespace: "test",
|
||||
UID: "test",
|
||||
},
|
||||
Spec: kruiseappsv1beta1.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}},
|
||||
Replicas: pointer.Int32Ptr(10),
|
||||
UpdateStrategy: kruiseappsv1beta1.StatefulSetUpdateStrategy{
|
||||
Type: apps.RollingUpdateStatefulSetStrategyType,
|
||||
RollingUpdate: &kruiseappsv1beta1.RollingUpdateStatefulSetStrategy{
|
||||
Partition: pointer.Int32Ptr(100),
|
||||
UnorderedUpdate: &kruiseappsv1beta1.UnorderedUpdateStrategy{
|
||||
PriorityStrategy: &appsv1pub.UpdatePriorityStrategy{
|
||||
OrderPriority: []appsv1pub.UpdatePriorityOrderTerm{
|
||||
{
|
||||
OrderedKey: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: kruiseappsv1beta1.StatefulSetStatus{
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 5,
|
||||
AvailableReplicas: 10,
|
||||
CurrentRevision: "stable-version",
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
},
|
||||
pods: func() []*corev1.Pod {
|
||||
stablePods := generatePods(5, "stable-version", "True")
|
||||
updatedReadyPods := generatePods(5, "update-version", "True")
|
||||
return append(stablePods, updatedReadyPods...)
|
||||
},
|
||||
release: func() *v1alpha1.BatchRelease {
|
||||
r := &v1alpha1.BatchRelease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-br",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
FailureThreshold: &percent,
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: percent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
},
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
return r
|
||||
},
|
||||
result: &batchcontext.BatchContext{
|
||||
FailureThreshold: &percent,
|
||||
CurrentBatch: 0,
|
||||
Replicas: 10,
|
||||
UpdatedReplicas: 5,
|
||||
UpdatedReadyReplicas: 5,
|
||||
PlannedUpdatedReplicas: 2,
|
||||
DesiredUpdatedReplicas: 2,
|
||||
UpdateRevision: "update-version",
|
||||
CurrentPartition: intstr.FromInt(100),
|
||||
DesiredPartition: intstr.FromInt(8),
|
||||
Pods: generatePods(10, "", ""),
|
||||
},
|
||||
},
|
||||
"with NoNeedUpdate": {
|
||||
workload: func() *kruiseappsv1beta1.StatefulSet {
|
||||
return &kruiseappsv1beta1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps.kruise.io/v1beta1",
|
||||
Kind: "StatefulSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-sts",
|
||||
Namespace: "test",
|
||||
UID: "test",
|
||||
},
|
||||
Spec: kruiseappsv1beta1.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}},
|
||||
Replicas: pointer.Int32Ptr(20),
|
||||
UpdateStrategy: kruiseappsv1beta1.StatefulSetUpdateStrategy{
|
||||
Type: apps.RollingUpdateStatefulSetStrategyType,
|
||||
RollingUpdate: &kruiseappsv1beta1.RollingUpdateStatefulSetStrategy{
|
||||
Partition: pointer.Int32Ptr(100),
|
||||
UnorderedUpdate: &kruiseappsv1beta1.UnorderedUpdateStrategy{
|
||||
PriorityStrategy: &appsv1pub.UpdatePriorityStrategy{
|
||||
OrderPriority: []appsv1pub.UpdatePriorityOrderTerm{
|
||||
{
|
||||
OrderedKey: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: kruiseappsv1beta1.StatefulSetStatus{
|
||||
Replicas: 20,
|
||||
UpdatedReplicas: 10,
|
||||
AvailableReplicas: 20,
|
||||
CurrentRevision: "stable-version",
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
},
|
||||
pods: func() []*corev1.Pod {
|
||||
stablePods := generatePods(10, "stable-version", "True")
|
||||
updatedReadyPods := generatePods(10, "update-version", "True")
|
||||
return append(stablePods, updatedReadyPods...)
|
||||
},
|
||||
release: func() *v1alpha1.BatchRelease {
|
||||
r := &v1alpha1.BatchRelease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-br",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
FailureThreshold: &percent,
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: percent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1alpha1.BatchReleaseStatus{
|
||||
CanaryStatus: v1alpha1.BatchReleaseCanaryStatus{
|
||||
CurrentBatch: 0,
|
||||
NoNeedUpdateReplicas: pointer.Int32(10),
|
||||
},
|
||||
UpdateRevision: "update-version",
|
||||
},
|
||||
}
|
||||
return r
|
||||
},
|
||||
result: &batchcontext.BatchContext{
|
||||
CurrentBatch: 0,
|
||||
UpdateRevision: "update-version",
|
||||
Replicas: 20,
|
||||
UpdatedReplicas: 10,
|
||||
UpdatedReadyReplicas: 10,
|
||||
NoNeedUpdatedReplicas: pointer.Int32Ptr(10),
|
||||
PlannedUpdatedReplicas: 4,
|
||||
DesiredUpdatedReplicas: 12,
|
||||
CurrentPartition: intstr.FromInt(100),
|
||||
DesiredPartition: intstr.FromInt(8),
|
||||
FailureThreshold: &percent,
|
||||
FilterFunc: labelpatch.FilterPodsForUnorderedUpdate,
|
||||
Pods: generatePods(20, "", ""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
pods := func() []client.Object {
|
||||
var objects []client.Object
|
||||
pods := cs.pods()
|
||||
for _, pod := range pods {
|
||||
objects = append(objects, pod)
|
||||
}
|
||||
return objects
|
||||
}()
|
||||
br := cs.release()
|
||||
sts := cs.workload()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(br, sts).WithObjects(pods...).Build()
|
||||
control := realController{
|
||||
client: cli,
|
||||
gvk: sts.GetObjectKind().GroupVersionKind(),
|
||||
key: types.NamespacedName{Namespace: "test", Name: "test-sts"},
|
||||
}
|
||||
c, err := control.BuildController()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
got, err := c.CalculateBatchContext(cs.release())
|
||||
fmt.Println(got.Log())
|
||||
fmt.Println(cs.result.Log())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.Log()).Should(Equal(cs.result.Log()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealController(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
release := releaseDemo.DeepCopy()
|
||||
release.Spec.ReleasePlan.RolloutID = "1"
|
||||
sts := stsDemo.DeepCopy()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(release, sts).Build()
|
||||
c := NewController(cli, stsKey, sts.GroupVersionKind()).(*realController)
|
||||
controller, err := c.BuildController()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = controller.Initialize(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
fetch := &kruiseappsv1beta1.StatefulSet{}
|
||||
Expect(cli.Get(context.TODO(), stsKey, fetch)).NotTo(HaveOccurred())
|
||||
Expect(*fetch.Spec.UpdateStrategy.RollingUpdate.Partition).Should(BeNumerically(">=", 1000))
|
||||
Expect(fetch.Annotations[util.BatchReleaseControlAnnotation]).Should(Equal(getControlInfo(release)))
|
||||
c.object = fetch // mock
|
||||
|
||||
for {
|
||||
batchContext, err := controller.CalculateBatchContext(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = controller.UpgradeBatch(batchContext)
|
||||
// mock
|
||||
fetch = &kruiseappsv1beta1.StatefulSet{}
|
||||
Expect(cli.Get(context.TODO(), stsKey, fetch)).NotTo(HaveOccurred())
|
||||
c.object = fetch
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
fetch = &kruiseappsv1beta1.StatefulSet{}
|
||||
Expect(cli.Get(context.TODO(), stsKey, fetch)).NotTo(HaveOccurred())
|
||||
Expect(*fetch.Spec.UpdateStrategy.RollingUpdate.Partition).Should(BeNumerically("==", 9))
|
||||
|
||||
// mock
|
||||
_ = controller.Finalize(release)
|
||||
fetch = &kruiseappsv1beta1.StatefulSet{}
|
||||
Expect(cli.Get(context.TODO(), stsKey, fetch)).NotTo(HaveOccurred())
|
||||
c.object = fetch
|
||||
err = controller.Finalize(release)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
fetch = &kruiseappsv1beta1.StatefulSet{}
|
||||
Expect(cli.Get(context.TODO(), stsKey, fetch)).NotTo(HaveOccurred())
|
||||
Expect(fetch.Annotations[util.BatchReleaseControlAnnotation]).Should(Equal(""))
|
||||
|
||||
stableInfo := controller.GetInfo()
|
||||
Expect(stableInfo).ShouldNot(BeNil())
|
||||
checkWorkloadInfo(stableInfo, sts)
|
||||
}
|
||||
|
||||
func checkWorkloadInfo(stableInfo *util.WorkloadInfo, sts *kruiseappsv1beta1.StatefulSet) {
|
||||
Expect(stableInfo.Replicas).Should(Equal(*sts.Spec.Replicas))
|
||||
Expect(stableInfo.Status.Replicas).Should(Equal(sts.Status.Replicas))
|
||||
Expect(stableInfo.Status.ReadyReplicas).Should(Equal(sts.Status.ReadyReplicas))
|
||||
Expect(stableInfo.Status.UpdatedReplicas).Should(Equal(sts.Status.UpdatedReplicas))
|
||||
Expect(stableInfo.Status.UpdateRevision).Should(Equal(sts.Status.UpdateRevision))
|
||||
Expect(stableInfo.Status.UpdatedReadyReplicas).Should(Equal(sts.Status.UpdatedReadyReplicas))
|
||||
Expect(stableInfo.Status.StableRevision).Should(Equal(sts.Status.CurrentRevision))
|
||||
Expect(stableInfo.Status.AvailableReplicas).Should(Equal(sts.Status.AvailableReplicas))
|
||||
Expect(stableInfo.Status.ObservedGeneration).Should(Equal(sts.Status.ObservedGeneration))
|
||||
}
|
||||
|
||||
func getControlInfo(release *v1alpha1.BatchRelease) string {
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(release, release.GetObjectKind().GroupVersionKind()))
|
||||
return string(owner)
|
||||
}
|
||||
|
||||
func generatePods(replicas int, version, readyStatus string) []*corev1.Pod {
|
||||
var pods []*corev1.Pod
|
||||
for replicas > 0 {
|
||||
pods = append(pods, &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "test",
|
||||
Name: fmt.Sprintf("pod-%s", rand.String(10)),
|
||||
Labels: map[string]string{
|
||||
"app": "foo",
|
||||
apps.ControllerRevisionHashLabelKey: version,
|
||||
},
|
||||
OwnerReferences: []metav1.OwnerReference{{
|
||||
UID: "test",
|
||||
Controller: pointer.Bool(true),
|
||||
}},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Conditions: []corev1.PodCondition{{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionStatus(readyStatus),
|
||||
}},
|
||||
},
|
||||
})
|
||||
replicas--
|
||||
}
|
||||
return pods
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 control
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// CalculateBatchReplicas return the planned updated replicas of current batch.
|
||||
func CalculateBatchReplicas(release *v1alpha1.BatchRelease, workloadReplicas, currentBatch int) int {
|
||||
batchSize, _ := intstr.GetScaledValueFromIntOrPercent(&release.Spec.ReleasePlan.Batches[currentBatch].CanaryReplicas, workloadReplicas, true)
|
||||
if batchSize > workloadReplicas {
|
||||
klog.Warningf("releasePlan has wrong batch replicas, batches[%d].replicas %v is more than workload.replicas %v", currentBatch, batchSize, workloadReplicas)
|
||||
batchSize = workloadReplicas
|
||||
} else if batchSize < 0 {
|
||||
klog.Warningf("releasePlan has wrong batch replicas, batches[%d].replicas %v is less than 0 %v", currentBatch, batchSize)
|
||||
batchSize = 0
|
||||
}
|
||||
|
||||
klog.V(3).InfoS("calculated the number of new pod size", "current batch", currentBatch, "new pod target", batchSize)
|
||||
return batchSize
|
||||
}
|
||||
|
||||
// IsControlledByBatchRelease return true if
|
||||
// * object ownerReference has referred release;
|
||||
// * object has batchRelease control info annotation about release.
|
||||
func IsControlledByBatchRelease(release *v1alpha1.BatchRelease, object client.Object) bool {
|
||||
if owner := metav1.GetControllerOfNoCopy(object); owner != nil && owner.UID == release.UID {
|
||||
return true
|
||||
}
|
||||
if controlInfo, ok := object.GetAnnotations()[util.BatchReleaseControlAnnotation]; ok && controlInfo != "" {
|
||||
ref := &metav1.OwnerReference{}
|
||||
err := json.Unmarshal([]byte(controlInfo), ref)
|
||||
if err == nil && ref.UID == release.UID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BuildReleaseControlInfo return a NewControllerRef of release with escaped `"`.
|
||||
func BuildReleaseControlInfo(release *v1alpha1.BatchRelease) string {
|
||||
owner, _ := json.Marshal(metav1.NewControllerRef(release, release.GetObjectKind().GroupVersionKind()))
|
||||
return strings.Replace(string(owner), `"`, `\"`, -1)
|
||||
}
|
||||
|
||||
// ParseIntegerAsPercentageIfPossible will return a percentage type IntOrString, such as "20%", "33%", but "33.3%" is illegal.
|
||||
// Given A, B, return P that should try best to satisfy ⌈P * B⌉ == A, and we ensure that the error is less than 1%.
|
||||
// For examples:
|
||||
// * Given stableReplicas 1, allReplicas 3, return "33%";
|
||||
// * Given stableReplicas 98, allReplicas 99, return "97%";
|
||||
// * Given stableReplicas 1, allReplicas 101, return "1";
|
||||
func ParseIntegerAsPercentageIfPossible(stableReplicas, allReplicas int32, canaryReplicas *intstr.IntOrString) intstr.IntOrString {
|
||||
if stableReplicas >= allReplicas {
|
||||
return intstr.FromString("100%")
|
||||
}
|
||||
|
||||
if stableReplicas <= 0 {
|
||||
return intstr.FromString("0%")
|
||||
}
|
||||
|
||||
pValue := stableReplicas * 100 / allReplicas
|
||||
percent := intstr.FromString(fmt.Sprintf("%v%%", pValue))
|
||||
restoredStableReplicas, _ := intstr.GetScaledValueFromIntOrPercent(&percent, int(allReplicas), true)
|
||||
// restoredStableReplicas == 0 is un-tolerated if user-defined canaryReplicas is not 100%.
|
||||
// we must make sure that at least one canary pod is created.
|
||||
if restoredStableReplicas <= 0 && canaryReplicas.StrVal != "100%" {
|
||||
return intstr.FromString("1%")
|
||||
}
|
||||
|
||||
return percent
|
||||
}
|
||||
|
||||
// GenerateNotFoundError return a not found error
|
||||
func GenerateNotFoundError(name, resource string) error {
|
||||
return errors.NewNotFound(schema.GroupResource{Group: "apps", Resource: resource}, name)
|
||||
}
|
||||
|
||||
func ShouldWaitResume(release *v1alpha1.BatchRelease) bool {
|
||||
return release.Spec.ReleasePlan.FinalizingPolicy == v1alpha1.WaitResumeFinalizingPolicyType
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 control
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func TestParseIntegerAsPercentage(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
supposeUpper := 10000
|
||||
for allReplicas := 1; allReplicas <= supposeUpper; allReplicas++ {
|
||||
for percent := 0; percent <= 100; percent++ {
|
||||
canaryPercent := intstr.FromString(fmt.Sprintf("%v%%", percent))
|
||||
canaryReplicas, _ := intstr.GetScaledValueFromIntOrPercent(&canaryPercent, allReplicas, true)
|
||||
partition := ParseIntegerAsPercentageIfPossible(int32(allReplicas-canaryReplicas), int32(allReplicas), &canaryPercent)
|
||||
stableReplicas, _ := intstr.GetScaledValueFromIntOrPercent(&partition, allReplicas, true)
|
||||
if percent == 0 {
|
||||
Expect(stableReplicas).Should(BeNumerically("==", allReplicas))
|
||||
} else if percent == 100 {
|
||||
Expect(stableReplicas).Should(BeNumerically("==", 0))
|
||||
} else if percent > 0 {
|
||||
Expect(allReplicas - stableReplicas).To(BeNumerically(">", 0))
|
||||
}
|
||||
Expect(stableReplicas).Should(BeNumerically("<=", allReplicas))
|
||||
Expect(math.Abs(float64((allReplicas - canaryReplicas) - stableReplicas))).Should(BeNumerically("<", float64(allReplicas)*0.01))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateBatchReplicas(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := map[string]struct {
|
||||
batchReplicas intstr.IntOrString
|
||||
workloadReplicas int32
|
||||
expectedReplicas int32
|
||||
}{
|
||||
"batch: 5, replicas: 10": {
|
||||
batchReplicas: intstr.FromInt(5),
|
||||
workloadReplicas: 10,
|
||||
expectedReplicas: 5,
|
||||
},
|
||||
"batch: 20%, replicas: 10": {
|
||||
batchReplicas: intstr.FromString("20%"),
|
||||
workloadReplicas: 10,
|
||||
expectedReplicas: 2,
|
||||
},
|
||||
"batch: 100%, replicas: 10": {
|
||||
batchReplicas: intstr.FromString("100%"),
|
||||
workloadReplicas: 10,
|
||||
expectedReplicas: 10,
|
||||
},
|
||||
"batch: 200%, replicas: 10": {
|
||||
batchReplicas: intstr.FromString("200%"),
|
||||
workloadReplicas: 10,
|
||||
expectedReplicas: 10,
|
||||
},
|
||||
"batch: 200, replicas: 10": {
|
||||
batchReplicas: intstr.FromInt(200),
|
||||
workloadReplicas: 10,
|
||||
expectedReplicas: 10,
|
||||
},
|
||||
"batch: 0, replicas: 10": {
|
||||
batchReplicas: intstr.FromInt(0),
|
||||
workloadReplicas: 10,
|
||||
expectedReplicas: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
release := &v1alpha1.BatchRelease{
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: cs.batchReplicas,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
got := CalculateBatchReplicas(release, int(cs.workloadReplicas), 0)
|
||||
Expect(got).Should(BeNumerically("==", cs.expectedReplicas))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsControlledByBatchRelease(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
release := &v1alpha1.BatchRelease{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rollouts.kruise.io/v1alpha1",
|
||||
Kind: "BatchRelease",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: "test",
|
||||
Name: "test",
|
||||
Namespace: "test",
|
||||
},
|
||||
}
|
||||
|
||||
controlInfo, _ := json.Marshal(metav1.NewControllerRef(release, release.GroupVersionKind()))
|
||||
|
||||
cases := map[string]struct {
|
||||
object *apps.Deployment
|
||||
result bool
|
||||
}{
|
||||
"ownerRef": {
|
||||
object: &apps.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
*metav1.NewControllerRef(release, release.GroupVersionKind()),
|
||||
},
|
||||
},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
"annoRef": {
|
||||
object: &apps.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(controlInfo),
|
||||
},
|
||||
},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
"notRef": {
|
||||
object: &apps.Deployment{},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := IsControlledByBatchRelease(release, cs.object)
|
||||
Expect(got == cs.result).To(BeTrue())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 labelpatch
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/utils/integer"
|
||||
)
|
||||
|
||||
// FilterPodsForUnorderedUpdate can filter pods before patch pod batch label when rolling back in batches.
|
||||
// for example:
|
||||
// * There are 20 replicas: 10 updated replicas (version 2), 10 replicas (version 1), and the release plan is
|
||||
// - batch 0: 20%
|
||||
// - batch 1: 50%
|
||||
// - batch 2: 100%
|
||||
// Currently, if we decide to roll back to version 1, if you use this function, can help you just rollback
|
||||
// the pods that are really need to be rolled back according to release plan, but patch batch label according
|
||||
// original release plan, and will patch the pods that are really rolled back in priority.
|
||||
// - in batch 0: really roll back (20 - 10) * 20% = 2 pods, but 20 * 20% = 4 pod will be patched batch label;
|
||||
// - in batch 0: really roll back (20 - 10) * 50% = 5 pods, but 20 * 50% = 10 pod will be patched batch label;
|
||||
// - in batch 0: really roll back (20 - 10) * 100% = 10 pods, but 20 * 100% = 20 pod will be patched batch label;
|
||||
//
|
||||
// Mainly for PaaS platform display pod list in conveniently.
|
||||
//
|
||||
// This function only works for such unordered update strategy, such as CloneSet, Deployment, or Advanced
|
||||
// StatefulSet with unordered update strategy.
|
||||
func FilterPodsForUnorderedUpdate(pods []*corev1.Pod, ctx *batchcontext.BatchContext) []*corev1.Pod {
|
||||
var terminatingPods []*corev1.Pod
|
||||
var lowPriorityPods []*corev1.Pod
|
||||
var highPriorityPods []*corev1.Pod
|
||||
|
||||
noNeedUpdate := int32(0)
|
||||
for _, pod := range pods {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
terminatingPods = append(terminatingPods, pod)
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pod, ctx.UpdateRevision) {
|
||||
continue
|
||||
}
|
||||
if pod.Labels[util.NoNeedUpdatePodLabel] == ctx.RolloutID && pod.Labels[util.RolloutIDLabel] != ctx.RolloutID {
|
||||
noNeedUpdate++
|
||||
lowPriorityPods = append(lowPriorityPods, pod)
|
||||
} else {
|
||||
highPriorityPods = append(highPriorityPods, pod)
|
||||
}
|
||||
}
|
||||
|
||||
needUpdate := ctx.DesiredUpdatedReplicas - noNeedUpdate
|
||||
if needUpdate <= 0 { // may never occur
|
||||
return pods
|
||||
}
|
||||
|
||||
diff := ctx.PlannedUpdatedReplicas - needUpdate
|
||||
if diff <= 0 {
|
||||
return append(highPriorityPods, terminatingPods...)
|
||||
}
|
||||
|
||||
lastIndex := integer.Int32Min(diff, int32(len(lowPriorityPods)))
|
||||
return append(append(highPriorityPods, lowPriorityPods[:lastIndex]...), terminatingPods...)
|
||||
}
|
||||
|
||||
// FilterPodsForOrderedUpdate can filter pods before patch pod batch label when rolling back in batches.
|
||||
// for example:
|
||||
// * There are 20 replicas: 10 updated replicas (version 2), 10 replicas (version 1), and the release plan is
|
||||
// - batch 0: 20%
|
||||
// - batch 1: 50%
|
||||
// - batch 2: 100%
|
||||
// Currently, if we decide to roll back to version 1, if you use this function, can help you just rollback
|
||||
// the pods that are really need to be rolled back according to release plan, but patch batch label according
|
||||
// original release plan, and will patch the pods that are really rolled back in priority.
|
||||
// - in batch 0: really roll back (20 - 10) * 20% = 2 pods, but 20 * 20% = 4 pod will be patched batch label;
|
||||
// - in batch 0: really roll back (20 - 10) * 50% = 5 pods, but 20 * 50% = 10 pod will be patched batch label;
|
||||
// - in batch 0: really roll back (20 - 10) * 100% = 10 pods, but 20 * 100% = 20 pod will be patched batch label;
|
||||
//
|
||||
// Mainly for PaaS platform display pod list in conveniently.
|
||||
//
|
||||
// This function only works for such unordered update strategy, such as Native StatefulSet, and Advanced StatefulSet
|
||||
// with ordered update strategy.
|
||||
// TODO: support advanced statefulSet reserveOrdinal feature
|
||||
func FilterPodsForOrderedUpdate(pods []*corev1.Pod, ctx *batchcontext.BatchContext) []*corev1.Pod {
|
||||
var terminatingPods []*corev1.Pod
|
||||
var lowPriorityPods []*corev1.Pod
|
||||
var highPriorityPods []*corev1.Pod
|
||||
|
||||
sortPodsByOrdinal(pods)
|
||||
partition, _ := intstr.GetScaledValueFromIntOrPercent(
|
||||
&ctx.DesiredPartition, int(ctx.Replicas), true)
|
||||
for _, pod := range pods {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
terminatingPods = append(terminatingPods, pod)
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pod, ctx.UpdateRevision) {
|
||||
continue
|
||||
}
|
||||
if getPodOrdinal(pod) >= partition {
|
||||
highPriorityPods = append(highPriorityPods, pod)
|
||||
} else {
|
||||
lowPriorityPods = append(lowPriorityPods, pod)
|
||||
}
|
||||
}
|
||||
needUpdate := ctx.Replicas - int32(partition)
|
||||
if needUpdate <= 0 { // may never occur
|
||||
return pods
|
||||
}
|
||||
|
||||
diff := ctx.PlannedUpdatedReplicas - needUpdate
|
||||
if diff <= 0 {
|
||||
return append(highPriorityPods, terminatingPods...)
|
||||
}
|
||||
|
||||
lastIndex := integer.Int32Min(diff, int32(len(lowPriorityPods)))
|
||||
return append(append(highPriorityPods, lowPriorityPods[:lastIndex]...), terminatingPods...)
|
||||
}
|
||||
|
||||
func sortPodsByOrdinal(pods []*corev1.Pod) {
|
||||
sort.Slice(pods, func(i, j int) bool {
|
||||
ordI, _ := strconv.Atoi(pods[i].Name[strings.LastIndex(pods[i].Name, "-"):])
|
||||
ordJ, _ := strconv.Atoi(pods[j].Name[strings.LastIndex(pods[j].Name, "-"):])
|
||||
return ordJ > ordI
|
||||
})
|
||||
}
|
||||
|
||||
func getPodOrdinal(pod *corev1.Pod) int {
|
||||
ord, _ := strconv.Atoi(pod.Name[strings.LastIndex(pod.Name, "-")+1:])
|
||||
return ord
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 labelpatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/ginkgo"
|
||||
"github.com/onsi/gomega"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func TestFilterPodsForUnorderedRollback(t *testing.T) {
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
GetPods func() []*corev1.Pod
|
||||
ExpectWithLabels int
|
||||
ExpectWithoutLabels int
|
||||
Replicas int32
|
||||
NoNeedUpdatedReplicas int32
|
||||
PlannedUpdatedReplicas int32
|
||||
DesiredUpdatedReplicas int32
|
||||
}{
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, noNeedRollback=5, stepCanary=20%, realCanary=6",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(10, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedUpdatedReplicas: 5,
|
||||
PlannedUpdatedReplicas: 2,
|
||||
DesiredUpdatedReplicas: 6,
|
||||
ExpectWithoutLabels: 5,
|
||||
ExpectWithLabels: 1,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, noNeedRollback=5, stepCanary=60%, realCanary=8",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(10, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedUpdatedReplicas: 5,
|
||||
PlannedUpdatedReplicas: 6,
|
||||
DesiredUpdatedReplicas: 8,
|
||||
ExpectWithoutLabels: 5,
|
||||
ExpectWithLabels: 3,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, noNeedRollback=5, stepCanary=100%, realCanary=10",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(10, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedUpdatedReplicas: 5,
|
||||
PlannedUpdatedReplicas: 10,
|
||||
DesiredUpdatedReplicas: 10,
|
||||
ExpectWithoutLabels: 5,
|
||||
ExpectWithLabels: 5,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=9, noNeedRollback=7, stepCanary=20%, realCanary=6",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(9, 7)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedUpdatedReplicas: 7,
|
||||
PlannedUpdatedReplicas: 2,
|
||||
DesiredUpdatedReplicas: 6,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 7,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=8, noNeedRollback=7, stepCanary=60%, realCanary=8",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(8, 7)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedUpdatedReplicas: 7,
|
||||
PlannedUpdatedReplicas: 6,
|
||||
DesiredUpdatedReplicas: 8,
|
||||
ExpectWithoutLabels: 1,
|
||||
ExpectWithLabels: 5,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=9, noNeedRollback=7, stepCanary=100%, realCanary=10",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(9, 7)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedUpdatedReplicas: 7,
|
||||
PlannedUpdatedReplicas: 10,
|
||||
DesiredUpdatedReplicas: 10,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 7,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=6, noNeedRollback=5, stepCanary=20%, realCanary=6",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(6, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedUpdatedReplicas: 5,
|
||||
PlannedUpdatedReplicas: 2,
|
||||
DesiredUpdatedReplicas: 6,
|
||||
ExpectWithoutLabels: 1,
|
||||
ExpectWithLabels: 1,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=6, noNeedRollback=5, stepCanary=60%, realCanary=8",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(6, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedUpdatedReplicas: 5,
|
||||
PlannedUpdatedReplicas: 6,
|
||||
DesiredUpdatedReplicas: 8,
|
||||
ExpectWithoutLabels: 1,
|
||||
ExpectWithLabels: 3,
|
||||
},
|
||||
}
|
||||
|
||||
check := func(pods []*corev1.Pod, expectWith, expectWithout int) bool {
|
||||
var with, without int
|
||||
for _, pod := range pods {
|
||||
if pod.Labels[util.NoNeedUpdatePodLabel] == "0x1" {
|
||||
with++
|
||||
} else {
|
||||
without++
|
||||
}
|
||||
}
|
||||
return with == expectWith && without == expectWithout
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
pods := cs.GetPods()
|
||||
for i := 0; i < 10; i++ {
|
||||
rand.Shuffle(len(pods), func(i, j int) {
|
||||
pods[i], pods[j] = pods[j], pods[i]
|
||||
})
|
||||
batchCtx := &batchcontext.BatchContext{
|
||||
Replicas: cs.Replicas,
|
||||
RolloutID: "0x1",
|
||||
UpdateRevision: "version-1",
|
||||
PlannedUpdatedReplicas: cs.PlannedUpdatedReplicas,
|
||||
DesiredUpdatedReplicas: cs.DesiredUpdatedReplicas,
|
||||
}
|
||||
filteredPods := FilterPodsForUnorderedUpdate(pods, batchCtx)
|
||||
var podName []string
|
||||
for i := range filteredPods {
|
||||
podName = append(podName, filteredPods[i].Name)
|
||||
}
|
||||
fmt.Println(podName)
|
||||
gomega.Expect(check(filteredPods, cs.ExpectWithLabels, cs.ExpectWithoutLabels)).To(gomega.BeTrue())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPodsForOrderedRollback(t *testing.T) {
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
GetPods func() []*corev1.Pod
|
||||
ExpectWithLabels int
|
||||
ExpectWithoutLabels int
|
||||
Replicas int32
|
||||
PlannedUpdatedReplicas int32
|
||||
DesiredUpdatedReplicas int32
|
||||
}{
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, stepCanary=40%, realCanary=2",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(10, 8)
|
||||
},
|
||||
Replicas: 10,
|
||||
PlannedUpdatedReplicas: 4,
|
||||
DesiredUpdatedReplicas: 8,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 2,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, stepCanary=60%, realCanary=2",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(10, 8)
|
||||
},
|
||||
Replicas: 10,
|
||||
PlannedUpdatedReplicas: 6,
|
||||
DesiredUpdatedReplicas: 8,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 4,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, stepCanary=100%, realCanary=10",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(10, 0)
|
||||
},
|
||||
Replicas: 10,
|
||||
PlannedUpdatedReplicas: 10,
|
||||
DesiredUpdatedReplicas: 0,
|
||||
ExpectWithoutLabels: 10,
|
||||
ExpectWithLabels: 0,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=9, stepCanary=20%, realCanary=2",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePodsWith(9, 8)
|
||||
},
|
||||
Replicas: 10,
|
||||
PlannedUpdatedReplicas: 2,
|
||||
DesiredUpdatedReplicas: 8,
|
||||
ExpectWithoutLabels: 1,
|
||||
ExpectWithLabels: 0,
|
||||
},
|
||||
}
|
||||
|
||||
check := func(pods []*corev1.Pod, expectWith, expectWithout int) bool {
|
||||
var with, without int
|
||||
for _, pod := range pods {
|
||||
if pod.Labels[util.NoNeedUpdatePodLabel] == "0x1" {
|
||||
with++
|
||||
} else {
|
||||
without++
|
||||
}
|
||||
}
|
||||
return with == expectWith && without == expectWithout
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
pods := cs.GetPods()
|
||||
for i := 0; i < 10; i++ {
|
||||
rand.Shuffle(len(pods), func(i, j int) {
|
||||
pods[i], pods[j] = pods[j], pods[i]
|
||||
})
|
||||
batchCtx := &batchcontext.BatchContext{
|
||||
DesiredUpdatedReplicas: cs.PlannedUpdatedReplicas,
|
||||
DesiredPartition: intstr.FromInt(int(cs.DesiredUpdatedReplicas)),
|
||||
Replicas: cs.Replicas,
|
||||
RolloutID: "0x1",
|
||||
UpdateRevision: "version-1",
|
||||
PlannedUpdatedReplicas: cs.PlannedUpdatedReplicas,
|
||||
}
|
||||
filteredPods := FilterPodsForOrderedUpdate(pods, batchCtx)
|
||||
var podName []string
|
||||
for i := range filteredPods {
|
||||
podName = append(podName, filteredPods[i].Name)
|
||||
}
|
||||
fmt.Println(podName)
|
||||
gomega.Expect(check(filteredPods, cs.ExpectWithLabels, cs.ExpectWithoutLabels)).To(gomega.BeTrue())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortPodsByOrdinal(t *testing.T) {
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
|
||||
pods := generatePodsWith(100, 10)
|
||||
rand.Shuffle(len(pods), func(i, j int) {
|
||||
pods[i], pods[j] = pods[j], pods[i]
|
||||
})
|
||||
sortPodsByOrdinal(pods)
|
||||
for i, pod := range pods {
|
||||
expectedName := fmt.Sprintf("pod-name-%d", 99-i)
|
||||
gomega.Expect(pod.Name == expectedName).Should(gomega.BeTrue())
|
||||
}
|
||||
}
|
||||
|
||||
func generatePodsWith(updatedReplicas, noNeedRollbackReplicas int) []*corev1.Pod {
|
||||
podsNoNeed := generateLabeledPods(map[string]string{
|
||||
util.NoNeedUpdatePodLabel: "0x1",
|
||||
apps.ControllerRevisionHashLabelKey: "version-1",
|
||||
}, noNeedRollbackReplicas, 0)
|
||||
return append(generateLabeledPods(map[string]string{
|
||||
apps.ControllerRevisionHashLabelKey: "version-1",
|
||||
}, updatedReplicas-noNeedRollbackReplicas, noNeedRollbackReplicas), podsNoNeed...)
|
||||
}
|
||||
|
||||
func generateLabeledPods(labels map[string]string, replicas int, beginOrder int) []*corev1.Pod {
|
||||
pods := make([]*corev1.Pod, replicas)
|
||||
for i := 0; i < replicas; i++ {
|
||||
pods[i] = &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("pod-name-%d", beginOrder+i),
|
||||
Labels: labels,
|
||||
},
|
||||
}
|
||||
}
|
||||
return pods
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 labelpatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type LabelPatcher interface {
|
||||
PatchPodBatchLabel(ctx *batchcontext.BatchContext) error
|
||||
}
|
||||
|
||||
func NewLabelPatcher(cli client.Client, logKey klog.ObjectRef) *realPatcher {
|
||||
return &realPatcher{Client: cli, logKey: logKey}
|
||||
}
|
||||
|
||||
type realPatcher struct {
|
||||
client.Client
|
||||
logKey klog.ObjectRef
|
||||
}
|
||||
|
||||
func (r *realPatcher) PatchPodBatchLabel(ctx *batchcontext.BatchContext) error {
|
||||
if ctx.RolloutID == "" || len(ctx.Pods) == 0 {
|
||||
return nil
|
||||
}
|
||||
pods := ctx.Pods
|
||||
if ctx.FilterFunc != nil {
|
||||
pods = ctx.FilterFunc(pods, ctx)
|
||||
}
|
||||
return r.patchPodBatchLabel(pods, ctx)
|
||||
}
|
||||
|
||||
// PatchPodBatchLabel will patch rollout-id && batch-id to pods
|
||||
func (r *realPatcher) patchPodBatchLabel(pods []*corev1.Pod, ctx *batchcontext.BatchContext) error {
|
||||
// the number of active pods that has been patched successfully.
|
||||
patchedUpdatedReplicas := int32(0)
|
||||
// the number of target active pods that should be patched batch label.
|
||||
plannedUpdatedReplicas := ctx.PlannedUpdatedReplicas
|
||||
|
||||
for _, pod := range pods {
|
||||
if !util.IsConsistentWithRevision(pod, ctx.UpdateRevision) {
|
||||
continue
|
||||
}
|
||||
|
||||
podRolloutID := pod.Labels[util.RolloutIDLabel]
|
||||
if pod.DeletionTimestamp.IsZero() && podRolloutID == ctx.RolloutID {
|
||||
patchedUpdatedReplicas++
|
||||
}
|
||||
}
|
||||
|
||||
// all pods that should be patched have been patched
|
||||
if patchedUpdatedReplicas >= plannedUpdatedReplicas {
|
||||
return nil // return fast
|
||||
}
|
||||
|
||||
for _, pod := range pods {
|
||||
if pod.DeletionTimestamp.IsZero() {
|
||||
// we don't patch label for the active old revision pod
|
||||
if !util.IsConsistentWithRevision(pod, ctx.UpdateRevision) {
|
||||
continue
|
||||
}
|
||||
// we don't continue to patch if the goal is met
|
||||
if patchedUpdatedReplicas >= ctx.PlannedUpdatedReplicas {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// if it has been patched, just ignore
|
||||
if pod.Labels[util.RolloutIDLabel] == ctx.RolloutID {
|
||||
continue
|
||||
}
|
||||
|
||||
clone := util.GetEmptyObjectWithKey(pod)
|
||||
by := fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s","%s":"%d"}}}`,
|
||||
util.RolloutIDLabel, ctx.RolloutID, util.RolloutBatchIDLabel, ctx.CurrentBatch+1)
|
||||
if err := r.Patch(context.TODO(), clone, client.RawPatch(types.StrategicMergePatchType, []byte(by))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pod.DeletionTimestamp.IsZero() {
|
||||
patchedUpdatedReplicas++
|
||||
}
|
||||
klog.Infof("Successfully patch Pod(%v) batchID %d label", klog.KObj(pod), ctx.CurrentBatch+1)
|
||||
}
|
||||
|
||||
if patchedUpdatedReplicas >= plannedUpdatedReplicas {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("patched %v pods for %v, however the goal is %d", patchedUpdatedReplicas, r.logKey, plannedUpdatedReplicas)
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 labelpatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
batchcontext "github.com/openkruise/rollouts/pkg/controller/batchrelease/context"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
)
|
||||
|
||||
func init() {
|
||||
corev1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
func TestLabelPatcher(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := map[string]struct {
|
||||
batchContext func() *batchcontext.BatchContext
|
||||
expectedPatched int
|
||||
}{
|
||||
"10 pods, 0 patched, 5 new patched": {
|
||||
batchContext: func() *batchcontext.BatchContext {
|
||||
ctx := &batchcontext.BatchContext{
|
||||
RolloutID: "rollout-1",
|
||||
UpdateRevision: "version-1",
|
||||
PlannedUpdatedReplicas: 5,
|
||||
CurrentBatch: 0,
|
||||
Replicas: 10,
|
||||
}
|
||||
pods := generatePods(1, ctx.Replicas, 0, "", "", ctx.UpdateRevision)
|
||||
ctx.Pods = pods
|
||||
return ctx
|
||||
},
|
||||
expectedPatched: 5,
|
||||
},
|
||||
"10 pods, 2 patched, 3 new patched": {
|
||||
batchContext: func() *batchcontext.BatchContext {
|
||||
ctx := &batchcontext.BatchContext{
|
||||
RolloutID: "rollout-1",
|
||||
UpdateRevision: "version-1",
|
||||
PlannedUpdatedReplicas: 5,
|
||||
Replicas: 10,
|
||||
}
|
||||
pods := generatePods(1, ctx.Replicas, 2,
|
||||
ctx.RolloutID, strconv.Itoa(int(ctx.CurrentBatch)), ctx.UpdateRevision)
|
||||
ctx.Pods = pods
|
||||
return ctx
|
||||
},
|
||||
expectedPatched: 5,
|
||||
},
|
||||
"10 pods, 5 patched, 0 new patched": {
|
||||
batchContext: func() *batchcontext.BatchContext {
|
||||
ctx := &batchcontext.BatchContext{
|
||||
RolloutID: "rollout-1",
|
||||
UpdateRevision: "version-1",
|
||||
PlannedUpdatedReplicas: 5,
|
||||
Replicas: 10,
|
||||
}
|
||||
pods := generatePods(1, ctx.Replicas, 5,
|
||||
ctx.RolloutID, strconv.Itoa(int(ctx.CurrentBatch)), ctx.UpdateRevision)
|
||||
ctx.Pods = pods
|
||||
return ctx
|
||||
},
|
||||
expectedPatched: 5,
|
||||
},
|
||||
"10 pods, 7 patched, 0 new patched": {
|
||||
batchContext: func() *batchcontext.BatchContext {
|
||||
ctx := &batchcontext.BatchContext{
|
||||
RolloutID: "rollout-1",
|
||||
UpdateRevision: "version-1",
|
||||
PlannedUpdatedReplicas: 5,
|
||||
Replicas: 10,
|
||||
}
|
||||
pods := generatePods(1, ctx.Replicas, 7,
|
||||
ctx.RolloutID, strconv.Itoa(int(ctx.CurrentBatch)), ctx.UpdateRevision)
|
||||
ctx.Pods = pods
|
||||
return ctx
|
||||
},
|
||||
expectedPatched: 7,
|
||||
},
|
||||
"2 pods, 0 patched, 2 new patched": {
|
||||
batchContext: func() *batchcontext.BatchContext {
|
||||
ctx := &batchcontext.BatchContext{
|
||||
RolloutID: "rollout-1",
|
||||
UpdateRevision: "version-1",
|
||||
PlannedUpdatedReplicas: 5,
|
||||
Replicas: 10,
|
||||
}
|
||||
pods := generatePods(1, 2, 0,
|
||||
ctx.RolloutID, strconv.Itoa(int(ctx.CurrentBatch)), ctx.UpdateRevision)
|
||||
ctx.Pods = pods
|
||||
return ctx
|
||||
},
|
||||
expectedPatched: 2,
|
||||
},
|
||||
"10 pods, 3 patched with old rollout-id, 5 new patched": {
|
||||
batchContext: func() *batchcontext.BatchContext {
|
||||
ctx := &batchcontext.BatchContext{
|
||||
RolloutID: "rollout-1",
|
||||
UpdateRevision: "version-1",
|
||||
PlannedUpdatedReplicas: 5,
|
||||
Replicas: 10,
|
||||
}
|
||||
pods := generatePods(1, ctx.Replicas, 3,
|
||||
"previous-rollout-id", strconv.Itoa(int(ctx.CurrentBatch)), ctx.UpdateRevision)
|
||||
ctx.Pods = pods
|
||||
return ctx
|
||||
},
|
||||
expectedPatched: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := cs.batchContext()
|
||||
var objects []client.Object
|
||||
for _, pod := range ctx.Pods {
|
||||
objects = append(objects, pod)
|
||||
}
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
|
||||
patcher := NewLabelPatcher(cli, klog.ObjectRef{Name: "test"})
|
||||
patchErr := patcher.patchPodBatchLabel(ctx.Pods, ctx)
|
||||
|
||||
podList := &corev1.PodList{}
|
||||
err := cli.List(context.TODO(), podList)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
patched := 0
|
||||
for _, pod := range podList.Items {
|
||||
if pod.Labels[util.RolloutIDLabel] == ctx.RolloutID {
|
||||
patched++
|
||||
}
|
||||
}
|
||||
Expect(patched).Should(BeNumerically("==", cs.expectedPatched))
|
||||
if patched < int(ctx.PlannedUpdatedReplicas) {
|
||||
Expect(patchErr).To(HaveOccurred())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generatePods(ordinalBegin, ordinalEnd, labeled int32, rolloutID, batchID, version string) []*corev1.Pod {
|
||||
podsWithLabel := generateLabeledPods(map[string]string{
|
||||
util.RolloutIDLabel: rolloutID,
|
||||
util.RolloutBatchIDLabel: batchID,
|
||||
apps.ControllerRevisionHashLabelKey: version,
|
||||
}, int(labeled), int(ordinalBegin))
|
||||
|
||||
total := ordinalEnd - ordinalBegin + 1
|
||||
podsWithoutLabel := generateLabeledPods(map[string]string{
|
||||
apps.ControllerRevisionHashLabelKey: version,
|
||||
}, int(total-labeled), int(labeled+ordinalBegin))
|
||||
return append(podsWithoutLabel, podsWithLabel...)
|
||||
}
|
||||
|
|
@ -1,405 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// CloneSetRolloutController is responsible for handling rollout CloneSet type of workloads
|
||||
type CloneSetRolloutController struct {
|
||||
cloneSetController
|
||||
clone *kruiseappsv1alpha1.CloneSet
|
||||
}
|
||||
|
||||
// NewCloneSetRolloutController creates a new CloneSet rollout controller
|
||||
func NewCloneSetRolloutController(cli client.Client, recorder record.EventRecorder, release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, targetNamespacedName types.NamespacedName) *CloneSetRolloutController {
|
||||
return &CloneSetRolloutController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
client: cli,
|
||||
recorder: recorder,
|
||||
release: release,
|
||||
newStatus: newStatus,
|
||||
},
|
||||
releasePlanKey: client.ObjectKeyFromObject(release),
|
||||
targetNamespacedName: targetNamespacedName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyWorkload verifies that the workload is ready to execute release plan
|
||||
func (c *CloneSetRolloutController) VerifyWorkload() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// prepareBeforeRollback makes sure that the updated pods have been patched no-need-update label.
|
||||
// return values:
|
||||
// - bool: whether all updated pods have been patched no-need-update label;
|
||||
// - *int32: how many pods have been patched;
|
||||
// - err: whether error occurs.
|
||||
func (c *CloneSetRolloutController) prepareBeforeRollback() (bool, *int32, error) {
|
||||
if c.release.Annotations[util.RollbackInBatchAnnotation] != "true" {
|
||||
return true, nil, nil
|
||||
}
|
||||
|
||||
noNeedRollbackReplicas := int32(0)
|
||||
rolloutID := c.release.Spec.ReleasePlan.RolloutID
|
||||
if rolloutID == "" {
|
||||
return true, &noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
pods, err := util.ListOwnedPods(c.client, c.clone)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to list pods for CloneSet %v", c.targetNamespacedName)
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
updateRevision := c.clone.Status.UpdateRevision
|
||||
var filterPods []*v1.Pod
|
||||
for i := range pods {
|
||||
if !pods[i].DeletionTimestamp.IsZero() {
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pods[i], updateRevision) {
|
||||
continue
|
||||
}
|
||||
if id, ok := pods[i].Labels[util.NoNeedUpdatePodLabel]; ok && id == rolloutID {
|
||||
noNeedRollbackReplicas++
|
||||
continue
|
||||
}
|
||||
filterPods = append(filterPods, pods[i])
|
||||
}
|
||||
|
||||
if len(filterPods) == 0 {
|
||||
return true, &noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
for _, pod := range filterPods {
|
||||
podClone := pod.DeepCopy()
|
||||
body := fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s"}}}`, util.NoNeedUpdatePodLabel, rolloutID)
|
||||
err = c.client.Patch(context.TODO(), podClone, client.RawPatch(types.StrategicMergePatchType, []byte(body)))
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to patch rollback labels[%s]=%s to pod %v", util.NoNeedUpdatePodLabel, rolloutID, client.ObjectKeyFromObject(pod))
|
||||
return false, &noNeedRollbackReplicas, err
|
||||
} else {
|
||||
klog.Info("Succeeded to patch rollback labels[%s]=%s to pod %v", util.NoNeedUpdatePodLabel, rolloutID, client.ObjectKeyFromObject(pod))
|
||||
}
|
||||
noNeedRollbackReplicas++
|
||||
}
|
||||
klog.Infof("BatchRelease(%v) find %v replicas no need to rollback", c.releasePlanKey, noNeedRollbackReplicas)
|
||||
return false, &noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
// PrepareBeforeProgress makes sure that the source and target CloneSet is under our control
|
||||
func (c *CloneSetRolloutController) PrepareBeforeProgress() (bool, *int32, error) {
|
||||
if err := c.fetchCloneSet(); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
done, noNeedRollbackReplicas, err := c.prepareBeforeRollback()
|
||||
if err != nil || !done {
|
||||
return false, noNeedRollbackReplicas, err
|
||||
}
|
||||
|
||||
// claim the cloneSet is under our control
|
||||
if _, err := c.claimCloneSet(c.clone); err != nil {
|
||||
return false, noNeedRollbackReplicas, err
|
||||
}
|
||||
|
||||
// record revisions and replicas info to BatchRelease.Status
|
||||
c.recordCloneSetRevisionAndReplicas()
|
||||
|
||||
c.recorder.Event(c.release, v1.EventTypeNormal, "InitializedSuccessfully", "Rollout resource are initialized")
|
||||
return true, noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
// UpgradeOneBatch calculates the number of pods we can upgrade once according to the rollout spec
|
||||
// and then set the partition accordingly
|
||||
func (c *CloneSetRolloutController) UpgradeOneBatch() (bool, error) {
|
||||
if err := c.fetchCloneSet(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if c.newStatus.ObservedWorkloadReplicas == 0 {
|
||||
klog.Infof("BatchRelease(%v) observed workload replicas is 0, no need to upgrade", c.releasePlanKey)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if the workload status is untrustworthy
|
||||
if c.clone.Status.ObservedGeneration != c.clone.Generation {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var pods []*v1.Pod
|
||||
if c.release.Spec.ReleasePlan.RolloutID != "" {
|
||||
pods, err = util.ListOwnedPods(c.client, c.clone)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to list pods for CloneSet %v", c.targetNamespacedName)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
var noNeedRollbackReplicas int32
|
||||
if c.newStatus.CanaryStatus.NoNeedUpdateReplicas != nil {
|
||||
noNeedRollbackReplicas = countNoNeedRollbackReplicas(pods, c.newStatus.UpdateRevision, c.release.Spec.ReleasePlan.RolloutID)
|
||||
c.newStatus.CanaryStatus.NoNeedUpdateReplicas = pointer.Int32(noNeedRollbackReplicas)
|
||||
}
|
||||
|
||||
updatedReplicas := c.clone.Status.UpdatedReplicas
|
||||
replicas := c.newStatus.ObservedWorkloadReplicas
|
||||
currentBatch := c.newStatus.CanaryStatus.CurrentBatch
|
||||
partitionedStableReplicas, _ := intstr.GetValueFromIntOrPercent(c.clone.Spec.UpdateStrategy.Partition, int(replicas), true)
|
||||
|
||||
// the number of canary pods should have in current batch in plan
|
||||
plannedBatchCanaryReplicas := c.calculateCurrentCanary(c.newStatus.ObservedWorkloadReplicas)
|
||||
// the number of canary pods that consider rollback context and other real-world situations
|
||||
expectedBatchCanaryReplicas := c.calculateCurrentCanary(replicas - noNeedRollbackReplicas)
|
||||
// the number of canary pods that consider rollback context and other real-world situations
|
||||
expectedBatchStableReplicas := replicas - noNeedRollbackReplicas - expectedBatchCanaryReplicas
|
||||
|
||||
// if canaryReplicas is int, then we use int;
|
||||
// if canaryReplicas is percentage, then we use percentage.
|
||||
var expectedPartition intstr.IntOrString
|
||||
canaryIntOrStr := c.release.Spec.ReleasePlan.Batches[currentBatch].CanaryReplicas
|
||||
if canaryIntOrStr.Type == intstr.Int {
|
||||
expectedPartition = intstr.FromInt(int(expectedBatchStableReplicas))
|
||||
} else if c.newStatus.ObservedWorkloadReplicas > 0 {
|
||||
expectedPartition = ParseIntegerAsPercentageIfPossible(expectedBatchStableReplicas, c.newStatus.ObservedWorkloadReplicas, &canaryIntOrStr)
|
||||
}
|
||||
|
||||
klog.V(3).InfoS("upgraded one batch, current info:",
|
||||
"BatchRelease", c.releasePlanKey,
|
||||
"currentBatch", currentBatch,
|
||||
"replicas", replicas,
|
||||
"updatedReplicas", updatedReplicas,
|
||||
"noNeedRollbackReplicas", noNeedRollbackReplicas,
|
||||
"partitionedStableReplicas", partitionedStableReplicas,
|
||||
"plannedBatchCanaryReplicas", plannedBatchCanaryReplicas,
|
||||
"expectedBatchCanaryReplicas", expectedBatchCanaryReplicas,
|
||||
"expectedBatchStableReplicas", expectedBatchStableReplicas,
|
||||
"expectedPartition", expectedPartition)
|
||||
|
||||
if err := c.patchCloneSetPartition(c.clone, &expectedPartition); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
patchDone, err := c.patchPodBatchLabel(pods, plannedBatchCanaryReplicas, expectedBatchStableReplicas)
|
||||
if !patchDone || err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
c.recorder.Eventf(c.release, v1.EventTypeNormal, "SetBatchDone", "Finished submitting all upgrade quests for batch %d", c.newStatus.CanaryStatus.CurrentBatch)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchReady checks to see if the pods are all available according to the rollout plan
|
||||
func (c *CloneSetRolloutController) CheckOneBatchReady() (bool, error) {
|
||||
if err := c.fetchCloneSet(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if the workload status is untrustworthy
|
||||
if c.clone.Status.ObservedGeneration != c.clone.Generation {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
rolloutID := c.release.Spec.ReleasePlan.RolloutID
|
||||
|
||||
var err error
|
||||
var pods []*v1.Pod
|
||||
// if rolloutID is not set, no need to list pods,
|
||||
// because we cannot patch correct batch label to pod.
|
||||
if rolloutID != "" {
|
||||
pods, err = util.ListOwnedPods(c.client, c.clone)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
var noNeedRollbackReplicas int32
|
||||
if c.newStatus.CanaryStatus.NoNeedUpdateReplicas != nil {
|
||||
noNeedRollbackReplicas = countNoNeedRollbackReplicas(pods, c.newStatus.UpdateRevision, c.release.Spec.ReleasePlan.RolloutID)
|
||||
c.newStatus.CanaryStatus.NoNeedUpdateReplicas = pointer.Int32(noNeedRollbackReplicas)
|
||||
}
|
||||
|
||||
replicas := *c.clone.Spec.Replicas
|
||||
// the number of updated pods
|
||||
updatedReplicas := c.clone.Status.UpdatedReplicas
|
||||
// the number of updated ready pods
|
||||
updatedReadyReplicas := c.clone.Status.UpdatedReadyReplicas
|
||||
|
||||
// current batch id
|
||||
currentBatch := c.newStatus.CanaryStatus.CurrentBatch
|
||||
// the number of canary pods should have in current batch in plan
|
||||
plannedUpdatedReplicas := c.calculateCurrentCanary(c.newStatus.ObservedWorkloadReplicas)
|
||||
// the number of pods will be partitioned by cloneSet
|
||||
partitionedStableReplicas, _ := intstr.GetValueFromIntOrPercent(c.clone.Spec.UpdateStrategy.Partition, int(replicas), true)
|
||||
// the number of canary pods that consider rollback context and other real-world situations
|
||||
expectedUpdatedReplicas := c.calculateCurrentCanary(replicas - noNeedRollbackReplicas)
|
||||
// the number of stable pods that consider rollback context and other real-world situations
|
||||
expectedStableReplicas := replicas - noNeedRollbackReplicas - expectedUpdatedReplicas
|
||||
// the number of canary pods that cloneSet will be upgraded
|
||||
realDesiredUpdatedReplicas := CalculateRealCanaryReplicasGoal(expectedStableReplicas, replicas, &c.release.Spec.ReleasePlan.Batches[currentBatch].CanaryReplicas)
|
||||
|
||||
klog.V(3).InfoS("check one batch, current info:",
|
||||
"BatchRelease", c.releasePlanKey,
|
||||
"currentBatch", currentBatch,
|
||||
"replicas", replicas,
|
||||
"updatedReplicas", updatedReplicas,
|
||||
"noNeedRollbackReplicas", noNeedRollbackReplicas,
|
||||
"partitionedStableReplicas", partitionedStableReplicas,
|
||||
"expectedUpdatedReplicas", expectedUpdatedReplicas,
|
||||
"realDesiredUpdatedReplicas", realDesiredUpdatedReplicas,
|
||||
"expectedStableReplicas", expectedStableReplicas)
|
||||
|
||||
if !isBatchReady(c.release, pods, c.clone.Spec.UpdateStrategy.MaxUnavailable,
|
||||
plannedUpdatedReplicas, realDesiredUpdatedReplicas, updatedReplicas, updatedReadyReplicas) {
|
||||
klog.Infof("BatchRelease(%v) batch is not ready yet, current batch=%d", klog.KObj(c.release), currentBatch)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
klog.Infof("BatchRelease(%v) batch is ready, current batch=%d", klog.KObj(c.release), currentBatch)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FinalizeProgress makes sure the CloneSet is all upgraded
|
||||
func (c *CloneSetRolloutController) FinalizeProgress(cleanup bool) (bool, error) {
|
||||
if err := c.fetchCloneSet(); client.IgnoreNotFound(err) != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, err := c.releaseCloneSet(c.clone, cleanup); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
c.recorder.Eventf(c.release, v1.EventTypeNormal, "FinalizedSuccessfully", "Rollout resource are finalized: cleanup=%v", cleanup)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SyncWorkloadInfo return change type if workload was changed during release
|
||||
func (c *CloneSetRolloutController) SyncWorkloadInfo() (WorkloadEventType, *util.WorkloadInfo, error) {
|
||||
// ignore the sync if the release plan is deleted
|
||||
if c.release.DeletionTimestamp != nil {
|
||||
return IgnoreWorkloadEvent, nil, nil
|
||||
}
|
||||
|
||||
if err := c.fetchCloneSet(); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return WorkloadHasGone, nil, err
|
||||
}
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// in case that the cloneSet status is untrustworthy
|
||||
if c.clone.Status.ObservedGeneration != c.clone.Generation {
|
||||
klog.Warningf("CloneSet(%v) is still reconciling, waiting for it to complete, generation: %v, observed: %v",
|
||||
c.targetNamespacedName, c.clone.Generation, c.clone.Status.ObservedGeneration)
|
||||
return WorkloadStillReconciling, nil, nil
|
||||
}
|
||||
|
||||
workloadInfo := &util.WorkloadInfo{
|
||||
Status: &util.WorkloadStatus{
|
||||
UpdatedReplicas: c.clone.Status.UpdatedReplicas,
|
||||
UpdatedReadyReplicas: c.clone.Status.UpdatedReadyReplicas,
|
||||
UpdateRevision: c.clone.Status.UpdateRevision,
|
||||
StableRevision: c.clone.Status.CurrentRevision,
|
||||
},
|
||||
}
|
||||
|
||||
// in case of that the updated revision of the workload is promoted
|
||||
if c.clone.Status.UpdatedReplicas == c.clone.Status.Replicas {
|
||||
return IgnoreWorkloadEvent, workloadInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload is scaling
|
||||
if *c.clone.Spec.Replicas != c.newStatus.ObservedWorkloadReplicas && c.newStatus.ObservedWorkloadReplicas != -1 {
|
||||
workloadInfo.Replicas = c.clone.Spec.Replicas
|
||||
klog.Warningf("CloneSet(%v) replicas changed during releasing, should pause and wait for it to complete, "+
|
||||
"replicas from: %v -> %v", c.targetNamespacedName, c.newStatus.ObservedWorkloadReplicas, *c.clone.Spec.Replicas)
|
||||
return WorkloadReplicasChanged, workloadInfo, nil
|
||||
}
|
||||
|
||||
// updateRevision == CurrentRevision means CloneSet is rolling back or newly-created.
|
||||
if c.clone.Status.UpdateRevision == c.clone.Status.CurrentRevision &&
|
||||
// stableRevision == UpdateRevision means CloneSet is rolling back instead of newly-created.
|
||||
c.newStatus.StableRevision == c.clone.Status.UpdateRevision &&
|
||||
// StableRevision != observed UpdateRevision means the rollback event have not been observed.
|
||||
c.newStatus.StableRevision != c.newStatus.UpdateRevision {
|
||||
klog.Warningf("CloneSet(%v) is rolling back in batches", c.targetNamespacedName)
|
||||
return WorkloadRollbackInBatch, workloadInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload was changed
|
||||
if c.clone.Status.UpdateRevision != c.newStatus.UpdateRevision {
|
||||
klog.Warningf("CloneSet(%v) updateRevision changed during releasing, should try to restart the release plan, "+
|
||||
"updateRevision from: %v -> %v", c.targetNamespacedName, c.newStatus.UpdateRevision, c.clone.Status.UpdateRevision)
|
||||
return WorkloadPodTemplateChanged, workloadInfo, nil
|
||||
}
|
||||
|
||||
return IgnoreWorkloadEvent, workloadInfo, nil
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
The functions below are helper functions
|
||||
------------------------------------- */
|
||||
// fetchCloneSet fetch cloneSet to c.clone
|
||||
func (c *CloneSetRolloutController) fetchCloneSet() error {
|
||||
clone := &kruiseappsv1alpha1.CloneSet{}
|
||||
if err := c.client.Get(context.TODO(), c.targetNamespacedName, clone); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
c.recorder.Event(c.release, v1.EventTypeWarning, "GetCloneSetFailed", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
c.clone = clone
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CloneSetRolloutController) recordCloneSetRevisionAndReplicas() {
|
||||
c.newStatus.ObservedWorkloadReplicas = *c.clone.Spec.Replicas
|
||||
c.newStatus.StableRevision = c.clone.Status.CurrentRevision
|
||||
c.newStatus.UpdateRevision = c.clone.Status.UpdateRevision
|
||||
}
|
||||
|
||||
func (c *CloneSetRolloutController) patchPodBatchLabel(pods []*v1.Pod, plannedBatchCanaryReplicas, expectedBatchStableReplicas int32) (bool, error) {
|
||||
rolloutID := c.release.Spec.ReleasePlan.RolloutID
|
||||
if rolloutID == "" || len(pods) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
updateRevision := c.release.Status.UpdateRevision
|
||||
batchID := c.release.Status.CanaryStatus.CurrentBatch + 1
|
||||
if c.newStatus.CanaryStatus.NoNeedUpdateReplicas != nil {
|
||||
pods = filterPodsForUnorderedRollback(pods, plannedBatchCanaryReplicas, expectedBatchStableReplicas, c.release.Status.ObservedWorkloadReplicas, rolloutID, updateRevision)
|
||||
}
|
||||
return patchPodBatchLabel(c.client, pods, rolloutID, batchID, updateRevision, plannedBatchCanaryReplicas, c.releasePlanKey)
|
||||
}
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// cloneSetController is the place to hold fields needed for handle CloneSet type of workloads
|
||||
type cloneSetController struct {
|
||||
workloadController
|
||||
releasePlanKey types.NamespacedName
|
||||
targetNamespacedName types.NamespacedName
|
||||
}
|
||||
|
||||
// add the parent controller to the owner of the deployment, unpause it and initialize the size
|
||||
// before kicking start the update and start from every pod in the old version
|
||||
func (c *cloneSetController) claimCloneSet(clone *kruiseappsv1alpha1.CloneSet) (bool, error) {
|
||||
var controlled bool
|
||||
if controlInfo, ok := clone.Annotations[util.BatchReleaseControlAnnotation]; ok && controlInfo != "" {
|
||||
ref := &metav1.OwnerReference{}
|
||||
err := json.Unmarshal([]byte(controlInfo), ref)
|
||||
if err == nil && ref.UID == c.release.UID {
|
||||
controlled = true
|
||||
klog.V(3).Infof("CloneSet(%v) has been controlled by this BatchRelease(%v), no need to claim again",
|
||||
c.targetNamespacedName, c.releasePlanKey)
|
||||
} else {
|
||||
klog.Errorf("Failed to parse controller info from CloneSet(%v) annotation, error: %v, controller info: %+v",
|
||||
c.targetNamespacedName, err, *ref)
|
||||
}
|
||||
}
|
||||
|
||||
patch := map[string]interface{}{}
|
||||
switch {
|
||||
// if the cloneSet has been claimed by this release
|
||||
case controlled:
|
||||
// make sure paused=false
|
||||
if clone.Spec.UpdateStrategy.Paused {
|
||||
patch = map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"updateStrategy": map[string]interface{}{
|
||||
"paused": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
patch = map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"updateStrategy": map[string]interface{}{
|
||||
"partition": &intstr.IntOrString{Type: intstr.String, StrVal: "100%"},
|
||||
"paused": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
controlInfo := metav1.NewControllerRef(c.release, c.release.GetObjectKind().GroupVersionKind())
|
||||
controlByte, _ := json.Marshal(controlInfo)
|
||||
patch["metadata"] = map[string]interface{}{
|
||||
"annotations": map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(controlByte),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(patch) > 0 {
|
||||
cloneObj := clone.DeepCopy()
|
||||
patchByte, _ := json.Marshal(patch)
|
||||
if err := c.client.Patch(context.TODO(), cloneObj, client.RawPatch(types.MergePatchType, patchByte)); err != nil {
|
||||
c.recorder.Eventf(c.release, v1.EventTypeWarning, "ClaimCloneSetFailed", err.Error())
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(3).Infof("Claim CloneSet(%v) Successfully", c.targetNamespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// remove the parent controller from the deployment's owner list
|
||||
func (c *cloneSetController) releaseCloneSet(clone *kruiseappsv1alpha1.CloneSet, cleanup bool) (bool, error) {
|
||||
if clone == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var found bool
|
||||
var refByte string
|
||||
if refByte, found = clone.Annotations[util.BatchReleaseControlAnnotation]; found && refByte != "" {
|
||||
ref := &metav1.OwnerReference{}
|
||||
if err := json.Unmarshal([]byte(refByte), ref); err != nil {
|
||||
found = false
|
||||
klog.Errorf("failed to decode controller annotations of BatchRelease")
|
||||
} else if ref.UID != c.release.UID {
|
||||
found = false
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
klog.V(3).Infof("the CloneSet(%v) is already released", c.targetNamespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
cloneObj := clone.DeepCopy()
|
||||
patchByte := []byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":null}}}`, util.BatchReleaseControlAnnotation))
|
||||
if err := c.client.Patch(context.TODO(), cloneObj, client.RawPatch(types.MergePatchType, patchByte)); err != nil {
|
||||
c.recorder.Eventf(c.release, v1.EventTypeWarning, "ReleaseCloneSetFailed", err.Error())
|
||||
return false, err
|
||||
}
|
||||
|
||||
klog.V(3).Infof("Release CloneSet(%v) Successfully", c.targetNamespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// scale the deployment
|
||||
func (c *cloneSetController) patchCloneSetPartition(clone *kruiseappsv1alpha1.CloneSet, partition *intstr.IntOrString) error {
|
||||
if reflect.DeepEqual(clone.Spec.UpdateStrategy.Partition, partition) {
|
||||
return nil
|
||||
}
|
||||
|
||||
patch := map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"updateStrategy": map[string]interface{}{
|
||||
"partition": partition,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cloneObj := clone.DeepCopy()
|
||||
patchByte, _ := json.Marshal(patch)
|
||||
if err := c.client.Patch(context.TODO(), cloneObj, client.RawPatch(types.MergePatchType, patchByte)); err != nil {
|
||||
c.recorder.Eventf(c.release, v1.EventTypeWarning, "PatchPartitionFailed",
|
||||
"Failed to update the CloneSet(%v) to the correct target partition %d, error: %v",
|
||||
c.targetNamespacedName, partition, err)
|
||||
return err
|
||||
}
|
||||
|
||||
klog.InfoS("Submitted modified partition quest for CloneSet", "CloneSet", c.targetNamespacedName,
|
||||
"target partition size", partition, "batch", c.newStatus.CanaryStatus.CurrentBatch)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// the canary workload size for the current batch
|
||||
func (c *cloneSetController) calculateCurrentCanary(totalSize int32) int32 {
|
||||
targetSize := int32(calculateNewBatchTarget(&c.release.Spec.ReleasePlan, int(totalSize), int(c.newStatus.CanaryStatus.CurrentBatch)))
|
||||
klog.InfoS("Calculated the number of pods in the target CloneSet after current batch",
|
||||
"CloneSet", c.targetNamespacedName, "BatchRelease", c.releasePlanKey,
|
||||
"current batch", c.newStatus.CanaryStatus.CurrentBatch, "workload updateRevision size", targetSize)
|
||||
return targetSize
|
||||
}
|
||||
|
||||
// the source workload size for the current batch
|
||||
func (c *cloneSetController) calculateCurrentStable(totalSize int32) int32 {
|
||||
sourceSize := totalSize - c.calculateCurrentCanary(totalSize)
|
||||
klog.InfoS("Calculated the number of pods in the source CloneSet after current batch",
|
||||
"CloneSet", c.targetNamespacedName, "BatchRelease", c.releasePlanKey,
|
||||
"current batch", c.newStatus.CanaryStatus.CurrentBatch, "workload stableRevision size", sourceSize)
|
||||
return sourceSize
|
||||
}
|
||||
|
||||
// ParseIntegerAsPercentageIfPossible will return a percentage type IntOrString, such as "20%", "33%", but "33.3%" is illegal.
|
||||
// Given A, B, return P that should try best to satisfy ⌈P * B⌉ == A, and we ensure that the error is less than 1%.
|
||||
// For examples:
|
||||
// * Given stableReplicas 1, allReplicas 3, return "33%";
|
||||
// * Given stableReplicas 98, allReplicas 99, return "97%";
|
||||
// * Given stableReplicas 1, allReplicas 101, return "1";
|
||||
func ParseIntegerAsPercentageIfPossible(stableReplicas, allReplicas int32, canaryReplicas *intstr.IntOrString) intstr.IntOrString {
|
||||
if stableReplicas >= allReplicas {
|
||||
return intstr.FromString("100%")
|
||||
}
|
||||
|
||||
if stableReplicas <= 0 {
|
||||
return intstr.FromString("0%")
|
||||
}
|
||||
|
||||
pValue := stableReplicas * 100 / allReplicas
|
||||
percent := intstr.FromString(fmt.Sprintf("%v%%", pValue))
|
||||
restoredStableReplicas, _ := intstr.GetScaledValueFromIntOrPercent(&percent, int(allReplicas), true)
|
||||
// restoredStableReplicas == 0 is un-tolerated if user-defined canaryReplicas is not 100%.
|
||||
// we must make sure that at least one canary pod is created.
|
||||
if restoredStableReplicas <= 0 && canaryReplicas.StrVal != "100%" {
|
||||
return intstr.FromString("1%")
|
||||
}
|
||||
|
||||
return percent
|
||||
}
|
||||
|
||||
func CalculateRealCanaryReplicasGoal(expectedStableReplicas, allReplicas int32, canaryReplicas *intstr.IntOrString) int32 {
|
||||
if canaryReplicas.Type == intstr.Int {
|
||||
return allReplicas - expectedStableReplicas
|
||||
}
|
||||
partition := ParseIntegerAsPercentageIfPossible(expectedStableReplicas, allReplicas, canaryReplicas)
|
||||
realStableReplicas, _ := intstr.GetScaledValueFromIntOrPercent(&partition, int(allReplicas), true)
|
||||
return allReplicas - int32(realStableReplicas)
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
apimachineryruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme *runtime.Scheme
|
||||
releaseClone = &v1alpha1.BatchRelease{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: v1alpha1.GroupVersion.String(),
|
||||
Kind: "BatchRelease",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "release",
|
||||
Namespace: "application",
|
||||
UID: uuid.NewUUID(),
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
TargetRef: v1alpha1.ObjectRef{
|
||||
WorkloadRef: &v1alpha1.WorkloadRef{
|
||||
APIVersion: "apps.kruise.io/v1alpha1",
|
||||
Kind: "CloneSet",
|
||||
Name: "sample",
|
||||
},
|
||||
},
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("10%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("50%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("80%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
stableClone = &kruiseappsv1alpha1.CloneSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: kruiseappsv1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "CloneSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "sample",
|
||||
Namespace: "application",
|
||||
UID: types.UID("87076677"),
|
||||
Generation: 1,
|
||||
Labels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"something": "whatever",
|
||||
},
|
||||
},
|
||||
Spec: kruiseappsv1alpha1.CloneSetSpec{
|
||||
Replicas: pointer.Int32Ptr(100),
|
||||
UpdateStrategy: kruiseappsv1alpha1.CloneSetUpdateStrategy{
|
||||
Partition: &intstr.IntOrString{Type: intstr.Int, IntVal: int32(1)},
|
||||
MaxSurge: &intstr.IntOrString{Type: intstr.Int, IntVal: int32(2)},
|
||||
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: int32(2)},
|
||||
},
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: containers("v2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: kruiseappsv1alpha1.CloneSetStatus{
|
||||
Replicas: 100,
|
||||
ReadyReplicas: 100,
|
||||
UpdatedReplicas: 0,
|
||||
UpdatedReadyReplicas: 0,
|
||||
ObservedGeneration: 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
scheme = runtime.NewScheme()
|
||||
apimachineryruntime.Must(apps.AddToScheme(scheme))
|
||||
apimachineryruntime.Must(v1alpha1.AddToScheme(scheme))
|
||||
apimachineryruntime.Must(kruiseappsv1alpha1.AddToScheme(scheme))
|
||||
|
||||
canaryTemplate := stableClone.Spec.Template.DeepCopy()
|
||||
stableTemplate := canaryTemplate.DeepCopy()
|
||||
stableTemplate.Spec.Containers = containers("v1")
|
||||
stableClone.Status.CurrentRevision = util.ComputeHash(stableTemplate, nil)
|
||||
stableClone.Status.UpdateRevision = util.ComputeHash(canaryTemplate, nil)
|
||||
}
|
||||
|
||||
func TestCloneSetController(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Paused bool
|
||||
Cleanup bool
|
||||
}{
|
||||
{
|
||||
Name: "paused=true, cleanup=true",
|
||||
Paused: true,
|
||||
Cleanup: true,
|
||||
},
|
||||
{
|
||||
Name: "paused=true, cleanup=false",
|
||||
Paused: true,
|
||||
Cleanup: false,
|
||||
},
|
||||
{
|
||||
Name: "paused=false cleanup=true",
|
||||
Paused: false,
|
||||
Cleanup: true,
|
||||
},
|
||||
{
|
||||
Name: "paused=false , cleanup=false",
|
||||
Paused: false,
|
||||
Cleanup: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(releaseClone.DeepCopy(), stableClone.DeepCopy()).Build()
|
||||
rec := record.NewFakeRecorder(100)
|
||||
c := cloneSetController{
|
||||
workloadController: workloadController{
|
||||
client: cli,
|
||||
recorder: rec,
|
||||
release: releaseClone,
|
||||
newStatus: &releaseClone.Status,
|
||||
},
|
||||
targetNamespacedName: client.ObjectKeyFromObject(stableClone),
|
||||
}
|
||||
oldObject := &kruiseappsv1alpha1.CloneSet{}
|
||||
Expect(cli.Get(context.TODO(), c.targetNamespacedName, oldObject)).NotTo(HaveOccurred())
|
||||
succeed, err := c.claimCloneSet(oldObject.DeepCopy())
|
||||
Expect(succeed).Should(BeTrue())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
newObject := &kruiseappsv1alpha1.CloneSet{}
|
||||
Expect(cli.Get(context.TODO(), c.targetNamespacedName, newObject)).NotTo(HaveOccurred())
|
||||
succeed, err = c.releaseCloneSet(newObject.DeepCopy(), cs.Cleanup)
|
||||
Expect(succeed).Should(BeTrue())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
newObject = &kruiseappsv1alpha1.CloneSet{}
|
||||
Expect(cli.Get(context.TODO(), c.targetNamespacedName, newObject)).NotTo(HaveOccurred())
|
||||
newObject.Spec.UpdateStrategy.Paused = oldObject.Spec.UpdateStrategy.Paused
|
||||
newObject.Spec.UpdateStrategy.Partition = oldObject.Spec.UpdateStrategy.Partition
|
||||
Expect(reflect.DeepEqual(oldObject.Spec, newObject.Spec)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(oldObject.Labels, newObject.Labels)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(oldObject.Finalizers, newObject.Finalizers)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(oldObject.Annotations, newObject.Annotations)).Should(BeTrue())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntegerAsPercentage(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
supposeUpper := 10000
|
||||
for allReplicas := 1; allReplicas <= supposeUpper; allReplicas++ {
|
||||
for percent := 0; percent <= 100; percent++ {
|
||||
canaryPercent := intstr.FromString(fmt.Sprintf("%v%%", percent))
|
||||
canaryReplicas, _ := intstr.GetScaledValueFromIntOrPercent(&canaryPercent, allReplicas, true)
|
||||
partition := ParseIntegerAsPercentageIfPossible(int32(allReplicas-canaryReplicas), int32(allReplicas), &canaryPercent)
|
||||
stableReplicas, _ := intstr.GetScaledValueFromIntOrPercent(&partition, allReplicas, true)
|
||||
if percent == 0 {
|
||||
Expect(stableReplicas).Should(BeNumerically("==", allReplicas))
|
||||
} else if percent == 100 {
|
||||
Expect(stableReplicas).Should(BeNumerically("==", 0))
|
||||
} else if percent > 0 {
|
||||
Expect(allReplicas - stableReplicas).To(BeNumerically(">", 0))
|
||||
}
|
||||
Expect(stableReplicas).Should(BeNumerically("<=", allReplicas))
|
||||
Expect(math.Abs(float64((allReplicas - canaryReplicas) - stableReplicas))).Should(BeNumerically("<", float64(allReplicas)*0.01))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
package workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/integer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func filterPodsForUnorderedRollback(pods []*corev1.Pod, plannedBatchCanaryReplicas, expectedBatchStableReplicas, replicas int32, rolloutID, updateRevision string) []*corev1.Pod {
|
||||
var noNeedRollbackReplicas int32
|
||||
var realNeedRollbackReplicas int32
|
||||
var expectedRollbackReplicas int32 // total need rollback
|
||||
|
||||
var terminatingPods []*corev1.Pod
|
||||
var needRollbackPods []*corev1.Pod
|
||||
var noNeedRollbackPods []*corev1.Pod
|
||||
|
||||
for _, pod := range pods {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
terminatingPods = append(terminatingPods, pod)
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pod, updateRevision) {
|
||||
continue
|
||||
}
|
||||
podRolloutID := pod.Labels[util.RolloutIDLabel]
|
||||
podRollbackID := pod.Labels[util.NoNeedUpdatePodLabel]
|
||||
if podRollbackID == rolloutID && podRolloutID != rolloutID {
|
||||
noNeedRollbackReplicas++
|
||||
noNeedRollbackPods = append(noNeedRollbackPods, pod)
|
||||
} else {
|
||||
needRollbackPods = append(needRollbackPods, pod)
|
||||
}
|
||||
}
|
||||
|
||||
expectedRollbackReplicas = replicas - expectedBatchStableReplicas
|
||||
realNeedRollbackReplicas = expectedRollbackReplicas - noNeedRollbackReplicas
|
||||
if realNeedRollbackReplicas <= 0 { // may never occur
|
||||
return pods
|
||||
}
|
||||
|
||||
diff := plannedBatchCanaryReplicas - realNeedRollbackReplicas
|
||||
if diff <= 0 {
|
||||
return append(needRollbackPods, terminatingPods...)
|
||||
}
|
||||
|
||||
lastIndex := integer.Int32Min(diff, int32(len(noNeedRollbackPods)))
|
||||
return append(append(needRollbackPods, noNeedRollbackPods[:lastIndex]...), terminatingPods...)
|
||||
}
|
||||
|
||||
// TODO: support advanced statefulSet reserveOrdinal feature
|
||||
func filterPodsForOrderedRollback(pods []*corev1.Pod, plannedBatchCanaryReplicas, expectedBatchStableReplicas, replicas int32, rolloutID, updateRevision string) []*corev1.Pod {
|
||||
var terminatingPods []*corev1.Pod
|
||||
var needRollbackPods []*corev1.Pod
|
||||
var noNeedRollbackPods []*corev1.Pod
|
||||
|
||||
sortPodsByOrdinal(pods)
|
||||
for _, pod := range pods {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
terminatingPods = append(terminatingPods, pod)
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pod, updateRevision) {
|
||||
continue
|
||||
}
|
||||
if getPodOrdinal(pod) >= int(expectedBatchStableReplicas) {
|
||||
needRollbackPods = append(needRollbackPods, pod)
|
||||
} else {
|
||||
noNeedRollbackPods = append(noNeedRollbackPods, pod)
|
||||
}
|
||||
}
|
||||
realNeedRollbackReplicas := replicas - expectedBatchStableReplicas
|
||||
if realNeedRollbackReplicas <= 0 { // may never occur
|
||||
return pods
|
||||
}
|
||||
|
||||
diff := plannedBatchCanaryReplicas - realNeedRollbackReplicas
|
||||
if diff <= 0 {
|
||||
return append(needRollbackPods, terminatingPods...)
|
||||
}
|
||||
|
||||
lastIndex := integer.Int32Min(diff, int32(len(noNeedRollbackPods)))
|
||||
return append(append(needRollbackPods, noNeedRollbackPods[:lastIndex]...), terminatingPods...)
|
||||
}
|
||||
|
||||
func countNoNeedRollbackReplicas(pods []*corev1.Pod, updateRevision, rolloutID string) int32 {
|
||||
noNeedRollbackReplicas := int32(0)
|
||||
for _, pod := range pods {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pod, updateRevision) {
|
||||
continue
|
||||
}
|
||||
id, ok := pod.Labels[util.NoNeedUpdatePodLabel]
|
||||
if ok && id == rolloutID {
|
||||
noNeedRollbackReplicas++
|
||||
}
|
||||
}
|
||||
return noNeedRollbackReplicas
|
||||
}
|
||||
|
||||
// patchPodBatchLabel will patch rollout-id && batch-id to pods
|
||||
func patchPodBatchLabel(c client.Client, pods []*corev1.Pod, rolloutID string, batchID int32, updateRevision string, replicas int32, logKey types.NamespacedName) (bool, error) {
|
||||
// the number of active pods that has been patched successfully.
|
||||
patchedUpdatedReplicas := int32(0)
|
||||
for _, pod := range pods {
|
||||
if !util.IsConsistentWithRevision(pod, updateRevision) {
|
||||
continue
|
||||
}
|
||||
|
||||
podRolloutID := pod.Labels[util.RolloutIDLabel]
|
||||
if pod.DeletionTimestamp.IsZero() && podRolloutID == rolloutID {
|
||||
patchedUpdatedReplicas++
|
||||
}
|
||||
}
|
||||
|
||||
for _, pod := range pods {
|
||||
podRolloutID := pod.Labels[util.RolloutIDLabel]
|
||||
if pod.DeletionTimestamp.IsZero() {
|
||||
// we don't patch label for the active old revision pod
|
||||
if !util.IsConsistentWithRevision(pod, updateRevision) {
|
||||
continue
|
||||
}
|
||||
// we don't continue to patch if the goal is met
|
||||
if patchedUpdatedReplicas >= replicas {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// if it has been patched, just ignore
|
||||
if podRolloutID == rolloutID {
|
||||
continue
|
||||
}
|
||||
|
||||
podClone := pod.DeepCopy()
|
||||
by := fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s","%s":"%d"}}}`, util.RolloutIDLabel, rolloutID, util.RolloutBatchIDLabel, batchID)
|
||||
err := c.Patch(context.TODO(), podClone, client.RawPatch(types.StrategicMergePatchType, []byte(by)))
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to patch Pod(%v) batchID, err: %v", client.ObjectKeyFromObject(pod), err)
|
||||
return false, err
|
||||
} else {
|
||||
klog.Infof("Succeed to patch Pod(%v) batchID, err: %v", client.ObjectKeyFromObject(pod), err)
|
||||
}
|
||||
|
||||
if pod.DeletionTimestamp.IsZero() {
|
||||
patchedUpdatedReplicas++
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(3).Infof("Patch %v pods with batchID for batchRelease %v, goal is %d pods", patchedUpdatedReplicas, logKey, replicas)
|
||||
return patchedUpdatedReplicas >= replicas, nil
|
||||
}
|
||||
|
||||
func releaseWorkload(c client.Client, object client.Object) error {
|
||||
_, found := object.GetAnnotations()[util.BatchReleaseControlAnnotation]
|
||||
if !found {
|
||||
klog.V(3).Infof("Workload(%v) is already released", client.ObjectKeyFromObject(object))
|
||||
return nil
|
||||
}
|
||||
|
||||
clone := object.DeepCopyObject().(client.Object)
|
||||
patchByte := []byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":null}}}`, util.BatchReleaseControlAnnotation))
|
||||
return c.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, patchByte))
|
||||
}
|
||||
|
||||
func claimWorkload(c client.Client, planController *v1alpha1.BatchRelease, object client.Object, patchUpdateStrategy map[string]interface{}) error {
|
||||
if controlInfo, ok := object.GetAnnotations()[util.BatchReleaseControlAnnotation]; ok && controlInfo != "" {
|
||||
ref := &metav1.OwnerReference{}
|
||||
err := json.Unmarshal([]byte(controlInfo), ref)
|
||||
if err == nil && ref.UID == planController.UID {
|
||||
klog.V(3).Infof("Workload(%v) has been controlled by this BatchRelease(%v), no need to claim again",
|
||||
client.ObjectKeyFromObject(object), client.ObjectKeyFromObject(planController))
|
||||
return nil
|
||||
} else {
|
||||
klog.Errorf("Failed to parse controller info from Workload(%v) annotation, error: %v, controller info: %+v",
|
||||
client.ObjectKeyFromObject(object), err, *ref)
|
||||
}
|
||||
}
|
||||
|
||||
controlInfo, _ := json.Marshal(metav1.NewControllerRef(planController, planController.GetObjectKind().GroupVersionKind()))
|
||||
patch := map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(controlInfo),
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"updateStrategy": patchUpdateStrategy,
|
||||
},
|
||||
}
|
||||
|
||||
patchByte, _ := json.Marshal(patch)
|
||||
clone := object.DeepCopyObject().(client.Object)
|
||||
return c.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, patchByte))
|
||||
}
|
||||
|
||||
func patchSpec(c client.Client, object client.Object, spec map[string]interface{}) error {
|
||||
patchByte, err := json.Marshal(map[string]interface{}{"spec": spec})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clone := object.DeepCopyObject().(client.Object)
|
||||
return c.Patch(context.TODO(), clone, client.RawPatch(types.MergePatchType, patchByte))
|
||||
}
|
||||
|
||||
func calculateNewBatchTarget(rolloutSpec *v1alpha1.ReleasePlan, workloadReplicas, currentBatch int) int {
|
||||
batchSize, _ := intstr.GetValueFromIntOrPercent(&rolloutSpec.Batches[currentBatch].CanaryReplicas, workloadReplicas, true)
|
||||
if batchSize > workloadReplicas {
|
||||
klog.Warningf("releasePlan has wrong batch replicas, batches[%d].replicas %v is more than workload.replicas %v", currentBatch, batchSize, workloadReplicas)
|
||||
batchSize = workloadReplicas
|
||||
} else if batchSize < 0 {
|
||||
klog.Warningf("releasePlan has wrong batch replicas, batches[%d].replicas %v is less than 0 %v", currentBatch, batchSize)
|
||||
batchSize = 0
|
||||
}
|
||||
|
||||
klog.V(3).InfoS("calculated the number of new pod size", "current batch", currentBatch, "new pod target", batchSize)
|
||||
return batchSize
|
||||
}
|
||||
|
||||
func sortPodsByOrdinal(pods []*corev1.Pod) {
|
||||
sort.Slice(pods, func(i, j int) bool {
|
||||
ordI, _ := strconv.Atoi(pods[i].Name[strings.LastIndex(pods[i].Name, "-"):])
|
||||
ordJ, _ := strconv.Atoi(pods[j].Name[strings.LastIndex(pods[j].Name, "-"):])
|
||||
return ordJ > ordI
|
||||
})
|
||||
}
|
||||
|
||||
func getPodOrdinal(pod *corev1.Pod) int {
|
||||
ord, _ := strconv.Atoi(pod.Name[strings.LastIndex(pod.Name, "-")+1:])
|
||||
return ord
|
||||
}
|
||||
|
||||
func failureThreshold(threshold, maxUnavailable *intstr.IntOrString, replicas int32) int32 {
|
||||
globalThreshold := 0
|
||||
if threshold != nil {
|
||||
globalThreshold, _ = intstr.GetScaledValueFromIntOrPercent(threshold, int(replicas), true)
|
||||
} else if maxUnavailable != nil {
|
||||
globalThreshold, _ = intstr.GetScaledValueFromIntOrPercent(maxUnavailable, int(replicas), true)
|
||||
}
|
||||
return int32(integer.IntMax(0, globalThreshold))
|
||||
}
|
||||
|
||||
func isBatchReady(release *v1alpha1.BatchRelease, pods []*corev1.Pod, maxUnavailable *intstr.IntOrString, labelDesired, desired, updated, updatedReady int32) bool {
|
||||
updateRevision := release.Status.UpdateRevision
|
||||
if updatedReady <= 0 { // Some workloads, such as StatefulSet, may not have such field
|
||||
updatedReady = int32(util.WrappedPodCount(pods, func(pod *corev1.Pod) bool {
|
||||
return pod.DeletionTimestamp.IsZero() && util.IsConsistentWithRevision(pod, updateRevision) && util.IsPodReady(pod)
|
||||
}))
|
||||
}
|
||||
|
||||
rolloutID := release.Spec.ReleasePlan.RolloutID
|
||||
threshold := failureThreshold(release.Spec.ReleasePlan.FailureThreshold, maxUnavailable, updated)
|
||||
podReady := updated >= desired && updatedReady+threshold >= desired && (desired == 0 || updatedReady > 0)
|
||||
return podReady && isPodBatchLabelSatisfied(pods, rolloutID, labelDesired)
|
||||
}
|
||||
|
||||
func isPodBatchLabelSatisfied(pods []*corev1.Pod, rolloutID string, targetCount int32) bool {
|
||||
if len(rolloutID) == 0 || len(pods) == 0 {
|
||||
return true
|
||||
}
|
||||
labeledCount := int32(0)
|
||||
for _, pod := range pods {
|
||||
if !pod.DeletionTimestamp.IsZero() {
|
||||
continue
|
||||
}
|
||||
if pod.Labels[util.RolloutIDLabel] == rolloutID {
|
||||
labeledCount++
|
||||
}
|
||||
}
|
||||
return labeledCount >= targetCount
|
||||
}
|
||||
|
|
@ -1,438 +0,0 @@
|
|||
package workloads
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func TestFilterPodsForUnorderedRollback(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
GetPods func() []*corev1.Pod
|
||||
ExpectWithLabels int
|
||||
ExpectWithoutLabels int
|
||||
Replicas int32
|
||||
NoNeedRollbackReplicas int32
|
||||
PlannedBatchCanaryReplicas int32
|
||||
ExpectedBatchStableReplicas int32
|
||||
}{
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, noNeedRollback=5, stepCanary=20%, realCanary=6",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(10, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedRollbackReplicas: 5,
|
||||
PlannedBatchCanaryReplicas: 2,
|
||||
ExpectedBatchStableReplicas: 4,
|
||||
ExpectWithoutLabels: 5,
|
||||
ExpectWithLabels: 1,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, noNeedRollback=5, stepCanary=60%, realCanary=8",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(10, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedRollbackReplicas: 5,
|
||||
PlannedBatchCanaryReplicas: 6,
|
||||
ExpectedBatchStableReplicas: 2,
|
||||
ExpectWithoutLabels: 5,
|
||||
ExpectWithLabels: 3,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, noNeedRollback=5, stepCanary=100%, realCanary=10",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(10, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedRollbackReplicas: 5,
|
||||
PlannedBatchCanaryReplicas: 10,
|
||||
ExpectedBatchStableReplicas: 0,
|
||||
ExpectWithoutLabels: 5,
|
||||
ExpectWithLabels: 5,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=9, noNeedRollback=7, stepCanary=20%, realCanary=6",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(9, 7)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedRollbackReplicas: 7,
|
||||
PlannedBatchCanaryReplicas: 2,
|
||||
ExpectedBatchStableReplicas: 2,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 1,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=9, noNeedRollback=7, stepCanary=60%, realCanary=8",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(9, 7)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedRollbackReplicas: 7,
|
||||
PlannedBatchCanaryReplicas: 6,
|
||||
ExpectedBatchStableReplicas: 1,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 4,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=9, noNeedRollback=7, stepCanary=100%, realCanary=10",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(9, 7)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedRollbackReplicas: 7,
|
||||
PlannedBatchCanaryReplicas: 10,
|
||||
ExpectedBatchStableReplicas: 0,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 7,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=6, noNeedRollback=5, stepCanary=20%, realCanary=6",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(6, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedRollbackReplicas: 5,
|
||||
PlannedBatchCanaryReplicas: 2,
|
||||
ExpectedBatchStableReplicas: 4,
|
||||
ExpectWithoutLabels: 1,
|
||||
ExpectWithLabels: 1,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=6, noNeedRollback=5, stepCanary=60%, realCanary=8",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(6, 5)
|
||||
},
|
||||
Replicas: 10,
|
||||
NoNeedRollbackReplicas: 5,
|
||||
PlannedBatchCanaryReplicas: 6,
|
||||
ExpectedBatchStableReplicas: 2,
|
||||
ExpectWithoutLabels: 1,
|
||||
ExpectWithLabels: 3,
|
||||
},
|
||||
}
|
||||
|
||||
check := func(pods []*corev1.Pod, expectWith, expectWithout int) bool {
|
||||
var with, without int
|
||||
for _, pod := range pods {
|
||||
if pod.Labels[util.NoNeedUpdatePodLabel] == "0x1" {
|
||||
with++
|
||||
} else {
|
||||
without++
|
||||
}
|
||||
}
|
||||
return with == expectWith && without == expectWithout
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
pods := cs.GetPods()
|
||||
for i := 0; i < 10; i++ {
|
||||
rand.Shuffle(len(pods), func(i, j int) {
|
||||
pods[i], pods[j] = pods[j], pods[i]
|
||||
})
|
||||
filteredPods := filterPodsForUnorderedRollback(pods, cs.PlannedBatchCanaryReplicas, cs.ExpectedBatchStableReplicas, cs.Replicas, "0x1", "version-1")
|
||||
var podName []string
|
||||
for i := range filteredPods {
|
||||
podName = append(podName, filteredPods[i].Name)
|
||||
}
|
||||
fmt.Println(podName)
|
||||
Expect(check(filteredPods, cs.ExpectWithLabels, cs.ExpectWithoutLabels)).To(BeTrue())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPodsForOrderedRollback(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
GetPods func() []*corev1.Pod
|
||||
ExpectWithLabels int
|
||||
ExpectWithoutLabels int
|
||||
Replicas int32
|
||||
PlannedBatchCanaryReplicas int32
|
||||
ExpectedBatchStableReplicas int32
|
||||
}{
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, stepCanary=40%, realCanary=2",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(10, 8)
|
||||
},
|
||||
Replicas: 10,
|
||||
PlannedBatchCanaryReplicas: 4,
|
||||
ExpectedBatchStableReplicas: 8,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 2,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, stepCanary=60%, realCanary=2",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(10, 8)
|
||||
},
|
||||
Replicas: 10,
|
||||
PlannedBatchCanaryReplicas: 6,
|
||||
ExpectedBatchStableReplicas: 8,
|
||||
ExpectWithoutLabels: 2,
|
||||
ExpectWithLabels: 4,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=10, stepCanary=100%, realCanary=10",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(10, 0)
|
||||
},
|
||||
Replicas: 10,
|
||||
PlannedBatchCanaryReplicas: 10,
|
||||
ExpectedBatchStableReplicas: 0,
|
||||
ExpectWithoutLabels: 10,
|
||||
ExpectWithLabels: 0,
|
||||
},
|
||||
{
|
||||
Name: "replicas=10, updatedReplicas=9, stepCanary=20%, realCanary=2",
|
||||
GetPods: func() []*corev1.Pod {
|
||||
return generatePods(9, 8)
|
||||
},
|
||||
Replicas: 10,
|
||||
PlannedBatchCanaryReplicas: 2,
|
||||
ExpectedBatchStableReplicas: 8,
|
||||
ExpectWithoutLabels: 1,
|
||||
ExpectWithLabels: 0,
|
||||
},
|
||||
}
|
||||
|
||||
check := func(pods []*corev1.Pod, expectWith, expectWithout int) bool {
|
||||
var with, without int
|
||||
for _, pod := range pods {
|
||||
if pod.Labels[util.NoNeedUpdatePodLabel] == "0x1" {
|
||||
with++
|
||||
} else {
|
||||
without++
|
||||
}
|
||||
}
|
||||
return with == expectWith && without == expectWithout
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
pods := cs.GetPods()
|
||||
for i := 0; i < 10; i++ {
|
||||
rand.Shuffle(len(pods), func(i, j int) {
|
||||
pods[i], pods[j] = pods[j], pods[i]
|
||||
})
|
||||
filteredPods := filterPodsForOrderedRollback(pods, cs.PlannedBatchCanaryReplicas, cs.ExpectedBatchStableReplicas, cs.Replicas, "0x1", "version-1")
|
||||
var podName []string
|
||||
for i := range filteredPods {
|
||||
podName = append(podName, filteredPods[i].Name)
|
||||
}
|
||||
fmt.Println(podName)
|
||||
Expect(check(filteredPods, cs.ExpectWithLabels, cs.ExpectWithoutLabels)).To(BeTrue())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBatchReady(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
p := func(f intstr.IntOrString) *intstr.IntOrString {
|
||||
return &f
|
||||
}
|
||||
r := func(f *intstr.IntOrString, id, revision string) *v1alpha1.BatchRelease {
|
||||
return &v1alpha1.BatchRelease{
|
||||
Spec: v1alpha1.BatchReleaseSpec{ReleasePlan: v1alpha1.ReleasePlan{RolloutID: id, FailureThreshold: f}},
|
||||
Status: v1alpha1.BatchReleaseStatus{UpdateRevision: revision},
|
||||
}
|
||||
}
|
||||
cases := map[string]struct {
|
||||
release *v1alpha1.BatchRelease
|
||||
pods []*corev1.Pod
|
||||
maxUnavailable *intstr.IntOrString
|
||||
labelDesired int32
|
||||
desired int32
|
||||
updated int32
|
||||
updatedReady int32
|
||||
result bool
|
||||
}{
|
||||
"ready: no-rollout-id, all pod ready": {
|
||||
release: r(p(intstr.FromInt(1)), "", "v2"),
|
||||
pods: nil,
|
||||
maxUnavailable: p(intstr.FromInt(1)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 5,
|
||||
result: true,
|
||||
},
|
||||
"ready: no-rollout-id, tolerated failed pods": {
|
||||
release: r(p(intstr.FromInt(1)), "", "v2"),
|
||||
pods: nil,
|
||||
maxUnavailable: p(intstr.FromInt(1)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 4,
|
||||
result: true,
|
||||
},
|
||||
"false: no-rollout-id, un-tolerated failed pods": {
|
||||
release: r(p(intstr.FromInt(1)), "", "v2"),
|
||||
pods: nil,
|
||||
maxUnavailable: p(intstr.FromInt(5)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 3,
|
||||
result: false,
|
||||
},
|
||||
"false: no-rollout-id, tolerated failed pods, but 1 pod isn't updated": {
|
||||
release: r(p(intstr.FromString("60%")), "", "v2"),
|
||||
pods: nil,
|
||||
maxUnavailable: p(intstr.FromInt(5)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 4,
|
||||
updatedReady: 4,
|
||||
result: false,
|
||||
},
|
||||
"false: no-rollout-id, tolerated, but no-pod-ready": {
|
||||
release: r(p(intstr.FromInt(100)), "", "v2"),
|
||||
pods: nil,
|
||||
maxUnavailable: p(intstr.FromInt(5)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 0,
|
||||
result: false,
|
||||
},
|
||||
"true: no-rollout-id, tolerated failed pods, failureThreshold=nil": {
|
||||
release: r(nil, "", "v2"),
|
||||
pods: nil,
|
||||
maxUnavailable: p(intstr.FromInt(3)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 3,
|
||||
result: true,
|
||||
},
|
||||
"false: no-rollout-id, un-tolerated failed pods, failureThreshold=nil": {
|
||||
release: r(nil, "", "v2"),
|
||||
pods: nil,
|
||||
maxUnavailable: p(intstr.FromInt(1)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 3,
|
||||
result: false,
|
||||
},
|
||||
"true: rollout-id, labeled pods satisfied": {
|
||||
release: r(p(intstr.FromInt(1)), "1", "version-1"),
|
||||
pods: generatePods(5, 0),
|
||||
maxUnavailable: p(intstr.FromInt(5)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 5,
|
||||
result: true,
|
||||
},
|
||||
"false: rollout-id, labeled pods not satisfied": {
|
||||
release: r(p(intstr.FromInt(1)), "1", "version-1"),
|
||||
pods: generatePods(3, 0),
|
||||
maxUnavailable: p(intstr.FromInt(5)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 5,
|
||||
result: false,
|
||||
},
|
||||
"true: rollout-id, no updated-ready field": {
|
||||
release: r(p(intstr.FromInt(1)), "1", "version-1"),
|
||||
pods: generatePods(5, 0),
|
||||
maxUnavailable: p(intstr.FromInt(5)),
|
||||
labelDesired: 5,
|
||||
desired: 5,
|
||||
updated: 5,
|
||||
updatedReady: 0,
|
||||
result: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, cs := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := isBatchReady(cs.release, cs.pods, cs.maxUnavailable, cs.labelDesired, cs.desired, cs.updated, cs.updatedReady)
|
||||
fmt.Printf("%v %v", got, cs.result)
|
||||
Expect(got).To(Equal(cs.result))
|
||||
fmt.Printf("%v %v", got, cs.result)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortPodsByOrdinal(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
pods := generatePods(100, 10)
|
||||
rand.Shuffle(len(pods), func(i, j int) {
|
||||
pods[i], pods[j] = pods[j], pods[i]
|
||||
})
|
||||
sortPodsByOrdinal(pods)
|
||||
for i, pod := range pods {
|
||||
expectedName := fmt.Sprintf("pod-name-%d", 99-i)
|
||||
Expect(pod.Name == expectedName).Should(BeTrue())
|
||||
}
|
||||
}
|
||||
|
||||
func generatePods(updatedReplicas, noNeedRollbackReplicas int) []*corev1.Pod {
|
||||
podsNoNeed := generatePodsWith(map[string]string{
|
||||
util.NoNeedUpdatePodLabel: "0x1",
|
||||
util.RolloutIDLabel: "1",
|
||||
apps.ControllerRevisionHashLabelKey: "version-1",
|
||||
}, noNeedRollbackReplicas, 0)
|
||||
return append(generatePodsWith(map[string]string{
|
||||
util.RolloutIDLabel: "1",
|
||||
apps.ControllerRevisionHashLabelKey: "version-1",
|
||||
}, updatedReplicas-noNeedRollbackReplicas, noNeedRollbackReplicas), podsNoNeed...)
|
||||
}
|
||||
|
||||
func generatePodsWith(labels map[string]string, replicas int, beginOrder int) []*corev1.Pod {
|
||||
pods := make([]*corev1.Pod, replicas)
|
||||
for i := 0; i < replicas; i++ {
|
||||
pods[i] = &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("pod-name-%d", beginOrder+i),
|
||||
Labels: labels,
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Conditions: []corev1.PodCondition{
|
||||
{
|
||||
Type: corev1.PodReady,
|
||||
Status: corev1.ConditionTrue,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return pods
|
||||
}
|
||||
|
||||
func containers(version string) []corev1.Container {
|
||||
return []corev1.Container{
|
||||
{
|
||||
Name: "busybox",
|
||||
Image: fmt.Sprintf("busybox:%v", version),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type WorkloadEventType string
|
||||
|
||||
const (
|
||||
// IgnoreWorkloadEvent means workload event should be ignored.
|
||||
IgnoreWorkloadEvent WorkloadEventType = "workload-event-ignore"
|
||||
// WorkloadPodTemplateChanged means workload revision changed, should be stopped to execute batch release plan.
|
||||
WorkloadPodTemplateChanged WorkloadEventType = "workload-pod-template-changed"
|
||||
// WorkloadReplicasChanged means workload is scaling during rollout, should recalculate upgraded pods in current batch.
|
||||
WorkloadReplicasChanged WorkloadEventType = "workload-replicas-changed"
|
||||
// WorkloadStillReconciling means workload status is untrusted Untrustworthy, we should wait workload controller to reconcile.
|
||||
WorkloadStillReconciling WorkloadEventType = "workload-is-reconciling"
|
||||
// WorkloadHasGone means workload is deleted during rollout, we should do something finalizing works if this event occurs.
|
||||
WorkloadHasGone WorkloadEventType = "workload-has-gone"
|
||||
// WorkloadUnHealthy means workload is at some unexpected state that our controller cannot handle, we should stop reconcile.
|
||||
WorkloadUnHealthy WorkloadEventType = "workload-is-unhealthy"
|
||||
// WorkloadRollbackInBatch means workload is rollback according to BatchRelease batch plan.
|
||||
WorkloadRollbackInBatch WorkloadEventType = "workload-rollback-in-batch"
|
||||
)
|
||||
|
||||
type workloadController struct {
|
||||
client client.Client
|
||||
recorder record.EventRecorder
|
||||
newStatus *v1alpha1.BatchReleaseStatus
|
||||
release *v1alpha1.BatchRelease
|
||||
}
|
||||
|
||||
// WorkloadController is the interface that all type of cloneSet controller implements
|
||||
type WorkloadController interface {
|
||||
// VerifyWorkload makes sure that the workload can be upgraded according to the release plan.
|
||||
// it returns 'true', if this verification is successful.
|
||||
// it returns 'false' or err != nil, if this verification is failed.
|
||||
// it returns not-empty error if the verification has something wrong, and should not retry.
|
||||
VerifyWorkload() (bool, error)
|
||||
|
||||
// PrepareBeforeProgress make sure that the resource is ready to be progressed.
|
||||
// this function is tasked to do any initialization work on the resources.
|
||||
// it returns 'true' if the preparation is succeeded.
|
||||
// it returns 'false' if the preparation should retry.
|
||||
// it returns not-empty error if the preparation has something wrong, and should not retry.
|
||||
PrepareBeforeProgress() (bool, *int32, error)
|
||||
|
||||
// UpgradeOneBatch tries to upgrade old replicas following the release plan.
|
||||
// it will upgrade the old replicas as the release plan allows in the current batch.
|
||||
// it returns 'true' if the progress is succeeded.
|
||||
// it returns 'false' if the progress should retry.
|
||||
// it returns not-empty error if the progress has something wrong, and should not retry.
|
||||
UpgradeOneBatch() (bool, error)
|
||||
|
||||
// CheckOneBatchReady checks how many replicas are ready to serve requests in the current batch.
|
||||
// it returns 'true' if the batch has been ready.
|
||||
// it returns 'false' if the batch should be reset and recheck.
|
||||
// it returns not-empty error if the check operation has something wrong, and should not retry.
|
||||
CheckOneBatchReady() (bool, error)
|
||||
|
||||
// FinalizeProgress makes sure the resources are in a good final state.
|
||||
// It might depend on if the rollout succeeded or not.
|
||||
// For example, we may remove the objects which created by batchRelease.
|
||||
// this function will always retry util it returns 'true'.
|
||||
// parameters:
|
||||
// - pause: 'nil' means keep current state, 'true' means pause workload, 'false' means do not pause workload
|
||||
// - cleanup: 'true' means clean up canary settings, 'false' means do not clean up.
|
||||
FinalizeProgress(cleanup bool) (bool, error)
|
||||
|
||||
// SyncWorkloadInfo will watch and compare the status recorded in BatchRelease.Status
|
||||
// and the real-time workload info. If workload status is inconsistent with that recorded
|
||||
// in release.status, will return the corresponding WorkloadEventType and info.
|
||||
SyncWorkloadInfo() (WorkloadEventType, *util.WorkloadInfo, error)
|
||||
}
|
||||
|
|
@ -1,351 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
utilclient "github.com/openkruise/rollouts/pkg/util/client"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
// DeploymentsRolloutController is responsible for handling Deployment type of workloads
|
||||
type DeploymentsRolloutController struct {
|
||||
deploymentController
|
||||
stable *apps.Deployment
|
||||
canary *apps.Deployment
|
||||
}
|
||||
|
||||
// NewDeploymentRolloutController creates a new Deployment rollout controller
|
||||
func NewDeploymentRolloutController(cli client.Client, recorder record.EventRecorder, release *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, stableNamespacedName types.NamespacedName) *DeploymentsRolloutController {
|
||||
return &DeploymentsRolloutController{
|
||||
deploymentController: deploymentController{
|
||||
workloadController: workloadController{
|
||||
client: cli,
|
||||
recorder: recorder,
|
||||
release: release,
|
||||
newStatus: newStatus,
|
||||
},
|
||||
stableNamespacedName: stableNamespacedName,
|
||||
canaryNamespacedName: stableNamespacedName,
|
||||
releaseKey: client.ObjectKeyFromObject(release),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyWorkload verifies that the workload is ready to execute release plan
|
||||
func (c *DeploymentsRolloutController) VerifyWorkload() (bool, error) {
|
||||
// claim the deployment is under our control, and create canary deployment if it needs.
|
||||
// Do not move this function to Preparing phase, otherwise multi canary deployments
|
||||
// will be repeatedly created due to informer cache latency.
|
||||
if _, err := c.claimDeployment(c.stable, c.canary); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
c.recorder.Event(c.release, v1.EventTypeNormal, "Verified", "ReleasePlan and the Deployment resource are verified")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PrepareBeforeProgress makes sure that the Deployment is under our control
|
||||
func (c *DeploymentsRolloutController) PrepareBeforeProgress() (bool, *int32, error) {
|
||||
// the workload is verified, and we should record revision and replicas info before progressing
|
||||
if err := c.recordDeploymentRevisionAndReplicas(); err != nil {
|
||||
klog.Errorf("Failed to record deployment(%v) revision and replicas info, error: %v", c.stableNamespacedName, err)
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
c.recorder.Event(c.release, v1.EventTypeNormal, "Initialized", "Rollout resource are initialized")
|
||||
return true, nil, nil
|
||||
}
|
||||
|
||||
// UpgradeOneBatch calculates the number of pods we can upgrade once
|
||||
// according to the release plan and then set the canary deployment replicas
|
||||
func (c *DeploymentsRolloutController) UpgradeOneBatch() (bool, error) {
|
||||
if err := c.fetchStableDeployment(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := c.fetchCanaryDeployment(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// canary replicas now we have at current state
|
||||
currentCanaryReplicas := *c.canary.Spec.Replicas
|
||||
|
||||
// canary goal we should achieve
|
||||
canaryGoal := c.calculateCurrentCanary(c.newStatus.ObservedWorkloadReplicas)
|
||||
|
||||
klog.V(3).InfoS("upgraded one batch, but no need to update replicas of canary Deployment",
|
||||
"Deployment", client.ObjectKeyFromObject(c.canary),
|
||||
"BatchRelease", c.releaseKey,
|
||||
"current-batch", c.newStatus.CanaryStatus.CurrentBatch,
|
||||
"canary-goal", canaryGoal,
|
||||
"current-canary-replicas", currentCanaryReplicas,
|
||||
"current-canary-status-replicas", c.canary.Status.UpdatedReplicas)
|
||||
|
||||
if err := c.patchDeploymentReplicas(c.canary, canaryGoal); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// patch current batch label to pods
|
||||
patchDone, err := c.patchPodBatchLabel(canaryGoal)
|
||||
if !patchDone || err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
c.recorder.Eventf(c.release, v1.EventTypeNormal, "Batch Rollout", "Finished submitting all upgrade quests for batch %d", c.newStatus.CanaryStatus.CurrentBatch)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchReady checks to see if the pods are all available according to the rollout plan
|
||||
func (c *DeploymentsRolloutController) CheckOneBatchReady() (bool, error) {
|
||||
if err := c.fetchStableDeployment(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := c.fetchCanaryDeployment(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// in case of workload status is Untrustworthy
|
||||
if c.canary.Status.ObservedGeneration != c.canary.Generation {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// canary pods that have been created
|
||||
canaryPodCount := c.canary.Status.Replicas
|
||||
// canary pods that have been available
|
||||
availableCanaryPodCount := c.canary.Status.AvailableReplicas
|
||||
// canary goal that should have in current batch
|
||||
canaryGoal := c.calculateCurrentCanary(c.newStatus.ObservedWorkloadReplicas)
|
||||
// max unavailable of deployment
|
||||
var maxUnavailable *intstr.IntOrString
|
||||
if c.canary.Spec.Strategy.RollingUpdate != nil {
|
||||
maxUnavailable = c.canary.Spec.Strategy.RollingUpdate.MaxUnavailable
|
||||
}
|
||||
|
||||
var err error
|
||||
var pods []*v1.Pod
|
||||
// if rolloutID is not set, no need to list pods,
|
||||
// because we cannot patch correct batch label to pod.
|
||||
if c.release.Spec.ReleasePlan.RolloutID != "" {
|
||||
pods, err = util.ListOwnedPods(c.client, c.canary)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
klog.InfoS("checking the batch releasing progress",
|
||||
"BatchRelease", c.releaseKey,
|
||||
"len(pods)", len(pods),
|
||||
"canary-goal", canaryGoal,
|
||||
"current-batch", c.newStatus.CanaryStatus.CurrentBatch,
|
||||
"canary-available-pod-count", availableCanaryPodCount,
|
||||
"stable-pod-status-replicas", c.stable.Status.Replicas)
|
||||
|
||||
if !isBatchReady(c.release, pods, maxUnavailable, canaryGoal, canaryGoal, canaryPodCount, availableCanaryPodCount) {
|
||||
klog.Infof("BatchRelease(%v) batch is not ready yet, current batch=%d", c.releaseKey, c.newStatus.CanaryStatus.CurrentBatch)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
klog.Infof("BatchRelease(%v) batch is ready, current batch=%d", c.releaseKey, c.newStatus.CanaryStatus.CurrentBatch)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FinalizeProgress makes sure restore deployments and clean up some canary settings
|
||||
func (c *DeploymentsRolloutController) FinalizeProgress(cleanup bool) (bool, error) {
|
||||
if err := c.fetchStableDeployment(); client.IgnoreNotFound(err) != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// make the deployment ride out of our control, and clean up canary resources
|
||||
succeed, err := c.releaseDeployment(c.stable, cleanup)
|
||||
if !succeed || err != nil {
|
||||
klog.Errorf("Failed to finalize deployment(%v), error: %v", c.stableNamespacedName, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
c.recorder.Eventf(c.release, v1.EventTypeNormal, "Finalized", "Finalized: cleanup=%v", cleanup)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SyncWorkloadInfo return workloadInfo if workload info is changed during rollout
|
||||
// TODO: abstract a WorkloadEventTypeJudge interface for these following `if` clauses
|
||||
func (c *DeploymentsRolloutController) SyncWorkloadInfo() (WorkloadEventType, *util.WorkloadInfo, error) {
|
||||
// ignore the sync if the release plan is deleted
|
||||
if c.release.DeletionTimestamp != nil {
|
||||
return IgnoreWorkloadEvent, nil, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
err = c.fetchStableDeployment()
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return WorkloadHasGone, nil, err
|
||||
}
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
err = c.fetchCanaryDeployment()
|
||||
if client.IgnoreNotFound(err) != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
workloadInfo := util.NewWorkloadInfo()
|
||||
if c.canary != nil {
|
||||
workloadInfo.Status = &util.WorkloadStatus{
|
||||
UpdatedReplicas: c.canary.Status.Replicas,
|
||||
UpdatedReadyReplicas: c.canary.Status.AvailableReplicas,
|
||||
}
|
||||
}
|
||||
|
||||
// in case of that the canary deployment is being deleted but still have the finalizer, it is out of our expectation
|
||||
if c.canary != nil && c.canary.DeletionTimestamp != nil && controllerutil.ContainsFinalizer(c.canary, util.CanaryDeploymentFinalizer) {
|
||||
return WorkloadUnHealthy, workloadInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload status is trustworthy
|
||||
if c.stable.Status.ObservedGeneration != c.stable.Generation {
|
||||
klog.Warningf("Deployment(%v) is still reconciling, waiting for it to complete, generation: %v, observed: %v",
|
||||
c.stableNamespacedName, c.stable.Generation, c.stable.Status.ObservedGeneration)
|
||||
return WorkloadStillReconciling, workloadInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload has been promoted
|
||||
if !c.stable.Spec.Paused && c.stable.Status.UpdatedReplicas == c.stable.Status.Replicas {
|
||||
return IgnoreWorkloadEvent, workloadInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload is scaling up/down
|
||||
if *c.stable.Spec.Replicas != c.newStatus.ObservedWorkloadReplicas && c.newStatus.ObservedWorkloadReplicas != -1 {
|
||||
workloadInfo.Replicas = c.stable.Spec.Replicas
|
||||
klog.Warningf("Deployment(%v) replicas changed during releasing, should pause and wait for it to complete, replicas from: %v -> %v",
|
||||
c.stableNamespacedName, c.newStatus.ObservedWorkloadReplicas, *c.stable.Spec.Replicas)
|
||||
return WorkloadReplicasChanged, workloadInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload revision was changed
|
||||
if hashRevision := util.ComputeHash(&c.stable.Spec.Template, nil); hashRevision != c.newStatus.UpdateRevision {
|
||||
workloadInfo.Status.UpdateRevision = hashRevision
|
||||
klog.Warningf("Deployment(%v) updateRevision changed during releasing", c.stableNamespacedName)
|
||||
return WorkloadPodTemplateChanged, workloadInfo, nil
|
||||
}
|
||||
|
||||
return IgnoreWorkloadEvent, workloadInfo, nil
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
The functions below are helper functions
|
||||
------------------------------------- */
|
||||
// fetchStableDeployment fetch stable deployment to c.stable
|
||||
func (c *DeploymentsRolloutController) fetchStableDeployment() error {
|
||||
if c.stable != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stable := &apps.Deployment{}
|
||||
if err := c.client.Get(context.TODO(), c.stableNamespacedName, stable); err != nil {
|
||||
klog.Errorf("BatchRelease(%v) get stable deployment error: %v", c.releaseKey, err)
|
||||
return err
|
||||
}
|
||||
c.stable = stable
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchCanaryDeployment fetch canary deployment to c.canary
|
||||
func (c *DeploymentsRolloutController) fetchCanaryDeployment() error {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
klog.Errorf("BatchRelease(%v) get canary deployment error: %v", c.releaseKey, err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = c.fetchStableDeployment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ds, err := c.listCanaryDeployment(client.InNamespace(c.stable.Namespace), utilclient.DisableDeepCopy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ds = util.FilterActiveDeployment(ds)
|
||||
sort.Slice(ds, func(i, j int) bool {
|
||||
return ds[i].CreationTimestamp.After(ds[j].CreationTimestamp.Time)
|
||||
})
|
||||
|
||||
if len(ds) == 0 || !util.EqualIgnoreHash(&ds[0].Spec.Template, &c.stable.Spec.Template) {
|
||||
err = apierrors.NewNotFound(schema.GroupResource{
|
||||
Group: apps.SchemeGroupVersion.Group,
|
||||
Resource: c.stable.Kind,
|
||||
}, fmt.Sprintf("%v-canary", c.canaryNamespacedName.Name))
|
||||
return err
|
||||
}
|
||||
|
||||
c.canary = ds[0].DeepCopy()
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordDeploymentRevisionAndReplicas records stableRevision, canaryRevision, workloadReplicas to BatchRelease.Status
|
||||
func (c *DeploymentsRolloutController) recordDeploymentRevisionAndReplicas() error {
|
||||
err := c.fetchStableDeployment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateRevision := util.ComputeHash(&c.stable.Spec.Template, nil)
|
||||
stableRevision, err := c.GetStablePodTemplateHash(c.stable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.newStatus.StableRevision = stableRevision
|
||||
c.newStatus.UpdateRevision = updateRevision
|
||||
c.newStatus.ObservedWorkloadReplicas = *c.stable.Spec.Replicas
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DeploymentsRolloutController) patchPodBatchLabel(canaryGoal int32) (bool, error) {
|
||||
rolloutID := c.release.Spec.ReleasePlan.RolloutID
|
||||
// if rolloutID is not set, no need to list pods,
|
||||
// because we cannot patch correct batch label to pod.
|
||||
if rolloutID == "" || c.canary == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
pods, err := util.ListOwnedPods(c.client, c.canary)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to list pods for Deployment %v", c.stableNamespacedName)
|
||||
return false, err
|
||||
}
|
||||
|
||||
batchID := c.release.Status.CanaryStatus.CurrentBatch + 1
|
||||
updateRevision := c.release.Status.UpdateRevision
|
||||
return patchPodBatchLabel(c.client, pods, rolloutID, batchID, updateRevision, canaryGoal, c.releaseKey)
|
||||
}
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
utilclient "github.com/openkruise/rollouts/pkg/util/client"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
)
|
||||
|
||||
// deploymentController is the place to hold fields needed for handle Deployment type of workloads
|
||||
type deploymentController struct {
|
||||
workloadController
|
||||
releaseKey types.NamespacedName
|
||||
stableNamespacedName types.NamespacedName
|
||||
canaryNamespacedName types.NamespacedName
|
||||
}
|
||||
|
||||
// add the parent controller to the owner of the deployment, unpause it and initialize the size
|
||||
// before kicking start the update and start from every pod in the old version
|
||||
func (c *deploymentController) claimDeployment(stableDeploy, canaryDeploy *apps.Deployment) (*apps.Deployment, error) {
|
||||
var controlled bool
|
||||
if controlInfo, ok := stableDeploy.Annotations[util.BatchReleaseControlAnnotation]; ok && controlInfo != "" {
|
||||
ref := &metav1.OwnerReference{}
|
||||
err := json.Unmarshal([]byte(controlInfo), ref)
|
||||
if err == nil && ref.UID == c.release.UID {
|
||||
klog.V(3).Infof("Deployment(%v) has been controlled by this BatchRelease(%v), no need to claim again",
|
||||
c.stableNamespacedName, c.releaseKey)
|
||||
controlled = true
|
||||
} else {
|
||||
klog.Errorf("Failed to parse controller info from Deployment(%v) annotation, error: %v, controller info: %+v",
|
||||
c.stableNamespacedName, err, *ref)
|
||||
}
|
||||
}
|
||||
|
||||
// patch control info to stable deployments if it needs
|
||||
if !controlled {
|
||||
controlInfo, _ := json.Marshal(metav1.NewControllerRef(c.release, c.release.GetObjectKind().GroupVersionKind()))
|
||||
patchedInfo := map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]string{
|
||||
util.BatchReleaseControlAnnotation: string(controlInfo),
|
||||
},
|
||||
},
|
||||
}
|
||||
cloneObj := stableDeploy.DeepCopy()
|
||||
patchedBody, _ := json.Marshal(patchedInfo)
|
||||
if err := c.client.Patch(context.TODO(), cloneObj, client.RawPatch(types.StrategicMergePatchType, patchedBody)); err != nil {
|
||||
klog.Errorf("Failed to patch controller info annotations to stable deployment(%v), error: %v", client.ObjectKeyFromObject(stableDeploy), err)
|
||||
return canaryDeploy, err
|
||||
}
|
||||
}
|
||||
|
||||
// create canary deployment if it needs
|
||||
if canaryDeploy == nil || !util.EqualIgnoreHash(&stableDeploy.Spec.Template, &canaryDeploy.Spec.Template) {
|
||||
var err error
|
||||
for {
|
||||
canaryDeploy, err = c.createCanaryDeployment(stableDeploy)
|
||||
if err != nil {
|
||||
if errors.IsAlreadyExists(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return canaryDeploy, nil
|
||||
}
|
||||
|
||||
func (c *deploymentController) createCanaryDeployment(stableDeploy *apps.Deployment) (*apps.Deployment, error) {
|
||||
// TODO: find a better way to generate canary deployment name
|
||||
suffix := util.GenRandomStr(3)
|
||||
canaryDeploy := &apps.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%v-%v", c.canaryNamespacedName.Name, suffix),
|
||||
Namespace: c.stableNamespacedName.Namespace,
|
||||
Labels: map[string]string{},
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
}
|
||||
for k, v := range stableDeploy.Labels {
|
||||
canaryDeploy.Labels[k] = v
|
||||
}
|
||||
for k, v := range stableDeploy.Annotations {
|
||||
canaryDeploy.Annotations[k] = v
|
||||
}
|
||||
for _, f := range stableDeploy.Finalizers {
|
||||
canaryDeploy.Finalizers = append(canaryDeploy.Finalizers, f)
|
||||
}
|
||||
for _, o := range stableDeploy.OwnerReferences {
|
||||
canaryDeploy.OwnerReferences = append(canaryDeploy.OwnerReferences, *o.DeepCopy())
|
||||
}
|
||||
|
||||
canaryDeploy.Finalizers = append(canaryDeploy.Finalizers, util.CanaryDeploymentFinalizer)
|
||||
canaryDeploy.OwnerReferences = append(canaryDeploy.OwnerReferences, *metav1.NewControllerRef(c.release, c.release.GroupVersionKind()))
|
||||
|
||||
// set extra labels & annotations
|
||||
canaryDeploy.Labels[util.CanaryDeploymentLabel] = c.stableNamespacedName.Name
|
||||
owner := metav1.NewControllerRef(c.release, c.release.GroupVersionKind())
|
||||
if owner != nil {
|
||||
ownerInfo, _ := json.Marshal(owner)
|
||||
canaryDeploy.Annotations[util.BatchReleaseControlAnnotation] = string(ownerInfo)
|
||||
}
|
||||
|
||||
// copy spec
|
||||
canaryDeploy.Spec = *stableDeploy.Spec.DeepCopy()
|
||||
canaryDeploy.Spec.Replicas = pointer.Int32Ptr(0)
|
||||
canaryDeploy.Spec.Paused = false
|
||||
|
||||
// create canary Deployment
|
||||
canaryKey := client.ObjectKeyFromObject(canaryDeploy)
|
||||
err := c.client.Create(context.TODO(), canaryDeploy)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to create canary Deployment(%v), error: %v", canaryKey, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canaryDeployInfo, _ := json.Marshal(canaryDeploy)
|
||||
klog.V(3).Infof("Create canary Deployment(%v) successfully, details: %v", canaryKey, string(canaryDeployInfo))
|
||||
return canaryDeploy, err
|
||||
}
|
||||
|
||||
func (c *deploymentController) releaseDeployment(stableDeploy *apps.Deployment, cleanup bool) (bool, error) {
|
||||
var patchErr, deleteErr error
|
||||
|
||||
// clean up control info for stable deployment if it needs
|
||||
if stableDeploy != nil && len(stableDeploy.Annotations[util.BatchReleaseControlAnnotation]) > 0 {
|
||||
var patchByte []byte
|
||||
cloneObj := stableDeploy.DeepCopy()
|
||||
patchByte = []byte(fmt.Sprintf(`{"metadata":{"annotations":{"%v":null}}}`, util.BatchReleaseControlAnnotation))
|
||||
patchErr = c.client.Patch(context.TODO(), cloneObj, client.RawPatch(types.StrategicMergePatchType, patchByte))
|
||||
if patchErr != nil {
|
||||
klog.Errorf("Error occurred when patching Deployment(%v), error: %v", c.stableNamespacedName, patchErr)
|
||||
return false, patchErr
|
||||
}
|
||||
}
|
||||
|
||||
// clean up canary deployment if it needs
|
||||
if cleanup {
|
||||
ds, err := c.listCanaryDeployment(client.InNamespace(c.stableNamespacedName.Namespace))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// must make sure the older is deleted firstly
|
||||
sort.Slice(ds, func(i, j int) bool {
|
||||
return ds[i].CreationTimestamp.Before(&ds[j].CreationTimestamp)
|
||||
})
|
||||
|
||||
// delete all the canary deployments
|
||||
for _, d := range ds {
|
||||
// clean up finalizers first
|
||||
if controllerutil.ContainsFinalizer(d, util.CanaryDeploymentFinalizer) {
|
||||
updateErr := util.UpdateFinalizer(c.client, d, util.RemoveFinalizerOpType, util.CanaryDeploymentFinalizer)
|
||||
if updateErr != nil && !errors.IsNotFound(updateErr) {
|
||||
klog.Error("Error occurred when updating Deployment(%v), error: %v", client.ObjectKeyFromObject(d), updateErr)
|
||||
return false, updateErr
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// delete the deployment
|
||||
deleteErr = c.client.Delete(context.TODO(), d)
|
||||
if deleteErr != nil && !errors.IsNotFound(deleteErr) {
|
||||
klog.Errorf("Error occurred when deleting Deployment(%v), error: %v", client.ObjectKeyFromObject(d), deleteErr)
|
||||
return false, deleteErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(3).Infof("Release Deployment(%v) Successfully", c.stableNamespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// scale the deployment
|
||||
func (c *deploymentController) patchDeploymentReplicas(deploy *apps.Deployment, replicas int32) error {
|
||||
if *deploy.Spec.Replicas >= replicas {
|
||||
return nil
|
||||
}
|
||||
|
||||
patch := map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"replicas": pointer.Int32Ptr(replicas),
|
||||
},
|
||||
}
|
||||
|
||||
cloneObj := deploy.DeepCopy()
|
||||
patchByte, _ := json.Marshal(patch)
|
||||
if err := c.client.Patch(context.TODO(), cloneObj, client.RawPatch(types.MergePatchType, patchByte)); err != nil {
|
||||
c.recorder.Eventf(c.release, v1.EventTypeWarning, "PatchPartitionFailed",
|
||||
"Failed to update the canary Deployment to the correct canary replicas %d, error: %v", replicas, err)
|
||||
return err
|
||||
}
|
||||
|
||||
klog.InfoS("Submitted modified partition quest for canary Deployment", "Deployment",
|
||||
client.ObjectKeyFromObject(deploy), "target canary replicas size", replicas, "batch", c.newStatus.CanaryStatus.CurrentBatch)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStablePodTemplateHash returns latest/stable revision hash of deployment
|
||||
func (c *deploymentController) GetStablePodTemplateHash(deploy *apps.Deployment) (string, error) {
|
||||
if deploy == nil {
|
||||
return "", fmt.Errorf("workload cannot be found, may be deleted or not be created yet")
|
||||
}
|
||||
|
||||
rss, err := c.listReplicaSetsFor(deploy)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sort.Slice(rss, func(i, j int) bool {
|
||||
return rss[i].CreationTimestamp.Before(&rss[j].CreationTimestamp)
|
||||
})
|
||||
|
||||
for _, rs := range rss {
|
||||
if rs.Spec.Replicas != nil && *rs.Spec.Replicas > 0 {
|
||||
return rs.Labels[apps.DefaultDeploymentUniqueLabelKey], nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("cannot get stable pod-template-hash for deployment(%v)", client.ObjectKeyFromObject(deploy))
|
||||
}
|
||||
|
||||
// listReplicaSetsFor list all owned replicaSets of deployment, including those have deletionTimestamp
|
||||
func (c *deploymentController) listReplicaSetsFor(deploy *apps.Deployment) ([]*apps.ReplicaSet, error) {
|
||||
deploySelector, err := metav1.LabelSelectorAsSelector(deploy.Spec.Selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rsList := &apps.ReplicaSetList{}
|
||||
err = c.client.List(context.TODO(), rsList, utilclient.DisableDeepCopy,
|
||||
&client.ListOptions{Namespace: deploy.Namespace, LabelSelector: deploySelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rss []*apps.ReplicaSet
|
||||
for i := range rsList.Items {
|
||||
rs := &rsList.Items[i]
|
||||
if rs.DeletionTimestamp != nil {
|
||||
continue
|
||||
}
|
||||
if owner := metav1.GetControllerOf(rs); owner == nil || owner.UID != deploy.UID {
|
||||
continue
|
||||
}
|
||||
rss = append(rss, rs)
|
||||
}
|
||||
return rss, nil
|
||||
}
|
||||
|
||||
func (c *deploymentController) listCanaryDeployment(options ...client.ListOption) ([]*apps.Deployment, error) {
|
||||
dList := &apps.DeploymentList{}
|
||||
if err := c.client.List(context.TODO(), dList, options...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ds []*apps.Deployment
|
||||
for i := range dList.Items {
|
||||
d := &dList.Items[i]
|
||||
o := metav1.GetControllerOf(d)
|
||||
if o == nil || o.UID != c.release.UID {
|
||||
continue
|
||||
}
|
||||
ds = append(ds, d)
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// the target workload size for the current batch
|
||||
func (c *deploymentController) calculateCurrentCanary(totalSize int32) int32 {
|
||||
targetSize := int32(calculateNewBatchTarget(&c.release.Spec.ReleasePlan, int(totalSize), int(c.newStatus.CanaryStatus.CurrentBatch)))
|
||||
klog.InfoS("Calculated the number of pods in the canary Deployment after current batch",
|
||||
"Deployment", c.stableNamespacedName, "BatchRelease", c.releaseKey,
|
||||
"current batch", c.newStatus.CanaryStatus.CurrentBatch, "workload updateRevision size", targetSize)
|
||||
return targetSize
|
||||
}
|
||||
|
||||
// the source workload size for the current batch
|
||||
func (c *deploymentController) calculateCurrentStable(totalSize int32) int32 {
|
||||
sourceSize := totalSize - c.calculateCurrentCanary(totalSize)
|
||||
klog.InfoS("Calculated the number of pods in the stable Deployment after current batch",
|
||||
"Deployment", c.stableNamespacedName, "BatchRelease", c.releaseKey,
|
||||
"current batch", c.newStatus.CanaryStatus.CurrentBatch, "workload stableRevision size", sourceSize)
|
||||
return sourceSize
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
var (
|
||||
releaseDeploy = &v1alpha1.BatchRelease{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: v1alpha1.GroupVersion.String(),
|
||||
Kind: "BatchRelease",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "release",
|
||||
Namespace: "application",
|
||||
UID: uuid.NewUUID(),
|
||||
},
|
||||
Spec: v1alpha1.BatchReleaseSpec{
|
||||
TargetRef: v1alpha1.ObjectRef{
|
||||
WorkloadRef: &v1alpha1.WorkloadRef{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "sample",
|
||||
},
|
||||
},
|
||||
ReleasePlan: v1alpha1.ReleasePlan{
|
||||
Batches: []v1alpha1.ReleaseBatch{
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("10%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("50%"),
|
||||
},
|
||||
{
|
||||
CanaryReplicas: intstr.FromString("80%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
stableDeploy = &apps.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: apps.SchemeGroupVersion.String(),
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "sample",
|
||||
Namespace: "application",
|
||||
UID: types.UID("87076677"),
|
||||
Generation: 2,
|
||||
Labels: map[string]string{
|
||||
"app": "busybox",
|
||||
apps.DefaultDeploymentUniqueLabelKey: "update-pod-hash",
|
||||
},
|
||||
},
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: pointer.Int32Ptr(100),
|
||||
Strategy: apps.DeploymentStrategy{
|
||||
Type: apps.RollingUpdateDeploymentStrategyType,
|
||||
RollingUpdate: &apps.RollingUpdateDeployment{
|
||||
MaxSurge: &intstr.IntOrString{Type: intstr.Int, IntVal: int32(1)},
|
||||
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: int32(2)},
|
||||
},
|
||||
},
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": "busybox",
|
||||
},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: containers("v2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: apps.DeploymentStatus{
|
||||
Replicas: 100,
|
||||
ReadyReplicas: 100,
|
||||
UpdatedReplicas: 0,
|
||||
AvailableReplicas: 100,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestDeploymentController(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Paused bool
|
||||
Cleanup bool
|
||||
}{
|
||||
{
|
||||
Name: "paused=true, cleanup=true",
|
||||
Paused: true,
|
||||
Cleanup: true,
|
||||
},
|
||||
{
|
||||
Name: "paused=true, cleanup=false",
|
||||
Paused: true,
|
||||
Cleanup: false,
|
||||
},
|
||||
{
|
||||
Name: "paused=false cleanup=true",
|
||||
Paused: false,
|
||||
Cleanup: true,
|
||||
},
|
||||
{
|
||||
Name: "paused=false , cleanup=false",
|
||||
Paused: false,
|
||||
Cleanup: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
release := releaseDeploy.DeepCopy()
|
||||
deploy := stableDeploy.DeepCopy()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(release, deploy).Build()
|
||||
rec := record.NewFakeRecorder(100)
|
||||
c := deploymentController{
|
||||
workloadController: workloadController{
|
||||
client: cli,
|
||||
recorder: rec,
|
||||
release: releaseDeploy,
|
||||
newStatus: &releaseDeploy.Status,
|
||||
},
|
||||
stableNamespacedName: client.ObjectKeyFromObject(stableDeploy),
|
||||
canaryNamespacedName: client.ObjectKeyFromObject(stableDeploy),
|
||||
}
|
||||
oldObject := &apps.Deployment{}
|
||||
Expect(cli.Get(context.TODO(), c.stableNamespacedName, oldObject)).NotTo(HaveOccurred())
|
||||
canary, err := c.claimDeployment(oldObject.DeepCopy(), nil)
|
||||
Expect(canary).ShouldNot(BeNil())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// The following logic should have been done in controller-runtime
|
||||
{
|
||||
dList := &apps.DeploymentList{}
|
||||
Expect(cli.List(context.TODO(), dList)).NotTo(HaveOccurred())
|
||||
for i := range dList.Items {
|
||||
d := &dList.Items[i]
|
||||
d.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: "apps", Version: "v1", Kind: "Deployment",
|
||||
})
|
||||
Expect(cli.Update(context.TODO(), d)).NotTo(HaveOccurred())
|
||||
}
|
||||
}
|
||||
|
||||
newObject := &apps.Deployment{}
|
||||
Expect(cli.Get(context.TODO(), c.stableNamespacedName, newObject)).NotTo(HaveOccurred())
|
||||
_, err = c.releaseDeployment(newObject.DeepCopy(), cs.Cleanup)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
newObject = &apps.Deployment{}
|
||||
Expect(cli.Get(context.TODO(), c.stableNamespacedName, newObject)).NotTo(HaveOccurred())
|
||||
newObject.Spec.Paused = oldObject.Spec.Paused
|
||||
Expect(reflect.DeepEqual(oldObject.Spec, newObject.Spec)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(oldObject.Labels, newObject.Labels)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(oldObject.Finalizers, newObject.Finalizers)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(oldObject.Annotations, newObject.Annotations)).Should(BeTrue())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
appsv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type StatefulSetLikeController struct {
|
||||
client.Client
|
||||
recorder record.EventRecorder
|
||||
release *appsv1alpha1.BatchRelease
|
||||
namespacedName types.NamespacedName
|
||||
workloadObj client.Object
|
||||
gvk schema.GroupVersionKind
|
||||
pods []*v1.Pod
|
||||
}
|
||||
|
||||
func NewStatefulSetLikeController(c client.Client, r record.EventRecorder, b *appsv1alpha1.BatchRelease, n types.NamespacedName, gvk schema.GroupVersionKind) UnifiedWorkloadController {
|
||||
return &StatefulSetLikeController{
|
||||
Client: c,
|
||||
recorder: r,
|
||||
release: b,
|
||||
namespacedName: n,
|
||||
gvk: gvk,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StatefulSetLikeController) GetWorkloadObject() (client.Object, error) {
|
||||
if c.workloadObj == nil {
|
||||
workloadObj := util.GetEmptyWorkloadObject(c.gvk)
|
||||
if workloadObj == nil {
|
||||
return nil, errors.NewNotFound(schema.GroupResource{Group: c.gvk.Group, Resource: c.gvk.Kind}, c.namespacedName.Name)
|
||||
}
|
||||
if err := c.Get(context.TODO(), c.namespacedName, workloadObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.workloadObj = workloadObj
|
||||
}
|
||||
return c.workloadObj, nil
|
||||
}
|
||||
|
||||
func (c *StatefulSetLikeController) GetWorkloadInfo() (*util.WorkloadInfo, error) {
|
||||
set, err := c.GetWorkloadObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workloadInfo := util.ParseStatefulSetInfo(set, c.namespacedName)
|
||||
workloadInfo.Paused = true
|
||||
if workloadInfo.Status.UpdatedReadyReplicas <= 0 {
|
||||
pods, err := c.ListOwnedPods()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updatedReadyReplicas, err := c.countUpdatedReadyPods(pods, workloadInfo.Status.UpdateRevision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workloadInfo.Status.UpdatedReadyReplicas = updatedReadyReplicas
|
||||
}
|
||||
|
||||
return workloadInfo, nil
|
||||
}
|
||||
|
||||
func (c *StatefulSetLikeController) ClaimWorkload() (bool, error) {
|
||||
set, err := c.GetWorkloadObject()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = claimWorkload(c.Client, c.release, set, map[string]interface{}{
|
||||
"type": apps.RollingUpdateStatefulSetStrategyType,
|
||||
"rollingUpdate": map[string]interface{}{
|
||||
"partition": pointer.Int32(util.GetReplicas(set)),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
klog.V(3).Infof("Claim StatefulSet(%v) Successfully", c.namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *StatefulSetLikeController) ReleaseWorkload(cleanup bool) (bool, error) {
|
||||
set, err := c.GetWorkloadObject()
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = releaseWorkload(c.Client, set)
|
||||
if err != nil {
|
||||
c.recorder.Eventf(c.release, v1.EventTypeWarning, "ReleaseFailed", err.Error())
|
||||
return false, err
|
||||
}
|
||||
|
||||
klog.V(3).Infof("Release StatefulSet(%v) Successfully", c.namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *StatefulSetLikeController) UpgradeBatch(canaryReplicasGoal, stableReplicasGoal int32) (bool, error) {
|
||||
set, err := c.GetWorkloadObject()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if no needs to patch partition
|
||||
partition := util.GetStatefulSetPartition(set)
|
||||
if partition <= stableReplicasGoal {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err = patchSpec(c.Client, set, map[string]interface{}{
|
||||
"updateStrategy": map[string]interface{}{
|
||||
"rollingUpdate": map[string]interface{}{
|
||||
"partition": pointer.Int32(stableReplicasGoal),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
klog.V(3).Infof("Upgrade StatefulSet(%v) Partition to %v Successfully", c.namespacedName, stableReplicasGoal)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *StatefulSetLikeController) IsOrderedUpdate() (bool, error) {
|
||||
set, err := c.GetWorkloadObject()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return !util.IsStatefulSetUnorderedUpdate(set), nil
|
||||
}
|
||||
|
||||
func (c *StatefulSetLikeController) ListOwnedPods() ([]*v1.Pod, error) {
|
||||
if c.pods != nil {
|
||||
return c.pods, nil
|
||||
}
|
||||
set, err := c.GetWorkloadObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.pods, err = util.ListOwnedPods(c.Client, set)
|
||||
return c.pods, err
|
||||
}
|
||||
|
||||
func (c *StatefulSetLikeController) countUpdatedReadyPods(pods []*v1.Pod, updateRevision string) (int32, error) {
|
||||
activePods := util.FilterActivePods(pods)
|
||||
updatedReadyReplicas := int32(0)
|
||||
for _, pod := range activePods {
|
||||
if util.IsConsistentWithRevision(pod, updateRevision) && util.IsPodReady(pod) {
|
||||
updatedReadyReplicas++
|
||||
}
|
||||
}
|
||||
return updatedReadyReplicas, nil
|
||||
}
|
||||
|
|
@ -1,407 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type UnifiedWorkloadController interface {
|
||||
GetWorkloadInfo() (*util.WorkloadInfo, error)
|
||||
ClaimWorkload() (bool, error)
|
||||
ReleaseWorkload(cleanup bool) (bool, error)
|
||||
UpgradeBatch(canaryReplicasGoal, stableReplicasGoal int32) (bool, error)
|
||||
ListOwnedPods() ([]*v1.Pod, error)
|
||||
IsOrderedUpdate() (bool, error)
|
||||
}
|
||||
|
||||
// UnifiedWorkloadRolloutControlPlane is responsible for handling rollout StatefulSet type of workloads
|
||||
type UnifiedWorkloadRolloutControlPlane struct {
|
||||
UnifiedWorkloadController
|
||||
client client.Client
|
||||
recorder record.EventRecorder
|
||||
release *v1alpha1.BatchRelease
|
||||
newStatus *v1alpha1.BatchReleaseStatus
|
||||
}
|
||||
|
||||
type NewUnifiedControllerFunc = func(c client.Client, r record.EventRecorder, p *v1alpha1.BatchRelease, n types.NamespacedName, gvk schema.GroupVersionKind) UnifiedWorkloadController
|
||||
|
||||
// NewUnifiedWorkloadRolloutControlPlane creates a new workload rollout controller
|
||||
func NewUnifiedWorkloadRolloutControlPlane(f NewUnifiedControllerFunc, c client.Client, r record.EventRecorder, p *v1alpha1.BatchRelease, newStatus *v1alpha1.BatchReleaseStatus, n types.NamespacedName, gvk schema.GroupVersionKind) *UnifiedWorkloadRolloutControlPlane {
|
||||
return &UnifiedWorkloadRolloutControlPlane{
|
||||
client: c,
|
||||
recorder: r,
|
||||
release: p,
|
||||
newStatus: newStatus,
|
||||
UnifiedWorkloadController: f(c, r, p, n, gvk),
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyWorkload verifies that the workload is ready to execute release plan
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) VerifyWorkload() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// prepareBeforeRollback makes sure that the updated pods have been patched no-need-update label.
|
||||
// return values:
|
||||
// - bool: whether all updated pods have been patched no-need-update label;
|
||||
// - *int32: how many pods have been patched;
|
||||
// - err: whether error occurs.
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) prepareBeforeRollback() (bool, *int32, error) {
|
||||
if c.release.Annotations[util.RollbackInBatchAnnotation] == "" {
|
||||
return true, nil, nil
|
||||
}
|
||||
|
||||
noNeedRollbackReplicas := int32(0)
|
||||
rolloutID := c.release.Spec.ReleasePlan.RolloutID
|
||||
if rolloutID == "" {
|
||||
return true, &noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
workloadInfo, err := c.GetWorkloadInfo()
|
||||
if err != nil {
|
||||
return false, &noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
pods, err := c.ListOwnedPods()
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to list pods for %v", workloadInfo.GVKWithName)
|
||||
return false, &noNeedRollbackReplicas, err
|
||||
}
|
||||
|
||||
updateRevision := workloadInfo.Status.UpdateRevision
|
||||
var filterPods []*v1.Pod
|
||||
for i := range pods {
|
||||
if !pods[i].DeletionTimestamp.IsZero() {
|
||||
continue
|
||||
}
|
||||
if !util.IsConsistentWithRevision(pods[i], updateRevision) {
|
||||
continue
|
||||
}
|
||||
if id, ok := pods[i].Labels[util.NoNeedUpdatePodLabel]; ok && id == rolloutID {
|
||||
noNeedRollbackReplicas++
|
||||
continue
|
||||
}
|
||||
filterPods = append(filterPods, pods[i])
|
||||
}
|
||||
|
||||
if len(filterPods) == 0 {
|
||||
return true, &noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
for _, pod := range filterPods {
|
||||
podClone := pod.DeepCopy()
|
||||
body := fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s"}}}`, util.NoNeedUpdatePodLabel, rolloutID)
|
||||
err = c.client.Patch(context.TODO(), podClone, client.RawPatch(types.StrategicMergePatchType, []byte(body)))
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to patch rollback labels[%s]=%s to pod %v", util.NoNeedUpdatePodLabel, rolloutID, client.ObjectKeyFromObject(pod))
|
||||
return false, &noNeedRollbackReplicas, err
|
||||
} else {
|
||||
klog.Info("Succeeded to patch rollback labels[%s]=%s to pod %v", util.NoNeedUpdatePodLabel, rolloutID, client.ObjectKeyFromObject(pod))
|
||||
}
|
||||
noNeedRollbackReplicas++
|
||||
}
|
||||
klog.Infof("BatchRelease(%v) find %v replicas no need to rollback", client.ObjectKeyFromObject(c.release), noNeedRollbackReplicas)
|
||||
return false, &noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
// PrepareBeforeProgress makes sure that the source and target workload is under our control
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) PrepareBeforeProgress() (bool, *int32, error) {
|
||||
done, noNeedRollbackReplicas, err := c.prepareBeforeRollback()
|
||||
if err != nil || !done {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
// claim the workload is under our control
|
||||
done, err = c.ClaimWorkload()
|
||||
if !done || err != nil {
|
||||
return false, noNeedRollbackReplicas, err
|
||||
}
|
||||
|
||||
// record revisions and replicas info to BatchRelease.Status
|
||||
err = c.RecordWorkloadRevisionAndReplicas()
|
||||
if err != nil {
|
||||
return false, noNeedRollbackReplicas, err
|
||||
}
|
||||
|
||||
c.recorder.Event(c.release, v1.EventTypeNormal, "InitializedSuccessfully", "Rollout resource are initialized")
|
||||
return true, noNeedRollbackReplicas, nil
|
||||
}
|
||||
|
||||
// UpgradeOneBatch calculates the number of pods we can upgrade once according to the rollout spec
|
||||
// and then set the partition accordingly
|
||||
// TODO: support advanced statefulSet reserveOrdinal feature0
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) UpgradeOneBatch() (bool, error) {
|
||||
workloadInfo, err := c.GetWorkloadInfo()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if c.release.Status.ObservedWorkloadReplicas == 0 {
|
||||
klog.Infof("BatchRelease(%v) observed workload replicas is 0, no need to upgrade", client.ObjectKeyFromObject(c.release))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if the workload status is untrustworthy
|
||||
if workloadInfo.Status.ObservedGeneration != workloadInfo.Generation {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pods, err := c.ListOwnedPods()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var noNeedRollbackReplicas int32
|
||||
if c.newStatus.CanaryStatus.NoNeedUpdateReplicas != nil {
|
||||
rolloutID := c.release.Spec.ReleasePlan.RolloutID
|
||||
noNeedRollbackReplicas = countNoNeedRollbackReplicas(pods, c.newStatus.UpdateRevision, rolloutID)
|
||||
c.newStatus.CanaryStatus.NoNeedUpdateReplicas = pointer.Int32(noNeedRollbackReplicas)
|
||||
}
|
||||
replicas := c.newStatus.ObservedWorkloadReplicas
|
||||
currentBatch := c.newStatus.CanaryStatus.CurrentBatch
|
||||
|
||||
// the number of canary pods should have in current batch in plan
|
||||
plannedBatchCanaryReplicas := c.calculateCurrentCanary(c.newStatus.ObservedWorkloadReplicas)
|
||||
// the number of canary pods that consider rollback context and other real-world situations
|
||||
expectedBatchCanaryReplicas := c.calculateCurrentCanary(replicas - noNeedRollbackReplicas)
|
||||
// the number of canary pods that consider rollback context and other real-world situations
|
||||
expectedBatchStableReplicas := replicas - expectedBatchCanaryReplicas
|
||||
|
||||
// if ordered update, partition is related with pod ordinals
|
||||
// if unordered update, partition just like cloneSet partition
|
||||
orderedUpdate, _ := c.IsOrderedUpdate()
|
||||
if !orderedUpdate {
|
||||
expectedBatchStableReplicas -= noNeedRollbackReplicas
|
||||
}
|
||||
|
||||
klog.V(3).InfoS("upgrade one batch, current info:",
|
||||
"BatchRelease", client.ObjectKeyFromObject(c.release),
|
||||
"currentBatch", currentBatch,
|
||||
"replicas", replicas,
|
||||
"noNeedRollbackReplicas", noNeedRollbackReplicas,
|
||||
"plannedBatchCanaryReplicas", plannedBatchCanaryReplicas,
|
||||
"expectedBatchCanaryReplicas", expectedBatchCanaryReplicas,
|
||||
"expectedBatchStableReplicas", expectedBatchStableReplicas)
|
||||
|
||||
isUpgradedDone, err := c.UpgradeBatch(expectedBatchCanaryReplicas, expectedBatchStableReplicas)
|
||||
if err != nil || !isUpgradedDone {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
isPatchedDone, err := c.patchPodBatchLabel(pods, plannedBatchCanaryReplicas, expectedBatchStableReplicas)
|
||||
if err != nil || !isPatchedDone {
|
||||
return false, err
|
||||
}
|
||||
|
||||
c.recorder.Eventf(c.release, v1.EventTypeNormal, "SetBatchDone",
|
||||
"Finished submitting all upgrade quests for batch %d", c.release.Status.CanaryStatus.CurrentBatch)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchReady checks to see if the pods are all available according to the rollout plan
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) CheckOneBatchReady() (bool, error) {
|
||||
workloadInfo, err := c.GetWorkloadInfo()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if c.release.Status.ObservedWorkloadReplicas == 0 {
|
||||
klog.Infof("BatchRelease(%v) observed workload replicas is 0, no need to check", client.ObjectKeyFromObject(c.release))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if the workload status is untrustworthy
|
||||
if workloadInfo.Status.ObservedGeneration != workloadInfo.Generation {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var noNeedRollbackReplicas int32
|
||||
if c.newStatus.CanaryStatus.NoNeedUpdateReplicas != nil {
|
||||
noNeedRollbackReplicas = *c.newStatus.CanaryStatus.NoNeedUpdateReplicas
|
||||
}
|
||||
|
||||
replicas := c.newStatus.ObservedWorkloadReplicas
|
||||
updatedReplicas := workloadInfo.Status.UpdatedReplicas
|
||||
updatedReadyReplicas := workloadInfo.Status.UpdatedReadyReplicas
|
||||
|
||||
currentBatch := c.newStatus.CanaryStatus.CurrentBatch
|
||||
// the number of canary pods should have in current batch in plan
|
||||
plannedUpdatedReplicas := c.calculateCurrentCanary(c.newStatus.ObservedWorkloadReplicas)
|
||||
// the number of canary pods that consider rollback context and other real-world situations
|
||||
expectedUpdatedReplicas := c.calculateCurrentCanary(replicas - noNeedRollbackReplicas)
|
||||
// the number of canary pods that consider rollback context and other real-world situations
|
||||
expectedStableReplicas := replicas - expectedUpdatedReplicas
|
||||
// the number of pods that should be upgraded in this batch
|
||||
updatedReplicasInBatch := plannedUpdatedReplicas
|
||||
if currentBatch > 0 {
|
||||
updatedReplicasInBatch -= int32(calculateNewBatchTarget(&c.release.Spec.ReleasePlan, int(replicas), int(currentBatch-1)))
|
||||
}
|
||||
|
||||
// if ordered update, partition is related with pod ordinals
|
||||
// if unordered update, partition just like cloneSet partition
|
||||
orderedUpdate, _ := c.IsOrderedUpdate()
|
||||
if !orderedUpdate {
|
||||
expectedStableReplicas -= noNeedRollbackReplicas
|
||||
}
|
||||
|
||||
klog.V(3).InfoS("check one batch, current info:",
|
||||
"BatchRelease", client.ObjectKeyFromObject(c.release),
|
||||
"currentBatch", currentBatch,
|
||||
"replicas", replicas,
|
||||
"noNeedRollbackReplicas", noNeedRollbackReplicas,
|
||||
"updatedReplicasInBatch", updatedReplicasInBatch,
|
||||
"plannedUpdatedReplicas", plannedUpdatedReplicas,
|
||||
"expectedUpdatedReplicas", expectedUpdatedReplicas,
|
||||
"expectedStableReplicas", expectedStableReplicas)
|
||||
|
||||
pods, err := c.ListOwnedPods()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !isBatchReady(c.release, pods, workloadInfo.MaxUnavailable,
|
||||
plannedUpdatedReplicas, expectedUpdatedReplicas, updatedReplicas, updatedReadyReplicas) {
|
||||
klog.Infof("BatchRelease(%v) batch is not ready yet, current batch=%d", klog.KObj(c.release), currentBatch)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
klog.Infof("BatchRelease(%v) %d batch is ready", klog.KObj(c.release), currentBatch)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FinalizeProgress makes sure the workload is all upgraded
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) FinalizeProgress(cleanup bool) (bool, error) {
|
||||
if _, err := c.ReleaseWorkload(cleanup); err != nil {
|
||||
return false, err
|
||||
}
|
||||
c.recorder.Eventf(c.release, v1.EventTypeNormal, "FinalizedSuccessfully", "Rollout resource are finalized: cleanup=%v", cleanup)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SyncWorkloadInfo return change type if workload was changed during release
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) SyncWorkloadInfo() (WorkloadEventType, *util.WorkloadInfo, error) {
|
||||
// ignore the sync if the release plan is deleted
|
||||
if c.release.DeletionTimestamp != nil {
|
||||
return IgnoreWorkloadEvent, nil, nil
|
||||
}
|
||||
|
||||
workloadInfo, err := c.GetWorkloadInfo()
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return WorkloadHasGone, nil, err
|
||||
}
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// in case that the workload status is untrustworthy
|
||||
if workloadInfo.Status.ObservedGeneration != workloadInfo.Generation {
|
||||
klog.Warningf("%v is still reconciling, waiting for it to complete, generation: %v, observed: %v",
|
||||
workloadInfo.GVKWithName, workloadInfo.Generation, workloadInfo.Status.ObservedGeneration)
|
||||
return WorkloadStillReconciling, nil, nil
|
||||
}
|
||||
|
||||
// in case of that the updated revision of the workload is promoted
|
||||
if workloadInfo.Status.UpdatedReplicas == workloadInfo.Status.Replicas {
|
||||
return IgnoreWorkloadEvent, workloadInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload is scaling
|
||||
if *workloadInfo.Replicas != c.release.Status.ObservedWorkloadReplicas {
|
||||
klog.Warningf("%v replicas changed during releasing, should pause and wait for it to complete, "+
|
||||
"replicas from: %v -> %v", workloadInfo.GVKWithName, c.release.Status.ObservedWorkloadReplicas, *workloadInfo.Replicas)
|
||||
return WorkloadReplicasChanged, workloadInfo, nil
|
||||
}
|
||||
|
||||
// updateRevision == CurrentRevision means CloneSet is rolling back or newly-created.
|
||||
if workloadInfo.Status.UpdateRevision == workloadInfo.Status.StableRevision &&
|
||||
// stableRevision == UpdateRevision means CloneSet is rolling back instead of newly-created.
|
||||
c.newStatus.StableRevision == workloadInfo.Status.UpdateRevision &&
|
||||
// StableRevision != observed UpdateRevision means the rollback event have not been observed.
|
||||
c.newStatus.StableRevision != c.newStatus.UpdateRevision {
|
||||
klog.Warningf("Workload(%v) is rolling back in batches", workloadInfo.GVKWithName)
|
||||
return WorkloadRollbackInBatch, workloadInfo, nil
|
||||
}
|
||||
|
||||
// in case of that the workload was changed
|
||||
if workloadInfo.Status.UpdateRevision != c.release.Status.UpdateRevision {
|
||||
klog.Warningf("%v updateRevision changed during releasing, should try to restart the release plan, "+
|
||||
"updateRevision from: %v -> %v", workloadInfo.GVKWithName, c.release.Status.UpdateRevision, workloadInfo.Status.UpdateRevision)
|
||||
return WorkloadPodTemplateChanged, workloadInfo, nil
|
||||
}
|
||||
|
||||
return IgnoreWorkloadEvent, workloadInfo, nil
|
||||
}
|
||||
|
||||
// the canary workload size for the current batch
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) calculateCurrentCanary(totalSize int32) int32 {
|
||||
canaryGoal := int32(calculateNewBatchTarget(&c.release.Spec.ReleasePlan, int(totalSize), int(c.release.Status.CanaryStatus.CurrentBatch)))
|
||||
klog.InfoS("Calculated the number of pods in the target workload after current batch", "BatchRelease", client.ObjectKeyFromObject(c.release),
|
||||
"current batch", c.release.Status.CanaryStatus.CurrentBatch, "workload canary goal replicas goal", canaryGoal)
|
||||
return canaryGoal
|
||||
}
|
||||
|
||||
// the source workload size for the current batch
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) calculateCurrentStable(totalSize int32) int32 {
|
||||
stableGoal := totalSize - c.calculateCurrentCanary(totalSize)
|
||||
klog.InfoS("Calculated the number of pods in the target workload after current batch", "BatchRelease", client.ObjectKeyFromObject(c.release),
|
||||
"current batch", c.release.Status.CanaryStatus.CurrentBatch, "workload stable goal replicas goal", stableGoal)
|
||||
return stableGoal
|
||||
}
|
||||
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) RecordWorkloadRevisionAndReplicas() error {
|
||||
workloadInfo, err := c.GetWorkloadInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.newStatus.ObservedWorkloadReplicas = *workloadInfo.Replicas
|
||||
c.newStatus.StableRevision = workloadInfo.Status.StableRevision
|
||||
c.newStatus.UpdateRevision = workloadInfo.Status.UpdateRevision
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UnifiedWorkloadRolloutControlPlane) patchPodBatchLabel(pods []*v1.Pod, plannedBatchCanaryReplicas, expectedBatchStableReplicas int32) (bool, error) {
|
||||
rolloutID := c.release.Spec.ReleasePlan.RolloutID
|
||||
if rolloutID == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
updateRevision := c.release.Status.UpdateRevision
|
||||
batchID := c.release.Status.CanaryStatus.CurrentBatch + 1
|
||||
if c.newStatus.CanaryStatus.NoNeedUpdateReplicas != nil {
|
||||
orderedUpdate, _ := c.IsOrderedUpdate()
|
||||
if orderedUpdate {
|
||||
pods = filterPodsForOrderedRollback(pods, plannedBatchCanaryReplicas, expectedBatchStableReplicas, c.release.Status.ObservedWorkloadReplicas, rolloutID, updateRevision)
|
||||
} else {
|
||||
pods = filterPodsForUnorderedRollback(pods, plannedBatchCanaryReplicas, expectedBatchStableReplicas, c.release.Status.ObservedWorkloadReplicas, rolloutID, updateRevision)
|
||||
}
|
||||
}
|
||||
return patchPodBatchLabel(c.client, pods, rolloutID, batchID, updateRevision, plannedBatchCanaryReplicas, client.ObjectKeyFromObject(c.release))
|
||||
}
|
||||
|
|
@ -22,13 +22,10 @@ import (
|
|||
"reflect"
|
||||
"strconv"
|
||||
|
||||
appsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
rolloutv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"github.com/openkruise/rollouts/pkg/util"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
|
@ -171,7 +168,6 @@ func (r *innerBatchRelease) Promote(index int32, isRollback, checkReady bool) (b
|
|||
batch.Annotations[util.RollbackInBatchAnnotation] = r.rollout.Annotations[util.RollbackInBatchAnnotation]
|
||||
}
|
||||
|
||||
batch.Spec.Paused = false
|
||||
if batch.Labels == nil {
|
||||
batch.Labels = map[string]string{}
|
||||
}
|
||||
|
|
@ -188,113 +184,52 @@ func (r *innerBatchRelease) Promote(index int32, isRollback, checkReady bool) (b
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (r *innerBatchRelease) resumeStableWorkload(checkReady bool) (bool, error) {
|
||||
// cloneSet
|
||||
switch r.rollout.Spec.ObjectRef.WorkloadRef.Kind {
|
||||
case util.ControllerKruiseKindCS.Kind:
|
||||
dName := r.rollout.Spec.ObjectRef.WorkloadRef.Name
|
||||
obj := &appsv1alpha1.CloneSet{}
|
||||
err := r.Get(context.TODO(), types.NamespacedName{Namespace: r.rollout.Namespace, Name: dName}, obj)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
klog.Warningf("rollout(%s/%s) cloneSet(%s) not found, and return true", r.rollout.Namespace, r.rollout.Name, dName)
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
// default partition.IntVal=0
|
||||
if !obj.Spec.UpdateStrategy.Paused && obj.Spec.UpdateStrategy.Partition.IntVal == 0 && obj.Spec.UpdateStrategy.Partition.Type == intstr.Int {
|
||||
func (r *innerBatchRelease) resumeStableWorkload(waitReady bool) (bool, error) {
|
||||
batch, err := r.FetchBatchRelease()
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = r.Get(context.TODO(), types.NamespacedName{Namespace: r.rollout.Namespace, Name: dName}, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
obj.Spec.UpdateStrategy.Paused = false
|
||||
obj.Spec.UpdateStrategy.Partition = nil
|
||||
return r.Update(context.TODO(), obj)
|
||||
})
|
||||
if err != nil {
|
||||
klog.Errorf("update rollout(%s/%s) cloneSet failed: %s", r.rollout.Namespace, r.rollout.Name, err.Error())
|
||||
return false, err
|
||||
}
|
||||
klog.Infof("resume rollout(%s/%s) cloneSet(paused=false,partition=nil) success", r.rollout.Namespace, r.rollout.Name)
|
||||
return true, nil
|
||||
|
||||
case util.ControllerKindDep.Kind:
|
||||
// deployment
|
||||
dName := r.rollout.Spec.ObjectRef.WorkloadRef.Name
|
||||
obj := &apps.Deployment{}
|
||||
err := r.Get(context.TODO(), types.NamespacedName{Namespace: r.rollout.Namespace, Name: dName}, obj)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
klog.Warningf("rollout(%s/%s) stable deployment(%s) not found, and return true", r.rollout.Namespace, r.rollout.Name, dName)
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
// set deployment paused=false
|
||||
if obj.Spec.Paused {
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = r.Get(context.TODO(), types.NamespacedName{Namespace: r.rollout.Namespace, Name: dName}, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
obj.Spec.Paused = false
|
||||
return r.Update(context.TODO(), obj)
|
||||
})
|
||||
if err != nil {
|
||||
klog.Errorf("update rollout(%s/%s) stable deployment failed: %s", r.rollout.Namespace, r.rollout.Name, err.Error())
|
||||
return false, err
|
||||
}
|
||||
klog.Infof("resume rollout(%s/%s) stable deployment(paused=false) success", r.rollout.Namespace, r.rollout.Name)
|
||||
}
|
||||
|
||||
// Whether to wait for pods are ready
|
||||
if !checkReady {
|
||||
return true, nil
|
||||
}
|
||||
data := util.DumpJSON(obj.Status)
|
||||
// wait for all pods are ready
|
||||
maxUnavailable, _ := intstr.GetScaledValueFromIntOrPercent(obj.Spec.Strategy.RollingUpdate.MaxUnavailable, int(*obj.Spec.Replicas), true)
|
||||
if obj.Status.ObservedGeneration != obj.Generation || obj.Status.UpdatedReplicas != *obj.Spec.Replicas ||
|
||||
obj.Status.Replicas != *obj.Spec.Replicas || *obj.Spec.Replicas-obj.Status.AvailableReplicas > int32(maxUnavailable) {
|
||||
klog.Infof("rollout(%s/%s) stable deployment status(%s), and wait a moment", r.rollout.Namespace, r.rollout.Name, data)
|
||||
return false, nil
|
||||
}
|
||||
klog.Infof("resume rollout(%s/%s) stable deployment(paused=false) status(%s) success", r.rollout.Namespace, r.rollout.Name, data)
|
||||
return true, nil
|
||||
|
||||
default:
|
||||
// statefulset-like workloads
|
||||
workloadRef := r.rollout.Spec.ObjectRef.WorkloadRef
|
||||
workloadNsn := types.NamespacedName{Namespace: r.rollout.Namespace, Name: workloadRef.Name}
|
||||
workloadGVK := schema.FromAPIVersionAndKind(workloadRef.APIVersion, workloadRef.Kind)
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetGroupVersionKind(workloadGVK)
|
||||
err := r.Get(context.TODO(), workloadNsn, obj)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
klog.Warningf("rollout(%s/%s) statefulset(%s) not found, and return true", r.rollout.Namespace, r.rollout.Name, workloadNsn.Name)
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if util.GetStatefulSetPartition(obj) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
cloneObj := obj.DeepCopy()
|
||||
body := fmt.Sprintf(`{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}`)
|
||||
err = r.Patch(context.TODO(), cloneObj, client.RawPatch(types.MergePatchType, []byte(body)))
|
||||
if err != nil {
|
||||
klog.Errorf("patch rollout(%s/%s) statefulset failed: %s", r.rollout.Namespace, r.rollout.Name, err.Error())
|
||||
return false, err
|
||||
}
|
||||
klog.Infof("resume rollout(%s/%s) statefulset(partition=0) success", r.rollout.Namespace, r.rollout.Name)
|
||||
// The Completed phase means batchRelease controller has processed all it
|
||||
// should process. If BatchRelease phase is completed, we can do nothing.
|
||||
if batch.Status.Phase == rolloutv1alpha1.RolloutPhaseCompleted {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If BatchPartition is nil, BatchRelease will directly resume workload via:
|
||||
// - * set workload Paused = false if it needs;
|
||||
// - * set workload Partition = null if it needs.
|
||||
if batch.Spec.ReleasePlan.BatchPartition == nil {
|
||||
// - If checkReady is true, finalizing policy must be "WaitResume";
|
||||
// - If checkReady is false, finalizing policy must be NOT "WaitResume";
|
||||
// Otherwise, we should correct it.
|
||||
switch batch.Spec.ReleasePlan.FinalizingPolicy {
|
||||
case rolloutv1alpha1.WaitResumeFinalizingPolicyType:
|
||||
if waitReady { // no need to patch again
|
||||
return false, nil
|
||||
}
|
||||
default:
|
||||
if !waitReady { // no need to patch again
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Correct finalizing policy.
|
||||
policy := rolloutv1alpha1.ImmediateFinalizingPolicyType
|
||||
if waitReady {
|
||||
policy = rolloutv1alpha1.WaitResumeFinalizingPolicyType
|
||||
}
|
||||
|
||||
// Patch BatchPartition and FinalizingPolicy, BatchPartition always patch null here.
|
||||
body := fmt.Sprintf(`{"spec":{"releasePlan":{"batchPartition":null,"finalizingPolicy":"%s"}}}`, policy)
|
||||
if err = r.Patch(context.TODO(), batch, client.RawPatch(types.MergePatchType, []byte(body))); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *innerBatchRelease) Finalize() (bool, error) {
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ func (r *ControllerFinder) getStatefulSetLikeWorkload(namespace string, ref *rol
|
|||
return nil, err
|
||||
}
|
||||
|
||||
workloadInfo := ParseStatefulSetInfo(set, key)
|
||||
workloadInfo := ParseWorkload(set)
|
||||
if workloadInfo.Generation != workloadInfo.Status.ObservedGeneration {
|
||||
return &Workload{IsStatusConsistent: false}, nil
|
||||
}
|
||||
|
|
@ -249,7 +249,7 @@ func (r *ControllerFinder) getStatefulSetLikeWorkload(namespace string, ref *rol
|
|||
CanaryReplicas: workloadInfo.Status.UpdatedReplicas,
|
||||
CanaryReadyReplicas: workloadInfo.Status.UpdatedReadyReplicas,
|
||||
ObjectMeta: workloadInfo.ObjectMeta,
|
||||
Replicas: *workloadInfo.Replicas,
|
||||
Replicas: workloadInfo.Replicas,
|
||||
PodTemplateHash: workloadInfo.Status.UpdateRevision,
|
||||
IsStatusConsistent: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 expectations
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
// Action is the action, like create and delete.
|
||||
type Action string
|
||||
|
||||
const (
|
||||
// Create action
|
||||
Create Action = "create"
|
||||
// Delete action
|
||||
Delete Action = "delete"
|
||||
)
|
||||
|
||||
var (
|
||||
ExpectationTimeout time.Duration
|
||||
ResourceExpectations = NewResourceExpectations()
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.DurationVar(&ExpectationTimeout, "expectation-timeout", time.Minute*5, "The expectation timeout. Defaults 5min")
|
||||
}
|
||||
|
||||
// Expectations is an interface that allows users to set and wait on expectations of resource creation and deletion.
|
||||
type Expectations interface {
|
||||
Expect(controllerKey string, action Action, name string)
|
||||
Observe(controllerKey string, action Action, name string)
|
||||
SatisfiedExpectations(controllerKey string) (bool, time.Duration, map[Action][]string)
|
||||
DeleteExpectations(controllerKey string)
|
||||
GetExpectations(controllerKey string) map[Action]sets.String
|
||||
}
|
||||
|
||||
// NewResourceExpectations returns a common Expectations.
|
||||
func NewResourceExpectations() Expectations {
|
||||
return &realResourceExpectations{
|
||||
controllerCache: make(map[string]*realControllerResourceExpectations),
|
||||
}
|
||||
}
|
||||
|
||||
type realResourceExpectations struct {
|
||||
sync.Mutex
|
||||
// key: parent key, workload namespace/name
|
||||
controllerCache map[string]*realControllerResourceExpectations
|
||||
}
|
||||
|
||||
type realControllerResourceExpectations struct {
|
||||
// item: name for this object
|
||||
objsCache map[Action]sets.String
|
||||
firstUnsatisfiedTimestamp time.Time
|
||||
}
|
||||
|
||||
func (r *realResourceExpectations) GetExpectations(controllerKey string) map[Action]sets.String {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
expectations := r.controllerCache[controllerKey]
|
||||
if expectations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := make(map[Action]sets.String, len(expectations.objsCache))
|
||||
for k, v := range expectations.objsCache {
|
||||
res[k] = sets.NewString(v.List()...)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (r *realResourceExpectations) Expect(controllerKey string, action Action, name string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
expectations := r.controllerCache[controllerKey]
|
||||
if expectations == nil {
|
||||
expectations = &realControllerResourceExpectations{
|
||||
objsCache: make(map[Action]sets.String),
|
||||
}
|
||||
r.controllerCache[controllerKey] = expectations
|
||||
}
|
||||
|
||||
if s := expectations.objsCache[action]; s != nil {
|
||||
s.Insert(name)
|
||||
} else {
|
||||
expectations.objsCache[action] = sets.NewString(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *realResourceExpectations) Observe(controllerKey string, action Action, name string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
expectations := r.controllerCache[controllerKey]
|
||||
if expectations == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s := expectations.objsCache[action]
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.Delete(name)
|
||||
|
||||
for _, s := range expectations.objsCache {
|
||||
if s.Len() > 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
delete(r.controllerCache, controllerKey)
|
||||
}
|
||||
|
||||
func (r *realResourceExpectations) SatisfiedExpectations(controllerKey string) (bool, time.Duration, map[Action][]string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
expectations := r.controllerCache[controllerKey]
|
||||
if expectations == nil {
|
||||
return true, 0, nil
|
||||
}
|
||||
|
||||
for a, s := range expectations.objsCache {
|
||||
if s.Len() > 0 {
|
||||
if expectations.firstUnsatisfiedTimestamp.IsZero() {
|
||||
expectations.firstUnsatisfiedTimestamp = time.Now()
|
||||
}
|
||||
return false, time.Since(expectations.firstUnsatisfiedTimestamp), map[Action][]string{a: s.List()}
|
||||
}
|
||||
}
|
||||
|
||||
delete(r.controllerCache, controllerKey)
|
||||
return true, 0, nil
|
||||
}
|
||||
|
||||
func (r *realResourceExpectations) DeleteExpectations(controllerKey string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
delete(r.controllerCache, controllerKey)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 expectations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceExpectations(t *testing.T) {
|
||||
e := NewResourceExpectations()
|
||||
controllerKey01 := "default/cs01"
|
||||
controllerKey02 := "default/cs02"
|
||||
pod01 := "pod01"
|
||||
pod02 := "pod02"
|
||||
|
||||
e.Expect(controllerKey01, Create, pod01)
|
||||
e.Expect(controllerKey01, Create, pod02)
|
||||
e.Expect(controllerKey01, Delete, pod01)
|
||||
if ok, _, _ := e.SatisfiedExpectations(controllerKey01); ok {
|
||||
t.Fatalf("expected not satisfied")
|
||||
}
|
||||
|
||||
e.Observe(controllerKey01, Create, pod02)
|
||||
e.Observe(controllerKey01, Create, pod01)
|
||||
if ok, _, _ := e.SatisfiedExpectations(controllerKey01); ok {
|
||||
t.Fatalf("expected not satisfied")
|
||||
}
|
||||
|
||||
e.Observe(controllerKey02, Delete, pod01)
|
||||
if ok, _, _ := e.SatisfiedExpectations(controllerKey01); ok {
|
||||
t.Fatalf("expected not satisfied")
|
||||
}
|
||||
|
||||
e.Observe(controllerKey01, Delete, pod01)
|
||||
if ok, _, _ := e.SatisfiedExpectations(controllerKey01); !ok {
|
||||
t.Fatalf("expected satisfied")
|
||||
}
|
||||
|
||||
e.Observe(controllerKey01, Create, pod01)
|
||||
e.Observe(controllerKey01, Create, pod02)
|
||||
e.DeleteExpectations(controllerKey01)
|
||||
if ok, _, _ := e.SatisfiedExpectations(controllerKey01); !ok {
|
||||
t.Fatalf("expected satisfied")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,25 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
appsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
appsv1beta1 "github.com/openkruise/kruise-api/apps/v1beta1"
|
||||
|
|
@ -11,29 +28,26 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func ParseStatefulSetInfo(object client.Object, namespacedName types.NamespacedName) *WorkloadInfo {
|
||||
workloadGVKWithName := fmt.Sprintf("%v(%v)", object.GetObjectKind().GroupVersionKind(), namespacedName)
|
||||
selector, err := getSelector(object)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to parse selector for workload(%v)", workloadGVKWithName)
|
||||
// ParseWorkload parse workload as WorkloadInfo
|
||||
func ParseWorkload(object client.Object) *WorkloadInfo {
|
||||
if object == nil || reflect.ValueOf(object).IsNil() {
|
||||
return nil
|
||||
}
|
||||
key := client.ObjectKeyFromObject(object)
|
||||
gvk := object.GetObjectKind().GroupVersionKind()
|
||||
return &WorkloadInfo{
|
||||
ObjectMeta: *getMetadata(object),
|
||||
MaxUnavailable: getStatefulSetMaxUnavailable(object),
|
||||
Replicas: pointer.Int32(GetReplicas(object)),
|
||||
Status: ParseWorkloadStatus(object),
|
||||
Selector: selector,
|
||||
GVKWithName: workloadGVKWithName,
|
||||
LogKey: fmt.Sprintf("%s (%s)", key, gvk),
|
||||
ObjectMeta: *getMetadata(object),
|
||||
Replicas: GetReplicas(object),
|
||||
Status: *ParseWorkloadStatus(object),
|
||||
}
|
||||
}
|
||||
|
||||
// IsStatefulSetRollingUpdate return true if updateStrategy of object is rollingUpdate type.
|
||||
func IsStatefulSetRollingUpdate(object client.Object) bool {
|
||||
switch o := object.(type) {
|
||||
case *apps.StatefulSet:
|
||||
|
|
@ -51,6 +65,7 @@ func IsStatefulSetRollingUpdate(object client.Object) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// SetStatefulSetPartition set partition to object
|
||||
func SetStatefulSetPartition(object client.Object, partition int32) {
|
||||
switch o := object.(type) {
|
||||
case *apps.StatefulSet:
|
||||
|
|
@ -97,6 +112,7 @@ func SetStatefulSetPartition(object client.Object, partition int32) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetStatefulSetPartition get partition of object
|
||||
func GetStatefulSetPartition(object client.Object) int32 {
|
||||
partition := int32(0)
|
||||
switch o := object.(type) {
|
||||
|
|
@ -119,6 +135,7 @@ func GetStatefulSetPartition(object client.Object) int32 {
|
|||
return partition
|
||||
}
|
||||
|
||||
// IsStatefulSetUnorderedUpdate return true if the updateStrategy of object is unordered update
|
||||
func IsStatefulSetUnorderedUpdate(object client.Object) bool {
|
||||
switch o := object.(type) {
|
||||
case *apps.StatefulSet:
|
||||
|
|
@ -136,6 +153,7 @@ func IsStatefulSetUnorderedUpdate(object client.Object) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// getStatefulSetMaxUnavailable return maxUnavailable field of object
|
||||
func getStatefulSetMaxUnavailable(object client.Object) *intstr.IntOrString {
|
||||
switch o := object.(type) {
|
||||
case *apps.StatefulSet:
|
||||
|
|
@ -156,6 +174,7 @@ func getStatefulSetMaxUnavailable(object client.Object) *intstr.IntOrString {
|
|||
}
|
||||
}
|
||||
|
||||
// ParseWorkloadStatus parse status of object as WorkloadStatus
|
||||
func ParseWorkloadStatus(object client.Object) *WorkloadStatus {
|
||||
switch o := object.(type) {
|
||||
case *apps.Deployment:
|
||||
|
|
@ -165,6 +184,7 @@ func ParseWorkloadStatus(object client.Object) *WorkloadStatus {
|
|||
AvailableReplicas: o.Status.AvailableReplicas,
|
||||
UpdatedReplicas: o.Status.UpdatedReplicas,
|
||||
ObservedGeneration: o.Status.ObservedGeneration,
|
||||
UpdateRevision: ComputeHash(&o.Spec.Template, nil),
|
||||
}
|
||||
|
||||
case *appsv1alpha1.CloneSet:
|
||||
|
|
@ -220,20 +240,22 @@ func ParseWorkloadStatus(object client.Object) *WorkloadStatus {
|
|||
|
||||
// GetReplicas return replicas from client workload object
|
||||
func GetReplicas(object client.Object) int32 {
|
||||
replicas := int32(1)
|
||||
switch o := object.(type) {
|
||||
case *apps.Deployment:
|
||||
return *o.Spec.Replicas
|
||||
replicas = *o.Spec.Replicas
|
||||
case *appsv1alpha1.CloneSet:
|
||||
return *o.Spec.Replicas
|
||||
replicas = *o.Spec.Replicas
|
||||
case *apps.StatefulSet:
|
||||
return *o.Spec.Replicas
|
||||
replicas = *o.Spec.Replicas
|
||||
case *appsv1beta1.StatefulSet:
|
||||
return *o.Spec.Replicas
|
||||
replicas = *o.Spec.Replicas
|
||||
case *unstructured.Unstructured:
|
||||
return parseReplicasFromUnstructured(o)
|
||||
replicas = parseReplicasFromUnstructured(o)
|
||||
default:
|
||||
panic("unsupported workload type to ParseReplicasFrom function")
|
||||
}
|
||||
return replicas
|
||||
}
|
||||
|
||||
// GetTemplate return pod template spec for client workload object
|
||||
|
|
@ -354,6 +376,7 @@ func parseMetadataFromUnstructured(object *unstructured.Unstructured) *metav1.Ob
|
|||
return meta
|
||||
}
|
||||
|
||||
// unmarshalIntStr return *intstr.IntOrString
|
||||
func unmarshalIntStr(m interface{}) *intstr.IntOrString {
|
||||
field := &intstr.IntOrString{}
|
||||
data, _ := json.Marshal(m)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 util
|
||||
|
||||
import (
|
||||
|
|
@ -329,12 +345,11 @@ func TestWorkloadParse(t *testing.T) {
|
|||
Expect(err).NotTo(HaveOccurred())
|
||||
uobject := &unstructured.Unstructured{Object: uo}
|
||||
Expect(reflect.DeepEqual(GetTemplate(uobject), &o.Spec.Template)).Should(BeTrue())
|
||||
statefulsetInfo := ParseStatefulSetInfo(uobject, client.ObjectKeyFromObject(uobject))
|
||||
statefulsetInfo := ParseWorkload(uobject)
|
||||
{
|
||||
Expect(statefulsetInfo.MaxUnavailable).Should(BeNil())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.ObjectMeta, o.ObjectMeta)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Generation, o.Generation)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Replicas, o.Spec.Replicas)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Replicas, *o.Spec.Replicas)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Status.Replicas, o.Status.Replicas)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Status.ReadyReplicas, o.Status.ReadyReplicas)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Status.AvailableReplicas, o.Status.AvailableReplicas)).Should(BeTrue())
|
||||
|
|
@ -343,21 +358,17 @@ func TestWorkloadParse(t *testing.T) {
|
|||
Expect(reflect.DeepEqual(statefulsetInfo.Status.StableRevision, o.Status.CurrentRevision)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Status.UpdateRevision, o.Status.UpdateRevision)).Should(BeTrue())
|
||||
Expect(statefulsetInfo.Status.UpdatedReadyReplicas).Should(BeNumerically("==", 0))
|
||||
selector, err := metav1.LabelSelectorAsSelector(o.Spec.Selector)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Selector, selector)).Should(BeTrue())
|
||||
}
|
||||
case *appsv1beta1.StatefulSet:
|
||||
uo, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
uobject := &unstructured.Unstructured{Object: uo}
|
||||
Expect(reflect.DeepEqual(GetTemplate(uobject), &o.Spec.Template)).Should(BeTrue())
|
||||
statefulsetInfo := ParseStatefulSetInfo(uobject, client.ObjectKeyFromObject(uobject))
|
||||
statefulsetInfo := ParseWorkload(uobject)
|
||||
{
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.ObjectMeta, o.ObjectMeta)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Generation, o.Generation)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Replicas, o.Spec.Replicas)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.MaxUnavailable, o.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Replicas, *o.Spec.Replicas)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Status.Replicas, o.Status.Replicas)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Status.ReadyReplicas, o.Status.ReadyReplicas)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Status.AvailableReplicas, o.Status.AvailableReplicas)).Should(BeTrue())
|
||||
|
|
@ -366,9 +377,6 @@ func TestWorkloadParse(t *testing.T) {
|
|||
Expect(reflect.DeepEqual(statefulsetInfo.Status.StableRevision, o.Status.CurrentRevision)).Should(BeTrue())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Status.UpdateRevision, o.Status.UpdateRevision)).Should(BeTrue())
|
||||
Expect(statefulsetInfo.Status.UpdatedReadyReplicas).Should(BeNumerically("==", 0))
|
||||
selector, err := metav1.LabelSelectorAsSelector(o.Spec.Selector)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(reflect.DeepEqual(statefulsetInfo.Selector, selector)).Should(BeTrue())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ func IsConsistentWithRevision(pod *v1.Pod, revision string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// IsEqualRevision return true if a and b have equal revision label
|
||||
func IsEqualRevision(a, b *v1.Pod) bool {
|
||||
if a.Labels[appsv1.DefaultDeploymentUniqueLabelKey] != "" &&
|
||||
a.Labels[appsv1.DefaultDeploymentUniqueLabelKey] == b.Labels[appsv1.DefaultDeploymentUniqueLabelKey] {
|
||||
|
|
|
|||
|
|
@ -30,14 +30,13 @@ import (
|
|||
"github.com/openkruise/rollouts/pkg/feature"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
netv1 "k8s.io/api/networking/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/rand"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
|
@ -68,18 +67,49 @@ type WorkloadStatus struct {
|
|||
|
||||
type WorkloadInfo struct {
|
||||
metav1.ObjectMeta
|
||||
Paused bool
|
||||
Replicas *int32
|
||||
GVKWithName string
|
||||
Selector labels.Selector
|
||||
MaxUnavailable *intstr.IntOrString
|
||||
Status *WorkloadStatus
|
||||
LogKey string
|
||||
Replicas int32
|
||||
Status WorkloadStatus
|
||||
}
|
||||
|
||||
func NewWorkloadInfo() *WorkloadInfo {
|
||||
return &WorkloadInfo{
|
||||
Status: &WorkloadStatus{},
|
||||
// IsStable return ture if observed generation >= generation
|
||||
func (w *WorkloadInfo) IsStable() bool {
|
||||
return w.Status.ObservedGeneration >= w.Generation
|
||||
}
|
||||
|
||||
// IsPromoted return true if replicas == updatedReplicas
|
||||
func (w *WorkloadInfo) IsPromoted() bool {
|
||||
return w.Status.Replicas == w.Status.UpdatedReplicas
|
||||
}
|
||||
|
||||
// IsScaling return true if observed replicas != replicas
|
||||
func (w *WorkloadInfo) IsScaling(observed int32) bool {
|
||||
if observed == -1 {
|
||||
return false
|
||||
}
|
||||
return w.Replicas != observed
|
||||
}
|
||||
|
||||
// IsRollback return true if workload stable revision equals to update revision.
|
||||
// this function is edge-triggerred.
|
||||
func (w *WorkloadInfo) IsRollback(observedStable, observedUpdate string) bool {
|
||||
if observedUpdate == "" {
|
||||
return false
|
||||
}
|
||||
// updateRevision == CurrentRevision means CloneSet is rolling back or newly-created.
|
||||
return w.Status.UpdateRevision == w.Status.StableRevision &&
|
||||
// stableRevision == UpdateRevision means CloneSet is rolling back instead of newly-created.
|
||||
observedStable == w.Status.UpdateRevision &&
|
||||
// StableRevision != observed UpdateRevision means the rollback event have not been observed.
|
||||
observedStable != observedUpdate
|
||||
}
|
||||
|
||||
// IsRevisionNotEqual this function will return true if observed update revision != update revision.
|
||||
func (w *WorkloadInfo) IsRevisionNotEqual(observed string) bool {
|
||||
if observed == "" {
|
||||
return false
|
||||
}
|
||||
return w.Status.UpdateRevision != observed
|
||||
}
|
||||
|
||||
// DeepHashObject writes specified object to hash using the spew library
|
||||
|
|
@ -269,8 +299,76 @@ func IsWorkloadType(object client.Object, t WorkloadType) bool {
|
|||
return WorkloadType(strings.ToLower(object.GetLabels()[WorkloadTypeLabel])) == t
|
||||
}
|
||||
|
||||
// GenRandomStr returns a safe encoded string with a specific length
|
||||
func GenRandomStr(length int) string {
|
||||
randStr := rand.String(length)
|
||||
return rand.SafeEncodeString(randStr)
|
||||
// DeploymentMaxUnavailable returns the maximum unavailable pods a rolling deployment can take.
|
||||
func DeploymentMaxUnavailable(deployment *apps.Deployment) int32 {
|
||||
strategy := deployment.Spec.Strategy
|
||||
if strategy.Type != apps.RollingUpdateDeploymentStrategyType || *(deployment.Spec.Replicas) == 0 {
|
||||
return int32(0)
|
||||
}
|
||||
// Error caught by validation
|
||||
_, maxUnavailable, _ := resolveFenceposts(strategy.RollingUpdate.MaxSurge, strategy.RollingUpdate.MaxUnavailable, *(deployment.Spec.Replicas))
|
||||
if maxUnavailable > *deployment.Spec.Replicas {
|
||||
return *deployment.Spec.Replicas
|
||||
}
|
||||
return maxUnavailable
|
||||
}
|
||||
|
||||
// resolveFenceposts resolves both maxSurge and maxUnavailable. This needs to happen in one
|
||||
// step. For example:
|
||||
//
|
||||
// 2 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1), then old(-1), then new(+1)
|
||||
// 1 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1)
|
||||
// 2 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
|
||||
// 1 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1)
|
||||
// 2 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
|
||||
// 1 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1)
|
||||
func resolveFenceposts(maxSurge, maxUnavailable *intstr.IntOrString, desired int32) (int32, int32, error) {
|
||||
surge, err := intstr.GetScaledValueFromIntOrPercent(intstr.ValueOrDefault(maxSurge, intstr.FromInt(0)), int(desired), true)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
unavailable, err := intstr.GetScaledValueFromIntOrPercent(intstr.ValueOrDefault(maxUnavailable, intstr.FromInt(0)), int(desired), false)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if surge == 0 && unavailable == 0 {
|
||||
// Validation should never allow the user to explicitly use zero values for both maxSurge
|
||||
// maxUnavailable. Due to rounding down maxUnavailable though, it may resolve to zero.
|
||||
// If both fenceposts resolve to zero, then we should set maxUnavailable to 1 on the
|
||||
// theory that surge might not work due to quota.
|
||||
unavailable = 1
|
||||
}
|
||||
|
||||
return int32(surge), int32(unavailable), nil
|
||||
}
|
||||
|
||||
// GetEmptyObjectWithKey return an empty object with the same namespaced name
|
||||
func GetEmptyObjectWithKey(object client.Object) client.Object {
|
||||
var empty client.Object
|
||||
switch object.(type) {
|
||||
case *v1.Pod:
|
||||
empty = &v1.Pod{}
|
||||
case *v1.Service:
|
||||
empty = &v1.Service{}
|
||||
case *netv1.Ingress:
|
||||
empty = &netv1.Ingress{}
|
||||
case *apps.Deployment:
|
||||
empty = &apps.Deployment{}
|
||||
case *apps.ReplicaSet:
|
||||
empty = &apps.ReplicaSet{}
|
||||
case *apps.StatefulSet:
|
||||
empty = &apps.StatefulSet{}
|
||||
case *appsv1alpha1.CloneSet:
|
||||
empty = &appsv1alpha1.CloneSet{}
|
||||
case *appsv1beta1.StatefulSet:
|
||||
empty = &appsv1beta1.StatefulSet{}
|
||||
case *unstructured.Unstructured:
|
||||
unstructure := &unstructured.Unstructured{}
|
||||
unstructure.SetGroupVersionKind(object.GetObjectKind().GroupVersionKind())
|
||||
empty = unstructure
|
||||
}
|
||||
empty.SetName(object.GetName())
|
||||
empty.SetNamespace(object.GetNamespace())
|
||||
return empty
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 util
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
Copyright 2022 The Kruise 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 validating
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package mutating
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net/http"
|
||||
|
||||
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
|
|
@ -189,7 +190,7 @@ func (h *WorkloadHandler) handleStatefulSetLikeWorkload(newObj, oldObj *unstruct
|
|||
}
|
||||
|
||||
changed = true
|
||||
util.SetStatefulSetPartition(newObj, replicas)
|
||||
util.SetStatefulSetPartition(newObj, math.MaxInt16)
|
||||
state := &util.RolloutState{RolloutName: rollout.Name}
|
||||
by, _ := json.Marshal(state)
|
||||
annotation := newObj.GetAnnotations()
|
||||
|
|
@ -276,8 +277,7 @@ func (h *WorkloadHandler) handleCloneSet(newObj, oldObj *kruiseappsv1alpha1.Clon
|
|||
|
||||
klog.Infof("cloneSet(%s/%s) will be in rollout progressing, and paused", newObj.Namespace, newObj.Name)
|
||||
changed = true
|
||||
// need set workload paused = true
|
||||
newObj.Spec.UpdateStrategy.Paused = true
|
||||
// need set workload partition = 100%
|
||||
newObj.Spec.UpdateStrategy.Partition = &intstr.IntOrString{Type: intstr.String, StrVal: "100%"}
|
||||
state := &util.RolloutState{RolloutName: rollout.Name}
|
||||
by, _ := json.Marshal(state)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package mutating
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
|
@ -428,7 +429,6 @@ func TestHandlerCloneSet(t *testing.T) {
|
|||
obj := cloneSetDemo.DeepCopy()
|
||||
obj.Spec.Template.Spec.Containers[0].Image = "echoserver:v2"
|
||||
obj.Annotations[util.InRolloutProgressingAnnotation] = `{"rolloutName":"rollout-demo"}`
|
||||
obj.Spec.UpdateStrategy.Paused = true
|
||||
obj.Spec.UpdateStrategy.Partition = &intstr.IntOrString{Type: intstr.String, StrVal: "100%"}
|
||||
return obj
|
||||
},
|
||||
|
|
@ -493,7 +493,7 @@ func TestHandleStatefulSet(t *testing.T) {
|
|||
obj := statefulset.DeepCopy()
|
||||
obj.Spec.Template.Spec.Containers[0].Image = "echoserver:v2"
|
||||
obj.Annotations[util.InRolloutProgressingAnnotation] = `{"rolloutName":"rollout-demo"}`
|
||||
obj.Spec.UpdateStrategy.RollingUpdate.Partition = pointer.Int32(10)
|
||||
obj.Spec.UpdateStrategy.RollingUpdate.Partition = pointer.Int32(math.MaxInt16)
|
||||
return obj
|
||||
},
|
||||
getRollout: func() *appsv1alpha1.Rollout {
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ var _ = SIGDescribe("BatchRelease", func() {
|
|||
clone := &rolloutsv1alpha1.BatchRelease{}
|
||||
Expect(GetObject(release.Namespace, release.Name, clone)).NotTo(HaveOccurred())
|
||||
return clone.Status.Phase
|
||||
}, 15*time.Minute, 5*time.Second).Should(Equal(rolloutsv1alpha1.RolloutPhaseCancelled))
|
||||
}, 15*time.Minute, 5*time.Second).Should(Equal(rolloutsv1alpha1.RolloutPhaseCompleted))
|
||||
})
|
||||
|
||||
It("V1->V2: ScalingUp, Percentage, 100%, Succeeded", func() {
|
||||
|
|
@ -799,7 +799,7 @@ var _ = SIGDescribe("BatchRelease", func() {
|
|||
clone := &rolloutsv1alpha1.BatchRelease{}
|
||||
Expect(GetObject(release.Namespace, release.Name, clone)).NotTo(HaveOccurred())
|
||||
return clone.Status.Phase
|
||||
}, 15*time.Minute, 5*time.Second).Should(Equal(rolloutsv1alpha1.RolloutPhaseCancelled))
|
||||
}, 15*time.Minute, 5*time.Second).Should(Equal(rolloutsv1alpha1.RolloutPhaseCompleted))
|
||||
})
|
||||
|
||||
It("V1->V2: ScalingUp, Percentage, 100%, Succeeded", func() {
|
||||
|
|
@ -1134,7 +1134,7 @@ var _ = SIGDescribe("BatchRelease", func() {
|
|||
clone := &rolloutsv1alpha1.BatchRelease{}
|
||||
Expect(GetObject(release.Namespace, release.Name, clone)).NotTo(HaveOccurred())
|
||||
return clone.Status.Phase
|
||||
}, 15*time.Minute, 5*time.Second).Should(Equal(rolloutsv1alpha1.RolloutPhaseCancelled))
|
||||
}, 15*time.Minute, 5*time.Second).Should(Equal(rolloutsv1alpha1.RolloutPhaseCompleted))
|
||||
})
|
||||
|
||||
It("Rollback V1->V2: Delete BatchRelease, Percentage, 100%, Succeeded", func() {
|
||||
|
|
|
|||
|
|
@ -1366,7 +1366,7 @@ var _ = SIGDescribe("Rollout", func() {
|
|||
workload.Spec.Template.Spec.Containers[0].Env = newEnvs
|
||||
UpdateDeployment(workload)
|
||||
By("Update deployment env NODE_NAME from(version1) -> to(version2)")
|
||||
time.Sleep(time.Second * 3)
|
||||
time.Sleep(time.Second * 10)
|
||||
|
||||
// check workload status & paused
|
||||
Expect(GetObject(workload.Name, workload)).NotTo(HaveOccurred())
|
||||
|
|
@ -1374,7 +1374,7 @@ var _ = SIGDescribe("Rollout", func() {
|
|||
By("check deployment status & paused success")
|
||||
|
||||
// delete rollout
|
||||
Expect(k8sClient.DeleteAllOf(context.TODO(), &rolloutsv1alpha1.Rollout{}, client.InNamespace(namespace), client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed())
|
||||
Expect(k8sClient.Delete(context.TODO(), rollout, client.PropagationPolicy(metav1.DeletePropagationBackground))).Should(Succeed())
|
||||
WaitRolloutNotFound(rollout.Name)
|
||||
WaitDeploymentAllPodsReady(workload)
|
||||
// check service & ingress & deployment
|
||||
|
|
@ -4158,9 +4158,7 @@ var _ = SIGDescribe("Rollout", func() {
|
|||
CheckPodBatchLabel(workload.Namespace, workload.Spec.Selector, "2", "4", 1)
|
||||
CheckPodBatchLabel(workload.Namespace, workload.Spec.Selector, "2", "5", 1)
|
||||
})
|
||||
})
|
||||
|
||||
KruiseDescribe("Test", func() {
|
||||
It("failure threshold", func() {
|
||||
By("Creating Rollout...")
|
||||
rollout := &rolloutsv1alpha1.Rollout{}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ spec:
|
|||
containers:
|
||||
- name: echoserver
|
||||
image: cilium/echoserver:latest
|
||||
# imagePullPolicy: IfNotPresent
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ spec:
|
|||
containers:
|
||||
- name: echoserver
|
||||
image: cilium/echoserver:latest
|
||||
# imagePullPolicy: IfNotPresent
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
|
|
|
|||
Loading…
Reference in New Issue