<commit_before>package main

import (
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"golang.org/x/net/context"

	"github.com/cenkalti/backoff"
	"github.com/go-kit/kit/log/level"
	"github.com/gorilla/handlers"
	"github.com/pkg/errors"
)

type stopChan chan struct{}

// RadioOptions enables some radio features
type RadioOptions struct {
	Broadcast bool
	Record    bool
}

// A Radio manages all the stations and recordings
type Radio struct {
	Server   *http.Server
	Presets  *Presets
	TapeDeck *TapeDeck
	Options  RadioOptions

	PathBroadcast string
	PathPreset    string

	stop stopChan
	wg   *sync.WaitGroup
}

// Record tunes into a station and records to a blank tape
func (r *Radio) StartRecording(s *Station) {
	r.wg.Add(1)
	defer r.wg.Done()

	// Use a context to provide cancellation of the http client
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		<-r.stop
		cancel()
	}()

	rec := func() error {
		stream, err := s.Tune(ctx)
		if err != nil {
			if strings.HasSuffix(err.Error(), "context canceled") {
				level.Debug(logger).Log(
					"msg", "canceled stream",
					"station", s.Name)
				return nil
			}

			level.Warn(logger).Log(
				"msg", "error tuning into station",
				"station", s.Name,
				"err", err)
			return err
		}

		tape, err := r.TapeDeck.BlankTape(ctx, s.Name, s.CurrentTime())
		if err != nil {
			level.Warn(logger).Log(
				"msg", "error loading blank tape",
				"err", err)
			return err
		}

		size := stream.Chunksize()
		level.Debug(logger).Log(
			"msg", fmt.Sprintf("Recording station with chunksize %d", size),
			"station", s.Name)

		if err := ChunkPipe(size, stream, tape); err != nil {
			if strings.HasSuffix(err.Error(), "context canceled") {
				level.Debug(logger).Log(
					"msg", "canceled stream",
					"station", s.Name)
				return nil
			}

			level.Warn(logger).Log(
				"msg", "error in chunkpipe",
				"station", s.Name,
				"err", err)
			return err
		}

		level.Debug(logger).Log(
			"msg", "chunkpipe returned",
			"station", s.Name)
		// ChunkPipe encountered an EOF but we should still retry
		return errors.New("ChunkPipe returned io.EOF")

	}

	b := backoff.NewExponentialBackOff()
	b.InitialInterval = 2 * time.Second
	b.RandomizationFactor = 0

	err := backoff.Retry(rec, b)
	if err != nil {
		level.Warn(logger).Log(
			"msg", "error after retrying",
			"err", err)
	}
}

// Turn on the radio and start recording presets
func (r *Radio) On() {
	level.Info(logger).Log(
		"msg", "Powering on the time machine",
		"record", r.Options.Record,
		"broadcast", r.Options.Broadcast)

	r.stop = make(stopChan)
	r.wg = &sync.WaitGroup{}
	r.wg.Add(1)

	if r.Options.Record {
		level.Info(logger).Log("msg", "Starting to record presets")

		stations, err := r.Presets.Load()
		if err != nil {
			level.Warn(logger).Log(
				"msg", "error loading presets",
				"err", err)
		}

		for _, s := range stations {
			s := s
			err = s.Init()
			if err != nil {
				level.Warn(logger).Log(
					"msg", "error loading preset",
					"station", s.Name,
					"err", err)
				continue
			}
			go r.StartRecording(&s)
		}

		// r.ManageRecordings(r.stop, r.wg)
	}

	if r.Options.Broadcast {
		r.Presets.RegisterServiceHandlers(r.PathPreset, http.DefaultServeMux)
		http.HandleFunc(r.PathBroadcast, r.Broadcast)

		// enable cors
		cors := handlers.CORS(
			handlers.AllowedHeaders([]string{"Content-Type"}),
			handlers.AllowedMethods([]string{"GET", "POST"}),
			handlers.AllowedOrigins([]string{"*"}))
		r.Server.Handler = cors(http.DefaultServeMux)

		level.Info(logger).Log("msg", "Starting broadcast and preset service")
	}

	// simple healthcheck
	http.HandleFunc("/", http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) },
	))

	go r.Server.ListenAndServe()
	level.Info(logger).Log("msg", "Time machine is operational")
}

func (r *Radio) Off() {
	level.Info(logger).Log("msg", "Powering down the time machine")

	timeout, _ := context.WithTimeout(context.Background(), 5*time.Second)
	r.Server.Shutdown(timeout)
	close(r.stop)
	r.wg.Done()
	r.wg.Wait()
}

// Broadcast listens for requests and streams a station
func (r *Radio) Broadcast(rw http.ResponseWriter, req *http.Request) {
	sp, err := ParsePath(strings.TrimPrefix(req.URL.Path, r.PathBroadcast))
	if err != nil {
		level.Warn(logger).Log(
			"msg", "Failed to broadcast",
			"client", req.RemoteAddr,
			"err", err)

		http.NotFound(rw, req)
		return
	}

	s, err := r.Presets.Lookup(sp.stationName)
	if err != nil {
		level.Warn(logger).Log(
			"msg", "Failed to broadcast",
			"station", sp.stationName,
			"client", req.RemoteAddr,
			"err", err)

		http.NotFound(rw, req)
		return
	}

	listenerTime := s.ListenerTime(sp.listenerLocation)
	tape, err := r.TapeDeck.RecordedTape(req.Context(), s.Name, listenerTime)
	if err != nil {
		level.Warn(logger).Log(
			"msg", "error loading recorded tape",
			"station", sp.stationName,
			"client", req.RemoteAddr,
			"err", err)

		http.Error(rw, "internal server error", http.StatusInternalServerError)
		return
	}

	level.Debug(logger).Log(
		"msg", "Broadcasting station",
		"station", s.Name,
		"client", req.RemoteAddr)

	// Set up trailers
	// thanks http://engineering.pivotal.io/post/http-trailers/
	trailerKey := http.CanonicalHeaderKey("X-Streaming-Error")
	rw.Header().Set("Trailer", trailerKey)

	if err := r.Stream(tape, rw); err != nil {
		level.Debug(logger).Log(
			"msg", "writing trailers",
			"station", s.Name,
			"client", req.RemoteAddr,
			"err", err)
		writeTrailers(err, rw, trailerKey)
	}

	level.Debug(logger).Log(
		"msg", "Broadcast complete",
		"station", s.Name,
		"client", req.RemoteAddr)
}

func writeTrailers(err error, rw http.ResponseWriter, trailerKey string) {
	rw.(http.Flusher).Flush()
	trailers := http.Header{}
	trailers.Set(trailerKey, err.Error())

	rw.(http.Flusher).Flush()
	conn, buf, _ := rw.(http.Hijacker).Hijack()

	buf.WriteString("0\r\n") // eof
	trailers.Write(buf)

	buf.WriteString("\r\n") // end of trailers
	buf.Flush()
	conn.Close()
}

var (
	errStreamCanceled   = errors.New("stream canceled")
	errStreamReadError  = errors.New("backend error")
	errStreamWriteError = errors.New("client error")
)

func (r *Radio) Stream(t *RecordedTape, rw http.ResponseWriter) error {
	pushchunk := func() error {
		chunk, err := t.tape.Read()
		if err != nil {
			level.Warn(logger).Log(
				"msg", "error reading from tape",
				"err", err)
			return errStreamReadError
		}
		if _, err := rw.Write(chunk); err != nil {
			level.Warn(logger).Log(
				"msg", "error writing to client",
				"err", err)
			return errStreamWriteError
		}
		return nil
	}

	// push some chunks to the client's buffer
	for i := 0; i < BufferChunks; i++ {
		if err := pushchunk(); err != nil {
			return err
		}
	}

	ticker := time.NewTicker(time.Second * time.Duration(ChunkSeconds))
	defer ticker.Stop()

	for {
		select {
		case <-r.stop:
			level.Debug(logger).Log("msg", "canceling stream")
			return errStreamCanceled
		case <-ticker.C:
			if err := pushchunk(); err != nil {
				return err
			}
		}
	}
}
<commit_msg>Retry if we see an unexpected EOF<commit_after>package main

import (
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"golang.org/x/net/context"

	"github.com/cenkalti/backoff"
	"github.com/go-kit/kit/log/level"
	"github.com/gorilla/handlers"
	"github.com/pkg/errors"
)

type stopChan chan struct{}

// RadioOptions enables some radio features
type RadioOptions struct {
	Broadcast bool
	Record    bool
}

// A Radio manages all the stations and recordings
type Radio struct {
	Server   *http.Server
	Presets  *Presets
	TapeDeck *TapeDeck
	Options  RadioOptions

	PathBroadcast string
	PathPreset    string

	stop stopChan
	wg   *sync.WaitGroup
}

// Record tunes into a station and records to a blank tape
func (r *Radio) StartRecording(s *Station) {
	r.wg.Add(1)
	defer r.wg.Done()

	// Use a context to provide cancellation of the http client
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		<-r.stop
		cancel()
	}()

	rec := func() error {
		stream, err := s.Tune(ctx)
		if err != nil {
			if strings.HasSuffix(err.Error(), "context canceled") {
				level.Debug(logger).Log(
					"msg", "canceled stream",
					"station", s.Name)
				return nil
			}

			level.Warn(logger).Log(
				"msg", "error tuning into station",
				"station", s.Name,
				"err", err)
			return err
		}

		tape, err := r.TapeDeck.BlankTape(ctx, s.Name, s.CurrentTime())
		if err != nil {
			level.Warn(logger).Log(
				"msg", "error loading blank tape",
				"err", err)
			return err
		}

		size := stream.Chunksize()
		level.Debug(logger).Log(
			"msg", fmt.Sprintf("Recording station with chunksize %d", size),
			"station", s.Name)

		if err := ChunkPipe(size, stream, tape); err != nil {
			if strings.HasSuffix(err.Error(), "context canceled") {
				level.Debug(logger).Log(
					"msg", "canceled stream",
					"station", s.Name)
				return nil
			}

			if strings.HasSuffix(err.Error(), "unexpected EOF") {
				level.Debug(logger).Log(
					"msg", "stream disconnected",
					"station", s.Name)
				return nil
			}

			level.Warn(logger).Log(
				"msg", "error in chunkpipe",
				"station", s.Name,
				"err", err)
			return err
		}

		level.Debug(logger).Log(
			"msg", "chunkpipe returned",
			"station", s.Name)
		// ChunkPipe encountered an EOF but we should still retry
		return errors.New("ChunkPipe returned io.EOF")

	}

	b := backoff.NewExponentialBackOff()
	b.InitialInterval = 2 * time.Second
	b.RandomizationFactor = 0

	err := backoff.Retry(rec, b)
	if err != nil {
		level.Warn(logger).Log(
			"msg", "error after retrying",
			"err", err)
	}
}

// Turn on the radio and start recording presets
func (r *Radio) On() {
	level.Info(logger).Log(
		"msg", "Powering on the time machine",
		"record", r.Options.Record,
		"broadcast", r.Options.Broadcast)

	r.stop = make(stopChan)
	r.wg = &sync.WaitGroup{}
	r.wg.Add(1)

	if r.Options.Record {
		level.Info(logger).Log("msg", "Starting to record presets")

		stations, err := r.Presets.Load()
		if err != nil {
			level.Warn(logger).Log(
				"msg", "error loading presets",
				"err", err)
		}

		for _, s := range stations {
			s := s
			err = s.Init()
			if err != nil {
				level.Warn(logger).Log(
					"msg", "error loading preset",
					"station", s.Name,
					"err", err)
				continue
			}
			go r.StartRecording(&s)
		}

		// r.ManageRecordings(r.stop, r.wg)
	}

	if r.Options.Broadcast {
		r.Presets.RegisterServiceHandlers(r.PathPreset, http.DefaultServeMux)
		http.HandleFunc(r.PathBroadcast, r.Broadcast)

		// enable cors
		cors := handlers.CORS(
			handlers.AllowedHeaders([]string{"Content-Type"}),
			handlers.AllowedMethods([]string{"GET", "POST"}),
			handlers.AllowedOrigins([]string{"*"}))
		r.Server.Handler = cors(http.DefaultServeMux)

		level.Info(logger).Log("msg", "Starting broadcast and preset service")
	}

	// simple healthcheck
	http.HandleFunc("/", http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) },
	))

	go r.Server.ListenAndServe()
	level.Info(logger).Log("msg", "Time machine is operational")
}

func (r *Radio) Off() {
	level.Info(logger).Log("msg", "Powering down the time machine")

	timeout, _ := context.WithTimeout(context.Background(), 5*time.Second)
	r.Server.Shutdown(timeout)
	close(r.stop)
	r.wg.Done()
	r.wg.Wait()
}

// Broadcast listens for requests and streams a station
func (r *Radio) Broadcast(rw http.ResponseWriter, req *http.Request) {
	sp, err := ParsePath(strings.TrimPrefix(req.URL.Path, r.PathBroadcast))
	if err != nil {
		level.Warn(logger).Log(
			"msg", "Failed to broadcast",
			"client", req.RemoteAddr,
			"err", err)

		http.NotFound(rw, req)
		return
	}

	s, err := r.Presets.Lookup(sp.stationName)
	if err != nil {
		level.Warn(logger).Log(
			"msg", "Failed to broadcast",
			"station", sp.stationName,
			"client", req.RemoteAddr,
			"err", err)

		http.NotFound(rw, req)
		return
	}

	listenerTime := s.ListenerTime(sp.listenerLocation)
	tape, err := r.TapeDeck.RecordedTape(req.Context(), s.Name, listenerTime)
	if err != nil {
		level.Warn(logger).Log(
			"msg", "error loading recorded tape",
			"station", sp.stationName,
			"client", req.RemoteAddr,
			"err", err)

		http.Error(rw, "internal server error", http.StatusInternalServerError)
		return
	}

	level.Debug(logger).Log(
		"msg", "Broadcasting station",
		"station", s.Name,
		"client", req.RemoteAddr)

	// Set up trailers
	// thanks http://engineering.pivotal.io/post/http-trailers/
	trailerKey := http.CanonicalHeaderKey("X-Streaming-Error")
	rw.Header().Set("Trailer", trailerKey)

	if err := r.Stream(tape, rw); err != nil {
		level.Debug(logger).Log(
			"msg", "writing trailers",
			"station", s.Name,
			"client", req.RemoteAddr,
			"err", err)
		writeTrailers(err, rw, trailerKey)
	}

	level.Debug(logger).Log(
		"msg", "Broadcast complete",
		"station", s.Name,
		"client", req.RemoteAddr)
}

func writeTrailers(err error, rw http.ResponseWriter, trailerKey string) {
	rw.(http.Flusher).Flush()
	trailers := http.Header{}
	trailers.Set(trailerKey, err.Error())

	rw.(http.Flusher).Flush()
	conn, buf, _ := rw.(http.Hijacker).Hijack()

	buf.WriteString("0\r\n") // eof
	trailers.Write(buf)

	buf.WriteString("\r\n") // end of trailers
	buf.Flush()
	conn.Close()
}

var (
	errStreamCanceled   = errors.New("stream canceled")
	errStreamReadError  = errors.New("backend error")
	errStreamWriteError = errors.New("client error")
)

func (r *Radio) Stream(t *RecordedTape, rw http.ResponseWriter) error {
	pushchunk := func() error {
		chunk, err := t.tape.Read()
		if err != nil {
			level.Warn(logger).Log(
				"msg", "error reading from tape",
				"err", err)
			return errStreamReadError
		}
		if _, err := rw.Write(chunk); err != nil {
			level.Warn(logger).Log(
				"msg", "error writing to client",
				"err", err)
			return errStreamWriteError
		}
		return nil
	}

	// push some chunks to the client's buffer
	for i := 0; i < BufferChunks; i++ {
		if err := pushchunk(); err != nil {
			return err
		}
	}

	ticker := time.NewTicker(time.Second * time.Duration(ChunkSeconds))
	defer ticker.Stop()

	for {
		select {
		case <-r.stop:
			level.Debug(logger).Log("msg", "canceling stream")
			return errStreamCanceled
		case <-ticker.C:
			if err := pushchunk(); err != nil {
				return err
			}
		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"github.com/eqinox76/RiseAndFallOfEmpires/state"
	"github.com/eqinox76/RiseAndFallOfEmpires/engine"
)

func main() {

	space := state.NewSpace()

	for i:=0 ; i< 8 ; i++ {
		bytes, _ := space.Serialize()
		fmt.Printf("serialize: %d, Planets: %d Ships: %d\n", len(bytes), len(space.Planets), len(space.Ships))
		engine.Step(&space)
	}

}
<commit_msg>added server port to send game state to clients<commit_after>package main

import (
	"fmt"
	"github.com/eqinox76/RiseAndFallOfEmpires/state"
	"github.com/eqinox76/RiseAndFallOfEmpires/engine"
	"net"
	"os"
	"sync"
	"encoding/binary"
	"time"
)

type Worker struct {
	input chan []byte
	Done  bool
}

func main() {

	space := state.NewSpace()
	fanOut := make(chan []byte)
	defer close(fanOut)

	go func() {
		workers := make([]*Worker, 0)
		mutex := sync.Mutex{}

		go func() {
			// Listen for incoming connections.
			l, err := net.Listen("tcp", "localhost:9076")
			if err != nil {
				fmt.Println("Error listening:", err.Error())
				os.Exit(1)
			}

			defer l.Close()

			fmt.Println("Listening on localhost:9076")
			for {
				// Listen for an incoming connection.
				con, err := l.Accept()
				if err != nil {
					fmt.Println("Error accepting: ", err.Error())
					os.Exit(1)
				}

				mutex.Lock()

				w := Worker{input: make(chan []byte)}
				workers = append(workers, &w)

				mutex.Unlock()
				go send(con, &w)
			}
		}()

		for msg := range fanOut {
			mutex.Lock()
			for i, w := range workers {
				fmt.Print(w)
				if w.Done {
					// this worker is done so remove it

					close(w.input)
					workers[i] = workers[ len(workers)-1]
					workers = workers[:len(workers)-1]
				} else {
					w.input <- msg
				}
			}
			mutex.Unlock()
		}

	}()

	for true {
		bytes, _ := space.Serialize()
		fmt.Printf("serialize: %d, Planets: %d Ships: %d\n", len(bytes), len(space.Planets), len(space.Ships))
		engine.Step(&space)
		time.Sleep(1 * time.Second)
		fanOut <- bytes
	}

}

func send(con net.Conn, worker *Worker) {
	for msg := range worker.input {

		length := make([]byte, 4)
		binary.LittleEndian.PutUint32(length, uint32(len(msg)))
		_, err := con.Write(length)

		if err != nil{
			break
		}

		_, err = con.Write(msg)

		if err != nil{
			break
		}
	}
	worker.Done = true
}
<|endoftext|>


<commit_before>package main

import (
  "bytes"
  "encoding/json"
  "errors"
  "fmt"
  "io/ioutil"
  "log"
  "os"
)

type Commands map[string]string

func getCommands() (Commands, error) {
  var commands Commands
  jsonStream, err := ioutil.ReadFile("./commands.json")
  if err != nil {
    return commands, err
  }

  decoder := json.NewDecoder(bytes.NewReader(jsonStream))
  if err := decoder.Decode(&commands); err != nil {
    return commands, err
  }

  return commands, nil
}

func start(args []string) error {
  if len(args) <= 1 {
    return errors.New("No files given.")
  }
  commands, err := getCommands()
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(commands[args[1]])
  return nil
}

func main() {
  if err := start(os.Args); err != nil {
    log.Fatal(err)
  }
}
<commit_msg>Implement commandForFile()<commit_after>package main

import (
  "bytes"
  "encoding/json"
  "errors"
  "fmt"
  "io/ioutil"
  "log"
  "os"
  "path/filepath"
  "strings"
)

type Commands map[string]string

func getCommands() (Commands, error) {
  var commands Commands
  jsonStream, err := ioutil.ReadFile("./commands.json")
  if err != nil {
    return commands, err
  }

  decoder := json.NewDecoder(bytes.NewReader(jsonStream))
  if err := decoder.Decode(&commands); err != nil {
    return commands, err
  }

  return commands, nil
}

func commandForFile(path string) (string, error) {
  commands, err := getCommands()
  if err != nil {
    return "", err
  }

  extension := strings.Replace(filepath.Ext(path), ".", "", -1)

  if command := commands[extension]; command != "" {
    return strings.Replace(command, "%", path, -1), nil
  } else {
    return "", errors.New("Run could not determine how to run this file because it does not have a known extension.")
  }
}

func start(args []string) error {
  if len(args) <= 1 {
    return errors.New("No files given.")
  }
  command, err := commandForFile(args[1])
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(command)
  return nil
}

func main() {
  if err := start(os.Args); err != nil {
    log.Fatal(err)
  }
}
<|endoftext|>


<commit_before>package main

import (
	"os"
	"path/filepath"
	"strings"
)

var cmdRun = &Command{
	Run:   runRun,
	Usage: "run [-e env] [-p port]",
	Short: "Run a one-off command",
	Long: `
Run a one-off command

Examples:

  forego run bin/migrate
`,
}

func init() {
	cmdRun.Flag.StringVar(&flagEnv, "e", ".env", "env")
}

func runRun(cmd *Command, args []string) {
	workDir, err := os.Getwd()
	if err != nil {
		handleError(err)
	}
	if flagEnv == "" {
		flagEnv = filepath.Join(workDir, ".env")
	}

	env, err := ReadEnv(flagEnv)
	handleError(err)

	const interactive = true
	ps := NewProcess(workDir, strings.Join(args, " "), env, interactive)
	ps.Stdin = os.Stdin
	ps.Stdout = os.Stdout
	ps.Stderr = os.Stderr

	err = ps.Start()
	handleError(err)

	err = ps.Wait()
	handleError(err)
}
<commit_msg>Show run help with no args<commit_after>package main

import (
	"os"
	"path/filepath"
	"strings"
)

var cmdRun = &Command{
	Run:   runRun,
	Usage: "run [-e env] [-p port]",
	Short: "Run a one-off command",
	Long: `
Run a one-off command

Examples:

  forego run bin/migrate
`,
}

func init() {
	cmdRun.Flag.StringVar(&flagEnv, "e", ".env", "env")
}

func runRun(cmd *Command, args []string) {
	if len(args) < 1 {
		cmd.printUsage()
		os.Exit(1)
	}
	workDir, err := os.Getwd()
	if err != nil {
		handleError(err)
	}
	if flagEnv == "" {
		flagEnv = filepath.Join(workDir, ".env")
	}

	env, err := ReadEnv(flagEnv)
	handleError(err)

	const interactive = true
	ps := NewProcess(workDir, strings.Join(args, " "), env, interactive)
	ps.Stdin = os.Stdin
	ps.Stdout = os.Stdout
	ps.Stderr = os.Stderr

	err = ps.Start()
	handleError(err)

	err = ps.Wait()
	handleError(err)
}
<|endoftext|>


<commit_before>package run

import (
	"bufio"
	"io"
	"os/exec"
	"sync"
)

// Run runs the command and returns a channel of output lines, errors and result of cmd.Wait
func Run(cmd *exec.Cmd, lines chan string) (error, error) {
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return nil, err
	}

	err = cmd.Start()
	if err != nil {
		return nil, err
	}

	var wg sync.WaitGroup
	errCh := make(chan error, 2)
	wg.Add(2)
	go tailReader(bufio.NewReader(stdout), lines, errCh, &wg)
	go tailReader(bufio.NewReader(stderr), lines, errCh, &wg)
	wg.Wait()
	select {
		case err := <-errCh:
		return nil, err
		default:
	}
	return cmd.Wait(), nil
}

func tailReader(
	r *bufio.Reader, ch chan string, errCh chan error,
	wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		line, _, err := r.ReadLine()
		if err != nil {
			// log.Printf("** %T | %#v | %v\n", err, err, err)

			if err != io.EOF {
				errCh <- err
			}
			break
		}
		ch <- string(line)
	}
}
<commit_msg>must close `lines` else we risk leaky goroutines<commit_after>package run

import (
	"bufio"
	"io"
	"os/exec"
	"sync"
)

// Run runs the command and returns a channel of output lines, errors and result of cmd.Wait
func Run(cmd *exec.Cmd, lines chan string) (error, error) {
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return nil, err
	}

	err = cmd.Start()
	if err != nil {
		return nil, err
	}

	var wg sync.WaitGroup
	errCh := make(chan error, 2)
	wg.Add(2)
	go tailReader(bufio.NewReader(stdout), lines, errCh, &wg)
	go tailReader(bufio.NewReader(stderr), lines, errCh, &wg)
	wg.Wait()
	close(lines)
	select {
		case err := <-errCh:
		return nil, err
		default:
	}
	return cmd.Wait(), nil
}

func tailReader(
	r *bufio.Reader, ch chan string, errCh chan error,
	wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		line, _, err := r.ReadLine()
		if err != nil {
			// log.Printf("** %T | %#v | %v\n", err, err, err)

			if err != io.EOF {
				errCh <- err
			}
			break
		}
		ch <- string(line)
	}
}
<|endoftext|>


<commit_before>package backends

import (
	"github.com/docker/libswarm/beam"

	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strings"
	"testing"
)

type requestStub struct {
	reqMethod string
	reqPath   string
	reqBody   string

	resStatus int
	resBody   string
}

func TestBackendSpawn(t *testing.T) {
	instance(t, nil)
}

func TestAttachAndStart(t *testing.T) {
	i := instance(t, nil)
	_, _, err := i.Attach("")
	if err != nil {
		t.Fatal(err)
	}
	err = i.Start()
	if err != nil {
		t.Fatal(err)
	}
}

func TestLs(t *testing.T) {
	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   "/containers/json",
		resBody: `
      [
        {"Names": ["/foo"]},
        {"Names": ["/bar"]}
      ]
    `,
	})
	i := instance(t, server)
	names, err := i.Ls()
	if err != nil {
		t.Fatal(err)
	}
	expected := []string{"foo", "bar"}
	if !reflect.DeepEqual(names, expected) {
		t.Fatalf("expected %#v, got %#v", expected, names)
	}
	server.Check()
}

func TestSpawn(t *testing.T) {
	server := makeServer(t, &requestStub{
		reqMethod: "POST",
		reqPath:   "/containers/create",
		reqBody:   "{}",

		resStatus: 201,
		resBody:   "{}",
	})
	i := instance(t, server)
	_, err := i.Spawn("{}")
	if err != nil {
		t.Fatal(err)
	}
	server.Check()
}

func TestAttachToChild(t *testing.T) {
	name := "foo"
	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   fmt.Sprintf("/containers/%s/json", name),

		resBody: "{}",
	})
	i := instance(t, server)
	child(t, server, i, name)
	server.Check()
}

func TestStartChild(t *testing.T) {
	name := "foo"
	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   fmt.Sprintf("/containers/%s/json", name),

		resBody: "{}",
	}, &requestStub{
		reqMethod: "POST",
		reqPath:   fmt.Sprintf("/containers/%s/start", name),
		reqBody:   "{}",

		resStatus: 204,
	})
	i := instance(t, server)
	c := child(t, server, i, name)
	err := c.Start()
	if err != nil {
		t.Fatal(err)
	}
	server.Check()
}

func TestStopChild(t *testing.T) {
	name := "foo"
	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   fmt.Sprintf("/containers/%s/json", name),

		resBody: "{}",
	}, &requestStub{
		reqMethod: "POST",
		reqPath:   fmt.Sprintf("/containers/%s/stop", name),

		resStatus: 204,
	})
	i := instance(t, server)
	c := child(t, server, i, name)
	err := c.Stop()
	if err != nil {
		t.Fatal(err)
	}
	server.Check()
}

type stubRecord struct {
	stub      *requestStub
	summary   string
	callCount int
}

type stubServer struct {
	*httptest.Server

	stubs []*stubRecord
	t     *testing.T
}

func makeServer(t *testing.T, stubs ...*requestStub) *stubServer {
	s := &stubServer{
		stubs: []*stubRecord{},
		t:     t,
	}

	s.Server = httptest.NewServer(http.HandlerFunc(s.ServeRequest))

	for _, stub := range stubs {
		stub.reqPath = fmt.Sprintf("/v1.11%s", stub.reqPath)
		s.stubs = append(s.stubs, &stubRecord{
			stub:      stub,
			summary:   fmt.Sprintf("%s %s", stub.reqMethod, stub.reqPath),
			callCount: 0,
		})
	}

	return s
}

func (s *stubServer) Check() {
	for _, record := range s.stubs {
		if record.callCount != 1 {
			s.t.Fatalf("Expected %#v to be called exactly once, but was called %d times",
				record.summary, record.callCount)
		}
	}
}

func (s *stubServer) ServeRequest(w http.ResponseWriter, r *http.Request) {
	for _, record := range s.stubs {
		stub := record.stub
		if r.Method == stub.reqMethod && r.URL.Path == stub.reqPath {
			body, err := ioutil.ReadAll(r.Body)
			if err != nil {
				s.t.Fatal(err)
			}
			if string(body) != stub.reqBody {
				s.t.Fatalf("expected body: %#v, got body: %#v", stub.reqBody, string(body))
			}

			if stub.resStatus > 0 {
				w.WriteHeader(stub.resStatus)
			}
			w.Write([]byte(stub.resBody))

			record.callCount += 1
			return
		}
	}

	s.t.Fatalf("Unexpected request: '%s %s'.\nStubs: %#v", r.Method, r.URL.Path, s.AllSummaries())
}

func (s *stubServer) AllSummaries() []string {
	summaries := []string{}
	for _, record := range s.stubs {
		summaries = append(summaries, record.summary)
	}
	return summaries
}

func instance(t *testing.T, server *stubServer) *beam.Object {
	url := "tcp://localhost:4243"
	if server != nil {
		url = strings.Replace(server.URL, "http://", "tcp://", 1)
	}

	backend := DockerClient()
	instance, err := beam.Obj(backend).Spawn(url)
	if err != nil {
		t.Fatal(err)
	}
	return instance
}

func child(t *testing.T, server *stubServer, i *beam.Object, name string) *beam.Object {
	_, child, err := i.Attach(name)
	if err != nil {
		t.Fatal(err)
	}
	return child
}
<commit_msg>Test for dockerclient Get<commit_after>package backends

import (
	"github.com/docker/libswarm/beam"

	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strings"
	"testing"
)

type requestStub struct {
	reqMethod string
	reqPath   string
	reqBody   string

	resStatus int
	resBody   string

	callCount int
}

func TestBackendSpawn(t *testing.T) {
	instance(t, nil)
}

func TestAttachAndStart(t *testing.T) {
	i := instance(t, nil)
	_, _, err := i.Attach("")
	if err != nil {
		t.Fatal(err)
	}
	err = i.Start()
	if err != nil {
		t.Fatal(err)
	}
}

func TestLs(t *testing.T) {
	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   "/containers/json",
		resBody: `
      [
        {"Names": ["/foo"]},
        {"Names": ["/bar"]}
      ]
    `,
	})
	i := instance(t, server)
	names, err := i.Ls()
	if err != nil {
		t.Fatal(err)
	}
	expected := []string{"foo", "bar"}
	if !reflect.DeepEqual(names, expected) {
		t.Fatalf("expected %#v, got %#v", expected, names)
	}
	server.Check()
}

func TestSpawn(t *testing.T) {
	server := makeServer(t, &requestStub{
		reqMethod: "POST",
		reqPath:   "/containers/create",
		reqBody:   "{}",

		resStatus: 201,
		resBody:   "{}",
	})
	i := instance(t, server)
	_, err := i.Spawn("{}")
	if err != nil {
		t.Fatal(err)
	}
	server.Check()
}

func TestAttachToChild(t *testing.T) {
	name := "foo"
	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   fmt.Sprintf("/containers/%s/json", name),

		resBody: "{}",
	})
	i := instance(t, server)
	child(t, server, i, name)
	server.Check()
}

func TestGetChild(t *testing.T) {
	name := "foo"
	expectedContent := `{"Image": "busybox", "Command": ["/bin/true"]}`

	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   fmt.Sprintf("/containers/%s/json", name),

		resBody: expectedContent,

		callCount: 2,
	})
	i := instance(t, server)
	c := child(t, server, i, name)
	content, err := c.Get()
	if err != nil {
		t.Fatal(err)
	}
	if content != expectedContent {
		t.Fatalf("Expected %#v, got %#v", expectedContent, content)
	}
	server.Check()
}

func TestStartChild(t *testing.T) {
	name := "foo"
	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   fmt.Sprintf("/containers/%s/json", name),

		resBody: "{}",
	}, &requestStub{
		reqMethod: "POST",
		reqPath:   fmt.Sprintf("/containers/%s/start", name),
		reqBody:   "{}",

		resStatus: 204,
	})
	i := instance(t, server)
	c := child(t, server, i, name)
	err := c.Start()
	if err != nil {
		t.Fatal(err)
	}
	server.Check()
}

func TestStopChild(t *testing.T) {
	name := "foo"
	server := makeServer(t, &requestStub{
		reqMethod: "GET",
		reqPath:   fmt.Sprintf("/containers/%s/json", name),

		resBody: "{}",
	}, &requestStub{
		reqMethod: "POST",
		reqPath:   fmt.Sprintf("/containers/%s/stop", name),

		resStatus: 204,
	})
	i := instance(t, server)
	c := child(t, server, i, name)
	err := c.Stop()
	if err != nil {
		t.Fatal(err)
	}
	server.Check()
}

type stubRecord struct {
	stub      *requestStub
	summary   string
	callCount int
}

type stubServer struct {
	*httptest.Server

	stubs []*stubRecord
	t     *testing.T
}

func makeServer(t *testing.T, stubs ...*requestStub) *stubServer {
	s := &stubServer{
		stubs: []*stubRecord{},
		t:     t,
	}

	s.Server = httptest.NewServer(http.HandlerFunc(s.ServeRequest))

	for _, stub := range stubs {
		stub.reqPath = fmt.Sprintf("/v1.11%s", stub.reqPath)
		if stub.callCount == 0 {
			stub.callCount = 1
		}

		s.stubs = append(s.stubs, &stubRecord{
			stub:      stub,
			summary:   fmt.Sprintf("%s %s", stub.reqMethod, stub.reqPath),
			callCount: 0,
		})
	}

	return s
}

func (s *stubServer) Check() {
	for _, record := range s.stubs {
		if record.callCount != record.stub.callCount {
			s.t.Fatalf("Expected %#v to be called %d times, but was called %d times",
				record.summary, record.stub.callCount, record.callCount)
		}
	}
}

func (s *stubServer) ServeRequest(w http.ResponseWriter, r *http.Request) {
	for _, record := range s.stubs {
		stub := record.stub
		if r.Method == stub.reqMethod && r.URL.Path == stub.reqPath {
			body, err := ioutil.ReadAll(r.Body)
			if err != nil {
				s.t.Fatal(err)
			}
			if string(body) != stub.reqBody {
				s.t.Fatalf("expected body: %#v, got body: %#v", stub.reqBody, string(body))
			}

			if stub.resStatus > 0 {
				w.WriteHeader(stub.resStatus)
			}
			w.Write([]byte(stub.resBody))

			record.callCount += 1
			return
		}
	}

	s.t.Fatalf("Unexpected request: '%s %s'.\nStubs: %#v", r.Method, r.URL.Path, s.AllSummaries())
}

func (s *stubServer) AllSummaries() []string {
	summaries := []string{}
	for _, record := range s.stubs {
		summaries = append(summaries, record.summary)
	}
	return summaries
}

func instance(t *testing.T, server *stubServer) *beam.Object {
	url := "tcp://localhost:4243"
	if server != nil {
		url = strings.Replace(server.URL, "http://", "tcp://", 1)
	}

	backend := DockerClient()
	instance, err := beam.Obj(backend).Spawn(url)
	if err != nil {
		t.Fatal(err)
	}
	return instance
}

func child(t *testing.T, server *stubServer, i *beam.Object, name string) *beam.Object {
	_, child, err := i.Attach(name)
	if err != nil {
		t.Fatal(err)
	}
	return child
}
<|endoftext|>


<commit_before>package service

import (
	"fmt"
	"math/big"

	"github.com/hyperledger/burrow/crypto"
	"github.com/hyperledger/burrow/execution/evm/abi"
	"github.com/hyperledger/burrow/execution/exec"
	"github.com/hyperledger/burrow/vent/types"
	"github.com/pkg/errors"
)

// decodeEvent unpacks & decodes event data
func decodeEvent(header *exec.Header, log *exec.LogEvent, abiSpec *abi.AbiSpec) (map[string]interface{}, error) {
	// to prepare decoded data and map to event item name
	data := make(map[string]interface{})

	var eventID abi.EventID
	var evAbi abi.EventSpec
	copy(eventID[:], log.Topics[0].Bytes())

	evAbi, ok := abiSpec.EventsById[eventID]
	if !ok {
		return nil, fmt.Errorf("Abi spec not found for event %x", eventID)
	}

	// decode header to get context data for each event
	data[types.EventNameLabel] = evAbi.Name
	data[types.BlockHeightLabel] = fmt.Sprintf("%v", header.GetHeight())
	data[types.EventTypeLabel] = header.GetEventType().String()
	data[types.TxTxHashLabel] = header.TxHash.String()

	// build expected interface type array to get log event values
	unpackedData := abi.GetPackingTypes(evAbi.Inputs)

	// unpack event data (topics & data part)
	if err := abi.UnpackEvent(evAbi, log.Topics, log.Data, unpackedData...); err != nil {
		return nil, errors.Wrap(err, "Could not unpack event data")
	}

	// for each decoded item value, stores it in given item name
	for i, input := range evAbi.Inputs {
		switch v := unpackedData[i].(type) {
		case *crypto.Address:
			data[input.Name] = v.String()
		case *big.Int:
			data[input.Name] = v.String()
		case *string:
			data[input.Name] = *v
		default:
			data[input.Name] = v
		}
	}

	return data, nil
}
<commit_msg>Fix EventSpec type<commit_after>package service

import (
	"fmt"
	"math/big"

	"github.com/hyperledger/burrow/crypto"
	"github.com/hyperledger/burrow/execution/evm/abi"
	"github.com/hyperledger/burrow/execution/exec"
	"github.com/hyperledger/burrow/vent/types"
	"github.com/pkg/errors"
)

// decodeEvent unpacks & decodes event data
func decodeEvent(header *exec.Header, log *exec.LogEvent, abiSpec *abi.AbiSpec) (map[string]interface{}, error) {
	// to prepare decoded data and map to event item name
	data := make(map[string]interface{})

	var eventID abi.EventID
	copy(eventID[:], log.Topics[0].Bytes())

	evAbi, ok := abiSpec.EventsById[eventID]
	if !ok {
		return nil, fmt.Errorf("Abi spec not found for event %x", eventID)
	}

	// decode header to get context data for each event
	data[types.EventNameLabel] = evAbi.Name
	data[types.BlockHeightLabel] = fmt.Sprintf("%v", header.GetHeight())
	data[types.EventTypeLabel] = header.GetEventType().String()
	data[types.TxTxHashLabel] = header.TxHash.String()

	// build expected interface type array to get log event values
	unpackedData := abi.GetPackingTypes(evAbi.Inputs)

	// unpack event data (topics & data part)
	if err := abi.UnpackEvent(&evAbi, log.Topics, log.Data, unpackedData...); err != nil {
		return nil, errors.Wrap(err, "Could not unpack event data")
	}

	// for each decoded item value, stores it in given item name
	for i, input := range evAbi.Inputs {
		switch v := unpackedData[i].(type) {
		case *crypto.Address:
			data[input.Name] = v.String()
		case *big.Int:
			data[input.Name] = v.String()
		case *string:
			data[input.Name] = *v
		default:
			data[input.Name] = v
		}
	}

	return data, nil
}
<|endoftext|>


<commit_before>// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 Authors of Cilium

//go:build !privileged_tests
// +build !privileged_tests

package api

