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

package main

import (
	"bytes"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
)

var (
	cmd                 *exec.Cmd
	state               sync.Mutex
	eventTime           = make(map[string]int64)
	scheduleTime        time.Time
	watchExts           = []string{".go"}
	watchExtsStatic     = []string{".html", ".tpl", ".js", ".css"}
	ignoredFilesRegExps = []string{
		`.#(\w+).go`,
		`.(\w+).go.swp`,
		`(\w+).go~`,
		`(\w+).tmp`,
	}
)

// NewWatcher starts an fsnotify Watcher on the specified paths
func NewWatcher(paths []string, files []string, isgenerate bool) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		logger.Fatalf("Failed to create watcher: %s", err)
	}

	go func() {
		for {
			select {
			case e := <-watcher.Events:
				isBuild := true

				if ifStaticFile(e.Name) || conf.EnableReload {
					sendReload(e.String())
					continue
				}
				// Skip ignored files
				if shouldIgnoreFile(e.Name) {
					continue
				}
				if !shouldWatchFileWithExtension(e.Name) {
					continue
				}

				mt := getFileModTime(e.Name)
				if t := eventTime[e.Name]; mt == t {
					logger.Infof(bold("Skipping: ")+"%s", e.String())
					isBuild = false
				}

				eventTime[e.Name] = mt

				if isBuild {
					logger.Infof("Event fired: %s", e)
					go func() {
						// Wait 1s before autobuild until there is no file change.
						scheduleTime = time.Now().Add(1 * time.Second)
						time.Sleep(scheduleTime.Sub(time.Now()))
						AutoBuild(files, isgenerate)

						if conf.EnableReload {
							sendReload(e.String())
						}
					}()
				}
			case err := <-watcher.Errors:
				logger.Warnf("Watcher error: %s", err.Error()) // No need to exit here
			}
		}
	}()

	logger.Info("Initializing watcher...")
	for _, path := range paths {
		logger.Infof(bold("Watching: ")+"%s", path)
		err = watcher.Add(path)
		if err != nil {
			logger.Fatalf("Failed to watch directory: %s", err)
		}
	}
}

// getFileModTime returns unix timestamp of `os.File.ModTime` for the given path.
func getFileModTime(path string) int64 {
	path = strings.Replace(path, "\\", "/", -1)
	f, err := os.Open(path)
	if err != nil {
		logger.Errorf("Failed to open file on '%s': %s", path, err)
		return time.Now().Unix()
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		logger.Errorf("Failed to get file stats: %s", err)
		return time.Now().Unix()
	}

	return fi.ModTime().Unix()
}

// AutoBuild builds the specified set of files
func AutoBuild(files []string, isgenerate bool) {
	state.Lock()
	defer state.Unlock()

	os.Chdir(currpath)

	cmdName := "go"
	if conf.Gopm.Enable {
		cmdName = "gopm"
	}

	var (
		err    error
		stderr bytes.Buffer
		stdout bytes.Buffer
	)
	// For applications use full import path like "github.com/.../.."
	// are able to use "go install" to reduce build time.
	if conf.GoInstall {
		icmd := exec.Command(cmdName, "install", "-v")
		icmd.Stdout = os.Stdout
		icmd.Stderr = os.Stderr
		icmd.Env = append(os.Environ(), "GOGC=off")
		icmd.Run()
	}
	if conf.Gopm.Install {
		icmd := exec.Command("go", "list", "./...")
		icmd.Stdout = &stdout
		icmd.Env = append(os.Environ(), "GOGC=off")
		err = icmd.Run()
		if err == nil {
			list := strings.Split(stdout.String(), "\n")[1:]
			for _, pkg := range list {
				if len(pkg) == 0 {
					continue
				}
				icmd = exec.Command(cmdName, "install", pkg)
				icmd.Stdout = os.Stdout
				icmd.Stderr = os.Stderr
				icmd.Env = append(os.Environ(), "GOGC=off")
				err = icmd.Run()
				if err != nil {
					break
				}
			}
		}
	}

	if isgenerate {
		logger.Info("Generating the docs...")
		icmd := exec.Command("bee", "generate", "docs")
		icmd.Env = append(os.Environ(), "GOGC=off")
		err = icmd.Run()
		if err != nil {
			logger.Errorf("Failed to generate the docs.")
			return
		}
		logger.Success("Docs generated!")
	}

	if err == nil {
		appName := appname
		if runtime.GOOS == "windows" {
			appName += ".exe"
		}

		args := []string{"build"}
		args = append(args, "-o", appName)
		if buildTags != "" {
			args = append(args, "-tags", buildTags)
		}
		args = append(args, files...)

		bcmd := exec.Command(cmdName, args...)
		bcmd.Env = append(os.Environ(), "GOGC=off")
		bcmd.Stderr = &stderr
		err = bcmd.Run()
		if err != nil {
			logger.Errorf("Failed to build the application: %s", stderr.String())
			return
		}
	}

	logger.Success("Built Successfully!")
	Restart(appname)
}

// Kill kills the running command process
func Kill() {
	defer func() {
		if e := recover(); e != nil {
			logger.Infof("Kill recover: %s", e)
		}
	}()
	if cmd != nil && cmd.Process != nil {
		err := cmd.Process.Kill()
		if err != nil {
			logger.Errorf("Error while killing cmd process: %s", err)
		}
	}
}

// Restart kills the running command process and starts it again
func Restart(appname string) {
	logger.Debugf("Kill running process", __FILE__(), __LINE__())
	Kill()
	go Start(appname)
}

// Start starts the command process
func Start(appname string) {
	logger.Infof("Restarting '%s'...", appname)
	if strings.Index(appname, "./") == -1 {
		appname = "./" + appname
	}

	cmd = exec.Command(appname)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Args = append([]string{appname}, conf.CmdArgs...)
	cmd.Env = append(os.Environ(), conf.Envs...)

	go cmd.Run()
	logger.Successf("'%s' is running...", appname)
	started <- true
}

func ifStaticFile(filename string) bool {
	for _, s := range watchExtsStatic {
		if strings.HasSuffix(filename, s) {
			return true
		}
	}
	return false
}

// shouldIgnoreFile ignores filenames generated by Emacs, Vim or SublimeText.
// It returns true if the file should be ignored, false otherwise.
func shouldIgnoreFile(filename string) bool {
	for _, regex := range ignoredFilesRegExps {
		r, err := regexp.Compile(regex)
		if err != nil {
			logger.Fatalf("Could not compile regular expression: %s", err)
		}
		if r.MatchString(filename) {
			return true
		}
		continue
	}
	return false
}

// shouldWatchFileWithExtension returns true if the name of the file
// hash a suffix that should be watched.
func shouldWatchFileWithExtension(name string) bool {
	for _, s := range watchExts {
		if strings.HasSuffix(name, s) {
			return true
		}
	}
	return false
}
<commit_msg>Update watch.go<commit_after>// Copyright 2013 bee authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package main

import (
	"bytes"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
)

var (
	cmd                 *exec.Cmd
	state               sync.Mutex
	eventTime           = make(map[string]int64)
	scheduleTime        time.Time
	watchExts           = []string{".go"}
	watchExtsStatic     = []string{".html", ".tpl", ".js", ".css"}
	ignoredFilesRegExps = []string{
		`.#(\w+).go`,
		`.(\w+).go.swp`,
		`(\w+).go~`,
		`(\w+).tmp`,
	}
)

// NewWatcher starts an fsnotify Watcher on the specified paths
func NewWatcher(paths []string, files []string, isgenerate bool) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		logger.Fatalf("Failed to create watcher: %s", err)
	}

	go func() {
		for {
			select {
			case e := <-watcher.Events:
				isBuild := true

				if ifStaticFile(e.Name) && conf.EnableReload {
					sendReload(e.String())
					continue
				}
				// Skip ignored files
				if shouldIgnoreFile(e.Name) {
					continue
				}
				if !shouldWatchFileWithExtension(e.Name) {
					continue
				}

				mt := getFileModTime(e.Name)
				if t := eventTime[e.Name]; mt == t {
					logger.Infof(bold("Skipping: ")+"%s", e.String())
					isBuild = false
				}

				eventTime[e.Name] = mt

				if isBuild {
					logger.Infof("Event fired: %s", e)
					go func() {
						// Wait 1s before autobuild until there is no file change.
						scheduleTime = time.Now().Add(1 * time.Second)
						time.Sleep(scheduleTime.Sub(time.Now()))
						AutoBuild(files, isgenerate)
					}()
				}
			case err := <-watcher.Errors:
				logger.Warnf("Watcher error: %s", err.Error()) // No need to exit here
			}
		}
	}()

	logger.Info("Initializing watcher...")
	for _, path := range paths {
		logger.Infof(bold("Watching: ")+"%s", path)
		err = watcher.Add(path)
		if err != nil {
			logger.Fatalf("Failed to watch directory: %s", err)
		}
	}
}

// getFileModTime returns unix timestamp of `os.File.ModTime` for the given path.
func getFileModTime(path string) int64 {
	path = strings.Replace(path, "\\", "/", -1)
	f, err := os.Open(path)
	if err != nil {
		logger.Errorf("Failed to open file on '%s': %s", path, err)
		return time.Now().Unix()
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		logger.Errorf("Failed to get file stats: %s", err)
		return time.Now().Unix()
	}

	return fi.ModTime().Unix()
}

// AutoBuild builds the specified set of files
func AutoBuild(files []string, isgenerate bool) {
	state.Lock()
	defer state.Unlock()

	os.Chdir(currpath)

	cmdName := "go"
	if conf.Gopm.Enable {
		cmdName = "gopm"
	}

	var (
		err    error
		stderr bytes.Buffer
		stdout bytes.Buffer
	)
	// For applications use full import path like "github.com/.../.."
	// are able to use "go install" to reduce build time.
	if conf.GoInstall {
		icmd := exec.Command(cmdName, "install", "-v")
		icmd.Stdout = os.Stdout
		icmd.Stderr = os.Stderr
		icmd.Env = append(os.Environ(), "GOGC=off")
		icmd.Run()
	}
	if conf.Gopm.Install {
		icmd := exec.Command("go", "list", "./...")
		icmd.Stdout = &stdout
		icmd.Env = append(os.Environ(), "GOGC=off")
		err = icmd.Run()
		if err == nil {
			list := strings.Split(stdout.String(), "\n")[1:]
			for _, pkg := range list {
				if len(pkg) == 0 {
					continue
				}
				icmd = exec.Command(cmdName, "install", pkg)
				icmd.Stdout = os.Stdout
				icmd.Stderr = os.Stderr
				icmd.Env = append(os.Environ(), "GOGC=off")
				err = icmd.Run()
				if err != nil {
					break
				}
			}
		}
	}

	if isgenerate {
		logger.Info("Generating the docs...")
		icmd := exec.Command("bee", "generate", "docs")
		icmd.Env = append(os.Environ(), "GOGC=off")
		err = icmd.Run()
		if err != nil {
			logger.Errorf("Failed to generate the docs.")
			return
		}
		logger.Success("Docs generated!")
	}

	if err == nil {
		appName := appname
		if runtime.GOOS == "windows" {
			appName += ".exe"
		}

		args := []string{"build"}
		args = append(args, "-o", appName)
		if buildTags != "" {
			args = append(args, "-tags", buildTags)
		}
		args = append(args, files...)

		bcmd := exec.Command(cmdName, args...)
		bcmd.Env = append(os.Environ(), "GOGC=off")
		bcmd.Stderr = &stderr
		err = bcmd.Run()
		if err != nil {
			logger.Errorf("Failed to build the application: %s", stderr.String())
			return
		}
	}

	logger.Success("Built Successfully!")
	Restart(appname)
}

// Kill kills the running command process
func Kill() {
	defer func() {
		if e := recover(); e != nil {
			logger.Infof("Kill recover: %s", e)
		}
	}()
	if cmd != nil && cmd.Process != nil {
		err := cmd.Process.Kill()
		if err != nil {
			logger.Errorf("Error while killing cmd process: %s", err)
		}
	}
}

