<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor:
// - Aaron Meihm ameihm@mozilla.com

package scribe

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"unicode"
)

// This is a go version of the librpm rpmvercmp() function, that compares
// two version strings to determine which is newer. It has undergone a
// series of tests but there are likely to be some edge cases and certain
// scenarios it does not handle.

const (
	_ = iota
	EVROP_LESS_THAN
	EVROP_EQUALS
	EVROP_UNKNOWN
)

type EVR struct {
	epoch   string
	version string
	release string
}

func evrLookupOperation(s string) int {
	switch s {
	case "<":
		return EVROP_LESS_THAN
	case "=":
		return EVROP_EQUALS
	}
	return EVROP_UNKNOWN
}

func evrOperationStr(val int) string {
	switch val {
	case EVROP_LESS_THAN:
		return "<"
	case EVROP_EQUALS:
		return "="
	default:
		return "?"
	}
}

func evrIsDigit(c rune) bool {
	return unicode.IsDigit(c)
}

func evrIsNumber(s string) bool {
	_, err := strconv.Atoi(s)
	if err != nil {
		return false
	} else {
		return true
	}
}

func evrExtract(s string) (EVR, error) {
	var ret EVR
	var idx int

	for _, c := range s {
		if !evrIsDigit(c) {
			break
		}
		idx++
	}

	if idx >= len(s) {
		return ret, fmt.Errorf("evrExtract: all digits")
	}

	if s[idx] == ':' {
		ret.epoch = s[:idx]
		idx++
	} else {
		ret.epoch = "0"
		idx = 0
	}

	if idx >= len(s) {
		return ret, fmt.Errorf("evrExtract: only epoch")
	}
	remain := s[idx:]

	rp0 := strings.LastIndex(remain, "-")
	if rp0 != -1 {
		ret.version = remain[:rp0]
		rp0++
		if rp0 >= len(remain) {
			return ret, fmt.Errorf("evrExtract: ends in dash")
		}
		ret.release = remain[rp0:]
	} else {
		ret.version = remain
		ret.release = ""
	}

	debugPrint("evrExtract(): epoch=%v, version=%v, revision=%v\n", ret.epoch, ret.version, ret.release)
	return ret, nil
}

func evrRpmTokenizer(s string) []string {
	re := regexp.MustCompile("[A-Za-z0-9]+")
	buf := re.FindAllString(s, -1)
	ret := make([]string, 0)
	var isnum bool
	var cmp string
	for _, x := range buf {
		cmp = ""
		for _, c := range x {
			if len(cmp) == 0 {
				if evrIsDigit(c) {
					isnum = true
				} else {
					isnum = false
				}
				cmp += string(c)
			} else {
				if isnum {
					if !evrIsDigit(c) {
						ret = append(ret, cmp)
						cmp = string(c)
						isnum = false
					} else {
						cmp += string(c)
					}
				} else {
					if evrIsDigit(c) {
						ret = append(ret, cmp)
						cmp = string(c)
						isnum = true
					} else {
						cmp += string(c)
					}
				}
			}
		}
		ret = append(ret, cmp)
	}
	return ret
}

func evrTrimZeros(s string) string {
	if len(s) == 1 {
		return s
	}
	_, err := strconv.Atoi(s)
	if err != nil {
		return s
	}
	return strings.TrimLeft(s, "0")
}

func evrRpmVerCmp(actual string, check string) int {
	if actual == check {
		return 0
	}

	acttokens := evrRpmTokenizer(actual)
	chktokens := evrRpmTokenizer(check)

	for i := range chktokens {
		if i >= len(acttokens) {
			// There are more tokens in the check value, the
			// check wins.
			return 1
		}

		// If the values are pure numbers, trim any leading 0's.
		acttest := evrTrimZeros(acttokens[i])
		chktest := evrTrimZeros(chktokens[i])

		// Numeric component will always win out over alpha.
		if evrIsDigit(rune(acttest[0])) && !evrIsDigit(rune(chktest[0])) {
			return -1
		}
		if evrIsDigit(rune(chktest[0])) && !evrIsDigit(rune(acttest[0])) {
			return 1
		}

		// If both values are pure numeric values, convert and check here
		if evrIsNumber(acttest) && evrIsNumber(chktest) {
			na, err := strconv.Atoi(acttest)
			if err != nil {
				panic("IsNumber and failed actual conversion")
			}
			nc, err := strconv.Atoi(chktest)
			if err != nil {
				panic("IsNumber and failed check conversion")
			}
			if nc > na {
				return 1
			} else if nc < na {
				return -1
			} else {
				continue
			}
		}

		// Do a lexical string comparison here, this should work
		// even with pure integer values.
		if chktest > acttest {
			return 1
		} else if chktest < acttest {
			return -1
		}
	}

	// If we get this far, see if the actual value still has more tokens
	// for comparison, if so actual wins.
	if len(acttokens) > len(chktokens) {
		return -1
	}

	return 0
}

func evrRpmCompare(actual EVR, check EVR) (int, error) {
	aepoch, err := strconv.Atoi(actual.epoch)
	if err != nil {
		return 0, fmt.Errorf("evrRpmCompare: bad actual epoch")
	}
	cepoch, err := strconv.Atoi(check.epoch)
	if err != nil {
		return 0, fmt.Errorf("evrRpmCompare: bad check epoch")
	}
	if cepoch > aepoch {
		return 1, nil
	} else if cepoch < aepoch {
		return -1, nil
	}

	ret := evrRpmVerCmp(actual.version, check.version)
	if ret != 0 {
		return ret, nil
	}

	ret = evrRpmVerCmp(actual.release, check.release)
	if ret != 0 {
		return ret, nil
	}

	return 0, nil
}

func evrCompare(op int, actual string, check string) (bool, error) {
	debugPrint("evrCompare(): %v %v %v\n", actual, evrOperationStr(op), check)

	evract, err := evrExtract(actual)
	if err != nil {
		return false, err
	}
	evrchk, err := evrExtract(check)
	if err != nil {
		return false, err
	}

	ret, err := evrRpmCompare(evract, evrchk)
	if err != nil {
		return false, err
	}
	switch op {
	case EVROP_EQUALS:
		if ret != 0 {
			return false, nil
		}
		return true, nil
	case EVROP_LESS_THAN:
		if ret == 1 {
			return true, nil
		}
		return false, nil
	}
	return false, fmt.Errorf("evrCompare: unknown operator")
}

// Exported version of evrCompare() used for testing in evrtest.
func TestEvrCompare(op int, actual string, check string) (bool, error) {
	return evrCompare(op, actual, check)
}
<commit_msg>add documentation around TestEvrCompare()<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor:
// - Aaron Meihm ameihm@mozilla.com

package scribe

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"unicode"
)

// This is a go version of the librpm rpmvercmp() function, that compares
// two version strings to determine which is newer. It has undergone a
// series of tests but there are likely to be some edge cases and certain
// scenarios it does not handle.

const (
	_ = iota
	EVROP_LESS_THAN
	EVROP_EQUALS
	EVROP_UNKNOWN
)

type EVR struct {
	epoch   string
	version string
	release string
}

func evrLookupOperation(s string) int {
	switch s {
	case "<":
		return EVROP_LESS_THAN
	case "=":
		return EVROP_EQUALS
	}
	return EVROP_UNKNOWN
}

func evrOperationStr(val int) string {
	switch val {
	case EVROP_LESS_THAN:
		return "<"
	case EVROP_EQUALS:
		return "="
	default:
		return "?"
	}
}

func evrIsDigit(c rune) bool {
	return unicode.IsDigit(c)
}

func evrIsNumber(s string) bool {
	_, err := strconv.Atoi(s)
	if err != nil {
		return false
	} else {
		return true
	}
}

func evrExtract(s string) (EVR, error) {
	var ret EVR
	var idx int

	for _, c := range s {
		if !evrIsDigit(c) {
			break
		}
		idx++
	}

	if idx >= len(s) {
		return ret, fmt.Errorf("evrExtract: all digits")
	}

	if s[idx] == ':' {
		ret.epoch = s[:idx]
		idx++
	} else {
		ret.epoch = "0"
		idx = 0
	}

	if idx >= len(s) {
		return ret, fmt.Errorf("evrExtract: only epoch")
	}
	remain := s[idx:]

	rp0 := strings.LastIndex(remain, "-")
	if rp0 != -1 {
		ret.version = remain[:rp0]
		rp0++
		if rp0 >= len(remain) {
			return ret, fmt.Errorf("evrExtract: ends in dash")
		}
		ret.release = remain[rp0:]
	} else {
		ret.version = remain
		ret.release = ""
	}

	debugPrint("evrExtract(): epoch=%v, version=%v, revision=%v\n", ret.epoch, ret.version, ret.release)
	return ret, nil
}

func evrRpmTokenizer(s string) []string {
	re := regexp.MustCompile("[A-Za-z0-9]+")
	buf := re.FindAllString(s, -1)
	ret := make([]string, 0)
	var isnum bool
	var cmp string
	for _, x := range buf {
		cmp = ""
		for _, c := range x {
			if len(cmp) == 0 {
				if evrIsDigit(c) {
					isnum = true
				} else {
					isnum = false
				}
				cmp += string(c)
			} else {
				if isnum {
					if !evrIsDigit(c) {
						ret = append(ret, cmp)
						cmp = string(c)
						isnum = false
					} else {
						cmp += string(c)
					}
				} else {
					if evrIsDigit(c) {
						ret = append(ret, cmp)
						cmp = string(c)
						isnum = true
					} else {
						cmp += string(c)
					}
				}
			}
		}
		ret = append(ret, cmp)
	}
	return ret
}

func evrTrimZeros(s string) string {
	if len(s) == 1 {
		return s
	}
	_, err := strconv.Atoi(s)
	if err != nil {
		return s
	}
	return strings.TrimLeft(s, "0")
}

func evrRpmVerCmp(actual string, check string) int {
	if actual == check {
		return 0
	}

	acttokens := evrRpmTokenizer(actual)
	chktokens := evrRpmTokenizer(check)

	for i := range chktokens {
		if i >= len(acttokens) {
			// There are more tokens in the check value, the
			// check wins.
			return 1
		}

		// If the values are pure numbers, trim any leading 0's.
		acttest := evrTrimZeros(acttokens[i])
		chktest := evrTrimZeros(chktokens[i])

		// Numeric component will always win out over alpha.
		if evrIsDigit(rune(acttest[0])) && !evrIsDigit(rune(chktest[0])) {
			return -1
		}
		if evrIsDigit(rune(chktest[0])) && !evrIsDigit(rune(acttest[0])) {
			return 1
		}

		// If both values are pure numeric values, convert and check here
		if evrIsNumber(acttest) && evrIsNumber(chktest) {
			na, err := strconv.Atoi(acttest)
			if err != nil {
				panic("IsNumber and failed actual conversion")
			}
			nc, err := strconv.Atoi(chktest)
			if err != nil {
				panic("IsNumber and failed check conversion")
			}
			if nc > na {
				return 1
			} else if nc < na {
				return -1
			} else {
				continue
			}
		}

		// Do a lexical string comparison here, this should work
		// even with pure integer values.
		if chktest > acttest {
			return 1
		} else if chktest < acttest {
			return -1
		}
	}

	// If we get this far, see if the actual value still has more tokens
	// for comparison, if so actual wins.
	if len(acttokens) > len(chktokens) {
		return -1
	}

	return 0
}

func evrRpmCompare(actual EVR, check EVR) (int, error) {
	aepoch, err := strconv.Atoi(actual.epoch)
	if err != nil {
		return 0, fmt.Errorf("evrRpmCompare: bad actual epoch")
	}
	cepoch, err := strconv.Atoi(check.epoch)
	if err != nil {
		return 0, fmt.Errorf("evrRpmCompare: bad check epoch")
	}
	if cepoch > aepoch {
		return 1, nil
	} else if cepoch < aepoch {
		return -1, nil
	}

	ret := evrRpmVerCmp(actual.version, check.version)
	if ret != 0 {
		return ret, nil
	}

	ret = evrRpmVerCmp(actual.release, check.release)
	if ret != 0 {
		return ret, nil
	}

	return 0, nil
}

