feat<rollout>:support time slices
This commit is contained in:
parent
3d1df9c315
commit
fbc1700362
|
|
@ -31,6 +31,10 @@ type RolloutSpec struct {
|
|||
// Important: Run "make" to regenerate code after modifying this file
|
||||
// ObjectRef indicates workload
|
||||
ObjectRef ObjectRef `json:"objectRef"`
|
||||
// TimeSlices define some time slices within which the CanaryStrategy is allowed to run in every day
|
||||
// if not define, TimeSlices is all day
|
||||
// +optional
|
||||
TimeSlices []TimeSlice `json:"timeSlices,omitempty"`
|
||||
// rollout strategy
|
||||
Strategy RolloutStrategy `json:"strategy"`
|
||||
// RolloutID should be changed before each workload revision publication.
|
||||
|
|
@ -81,6 +85,14 @@ type RolloutStrategy struct {
|
|||
// BlueGreen *BlueGreenStrategy `json:"blueGreen,omitempty"`
|
||||
}
|
||||
|
||||
//TimeSlice define the start time and end time
|
||||
type TimeSlice struct {
|
||||
//StartTime is this TimeSlice start time
|
||||
StartTime string `json:"startTime,omitempty"`
|
||||
//EndTime is this TimeSlice end time
|
||||
EndTime string `json:"endTime,omitempty"`
|
||||
}
|
||||
|
||||
type RolloutStrategyType string
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -428,6 +428,11 @@ func (in *RolloutPause) DeepCopy() *RolloutPause {
|
|||
func (in *RolloutSpec) DeepCopyInto(out *RolloutSpec) {
|
||||
*out = *in
|
||||
in.ObjectRef.DeepCopyInto(&out.ObjectRef)
|
||||
if in.TimeSlices != nil {
|
||||
in, out := &in.TimeSlices, &out.TimeSlices
|
||||
*out = make([]TimeSlice, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
in.Strategy.DeepCopyInto(&out.Strategy)
|
||||
}
|
||||
|
||||
|
|
@ -488,6 +493,21 @@ func (in *RolloutStrategy) DeepCopy() *RolloutStrategy {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TimeSlice) DeepCopyInto(out *TimeSlice) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeSlice.
|
||||
func (in *TimeSlice) DeepCopy() *TimeSlice {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TimeSlice)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TrafficRouting) DeepCopyInto(out *TrafficRouting) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -138,9 +138,21 @@ func (r *rolloutContext) doCanaryUpgrade() (bool, error) {
|
|||
return false, nil
|
||||
}*/
|
||||
|
||||
// verify whether batchRelease configuration is the latest
|
||||
//verify the step run time (now) whether in time slices
|
||||
steps := len(r.rollout.Spec.Strategy.Canary.Steps)
|
||||
canaryStatus := r.newStatus.CanaryStatus
|
||||
cond := util.GetRolloutCondition(*r.newStatus, rolloutv1alpha1.RolloutConditionProgressing)
|
||||
expectedTime, ok := r.isAllowRun(time.Now())
|
||||
if !ok {
|
||||
cond.Message = fmt.Sprintf("Rollout (%d/%d) will be start at time %s, because now is not in time slices",
|
||||
canaryStatus.CurrentStepIndex, steps,
|
||||
expectedTime.Format("2006-01-02 15:04:05"))
|
||||
r.newStatus.Message = cond.Message
|
||||
r.recheckTime = &expectedTime
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// verify whether batchRelease configuration is the latest
|
||||
isLatest, err := r.batchControl.Verify(canaryStatus.CurrentStepIndex)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -158,7 +170,6 @@ func (r *rolloutContext) doCanaryUpgrade() (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
batchData := util.DumpJSON(batch.Status)
|
||||
cond := util.GetRolloutCondition(*r.newStatus, rolloutv1alpha1.RolloutConditionProgressing)
|
||||
cond.Message = fmt.Sprintf("Rollout is in step(%d/%d), and upgrade workload new versions", canaryStatus.CurrentStepIndex, steps)
|
||||
r.newStatus.Message = cond.Message
|
||||
// promote workload next batch release
|
||||
|
|
|
|||
|
|
@ -95,3 +95,8 @@ func (r *rolloutContext) podRevisionLabelKey() string {
|
|||
}
|
||||
return r.workload.RevisionLabelKey
|
||||
}
|
||||
|
||||
//isAllowRun return next allow run time
|
||||
func (r *rolloutContext) isAllowRun(expectedTime time.Time) (time.Time, bool) {
|
||||
return util.TimeInSlice(expectedTime, r.rollout.Spec.TimeSlices)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Copyright 2022 The KubePort Authors.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
rolloutv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"k8s.io/klog/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
//ValidateTime used to validate _time whether right
|
||||
func ValidateTime(date, _time string) (time.Time, error) {
|
||||
layout := "2006-01-02 15:04:05"
|
||||
if date == "" {
|
||||
date = "2022-08-21"
|
||||
}
|
||||
return time.ParseInLocation(layout, fmt.Sprintf("%s %s", date, _time), time.Local)
|
||||
}
|
||||
|
||||
//TimeInSlice used to validate the expectedTime whether in the timeSlices.
|
||||
//it returns expectedTime and 'false' if the timeSlices is wrong,so you have to make sure the Time Slice is correct.
|
||||
//it returns expectedTime and 'true' if the expectedTime is in this timeSlices.
|
||||
//it returns adjacent time and 'false' if the expectedTime is not in this timeSlices.
|
||||
func TimeInSlice(expectedTime time.Time, timeSlices []rolloutv1alpha1.TimeSlice) (time.Time, bool) {
|
||||
date := expectedTime.Format("2006-01-02")
|
||||
if len(timeSlices) == 0 {
|
||||
return expectedTime, true
|
||||
}
|
||||
var (
|
||||
err error
|
||||
start time.Time
|
||||
end time.Time
|
||||
minSub = time.Hour * 48
|
||||
)
|
||||
for i, timeSlice := range timeSlices {
|
||||
start, err = ValidateTime(date, timeSlice.StartTime)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("timeSlices[%d] StartTime is err %s", i, err.Error())
|
||||
return expectedTime, false
|
||||
}
|
||||
end, err = ValidateTime(date, timeSlice.EndTime)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("timeSlices[%d] EndTime is err %s", i, err.Error())
|
||||
return expectedTime, false
|
||||
}
|
||||
|
||||
if expectedTime.After(start) && expectedTime.Before(end) {
|
||||
return expectedTime, true
|
||||
}
|
||||
|
||||
subTime := start.Sub(expectedTime)
|
||||
if subTime < 0 {
|
||||
subTime += time.Hour * 24
|
||||
}
|
||||
if subTime < minSub {
|
||||
minSub = subTime
|
||||
}
|
||||
}
|
||||
return expectedTime.Add(minSub), false
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
Copyright 2022 The KubePort Authors.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
rolloutv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var timeSlices = []rolloutv1alpha1.TimeSlice{
|
||||
{
|
||||
StartTime: "00:00:00",
|
||||
EndTime: "2:00:00",
|
||||
},
|
||||
{
|
||||
StartTime: "10:00:00",
|
||||
EndTime: "12:00:00",
|
||||
},
|
||||
{
|
||||
StartTime: "16:00:00",
|
||||
EndTime: "20:00:00",
|
||||
},
|
||||
}
|
||||
var layout = "2006-01-02 15:04:05"
|
||||
|
||||
func TestTimeInSlice(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
test := []struct {
|
||||
Name string
|
||||
TestTime string
|
||||
ExpectedTime string
|
||||
ExpectedRes bool
|
||||
}{
|
||||
{
|
||||
Name: "in current slice",
|
||||
TestTime: "2022-08-08 1:03:03",
|
||||
ExpectedTime: "2022-08-08 1:03:03",
|
||||
ExpectedRes: true,
|
||||
},
|
||||
{
|
||||
Name: "in current day",
|
||||
TestTime: "2022-08-08 13:00:00",
|
||||
ExpectedTime: "2022-08-08 16:00:00",
|
||||
ExpectedRes: false,
|
||||
},
|
||||
{
|
||||
Name: "in next day",
|
||||
TestTime: "2022-08-08 22:03:03",
|
||||
ExpectedTime: "2022-08-09 00:00:00",
|
||||
ExpectedRes: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range test {
|
||||
t.Run(s.Name, func(t *testing.T) {
|
||||
testTime, _ := time.ParseInLocation(layout, s.TestTime, time.Local)
|
||||
expectedTime, _ := time.ParseInLocation(layout, s.ExpectedTime, time.Local)
|
||||
resTime, res := TimeInSlice(testTime, timeSlices)
|
||||
Expect(expectedTime.Unix()).Should(Equal(resTime.Unix()))
|
||||
Expect(s.ExpectedRes).Should(Equal(res))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTime(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
test := []struct {
|
||||
Name string
|
||||
Date string
|
||||
Time string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
Name: "right: date not empty",
|
||||
Date: "2022-08-08",
|
||||
Time: "00:00:00",
|
||||
expect: "2022-08-08 00:00:00",
|
||||
},
|
||||
{
|
||||
Name: "right: date is empty",
|
||||
Date: "",
|
||||
Time: "01:00:00",
|
||||
expect: "2022-08-21 01:00:00",
|
||||
},
|
||||
{
|
||||
Name: "wrong: time more then 24h",
|
||||
Date: "",
|
||||
Time: "25:00:00",
|
||||
},
|
||||
{
|
||||
Name: "wrong: time less then 0h",
|
||||
Date: "",
|
||||
Time: "-01:00:00",
|
||||
},
|
||||
{
|
||||
Name: "wrong: time is incomplete",
|
||||
Date: "",
|
||||
Time: "21:00",
|
||||
},
|
||||
}
|
||||
for _, s := range test {
|
||||
t.Run(s.Name, func(t *testing.T) {
|
||||
resTime, err := ValidateTime(s.Date, s.Time)
|
||||
if s.expect != "" {
|
||||
expectedTime, _ := time.ParseInLocation(layout, s.expect, time.Local)
|
||||
Expect(expectedTime.Unix()).Should(Equal(resTime.Unix()))
|
||||
} else {
|
||||
Expect(len(err.Error()) != 0).Should(BeTrue())
|
||||
t.Log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +140,7 @@ func (h *RolloutCreateUpdateHandler) validateRolloutConflict(rollout *appsv1alph
|
|||
|
||||
func validateRolloutSpec(rollout *appsv1alpha1.Rollout, fldPath *field.Path) field.ErrorList {
|
||||
errList := validateRolloutSpecObjectRef(&rollout.Spec.ObjectRef, fldPath.Child("ObjectRef"))
|
||||
errList = append(errList, validateRolloutSpecTimeSlices(rollout.Spec.TimeSlices, fldPath.Child("TimeSlices"))...)
|
||||
errList = append(errList, validateRolloutSpecStrategy(&rollout.Spec.Strategy, fldPath.Child("Strategy"))...)
|
||||
return errList
|
||||
}
|
||||
|
|
@ -155,6 +156,28 @@ func validateRolloutSpecObjectRef(objectRef *appsv1alpha1.ObjectRef, fldPath *fi
|
|||
}
|
||||
return nil
|
||||
}
|
||||
func validateRolloutSpecTimeSlices(timeSlices []appsv1alpha1.TimeSlice, fldPath *field.Path) field.ErrorList {
|
||||
if len(timeSlices) == 0 {
|
||||
return field.ErrorList{}
|
||||
}
|
||||
errList := field.ErrorList{}
|
||||
for i, t := range timeSlices {
|
||||
//Parse time, TimeDuration need >= 0 && <= 86400
|
||||
start, err := util.ValidateTime("", t.StartTime)
|
||||
if err != nil {
|
||||
return append(errList, field.Invalid(fldPath.Index(i).Child("StartTime"), t.StartTime, err.Error()))
|
||||
}
|
||||
end, err := util.ValidateTime("", t.EndTime)
|
||||
if err != nil {
|
||||
return append(errList, field.Invalid(fldPath.Index(i).Child("EndTime"), t.EndTime, err.Error()))
|
||||
}
|
||||
//startTime need less than endTime
|
||||
if start.After(end) {
|
||||
return append(errList, field.Invalid(fldPath.Index(i).Child("timeRange"), t, "startTime need less then endTime"))
|
||||
}
|
||||
}
|
||||
return errList
|
||||
}
|
||||
|
||||
func validateRolloutSpecStrategy(strategy *appsv1alpha1.RolloutStrategy, fldPath *field.Path) field.ErrorList {
|
||||
return validateRolloutSpecCanaryStrategy(strategy.Canary, fldPath.Child("Canary"))
|
||||
|
|
|
|||
|
|
@ -35,6 +35,20 @@ var (
|
|||
Name: "deployment-demo",
|
||||
},
|
||||
},
|
||||
TimeSlices: []appsv1alpha1.TimeSlice{
|
||||
{
|
||||
StartTime: "00:00:00",
|
||||
EndTime: "2:00:00",
|
||||
},
|
||||
{
|
||||
StartTime: "10:00:00",
|
||||
EndTime: "12:00:00",
|
||||
},
|
||||
{
|
||||
StartTime: "16:00:00",
|
||||
EndTime: "20:00:00",
|
||||
},
|
||||
},
|
||||
Strategy: appsv1alpha1.RolloutStrategy{
|
||||
Canary: &appsv1alpha1.CanaryStrategy{
|
||||
Steps: []appsv1alpha1.CanaryStep{
|
||||
|
|
@ -95,6 +109,15 @@ func TestRolloutValidateCreate(t *testing.T) {
|
|||
return []client.Object{rollout.DeepCopy()}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Normal case : time slices is nil",
|
||||
Succeed: true,
|
||||
GetObject: func() []client.Object {
|
||||
object := rollout.DeepCopy()
|
||||
object.Spec.TimeSlices = nil
|
||||
return []client.Object{object}
|
||||
},
|
||||
},
|
||||
/***********************************************************
|
||||
The following cases may lead to controller panic
|
||||
**********************************************************/
|
||||
|
|
@ -119,6 +142,44 @@ func TestRolloutValidateCreate(t *testing.T) {
|
|||
/****************************************************************
|
||||
The following cases may lead to that controller cannot work
|
||||
***************************************************************/
|
||||
{
|
||||
Name: "TimeSlices time is empty",
|
||||
Succeed: false,
|
||||
GetObject: func() []client.Object {
|
||||
object := rollout.DeepCopy()
|
||||
object.Spec.TimeSlices[0].StartTime = ""
|
||||
object.Spec.TimeSlices[0].EndTime = ""
|
||||
return []client.Object{object}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TimeSlices time is incomplete",
|
||||
Succeed: false,
|
||||
GetObject: func() []client.Object {
|
||||
object := rollout.DeepCopy()
|
||||
object.Spec.TimeSlices[0].StartTime = "00:00"
|
||||
return []client.Object{object}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TimeSlices start time more than end time",
|
||||
Succeed: false,
|
||||
GetObject: func() []client.Object {
|
||||
object := rollout.DeepCopy()
|
||||
object.Spec.TimeSlices[0].StartTime = "02:00:00"
|
||||
object.Spec.TimeSlices[0].EndTime = "00:00:00"
|
||||
return []client.Object{object}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TimeSlices time out of range",
|
||||
Succeed: false,
|
||||
GetObject: func() []client.Object {
|
||||
object := rollout.DeepCopy()
|
||||
object.Spec.TimeSlices[0].EndTime = "26:00:00"
|
||||
return []client.Object{object}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Service name is empty",
|
||||
Succeed: false,
|
||||
|
|
|
|||
Loading…
Reference in New Issue