// Restart kills the running command process and starts it again
func Restart(appname string) {
	logger.Debugf("Kill running process", __FILE__(), __LINE__())
	Kill()
	go Start(appname)
}

// Start starts the command process
func Start(appname string) {
	logger.Infof("Restarting '%s'...", appname)
	if strings.Index(appname, "./") == -1 {
		appname = "./" + appname
	}

	cmd = exec.Command(appname)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Args = append([]string{appname}, conf.CmdArgs...)
	cmd.Env = append(os.Environ(), conf.Envs...)

	go cmd.Run()
	logger.Successf("'%s' is running...", appname)
	started <- true
}

func ifStaticFile(filename string) bool {
	for _, s := range watchExtsStatic {
		if strings.HasSuffix(filename, s) {
			return true
		}
	}
	return false
}

// shouldIgnoreFile ignores filenames generated by Emacs, Vim or SublimeText.
// It returns true if the file should be ignored, false otherwise.
func shouldIgnoreFile(filename string) bool {
	for _, regex := range ignoredFilesRegExps {
		r, err := regexp.Compile(regex)
		if err != nil {
			logger.Fatalf("Could not compile regular expression: %s", err)
		}
		if r.MatchString(filename) {
			return true
		}
		continue
	}
	return false
}

// shouldWatchFileWithExtension returns true if the name of the file
// hash a suffix that should be watched.
func shouldWatchFileWithExtension(name string) bool {
	for _, s := range watchExts {
		if strings.HasSuffix(name, s) {
			return true
		}
	}
	return false
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"os"
	"os/exec"
	"reflect"
	"syscall"
	"time"

	"github.com/armon/consul-kv"
)

// Configuration for watches.
type WatchConfig struct {
	ConsulAddr string
	ConsulDC   string
	Prefix     string
	Cmd        []string
	Reload     bool
}

// Connects to Consul and watches a given K/V prefix and uses that to
// execute a child process.
func watchAndExec(config *WatchConfig) (int, error) {
	kvConfig := consulkv.DefaultConfig()
	kvConfig.Address = config.ConsulAddr
	kvConfig.Datacenter = config.ConsulDC

	client, err := consulkv.NewClient(kvConfig)
	if err != nil {
		return 0, err
	}

	// Start the watcher goroutine that watches for changes in the
	// K/V and notifies us on a channel.
	pairCh := make(chan consulkv.KVPairs)
	errCh := make(chan error, 1)
	quitCh := make(chan struct{})
	defer close(quitCh)
	go watch(client, config.Prefix, pairCh, errCh, quitCh)

	// This channel is what is sent to when a process exits that we
	// are running. We start it out as `nil` since we have no process.
	var exitCh chan int

	var env map[string]string
	var cmd *exec.Cmd
	for {
		var pairs consulkv.KVPairs

		// Wait for new pairs to come on our channel or an error
		// to occur.
		select {
		case exit := <-exitCh:
			return exit, nil
		case pairs = <-pairCh:
		case err := <-errCh:
			return 0, err
		}

		newEnv := make(map[string]string)
		for _, pair := range pairs {
			newEnv[pair.Key] = string(pair.Value)
		}

		// If the environmental variables didn't actually change,
		// then don't do anything.
		if reflect.DeepEqual(env, newEnv) {
			continue
		}

		// Configuration changed, reload the process.
		if cmd != nil {
			if !config.Reload {
				// We don't want to reload the process... just ignore.
				continue
			}

			// Kill the process
			exited := false
			if err := cmd.Process.Signal(syscall.SIGTERM); err == nil {
				// Wait a few seconds for it to exit
				killCh := make(chan struct{})
				go func() {
					defer close(killCh)
					cmd.Process.Wait()
				}()

				select {
				case <-killCh:
					exited = true
				case <-time.After(3 * time.Second):
				}
			}

			// If we still haven't exited from a SIGKILL
			if !exited {
				cmd.Process.Kill()
			}

			cmd = nil
		}

		cmdEnv := make([]string, 0, len(newEnv))
		for k, v := range newEnv {
			cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v))
		}
		cmd = exec.Command(config.Cmd[0], config.Cmd[1:]...)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		cmd.Env = cmdEnv
		err := cmd.Start()
		if err != nil {
			return 111, err
		}

		// Create a new exitCh so that previously invoked commands
		// (if any) don't cause us to exit, and start a goroutine
		// to wait for that process to end.
		exitCh = make(chan int, 1)
		go func(cmd *exec.Cmd) {
			err := cmd.Wait()
			if err == nil {
				exitCh <- 0
				return
			}

			if exiterr, ok := err.(*exec.ExitError); ok {
				// The program has exited with an exit code != 0
				if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
					exitCh <- status.ExitStatus()
					return
				}
			}

			exitCh <- 111
		}(cmd)
	}

	return 0, nil
}

func watch(
	client *consulkv.Client,
	prefix string,
	pairCh chan<- consulkv.KVPairs,
	errCh chan<- error,
	quitCh <-chan struct{}) {
	// Get the initial list of k/v pairs. We don't do a retryableList
	// here because we want a fast fail if the initial request fails.
	meta, pairs, err := client.List(prefix)
	if err != nil {
		errCh <- err
		return
	}

	// Send the initial list out right away
	pairCh <- pairs

	// Loop forever (or until quitCh is closed) and watch the keys
	// for changes.
	curIndex := meta.ModifyIndex
	for {
		select {
		case <-quitCh:
			return
		default:
		}

		meta, pairs, err = retryableList(
			func() (*consulkv.KVMeta, consulkv.KVPairs, error) {
				return client.WatchList(prefix, curIndex)
			})
		if err != nil {
			errCh <- err
			return
		}

		// If nothing actually changed (request just timed out), return
		if meta.ModifyIndex == curIndex {
			continue
		}

		pairCh <- pairs
		curIndex = meta.ModifyIndex
	}
}

// This function is able to call KV listing functions and retry them.
// We want to retry if there are errors because it is safe (GET request),
// and erroring early is MUCH more costly than retrying over time and
// delaying the configuration propagation.
func retryableList(f func() (*consulkv.KVMeta, consulkv.KVPairs, error)) (*consulkv.KVMeta, consulkv.KVPairs, error) {
	i := 0
	for {
		m, p, e := f()
		if e != nil {
			if i >= 3 {
				return nil, nil, e
			}

			i++

			// Reasonably arbitrary sleep to just try again... It is
			// a GET request so this is safe.
			time.Sleep(time.Duration(i*2) * time.Second)
		}

		return m, p, e
	}
}
<commit_msg>Trim the prefix from the key<commit_after>package main

import (
	"fmt"
	"os"
	"os/exec"
	"reflect"
	"syscall"
	"time"

	"github.com/armon/consul-kv"
)

// Configuration for watches.
type WatchConfig struct {
	ConsulAddr string
	ConsulDC   string
	Prefix     string
	Cmd        []string
	Reload     bool
}

// Connects to Consul and watches a given K/V prefix and uses that to
// execute a child process.
func watchAndExec(config *WatchConfig) (int, error) {
	kvConfig := consulkv.DefaultConfig()
	kvConfig.Address = config.ConsulAddr
	kvConfig.Datacenter = config.ConsulDC

	client, err := consulkv.NewClient(kvConfig)
	if err != nil {
		return 0, err
	}

	// Start the watcher goroutine that watches for changes in the
	// K/V and notifies us on a channel.
	pairCh := make(chan consulkv.KVPairs)
	errCh := make(chan error, 1)
	quitCh := make(chan struct{})
	defer close(quitCh)
	go watch(client, config.Prefix, pairCh, errCh, quitCh)

	// This channel is what is sent to when a process exits that we
	// are running. We start it out as `nil` since we have no process.
	var exitCh chan int

	var env map[string]string
	var cmd *exec.Cmd
	for {
		var pairs consulkv.KVPairs

		// Wait for new pairs to come on our channel or an error
		// to occur.
		select {
		case exit := <-exitCh:
			return exit, nil
		case pairs = <-pairCh:
		case err := <-errCh:
			return 0, err
		}

		newEnv := make(map[string]string)
		for _, pair := range pairs {
			k := pair.Key[len(config.Prefix)+1:]
			newEnv[k] = string(pair.Value)
		}

		// If the environmental variables didn't actually change,
		// then don't do anything.
		if reflect.DeepEqual(env, newEnv) {
			continue
		}

		// Configuration changed, reload the process.
		if cmd != nil {
			if !config.Reload {
				// We don't want to reload the process... just ignore.
				continue
			}

			// Kill the process
			exited := false
			if err := cmd.Process.Signal(syscall.SIGTERM); err == nil {
				// Wait a few seconds for it to exit
				killCh := make(chan struct{})
				go func() {
					defer close(killCh)
					cmd.Process.Wait()
				}()

				select {
				case <-killCh:
					exited = true
				case <-time.After(3 * time.Second):
				}
			}

			// If we still haven't exited from a SIGKILL
			if !exited {
				cmd.Process.Kill()
			}

			cmd = nil
		}

		cmdEnv := make([]string, 0, len(newEnv))
		for k, v := range newEnv {
			cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v))
		}
		cmd = exec.Command(config.Cmd[0], config.Cmd[1:]...)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		cmd.Env = cmdEnv
		err := cmd.Start()
		if err != nil {
			return 111, err
		}

		// Create a new exitCh so that previously invoked commands
		// (if any) don't cause us to exit, and start a goroutine
		// to wait for that process to end.
		exitCh = make(chan int, 1)
		go func(cmd *exec.Cmd) {
			err := cmd.Wait()
			if err == nil {
				exitCh <- 0
				return
			}

			if exiterr, ok := err.(*exec.ExitError); ok {
				// The program has exited with an exit code != 0
				if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
					exitCh <- status.ExitStatus()
					return
				}
			}

			exitCh <- 111
		}(cmd)
	}

	return 0, nil
}

func watch(
	client *consulkv.Client,
	prefix string,
	pairCh chan<- consulkv.KVPairs,
	errCh chan<- error,
	quitCh <-chan struct{}) {
	// Get the initial list of k/v pairs. We don't do a retryableList
	// here because we want a fast fail if the initial request fails.
	meta, pairs, err := client.List(prefix)
	if err != nil {
		errCh <- err
		return
	}

	// Send the initial list out right away
	pairCh <- pairs

	// Loop forever (or until quitCh is closed) and watch the keys
	// for changes.
	curIndex := meta.ModifyIndex
	for {
		select {
		case <-quitCh:
			return
		default:
		}

		meta, pairs, err = retryableList(
			func() (*consulkv.KVMeta, consulkv.KVPairs, error) {
				return client.WatchList(prefix, curIndex)
			})
		if err != nil {
			errCh <- err
			return
		}

		// If nothing actually changed (request just timed out), return
		if meta.ModifyIndex == curIndex {
			continue
		}

		pairCh <- pairs
		curIndex = meta.ModifyIndex
	}
}

// This function is able to call KV listing functions and retry them.
// We want to retry if there are errors because it is safe (GET request),
// and erroring early is MUCH more costly than retrying over time and
// delaying the configuration propagation.
func retryableList(f func() (*consulkv.KVMeta, consulkv.KVPairs, error)) (*consulkv.KVMeta, consulkv.KVPairs, error) {
	i := 0
	for {
		m, p, e := f()
		if e != nil {
			if i >= 3 {
				return nil, nil, e
			}

			i++

			// Reasonably arbitrary sleep to just try again... It is
			// a GET request so this is safe.
			time.Sleep(time.Duration(i*2) * time.Second)
		}

		return m, p, e
	}
}
<|endoftext|>


<commit_before>package client

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"

	tsuruErrors "github.com/tsuru/tsuru/errors"

	"github.com/tsuru/tsuru/cmd"
)