func evrCompare(op int, actual string, check string) (bool, error) {
	debugPrint("evrCompare(): %v %v %v\n", actual, evrOperationStr(op), check)

	evract, err := evrExtract(actual)
	if err != nil {
		return false, err
	}
	evrchk, err := evrExtract(check)
	if err != nil {
		return false, err
	}

	ret, err := evrRpmCompare(evract, evrchk)
	if err != nil {
		return false, err
	}
	switch op {
	case EVROP_EQUALS:
		if ret != 0 {
			return false, nil
		}
		return true, nil
	case EVROP_LESS_THAN:
		if ret == 1 {
			return true, nil
		}
		return false, nil
	}
	return false, fmt.Errorf("evrCompare: unknown operator")
}

// An exported version of the EVR comparison operation. op is used to specify
// an EVR comparison operation (e.g., EVROP_LESS_THAN). actual and check are
// the version strings to test. Returns status of test evaluation, or an error
// if an error occurs.
func TestEvrCompare(op int, actual string, check string) (bool, error) {
	return evrCompare(op, actual, check)
}
<|endoftext|>


<commit_before>package usecases

import (
	"io/ioutil"
	"k8s.io/client-go/pkg/api"
	_ "k8s.io/client-go/pkg/api/install"
	_ "k8s.io/client-go/pkg/apis/extensions/install"
	_ "k8s.io/client-go/pkg/apis/rbac/install"
	"k8s.io/client-go/pkg/api/v1"
	"k8s.io/client-go/pkg/apis/rbac/v1beta1"
	"log"
	"os"
	"regexp"
	"k8s.io/apimachinery/pkg/runtime"
	"fmt"
	"strings"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

type CustomRolesAndBindings struct {
	Roles    				map[string]bool
	RoleBindings 			map[string]bool
	ClusterRoles 			map[string]bool
	ClusterRoleBindings 	map[string]bool
	ServiceAccounts 		map[string]bool
}

func ReadAndApplyCustomRolesAndBindings() CustomRolesAndBindings {
	res := CustomRolesAndBindings{}

	customDir := getCustomRoleDir()
	customRolesPresent, err := fileExists(customDir)
	if err != nil {
		log.Printf("An error occurred while trying to read custom roles from directory %s. Err: %s", customDir, err)
		return res
	}
	if !customRolesPresent {
		log.Println("No custom-roles directory present, skipping step...")
		return res
	}

	files, err := ioutil.ReadDir(customDir)
	if err != nil {
		log.Printf("An error occurred while trying to read custom roles from directory %s. Err: %s", customDir, err)
		return res
	}

	regExp := regexp.MustCompile(`.*(\.yml|\.yaml)`)
	k8sclient := getK8sClient()
	for _, f := range files {
		isYaml := regExp.MatchString(f.Name())

		if !f.IsDir() && isYaml {
			fileR, err := ioutil.ReadFile(f.Name())
			if err != nil {
				log.Printf("An error occurred while reading file %s from directory %s. Err: %s", f.Name(), customDir, err)
				return res
			}

			objects := parseK8sYaml(fileR)
			for _, o := range objects {
				switch o := o.(type) {
				case *v1beta1.Role:
					res.Roles[o.Name] = true
					k8sclient.RbacV1beta1().Roles(o.Namespace).Create(o)
					log.Printf("Applied Custom Role %s in Namespace %s", o.Name, o.Namespace)
				case *v1beta1.RoleBinding:
					res.RoleBindings[o.Name] = true
					k8sclient.RbacV1beta1().RoleBindings(o.Namespace).Create(o)
					log.Printf("Applied Custom RoleBinding %s in Namespace %s", o.Name, o.Namespace)
				case *v1beta1.ClusterRole:
					res.ClusterRoles[o.Name] = true
					k8sclient.RbacV1beta1().ClusterRoles().Create(o)
					log.Printf("Applied Custom ClusterRole %s", o.Name)
				case *v1beta1.ClusterRoleBinding:
					res.ClusterRoleBindings[o.Name] = true
					k8sclient.RbacV1beta1().ClusterRoleBindings().Create(o)
					log.Printf("Applied Custom ClusterRoleBinding %s", o.Name)
				case *v1.ServiceAccount:
					res.ServiceAccounts[o.Name] = true
					k8sclient.CoreV1().ServiceAccounts(o.Namespace).Create(o)
					log.Printf("Applied Custom ServiceAccount %s in Namespace %s", o.Name, o.Namespace)
				}
			}
		}
	}
	return res
}

func getK8sClient() *kubernetes.Clientset {
	// creates the in-cluster config
	config, err := rest.InClusterConfig()
	if check(err) {
		log.Fatal(err)
	}

	// creates the clientset
	clientset, err := kubernetes.NewForConfig(config)

	if check(err) {
		log.Fatal(err)
	}
	return clientset
}

func parseK8sYaml(fileR []byte) []runtime.Object {

	acceptedK8sTypes := regexp.MustCompile(`(Role|ClusterRole|RoleBinding|ClusterRoleBinding|ServiceAccount)`)
	fileAsString := string(fileR[:])
	sepYamlfiles := strings.Split(fileAsString, "---")
	retVal := make([]runtime.Object,0, len(sepYamlfiles))
	for _, f := range sepYamlfiles {
		if f == "\n" || f == "" {
			// ignore empty cases
			continue
		}

		decode := api.Codecs.UniversalDeserializer().Decode
		obj, groupVersionKind, err := decode([]byte(f), nil, nil)

		if err != nil {
			log.Println(fmt.Sprintf("Error while decoding YAML object. Err was: %s", err))
			continue
		}

		if !acceptedK8sTypes.MatchString(groupVersionKind.Kind) {
			log.Printf("The custom-roles configMap contained K8s object types which are not supported! Skipping object with type: %s", groupVersionKind.Kind)
		} else {
			retVal = append(retVal, obj)
		}

	}
	return retVal
}

func fileExists(filename string) (bool, error) {
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		return false, nil
	} else if err != nil {
		return false, err
	}
	return true, nil
}

func getCustomRoleDir() string {
	dir := "/etc/custom-roles"
	envDir := os.Getenv("CUSTOM_ROLE_DIR")
	if envDir != "" {
		dir = envDir
	}
	return dir
}
<commit_msg>fixed wrong path to custom-role files<commit_after>package usecases

import (
	"io/ioutil"
	"k8s.io/client-go/pkg/api"
	_ "k8s.io/client-go/pkg/api/install"
	_ "k8s.io/client-go/pkg/apis/extensions/install"
	_ "k8s.io/client-go/pkg/apis/rbac/install"
	"k8s.io/client-go/pkg/api/v1"
	"k8s.io/client-go/pkg/apis/rbac/v1beta1"
	"log"
	"os"
	"regexp"
	"k8s.io/apimachinery/pkg/runtime"
	"fmt"
	"strings"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

type CustomRolesAndBindings struct {
	Roles    				map[string]bool
	RoleBindings 			map[string]bool
	ClusterRoles 			map[string]bool
	ClusterRoleBindings 	map[string]bool
	ServiceAccounts 		map[string]bool
}

func ReadAndApplyCustomRolesAndBindings() CustomRolesAndBindings {
	res := CustomRolesAndBindings{}

	customDir := getCustomRoleDir()
	customRolesPresent, err := fileExists(customDir)
	if err != nil {
		log.Printf("An error occurred while trying to read custom roles from directory %s. Err: %s", customDir, err)
		return res
	}
	if !customRolesPresent {
		log.Println("No custom-roles directory present, skipping step...")
		return res
	}

	files, err := ioutil.ReadDir(customDir)
	if err != nil {
		log.Printf("An error occurred while trying to read custom roles from directory %s. Err: %s", customDir, err)
		return res
	}

	regExp := regexp.MustCompile(`.*(\.yml|\.yaml)`)
	k8sclient := getK8sClient()
	for _, f := range files {
		isYaml := regExp.MatchString(f.Name())

		if !f.IsDir() && isYaml {
			fileR, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", customDir, f.Name()))
			if err != nil {
				log.Printf("An error occurred while reading file %s from directory %s. Err: %s", f.Name(), customDir, err)
				return res
			}

			objects := parseK8sYaml(fileR)
			for _, o := range objects {
				switch o := o.(type) {
				case *v1beta1.Role:
					res.Roles[o.Name] = true
					k8sclient.RbacV1beta1().Roles(o.Namespace).Create(o)
					log.Printf("Applied Custom Role %s in Namespace %s", o.Name, o.Namespace)
				case *v1beta1.RoleBinding:
					res.RoleBindings[o.Name] = true
					k8sclient.RbacV1beta1().RoleBindings(o.Namespace).Create(o)
					log.Printf("Applied Custom RoleBinding %s in Namespace %s", o.Name, o.Namespace)
				case *v1beta1.ClusterRole:
					res.ClusterRoles[o.Name] = true
					k8sclient.RbacV1beta1().ClusterRoles().Create(o)
					log.Printf("Applied Custom ClusterRole %s", o.Name)
				case *v1beta1.ClusterRoleBinding:
					res.ClusterRoleBindings[o.Name] = true
					k8sclient.RbacV1beta1().ClusterRoleBindings().Create(o)
					log.Printf("Applied Custom ClusterRoleBinding %s", o.Name)
				case *v1.ServiceAccount:
					res.ServiceAccounts[o.Name] = true
					k8sclient.CoreV1().ServiceAccounts(o.Namespace).Create(o)
					log.Printf("Applied Custom ServiceAccount %s in Namespace %s", o.Name, o.Namespace)
				}
			}
		}
	}
	return res
}

func getK8sClient() *kubernetes.Clientset {
	// creates the in-cluster config
	config, err := rest.InClusterConfig()
	if check(err) {
		log.Fatal(err)
	}

	// creates the clientset
	clientset, err := kubernetes.NewForConfig(config)

	if check(err) {
		log.Fatal(err)
	}
	return clientset
}

func parseK8sYaml(fileR []byte) []runtime.Object {

	acceptedK8sTypes := regexp.MustCompile(`(Role|ClusterRole|RoleBinding|ClusterRoleBinding|ServiceAccount)`)
	fileAsString := string(fileR[:])
	sepYamlfiles := strings.Split(fileAsString, "---")
	retVal := make([]runtime.Object,0, len(sepYamlfiles))
	for _, f := range sepYamlfiles {
		if f == "\n" || f == "" {
			// ignore empty cases
			continue
		}

		decode := api.Codecs.UniversalDeserializer().Decode
		obj, groupVersionKind, err := decode([]byte(f), nil, nil)

		if err != nil {
			log.Println(fmt.Sprintf("Error while decoding YAML object. Err was: %s", err))
			continue
		}

		if !acceptedK8sTypes.MatchString(groupVersionKind.Kind) {
			log.Printf("The custom-roles configMap contained K8s object types which are not supported! Skipping object with type: %s", groupVersionKind.Kind)
		} else {
			retVal = append(retVal, obj)
		}

	}
	return retVal
}

func fileExists(filename string) (bool, error) {
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		return false, nil
	} else if err != nil {
		return false, err
	}
	return true, nil
}

func getCustomRoleDir() string {
	dir := "/etc/custom-roles"
	envDir := os.Getenv("CUSTOM_ROLE_DIR")
	if envDir != "" {
		dir = envDir
	}
	return dir
}
<|endoftext|>


<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor:
// - Aaron Meihm ameihm@mozilla.com

package scribe

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"unicode"
)

// This is a go version of the librpm rpmvercmp() function, that compares
// two version strings to determine which is newer. It has undergone a
// series of tests but there are likely to be some edge cases and certain
// scenarios it does not handle.

const (
	_ = iota
	EVROP_LESS_THAN
	EVROP_EQUALS
	EVROP_UNKNOWN
)

type EVR struct {
	epoch   string
	version string
	release string
}

func evrLookupOperation(s string) int {
	switch s {
	case "<":
		return EVROP_LESS_THAN
	case "=":
		return EVROP_EQUALS
	}
	return EVROP_UNKNOWN
}

func evrOperationStr(val int) string {
	switch val {
	case EVROP_LESS_THAN:
		return "<"
	case EVROP_EQUALS:
		return "="
	default:
		return "?"
	}
}

func evrIsDigit(c rune) bool {
	return unicode.IsDigit(c)
}

func evrIsNumber(s string) bool {
	_, err := strconv.Atoi(s)
	if err != nil {
		return false
	} else {
		return true
	}
}

