<commit_before>package main

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"github.com/nsf/termbox-go"
	"github.com/pmylund/sortutil"
	"io/ioutil"
	"log"
	"net"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"
)

var VERSION = "0.2.0"

type statusLine struct {
	name    string
	queued  string
	running string
	workers string
}

type fieldWidths struct {
	name    int
	queued  int
	running int
	workers int
}

type gearmanStatus struct {
	statusLines    []statusLine
	statusLineDims fieldWidths
}

type sortType struct {
	field     string
	ascending bool
}

var columnNames = statusLine{"Job name", "Queued", "Running", "Workers"}
var sortFields = []string{"name", "queued", "running", "workers"}
var sortOrder = sortType{}

type byQueued []statusLine

func (a byQueued) Len() int      { return len(a) }
func (a byQueued) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byQueued) Less(i, j int) bool {
	inti, _ := strconv.Atoi(a[i].queued)
	intj, _ := strconv.Atoi(a[j].queued)
	return inti < intj
}

type byRunning []statusLine

func (a byRunning) Len() int      { return len(a) }
func (a byRunning) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byRunning) Less(i, j int) bool {
	inti, _ := strconv.Atoi(a[i].running)
	intj, _ := strconv.Atoi(a[j].running)
	return inti < intj
}

type byWorkers []statusLine

func (a byWorkers) Len() int      { return len(a) }
func (a byWorkers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byWorkers) Less(i, j int) bool {
	inti, _ := strconv.Atoi(a[i].workers)
	intj, _ := strconv.Atoi(a[j].workers)
	return inti < intj
}

func fieldWidthsFactory(line statusLine) fieldWidths {
	return fieldWidths{
		len(line.name),
		len(line.queued),
		len(line.running),
		len(line.workers)}
}

func fatal(msg string) {
	termbox.Close()
	log.Println("Exiting: ", msg)
	fmt.Println(msg)
	os.Exit(2)
}

func statusLineFromString(line string) (statusLine, error) {
	parts := strings.Fields(line)
	if len(parts) != 4 {
		return statusLine{}, errors.New("Wrong number of fields")
	}
	return statusLine{parts[0], parts[1], parts[2], parts[3]}, nil
}

func max(a, b int) int {
	if a >= b {
		return a
	}
	return b
}

func initialiseFilters() (include, exclude []string) {
	if len(queueNameInclude) > 0 {
		queueNameInclude = strings.ToLower(queueNameInclude)
		include = strings.Split(queueNameInclude, ",")
	}
	if len(queueNameExclude) > 0 {
		queueNameExclude = strings.ToLower(queueNameExclude)
		exclude = strings.Split(queueNameExclude, ",")
	}
	log.Printf("Including: %d %v", len(include), include)
	log.Printf("Excluding: %d %v", len(exclude), exclude)
	return
}

func includeLine(line statusLine, includeTerms, excludeTerms []string) bool {
	if len(includeTerms) == 0 && len(excludeTerms) == 0 {
		return true
	}
	name := strings.ToLower(line.name)
	for _, excludeTerm := range excludeTerms {
		if strings.Contains(name, excludeTerm) {
			return false
		}
	}
	for _, includeTerm := range includeTerms {
		if strings.Contains(name, includeTerm) {
			return true
		}
	}
	if len(includeTerms) == 0 {
		return true
	}
	return false
}

func getStatus(c chan gearmanStatus) {
	log.Println("Connecting to gearman")
	const waitTime = 1000 * time.Millisecond
	gearman, err := net.DialTimeout("tcp", gearmanHost+":"+gearmanPort, 1*time.Second)
	if err != nil {
		fatal("Couldn't connect to gearman on " + gearmanHost + ":" + gearmanPort)
		return
	}
	defer gearman.Close()
	gearmanStream := bufio.NewReader(gearman)
	includeTerms, excludeTerms := initialiseFilters()
	for {
		log.Println("Getting status")
		start := time.Now()
		widths := fieldWidthsFactory(columnNames)
		gearman.Write([]byte("status\n"))
		statusLines := make([]statusLine, 0)
		for {
			line, err := gearmanStream.ReadString('\n')
			if err != nil {
				break
			}
			if line == ".\n" {
				break
			}
			statusLine, ok := statusLineFromString(line)
			if ok != nil {
				continue
			}
			if !showAll && statusLine.queued == "0" &&
				statusLine.running == "0" && statusLine.workers == "0" {
				continue
			}
			if !includeLine(statusLine, includeTerms, excludeTerms) {
				continue
			}
			widths.name = max(len(statusLine.name)+1, widths.name)
			widths.queued = max(len(statusLine.queued)+1, widths.queued)
			widths.running = max(len(statusLine.running)+1, widths.running)
			widths.workers = max(len(statusLine.workers), widths.workers)
			statusLines = append(statusLines, statusLine)
		}
		c <- gearmanStatus{statusLines, widths}
		duration := time.Since(start)
		time.Sleep(waitTime - duration)
	}
}

func print_tb(x, y int, fg, bg termbox.Attribute, msg string) {
	for _, c := range msg {
		termbox.SetCell(x, y, c, fg, bg)
		x++
	}
}

func printf_tb(x, y int, fg, bg termbox.Attribute, format string, args ...interface{}) {
	s := fmt.Sprintf(format, args...)
	print_tb(x, y, fg, bg, s)
}

func printField(x, y, fieldWidth int, value string, bold bool) int {
	intValue, ok := strconv.Atoi(value)
	if ok == nil {
		value = fmt.Sprintf("%"+strconv.Itoa(fieldWidth)+"d", intValue) + " "
	}
	fg := termbox.ColorDefault
	if bold {
		fg |= termbox.AttrBold
	}
	print_tb(x, y, fg, termbox.ColorDefault, value)
	return x + fieldWidth + 1
}

func printLine(y int, widths fieldWidths, line statusLine, bold bool) {
	x := 0
	if len(line.name) > widths.name {
		line.name = line.name[:widths.name]
	}
	x = printField(x, y, widths.name, line.name, bold)
	x = printField(x, y, widths.queued, line.queued, bold)
	x = printField(x, y, widths.running, line.running, bold)
	x = printField(x, y, widths.workers, line.workers, bold)
}

func sortStatusLines(gearmanStatus *gearmanStatus) {
	switch sortOrder.field {
	case "name":
		if sortOrder.ascending {
			sortutil.CiAscByField(gearmanStatus.statusLines, sortOrder.field)
		} else {
			sortutil.CiDescByField(gearmanStatus.statusLines, sortOrder.field)
		}
	case "queued":
		if sortOrder.ascending {
			sort.Sort(byQueued(gearmanStatus.statusLines))
		} else {
			sortutil.SortReverseInterface(byQueued(gearmanStatus.statusLines))
		}
	case "running":
		if sortOrder.ascending {
			sort.Sort(byRunning(gearmanStatus.statusLines))
		} else {
			sortutil.SortReverseInterface(byRunning(gearmanStatus.statusLines))
		}
	case "workers":
		if sortOrder.ascending {
			sort.Sort(byWorkers(gearmanStatus.statusLines))
		} else {
			sortutil.SortReverseInterface(byWorkers(gearmanStatus.statusLines))
		}
	}
}

func drawStatusLine(gearmanStatus gearmanStatus, position, y, width int) {
	progress := fmt.Sprintf("%d/%d", y+position-1, len(gearmanStatus.statusLines))
	print_tb(width-len(progress), y, termbox.ColorDefault, termbox.ColorDefault, progress)
}

func drawStatus(gearmanStatus gearmanStatus, position, height, width int) {
	sortStatusLines(&gearmanStatus)
	lines := gearmanStatus.statusLines
	if len(lines) > 0 {
		log.Print("First line: ", lines[0])
		log.Print("Last line: ", lines[len(lines)-1])
	} else {
		log.Print("No lines")
	}
	termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
	y := 0
	printY := 0

	widths := gearmanStatus.statusLineDims
	totalWidth := widths.name + widths.queued + widths.running + widths.workers + 3
	widths.name += width - totalWidth
	printLine(0, widths, columnNames, true)
	printY = y - position + 1
	for _, line := range lines {
		if printY > height-1 {
			break
		}
		if printY < 1 {
			printY++
			continue
		}
		printLine(printY, widths, line, false)
		printY++
	}
	drawStatusLine(gearmanStatus, position, height, width)
	termbox.Flush()
}

func sortEvent(index rune) {
	sortIndex, _ := strconv.Atoi(string(index))
	sortField := sortFields[sortIndex-1]
	if sortOrder.field == sortField {
		sortOrder.ascending = !sortOrder.ascending
	} else if sortIndex == 1 {
		sortOrder.ascending = true
	} else {
		sortOrder.ascending = false
	}
	sortOrder.field = sortField
}

func handleEvents(direction chan int, resized chan termbox.Event, doRedraw chan bool, quit chan bool) {
	for {
		event := termbox.PollEvent()
		log.Println("Recieved event: ", event)
		switch event.Type {
		case termbox.EventKey:
			switch event.Ch {
			case 'q':
				quit <- true
			case '1', '2', '3', '4':
				sortEvent(event.Ch)
				doRedraw <- true
			default:
				switch event.Key {
				case termbox.KeyCtrlC:
					quit <- true
				case termbox.KeyArrowUp:
					direction <- -1
				case termbox.KeyArrowDown:
					direction <- 1
				}
			}
		case termbox.EventResize:
			resized <- event
		}
	}
}

func calculatePosition(currentPosition int, direction int, gearmanStatus gearmanStatus) (int, bool) {
	_, height := getDisplayArea()
	scrolledToBottom := len(gearmanStatus.statusLines) < (currentPosition + height)
	scrolledToTop := currentPosition == 0
	if (direction < 0 && !scrolledToTop) || (direction > 0 && !scrolledToBottom) {
		log.Println("Moving")
		return currentPosition + direction, true
	}
	return currentPosition, false
}

func getDisplayArea() (width, height int) {
	width, height = termbox.Size()
	height--
	return
}

func scrollOutput(direction int, scroll chan int, position int, currentGearmanStatus gearmanStatus) int {
	positionUpdated := false
	log.Println("Scrolling")
	for {
		width, height := getDisplayArea()
		position, positionUpdated = calculatePosition(position, direction, currentGearmanStatus)
		if positionUpdated {
			drawStatus(currentGearmanStatus, position, height, width)
		}
		select {
		case direction = <-scroll:
			log.Println("Collating scrolling")
		default:
			return position
		}
	}
	panic("unreachable")
}

func initLogging() *os.File {
	f, err := os.OpenFile("/tmp/gearman_gtop.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		panic(err)
	}
	log.SetOutput(f)
	return f
}

var doLogging bool
var showAll bool
var gearmanHost string
var gearmanPort string
var initialSortIndex string
var queueNameInclude string
var queueNameExclude string

func init() {
	logDefault := false
	logUsage := "Log debug to /tmp/gearman_gtop.log"
	flag.BoolVar(&doLogging, "log", logDefault, logUsage)
	flag.BoolVar(&doLogging, "l", logDefault, logUsage+" (shorthand)")
	allDefault := false
	allUsage := "Show all queues, even if the have no workers or jobs"
	flag.BoolVar(&showAll, "all", allDefault, allUsage)
	flag.BoolVar(&showAll, "a", allDefault, allUsage+" (shorthand)")
	hostDefault := "localhost"
	hostUsage := "Gearmand host to connect to"
	flag.StringVar(&gearmanHost, "host", hostDefault, hostUsage)
	flag.StringVar(&gearmanHost, "h", hostDefault, hostUsage+" (shorthand)")
	portDefault := "4730"
	portUsage := "Gearmand port to connect to"
	flag.StringVar(&gearmanPort, "port", portDefault, portUsage)
	flag.StringVar(&gearmanPort, "p", portDefault, portUsage+" (shorthand)")
	flag.StringVar(&initialSortIndex, "sort", "1", "Index of the column to sort by")
	flag.StringVar(&queueNameInclude, "filterInclude", "", "Include queues containing this string. Can provide multiple separated by commas.")
	flag.StringVar(&queueNameExclude, "filterExclude", "", "Exclude queues containing this string. Can provide multiple separated by commas.")
}

func redraw(currentGearmanStatus gearmanStatus, position int) {
	width, height := getDisplayArea()
	drawStatus(currentGearmanStatus, position, height, width)
}

func main() {
	flag.Parse()
	if doLogging {
		defer (initLogging()).Close()
	} else {
		log.SetOutput(ioutil.Discard)
	}
	sortEvent(rune(initialSortIndex[0]))

	var currentGearmanStatus gearmanStatus
	position := 0
	status := make(chan gearmanStatus)
	doRedraw := make(chan bool)
	quit := make(chan bool)
	resized := make(chan termbox.Event)
	scroll := make(chan int, 3)

	err := termbox.Init()
	if err != nil {
		fatal(err.Error())
	}
	defer termbox.Close()
	termbox.SetInputMode(termbox.InputEsc)

	go getStatus(status)
	go handleEvents(scroll, resized, doRedraw, quit)
	for {
		select {
		case currentGearmanStatus = <-status:
			log.Println("Redrawing for updated status")
			redraw(currentGearmanStatus, position)
		case ev := <-resized:
			log.Println("Redrawing for resize")
			drawStatus(currentGearmanStatus, position, ev.Height, ev.Width)
		case direction := <-scroll:
			position = scrollOutput(direction, scroll, position, currentGearmanStatus)
		case <-doRedraw:
			redraw(currentGearmanStatus, position)
		case <-quit:
			log.Println("Exiting")
			return
		}
	}
}
<commit_msg>Extract gearman status retrieval into separate package<commit_after>package main

import (
	"flag"
	"fmt"
	"github.com/nickpeirson/gearadmin"
	"github.com/nsf/termbox-go"
	"io/ioutil"
	"log"
	"os"
	"strconv"
	"strings"
	"time"
)

var VERSION = "0.2.0"
var pollInterval = 1 * time.Second
var status = make(chan gearmanStatus)
var doRedraw = make(chan bool)
var quit = make(chan bool)
var resized = make(chan termbox.Event)
var scroll = make(chan int, 3)

type gearmanStatus struct {
	statusLines gearadmin.StatusLines
	fieldWidths fieldWidths
}

type fieldWidths struct {
	name    int
	queued  int
	running int
	workers int
	total   int
}

var columnNames = gearadmin.StatusLine{"Job name", "Queued", "Running", "Workers"}

func fieldWidthsFactory(status gearadmin.StatusLines) (widths fieldWidths) {
	widths = fieldWidths{
		len(columnNames.Name),
		len(columnNames.Queued),
		len(columnNames.Running),
		len(columnNames.Workers),
		0,
	}
	for _, statusLine := range status {
		widths.name = max(len(statusLine.Name)+1, widths.name)
		widths.queued = max(len(statusLine.Queued)+1, widths.queued)
		widths.running = max(len(statusLine.Running)+1, widths.running)
		widths.workers = max(len(statusLine.Workers), widths.workers)
	}
	widths.total = widths.name + widths.queued + widths.running + widths.workers + 3
	return
}

var doLogging bool
var showAll bool
var gearmanHost string
var gearmanPort string
var initialSortIndex string
var queueNameInclude string
var queueNameExclude string

func init() {
	logDefault := false
	logUsage := "Log debug to /tmp/gearman_gtop.log"
	flag.BoolVar(&doLogging, "log", logDefault, logUsage)
	flag.BoolVar(&doLogging, "l", logDefault, logUsage+" (shorthand)")
	allDefault := false
	allUsage := "Show all queues, even if the have no workers or jobs"
	flag.BoolVar(&showAll, "all", allDefault, allUsage)
	flag.BoolVar(&showAll, "a", allDefault, allUsage+" (shorthand)")
	hostDefault := "localhost"
	hostUsage := "Gearmand host to connect to"
	flag.StringVar(&gearmanHost, "host", hostDefault, hostUsage)
	flag.StringVar(&gearmanHost, "h", hostDefault, hostUsage+" (shorthand)")
	portDefault := "4730"
	portUsage := "Gearmand port to connect to"
	flag.StringVar(&gearmanPort, "port", portDefault, portUsage)
	flag.StringVar(&gearmanPort, "p", portDefault, portUsage+" (shorthand)")
	flag.StringVar(&initialSortIndex, "sort", "1", "Index of the column to sort by")
	flag.StringVar(&queueNameInclude, "filterInclude", "", "Include queues containing this string. Can provide multiple separated by commas.")
	flag.StringVar(&queueNameExclude, "filterExclude", "", "Exclude queues containing this string. Can provide multiple separated by commas.")
}

func main() {
	flag.Parse()
	if doLogging {
		defer (initLogging()).Close()
	} else {
		log.SetOutput(ioutil.Discard)
	}
	sortEvent(rune(initialSortIndex[0]))

	var currentGearmanStatus gearmanStatus
	position := 0

	err := termbox.Init()
	if err != nil {
		fatal(err.Error())
	}
	defer termbox.Close()
	termbox.SetInputMode(termbox.InputEsc)

	go getStatus()
	go handleEvents()
	for {
		select {
		case currentGearmanStatus = <-status:
			log.Println("Redrawing for updated status")
			redraw(currentGearmanStatus, position)
		case ev := <-resized:
			log.Println("Redrawing for resize")
			drawStatus(currentGearmanStatus, position, ev.Height, ev.Width)
		case direction := <-scroll:
			position = scrollOutput(direction, scroll, position, currentGearmanStatus)
		case <-doRedraw:
			redraw(currentGearmanStatus, position)
		case <-quit:
			log.Println("Exiting")
			return
		}
	}
}

func getStatus() {
	log.Println("Connecting to gearman")
	gearadminClient := gearadmin.New(gearmanHost, gearmanPort)
	defer gearadminClient.Close()
	responseFilter := statusFilter(initialiseFilters())
	for {
		log.Println("Getting status")
		start := time.Now()
		statusLines, err := gearadminClient.StatusFiltered(responseFilter)
		if err != nil {
			fatal("Couldn't get gearman status from " + gearmanHost + ":" + gearmanPort + " (Error: " + err.Error() + ")")
			return
		}
		status <- gearmanStatus{statusLines, fieldWidthsFactory(statusLines)}
		duration := time.Since(start)
		time.Sleep(pollInterval - duration)
	}
}

func handleEvents() {
	for {
		event := termbox.PollEvent()
		log.Println("Recieved event: ", event)
		switch event.Type {
		case termbox.EventKey:
			switch event.Ch {
			case 'q':
				quit <- true
			case '1', '2', '3', '4':
				sortEvent(event.Ch)
				doRedraw <- true
			default:
				switch event.Key {
				case termbox.KeyCtrlC:
					quit <- true
				case termbox.KeyArrowUp:
					scroll <- -1
				case termbox.KeyArrowDown:
					scroll <- 1
				}
			}
		case termbox.EventResize:
			resized <- event
		}
	}
}

func drawStatus(gearmanStatus gearmanStatus, position, height, width int) {
	gearmanStatus.statusLines.Sort(sortFields[sortOrder.field], sortOrder.ascending)
	lines := gearmanStatus.statusLines

	widths := gearmanStatus.fieldWidths
	widths.name += width - widths.total

	termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)

	headerHeight := drawHeader(widths)
	footerHeight := drawFooter(gearmanStatus, position, height, width)
	printY := headerHeight
	printLines := lines[position:]
	if len(printLines) > height-footerHeight {
		printLines = printLines[:height-footerHeight]
	}
	for _, line := range printLines {
		drawLine(printY, widths, line, false)
		printY++
	}

	termbox.Flush()
}

func drawHeader(widths fieldWidths) int {
	drawLine(0, widths, columnNames, true)
	return 1
}

func drawLine(y int, widths fieldWidths, line gearadmin.StatusLine, bold bool) {
	x := 0
	if len(line.Name) > widths.name {
		line.Name = line.Name[:widths.name]
	}
	x = drawField(x, y, widths.name, line.Name, bold)
	x = drawField(x, y, widths.queued, line.Queued, bold)
	x = drawField(x, y, widths.running, line.Running, bold)
	x = drawField(x, y, widths.workers, line.Workers, bold)
}

func drawField(x, y, fieldWidth int, value string, bold bool) int {
	intValue, ok := strconv.Atoi(value)
	if ok == nil {
		value = fmt.Sprintf("%"+strconv.Itoa(fieldWidth)+"d", intValue) + " "
	}
	fg := termbox.ColorDefault
	if bold {
		fg |= termbox.AttrBold
	}
	print_tb(x, y, fg, termbox.ColorDefault, value)
	return x + fieldWidth + 1
}

func drawFooter(gearmanStatus gearmanStatus, position, y, width int) int {
	displayedLines := y + position - 1
	totalLines := len(gearmanStatus.statusLines)
	progress := fmt.Sprintf("%d/%d", min(displayedLines, totalLines), totalLines)
	print_tb(width-len(progress), y, termbox.ColorDefault, termbox.ColorDefault, progress)
	//	progress := fmt.Sprintf("%d/%d", y+position-1, len(gearmanStatus.statusLines))
	//	print_tb(width-len(progress), y, termbox.ColorDefault, termbox.ColorDefault, progress)
	return 1
}

func scrollOutput(direction int, scroll chan int, position int, currentGearmanStatus gearmanStatus) int {
	positionUpdated := false
	log.Println("Scrolling")
	for {
		width, height := getDisplayArea()
		position, positionUpdated = calculatePosition(position, direction, currentGearmanStatus)
		if positionUpdated {
			drawStatus(currentGearmanStatus, position, height, width)
		}
		select {
		case direction = <-scroll:
			log.Println("Collating scrolling")
		default:
			return position
		}
	}
	panic("unreachable")
}

func calculatePosition(currentPosition int, direction int, gearmanStatus gearmanStatus) (int, bool) {
	_, height := getDisplayArea()
	scrolledToBottom := len(gearmanStatus.statusLines) < (currentPosition + height)
	scrolledToTop := currentPosition == 0
	if (direction < 0 && !scrolledToTop) || (direction > 0 && !scrolledToBottom) {
		log.Println("Moving")
		return currentPosition + direction, true
	}
	return currentPosition, false
}

func statusFilter(includeTerms, excludeTerms []string) gearadmin.StatusLineFilter {
	return func(line gearadmin.StatusLine) bool {
		if !showAll && line.Queued == "0" &&
			line.Running == "0" && line.Workers == "0" {
			return false
		}
		if len(includeTerms) == 0 && len(excludeTerms) == 0 {
			return true
		}
		name := strings.ToLower(line.Name)
		for _, excludeTerm := range excludeTerms {
			if strings.Contains(name, excludeTerm) {
				return false
			}
		}
		for _, includeTerm := range includeTerms {
			if strings.Contains(name, includeTerm) {
				return true
			}
		}
		return len(includeTerms) == 0
	}
}

func initialiseFilters() (include, exclude []string) {
	if len(queueNameInclude) > 0 {
		queueNameInclude = strings.ToLower(queueNameInclude)
		include = strings.Split(queueNameInclude, ",")
	}
	if len(queueNameExclude) > 0 {
		queueNameExclude = strings.ToLower(queueNameExclude)
		exclude = strings.Split(queueNameExclude, ",")
	}
	log.Printf("Including: %d %v", len(include), include)
	log.Printf("Excluding: %d %v", len(exclude), exclude)
	return
}

type sortType struct {
	field     rune
	ascending bool
}

var sortFields = map[rune]gearadmin.By{
	'1': gearadmin.ByName,
	'2': gearadmin.ByQueued,
	'3': gearadmin.ByRunning,
	'4': gearadmin.ByWorkers,
}
var sortOrder = sortType{}

func sortEvent(index rune) {
	if sortOrder.field == index {
		sortOrder.ascending = !sortOrder.ascending
	} else if index == '1' {
		sortOrder.ascending = true
	} else {
		sortOrder.ascending = false
	}
	sortOrder.field = index
}

func getDisplayArea() (width, height int) {
	width, height = termbox.Size()
	height--
	return
}

func redraw(currentGearmanStatus gearmanStatus, position int) {
	width, height := getDisplayArea()
	drawStatus(currentGearmanStatus, position, height, width)
}

func initLogging() *os.File {
	f, err := os.OpenFile("/tmp/gearman_gtop.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		panic(err)
	}
	log.SetOutput(f)
	return f
}

func print_tb(x, y int, fg, bg termbox.Attribute, msg string) {
	for _, c := range msg {
		termbox.SetCell(x, y, c, fg, bg)
		x++
	}
}

func fatal(msg string) {
	termbox.Close()
	log.Println("Exiting: ", msg)
	fmt.Println(msg)
	os.Exit(2)
}

func max(a, b int) int {
	if a >= b {
		return a
	}
	return b
}

func min(a, b int) int {
	if a <= b {
		return a
	}
	return b
}
<|endoftext|>


<commit_before>package translation

import (
	"os"
	"log"
	"fmt"
	"math"
	"encoding/json"

	"github.com/thomaspeugeot/tkv/barnes-hut"
	"github.com/thomaspeugeot/tkv/quadtree"
	"github.com/thomaspeugeot/tkv/grump"

)

type Country struct {
	grump.Country

	NbBodies int // nb of bodies according to the filename

	bodiesOrig * []quadtree.Body // original bodies position in the quatree
	bodiesSpread * []quadtree.Body // bodies position in the quatree after the spread simulation
	VilCoordinates [][]int
	Step int // step when the simulation stopped

	villages [][]Village
}

type Point struct {
	X, Y float64
}

func MakePoint(x float64, y float64) Point {
	return Point{X: x, Y: y}
}

type PointList []Point

type BodySetChoice string
const (
	ORIGINAL_CONFIGURATION = "ORIGINAL_CONFIGURATION"
	SPREAD_CONFIGURATION = "SPREAD_CONFIGURATION"
)

// number of village per X or Y axis. For 10 000 villages, this number is 100
// this value can be set interactively during the run
var nbVillagePerAxe int = 100
var numberOfVillagePerAxe float64 = 100.0

// init variables
func (country * Country) Init() {

	// unserialize from conf-<country trigram>.coord
	// store step because the unseralize set it to a wrong value
	step := country.Step
	country.Unserialize()
	country.Step = step

	Info.Printf("Init after Unserialize name %s", country.Name)
	Info.Printf("Init after Unserialize step %d", country.Step)

	country.LoadConfig( true ) // load config at the end  of the simulation
	country.LoadConfig( false ) // load config at the start of the simulation

	// init village array
	country.villages = make( [][]Village, nbVillagePerAxe )
	for x,_  := range country.villages {
		country.villages[x] = make([]Village, nbVillagePerAxe)
	}

	country.VilCoordinates = make( [][]int, country.NbBodies)
	for idx, _ := range country.VilCoordinates {
		country.VilCoordinates[idx] = make( []int, 2)
	}

	country.ComputeBaryCenters()

}

// load configuration from filename into country
// check that it matches the
func (country * Country) LoadConfig( isOriginal bool) bool {

	Info.Printf( "Load Config begin : Country is %s, step %d isOriginal %t", country.Name, country.Step, isOriginal)

	// computing the file name from the step
	step := 0

	// if isOrignal load the file with the step number 0, else use spread
	if ! isOriginal { step = country.Step }

	filename := fmt.Sprintf( barnes_hut.CountryBodiesNamePattern, country.Name, country.NbBodies, step)
	Info.Printf( "LoadConfig original %t file %s for country %s at step %d", isOriginal, filename, country.Name, step)

	file, err := os.Open(filename)
	if( err != nil) {
		log.Fatal(err)
		return false
	}

	jsonParser := json.NewDecoder(file)

	bodies := (make([]quadtree.Body, 0))
	if isOriginal {
		country.bodiesOrig = & bodies
		if err = jsonParser.Decode( country.bodiesOrig); err != nil {
			log.Fatal( fmt.Sprintf( "parsing config file %s", err.Error()))
		}
		Info.Printf( "nb item parsed in file for orig %d\n", len( *country.bodiesOrig))
	} else {
		country.bodiesSpread = & bodies
		if err = jsonParser.Decode( country.bodiesSpread); err != nil {
			log.Fatal( fmt.Sprintf( "parsing config file %s", err.Error()))
		}
		Info.Printf( "nb item parsed in file for spread %d\n", len( *country.bodiesSpread))
	}

	file.Close()

	Info.Printf( "Load Config end : Country is %s, step %d", country.Name, country.Step)

	return true
}

// compute villages barycenters
func (country * Country) ComputeBaryCenters() {
	Info.Printf("ComputeBaryCenters begins for country %s", country.Name)

	// parse bodiesSpread to compute bary centers
	// use bodiesOrig to compute bary centers
	for index,b := range *country.bodiesSpread {

		// compute village coordinate (from 0 to nbVillagePerAxe-1)
		villageX := int( math.Floor(float64( nbVillagePerAxe) * b.X))
		villageY := int( math.Floor(float64( nbVillagePerAxe) * b.Y))

		Trace.Printf("Adding body index %d to village %d %d", index, villageX, villageY)

		// add body (original) to the barycenter of the village
		bOrig := (*country.bodiesOrig)[index]
		country.villages[villageX][villageY].addBody( bOrig)

		country.VilCoordinates[index][0] = villageX
		country.VilCoordinates[index][1] = villageY
	}
}

// given lat, lng
// return xClosest, yClosest, distance, latClosest, lngClosest of the closest body within the country (original position)
func (country * Country) ClosestBodyInOriginalPosition( lat, lng float64) (xRelClosest, yRelClosest, distance, latClosest, lngClosest, xSpread, ySpread float64, closestIndex int) {

	// compute relative coordinates within the square
	xRel, yRel := country.LatLng2XY( lat, lng)

	// parse all bodies and get closest body
	closestIndex = -1
	minDistance := 1000000000.0 // we start from away
	for index,b := range *country.bodiesOrig {
		distanceX := b.X - xRel
		distanceY := b.Y - yRel
		distance := math.Sqrt( (distanceX*distanceX) + (distanceY*distanceY))

		if( distance < minDistance ) {
			closestIndex = index
			minDistance = distance
		}
	}

	xRelClosest = (*country.bodiesOrig)[closestIndex].X
	yRelClosest = (*country.bodiesOrig)[closestIndex].Y

	latOptimClosest, lngOptimClosest := country.XY2LatLng( xRelClosest, yRelClosest)

	Info.Printf( "ClosestBodyInOriginalPosition %f %f relative to country %f %f", lat, lng, xRel, yRel)
	Info.Printf( "ClosestBodyInOriginalPosition rel closest %f %f lat lng closest %f %f", xRelClosest, yRelClosest, latOptimClosest, lngOptimClosest)

	// compute x, y in spread bodies
	xSpread = (*country.bodiesSpread)[closestIndex].X
	ySpread = (*country.bodiesSpread)[closestIndex].Y

	Info.Printf( "ClosestBodyInOriginalPosition village %f %f index %d", xSpread, ySpread, closestIndex)

	return xRelClosest, yRelClosest, minDistance, latOptimClosest, lngOptimClosest, xSpread, ySpread, closestIndex
}

func (country * Country) XYSpreadToLatLngOrig( x, y float64) (lat, lng float64) {

	Info.Printf( "XYSpreadToLatLngOrig input x %f y %f", x, y)

	// parse all bodies and get closest body
	closestIndex := -1
	minDistance := 1000000000.0 // we start from away
	for index,b := range *country.bodiesSpread {
		distanceX := b.X - x
		distanceY := b.Y - y
		distance := math.Sqrt( (distanceX*distanceX) + (distanceY*distanceY))

		if( distance < minDistance ) {
			closestIndex = index
			minDistance = distance
		}
	}

	xRelClosest := (*country.bodiesOrig)[closestIndex].X
	yRelClosest := (*country.bodiesOrig)[closestIndex].Y
	latOptimClosest, lngOptimClosest := country.XY2LatLng( xRelClosest, yRelClosest)
	Info.Printf("XYSpreadToLatLngOrig target x %f y %f index %d distance %f", xRelClosest, yRelClosest, closestIndex, minDistance)

	Info.Printf("XYSpreadToLatLngOrig target lat %f lng %f", latOptimClosest, lngOptimClosest)

	return latOptimClosest, lngOptimClosest
}

// get the bodies of a village from x, y spread coordinates
func (country * Country) XYSpreadToTerritoryBorder( x, y float64) PointList {

	Info.Printf( "XYSpreadToTerritoryBorder Country %s x %f y %f", country.Name, x, y)

	points := make(PointList, 0)

	// compute village min & max coord
	xMinVillage := float64( int( x*numberOfVillagePerAxe))/numberOfVillagePerAxe
	xMaxVillage := float64( int( x*numberOfVillagePerAxe + 1.0))/numberOfVillagePerAxe
	yMinVillage := float64( int( y*numberOfVillagePerAxe))/numberOfVillagePerAxe
	yMaxVillage := float64( int( y*numberOfVillagePerAxe + 1.0))/numberOfVillagePerAxe

	Info.Printf( "XYSpreadToLatLngOrig input village Min x %f Max x %f", xMinVillage, xMaxVillage)

	// parse all bodies and get closest body
	for index,b := range *country.bodiesSpread {
		if (xMinVillage <= b.X) && (b.X < xMaxVillage) && (yMinVillage <= b.Y) && (b.Y < yMaxVillage) {

			xRelClosest := (*country.bodiesOrig)[index].X
			yRelClosest := (*country.bodiesOrig)[index].Y
			latOptimClosest, lngOptimClosest := country.XY2LatLng( xRelClosest, yRelClosest)

			points = append(points, MakePoint(latOptimClosest, lngOptimClosest))
		}
	}

	Info.Printf( "XYSpreadToTerritoryBorder Country %s nb bodies in village %d", country.Name, len(points))
	return points
}

// given x, y of a point, return the border in the country
func (country * Country) LatLngToTerritoryBorder( lat, lng float64) PointList {

	Info.Printf( "")
	Info.Printf( "LatLngToTerritoryBorder country %s input lat %f lng %f", country.Name, lat, lng)

	// from input lat, lng, get the xSpread, ySpread
	_, _, _, _, _, xSpread, ySpread, _ := country.ClosestBodyInOriginalPosition(lat, lng)
	Info.Printf( "LatLngToTerritoryBorder country %s input xSpread %f ySpread %f", country.Name, xSpread, ySpread)

	points := country.XYSpreadToTerritoryBorder( xSpread, ySpread)

	Info.Printf( "LatLngToTerritoryBorder nb of border points %d", len(points))
	Info.Printf( "")

	return points
}
<commit_msg>more trace<commit_after>package translation

import (
	"os"
	"log"
	"fmt"
	"math"
	"encoding/json"

	"github.com/thomaspeugeot/tkv/barnes-hut"
	"github.com/thomaspeugeot/tkv/quadtree"
	"github.com/thomaspeugeot/tkv/grump"

)

type Country struct {
	grump.Country

	NbBodies int // nb of bodies according to the filename

	bodiesOrig * []quadtree.Body // original bodies position in the quatree
	bodiesSpread * []quadtree.Body // bodies position in the quatree after the spread simulation
	VilCoordinates [][]int
	Step int // step when the simulation stopped

	villages [][]Village
}

type Point struct {
	X, Y float64
}

func MakePoint(x float64, y float64) Point {
	return Point{X: x, Y: y}
}

type PointList []Point

type BodySetChoice string
const (
	ORIGINAL_CONFIGURATION = "ORIGINAL_CONFIGURATION"
	SPREAD_CONFIGURATION = "SPREAD_CONFIGURATION"
)

// number of village per X or Y axis. For 10 000 villages, this number is 100
// this value can be set interactively during the run
var nbVillagePerAxe int = 100
var numberOfVillagePerAxe float64 = 100.0

// init variables
func (country * Country) Init() {

	// unserialize from conf-<country trigram>.coord
	// store step because the unseralize set it to a wrong value
	step := country.Step
	country.Unserialize()
	country.Step = step

	Info.Printf("Init after Unserialize name %s", country.Name)
	Info.Printf("Init after Unserialize step %d", country.Step)

	country.LoadConfig( true ) // load config at the end  of the simulation
	country.LoadConfig( false ) // load config at the start of the simulation

	// init village array
	country.villages = make( [][]Village, nbVillagePerAxe )
	for x,_  := range country.villages {
		country.villages[x] = make([]Village, nbVillagePerAxe)
	}

	country.VilCoordinates = make( [][]int, country.NbBodies)
	for idx, _ := range country.VilCoordinates {
		country.VilCoordinates[idx] = make( []int, 2)
	}

	country.ComputeBaryCenters()

}

