2015-07-10 02:11:21 +00:00
|
|
|
/*
|
|
|
|
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"
|
|
|
|
|
2015-08-05 22:03:47 +00:00
|
|
|
kclient "k8s.io/kubernetes/pkg/client"
|
|
|
|
"k8s.io/kubernetes/pkg/labels"
|
2015-07-10 02:11:21 +00:00
|
|
|
|
|
|
|
"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)
|
|
|
|
}
|
|
|
|
}
|