<commit_before>// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package http

import (
	"io"
	"net/http/httptrace"
	"net/textproto"
	"sort"
	"strings"
	"sync"
	"time"
)

// A Header represents the key-value pairs in an HTTP header.
type Header map[string][]string

// Add adds the key, value pair to the header.
// It appends to any existing values associated with key.
// The key is case insensitive; it is canonicalized by
// textproto.CanonicalMIMEHeaderKey.
func (h Header) Add(key, value string) {
	textproto.MIMEHeader(h).Add(key, value)
}

// Set sets the header entries associated with key to the
// single element value. It replaces any existing values
// associated with key. The key is case insensitive; it is
// canonicalized by textproto.CanonicalMIMEHeaderKey.
// To use non-canonical keys, assign to the map directly.
func (h Header) Set(key, value string) {
	textproto.MIMEHeader(h).Set(key, value)
}

// Get gets the first value associated with the given key. If
// there are no values associated with the key, Get returns "".
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. To access multiple
// values of a key, or to use non-canonical keys, access the
// map directly.
func (h Header) Get(key string) string {
	return textproto.MIMEHeader(h).Get(key)
}

// get is like Get, but key must already be in CanonicalHeaderKey form.
func (h Header) get(key string) string {
	if v := h[key]; len(v) > 0 {
		return v[0]
	}
	return ""
}

// has reports whether h has the provided key defined, even if it's
// set to 0-length slice.
func (h Header) has(key string) bool {
	_, ok := h[key]
	return ok
}

// Del deletes the values associated with key.
// The key is case insensitive; it is canonicalized by
// textproto.CanonicalMIMEHeaderKey.
func (h Header) Del(key string) {
	textproto.MIMEHeader(h).Del(key)
}

// Write writes a header in wire format.
func (h Header) Write(w io.Writer) error {
	return h.write(w, nil)
}

func (h Header) write(w io.Writer, trace *httptrace.ClientTrace) error {
	return h.writeSubset(w, nil, trace)
}

func (h Header) clone() Header {
	h2 := make(Header, len(h))
	for k, vv := range h {
		vv2 := make([]string, len(vv))
		copy(vv2, vv)
		h2[k] = vv2
	}
	return h2
}

var timeFormats = []string{
	TimeFormat,
	time.RFC850,
	time.ANSIC,
}

// ParseTime parses a time header (such as the Date: header),
// trying each of the three formats allowed by HTTP/1.1:
// TimeFormat, time.RFC850, and time.ANSIC.
func ParseTime(text string) (t time.Time, err error) {
	for _, layout := range timeFormats {
		t, err = time.Parse(layout, text)
		if err == nil {
			return
		}
	}
	return
}

var headerNewlineToSpace = strings.NewReplacer("\n", " ", "\r", " ")

// stringWriter implements WriteString on a Writer.
type stringWriter struct {
	w io.Writer
}

func (w stringWriter) WriteString(s string) (n int, err error) {
	return w.w.Write([]byte(s))
}

type keyValues struct {
	key    string
	values []string
}

// A headerSorter implements sort.Interface by sorting a []keyValues
// by key. It's used as a pointer, so it can fit in a sort.Interface
// interface value without allocation.
type headerSorter struct {
	kvs []keyValues
}

func (s *headerSorter) Len() int           { return len(s.kvs) }
func (s *headerSorter) Swap(i, j int)      { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] }
func (s *headerSorter) Less(i, j int) bool { return s.kvs[i].key < s.kvs[j].key }

var headerSorterPool = sync.Pool{
	New: func() interface{} { return new(headerSorter) },
}

// sortedKeyValues returns h's keys sorted in the returned kvs
// slice. The headerSorter used to sort is also returned, for possible
// return to headerSorterCache.
func (h Header) sortedKeyValues(exclude map[string]bool) (kvs []keyValues, hs *headerSorter) {
	hs = headerSorterPool.Get().(*headerSorter)
	if cap(hs.kvs) < len(h) {
		hs.kvs = make([]keyValues, 0, len(h))
	}
	kvs = hs.kvs[:0]
	for k, vv := range h {
		if !exclude[k] {
			kvs = append(kvs, keyValues{k, vv})
		}
	}
	hs.kvs = kvs
	sort.Sort(hs)
	return kvs, hs
}

// WriteSubset writes a header in wire format.
// If exclude is not nil, keys where exclude[key] == true are not written.
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error {
	return h.writeSubset(w, exclude, nil)
}

func (h Header) writeSubset(w io.Writer, exclude map[string]bool, trace *httptrace.ClientTrace) error {
	ws, ok := w.(io.StringWriter)
	if !ok {
		ws = stringWriter{w}
	}
	kvs, sorter := h.sortedKeyValues(exclude)
	var formattedVals []string
	for _, kv := range kvs {
		for _, v := range kv.values {
			v = headerNewlineToSpace.Replace(v)
			v = textproto.TrimString(v)
			for _, s := range []string{kv.key, ": ", v, "\r\n"} {
				if _, err := ws.WriteString(s); err != nil {
					headerSorterPool.Put(sorter)
					return err
				}
			}
			if trace != nil && trace.WroteHeaderField != nil {
				formattedVals = append(formattedVals, v)
			}
		}
		if trace != nil && trace.WroteHeaderField != nil {
			trace.WroteHeaderField(kv.key, formattedVals)
			formattedVals = nil
		}
	}
	headerSorterPool.Put(sorter)
	return nil
}

// CanonicalHeaderKey returns the canonical format of the
// header key s. The canonicalization converts the first
// letter and any letter following a hyphen to upper case;
// the rest are converted to lowercase. For example, the
// canonical key for "accept-encoding" is "Accept-Encoding".
// If s contains a space or invalid header field bytes, it is
// returned without modifications.
func CanonicalHeaderKey(s string) string { return textproto.CanonicalMIMEHeaderKey(s) }

// hasToken reports whether token appears with v, ASCII
// case-insensitive, with space or comma boundaries.
// token must be all lowercase.
// v may contain mixed cased.
func hasToken(v, token string) bool {
	if len(token) > len(v) || token == "" {
		return false
	}
	if v == token {
		return true
	}
	for sp := 0; sp <= len(v)-len(token); sp++ {
		// Check that first character is good.
		// The token is ASCII, so checking only a single byte
		// is sufficient. We skip this potential starting
		// position if both the first byte and its potential
		// ASCII uppercase equivalent (b|0x20) don't match.
		// False positives ('^' => '~') are caught by EqualFold.
		if b := v[sp]; b != token[0] && b|0x20 != token[0] {
			continue
		}
		// Check that start pos is on a valid token boundary.
		if sp > 0 && !isTokenBoundary(v[sp-1]) {
			continue
		}
		// Check that end pos is on a valid token boundary.
		if endPos := sp + len(token); endPos != len(v) && !isTokenBoundary(v[endPos]) {
			continue
		}
		if strings.EqualFold(v[sp:sp+len(token)], token) {
			return true
		}
	}
	return false
}

func isTokenBoundary(b byte) bool {
	return b == ' ' || b == ',' || b == '\t'
}
<commit_msg>net/http: document CanonicalHeaderKey from Header<commit_after>// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package http

import (
	"io"
	"net/http/httptrace"
	"net/textproto"
	"sort"
	"strings"
	"sync"
	"time"
)

// A Header represents the key-value pairs in an HTTP header.
//
// The keys should be in canonical form, as returned by
// CanonicalHeaderKey.
type Header map[string][]string

// Add adds the key, value pair to the header.
// It appends to any existing values associated with key.
// The key is case insensitive; it is canonicalized by
// CanonicalHeaderKey.
func (h Header) Add(key, value string) {
	textproto.MIMEHeader(h).Add(key, value)
}

// Set sets the header entries associated with key to the
// single element value. It replaces any existing values
// associated with key. The key is case insensitive; it is
// canonicalized by textproto.CanonicalMIMEHeaderKey.
// To use non-canonical keys, assign to the map directly.
func (h Header) Set(key, value string) {
	textproto.MIMEHeader(h).Set(key, value)
}

// Get gets the first value associated with the given key. If
// there are no values associated with the key, Get returns "".
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. To access multiple
// values of a key, or to use non-canonical keys, access the
// map directly.
func (h Header) Get(key string) string {
	return textproto.MIMEHeader(h).Get(key)
}

// get is like Get, but key must already be in CanonicalHeaderKey form.
func (h Header) get(key string) string {
	if v := h[key]; len(v) > 0 {
		return v[0]
	}
	return ""
}

// has reports whether h has the provided key defined, even if it's
// set to 0-length slice.
func (h Header) has(key string) bool {
	_, ok := h[key]
	return ok
}

// Del deletes the values associated with key.
// The key is case insensitive; it is canonicalized by
// CanonicalHeaderKey.
func (h Header) Del(key string) {
	textproto.MIMEHeader(h).Del(key)
}

// Write writes a header in wire format.
func (h Header) Write(w io.Writer) error {
	return h.write(w, nil)
}

func (h Header) write(w io.Writer, trace *httptrace.ClientTrace) error {
	return h.writeSubset(w, nil, trace)
}

func (h Header) clone() Header {
	h2 := make(Header, len(h))
	for k, vv := range h {
		vv2 := make([]string, len(vv))
		copy(vv2, vv)
		h2[k] = vv2
	}
	return h2
}

var timeFormats = []string{
	TimeFormat,
	time.RFC850,
	time.ANSIC,
}

// ParseTime parses a time header (such as the Date: header),
// trying each of the three formats allowed by HTTP/1.1:
// TimeFormat, time.RFC850, and time.ANSIC.
func ParseTime(text string) (t time.Time, err error) {
	for _, layout := range timeFormats {
		t, err = time.Parse(layout, text)
		if err == nil {
			return
		}
	}
	return
}

var headerNewlineToSpace = strings.NewReplacer("\n", " ", "\r", " ")

// stringWriter implements WriteString on a Writer.
type stringWriter struct {
	w io.Writer
}

func (w stringWriter) WriteString(s string) (n int, err error) {
	return w.w.Write([]byte(s))
}

type keyValues struct {
	key    string
	values []string
}

// A headerSorter implements sort.Interface by sorting a []keyValues
// by key. It's used as a pointer, so it can fit in a sort.Interface
// interface value without allocation.
type headerSorter struct {
	kvs []keyValues
}

func (s *headerSorter) Len() int           { return len(s.kvs) }
func (s *headerSorter) Swap(i, j int)      { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] }
func (s *headerSorter) Less(i, j int) bool { return s.kvs[i].key < s.kvs[j].key }

var headerSorterPool = sync.Pool{
	New: func() interface{} { return new(headerSorter) },
}

// sortedKeyValues returns h's keys sorted in the returned kvs
// slice. The headerSorter used to sort is also returned, for possible
// return to headerSorterCache.
func (h Header) sortedKeyValues(exclude map[string]bool) (kvs []keyValues, hs *headerSorter) {
	hs = headerSorterPool.Get().(*headerSorter)
	if cap(hs.kvs) < len(h) {
		hs.kvs = make([]keyValues, 0, len(h))
	}
	kvs = hs.kvs[:0]
	for k, vv := range h {
		if !exclude[k] {
			kvs = append(kvs, keyValues{k, vv})
		}
	}
	hs.kvs = kvs
	sort.Sort(hs)
	return kvs, hs
}

// WriteSubset writes a header in wire format.
// If exclude is not nil, keys where exclude[key] == true are not written.
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error {
	return h.writeSubset(w, exclude, nil)
}

func (h Header) writeSubset(w io.Writer, exclude map[string]bool, trace *httptrace.ClientTrace) error {
	ws, ok := w.(io.StringWriter)
	if !ok {
		ws = stringWriter{w}
	}
	kvs, sorter := h.sortedKeyValues(exclude)
	var formattedVals []string
	for _, kv := range kvs {
		for _, v := range kv.values {
			v = headerNewlineToSpace.Replace(v)
			v = textproto.TrimString(v)
			for _, s := range []string{kv.key, ": ", v, "\r\n"} {
				if _, err := ws.WriteString(s); err != nil {
					headerSorterPool.Put(sorter)
					return err
				}
			}
			if trace != nil && trace.WroteHeaderField != nil {
				formattedVals = append(formattedVals, v)
			}
		}
		if trace != nil && trace.WroteHeaderField != nil {
			trace.WroteHeaderField(kv.key, formattedVals)
			formattedVals = nil
		}
	}
	headerSorterPool.Put(sorter)
	return nil
}

// CanonicalHeaderKey returns the canonical format of the
// header key s. The canonicalization converts the first
// letter and any letter following a hyphen to upper case;
// the rest are converted to lowercase. For example, the
// canonical key for "accept-encoding" is "Accept-Encoding".
// If s contains a space or invalid header field bytes, it is
// returned without modifications.
func CanonicalHeaderKey(s string) string { return textproto.CanonicalMIMEHeaderKey(s) }

// hasToken reports whether token appears with v, ASCII
// case-insensitive, with space or comma boundaries.
// token must be all lowercase.
// v may contain mixed cased.
func hasToken(v, token string) bool {
	if len(token) > len(v) || token == "" {
		return false
	}
	if v == token {
		return true
	}
	for sp := 0; sp <= len(v)-len(token); sp++ {
		// Check that first character is good.
		// The token is ASCII, so checking only a single byte
		// is sufficient. We skip this potential starting
		// position if both the first byte and its potential
		// ASCII uppercase equivalent (b|0x20) don't match.
		// False positives ('^' => '~') are caught by EqualFold.
		if b := v[sp]; b != token[0] && b|0x20 != token[0] {
			continue
		}
		// Check that start pos is on a valid token boundary.
		if sp > 0 && !isTokenBoundary(v[sp-1]) {
			continue
		}
		// Check that end pos is on a valid token boundary.
		if endPos := sp + len(token); endPos != len(v) && !isTokenBoundary(v[endPos]) {
			continue
		}
		if strings.EqualFold(v[sp:sp+len(token)], token) {
			return true
		}
	}
	return false
}

func isTokenBoundary(b byte) bool {
	return b == ' ' || b == ',' || b == '\t'
}
<|endoftext|>


<commit_before>package button

import (
	"log"

	"github.com/forestgiant/eff"
	"github.com/forestgiant/eff/util"
)

func defaultBGColor() eff.Color {
	return eff.Color{R: 219, G: 217, B: 217, A: 255}
}

func defaultTextColor() eff.Color {
	return eff.Black()
}

func overBGColor() eff.Color {
	return eff.Color{R: 238, G: 238, B: 238, A: 255}
}

func overTextColor() eff.Color {
	return eff.Black()
}

func downBGColor() eff.Color {
	return eff.Color{R: 246, G: 246, B: 246, A: 255}
}

func downTextColor() eff.Color {
	return eff.Black()
}

// Click function that is called when the button is clicked
type Click func(*Button)

// Button defines an eff.Drawable that maintains the state of a button
type Button struct {
	eff.Shape

	mouseDown        bool
	mouseOver        bool
	Text             string
	ClickHandler     func(b *Button)
	font             eff.Font
	defaultBGColor   eff.Color
	defaultTextColor eff.Color
	overBGColor      eff.Color
	overTextColor    eff.Color
	downBGColor      eff.Color
	downTextColor    eff.Color
	bgColor          eff.Color
	textColor        eff.Color
}

// Hitbox returns the hitbox rect of the button, this is the same as Button.Rect
func (b *Button) Hitbox() eff.Rect {
	return b.ParentOffsetRect()
}

// MouseDown function that is called when any mouse button is pressed down while the cursor is inside the hitbox
func (b *Button) MouseDown(leftState bool, middleState bool, rightState bool) {
	b.mouseDown = true

	b.drawButton()
}

// MouseUp function that is called when any mouse button is released while the cursor is inside the hitbox
func (b *Button) MouseUp(leftState bool, middleState bool, rightState bool) {
	if b.mouseDown {
		b.mouseDown = false
		b.ClickHandler(b)
	}

	b.drawButton()
}

// MouseOver function that is called when the mouse moves into the hitbox
func (b *Button) MouseOver() {
	b.mouseOver = true

	b.drawButton()
}

// MouseOut function that is called when the mouse moves out of the hitbox
func (b *Button) MouseOut() {
	b.mouseOver = false
	b.mouseDown = false

	b.drawButton()
}

func (b *Button) drawButton() {
	if b.Graphics() == nil {
		return
	}

	b.Clear()
	text, err := util.EllipseText(b.font, b.Text, b.Rect().W, b.Graphics())
	if err != nil {
		log.Fatal(err)
	}

	textW, textH, err := b.Graphics().GetTextSize(b.font, text)
	if err != nil {
		log.Fatal(err)
	}

	textPoint := eff.Point{
		X: (b.Rect().W - textW) / 2,
		Y: (b.Rect().H - textH) / 2,
	}

	bgColor := b.defaultBGColor
	textColor := b.defaultTextColor
	if b.mouseDown {
		bgColor = b.downBGColor
		textColor = b.downTextColor
	} else if b.mouseOver {
		bgColor = b.overBGColor
		textColor = b.overTextColor
	}

	b.SetBackgroundColor(bgColor)
	b.DrawText(b.font, text, textColor, textPoint)
}

// IsMouseOver function that returns true if the mouse cursor is currently inside the hitbox
func (b *Button) IsMouseOver() bool { return b.mouseOver }

func (b *Button) SetDefaultBGColor(c eff.Color) {
	b.defaultBGColor = c
	b.drawButton()
}

func (b *Button) DefaultBGColor() eff.Color {
	return b.defaultBGColor
}

func (b *Button) SetDefaultTextColor(c eff.Color) {
	b.defaultTextColor = c
	b.drawButton()
}

func (b *Button) DefaultTextColor() eff.Color {
	return b.defaultTextColor
}

func (b *Button) SetOverBGColor(c eff.Color) {
	b.overBGColor = c
	b.drawButton()
}

func (b *Button) OverBGColor() eff.Color {
	return b.overBGColor
}

func (b *Button) SetOverTextColor(c eff.Color) {
	b.overTextColor = c
	b.drawButton()
}

func (b *Button) OverTextColor() eff.Color {
	return b.overTextColor
}

func (b *Button) SetDownBGColor(c eff.Color) {
	b.downBGColor = c
	b.drawButton()
}

func (b *Button) DownBGColor() eff.Color {
	return b.downBGColor
}

func (b *Button) SetDownTextColor(c eff.Color) {
	b.downTextColor = c
	b.drawButton()
}

func (b *Button) DownTextColor() eff.Color {
	return b.downTextColor
}

// NewButton function that creates an instance of the component button
func NewButton(font eff.Font, text string, rect eff.Rect, clickhandler Click) *Button {
	b := &Button{
		font:         font,
		Text:         text,
		ClickHandler: clickhandler,
	}

	b.SetRect(rect)
	b.defaultBGColor = defaultBGColor()
	b.defaultTextColor = defaultTextColor()
	b.downBGColor = downBGColor()
	b.downTextColor = downTextColor()
	b.overBGColor = overBGColor()
	b.overTextColor = overTextColor()
	b.bgColor = defaultBGColor()
	b.textColor = defaultTextColor()
	b.SetGraphicsReadyHandler(func() {
		b.drawButton()
	})

	return b
}
<commit_msg>Added selected state to button component, added nil check to clickhander call<commit_after>package button

import (
	"log"

	"github.com/forestgiant/eff"
	"github.com/forestgiant/eff/util"
)

func defaultBGColor() eff.Color {
	return eff.Color{R: 219, G: 217, B: 217, A: 255}
}

func defaultTextColor() eff.Color {
	return eff.Black()
}

func overBGColor() eff.Color {
	return eff.Color{R: 238, G: 238, B: 238, A: 255}
}

func overTextColor() eff.Color {
	return eff.Black()
}

func downBGColor() eff.Color {
	return eff.Color{R: 246, G: 246, B: 246, A: 255}
}

func downTextColor() eff.Color {
	return eff.Black()
}

func selectedBGColor() eff.Color {
	return eff.Black()
}

func selectedTextColor() eff.Color {
	return eff.White()
}

// Click function that is called when the button is clicked
type Click func(*Button)

// Button defines an eff.Drawable that maintains the state of a button
type Button struct {
	eff.Shape

	mouseDown         bool
	mouseOver         bool
	selected          bool
	Text              string
	ClickHandler      func(b *Button)
	font              eff.Font
	defaultBGColor    eff.Color
	defaultTextColor  eff.Color
	overBGColor       eff.Color
	overTextColor     eff.Color
	downBGColor       eff.Color
	downTextColor     eff.Color
	selectedBGColor   eff.Color
	selectedTextColor eff.Color
	bgColor           eff.Color
	textColor         eff.Color
}

// Hitbox returns the hitbox rect of the button, this is the same as Button.Rect
func (b *Button) Hitbox() eff.Rect {
	return b.ParentOffsetRect()
}

// MouseDown function that is called when any mouse button is pressed down while the cursor is inside the hitbox
func (b *Button) MouseDown(leftState bool, middleState bool, rightState bool) {
	b.mouseDown = true

	b.drawButton()
}

// MouseUp function that is called when any mouse button is released while the cursor is inside the hitbox
func (b *Button) MouseUp(leftState bool, middleState bool, rightState bool) {
	if b.mouseDown {
		b.mouseDown = false
		if b.ClickHandler != nil {
			b.ClickHandler(b)
		}

	}

	b.drawButton()
}

// MouseOver function that is called when the mouse moves into the hitbox
func (b *Button) MouseOver() {
	b.mouseOver = true

	b.drawButton()
}

// MouseOut function that is called when the mouse moves out of the hitbox
func (b *Button) MouseOut() {
	b.mouseOver = false
	b.mouseDown = false

	b.drawButton()
}

func (b *Button) drawButton() {
	if b.Graphics() == nil {
		return
	}

	b.Clear()
	text, err := util.EllipseText(b.font, b.Text, b.Rect().W, b.Graphics())
	if err != nil {
		log.Fatal(err)
	}

	textW, textH, err := b.Graphics().GetTextSize(b.font, text)
	if err != nil {
		log.Fatal(err)
	}

	textPoint := eff.Point{
		X: (b.Rect().W - textW) / 2,
		Y: (b.Rect().H - textH) / 2,
	}

	bgColor := b.defaultBGColor
	textColor := b.defaultTextColor
	if b.selected {
		bgColor = b.selectedBGColor
		textColor = b.selectedTextColor
	} else if b.mouseDown {
		bgColor = b.downBGColor
		textColor = b.downTextColor
	} else if b.mouseOver {
		bgColor = b.overBGColor
		textColor = b.overTextColor
	}
	b.SetBackgroundColor(bgColor)
	b.DrawText(b.font, text, textColor, textPoint)
}

// IsMouseOver function that returns true if the mouse cursor is currently inside the hitbox
func (b *Button) IsMouseOver() bool { return b.mouseOver }

func (b *Button) SetDefaultBGColor(c eff.Color) {
	b.defaultBGColor = c
	b.drawButton()
}

func (b *Button) DefaultBGColor() eff.Color {
	return b.defaultBGColor
}

func (b *Button) SetDefaultTextColor(c eff.Color) {
	b.defaultTextColor = c
	b.drawButton()
}

func (b *Button) DefaultTextColor() eff.Color {
	return b.defaultTextColor
}

func (b *Button) SetOverBGColor(c eff.Color) {
	b.overBGColor = c
	b.drawButton()
}

func (b *Button) OverBGColor() eff.Color {
	return b.overBGColor
}

func (b *Button) SetOverTextColor(c eff.Color) {
	b.overTextColor = c
	b.drawButton()
}

func (b *Button) OverTextColor() eff.Color {
	return b.overTextColor
}

func (b *Button) SetDownBGColor(c eff.Color) {
	b.downBGColor = c
	b.drawButton()
}

func (b *Button) DownBGColor() eff.Color {
	return b.downBGColor
}

func (b *Button) SetDownTextColor(c eff.Color) {
	b.downTextColor = c
	b.drawButton()
}

func (b *Button) DownTextColor() eff.Color {
	return b.downTextColor
}

func (b *Button) SetSelectedBGColor(c eff.Color) {
	b.selectedBGColor = c
	b.drawButton()
}

func (b *Button) SelectedBGColor() eff.Color {
	return b.selectedBGColor
}

func (b *Button) SetSelectedTextColor(c eff.Color) {
	b.selectedTextColor = c
	b.drawButton()
}

func (b *Button) SelectedTextColor() eff.Color {
	return b.selectedTextColor
}

func (b *Button) Selected() bool {
	return b.selected
}

func (b *Button) SetSelected(selected bool) {
	b.selected = selected
	b.drawButton()
}

// NewButton function that creates an instance of the component button
func NewButton(font eff.Font, text string, rect eff.Rect, clickhandler Click) *Button {
	b := &Button{
		font:         font,
		Text:         text,
		ClickHandler: clickhandler,
	}

	b.SetRect(rect)
	b.defaultBGColor = defaultBGColor()
	b.defaultTextColor = defaultTextColor()
	b.downBGColor = downBGColor()
	b.downTextColor = downTextColor()
	b.overBGColor = overBGColor()
	b.overTextColor = overTextColor()
	b.selectedBGColor = selectedBGColor()
	b.selectedTextColor = selectedTextColor()
	b.bgColor = defaultBGColor()
	b.textColor = defaultTextColor()

	b.SetGraphicsReadyHandler(func() {
		b.drawButton()
	})
	b.SetResizeHandler(func() {
		b.drawButton()
	})

	return b
}
<|endoftext|>


<commit_before>// Copyright 2013, Cong Ding. 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.
//
// author: Cong Ding <dinggnu@gmail.com>
//
package logging

import (
	"fmt"
	"strings"
)

// pre-defined formats
const (
	BasicFormat = "%s [%s] %s - %s\n name,levelname,asctime,message"
	RichFormat  = "%s [%s] %s - %d - %s:%s:%s:%d - %s\n name, levelname, asctime, thread, module, filename, funcName, lineno, message"
)

func (logger *logging) genLog(level Level, message string) string {
	format := strings.Split(logger.format, "\n")
	if len(format) != 2 {
		return "logging format error"
	}
	args := strings.Split(format[1], ",")
	fs := make([]interface{}, len(args))
	l := new(log)
	l.message = message
	l.level = level
	for k, v := range args {
		fs[k] = fields[strings.TrimSpace(v)](logger, l)
	}
	return fmt.Sprintf(format[0], fs...)
}
<commit_msg>delete module name from richformat<commit_after>// Copyright 2013, Cong Ding. 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.
//
// author: Cong Ding <dinggnu@gmail.com>
//
package logging

import (
	"fmt"
	"strings"
)

// pre-defined formats
const (
	BasicFormat = "%s [%s] %s - %s\n name,levelname,asctime,message"
	RichFormat  = "%s [%s] %s - %d - %s:%s:%d - %s\n name, levelname, asctime, thread, filename, funcName, lineno, message"
)

func (logger *logging) genLog(level Level, message string) string {
	format := strings.Split(logger.format, "\n")
	if len(format) != 2 {
		return "logging format error"
	}
	args := strings.Split(format[1], ",")
	fs := make([]interface{}, len(args))
	l := new(log)
	l.message = message
	l.level = level
	for k, v := range args {
		fs[k] = fields[strings.TrimSpace(v)](logger, l)
	}
	return fmt.Sprintf(format[0], fs...)
}
<|endoftext|>


<commit_before>package login

import (
	"bytes"
	"html/template"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/tarent/loginsrv/logging"
	"github.com/tarent/loginsrv/model"
)

const partials = `

{{define "styles"}}
    <link uic-remove rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <link uic-remove rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.1.1/bootstrap-social.min.css">
    <link uic-remove rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css">

{{end}}

{{define "userInfo"}}
              {{with .UserInfo}}

Welcome {{.Sub}}!

                {{if .Picture}}{{end}}
                {{if .Name}}
{{.Name}}
{{end}}
              {{end}}

              Logout
{{end}}

{{define "login"}}
              {{ range $providerName, $opts := .Config.Oauth }}

                   Sign in with {{ $providerName | ucfirst }}

              {{end}}

              {{if and (not (eq (len .Config.Backends) 0)) (not (eq (len .Config.Oauth) 0))}}

or

              {{end}}

              {{if not (eq (len .Config.Backends) 0) }}

Sign in

                      {{ if .Failure}}
Invalid credentials
{{end}}

		    <form accept-charset="UTF-8" role="form" method="POST" action="{{.Config.LoginPath}}">
                      <fieldset>

		          <input class="form-control" placeholder="Username" name="username" value="{{.UserInfo.Sub}}" type="text">

		          <input class="form-control" placeholder="Password" name="password" type="password" value="">

		        <input class="btn btn-lg btn-success btn-block" type="submit" value="Login">
		      </fieldset>
		    </form>

              {{end}}
{{end}}`

var layout = `<!DOCTYPE html>

    <meta name="viewport" content="width=device-width, initial-scale=1">
    {{ template "styles" . }}

    <uic-fragment name="content">

            {{ if .Error}}

                Internal Error.  Please try again later.

            {{end}}

            {{if .Authenticated}}

              {{template "userInfo" . }}

            {{else}}

              {{template "login" . }}

            {{end}}

    </uic-fragment>

`

type loginFormData struct {
	Error         bool
	Failure       bool
	Config        *Config
	Authenticated bool
	UserInfo      model.UserInfo
}

func writeLoginForm(w http.ResponseWriter, params loginFormData) {
	funcMap := template.FuncMap{
		"ucfirst": ucfirst,
	}
	templateName := "loginForm"
	if params.Config != nil && params.Config.Template != "" {
		templateName = params.Config.Template
	}
	t := template.New(templateName).Funcs(funcMap)
	t = template.Must(t.Parse(partials))
	if params.Config != nil && params.Config.Template != "" {
		customTemplate, err := ioutil.ReadFile(params.Config.Template)
		if err != nil {
			logging.Logger.WithError(err).Error()
			w.WriteHeader(500)
			w.Write([]byte(`Internal Server Error`))
			return
		}

		t, err = t.Parse(string(customTemplate))
		if err != nil {
			logging.Logger.WithError(err).Error()
			w.WriteHeader(500)
			w.Write([]byte(`Internal Server Error`))
			return
		}
	} else {
		t = template.Must(t.Parse(layout))
	}

	b := bytes.NewBuffer(nil)
	err := t.Execute(b, params)
	if err != nil {
		logging.Logger.WithError(err).Error()
		w.WriteHeader(500)
		w.Write([]byte(`Internal Server Error`))
		return
	}

	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
	w.Header().Set("Content-Type", contentTypeHTML)
	if params.Error {
		w.WriteHeader(500)
	}

	w.Write(b.Bytes())
}

func ucfirst(in string) string {
	if in == "" {
		return ""
	}

	return strings.ToUpper(in[0:1]) + in[1:]
}
<commit_msg>Remove rightmost "/" from login_path<commit_after>package login

import (
	"bytes"
	"html/template"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/tarent/loginsrv/logging"
	"github.com/tarent/loginsrv/model"
)

const partials = `

{{define "styles"}}
    <link uic-remove rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <link uic-remove rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.1.1/bootstrap-social.min.css">
    <link uic-remove rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css">

{{end}}

{{define "userInfo"}}
              {{with .UserInfo}}

Welcome {{.Sub}}!

                {{if .Picture}}{{end}}
                {{if .Name}}
{{.Name}}
{{end}}
              {{end}}

              Logout
{{end}}

{{define "login"}}
              {{ range $providerName, $opts := .Config.Oauth }}

                   Sign in with {{ $providerName | ucfirst }}

              {{end}}

              {{if and (not (eq (len .Config.Backends) 0)) (not (eq (len .Config.Oauth) 0))}}

or

              {{end}}

              {{if not (eq (len .Config.Backends) 0) }}

Sign in

                      {{ if .Failure}}
Invalid credentials
{{end}}

		    <form accept-charset="UTF-8" role="form" method="POST" action="{{.Config.LoginPath}}">
                      <fieldset>

		          <input class="form-control" placeholder="Username" name="username" value="{{.UserInfo.Sub}}" type="text">

		          <input class="form-control" placeholder="Password" name="password" type="password" value="">

		        <input class="btn btn-lg btn-success btn-block" type="submit" value="Login">
		      </fieldset>
		    </form>

              {{end}}
{{end}}`

var layout = `<!DOCTYPE html>

    <meta name="viewport" content="width=device-width, initial-scale=1">
    {{ template "styles" . }}

    <uic-fragment name="content">

            {{ if .Error}}

                Internal Error.  Please try again later.

            {{end}}

            {{if .Authenticated}}

              {{template "userInfo" . }}

            {{else}}

              {{template "login" . }}

            {{end}}

    </uic-fragment>

`

type loginFormData struct {
	Error         bool
	Failure       bool
	Config        *Config
	Authenticated bool
	UserInfo      model.UserInfo
}

func writeLoginForm(w http.ResponseWriter, params loginFormData) {
	funcMap := template.FuncMap{
		"ucfirst":   ucfirst,
		"trimRight": strings.TrimRight,
	}
	templateName := "loginForm"
	if params.Config != nil && params.Config.Template != "" {
		templateName = params.Config.Template
	}
	t := template.New(templateName).Funcs(funcMap)
	t = template.Must(t.Parse(partials))
	if params.Config != nil && params.Config.Template != "" {
		customTemplate, err := ioutil.ReadFile(params.Config.Template)
		if err != nil {
			logging.Logger.WithError(err).Error()
			w.WriteHeader(500)
			w.Write([]byte(`Internal Server Error`))
			return
		}

		t, err = t.Parse(string(customTemplate))
		if err != nil {
			logging.Logger.WithError(err).Error()
			w.WriteHeader(500)
			w.Write([]byte(`Internal Server Error`))
			return
		}
	} else {
		t = template.Must(t.Parse(layout))
	}

	b := bytes.NewBuffer(nil)
	err := t.Execute(b, params)
	if err != nil {
		logging.Logger.WithError(err).Error()
		w.WriteHeader(500)
		w.Write([]byte(`Internal Server Error`))
		return
	}

	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
	w.Header().Set("Content-Type", contentTypeHTML)
	if params.Error {
		w.WriteHeader(500)
	}

	w.Write(b.Bytes())
}

func ucfirst(in string) string {
	if in == "" {
		return ""
	}

	return strings.ToUpper(in[0:1]) + in[1:]
}
<|endoftext|>


<commit_before>// Copyright 2016 Circonus, Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"
)

var (
	testCheckBundleMetrics = CheckBundleMetrics{
		CID: "/check_bundle_metrics/1234",
		Metrics: []CheckBundleMetric{
			CheckBundleMetric{Name: "foo", Type: "numeric", Status: "active"},
			CheckBundleMetric{Name: "bar", Type: "histogram", Status: "active"},
			CheckBundleMetric{Name: "baz", Type: "text", Status: "available"},
			CheckBundleMetric{Name: "fum", Type: "composite", Status: "active", Tags: []string{"cat:tag"}},
			CheckBundleMetric{Name: "zot", Type: "caql", Status: "active", Units: "milliseconds"},
		},
	}
)

func testCheckBundleMetricsServer() *httptest.Server {
	f := func(w http.ResponseWriter, r *http.Request) {
		path := r.URL.Path
		if path == "/check_bundle_metrics/1234" {
			switch r.Method {
			case "GET":
				ret, err := json.Marshal(testCheckBundleMetrics)
				if err != nil {
					panic(err)
				}
				w.WriteHeader(200)
				w.Header().Set("Content-Type", "application/json")
				fmt.Fprintln(w, string(ret))
			case "PUT":
				defer r.Body.Close()
				b, err := ioutil.ReadAll(r.Body)
				if err != nil {
					panic(err)
				}
				w.WriteHeader(200)
				w.Header().Set("Content-Type", "application/json")
				fmt.Fprintln(w, string(b))
			default:
				w.WriteHeader(404)
				fmt.Fprintln(w, fmt.Sprintf("not found: %s %s", r.Method, path))
			}
		} else {
			w.WriteHeader(404)
			fmt.Fprintln(w, fmt.Sprintf("not found: %s %s", r.Method, path))
		}
	}

	return httptest.NewServer(http.HandlerFunc(f))
}

func TestFetchCheckBundleMetrics(t *testing.T) {
	server := testCheckBundleMetricsServer()
	defer server.Close()

	ac := &Config{
		TokenKey: "abc123",
		TokenApp: "test",
		URL:      server.URL,
	}
	apih, err := NewAPI(ac)
	if err != nil {
		t.Errorf("Expected no error, got '%v'", err)
	}

	t.Log("without CID")
	{
		cid := ""
		expectedError := errors.New("Invalid check bundle metrics CID [none]")
		_, err := apih.FetchCheckBundleMetrics(CIDType(&cid))
		if err == nil {
			t.Fatalf("Expected error")
		}
		if err.Error() != expectedError.Error() {
			t.Fatalf("Expected %+v got '%+v'", expectedError, err)
		}
	}

	t.Log("with valid CID")
	{
		cid := "/check_bundle_metrics/1234"
		metrics, err := apih.FetchCheckBundleMetrics(CIDType(&cid))
		if err != nil {
			t.Fatalf("Expected no error, got '%v'", err)
		}

		actualType := reflect.TypeOf(metrics)
		expectedType := "*api.CheckBundleMetrics"
		if actualType.String() != expectedType {
			t.Fatalf("Expected %s, got %s", expectedType, actualType.String())
		}

		if metrics.CID != testCheckBundleMetrics.CID {
			t.Fatalf("CIDs do not match: %+v != %+v\n", metrics, testCheckBundleMetrics)
		}
	}

	t.Log("with invalid CID")
	{
		cid := "/invalid"
		expectedError := errors.New("Invalid check bundle metrics CID [/invalid]")
		_, err := apih.FetchCheckBundleMetrics(CIDType(&cid))
		if err == nil {
			t.Fatalf("Expected error")
		}
		if err.Error() != expectedError.Error() {
			t.Fatalf("Expected %+v got '%+v'", expectedError, err)
		}
	}
}

func TestUpdateCheckBundleMetrics(t *testing.T) {
	server := testCheckBundleMetricsServer()
	defer server.Close()

	var apih *API

	ac := &Config{
		TokenKey: "abc123",
		TokenApp: "test",
		URL:      server.URL,
	}
	apih, err := NewAPI(ac)
	if err != nil {
		t.Errorf("Expected no error, got '%v'", err)
	}

	t.Log("valid CheckBundleMetrics")
	{
		metrics, err := apih.UpdateCheckBundleMetrics(&testCheckBundleMetrics)
		if err != nil {
			t.Fatalf("Expected no error, got '%v'", err)
		}

		actualType := reflect.TypeOf(metrics)
		expectedType := "*api.CheckBundleMetrics"
		if actualType.String() != expectedType {
			t.Fatalf("Expected %s, got %s", expectedType, actualType.String())
		}
	}

	t.Log("Test with invalid CID")
	{
		expectedError := errors.New("Invalid check bundle metrics CID [/invalid]")
		x := &CheckBundleMetrics{CID: "/invalid"}
		_, err := apih.UpdateCheckBundleMetrics(x)
		if err == nil {
			t.Fatal("Expected an error")
		}
		if err.Error() != expectedError.Error() {
			t.Fatalf("Expected %+v got '%+v'", expectedError, err)
		}
	}
}
<commit_msg>upd: increase test coverage<commit_after>// Copyright 2016 Circonus, Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"
)

var (
	testCheckBundleMetrics = CheckBundleMetrics{
		CID: "/check_bundle_metrics/1234",
		Metrics: []CheckBundleMetric{
			CheckBundleMetric{Name: "foo", Type: "numeric", Status: "active"},
			CheckBundleMetric{Name: "bar", Type: "histogram", Status: "active"},
			CheckBundleMetric{Name: "baz", Type: "text", Status: "available"},
			CheckBundleMetric{Name: "fum", Type: "composite", Status: "active", Tags: []string{"cat:tag"}},
			CheckBundleMetric{Name: "zot", Type: "caql", Status: "active", Units: "milliseconds"},
		},
	}
)

func testCheckBundleMetricsServer() *httptest.Server {
	f := func(w http.ResponseWriter, r *http.Request) {
		path := r.URL.Path
		if path == "/check_bundle_metrics/1234" {
			switch r.Method {
			case "GET":
				ret, err := json.Marshal(testCheckBundleMetrics)
				if err != nil {
					panic(err)
				}
				w.WriteHeader(200)
				w.Header().Set("Content-Type", "application/json")
				fmt.Fprintln(w, string(ret))
			case "PUT":
				defer r.Body.Close()
				b, err := ioutil.ReadAll(r.Body)
				if err != nil {
					panic(err)
				}
				w.WriteHeader(200)
				w.Header().Set("Content-Type", "application/json")
				fmt.Fprintln(w, string(b))
			default:
				w.WriteHeader(404)
				fmt.Fprintln(w, fmt.Sprintf("not found: %s %s", r.Method, path))
			}
		} else {
			w.WriteHeader(404)
			fmt.Fprintln(w, fmt.Sprintf("not found: %s %s", r.Method, path))
		}
	}

	return httptest.NewServer(http.HandlerFunc(f))
}

func TestFetchCheckBundleMetrics(t *testing.T) {
	server := testCheckBundleMetricsServer()
	defer server.Close()

	ac := &Config{
		TokenKey: "abc123",
		TokenApp: "test",
		URL:      server.URL,
	}
	apih, err := NewAPI(ac)
	if err != nil {
		t.Errorf("Expected no error, got '%v'", err)
	}

	t.Log("invalid CID [nil]")
	{
		expectedError := errors.New("Invalid check bundle metrics CID [none]")
		_, err := apih.FetchCheckBundleMetrics(nil)
		if err == nil {
			t.Fatalf("Expected error")
		}
		if err.Error() != expectedError.Error() {
			t.Fatalf("Expected %+v got '%+v'", expectedError, err)
		}
	}

	t.Log("invalid CID [\"\"]")
	{
		cid := ""
		expectedError := errors.New("Invalid check bundle metrics CID [none]")
		_, err := apih.FetchCheckBundleMetrics(CIDType(&cid))
		if err == nil {
			t.Fatalf("Expected error")
		}
		if err.Error() != expectedError.Error() {
			t.Fatalf("Expected %+v got '%+v'", expectedError, err)
		}
	}

	t.Log("invalid CID [/invalid]")
	{
		cid := "/invalid"
		expectedError := errors.New("Invalid check bundle metrics CID [/invalid]")
		_, err := apih.FetchCheckBundleMetrics(CIDType(&cid))
		if err == nil {
			t.Fatalf("Expected error")
		}
		if err.Error() != expectedError.Error() {
			t.Fatalf("Expected %+v got '%+v'", expectedError, err)
		}
	}

	t.Log("valid CID")
	{
		cid := "/check_bundle_metrics/1234"
		metrics, err := apih.FetchCheckBundleMetrics(CIDType(&cid))
		if err != nil {
			t.Fatalf("Expected no error, got '%v'", err)
		}

		actualType := reflect.TypeOf(metrics)
		expectedType := "*api.CheckBundleMetrics"
		if actualType.String() != expectedType {
			t.Fatalf("Expected %s, got %s", expectedType, actualType.String())
		}

		if metrics.CID != testCheckBundleMetrics.CID {
			t.Fatalf("CIDs do not match: %+v != %+v\n", metrics, testCheckBundleMetrics)
		}
	}
}

func TestUpdateCheckBundleMetrics(t *testing.T) {
	server := testCheckBundleMetricsServer()
	defer server.Close()

	var apih *API

	ac := &Config{
		TokenKey: "abc123",
		TokenApp: "test",
		URL:      server.URL,
	}
	apih, err := NewAPI(ac)
	if err != nil {
		t.Errorf("Expected no error, got '%v'", err)
	}

	t.Log("invalid config [nil]")
	{
		expectedError := errors.New("Invalid check bundle metrics config [nil]")
		_, err := apih.UpdateCheckBundleMetrics(nil)
		if err == nil {
			t.Fatal("Expected an error")
		}
		if err.Error() != expectedError.Error() {
			t.Fatalf("Expected %+v got '%+v'", expectedError, err)
		}
	}

	t.Log("invalid config [CID /invalid]")
	{
		expectedError := errors.New("Invalid check bundle metrics CID [/invalid]")
		x := &CheckBundleMetrics{CID: "/invalid"}
		_, err := apih.UpdateCheckBundleMetrics(x)
		if err == nil {
			t.Fatal("Expected an error")
		}
		if err.Error() != expectedError.Error() {
			t.Fatalf("Expected %+v got '%+v'", expectedError, err)
		}
	}

	t.Log("valid config")
	{
		metrics, err := apih.UpdateCheckBundleMetrics(&testCheckBundleMetrics)
		if err != nil {
			t.Fatalf("Expected no error, got '%v'", err)
		}

		actualType := reflect.TypeOf(metrics)
		expectedType := "*api.CheckBundleMetrics"
		if actualType.String() != expectedType {
			t.Fatalf("Expected %s, got %s", expectedType, actualType.String())
		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"strings"

	"github.com/miekg/dns"
	"github.com/spf13/cobra"
)

type cmdForkDNS struct {
	global *cmdGlobal
}

type dnsHandler struct {
	domain  string
	servers []string
}

func (h *dnsHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
	msg := dns.Msg{}
	msg.SetReply(r)

	// We only support single questions for now
	if len(r.Question) != 1 {
		msg.SetRcode(r, dns.RcodeNameError)
		w.WriteMsg(&msg)
		return
	}

	// Rewrite the question to the internal domain
	origName := r.Question[0].Name
	newName := origName
	if strings.HasSuffix(r.Question[0].Name, fmt.Sprintf(".%s.", h.domain)) {
		newName = fmt.Sprintf("%s.__internal.", strings.SplitN(r.Question[0].Name, fmt.Sprintf(".%s.", h.domain), 2)[0])
	}

	// Query all the servers
	for _, server := range h.servers {
		// Send the request to the backend server
		r.Question[0].Name = newName
		resp, err := dns.Exchange(r, fmt.Sprintf("%s:53", server))
		r.Question[0].Name = origName
		if err != nil || len(resp.Answer) == 0 {
			// Error or empty response, try the next one
			continue
		}

		// Send back the answer
		answers := []dns.RR{}
		for _, answer := range resp.Answer {
			rr, err := dns.NewRR(strings.Replace(answer.String(), ".__internal.", fmt.Sprintf(".%s.", h.domain), -1))
			if err != nil {
				continue
			}

			answers = append(answers, rr)
		}
		msg.Answer = answers
		w.WriteMsg(&msg)
		return
	}

	// Fallback to NXDOMAIN
	msg.SetRcode(r, dns.RcodeNameError)
	w.WriteMsg(&msg)
}

func (c *cmdForkDNS) Command() *cobra.Command {
	// Main subcommand
	cmd := &cobra.Command{}
	cmd.Use = "forkdns <listen address> <domain> <servers...>"
	cmd.Short = "Internal DNS proxy for clustering"
	cmd.Long = `Description:
  Spawns a tiny DNS server which forwards to all upstream servers until
  one returns a valid record.
`
	cmd.RunE = c.Run
	cmd.Hidden = true

	return cmd
}

func (c *cmdForkDNS) Run(cmd *cobra.Command, args []string) error {
	// Sanity checks
	if len(args) < 3 {
		cmd.Help()

		if len(args) == 0 {
			return nil
		}

		return fmt.Errorf("Missing required arguments")
	}

	srv := &dns.Server{
		Addr: args[0],
		Net:  "udp",
	}

	srv.Handler = &dnsHandler{
		domain:  args[1],
		servers: args[2:],
	}

	err := srv.ListenAndServe()
	if err != nil {
		return fmt.Errorf("Failed to set udp listener: %v\n", err)
	}

	return nil
}
<commit_msg>forkdns: Answers PTR and A requests from leases file<commit_after>// forkdns provides a specialised DNS server designed for relaying A and PTR queries.
package main

import (
	"bufio"
	"errors"
	"fmt"
	"net"
	"os"
	"strings"

	"github.com/miekg/dns"
	"github.com/spf13/cobra"

	"github.com/lxc/lxd/shared/dnsutil"
	"github.com/lxc/lxd/shared/logger"
	"github.com/lxc/lxd/shared/logging"
)

type cmdForkDNS struct {
	global *cmdGlobal
}

type dnsHandler struct {
	domain    string
	leaseFile string
	servers   []string
}

func (h *dnsHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
	var err error
	msg := dns.Msg{}
	msg.SetReply(r)

	// We only support single questions for now
	if len(r.Question) != 1 {
		msg.SetRcode(r, dns.RcodeNameError)
	} else if r.Question[0].Qtype == dns.TypePTR {
		msg, err = h.handlePTR(r)
		if err != nil {
			logger.Errorf("PTR record lookup failed for %s: %v", r.Question[0].Name, err)
			msg.SetRcode(r, dns.RcodeNameError)
		}
	} else if r.Question[0].Qtype == dns.TypeA {
		msg, err = h.handleA(r)
		if err != nil {
			logger.Errorf("A record lookup failed for %s: %v", r.Question[0].Name, err)
			msg.SetRcode(r, dns.RcodeNameError)
		}
	} else {
		// Fallback to NXDOMAIN
		msg.SetRcode(r, dns.RcodeNameError)
	}

	err = w.WriteMsg(&msg)
	if err != nil {
		logger.Errorf("Failed sending response for %s: %v", r.Question[0].Name, err)
	}
}

// handlePTR answers requests for reverse DNS records.
// It is used with cluster networking to provide cluster wide DNS PTR resolution by consulting the
// local DHCP leases file and if not found, then relaying the question to the other cluster member's
// forkdns instance. Returns DNS message to be sent as response.
func (h *dnsHandler) handlePTR(r *dns.Msg) (dns.Msg, error) {
	msg := dns.Msg{}
	msg.SetReply(r)

	// If request is marked as non-recursive it means it is from another forkdns and we should
	// attempt to answer it using the local dnsmasq lease file and not relay it.
	if !r.RecursionDesired {
		// Check if the local DHCP leases file contains a lease for the requested reverse DNS name.
		// If this fails with an error or no record found, then check other cluster hosts.
		hostname, err := h.getLeaseHostByReverseIPName(r.Question[0].Name)
		if err != nil {
			logger.Errorf("PTR record lease lookup failed for %s: %v", r.Question[0].Name, err)
		}

		// Record found in local DHCP leases file, generate answer response and send.
		if hostname != "" {
			msg.Authoritative = true
			msg.Answer = append(msg.Answer, &dns.PTR{
				Hdr: dns.RR_Header{
					Name:   r.Question[0].Name,
					Rrtype: dns.TypePTR,
					Class:  dns.ClassINET,
					Ttl:    0,
				},
				// Suffix the hostname in the lease with the cluster DNS zone name (e.g. ".lxd.")
				// The final full stop is important as the response needs to be a FQDN.
				Ptr: fmt.Sprintf("%s.%s.", hostname, h.domain),
			})

			return msg, nil
		}

		// Record not found locally, return NXDOMAIN.
		msg.SetRcode(r, dns.RcodeNameError)
		return msg, nil
	}

	// If we get here, then the recursion desired flag was set, meaning we cannot answer the
	// query locally and need to relay it to the other forkdns instances.

	// This tells the remote node we only want to query their local data (to stop loops).
	r.RecursionDesired = false

	// Query all the servers
	for _, server := range h.servers {
		resp, err := dns.Exchange(r, fmt.Sprintf("%s:1053", server))
		if err != nil || len(resp.Answer) == 0 {
			// Error or empty response, try the next one
			continue
		}

		return *resp, nil
	}

	// Record not found in any of the remove servers.
	msg.SetRcode(r, dns.RcodeNameError)
	return msg, nil
}

// getLeaseHostByReverseIPName finds the hostname used in the DHCP lease by supplying a reverse
// DNS hostname of the device's IP.
func (h *dnsHandler) getLeaseHostByReverseIPName(reverseName string) (string, error) {
	ip := dnsutil.ExtractAddressFromReverse(reverseName)
	if ip == "" {
		return "", errors.New("Failed to convert reverse name to IP")
	}

	file, err := os.Open(h.leaseFile)
	if err != nil {
		return "", err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) >= 5 {
			address := fields[2]
			if address == ip {
				return fields[3], nil
			}
		}
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}

	return "", nil
}

// handleA answers requests for A DNS records.
// It is used with cluster networking to provide cluster wide DNS A resolution by consulting the
// local DHCP leases file and if not found, then relaying the question to the other cluster member's
// forkdns instance. Returns DNS message to be sent as response.
func (h *dnsHandler) handleA(r *dns.Msg) (dns.Msg, error) {
	msg := dns.Msg{}
	msg.SetReply(r)

	// If request is marked as non-recursive it means it is from another forkdns and we should
	// attempt to answer it using the local dnsmasq lease file and not relay it.
	if !r.RecursionDesired {
		// Check if the local DHCP leases file contains a lease for the requested hostname name.
		// If this fails with an error or no record found, then check other cluster hosts.
		ip, err := h.getLeaseHostByDNSName(r.Question[0].Name)
		if err != nil {
			logger.Errorf("A record lease lookup failed for %s: %v", r.Question[0].Name, err)
		}

		// Record found in local DHCP leases file, generate answer response and send.
		if ip != "" {
			msg.Authoritative = true
			msg.Answer = append(msg.Answer, &dns.A{
				Hdr: dns.RR_Header{
					Name:   r.Question[0].Name,
					Rrtype: dns.TypeA,
					Class:  dns.ClassINET,
					Ttl:    0,
				},
				A: net.ParseIP(ip),
			})

			return msg, nil
		}

		// Record not found locally, return NXDOMAIN.
		msg.SetRcode(r, dns.RcodeNameError)
		return msg, nil
	}

	// If we get here, then the recursion desired flag was set, meaning we cannot answer the
	// query locally and need to relay it to the other forkdns instances.

	// This tells the remote node we only want to query their local data (to stop loops).
	r.RecursionDesired = false

	// Query all the servers
	for _, server := range h.servers {
		resp, err := dns.Exchange(r, fmt.Sprintf("%s:1053", server))
		if err != nil || len(resp.Answer) == 0 {
			// Error or empty response, try the next one
			continue
		}

		return *resp, nil
	}

	// Record not found in any of the remove servers.
	msg.SetRcode(r, dns.RcodeNameError)
	return msg, nil
}

// getLeaseHostByDNSName finds the hostname used in the DHCP lease by supplying a DNS A name
func (h *dnsHandler) getLeaseHostByDNSName(dnsName string) (string, error) {
	host := strings.TrimSuffix(dnsName, fmt.Sprintf(".%s.", h.domain))

	file, err := os.Open(h.leaseFile)
	if err != nil {
		return "", err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) >= 5 {
			hostname := fields[3]
			if hostname == host {
				return fields[2], nil
			}
		}
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}

	return "", nil
}