// load configuration from filename into country
// check that it matches the
func (country * Country) LoadConfig( isOriginal bool) bool {

	Info.Printf( "Load Config begin : Country is %s, step %d isOriginal %t", country.Name, country.Step, isOriginal)

	// computing the file name from the step
	step := 0

	// if isOrignal load the file with the step number 0, else use spread
	if ! isOriginal { step = country.Step }

	filename := fmt.Sprintf( barnes_hut.CountryBodiesNamePattern, country.Name, country.NbBodies, step)
	Info.Printf( "LoadConfig original %t file %s for country %s at step %d", isOriginal, filename, country.Name, step)

	file, err := os.Open(filename)
	if( err != nil) {
		log.Fatal(err)
		return false
	}

	jsonParser := json.NewDecoder(file)

	bodies := (make([]quadtree.Body, 0))
	if isOriginal {
		country.bodiesOrig = & bodies
		if err = jsonParser.Decode( country.bodiesOrig); err != nil {
			log.Fatal( fmt.Sprintf( "parsing config file %s", err.Error()))
		}
		Info.Printf( "nb item parsed in file for orig %d\n", len( *country.bodiesOrig))
	} else {
		country.bodiesSpread = & bodies
		if err = jsonParser.Decode( country.bodiesSpread); err != nil {
			log.Fatal( fmt.Sprintf( "parsing config file %s", err.Error()))
		}
		Info.Printf( "nb item parsed in file for spread %d\n", len( *country.bodiesSpread))
	}

	file.Close()

	Info.Printf( "Load Config end : Country is %s, step %d", country.Name, country.Step)

	return true
}

// compute villages barycenters
func (country * Country) ComputeBaryCenters() {
	Info.Printf("ComputeBaryCenters begins for country %s", country.Name)

	// parse bodiesSpread to compute bary centers
	// use bodiesOrig to compute bary centers
	for index,b := range *country.bodiesSpread {

		// compute village coordinate (from 0 to nbVillagePerAxe-1)
		villageX := int( math.Floor(float64( nbVillagePerAxe) * b.X))
		villageY := int( math.Floor(float64( nbVillagePerAxe) * b.Y))

		Trace.Printf("Adding body index %d to village %d %d", index, villageX, villageY)

		// add body (original) to the barycenter of the village
		bOrig := (*country.bodiesOrig)[index]
		country.villages[villageX][villageY].addBody( bOrig)

		country.VilCoordinates[index][0] = villageX
		country.VilCoordinates[index][1] = villageY
	}
}

// given lat, lng
// return xClosest, yClosest, distance, latClosest, lngClosest of the closest body within the country (original position)
func (country * Country) ClosestBodyInOriginalPosition( lat, lng float64) (xRelClosest, yRelClosest, distance, latClosest, lngClosest, xSpread, ySpread float64, closestIndex int) {

	// compute relative coordinates within the square
	xRel, yRel := country.LatLng2XY( lat, lng)

	// parse all bodies and get closest body
	closestIndex = -1
	minDistance := 1000000000.0 // we start from away
	for index,b := range *country.bodiesOrig {
		distanceX := b.X - xRel
		distanceY := b.Y - yRel
		distance := math.Sqrt( (distanceX*distanceX) + (distanceY*distanceY))

		if( distance < minDistance ) {
			closestIndex = index
			minDistance = distance
		}
	}

	xRelClosest = (*country.bodiesOrig)[closestIndex].X
	yRelClosest = (*country.bodiesOrig)[closestIndex].Y

	latOptimClosest, lngOptimClosest := country.XY2LatLng( xRelClosest, yRelClosest)

	Info.Printf( "ClosestBodyInOriginalPosition %f %f relative to country %f %f", lat, lng, xRel, yRel)
	Info.Printf( "ClosestBodyInOriginalPosition rel closest %f %f lat lng closest %f %f", xRelClosest, yRelClosest, latOptimClosest, lngOptimClosest)

	// compute x, y in spread bodies
	xSpread = (*country.bodiesSpread)[closestIndex].X
	ySpread = (*country.bodiesSpread)[closestIndex].Y

	Info.Printf( "ClosestBodyInOriginalPosition village %f %f index %d", xSpread, ySpread, closestIndex)

	return xRelClosest, yRelClosest, minDistance, latOptimClosest, lngOptimClosest, xSpread, ySpread, closestIndex
}

func (country * Country) XYSpreadToLatLngOrig( x, y float64) (lat, lng float64) {

	Info.Printf( "XYSpreadToLatLngOrig input x %f y %f", x, y)

	// parse all bodies and get closest body
	closestIndex := -1
	minDistance := 1000000000.0 // we start from away
	for index,b := range *country.bodiesSpread {
		distanceX := b.X - x
		distanceY := b.Y - y
		distance := math.Sqrt( (distanceX*distanceX) + (distanceY*distanceY))

		if( distance < minDistance ) {
			closestIndex = index
			minDistance = distance
		}
	}

	xRelClosest := (*country.bodiesOrig)[closestIndex].X
	yRelClosest := (*country.bodiesOrig)[closestIndex].Y
	latOptimClosest, lngOptimClosest := country.XY2LatLng( xRelClosest, yRelClosest)
	Info.Printf("XYSpreadToLatLngOrig target x %f y %f index %d distance %f", xRelClosest, yRelClosest, closestIndex, minDistance)

	Info.Printf("XYSpreadToLatLngOrig target lat %f lng %f", latOptimClosest, lngOptimClosest)

	return latOptimClosest, lngOptimClosest
}

// get the bodies of a village from x, y spread coordinates
func (country * Country) XYSpreadToTerritoryBorder( x, y float64) PointList {

	Info.Printf( "XYSpreadToTerritoryBorder Country %s x %f y %f", country.Name, x, y)

	points := make(PointList, 0)

	// compute village min & max coord
	xMinVillage := float64( int( x*numberOfVillagePerAxe))/numberOfVillagePerAxe
	xMaxVillage := float64( int( x*numberOfVillagePerAxe + 1.0))/numberOfVillagePerAxe
	yMinVillage := float64( int( y*numberOfVillagePerAxe))/numberOfVillagePerAxe
	yMaxVillage := float64( int( y*numberOfVillagePerAxe + 1.0))/numberOfVillagePerAxe

	Info.Printf( "XYSpreadToTerritoryBorder input village Min x %f Max x %f", xMinVillage, xMaxVillage)

	// parse all bodies and get closest body
	for index,b := range *country.bodiesSpread {
		if (xMinVillage <= b.X) && (b.X < xMaxVillage) && (yMinVillage <= b.Y) && (b.Y < yMaxVillage) {

			xRelClosest := (*country.bodiesOrig)[index].X
			yRelClosest := (*country.bodiesOrig)[index].Y
			latOptimClosest, lngOptimClosest := country.XY2LatLng( xRelClosest, yRelClosest)

			points = append(points, MakePoint(latOptimClosest, lngOptimClosest))
			Info.Printf("XYSpreadToTerritoryBorder %f %f", latOptimClosest, lngOptimClosest)
		}
	}

	Info.Printf( "XYSpreadToTerritoryBorder Country %s nb bodies in village %d", country.Name, len(points))
	return points
}

// given x, y of a point, return the border in the country
func (country * Country) LatLngToTerritoryBorder( lat, lng float64) PointList {

	Info.Printf( "")
	Info.Printf( "LatLngToTerritoryBorder country %s input lat %f lng %f", country.Name, lat, lng)

	// from input lat, lng, get the xSpread, ySpread
	_, _, _, _, _, xSpread, ySpread, _ := country.ClosestBodyInOriginalPosition(lat, lng)
	Info.Printf( "LatLngToTerritoryBorder country %s input xSpread %f ySpread %f", country.Name, xSpread, ySpread)

	points := country.XYSpreadToTerritoryBorder( xSpread, ySpread)

	Info.Printf( "LatLngToTerritoryBorder nb of border points %d", len(points))
	Info.Printf( "")

	return points
}
<|endoftext|>


<commit_before>package tokencmd

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"strings"

	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"

	"github.com/openshift/origin/pkg/auth/server/tokenrequest"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
)

const (
	accessTokenRedirectPattern = `#access_token=([\w]+)&`
)

var (
	accessTokenRedirectRegex = regexp.MustCompile(accessTokenRedirectPattern)
)

type tokenGetterInfo struct {
	accessToken string
}

// RequestToken uses the cmd arguments to locate an openshift oauth server and attempts to authenticate
// it returns the access token if it gets one.  An error if it does not
func RequestToken(clientCfg *clientcmd.Config, reader io.Reader) (string, error) {
	tokenGetter := &tokenGetterInfo{}

	kubeCfg := clientCfg.KubeConfig()
	kubeClient, err := kclient.New(kubeCfg)
	if err != nil {
		return "", err
	}

	// get the transport, so that we can use it to build our own client that wraps it
	// our client understands certain challenges and can respond to them
	clientTransport, err := kclient.TransportFor(kubeCfg)
	if err != nil {
		return "", err
	}
	httpClient := &http.Client{
		Transport:     clientTransport,
		CheckRedirect: tokenGetter.checkRedirect,
	}
	kubeClient.Client = &challengingClient{httpClient, reader}

	_ = kubeClient.Get().AbsPath("oauth").Resource("authorize").Param("response_type", "token").Param("client_id", "openshift-challenging-client").Do()

	if len(tokenGetter.accessToken) == 0 {
		requestTokenURL := kubeCfg.Host + "/oauth" /* clean up after auth.go dies */ + tokenrequest.RequestTokenEndpoint
		return "", errors.New("Unable to get token.  Try visiting " + requestTokenURL + " for a new token.")
	}

	return tokenGetter.accessToken, nil
}

// checkRedirect watches the redirects to see if any contain the access_token anchor.  It then stores the value of the access token for later retrieval
func (tokenGetter *tokenGetterInfo) checkRedirect(req *http.Request, via []*http.Request) error {
	// if we're redirected with an access token in the anchor, use it to set our transport to a proper bearer auth
	if matches := accessTokenRedirectRegex.FindAllStringSubmatch(req.URL.String(), 1); matches != nil {
		tokenGetter.accessToken = matches[0][1]
	}

	if len(via) >= 10 {
		return errors.New("stopped after 10 redirects")
	}

	return nil
}

// challengingClient conforms the kclient.HTTPClient interface.  It introspects responses for auth challenges and
// tries to response to those challenges in order to get a token back.
type challengingClient struct {
	delegate *http.Client
	reader   io.Reader
}

const (
	basicAuthPattern = `[\s]*Basic[\s]*realm="([\w]+)"`
)

var (
	basicAuthRegex = regexp.MustCompile(basicAuthPattern)
)

// Do watches for unauthorized challenges.  If we know to respond, we respond to the challenge
func (client *challengingClient) Do(req *http.Request) (*http.Response, error) {
	resp, err := client.delegate.Do(req)
	if resp.StatusCode == http.StatusUnauthorized {
		if wantsBasicAuth, realm := isBasicAuthChallenge(resp); wantsBasicAuth {
			fmt.Printf("Authenticate for \"%v\"\n", realm)
			username := promptForString("username", client.reader)
			password := promptForString("password", client.reader)

			client.delegate.Transport = kclient.NewBasicAuthRoundTripper(username, password, client.delegate.Transport)
			return client.Do(resp.Request)
		}
	}
	return resp, err
}

func isBasicAuthChallenge(resp *http.Response) (bool, string) {
	for currHeader, headerValue := range resp.Header {
		if strings.EqualFold(currHeader, "WWW-Authenticate") {
			for _, currAuthorizeHeader := range headerValue {
				if matches := basicAuthRegex.FindAllStringSubmatch(currAuthorizeHeader, 1); matches != nil {
					return true, matches[0][1]
				}
			}
		}
	}

	return false, ""
}