type Init struct{}

func (i *Init) Info() *cmd.Info {
	return &cmd.Info{
		Name:  "init",
		Usage: "init",
		Desc: `
Creates a standard example of .tsuruignore , tsuru.yaml and Procfile
	on the current project directory.

"Procfile" describes the components required to run an application.
	It is the way to tell tsuru how to run your applications;

".tsuruignore" describes to tsuru what it should not add into your
	deploy process, via "tsuru app-deploy" command. You can use
	".tsuruignore" to avoid sending files that were committed,
	but aren't necessary for running the app, like tests and
	documentation;

"tsuru.yaml" describes certain aspects of your app, like information
	about deployment hooks and deployment time health checks.`,
	}
}

func (i *Init) Run(context *cmd.Context, client *cmd.Client) (err error) {
	err = createInitFiles()
	if err != nil {
		return
	}
	const msg = `
Initialized Tsuru sample files: "Procfile", ".tsuruignore" and "tsuru.yaml",
for more info please refer to "tsuru init -h" or the docs at docs.tsuru.io`
	_, err = context.Stdout.Write([]byte(msg))
	if err != nil {
		return
	}
	err = writeTsuruYaml()
	if err != nil {
		return
	}
	err = writeProcfile()
	if err != nil {
		return
	}
	return copyGitIgnore()
}

func createInitFiles() (err error) {
	wd, err := os.Getwd()
	if err != nil {
		return
	}
	fi, err := os.Open(wd)
	if err != nil {
		return
	}
	defer func() {
		if errClose := fi.Close(); errClose != nil {
			err = tsuruErrors.NewMultiError(err, errClose)
		}
	}()
	dirFiles, err := fi.Readdir(0)
	if err != nil {
		return
	}
	initFiles := map[string]string{
		".tsuruignore": "",
		"Procfile":     "",
		"tsuru.yaml":   "",
	}
	for _, f := range dirFiles {
		if _, ok := initFiles[f.Name()]; ok {
			delete(initFiles, f.Name())
		}
	}
	for f := range initFiles {
		_, err = os.Create(f)
		if err != nil {
			return
		}
	}
	return
}

func copyGitIgnore() (err error) {
	in, err := os.Open(".gitignore")
	if err != nil {
		dotGit := []byte(".git\n.gitignore\n")
		return ioutil.WriteFile(".tsuruignore", dotGit, 0644)
	}
	defer func() {
		if errClose := in.Close(); errClose != nil {
			err = tsuruErrors.NewMultiError(err, errClose)
		}
	}()
	out, err := os.OpenFile(".tsuruignore", os.O_APPEND|os.O_WRONLY, os.ModeAppend)
	if err != nil {
		return
	}
	defer func() {
		if errClose := out.Close(); errClose != nil {
			err = tsuruErrors.NewMultiError(err, errClose)
		}
	}()
	_, err = io.Copy(out, in)
	if err != nil {
		return
	}
	_, err = out.WriteString("\n.git\n.gitignore\n")
	return
}

func writeTsuruYaml() error {
	return ioutil.WriteFile("tsuru.yaml", nil, 0644)
}

func writeProcfile() (err error) {
	wd, err := os.Getwd()
	if err != nil {
		return
	}
	projectPathSplitted := strings.Split(wd, "/")
	projectName := projectPathSplitted[len(projectPathSplitted)-1]
	procfile := fmt.Sprintf("web: %s", projectName)
	return ioutil.WriteFile("Procfile", []byte(procfile), 0644)
}
<commit_msg>client/init: use filepath.Base instead of split<commit_after>package client

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"

	tsuruErrors "github.com/tsuru/tsuru/errors"

	"github.com/tsuru/tsuru/cmd"
)

type Init struct{}

func (i *Init) Info() *cmd.Info {
	return &cmd.Info{
		Name:  "init",
		Usage: "init",
		Desc: `
Creates a standard example of .tsuruignore , tsuru.yaml and Procfile
	on the current project directory.

"Procfile" describes the components required to run an application.
	It is the way to tell tsuru how to run your applications;

".tsuruignore" describes to tsuru what it should not add into your
	deploy process, via "tsuru app-deploy" command. You can use
	".tsuruignore" to avoid sending files that were committed,
	but aren't necessary for running the app, like tests and
	documentation;

"tsuru.yaml" describes certain aspects of your app, like information
	about deployment hooks and deployment time health checks.`,
	}
}

func (i *Init) Run(context *cmd.Context, client *cmd.Client) (err error) {
	err = createInitFiles()
	if err != nil {
		return
	}
	const msg = `
Initialized Tsuru sample files: "Procfile", ".tsuruignore" and "tsuru.yaml",
for more info please refer to "tsuru init -h" or the docs at docs.tsuru.io`
	_, err = context.Stdout.Write([]byte(msg))
	if err != nil {
		return
	}
	err = writeTsuruYaml()
	if err != nil {
		return
	}
	err = writeProcfile()
	if err != nil {
		return
	}
	return copyGitIgnore()
}

func createInitFiles() (err error) {
	wd, err := os.Getwd()
	if err != nil {
		return
	}
	fi, err := os.Open(wd)
	if err != nil {
		return
	}
	defer func() {
		if errClose := fi.Close(); errClose != nil {
			err = tsuruErrors.NewMultiError(err, errClose)
		}
	}()
	dirFiles, err := fi.Readdir(0)
	if err != nil {
		return
	}
	initFiles := map[string]string{
		".tsuruignore": "",
		"Procfile":     "",
		"tsuru.yaml":   "",
	}
	for _, f := range dirFiles {
		if _, ok := initFiles[f.Name()]; ok {
			delete(initFiles, f.Name())
		}
	}
	for f := range initFiles {
		_, err = os.Create(f)
		if err != nil {
			return
		}
	}
	return
}

func copyGitIgnore() (err error) {
	in, err := os.Open(".gitignore")
	if err != nil {
		dotGit := []byte(".git\n.gitignore\n")
		return ioutil.WriteFile(".tsuruignore", dotGit, 0644)
	}
	defer func() {
		if errClose := in.Close(); errClose != nil {
			err = tsuruErrors.NewMultiError(err, errClose)
		}
	}()
	out, err := os.OpenFile(".tsuruignore", os.O_APPEND|os.O_WRONLY, os.ModeAppend)
	if err != nil {
		return
	}
	defer func() {
		if errClose := out.Close(); errClose != nil {
			err = tsuruErrors.NewMultiError(err, errClose)
		}
	}()
	_, err = io.Copy(out, in)
	if err != nil {
		return
	}
	_, err = out.WriteString("\n.git\n.gitignore\n")
	return
}

func writeTsuruYaml() error {
	return ioutil.WriteFile("tsuru.yaml", nil, 0644)
}

func writeProcfile() (err error) {
	wd, err := os.Getwd()
	if err != nil {
		return
	}
	projectName := filepath.Base(wd)
	procfile := fmt.Sprintf("web: %s", projectName)
	return ioutil.WriteFile("Procfile", []byte(procfile), 0644)
}
<|endoftext|>


<commit_before>package v7action_test

import (
	. "code.cloudfoundry.org/cli/actor/v7action"
	"code.cloudfoundry.org/cli/actor/v7action/v7actionfakes"
	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
	"errors"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Revisions Actions", func() {

	Describe("GetRevisionsByApplicationNameAndSpace", func() {
		var (
			actor                     *Actor
			fakeCloudControllerClient *v7actionfakes.FakeCloudControllerClient
			appName                   string
			spaceGUID                 string
			fetchedRevisions          Revisions
			executeErr                error
			warnings                  Warnings
		)

		BeforeEach(func() {
			fakeCloudControllerClient = new(v7actionfakes.FakeCloudControllerClient)
			actor = NewActor(fakeCloudControllerClient, nil, nil, nil, nil)
			appName = "some-app"
			spaceGUID = "space-guid"
		})

		JustBeforeEach(func() {
			fetchedRevisions, warnings, executeErr = actor.GetRevisionsByApplicationNameAndSpace(appName, spaceGUID)
		})

		When("finding the app fails", func() {
			BeforeEach(func() {
				fakeCloudControllerClient.GetApplicationsReturns(nil, ccv3.Warnings{"get-application-warning"}, errors.New("get-application-error"))
			})

			It("returns an error", func() {
				Expect(executeErr).To(MatchError("get-application-error"))
				Expect(warnings).To(ConsistOf("get-application-warning"))
			})
		})

		When("finding the app succeeds", func() {
			BeforeEach(func() {
				fakeCloudControllerClient.GetApplicationsReturns([]ccv3.Application{{Name: "some-app", GUID: "some-app-guid"}}, ccv3.Warnings{"get-application-warning"}, nil)
			})

			When("getting the app revisions fails", func() {
				BeforeEach(func() {
					fakeCloudControllerClient.GetApplicationRevisionsReturns([]ccv3.Revision{}, ccv3.Warnings{"some-revisions-warnings"}, errors.New("some-revisions-error"))
				})
				It("returns an error", func() {
					Expect(executeErr).To(MatchError("some-revisions-error"))
					Expect(warnings).To(ConsistOf("get-application-warning", "some-revisions-warnings"))
				})
			})

			When("getting the app revisions succeeds", func() {
				BeforeEach(func() {
					fakeCloudControllerClient.GetApplicationRevisionsReturns(
						Revisions{
							{GUID: "1"},
							{GUID: "2"},
							{GUID: "3"},
						},
						ccv3.Warnings{"some-evil-revisions-warnings"},
						nil,
					)
				})

				It("makes the API call to get the app revisions and returns all warnings", func() {
					Expect(fakeCloudControllerClient.GetApplicationsCallCount()).To(Equal(1))
					Expect(fakeCloudControllerClient.GetApplicationsArgsForCall(0)).To(ConsistOf(
						ccv3.Query{Key: ccv3.NameFilter, Values: []string{appName}},
						ccv3.Query{Key: ccv3.SpaceGUIDFilter, Values: []string{spaceGUID}},
					))

					Expect(fakeCloudControllerClient.GetApplicationRevisionsCallCount()).To(Equal(1))
					Expect(fakeCloudControllerClient.GetApplicationRevisionsArgsForCall(0)).To(Equal("some-app-guid"))

					Expect(fetchedRevisions).To(Equal(
						Revisions{
							{GUID: "1"},
							{GUID: "2"},
							{GUID: "3"},
						}))
					Expect(executeErr).ToNot(HaveOccurred())
					Expect(warnings).To(ConsistOf("get-application-warning", "some-evil-revisions-warnings"))
				})
			})
		})
	})

})
<commit_msg>Last try to get Travis to trigger<commit_after>package v7action_test

