Skip to content

Commit

Permalink
rdar://131959392 Adding StartDateTime, EndDateTime, and Timezones
Browse files Browse the repository at this point in the history
  • Loading branch information
ant-smith committed Jul 18, 2024
1 parent 481978a commit 5b629d7
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 8 deletions.
19 changes: 19 additions & 0 deletions kwok/charts/crds/karpenter.sh_nodepools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,13 @@ spec:
a 0s at the end.
pattern: ^((([0-9]+(h|m))|([0-9]+h[0-9]+m))(0s)?)$
type: string
endDateTime:
description: |-
EndDateTime specifies the specific ending datetime the budget is active, following
the RFC3339 standard of "2006-01-02T15:04:05" - "yyyy-MM-DDTHH:MM:SS".
Timezone here is UTC if no TZ is specified within the spec.
pattern: ^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})$
type: string
nodes:
default: 10%
description: |-
Expand Down Expand Up @@ -608,6 +615,18 @@ spec:
This field is required if Duration is set.
pattern: ^(@(annually|yearly|monthly|weekly|daily|midnight|hourly))|((.+)\s(.+)\s(.+)\s(.+)\s(.+))$
type: string
startDateTime:
description: |-
StartDateTime specifies the specific starting datetime the budget is active, following
the RFC3339 standard of "2006-01-02T15:04:05" - "yyyy-MM-DDTHH:MM:SS".
Timezone here is UTC if no TZ is specified within the spec.
pattern: ^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})$
type: string
tz:
description: |-
TZ specifies the timezone of the budget. Follows the standard IANA "America/New_York" format. If not specified
defaults to "UTC".
type: string
required:
- nodes
type: object
Expand Down
19 changes: 19 additions & 0 deletions pkg/apis/crds/karpenter.sh_nodepools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,13 @@ spec:
a 0s at the end.
pattern: ^((([0-9]+(h|m))|([0-9]+h[0-9]+m))(0s)?)$
type: string
endDateTime:
description: |-
EndDateTime specifies the specific ending datetime the budget is active, following
the RFC3339 standard of "2006-01-02T15:04:05" - "yyyy-MM-DDTHH:MM:SS".
Timezone here is UTC if no TZ is specified within the spec.
pattern: ^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})$
type: string
nodes:
default: 10%
description: |-
Expand Down Expand Up @@ -606,6 +613,18 @@ spec:
This field is required if Duration is set.
pattern: ^(@(annually|yearly|monthly|weekly|daily|midnight|hourly))|((.+)\s(.+)\s(.+)\s(.+)\s(.+))$
type: string
startDateTime:
description: |-
StartDateTime specifies the specific starting datetime the budget is active, following
the RFC3339 standard of "2006-01-02T15:04:05" - "yyyy-MM-DDTHH:MM:SS".
Timezone here is UTC if no TZ is specified within the spec.
pattern: ^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})$
type: string
tz:
description: |-
TZ specifies the timezone of the budget. Follows the standard IANA "America/New_York" format. If not specified
defaults to "UTC".
type: string
required:
- nodes
type: object
Expand Down
74 changes: 70 additions & 4 deletions pkg/apis/v1beta1/nodepool.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
"math"
"sort"
"strconv"
"time"

_ "time/tzdata"

"github.com/mitchellh/hashstructure/v2"
"github.com/robfig/cron/v3"
Expand Down Expand Up @@ -132,6 +135,25 @@ type Budget struct {
// +kubebuilder:validation:Type="string"
// +optional
Duration *metav1.Duration `json:"duration,omitempty" hash:"ignore"`
// StartDateTime specifies the specific starting datetime the budget is active, following
// the RFC3339 standard of "2006-01-02T15:04:05" - "yyyy-MM-DDTHH:MM:SS".
// Timezone here is UTC if no TZ is specified within the spec.
// +kubebuilder:validation:Pattern=`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})$`
// +kubebuilder:validation:Type="string"
// +optional
StartDateTime *string `json:"startDateTime,omitempty" hash:"ignore"`
// EndDateTime specifies the specific ending datetime the budget is active, following
// the RFC3339 standard of "2006-01-02T15:04:05" - "yyyy-MM-DDTHH:MM:SS".
// Timezone here is UTC if no TZ is specified within the spec.
// +kubebuilder:validation:Pattern=`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})$`
// +kubebuilder:validation:Type="string"
// +optional
EndDateTime *string `json:"endDateTime,omitempty" hash:"ignore"`
// TZ specifies the timezone of the budget. Follows the standard IANA "America/New_York" format. If not specified
// defaults to "UTC".
// +kubebuilder:validation:Type="string"
// +optional
TZ *string `json:"tz,omitempty" hash:"ignore"`
}