func evrExtract(s string) (EVR, error) {
	var ret EVR
	var idx int

	for _, c := range s {
		if !evrIsDigit(c) {
			break
		}
		idx++
	}

	if idx >= len(s) {
		return ret, fmt.Errorf("evrExtract: all digits")
	}

	if s[idx] == ':' {
		ret.epoch = s[:idx]
		idx++
	} else {
		ret.epoch = "0"
		idx = 0
	}

	if idx >= len(s) {
		return ret, fmt.Errorf("evrExtract: only epoch")
	}
	remain := s[idx:]

	rp0 := strings.LastIndex(remain, "-")
	if rp0 != -1 {
		ret.version = remain[:rp0]
		rp0++
		if rp0 >= len(remain) {
			return ret, fmt.Errorf("evrExtract: ends in dash")
		}
		ret.release = remain[rp0:]
	} else {
		ret.version = remain
		ret.release = ""
	}

	debugPrint("evrExtract(): epoch=%v, version=%v, revision=%v\n", ret.epoch, ret.version, ret.release)
	return ret, nil
}

func evrRpmTokenizer(s string) []string {
	re := regexp.MustCompile("[A-Za-z0-9]+")
	buf := re.FindAllString(s, -1)
	ret := make([]string, 0)
	var isnum bool
	var cmp string
	for _, x := range buf {
		cmp = ""
		for _, c := range x {
			if len(cmp) == 0 {
				if evrIsDigit(c) {
					isnum = true
				} else {
					isnum = false
				}
				cmp += string(c)
			} else {
				if isnum {
					if !evrIsDigit(c) {
						ret = append(ret, cmp)
						cmp = string(c)
						isnum = false
					} else {
						cmp += string(c)
					}
				} else {
					if evrIsDigit(c) {
						ret = append(ret, cmp)
						cmp = string(c)
						isnum = true
					} else {
						cmp += string(c)
					}
				}
			}
		}
		ret = append(ret, cmp)
	}
	return ret
}

func evrTrimZeros(s string) string {
	if len(s) == 1 {
		return s
	}
	_, err := strconv.Atoi(s)
	if err != nil {
		return s
	}
	return strings.TrimLeft(s, "0")
}

func evrRpmVerCmp(actual string, check string) int {
	if actual == check {
		return 0
	}

	acttokens := evrRpmTokenizer(actual)
	chktokens := evrRpmTokenizer(check)

	for i := range chktokens {
		if i >= len(acttokens) {
			// There are more tokens in the check value, the
			// check wins.
			return 1
		}

		// If the values are pure numbers, trim any leading 0's.
		acttest := evrTrimZeros(acttokens[i])
		chktest := evrTrimZeros(chktokens[i])

		// Numeric component will always win out over alpha.
		if evrIsDigit(rune(acttest[0])) && !evrIsDigit(rune(chktest[0])) {
			return -1
		}
		if evrIsDigit(rune(chktest[0])) && !evrIsDigit(rune(acttest[0])) {
			return 1
		}

		// If both values are pure numeric values, convert and check here
		if evrIsNumber(acttest) && evrIsNumber(chktest) {
			na, err := strconv.Atoi(acttest)
			if err != nil {
				panic("IsNumber and failed actual conversion")
			}
			nc, err := strconv.Atoi(chktest)
			if err != nil {
				panic("IsNumber and failed check conversion")
			}
			if nc > na {
				return 1
			} else if nc < na {
				return -1
			} else {
				continue
			}
		}

		// Do a lexical string comparison here, this should work
		// even with pure integer values.
		if chktest > acttest {
			return 1
		} else if chktest < acttest {
			return -1
		}
	}

	// If we get this far, see if the actual value still has more tokens
	// for comparison, if so actual wins.
	if len(acttokens) > len(chktokens) {
		return -1
	}

	return 0
}

func evrRpmCompare(actual EVR, check EVR) (int, error) {
	aepoch, err := strconv.Atoi(actual.epoch)
	if err != nil {
		return 0, fmt.Errorf("evrRpmCompare: bad actual epoch")
	}
	cepoch, err := strconv.Atoi(check.epoch)
	if err != nil {
		return 0, fmt.Errorf("evrRpmCompare: bad check epoch")
	}
	if cepoch > aepoch {
		return 1, nil
	} else if cepoch < aepoch {
		return -1, nil
	}

	ret := evrRpmVerCmp(actual.version, check.version)
	if ret != 0 {
		return ret, nil
	}

	ret = evrRpmVerCmp(actual.release, check.release)
	if ret != 0 {
		return ret, nil
	}

	return 0, nil
}

func evrCompare(op int, actual string, check string) (bool, error) {
	debugPrint("evrCompare(): %v %v %v\n", actual, evrOperationStr(op), check)

	evract, err := evrExtract(actual)
	if err != nil {
		return false, err
	}
	evrchk, err := evrExtract(check)
	if err != nil {
		return false, err
	}

	ret, err := evrRpmCompare(evract, evrchk)
	if err != nil {
		return false, err
	}
	switch op {
	case EVROP_EQUALS:
		if ret != 0 {
			return false, nil
		}
		return true, nil
	case EVROP_LESS_THAN:
		if ret == 1 {
			return true, nil
		}
		return false, nil
	}
	return false, fmt.Errorf("evrCompare: unknown operator")
}

// An exported version of the EVR comparison operation. op is used to specify
// an EVR comparison operation (e.g., EVROP_LESS_THAN). actual and check are
// the version strings to test. Returns status of test evaluation, or an error
// if an error occurs.
func TestEvrCompare(op int, actual string, check string) (bool, error) {
	return evrCompare(op, actual, check)
}
<commit_msg>dont export EVR type<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor:
// - Aaron Meihm ameihm@mozilla.com

package scribe

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"unicode"
)

// This is a go version of the librpm rpmvercmp() function, that compares
// two version strings to determine which is newer. It has undergone a
// series of tests but there are likely to be some edge cases and certain
// scenarios it does not handle.

// EVR operation constants
const (
	_ = iota
	EVROP_LESS_THAN
	EVROP_EQUALS
	EVROP_UNKNOWN
)

type evr struct {
	epoch   string
	version string
	release string
}

func evrLookupOperation(s string) int {
	switch s {
	case "<":
		return EVROP_LESS_THAN
	case "=":
		return EVROP_EQUALS
	}
	return EVROP_UNKNOWN
}

func evrOperationStr(val int) string {
	switch val {
	case EVROP_LESS_THAN:
		return "<"
	case EVROP_EQUALS:
		return "="
	default:
		return "?"
	}
}

func evrIsDigit(c rune) bool {
	return unicode.IsDigit(c)
}

func evrIsNumber(s string) bool {
	_, err := strconv.Atoi(s)
	if err != nil {
		return false
	} else {
		return true
	}
}

func evrExtract(s string) (evr, error) {
	var ret evr
	var idx int

	for _, c := range s {
		if !evrIsDigit(c) {
			break
		}
		idx++
	}

	if idx >= len(s) {
		return ret, fmt.Errorf("evrExtract: all digits")
	}

	if s[idx] == ':' {
		ret.epoch = s[:idx]
		idx++
	} else {
		ret.epoch = "0"
		idx = 0
	}

	if idx >= len(s) {
		return ret, fmt.Errorf("evrExtract: only epoch")
	}
	remain := s[idx:]

	rp0 := strings.LastIndex(remain, "-")
	if rp0 != -1 {
		ret.version = remain[:rp0]
		rp0++
		if rp0 >= len(remain) {
			return ret, fmt.Errorf("evrExtract: ends in dash")
		}
		ret.release = remain[rp0:]
	} else {
		ret.version = remain
		ret.release = ""
	}

	debugPrint("evrExtract(): epoch=%v, version=%v, revision=%v\n", ret.epoch, ret.version, ret.release)
	return ret, nil
}

func evrRpmTokenizer(s string) []string {
	re := regexp.MustCompile("[A-Za-z0-9]+")
	buf := re.FindAllString(s, -1)
	ret := make([]string, 0)
	var isnum bool
	var cmp string
	for _, x := range buf {
		cmp = ""
		for _, c := range x {
			if len(cmp) == 0 {
				if evrIsDigit(c) {
					isnum = true
				} else {
					isnum = false
				}
				cmp += string(c)
			} else {
				if isnum {
					if !evrIsDigit(c) {
						ret = append(ret, cmp)
						cmp = string(c)
						isnum = false
					} else {
						cmp += string(c)
					}
				} else {
					if evrIsDigit(c) {
						ret = append(ret, cmp)
						cmp = string(c)
						isnum = true
					} else {
						cmp += string(c)
					}
				}
			}
		}
		ret = append(ret, cmp)
	}
	return ret
}

func evrTrimZeros(s string) string {
	if len(s) == 1 {
		return s
	}
	_, err := strconv.Atoi(s)
	if err != nil {
		return s
	}
	return strings.TrimLeft(s, "0")
}

func evrRpmVerCmp(actual string, check string) int {
	if actual == check {
		return 0
	}

	acttokens := evrRpmTokenizer(actual)
	chktokens := evrRpmTokenizer(check)

	for i := range chktokens {
		if i >= len(acttokens) {
			// There are more tokens in the check value, the
			// check wins.
			return 1
		}

		// If the values are pure numbers, trim any leading 0's.
		acttest := evrTrimZeros(acttokens[i])
		chktest := evrTrimZeros(chktokens[i])

		// Numeric component will always win out over alpha.
		if evrIsDigit(rune(acttest[0])) && !evrIsDigit(rune(chktest[0])) {
			return -1
		}
		if evrIsDigit(rune(chktest[0])) && !evrIsDigit(rune(acttest[0])) {
			return 1
		}

		// If both values are pure numeric values, convert and check here
		if evrIsNumber(acttest) && evrIsNumber(chktest) {
			na, err := strconv.Atoi(acttest)
			if err != nil {
				panic("IsNumber and failed actual conversion")
			}
			nc, err := strconv.Atoi(chktest)
			if err != nil {
				panic("IsNumber and failed check conversion")
			}
			if nc > na {
				return 1
			} else if nc < na {
				return -1
			} else {
				continue
			}
		}

		// Do a lexical string comparison here, this should work
		// even with pure integer values.
		if chktest > acttest {
			return 1
		} else if chktest < acttest {
			return -1
		}
	}

	// If we get this far, see if the actual value still has more tokens
	// for comparison, if so actual wins.
	if len(acttokens) > len(chktokens) {
		return -1
	}

	return 0
}

func evrRpmCompare(actual evr, check evr) (int, error) {
	aepoch, err := strconv.Atoi(actual.epoch)
	if err != nil {
		return 0, fmt.Errorf("evrRpmCompare: bad actual epoch")
	}
	cepoch, err := strconv.Atoi(check.epoch)
	if err != nil {
		return 0, fmt.Errorf("evrRpmCompare: bad check epoch")
	}
	if cepoch > aepoch {
		return 1, nil
	} else if cepoch < aepoch {
		return -1, nil
	}

	ret := evrRpmVerCmp(actual.version, check.version)
	if ret != 0 {
		return ret, nil
	}

	ret = evrRpmVerCmp(actual.release, check.release)
	if ret != 0 {
		return ret, nil
	}

	return 0, nil
}

func evrCompare(op int, actual string, check string) (bool, error) {
	debugPrint("evrCompare(): %v %v %v\n", actual, evrOperationStr(op), check)

	evract, err := evrExtract(actual)
	if err != nil {
		return false, err
	}
	evrchk, err := evrExtract(check)
	if err != nil {
		return false, err
	}

	ret, err := evrRpmCompare(evract, evrchk)
	if err != nil {
		return false, err
	}
	switch op {
	case EVROP_EQUALS:
		if ret != 0 {
			return false, nil
		}
		return true, nil
	case EVROP_LESS_THAN:
		if ret == 1 {
			return true, nil
		}
		return false, nil
	}
	return false, fmt.Errorf("evrCompare: unknown operator")
}

// An exported version of the EVR comparison operation. op is used to specify
// an EVR comparison operation (e.g., EVROP_LESS_THAN). actual and check are
// the version strings to test. Returns status of test evaluation, or an error
// if an error occurs.
func TestEvrCompare(op int, actual string, check string) (bool, error) {
	return evrCompare(op, actual, check)
}
<|endoftext|>


<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor:
// - Aaron Meihm ameihm@mozilla.com

package scribe

import (
	"fmt"
	"strings"
)

