Merge a8b1b0e618 into a325cc745a
This commit is contained in:
commit
c04bb01fe9
|
|
@ -41,6 +41,8 @@ import (
|
||||||
//+kubebuilder:printcolumn:JSONPath=".status.runningEphemeralRunners",name=Running Runners,type=integer
|
//+kubebuilder:printcolumn:JSONPath=".status.runningEphemeralRunners",name=Running Runners,type=integer
|
||||||
//+kubebuilder:printcolumn:JSONPath=".status.finishedEphemeralRunners",name=Finished Runners,type=integer
|
//+kubebuilder:printcolumn:JSONPath=".status.finishedEphemeralRunners",name=Finished Runners,type=integer
|
||||||
//+kubebuilder:printcolumn:JSONPath=".status.deletingEphemeralRunners",name=Deleting Runners,type=integer
|
//+kubebuilder:printcolumn:JSONPath=".status.deletingEphemeralRunners",name=Deleting Runners,type=integer
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".status.desiredMinRunners",name=Desired Minimum Runners,type=integer
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".status.scheduledOverridesSummary",name=Schedule,type=string
|
||||||
|
|
||||||
// AutoscalingRunnerSet is the Schema for the autoscalingrunnersets API
|
// AutoscalingRunnerSet is the Schema for the autoscalingrunnersets API
|
||||||
type AutoscalingRunnerSet struct {
|
type AutoscalingRunnerSet struct {
|
||||||
|
|
@ -84,6 +86,13 @@ type AutoscalingRunnerSetSpec struct {
|
||||||
// +optional
|
// +optional
|
||||||
// +kubebuilder:validation:Minimum:=0
|
// +kubebuilder:validation:Minimum:=0
|
||||||
MinRunners *int `json:"minRunners,omitempty"`
|
MinRunners *int `json:"minRunners,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// ScheduledOverrides is the list of ScheduledOverride.
|
||||||
|
// It can be used to override a few fields of AutoscalingRunnerSetSpec on schedule.
|
||||||
|
// The earlier a scheduled override is, the higher it is prioritized.
|
||||||
|
// +optional
|
||||||
|
ScheduledOverrides []ScheduledOverride `json:"scheduledOverrides,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitHubServerTLSConfig struct {
|
type GitHubServerTLSConfig struct {
|
||||||
|
|
@ -232,6 +241,40 @@ type ProxyServerConfig struct {
|
||||||
CredentialSecretRef string `json:"credentialSecretRef,omitempty"`
|
CredentialSecretRef string `json:"credentialSecretRef,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScheduledOverride can be used to override a few fields of AutoscalingRunnerSetSpec on schedule.
|
||||||
|
// A schedule can optionally be recurring, so that the corresponding override happens every day, week, month, or year.
|
||||||
|
type ScheduledOverride struct {
|
||||||
|
// StartTime is the time at which the first override starts.
|
||||||
|
StartTime metav1.Time `json:"startTime"`
|
||||||
|
|
||||||
|
// EndTime is the time at which the first override ends.
|
||||||
|
EndTime metav1.Time `json:"endTime"`
|
||||||
|
|
||||||
|
// MinRunners is the number of runners while overriding.
|
||||||
|
// If omitted, it doesn't override minRunners.
|
||||||
|
// +optional
|
||||||
|
// +nullable
|
||||||
|
// +kubebuilder:validation:Minimum=0
|
||||||
|
MinRunners *int `json:"minRunners,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
RecurrenceRule RecurrenceRule `json:"recurrenceRule,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecurrenceRule struct {
|
||||||
|
// Frequency is the name of a predefined interval of each recurrence.
|
||||||
|
// The valid values are "Daily", "Weekly", "Monthly", and "Yearly".
|
||||||
|
// If empty, the corresponding override happens only once.
|
||||||
|
// +optional
|
||||||
|
// +kubebuilder:validation:Enum=Daily;Weekly;Monthly;Yearly
|
||||||
|
Frequency string `json:"frequency,omitempty"`
|
||||||
|
|
||||||
|
// UntilTime is the time of the final recurrence.
|
||||||
|
// If empty, the schedule recurs forever.
|
||||||
|
// +optional
|
||||||
|
UntilTime metav1.Time `json:"untilTime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet
|
// AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet
|
||||||
type AutoscalingRunnerSetStatus struct {
|
type AutoscalingRunnerSetStatus struct {
|
||||||
// +optional
|
// +optional
|
||||||
|
|
@ -248,6 +291,12 @@ type AutoscalingRunnerSetStatus struct {
|
||||||
RunningEphemeralRunners int `json:"runningEphemeralRunners"`
|
RunningEphemeralRunners int `json:"runningEphemeralRunners"`
|
||||||
// +optional
|
// +optional
|
||||||
FailedEphemeralRunners int `json:"failedEphemeralRunners"`
|
FailedEphemeralRunners int `json:"failedEphemeralRunners"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// +kubebuilder:validation:Minimum:=0
|
||||||
|
DesiredMinRunners int `json:"desiredMinRunners"`
|
||||||
|
// +optional
|
||||||
|
ScheduledOverridesSummary *string `json:"scheduledOverridesSummary,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ars *AutoscalingRunnerSet) ListenerSpecHash() string {
|
func (ars *AutoscalingRunnerSet) ListenerSpecHash() string {
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,11 @@ spec:
|
||||||
minRunners: {{ .Values.minRunners | int }}
|
minRunners: {{ .Values.minRunners | int }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if and .Values.scheduledOverrides (kindIs "slice" .Values.scheduledOverrides) }}
|
||||||
|
scheduledOverrides:
|
||||||
|
{{- .Values.scheduledOverrides | toYaml | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
{{- with .Values.listenerTemplate}}
|
{{- with .Values.listenerTemplate}}
|
||||||
listenerTemplate:
|
listenerTemplate:
|
||||||
{{- toYaml . | nindent 4}}
|
{{- toYaml . | nindent 4}}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ githubConfigSecret:
|
||||||
## calculated as a sum of minRunners and the number of jobs assigned to the scale set.
|
## calculated as a sum of minRunners and the number of jobs assigned to the scale set.
|
||||||
# minRunners: 0
|
# minRunners: 0
|
||||||
|
|
||||||
|
## scheduledOverrides is a list of scheduled overrides to the minReplicas.
|
||||||
|
## See docs for more info: https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides
|
||||||
|
## Please note that the field minReplicas was renamed to minRunners
|
||||||
|
# scheduledOverrides: []
|
||||||
|
|
||||||
# runnerGroup: "default"
|
# runnerGroup: "default"
|
||||||
|
|
||||||
## name of the runner scale set to create. Defaults to the helm release name
|
## name of the runner scale set to create. Defaults to the helm release name
|
||||||
|
|
@ -205,7 +210,6 @@ template:
|
||||||
- name: runner
|
- name: runner
|
||||||
image: ghcr.io/actions/actions-runner:latest
|
image: ghcr.io/actions/actions-runner:latest
|
||||||
command: ["/home/runner/run.sh"]
|
command: ["/home/runner/run.sh"]
|
||||||
|
|
||||||
## Optional controller service account that needs to have required Role and RoleBinding
|
## Optional controller service account that needs to have required Role and RoleBinding
|
||||||
## to operate this gha-runner-scale-set installation.
|
## to operate this gha-runner-scale-set installation.
|
||||||
## The helm chart will try to find the controller deployment and its service account at installation time.
|
## The helm chart will try to find the controller deployment and its service account at installation time.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
"github.com/actions/actions-runner-controller/build"
|
"github.com/actions/actions-runner-controller/build"
|
||||||
|
|
@ -244,10 +245,17 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
|
||||||
log.Info("AutoscalingListener does not exist.")
|
log.Info("AutoscalingListener does not exist.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
desiredMinRunnersChanged := false
|
||||||
|
// If the listener's minRunners value is different from the autoscalingRunnerSet desired minRunners, we recreate the listener
|
||||||
|
if listenerFound && listener.Spec.MinRunners != autoscalingRunnerSet.Status.DesiredMinRunners {
|
||||||
|
log.Info("RunnerScaleSetListener minRunners is different from the autoscalingRunnerSet desired minRunners", "current", listener.Spec.MinRunners, "desired", autoscalingRunnerSet.Status.DesiredMinRunners)
|
||||||
|
desiredMinRunnersChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
// Our listener pod is out of date, so we need to delete it to get a new recreate.
|
// Our listener pod is out of date, so we need to delete it to get a new recreate.
|
||||||
listenerValuesHashChanged := listener.Annotations[annotationKeyValuesHash] != autoscalingRunnerSet.Annotations[annotationKeyValuesHash]
|
listenerValuesHashChanged := listener.Annotations[annotationKeyValuesHash] != autoscalingRunnerSet.Annotations[annotationKeyValuesHash]
|
||||||
listenerSpecHashChanged := listener.Annotations[annotationKeyRunnerSpecHash] != autoscalingRunnerSet.ListenerSpecHash()
|
listenerSpecHashChanged := listener.Annotations[annotationKeyRunnerSpecHash] != autoscalingRunnerSet.ListenerSpecHash()
|
||||||
if listenerFound && (listenerValuesHashChanged || listenerSpecHashChanged) {
|
if listenerFound && (listenerValuesHashChanged || listenerSpecHashChanged || desiredMinRunnersChanged) {
|
||||||
log.Info("RunnerScaleSetListener is out of date. Deleting it so that it is recreated", "name", listener.Name)
|
log.Info("RunnerScaleSetListener is out of date. Deleting it so that it is recreated", "name", listener.Name)
|
||||||
if err := r.Delete(ctx, listener); err != nil {
|
if err := r.Delete(ctx, listener); err != nil {
|
||||||
if kerrors.IsNotFound(err) {
|
if kerrors.IsNotFound(err) {
|
||||||
|
|
@ -300,20 +308,61 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
|
||||||
return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, log)
|
return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the status of autoscaling runner set.
|
minRunners, active, upcoming, err := r.getMinRunners(log, time.Now(), *autoscalingRunnerSet)
|
||||||
if latestRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners {
|
if err != nil {
|
||||||
if err := patchSubResource(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
|
log.Error(err, "Could not compute desired min runners")
|
||||||
obj.Status.CurrentRunners = latestRunnerSet.Status.CurrentReplicas
|
|
||||||
obj.Status.PendingEphemeralRunners = latestRunnerSet.Status.PendingEphemeralRunners
|
return ctrl.Result{}, err
|
||||||
obj.Status.RunningEphemeralRunners = latestRunnerSet.Status.RunningEphemeralRunners
|
|
||||||
obj.Status.FailedEphemeralRunners = latestRunnerSet.Status.FailedEphemeralRunners
|
|
||||||
}); err != nil {
|
|
||||||
log.Error(err, "Failed to update autoscaling runner set status with current runner count")
|
|
||||||
return ctrl.Result{}, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctrl.Result{}, nil
|
var overridesSummary string
|
||||||
|
|
||||||
|
currentReplicasOutOfDate := latestRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners
|
||||||
|
minReplicasOutOfDate := autoscalingRunnerSet.Status.DesiredMinRunners != minRunners
|
||||||
|
|
||||||
|
// Update the status of autoscaling runner set.
|
||||||
|
if minReplicasOutOfDate || currentReplicasOutOfDate {
|
||||||
|
if err := patchSubResource(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
|
||||||
|
if currentReplicasOutOfDate {
|
||||||
|
obj.Status.CurrentRunners = latestRunnerSet.Status.CurrentReplicas
|
||||||
|
obj.Status.PendingEphemeralRunners = latestRunnerSet.Status.PendingEphemeralRunners
|
||||||
|
obj.Status.RunningEphemeralRunners = latestRunnerSet.Status.RunningEphemeralRunners
|
||||||
|
obj.Status.FailedEphemeralRunners = latestRunnerSet.Status.FailedEphemeralRunners
|
||||||
|
}
|
||||||
|
|
||||||
|
if minReplicasOutOfDate {
|
||||||
|
obj.Status.DesiredMinRunners = minRunners
|
||||||
|
|
||||||
|
if (active != nil && upcoming == nil) || (active != nil && upcoming != nil && active.Period.EndTime.Before(upcoming.Period.StartTime)) {
|
||||||
|
var after int
|
||||||
|
if obj.Status.DesiredMinRunners >= 0 {
|
||||||
|
after = obj.Status.DesiredMinRunners
|
||||||
|
}
|
||||||
|
|
||||||
|
overridesSummary = fmt.Sprintf("min=%d status=active endTime=%s", after, active.Period.EndTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if active == nil && upcoming != nil || (active != nil && upcoming != nil && active.Period.EndTime.After(upcoming.Period.StartTime)) {
|
||||||
|
if upcoming.ScheduledOverride.MinRunners != nil {
|
||||||
|
overridesSummary = fmt.Sprintf("min=%d status=upcoming startTime=%s", *upcoming.ScheduledOverride.MinRunners, upcoming.Period.StartTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if overridesSummary != "" {
|
||||||
|
obj.Status.ScheduledOverridesSummary = &overridesSummary
|
||||||
|
} else {
|
||||||
|
obj.Status.ScheduledOverridesSummary = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
log.Error(err, "Failed to update autoscaling runner set status")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{RequeueAfter: DefaultScaleSetHealthyRequeueAfter}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevents overprovisioning of runners.
|
// Prevents overprovisioning of runners.
|
||||||
|
|
@ -766,6 +815,84 @@ func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) erro
|
||||||
Complete(r)
|
Complete(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Override struct {
|
||||||
|
ScheduledOverride v1alpha1.ScheduledOverride
|
||||||
|
Period Period
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) matchScheduledOverrides(log logr.Logger, now time.Time, autoscalingRunnerSet v1alpha1.AutoscalingRunnerSet) (*int, *Override, *Override, error) {
|
||||||
|
var minRunners *int
|
||||||
|
var active, upcoming *Override
|
||||||
|
|
||||||
|
for _, o := range autoscalingRunnerSet.Spec.ScheduledOverrides {
|
||||||
|
log.V(1).Info(
|
||||||
|
"Checking scheduled override",
|
||||||
|
"now", now,
|
||||||
|
"startTime", o.StartTime,
|
||||||
|
"endTime", o.EndTime,
|
||||||
|
"frequency", o.RecurrenceRule.Frequency,
|
||||||
|
"untilTime", o.RecurrenceRule.UntilTime,
|
||||||
|
)
|
||||||
|
|
||||||
|
a, u, err := MatchSchedule(
|
||||||
|
now, o.StartTime.Time, o.EndTime.Time,
|
||||||
|
RecurrenceRule{
|
||||||
|
Frequency: o.RecurrenceRule.Frequency,
|
||||||
|
UntilTime: o.RecurrenceRule.UntilTime.Time,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return minRunners, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first when there are two or more active scheduled overrides,
|
||||||
|
// as the spec defines that the earlier scheduled override is prioritized higher than later ones.
|
||||||
|
if a != nil && active == nil {
|
||||||
|
active = &Override{Period: *a, ScheduledOverride: o}
|
||||||
|
|
||||||
|
if o.MinRunners != nil {
|
||||||
|
minRunners = o.MinRunners
|
||||||
|
|
||||||
|
log.V(1).Info(
|
||||||
|
"Found active scheduled override",
|
||||||
|
"activeStartTime", a.StartTime,
|
||||||
|
"activeEndTime", a.EndTime,
|
||||||
|
"activeMinRunners", minRunners,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if u != nil && (upcoming == nil || u.StartTime.Before(upcoming.Period.StartTime)) {
|
||||||
|
upcoming = &Override{Period: *u, ScheduledOverride: o}
|
||||||
|
|
||||||
|
log.V(1).Info(
|
||||||
|
"Found upcoming scheduled override",
|
||||||
|
"upcomingStartTime", u.StartTime,
|
||||||
|
"upcomingEndTime", u.EndTime,
|
||||||
|
"upcomingMinRunners", o.MinRunners,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minRunners, active, upcoming, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) getMinRunners(log logr.Logger, now time.Time, autoscalingRunnerSet v1alpha1.AutoscalingRunnerSet) (int, *Override, *Override, error) {
|
||||||
|
var minRunners int
|
||||||
|
if autoscalingRunnerSet.Spec.MinRunners != nil && *autoscalingRunnerSet.Spec.MinRunners >= 0 {
|
||||||
|
minRunners = *autoscalingRunnerSet.Spec.MinRunners
|
||||||
|
}
|
||||||
|
|
||||||
|
m, active, upcoming, err := r.matchScheduledOverrides(log, now, autoscalingRunnerSet)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, nil, err
|
||||||
|
} else if m != nil {
|
||||||
|
minRunners = *m
|
||||||
|
}
|
||||||
|
|
||||||
|
return minRunners, active, upcoming, nil
|
||||||
|
}
|
||||||
|
|
||||||
type autoscalingRunnerSetFinalizerDependencyCleaner struct {
|
type autoscalingRunnerSetFinalizerDependencyCleaner struct {
|
||||||
// configuration fields
|
// configuration fields
|
||||||
client client.Client
|
client client.Client
|
||||||
|
|
|
||||||
|
|
@ -585,6 +585,101 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("When a scheduled override is active in an AutoscalingRunnerSet", func() {
|
||||||
|
It("It should override min runners in the AutoscalingListener", func() {
|
||||||
|
// Wait till the listener is created
|
||||||
|
listener := new(v1alpha1.AutoscalingListener)
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
|
||||||
|
},
|
||||||
|
autoscalingRunnerSetTestTimeout,
|
||||||
|
autoscalingRunnerSetTestInterval,
|
||||||
|
).Should(Succeed(), "Listener should be created")
|
||||||
|
|
||||||
|
runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
|
||||||
|
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
|
||||||
|
Expect(len(runnerSetList.Items)).To(Equal(1), "There should be 1 EphemeralRunnerSet")
|
||||||
|
runnerSet := runnerSetList.Items[0]
|
||||||
|
|
||||||
|
minOverride := 0
|
||||||
|
|
||||||
|
// Patch the AutoScalingRunnerSet with a scheduled override
|
||||||
|
patched := autoscalingRunnerSet.DeepCopy()
|
||||||
|
patched.Spec.ScheduledOverrides = []v1alpha1.ScheduledOverride{
|
||||||
|
{
|
||||||
|
StartTime: metav1.Now(),
|
||||||
|
EndTime: metav1.NewTime(metav1.Now().Add(1 * time.Hour)),
|
||||||
|
MinRunners: &minOverride,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")
|
||||||
|
autoscalingRunnerSet = patched.DeepCopy()
|
||||||
|
|
||||||
|
// We should not re-create a new EphemeralRunnerSet
|
||||||
|
Consistently(
|
||||||
|
func() (string, error) {
|
||||||
|
runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
|
||||||
|
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSetList.Items) != 1 {
|
||||||
|
return "", fmt.Errorf("We should have only 1 EphemeralRunnerSet, but got %v", len(runnerSetList.Items))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(runnerSetList.Items[0].UID), nil
|
||||||
|
},
|
||||||
|
autoscalingRunnerSetTestTimeout,
|
||||||
|
autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(string(runnerSet.UID)), "New EphemeralRunnerSet should not be created")
|
||||||
|
|
||||||
|
// We should only re-create a new listener
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
listener := new(v1alpha1.AutoscalingListener)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(listener.UID), nil
|
||||||
|
},
|
||||||
|
autoscalingRunnerSetTestTimeout,
|
||||||
|
autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(string(listener.UID)), "New Listener should be created")
|
||||||
|
|
||||||
|
// Check if the listener has the min runners override
|
||||||
|
Expect(listener.Spec.MinRunners).To(Equal(minOverride), "Listener should have the min runners override")
|
||||||
|
|
||||||
|
desiredScheduledOverridesSummary := fmt.Sprintf("min=%d status=active endTime=%s", *autoscalingRunnerSet.Spec.ScheduledOverrides[0].MinRunners, autoscalingRunnerSet.Spec.ScheduledOverrides[0].EndTime.Time)
|
||||||
|
|
||||||
|
desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{
|
||||||
|
CurrentRunners: 0,
|
||||||
|
State: "",
|
||||||
|
PendingEphemeralRunners: 0,
|
||||||
|
RunningEphemeralRunners: 0,
|
||||||
|
FailedEphemeralRunners: 0,
|
||||||
|
ScheduledOverridesSummary: &desiredScheduledOverridesSummary,
|
||||||
|
DesiredMinRunners: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (v1alpha1.AutoscalingRunnerSetStatus, error) {
|
||||||
|
updated := new(v1alpha1.AutoscalingRunnerSet)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, updated)
|
||||||
|
if err != nil {
|
||||||
|
return v1alpha1.AutoscalingRunnerSetStatus{}, fmt.Errorf("failed to get AutoScalingRunnerSet: %w", err)
|
||||||
|
}
|
||||||
|
return updated.Status, nil
|
||||||
|
},
|
||||||
|
autoscalingRunnerSetTestTimeout,
|
||||||
|
autoscalingRunnerSetTestInterval,
|
||||||
|
).Should(BeEquivalentTo(desiredStatus), "AutoScalingRunnerSet status should be updated")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
It("Should update Status on EphemeralRunnerSet status Update", func() {
|
It("Should update Status on EphemeralRunnerSet status Update", func() {
|
||||||
ars := new(v1alpha1.AutoscalingRunnerSet)
|
ars := new(v1alpha1.AutoscalingRunnerSet)
|
||||||
Eventually(
|
Eventually(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package actionsgithubcom
|
package actionsgithubcom
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/actions/actions-runner-controller/logging"
|
"github.com/actions/actions-runner-controller/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -68,6 +70,9 @@ const DefaultScaleSetListenerLogLevel = string(logging.LogLevelDebug)
|
||||||
// DefaultScaleSetListenerLogFormat is the default log format applied
|
// DefaultScaleSetListenerLogFormat is the default log format applied
|
||||||
const DefaultScaleSetListenerLogFormat = string(logging.LogFormatText)
|
const DefaultScaleSetListenerLogFormat = string(logging.LogFormatText)
|
||||||
|
|
||||||
|
// DefaultScaleSetHealthyRequeueAfter is the default requeue time for healthy scale sets
|
||||||
|
const DefaultScaleSetHealthyRequeueAfter = time.Duration(1 * time.Minute)
|
||||||
|
|
||||||
// ownerKey is field selector matching the owner name of a particular resource
|
// ownerKey is field selector matching the owner name of a particular resource
|
||||||
const resourceOwnerKey = ".metadata.controller"
|
const resourceOwnerKey = ".metadata.controller"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,14 +79,11 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
effectiveMinRunners := 0
|
effectiveMinRunners := autoscalingRunnerSet.Status.DesiredMinRunners
|
||||||
effectiveMaxRunners := math.MaxInt32
|
effectiveMaxRunners := math.MaxInt32
|
||||||
if autoscalingRunnerSet.Spec.MaxRunners != nil {
|
if autoscalingRunnerSet.Spec.MaxRunners != nil {
|
||||||
effectiveMaxRunners = *autoscalingRunnerSet.Spec.MaxRunners
|
effectiveMaxRunners = *autoscalingRunnerSet.Spec.MaxRunners
|
||||||
}
|
}
|
||||||
if autoscalingRunnerSet.Spec.MinRunners != nil {
|
|
||||||
effectiveMinRunners = *autoscalingRunnerSet.Spec.MinRunners
|
|
||||||
}
|
|
||||||
|
|
||||||
labels := b.mergeLabels(autoscalingRunnerSet.Labels, map[string]string{
|
labels := b.mergeLabels(autoscalingRunnerSet.Labels, map[string]string{
|
||||||
LabelKeyGitHubScaleSetNamespace: autoscalingRunnerSet.Namespace,
|
LabelKeyGitHubScaleSetNamespace: autoscalingRunnerSet.Namespace,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/teambition/rrule-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecurrenceRule struct {
|
||||||
|
Frequency string
|
||||||
|
UntilTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Period struct {
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Period) String() string {
|
||||||
|
if r == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.StartTime.Format(time.RFC3339) + "-" + r.EndTime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchSchedule(now time.Time, startTime, endTime time.Time, recurrenceRule RecurrenceRule) (*Period, *Period, error) {
|
||||||
|
return calculateActiveAndUpcomingRecurringPeriods(
|
||||||
|
now,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
recurrenceRule.Frequency,
|
||||||
|
recurrenceRule.UntilTime,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateActiveAndUpcomingRecurringPeriods(now, startTime, endTime time.Time, frequency string, untilTime time.Time) (*Period, *Period, error) {
|
||||||
|
var freqValue rrule.Frequency
|
||||||
|
|
||||||
|
var freqDurationDay int
|
||||||
|
var freqDurationMonth int
|
||||||
|
var freqDurationYear int
|
||||||
|
|
||||||
|
switch frequency {
|
||||||
|
case "Daily":
|
||||||
|
freqValue = rrule.DAILY
|
||||||
|
freqDurationDay = 1
|
||||||
|
case "Weekly":
|
||||||
|
freqValue = rrule.WEEKLY
|
||||||
|
freqDurationDay = 7
|
||||||
|
case "Monthly":
|
||||||
|
freqValue = rrule.MONTHLY
|
||||||
|
freqDurationMonth = 1
|
||||||
|
case "Yearly":
|
||||||
|
freqValue = rrule.YEARLY
|
||||||
|
freqDurationYear = 1
|
||||||
|
case "":
|
||||||
|
if now.Before(startTime) {
|
||||||
|
return nil, &Period{StartTime: startTime, EndTime: endTime}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if now.Before(endTime) {
|
||||||
|
return &Period{StartTime: startTime, EndTime: endTime}, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, nil
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf(`invalid freq %q: It must be one of "Daily", "Weekly", "Monthly", and "Yearly"`, frequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
freqDurationLater := time.Date(
|
||||||
|
now.Year()+freqDurationYear,
|
||||||
|
time.Month(int(now.Month())+freqDurationMonth),
|
||||||
|
now.Day()+freqDurationDay,
|
||||||
|
now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), now.Location(),
|
||||||
|
)
|
||||||
|
|
||||||
|
freqDuration := freqDurationLater.Sub(now)
|
||||||
|
|
||||||
|
overrideDuration := endTime.Sub(startTime)
|
||||||
|
if overrideDuration > freqDuration {
|
||||||
|
return nil, nil, fmt.Errorf("override's duration %s must be equal to or shorter than the duration implied by freq %q (%s)", overrideDuration, frequency, freqDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
rrule, err := rrule.NewRRule(rrule.ROption{
|
||||||
|
Freq: freqValue,
|
||||||
|
Dtstart: startTime,
|
||||||
|
Until: untilTime,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideDurationBefore := now.Add(-overrideDuration + 1)
|
||||||
|
activeOverrideStarts := rrule.Between(overrideDurationBefore, now, true)
|
||||||
|
|
||||||
|
var active *Period
|
||||||
|
|
||||||
|
if len(activeOverrideStarts) > 1 {
|
||||||
|
return nil, nil, fmt.Errorf("[bug] unexpted number of active overrides found: %v", activeOverrideStarts)
|
||||||
|
} else if len(activeOverrideStarts) == 1 {
|
||||||
|
active = &Period{
|
||||||
|
StartTime: activeOverrideStarts[0],
|
||||||
|
EndTime: activeOverrideStarts[0].Add(overrideDuration),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oneSecondLater := now.Add(1)
|
||||||
|
upcomingOverrideStarts := rrule.Between(oneSecondLater, freqDurationLater, true)
|
||||||
|
|
||||||
|
var next *Period
|
||||||
|
|
||||||
|
if len(upcomingOverrideStarts) > 0 {
|
||||||
|
next = &Period{
|
||||||
|
StartTime: upcomingOverrideStarts[0],
|
||||||
|
EndTime: upcomingOverrideStarts[0].Add(overrideDuration),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return active, next, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,617 @@
|
||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCalculateActiveAndUpcomingRecurringPeriods(t *testing.T) {
|
||||||
|
type recurrence struct {
|
||||||
|
Start string
|
||||||
|
End string
|
||||||
|
Freq string
|
||||||
|
Until string
|
||||||
|
}
|
||||||
|
|
||||||
|
type testcase struct {
|
||||||
|
now string
|
||||||
|
|
||||||
|
recurrence recurrence
|
||||||
|
|
||||||
|
wantActive string
|
||||||
|
wantUpcoming string
|
||||||
|
}
|
||||||
|
|
||||||
|
check := func(t *testing.T, tc testcase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
_, err := time.Parse(time.RFC3339, "2021-05-08T00:00:00Z")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now, err := time.Parse(time.RFC3339, tc.now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
active, upcoming, err := parseAndMatchRecurringPeriod(now, tc.recurrence.Start, tc.recurrence.End, tc.recurrence.Freq, tc.recurrence.Until)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if active.String() != tc.wantActive {
|
||||||
|
t.Errorf("unexpected active: want %q, got %q", tc.wantActive, active)
|
||||||
|
}
|
||||||
|
|
||||||
|
if upcoming.String() != tc.wantUpcoming {
|
||||||
|
t.Errorf("unexpected upcoming: want %q, got %q", tc.wantUpcoming, upcoming)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("onetime override about to start", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-04-30T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "2021-05-01T00:00:00+09:00-2021-05-03T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("onetime override started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-01T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-01T00:00:00+09:00-2021-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("onetime override about to end", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-02T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-01T00:00:00+09:00-2021-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("onetime override ended", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-03T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override about to start", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-04-30T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "2021-05-01T00:00:00+09:00-2021-05-03T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-01T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-01T00:00:00+09:00-2021-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2021-05-08T00:00:00+09:00-2021-05-10T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override about to end", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-02T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-01T00:00:00+09:00-2021-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2021-05-08T00:00:00+09:00-2021-05-10T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override ended", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-03T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "2021-05-08T00:00:00+09:00-2021-05-10T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override reccurrence about to start", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-07T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "2021-05-08T00:00:00+09:00-2021-05-10T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override reccurrence started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-08T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-08T00:00:00+09:00-2021-05-10T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2021-05-15T00:00:00+09:00-2021-05-17T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override reccurrence about to end", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-09T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-08T00:00:00+09:00-2021-05-10T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2021-05-15T00:00:00+09:00-2021-05-17T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override reccurrence ended", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-10T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "2021-05-15T00:00:00+09:00-2021-05-17T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override's last reccurrence about to start", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-04-29T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "2022-04-30T00:00:00+09:00-2022-05-02T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override reccurrence started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-04-30T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2022-04-30T00:00:00+09:00-2022-05-02T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override reccurrence about to end", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-05-01T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "2022-04-30T00:00:00+09:00-2022-05-02T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override reccurrence ended", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-05-02T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("weekly override repeated forever started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Weekly",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-08T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-08T00:00:00+09:00-2021-05-10T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2021-05-15T00:00:00+09:00-2021-05-17T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("monthly override started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Monthly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-01T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-01T00:00:00+09:00-2021-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2021-06-01T00:00:00+09:00-2021-06-03T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("monthly override recurrence started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Monthly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-06-01T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-06-01T00:00:00+09:00-2021-06-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2021-07-01T00:00:00+09:00-2021-07-03T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("monthly override's last reccurence about to start", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Monthly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-04-30T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "2022-05-01T00:00:00+09:00-2022-05-03T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("monthly override's last reccurence started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Monthly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-05-01T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2022-05-01T00:00:00+09:00-2022-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("monthly override's last reccurence started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Monthly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-05-01T00:00:01+09:00",
|
||||||
|
|
||||||
|
wantActive: "2022-05-01T00:00:00+09:00-2022-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("monthly override's last reccurence ending", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Monthly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-05-02T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "2022-05-01T00:00:00+09:00-2022-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("monthly override's last reccurence ended", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Monthly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-05-03T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("yearly override started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Yearly",
|
||||||
|
Until: "2022-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2021-05-01T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2021-05-01T00:00:00+09:00-2021-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2022-05-01T00:00:00+09:00-2022-05-03T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("yearly override reccurrence started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Yearly",
|
||||||
|
Until: "2023-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2022-05-01T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2022-05-01T00:00:00+09:00-2022-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "2023-05-01T00:00:00+09:00-2023-05-03T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("yearly override's last recurrence about to start", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Yearly",
|
||||||
|
Until: "2023-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2023-04-30T23:59:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "2023-05-01T00:00:00+09:00-2023-05-03T00:00:00+09:00",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("yearly override's last recurrence started", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Yearly",
|
||||||
|
Until: "2023-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2023-05-01T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "2023-05-01T00:00:00+09:00-2023-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("yearly override's last recurrence ending", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Yearly",
|
||||||
|
Until: "2023-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2023-05-02T23:23:59+09:00",
|
||||||
|
|
||||||
|
wantActive: "2023-05-01T00:00:00+09:00-2023-05-03T00:00:00+09:00",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("yearly override's last recurrence ended", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
check(t, testcase{
|
||||||
|
recurrence: recurrence{
|
||||||
|
Start: "2021-05-01T00:00:00+09:00",
|
||||||
|
End: "2021-05-03T00:00:00+09:00",
|
||||||
|
Freq: "Yearly",
|
||||||
|
Until: "2023-05-01T00:00:00+09:00",
|
||||||
|
},
|
||||||
|
|
||||||
|
now: "2023-05-03T00:00:00+09:00",
|
||||||
|
|
||||||
|
wantActive: "",
|
||||||
|
wantUpcoming: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAndMatchRecurringPeriod(now time.Time, start, end, frequency, until string) (*Period, *Period, error) {
|
||||||
|
startTime, err := time.Parse(time.RFC3339, start)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endTime, err := time.Parse(time.RFC3339, end)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var untilTime time.Time
|
||||||
|
|
||||||
|
if until != "" {
|
||||||
|
ut, err := time.Parse(time.RFC3339, until)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
untilTime = ut
|
||||||
|
}
|
||||||
|
|
||||||
|
return MatchSchedule(now, startTime, endTime, RecurrenceRule{Frequency: frequency, UntilTime: untilTime})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzMatchSchedule(f *testing.F) {
|
||||||
|
start := time.Now()
|
||||||
|
end := time.Now()
|
||||||
|
now := time.Now()
|
||||||
|
f.Fuzz(func(t *testing.T, freq string) {
|
||||||
|
// Verify that it never panics
|
||||||
|
_, _, _ = MatchSchedule(now, start, end, RecurrenceRule{Frequency: freq})
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue