feat<rollout>:support time slices

This commit is contained in:
zhousong 2022-09-01 20:07:15 +08:00
parent 3d1df9c315
commit fbc1700362
8 changed files with 315 additions and 2 deletions

View File

@ -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 (

View File

@ -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

View File

@ -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

View File

@ -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)
}

63
pkg/util/time_utils.go Normal file
View File

@ -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
}

118
pkg/util/time_utils_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -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"))

View File

@ -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,