type TestResult struct {
	// The name of the test.
	Name string `json:"name"`
	// The identifier for the test.
	Identifier string `json:"identifier"`
	// Aliases for the test.
	Aliases []string `json:"aliases"`

	// True if error encountered during evaluation.
	IsError bool `json:"iserror"`
	// Error associated with test.
	Error string `json:"error"`

	// Master result of test.
	MasterResult bool `json:"masterresult"`
	// True if some source evaluations resulted in the test being true.
	HasTrueResults bool `json:"hastrueresults"`

	Results []TestSubResult `json:"results"`
}

type TestSubResult struct {
	// The result of evaluation for an identifier source.
	Result bool `json:"result"`
	// The identifier for the source.
	Identifier string `json:"identifier"`
}

// Return test results for a given test. Returns an error if an
// error occured during test preparation or execution.
func GetResults(d Document, name string) (TestResult, error) {
	t, err := d.getTest(name)
	if err != nil {
		return TestResult{}, err
	}
	if t.err != nil {
		return TestResult{}, t.err
	}
	ret := TestResult{}
	ret.Name = t.Name
	ret.Aliases = t.Aliases
	ret.Identifier = t.Identifier
	if t.err != nil {
		ret.Error = fmt.Sprintf("%v", t.err)
	}
	ret.MasterResult = t.masterResult
	ret.HasTrueResults = t.hasTrueResults
	for _, x := range t.results {
		nr := TestSubResult{}
		nr.Result = x.result
		nr.Identifier = x.criteria.identifier
		ret.Results = append(ret.Results, nr)
	}
	return ret, nil
}

// A helper function to convert Testresult r into greppable single line
// results.
func GrepResult(r TestResult) string {
	lns := make([]string, 0)

	rs := "[error]"
	if r.Error == "" {
		if r.MasterResult {
			rs = "[true]"
		} else {
			rs = "[false]"
		}
	}
	buf := fmt.Sprintf("master %v name:\"%v\" hastrue:%v error:\"%v\"", rs, r.Name, r.HasTrueResults, r.Error)
	lns = append(lns, buf)

	for _, x := range r.Results {
		if x.Result {
			rs = "[true]"
		} else {
			rs = "[false]"
		}
		buf := fmt.Sprintf("sub %v name:\"%v\" identifier:\"%v\"", rs, r.Name, x.Identifier)
		lns = append(lns, buf)
	}

	return strings.Join(lns, "\n") + "\n"
}

// A helper function to convert TestResult r into a human readable result
// suitable for display.
func HumanResult(r TestResult) string {
	lns := make([]string, 0)
	lns = append(lns, fmt.Sprintf("result for \"%v\"", r.Name))
	if r.MasterResult {
		lns = append(lns, "\tmaster result: true")
	} else {
		buf := "\tmaster result: false"
		if r.HasTrueResults {
			buf = buf + ", has true results, failure caused by dependency"
		}
		lns = append(lns, buf)
	}
	if r.Error != "" {
		buf := fmt.Sprintf("[error] error: %v", r.Error)
		lns = append(lns, buf)
	}
	for _, x := range r.Results {
		buf := fmt.Sprintf("\t[%v] identifier: \"%v\"", x.Result, x.Identifier)
		lns = append(lns, buf)
	}
	return strings.Join(lns, "\n") + "\n"
}
<commit_msg>document some results functions<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor:
// - Aaron Meihm ameihm@mozilla.com

package scribe

import (
	"fmt"
	"strings"
)

// Describes the results of a test. The type can be marshaled into a JSON
// string as required.
type TestResult struct {
	// The name of the test.
	Name string `json:"name"`
	// The identifier for the test.
	Identifier string `json:"identifier"`
	// Aliases for the test.
	Aliases []string `json:"aliases"`

	// True if error encountered during evaluation.
	IsError bool `json:"iserror"`
	// Error associated with test.
	Error string `json:"error"`

	// Master result of test.
	MasterResult bool `json:"masterresult"`
	// True if some source evaluations resulted in the test being true.
	HasTrueResults bool `json:"hastrueresults"`

	Results []TestSubResult `json:"results"`
}

// For a given test, a number of sources can be identified that match the
// criteria. For example, multiple files can be identifier with a given
// filename. Each test tracks individual results for these cases.
type TestSubResult struct {
	// The result of evaluation for an identifier source.
	Result bool `json:"result"`
	// The identifier for the source.
	Identifier string `json:"identifier"`
}

// Return test results for a given test. Returns an error if an
// error occured during test preparation or execution.
func GetResults(d Document, name string) (TestResult, error) {
	t, err := d.getTest(name)
	if err != nil {
		return TestResult{}, err
	}
	if t.err != nil {
		return TestResult{}, t.err
	}
	ret := TestResult{}
	ret.Name = t.Name
	ret.Aliases = t.Aliases
	ret.Identifier = t.Identifier
	if t.err != nil {
		ret.Error = fmt.Sprintf("%v", t.err)
	}
	ret.MasterResult = t.masterResult
	ret.HasTrueResults = t.hasTrueResults
	for _, x := range t.results {
		nr := TestSubResult{}
		nr.Result = x.result
		nr.Identifier = x.criteria.identifier
		ret.Results = append(ret.Results, nr)
	}
	return ret, nil
}

// A helper function to convert Testresult r into greppable single line
// results.
func GrepResult(r TestResult) string {
	lns := make([]string, 0)

	rs := "[error]"
	if r.Error == "" {
		if r.MasterResult {
			rs = "[true]"
		} else {
			rs = "[false]"
		}
	}
	buf := fmt.Sprintf("master %v name:\"%v\" hastrue:%v error:\"%v\"", rs, r.Name, r.HasTrueResults, r.Error)
	lns = append(lns, buf)

	for _, x := range r.Results {
		if x.Result {
			rs = "[true]"
		} else {
			rs = "[false]"
		}
		buf := fmt.Sprintf("sub %v name:\"%v\" identifier:\"%v\"", rs, r.Name, x.Identifier)
		lns = append(lns, buf)
	}

	return strings.Join(lns, "\n") + "\n"
}

// A helper function to convert TestResult r into a human readable result
// suitable for display.
func HumanResult(r TestResult) string {
	lns := make([]string, 0)
	lns = append(lns, fmt.Sprintf("result for \"%v\"", r.Name))
	if r.MasterResult {
		lns = append(lns, "\tmaster result: true")
	} else {
		buf := "\tmaster result: false"
		if r.HasTrueResults {
			buf = buf + ", has true results, failure caused by dependency"
		}
		lns = append(lns, buf)
	}
	if r.Error != "" {
		buf := fmt.Sprintf("[error] error: %v", r.Error)
		lns = append(lns, buf)
	}
	for _, x := range r.Results {
		buf := fmt.Sprintf("\t[%v] identifier: \"%v\"", x.Result, x.Identifier)
		lns = append(lns, buf)
	}
	return strings.Join(lns, "\n") + "\n"
}
<|endoftext|>


<commit_before>/*
Copyright 2017 Google Inc.

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

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

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

// Package vtworkerclienttest contains the testsuite against which each
// RPC implementation of the vtworkerclient interface must be tested.
package vtworkerclienttest

// NOTE: This file is not test-only code because it is referenced by
// tests in other packages and therefore it has to be regularly
// visible.

// NOTE: This code is in its own package such that its dependencies
// (e.g.  zookeeper) won't be drawn into production binaries as well.

import (
	"io"
	"strings"
	"sync"
	"testing"
	"time"

	"golang.org/x/net/context"

	"vitess.io/vitess/go/vt/logutil"
	"vitess.io/vitess/go/vt/topo/memorytopo"
	"vitess.io/vitess/go/vt/vterrors"
	"vitess.io/vitess/go/vt/vttablet/tmclient"
	"vitess.io/vitess/go/vt/worker"
	"vitess.io/vitess/go/vt/worker/vtworkerclient"

	// Import the gRPC client implementation for tablet manager because the real
	// vtworker implementation requires it.
	_ "vitess.io/vitess/go/vt/vttablet/grpctmclient"

	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
)

func init() {
	// enforce we will use the right protocol (gRPC) (note the
	// client is unused, but it is initialized, so it needs to exist)
	*tmclient.TabletManagerProtocol = "grpc"
}

// CreateWorkerInstance returns a properly configured vtworker instance.
func CreateWorkerInstance(t *testing.T) *worker.Instance {
	ts := memorytopo.NewServer("cell1", "cell2")
	return worker.NewInstance(ts, "cell1", 1*time.Second)
}

// TestSuite runs the test suite on the given vtworker and vtworkerclient.
func TestSuite(t *testing.T, c vtworkerclient.Client) {
	commandSucceeds(t, c)

	commandErrors(t, c)

	commandErrorsBecauseBusy(t, c, false /* client side cancelation */)

	commandErrorsBecauseBusy(t, c, true /* server side cancelation */)

	commandPanics(t, c)
}

func commandSucceeds(t *testing.T, client vtworkerclient.Client) {
	stream, err := client.ExecuteVtworkerCommand(context.Background(), []string{"Ping", "pong"})
	if err != nil {
		t.Fatalf("Cannot execute remote command: %v", err)
	}

	got, err := stream.Recv()
	if err != nil {
		t.Fatalf("failed to get first line: %v", err)
	}
	expected := "Ping command was called with message: 'pong'.\n"
	if logutil.EventString(got) != expected {
		t.Errorf("Got unexpected log line '%v' expected '%v'", got.String(), expected)
	}
	_, err = stream.Recv()
	if err != io.EOF {
		t.Fatalf("Didn't get EOF as expected: %v", err)
	}

	// Reset vtworker for the next test function.
	if err := runVtworkerCommand(client, []string{"Reset"}); err != nil {
		t.Fatal(err)
	}
}

func runVtworkerCommand(client vtworkerclient.Client, args []string) error {
	stream, err := client.ExecuteVtworkerCommand(context.Background(), args)
	if err != nil {
		return vterrors.Wrap(err, "cannot execute remote command")
	}

	for {
		_, err := stream.Recv()
		switch err {
		case nil:
			// Consume next response.
		case io.EOF:
			return nil
		default:
			return vterrors.Wrap(err, "unexpected error when reading the stream")
		}
	}
}

// commandErrorsBecauseBusy tests that concurrent commands are rejected with
// TRANSIENT_ERROR while a command is already running.
// It also tests the correct propagation of the CANCELED error code.
func commandErrorsBecauseBusy(t *testing.T, client vtworkerclient.Client, serverSideCancelation bool) {
	// Run the vtworker "Block" command which blocks until we cancel the context.
	var wg sync.WaitGroup
	ctx, cancel := context.WithCancel(context.Background())
	// blockCommandStarted will be closed after we're sure that vtworker is
	// running the "Block" command.
	blockCommandStarted := make(chan struct{})
	var errorCodeCheck error
	wg.Add(1)
	errChan := make(chan error, 1)
	defer close(errChan)
	go func() {
		defer wg.Done()
		stream, err := client.ExecuteVtworkerCommand(ctx, []string{"Block"})
		if err != nil {
			errChan <- err
			close(blockCommandStarted)
			return
		}

		firstLineReceived := false
		for {
			if _, err := stream.Recv(); err != nil {
				// We see CANCELED from the RPC client (client side cancelation) or
				// from vtworker itself (server side cancelation).
				if vterrors.Code(err) != vtrpcpb.Code_CANCELED {
					errorCodeCheck = vterrors.Wrap(err, "Block command should only error due to canceled context")
				}
				// Stream has finished.
				break
			}

			if !firstLineReceived {
				firstLineReceived = true
				// The first log line will come from the "Block" command, so we are sure
				// now that vtworker is actually executing it.
				close(blockCommandStarted)
			}
		}
	}()

	// Try to run a second, concurrent vtworker command.
	// vtworker should send an error back that it's busy and we should retry later.
	<-blockCommandStarted
	gotErr := runVtworkerCommand(client, []string{"Ping", "Are you busy?"})
	wantCode := vtrpcpb.Code_UNAVAILABLE
	if gotCode := vterrors.Code(gotErr); gotCode != wantCode {
		t.Fatalf("wrong error code for second cmd: got = %v, want = %v, err: %v", gotCode, wantCode, gotErr)
	}

	// Cancel running "Block" command.
	if serverSideCancelation {
		if err := runVtworkerCommand(client, []string{"Cancel"}); err != nil {
			t.Fatal(err)
		}
	}
	// Always cancel the context to not leak it (regardless of client or server
	// side cancelation).
	cancel()

	wg.Wait()
	if err := <-errChan; err != nil {
		t.Fatalf("Block command should not have failed: %v", err)
	}
	if errorCodeCheck != nil {
		t.Fatalf("Block command did not return the CANCELED error code: %v", errorCodeCheck)
	}

	// vtworker is now in a special state where the current job is already
	// canceled but not reset yet. New commands are still failing with a
	// retryable error.
	gotErr2 := runVtworkerCommand(client, []string{"Ping", "canceled and still busy?"})
	wantCode2 := vtrpcpb.Code_UNAVAILABLE
	if gotCode2 := vterrors.Code(gotErr2); gotCode2 != wantCode2 {
		t.Fatalf("wrong error code for second cmd before reset: got = %v, want = %v, err: %v", gotCode2, wantCode2, gotErr2)
	}

	// Reset vtworker for the next test function.
	if err := resetVtworker(t, client); err != nil {
		t.Fatal(err)
	}

	// Second vtworker command should succeed now after the first has finished.
	if err := runVtworkerCommand(client, []string{"Ping", "You should not be busy anymore!"}); err != nil {
		t.Fatalf("second cmd should not have failed: %v", err)
	}

	// Reset vtworker for the next test function.
	if err := runVtworkerCommand(client, []string{"Reset"}); err != nil {
		t.Fatal(err)
	}
}