import (
	"context"
	"os"
	"testing"
	"time"

	"gopkg.in/check.v1"

	"github.com/cilium/cilium/pkg/api/metrics/mock"
)

func Test(t *testing.T) {
	check.TestingT(t)
}

type ApiSuite struct{}

var _ = check.Suite(&ApiSuite{})

func (a *ApiSuite) TestRateLimit(c *check.C) {
	// Since github.com/Azure/go-autorest/autorest/azure/auth v0.5.6, the
	// MSI_ENDPOINT environment variable must be set, otherwise this test will
	// fail with the error "failed to get oauth token from MSI: MSI not
	// available".
	//
	// Temporarily set the environment variable for the duration of the test,
	// using the same workaround as used in
	// github.com/Azure/go-autorest/autorest/azure/auth's tests.
	os.Setenv("MSI_ENDPOINT", "http://localhost")
	defer func() {
		os.Unsetenv("MSI_ENDPOINT")
	}()

	metricsAPI := mock.NewMockMetrics()
	client, err := NewClient("AZUREPUBLICCLOUD", "dummy-subscription", "dummy-resource-group", "", metricsAPI, 10.0, 4, true)
	c.Assert(err, check.IsNil)
	c.Assert(client, check.Not(check.IsNil))

	for i := 0; i < 10; i++ {
		client.limiter.Limit(context.TODO(), "test")
	}

	c.Assert(metricsAPI.RateLimit("test"), check.Not(check.DeepEquals), time.Duration(0))
}
<commit_msg>azure/api: remove TestRateLimit<commit_after><|endoftext|>


<commit_before>package cephmgr

import (
	"fmt"
	"log"
	"path"
	"strings"

	ctx "golang.org/x/net/context"

	etcd "github.com/coreos/etcd/client"
	"github.com/coreos/etcd/store"
	"github.com/quantum/castle/pkg/cephmgr/client"
	"github.com/quantum/castle/pkg/clusterd"
	"github.com/quantum/castle/pkg/clusterd/inventory"
)

// Interface implemented by a service that has been elected leader
type cephLeader struct {
	monLeader *monLeader
	cluster   *ClusterInfo
	events    chan clusterd.LeaderEvent
	factory   client.ConnectionFactory
}

func newCephLeader(factory client.ConnectionFactory) *cephLeader {
	return &cephLeader{factory: factory, monLeader: newMonLeader()}
}

func (c *cephLeader) RefreshKeys() []*clusterd.RefreshKey {
	// when devices are added or removed we will want to trigger an orchestration
	deviceChange := &clusterd.RefreshKey{
		Path:      path.Join(cephKey, osdAgentName, desiredKey),
		Triggered: handleDeviceChanged,
	}
	return []*clusterd.RefreshKey{deviceChange}
}

func (c *cephLeader) StartWatchEvents() {
	if c.events != nil {
		close(c.events)
	}
	c.events = make(chan clusterd.LeaderEvent, 10)
	go c.handleOrchestratorEvents()
}

func (c *cephLeader) Events() chan clusterd.LeaderEvent {
	return c.events
}

func (c *cephLeader) Close() error {
	close(c.events)
	c.events = nil
	return nil
}

func (c *cephLeader) handleOrchestratorEvents() {
	// Listen for events from the orchestrator indicating that a refresh is needed or nodes have been added
	for e := range c.events {
		log.Printf("ceph leader received event %s", e.Name())

		var osdsToRefresh []string
		refreshMon := false
		if r, ok := e.(*clusterd.RefreshEvent); ok {
			if r.NodeID() != "" {
				// refresh a single node, which is currently only for adding and removing devices
				osdsToRefresh = []string{r.NodeID()}
			} else {
				// refresh the whole cluster
				refreshMon = true
				osdsToRefresh = getSlice(e.Context().Inventory.Nodes)
			}

		} else if nodeAdded, ok := e.(*clusterd.AddNodeEvent); ok {
			osdsToRefresh = []string{nodeAdded.Node()}

		} else if unhealthyEvent, ok := e.(*clusterd.UnhealthyNodeEvent); ok {
			var err error
			refreshMon, err = monsOnUnhealthyNode(e.Context(), unhealthyEvent.Nodes())
			if err != nil {
				log.Printf("failed to handle unhealthy nodes. %v", err)
			}

		} else {
			// if we don't recognize the event we will skip the refresh
		}

		if refreshMon {
			// Perform a full refresh of the cluster to ensure the monitors are running with quorum
			err := c.configureCephMons(e.Context())
			if err != nil {
				log.Printf("FAILED TO CONFIGURE CEPH MONS. %v", err)
				continue
			}
		}

		if osdsToRefresh != nil {
			// Configure the OSDs
			err := configureOSDs(e.Context(), osdsToRefresh)
			if err != nil {
				log.Printf("FAILED TO CONFIGURE CEPH OSDs. %v", err)
				continue
			}
		}

		log.Printf("ceph leader completed event %s", e.Name())
	}
}

// Apply the desired state to the cluster. The context provides all the information needed to make changes to the service.
func (c *cephLeader) configureCephMons(context *clusterd.Context) error {

	// Create or get the basic cluster info
	var err error
	c.cluster, err = createOrGetClusterInfo(c.factory, context.EtcdClient)
	if err != nil {
		return err
	}

	// Select the monitors, instruct them to start, and wait for quorum
	return c.monLeader.configureMonitors(c.factory, context, c.cluster)
}

func createOrGetClusterInfo(factory client.ConnectionFactory, etcdClient etcd.KeysAPI) (*ClusterInfo, error) {
	// load any existing cluster info that may have previously been created
	cluster, err := LoadClusterInfo(etcdClient)
	if err != nil {
		return nil, fmt.Errorf("failed to load cluster info: %+v", err)
	}

	if cluster == nil {
		// the cluster info is not yet set, go ahead and set it now
		cluster, err = createClusterInfo(factory)
		if err != nil {
			return nil, fmt.Errorf("failed to create cluster info: %+v", err)
		}

		log.Printf("Created new cluster info: %+v", cluster)
		err = saveClusterInfo(cluster, etcdClient)
		if err != nil {
			return nil, fmt.Errorf("failed to save new cluster info: %+v", err)
		}
	} else {
		// the cluster has already been created
		log.Printf("Cluster already exists: %+v", cluster)
	}

	return cluster, nil
}

// create new cluster info (FSID, shared keys)
func createClusterInfo(factory client.ConnectionFactory) (*ClusterInfo, error) {
	fsid, err := factory.NewFsid()
	if err != nil {
		return nil, err
	}

	monSecret, err := factory.NewSecretKey()
	if err != nil {
		return nil, err
	}

	adminSecret, err := factory.NewSecretKey()
	if err != nil {
		return nil, err
	}

	return &ClusterInfo{
		FSID:          fsid,
		MonitorSecret: monSecret,
		AdminSecret:   adminSecret,
		Name:          "castlecluster",
	}, nil
}

// save the given cluster info to the key value store
func saveClusterInfo(c *ClusterInfo, etcdClient etcd.KeysAPI) error {
	_, err := etcdClient.Set(ctx.Background(), path.Join(cephKey, "fsid"), c.FSID, nil)
	if err != nil {
		return err
	}

	_, err = etcdClient.Set(ctx.Background(), path.Join(cephKey, "name"), c.Name, nil)
	if err != nil {
		return err
	}

	secretsKey := path.Join(cephKey, "_secrets")

	_, err = etcdClient.Set(ctx.Background(), path.Join(secretsKey, "monitor"), c.MonitorSecret, nil)
	if err != nil {
		return err
	}

	_, err = etcdClient.Set(ctx.Background(), path.Join(secretsKey, "admin"), c.AdminSecret, nil)
	if err != nil {
		return err
	}

	return nil
}

func getSlice(nodeMap map[string]*inventory.NodeConfig) []string {

	// Convert the node IDs to a simple slice
	nodes := make([]string, len(nodeMap))
	i := 0
	for node := range nodeMap {
		nodes[i] = node
		i++
	}

	return nodes
}

func handleDeviceChanged(response *etcd.Response, refresher *clusterd.ClusterRefresher) {
	if response.Action == store.Create || response.Action == store.Delete {
		nodeID, err := extractNodeIDFromDesiredDevice(response.Node.Key)
		if err != nil {
			log.Printf("ignored device changed event. %v", err)
			return
		}

		log.Printf("device changed: %s", nodeID)

		// trigger an orchestration to add or remove the device
		refresher.TriggerNodeRefresh(nodeID)
	}
}

// Get the node ID from the etcd key to a desired device
// For example: /castle/services/ceph/osd/desired/9b69e58300f9/device/sdb
func extractNodeIDFromDesiredDevice(path string) (string, error) {
	parts := strings.Split(path, "/")
	const nodeIDOffset = 6
	if len(parts) < nodeIDOffset+1 {
		return "", fmt.Errorf("cannot get node ID from %s", path)
	}

	return parts[nodeIDOffset], nil
}
<commit_msg>trigger adding monitors when a node is added<commit_after>package cephmgr

import (
	"fmt"
	"log"
	"path"
	"strings"

	ctx "golang.org/x/net/context"

	etcd "github.com/coreos/etcd/client"
	"github.com/coreos/etcd/store"
	"github.com/quantum/castle/pkg/cephmgr/client"
	"github.com/quantum/castle/pkg/clusterd"
	"github.com/quantum/castle/pkg/clusterd/inventory"
)

// Interface implemented by a service that has been elected leader
type cephLeader struct {
	monLeader *monLeader
	cluster   *ClusterInfo
	events    chan clusterd.LeaderEvent
	factory   client.ConnectionFactory
}

func newCephLeader(factory client.ConnectionFactory) *cephLeader {
	return &cephLeader{factory: factory, monLeader: newMonLeader()}
}

func (c *cephLeader) RefreshKeys() []*clusterd.RefreshKey {
	// when devices are added or removed we will want to trigger an orchestration
	deviceChange := &clusterd.RefreshKey{
		Path:      path.Join(cephKey, osdAgentName, desiredKey),
		Triggered: handleDeviceChanged,
	}
	return []*clusterd.RefreshKey{deviceChange}
}

func (c *cephLeader) StartWatchEvents() {
	if c.events != nil {
		close(c.events)
	}
	c.events = make(chan clusterd.LeaderEvent, 10)
	go c.handleOrchestratorEvents()
}

func (c *cephLeader) Events() chan clusterd.LeaderEvent {
	return c.events
}

func (c *cephLeader) Close() error {
	close(c.events)
	c.events = nil
	return nil
}

func (c *cephLeader) handleOrchestratorEvents() {
	// Listen for events from the orchestrator indicating that a refresh is needed or nodes have been added
	for e := range c.events {
		log.Printf("ceph leader received event %s", e.Name())

		var osdsToRefresh []string
		refreshMon := false
		if r, ok := e.(*clusterd.RefreshEvent); ok {
			log.Printf("Refresh event")
			refreshMon = true
			if r.NodeID() != "" {
				// refresh a single node, which is currently only for adding and removing devices
				osdsToRefresh = []string{r.NodeID()}
			} else {
				// refresh the whole cluster
				osdsToRefresh = getSlice(e.Context().Inventory.Nodes)
			}

		} else if nodeAdded, ok := e.(*clusterd.AddNodeEvent); ok {
			log.Printf("Added node %s event", nodeAdded.Node())
			refreshMon = true
			osdsToRefresh = []string{nodeAdded.Node()}

		} else if unhealthyEvent, ok := e.(*clusterd.UnhealthyNodeEvent); ok {
			var err error
			refreshMon, err = monsOnUnhealthyNode(e.Context(), unhealthyEvent.Nodes())
			if err != nil {
				log.Printf("failed to handle unhealthy nodes. %v", err)
			}

		} else {
			// if we don't recognize the event we will skip the refresh
		}

		if refreshMon {
			// Perform a full refresh of the cluster to ensure the monitors are running with quorum
			err := c.configureCephMons(e.Context())
			if err != nil {
				log.Printf("FAILED TO CONFIGURE CEPH MONS. %v", err)
				continue
			}
		}

		if osdsToRefresh != nil {
			// Configure the OSDs
			err := configureOSDs(e.Context(), osdsToRefresh)
			if err != nil {
				log.Printf("FAILED TO CONFIGURE CEPH OSDs. %v", err)
				continue
			}
		}

		log.Printf("ceph leader completed event %s", e.Name())
	}
}

// Apply the desired state to the cluster. The context provides all the information needed to make changes to the service.
func (c *cephLeader) configureCephMons(context *clusterd.Context) error {

	// Create or get the basic cluster info
	var err error
	c.cluster, err = createOrGetClusterInfo(c.factory, context.EtcdClient)
	if err != nil {
		return err
	}

	// Select the monitors, instruct them to start, and wait for quorum
	return c.monLeader.configureMonitors(c.factory, context, c.cluster)
}

func createOrGetClusterInfo(factory client.ConnectionFactory, etcdClient etcd.KeysAPI) (*ClusterInfo, error) {
	// load any existing cluster info that may have previously been created
	cluster, err := LoadClusterInfo(etcdClient)
	if err != nil {
		return nil, fmt.Errorf("failed to load cluster info: %+v", err)
	}

	if cluster == nil {
		// the cluster info is not yet set, go ahead and set it now
		cluster, err = createClusterInfo(factory)
		if err != nil {
			return nil, fmt.Errorf("failed to create cluster info: %+v", err)
		}

		log.Printf("Created new cluster info: %+v", cluster)
		err = saveClusterInfo(cluster, etcdClient)
		if err != nil {
			return nil, fmt.Errorf("failed to save new cluster info: %+v", err)
		}
	} else {
		// the cluster has already been created
		log.Printf("Cluster already exists: %+v", cluster)
	}

	return cluster, nil
}