func (c *cmdForkDNS) Command() *cobra.Command {
	// Main subcommand
	cmd := &cobra.Command{}
	cmd.Use = "forkdns <listen address> <domain> <leases file> <servers...>"
	cmd.Short = "Internal DNS proxy for clustering"
	cmd.Long = `Description:
  Spawns a specialised DNS server designed for relaying A and PTR queries that cannot be answered by
  the local dnsmasq process to the other cluster member's forkdns process where it will inspect the
  local dnsmasq lease file looking for an answer to the query.
  It uses the "recursion desired" flag in incoming DNS requests to modify its behaviour.
  When "recursion desired" is set to yes, the query is immediately relayed to the other cluster nodes
  (with the "recursion desired" flag set to no) as it indicates that the local dnsmasq process was
  unable to answer it from the local lease file.
  When "recursion desired" flag is set to no, this indicates the request has been sent from another
  forkdns process, and the local dnsmasq lease file only is parsed to try and answer the query.
`
	cmd.RunE = c.Run
	cmd.Hidden = true

	return cmd
}

func (c *cmdForkDNS) Run(cmd *cobra.Command, args []string) error {
	// Sanity checks
	if len(args) < 4 {
		cmd.Help()

		if len(args) == 0 {
			return nil
		}

		return fmt.Errorf("Missing required arguments")
	}

	log, err := logging.GetLogger("lxd-forkdns", "", false, false, eventsHandler{})
	if err != nil {
		return err
	}
	logger.Log = log
	logger.Info("Started")

	srv := &dns.Server{
		Addr: args[0],
		Net:  "udp",
	}

	srv.Handler = &dnsHandler{
		domain:    args[1],
		leaseFile: args[2],
		servers:   args[3:],
	}

	err = srv.ListenAndServe()
	if err != nil {
		return fmt.Errorf("Failed to set udp listener: %v\n", err)
	}

	return nil
}
<|endoftext|>


<commit_before>// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package job

import (
	"encoding/json"
	"sort"
	"time"

	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes"
	"google.golang.org/protobuf/types/known/durationpb"

	bbpb "go.chromium.org/luci/buildbucket/proto"
	"go.chromium.org/luci/common/data/stringset"
	"go.chromium.org/luci/common/data/strpair"
	"go.chromium.org/luci/common/errors"
	swarmingpb "go.chromium.org/luci/swarming/proto/api"
)

type buildbucketEditor struct {
	jd *Definition
	bb *Buildbucket

	err error
}

var _ HighLevelEditor = (*buildbucketEditor)(nil)

func newBuildbucketEditor(jd *Definition) *buildbucketEditor {
	bb := jd.GetBuildbucket()
	if bb == nil {
		panic(errors.New("impossible: only supported for Buildbucket builds"))
	}
	bb.EnsureBasics()

	return &buildbucketEditor{jd, bb, nil}
}

func (bbe *buildbucketEditor) Close() error {
	return bbe.err
}

func (bbe *buildbucketEditor) tweak(fn func() error) {
	if bbe.err == nil {
		bbe.err = fn()
	}
}

func (bbe *buildbucketEditor) Tags(values []string) {
	if len(values) == 0 {
		return
	}

	bbe.tweak(func() (err error) {
		if err = validateTags(values); err == nil {
			tags := bbe.bb.BbagentArgs.Build.Tags
			for _, tag := range values {
				k, v := strpair.Parse(tag)
				tags = append(tags, &bbpb.StringPair{
					Key:   k,
					Value: v,
				})
			}
			sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key })
			bbe.bb.BbagentArgs.Build.Tags = tags
		}
		return nil
	})
}

func (bbe *buildbucketEditor) TaskPayloadSource(cipdPkg, cipdVers string) {
	bbe.tweak(func() error {
		exe := bbe.bb.BbagentArgs.Build.Exe
		if cipdPkg != "" {
			exe.CipdPackage = cipdPkg
			if cipdVers == "" {
				exe.CipdVersion = "latest"
			} else {
				exe.CipdVersion = cipdVers
			}
		} else if cipdPkg == "" && cipdVers == "" {
			exe.CipdPackage = ""
			exe.CipdVersion = ""
		} else {
			return errors.Reason(
				"cipdPkg and cipdVers must both be set or both be empty: cipdPkg=%q cipdVers=%q",
				cipdPkg, cipdVers).Err()
		}
		return nil
	})
}

func (bbe *buildbucketEditor) TaskPayloadPath(path string) {
	bbe.tweak(func() error {
		bbe.bb.UpdatePayloadPath(path)
		return nil
	})
}

func (bbe *buildbucketEditor) CASTaskPayload(path string, casRef *swarmingpb.CASReference) {
	bbe.tweak(func() error {
		if path != "" {
			bbe.TaskPayloadPath(path)
		} else {
			purposes := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.GetAgent().GetPurposes()
			for dir, pur := range purposes {
				if pur == bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD {
					path = dir
					break
				}
			}
		}

		if path == "" {
			return errors.Reason("failed to get exe payload path").Err()
		}

		input := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input
		if input == nil {
			input = &bbpb.BuildInfra_Buildbucket_Agent_Input{}
			bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input = input
		}
		inputData := input.GetData()
		if len(inputData) == 0 {
			inputData = make(map[string]*bbpb.InputDataRef)
			input.Data = inputData
		}

		if ref, ok := inputData[path]; ok && ref.GetCas() != nil {
			if casRef.CasInstance != "" {
				ref.GetCas().CasInstance = casRef.CasInstance
			}
			ref.GetCas().Digest = &bbpb.InputDataRef_CAS_Digest{
				Hash:      casRef.GetDigest().GetHash(),
				SizeBytes: casRef.GetDigest().GetSizeBytes(),
			}
		} else {
			inputData[path] = &bbpb.InputDataRef{
				DataType: &bbpb.InputDataRef_Cas{
					Cas: &bbpb.InputDataRef_CAS{
						CasInstance: casRef.CasInstance,
						Digest: &bbpb.InputDataRef_CAS_Digest{
							Hash:      casRef.GetDigest().GetHash(),
							SizeBytes: casRef.GetDigest().GetSizeBytes(),
						},
					},
				},
			}
		}
		return nil
	})
}

func (bbe *buildbucketEditor) TaskPayloadCmd(args []string) {
	bbe.tweak(func() error {
		if len(args) == 0 {
			args = []string{"luciexe"}
		}
		bbe.bb.BbagentArgs.Build.Exe.Cmd = args
		return nil
	})
}

func (bbe *buildbucketEditor) ClearCurrentIsolated() {
	bbe.tweak(func() error {
		agent := bbe.bb.BbagentArgs.Build.GetInfra().GetBuildbucket().GetAgent()
		if agent == nil {
			return nil
		}

		payloadPath := ""
		for p, purpose := range agent.GetPurposes() {
			if purpose == bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD {
				payloadPath = p
				break
			}
		}
		if payloadPath == "" {
			return nil
		}
		inputData := agent.GetInput().GetData()
		if ref, ok := inputData[payloadPath]; ok {
			if ref.GetCas() != nil {
				delete(inputData, payloadPath)
			}
		}
		return nil
	})
}