type ConsolidationPolicy string
Expand Down Expand Up @@ -308,26 +330,70 @@ func (in *Budget) GetAllowedDisruptions(c clock.Clock, numNodes int) (int, error
return res, nil
}

func (in *Budget) IsActiveDateTime(c clock.Clock, loc *time.Location) (bool, error) {
if in.StartDateTime != nil {
startTime, errST := time.ParseInLocation("2006-01-02T15:04:05", lo.FromPtr(in.StartDateTime), loc)
if errST != nil {
return false, fmt.Errorf("error parsing start datetime %v: %w", lo.FromPtr(in.StartDateTime), errST)
}
// Check to see if current datetime is before starting time
if !c.Now().In(loc).After(startTime) {
return false, nil
}
// If a defined startDateTime and duration exist, check that case
if in.Duration != nil && in.Schedule == nil {
if !c.Now().In(loc).Before(startTime.Add(-lo.FromPtr(in.Duration).Duration)) {
return false, nil
}
}
}
if in.EndDateTime != nil {
endTime, errET := time.ParseInLocation("2006-01-02T15:04:05", lo.FromPtr(in.EndDateTime), loc)
if errET != nil {
return false, fmt.Errorf("error parsing end datetime %v: %w", lo.FromPtr(in.EndDateTime), errET)
}
if !c.Now().In(loc).Before(endTime) {
return false, nil
}
}
return true, nil
}

// IsActive takes a clock as input and returns if a budget is active.
// It walks back in time the time.Duration associated with the schedule,
// and checks if the next time the schedule will hit is before the current time.
// If the last schedule hit is exactly the duration in the past, this means the
// schedule is active, as any more schedule hits in between would only extend this
// window. This ensures that any previous schedule hits for a schedule are considered.
func (in *Budget) IsActive(c clock.Clock) (bool, error) {
if in.Schedule == nil && in.Duration == nil {
if in.Schedule == nil && in.Duration == nil && in.StartDateTime == nil && in.EndDateTime == nil {
return true, nil
}
schedule, err := cron.ParseStandard(fmt.Sprintf("TZ=UTC %s", lo.FromPtr(in.Schedule)))
tz := "UTC"
if in.TZ != nil {
tz = lo.FromPtr(in.TZ)
}
loc, err := time.LoadLocation(tz)
if err != nil {
return false, fmt.Errorf("error loading timezone %v: %w", tz, err)
}
isActiveDateTime, err := in.IsActiveDateTime(c, loc)
if err != nil {
return false, err
}
if !isActiveDateTime {
return false, nil
}
schedule, err := cron.ParseStandard(fmt.Sprintf("TZ=%s %s", tz, lo.FromPtr(in.Schedule)))
if err != nil {
// Should only occur if there's a discrepancy
// with the validation regex and the cron package.
return false, fmt.Errorf("invariant violated, invalid cron %s", schedule)
}
// Walk back in time for the duration associated with the schedule
checkPoint := c.Now().UTC().Add(-lo.FromPtr(in.Duration).Duration)
checkPoint := c.Now().In(loc).Add(-lo.FromPtr(in.Duration).Duration)
nextHit := schedule.Next(checkPoint)
return !nextHit.After(c.Now().UTC()), nil
return !nextHit.After(c.Now().In(loc)), nil
}

func GetIntStrFromValue(str string) intstr.IntOrString {
Expand Down
Loading

0 comments on commit 5b629d7

Please sign in to comment.