grim/convey

Bump the version for release
v0.14.0-alpha3
2018-02-20, Gary Kramlich
166a6d1979fa
Bump the version for release
// Convey
// Copyright 2016-2018 Gary Kramlich <grim@reaperworld.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
"os"
"path/filepath"
"runtime/debug"
"strings"
"github.com/alecthomas/kingpin"
"github.com/aphistic/gomol"
"bitbucket.org/rw_grim/convey/config"
"bitbucket.org/rw_grim/convey/consts"
"bitbucket.org/rw_grim/convey/environment"
"bitbucket.org/rw_grim/convey/loaders/bitbucket"
"bitbucket.org/rw_grim/convey/loaders/codebuild"
"bitbucket.org/rw_grim/convey/loaders/convey"
"bitbucket.org/rw_grim/convey/logging"
"bitbucket.org/rw_grim/convey/runners"
"bitbucket.org/rw_grim/convey/ssh"
"bitbucket.org/rw_grim/convey/state"
)
var (
app = kingpin.New("convey", "Convey is a container pipeline runner.").Version(consts.Version)
color = app.Flag("color", "Enable colorized output").Default("true").Bool()
configLoader = app.Flag("config-loader", "Select the configuration loader").Short('l').Default("convey").Enum("convey", "bitbucket", "codebuild")
configFile = app.Flag("config", "The config file name to use").Short('f').String()
cpuShares = app.Flag("cpu-shares", "The amount of cpu shares to give to a run task").Short('c').String()
dockerConfig = app.Flag("docker-config", "Location of docker client config files").String()
env = app.Flag("env", "Set environment variables").Short('e').Strings()
forceSequential = app.Flag("force-sequential", "Don't run anything concurrently").Short('S').Default("False").Bool()
keep = app.Flag("keep", "Keep the workspace volume").Short('k').Hidden().Default("False").Bool()
memory = app.Flag("memory", "The amount of memory to give the run task").Short('m').String()
sshAgent = app.Flag("ssh-agent", "A shortcut for --ssh-identity=*").Default("false").Bool()
sshIdentities = app.Flag("ssh-identity", "Enable ssh-agent for the given identities").Strings()
planTimeout = app.Flag("timeout", "The maximum amount of time a plan can run. 0 to disable. Units must be specified.").Default("15m").Duration()
verbose = app.Flag("verbose", "Be more verbose").Short('v').Default("False").Bool()
disableDeprecated = app.Flag("disable-deprecated", "Allow the use of deprecated features").Default("False").Bool()
options = app.Flag("opt", "Options to pass to the config loader").Short('o').Strings()
// deprecated options
graphviz = app.Flag("graphviz", "Output a graphviz diagram of the config file").Short('g').Hidden().Default("False").Bool()
listEnvironment = app.Flag("list-environment", "List the environment variables that are available").Short('E').Hidden().Default("false").Bool()
listMetaPlans = app.Flag("list-meta-plans", "List the meta plans that are available").Short('M').Hidden().Default("false").Bool()
listPlans = app.Flag("list-plans", "List the plans that are available").Short('P').Hidden().Default("false").Bool()
listTasks = app.Flag("list-tasks", "List the supported tasks").Short('L').Hidden().Default("false").Bool()
showConfig = app.Flag("show-config", "Show a dump of the config file").Short('C').Hidden().Default("false").Bool()
// simple commands
_ = app.Command("config", "Show a dump of the config file")
_ = app.Command("environment", "List the environment variables that are available")
_ = app.Command("graphviz", "Output a graphviz diagram of the config file")
// run command
run = app.Command("run", "Run a plan or metaplan").Default()
planNames = run.Arg("plan", "The plan or list of plans to run in specified order").Default("default").Strings()
// list commands
ls = app.Command("list", "List information").Alias("ls")
_ = ls.Command("environment", "List the environment variables that are available.").Alias("env")
_ = ls.Command("metaplans", "List the metaplans defined in the configuration file.")
_ = ls.Command("plans", "List the plans defined in the configuration file.")
_ = ls.Command("tasks", "List the tasks defined in the configuration file.")
)
// determineLoader will return the config loader to use.
func determineLoader() config.Loader {
switch *configLoader {
case "bitbucket":
return &bitbucket.Loader{}
case "codebuild":
return &codebuild.Loader{}
default:
return &convey.Loader{}
}
}
// determineRunner will return the runner that handles the given command.
func determineRunner(command string) runners.Runner {
switch command {
case "graphviz":
return &runners.Graphviz{}
case "list environment":
return &runners.ListEnvironment{}
case "list metaplans":
return &runners.ListMetaPlans{}
case "list plans":
return &runners.ListPlans{}
case "list tasks":
return &runners.ListTasks{}
case "config":
return &runners.ShowConfig{}
default:
return &runners.Convey{}
}
}
// handleDeprecatedCommands will check deprecated command line flags and return
// the current command that they should run.
func handleDeprecatedCommands() string {
if *disableDeprecated {
return ""
}
command := ""
if *graphviz {
fmt.Printf("--graphviz is deprecated, use 'convey graphviz` instead")
command = "graphviz"
} else if *listEnvironment {
fmt.Printf("--list-environment is deprecated, use 'convey list environment` instead")
command = "list environment"
} else if *listMetaPlans {
fmt.Printf("--list-metaplans is deprecated, use `convey list metaplans` instead")
command = "list metaplans"
} else if *listPlans {
fmt.Printf("--list-plans is deprecated, use `convey list plans` instead")
command = "list plans"
} else if *listTasks {
fmt.Printf("--list-tasks is deprecated, use `convey list tasks` instead")
command = "list tasks"
} else if *showConfig {
fmt.Printf("--list-plans is deprecated, use `convey config` instead")
command = "config"
}
return command
}
// resolvePlans will run through all of the plans in the config and resolve
// their names via the loader.
func resolvePlans(cfg *config.Config, loader config.Loader, st *state.State) []string {
realPlans := []string{}
for _, planName := range *planNames {
if metaPlan, found := cfg.MetaPlans[planName]; found {
realPlans = append(realPlans, metaPlan.Plans...)
} else {
realPlans = append(realPlans, loader.ResolvePlanName(planName, cfg, st))
}
}
return realPlans
}
// loadConfig will load the config and it's path or an error.
func loadConfig(loader config.Loader) (*config.Config, string, error) {
// if a config file was not provided search for the loader's default file
if *configFile == "" {
for _, filename := range loader.Filenames() {
if _, err := os.Stat(filename); os.IsNotExist(err) {
continue
}
*configFile = filename
break
}
}
// now make sure we found a config file
if *configFile == "" {
err := fmt.Errorf("config file not found, looking for %s", strings.Join(loader.Filenames(), ","))
return nil, "", err
}
// figure out the path to the config file
cfgPath, err := filepath.Abs(filepath.Dir(*configFile))
if err != nil {
return nil, "", err
}
cfg, err := config.LoadFile(*configFile, loader, *options, *disableDeprecated)
if err != nil {
return nil, "", err
}
return cfg, cfgPath, nil
}
func gomain() int {
args := os.Args[1:]
command, err := app.Parse(args)
if err != nil {
app.Usage(args)
return 1
}
// create our state
st := state.New()
// setup logging
if err = logging.Setup(*color, *verbose); err != nil {
fmt.Printf("failed to setup logging: %s\n", err)
return 1
}
defer func() {
e := gomol.ShutdownLoggers()
if e != nil {
fmt.Printf("error: %s\n", err)
}
}()
// now defer the state shutdown so we can still log
defer st.Destroy()
// now load the config
loader := determineLoader()
cfg, cfgPath, err := loadConfig(loader)
if err != nil {
fmt.Printf("%s\n", err)
return 1
}
// find our default environment variables and then update them with the
// values from the command line
defEnv, err := environment.Initialize(cfgPath)
if err != nil {
fmt.Printf("%s\n", err)
return 1
}
// if the user specified the shortcut, add * to the list of acceptable keys
if *sshAgent {
*sshIdentities = append(*sshIdentities, "*")
}
// now merge in the keys from the config
*sshIdentities = append(*sshIdentities, cfg.SSHIdentities...)
// now check if we have any keys and make sure one of them is usable
enableSSHAgent, err := ssh.ShouldEnable(*sshIdentities)
if err != nil {
fmt.Printf("%s\n", err)
return 1
}
// set the state's variables and validate it
st.CfgPath = cfgPath
st.KeepWorkspace = *keep
st.DisableDeprecated = *disableDeprecated
st.ForceSequential = *forceSequential
st.EnableSSHAgent = enableSSHAgent
st.PlanTimeout = *planTimeout
st.DockerConfig = *dockerConfig
st.CPUShares = *cpuShares
st.Memory = *memory
st.MergeEnv(environment.Merge(defEnv, *env))
if err := st.Valid(); err != nil {
fmt.Printf("%s\n", err)
return 1
}
if commandOverride := handleDeprecatedCommands(); commandOverride != "" {
command = commandOverride
}
runner := determineRunner(command)
if len(*planNames) == 0 {
*planNames = []string{loader.DefaultPlan()}
}
// resolve the plan name with the load and options
realPlans := resolvePlans(cfg, loader, st)
return runner.Run(cfg, realPlans, *env, st)
}
func main() {
exitCode := 0
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic: %s\n%s", r, debug.Stack())
}
os.Exit(exitCode)
}()
exitCode = gomain()
}