// create new cluster info (FSID, shared keys)
func createClusterInfo(factory client.ConnectionFactory) (*ClusterInfo, error) {
	fsid, err := factory.NewFsid()
	if err != nil {
		return nil, err
	}

	monSecret, err := factory.NewSecretKey()
	if err != nil {
		return nil, err
	}

	adminSecret, err := factory.NewSecretKey()
	if err != nil {
		return nil, err
	}

	return &ClusterInfo{
		FSID:          fsid,
		MonitorSecret: monSecret,
		AdminSecret:   adminSecret,
		Name:          "castlecluster",
	}, nil
}

// save the given cluster info to the key value store
func saveClusterInfo(c *ClusterInfo, etcdClient etcd.KeysAPI) error {
	_, err := etcdClient.Set(ctx.Background(), path.Join(cephKey, "fsid"), c.FSID, nil)
	if err != nil {
		return err
	}

	_, err = etcdClient.Set(ctx.Background(), path.Join(cephKey, "name"), c.Name, nil)
	if err != nil {
		return err
	}

	secretsKey := path.Join(cephKey, "_secrets")

	_, err = etcdClient.Set(ctx.Background(), path.Join(secretsKey, "monitor"), c.MonitorSecret, nil)
	if err != nil {
		return err
	}

	_, err = etcdClient.Set(ctx.Background(), path.Join(secretsKey, "admin"), c.AdminSecret, nil)
	if err != nil {
		return err
	}

	return nil
}

func getSlice(nodeMap map[string]*inventory.NodeConfig) []string {

	// Convert the node IDs to a simple slice
	nodes := make([]string, len(nodeMap))
	i := 0
	for node := range nodeMap {
		nodes[i] = node
		i++
	}

	return nodes
}

func handleDeviceChanged(response *etcd.Response, refresher *clusterd.ClusterRefresher) {
	if response.Action == store.Create || response.Action == store.Delete {
		nodeID, err := extractNodeIDFromDesiredDevice(response.Node.Key)
		if err != nil {
			log.Printf("ignored device changed event. %v", err)
			return
		}

		log.Printf("device changed: %s", nodeID)

		// trigger an orchestration to add or remove the device
		refresher.TriggerNodeRefresh(nodeID)
	}
}

// Get the node ID from the etcd key to a desired device
// For example: /castle/services/ceph/osd/desired/9b69e58300f9/device/sdb
func extractNodeIDFromDesiredDevice(path string) (string, error) {
	parts := strings.Split(path, "/")
	const nodeIDOffset = 6
	if len(parts) < nodeIDOffset+1 {
		return "", fmt.Errorf("cannot get node ID from %s", path)
	}

	return parts[nodeIDOffset], nil
}
<|endoftext|>


<commit_before>// Copyright 2020 Google LLC
//
// 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
//
//     https://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 setup contains common logic for setting up the export package across binaries.
package setup

import (
	"fmt"
	"os"
	"strconv"

	"cloud.google.com/go/compute/metadata"
	"github.com/GoogleCloudPlatform/prometheus-engine/pkg/export"
	"github.com/GoogleCloudPlatform/prometheus-engine/pkg/lease"
	"github.com/go-kit/kit/log"
	"github.com/pkg/errors"
	"github.com/prometheus/client_golang/prometheus"
	kingpin "gopkg.in/alecthomas/kingpin.v2"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"

	// Blank import required to register auth handlers to talk use different auth mechanisms
	// for talking to the Kubernetes API server.
	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
)

// Supported HA backend modes.
const (
	HABackendNone       = "none"
	HABackendKubernetes = "kube"
)

// Generally, global state is not a good approach and actively discouraged throughout
// the Prometheus code bases. However, this is the most practical way to inject the export
// path into lower layers of Prometheus without touching an excessive amount of functions
// in our fork to propagate it.
var globalExporter *export.Exporter

// SetGlobal sets the global instance of the GCM exporter.
func SetGlobal(exporter *export.Exporter) (err error) {
	globalExporter = exporter
	return err
}

// Global returns the global instance of the GCM exporter.
func Global() *export.Exporter {
	if globalExporter == nil {
		// This should usually be a panic but we set an inactive default exporter in this case
		// to not break existing tests in Prometheus.
		fmt.Fprintln(os.Stderr, "No global GCM exporter was set, setting default inactive exporter.")
		return export.NopExporter()
	}
	return globalExporter
}

// FromFlags returns a constructor for a new exporter that is configured through flags that are
// registered with the given application. The constructor must be called after the flags
// have been parsed.
func FromFlags(a *kingpin.Application, userAgent string) func(log.Logger, prometheus.Registerer) (*export.Exporter, error) {
	var opts export.ExporterOpts

	// Default target fields if we can detect them in GCP.
	if metadata.OnGCE() {
		opts.ProjectID, _ = metadata.ProjectID()
		opts.Cluster, _ = metadata.InstanceAttributeValue("cluster-name")
		// These attributes are set for GKE nodes. For the location, we first check
		// the clustr location, which may be a zone or a region. We must always use that value
		// to avoid collisions with other clusters, as the same cluster name may be reused
		// in different locations.
		// In particular, we cannot set the location to the node's zone for a regional cluster,
		// even though this would provide more accuracy, as there may also be a zonal cluster
		// with the same name.
		// We only fallback to the node zone as the location if no cluster location exists to
		// default for deployments on GCE.
		if loc, _ := metadata.InstanceAttributeValue("cluster-location"); loc != "" {
			opts.Location = loc
		} else {
			opts.Location, _ = metadata.Zone()
		}
	}

	a.Flag("export.disable", "Disable exporting to GCM.").
		Default("false").BoolVar(&opts.Disable)

	a.Flag("export.endpoint", "GCM API endpoint to send metric data to.").
		Default("monitoring.googleapis.com:443").StringVar(&opts.Endpoint)

	a.Flag("export.compression", "The compression format to use for gRPC requests ('none' or 'gzip').").
		Default(export.CompressionNone).EnumVar(&opts.Compression, export.CompressionNone, export.CompressionGZIP)

	a.Flag("export.credentials-file", "Credentials file for authentication with the GCM API.").
		Default("").StringVar(&opts.CredentialsFile)

	a.Flag("export.label.project-id", fmt.Sprintf("Default project ID set for all exported data. Prefer setting the external label %q in the Prometheus configuration if not using the auto-discovered default.", export.KeyProjectID)).
		Default(opts.ProjectID).StringVar(&opts.ProjectID)

	a.Flag("export.user-agent", "Override for the user agent used for requests against the GCM API.").
		Default(userAgent).StringVar(&opts.UserAgent)

	// The location and cluster flag should probably not be used. On the other hand, they make it easy
	// to populate these important values in the monitored resource without interfering with existing
	// Prometheus configuration.
	a.Flag("export.label.location", fmt.Sprintf("The default location set for all exported data. Prefer setting the external label %q in the Prometheus configuration if not using the auto-discovered default.", export.KeyLocation)).
		Default(opts.Location).StringVar(&opts.Location)

	a.Flag("export.label.cluster", fmt.Sprintf("The default cluster set for all scraped targets. Prefer setting the external label %q in the Prometheus configuration if not using the auto-discovered default.", export.KeyCluster)).
		Default(opts.Cluster).StringVar(&opts.Cluster)

	a.Flag("export.match", `A Prometheus time series matcher. Can be repeated. Every time series must match at least one of the matchers to be exported. This flag can be used equivalently to the match[] parameter of the Prometheus federation endpoint to selectively export data. (Example: --export.match='{job="prometheus"}' --export.match='{__name__=~"job:.*"})`).
		Default("").SetValue(&opts.Matchers)

	a.Flag("export.debug.metric-prefix", "Google Cloud Monitoring metric prefix to use.").
		Default(export.MetricTypePrefix).StringVar(&opts.MetricTypePrefix)

	a.Flag("export.debug.disable-auth", "Disable authentication (for debugging purposes).").
		Default("false").BoolVar(&opts.DisableAuth)

	a.Flag("export.debug.batch-size", "Maximum number of points to send in one batch to the GCM API.").
		Default(strconv.Itoa(export.BatchSizeMax)).UintVar(&opts.BatchSize)

	a.Flag("export.token-url", "The request URL to generate token that's needed to ingest metrics to an alternative project").
		StringVar(&opts.TokenURL)

	a.Flag("export.token-body", "The request Body to generate token that's needed to ingest metrics to an alternative project.").
		StringVar(&opts.TokenBody)

	a.Flag("export.quota-project", "The projectID of an alternative project for quota attribution.").
		StringVar(&opts.QuotaProject)

	haBackend := a.Flag("export.ha.backend", fmt.Sprintf("Which backend to use to coordinate HA pairs that both send metric data to the GCM API. Valid values are %q or %q", HABackendNone, HABackendKubernetes)).
		Default(HABackendNone).Enum(HABackendNone, HABackendKubernetes)

	kubeConfigPath := a.Flag("export.ha.kube.config", "Path to kube config file.").
		Default("").String()
	kubeNamespace := a.Flag("export.ha.kube.namespace", "Namespace for the HA locking resource. Must be identical across replicas. May be set through the KUBE_NAMESPACE environment variable.").
		Default("").OverrideDefaultFromEnvar("KUBE_NAMESPACE").String()
	kubeName := a.Flag("export.ha.kube.name", "Name for the HA locking resource. Must be identical across replicas. May be set through the KUBE_NAME environment variable.").
		Default("").OverrideDefaultFromEnvar("KUBE_NAME").String()

	return func(logger log.Logger, metrics prometheus.Registerer) (*export.Exporter, error) {
		switch *haBackend {
		case HABackendNone:
		case HABackendKubernetes:
			kubecfg, err := loadKubeConfig(*kubeConfigPath)
			if err != nil {
				return nil, errors.Wrap(err, "loading kube config failed")
			}
			opts.Lease, err = lease.NewKubernetes(
				logger,
				metrics,
				kubecfg,
				*kubeNamespace, *kubeName,
				&lease.Options{},
			)
			if err != nil {
				return nil, errors.Wrap(err, "set up Kubernetes lease")
			}
		default:
			return nil, errors.Errorf("unexpected HA backend %q", haBackend)
		}
		return export.New(logger, metrics, opts)
	}
}

func loadKubeConfig(kubeconfigPath string) (*rest.Config, error) {
	if kubeconfigPath == "" {
		cfg, err := rest.InClusterConfig()
		if err == nil {
			return cfg, nil
		}
		// Fallback to default config.
	}
	rules := clientcmd.NewDefaultClientConfigLoadingRules()
	rules.ExplicitPath = kubeconfigPath

	return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, nil).ClientConfig()
}
<commit_msg>Minor update on the help message for flag export.token-url and export.token-body.<commit_after>// Copyright 2020 Google LLC
//
// 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
//
//     https://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 setup contains common logic for setting up the export package across binaries.
package setup

import (
	"fmt"
	"os"
	"strconv"

	"cloud.google.com/go/compute/metadata"
	"github.com/GoogleCloudPlatform/prometheus-engine/pkg/export"
	"github.com/GoogleCloudPlatform/prometheus-engine/pkg/lease"
	"github.com/go-kit/kit/log"
	"github.com/pkg/errors"
	"github.com/prometheus/client_golang/prometheus"
	kingpin "gopkg.in/alecthomas/kingpin.v2"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"

	// Blank import required to register auth handlers to talk use different auth mechanisms
	// for talking to the Kubernetes API server.
	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
)

// Supported HA backend modes.
const (
	HABackendNone       = "none"
	HABackendKubernetes = "kube"
)

// Generally, global state is not a good approach and actively discouraged throughout
// the Prometheus code bases. However, this is the most practical way to inject the export
// path into lower layers of Prometheus without touching an excessive amount of functions
// in our fork to propagate it.
var globalExporter *export.Exporter

// SetGlobal sets the global instance of the GCM exporter.
func SetGlobal(exporter *export.Exporter) (err error) {
	globalExporter = exporter
	return err
}

// Global returns the global instance of the GCM exporter.
func Global() *export.Exporter {
	if globalExporter == nil {
		// This should usually be a panic but we set an inactive default exporter in this case
		// to not break existing tests in Prometheus.
		fmt.Fprintln(os.Stderr, "No global GCM exporter was set, setting default inactive exporter.")
		return export.NopExporter()
	}
	return globalExporter
}

