mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-03 19:58:17 +00:00 
			
		
		
		
	Add a simple diurnal controller.
The diurnal controller changes the number of replicas of a replication controller based on a list of times and replica counts. It is meant to be run under a replication controller.
This commit is contained in:
		
							
								
								
									
										8
									
								
								contrib/diurnal/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								contrib/diurnal/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					FROM busybox
 | 
				
			||||||
 | 
					MAINTAINER Muhammed Uluyol "uluyol@google.com"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ADD dc /diurnal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN chown root:users /diurnal && chmod 755 /diurnal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ENTRYPOINT ["/diurnal"]
 | 
				
			||||||
							
								
								
									
										24
									
								
								contrib/diurnal/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								contrib/diurnal/Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					.PHONY: build push vet test clean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TAG = 0.5
 | 
				
			||||||
 | 
					REPO = uluyol/kube-diurnal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BIN = dc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dc: dc.go time.go
 | 
				
			||||||
 | 
						CGO_ENABLED=0 godep go build -a -installsuffix cgo -o dc dc.go time.go
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					vet:
 | 
				
			||||||
 | 
						godep go vet .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test:
 | 
				
			||||||
 | 
						godep go test .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					build: $(BIN)
 | 
				
			||||||
 | 
						docker build -t $(REPO):$(TAG) .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					push:
 | 
				
			||||||
 | 
						docker push $(REPO):$(TAG)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					clean:
 | 
				
			||||||
 | 
						rm -f $(BIN)
 | 
				
			||||||
							
								
								
									
										44
									
								
								contrib/diurnal/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								contrib/diurnal/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					## Diurnal Controller
 | 
				
			||||||
 | 
					This controller manipulates the number of replicas maintained by a replication controller throughout the day based on a provided list of times of day (according to ISO 8601) and replica counts. It should be run under a replication controller that is in the same namespace as the replication controller that it is manipulating.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For example, to set the replica counts of the pods with the labels "tier=backend,track=canary" to 10 at noon UTC and 6 at midnight UTC, we can use `-labels tier=backend,track=canary -times 00:00Z,12:00Z -counts 6,10`. An example replication controller config can be found [here](example-diurnal-controller.yaml).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Instead of providing replica counts and times of day directly, you may use a script like the one below to generate them using mathematical functions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```python
 | 
				
			||||||
 | 
					from math import *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _day_to_2pi(t):
 | 
				
			||||||
 | 
					    return float(t) * 2 * pi / (24*3600)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main(args):
 | 
				
			||||||
 | 
					    if len(args) < 3:
 | 
				
			||||||
 | 
					        print "Usage: %s sample_interval func" % (args[0],)
 | 
				
			||||||
 | 
					        print "func should be a function of the variable t, where t will range from 0"
 | 
				
			||||||
 | 
					        print "to 2pi over the course of the day"
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					    sampling_interval = int(args[1])
 | 
				
			||||||
 | 
					    exec "def f(t): return " + args[2]
 | 
				
			||||||
 | 
					    i = 0
 | 
				
			||||||
 | 
					    times = []
 | 
				
			||||||
 | 
					    counts = []
 | 
				
			||||||
 | 
					    while i < 24*60*60:
 | 
				
			||||||
 | 
					        hours = i / 3600
 | 
				
			||||||
 | 
					        left = i - hours*3600
 | 
				
			||||||
 | 
					        min = left / 60
 | 
				
			||||||
 | 
					        sec = left - min*60
 | 
				
			||||||
 | 
					        times.append("%dh%dm%ds" % (hours, min, sec))
 | 
				
			||||||
 | 
					        count = int(round(f(_day_to_2pi(i))))
 | 
				
			||||||
 | 
					        counts.append(str(count))
 | 
				
			||||||
 | 
					        i += sampling_interval
 | 
				
			||||||
 | 
					    print "-times %s -counts %s" % (",".join(times), ",".join(counts))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main(sys.argv)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[]()
 | 
				
			||||||
							
								
								
									
										283
									
								
								contrib/diurnal/dc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								contrib/diurnal/dc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,283 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2015 The Kubernetes Authors All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// An external diurnal controller for kubernetes. With this, it's possible to manage
 | 
				
			||||||
 | 
					// known replica counts that vary throughout the day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"flag"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/golang/glog"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dayPeriod = 24 * time.Hour
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type timeCount struct {
 | 
				
			||||||
 | 
						time  time.Duration
 | 
				
			||||||
 | 
						count int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (tc timeCount) String() string {
 | 
				
			||||||
 | 
						h := tc.time / time.Hour
 | 
				
			||||||
 | 
						m := (tc.time % time.Hour) / time.Minute
 | 
				
			||||||
 | 
						s := (tc.time % time.Minute) / time.Second
 | 
				
			||||||
 | 
						if m == 0 && s == 0 {
 | 
				
			||||||
 | 
							return fmt.Sprintf("(%02dZ, %d)", h, tc.count)
 | 
				
			||||||
 | 
						} else if s == 0 {
 | 
				
			||||||
 | 
							return fmt.Sprintf("(%02d:%02dZ, %d)", h, m, tc.count)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fmt.Sprintf("(%02d:%02d:%02dZ, %d)", h, m, s, tc.count)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type byTime []timeCount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (tc byTime) Len() int           { return len(tc) }
 | 
				
			||||||
 | 
					func (tc byTime) Swap(i, j int)      { tc[i], tc[j] = tc[j], tc[i] }
 | 
				
			||||||
 | 
					func (tc byTime) Less(i, j int) bool { return tc[i].time < tc[j].time }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func timeMustParse(layout, s string) time.Time {
 | 
				
			||||||
 | 
						t, err := time.Parse(layout, s)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return t
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// first argument is a format string equivalent to HHMMSS. See time.Parse for details.
 | 
				
			||||||
 | 
					var epoch = timeMustParse("150405", "000000")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseTimeRelative(s string) (time.Duration, error) {
 | 
				
			||||||
 | 
						t, err := parseTimeISO8601(s)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("unable to parse %s: %v", s, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return (t.Sub(epoch) + dayPeriod) % dayPeriod, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseTimeCounts(times string, counts string) ([]timeCount, error) {
 | 
				
			||||||
 | 
						ts := strings.Split(times, ",")
 | 
				
			||||||
 | 
						cs := strings.Split(counts, ",")
 | 
				
			||||||
 | 
						if len(ts) != len(cs) {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("provided %d times but %d replica counts", len(ts), len(cs))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var tc []timeCount
 | 
				
			||||||
 | 
						for i := range ts {
 | 
				
			||||||
 | 
							t, err := parseTimeRelative(ts[i])
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c, err := strconv.ParseInt(cs[i], 10, 64)
 | 
				
			||||||
 | 
							if c < 0 {
 | 
				
			||||||
 | 
								return nil, errors.New("counts must be non-negative")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							tc = append(tc, timeCount{t, int(c)})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sort.Sort(byTime(tc))
 | 
				
			||||||
 | 
						return tc, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Scaler struct {
 | 
				
			||||||
 | 
						timeCounts []timeCount
 | 
				
			||||||
 | 
						selector   labels.Selector
 | 
				
			||||||
 | 
						start      time.Time
 | 
				
			||||||
 | 
						pos        int
 | 
				
			||||||
 | 
						done       chan struct{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var posError = errors.New("could not find position")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func findPos(tc []timeCount, cur int, offset time.Duration) int {
 | 
				
			||||||
 | 
						first := true
 | 
				
			||||||
 | 
						for i := cur; i != cur || first; i = (i + 1) % len(tc) {
 | 
				
			||||||
 | 
							if tc[i].time > offset {
 | 
				
			||||||
 | 
								return i
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							first = false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Scaler) setCount(c int) {
 | 
				
			||||||
 | 
						glog.Infof("scaling to %d replicas", c)
 | 
				
			||||||
 | 
						rcList, err := client.ReplicationControllers(namespace).List(s.selector)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							glog.Errorf("could not get replication controllers: %v", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, rc := range rcList.Items {
 | 
				
			||||||
 | 
							rc.Spec.Replicas = c
 | 
				
			||||||
 | 
							if _, err = client.ReplicationControllers(namespace).Update(&rc); err != nil {
 | 
				
			||||||
 | 
								glog.Errorf("unable to scale replication controller: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Scaler) timeOffset() time.Duration {
 | 
				
			||||||
 | 
						return time.Since(s.start) % dayPeriod
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Scaler) curpos(offset time.Duration) int {
 | 
				
			||||||
 | 
						return findPos(s.timeCounts, s.pos, offset)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Scaler) scale() {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-s.done:
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								offset := s.timeOffset()
 | 
				
			||||||
 | 
								s.pos = s.curpos(offset)
 | 
				
			||||||
 | 
								if s.timeCounts[s.pos].time < offset {
 | 
				
			||||||
 | 
									time.Sleep(dayPeriod - offset)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								time.Sleep(s.timeCounts[s.pos].time - offset)
 | 
				
			||||||
 | 
								s.setCount(s.timeCounts[s.pos].count)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Scaler) Start() error {
 | 
				
			||||||
 | 
						now := time.Now().UTC()
 | 
				
			||||||
 | 
						s.start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
 | 
				
			||||||
 | 
						if *startNow {
 | 
				
			||||||
 | 
							s.start = now
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// set initial count
 | 
				
			||||||
 | 
						pos := s.curpos(s.timeOffset())
 | 
				
			||||||
 | 
						// add the len to avoid getting a negative index
 | 
				
			||||||
 | 
						pos = (pos - 1 + len(s.timeCounts)) % len(s.timeCounts)
 | 
				
			||||||
 | 
						s.setCount(s.timeCounts[pos].count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s.done = make(chan struct{})
 | 
				
			||||||
 | 
						go s.scale()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func safeclose(c chan<- struct{}) (err error) {
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if e := recover(); e != nil {
 | 
				
			||||||
 | 
								err = e.(error)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						close(c)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Scaler) Stop() error {
 | 
				
			||||||
 | 
						if err := safeclose(s.done); err != nil {
 | 
				
			||||||
 | 
							return errors.New("already stopped scaling")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						counts     = flag.String("counts", "", "replica counts, must have at least one (csv)")
 | 
				
			||||||
 | 
						times      = flag.String("times", "", "times to set replica counts relative to UTC following ISO 8601 (csv)")
 | 
				
			||||||
 | 
						userLabels = flag.String("labels", "", "replication controller labels, syntax should follow https://godoc.org/github.com/GoogleCloudPlatform/kubernetes/pkg/labels#Parse")
 | 
				
			||||||
 | 
						startNow   = flag.Bool("now", false, "times are relative to now not 0:00 UTC (for demos)")
 | 
				
			||||||
 | 
						local      = flag.Bool("local", false, "set to true if running on local machine not within cluster")
 | 
				
			||||||
 | 
						localPort  = flag.Int("localport", 8001, "port that kubectl proxy is running on (local must be true)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						namespace string = os.Getenv("POD_NAMESPACE")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client *kclient.Client
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const usageNotes = `
 | 
				
			||||||
 | 
					counts and times must both be set and be of equal length. Example usage:
 | 
				
			||||||
 | 
					  diurnal -labels name=redis-slave -times 00:00:00Z,06:00:00Z -counts 3,9
 | 
				
			||||||
 | 
					  diurnal -labels name=redis-slave -times 0600-0500,0900-0500,1700-0500,2200-0500 -counts 15,20,13,6
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func usage() {
 | 
				
			||||||
 | 
						fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
 | 
				
			||||||
 | 
						flag.PrintDefaults()
 | 
				
			||||||
 | 
						fmt.Fprint(os.Stderr, usageNotes)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
						flag.Usage = usage
 | 
				
			||||||
 | 
						flag.Parse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							cfg *kclient.Config
 | 
				
			||||||
 | 
							err error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if *local {
 | 
				
			||||||
 | 
							cfg = &kclient.Config{Host: fmt.Sprintf("http://localhost:%d", *localPort)}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							cfg, err = kclient.InClusterConfig()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								glog.Errorf("failed to load config: %v", err)
 | 
				
			||||||
 | 
								flag.Usage()
 | 
				
			||||||
 | 
								os.Exit(1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						client, err = kclient.New(cfg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						selector, err := labels.Parse(*userLabels)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							glog.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tc, err := parseTimeCounts(*times, *counts)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							glog.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if namespace == "" {
 | 
				
			||||||
 | 
							glog.Fatal("POD_NAMESPACE is not set. Set to the namespace of the replication controller if running locally.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						scaler := Scaler{timeCounts: tc, selector: selector}
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							glog.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sigChan := make(chan os.Signal, 1)
 | 
				
			||||||
 | 
						signal.Notify(sigChan,
 | 
				
			||||||
 | 
							syscall.SIGHUP,
 | 
				
			||||||
 | 
							syscall.SIGINT,
 | 
				
			||||||
 | 
							syscall.SIGQUIT,
 | 
				
			||||||
 | 
							syscall.SIGTERM)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						glog.Info("starting scaling")
 | 
				
			||||||
 | 
						if err := scaler.Start(); err != nil {
 | 
				
			||||||
 | 
							glog.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						<-sigChan
 | 
				
			||||||
 | 
						glog.Info("stopping scaling")
 | 
				
			||||||
 | 
						if err := scaler.Stop(); err != nil {
 | 
				
			||||||
 | 
							glog.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										100
									
								
								contrib/diurnal/dc_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								contrib/diurnal/dc_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2015 The Kubernetes Authors All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func equalsTimeCounts(a, b []timeCount) bool {
 | 
				
			||||||
 | 
						if len(a) != len(b) {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i := range a {
 | 
				
			||||||
 | 
							if a[i].time != b[i].time || a[i].count != b[i].count {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestParseTimeCounts(t *testing.T) {
 | 
				
			||||||
 | 
						cases := []struct {
 | 
				
			||||||
 | 
							times  string
 | 
				
			||||||
 | 
							counts string
 | 
				
			||||||
 | 
							out    []timeCount
 | 
				
			||||||
 | 
							err    bool
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								"00:00:01Z,00:02Z,03:00Z,04:00Z", "1,4,1,8", []timeCount{
 | 
				
			||||||
 | 
									{time.Second, 1},
 | 
				
			||||||
 | 
									{2 * time.Minute, 4},
 | 
				
			||||||
 | 
									{3 * time.Hour, 1},
 | 
				
			||||||
 | 
									{4 * time.Hour, 8},
 | 
				
			||||||
 | 
								}, false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								"00:01Z,00:02Z,00:05Z,00:03Z", "1,2,3,4", []timeCount{
 | 
				
			||||||
 | 
									{1 * time.Minute, 1},
 | 
				
			||||||
 | 
									{2 * time.Minute, 2},
 | 
				
			||||||
 | 
									{3 * time.Minute, 4},
 | 
				
			||||||
 | 
									{5 * time.Minute, 3},
 | 
				
			||||||
 | 
								}, false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{"00:00Z,00:01Z", "1,0", []timeCount{{0, 1}, {1 * time.Minute, 0}}, false},
 | 
				
			||||||
 | 
							{"00:00+00,00:01+00:00,01:00Z", "0,-1,0", nil, true},
 | 
				
			||||||
 | 
							{"-00:01Z,01:00Z", "0,1", nil, true},
 | 
				
			||||||
 | 
							{"00:00Z", "1,2,3", nil, true},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i, test := range cases {
 | 
				
			||||||
 | 
							out, err := parseTimeCounts(test.times, test.counts)
 | 
				
			||||||
 | 
							if test.err && err == nil {
 | 
				
			||||||
 | 
								t.Errorf("case %d: expected error", i)
 | 
				
			||||||
 | 
							} else if !test.err && err != nil {
 | 
				
			||||||
 | 
								t.Errorf("case %d: unexpected error: %v", i, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !test.err {
 | 
				
			||||||
 | 
								if !equalsTimeCounts(test.out, out) {
 | 
				
			||||||
 | 
									t.Errorf("case %d: expected timeCounts: %v got %v", i, test.out, out)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestFindPos(t *testing.T) {
 | 
				
			||||||
 | 
						cases := []struct {
 | 
				
			||||||
 | 
							tc       []timeCount
 | 
				
			||||||
 | 
							cur      int
 | 
				
			||||||
 | 
							offset   time.Duration
 | 
				
			||||||
 | 
							expected int
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{[]timeCount{{0, 1}, {4, 0}}, 1, 1, 1},
 | 
				
			||||||
 | 
							{[]timeCount{{0, 1}, {4, 0}}, 0, 1, 1},
 | 
				
			||||||
 | 
							{[]timeCount{{0, 1}, {4, 0}}, 1, 70, 0},
 | 
				
			||||||
 | 
							{[]timeCount{{5, 1}, {100, 9000}, {4000, 2}, {10000, 4}}, 0, 0, 0},
 | 
				
			||||||
 | 
							{[]timeCount{{5, 1}, {100, 9000}, {4000, 2}, {10000, 4}}, 1, 5000, 3},
 | 
				
			||||||
 | 
							{[]timeCount{{5, 1}, {100, 9000}, {4000, 2}, {10000, 4}}, 2, 10000000, 0},
 | 
				
			||||||
 | 
							{[]timeCount{{5, 1}, {100, 9000}, {4000, 2}, {10000, 4}}, 0, 50, 1},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i, test := range cases {
 | 
				
			||||||
 | 
							pos := findPos(test.tc, test.cur, test.offset)
 | 
				
			||||||
 | 
							if pos != test.expected {
 | 
				
			||||||
 | 
								t.Errorf("case %d: expected %d got %d", i, test.expected, pos)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								contrib/diurnal/example-diurnal-controller.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								contrib/diurnal/example-diurnal-controller.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					apiVersion: v1
 | 
				
			||||||
 | 
					kind: ReplicationController
 | 
				
			||||||
 | 
					metadata:
 | 
				
			||||||
 | 
					  labels:
 | 
				
			||||||
 | 
					    name: diurnal-controller
 | 
				
			||||||
 | 
					  name: diurnal-controller
 | 
				
			||||||
 | 
					spec:
 | 
				
			||||||
 | 
					  replicas: 1
 | 
				
			||||||
 | 
					  selector:
 | 
				
			||||||
 | 
					    name: diurnal-controller
 | 
				
			||||||
 | 
					  template:
 | 
				
			||||||
 | 
					    metadata:
 | 
				
			||||||
 | 
					      labels:
 | 
				
			||||||
 | 
					        name: diurnal-controller
 | 
				
			||||||
 | 
					    spec:
 | 
				
			||||||
 | 
					      containers:
 | 
				
			||||||
 | 
					        - args: ["-labels", "name=redis-slave", "-times", "00:00Z,00:02Z,01:00Z,02:30Z", "-counts", "3,7,6,9"]
 | 
				
			||||||
 | 
					          resources:
 | 
				
			||||||
 | 
					            limits:
 | 
				
			||||||
 | 
					              cpu: 0.1
 | 
				
			||||||
 | 
					          env:
 | 
				
			||||||
 | 
					            - name: POD_NAMESPACE
 | 
				
			||||||
 | 
					              valueFrom:
 | 
				
			||||||
 | 
					                fieldRef:
 | 
				
			||||||
 | 
					                  fieldPath: metadata.namespace
 | 
				
			||||||
 | 
					          image: uluyol/kube-diurnal:0.5
 | 
				
			||||||
 | 
					          name: diurnal-controller
 | 
				
			||||||
							
								
								
									
										226
									
								
								contrib/diurnal/time.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								contrib/diurnal/time.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,226 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2015 The Kubernetes Authors All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type parseTimeState int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						sHour parseTimeState = iota + 1
 | 
				
			||||||
 | 
						sMinute
 | 
				
			||||||
 | 
						sSecond
 | 
				
			||||||
 | 
						sUTC
 | 
				
			||||||
 | 
						sOffHour
 | 
				
			||||||
 | 
						sOffMinute
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var parseTimeStateString = map[parseTimeState]string{
 | 
				
			||||||
 | 
						sHour:      "hour",
 | 
				
			||||||
 | 
						sMinute:    "minute",
 | 
				
			||||||
 | 
						sSecond:    "second",
 | 
				
			||||||
 | 
						sUTC:       "UTC",
 | 
				
			||||||
 | 
						sOffHour:   "offset hour",
 | 
				
			||||||
 | 
						sOffMinute: "offset minute",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type timeParseErr struct {
 | 
				
			||||||
 | 
						state parseTimeState
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t timeParseErr) Error() string {
 | 
				
			||||||
 | 
						return "expected two digits for " + parseTimeStateString[t.state]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getTwoDigits(s string) (int, bool) {
 | 
				
			||||||
 | 
						if len(s) >= 2 && '0' <= s[0] && s[0] <= '9' && '0' <= s[1] && s[1] <= '9' {
 | 
				
			||||||
 | 
							return int(s[0]-'0')*10 + int(s[1]-'0'), true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0, false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func zoneChar(b byte) bool {
 | 
				
			||||||
 | 
						return b == 'Z' || b == '+' || b == '-'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func validate(x, min, max int, name string) error {
 | 
				
			||||||
 | 
						if x < min || max < x {
 | 
				
			||||||
 | 
							return fmt.Errorf("the %s must be within the range %d...%d", name, min, max)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type triState int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						unset triState = iota
 | 
				
			||||||
 | 
						setFalse
 | 
				
			||||||
 | 
						setTrue
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// parseTimeISO8601 parses times (without dates) according to the ISO 8601
 | 
				
			||||||
 | 
					// standard. The standard time package can understand layouts which accept
 | 
				
			||||||
 | 
					// valid ISO 8601 input. However, these layouts also accept input which is
 | 
				
			||||||
 | 
					// not valid ISO 8601 (in particular, negative zero time offset or "-00").
 | 
				
			||||||
 | 
					// Furthermore, there are a number of acceptable layouts, and to handle
 | 
				
			||||||
 | 
					// all of them using the time package requires trying them one at a time.
 | 
				
			||||||
 | 
					// This is error-prone, slow, not obviously correct, and again, allows
 | 
				
			||||||
 | 
					// a wider range of input to be accepted than is desirable. For these
 | 
				
			||||||
 | 
					// reasons, we implement ISO 8601 parsing without the use of the time
 | 
				
			||||||
 | 
					// package.
 | 
				
			||||||
 | 
					func parseTimeISO8601(s string) (time.Time, error) {
 | 
				
			||||||
 | 
						theTime := struct {
 | 
				
			||||||
 | 
							hour      int
 | 
				
			||||||
 | 
							minute    int
 | 
				
			||||||
 | 
							second    int
 | 
				
			||||||
 | 
							utc       triState
 | 
				
			||||||
 | 
							offNeg    bool
 | 
				
			||||||
 | 
							offHour   int
 | 
				
			||||||
 | 
							offMinute int
 | 
				
			||||||
 | 
						}{}
 | 
				
			||||||
 | 
						state := sHour
 | 
				
			||||||
 | 
						isExtended := false
 | 
				
			||||||
 | 
						for s != "" {
 | 
				
			||||||
 | 
							switch state {
 | 
				
			||||||
 | 
							case sHour:
 | 
				
			||||||
 | 
								v, ok := getTwoDigits(s)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									return time.Time{}, timeParseErr{state}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								theTime.hour = v
 | 
				
			||||||
 | 
								s = s[2:]
 | 
				
			||||||
 | 
							case sMinute:
 | 
				
			||||||
 | 
								if !zoneChar(s[0]) {
 | 
				
			||||||
 | 
									if s[0] == ':' {
 | 
				
			||||||
 | 
										isExtended = true
 | 
				
			||||||
 | 
										s = s[1:]
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									v, ok := getTwoDigits(s)
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										return time.Time{}, timeParseErr{state}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									theTime.minute = v
 | 
				
			||||||
 | 
									s = s[2:]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case sSecond:
 | 
				
			||||||
 | 
								if !zoneChar(s[0]) {
 | 
				
			||||||
 | 
									if s[0] == ':' {
 | 
				
			||||||
 | 
										if isExtended {
 | 
				
			||||||
 | 
											s = s[1:]
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											return time.Time{}, errors.New("unexpected ':' before 'second' value")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									} else if isExtended {
 | 
				
			||||||
 | 
										return time.Time{}, errors.New("expected ':' before 'second' value")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									v, ok := getTwoDigits(s)
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										return time.Time{}, timeParseErr{state}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									theTime.second = v
 | 
				
			||||||
 | 
									s = s[2:]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case sUTC:
 | 
				
			||||||
 | 
								if s[0] == 'Z' {
 | 
				
			||||||
 | 
									theTime.utc = setTrue
 | 
				
			||||||
 | 
									s = s[1:]
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									theTime.utc = setFalse
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case sOffHour:
 | 
				
			||||||
 | 
								if theTime.utc == setTrue {
 | 
				
			||||||
 | 
									return time.Time{}, errors.New("unexpected offset, already specified UTC")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								var sign int
 | 
				
			||||||
 | 
								if s[0] == '+' {
 | 
				
			||||||
 | 
									sign = 1
 | 
				
			||||||
 | 
								} else if s[0] == '-' {
 | 
				
			||||||
 | 
									sign = -1
 | 
				
			||||||
 | 
									theTime.offNeg = true
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									return time.Time{}, errors.New("offset must begin with '+' or '-'")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								s = s[1:]
 | 
				
			||||||
 | 
								v, ok := getTwoDigits(s)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									return time.Time{}, timeParseErr{state}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								theTime.offHour = sign * v
 | 
				
			||||||
 | 
								s = s[2:]
 | 
				
			||||||
 | 
							case sOffMinute:
 | 
				
			||||||
 | 
								if s[0] == ':' {
 | 
				
			||||||
 | 
									if isExtended {
 | 
				
			||||||
 | 
										s = s[1:]
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										return time.Time{}, errors.New("unexpected ':' before 'minute' value")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else if isExtended {
 | 
				
			||||||
 | 
									return time.Time{}, errors.New("expected ':' before 'second' value")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								v, ok := getTwoDigits(s)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									return time.Time{}, timeParseErr{state}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								theTime.offMinute = v
 | 
				
			||||||
 | 
								s = s[2:]
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return time.Time{}, errors.New("an unknown error occured")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							state++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := validate(theTime.hour, 0, 23, "hour"); err != nil {
 | 
				
			||||||
 | 
							return time.Time{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := validate(theTime.minute, 0, 59, "minute"); err != nil {
 | 
				
			||||||
 | 
							return time.Time{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := validate(theTime.second, 0, 59, "second"); err != nil {
 | 
				
			||||||
 | 
							return time.Time{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := validate(theTime.offHour, -12, 14, "offset hour"); err != nil {
 | 
				
			||||||
 | 
							return time.Time{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := validate(theTime.offMinute, 0, 59, "offset minute"); err != nil {
 | 
				
			||||||
 | 
							return time.Time{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if theTime.offNeg && theTime.offHour == 0 && theTime.offMinute == 0 {
 | 
				
			||||||
 | 
							return time.Time{}, errors.New("an offset of -00 may not be used, must use +00")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							loc *time.Location
 | 
				
			||||||
 | 
							err error
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if theTime.utc == setTrue {
 | 
				
			||||||
 | 
							loc, err = time.LoadLocation("UTC")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								panic(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if theTime.utc == setFalse {
 | 
				
			||||||
 | 
							loc = time.FixedZone("Zone", theTime.offMinute*60+theTime.offHour*3600)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							loc, err = time.LoadLocation("Local")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								panic(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						t := time.Date(1, time.January, 1, theTime.hour, theTime.minute, theTime.second, 0, loc)
 | 
				
			||||||
 | 
						return t, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										104
									
								
								contrib/diurnal/time_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								contrib/diurnal/time_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2015 The Kubernetes Authors All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestParseTimeISO8601(t *testing.T) {
 | 
				
			||||||
 | 
						cases := []struct {
 | 
				
			||||||
 | 
							input    string
 | 
				
			||||||
 | 
							expected time.Time
 | 
				
			||||||
 | 
							err      bool
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{"00", timeMustParse("15", "00"), false},
 | 
				
			||||||
 | 
							{"49", time.Time{}, true},
 | 
				
			||||||
 | 
							{"-2", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12:34:56", timeMustParse("15:04:05", "12:34:56"), false},
 | 
				
			||||||
 | 
							{"123456", timeMustParse("15:04:05", "12:34:56"), false},
 | 
				
			||||||
 | 
							{"12:34", timeMustParse("15:04:05", "12:34:00"), false},
 | 
				
			||||||
 | 
							{"1234", timeMustParse("15:04:05", "12:34:00"), false},
 | 
				
			||||||
 | 
							{"1234:56", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12:3456", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12:34:96", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12:34:-00", time.Time{}, true},
 | 
				
			||||||
 | 
							{"123476", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12:-34", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12104", time.Time{}, true},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"00Z", timeMustParse("15 MST", "00 UTC"), false},
 | 
				
			||||||
 | 
							{"-2Z", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12:34:56Z", timeMustParse("15:04:05 MST", "12:34:56 UTC"), false},
 | 
				
			||||||
 | 
							{"12:34Z", timeMustParse("15:04:05 MST", "12:34:00 UTC"), false},
 | 
				
			||||||
 | 
							{"12:34:-00Z", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12104Z", time.Time{}, true},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"00+00", timeMustParse("15 MST", "00 UTC"), false},
 | 
				
			||||||
 | 
							{"-2+03", time.Time{}, true},
 | 
				
			||||||
 | 
							{"11:34:56+12", timeMustParse("15:04:05 MST", "23:34:56 UTC"), false},
 | 
				
			||||||
 | 
							{"12:34:14+10:30", timeMustParse("15:04:05 MST", "23:04:00 UTC"), false},
 | 
				
			||||||
 | 
							{"12:34:-00+10", time.Time{}, true},
 | 
				
			||||||
 | 
							{"1210+00:00", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12:10+0000", time.Time{}, true},
 | 
				
			||||||
 | 
							{"1210Z+00", time.Time{}, true},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{"00-00", time.Time{}, true},
 | 
				
			||||||
 | 
							{"-2-03", time.Time{}, true},
 | 
				
			||||||
 | 
							{"11:34:56-11", timeMustParse("15:04:05 MST", "00:34:56 UTC"), false},
 | 
				
			||||||
 | 
							{"12:34:14-10:30", timeMustParse("15:04:05 MST", "02:04:00 UTC"), false},
 | 
				
			||||||
 | 
							{"12:34:-00-10", time.Time{}, true},
 | 
				
			||||||
 | 
							{"1210-00:00", time.Time{}, true},
 | 
				
			||||||
 | 
							{"12:10-0000", time.Time{}, true},
 | 
				
			||||||
 | 
							{"1210Z-00", time.Time{}, true},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// boundary cases
 | 
				
			||||||
 | 
							{"-01", time.Time{}, true},
 | 
				
			||||||
 | 
							{"00", timeMustParse("15", "00"), false},
 | 
				
			||||||
 | 
							{"23", timeMustParse("15", "23"), false},
 | 
				
			||||||
 | 
							{"24", time.Time{}, true},
 | 
				
			||||||
 | 
							{"00:-01", time.Time{}, true},
 | 
				
			||||||
 | 
							{"00:00", timeMustParse("15:04", "00:00"), false},
 | 
				
			||||||
 | 
							{"00:59", timeMustParse("15:04", "00:59"), false},
 | 
				
			||||||
 | 
							{"00:60", time.Time{}, true},
 | 
				
			||||||
 | 
							{"01:02:-01", time.Time{}, true},
 | 
				
			||||||
 | 
							{"01:02:00", timeMustParse("15:04:05", "01:02:00"), false},
 | 
				
			||||||
 | 
							{"01:02:59", timeMustParse("15:04:05", "01:02:59"), false},
 | 
				
			||||||
 | 
							{"01:02:60", time.Time{}, true},
 | 
				
			||||||
 | 
							{"01:02:03-13", time.Time{}, true},
 | 
				
			||||||
 | 
							{"01:02:03-12", timeMustParse("15:04:05 MST", "01:02:03 UTC").Add(-12 * time.Hour), false},
 | 
				
			||||||
 | 
							{"01:02:03+14", timeMustParse("15:04:05 MST", "15:02:03 UTC"), false},
 | 
				
			||||||
 | 
							{"01:02:03+15", time.Time{}, true},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i, test := range cases {
 | 
				
			||||||
 | 
							curTime, err := parseTimeISO8601(test.input)
 | 
				
			||||||
 | 
							if test.err {
 | 
				
			||||||
 | 
								if err == nil {
 | 
				
			||||||
 | 
									t.Errorf("case %d [%s]: expected error, got: %v", i, test.input, curTime)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Errorf("case %d [%s]: unexpected error: %v", i, test.input, err)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if test.expected.Equal(curTime) {
 | 
				
			||||||
 | 
								t.Errorf("case %d [%s]: expected: %v got: %v", i, test.input, test.expected, curTime)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user