mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-03 19:58:17 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			309 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			309 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package cron
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"math"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
)
 | 
						|
 | 
						|
// ParseStandard returns a new crontab schedule representing the given standardSpec
 | 
						|
// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
 | 
						|
// pass 5 entries representing: minute, hour, day of month, month and day of week,
 | 
						|
// in that order. It returns a descriptive error if the spec is not valid.
 | 
						|
//
 | 
						|
// It accepts
 | 
						|
//   - Standard crontab specs, e.g. "* * * * ?"
 | 
						|
//   - Descriptors, e.g. "@midnight", "@every 1h30m"
 | 
						|
func ParseStandard(standardSpec string) (Schedule, error) {
 | 
						|
	if standardSpec[0] == '@' {
 | 
						|
		return parseDescriptor(standardSpec)
 | 
						|
	}
 | 
						|
 | 
						|
	// Split on whitespace.  We require exactly 5 fields.
 | 
						|
	// (minute) (hour) (day of month) (month) (day of week)
 | 
						|
	fields := strings.Fields(standardSpec)
 | 
						|
	if len(fields) != 5 {
 | 
						|
		return nil, fmt.Errorf("Expected exactly 5 fields, found %d: %s", len(fields), standardSpec)
 | 
						|
	}
 | 
						|
 | 
						|
	var err error
 | 
						|
	field := func(field string, r bounds) uint64 {
 | 
						|
		if err != nil {
 | 
						|
			return uint64(0)
 | 
						|
		}
 | 
						|
		var bits uint64
 | 
						|
		bits, err = getField(field, r)
 | 
						|
		return bits
 | 
						|
	}
 | 
						|
	var (
 | 
						|
		minute     = field(fields[0], minutes)
 | 
						|
		hour       = field(fields[1], hours)
 | 
						|
		dayofmonth = field(fields[2], dom)
 | 
						|
		month      = field(fields[3], months)
 | 
						|
		dayofweek  = field(fields[4], dow)
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return &SpecSchedule{
 | 
						|
		Second: 1 << seconds.min,
 | 
						|
		Minute: minute,
 | 
						|
		Hour:   hour,
 | 
						|
		Dom:    dayofmonth,
 | 
						|
		Month:  month,
 | 
						|
		Dow:    dayofweek,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// Parse returns a new crontab schedule representing the given spec.
 | 
						|
// It returns a descriptive error if the spec is not valid.
 | 
						|
//
 | 
						|
// It accepts
 | 
						|
//   - Full crontab specs, e.g. "* * * * * ?"
 | 
						|
//   - Descriptors, e.g. "@midnight", "@every 1h30m"
 | 
						|
func Parse(spec string) (Schedule, error) {
 | 
						|
	if spec[0] == '@' {
 | 
						|
		return parseDescriptor(spec)
 | 
						|
	}
 | 
						|
 | 
						|
	// Split on whitespace.  We require 5 or 6 fields.
 | 
						|
	// (second) (minute) (hour) (day of month) (month) (day of week, optional)
 | 
						|
	fields := strings.Fields(spec)
 | 
						|
	if len(fields) != 5 && len(fields) != 6 {
 | 
						|
		return nil, fmt.Errorf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
 | 
						|
	}
 | 
						|
 | 
						|
	// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
 | 
						|
	if len(fields) == 5 {
 | 
						|
		fields = append(fields, "*")
 | 
						|
	}
 | 
						|
 | 
						|
	var err error
 | 
						|
	field := func(field string, r bounds) uint64 {
 | 
						|
		if err != nil {
 | 
						|
			return uint64(0)
 | 
						|
		}
 | 
						|
		var bits uint64
 | 
						|
		bits, err = getField(field, r)
 | 
						|
		return bits
 | 
						|
	}
 | 
						|
	var (
 | 
						|
		second     = field(fields[0], seconds)
 | 
						|
		minute     = field(fields[1], minutes)
 | 
						|
		hour       = field(fields[2], hours)
 | 
						|
		dayofmonth = field(fields[3], dom)
 | 
						|
		month      = field(fields[4], months)
 | 
						|
		dayofweek  = field(fields[5], dow)
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return &SpecSchedule{
 | 
						|
		Second: second,
 | 
						|
		Minute: minute,
 | 
						|
		Hour:   hour,
 | 
						|
		Dom:    dayofmonth,
 | 
						|
		Month:  month,
 | 
						|
		Dow:    dayofweek,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// getField returns an Int with the bits set representing all of the times that
 | 
						|
// the field represents or error parsing field value.  A "field" is a comma-separated
 | 
						|
// list of "ranges".
 | 
						|
func getField(field string, r bounds) (uint64, error) {
 | 
						|
	var bits uint64
 | 
						|
	ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
 | 
						|
	for _, expr := range ranges {
 | 
						|
		bit, err := getRange(expr, r)
 | 
						|
		if err != nil {
 | 
						|
			return bits, err
 | 
						|
		}
 | 
						|
		bits |= bit
 | 
						|
	}
 | 
						|
	return bits, nil
 | 
						|
}
 | 
						|
 | 
						|
// getRange returns the bits indicated by the given expression:
 | 
						|
//   number | number "-" number [ "/" number ]
 | 
						|
// or error parsing range.
 | 
						|
func getRange(expr string, r bounds) (uint64, error) {
 | 
						|
	var (
 | 
						|
		start, end, step uint
 | 
						|
		rangeAndStep     = strings.Split(expr, "/")
 | 
						|
		lowAndHigh       = strings.Split(rangeAndStep[0], "-")
 | 
						|
		singleDigit      = len(lowAndHigh) == 1
 | 
						|
		err              error
 | 
						|
		zero             = uint64(0)
 | 
						|
	)
 | 
						|
 | 
						|
	var extra_star uint64
 | 
						|
	if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
 | 
						|
		start = r.min
 | 
						|
		end = r.max
 | 
						|
		extra_star = starBit
 | 
						|
	} else {
 | 
						|
		start, err = parseIntOrName(lowAndHigh[0], r.names)
 | 
						|
		if err != nil {
 | 
						|
			return zero, err
 | 
						|
		}
 | 
						|
		switch len(lowAndHigh) {
 | 
						|
		case 1:
 | 
						|
			end = start
 | 
						|
		case 2:
 | 
						|
			end, err = parseIntOrName(lowAndHigh[1], r.names)
 | 
						|
			if err != nil {
 | 
						|
				return zero, err
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			return zero, fmt.Errorf("Too many hyphens: %s", expr)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	switch len(rangeAndStep) {
 | 
						|
	case 1:
 | 
						|
		step = 1
 | 
						|
	case 2:
 | 
						|
		step, err = mustParseInt(rangeAndStep[1])
 | 
						|
		if err != nil {
 | 
						|
			return zero, err
 | 
						|
		}
 | 
						|
 | 
						|
		// Special handling: "N/step" means "N-max/step".
 | 
						|
		if singleDigit {
 | 
						|
			end = r.max
 | 
						|
		}
 | 
						|
	default:
 | 
						|
		return zero, fmt.Errorf("Too many slashes: %s", expr)
 | 
						|
	}
 | 
						|
 | 
						|
	if start < r.min {
 | 
						|
		return zero, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
 | 
						|
	}
 | 
						|
	if end > r.max {
 | 
						|
		return zero, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
 | 
						|
	}
 | 
						|
	if start > end {
 | 
						|
		return zero, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
 | 
						|
	}
 | 
						|
	if step == 0 {
 | 
						|
		return zero, fmt.Errorf("Step of range should be a positive number: %s", expr)
 | 
						|
	}
 | 
						|
 | 
						|
	return getBits(start, end, step) | extra_star, nil
 | 
						|
}
 | 
						|
 | 
						|
// parseIntOrName returns the (possibly-named) integer contained in expr.
 | 
						|
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
 | 
						|
	if names != nil {
 | 
						|
		if namedInt, ok := names[strings.ToLower(expr)]; ok {
 | 
						|
			return namedInt, nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return mustParseInt(expr)
 | 
						|
}
 | 
						|
 | 
						|
// mustParseInt parses the given expression as an int or returns an error.
 | 
						|
func mustParseInt(expr string) (uint, error) {
 | 
						|
	num, err := strconv.Atoi(expr)
 | 
						|
	if err != nil {
 | 
						|
		return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
 | 
						|
	}
 | 
						|
	if num < 0 {
 | 
						|
		return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
 | 
						|
	}
 | 
						|
 | 
						|
	return uint(num), nil
 | 
						|
}
 | 
						|
 | 
						|
// getBits sets all bits in the range [min, max], modulo the given step size.
 | 
						|
func getBits(min, max, step uint) uint64 {
 | 
						|
	var bits uint64
 | 
						|
 | 
						|
	// If step is 1, use shifts.
 | 
						|
	if step == 1 {
 | 
						|
		return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
 | 
						|
	}
 | 
						|
 | 
						|
	// Else, use a simple loop.
 | 
						|
	for i := min; i <= max; i += step {
 | 
						|
		bits |= 1 << i
 | 
						|
	}
 | 
						|
	return bits
 | 
						|
}
 | 
						|
 | 
						|
// all returns all bits within the given bounds.  (plus the star bit)
 | 
						|
func all(r bounds) uint64 {
 | 
						|
	return getBits(r.min, r.max, 1) | starBit
 | 
						|
}
 | 
						|
 | 
						|
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
 | 
						|
func parseDescriptor(descriptor string) (Schedule, error) {
 | 
						|
	switch descriptor {
 | 
						|
	case "@yearly", "@annually":
 | 
						|
		return &SpecSchedule{
 | 
						|
			Second: 1 << seconds.min,
 | 
						|
			Minute: 1 << minutes.min,
 | 
						|
			Hour:   1 << hours.min,
 | 
						|
			Dom:    1 << dom.min,
 | 
						|
			Month:  1 << months.min,
 | 
						|
			Dow:    all(dow),
 | 
						|
		}, nil
 | 
						|
 | 
						|
	case "@monthly":
 | 
						|
		return &SpecSchedule{
 | 
						|
			Second: 1 << seconds.min,
 | 
						|
			Minute: 1 << minutes.min,
 | 
						|
			Hour:   1 << hours.min,
 | 
						|
			Dom:    1 << dom.min,
 | 
						|
			Month:  all(months),
 | 
						|
			Dow:    all(dow),
 | 
						|
		}, nil
 | 
						|
 | 
						|
	case "@weekly":
 | 
						|
		return &SpecSchedule{
 | 
						|
			Second: 1 << seconds.min,
 | 
						|
			Minute: 1 << minutes.min,
 | 
						|
			Hour:   1 << hours.min,
 | 
						|
			Dom:    all(dom),
 | 
						|
			Month:  all(months),
 | 
						|
			Dow:    1 << dow.min,
 | 
						|
		}, nil
 | 
						|
 | 
						|
	case "@daily", "@midnight":
 | 
						|
		return &SpecSchedule{
 | 
						|
			Second: 1 << seconds.min,
 | 
						|
			Minute: 1 << minutes.min,
 | 
						|
			Hour:   1 << hours.min,
 | 
						|
			Dom:    all(dom),
 | 
						|
			Month:  all(months),
 | 
						|
			Dow:    all(dow),
 | 
						|
		}, nil
 | 
						|
 | 
						|
	case "@hourly":
 | 
						|
		return &SpecSchedule{
 | 
						|
			Second: 1 << seconds.min,
 | 
						|
			Minute: 1 << minutes.min,
 | 
						|
			Hour:   all(hours),
 | 
						|
			Dom:    all(dom),
 | 
						|
			Month:  all(months),
 | 
						|
			Dow:    all(dow),
 | 
						|
		}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	const every = "@every "
 | 
						|
	if strings.HasPrefix(descriptor, every) {
 | 
						|
		duration, err := time.ParseDuration(descriptor[len(every):])
 | 
						|
		if err != nil {
 | 
						|
			return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
 | 
						|
		}
 | 
						|
		return Every(duration), nil
 | 
						|
	}
 | 
						|
 | 
						|
	return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
 | 
						|
}
 |