func promptForString(field string, r io.Reader) string {
	fmt.Printf("Please enter %s: ", field)
	var result string
	fmt.Fscan(r, &result)
	return result
}
<commit_msg>Use correct client config to run token cmd<commit_after>package tokencmd

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"strings"

	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"

	"github.com/openshift/origin/pkg/auth/server/tokenrequest"
	"github.com/openshift/origin/pkg/client"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
)

const (
	accessTokenRedirectPattern = `#access_token=([\w]+)&`
)

var (
	accessTokenRedirectRegex = regexp.MustCompile(accessTokenRedirectPattern)
)

type tokenGetterInfo struct {
	accessToken string
}

// RequestToken uses the cmd arguments to locate an openshift oauth server and attempts to authenticate
// it returns the access token if it gets one.  An error if it does not
func RequestToken(clientCfg *clientcmd.Config, reader io.Reader) (string, error) {
	tokenGetter := &tokenGetterInfo{}

	osCfg := clientCfg.OpenShiftConfig()
	osClient, err := client.New(osCfg)
	if err != nil {
		return "", err
	}

	// get the transport, so that we can use it to build our own client that wraps it
	// our client understands certain challenges and can respond to them
	clientTransport, err := kclient.TransportFor(osCfg)
	if err != nil {
		return "", err
	}
	httpClient := &http.Client{
		Transport:     clientTransport,
		CheckRedirect: tokenGetter.checkRedirect,
	}
	osClient.Client = &challengingClient{httpClient, reader}

	_ = osClient.Get().AbsPath("oauth").Resource("authorize").Param("response_type", "token").Param("client_id", "openshift-challenging-client").Do()

	if len(tokenGetter.accessToken) == 0 {
		requestTokenURL := osCfg.Host + "/oauth" /* clean up after auth.go dies */ + tokenrequest.RequestTokenEndpoint
		return "", errors.New("Unable to get token.  Try visiting " + requestTokenURL + " for a new token.")
	}

	return tokenGetter.accessToken, nil
}

// checkRedirect watches the redirects to see if any contain the access_token anchor.  It then stores the value of the access token for later retrieval
func (tokenGetter *tokenGetterInfo) checkRedirect(req *http.Request, via []*http.Request) error {
	// if we're redirected with an access token in the anchor, use it to set our transport to a proper bearer auth
	if matches := accessTokenRedirectRegex.FindAllStringSubmatch(req.URL.String(), 1); matches != nil {
		tokenGetter.accessToken = matches[0][1]
	}

	if len(via) >= 10 {
		return errors.New("stopped after 10 redirects")
	}

	return nil
}

// challengingClient conforms the kclient.HTTPClient interface.  It introspects responses for auth challenges and
// tries to response to those challenges in order to get a token back.
type challengingClient struct {
	delegate *http.Client
	reader   io.Reader
}

const (
	basicAuthPattern = `[\s]*Basic[\s]*realm="([\w]+)"`
)

var (
	basicAuthRegex = regexp.MustCompile(basicAuthPattern)
)

// Do watches for unauthorized challenges.  If we know to respond, we respond to the challenge
func (client *challengingClient) Do(req *http.Request) (*http.Response, error) {
	resp, err := client.delegate.Do(req)
	if resp.StatusCode == http.StatusUnauthorized {
		if wantsBasicAuth, realm := isBasicAuthChallenge(resp); wantsBasicAuth {
			fmt.Printf("Authenticate for \"%v\"\n", realm)
			username := promptForString("username", client.reader)
			password := promptForString("password", client.reader)

			client.delegate.Transport = kclient.NewBasicAuthRoundTripper(username, password, client.delegate.Transport)
			return client.Do(resp.Request)
		}
	}
	return resp, err
}

func isBasicAuthChallenge(resp *http.Response) (bool, string) {
	for currHeader, headerValue := range resp.Header {
		if strings.EqualFold(currHeader, "WWW-Authenticate") {
			for _, currAuthorizeHeader := range headerValue {
				if matches := basicAuthRegex.FindAllStringSubmatch(currAuthorizeHeader, 1); matches != nil {
					return true, matches[0][1]
				}
			}
		}
	}

	return false, ""
}

func promptForString(field string, r io.Reader) string {
	fmt.Printf("Please enter %s: ", field)
	var result string
	fmt.Fscan(r, &result)
	return result
}
<|endoftext|>


<commit_before>package install

import (
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apiserver/pkg/authorization/authorizer"
	rbacauthorizer "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"

	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
)

// RuleChecker is used to verify whether PolicyRules are satisfied by existing Roles or ClusterRoles
type RuleChecker interface {
	// RuleSatisfied determines whether a PolicyRule is satisfied for a ServiceAccount
	// by existing Roles and ClusterRoles
	RuleSatisfied(sa *corev1.ServiceAccount, namespace string, rule rbacv1.PolicyRule) (bool, error)
}

// CSVRuleChecker determines whether a PolicyRule is satisfied for a ServiceAccount
// by existing Roles and ClusterRoles
type CSVRuleChecker struct {
	opClient operatorclient.ClientInterface
	csv      *v1alpha1.ClusterServiceVersion
}

func NewCSVRuleChecker(opClient operatorclient.ClientInterface, csv *v1alpha1.ClusterServiceVersion) *CSVRuleChecker {
	return &CSVRuleChecker{
		opClient: opClient,
		csv:      csv.DeepCopy(),
	}
}

// RuleSatisfied returns true if a ServiceAccount is authorized to perform all actions described by a PolicyRule in a namespace
func (c *CSVRuleChecker) RuleSatisfied(sa *corev1.ServiceAccount, namespace string, rule rbacv1.PolicyRule) (bool, error) {
	// get attributes set for the given Role and ServiceAccount
	user := toDefaultInfo(sa)
	attributesSet := toAttributesSet(user, namespace, rule)

	// create a new RBACAuthorizer
	rbacAuthorizer := rbacauthorizer.New(c, c, c, c)

	// ensure all attributes are authorized
	for _, attributes := range attributesSet {
		decision, _, err := rbacAuthorizer.Authorize(attributes)
		if err != nil {
			return false, err
		}

		if decision == authorizer.DecisionDeny || decision == authorizer.DecisionNoOpinion {
			return false, nil
		}

	}

	return true, nil
}

func (c *CSVRuleChecker) GetRole(namespace, name string) (*rbacv1.Role, error) {
	// get the Role
	role, err := c.opClient.KubernetesInterface().RbacV1().Roles(namespace).Get(name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	// check if the Role has an OwnerConflict with the client's CSV
	if role != nil && c.hasOwnerConflicts(role.GetOwnerReferences()) {
		return &rbacv1.Role{}, nil
	}

	return role, nil
}

func (c *CSVRuleChecker) ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) {
	// get all RoleBindings
	rbList, err := c.opClient.KubernetesInterface().RbacV1().RoleBindings(namespace).List(metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	// filter based on OwnerReferences
	var filtered []*rbacv1.RoleBinding
	for i := 0; i < len(rbList.Items); i++ {
		if !c.hasOwnerConflicts(rbList.Items[i].GetOwnerReferences()) {
			filtered = append(filtered, &rbList.Items[i])
		}
	}

	return filtered, nil
}

func (c *CSVRuleChecker) GetClusterRole(name string) (*rbacv1.ClusterRole, error) {
	// get the ClusterRole
	clusterRole, err := c.opClient.KubernetesInterface().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	// check if the ClusterRole has an OwnerConflict with the client's CSV
	if clusterRole != nil && c.hasOwnerConflicts(clusterRole.GetOwnerReferences()) {
		return &rbacv1.ClusterRole{}, nil
	}

	return clusterRole, nil
}

func (c *CSVRuleChecker) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) {
	// get all RoleBindings
	crbList, err := c.opClient.KubernetesInterface().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	// filter based on OwnerReferences
	var filtered []*rbacv1.ClusterRoleBinding
	for i := 0; i < len(crbList.Items); i++ {
		item := crbList.Items[i]
		if !c.hasOwnerConflicts(item.GetOwnerReferences()) {
			filtered = append(filtered, &item)
		}
	}

	return filtered, nil
}

func (c *CSVRuleChecker) hasOwnerConflicts(ownerRefs []metav1.OwnerReference) bool {
	conflicts := false
	for _, ownerRef := range ownerRefs {
		if ownerRef.Kind == v1alpha1.ClusterServiceVersionKind {
			if ownerRef.Name == c.csv.GetName() && ownerRef.UID == c.csv.GetUID() {
				return false
			}

			conflicts = true
		}
	}

	return conflicts
}
<commit_msg>docs(rule_checker): add reasoning note to hasOwnerConflicts method<commit_after>package install

import (
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apiserver/pkg/authorization/authorizer"
	rbacauthorizer "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"

	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
)

// RuleChecker is used to verify whether PolicyRules are satisfied by existing Roles or ClusterRoles
type RuleChecker interface {
	// RuleSatisfied determines whether a PolicyRule is satisfied for a ServiceAccount
	// by existing Roles and ClusterRoles
	RuleSatisfied(sa *corev1.ServiceAccount, namespace string, rule rbacv1.PolicyRule) (bool, error)
}

// CSVRuleChecker determines whether a PolicyRule is satisfied for a ServiceAccount
// by existing Roles and ClusterRoles
type CSVRuleChecker struct {
	opClient operatorclient.ClientInterface
	csv      *v1alpha1.ClusterServiceVersion
}

func NewCSVRuleChecker(opClient operatorclient.ClientInterface, csv *v1alpha1.ClusterServiceVersion) *CSVRuleChecker {
	return &CSVRuleChecker{
		opClient: opClient,
		csv:      csv.DeepCopy(),
	}
}

// RuleSatisfied returns true if a ServiceAccount is authorized to perform all actions described by a PolicyRule in a namespace
func (c *CSVRuleChecker) RuleSatisfied(sa *corev1.ServiceAccount, namespace string, rule rbacv1.PolicyRule) (bool, error) {
	// get attributes set for the given Role and ServiceAccount
	user := toDefaultInfo(sa)
	attributesSet := toAttributesSet(user, namespace, rule)

	// create a new RBACAuthorizer
	rbacAuthorizer := rbacauthorizer.New(c, c, c, c)

	// ensure all attributes are authorized
	for _, attributes := range attributesSet {
		decision, _, err := rbacAuthorizer.Authorize(attributes)
		if err != nil {
			return false, err
		}

		if decision == authorizer.DecisionDeny || decision == authorizer.DecisionNoOpinion {
			return false, nil
		}

	}

	return true, nil
}