// resetVtworker will retry to "Reset" vtworker until it succeeds.
// Retries are necessary to cope with the following race:
// a) RPC started vtworker command e.g. "Block".
// b) client cancels RPC and triggers vtworker to cancel the running command.
// c) RPC returns with a response after cancelation was received by vtworker.
// d) vtworker is still canceling and shutting down the command.
// e) A new vtworker command e.g. "Reset" would fail at this point with
// "vtworker still executing" until the cancelation is complete.
func resetVtworker(t *testing.T, client vtworkerclient.Client) error {
	start := time.Now()
	attempts := 0
	for {
		attempts++
		err := runVtworkerCommand(client, []string{"Reset"})

		if err == nil {
			return nil
		}

		if time.Since(start) > 5*time.Second {
			return vterrors.Wrapf(err, "Reset was not successful after 5s and %d attempts", attempts)
		}

		if !strings.Contains(err.Error(), "worker still executing") {
			return vterrors.Wrap(err, "Reset must not fail")
		}

		t.Logf("retrying to Reset vtworker because the previous command has not finished yet. got err: %v", err)
		continue
	}
}

func commandErrors(t *testing.T, client vtworkerclient.Client) {
	stream, err := client.ExecuteVtworkerCommand(context.Background(), []string{"NonexistingCommand"})
	// The expected error could already be seen now or after the output channel is closed.
	// To avoid checking for the same error twice, we don't check it here yet.

	if err == nil {
		// Don't check for errors until the output channel is closed.
		// We expect the usage to be sent as output. However, we have to consider it
		// optional and do not test for it because not all RPC implementations send
		// the output after an error.
		for {
			_, err = stream.Recv()
			if err != nil {
				break
			}
		}
	}

	expected := "unknown command: NonexistingCommand"
	if err == nil || !strings.Contains(err.Error(), expected) {
		t.Fatalf("Unexpected remote error, got: '%v' was expecting to find '%v'", err, expected)
	}
}

func commandPanics(t *testing.T, client vtworkerclient.Client) {
	stream, err := client.ExecuteVtworkerCommand(context.Background(), []string{"Panic"})
	// The expected error could already be seen now or after the output channel is closed.
	// To avoid checking for the same error twice, we don't check it here yet.

	if err == nil {
		// Don't check for errors until the output channel is closed.
		// No output expected in this case.
		_, err = stream.Recv()
	}

	expected := "uncaught vtworker panic: Panic command was called. This should be caught by the vtworker framework and logged as an error."
	if err == nil || !strings.Contains(err.Error(), expected) {
		t.Fatalf("Unexpected remote error, got: '%v' was expecting to find '%v'", err, expected)
	}
}
<commit_msg>send to errchan anyway to cancel race<commit_after>/*
Copyright 2017 Google Inc.

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

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

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

// Package vtworkerclienttest contains the testsuite against which each
// RPC implementation of the vtworkerclient interface must be tested.
package vtworkerclienttest

// NOTE: This file is not test-only code because it is referenced by
// tests in other packages and therefore it has to be regularly
// visible.

// NOTE: This code is in its own package such that its dependencies
// (e.g.  zookeeper) won't be drawn into production binaries as well.

import (
	"io"
	"strings"
	"sync"
	"testing"
	"time"

	"golang.org/x/net/context"

	"vitess.io/vitess/go/vt/logutil"
	"vitess.io/vitess/go/vt/topo/memorytopo"
	"vitess.io/vitess/go/vt/vterrors"
	"vitess.io/vitess/go/vt/vttablet/tmclient"
	"vitess.io/vitess/go/vt/worker"
	"vitess.io/vitess/go/vt/worker/vtworkerclient"

	// Import the gRPC client implementation for tablet manager because the real
	// vtworker implementation requires it.
	_ "vitess.io/vitess/go/vt/vttablet/grpctmclient"

	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
)

func init() {
	// enforce we will use the right protocol (gRPC) (note the
	// client is unused, but it is initialized, so it needs to exist)
	*tmclient.TabletManagerProtocol = "grpc"
}

// CreateWorkerInstance returns a properly configured vtworker instance.
func CreateWorkerInstance(t *testing.T) *worker.Instance {
	ts := memorytopo.NewServer("cell1", "cell2")
	return worker.NewInstance(ts, "cell1", 1*time.Second)
}

// TestSuite runs the test suite on the given vtworker and vtworkerclient.
func TestSuite(t *testing.T, c vtworkerclient.Client) {
	commandSucceeds(t, c)

	commandErrors(t, c)

	commandErrorsBecauseBusy(t, c, false /* client side cancelation */)

	commandErrorsBecauseBusy(t, c, true /* server side cancelation */)

	commandPanics(t, c)
}

func commandSucceeds(t *testing.T, client vtworkerclient.Client) {
	stream, err := client.ExecuteVtworkerCommand(context.Background(), []string{"Ping", "pong"})
	if err != nil {
		t.Fatalf("Cannot execute remote command: %v", err)
	}

	got, err := stream.Recv()
	if err != nil {
		t.Fatalf("failed to get first line: %v", err)
	}
	expected := "Ping command was called with message: 'pong'.\n"
	if logutil.EventString(got) != expected {
		t.Errorf("Got unexpected log line '%v' expected '%v'", got.String(), expected)
	}
	_, err = stream.Recv()
	if err != io.EOF {
		t.Fatalf("Didn't get EOF as expected: %v", err)
	}

	// Reset vtworker for the next test function.
	if err := runVtworkerCommand(client, []string{"Reset"}); err != nil {
		t.Fatal(err)
	}
}

func runVtworkerCommand(client vtworkerclient.Client, args []string) error {
	stream, err := client.ExecuteVtworkerCommand(context.Background(), args)
	if err != nil {
		return vterrors.Wrap(err, "cannot execute remote command")
	}

	for {
		_, err := stream.Recv()
		switch err {
		case nil:
			// Consume next response.
		case io.EOF:
			return nil
		default:
			return vterrors.Wrap(err, "unexpected error when reading the stream")
		}
	}
}

// commandErrorsBecauseBusy tests that concurrent commands are rejected with
// TRANSIENT_ERROR while a command is already running.
// It also tests the correct propagation of the CANCELED error code.
func commandErrorsBecauseBusy(t *testing.T, client vtworkerclient.Client, serverSideCancelation bool) {
	// Run the vtworker "Block" command which blocks until we cancel the context.
	var wg sync.WaitGroup
	ctx, cancel := context.WithCancel(context.Background())
	// blockCommandStarted will be closed after we're sure that vtworker is
	// running the "Block" command.
	blockCommandStarted := make(chan struct{})
	var errorCodeCheck error
	wg.Add(1)
	errChan := make(chan error, 1)
	defer close(errChan)
	go func() {
		defer wg.Done()
		stream, err := client.ExecuteVtworkerCommand(ctx, []string{"Block"})
		if err != nil {
			errChan <- err
			close(blockCommandStarted)
			return
		} else {
			errChan <- nil
		}

		firstLineReceived := false
		for {
			if _, err := stream.Recv(); err != nil {
				// We see CANCELED from the RPC client (client side cancelation) or
				// from vtworker itself (server side cancelation).
				if vterrors.Code(err) != vtrpcpb.Code_CANCELED {
					errorCodeCheck = vterrors.Wrap(err, "Block command should only error due to canceled context")
				}
				// Stream has finished.
				break
			}

			if !firstLineReceived {
				firstLineReceived = true
				// The first log line will come from the "Block" command, so we are sure
				// now that vtworker is actually executing it.
				close(blockCommandStarted)
			}
		}
	}()

	// Try to run a second, concurrent vtworker command.
	// vtworker should send an error back that it's busy and we should retry later.
	<-blockCommandStarted
	gotErr := runVtworkerCommand(client, []string{"Ping", "Are you busy?"})
	wantCode := vtrpcpb.Code_UNAVAILABLE
	if gotCode := vterrors.Code(gotErr); gotCode != wantCode {
		t.Fatalf("wrong error code for second cmd: got = %v, want = %v, err: %v", gotCode, wantCode, gotErr)
	}

	// Cancel running "Block" command.
	if serverSideCancelation {
		if err := runVtworkerCommand(client, []string{"Cancel"}); err != nil {
			t.Fatal(err)
		}
	}
	// Always cancel the context to not leak it (regardless of client or server
	// side cancelation).
	cancel()

	wg.Wait()
	if err := <-errChan; err != nil {
		t.Fatalf("Block command should not have failed: %v", err)
	}
	if errorCodeCheck != nil {
		t.Fatalf("Block command did not return the CANCELED error code: %v", errorCodeCheck)
	}

	// vtworker is now in a special state where the current job is already
	// canceled but not reset yet. New commands are still failing with a
	// retryable error.
	gotErr2 := runVtworkerCommand(client, []string{"Ping", "canceled and still busy?"})
	wantCode2 := vtrpcpb.Code_UNAVAILABLE
	if gotCode2 := vterrors.Code(gotErr2); gotCode2 != wantCode2 {
		t.Fatalf("wrong error code for second cmd before reset: got = %v, want = %v, err: %v", gotCode2, wantCode2, gotErr2)
	}

	// Reset vtworker for the next test function.
	if err := resetVtworker(t, client); err != nil {
		t.Fatal(err)
	}

	// Second vtworker command should succeed now after the first has finished.
	if err := runVtworkerCommand(client, []string{"Ping", "You should not be busy anymore!"}); err != nil {
		t.Fatalf("second cmd should not have failed: %v", err)
	}

	// Reset vtworker for the next test function.
	if err := runVtworkerCommand(client, []string{"Reset"}); err != nil {
		t.Fatal(err)
	}
}

// resetVtworker will retry to "Reset" vtworker until it succeeds.
// Retries are necessary to cope with the following race:
// a) RPC started vtworker command e.g. "Block".
// b) client cancels RPC and triggers vtworker to cancel the running command.
// c) RPC returns with a response after cancelation was received by vtworker.
// d) vtworker is still canceling and shutting down the command.
// e) A new vtworker command e.g. "Reset" would fail at this point with
// "vtworker still executing" until the cancelation is complete.
func resetVtworker(t *testing.T, client vtworkerclient.Client) error {
	start := time.Now()
	attempts := 0
	for {
		attempts++
		err := runVtworkerCommand(client, []string{"Reset"})

		if err == nil {
			return nil
		}

		if time.Since(start) > 5*time.Second {
			return vterrors.Wrapf(err, "Reset was not successful after 5s and %d attempts", attempts)
		}

		if !strings.Contains(err.Error(), "worker still executing") {
			return vterrors.Wrap(err, "Reset must not fail")
		}

		t.Logf("retrying to Reset vtworker because the previous command has not finished yet. got err: %v", err)
		continue
	}
}

