Commit 1b1f13b6 authored by Kamil Trzciński's avatar Kamil Trzciński

Update command line interface

- Automatically provide command line options for all structure fields
- Make register expose all options
- Rewrite all other commands to use a new command processing scheme
parent 82db7cd4
......@@ -34,6 +34,10 @@
"ImportPath": "github.com/kardianos/osext",
"Rev": "efacde03154693404c65e7aa7d461ac9014acd0c"
},
{
"ImportPath": "gitlab.com/ayufan/golang-cli-helpers",
"Rev": "0a14b63a7466ee44de4a90f998fad73afa8482bf"
},
{
"ImportPath": "gopkg.in/yaml.v1",
"Rev": "9f9df34309c04878acc86042b16630b0f696e1de"
......
# Created by .ignore support plugin (hsz.mobi)
### Go template
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
package clihelpers
// Copyright 2012 Jesse van den Kieboom. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The source is taken from: https://raw.githubusercontent.com/jessevdk/go-flags/master/convert.go
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
)
// Marshaler is the interface implemented by types that can marshal themselves
// to a string representation of the flag.
type Marshaler interface {
// MarshalFlag marshals a flag value to its string representation.
MarshalFlag() (string, error)
}
// Unmarshaler is the interface implemented by types that can unmarshal a flag
// argument to themselves. The provided value is directly passed from the
// command line.
type Unmarshaler interface {
// UnmarshalFlag unmarshals a string value representation to the flag
// value (which therefore needs to be a pointer receiver).
UnmarshalFlag(value string) error
}
func getBase(options reflect.StructTag, base int) (int, error) {
sbase := options.Get("base")
var err error
var ivbase int64
if sbase != "" {
ivbase, err = strconv.ParseInt(sbase, 10, 32)
base = int(ivbase)
}
return base, err
}
func convertMarshal(val reflect.Value) (bool, string, error) {
// Check first for the Marshaler interface
if val.Type().NumMethod() > 0 && val.CanInterface() {
if marshaler, ok := val.Interface().(Marshaler); ok {
ret, err := marshaler.MarshalFlag()
return true, ret, err
}
}
return false, "", nil
}
func convertToString(val reflect.Value, options reflect.StructTag) (string, error) {
if ok, ret, err := convertMarshal(val); ok {
return ret, err
}
tp := val.Type()
// Support for time.Duration
if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() {
stringer := val.Interface().(fmt.Stringer)
return stringer.String(), nil
}
switch tp.Kind() {
case reflect.String:
return val.String(), nil
case reflect.Bool:
if val.Bool() {
return "true", nil
}
return "false", nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
base, err := getBase(options, 10)
if err != nil {
return "", err
}
return strconv.FormatInt(val.Int(), base), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
base, err := getBase(options, 10)
if err != nil {
return "", err
}
return strconv.FormatUint(val.Uint(), base), nil
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(val.Float(), 'g', -1, tp.Bits()), nil
case reflect.Slice:
if val.Len() == 0 {
return "", nil
}
ret := "["
for i := 0; i < val.Len(); i++ {
if i != 0 {
ret += ", "
}
item, err := convertToString(val.Index(i), options)
if err != nil {
return "", err
}
ret += item
}
return ret + "]", nil
case reflect.Map:
ret := "{"
for i, key := range val.MapKeys() {
if i != 0 {
ret += ", "
}
keyitem, err := convertToString(key, options)
if err != nil {
return "", err
}
item, err := convertToString(val.MapIndex(key), options)
if err != nil {
return "", err
}
ret += keyitem + ":" + item
}
return ret + "}", nil
case reflect.Ptr:
return convertToString(reflect.Indirect(val), options)
case reflect.Interface:
if !val.IsNil() {
return convertToString(val.Elem(), options)
}
}
return "", nil
}
func convertUnmarshal(val string, retval reflect.Value) (bool, error) {
if retval.Type().NumMethod() > 0 && retval.CanInterface() {
if unmarshaler, ok := retval.Interface().(Unmarshaler); ok {
return true, unmarshaler.UnmarshalFlag(val)
}
}
if retval.Type().Kind() != reflect.Ptr && retval.CanAddr() {
return convertUnmarshal(val, retval.Addr())
}
if retval.Type().Kind() == reflect.Interface && !retval.IsNil() {
return convertUnmarshal(val, retval.Elem())
}
return false, nil
}
func convert(val string, retval reflect.Value, options reflect.StructTag) error {
if ok, err := convertUnmarshal(val, retval); ok {
return err
}
tp := retval.Type()
// Support for time.Duration
if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() {
parsed, err := time.ParseDuration(val)
if err != nil {
return err
}
retval.SetInt(int64(parsed))
return nil
}
switch tp.Kind() {
case reflect.String:
retval.SetString(val)
case reflect.Bool:
if val == "" {
retval.SetBool(true)
} else {
b, err := strconv.ParseBool(val)
if err != nil {
return err
}
retval.SetBool(b)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
base, err := getBase(options, 10)
if err != nil {
return err
}
parsed, err := strconv.ParseInt(val, base, tp.Bits())
if err != nil {
return err
}
retval.SetInt(parsed)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
base, err := getBase(options, 10)
if err != nil {
return err
}
parsed, err := strconv.ParseUint(val, base, tp.Bits())
if err != nil {
return err
}
retval.SetUint(parsed)
case reflect.Float32, reflect.Float64:
parsed, err := strconv.ParseFloat(val, tp.Bits())
if err != nil {
return err
}
retval.SetFloat(parsed)
case reflect.Slice:
elemtp := tp.Elem()
elemvalptr := reflect.New(elemtp)
elemval := reflect.Indirect(elemvalptr)
if err := convert(val, elemval, options); err != nil {
return err
}
retval.Set(reflect.Append(retval, elemval))
case reflect.Map:
parts := strings.SplitN(val, ":", 2)
key := parts[0]
var value string
if len(parts) == 2 {
value = parts[1]
}
keytp := tp.Key()
keyval := reflect.New(keytp)
if err := convert(key, keyval, options); err != nil {
return err
}
valuetp := tp.Elem()
valueval := reflect.New(valuetp)
if err := convert(value, valueval, options); err != nil {
return err
}
if retval.IsNil() {
retval.Set(reflect.MakeMap(tp))
}
retval.SetMapIndex(reflect.Indirect(keyval), reflect.Indirect(valueval))
case reflect.Ptr:
if retval.IsNil() {
retval.Set(reflect.New(retval.Type().Elem()))
}
return convert(val, reflect.Indirect(retval), options)
case reflect.Interface:
if !retval.IsNil() {
return convert(val, retval.Elem(), options)
}
}
return nil
}
func isPrint(s string) bool {
for _, c := range s {
if !strconv.IsPrint(c) {
return false
}
}
return true
}
func quoteIfNeeded(s string) string {
if !isPrint(s) {
return strconv.Quote(s)
}
return s
}
func quoteIfNeededV(s []string) []string {
ret := make([]string, len(s))
for i, v := range s {
ret[i] = quoteIfNeeded(v)
}
return ret
}
func quoteV(s []string) []string {
ret := make([]string, len(s))
for i, v := range s {
ret[i] = strconv.Quote(v)
}
return ret
}
func unquoteIfPossible(s string) (string, error) {
if len(s) == 0 || s[0] != '"' {
return s, nil
}
return strconv.Unquote(s)
}
func wrapText(s string, l int, prefix string) string {
// Basic text wrapping of s at spaces to fit in l
var ret string
s = strings.TrimSpace(s)
for len(s) > l {
// Try to split on space
suffix := ""
pos := strings.LastIndex(s[:l], " ")
if pos < 0 {
pos = l - 1
suffix = "-\n"
}
if len(ret) != 0 {
ret += "\n" + prefix
}
ret += strings.TrimSpace(s[:pos]) + suffix
s = strings.TrimSpace(s[pos:])
}
if len(s) > 0 {
if len(ret) != 0 {
ret += "\n" + prefix
}
return ret + s
}
return ret
}
package clihelpers
import (
"strings"
"reflect"
"github.com/codegangsta/cli"
)
type StructFieldValue struct {
field reflect.StructField
value reflect.Value
}
func (f StructFieldValue) IsBoolFlag() bool {
if f.value.Kind() == reflect.Bool {
return true
} else if f.value.Kind() == reflect.Ptr && f.value.Elem().Kind() == reflect.Bool {
return true
} else {
return false
}
}
func (s StructFieldValue) Set(val string) error {
return convert(val, s.value, s.field.Tag)
}
func (s StructFieldValue) String() string {
if s.value.Kind() == reflect.Ptr && s.value.IsNil() {
return ""
}
retval, _ := convertToString(s.value, s.field.Tag)
return retval
}
type StructFieldFlag struct {
cli.GenericFlag
}
func (f StructFieldFlag) String() string {
if sf, ok := f.Value.(StructFieldValue); ok {
if sf.IsBoolFlag() {
flag := &cli.BoolFlag{
Name: f.Name,
Usage: f.Usage,
EnvVar: f.EnvVar,
}
return flag.String()
} else {
flag := &cli.StringFlag{
Name: f.Name,
Value: sf.String(),
Usage: f.Usage,
EnvVar: f.EnvVar,
}
return flag.String()
}
} else {
return f.GenericFlag.String()
}
}
func getStructFieldFlag(field reflect.StructField, fieldValue reflect.Value, ns []string) []cli.Flag {
var names []string
if name := field.Tag.Get("short"); name != "" {
names = append(names, strings.Join(append(ns, name), "-"))
}
if name := field.Tag.Get("long"); name != "" {
names = append(names, strings.Join(append(ns, name), "-"))
}
if len(names) == 0 {
return []cli.Flag{}
}
flag := cli.GenericFlag{
Name: strings.Join(names, ", "),
Value: StructFieldValue{
field: field,
value: fieldValue,
},
Usage: field.Tag.Get("description"),
EnvVar: field.Tag.Get("env"),
}
return []cli.Flag{StructFieldFlag{GenericFlag: flag}}
}
func getFlagsForStructField(field reflect.StructField, fieldValue reflect.Value, ns []string) []cli.Flag {
if !fieldValue.IsValid() {
return []cli.Flag{}
}
switch field.Type.Kind() {
case reflect.Struct:
if newNs := field.Tag.Get("namespace"); newNs != "" {
return getFlagsForValue(fieldValue, append(ns, newNs))
} else if field.Anonymous {
return getFlagsForValue(fieldValue, ns)
}
break
case reflect.Ptr:
if field.Type.Elem().Kind() == reflect.Struct {
if newNs := field.Tag.Get("namespace"); newNs != "" {
return getFlagsForValue(fieldValue, append(ns, newNs))
}
} else {
return getStructFieldFlag(field, fieldValue, ns)
}
break
case reflect.Chan:
case reflect.Func:
case reflect.Interface:
case reflect.UnsafePointer:
break
default:
return getStructFieldFlag(field, fieldValue, ns)
}
return []cli.Flag{}
}
func getFlagsForValue(value reflect.Value, ns []string) []cli.Flag {
var flags []cli.Flag
if value.Type().Kind() == reflect.Ptr && value.Type().Elem().Kind() == reflect.Struct {
if value.IsNil() {
value.Set(reflect.New(value.Type().Elem()))
}
value = reflect.Indirect(value)
} else if value.Type().Kind() != reflect.Struct {
return []cli.Flag{}
}
valueType := value.Type()
for i := 0; i < valueType.NumField(); i++ {
newFlags := getFlagsForStructField(valueType.Field(i), value.Field(i), ns)
flags = append(flags, newFlags...)
}
return flags
}
func GetFlagsFromStruct(data interface{}, ns... string) []cli.Flag {
return getFlagsForValue(reflect.ValueOf(data), ns)
}
package commands
import (
"gitlab.com/gitlab-org/gitlab-ci-multi-runner/common"
"os"
)
type configOptions struct {
config *common.Config
ConfigFile string `short:"c" long:"config" env:"CONFIG_FILE" description:"Config file"`
}
func (c *configOptions) saveConfig() error {
return c.config.SaveConfig(c.ConfigFile)
}
func (c *configOptions) loadConfig() error {
config := common.NewConfig()
err := config.LoadConfig(c.ConfigFile)
if err != nil {
return err
}
c.config = config
return nil
}
func (c *configOptions) touchConfig() error {
// try to load existing config
err := c.loadConfig()
if err != nil {
return err
}
// save config for the first time
if !c.config.Loaded {
return c.saveConfig()
}
return nil
}
func init() {
configFile := os.Getenv("CONFIG_FILE")
if configFile == "" {
os.Setenv("CONFIG_FILE", getDefaultConfigFile())
}
}
......@@ -25,43 +25,46 @@ type RunnerHealth struct {
lastCheck time.Time
}
type MultiRunner struct {
config *common.Config
configFile string
workingDirectory string
user string
builds []*common.Build
buildsLock sync.RWMutex
healthy map[string]*RunnerHealth
healthyLock sync.Mutex
finished bool
abortBuilds chan os.Signal
interruptSignal chan os.Signal
reloadSignal chan os.Signal
doneSignal chan int
type RunCommand struct {
configOptions
ServiceName string `short:"n" long:"service" description:"Use different names for different services"`
WorkingDirectory string `short:"d" long:"working-directory" description:"Specify custom working directory"`
User string `short:"u" long:"user" description:"Use specific user to execute shell scripts"`
Syslog bool `long:"syslog" description:"Log to syslog"`
builds []*common.Build
buildsLock sync.RWMutex
healthy map[string]*RunnerHealth
healthyLock sync.Mutex
finished bool
abortBuilds chan os.Signal
interruptSignal chan os.Signal
reloadSignal chan os.Signal
doneSignal chan int
}
func (mr *MultiRunner) errorln(args ...interface{}) {
func (mr *RunCommand) errorln(args ...interface{}) {
args = append([]interface{}{len(mr.builds)}, args...)
log.Errorln(args...)
}
func (mr *MultiRunner) warningln(args ...interface{}) {
func (mr *RunCommand) warningln(args ...interface{}) {
args = append([]interface{}{len(mr.builds)}, args...)
log.Warningln(args...)
}
func (mr *MultiRunner) debugln(args ...interface{}) {
func (mr *RunCommand) debugln(args ...interface{}) {
args = append([]interface{}{len(mr.builds)}, args...)
log.Debugln(args...)
}
func (mr *MultiRunner) println(args ...interface{}) {
func (mr *RunCommand) println(args ...interface{}) {
args = append([]interface{}{len(mr.builds)}, args...)
log.Println(args...)
}
func (mr *MultiRunner) getHealth(runner *common.RunnerConfig) *RunnerHealth {
func (mr *RunCommand) getHealth(runner *common.RunnerConfig) *RunnerHealth {
mr.healthyLock.Lock()
defer mr.healthyLock.Unlock()
......@@ -78,7 +81,7 @@ func (mr *MultiRunner) getHealth(runner *common.RunnerConfig) *RunnerHealth {
return health
}
func (mr *MultiRunner) isHealthy(runner *common.RunnerConfig) bool {
func (mr *RunCommand) isHealthy(runner *common.RunnerConfig) bool {
health := mr.getHealth(runner)
if health.failures < common.HealthyChecks {
return true
......@@ -94,13 +97,13 @@ func (mr *MultiRunner) isHealthy(runner *common.RunnerConfig) bool {
return false
}
func (mr *MultiRunner) makeHealthy(runner *common.RunnerConfig) {
func (mr *RunCommand) makeHealthy(runner *common.RunnerConfig) {