// FromFlags returns a constructor for a new exporter that is configured through flags that are
// registered with the given application. The constructor must be called after the flags
// have been parsed.
func FromFlags(a *kingpin.Application, userAgent string) func(log.Logger, prometheus.Registerer) (*export.Exporter, error) {
	var opts export.ExporterOpts

	// Default target fields if we can detect them in GCP.
	if metadata.OnGCE() {
		opts.ProjectID, _ = metadata.ProjectID()
		opts.Cluster, _ = metadata.InstanceAttributeValue("cluster-name")
		// These attributes are set for GKE nodes. For the location, we first check
		// the clustr location, which may be a zone or a region. We must always use that value
		// to avoid collisions with other clusters, as the same cluster name may be reused
		// in different locations.
		// In particular, we cannot set the location to the node's zone for a regional cluster,
		// even though this would provide more accuracy, as there may also be a zonal cluster
		// with the same name.
		// We only fallback to the node zone as the location if no cluster location exists to
		// default for deployments on GCE.
		if loc, _ := metadata.InstanceAttributeValue("cluster-location"); loc != "" {
			opts.Location = loc
		} else {
			opts.Location, _ = metadata.Zone()
		}
	}

	a.Flag("export.disable", "Disable exporting to GCM.").
		Default("false").BoolVar(&opts.Disable)

	a.Flag("export.endpoint", "GCM API endpoint to send metric data to.").
		Default("monitoring.googleapis.com:443").StringVar(&opts.Endpoint)

	a.Flag("export.compression", "The compression format to use for gRPC requests ('none' or 'gzip').").
		Default(export.CompressionNone).EnumVar(&opts.Compression, export.CompressionNone, export.CompressionGZIP)

	a.Flag("export.credentials-file", "Credentials file for authentication with the GCM API.").
		Default("").StringVar(&opts.CredentialsFile)

	a.Flag("export.label.project-id", fmt.Sprintf("Default project ID set for all exported data. Prefer setting the external label %q in the Prometheus configuration if not using the auto-discovered default.", export.KeyProjectID)).
		Default(opts.ProjectID).StringVar(&opts.ProjectID)

	a.Flag("export.user-agent", "Override for the user agent used for requests against the GCM API.").
		Default(userAgent).StringVar(&opts.UserAgent)

	// The location and cluster flag should probably not be used. On the other hand, they make it easy
	// to populate these important values in the monitored resource without interfering with existing
	// Prometheus configuration.
	a.Flag("export.label.location", fmt.Sprintf("The default location set for all exported data. Prefer setting the external label %q in the Prometheus configuration if not using the auto-discovered default.", export.KeyLocation)).
		Default(opts.Location).StringVar(&opts.Location)

	a.Flag("export.label.cluster", fmt.Sprintf("The default cluster set for all scraped targets. Prefer setting the external label %q in the Prometheus configuration if not using the auto-discovered default.", export.KeyCluster)).
		Default(opts.Cluster).StringVar(&opts.Cluster)

	a.Flag("export.match", `A Prometheus time series matcher. Can be repeated. Every time series must match at least one of the matchers to be exported. This flag can be used equivalently to the match[] parameter of the Prometheus federation endpoint to selectively export data. (Example: --export.match='{job="prometheus"}' --export.match='{__name__=~"job:.*"})`).
		Default("").SetValue(&opts.Matchers)

	a.Flag("export.debug.metric-prefix", "Google Cloud Monitoring metric prefix to use.").
		Default(export.MetricTypePrefix).StringVar(&opts.MetricTypePrefix)

	a.Flag("export.debug.disable-auth", "Disable authentication (for debugging purposes).").
		Default("false").BoolVar(&opts.DisableAuth)

	a.Flag("export.debug.batch-size", "Maximum number of points to send in one batch to the GCM API.").
		Default(strconv.Itoa(export.BatchSizeMax)).UintVar(&opts.BatchSize)

	a.Flag("export.token-url", "The request URL to generate token that's needed to ingest metrics to the project").
		StringVar(&opts.TokenURL)

	a.Flag("export.token-body", "The request Body to generate token that's needed to ingest metrics to the project.").
		StringVar(&opts.TokenBody)

	a.Flag("export.quota-project", "The projectID of an alternative project for quota attribution.").
		StringVar(&opts.QuotaProject)

	haBackend := a.Flag("export.ha.backend", fmt.Sprintf("Which backend to use to coordinate HA pairs that both send metric data to the GCM API. Valid values are %q or %q", HABackendNone, HABackendKubernetes)).
		Default(HABackendNone).Enum(HABackendNone, HABackendKubernetes)

	kubeConfigPath := a.Flag("export.ha.kube.config", "Path to kube config file.").
		Default("").String()
	kubeNamespace := a.Flag("export.ha.kube.namespace", "Namespace for the HA locking resource. Must be identical across replicas. May be set through the KUBE_NAMESPACE environment variable.").
		Default("").OverrideDefaultFromEnvar("KUBE_NAMESPACE").String()
	kubeName := a.Flag("export.ha.kube.name", "Name for the HA locking resource. Must be identical across replicas. May be set through the KUBE_NAME environment variable.").
		Default("").OverrideDefaultFromEnvar("KUBE_NAME").String()

	return func(logger log.Logger, metrics prometheus.Registerer) (*export.Exporter, error) {
		switch *haBackend {
		case HABackendNone:
		case HABackendKubernetes:
			kubecfg, err := loadKubeConfig(*kubeConfigPath)
			if err != nil {
				return nil, errors.Wrap(err, "loading kube config failed")
			}
			opts.Lease, err = lease.NewKubernetes(
				logger,
				metrics,
				kubecfg,
				*kubeNamespace, *kubeName,
				&lease.Options{},
			)
			if err != nil {
				return nil, errors.Wrap(err, "set up Kubernetes lease")
			}
		default:
			return nil, errors.Errorf("unexpected HA backend %q", haBackend)
		}
		return export.New(logger, metrics, opts)
	}
}

func loadKubeConfig(kubeconfigPath string) (*rest.Config, error) {
	if kubeconfigPath == "" {
		cfg, err := rest.InClusterConfig()
		if err == nil {
			return cfg, nil
		}
		// Fallback to default config.
	}
	rules := clientcmd.NewDefaultClientConfigLoadingRules()
	rules.ExplicitPath = kubeconfigPath

	return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, nil).ClientConfig()
}
<|endoftext|>


<commit_before>package xml

import (
	"encoding/xml"
	"io"
	"time"

	"github.com/mbrt/gmailctl/pkg/config"
	"github.com/mbrt/gmailctl/pkg/filter"
)

// Exporter exports the given entries to the Gmail xml format.
type Exporter interface {
	// Export exports Gmail filters into the Gmail xml format.
	Export(author config.Author, filters filter.Filters, w io.Writer) error
}

// DefaultExporter returns a default implementation of the XMLExporter interface.
func DefaultExporter() Exporter {
	return xmlExporter{now: defaultNow}
}

// nowFunc returns the current time
type nowFunc func() time.Time

var defaultNow nowFunc = func() time.Time { return time.Now() }

type xmlDoc struct {
	XMLName     xml.Name  `xml:"feed"`
	XMLNS       string    `xml:"xmlns,attr"`
	XMLNSApps   string    `xml:"xmlns:apps,attr"`
	Title       string    `xml:"title"`
	ID          string    `xml:"id"`
	Updated     time.Time `xml:"updated"`
	AuthorName  string    `xml:"author>name"`
	AuthorEmail string    `xml:"author>email"`
	Entries     []xmlEntry
}

type xmlEntry struct {
	XMLName    xml.Name    `xml:"entry"`
	Category   xmlCategory `xml:"category"`
	Title      string      `xml:"title"`
	Content    string      `xml:"content"`
	Properties []xmlProperty
}

type xmlCategory struct {
	Term string `xml:"term,attr"`
}

type xmlProperty struct {
	XMLName xml.Name `xml:"apps:property"`
	Name    string   `xml:"name,attr"`
	Value   string   `xml:"value,attr"`
}

type xmlExporter struct {
	// Allows to be mocked away
	now nowFunc
}

func (x xmlExporter) Export(author config.Author, filters filter.Filters, w io.Writer) error {
	doc, err := x.toXML(author, filters)
	if err != nil {
		return err
	}
	out, err := xml.MarshalIndent(doc, "", "  ")
	if err != nil {
		return err
	}
	_, err = w.Write([]byte(xml.Header))
	if err != nil {
		return err
	}
	_, err = w.Write(out)
	return err
}

func (x xmlExporter) toXML(author config.Author, filters filter.Filters) (xmlDoc, error) {
	entries, err := x.entriesToXML(filters)
	res := xmlDoc{
		XMLNS:       "http://www.w3.org/2005/Atom",
		XMLNSApps:   "http://schemas.google.com/apps/2006",
		Title:       "Mail Filters",
		ID:          "tag:mail.google.com,2008:filters:",
		Updated:     x.now(),
		AuthorName:  author.Name,
		AuthorEmail: author.Email,
		Entries:     entries,
	}
	return res, err
}

func (x xmlExporter) entriesToXML(filters filter.Filters) ([]xmlEntry, error) {
	res := make([]xmlEntry, len(filters))
	for i, f := range filters {
		props, err := x.propertiesToXML(f)
		if err != nil {
			return nil, err
		}
		xentry := xmlEntry{
			Category:   xmlCategory{"filter"},
			Title:      "Mail Filter",
			Content:    "",
			Properties: props,
		}
		res[i] = xentry
	}
	return res, nil
}

func (x xmlExporter) propertiesToXML(f filter.Filter) ([]xmlProperty, error) {
	res := x.criteriaProperties(f.Criteria)
	ap, err := x.actionProperties(f.Action)
	if err != nil {
		return nil, err
	}
	res = append(res, ap...)
	return res, nil
}

func (x xmlExporter) criteriaProperties(c filter.Criteria) []xmlProperty {
	res := []xmlProperty{}
	res = x.appendStringProperty(res, PropertyFrom, c.From)
	res = x.appendStringProperty(res, PropertyTo, c.To)
	res = x.appendStringProperty(res, PropertySubject, c.Subject)
	res = x.appendStringProperty(res, PropertyHas, c.Query)
	return res
}

func (x xmlExporter) actionProperties(a filter.Action) ([]xmlProperty, error) {
	res := []xmlProperty{}
	res = x.appendBoolProperty(res, PropertyArchive, a.Archive)
	res = x.appendBoolProperty(res, PropertyDelete, a.Delete)
	res = x.appendBoolProperty(res, PropertyMarkImportant, a.MarkImportant)
	res = x.appendBoolProperty(res, PropertyMarkRead, a.MarkRead)
	res = x.appendStringProperty(res, PropertyApplyLabel, a.AddLabel)

	if a.Category != "" {
		cat, err := categoryToSmartLabel(a.Category)
		if err != nil {
			return nil, err
		}
		res = x.appendStringProperty(res, PropertyApplyCategory, cat)
	}

	return res, nil
}

func (x xmlExporter) appendStringProperty(res []xmlProperty, name, value string) []xmlProperty {
	if value == "" {
		return res
	}
	p := xmlProperty{
		Name:  name,
		Value: value,
	}
	return append(res, p)
}

func (x xmlExporter) appendBoolProperty(res []xmlProperty, name string, value bool) []xmlProperty {
	if !value {
		return res
	}
	p := xmlProperty{
		Name:  name,
		Value: "true",
	}
	return append(res, p)
}
<commit_msg>fix missing newline from export<commit_after>package xml

import (
	"encoding/xml"
	"io"
	"time"

	"github.com/mbrt/gmailctl/pkg/config"
	"github.com/mbrt/gmailctl/pkg/filter"
)

// Exporter exports the given entries to the Gmail xml format.
type Exporter interface {
	// Export exports Gmail filters into the Gmail xml format.
	Export(author config.Author, filters filter.Filters, w io.Writer) error
}

// DefaultExporter returns a default implementation of the XMLExporter interface.
func DefaultExporter() Exporter {
	return xmlExporter{now: defaultNow}
}

// nowFunc returns the current time
type nowFunc func() time.Time

var defaultNow nowFunc = func() time.Time { return time.Now() }

type xmlDoc struct {
	XMLName     xml.Name  `xml:"feed"`
	XMLNS       string    `xml:"xmlns,attr"`
	XMLNSApps   string    `xml:"xmlns:apps,attr"`
	Title       string    `xml:"title"`
	ID          string    `xml:"id"`
	Updated     time.Time `xml:"updated"`
	AuthorName  string    `xml:"author>name"`
	AuthorEmail string    `xml:"author>email"`
	Entries     []xmlEntry
}

type xmlEntry struct {
	XMLName    xml.Name    `xml:"entry"`
	Category   xmlCategory `xml:"category"`
	Title      string      `xml:"title"`
	Content    string      `xml:"content"`
	Properties []xmlProperty
}

type xmlCategory struct {
	Term string `xml:"term,attr"`
}

type xmlProperty struct {
	XMLName xml.Name `xml:"apps:property"`
	Name    string   `xml:"name,attr"`
	Value   string   `xml:"value,attr"`
}

type xmlExporter struct {
	// Allows to be mocked away
	now nowFunc
}

func (x xmlExporter) Export(author config.Author, filters filter.Filters, w io.Writer) error {
	doc, err := x.toXML(author, filters)
	if err != nil {
		return err
	}
	out, err := xml.MarshalIndent(doc, "", "  ")
	if err != nil {
		return err
	}
	_, err = w.Write([]byte(xml.Header))
	if err != nil {
		return err
	}
	_, err = w.Write(out)
	if err != nil {
		return err
	}
	_, err = io.WriteString(w, "\n")
	return err
}

func (x xmlExporter) toXML(author config.Author, filters filter.Filters) (xmlDoc, error) {
	entries, err := x.entriesToXML(filters)
	res := xmlDoc{
		XMLNS:       "http://www.w3.org/2005/Atom",
		XMLNSApps:   "http://schemas.google.com/apps/2006",
		Title:       "Mail Filters",
		ID:          "tag:mail.google.com,2008:filters:",
		Updated:     x.now(),
		AuthorName:  author.Name,
		AuthorEmail: author.Email,
		Entries:     entries,
	}
	return res, err
}

