vault/sdk/rotation/schedule.go

113 lines
3.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package rotation
import (
"fmt"
"time"
"github.com/robfig/cron/v3"
)
const (
// Minimum allowed value for rotation_window
minRotationWindowSeconds = 3600
parseOptions = cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow
)
// RotationSchedule holds the parsed and unparsed versions of the schedule, along with the projected next rotation time.
type RotationSchedule struct {
Schedule *cron.SpecSchedule `json:"schedule"`
RotationWindow time.Duration `json:"rotation_window"` // seconds of window
RotationSchedule string `json:"rotation_schedule"`
RotationPeriod time.Duration `json:"rotation_period"`
NextVaultRotation time.Time `json:"next_vault_rotation"`
LastVaultRotation time.Time `json:"last_vault_rotation"`
}
type Scheduler interface {
Parse(rotationSchedule string) (*cron.SpecSchedule, error)
ValidateRotationWindow(s int) error
NextRotationTimeFromInput(rs *RotationSchedule, input time.Time) time.Time
IsInsideRotationWindow(rs *RotationSchedule, t time.Time) bool
ShouldRotate(rs *RotationSchedule, priority int64, t time.Time) bool
NextRotationTime(rs *RotationSchedule) time.Time
SetNextVaultRotation(rs *RotationSchedule, t time.Time)
UsesTTL(rs *RotationSchedule) bool
UsesRotationSchedule(rs *RotationSchedule) bool
}
var DefaultScheduler Scheduler = &DefaultSchedule{}
type DefaultSchedule struct{}
func (d *DefaultSchedule) Parse(rotationSchedule string) (*cron.SpecSchedule, error) {
parser := cron.NewParser(parseOptions)
schedule, err := parser.Parse(rotationSchedule)
if err != nil {
return nil, err
}
sched, ok := schedule.(*cron.SpecSchedule)
if !ok {
return nil, fmt.Errorf("invalid rotation schedule")
}
return sched, nil
}
func (d *DefaultSchedule) ValidateRotationWindow(s int) error {
if s < minRotationWindowSeconds {
return fmt.Errorf("rotation_window must be %d seconds or more", minRotationWindowSeconds)
}
return nil
}
func (d *DefaultSchedule) UsesRotationSchedule(rs *RotationSchedule) bool {
return rs.RotationSchedule != "" && rs.RotationPeriod == 0
}
func (d *DefaultSchedule) UsesTTL(rs *RotationSchedule) bool {
return rs.RotationPeriod != 0 && rs.RotationSchedule == ""
}
// NextRotationTime calculates the next scheduled rotation
func (d *DefaultSchedule) NextRotationTime(rs *RotationSchedule) time.Time {
if d.UsesTTL(rs) {
return rs.LastVaultRotation.Add(rs.RotationPeriod)
}
return rs.Schedule.Next(time.Now())
}
// NextRotationTimeFromInput calculates and returns the next rotation time based on the provided schedule and input time
func (d *DefaultSchedule) NextRotationTimeFromInput(rs *RotationSchedule, input time.Time) time.Time {
if d.UsesTTL(rs) {
return input.Add(rs.RotationPeriod)
}
return rs.Schedule.Next(input)
}
// IsInsideRotationWindow checks if the current time is before the calculated end of the rotation window,
// to make sure that t time is within the specified rotation window
// It returns true if rotation window is not specified
func (d *DefaultSchedule) IsInsideRotationWindow(rs *RotationSchedule, t time.Time) bool {
if rs.RotationWindow != 0 {
return t.Before(rs.NextVaultRotation.Add(rs.RotationWindow))
}
return true
}
// ShouldRotate checks if the rotation should occur based on priority, current time, and rotation window
// It returns true if the priority is less than or equal to the current time and the current time is within the rotation window
func (d *DefaultSchedule) ShouldRotate(rs *RotationSchedule, priority int64, t time.Time) bool {
return priority <= t.Unix() && d.IsInsideRotationWindow(rs, t)
}
// SetNextVaultRotation calculates the next rotation time of a given schedule based on the time.
func (d *DefaultSchedule) SetNextVaultRotation(rs *RotationSchedule, t time.Time) {
if d.UsesTTL(rs) {
rs.NextVaultRotation = t.Add(rs.RotationPeriod)
} else {
rs.NextVaultRotation = rs.Schedule.Next(t)
}
}