func (c *CSVRuleChecker) GetRole(namespace, name string) (*rbacv1.Role, error) {
	// get the Role
	role, err := c.opClient.KubernetesInterface().RbacV1().Roles(namespace).Get(name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	// check if the Role has an OwnerConflict with the client's CSV
	if role != nil && c.hasOwnerConflicts(role.GetOwnerReferences()) {
		return &rbacv1.Role{}, nil
	}

	return role, nil
}

func (c *CSVRuleChecker) ListRoleBindings(namespace string) ([]*rbacv1.RoleBinding, error) {
	// get all RoleBindings
	rbList, err := c.opClient.KubernetesInterface().RbacV1().RoleBindings(namespace).List(metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	// filter based on OwnerReferences
	var filtered []*rbacv1.RoleBinding
	for i := 0; i < len(rbList.Items); i++ {
		if !c.hasOwnerConflicts(rbList.Items[i].GetOwnerReferences()) {
			filtered = append(filtered, &rbList.Items[i])
		}
	}

	return filtered, nil
}

func (c *CSVRuleChecker) GetClusterRole(name string) (*rbacv1.ClusterRole, error) {
	// get the ClusterRole
	clusterRole, err := c.opClient.KubernetesInterface().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	// check if the ClusterRole has an OwnerConflict with the client's CSV
	if clusterRole != nil && c.hasOwnerConflicts(clusterRole.GetOwnerReferences()) {
		return &rbacv1.ClusterRole{}, nil
	}

	return clusterRole, nil
}

func (c *CSVRuleChecker) ListClusterRoleBindings() ([]*rbacv1.ClusterRoleBinding, error) {
	// get all RoleBindings
	crbList, err := c.opClient.KubernetesInterface().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	// filter based on OwnerReferences
	var filtered []*rbacv1.ClusterRoleBinding
	for i := 0; i < len(crbList.Items); i++ {
		item := crbList.Items[i]
		if !c.hasOwnerConflicts(item.GetOwnerReferences()) {
			filtered = append(filtered, &item)
		}
	}

	return filtered, nil
}

// hasOwnerConflicts checks if the given list of OwnerReferences points to CSVs other than the
// CSVRuleChecker's. The method returns false if there exists other OwnerReferences of Kind
// ClusterServiceVersion, then the list must contain an element that points to the CSVRuleChecker's
// CSV, otherwise the method will return true.
//
// Note: This is imporant when determining if a Role, RoleBinding, ClusterRole, or ClusterRoleBinding
// can be used to satisfy permissions of a CSV. If the CSVRuleChecker's CSV is not a member of the RBAC resource's
// OwnerReferences, then we know the resource can be garbage collected by OLM independently of the CSVRuleChecker's
// CSV
func (c *CSVRuleChecker) hasOwnerConflicts(ownerRefs []metav1.OwnerReference) bool {
	conflicts := false
	for _, ownerRef := range ownerRefs {
		if ownerRef.Kind == v1alpha1.ClusterServiceVersionKind {
			if ownerRef.Name == c.csv.GetName() && ownerRef.UID == c.csv.GetUID() {
				return false
			}

			conflicts = true
		}
	}

	return conflicts
}
<|endoftext|>


<commit_before>// Copyright (c) 2017 Huawei Technologies Co., Ltd. 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.

/*
This module implements the policy-based scheduling by parsing storage
profiles configured by admin.

*/

package selector

import (
	"reflect"
	"testing"

	"github.com/opensds/opensds/pkg/model"
	"github.com/opensds/opensds/pkg/utils"
)

func TestGetPoolCapabilityMap(t *testing.T) {
	Pool := model.StoragePoolSpec{
		BaseModel: &model.BaseModel{
			Id:        "f4486139-78d5-462d-a7b9-fdaf6c797e11",
			CreatedAt: "2017-10-24T15:04:05",
		},
		FreeCapacity:     int64(50),
		AvailabilityZone: "az1",
		Extras: model.StoragePoolExtraSpec{
			DataStorage: model.DataStorageLoS{
				RecoveryTimeObjective: 3,
				ProvisioningPolicy:    "Thin",
				IsSpaceEfficient:      true,
			},
			IOConnectivity: model.IOConnectivityLoS{
				AccessProtocol: "rbd",
				MaxIOPS:        1,
				MaxBWS:         1000,
			},
			DataProtection: model.DataProtectionLos{},
			Advanced: model.ExtraSpec{
				"thin":     true,
				"dedupe":   true,
				"compress": true,
				"diskType": "SSD",
			},
		},
	}

	var mapA map[string]interface{}
	mapA = make(map[string]interface{})
	mapA["key1"] = "value1"
	mapA["key2"] = "value2"
	Pool.Extras.Advanced["mapA"] = mapA

	result, err := GetPoolCapabilityMap(&Pool)
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, result)
	}

	CreatedAt, ok := result["createdAt"].(string)
	if (!ok) || (Pool.CreatedAt != CreatedAt) {
		t.Errorf("Expected %v/%v, get %v/%v", true, Pool.CreatedAt, ok, CreatedAt)
	}

	FreeCapacity, ok := result["freeCapacity"].(float64)
	if (!ok) || (!utils.IsFloatEqual(FreeCapacity, float64(Pool.FreeCapacity))) {
		t.Errorf("Expected %v/%v, get %v/%v", true, float64(Pool.FreeCapacity), ok, FreeCapacity)
	}

	thin, ok := result["extras.advanced.thin"].(bool)
	if (!ok) || (Pool.Extras.Advanced["thin"] != thin) {
		t.Errorf("Expected %v/%v, get %v/%v", true, Pool.Extras.Advanced["thin"], ok, thin)
	}

	value1, ok := result["extras.advanced.mapA.key1"].(string)
	if (!ok) || ("value1" != value1) {
		t.Errorf("Expected %v/%v, get %v/%v", true, "value1", ok, value1)
	}

	RecoveryTimeObjective, ok := result["extras.dataStorage.recoveryTimeObjective"].(float64)
	if (!ok) || (!utils.IsFloatEqual(RecoveryTimeObjective, float64(Pool.Extras.DataStorage.RecoveryTimeObjective))) {
		t.Errorf("Expected %v/%v, get %v/%v", true, float64(Pool.Extras.DataStorage.RecoveryTimeObjective), ok, RecoveryTimeObjective)
	}

	ProvisioningPolicy, ok := result["extras.dataStorage.provisioningPolicy"].(string)
	if (!ok) || (Pool.Extras.DataStorage.ProvisioningPolicy != ProvisioningPolicy) {
		t.Errorf("Expected %v/%v, get %v/%v", true, Pool.Extras.DataStorage.ProvisioningPolicy, ok, ProvisioningPolicy)
	}

	IsSpaceEfficient, ok := result["extras.dataStorage.isSpaceEfficient"].(bool)
	if (!ok) || (Pool.Extras.DataStorage.IsSpaceEfficient != IsSpaceEfficient) {
		t.Errorf("Expected %v/%v, get %v/%v", true, Pool.Extras.DataStorage.IsSpaceEfficient, ok, IsSpaceEfficient)
	}
}

var (
	FakePools = []*model.StoragePoolSpec{
		&model.StoragePoolSpec{},
		&model.StoragePoolSpec{},
		&model.StoragePoolSpec{},
	}

	TestCases = []struct {
		request  map[string]interface{}
		pools    []*model.StoragePoolSpec
		expected []*model.StoragePoolSpec
	}{
		{
			pools: FakePools,
		},
		{
			pools: FakePools,
		},
	}
)

func TestCreatedAtFilter(t *testing.T) {
	FakePools[0].BaseModel = &model.BaseModel{
		CreatedAt: "2017-10-24T15:04:05",
	}
	FakePools[1].BaseModel = &model.BaseModel{
		CreatedAt: "2017-10-24T15:04:06",
	}
	FakePools[2].BaseModel = &model.BaseModel{
		CreatedAt: "2017-10-24T15:04:07",
	}

	TestCases[0].request = map[string]interface{}{
		"createdAt": "s== 2017-10-24T15:04:05",
	}
	TestCases[0].expected = []*model.StoragePoolSpec{
		FakePools[0],
	}

	TestCases[1].request = map[string]interface{}{
		"createdAt": "s>= 2017-10-24T15:04:06",
	}
	TestCases[1].expected = []*model.StoragePoolSpec{
		FakePools[1],
		FakePools[2],
	}

	for _, testCase := range TestCases {
		result, _ := SelectSupportedPools(len(testCase.pools), testCase.request,
			testCase.pools)

		if !reflect.DeepEqual(result, testCase.expected) {
			t.Errorf("Expected %v, get %v", testCase.expected, result)
		}
	}
}

func TestFreeCapacityFilter(t *testing.T) {
	FakePools[0].FreeCapacity = 100
	FakePools[1].FreeCapacity = 50
	FakePools[2].FreeCapacity = 66

	TestCases[0].request = map[string]interface{}{
		"freeCapacity": ">= 66",
	}
	TestCases[0].expected = []*model.StoragePoolSpec{
		FakePools[0],
		FakePools[2],
	}

	TestCases[1].request = map[string]interface{}{
		"freeCapacity": "> 100",
	}
	TestCases[1].expected = nil

	for _, testCase := range TestCases {
		result, _ := SelectSupportedPools(len(testCase.pools), testCase.request,
			testCase.pools)

		if !reflect.DeepEqual(result, testCase.expected) {
			t.Errorf("Expected %v, get %v", testCase.expected, result)
		}
	}
}

func TestAccessProtocolFilter(t *testing.T) {
	FakePools[0].Extras.IOConnectivity = model.IOConnectivityLoS{
		AccessProtocol: "dbr",
	}
	FakePools[1].Extras.IOConnectivity = model.IOConnectivityLoS{
		AccessProtocol: "rbd",
	}
	FakePools[2].Extras.IOConnectivity = model.IOConnectivityLoS{
		AccessProtocol: "brd",
	}

	TestCases[0].request = map[string]interface{}{
		"extras.ioConnectivity.accessProtocol": "rbd",
	}
	TestCases[0].expected = []*model.StoragePoolSpec{
		FakePools[1],
	}

	TestCases[1].request = map[string]interface{}{
		"extras.ioConnectivity.accessProtocol": "s!= rbd",
	}

	TestCases[1].expected = []*model.StoragePoolSpec{
		FakePools[0],
		FakePools[2],
	}

	for _, testCase := range TestCases {
		result, _ := SelectSupportedPools(len(testCase.pools), testCase.request,
			testCase.pools)

		if !reflect.DeepEqual(result, testCase.expected) {
			t.Errorf("Expected %v, get %v", testCase.expected, result)
		}
	}
}

func TestAdvancedFilter(t *testing.T) {
	FakePools[0].Extras.Advanced = model.ExtraSpec{
		"compress": true,
	}
	FakePools[1].Extras.Advanced = model.ExtraSpec{
		"compress": true,
	}
	FakePools[2].Extras.Advanced = model.ExtraSpec{
		"compress": false,
	}

	TestCases[0].request = map[string]interface{}{
		"extras.advanced.compress": true,
	}
	TestCases[0].expected = []*model.StoragePoolSpec{
		FakePools[0],
		FakePools[1],
	}

	TestCases[1].request = map[string]interface{}{
		"extras.advanced.compress": false,
	}
	TestCases[1].expected = []*model.StoragePoolSpec{
		FakePools[2],
	}

	for _, testCase := range TestCases {
		result, _ := SelectSupportedPools(len(testCase.pools), testCase.request,
			testCase.pools)

		if !reflect.DeepEqual(result, testCase.expected) {
			t.Errorf("Expected %v, get %v", testCase.expected, result)
		}
	}
}
<commit_msg>mod filter test cases<commit_after>// Copyright (c) 2017 Huawei Technologies Co., Ltd. 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.

/*
This module implements the policy-based scheduling by parsing storage
profiles configured by admin.

*/

package selector

import (
	"reflect"
	"testing"

	"github.com/opensds/opensds/pkg/model"
	"github.com/opensds/opensds/pkg/utils"
	. "github.com/opensds/opensds/testutils/collection"
)

type FilterCaseSpec struct {
	request  map[string]interface{}
	expected []*model.StoragePoolSpec
}

var (
	FakePools = []*model.StoragePoolSpec{
		&SamplePools[0],
		&SamplePools[1],
	}
)

func TestSimplifyPoolCapabilityMap(t *testing.T) {
	input := make(map[string]interface{})
	input["name"] = "sample-pool-02"
	input["extras.advanced"] = map[string]interface{}{
		"diskType": "SAS",
	}
	simpleMap, unSimpleMap := simplifyPoolCapabilityMap(input)

	if 2 != len(simpleMap) {
		t.Errorf("Expected %v, get %v", 2, len(simpleMap))
	}

	if 0 != len(unSimpleMap) {
		t.Errorf("Expected %v, get %v", 0, len(simpleMap))
	}

	name, ok := simpleMap["name"].(string)
	if (!ok) || (input["name"] != name) {
		t.Errorf("Expected %v/%v, get %v/%v", true, input["name"], ok, name)
	}

	diskType, ok := simpleMap["extras.advanced.diskType"].(string)
	if (!ok) || ("SAS" != diskType) {
		t.Errorf("Expected %v/%v, get %v/%v", true, "SAS", ok, diskType)
	}
}

