mirror of https://github.com/k3s-io/k3s
commit
6129d3d4eb
|
@ -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"]
|
|
@ -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)
|
|
@ -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)
|
||||
```
|
||||
|
||||
|
||||
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/contrib/diurnal/README.md?pixel)]()
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue