Start Up is a simple deployment tool that performs given set of commands on multiple hosts in parallel.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

332 lines
7.1 KiB

package stup
import (
"fmt"
"os"
"os/exec"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
// Stupfile represents the Stack Up configuration YAML file.
type Stupfile struct {
Networks Networks `yaml:"networks"`
Commands Commands `yaml:"commands"`
Targets Targets `yaml:"targets"`
Env EnvList `yaml:"env"`
Version string `yaml:"version"`
}
type Accunt struct {
User string `yaml:"user"`
Password string `yaml:"password"`
Identity string `yaml:"identity"`
Sudo bool `yaml:"sudo"`
}
type Instance struct {
Address string `yaml:"address"`
Accunt
}
// Network is group of hosts with extra custom env vars.
type Network struct {
Env EnvList `yaml:"env"`
Inventory string `yaml:"inventory"`
Hosts []Instance `yaml:"hosts"`
Bastion string `yaml:"bastion"`
Accunt
}
// Networks is a list of user-defined networks
type Networks struct {
Names []string
nets map[string]Network
}
func (n *Networks) UnmarshalYAML(unmarshal func(interface{}) error) error {
err := unmarshal(&n.nets)
if err != nil {
return err
}
var items yaml.MapSlice
err = unmarshal(&items)
if err != nil {
return err
}
n.Names = make([]string, len(items))
for i, item := range items {
n.Names[i] = item.Key.(string)
}
return nil
}
func (n *Networks) Get(name string) (Network, bool) {
net, ok := n.nets[name]
return net, ok
}
// Command represents command(s) to be run remotely.
type Command struct {
Name string `yaml:"-"` // Command name.
Desc string `yaml:"desc"` // Command description.
Local string `yaml:"local"` // Command(s) to be run locally.
Run string `yaml:"run"` // Command(s) to be run remotelly.
Script string `yaml:"script"` // Load command(s) from script and run it remotelly.
Upload []Upload `yaml:"upload"` // See Upload struct.
Stdin bool `yaml:"stdin"` // Attach localhost STDOUT to remote commands' STDIN?
Once bool `yaml:"once"` // The command should be run "once" (on one host only).
Serial int `yaml:"serial"` // Max number of clients processing a task in parallel.
}
// Commands is a list of user-defined commands
type Commands struct {
Names []string
cmds map[string]Command
}
func (c *Commands) UnmarshalYAML(unmarshal func(interface{}) error) error {
err := unmarshal(&c.cmds)
if err != nil {
return err
}
var items yaml.MapSlice
err = unmarshal(&items)
if err != nil {
return err
}
c.Names = make([]string, len(items))
for i, item := range items {
c.Names[i] = item.Key.(string)
}
return nil
}
func (c *Commands) Get(name string) (Command, bool) {
cmd, ok := c.cmds[name]
return cmd, ok
}
// Targets is a list of user-defined targets
type Targets struct {
Names []string
targets map[string][]string
}
func (t *Targets) UnmarshalYAML(unmarshal func(interface{}) error) error {
err := unmarshal(&t.targets)
if err != nil {
return err
}
var items yaml.MapSlice
err = unmarshal(&items)
if err != nil {
return err
}
t.Names = make([]string, len(items))
for i, item := range items {
t.Names[i] = item.Key.(string)
}
return nil
}
func (t *Targets) Get(name string) ([]string, bool) {
cmds, ok := t.targets[name]
return cmds, ok
}
// Upload represents file copy operation from localhost Src path to Dst
// path of every host in a given Network.
type Upload struct {
Src string `yaml:"src"`
Dst string `yaml:"dst"`
Exc string `yaml:"exclude"`
}
// EnvVar represents an environment variable
type EnvVar struct {
Key string
Value string
}
func (e EnvVar) String() string {
return e.Key + `=` + e.Value
}
// AsExport returns the environment variable as a bash export statement
func (e EnvVar) AsExport() string {
return `export ` + e.Key + `="` + e.Value + `";`
}
// EnvList is a list of environment variables that maps to a YAML map,
// but maintains order, enabling late variables to reference early variables.
type EnvList []*EnvVar
func (e EnvList) Slice() []string {
envs := make([]string, len(e))
for i, env := range e {
envs[i] = env.String()
}
return envs
}
func (e *EnvList) UnmarshalYAML(unmarshal func(interface{}) error) error {
items := []yaml.MapItem{}
err := unmarshal(&items)
if err != nil {
return err
}
*e = make(EnvList, 0, len(items))
for _, v := range items {
e.Set(fmt.Sprintf("%v", v.Key), fmt.Sprintf("%v", v.Value))
}
return nil
}
// Set key to be equal value in this list.
func (e *EnvList) Set(key, value string) {
for i, v := range *e {
if v.Key == key {
(*e)[i].Value = value
return
}
}
*e = append(*e, &EnvVar{
Key: key,
Value: value,
})
}
func (e *EnvList) ResolveValues() error {
if len(*e) == 0 {
return nil
}
exports := ""
for i, v := range *e {
exports += v.AsExport()
cmd := exec.Command("bash", "-c", exports+"echo -n "+v.Value+";")
cwd, err := os.Getwd()
if err != nil {
return err
}
cmd.Dir = cwd
resolvedValue, err := cmd.Output()
if err != nil {
return errors.Wrapf(err, "resolving env var %v failed", v.Key)
}
(*e)[i].Value = string(resolvedValue)
}
return nil
}
func (e *EnvList) AsExport() string {
// Process all ENVs into a string of form
// `export FOO="bar"; export BAR="baz";`.
exports := ``
for _, v := range *e {
exports += v.AsExport() + " "
}
return exports
}
type ErrMustUpdate struct {
Msg string
}
type ErrUnsupportedSupfileVersion struct {
Msg string
}
func (e ErrMustUpdate) Error() string {
return fmt.Sprintf("%v\n\nPlease update stup", e.Msg)
}
func (e ErrUnsupportedSupfileVersion) Error() string {
return fmt.Sprintf("%v\n\nCheck your Stupfile version", e.Msg)
}
// NewStupfile parses configuration file and returns Supfile or error.
func NewStupfile(data []byte) (*Stupfile, error) {
var conf Stupfile
if err := yaml.Unmarshal(data, &conf); err != nil {
return nil, err
}
// API backward compatibility. Will be deprecated in v1.0.
switch conf.Version {
case "":
conf.Version = "1"
fallthrough
case "0":
for _, cmd := range conf.Commands.cmds {
if cmd.Once {
return nil, ErrMustUpdate{"command.once is not supported in Supfile v" + conf.Version}
}
if cmd.Local != "" {
return nil, ErrMustUpdate{"command.local is not supported in Supfile v" + conf.Version}
}
if cmd.Serial != 0 {
return nil, ErrMustUpdate{"command.serial is not supported in Supfile v" + conf.Version}
}
}
for _, network := range conf.Networks.nets {
if network.Inventory != "" {
return nil, ErrMustUpdate{"network.inventory is not supported in Supfile v" + conf.Version}
}
}
fallthrough
case "1":
default:
return nil, ErrUnsupportedSupfileVersion{"unsupported Supfile version " + conf.Version}
}
return &conf, nil
}
// ParseInventory runs the inventory command, if provided, and appends
// the command's output lines to the manually defined list of hosts.
func (n Network) ParseInventory() ([]Instance, error) {
if n.Inventory == "" {
return nil, nil
}
cmd := exec.Command("/bin/sh", "-c", n.Inventory)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, n.Env.Slice()...)
cmd.Stderr = os.Stderr
output, err := cmd.Output()
if err != nil {
return nil, err
}
var hosts []Instance
if err := yaml.Unmarshal(output, hosts); err != nil {
return nil, err
}
return hosts, nil
}