func (bbe *buildbucketEditor) ClearDimensions() {
	bbe.tweak(func() error {
		bbe.bb.BbagentArgs.Build.Infra.Swarming.TaskDimensions = nil
		return nil
	})
}

func (bbe *buildbucketEditor) SetDimensions(dims ExpiringDimensions) {
	bbe.ClearDimensions()
	dec := DimensionEditCommands{}
	for key, vals := range dims {
		dec[key] = &DimensionEditCommand{SetValues: vals}
	}
	bbe.EditDimensions(dec)
}

func (bbe *buildbucketEditor) EditDimensions(dimEdits DimensionEditCommands) {
	if len(dimEdits) == 0 {
		return
	}

	bbe.tweak(func() error {
		dims, err := bbe.jd.Info().Dimensions()
		if err != nil {
			return err
		}

		dimMap := dims.toLogical()
		dimEdits.apply(dimMap, 0)

		sw := bbe.bb.BbagentArgs.Build.Infra.Swarming
		var maxExp time.Duration
		newDims := make([]*bbpb.RequestedDimension, 0,
			len(sw.TaskDimensions)+len(dimEdits))
		for _, key := range keysOf(dimMap) {
			valueExp := dimMap[key]
			for _, value := range keysOf(valueExp) {
				exp := valueExp[value]
				if exp > maxExp {
					maxExp = exp
				}

				toAdd := &bbpb.RequestedDimension{
					Key:   key,
					Value: value,
				}
				if exp > 0 {
					toAdd.Expiration = durationpb.New(exp)
				}
				newDims = append(newDims, toAdd)
			}
		}
		sw.TaskDimensions = newDims

		build := bbe.bb.BbagentArgs.Build
		var curTimeout time.Duration
		if build.SchedulingTimeout != nil {
			var err error
			if curTimeout, err = ptypes.Duration(build.SchedulingTimeout); err != nil {
				return err
			}
		}
		if maxExp > curTimeout {
			build.SchedulingTimeout = durationpb.New(maxExp)
		}
		return nil
	})
}

func (bbe *buildbucketEditor) Env(env map[string]string) {
	if len(env) == 0 {
		return
	}

	bbe.tweak(func() error {
		updateStringPairList(&bbe.bb.EnvVars, env)
		return nil
	})
}

func (bbe *buildbucketEditor) Priority(priority int32) {
	bbe.tweak(func() error {
		if priority < 0 {
			return errors.Reason("negative Priority argument: %d", priority).Err()
		}

		bbe.bb.BbagentArgs.Build.Infra.Swarming.Priority = priority
		return nil
	})
}

func (bbe *buildbucketEditor) Properties(props map[string]string, auto bool) {
	if len(props) == 0 {
		return
	}
	bbe.tweak(func() error {
		toWrite := map[string]interface{}{}

		for k, v := range props {
			if v == "" {
				toWrite[k] = nil
			} else {
				var obj interface{}
				if err := json.Unmarshal([]byte(v), &obj); err != nil {
					if !auto {
						return err
					}
					obj = v
				}
				toWrite[k] = obj
			}
		}

		bbe.bb.WriteProperties(toWrite)
		return nil
	})
}

func (bbe *buildbucketEditor) CIPDPkgs(cipdPkgs CIPDPkgs) {
	bbe.tweak(func() error {
		if !bbe.bb.BbagentDownloadCIPDPkgs() {
			cipdPkgs.updateCipdPkgs(&bbe.bb.CipdPackages)
			return nil
		}
		return errors.Reason("not supported for Buildbucket v2 builds").Err()
	})
}

func (bbe *buildbucketEditor) SwarmingHostname(host string) {
	bbe.tweak(func() (err error) {
		if host == "" {
			return errors.New("empty SwarmingHostname")
		}

		bbe.bb.BbagentArgs.Build.Infra.Swarming.Hostname = host
		return
	})
}

func (bbe *buildbucketEditor) TaskName(name string) {
	bbe.tweak(func() (err error) {
		bbe.bb.Name = name
		return
	})
}

func (bbe *buildbucketEditor) Experimental(isExperimental bool) {
	bbe.tweak(func() error {
		bbe.bb.BbagentArgs.Build.Input.Experimental = isExperimental
		return nil
	})
}

func (bbe *buildbucketEditor) Experiments(exps map[string]bool) {
	bbe.tweak(func() error {
		enabled := stringset.NewFromSlice(bbe.bb.BbagentArgs.Build.Input.Experiments...)
		for k, v := range exps {
			if v {
				enabled.Add(k)
			} else {
				enabled.Del(k)
			}
		}
		bbe.bb.BbagentArgs.Build.Input.Experiments = enabled.ToSortedSlice()
		return nil
	})
}

func (bbe *buildbucketEditor) PrefixPathEnv(values []string) {
	if len(values) == 0 {
		return
	}

	bbe.tweak(func() error {
		updatePrefixPathEnv(values, &bbe.bb.EnvPrefixes)
		return nil
	})
}

func (bbe *buildbucketEditor) ClearGerritChanges() {
	bbe.tweak(func() error {
		bbe.bb.BbagentArgs.Build.Input.GerritChanges = nil
		return nil
	})
}

func (bbe *buildbucketEditor) AddGerritChange(cl *bbpb.GerritChange) {
	if cl == nil {
		return
	}

	bbe.tweak(func() error {
		gc := &bbe.bb.BbagentArgs.Build.Input.GerritChanges
		for _, change := range *gc {
			if proto.Equal(change, cl) {
				return nil
			}
		}
		*gc = append(*gc, cl)
		return nil
	})
}

func (bbe *buildbucketEditor) RemoveGerritChange(cl *bbpb.GerritChange) {
	if cl == nil {
		return
	}

	bbe.tweak(func() error {
		gc := &bbe.bb.BbagentArgs.Build.Input.GerritChanges
		for idx, change := range *gc {
			if proto.Equal(change, cl) {
				*gc = append((*gc)[:idx], (*gc)[idx+1:]...)
				return nil
			}
		}
		return nil
	})
}

func (bbe *buildbucketEditor) GitilesCommit(commit *bbpb.GitilesCommit) {
	bbe.tweak(func() error {
		bbe.bb.BbagentArgs.Build.Input.GitilesCommit = commit
		return nil
	})
}
<commit_msg>[led] edit-cipd-pkgs: do not return error if nothing to change<commit_after>// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package job

import (
	"encoding/json"
	"sort"
	"time"

	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes"
	"google.golang.org/protobuf/types/known/durationpb"

	bbpb "go.chromium.org/luci/buildbucket/proto"
	"go.chromium.org/luci/common/data/stringset"
	"go.chromium.org/luci/common/data/strpair"
	"go.chromium.org/luci/common/errors"
	swarmingpb "go.chromium.org/luci/swarming/proto/api"
)

type buildbucketEditor struct {
	jd *Definition
	bb *Buildbucket

	err error
}

var _ HighLevelEditor = (*buildbucketEditor)(nil)

func newBuildbucketEditor(jd *Definition) *buildbucketEditor {
	bb := jd.GetBuildbucket()
	if bb == nil {
		panic(errors.New("impossible: only supported for Buildbucket builds"))
	}
	bb.EnsureBasics()

	return &buildbucketEditor{jd, bb, nil}
}

func (bbe *buildbucketEditor) Close() error {
	return bbe.err
}

func (bbe *buildbucketEditor) tweak(fn func() error) {
	if bbe.err == nil {
		bbe.err = fn()
	}
}

func (bbe *buildbucketEditor) Tags(values []string) {
	if len(values) == 0 {
		return
	}

	bbe.tweak(func() (err error) {
		if err = validateTags(values); err == nil {
			tags := bbe.bb.BbagentArgs.Build.Tags
			for _, tag := range values {
				k, v := strpair.Parse(tag)
				tags = append(tags, &bbpb.StringPair{
					Key:   k,
					Value: v,
				})
			}
			sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key })
			bbe.bb.BbagentArgs.Build.Tags = tags
		}
		return nil
	})
}

func (bbe *buildbucketEditor) TaskPayloadSource(cipdPkg, cipdVers string) {
	bbe.tweak(func() error {
		exe := bbe.bb.BbagentArgs.Build.Exe
		if cipdPkg != "" {
			exe.CipdPackage = cipdPkg
			if cipdVers == "" {
				exe.CipdVersion = "latest"
			} else {
				exe.CipdVersion = cipdVers
			}
		} else if cipdPkg == "" && cipdVers == "" {
			exe.CipdPackage = ""
			exe.CipdVersion = ""
		} else {
			return errors.Reason(
				"cipdPkg and cipdVers must both be set or both be empty: cipdPkg=%q cipdVers=%q",
				cipdPkg, cipdVers).Err()
		}
		return nil
	})
}

func (bbe *buildbucketEditor) TaskPayloadPath(path string) {
	bbe.tweak(func() error {
		bbe.bb.UpdatePayloadPath(path)
		return nil
	})
}

func (bbe *buildbucketEditor) CASTaskPayload(path string, casRef *swarmingpb.CASReference) {
	bbe.tweak(func() error {
		if path != "" {
			bbe.TaskPayloadPath(path)
		} else {
			purposes := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.GetAgent().GetPurposes()
			for dir, pur := range purposes {
				if pur == bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD {
					path = dir
					break
				}
			}
		}

		if path == "" {
			return errors.Reason("failed to get exe payload path").Err()
		}

		input := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input
		if input == nil {
			input = &bbpb.BuildInfra_Buildbucket_Agent_Input{}
			bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input = input
		}
		inputData := input.GetData()
		if len(inputData) == 0 {
			inputData = make(map[string]*bbpb.InputDataRef)
			input.Data = inputData
		}

		if ref, ok := inputData[path]; ok && ref.GetCas() != nil {
			if casRef.CasInstance != "" {
				ref.GetCas().CasInstance = casRef.CasInstance
			}
			ref.GetCas().Digest = &bbpb.InputDataRef_CAS_Digest{
				Hash:      casRef.GetDigest().GetHash(),
				SizeBytes: casRef.GetDigest().GetSizeBytes(),
			}
		} else {
			inputData[path] = &bbpb.InputDataRef{
				DataType: &bbpb.InputDataRef_Cas{
					Cas: &bbpb.InputDataRef_CAS{
						CasInstance: casRef.CasInstance,
						Digest: &bbpb.InputDataRef_CAS_Digest{
							Hash:      casRef.GetDigest().GetHash(),
							SizeBytes: casRef.GetDigest().GetSizeBytes(),
						},
					},
				},
			}
		}
		return nil
	})
}

