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) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	minRunners, active, upcoming, err := r.getMinRunners(log, time.Now(), *autoscalingRunnerSet) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error(err, "Could not compute desired min runners") | ||||||
|  | 
 | ||||||
|  | 		return ctrl.Result{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var overridesSummary string | ||||||
|  | 
 | ||||||
|  | 	currentReplicasOutOfDate := latestRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners | ||||||
|  | 	minReplicasOutOfDate := autoscalingRunnerSet.Status.DesiredMinRunners != minRunners | ||||||
|  | 
 | ||||||
| 	// Update the status of autoscaling runner set.
 | 	// Update the status of autoscaling runner set.
 | ||||||
| 	if latestRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners { | 	if minReplicasOutOfDate || currentReplicasOutOfDate { | ||||||
| 		if err := patchSubResource(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) { | 		if err := patchSubResource(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) { | ||||||
|  | 			if currentReplicasOutOfDate { | ||||||
| 				obj.Status.CurrentRunners = latestRunnerSet.Status.CurrentReplicas | 				obj.Status.CurrentRunners = latestRunnerSet.Status.CurrentReplicas | ||||||
| 				obj.Status.PendingEphemeralRunners = latestRunnerSet.Status.PendingEphemeralRunners | 				obj.Status.PendingEphemeralRunners = latestRunnerSet.Status.PendingEphemeralRunners | ||||||
| 				obj.Status.RunningEphemeralRunners = latestRunnerSet.Status.RunningEphemeralRunners | 				obj.Status.RunningEphemeralRunners = latestRunnerSet.Status.RunningEphemeralRunners | ||||||
| 				obj.Status.FailedEphemeralRunners = latestRunnerSet.Status.FailedEphemeralRunners | 				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 | 			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{}, 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