import (
	. "code.cloudfoundry.org/cli/actor/v7action"
	"code.cloudfoundry.org/cli/actor/v7action/v7actionfakes"
	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
	"errors"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Revisions Actions", func() {

	Describe("GetRevisionsByApplicationNameAndSpace", func() {
		var (
			actor                     *Actor
			fakeCloudControllerClient *v7actionfakes.FakeCloudControllerClient
			appName                   string
			spaceGUID                 string
			fetchedRevisions          Revisions
			executeErr                error
			warnings                  Warnings
		)

		BeforeEach(func() {
			fakeCloudControllerClient = new(v7actionfakes.FakeCloudControllerClient)
			actor = NewActor(fakeCloudControllerClient, nil, nil, nil, nil)
			appName = "some-app"
			spaceGUID = "space-guid"
		})

		JustBeforeEach(func() {
			fetchedRevisions, warnings, executeErr = actor.GetRevisionsByApplicationNameAndSpace(appName, spaceGUID)
		})

		When("finding the app fails", func() {
			BeforeEach(func() {
				fakeCloudControllerClient.GetApplicationsReturns(nil, ccv3.Warnings{"get-application-warning"}, errors.New("get-application-error"))
			})

			It("returns an error", func() {
				Expect(executeErr).To(MatchError("get-application-error"))
				Expect(warnings).To(ConsistOf("get-application-warning"))
			})
		})

		When("finding the app succeeds", func() {
			BeforeEach(func() {
				fakeCloudControllerClient.GetApplicationsReturns([]ccv3.Application{{Name: "some-app", GUID: "some-app-guid"}}, ccv3.Warnings{"get-application-warning"}, nil)
			})

			When("getting the app revisions fails", func() {
				BeforeEach(func() {
					fakeCloudControllerClient.GetApplicationRevisionsReturns([]ccv3.Revision{}, ccv3.Warnings{"some-revisions-warnings"}, errors.New("some-revisions-error"))
				})
				It("returns an error", func() {
					Expect(executeErr).To(MatchError("some-revisions-error"))
					Expect(warnings).To(ConsistOf("get-application-warning", "some-revisions-warnings"))
				})
			})

			When("getting the app revisions succeeds", func() {
				BeforeEach(func() {
					fakeCloudControllerClient.GetApplicationRevisionsReturns(
						Revisions{
							{GUID: "1"},
							{GUID: "2"},
							{GUID: "3"},
						},
						ccv3.Warnings{"some-revisions-warnings"},
						nil,
					)
				})

				It("makes the API call to get the app revisions and returns all warnings", func() {
					Expect(fakeCloudControllerClient.GetApplicationsCallCount()).To(Equal(1))
					Expect(fakeCloudControllerClient.GetApplicationsArgsForCall(0)).To(ConsistOf(
						ccv3.Query{Key: ccv3.NameFilter, Values: []string{appName}},
						ccv3.Query{Key: ccv3.SpaceGUIDFilter, Values: []string{spaceGUID}},
					))

					Expect(fakeCloudControllerClient.GetApplicationRevisionsCallCount()).To(Equal(1))
					Expect(fakeCloudControllerClient.GetApplicationRevisionsArgsForCall(0)).To(Equal("some-app-guid"))

					Expect(fetchedRevisions).To(Equal(
						Revisions{
							{GUID: "1"},
							{GUID: "2"},
							{GUID: "3"},
						}))
					Expect(executeErr).ToNot(HaveOccurred())
					Expect(warnings).To(ConsistOf("get-application-warning", "some-revisions-warnings"))
				})
			})
		})
	})

})
<|endoftext|>


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

package backend

import (
	"context"
	"time"

	"go.chromium.org/luci/common/clock"
	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/logging"
	"go.chromium.org/luci/common/tsmon/distribution"
	"go.chromium.org/luci/common/tsmon/field"
	"go.chromium.org/luci/common/tsmon/metric"
	"go.chromium.org/luci/common/tsmon/types"

	"go.chromium.org/luci/resultdb/internal/tasks"
)

// Statuses of invocation tasks.
const (
	// The task completes successfully.
	success = "SUCCESS"

	// The task runs into a failure that can be resolved by retrying.
	transientFailure = "TRANSIENT_FAILURE"

	// The task runs into a permanent failure.
	permanentFailure = "PERMANENT_FAILURE"

	// Minimum interval between cron jobs in the same group.
	minInterval = 8 * time.Second
)

var (
	taskAttemptMetric = metric.NewCounter(
		"resultdb/task/attempts",
		"Counts of invocation task attempts.",
		nil,
		field.String("type"),   // tasks.Type
		field.String("status")) // SUCCESS || TRANSIENT_FAILURE || PERMANENT_FAILURE

	taskDurationMetric = metric.NewCumulativeDistribution(
		"resultdb/task/duration",
		"Distribution of an attempt’s execution duration.",
		&types.MetricMetadata{Units: types.Milliseconds},
		distribution.DefaultBucketer,
		field.String("type"),   // tasks.Type
		field.String("status")) // SUCCESS || TRANSIENT_FAILURE || PERMANENT_FAILURE
)

// permanentInvocationTaskErrTag set in an error indicates that the err is not
// resolvable by retry.
var permanentInvocationTaskErrTag = errors.BoolTag{
	Key: errors.NewTagKey("permanent failure to process invocation task"),
}

// runTask leases, runs and deletes a task.
// May return tasks.ErrConflict.
// Updates taskAttemptMetric and taskDurationMetric.
func (b *backend) runTask(ctx context.Context, taskType tasks.Type, id string) (err error) {
	leaseDuration := 10 * time.Minute
	switch taskType {
	case tasks.TryFinalizeInvocation:
		leaseDuration = time.Minute
	}
	if b.ForceLeaseDuration > 0 {
		leaseDuration = b.ForceLeaseDuration
	}

	// Update metrics.
	startTime := clock.Now(ctx)
	taskStatus := transientFailure
	defer func() {
		// Send metrics to tsmon.
		typeStr := string(taskType)

		duration := float64(time.Since(startTime).Milliseconds())
		taskDurationMetric.Add(ctx, duration, typeStr, taskStatus)

		taskAttemptMetric.Add(ctx, 1, typeStr, taskStatus)
	}()

	invID, payload, err := tasks.Lease(ctx, taskType, id, leaseDuration)
	if err != nil {
		return err
	}

	switch taskType {
	case tasks.BQExport:
		err = b.exportResultsToBigQuery(ctx, invID, payload)
	case tasks.TryFinalizeInvocation:
		err = tryFinalizeInvocation(ctx, invID)
	default:
		err = errors.Reason("unexpected task type %q", taskType).Err()
	}
	if err != nil {
		if permanentInvocationTaskErrTag.In(err) {
			taskStatus = permanentFailure
			logging.Errorf(ctx, "permanent failure to process the task: %s", err)
			return tasks.Delete(ctx, taskType, id)
		}
		return err
	}

	// Invocation task is done, delete the row from spanner.
	if err := tasks.Delete(ctx, taskType, id); err != nil {
		return err
	}

	taskStatus = success
	return nil
}

