feat<rollout>:support time slice and add test

Signed-off-by: MrSumeng <1206834441@qq.com>

Signed-off-by: MrSumeng <1206834441@qq.com>
This commit is contained in:
MrSumeng 2022-09-05 11:32:23 +08:00
parent e1ba1b0ea6
commit df21444717
9 changed files with 477 additions and 2 deletions

View File

@ -31,6 +31,9 @@ type RolloutSpec struct {
// Important: Run "make" to regenerate code after modifying this file
// ObjectRef indicates workload
ObjectRef ObjectRef `json:"objectRef"`
//AllowRunTime define allow run time for the rollout
//+optional
AllowRunTime AllowRunTime `json:"allowRunTime,omitempty"`
// rollout strategy
Strategy RolloutStrategy `json:"strategy"`
// RolloutID should be changed before each workload revision publication.
@ -70,6 +73,33 @@ type WorkloadRef struct {
SourceRevisionName string `json:"sourceRevisionName"`
}*/
type AllowRunTime struct {
// TimeZone allowed user set the time zone
// +optional
TimeZone *TimeZone `json:"timeZone,omitempty"`
// 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"`
}
type TimeZone struct {
//Name of the time zone
Name string `json:"name"`
//Offset
// +kubebuilder:validation:Minimum=-43200
// +kubebuilder:validation:Maximum=43200
Offset int `json:"offset"`
}
//TimeSlice define the start time and end time
type TimeSlice struct {
//StartTime is this TimeSlice start time
StartTime string `json:"startTime"`
//EndTime is this TimeSlice end time
EndTime string `json:"endTime"`
}
// RolloutStrategy defines strategy to apply during next rollout
type RolloutStrategy struct {
// Paused indicates that the Rollout is paused.

View File

@ -26,6 +26,31 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowRunTime) DeepCopyInto(out *AllowRunTime) {
*out = *in
if in.TimeZone != nil {
in, out := &in.TimeZone, &out.TimeZone
*out = new(TimeZone)
**out = **in
}
if in.TimeSlices != nil {
in, out := &in.TimeSlices, &out.TimeSlices
*out = make([]TimeSlice, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowRunTime.
func (in *AllowRunTime) DeepCopy() *AllowRunTime {
if in == nil {
return nil
}
out := new(AllowRunTime)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BatchRelease) DeepCopyInto(out *BatchRelease) {
*out = *in
@ -428,6 +453,7 @@ func (in *RolloutPause) DeepCopy() *RolloutPause {
func (in *RolloutSpec) DeepCopyInto(out *RolloutSpec) {
*out = *in
in.ObjectRef.DeepCopyInto(&out.ObjectRef)
in.AllowRunTime.DeepCopyInto(&out.AllowRunTime)
in.Strategy.DeepCopyInto(&out.Strategy)
}
@ -488,6 +514,36 @@ 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 *TimeZone) DeepCopyInto(out *TimeZone) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeZone.
func (in *TimeZone) DeepCopy() *TimeZone {
if in == nil {
return nil
}
out := new(TimeZone)
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

@ -56,6 +56,43 @@ spec:
spec:
description: RolloutSpec defines the desired state of Rollout
properties:
allowRunTime:
description: AllowRunTime define allow run time for the rollout
properties:
timeSlices:
description: TimeSlices define some time slices within which the
CanaryStrategy is allowed to run in every day if not define,
TimeSlices is all day
items:
description: TimeSlice define the start time and end time
properties:
endTime:
description: EndTime is this TimeSlice end time
type: string
startTime:
description: StartTime is this TimeSlice start time
type: string
required:
- endTime
- startTime
type: object
type: array
timeZone:
description: TimeZone allowed user set the time zone
properties:
name:
description: Name of the time zone
type: string
offset:
description: Offset
maximum: 43200
minimum: -43200
type: integer
required:
- name
- offset
type: object
type: object
objectRef:
description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Important: Run "make" to regenerate code after modifying this file

View File

@ -138,9 +138,28 @@ 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 {
localTime := expectedTime
if &r.rollout.Spec != nil && &r.rollout.Spec.AllowRunTime != nil {
localTime.In(util.TimeZone(r.rollout.Spec.AllowRunTime.TimeZone))
}
msg := fmt.Sprintf("Rollout (%d/%d) will be start at time %s(%s), because now is not in time slices",
canaryStatus.CurrentStepIndex, steps,
expectedTime.Format(util.DateTimeLayout),
localTime.Format(util.DateTimeZoneLayout))
klog.Info(msg)
cond.Message = msg
r.newStatus.Message = cond.Message
r.recheckTime = &expectedTime
return false, nil
}
// verify whether batchRelease configuration is the latest
isLatest, err := r.batchControl.Verify(canaryStatus.CurrentStepIndex)
if err != nil {
return false, err
@ -158,7 +177,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

@ -108,3 +108,7 @@ func getRolloutID(workload *util.Workload, rollout *rolloutv1alpha1.Rollout) str
}
return ""
}
func (r *rolloutContext) isAllowRun(expectedTime time.Time) (time.Time, bool) {
return util.TimeInSlice(expectedTime, &r.rollout.Spec.AllowRunTime)
}

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

@ -0,0 +1,85 @@
/*
Copyright 2022 The KubePort Authors.
*/
package util
import (
"fmt"
"time"
rolloutv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
"k8s.io/klog/v2"
)
const (
DateTimeZoneLayout = "2006-01-02 15:04:05 MST"
DateTimeLayout = "2006-01-02 15:04:05"
DateLayout = "2006-01-02"
)
//ValidateTime used to validate _time whether right
func ValidateTime(date, _time string, zone *time.Location) (time.Time, error) {
if zone == nil {
zone = time.Local
}
if date == "" {
date = DateLayout
}
return time.ParseInLocation(DateTimeLayout, fmt.Sprintf("%s %s", date, _time), zone)
}
func TimeZone(zone *rolloutv1alpha1.TimeZone) *time.Location {
if zone != nil {
return time.FixedZone(zone.Name, zone.Offset)
}
return 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, allowRunTime *rolloutv1alpha1.AllowRunTime) (time.Time, bool) {
if allowRunTime == nil {
return expectedTime, true
}
if len(allowRunTime.TimeSlices) == 0 {
return expectedTime, true
}
var (
err error
start time.Time
end time.Time
minSub = time.Hour * 48
)
date := expectedTime.Format(DateLayout)
timeZone := TimeZone(allowRunTime.TimeZone)
for i, timeSlice := range allowRunTime.TimeSlices {
start, err = ValidateTime(date, timeSlice.StartTime, timeZone)
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, timeZone)
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
}

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

@ -0,0 +1,152 @@
/*
Copyright 2022 The KubePort Authors.
*/
package util
import (
"testing"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
rolloutv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
)
var allowTime = &rolloutv1alpha1.AllowRunTime{
TimeZone: nil,
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",
},
},
}
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(DateTimeLayout, s.TestTime, time.Local)
expectedTime, _ := time.ParseInLocation(DateTimeLayout, s.ExpectedTime, time.Local)
resTime, res := TimeInSlice(testTime, allowTime)
Expect(expectedTime.Unix()).Should(Equal(resTime.Unix()))
Expect(s.ExpectedRes).Should(Equal(res))
})
}
}
func TestTimeZone(t *testing.T) {
RegisterFailHandler(Fail)
test := []struct {
name string
data *rolloutv1alpha1.TimeZone
expect *time.Location
}{
{
name: "local",
data: nil,
expect: time.Local,
},
{
name: "UTC",
data: &rolloutv1alpha1.TimeZone{
Name: "UTC",
Offset: 28800,
},
expect: time.FixedZone("UTC", 28800),
},
}
for _, s := range test {
t.Run(s.name, func(t *testing.T) {
zone := TimeZone(s.data)
now := time.Now()
Expect(now.In(zone).Unix()).Should(Equal(now.In(s.expect).Unix()))
})
}
}
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: "2006-01-02 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, nil)
if s.expect != "" {
expectedTime, _ := time.ParseInLocation(DateTimeLayout, 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, validateRolloutSpecAllowRunTime(&rollout.Spec.AllowRunTime, fldPath.Child("AllowRunTime"))...)
errList = append(errList, validateRolloutSpecStrategy(&rollout.Spec.Strategy, fldPath.Child("Strategy"))...)
return errList
}
@ -156,6 +157,35 @@ func validateRolloutSpecObjectRef(objectRef *appsv1alpha1.ObjectRef, fldPath *fi
return nil
}
func validateRolloutSpecAllowRunTime(allowRunTime *appsv1alpha1.AllowRunTime, fldPath *field.Path) field.ErrorList {
if allowRunTime != nil {
return validateRolloutSpecTimeSlices(allowRunTime.TimeSlices, fldPath.Child("TimeSlices"))
}
return field.ErrorList{}
}
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, nil)
if err != nil {
return append(errList, field.Invalid(fldPath.Index(i).Child("StartTime"), t.StartTime, err.Error()))
}
end, err := util.ValidateTime("", t.EndTime, nil)
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,22 @@ var (
Name: "deployment-demo",
},
},
AllowRunTime: appsv1alpha1.AllowRunTime{
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 +111,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.AllowRunTime.TimeSlices = nil
return []client.Object{object}
},
},
/***********************************************************
The following cases may lead to controller panic
**********************************************************/
@ -119,6 +144,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.AllowRunTime.TimeSlices[0].StartTime = ""
object.Spec.AllowRunTime.TimeSlices[0].EndTime = ""
return []client.Object{object}
},
},
{
Name: "TimeSlices time is incomplete",
Succeed: false,
GetObject: func() []client.Object {
object := rollout.DeepCopy()
object.Spec.AllowRunTime.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.AllowRunTime.TimeSlices[0].StartTime = "02:00:00"
object.Spec.AllowRunTime.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.AllowRunTime.TimeSlices[0].EndTime = "26:00:00"
return []client.Object{object}
},
},
{
Name: "Service name is empty",
Succeed: false,