func TestGetPoolCapabilityMap(t *testing.T) {
	Pool := SamplePools[0]
	result, err := GetPoolCapabilityMap(&Pool)
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	id, ok := result["id"].(string)
	if (!ok) || (Pool.Id != id) {
		t.Errorf("Expected %v/%v, get %v/%v", true, Pool.Id, ok, id)
	}

	FreeCapacity, ok := result["freeCapacity"].(float64)
	if (!ok) || (!utils.IsFloatEqual(FreeCapacity, float64(Pool.FreeCapacity))) {
		t.Errorf("Expected %v/%v, get %v/%v", true, float64(Pool.FreeCapacity), ok, FreeCapacity)
	}

	IsSpaceEfficient, ok := result["extras.dataStorage.isSpaceEfficient"].(bool)
	if (!ok) || (Pool.Extras.DataStorage.IsSpaceEfficient != IsSpaceEfficient) {
		t.Errorf("Expected %v/%v, get %v/%v", true, Pool.Extras.DataStorage.IsSpaceEfficient, ok, IsSpaceEfficient)
	}

	latency, ok := result["extras.advanced.latency"].(string)
	if (!ok) || (Pool.Extras.Advanced["latency"] != latency) {
		t.Errorf("Expected %v/%v, get %v/%v", true, Pool.Extras.Advanced["latency"], ok, latency)
	}
}

func TestIsAvailablePool(t *testing.T) {
	filterReq := make(map[string]interface{})
	filterReq["totalCapacity"] = "<= 100"
	isAvailable, err := IsAvailablePool(filterReq, &SamplePools[0])
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != isAvailable {
		t.Errorf("Expected %v, get %v", true, isAvailable)
	}

	filterReq["totalCapacity"] = "!= 200"
	isAvailable, err = IsAvailablePool(filterReq, &SamplePools[1])
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if false != isAvailable {
		t.Errorf("Expected %v, get %v", false, isAvailable)
	}

}

func TestMatch(t *testing.T) {

	isMatch, err := match("availabilityZone", "default", "<in> defau")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != isMatch {
		t.Errorf("Expected %v, get %v", true, isMatch)
	}

	isMatch, err = match("availabilityZone", "default", "<in> defau1")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if false != isMatch {
		t.Errorf("Expected %v, get %v", false, isMatch)
	}
}

func TestInOperator(t *testing.T) {

	result, err := InOperator("availabilityZone", "fau", "default")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != result {
		t.Errorf("Expected %v, get %v", true, result)
	}

	result, err = InOperator("availabilityZone", "default1", "default")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if false != result {
		t.Errorf("Expected %v, get %v", false, result)
	}
}

func TestCompareOperator(t *testing.T) {

	result, err := CompareOperator("s<", "dockId", "123", "122")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != result {
		t.Errorf("Expected %v, get %v", true, result)
	}

	result, err = CompareOperator("s<", "dockId", "123", "124")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if false != result {
		t.Errorf("Expected %v, get %v", false, result)
	}
}

func TestStringCompare(t *testing.T) {

	result, err := StringCompare("s<", "dockId", "123", "122")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if false != result {
		t.Errorf("Expected %v, get %v", false, result)
	}

	result, err = StringCompare("s<", "dockId", "123", "124")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != result {
		t.Errorf("Expected %v, get %v", true, result)
	}
}

func TestParseBoolAndCompare(t *testing.T) {

	result, err := ParseBoolAndCompare("isSpaceEfficient", true, "T")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != result {
		t.Errorf("Expected %v, get %v", true, result)
	}

	result, err = ParseBoolAndCompare("isSpaceEfficient", true, "0")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if false != result {
		t.Errorf("Expected %v, get %v", false, result)
	}
}

func TestParseFloat64AndCompare(t *testing.T) {

	result, err := ParseFloat64AndCompare("==", "freeCapacity", 60.0, "60")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != result {
		t.Errorf("Expected %v, get %v", true, result)
	}

	result, err = ParseFloat64AndCompare("!=", "freeCapacity", 61.0, "60")
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != result {
		t.Errorf("Expected %v, get %v", true, result)
	}
}

func TestOrOperator(t *testing.T) {
	words := []string{"<or>", "10", "<or>", "20"}
	result, err := OrOperator("freeCapacity", words, 20.0)
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if true != result {
		t.Errorf("Expected %v, get %v", true, result)
	}

	result, err = OrOperator("freeCapacity", words, 30.0)
	if nil != err {
		t.Errorf("Expected %v, get %v", nil, err)
	}

	if false != result {
		t.Errorf("Expected %v, get %v", false, result)
	}
}

func TestIdFilter(t *testing.T) {
	testCases := []FilterCaseSpec{
		{
			request: map[string]interface{}{
				"id": "084bf71e-a102-11e7-88a8-e31fe6d52248",
			},
			expected: []*model.StoragePoolSpec{
				&SamplePools[0],
			},
		},
		{
			request: map[string]interface{}{
				"id": "s== a594b8ac-a103-11e7-985f-d723bcf01b5f",
			},
			expected: []*model.StoragePoolSpec{
				&SamplePools[1],
			},
		},
	}

	for _, testCase := range testCases {
		result, _ := SelectSupportedPools(len(FakePools), testCase.request,
			FakePools)

		if !reflect.DeepEqual(result, testCase.expected) {
			t.Errorf("Expected %v, get %v", testCase.expected, result)
		}
	}
}

func TestFreeCapacityFilter(t *testing.T) {
	testCases := []FilterCaseSpec{
		{
			request: map[string]interface{}{
				"freeCapacity": ">= 170",
			},
			expected: []*model.StoragePoolSpec{
				&SamplePools[1],
			},
		},
		{
			request: map[string]interface{}{
				"freeCapacity": "== 90",
			},
			expected: []*model.StoragePoolSpec{
				&SamplePools[0],
			},
		},
	}

	for _, testCase := range testCases {
		result, _ := SelectSupportedPools(len(FakePools), testCase.request,
			FakePools)

		if !reflect.DeepEqual(result, testCase.expected) {
			t.Errorf("Expected %v, get %v", testCase.expected, result)
		}
	}
}

func TestIsSpaceEfficientFilter(t *testing.T) {
	testCases := []FilterCaseSpec{
		{
			request: map[string]interface{}{
				"extras.dataStorage.isSpaceEfficient": "<is> true",
			},
			expected: []*model.StoragePoolSpec{
				&SamplePools[0],
				&SamplePools[1],
			},
		},
		{
			request: map[string]interface{}{
				"extras.dataStorage.isSpaceEfficient": "<is> false",
			},
			expected: nil,
		},
	}

	for _, testCase := range testCases {
		result, _ := SelectSupportedPools(len(FakePools), testCase.request,
			FakePools)

		if !reflect.DeepEqual(result, testCase.expected) {
			t.Errorf("Expected %v, get %v", testCase.expected, result)
		}
	}
}

func TestAdvancedFilter(t *testing.T) {
	testCases := []FilterCaseSpec{
		{
			request: map[string]interface{}{
				"extras.advanced.diskType": "SAS",
			},
			expected: []*model.StoragePoolSpec{
				&SamplePools[1],
			},
		},
		{
			request: map[string]interface{}{
				"extras.advanced.diskType": "s>= SSD",
			},
			expected: []*model.StoragePoolSpec{
				&SamplePools[0],
			},
		},
	}

	for _, testCase := range testCases {
		result, _ := SelectSupportedPools(len(FakePools), testCase.request,
			FakePools)

		if !reflect.DeepEqual(result, testCase.expected) {
			t.Errorf("Expected %v, get %v", testCase.expected, result)
		}
	}
}
<|endoftext|>


<commit_before>package servicing

import (
	"time"

	"github.com/sapcc/kubernikus/pkg/controller/metrics"

	"github.com/prometheus/client_golang/prometheus"

	"github.com/go-kit/kit/log"
	"github.com/pkg/errors"
	"k8s.io/client-go/tools/record"

	"github.com/sapcc/kubernikus/pkg/api/models"
	v1 "github.com/sapcc/kubernikus/pkg/apis/kubernikus/v1"
	"github.com/sapcc/kubernikus/pkg/controller/config"
	client "github.com/sapcc/kubernikus/pkg/generated/clientset/typed/kubernikus/v1"
	listers_kubernikus_v1 "github.com/sapcc/kubernikus/pkg/generated/listers/kubernikus/v1"
	"github.com/sapcc/kubernikus/pkg/util"
)

const (
	// AnnotationServicingTimestamp show when the kluster was last serviced
	AnnotationServicingTimestamp = "kubernikus.cloud.sap/servicingTimestamp"

	// AnnotationUpdateTImestamp shows when a node update was started
	AnnotationUpdateTimestamp = "kubernikus.cloud.sap/updateTimestamp"
)

var (
	// ServiceInterval defines how often a kluster is serviced
	ServiceInterval = 1 * time.Hour
	UpdateTimeout   = 10 * time.Minute
)

type (
	// Reconciler checks a specific kluster's nodes for updates/upgrades
	Reconciler interface {
		Do() error
	}

	// ReconcilerFactory produces a Reconciler
	ReconcilerFactory interface {
		Make(*v1.Kluster) (Reconciler, error)
	}

	// KlusterReconcilerFactory produces a Reconciler for a specific kluster
	KlusterReconcilerFactory struct {
		Logger            log.Logger
		ListerFactory     ListerFactory
		LifeCyclerFactory LifeCyclerFactory
		KlusterLister     listers_kubernikus_v1.KlusterLister
		KubernikusClient  client.KubernikusV1Interface
	}

	// KlusterReconciler is a concrete implementation of a Reconciler
	KlusterReconciler struct {
		Logger           log.Logger
		Kluster          *v1.Kluster
		Lister           Lister
		LifeCycler       LifeCycler
		KlusterLister    listers_kubernikus_v1.KlusterLister
		KubernikusClient client.KubernikusV1Interface

		Status *prometheus.GaugeVec
	}

	// LoggingReconciler decorates a Reconciler with log messages
	LoggingReconciler struct {
		Logger     log.Logger
		Reconciler Reconciler
	}
)

// NewKlusterReconcilerFactory produces a new Factory
func NewKlusterReconcilerFactory(logger log.Logger, recorder record.EventRecorder, factories config.Factories, clients config.Clients) ReconcilerFactory {
	return &KlusterReconcilerFactory{
		Logger:            logger,
		ListerFactory:     NewNodeListerFactory(logger, recorder, factories, clients),
		LifeCyclerFactory: NewNodeLifeCyclerFactory(logger, recorder, factories, clients),
		KlusterLister:     factories.Kubernikus.Kubernikus().V1().Klusters().Lister(),
		KubernikusClient:  clients.Kubernikus.Kubernikus(),
	}
}

// Make a new Factory
func (f *KlusterReconcilerFactory) Make(k *v1.Kluster) (Reconciler, error) {
	logger := log.With(f.Logger, "kluster", k.Spec.Name, "project", k.Account())
	lister, err := f.ListerFactory.Make(k)
	if err != nil {
		return nil, err
	}

	cycler, err := f.LifeCyclerFactory.Make(k)
	if err != nil {
		return nil, errors.Wrap(err, "Couldn't make LifeCyclerFactory")
	}

	var reconciler Reconciler
	reconciler = &KlusterReconciler{
		Logger:           logger,
		Kluster:          k,
		Lister:           lister,
		LifeCycler:       cycler,
		KlusterLister:    f.KlusterLister,
		KubernikusClient: f.KubernikusClient,
		Status:           metrics.ServicingStatusNodes,
	}

	reconciler = &LoggingReconciler{
		Logger:     logger,
		Reconciler: reconciler,
	}

	return reconciler, nil
}