func (x xmlExporter) entriesToXML(filters filter.Filters) ([]xmlEntry, error) {
	res := make([]xmlEntry, len(filters))
	for i, f := range filters {
		props, err := x.propertiesToXML(f)
		if err != nil {
			return nil, err
		}
		xentry := xmlEntry{
			Category:   xmlCategory{"filter"},
			Title:      "Mail Filter",
			Content:    "",
			Properties: props,
		}
		res[i] = xentry
	}
	return res, nil
}

func (x xmlExporter) propertiesToXML(f filter.Filter) ([]xmlProperty, error) {
	res := x.criteriaProperties(f.Criteria)
	ap, err := x.actionProperties(f.Action)
	if err != nil {
		return nil, err
	}
	res = append(res, ap...)
	return res, nil
}

func (x xmlExporter) criteriaProperties(c filter.Criteria) []xmlProperty {
	res := []xmlProperty{}
	res = x.appendStringProperty(res, PropertyFrom, c.From)
	res = x.appendStringProperty(res, PropertyTo, c.To)
	res = x.appendStringProperty(res, PropertySubject, c.Subject)
	res = x.appendStringProperty(res, PropertyHas, c.Query)
	return res
}

func (x xmlExporter) actionProperties(a filter.Action) ([]xmlProperty, error) {
	res := []xmlProperty{}
	res = x.appendBoolProperty(res, PropertyArchive, a.Archive)
	res = x.appendBoolProperty(res, PropertyDelete, a.Delete)
	res = x.appendBoolProperty(res, PropertyMarkImportant, a.MarkImportant)
	res = x.appendBoolProperty(res, PropertyMarkRead, a.MarkRead)
	res = x.appendStringProperty(res, PropertyApplyLabel, a.AddLabel)

	if a.Category != "" {
		cat, err := categoryToSmartLabel(a.Category)
		if err != nil {
			return nil, err
		}
		res = x.appendStringProperty(res, PropertyApplyCategory, cat)
	}

	return res, nil
}

func (x xmlExporter) appendStringProperty(res []xmlProperty, name, value string) []xmlProperty {
	if value == "" {
		return res
	}
	p := xmlProperty{
		Name:  name,
		Value: value,
	}
	return append(res, p)
}

func (x xmlExporter) appendBoolProperty(res []xmlProperty, name string, value bool) []xmlProperty {
	if !value {
		return res
	}
	p := xmlProperty{
		Name:  name,
		Value: "true",
	}
	return append(res, p)
}
<|endoftext|>


<commit_before>// Copyright 2016 Cisco Systems, Inc.
//
// 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 hostagent

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"
	"strings"

	"github.com/Sirupsen/logrus"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/cache"

	"k8s.io/kubernetes/pkg/controller"
)

type opflexServiceMapping struct {
	ServiceIp    string `json:"service-ip,omitempty"`
	ServiceProto string `json:"service-proto,omitempty"`
	ServicePort  uint16 `json:"service-port,omitempty"`

	NextHopIps  []string `json:"next-hop-ips"`
	NextHopPort uint16   `json:"next-hop-port,omitempty"`

	Conntrack bool `json:"conntrack-enabled"`
}

type opflexService struct {
	Uuid string `json:"uuid"`

	DomainPolicySpace string `json:"domain-policy-space,omitempty"`
	DomainName        string `json:"domain-name,omitempty"`

	ServiceMode   string `json:"service-mode,omitempty"`
	ServiceMac    string `json:"service-mac,omitempty"`
	InterfaceName string `json:"interface-name,omitempty"`
	InterfaceIp   string `json:"interface-ip,omitempty"`
	InterfaceVlan uint16 `json:"interface-vlan,omitempty"`

	ServiceMappings []opflexServiceMapping `json:"service-mapping"`

	Attributes map[string]string `json:"attributes,omitempty"`
}

func (agent *HostAgent) initEndpointsInformerFromClient(
	kubeClient *kubernetes.Clientset) {
	agent.initEndpointsInformerBase(
		&cache.ListWatch{
			ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
				return kubeClient.Core().Endpoints(metav1.NamespaceAll).List(options)
			},
			WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
				return kubeClient.Core().Endpoints(metav1.NamespaceAll).Watch(options)
			},
		})
}

func (agent *HostAgent) initEndpointsInformerBase(listWatch *cache.ListWatch) {
	agent.endpointsInformer = cache.NewSharedIndexInformer(
		listWatch,
		&v1.Endpoints{},
		controller.NoResyncPeriodFunc(),
		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
	)
	agent.endpointsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			agent.endpointsChanged(obj)
		},
		UpdateFunc: func(_ interface{}, obj interface{}) {
			agent.endpointsChanged(obj)
		},
		DeleteFunc: func(obj interface{}) {
			agent.endpointsChanged(obj)
		},
	})
}

func (agent *HostAgent) initServiceInformerFromClient(
	kubeClient *kubernetes.Clientset) {
	agent.initServiceInformerBase(
		&cache.ListWatch{
			ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
				return kubeClient.Core().Services(metav1.NamespaceAll).List(options)
			},
			WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
				return kubeClient.Core().Services(metav1.NamespaceAll).Watch(options)
			},
		})
}

func (agent *HostAgent) initServiceInformerBase(listWatch *cache.ListWatch) {
	agent.serviceInformer = cache.NewSharedIndexInformer(
		listWatch,
		&v1.Service{},
		controller.NoResyncPeriodFunc(),
		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
	)
	agent.serviceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			agent.serviceChanged(obj)
		},
		UpdateFunc: func(_ interface{}, obj interface{}) {
			agent.serviceChanged(obj)
		},
		DeleteFunc: func(obj interface{}) {
			agent.serviceDeleted(obj)
		},
	})
}

func getAs(asfile string) (string, error) {
	raw, err := ioutil.ReadFile(asfile)
	if err != nil {
		return "", err
	}
	return string(raw), err
}

func writeAs(asfile string, as *opflexService) (bool, error) {
	newdata, err := json.MarshalIndent(as, "", "  ")
	if err != nil {
		return true, err
	}
	existingdata, err := ioutil.ReadFile(asfile)
	if err == nil && reflect.DeepEqual(existingdata, newdata) {
		return false, nil
	}

	err = ioutil.WriteFile(asfile, newdata, 0644)
	return true, err
}

func serviceLogger(log *logrus.Logger, as *v1.Service) *logrus.Entry {
	return log.WithFields(logrus.Fields{
		"namespace": as.ObjectMeta.Namespace,
		"name":      as.ObjectMeta.Name,
		"type":      as.Spec.Type,
	})
}

func opflexServiceLogger(log *logrus.Logger, as *opflexService) *logrus.Entry {
	return log.WithFields(logrus.Fields{
		"namespace": as.Attributes["namespace"],
		"name":      as.Attributes["name"],
		"uuid":      as.Uuid,
		"tenant":    as.DomainPolicySpace,
		"vrf":       as.DomainName,
	})
}

func (agent *HostAgent) syncServices() bool {
	if !agent.syncEnabled {
		return false
	}

	agent.log.Debug("Syncing services")
	agent.indexMutex.Lock()
	opflexServices := make(map[string]*opflexService)
	for k, v := range agent.opflexServices {
		opflexServices[k] = v
	}
	agent.indexMutex.Unlock()

	files, err := ioutil.ReadDir(agent.config.OpFlexServiceDir)
	if err != nil {
		agent.log.WithFields(
			logrus.Fields{"serviceDir": agent.config.OpFlexServiceDir},
		).Error("Could not read directory " + err.Error())
		return true
	}
	seen := make(map[string]bool)
	for _, f := range files {
		uuid := f.Name()
		if strings.HasSuffix(uuid, ".as") {
			uuid = uuid[:len(uuid)-3]
		} else if strings.HasSuffix(f.Name(), ".service") {
			uuid = uuid[:len(uuid)-8]
		} else {
			continue
		}

		asfile := filepath.Join(agent.config.OpFlexServiceDir, f.Name())
		logger := agent.log.WithFields(
			logrus.Fields{"Uuid": uuid},
		)

		existing, ok := opflexServices[uuid]
		if ok {
			wrote, err := writeAs(asfile, existing)
			if err != nil {
				opflexServiceLogger(agent.log, existing).
					Error("Error writing service file: ", err)
			} else if wrote {
				opflexServiceLogger(agent.log, existing).Info("Updated service")
			}
			seen[uuid] = true
		} else {
			logger.Info("Removing service")
			os.Remove(asfile)
		}
	}

	for _, as := range opflexServices {
		if seen[as.Uuid] {
			continue
		}

		opflexServiceLogger(agent.log, as).Info("Adding service")
		asfile :=
			filepath.Join(agent.config.OpFlexServiceDir, as.Uuid+".service")
		_, err = writeAs(asfile, as)
		if err != nil {
			opflexServiceLogger(agent.log, as).
				Error("Error writing service file: ", err)
		}
	}

	agent.log.Debug("Finished service sync")
	return false
}

// Must have index lock
func (agent *HostAgent) updateServiceDesc(external bool, as *v1.Service,
	endpoints *v1.Endpoints) bool {
	ofas := &opflexService{
		Uuid:              string(as.ObjectMeta.UID),
		DomainPolicySpace: agent.config.AciVrfTenant,
		DomainName:        agent.config.AciVrf,
		ServiceMode:       "loadbalancer",
		ServiceMappings:   make([]opflexServiceMapping, 0),
	}

	if external {
		if agent.config.UplinkIface == "" ||
			agent.serviceEp.Ipv4 == nil ||
			agent.serviceEp.Mac == "" {
			return false
		}

		ofas.InterfaceName = agent.config.UplinkIface
		ofas.InterfaceVlan = uint16(agent.config.ServiceVlan)
		ofas.ServiceMac = agent.serviceEp.Mac
		ofas.InterfaceIp = agent.serviceEp.Ipv4.String()
		ofas.Uuid = ofas.Uuid + "-external"
	}

	hasValidMapping := false
	for _, sp := range as.Spec.Ports {
		for _, e := range endpoints.Subsets {
			for _, p := range e.Ports {
				if p.Protocol != sp.Protocol {
					continue
				}
				if p.Name != sp.Name {
					continue
				}

				sm := &opflexServiceMapping{
					ServicePort:  uint16(sp.Port),
					ServiceProto: strings.ToLower(string(sp.Protocol)),
					NextHopIps:   make([]string, 0),
					NextHopPort:  uint16(p.Port),
					Conntrack:    true,
				}

				if external {
					if as.Spec.Type == v1.ServiceTypeLoadBalancer &&
						len(as.Status.LoadBalancer.Ingress) > 0 {
						sm.ServiceIp = as.Status.LoadBalancer.Ingress[0].IP
					}
				} else {
					sm.ServiceIp = as.Spec.ClusterIP
				}

				for _, a := range e.Addresses {
					if !external ||
						(a.NodeName != nil && *a.NodeName == agent.config.NodeName) {
						sm.NextHopIps = append(sm.NextHopIps, a.IP)
					}
				}
				if sm.ServiceIp != "" && len(sm.NextHopIps) > 0 {
					hasValidMapping = true
				}
				ofas.ServiceMappings = append(ofas.ServiceMappings, *sm)
			}
		}
	}

	id := fmt.Sprintf("%s_%s", as.ObjectMeta.Namespace, as.ObjectMeta.Name)
	ofas.Attributes = as.ObjectMeta.Labels
	if ofas.Attributes == nil {
		ofas.Attributes = make(map[string]string)
	}
	ofas.Attributes["namespace"] = as.ObjectMeta.Namespace
	ofas.Attributes["name"] = as.ObjectMeta.Name
	ofas.Attributes["service-name"] = id

	existing, ok := agent.opflexServices[ofas.Uuid]
	if hasValidMapping {
		if (ok && !reflect.DeepEqual(existing, ofas)) || !ok {
			agent.opflexServices[ofas.Uuid] = ofas
			return true
		}
	} else {
		if ok {
			delete(agent.opflexServices, ofas.Uuid)
			return true
		}
	}

	return false
}

// must have index lock
func (agent *HostAgent) doUpdateService(key string) {
	endpointsobj, exists, err :=
		agent.endpointsInformer.GetStore().GetByKey(key)
	if err != nil {
		agent.log.Error("Could not lookup endpoints for " +
			key + ": " + err.Error())
		return
	}
	if !exists || endpointsobj == nil {
		return
	}
	asobj, exists, err := agent.serviceInformer.GetStore().GetByKey(key)
	if err != nil {
		agent.log.Error("Could not lookup service for " +
			key + ": " + err.Error())
		return
	}
	if !exists || asobj == nil {
		return
	}

	endpoints := endpointsobj.(*v1.Endpoints)
	as := asobj.(*v1.Service)

	doSync := false
	doSync = agent.updateServiceDesc(false, as, endpoints) || doSync
	doSync = agent.updateServiceDesc(true, as, endpoints) || doSync
	if doSync {
		agent.scheduleSyncServices()
	}
}

func (agent *HostAgent) endpointsChanged(obj interface{}) {
	agent.indexMutex.Lock()
	defer agent.indexMutex.Unlock()

	endpoints := obj.(*v1.Endpoints)

	key, err := cache.MetaNamespaceKeyFunc(endpoints)
	if err != nil {
		agent.log.Error("Could not create key:" + err.Error())
		return
	}
	agent.doUpdateService(key)
}

func (agent *HostAgent) serviceChanged(obj interface{}) {
	agent.indexMutex.Lock()
	defer agent.indexMutex.Unlock()

	as := obj.(*v1.Service)

	key, err := cache.MetaNamespaceKeyFunc(as)
	if err != nil {
		serviceLogger(agent.log, as).
			Error("Could not create key:" + err.Error())
		return
	}

	agent.doUpdateService(key)
}

