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