// Do it
func (r *KlusterReconciler) Do() error {
	r.Logger.Log("msg", "reconciling", "v", 2)

	if r.Kluster.Status.Phase != models.KlusterPhaseRunning {
		r.Logger.Log("msg", "skipped upgrades because kluster is not running", "v", 2)
		return nil
	}

	defer r.collectMetrics()

	// Default to skip klusters without the servicing annotation
	if !util.EnabledValue(r.Kluster.ObjectMeta.Annotations[AnnotationServicingSafeguard]) {
		r.Logger.Log("msg", "Skippig upgrades. Manually disabled with safeguard annotation.")
		return nil
	}

	for _, node := range r.Lister.Successful() {
		if err := r.LifeCycler.Uncordon(node); err != nil {
			return errors.Wrap(err, "Failed to uncordon successfully updated node.")
		}
	}

	if len(r.Lister.Failed()) > 0 {
		r.Logger.Log("msg", "skipped upgrades because there is a failed upgrade")
		return nil
	}

	if len(r.Lister.Updating()) > 0 {
		r.Logger.Log("msg", "skipped upgrades because there is sitll nodes being updated", "v", 2)
		return nil
	}

	if !r.isServiceIntervalElapsed() {
		r.Logger.Log("msg", "skipped upgrades because kluster service interval not elapsed yet", "v", 2)
		return nil
	}

	if len(r.Lister.NotReady()) > 0 {
		r.Logger.Log("msg", "skipped upgrades because kluster is not healthy", "v", 2)
		return nil
	}

	replace := r.Lister.Replace()
	reboot := r.Lister.Reboot()
	if len(replace) > 0 {
		if err := r.LifeCycler.Drain(replace[0]); err != nil {
			return errors.Wrap(err, "Failed to drain node that is about to be replaces")
		}

		if err := r.LifeCycler.Replace(replace[0]); err != nil {
			return errors.Wrap(err, "Failed to replace node")
		}
	} else if len(reboot) > 0 {
		if err := r.LifeCycler.Drain(reboot[0]); err != nil {
			return errors.Wrap(err, "Failed to drain node that is about to be rebooted")
		}

		if err := r.LifeCycler.Reboot(reboot[0]); err != nil {
			return errors.Wrap(err, "Failed to reboot node")
		}
	}

	return r.updateLastServicingTime()
}

// Do log it
func (r *LoggingReconciler) Do() (err error) {
	defer func(begin time.Time) {
		r.Logger.Log(
			"msg", "reconciled",
			"took", time.Since(begin),
			"v", 2,
			"err", err,
		)
	}(time.Now())
	return r.Reconciler.Do()
}

func (r *KlusterReconciler) updateLastServicingTime() error {
	client := r.KubernikusClient.Klusters(r.Kluster.Namespace)
	lister := r.KlusterLister.Klusters(r.Kluster.Namespace)
	_, err := util.UpdateKlusterWithRetries(client, lister, r.Kluster.Name, func(kluster *v1.Kluster) error {
		kluster.ObjectMeta.Annotations[AnnotationServicingTimestamp] = Now().UTC().Format(time.RFC3339)
		return nil
	})
	return err
}

func (r *KlusterReconciler) getLastServicingTime(annotations map[string]string) time.Time {
	t, ok := annotations[AnnotationServicingTimestamp]
	if !ok {
		return time.Unix(0, 0)
	}

	pt, err := time.Parse(time.RFC3339, t)
	if err != nil {
		return time.Unix(0, 0)
	}

	return pt
}

func (r *KlusterReconciler) isServiceIntervalElapsed() bool {
	nextServiceTime := r.getLastServicingTime(r.Kluster.ObjectMeta.GetAnnotations()).Add(ServiceInterval)
	return Now().After(nextServiceTime)
}

func (r *KlusterReconciler) collectMetrics() {
	reboot := float64(len(r.Lister.Reboot()))
	replace := float64(len(r.Lister.Replace()))
	uptodate := float64(len(r.Lister.All())) - reboot - replace

	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "started"}).Set(float64(len(r.Lister.Updating())))
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "failed"}).Set(float64(len(r.Lister.Failed())))
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "successful"}).Set(float64(len(r.Lister.Successful())))
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "reboot"}).Set(reboot)
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "replace"}).Set(replace)
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "uptodate"}).Set(uptodate)
}
<commit_msg>delete metrics for terminating clusters<commit_after>package servicing

import (
	"time"

	"github.com/sapcc/kubernikus/pkg/controller/metrics"

	"github.com/prometheus/client_golang/prometheus"

	"github.com/go-kit/kit/log"
	"github.com/pkg/errors"
	"k8s.io/client-go/tools/record"

	"github.com/sapcc/kubernikus/pkg/api/models"
	v1 "github.com/sapcc/kubernikus/pkg/apis/kubernikus/v1"
	"github.com/sapcc/kubernikus/pkg/controller/config"
	client "github.com/sapcc/kubernikus/pkg/generated/clientset/typed/kubernikus/v1"
	listers_kubernikus_v1 "github.com/sapcc/kubernikus/pkg/generated/listers/kubernikus/v1"
	"github.com/sapcc/kubernikus/pkg/util"
)

const (
	// AnnotationServicingTimestamp show when the kluster was last serviced
	AnnotationServicingTimestamp = "kubernikus.cloud.sap/servicingTimestamp"

	// AnnotationUpdateTImestamp shows when a node update was started
	AnnotationUpdateTimestamp = "kubernikus.cloud.sap/updateTimestamp"
)

var (
	// ServiceInterval defines how often a kluster is serviced
	ServiceInterval = 1 * time.Hour
	UpdateTimeout   = 10 * time.Minute
)

type (
	// Reconciler checks a specific kluster's nodes for updates/upgrades
	Reconciler interface {
		Do() error
	}

	// ReconcilerFactory produces a Reconciler
	ReconcilerFactory interface {
		Make(*v1.Kluster) (Reconciler, error)
	}

	// KlusterReconcilerFactory produces a Reconciler for a specific kluster
	KlusterReconcilerFactory struct {
		Logger            log.Logger
		ListerFactory     ListerFactory
		LifeCyclerFactory LifeCyclerFactory
		KlusterLister     listers_kubernikus_v1.KlusterLister
		KubernikusClient  client.KubernikusV1Interface
	}

	// KlusterReconciler is a concrete implementation of a Reconciler
	KlusterReconciler struct {
		Logger           log.Logger
		Kluster          *v1.Kluster
		Lister           Lister
		LifeCycler       LifeCycler
		KlusterLister    listers_kubernikus_v1.KlusterLister
		KubernikusClient client.KubernikusV1Interface

		Status *prometheus.GaugeVec
	}

	// LoggingReconciler decorates a Reconciler with log messages
	LoggingReconciler struct {
		Logger     log.Logger
		Reconciler Reconciler
	}
)

// NewKlusterReconcilerFactory produces a new Factory
func NewKlusterReconcilerFactory(logger log.Logger, recorder record.EventRecorder, factories config.Factories, clients config.Clients) ReconcilerFactory {
	return &KlusterReconcilerFactory{
		Logger:            logger,
		ListerFactory:     NewNodeListerFactory(logger, recorder, factories, clients),
		LifeCyclerFactory: NewNodeLifeCyclerFactory(logger, recorder, factories, clients),
		KlusterLister:     factories.Kubernikus.Kubernikus().V1().Klusters().Lister(),
		KubernikusClient:  clients.Kubernikus.Kubernikus(),
	}
}

// Make a new Factory
func (f *KlusterReconcilerFactory) Make(k *v1.Kluster) (Reconciler, error) {
	logger := log.With(f.Logger, "kluster", k.Spec.Name, "project", k.Account())
	lister, err := f.ListerFactory.Make(k)
	if err != nil {
		return nil, err
	}

	cycler, err := f.LifeCyclerFactory.Make(k)
	if err != nil {
		return nil, errors.Wrap(err, "Couldn't make LifeCyclerFactory")
	}

	var reconciler Reconciler
	reconciler = &KlusterReconciler{
		Logger:           logger,
		Kluster:          k,
		Lister:           lister,
		LifeCycler:       cycler,
		KlusterLister:    f.KlusterLister,
		KubernikusClient: f.KubernikusClient,
		Status:           metrics.ServicingStatusNodes,
	}

	reconciler = &LoggingReconciler{
		Logger:     logger,
		Reconciler: reconciler,
	}

	return reconciler, nil
}

// Do it
func (r *KlusterReconciler) Do() error {
	r.Logger.Log("msg", "reconciling", "v", 2)

	if r.Kluster.Status.Phase != models.KlusterPhaseRunning {
		r.Logger.Log("msg", "skipped upgrades because kluster is not running", "v", 2)
		return nil
	}

	defer r.collectMetrics()

	// Default to skip klusters without the servicing annotation
	if !util.EnabledValue(r.Kluster.ObjectMeta.Annotations[AnnotationServicingSafeguard]) {
		r.Logger.Log("msg", "Skippig upgrades. Manually disabled with safeguard annotation.")
		return nil
	}

	for _, node := range r.Lister.Successful() {
		if err := r.LifeCycler.Uncordon(node); err != nil {
			return errors.Wrap(err, "Failed to uncordon successfully updated node.")
		}
	}

	if len(r.Lister.Failed()) > 0 {
		r.Logger.Log("msg", "skipped upgrades because there is a failed upgrade")
		return nil
	}

	if len(r.Lister.Updating()) > 0 {
		r.Logger.Log("msg", "skipped upgrades because there is sitll nodes being updated", "v", 2)
		return nil
	}

	if !r.isServiceIntervalElapsed() {
		r.Logger.Log("msg", "skipped upgrades because kluster service interval not elapsed yet", "v", 2)
		return nil
	}

	if len(r.Lister.NotReady()) > 0 {
		r.Logger.Log("msg", "skipped upgrades because kluster is not healthy", "v", 2)
		return nil
	}

	replace := r.Lister.Replace()
	reboot := r.Lister.Reboot()
	if len(replace) > 0 {
		if err := r.LifeCycler.Drain(replace[0]); err != nil {
			return errors.Wrap(err, "Failed to drain node that is about to be replaces")
		}

		if err := r.LifeCycler.Replace(replace[0]); err != nil {
			return errors.Wrap(err, "Failed to replace node")
		}
	} else if len(reboot) > 0 {
		if err := r.LifeCycler.Drain(reboot[0]); err != nil {
			return errors.Wrap(err, "Failed to drain node that is about to be rebooted")
		}

		if err := r.LifeCycler.Reboot(reboot[0]); err != nil {
			return errors.Wrap(err, "Failed to reboot node")
		}
	}

	return r.updateLastServicingTime()
}

// Do log it
func (r *LoggingReconciler) Do() (err error) {
	defer func(begin time.Time) {
		r.Logger.Log(
			"msg", "reconciled",
			"took", time.Since(begin),
			"v", 2,
			"err", err,
		)
	}(time.Now())
	return r.Reconciler.Do()
}

func (r *KlusterReconciler) updateLastServicingTime() error {
	client := r.KubernikusClient.Klusters(r.Kluster.Namespace)
	lister := r.KlusterLister.Klusters(r.Kluster.Namespace)
	_, err := util.UpdateKlusterWithRetries(client, lister, r.Kluster.Name, func(kluster *v1.Kluster) error {
		kluster.ObjectMeta.Annotations[AnnotationServicingTimestamp] = Now().UTC().Format(time.RFC3339)
		return nil
	})
	return err
}

func (r *KlusterReconciler) getLastServicingTime(annotations map[string]string) time.Time {
	t, ok := annotations[AnnotationServicingTimestamp]
	if !ok {
		return time.Unix(0, 0)
	}

	pt, err := time.Parse(time.RFC3339, t)
	if err != nil {
		return time.Unix(0, 0)
	}

	return pt
}

func (r *KlusterReconciler) isServiceIntervalElapsed() bool {
	nextServiceTime := r.getLastServicingTime(r.Kluster.ObjectMeta.GetAnnotations()).Add(ServiceInterval)
	return Now().After(nextServiceTime)
}

func (r *KlusterReconciler) collectMetrics() {
	if r.Kluster.Status.Phase == models.KlusterPhaseTerminating {
		r.Status.Delete(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "started"})
		r.Status.Delete(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "failed"})
		r.Status.Delete(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "successful"})
		r.Status.Delete(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "reboot"})
		r.Status.Delete(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "replace"})
		r.Status.Delete(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "uptodate"})
		return
	}

	reboot := float64(len(r.Lister.Reboot()))
	replace := float64(len(r.Lister.Replace()))
	uptodate := float64(len(r.Lister.All())) - reboot - replace

	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "started"}).Set(float64(len(r.Lister.Updating())))
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "failed"}).Set(float64(len(r.Lister.Failed())))
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "updating", "status": "successful"}).Set(float64(len(r.Lister.Successful())))
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "reboot"}).Set(reboot)
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "replace"}).Set(replace)
	r.Status.With(prometheus.Labels{"kluster_id": r.Kluster.GetName(), "action": "waiting", "status": "uptodate"}).Set(uptodate)
}
<|endoftext|>