func (bbe *buildbucketEditor) TaskPayloadCmd(args []string) {
	bbe.tweak(func() error {
		if len(args) == 0 {
			args = []string{"luciexe"}
		}
		bbe.bb.BbagentArgs.Build.Exe.Cmd = args
		return nil
	})
}

func (bbe *buildbucketEditor) ClearCurrentIsolated() {
	bbe.tweak(func() error {
		agent := bbe.bb.BbagentArgs.Build.GetInfra().GetBuildbucket().GetAgent()
		if agent == nil {
			return nil
		}

		payloadPath := ""
		for p, purpose := range agent.GetPurposes() {
			if purpose == bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD {
				payloadPath = p
				break
			}
		}
		if payloadPath == "" {
			return nil
		}
		inputData := agent.GetInput().GetData()
		if ref, ok := inputData[payloadPath]; ok {
			if ref.GetCas() != nil {
				delete(inputData, payloadPath)
			}
		}
		return nil
	})
}

func (bbe *buildbucketEditor) ClearDimensions() {
	bbe.tweak(func() error {
		bbe.bb.BbagentArgs.Build.Infra.Swarming.TaskDimensions = nil
		return nil
	})
}

func (bbe *buildbucketEditor) SetDimensions(dims ExpiringDimensions) {
	bbe.ClearDimensions()
	dec := DimensionEditCommands{}
	for key, vals := range dims {
		dec[key] = &DimensionEditCommand{SetValues: vals}
	}
	bbe.EditDimensions(dec)
}

func (bbe *buildbucketEditor) EditDimensions(dimEdits DimensionEditCommands) {
	if len(dimEdits) == 0 {
		return
	}

	bbe.tweak(func() error {
		dims, err := bbe.jd.Info().Dimensions()
		if err != nil {
			return err
		}

		dimMap := dims.toLogical()
		dimEdits.apply(dimMap, 0)

		sw := bbe.bb.BbagentArgs.Build.Infra.Swarming
		var maxExp time.Duration
		newDims := make([]*bbpb.RequestedDimension, 0,
			len(sw.TaskDimensions)+len(dimEdits))
		for _, key := range keysOf(dimMap) {
			valueExp := dimMap[key]
			for _, value := range keysOf(valueExp) {
				exp := valueExp[value]
				if exp > maxExp {
					maxExp = exp
				}

				toAdd := &bbpb.RequestedDimension{
					Key:   key,
					Value: value,
				}
				if exp > 0 {
					toAdd.Expiration = durationpb.New(exp)
				}
				newDims = append(newDims, toAdd)
			}
		}
		sw.TaskDimensions = newDims

		build := bbe.bb.BbagentArgs.Build
		var curTimeout time.Duration
		if build.SchedulingTimeout != nil {
			var err error
			if curTimeout, err = ptypes.Duration(build.SchedulingTimeout); err != nil {
				return err
			}
		}
		if maxExp > curTimeout {
			build.SchedulingTimeout = durationpb.New(maxExp)
		}
		return nil
	})
}

func (bbe *buildbucketEditor) Env(env map[string]string) {
	if len(env) == 0 {
		return
	}

	bbe.tweak(func() error {
		updateStringPairList(&bbe.bb.EnvVars, env)
		return nil
	})
}

func (bbe *buildbucketEditor) Priority(priority int32) {
	bbe.tweak(func() error {
		if priority < 0 {
			return errors.Reason("negative Priority argument: %d", priority).Err()
		}

		bbe.bb.BbagentArgs.Build.Infra.Swarming.Priority = priority
		return nil
	})
}

func (bbe *buildbucketEditor) Properties(props map[string]string, auto bool) {
	if len(props) == 0 {
		return
	}
	bbe.tweak(func() error {
		toWrite := map[string]interface{}{}

		for k, v := range props {
			if v == "" {
				toWrite[k] = nil
			} else {
				var obj interface{}
				if err := json.Unmarshal([]byte(v), &obj); err != nil {
					if !auto {
						return err
					}
					obj = v
				}
				toWrite[k] = obj
			}
		}

		bbe.bb.WriteProperties(toWrite)
		return nil
	})
}

func (bbe *buildbucketEditor) CIPDPkgs(cipdPkgs CIPDPkgs) {
	if len(cipdPkgs) == 0 {
		return
	}

	bbe.tweak(func() error {
		if !bbe.bb.BbagentDownloadCIPDPkgs() {
			cipdPkgs.updateCipdPkgs(&bbe.bb.CipdPackages)
			return nil
		}
		return errors.Reason("not supported for Buildbucket v2 builds").Err()
	})
}

func (bbe *buildbucketEditor) SwarmingHostname(host string) {
	bbe.tweak(func() (err error) {
		if host == "" {
			return errors.New("empty SwarmingHostname")
		}

		bbe.bb.BbagentArgs.Build.Infra.Swarming.Hostname = host
		return
	})
}

func (bbe *buildbucketEditor) TaskName(name string) {
	bbe.tweak(func() (err error) {
		bbe.bb.Name = name
		return
	})
}

func (bbe *buildbucketEditor) Experimental(isExperimental bool) {
	bbe.tweak(func() error {
		bbe.bb.BbagentArgs.Build.Input.Experimental = isExperimental
		return nil
	})
}

func (bbe *buildbucketEditor) Experiments(exps map[string]bool) {
	bbe.tweak(func() error {
		enabled := stringset.NewFromSlice(bbe.bb.BbagentArgs.Build.Input.Experiments...)
		for k, v := range exps {
			if v {
				enabled.Add(k)
			} else {
				enabled.Del(k)
			}
		}
		bbe.bb.BbagentArgs.Build.Input.Experiments = enabled.ToSortedSlice()
		return nil
	})
}

func (bbe *buildbucketEditor) PrefixPathEnv(values []string) {
	if len(values) == 0 {
		return
	}

	bbe.tweak(func() error {
		updatePrefixPathEnv(values, &bbe.bb.EnvPrefixes)
		return nil
	})
}

func (bbe *buildbucketEditor) ClearGerritChanges() {
	bbe.tweak(func() error {
		bbe.bb.BbagentArgs.Build.Input.GerritChanges = nil
		return nil
	})
}

func (bbe *buildbucketEditor) AddGerritChange(cl *bbpb.GerritChange) {
	if cl == nil {
		return
	}

	bbe.tweak(func() error {
		gc := &bbe.bb.BbagentArgs.Build.Input.GerritChanges
		for _, change := range *gc {
			if proto.Equal(change, cl) {
				return nil
			}
		}
		*gc = append(*gc, cl)
		return nil
	})
}

func (bbe *buildbucketEditor) RemoveGerritChange(cl *bbpb.GerritChange) {
	if cl == nil {
		return
	}

	bbe.tweak(func() error {
		gc := &bbe.bb.BbagentArgs.Build.Input.GerritChanges
		for idx, change := range *gc {
			if proto.Equal(change, cl) {
				*gc = append((*gc)[:idx], (*gc)[idx+1:]...)
				return nil
			}
		}
		return nil
	})
}

func (bbe *buildbucketEditor) GitilesCommit(commit *bbpb.GitilesCommit) {
	bbe.tweak(func() error {
		bbe.bb.BbagentArgs.Build.Input.GitilesCommit = commit
		return nil
	})
}
<|endoftext|>


<commit_before>// Router for Upwork API
//
// Licensed under the Upwork's API Terms of Use;
// you may not use this file except in compliance with the Terms.
//
// 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.
//
// Author::    Maksym Novozhylov (mnovozhilov@upwork.com)
// Copyright:: Copyright 2015(c) Upwork.com
// License::   See LICENSE.txt and TOS - https://developers.upwork.com/api-tos.html
package metadata

import (
    "net/http"
    "github.com/upwork/golang-upwork-oauth2/api"
)

const (
    EntryPoint = "api"
)

type a struct {
    client *api.ApiClient
}

// Constructor
func New(c *api.ApiClient) *a {
    c.SetEntryPoint(EntryPoint)

    return &a{c}
}

// Get categories (V2)
func (r a) GetCategoriesV2() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v2/metadata/categories", nil)
}

// Get skills
func (r a) GetSkills() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/skills", nil)
}

// Get skills (V2)
func (r a) GetSkillsV2() (*http.Response, []byte) {
    return r.client.Get("/profiles/v2/metadata/skills", nil)
}

// Get specialties
func (r a) GetSpecialties() (*http.Response, []byte) {
    return r.client.Get("/profiles/v1/metadata/specialties", nil)
}

// Get regions
func (r a) GetRegions() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/regions", nil)
}

// Get tests
func (r a) GetTests() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/tests", nil)
}

// Get reasons
func (r a) GetReasons(params map[string]string) (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/reasons", params)
}
<commit_msg>fix v2.1.0<commit_after>// Router for Upwork API
//
// Licensed under the Upwork's API Terms of Use;
// you may not use this file except in compliance with the Terms.
//
// 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.
//
// Author::    Maksym Novozhylov (mnovozhilov@upwork.com)
// Copyright:: Copyright 2015(c) Upwork.com
// License::   See LICENSE.txt and TOS - https://developers.upwork.com/api-tos.html
package metadata

import (
    "net/http"
    "github.com/upwork/golang-upwork-oauth2/api"
)

const (
    EntryPoint = "api"
)

type a struct {
    client *api.ApiClient
}

// Constructor
func New(c *api.ApiClient) *a {
    c.SetEntryPoint(EntryPoint)

    return &a{c}
}

// Get categories (V2)
func (r a) GetCategoriesV2() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v2/metadata/categories", nil)
}

// Get skills
func (r a) GetSkills() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/skills", nil)
}

// Get skills (V2)
func (r a) GetSkillsV2() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v2/metadata/skills", nil)
}

// Get specialties
func (r a) GetSpecialties() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/specialties", nil)
}

// Get regions
func (r a) GetRegions() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/regions", nil)
}

// Get tests
func (r a) GetTests() (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/tests", nil)
}

// Get reasons
func (r a) GetReasons(params map[string]string) (*http.Response, interface{}) {
    return r.client.Get("/profiles/v1/metadata/reasons", params)
}
<|endoftext|>