// runInvocationTasks gets invocation tasks and dispatches them to workers.
func (b *backend) runInvocationTasks(ctx context.Context, taskType tasks.Type) {
	workers := b.TaskWorkers
	if workers == 0 {
		workers = 1
	}
	b.cronGroup(ctx, workers, minInterval, func(ctx context.Context, replica int) error {
		ids, err := tasks.Sample(ctx, taskType, time.Now(), 100)
		if err != nil {
			return errors.Annotate(err, "failed to query invocation tasks").Err()
		}

		for _, id := range ids {
			ctx := logging.SetField(ctx, "task_id", id)
			switch err := b.runTask(ctx, taskType, id); {

			case err == tasks.ErrConflict:
				// It's possible another worker has leased the task, and it's fine, skip.
				logging.Warningf(ctx, "Conflict while trying to lease the task")

			case err != nil:
				// Do not bail on other task ids, otherwise we sample tasks too often
				// which is expensive. Just log the error and move on to the next task.
				how := "transiently"
				if permanentInvocationTaskErrTag.In(err) {
					how = "permanently"
				}
				logging.Errorf(ctx, "Task failed %s: %s", how, err)

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

package backend

import (
	"context"
	"time"

	"go.chromium.org/luci/common/clock"
	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/logging"
	"go.chromium.org/luci/common/tsmon/distribution"
	"go.chromium.org/luci/common/tsmon/field"
	"go.chromium.org/luci/common/tsmon/metric"
	"go.chromium.org/luci/common/tsmon/types"

	"go.chromium.org/luci/resultdb/internal/tasks"
)

// Statuses of invocation tasks.
const (
	// The task completes successfully.
	success = "SUCCESS"

	// The task is skipped, possibly because another worker has leased it.
	skipped = "SKIPPED"

	// The task runs into a failure that can be resolved by retrying.
	transientFailure = "TRANSIENT_FAILURE"

	// The task runs into a permanent failure.
	permanentFailure = "PERMANENT_FAILURE"

	// Minimum interval between cron jobs in the same group.
	minInterval = 8 * time.Second
)

var (
	taskAttemptMetric = metric.NewCounter(
		"resultdb/task/attempts",
		"Counts of invocation task attempts.",
		nil,
		field.String("type"),   // tasks.Type
		field.String("status")) // SUCCESS || TRANSIENT_FAILURE || PERMANENT_FAILURE

	taskDurationMetric = metric.NewCumulativeDistribution(
		"resultdb/task/duration",
		"Distribution of an attempt’s execution duration.",
		&types.MetricMetadata{Units: types.Milliseconds},
		distribution.DefaultBucketer,
		field.String("type"),   // tasks.Type
		field.String("status")) // SUCCESS || TRANSIENT_FAILURE || PERMANENT_FAILURE
)

// permanentInvocationTaskErrTag set in an error indicates that the err is not
// resolvable by retry.
var permanentInvocationTaskErrTag = errors.BoolTag{
	Key: errors.NewTagKey("permanent failure to process invocation task"),
}

// runTask leases, runs and deletes a task.
// May return tasks.ErrConflict.
// Updates taskAttemptMetric and taskDurationMetric.
func (b *backend) runTask(ctx context.Context, taskType tasks.Type, id string) (err error) {
	leaseDuration := 10 * time.Minute
	switch taskType {
	case tasks.TryFinalizeInvocation:
		leaseDuration = time.Minute
	}
	if b.ForceLeaseDuration > 0 {
		leaseDuration = b.ForceLeaseDuration
	}

	// Update metrics.
	startTime := clock.Now(ctx)
	taskStatus := transientFailure
	defer func() {
		// Send metrics to tsmon.
		typeStr := string(taskType)

		duration := float64(time.Since(startTime).Milliseconds())
		taskDurationMetric.Add(ctx, duration, typeStr, taskStatus)

		taskAttemptMetric.Add(ctx, 1, typeStr, taskStatus)
	}()

	invID, payload, err := tasks.Lease(ctx, taskType, id, leaseDuration)
	if err != nil {
		if err == tasks.ErrConflict {
			taskStatus = skipped
		}
		return err
	}

	switch taskType {
	case tasks.BQExport:
		err = b.exportResultsToBigQuery(ctx, invID, payload)
	case tasks.TryFinalizeInvocation:
		err = tryFinalizeInvocation(ctx, invID)
	default:
		err = errors.Reason("unexpected task type %q", taskType).Err()
	}
	if err != nil {
		if permanentInvocationTaskErrTag.In(err) {
			taskStatus = permanentFailure
			logging.Errorf(ctx, "permanent failure to process the task: %s", err)
			return tasks.Delete(ctx, taskType, id)
		}
		return err
	}

	// Invocation task is done, delete the row from spanner.
	if err := tasks.Delete(ctx, taskType, id); err != nil {
		return err
	}

	taskStatus = success
	return nil
}

// runInvocationTasks gets invocation tasks and dispatches them to workers.
func (b *backend) runInvocationTasks(ctx context.Context, taskType tasks.Type) {
	workers := b.TaskWorkers
	if workers == 0 {
		workers = 1
	}
	b.cronGroup(ctx, workers, minInterval, func(ctx context.Context, replica int) error {
		ids, err := tasks.Sample(ctx, taskType, time.Now(), 100)
		if err != nil {
			return errors.Annotate(err, "failed to query invocation tasks").Err()
		}

		for _, id := range ids {
			ctx := logging.SetField(ctx, "task_id", id)
			switch err := b.runTask(ctx, taskType, id); {

			case err == tasks.ErrConflict:
				// It's possible another worker has leased the task, and it's fine, skip.
				logging.Warningf(ctx, "Conflict while trying to lease the task")

			case err != nil:
				// Do not bail on other task ids, otherwise we sample tasks too often
				// which is expensive. Just log the error and move on to the next task.
				how := "transiently"
				if permanentInvocationTaskErrTag.In(err) {
					how = "permanently"
				}
				logging.Errorf(ctx, "Task failed %s: %s", how, err)

			default:
				logging.Infof(ctx, "Task succeeded")
			}
		}
		return nil
	})
}
<|endoftext|>


<commit_before>/*
Copyright (c) 2014 VMware, Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package soap

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/cookiejar"
	"net/http/httputil"
	"net/url"
	"os"
	"strings"
	"sync/atomic"
	"time"

	"github.com/vmware/govmomi/vim25"
	"github.com/vmware/govmomi/vim25/debug"
	"github.com/vmware/govmomi/vim25/types"
	"github.com/vmware/govmomi/vim25/xml"
)

type HasFault interface {
	Fault() *Fault
}

type RoundTripper interface {
	RoundTrip(req, res HasFault) error
}

var cn uint64 // Client counter

type Client struct {
	http.Client

	u url.URL

	cn  uint64         // Client counter
	rn  uint64         // Request counter
	log io.WriteCloser // Request log
}

func NewClient(u url.URL) *Client {

	c := Client{
		u: u,

		cn: atomic.AddUint64(&cn, 1),
		rn: 0,
	}

	if c.u.Scheme == "https" {
		c.Client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
	}

	c.Jar, _ = cookiejar.New(nil)
	c.u.User = nil

	if debug.Enabled() {
		c.log = debug.NewFile(fmt.Sprintf("%d-client.log", c.cn))
	}

	return &c
}

func (c *Client) URL() url.URL {
	return c.u
}

type marshaledClient struct {
	Cookies []*http.Cookie
	URL     *url.URL
}

func (c *Client) MarshalJSON() ([]byte, error) {
	m := marshaledClient{
		Cookies: c.Jar.Cookies(&c.u),
		URL:     &c.u,
	}

	return json.Marshal(m)
}

func (c *Client) UnmarshalJSON(b []byte) error {
	var m marshaledClient

	err := json.Unmarshal(b, &m)
	if err != nil {
		return err
	}

	*c = *NewClient(*m.URL)
	c.Jar.SetCookies(m.URL, m.Cookies)

	return nil
}

func (c *Client) RoundTrip(reqBody, resBody HasFault) error {
	var httpreq *http.Request
	var httpres *http.Response
	var err error

	reqEnv := Envelope{Body: reqBody}
	resEnv := Envelope{Body: resBody}

	num := atomic.AddUint64(&c.rn, 1)

	b, err := xml.Marshal(reqEnv)
	if err != nil {
		panic(err)
	}

	rawreqbody := io.MultiReader(strings.NewReader(xml.Header), bytes.NewReader(b))
	if debug.Enabled() {
		f := debug.NewFile(fmt.Sprintf("%d-%04d.req.xml", c.cn, num))
		rawreqbody = io.TeeReader(rawreqbody, f)
	}

	httpreq, err = http.NewRequest("POST", c.u.String(), rawreqbody)
	if err != nil {
		panic(err)
	}

	httpreq.Header.Set(`Content-Type`, `text/xml; charset="utf-8"`)
	httpreq.Header.Set(`SOAPAction`, `urn:vim25/5.5`)

	if debug.Enabled() {
		b, _ := httputil.DumpRequest(httpreq, false)
		wc := debug.NewFile(fmt.Sprintf("%d-%04d.req.headers", c.cn, num))
		wc.Write(b)
		wc.Close()
	}

	tstart := time.Now()
	httpres, err = c.Client.Do(httpreq)
	tstop := time.Now()

	if debug.Enabled() {
		now := time.Now().Format("2006-01-02T15-04-05.999999999")
		ms := tstop.Sub(tstart) / time.Millisecond
		fmt.Fprintf(c.log, "%s: %4d took %6dms\n", now, num, ms)
	}

	if err != nil {
		return err
	}

	var rawresbody io.Reader = httpres.Body
	defer httpres.Body.Close()

	if debug.Enabled() {
		f := debug.NewFile(fmt.Sprintf("%d-%04d.res.xml", c.cn, num))
		rawresbody = io.TeeReader(rawresbody, f)
	}

	if debug.Enabled() {
		b, _ := httputil.DumpResponse(httpres, false)
		wc := debug.NewFile(fmt.Sprintf("%d-%04d.res.headers", c.cn, num))
		wc.Write(b)
		wc.Close()
	}

	dec := xml.NewDecoder(rawresbody)
	dec.TypeFunc = types.TypeFunc()
	err = dec.Decode(&resEnv)
	if err != nil {
		return err
	}

	if f := resBody.Fault(); f != nil {
		return WrapSoapFault(f)
	}

	return err
}

// ParseURL wraps url.Parse to rewrite the URL.Host field
// In the case of VM guest uploads or NFC lease URLs, a Host
// field with a value of "*" is rewritten to the Client's Host.
func (c *Client) ParseURL(urlStr string) (*url.URL, error) {
	u, err := url.Parse(urlStr)
	if err != nil {
		return nil, err
	}

	host := strings.Split(u.Host, ":")
	if host[0] == "*" {
		host[0] = strings.Split(c.URL().Host, ":")[0]
		u.Host = strings.Join(host, ":")
	}

	return u, nil
}

type Upload struct {
	Type       string
	Method     string
	ProgressCh chan<- vim25.Progress
}

var DefaultUpload = Upload{
	Type:   "application/octet-stream",
	Method: "PUT",
}

// UploadFile PUTs the local file to the given URL
func (c *Client) UploadFile(file string, u *url.URL, param *Upload) error {
	var err error

	if param == nil {
		param = &DefaultUpload
	}

	pr := progressReader{
		ch: param.ProgressCh,
	}

	// Mark progress reader as done when returning from this function.
	defer func() {
		pr.Done(err)
	}()

	s, err := os.Stat(file)
	if err != nil {
		return err
	}

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

	pr.r = f
	pr.size = s.Size()
	req, err := http.NewRequest(param.Method, u.String(), &pr)
	if err != nil {
		return err
	}

	req.ContentLength = s.Size()
	req.Header.Set("Content-Type", param.Type)

	res, err := c.Client.Do(req)
	if err != nil {
		return err
	}

	switch res.StatusCode {
	case http.StatusOK:
	case http.StatusCreated:
	default:
		err = errors.New(res.Status)
	}

	return err
}

type Download struct {
	Method     string
	ProgressCh chan<- vim25.Progress
}

var DefaultDownload = Download{
	Method: "GET",
}

// DownloadFile GETs the given URL to a local file
func (c *Client) DownloadFile(file string, u *url.URL, param *Download) error {
	var err error

	if param == nil {
		param = &DefaultDownload
	}

	pr := progressReader{
		ch: param.ProgressCh,
	}

	// Mark progress reader as done when returning from this function.
	defer func() {
		pr.Done(err)
	}()

	fh, err := os.Create(file)
	if err != nil {
		return err
	}
	defer fh.Close()

	req, err := http.NewRequest(param.Method, u.String(), nil)
	if err != nil {
		return err
	}

	res, err := c.Client.Do(req)
	if err != nil {
		return err
	}

	defer res.Body.Close()

	switch res.StatusCode {
	case http.StatusOK:
	default:
		err = errors.New(res.Status)
	}

	if err != nil {
		return err
	}

	pr.r = res.Body
	pr.size = res.ContentLength
	_, err = io.Copy(fh, &pr)
	if err != nil {
		return err
	}

	// Assign error before returning so that it gets picked up by the deferred
	// function marking the progress reader as done.
	err = fh.Close()
	if err != nil {
		return err
	}

	return nil
}
<commit_msg>Include Client.URL's port in ParseURL<commit_after>/*
Copyright (c) 2014 VMware, Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package soap

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/cookiejar"
	"net/http/httputil"
	"net/url"
	"os"
	"strings"
	"sync/atomic"
	"time"

	"github.com/vmware/govmomi/vim25"
	"github.com/vmware/govmomi/vim25/debug"
	"github.com/vmware/govmomi/vim25/types"
	"github.com/vmware/govmomi/vim25/xml"
)

type HasFault interface {
	Fault() *Fault
}

type RoundTripper interface {
	RoundTrip(req, res HasFault) error
}

var cn uint64 // Client counter

type Client struct {
	http.Client

	u url.URL

	cn  uint64         // Client counter
	rn  uint64         // Request counter
	log io.WriteCloser // Request log
}

func NewClient(u url.URL) *Client {

	c := Client{
		u: u,

		cn: atomic.AddUint64(&cn, 1),
		rn: 0,
	}

	if c.u.Scheme == "https" {
		c.Client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
	}

	c.Jar, _ = cookiejar.New(nil)
	c.u.User = nil

	if debug.Enabled() {
		c.log = debug.NewFile(fmt.Sprintf("%d-client.log", c.cn))
	}

	return &c
}

func (c *Client) URL() url.URL {
	return c.u
}

type marshaledClient struct {
	Cookies []*http.Cookie
	URL     *url.URL
}

func (c *Client) MarshalJSON() ([]byte, error) {
	m := marshaledClient{
		Cookies: c.Jar.Cookies(&c.u),
		URL:     &c.u,
	}

	return json.Marshal(m)
}

func (c *Client) UnmarshalJSON(b []byte) error {
	var m marshaledClient

	err := json.Unmarshal(b, &m)
	if err != nil {
		return err
	}

	*c = *NewClient(*m.URL)
	c.Jar.SetCookies(m.URL, m.Cookies)

	return nil
}

func (c *Client) RoundTrip(reqBody, resBody HasFault) error {
	var httpreq *http.Request
	var httpres *http.Response
	var err error

	reqEnv := Envelope{Body: reqBody}
	resEnv := Envelope{Body: resBody}

	num := atomic.AddUint64(&c.rn, 1)

	b, err := xml.Marshal(reqEnv)
	if err != nil {
		panic(err)
	}

	rawreqbody := io.MultiReader(strings.NewReader(xml.Header), bytes.NewReader(b))
	if debug.Enabled() {
		f := debug.NewFile(fmt.Sprintf("%d-%04d.req.xml", c.cn, num))
		rawreqbody = io.TeeReader(rawreqbody, f)
	}

	httpreq, err = http.NewRequest("POST", c.u.String(), rawreqbody)
	if err != nil {
		panic(err)
	}

	httpreq.Header.Set(`Content-Type`, `text/xml; charset="utf-8"`)
	httpreq.Header.Set(`SOAPAction`, `urn:vim25/5.5`)

	if debug.Enabled() {
		b, _ := httputil.DumpRequest(httpreq, false)
		wc := debug.NewFile(fmt.Sprintf("%d-%04d.req.headers", c.cn, num))
		wc.Write(b)
		wc.Close()
	}

	tstart := time.Now()
	httpres, err = c.Client.Do(httpreq)
	tstop := time.Now()

	if debug.Enabled() {
		now := time.Now().Format("2006-01-02T15-04-05.999999999")
		ms := tstop.Sub(tstart) / time.Millisecond
		fmt.Fprintf(c.log, "%s: %4d took %6dms\n", now, num, ms)
	}

	if err != nil {
		return err
	}

	var rawresbody io.Reader = httpres.Body
	defer httpres.Body.Close()

	if debug.Enabled() {
		f := debug.NewFile(fmt.Sprintf("%d-%04d.res.xml", c.cn, num))
		rawresbody = io.TeeReader(rawresbody, f)
	}

	if debug.Enabled() {
		b, _ := httputil.DumpResponse(httpres, false)
		wc := debug.NewFile(fmt.Sprintf("%d-%04d.res.headers", c.cn, num))
		wc.Write(b)
		wc.Close()
	}

	dec := xml.NewDecoder(rawresbody)
	dec.TypeFunc = types.TypeFunc()
	err = dec.Decode(&resEnv)
	if err != nil {
		return err
	}

	if f := resBody.Fault(); f != nil {
		return WrapSoapFault(f)
	}

	return err
}

// ParseURL wraps url.Parse to rewrite the URL.Host field
// In the case of VM guest uploads or NFC lease URLs, a Host
// field with a value of "*" is rewritten to the Client's URL.Host.
func (c *Client) ParseURL(urlStr string) (*url.URL, error) {
	u, err := url.Parse(urlStr)
	if err != nil {
		return nil, err
	}

	host := strings.Split(u.Host, ":")
	if host[0] == "*" {
		// Also use Client's port, to support port forwarding
		u.Host = c.URL().Host
	}

	return u, nil
}

type Upload struct {
	Type       string
	Method     string
	ProgressCh chan<- vim25.Progress
}

var DefaultUpload = Upload{
	Type:   "application/octet-stream",
	Method: "PUT",
}

// UploadFile PUTs the local file to the given URL
func (c *Client) UploadFile(file string, u *url.URL, param *Upload) error {
	var err error

	if param == nil {
		param = &DefaultUpload
	}

	pr := progressReader{
		ch: param.ProgressCh,
	}

	// Mark progress reader as done when returning from this function.
	defer func() {
		pr.Done(err)
	}()

	s, err := os.Stat(file)
	if err != nil {
		return err
	}

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

	pr.r = f
	pr.size = s.Size()
	req, err := http.NewRequest(param.Method, u.String(), &pr)
	if err != nil {
		return err
	}

	req.ContentLength = s.Size()
	req.Header.Set("Content-Type", param.Type)

	res, err := c.Client.Do(req)
	if err != nil {
		return err
	}

	switch res.StatusCode {
	case http.StatusOK:
	case http.StatusCreated:
	default:
		err = errors.New(res.Status)
	}

	return err
}

type Download struct {
	Method     string
	ProgressCh chan<- vim25.Progress
}

var DefaultDownload = Download{
	Method: "GET",
}

// DownloadFile GETs the given URL to a local file
func (c *Client) DownloadFile(file string, u *url.URL, param *Download) error {
	var err error

	if param == nil {
		param = &DefaultDownload
	}

	pr := progressReader{
		ch: param.ProgressCh,
	}

	// Mark progress reader as done when returning from this function.
	defer func() {
		pr.Done(err)
	}()

	fh, err := os.Create(file)
	if err != nil {
		return err
	}
	defer fh.Close()

	req, err := http.NewRequest(param.Method, u.String(), nil)
	if err != nil {
		return err
	}

	res, err := c.Client.Do(req)
	if err != nil {
		return err
	}

	defer res.Body.Close()

	switch res.StatusCode {
	case http.StatusOK:
	default:
		err = errors.New(res.Status)
	}

	if err != nil {
		return err
	}

	pr.r = res.Body
	pr.size = res.ContentLength
	_, err = io.Copy(fh, &pr)
	if err != nil {
		return err
	}

	// Assign error before returning so that it gets picked up by the deferred
	// function marking the progress reader as done.
	err = fh.Close()
	if err != nil {
		return err
	}

	return nil
}
<|endoftext|>


<commit_before>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package printers

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"reflect"

	"k8s.io/apimachinery/pkg/runtime"

	"github.com/ghodss/yaml"
)

// JSONPrinter is an implementation of ResourcePrinter which outputs an object as JSON.
type JSONPrinter struct{}

// PrintObj is an implementation of ResourcePrinter.PrintObj which simply writes the object to the Writer.
func (p *JSONPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
	// we use reflect.Indirect here in order to obtain the actual value from a pointer.
	// we need an actual value in order to retrieve the package path for an object.
	// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
	if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
		return fmt.Errorf(InternalObjectPrinterErr)
	}

	switch obj := obj.(type) {
	case *runtime.Unknown:
		var buf bytes.Buffer
		err := json.Indent(&buf, obj.Raw, "", "    ")
		if err != nil {
			return err
		}
		buf.WriteRune('\n')
		_, err = buf.WriteTo(w)
		return err
	}

	if obj.GetObjectKind().GroupVersionKind().Empty() {
		return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
	}

	data, err := json.MarshalIndent(obj, "", "    ")
	if err != nil {
		return err
	}
	data = append(data, '\n')
	_, err = w.Write(data)
	return err
}

// YAMLPrinter is an implementation of ResourcePrinter which outputs an object as YAML.
// The input object is assumed to be in the internal version of an API and is converted
// to the given version first.
type YAMLPrinter struct {
	version   string
	converter runtime.ObjectConvertor
}

// PrintObj prints the data as YAML.
func (p *YAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
	// we use reflect.Indirect here in order to obtain the actual value from a pointer.
	// we need an actual value in order to retrieve the package path for an object.
	// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
	if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
		return fmt.Errorf(InternalObjectPrinterErr)
	}

	switch obj := obj.(type) {
	case *runtime.Unknown:
		data, err := yaml.JSONToYAML(obj.Raw)
		if err != nil {
			return err
		}
		_, err = w.Write(data)
		return err
	}

	if obj.GetObjectKind().GroupVersionKind().Empty() {
		return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
	}

	output, err := yaml.Marshal(obj)
	if err != nil {
		return err
	}
	_, err = fmt.Fprint(w, string(output))
	return err
}
<commit_msg>Remove unused fields from YAMLPrinter struct<commit_after>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package printers

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"reflect"

	"k8s.io/apimachinery/pkg/runtime"

	"github.com/ghodss/yaml"
)

// JSONPrinter is an implementation of ResourcePrinter which outputs an object as JSON.
type JSONPrinter struct{}

// PrintObj is an implementation of ResourcePrinter.PrintObj which simply writes the object to the Writer.
func (p *JSONPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
	// we use reflect.Indirect here in order to obtain the actual value from a pointer.
	// we need an actual value in order to retrieve the package path for an object.
	// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
	if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
		return fmt.Errorf(InternalObjectPrinterErr)
	}

	switch obj := obj.(type) {
	case *runtime.Unknown:
		var buf bytes.Buffer
		err := json.Indent(&buf, obj.Raw, "", "    ")
		if err != nil {
			return err
		}
		buf.WriteRune('\n')
		_, err = buf.WriteTo(w)
		return err
	}

	if obj.GetObjectKind().GroupVersionKind().Empty() {
		return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
	}

	data, err := json.MarshalIndent(obj, "", "    ")
	if err != nil {
		return err
	}
	data = append(data, '\n')
	_, err = w.Write(data)
	return err
}

// YAMLPrinter is an implementation of ResourcePrinter which outputs an object as YAML.
// The input object is assumed to be in the internal version of an API and is converted
// to the given version first.
type YAMLPrinter struct{}

// PrintObj prints the data as YAML.
func (p *YAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
	// we use reflect.Indirect here in order to obtain the actual value from a pointer.
	// we need an actual value in order to retrieve the package path for an object.
	// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
	if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
		return fmt.Errorf(InternalObjectPrinterErr)
	}

	switch obj := obj.(type) {
	case *runtime.Unknown:
		data, err := yaml.JSONToYAML(obj.Raw)
		if err != nil {
			return err
		}
		_, err = w.Write(data)
		return err
	}

	if obj.GetObjectKind().GroupVersionKind().Empty() {
		return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
	}

	output, err := yaml.Marshal(obj)
	if err != nil {
		return err
	}
	_, err = fmt.Fprint(w, string(output))
	return err
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package awsmodel

import (
	"fmt"
	"sort"
	"time"

	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/pkg/dns"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awstasks"
	"k8s.io/kops/upup/pkg/fi/fitasks"
)

const LoadBalancerDefaultIdleTimeout = 5 * time.Minute

// APILoadBalancerBuilder builds a LoadBalancer for accessing the API
type APILoadBalancerBuilder struct {
	*AWSModelContext
	Lifecycle         *fi.Lifecycle
	SecurityLifecycle *fi.Lifecycle
}

var _ fi.ModelBuilder = &APILoadBalancerBuilder{}

func (b *APILoadBalancerBuilder) Build(c *fi.ModelBuilderContext) error {
	// Configuration where an ELB fronts the API
	if !b.UseLoadBalancerForAPI() {
		return nil
	}

	lbSpec := b.Cluster.Spec.API.LoadBalancer
	if lbSpec == nil {
		// Skipping API ELB creation; not requested in Spec
		return nil
	}

	switch lbSpec.Type {
	case kops.LoadBalancerTypeInternal, kops.LoadBalancerTypePublic:
	// OK

	default:
		return fmt.Errorf("unhandled LoadBalancer type %q", lbSpec.Type)
	}

	// Compute the subnets - only one per zone, and then break ties based on chooseBestSubnetForELB
	var elbSubnets []*awstasks.Subnet
	{
		subnetsByZone := make(map[string][]*kops.ClusterSubnetSpec)
		for i := range b.Cluster.Spec.Subnets {
			subnet := &b.Cluster.Spec.Subnets[i]

			switch subnet.Type {
			case kops.SubnetTypePublic, kops.SubnetTypeUtility:
				if lbSpec.Type != kops.LoadBalancerTypePublic {
					continue
				}

			case kops.SubnetTypePrivate:
				if lbSpec.Type != kops.LoadBalancerTypeInternal {
					continue
				}

			default:
				return fmt.Errorf("subnet %q had unknown type %q", subnet.Name, subnet.Type)
			}

			subnetsByZone[subnet.Zone] = append(subnetsByZone[subnet.Zone], subnet)
		}

		for zone, subnets := range subnetsByZone {
			subnet := b.chooseBestSubnetForELB(zone, subnets)

			elbSubnets = append(elbSubnets, b.LinkToSubnet(subnet))
		}
	}

	var elb *awstasks.LoadBalancer
	{
		loadBalancerName := b.GetELBName32("api")

		idleTimeout := LoadBalancerDefaultIdleTimeout
		if lbSpec.IdleTimeoutSeconds != nil {
			idleTimeout = time.Second * time.Duration(*lbSpec.IdleTimeoutSeconds)
		}

		elb = &awstasks.LoadBalancer{
			Name:      s("api." + b.ClusterName()),
			Lifecycle: b.Lifecycle,

			LoadBalancerName: s(loadBalancerName),
			SecurityGroups: []*awstasks.SecurityGroup{
				b.LinkToELBSecurityGroup("api"),
			},
			Subnets: elbSubnets,
			Listeners: map[string]*awstasks.LoadBalancerListener{
				"443": {InstancePort: 443},
			},

			// Configure fast-recovery health-checks
			HealthCheck: &awstasks.LoadBalancerHealthCheck{
				Target:             s("SSL:443"),
				Timeout:            i64(5),
				Interval:           i64(10),
				HealthyThreshold:   i64(2),
				UnhealthyThreshold: i64(2),
			},

			ConnectionSettings: &awstasks.LoadBalancerConnectionSettings{
				IdleTimeout: i64(int64(idleTimeout.Seconds())),
			},
		}

		switch lbSpec.Type {
		case kops.LoadBalancerTypeInternal:
			elb.Scheme = s("internal")
		case kops.LoadBalancerTypePublic:
			elb.Scheme = nil
		default:
			return fmt.Errorf("unknown elb Type: %q", lbSpec.Type)
		}

		c.AddTask(elb)
	}

	// Create security group for API ELB
	{
		t := &awstasks.SecurityGroup{
			Name:      s(b.ELBSecurityGroupName("api")),
			Lifecycle: b.SecurityLifecycle,

			VPC:              b.LinkToVPC(),
			Description:      s("Security group for api ELB"),
			RemoveExtraRules: []string{"port=443"},
		}
		c.AddTask(t)
	}

	// Allow traffic from ELB to egress freely
	{
		t := &awstasks.SecurityGroupRule{
			Name:      s("api-elb-egress"),
			Lifecycle: b.SecurityLifecycle,

			SecurityGroup: b.LinkToELBSecurityGroup("api"),
			Egress:        fi.Bool(true),
			CIDR:          s("0.0.0.0/0"),
		}
		c.AddTask(t)
	}

	// Allow traffic into the ELB from KubernetesAPIAccess CIDRs
	{
		for _, cidr := range b.Cluster.Spec.KubernetesAPIAccess {
			t := &awstasks.SecurityGroupRule{
				Name:      s("https-api-elb-" + cidr),
				Lifecycle: b.SecurityLifecycle,

				SecurityGroup: b.LinkToELBSecurityGroup("api"),
				CIDR:          s(cidr),
				FromPort:      i64(443),
				ToPort:        i64(443),
				Protocol:      s("tcp"),
			}
			c.AddTask(t)
		}
	}

	// Allow HTTPS to the master instances from the ELB
	{
		t := &awstasks.SecurityGroupRule{
			Name:      s("https-elb-to-master"),
			Lifecycle: b.SecurityLifecycle,

			SecurityGroup: b.LinkToSecurityGroup(kops.InstanceGroupRoleMaster),
			SourceGroup:   b.LinkToELBSecurityGroup("api"),
			FromPort:      i64(443),
			ToPort:        i64(443),
			Protocol:      s("tcp"),
		}
		c.AddTask(t)
	}

	if dns.IsGossipHostname(b.Cluster.Name) {
		// Ensure the ELB hostname is included in the TLS certificate,
		// if we're not going to use an alias for it
		// TODO: I don't love this technique for finding the task by name & modifying it
		masterKeypairTask, found := c.Tasks["Keypair/master"]
		if !found {
			return fmt.Errorf("keypair/master task not found")
		}
		masterKeypair := masterKeypairTask.(*fitasks.Keypair)
		masterKeypair.AlternateNameTasks = append(masterKeypair.AlternateNameTasks, elb)
	}

	for _, ig := range b.MasterInstanceGroups() {
		t := &awstasks.LoadBalancerAttachment{
			Name:      s("api-" + ig.ObjectMeta.Name),
			Lifecycle: b.Lifecycle,

			LoadBalancer:     b.LinkToELB("api"),
			AutoscalingGroup: b.LinkToAutoscalingGroup(ig),
		}

		c.AddTask(t)
	}

	return nil

}

type scoredSubnet struct {
	score  int
	subnet *kops.ClusterSubnetSpec
}

type ByScoreDescending []*scoredSubnet

func (a ByScoreDescending) Len() int      { return len(a) }
func (a ByScoreDescending) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByScoreDescending) Less(i, j int) bool {
	if a[i].score != a[j].score {
		// ! to sort highest score first
		return !(a[i].score < a[j].score)
	}
	// Use name to break ties consistently
	return a[i].subnet.Name < a[j].subnet.Name
}

// Choose between subnets in a zone.
// We have already applied the rules to match internal subnets to internal ELBs and vice-versa for public-facing ELBs.
// For internal ELBs: we prefer the master subnets
// For public facing ELBs: we prefer the utility subnets
func (b *APILoadBalancerBuilder) chooseBestSubnetForELB(zone string, subnets []*kops.ClusterSubnetSpec) *kops.ClusterSubnetSpec {
	if len(subnets) == 0 {
		return nil
	}
	if len(subnets) == 1 {
		return subnets[0]
	}

	migSubnets := sets.NewString()
	for _, ig := range b.MasterInstanceGroups() {
		for _, subnet := range ig.Spec.Subnets {
			migSubnets.Insert(subnet)
		}
	}

	var scoredSubnets []*scoredSubnet
	for _, subnet := range subnets {
		score := 0

		if migSubnets.Has(subnet.Name) {
			score += 1
		}

		if subnet.Type == kops.SubnetTypeUtility {
			score += 1
		}

		scoredSubnets = append(scoredSubnets, &scoredSubnet{
			score:  score,
			subnet: subnet,
		})
	}

	sort.Sort(ByScoreDescending(scoredSubnets))

	if scoredSubnets[0].score == scoredSubnets[1].score {
		glog.V(2).Infof("Making arbitrary choice between subnets in zone %q to attach to ELB (%q vs %q)", zone, scoredSubnets[0].subnet.Name, scoredSubnets[1].subnet.Name)
	}

	return scoredSubnets[0].subnet
}
<commit_msg>Include ELB hostname in certificate when using private DNS<commit_after>/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package awsmodel

import (
	"fmt"
	"sort"
	"time"

	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/pkg/dns"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awstasks"
	"k8s.io/kops/upup/pkg/fi/fitasks"
)

const LoadBalancerDefaultIdleTimeout = 5 * time.Minute

// APILoadBalancerBuilder builds a LoadBalancer for accessing the API
type APILoadBalancerBuilder struct {
	*AWSModelContext
	Lifecycle         *fi.Lifecycle
	SecurityLifecycle *fi.Lifecycle
}

var _ fi.ModelBuilder = &APILoadBalancerBuilder{}

func (b *APILoadBalancerBuilder) Build(c *fi.ModelBuilderContext) error {
	// Configuration where an ELB fronts the API
	if !b.UseLoadBalancerForAPI() {
		return nil
	}

	lbSpec := b.Cluster.Spec.API.LoadBalancer
	if lbSpec == nil {
		// Skipping API ELB creation; not requested in Spec
		return nil
	}

	switch lbSpec.Type {
	case kops.LoadBalancerTypeInternal, kops.LoadBalancerTypePublic:
	// OK

	default:
		return fmt.Errorf("unhandled LoadBalancer type %q", lbSpec.Type)
	}

	// Compute the subnets - only one per zone, and then break ties based on chooseBestSubnetForELB
	var elbSubnets []*awstasks.Subnet
	{
		subnetsByZone := make(map[string][]*kops.ClusterSubnetSpec)
		for i := range b.Cluster.Spec.Subnets {
			subnet := &b.Cluster.Spec.Subnets[i]

			switch subnet.Type {
			case kops.SubnetTypePublic, kops.SubnetTypeUtility:
				if lbSpec.Type != kops.LoadBalancerTypePublic {
					continue
				}

			case kops.SubnetTypePrivate:
				if lbSpec.Type != kops.LoadBalancerTypeInternal {
					continue
				}

			default:
				return fmt.Errorf("subnet %q had unknown type %q", subnet.Name, subnet.Type)
			}

			subnetsByZone[subnet.Zone] = append(subnetsByZone[subnet.Zone], subnet)
		}

		for zone, subnets := range subnetsByZone {
			subnet := b.chooseBestSubnetForELB(zone, subnets)

			elbSubnets = append(elbSubnets, b.LinkToSubnet(subnet))
		}
	}

	var elb *awstasks.LoadBalancer
	{
		loadBalancerName := b.GetELBName32("api")

		idleTimeout := LoadBalancerDefaultIdleTimeout
		if lbSpec.IdleTimeoutSeconds != nil {
			idleTimeout = time.Second * time.Duration(*lbSpec.IdleTimeoutSeconds)
		}

		elb = &awstasks.LoadBalancer{
			Name:      s("api." + b.ClusterName()),
			Lifecycle: b.Lifecycle,

			LoadBalancerName: s(loadBalancerName),
			SecurityGroups: []*awstasks.SecurityGroup{
				b.LinkToELBSecurityGroup("api"),
			},
			Subnets: elbSubnets,
			Listeners: map[string]*awstasks.LoadBalancerListener{
				"443": {InstancePort: 443},
			},

			// Configure fast-recovery health-checks
			HealthCheck: &awstasks.LoadBalancerHealthCheck{
				Target:             s("SSL:443"),
				Timeout:            i64(5),
				Interval:           i64(10),
				HealthyThreshold:   i64(2),
				UnhealthyThreshold: i64(2),
			},

			ConnectionSettings: &awstasks.LoadBalancerConnectionSettings{
				IdleTimeout: i64(int64(idleTimeout.Seconds())),
			},
		}

		switch lbSpec.Type {
		case kops.LoadBalancerTypeInternal:
			elb.Scheme = s("internal")
		case kops.LoadBalancerTypePublic:
			elb.Scheme = nil
		default:
			return fmt.Errorf("unknown elb Type: %q", lbSpec.Type)
		}

		c.AddTask(elb)
	}

	// Create security group for API ELB
	{
		t := &awstasks.SecurityGroup{
			Name:      s(b.ELBSecurityGroupName("api")),
			Lifecycle: b.SecurityLifecycle,

			VPC:              b.LinkToVPC(),
			Description:      s("Security group for api ELB"),
			RemoveExtraRules: []string{"port=443"},
		}
		c.AddTask(t)
	}

	// Allow traffic from ELB to egress freely
	{
		t := &awstasks.SecurityGroupRule{
			Name:      s("api-elb-egress"),
			Lifecycle: b.SecurityLifecycle,

			SecurityGroup: b.LinkToELBSecurityGroup("api"),
			Egress:        fi.Bool(true),
			CIDR:          s("0.0.0.0/0"),
		}
		c.AddTask(t)
	}

	// Allow traffic into the ELB from KubernetesAPIAccess CIDRs
	{
		for _, cidr := range b.Cluster.Spec.KubernetesAPIAccess {
			t := &awstasks.SecurityGroupRule{
				Name:      s("https-api-elb-" + cidr),
				Lifecycle: b.SecurityLifecycle,

				SecurityGroup: b.LinkToELBSecurityGroup("api"),
				CIDR:          s(cidr),
				FromPort:      i64(443),
				ToPort:        i64(443),
				Protocol:      s("tcp"),
			}
			c.AddTask(t)
		}
	}

	// Allow HTTPS to the master instances from the ELB
	{
		t := &awstasks.SecurityGroupRule{
			Name:      s("https-elb-to-master"),
			Lifecycle: b.SecurityLifecycle,

			SecurityGroup: b.LinkToSecurityGroup(kops.InstanceGroupRoleMaster),
			SourceGroup:   b.LinkToELBSecurityGroup("api"),
			FromPort:      i64(443),
			ToPort:        i64(443),
			Protocol:      s("tcp"),
		}
		c.AddTask(t)
	}

	if dns.IsGossipHostname(b.Cluster.Name) || b.UsePrivateDNS() {
		// Ensure the ELB hostname is included in the TLS certificate,
		// if we're not going to use an alias for it
		// TODO: I don't love this technique for finding the task by name & modifying it
		masterKeypairTask, found := c.Tasks["Keypair/master"]
		if !found {
			return fmt.Errorf("keypair/master task not found")
		}
		masterKeypair := masterKeypairTask.(*fitasks.Keypair)
		masterKeypair.AlternateNameTasks = append(masterKeypair.AlternateNameTasks, elb)
	}

	for _, ig := range b.MasterInstanceGroups() {
		t := &awstasks.LoadBalancerAttachment{
			Name:      s("api-" + ig.ObjectMeta.Name),
			Lifecycle: b.Lifecycle,

			LoadBalancer:     b.LinkToELB("api"),
			AutoscalingGroup: b.LinkToAutoscalingGroup(ig),
		}

		c.AddTask(t)
	}

	return nil

}

type scoredSubnet struct {
	score  int
	subnet *kops.ClusterSubnetSpec
}

type ByScoreDescending []*scoredSubnet

func (a ByScoreDescending) Len() int      { return len(a) }
func (a ByScoreDescending) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByScoreDescending) Less(i, j int) bool {
	if a[i].score != a[j].score {
		// ! to sort highest score first
		return !(a[i].score < a[j].score)
	}
	// Use name to break ties consistently
	return a[i].subnet.Name < a[j].subnet.Name
}

// Choose between subnets in a zone.
// We have already applied the rules to match internal subnets to internal ELBs and vice-versa for public-facing ELBs.
// For internal ELBs: we prefer the master subnets
// For public facing ELBs: we prefer the utility subnets
func (b *APILoadBalancerBuilder) chooseBestSubnetForELB(zone string, subnets []*kops.ClusterSubnetSpec) *kops.ClusterSubnetSpec {
	if len(subnets) == 0 {
		return nil
	}
	if len(subnets) == 1 {
		return subnets[0]
	}

	migSubnets := sets.NewString()
	for _, ig := range b.MasterInstanceGroups() {
		for _, subnet := range ig.Spec.Subnets {
			migSubnets.Insert(subnet)
		}
	}

	var scoredSubnets []*scoredSubnet
	for _, subnet := range subnets {
		score := 0

		if migSubnets.Has(subnet.Name) {
			score += 1
		}

		if subnet.Type == kops.SubnetTypeUtility {
			score += 1
		}

		scoredSubnets = append(scoredSubnets, &scoredSubnet{
			score:  score,
			subnet: subnet,
		})
	}

	sort.Sort(ByScoreDescending(scoredSubnets))

	if scoredSubnets[0].score == scoredSubnets[1].score {
		glog.V(2).Infof("Making arbitrary choice between subnets in zone %q to attach to ELB (%q vs %q)", zone, scoredSubnets[0].subnet.Name, scoredSubnets[1].subnet.Name)
	}

	return scoredSubnets[0].subnet
}
<|endoftext|>


<commit_before>/*
Copyright 2020 The Rook Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package cluster to manage a Ceph cluster.
package cluster

import (
	"context"

	"github.com/google/go-cmp/cmp"
	cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
	"github.com/rook/rook/pkg/clusterd"
	discoverDaemon "github.com/rook/rook/pkg/daemon/discover"
	"github.com/rook/rook/pkg/operator/ceph/controller"
	"github.com/rook/rook/pkg/operator/k8sutil"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicateForNodeWatcher is the predicate function to trigger reconcile on Node events
func predicateForNodeWatcher(ctx context.Context, client client.Client, context *clusterd.Context) predicate.Funcs {
	return predicate.Funcs{
		CreateFunc: func(e event.CreateEvent) bool {
			clientCluster := newClientCluster(client, e.Object.GetNamespace(), context)
			return clientCluster.onK8sNode(ctx, e.Object)
		},

		UpdateFunc: func(e event.UpdateEvent) bool {
			clientCluster := newClientCluster(client, e.ObjectNew.GetNamespace(), context)
			return clientCluster.onK8sNode(ctx, e.ObjectNew)
		},

		DeleteFunc: func(e event.DeleteEvent) bool {
			return false
		},

		GenericFunc: func(e event.GenericEvent) bool {
			return false
		},
	}
}

// predicateForHotPlugCMWatcher is the predicate function to trigger reconcile on ConfigMap events (hot-plug)
func predicateForHotPlugCMWatcher(client client.Client) predicate.Funcs {
	return predicate.Funcs{
		UpdateFunc: func(e event.UpdateEvent) bool {
			isHotPlugCM := isHotPlugCM(e.ObjectNew)
			if !isHotPlugCM {
				logger.Debugf("hot-plug cm watcher: only reconcile on hot plug cm changes, this %q cm is handled by another watcher", e.ObjectNew.GetName())
				return false
			}

			clientCluster := newClientCluster(client, e.ObjectNew.GetNamespace(), &clusterd.Context{})
			return clientCluster.onDeviceCMUpdate(e.ObjectOld, e.ObjectNew)
		},

		DeleteFunc: func(e event.DeleteEvent) bool {
			// TODO: if the configmap goes away we could retrigger rook-discover DS
			// However at this point the returned bool can only trigger a reconcile of the CephCluster object
			// Definitely non-trivial but nice to have in the future
			return false
		},

		CreateFunc: func(e event.CreateEvent) bool {
			return false
		},

		GenericFunc: func(e event.GenericEvent) bool {
			return false
		},
	}
}

// isHotPlugCM informs whether the object is the cm for hot-plug disk
func isHotPlugCM(obj runtime.Object) bool {
	// If not a ConfigMap, let's not reconcile
	cm, ok := obj.(*corev1.ConfigMap)
	if !ok {
		return false
	}

	// Get the labels
	labels := cm.GetLabels()

	labelVal, labelKeyExist := labels[k8sutil.AppAttr]
	if labelKeyExist && labelVal == discoverDaemon.AppName {
		return true
	}

	return false
}

func watchControllerPredicate(ctx context.Context, c client.Client) predicate.Funcs {
	return predicate.Funcs{
		CreateFunc: func(e event.CreateEvent) bool {
			if controller.DuplicateCephClusters(ctx, c, e.Object, true) {
				return false
			}
			logger.Debug("create event from a CR")
			return true
		},
		DeleteFunc: func(e event.DeleteEvent) bool {
			logger.Debug("delete event from a CR")
			return true
		},
		UpdateFunc: func(e event.UpdateEvent) bool {
			// We still need to check on update event since the user must delete the additional CR
			// Until this is done, the user can still update the CR and the operator will reconcile
			// This should not happen
			if controller.DuplicateCephClusters(ctx, c, e.ObjectOld, true) {
				return false
			}

			// resource.Quantity has non-exportable fields, so we use its comparator method
			resourceQtyComparer := cmp.Comparer(func(x, y resource.Quantity) bool { return x.Cmp(y) == 0 })

			switch objOld := e.ObjectOld.(type) {
			case *cephv1.CephCluster:
				objNew := e.ObjectNew.(*cephv1.CephCluster)
				logger.Debug("update event on CephCluster CR")
				// If the labels "do_not_reconcile" is set on the object, let's not reconcile that request
				isDoNotReconcile := controller.IsDoNotReconcile(objNew.GetLabels())
				if isDoNotReconcile {
					logger.Debugf("object %q matched on update but %q label is set, doing nothing", controller.DoNotReconcileLabelName, objNew.Name)
					return false
				}
				diff := cmp.Diff(objOld.Spec, objNew.Spec, resourceQtyComparer)
				if diff != "" {
					logger.Infof("CR has changed for %q. diff=%s", objNew.Name, diff)

					if objNew.Spec.CleanupPolicy.HasDataDirCleanPolicy() {
						logger.Infof("skipping orchestration for cluster object %q in namespace %q because its cleanup policy is set. not reloading the manager", objNew.GetName(), objNew.GetNamespace())
						return false
					}

					// Stop any ongoing orchestration
					controller.ReloadManager()

					return false

				} else if !objOld.GetDeletionTimestamp().Equal(objNew.GetDeletionTimestamp()) {
					logger.Infof("CR %q is going be deleted, cancelling any ongoing orchestration", objNew.Name)

					// Stop any ongoing orchestration
					controller.ReloadManager()

					return false

				} else if objOld.GetGeneration() != objNew.GetGeneration() {
					logger.Debugf("skipping resource %q update with unchanged spec", objNew.Name)
				}
			}

			return false
		},
		GenericFunc: func(e event.GenericEvent) bool {
			return false
		},
	}
}
<commit_msg>operator: silence 'cm is handled by another watcher' DEBUG messages<commit_after>/*
Copyright 2020 The Rook Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package cluster to manage a Ceph cluster.
package cluster

import (
	"context"

	"github.com/google/go-cmp/cmp"
	cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
	"github.com/rook/rook/pkg/clusterd"
	discoverDaemon "github.com/rook/rook/pkg/daemon/discover"
	"github.com/rook/rook/pkg/operator/ceph/controller"
	"github.com/rook/rook/pkg/operator/k8sutil"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicateForNodeWatcher is the predicate function to trigger reconcile on Node events
func predicateForNodeWatcher(ctx context.Context, client client.Client, context *clusterd.Context) predicate.Funcs {
	return predicate.Funcs{
		CreateFunc: func(e event.CreateEvent) bool {
			clientCluster := newClientCluster(client, e.Object.GetNamespace(), context)
			return clientCluster.onK8sNode(ctx, e.Object)
		},

		UpdateFunc: func(e event.UpdateEvent) bool {
			clientCluster := newClientCluster(client, e.ObjectNew.GetNamespace(), context)
			return clientCluster.onK8sNode(ctx, e.ObjectNew)
		},

		DeleteFunc: func(e event.DeleteEvent) bool {
			return false
		},

		GenericFunc: func(e event.GenericEvent) bool {
			return false
		},
	}
}

// predicateForHotPlugCMWatcher is the predicate function to trigger reconcile on ConfigMap events (hot-plug)
func predicateForHotPlugCMWatcher(client client.Client) predicate.Funcs {
	return predicate.Funcs{
		UpdateFunc: func(e event.UpdateEvent) bool {
			isHotPlugCM := isHotPlugCM(e.ObjectNew)
			if !isHotPlugCM {
				return false
			}

			clientCluster := newClientCluster(client, e.ObjectNew.GetNamespace(), &clusterd.Context{})
			return clientCluster.onDeviceCMUpdate(e.ObjectOld, e.ObjectNew)
		},

		DeleteFunc: func(e event.DeleteEvent) bool {
			// TODO: if the configmap goes away we could retrigger rook-discover DS
			// However at this point the returned bool can only trigger a reconcile of the CephCluster object
			// Definitely non-trivial but nice to have in the future
			return false
		},

		CreateFunc: func(e event.CreateEvent) bool {
			return false
		},

		GenericFunc: func(e event.GenericEvent) bool {
			return false
		},
	}
}

// isHotPlugCM informs whether the object is the cm for hot-plug disk
func isHotPlugCM(obj runtime.Object) bool {
	// If not a ConfigMap, let's not reconcile
	cm, ok := obj.(*corev1.ConfigMap)
	if !ok {
		return false
	}

	// Get the labels
	labels := cm.GetLabels()

	labelVal, labelKeyExist := labels[k8sutil.AppAttr]
	if labelKeyExist && labelVal == discoverDaemon.AppName {
		return true
	}

	return false
}

func watchControllerPredicate(ctx context.Context, c client.Client) predicate.Funcs {
	return predicate.Funcs{
		CreateFunc: func(e event.CreateEvent) bool {
			if controller.DuplicateCephClusters(ctx, c, e.Object, true) {
				return false
			}
			logger.Debug("create event from a CR")
			return true
		},
		DeleteFunc: func(e event.DeleteEvent) bool {
			logger.Debug("delete event from a CR")
			return true
		},
		UpdateFunc: func(e event.UpdateEvent) bool {
			// We still need to check on update event since the user must delete the additional CR
			// Until this is done, the user can still update the CR and the operator will reconcile
			// This should not happen
			if controller.DuplicateCephClusters(ctx, c, e.ObjectOld, true) {
				return false
			}

			// resource.Quantity has non-exportable fields, so we use its comparator method
			resourceQtyComparer := cmp.Comparer(func(x, y resource.Quantity) bool { return x.Cmp(y) == 0 })

			switch objOld := e.ObjectOld.(type) {
			case *cephv1.CephCluster:
				objNew := e.ObjectNew.(*cephv1.CephCluster)
				logger.Debug("update event on CephCluster CR")
				// If the labels "do_not_reconcile" is set on the object, let's not reconcile that request
				isDoNotReconcile := controller.IsDoNotReconcile(objNew.GetLabels())
				if isDoNotReconcile {
					logger.Debugf("object %q matched on update but %q label is set, doing nothing", controller.DoNotReconcileLabelName, objNew.Name)
					return false
				}
				diff := cmp.Diff(objOld.Spec, objNew.Spec, resourceQtyComparer)
				if diff != "" {
					logger.Infof("CR has changed for %q. diff=%s", objNew.Name, diff)

					if objNew.Spec.CleanupPolicy.HasDataDirCleanPolicy() {
						logger.Infof("skipping orchestration for cluster object %q in namespace %q because its cleanup policy is set. not reloading the manager", objNew.GetName(), objNew.GetNamespace())
						return false
					}

					// Stop any ongoing orchestration
					controller.ReloadManager()

					return false

				} else if !objOld.GetDeletionTimestamp().Equal(objNew.GetDeletionTimestamp()) {
					logger.Infof("CR %q is going be deleted, cancelling any ongoing orchestration", objNew.Name)

					// Stop any ongoing orchestration
					controller.ReloadManager()

					return false

				} else if objOld.GetGeneration() != objNew.GetGeneration() {
					logger.Debugf("skipping resource %q update with unchanged spec", objNew.Name)
				}
			}

			return false
		},
		GenericFunc: func(e event.GenericEvent) bool {
			return false
		},
	}
}
<|endoftext|>