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.finishedEphemeralRunners",name=Finished 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
 | ||||
| type AutoscalingRunnerSet struct { | ||||
|  | @ -84,6 +86,13 @@ type AutoscalingRunnerSetSpec struct { | |||
| 	// +optional
 | ||||
| 	// +kubebuilder:validation:Minimum:=0
 | ||||
| 	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 { | ||||
|  | @ -232,6 +241,40 @@ type ProxyServerConfig struct { | |||
| 	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
 | ||||
| type AutoscalingRunnerSetStatus struct { | ||||
| 	// +optional
 | ||||
|  | @ -248,6 +291,12 @@ type AutoscalingRunnerSetStatus struct { | |||
| 	RunningEphemeralRunners int `json:"runningEphemeralRunners"` | ||||
| 	// +optional
 | ||||
| 	FailedEphemeralRunners int `json:"failedEphemeralRunners"` | ||||
| 
 | ||||
| 	// +optional
 | ||||
| 	// +kubebuilder:validation:Minimum:=0
 | ||||
| 	DesiredMinRunners int `json:"desiredMinRunners"` | ||||
| 	// +optional
 | ||||
| 	ScheduledOverridesSummary *string `json:"scheduledOverridesSummary,omitempty"` | ||||
| } | ||||
| 
 | ||||
| func (ars *AutoscalingRunnerSet) ListenerSpecHash() string { | ||||
|  |  | |||
|  | @ -106,6 +106,11 @@ spec: | |||
|   minRunners: {{ .Values.minRunners | int }} | ||||
|   {{- end }} | ||||
| 
 | ||||
|   {{- if and .Values.scheduledOverrides (kindIs "slice" .Values.scheduledOverrides) }} | ||||
|   scheduledOverrides: | ||||
|     {{- .Values.scheduledOverrides | toYaml | nindent 4 }} | ||||
|   {{- end }} | ||||
| 
 | ||||
|   {{- with .Values.listenerTemplate}} | ||||
|   listenerTemplate: | ||||
|     {{- toYaml . | nindent 4}} | ||||
|  |  | |||
|  | @ -58,6 +58,11 @@ githubConfigSecret: | |||
| ## calculated as a sum of minRunners and the number of jobs assigned to the scale set. | ||||
| # 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" | ||||
| 
 | ||||
| ## name of the runner scale set to create.  Defaults to the helm release name | ||||
|  | @ -205,7 +210,6 @@ template: | |||
|       - name: runner | ||||
|         image: ghcr.io/actions/actions-runner:latest | ||||
|         command: ["/home/runner/run.sh"] | ||||
| 
 | ||||
| ## Optional controller service account that needs to have required Role and RoleBinding | ||||
| ## 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. | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ import ( | |||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" | ||||
| 	"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.") | ||||
| 	} | ||||
| 
 | ||||
| 	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.
 | ||||
| 	listenerValuesHashChanged := listener.Annotations[annotationKeyValuesHash] != autoscalingRunnerSet.Annotations[annotationKeyValuesHash] | ||||
| 	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) | ||||
| 		if err := r.Delete(ctx, listener); err != nil { | ||||
| 			if kerrors.IsNotFound(err) { | ||||
|  | @ -300,20 +308,61 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl | |||
| 		return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, log) | ||||
| 	} | ||||
| 
 | ||||
| 	// Update the status of autoscaling runner set.
 | ||||
| 	if latestRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners { | ||||
| 		if err := patchSubResource(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) { | ||||
| 			obj.Status.CurrentRunners = latestRunnerSet.Status.CurrentReplicas | ||||
| 			obj.Status.PendingEphemeralRunners = latestRunnerSet.Status.PendingEphemeralRunners | ||||
| 			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 | ||||
| 		} | ||||
| 	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 | ||||
| 	} | ||||
| 
 | ||||
| 	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.
 | ||||
|  | @ -766,6 +815,84 @@ func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) erro | |||
| 		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 { | ||||
| 	// configuration fields
 | ||||
| 	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() { | ||||
| 		ars := new(v1alpha1.AutoscalingRunnerSet) | ||||
| 		Eventually( | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| package actionsgithubcom | ||||
| 
 | ||||
| import ( | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/actions/actions-runner-controller/logging" | ||||
| ) | ||||
| 
 | ||||
|  | @ -68,6 +70,9 @@ const DefaultScaleSetListenerLogLevel = string(logging.LogLevelDebug) | |||
| // DefaultScaleSetListenerLogFormat is the default log format applied
 | ||||
| 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
 | ||||
| const resourceOwnerKey = ".metadata.controller" | ||||
| 
 | ||||
|  |  | |||
|  | @ -79,14 +79,11 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1. | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	effectiveMinRunners := 0 | ||||
| 	effectiveMinRunners := autoscalingRunnerSet.Status.DesiredMinRunners | ||||
| 	effectiveMaxRunners := math.MaxInt32 | ||||
| 	if autoscalingRunnerSet.Spec.MaxRunners != nil { | ||||
| 		effectiveMaxRunners = *autoscalingRunnerSet.Spec.MaxRunners | ||||
| 	} | ||||
| 	if autoscalingRunnerSet.Spec.MinRunners != nil { | ||||
| 		effectiveMinRunners = *autoscalingRunnerSet.Spec.MinRunners | ||||
| 	} | ||||
| 
 | ||||
| 	labels := b.mergeLabels(autoscalingRunnerSet.Labels, map[string]string{ | ||||
| 		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