func commandErrors(t *testing.T, client vtworkerclient.Client) {
	stream, err := client.ExecuteVtworkerCommand(context.Background(), []string{"NonexistingCommand"})
	// The expected error could already be seen now or after the output channel is closed.
	// To avoid checking for the same error twice, we don't check it here yet.

	if err == nil {
		// Don't check for errors until the output channel is closed.
		// We expect the usage to be sent as output. However, we have to consider it
		// optional and do not test for it because not all RPC implementations send
		// the output after an error.
		for {
			_, err = stream.Recv()
			if err != nil {
				break
			}
		}
	}

	expected := "unknown command: NonexistingCommand"
	if err == nil || !strings.Contains(err.Error(), expected) {
		t.Fatalf("Unexpected remote error, got: '%v' was expecting to find '%v'", err, expected)
	}
}

func commandPanics(t *testing.T, client vtworkerclient.Client) {
	stream, err := client.ExecuteVtworkerCommand(context.Background(), []string{"Panic"})
	// The expected error could already be seen now or after the output channel is closed.
	// To avoid checking for the same error twice, we don't check it here yet.

	if err == nil {
		// Don't check for errors until the output channel is closed.
		// No output expected in this case.
		_, err = stream.Recv()
	}

	expected := "uncaught vtworker panic: Panic command was called. This should be caught by the vtworker framework and logged as an error."
	if err == nil || !strings.Contains(err.Error(), expected) {
		t.Fatalf("Unexpected remote error, got: '%v' was expecting to find '%v'", err, expected)
	}
}
<|endoftext|>


<commit_before>//
// DISCLAIMER
//
// Copyright 2017 ArangoDB GmbH, Cologne, Germany
//
// 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.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//

package service

//
// Arangod options are configured in the following places:
// - arangod.conf:
//     This holds all settings that are considered static for the lifetime of the cluster.
//     Using new/different settings on the Starter will not change these settings.
// - arangod commandline:
//     This holds all settings that can change over the lifetime of the cluster.
//     These settings mostly involve the cluster layout.
//     Using new/different settings on the Starter will change these settings.
//     Passthrough options are always added here.
//

import (
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/rs/zerolog"
)

var (
	urlFixer = strings.NewReplacer(
		"http://", "tcp://",
		"https://", "ssl://",
	)
)

// FixupEndpointURLScheme changes endpoint URL schemes used by arangod to ones used by go.
// E.g. "http://localhost:8529" -> "tcp://localhost:8529"
func FixupEndpointURLScheme(u string) string {
	return urlFixer.Replace(u)
}

// createArangodConf creates an arangod.conf file in the given host directory if it does not yet exists.
// The arangod.conf file contains all settings that are considered static for the lifetime of the server.
func createArangodConf(log zerolog.Logger, bsCfg BootstrapConfig, myHostDir, myContainerDir, myPort string, serverType ServerType, features DatabaseFeatures) ([]Volume, configFile, error) {
	hostConfFileName := filepath.Join(myHostDir, arangodConfFileName)
	containerConfFileName := filepath.Join(myContainerDir, arangodConfFileName)
	volumes := addVolume(nil, hostConfFileName, containerConfFileName, true)

	if _, err := os.Stat(hostConfFileName); err == nil {
		// Arangod.conf already exists
		// Read config file
		if cfg, err := readConfigFile(hostConfFileName); err != nil {
			return nil, nil, maskAny(err)
		} else {
			return volumes, cfg, nil
		}
	}

	// Arangod.conf does not exist. Create it.
	logLevel := "INFO"
	listenAddr := "[::]"
	if bsCfg.DisableIPv6 {
		listenAddr = "0.0.0.0"
	}
	scheme := NewURLSchemes(bsCfg.SslKeyFile != "").Arangod
	serverSection := &configSection{
		Name: "server",
		Settings: map[string]string{
			"endpoint":       fmt.Sprintf("%s://%s:%s", scheme, listenAddr, myPort),
			"authentication": "false",
		},
	}
	if bsCfg.JwtSecret != "" {
		serverSection.Settings["authentication"] = "true"
		serverSection.Settings["jwt-secret"] = bsCfg.JwtSecret
	}
	if features.HasStorageEngineOption() {
		serverSection.Settings["storage-engine"] = bsCfg.ServerStorageEngine
	}
	config := configFile{
		serverSection,
		&configSection{
			Name: "log",
			Settings: map[string]string{
				"level": logLevel,
			},
		},
	}
	if bsCfg.SslKeyFile != "" {
		sslSection := &configSection{
			Name: "ssl",
			Settings: map[string]string{
				"keyfile": bsCfg.SslKeyFile,
			},
		}
		if bsCfg.SslCAFile != "" {
			sslSection.Settings["cafile"] = bsCfg.SslCAFile
		}
		config = append(config, sslSection)
	}
	if bsCfg.RocksDBEncryptionKeyFile != "" {
		rocksdbSection := &configSection{
			Name: "rocksdb",
			Settings: map[string]string{
				"encryption-keyfile": bsCfg.RocksDBEncryptionKeyFile,
			},
		}
		config = append(config, rocksdbSection)
	}

	out, err := os.Create(hostConfFileName)
	if err != nil {
		log.Fatal().Err(err).Msgf("Could not create configuration file %s", hostConfFileName)
		return nil, nil, maskAny(err)
	}
	defer out.Close()
	if _, err := config.WriteTo(out); err != nil {
		log.Fatal().Err(err).Msg("Cannot create config file")
		return nil, nil, maskAny(err)
	}

	return volumes, config, nil
}

// createArangodArgs returns the command line arguments needed to run an arangod server of given type.
func createArangodArgs(log zerolog.Logger, config Config, clusterConfig ClusterConfig, myContainerDir, myContainerLogFile string,
	myPeerID, myAddress, myPort string, serverType ServerType, arangodConfig configFile, agentRecoveryID string, databaseAutoUpgrade bool) []string {
	containerConfFileName := filepath.Join(myContainerDir, arangodConfFileName)

	args := make([]string, 0, 40)
	options := make([]optionPair, 0, 32)
	executable := config.ArangodPath
	jsStartup := config.ArangodJSPath
	if config.RrPath != "" {
		args = append(args, config.RrPath)
	}
	args = append(args,
		executable,
		"-c", slasher(containerConfFileName),
	)

	options = append(options,
		optionPair{"--database.directory", slasher(filepath.Join(myContainerDir, "data"))},
		optionPair{"--javascript.startup-directory", slasher(jsStartup)},
		optionPair{"--javascript.app-path", slasher(filepath.Join(myContainerDir, "apps"))},
		optionPair{"--log.file", slasher(myContainerLogFile)},
		optionPair{"--log.force-direct", "false"},
	)
	if databaseAutoUpgrade {
		options = append(options,
			optionPair{"--database.auto-upgrade", "true"})
	}
	if config.ServerThreads != 0 {
		options = append(options,
			optionPair{"--server.threads", strconv.Itoa(config.ServerThreads)})
	}
	if config.DebugCluster {
		options = append(options,
			optionPair{"--log.level", "startup=trace"})
	}
	scheme := NewURLSchemes(clusterConfig.IsSecure()).Arangod
	myTCPURL := scheme + "://" + net.JoinHostPort(myAddress, myPort)
	switch serverType {
	case ServerTypeAgent:
		options = append(options,
			optionPair{"--agency.activate", "true"},
			optionPair{"--agency.my-address", myTCPURL},
			optionPair{"--agency.size", strconv.Itoa(clusterConfig.AgencySize)},
			optionPair{"--agency.supervision", "true"},
			optionPair{"--foxx.queues", "false"},
			optionPair{"--server.statistics", "false"},
		)
		for _, p := range clusterConfig.AllAgents() {
			if p.ID != myPeerID {
				options = append(options,
					optionPair{"--agency.endpoint", fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(p.Address, strconv.Itoa(p.Port+p.PortOffset+_portOffsetAgent)))},
				)
			}
		}
		if agentRecoveryID != "" {
			options = append(options,
				optionPair{"--agency.disaster-recovery-id", agentRecoveryID},
			)
		}
	case ServerTypeDBServer:
		options = append(options,
			optionPair{"--cluster.my-address", myTCPURL},
			optionPair{"--cluster.my-role", "PRIMARY"},
			optionPair{"--foxx.queues", "false"},
			optionPair{"--server.statistics", "true"},
		)
	case ServerTypeCoordinator:
		options = append(options,
			optionPair{"--cluster.my-address", myTCPURL},
			optionPair{"--cluster.my-role", "COORDINATOR"},
			optionPair{"--foxx.queues", "true"},
			optionPair{"--server.statistics", "true"},
		)
		if config.AdvertisedEndpoint != "" {
			options = append(options,
				optionPair{"--cluster.my-advertised-endpoint", FixupEndpointURLScheme(config.AdvertisedEndpoint)},
			)
		}
	case ServerTypeSingle:
		options = append(options,
			optionPair{"--foxx.queues", "true"},
			optionPair{"--server.statistics", "true"},
		)
	case ServerTypeResilientSingle:
		options = append(options,
			optionPair{"--foxx.queues", "true"},
			optionPair{"--server.statistics", "true"},
			optionPair{"--replication.automatic-failover", "true"},
			optionPair{"--cluster.my-address", myTCPURL},
			optionPair{"--cluster.my-role", "SINGLE"},
		)
		if config.AdvertisedEndpoint != "" {
			options = append(options,
				optionPair{"--cluster.my-advertised-endpoint", FixupEndpointURLScheme(config.AdvertisedEndpoint)},
			)
		}
	}
	if serverType != ServerTypeAgent && serverType != ServerTypeSingle {
		for _, p := range clusterConfig.AllAgents() {
			options = append(options,
				optionPair{"--cluster.agency-endpoint",
					fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(p.Address, strconv.Itoa(p.Port+p.PortOffset+_portOffsetAgent)))},
			)
		}
	}
	for _, opt := range options {
		ptValues := config.passthroughOptionValuesForServerType(strings.TrimPrefix(opt.Key, "--"), serverType)
		if len(ptValues) > 0 {
			log.Warn().Msgf("Pass through option %s conflicts with automatically generated option with value '%s'", opt.Key, opt.Value)
		} else {
			args = append(args, opt.Key, opt.Value)
		}
	}
	for _, ptOpt := range config.PassthroughOptions {
		values := ptOpt.valueForServerType(serverType)
		if len(values) == 0 {
			continue
		}
		// Look for overrides of configuration sections
		if section := arangodConfig.FindSection(ptOpt.sectionName()); section != nil {
			if confValue, found := section.Settings[ptOpt.sectionKey()]; found {
				log.Warn().Msgf("Pass through option %s overrides generated configuration option with value '%s'", ptOpt.Name, confValue)
			}
		}
		// Append all values
		for _, value := range values {
			args = append(args, ptOpt.FormattedOptionName(), value)
		}
	}
	return args
}
<commit_msg>touch up comment<commit_after>//
// DISCLAIMER
//
// Copyright 2017 ArangoDB GmbH, Cologne, Germany
//
// 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.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//

package service

//
// Arangod options are configured in the following places:
// - arangod.conf:
//     This holds all settings that are considered static for the lifetime of the cluster.
//     Using new/different settings on the Starter will not change these settings.
// - arangod commandline:
//     This holds all settings that can change over the lifetime of the cluster.
//     These settings mostly involve the cluster layout.
//     Using new/different settings on the Starter will change these settings.
//     Passthrough options are always added here.
//

import (
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/rs/zerolog"
)

var (
	urlFixer = strings.NewReplacer(
		"http://", "tcp://",
		"https://", "ssl://",
	)
)

// FixupEndpointURLScheme changes endpoint URL schemes used by arangod to ones used by arangodb.
// E.g. "http://localhost:8529" -> "tcp://localhost:8529"
func FixupEndpointURLScheme(u string) string {
	return urlFixer.Replace(u)
}