func (agent *HostAgent) serviceDeleted(obj interface{}) {
	agent.indexMutex.Lock()
	defer agent.indexMutex.Unlock()

	as := obj.(*v1.Service)

	u := string(as.ObjectMeta.UID)
	if _, ok := agent.opflexServices[u]; ok {
		delete(agent.opflexServices, u)
		delete(agent.opflexServices, u+"-external")
		agent.scheduleSyncServices()
	}
}

func (agent *HostAgent) updateAllServices() {
	if agent.serviceInformer == nil {
		return
	}
	store := agent.serviceInformer.GetStore()
	if store == nil {
		return
	}
	keys := agent.serviceInformer.GetStore().ListKeys()
	if keys == nil {
		return
	}

	agent.indexMutex.Lock()
	defer agent.indexMutex.Unlock()
	for _, key := range keys {
		agent.doUpdateService(key)
	}
}
<commit_msg>CSCvp26247 Ignore headless service processing Added a check in hostagent to not process headless service (ClusterIP: None)<commit_after>// Copyright 2016 Cisco Systems, Inc.
//
// 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 hostagent

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"
	"strings"

	"github.com/Sirupsen/logrus"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/cache"

	"k8s.io/kubernetes/pkg/controller"
)

type opflexServiceMapping struct {
	ServiceIp    string `json:"service-ip,omitempty"`
	ServiceProto string `json:"service-proto,omitempty"`
	ServicePort  uint16 `json:"service-port,omitempty"`

	NextHopIps  []string `json:"next-hop-ips"`
	NextHopPort uint16   `json:"next-hop-port,omitempty"`

	Conntrack bool `json:"conntrack-enabled"`
}

type opflexService struct {
	Uuid string `json:"uuid"`

	DomainPolicySpace string `json:"domain-policy-space,omitempty"`
	DomainName        string `json:"domain-name,omitempty"`

	ServiceMode   string `json:"service-mode,omitempty"`
	ServiceMac    string `json:"service-mac,omitempty"`
	InterfaceName string `json:"interface-name,omitempty"`
	InterfaceIp   string `json:"interface-ip,omitempty"`
	InterfaceVlan uint16 `json:"interface-vlan,omitempty"`

	ServiceMappings []opflexServiceMapping `json:"service-mapping"`

	Attributes map[string]string `json:"attributes,omitempty"`
}

func (agent *HostAgent) initEndpointsInformerFromClient(
	kubeClient *kubernetes.Clientset) {
	agent.initEndpointsInformerBase(
		&cache.ListWatch{
			ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
				return kubeClient.Core().Endpoints(metav1.NamespaceAll).List(options)
			},
			WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
				return kubeClient.Core().Endpoints(metav1.NamespaceAll).Watch(options)
			},
		})
}

func (agent *HostAgent) initEndpointsInformerBase(listWatch *cache.ListWatch) {
	agent.endpointsInformer = cache.NewSharedIndexInformer(
		listWatch,
		&v1.Endpoints{},
		controller.NoResyncPeriodFunc(),
		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
	)
	agent.endpointsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			agent.endpointsChanged(obj)
		},
		UpdateFunc: func(_ interface{}, obj interface{}) {
			agent.endpointsChanged(obj)
		},
		DeleteFunc: func(obj interface{}) {
			agent.endpointsChanged(obj)
		},
	})
}

func (agent *HostAgent) initServiceInformerFromClient(
	kubeClient *kubernetes.Clientset) {
	agent.initServiceInformerBase(
		&cache.ListWatch{
			ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
				return kubeClient.Core().Services(metav1.NamespaceAll).List(options)
			},
			WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
				return kubeClient.Core().Services(metav1.NamespaceAll).Watch(options)
			},
		})
}

func (agent *HostAgent) initServiceInformerBase(listWatch *cache.ListWatch) {
	agent.serviceInformer = cache.NewSharedIndexInformer(
		listWatch,
		&v1.Service{},
		controller.NoResyncPeriodFunc(),
		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
	)
	agent.serviceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			agent.serviceChanged(obj)
		},
		UpdateFunc: func(_ interface{}, obj interface{}) {
			agent.serviceChanged(obj)
		},
		DeleteFunc: func(obj interface{}) {
			agent.serviceDeleted(obj)
		},
	})
}

func getAs(asfile string) (string, error) {
	raw, err := ioutil.ReadFile(asfile)
	if err != nil {
		return "", err
	}
	return string(raw), err
}

func writeAs(asfile string, as *opflexService) (bool, error) {
	newdata, err := json.MarshalIndent(as, "", "  ")
	if err != nil {
		return true, err
	}
	existingdata, err := ioutil.ReadFile(asfile)
	if err == nil && reflect.DeepEqual(existingdata, newdata) {
		return false, nil
	}

	err = ioutil.WriteFile(asfile, newdata, 0644)
	return true, err
}

func serviceLogger(log *logrus.Logger, as *v1.Service) *logrus.Entry {
	return log.WithFields(logrus.Fields{
		"namespace": as.ObjectMeta.Namespace,
		"name":      as.ObjectMeta.Name,
		"type":      as.Spec.Type,
	})
}

func opflexServiceLogger(log *logrus.Logger, as *opflexService) *logrus.Entry {
	return log.WithFields(logrus.Fields{
		"namespace": as.Attributes["namespace"],
		"name":      as.Attributes["name"],
		"uuid":      as.Uuid,
		"tenant":    as.DomainPolicySpace,
		"vrf":       as.DomainName,
	})
}

func (agent *HostAgent) syncServices() bool {
	if !agent.syncEnabled {
		return false
	}

	agent.log.Debug("Syncing services")
	agent.indexMutex.Lock()
	opflexServices := make(map[string]*opflexService)
	for k, v := range agent.opflexServices {
		opflexServices[k] = v
	}
	agent.indexMutex.Unlock()

	files, err := ioutil.ReadDir(agent.config.OpFlexServiceDir)
	if err != nil {
		agent.log.WithFields(
			logrus.Fields{"serviceDir": agent.config.OpFlexServiceDir},
		).Error("Could not read directory " + err.Error())
		return true
	}
	seen := make(map[string]bool)
	for _, f := range files {
		uuid := f.Name()
		if strings.HasSuffix(uuid, ".as") {
			uuid = uuid[:len(uuid)-3]
		} else if strings.HasSuffix(f.Name(), ".service") {
			uuid = uuid[:len(uuid)-8]
		} else {
			continue
		}

		asfile := filepath.Join(agent.config.OpFlexServiceDir, f.Name())
		logger := agent.log.WithFields(
			logrus.Fields{"Uuid": uuid},
		)

		existing, ok := opflexServices[uuid]
		if ok {
			wrote, err := writeAs(asfile, existing)
			if err != nil {
				opflexServiceLogger(agent.log, existing).
					Error("Error writing service file: ", err)
			} else if wrote {
				opflexServiceLogger(agent.log, existing).Info("Updated service")
			}
			seen[uuid] = true
		} else {
			logger.Info("Removing service")
			os.Remove(asfile)
		}
	}

	for _, as := range opflexServices {
		if seen[as.Uuid] {
			continue
		}

		opflexServiceLogger(agent.log, as).Info("Adding service")
		asfile :=
			filepath.Join(agent.config.OpFlexServiceDir, as.Uuid+".service")
		_, err = writeAs(asfile, as)
		if err != nil {
			opflexServiceLogger(agent.log, as).
				Error("Error writing service file: ", err)
		}
	}

	agent.log.Debug("Finished service sync")
	return false
}

// Must have index lock
func (agent *HostAgent) updateServiceDesc(external bool, as *v1.Service,
	endpoints *v1.Endpoints) bool {

       if as.Spec.ClusterIP == "None" {
	   agent.log.Debug("ClusterIP is set to None")
           return true
       }

	ofas := &opflexService{
		Uuid:              string(as.ObjectMeta.UID),
		DomainPolicySpace: agent.config.AciVrfTenant,
		DomainName:        agent.config.AciVrf,
		ServiceMode:       "loadbalancer",
		ServiceMappings:   make([]opflexServiceMapping, 0),
	}

	if external {
		if agent.config.UplinkIface == "" ||
			agent.serviceEp.Ipv4 == nil ||
			agent.serviceEp.Mac == "" {
			return false
		}

		ofas.InterfaceName = agent.config.UplinkIface
		ofas.InterfaceVlan = uint16(agent.config.ServiceVlan)
		ofas.ServiceMac = agent.serviceEp.Mac
		ofas.InterfaceIp = agent.serviceEp.Ipv4.String()
		ofas.Uuid = ofas.Uuid + "-external"
	}

	hasValidMapping := false
	for _, sp := range as.Spec.Ports {
		for _, e := range endpoints.Subsets {
			for _, p := range e.Ports {
				if p.Protocol != sp.Protocol {
					continue
				}
				if p.Name != sp.Name {
					continue
				}

				sm := &opflexServiceMapping{
					ServicePort:  uint16(sp.Port),
					ServiceProto: strings.ToLower(string(sp.Protocol)),
					NextHopIps:   make([]string, 0),
					NextHopPort:  uint16(p.Port),
					Conntrack:    true,
				}

				if external {
					if as.Spec.Type == v1.ServiceTypeLoadBalancer &&
						len(as.Status.LoadBalancer.Ingress) > 0 {
						sm.ServiceIp = as.Status.LoadBalancer.Ingress[0].IP
					}
				} else {
					sm.ServiceIp = as.Spec.ClusterIP
				}

				for _, a := range e.Addresses {
					if !external ||
						(a.NodeName != nil && *a.NodeName == agent.config.NodeName) {
						sm.NextHopIps = append(sm.NextHopIps, a.IP)
					}
				}
				if sm.ServiceIp != "" && len(sm.NextHopIps) > 0 {
					hasValidMapping = true
				}
				ofas.ServiceMappings = append(ofas.ServiceMappings, *sm)
			}
		}
	}

	id := fmt.Sprintf("%s_%s", as.ObjectMeta.Namespace, as.ObjectMeta.Name)
	ofas.Attributes = as.ObjectMeta.Labels
	if ofas.Attributes == nil {
		ofas.Attributes = make(map[string]string)
	}
	ofas.Attributes["namespace"] = as.ObjectMeta.Namespace
	ofas.Attributes["name"] = as.ObjectMeta.Name
	ofas.Attributes["service-name"] = id

	existing, ok := agent.opflexServices[ofas.Uuid]
	if hasValidMapping {
		if (ok && !reflect.DeepEqual(existing, ofas)) || !ok {
			agent.opflexServices[ofas.Uuid] = ofas
			return true
		}
	} else {
		if ok {
			delete(agent.opflexServices, ofas.Uuid)
			return true
		}
	}

	return false
}

// must have index lock
func (agent *HostAgent) doUpdateService(key string) {
	endpointsobj, exists, err :=
		agent.endpointsInformer.GetStore().GetByKey(key)
	if err != nil {
		agent.log.Error("Could not lookup endpoints for " +
			key + ": " + err.Error())
		return
	}
	if !exists || endpointsobj == nil {
		return
	}
	asobj, exists, err := agent.serviceInformer.GetStore().GetByKey(key)
	if err != nil {
		agent.log.Error("Could not lookup service for " +
			key + ": " + err.Error())
		return
	}
	if !exists || asobj == nil {
		return
	}

	endpoints := endpointsobj.(*v1.Endpoints)
	as := asobj.(*v1.Service)

	doSync := false
	doSync = agent.updateServiceDesc(false, as, endpoints) || doSync
	doSync = agent.updateServiceDesc(true, as, endpoints) || doSync
	if doSync {
		agent.scheduleSyncServices()
	}
}

func (agent *HostAgent) endpointsChanged(obj interface{}) {
	agent.indexMutex.Lock()
	defer agent.indexMutex.Unlock()

	endpoints := obj.(*v1.Endpoints)

	key, err := cache.MetaNamespaceKeyFunc(endpoints)
	if err != nil {
		agent.log.Error("Could not create key:" + err.Error())
		return
	}
	agent.doUpdateService(key)
}

func (agent *HostAgent) serviceChanged(obj interface{}) {
	agent.indexMutex.Lock()
	defer agent.indexMutex.Unlock()

	as := obj.(*v1.Service)

	key, err := cache.MetaNamespaceKeyFunc(as)
	if err != nil {
		serviceLogger(agent.log, as).
			Error("Could not create key:" + err.Error())
		return
	}

	agent.doUpdateService(key)
}

func (agent *HostAgent) serviceDeleted(obj interface{}) {
	agent.indexMutex.Lock()
	defer agent.indexMutex.Unlock()

	as := obj.(*v1.Service)

	u := string(as.ObjectMeta.UID)
	if _, ok := agent.opflexServices[u]; ok {
		delete(agent.opflexServices, u)
		delete(agent.opflexServices, u+"-external")
		agent.scheduleSyncServices()
	}
}

func (agent *HostAgent) updateAllServices() {
	if agent.serviceInformer == nil {
		return
	}
	store := agent.serviceInformer.GetStore()
	if store == nil {
		return
	}
	keys := agent.serviceInformer.GetStore().ListKeys()
	if keys == nil {
		return
	}

	agent.indexMutex.Lock()
	defer agent.indexMutex.Unlock()
	for _, key := range keys {
		agent.doUpdateService(key)
	}
}
<|endoftext|>