// createArangodConf creates an arangod.conf file in the given host directory if it does not yet exists.
// The arangod.conf file contains all settings that are considered static for the lifetime of the server.
func createArangodConf(log zerolog.Logger, bsCfg BootstrapConfig, myHostDir, myContainerDir, myPort string, serverType ServerType, features DatabaseFeatures) ([]Volume, configFile, error) {
	hostConfFileName := filepath.Join(myHostDir, arangodConfFileName)
	containerConfFileName := filepath.Join(myContainerDir, arangodConfFileName)
	volumes := addVolume(nil, hostConfFileName, containerConfFileName, true)

	if _, err := os.Stat(hostConfFileName); err == nil {
		// Arangod.conf already exists
		// Read config file
		if cfg, err := readConfigFile(hostConfFileName); err != nil {
			return nil, nil, maskAny(err)
		} else {
			return volumes, cfg, nil
		}
	}

	// Arangod.conf does not exist. Create it.
	logLevel := "INFO"
	listenAddr := "[::]"
	if bsCfg.DisableIPv6 {
		listenAddr = "0.0.0.0"
	}
	scheme := NewURLSchemes(bsCfg.SslKeyFile != "").Arangod
	serverSection := &configSection{
		Name: "server",
		Settings: map[string]string{
			"endpoint":       fmt.Sprintf("%s://%s:%s", scheme, listenAddr, myPort),
			"authentication": "false",
		},
	}
	if bsCfg.JwtSecret != "" {
		serverSection.Settings["authentication"] = "true"
		serverSection.Settings["jwt-secret"] = bsCfg.JwtSecret
	}
	if features.HasStorageEngineOption() {
		serverSection.Settings["storage-engine"] = bsCfg.ServerStorageEngine
	}
	config := configFile{
		serverSection,
		&configSection{
			Name: "log",
			Settings: map[string]string{
				"level": logLevel,
			},
		},
	}
	if bsCfg.SslKeyFile != "" {
		sslSection := &configSection{
			Name: "ssl",
			Settings: map[string]string{
				"keyfile": bsCfg.SslKeyFile,
			},
		}
		if bsCfg.SslCAFile != "" {
			sslSection.Settings["cafile"] = bsCfg.SslCAFile
		}
		config = append(config, sslSection)
	}
	if bsCfg.RocksDBEncryptionKeyFile != "" {
		rocksdbSection := &configSection{
			Name: "rocksdb",
			Settings: map[string]string{
				"encryption-keyfile": bsCfg.RocksDBEncryptionKeyFile,
			},
		}
		config = append(config, rocksdbSection)
	}

	out, err := os.Create(hostConfFileName)
	if err != nil {
		log.Fatal().Err(err).Msgf("Could not create configuration file %s", hostConfFileName)
		return nil, nil, maskAny(err)
	}
	defer out.Close()
	if _, err := config.WriteTo(out); err != nil {
		log.Fatal().Err(err).Msg("Cannot create config file")
		return nil, nil, maskAny(err)
	}

	return volumes, config, nil
}

// createArangodArgs returns the command line arguments needed to run an arangod server of given type.
func createArangodArgs(log zerolog.Logger, config Config, clusterConfig ClusterConfig, myContainerDir, myContainerLogFile string,
	myPeerID, myAddress, myPort string, serverType ServerType, arangodConfig configFile, agentRecoveryID string, databaseAutoUpgrade bool) []string {
	containerConfFileName := filepath.Join(myContainerDir, arangodConfFileName)

	args := make([]string, 0, 40)
	options := make([]optionPair, 0, 32)
	executable := config.ArangodPath
	jsStartup := config.ArangodJSPath
	if config.RrPath != "" {
		args = append(args, config.RrPath)
	}
	args = append(args,
		executable,
		"-c", slasher(containerConfFileName),
	)

	options = append(options,
		optionPair{"--database.directory", slasher(filepath.Join(myContainerDir, "data"))},
		optionPair{"--javascript.startup-directory", slasher(jsStartup)},
		optionPair{"--javascript.app-path", slasher(filepath.Join(myContainerDir, "apps"))},
		optionPair{"--log.file", slasher(myContainerLogFile)},
		optionPair{"--log.force-direct", "false"},
	)
	if databaseAutoUpgrade {
		options = append(options,
			optionPair{"--database.auto-upgrade", "true"})
	}
	if config.ServerThreads != 0 {
		options = append(options,
			optionPair{"--server.threads", strconv.Itoa(config.ServerThreads)})
	}
	if config.DebugCluster {
		options = append(options,
			optionPair{"--log.level", "startup=trace"})
	}
	scheme := NewURLSchemes(clusterConfig.IsSecure()).Arangod
	myTCPURL := scheme + "://" + net.JoinHostPort(myAddress, myPort)
	switch serverType {
	case ServerTypeAgent:
		options = append(options,
			optionPair{"--agency.activate", "true"},
			optionPair{"--agency.my-address", myTCPURL},
			optionPair{"--agency.size", strconv.Itoa(clusterConfig.AgencySize)},
			optionPair{"--agency.supervision", "true"},
			optionPair{"--foxx.queues", "false"},
			optionPair{"--server.statistics", "false"},
		)
		for _, p := range clusterConfig.AllAgents() {
			if p.ID != myPeerID {
				options = append(options,
					optionPair{"--agency.endpoint", fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(p.Address, strconv.Itoa(p.Port+p.PortOffset+_portOffsetAgent)))},
				)
			}
		}
		if agentRecoveryID != "" {
			options = append(options,
				optionPair{"--agency.disaster-recovery-id", agentRecoveryID},
			)
		}
	case ServerTypeDBServer:
		options = append(options,
			optionPair{"--cluster.my-address", myTCPURL},
			optionPair{"--cluster.my-role", "PRIMARY"},
			optionPair{"--foxx.queues", "false"},
			optionPair{"--server.statistics", "true"},
		)
	case ServerTypeCoordinator:
		options = append(options,
			optionPair{"--cluster.my-address", myTCPURL},
			optionPair{"--cluster.my-role", "COORDINATOR"},
			optionPair{"--foxx.queues", "true"},
			optionPair{"--server.statistics", "true"},
		)
		if config.AdvertisedEndpoint != "" {
			options = append(options,
				optionPair{"--cluster.my-advertised-endpoint", FixupEndpointURLScheme(config.AdvertisedEndpoint)},
			)
		}
	case ServerTypeSingle:
		options = append(options,
			optionPair{"--foxx.queues", "true"},
			optionPair{"--server.statistics", "true"},
		)
	case ServerTypeResilientSingle:
		options = append(options,
			optionPair{"--foxx.queues", "true"},
			optionPair{"--server.statistics", "true"},
			optionPair{"--replication.automatic-failover", "true"},
			optionPair{"--cluster.my-address", myTCPURL},
			optionPair{"--cluster.my-role", "SINGLE"},
		)
		if config.AdvertisedEndpoint != "" {
			options = append(options,
				optionPair{"--cluster.my-advertised-endpoint", FixupEndpointURLScheme(config.AdvertisedEndpoint)},
			)
		}
	}
	if serverType != ServerTypeAgent && serverType != ServerTypeSingle {
		for _, p := range clusterConfig.AllAgents() {
			options = append(options,
				optionPair{"--cluster.agency-endpoint",
					fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(p.Address, strconv.Itoa(p.Port+p.PortOffset+_portOffsetAgent)))},
			)
		}
	}
	for _, opt := range options {
		ptValues := config.passthroughOptionValuesForServerType(strings.TrimPrefix(opt.Key, "--"), serverType)
		if len(ptValues) > 0 {
			log.Warn().Msgf("Pass through option %s conflicts with automatically generated option with value '%s'", opt.Key, opt.Value)
		} else {
			args = append(args, opt.Key, opt.Value)
		}
	}
	for _, ptOpt := range config.PassthroughOptions {
		values := ptOpt.valueForServerType(serverType)
		if len(values) == 0 {
			continue
		}
		// Look for overrides of configuration sections
		if section := arangodConfig.FindSection(ptOpt.sectionName()); section != nil {
			if confValue, found := section.Settings[ptOpt.sectionKey()]; found {
				log.Warn().Msgf("Pass through option %s overrides generated configuration option with value '%s'", ptOpt.Name, confValue)
			}
		}
		// Append all values
		for _, value := range values {
			args = append(args, ptOpt.FormattedOptionName(), value)
		}
	}
	return args
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"time"
)

func responseBody(url string) ([]byte, error) {
	response, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	contents, err := ioutil.ReadAll(response.Body)
	return contents, nil
}

func JenkinsJobData(jenkins, jobName *string) (Job, error) {
	url := "http://" + *jenkins + "/job/" + *jobName + "/api/json?pretty=true"
	// fmt.Printf("%s\n", url)
	contents, err := responseBody(url)
	if err != nil {
		return Job{}, err
	}
	// fmt.Printf("%s\n", string(contents))

	var data Job
	err = json.Unmarshal(contents, &data)
	if err != nil {
		return Job{}, err
	}
	return data, nil
}

func JenkinsBuild(jenkins, jobName *string, buildNumber *int) (Build, error) {
	build := Build{}

	url := "http://" + *jenkins +
		"/job/" + *jobName + "/" +
		strconv.Itoa(*buildNumber) + "/api/json?pretty=true"

	// fmt.Printf("%s\n", url)
	contents, err := responseBody(url)
	if err != nil {
		return build, err
	}
	// fmt.Printf("%s\n", string(contents))

	err = json.Unmarshal(contents, &build)
	if err != nil {
		return build, err
	}
	return build, nil
}

type Job struct {
	Name            string    `json:"name"`
	URL             string    `json:"url"`
	Color           string    `json:"color"`
	NextBuildNumber int       `json:"nextBuildNumber"`
	InQueue         bool      `json:"inQueue"`
	LastBuild       LastBuild `json:"lastBuild"`
}
type LastBuild struct {
	Number int    `json:"number"`
	URL    string `json:"url"`
}

type Build struct {
	Number            int    `json:"number"`
	Duration          int    `json:"duration"`
	EstimatedDuration int    `json:"estimatedDuration"`
	Timestamp         int    `json:"timestamp"`
	URL               string `json:"url"`
	Result            string `json:"result"`
	Building          bool   `json:"building"`
}

type JobState string

const (
	Unknown  JobState = "Unknown" // no contact to jenkins
	InQueue  JobState = "InQueue"
	Building JobState = "Building"
	Finished JobState = "Finished"
)

type JenkinsJob struct {
	Name     string   `json:"name"`
	JobState JobState `json:"state"`
}

func JenkinsJobState(jenkins, jobName *string) JenkinsJob {
	jenkinsJob := JenkinsJob{Name: *jobName}
	var buildNumber int

	// query /job while InQueue
	// set buildNumber
	job, err := JenkinsJobData(jenkins, jobName)
	if err != nil {
		fmt.Printf("%s", err)
		jenkinsJob.JobState = Unknown
		return jenkinsJob
	}
	fmt.Println("InQueue:", job.InQueue)

	if job.InQueue {
		jenkinsJob.JobState = InQueue
		return jenkinsJob
	} else {
		buildNumber = job.LastBuild.Number
	}

	// query /job/{{buildNo}} for Building
	build, err := JenkinsBuild(jenkins, jobName, &buildNumber)
	if err != nil {
		fmt.Printf("%s", err)
		jenkinsJob.JobState = Unknown
		return jenkinsJob
	}
	if build.Building {
		start := time.Unix(int64(build.Timestamp/1000), 0)
		estEnd := time.Unix(int64((build.Timestamp+build.EstimatedDuration)/1000), 0)

		buildDuration := int(time.Since(start).Seconds())
		buildCountdown := (build.EstimatedDuration / 1000) - buildDuration

		fmt.Println("Building:", build.Building)
		fmt.Println("Start:", start)
		fmt.Println("Build Duration: (s): ", buildDuration)
		fmt.Println("Build Countdown (s): ", buildCountdown)
		fmt.Println("Estimated End:", estEnd)
		fmt.Println("Estimated Duration (s): ", build.EstimatedDuration/1000)
		jenkinsJob.JobState = Building
	} else {
		// show build result
		fmt.Println("Duration:", build.Duration)
		fmt.Println("Result:", build.Result)
		jenkinsJob.JobState = Finished
	}
	return jenkinsJob
}

func main() {
	jenkins := flag.String("jenkins", "127.0.0.1:8080", "Jenkins hostname")
	jobName := flag.String("jobName", "test", "Jenkins job name")
	flag.Parse()
	for {
		state := JenkinsJobState(jenkins, jobName)
		fmt.Println("Time:", time.Now())
		fmt.Println("state:", state)
		time.Sleep(time.Second)
		fmt.Printf("\n\n")
	}

}
<commit_msg>report only building and new state<commit_after>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"time"
)

func responseBody(url string) ([]byte, error) {
	response, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	contents, err := ioutil.ReadAll(response.Body)
	return contents, nil
}

func JenkinsJobData(jenkins, jobName *string) (Job, error) {
	url := "http://" + *jenkins + "/job/" + *jobName + "/api/json?pretty=true"
	// fmt.Printf("%s\n", url)
	contents, err := responseBody(url)
	if err != nil {
		return Job{}, err
	}
	// fmt.Printf("%s\n", string(contents))

	var data Job
	err = json.Unmarshal(contents, &data)
	if err != nil {
		return Job{}, err
	}
	return data, nil
}

func JenkinsBuild(jenkins, jobName *string, buildNumber *int) (Build, error) {
	build := Build{}

	url := "http://" + *jenkins +
		"/job/" + *jobName + "/" +
		strconv.Itoa(*buildNumber) + "/api/json?pretty=true"

	// fmt.Printf("%s\n", url)
	contents, err := responseBody(url)
	if err != nil {
		return build, err
	}
	// fmt.Printf("%s\n", string(contents))

	err = json.Unmarshal(contents, &build)
	if err != nil {
		return build, err
	}
	return build, nil
}

type Job struct {
	Name            string    `json:"name"`
	URL             string    `json:"url"`
	Color           string    `json:"color"`
	NextBuildNumber int       `json:"nextBuildNumber"`
	InQueue         bool      `json:"inQueue"`
	LastBuild       LastBuild `json:"lastBuild"`
}
type LastBuild struct {
	Number int    `json:"number"`
	URL    string `json:"url"`
}

type Build struct {
	Number            int    `json:"number"`
	Duration          int    `json:"duration"`
	EstimatedDuration int    `json:"estimatedDuration"`
	Timestamp         int    `json:"timestamp"`
	URL               string `json:"url"`
	Result            string `json:"result"`
	Building          bool   `json:"building"`
}

type JobState string

const (
	Unknown  JobState = "Unknown" // no contact to jenkins
	InQueue  JobState = "InQueue"
	Building JobState = "Building"
	Finished JobState = "Finished"
)

type JenkinsJob struct {
	Name     string   `json:"name"`
	JobState JobState `json:"state"`
}

func JenkinsJobState(jenkins, jobName *string) JenkinsJob {
	jenkinsJob := JenkinsJob{Name: *jobName}
	var buildNumber int

	// query /job while InQueue
	// set buildNumber
	job, err := JenkinsJobData(jenkins, jobName)
	if err != nil {
		fmt.Printf("%s", err)
		jenkinsJob.JobState = Unknown
		return jenkinsJob
	}
	fmt.Println("InQueue:", job.InQueue)

	if job.InQueue {
		jenkinsJob.JobState = InQueue
		return jenkinsJob
	} else {
		buildNumber = job.LastBuild.Number
	}

	// query /job/{{buildNo}} for Building
	build, err := JenkinsBuild(jenkins, jobName, &buildNumber)
	if err != nil {
		fmt.Printf("%s", err)
		jenkinsJob.JobState = Unknown
		return jenkinsJob
	}
	if build.Building {
		start := time.Unix(int64(build.Timestamp/1000), 0)
		estEnd := time.Unix(int64((build.Timestamp+build.EstimatedDuration)/1000), 0)

		buildDuration := int(time.Since(start).Seconds())
		buildCountdown := (build.EstimatedDuration / 1000) - buildDuration

		fmt.Println("Building:", build.Building)
		fmt.Println("Start:", start)
		fmt.Println("Build Duration: (s): ", buildDuration)
		fmt.Println("Build Countdown (s): ", buildCountdown)
		fmt.Println("Estimated End:", estEnd)
		fmt.Println("Estimated Duration (s): ", build.EstimatedDuration/1000)
		jenkinsJob.JobState = Building
	} else {
		// show build result
		fmt.Println("Duration:", build.Duration)
		fmt.Println("Result:", build.Result)
		jenkinsJob.JobState = Finished
	}
	return jenkinsJob
}

func main() {
	jenkins := flag.String("jenkins", "127.0.0.1:8080", "Jenkins hostname")
	jobName := flag.String("jobName", "test", "Jenkins job name")
	flag.Parse()
	oldState := JenkinsJobState(jenkins, jobName)
	fmt.Println("Time:", time.Now())
	fmt.Println("oldState:", oldState)
	for {
		time.Sleep(time.Second)
		newState := JenkinsJobState(jenkins, jobName)
		if newState.JobState != oldState.JobState || newState.JobState == Building {
			fmt.Println("Time:", time.Now())
			fmt.Println("newState:", newState)
			fmt.Printf("\n\n")
		}
		oldState = newState
	}

}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strconv"
	"time"
)

func responseBody(url string) ([]byte, error) {
	response, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	contents, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	return contents, nil
}

func JenkinsJob(jenkins, jobName *string) (Job, error) {
	url := "http://" + *jenkins + "/job/" + *jobName + "/api/json?pretty=true"
	fmt.Printf("%s\n", url)
	contents, err := responseBody(url)
	if err != nil {
		return Job{}, err
	}
	fmt.Printf("%s\n", string(contents))

	var data Job
	err = json.Unmarshal(contents, &data)
	if err != nil {
		return Job{}, err
	}
	return data, nil
}

func JenkinsBuild(jenkins, jobName *string, buildNumber *int) (Build, error) {
	build := Build{}

	url := "http://" + *jenkins +
		"/job/" + *jobName + "/" +
		strconv.Itoa(*buildNumber) + "/api/json?pretty=true"

	fmt.Printf("%s\n", url)
	contents, err := responseBody(url)
	if err != nil {
		return build, err
	}
	// fmt.Printf("%s\n", string(contents))

	err = json.Unmarshal(contents, &build)
	if err != nil {
		return build, err
	}
	return build, nil
}

type Job struct {
	Name            string    `json:"name"`
	URL             string    `json:"url"`
	Color           string    `json:"color"`
	NextBuildNumber int       `json:"nextBuildNumber"`
	InQueue         bool      `json:"inQueue"`
	LastBuild       LastBuild `json:"lastBuild"`
}
type LastBuild struct {
	Number int    `json:"number"`
	URL    string `json:"url"`
}

type Build struct {
	Number            int    `json:"number"`
	Duration          int    `json:"duration"`
	EstimatedDuration int    `json:"estimatedDuration"`
	Timestamp         int    `json:"timestamp"`
	URL               string `json:"url"`
	Result            string `json:"result"`
	Building          bool   `json:"building"`
}

func main() {
	jenkins := flag.String("jenkins", "127.0.0.1:8080", "Jenkins hostname")
	jobName := flag.String("jobName", "test", "Jenkins job name")
	flag.Parse()
	var buildNumber int

	for {
		// query /job while InQueue
		for {
			time.Sleep(time.Second)
			job, err := JenkinsJob(jenkins, jobName)
			if err != nil {
				fmt.Printf("%s", err)
				os.Exit(1)
			}
			fmt.Println("Time:", time.Now())
			fmt.Println("Name:", job.Name)
			fmt.Println("InQueue:", job.InQueue)

			if !job.InQueue {
				buildNumber = job.LastBuild.Number
				break
			}
		}

		// query /job/{{buildNo}} while Building
		for {
			time.Sleep(time.Second)
			build, err := JenkinsBuild(jenkins, jobName, &buildNumber)
			if err != nil {
				fmt.Printf("%s", err)
				os.Exit(1)
			}
			now := time.Now()
			start := time.Unix(int64(build.Timestamp/1000), 0)
			estEnd := time.Unix(int64((build.Timestamp+build.EstimatedDuration)/1000), 0)

			buildDuration := int(time.Since(start).Seconds())
			buildCountdown := (build.EstimatedDuration / 1000) - buildDuration

			fmt.Println("Time:", now)
			fmt.Println("Number:", build.Number)
			fmt.Println("Building:", build.Building)
			fmt.Println("Timestamp:", build.Timestamp)
			fmt.Println("Start:", start)
			fmt.Println("Build Duration: (s): ", buildDuration)
			fmt.Println("Build Countdown (s): ", buildCountdown)
			fmt.Println("Estimated End:", estEnd)
			fmt.Println("Estimated Duration (s): ", build.EstimatedDuration/1000)
			if !build.Building {
				break
			}
		}

		// show build result
		build, err := JenkinsBuild(jenkins, jobName, &buildNumber)
		if err != nil {
			fmt.Printf("%s", err)
			os.Exit(1)
		}
		fmt.Println("Time:", time.Now())
		fmt.Println("Number:", build.Number)
		fmt.Println("Building:", build.Building)
		fmt.Println("Timestamp:", build.Timestamp)
		fmt.Println("Duration:", build.Duration)
		fmt.Println("EstimatedDuration:", build.EstimatedDuration)
		fmt.Println("Result:", build.Result)
	}
}
<commit_msg>update<commit_after>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strconv"
	"time"
)

func responseBody(url string) ([]byte, error) {
	response, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	contents, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	return contents, nil
}

func JenkinsJob(jenkins, jobName *string) (Job, error) {
	url := "http://" + *jenkins + "/job/" + *jobName + "/api/json?pretty=true"
	fmt.Printf("%s\n", url)
	contents, err := responseBody(url)
	if err != nil {
		return Job{}, err
	}
	// fmt.Printf("%s\n", string(contents))

	var data Job
	err = json.Unmarshal(contents, &data)
	if err != nil {
		return Job{}, err
	}
	return data, nil
}

func JenkinsBuild(jenkins, jobName *string, buildNumber *int) (Build, error) {
	build := Build{}

	url := "http://" + *jenkins +
		"/job/" + *jobName + "/" +
		strconv.Itoa(*buildNumber) + "/api/json?pretty=true"

	fmt.Printf("%s\n", url)
	contents, err := responseBody(url)
	if err != nil {
		return build, err
	}
	// fmt.Printf("%s\n", string(contents))

	err = json.Unmarshal(contents, &build)
	if err != nil {
		return build, err
	}
	return build, nil
}

type Job struct {
	Name            string    `json:"name"`
	URL             string    `json:"url"`
	Color           string    `json:"color"`
	NextBuildNumber int       `json:"nextBuildNumber"`
	InQueue         bool      `json:"inQueue"`
	LastBuild       LastBuild `json:"lastBuild"`
}
type LastBuild struct {
	Number int    `json:"number"`
	URL    string `json:"url"`
}

type Build struct {
	Number            int    `json:"number"`
	Duration          int    `json:"duration"`
	EstimatedDuration int    `json:"estimatedDuration"`
	Timestamp         int    `json:"timestamp"`
	URL               string `json:"url"`
	Result            string `json:"result"`
	Building          bool   `json:"building"`
}

func main() {
	jenkins := flag.String("jenkins", "127.0.0.1:8080", "Jenkins hostname")
	jobName := flag.String("jobName", "test", "Jenkins job name")
	flag.Parse()
	var buildNumber int

	for {
		// query /job while InQueue
		// set buildNumber
		for {
			time.Sleep(time.Second)
			job, err := JenkinsJob(jenkins, jobName)
			if err != nil {
				fmt.Printf("%s", err)
				os.Exit(1)
			}
			fmt.Println("Time:", time.Now())
			fmt.Println("Name:", job.Name)
			fmt.Println("InQueue:", job.InQueue)

			if !job.InQueue {
				buildNumber = job.LastBuild.Number
				break
			}
		}

		// query /job/{{buildNo}} while Building
		for {
			time.Sleep(time.Second)
			build, err := JenkinsBuild(jenkins, jobName, &buildNumber)
			if err != nil {
				fmt.Printf("%s", err)
				os.Exit(1)
			}
			now := time.Now()
			start := time.Unix(int64(build.Timestamp/1000), 0)
			estEnd := time.Unix(int64((build.Timestamp+build.EstimatedDuration)/1000), 0)

			buildDuration := int(time.Since(start).Seconds())
			buildCountdown := (build.EstimatedDuration / 1000) - buildDuration

			fmt.Println("Time:", now)
			fmt.Println("Building:", build.Building)
			fmt.Println("Start:", start)
			fmt.Println("Build Duration: (s): ", buildDuration)
			fmt.Println("Build Countdown (s): ", buildCountdown)
			fmt.Println("Estimated End:", estEnd)
			fmt.Println("Estimated Duration (s): ", build.EstimatedDuration/1000)
			if !build.Building {
				break
			}
		}

		// show build result
		build, err := JenkinsBuild(jenkins, jobName, &buildNumber)
		if err != nil {
			fmt.Printf("%s", err)
			os.Exit(1)
		}
		fmt.Println("Time:", time.Now())
		fmt.Println("Number:", build.Number)
		fmt.Println("Duration:", build.Duration)
		fmt.Println("Result:", build.Result)
	}
}
<|endoftext|>