forked from cerc-io/ipld-eth-server
367 lines
8.1 KiB
Go
367 lines
8.1 KiB
Go
package sup
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// Supfile represents the Stack Up configuration YAML file.
|
|
type Supfile struct {
|
|
Networks Networks `yaml:"networks"`
|
|
Commands Commands `yaml:"commands"`
|
|
Targets Targets `yaml:"targets"`
|
|
Env EnvList `yaml:"env"`
|
|
Version string `yaml:"version"`
|
|
}
|
|
|
|
// Network is group of hosts with extra custom env vars.
|
|
type Network struct {
|
|
Env EnvList `yaml:"env"`
|
|
Inventory string `yaml:"inventory"`
|
|
Hosts []string `yaml:"hosts"`
|
|
Bastion string `yaml:"bastion"` // Jump host for the environment
|
|
|
|
// Should these live on Hosts too? We'd have to change []string to struct, even in Supfile.
|
|
User string // `yaml:"user"`
|
|
IdentityFile string // `yaml:"identity_file"`
|
|
}
|
|
|
|
// 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.
|
|
|
|
// API backward compatibility. Will be deprecated in v1.0.
|
|
RunOnce bool `yaml:"run_once"` // The command should be run once only.
|
|
}
|
|
|
|
// 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 sup by `go get -u github.com/pressly/sup/cmd/sup`", e.Msg)
|
|
}
|
|
|
|
func (e ErrUnsupportedSupfileVersion) Error() string {
|
|
return fmt.Sprintf("%v\n\nCheck your Supfile version (available latest version: v0.5)", e.Msg)
|
|
}
|
|
|
|
// NewSupfile parses configuration file and returns Supfile or error.
|
|
func NewSupfile(data []byte) (*Supfile, error) {
|
|
var conf Supfile
|
|
|
|
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 = "0.1"
|
|
fallthrough
|
|
|
|
case "0.1":
|
|
for _, cmd := range conf.Commands.cmds {
|
|
if cmd.RunOnce {
|
|
return nil, ErrMustUpdate{"command.run_once is not supported in Supfile v" + conf.Version}
|
|
}
|
|
}
|
|
fallthrough
|
|
|
|
case "0.2":
|
|
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 "0.3":
|
|
var warning string
|
|
for key, cmd := range conf.Commands.cmds {
|
|
if cmd.RunOnce {
|
|
warning = "Warning: command.run_once was deprecated by command.once in Supfile v" + conf.Version + "\n"
|
|
cmd.Once = true
|
|
conf.Commands.cmds[key] = cmd
|
|
}
|
|
}
|
|
if warning != "" {
|
|
fmt.Fprintf(os.Stderr, warning)
|
|
}
|
|
|
|
fallthrough
|
|
|
|
case "0.4", "0.5":
|
|
|
|
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() ([]string, 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 []string
|
|
buf := bytes.NewBuffer(output)
|
|
for {
|
|
host, err := buf.ReadString('\n')
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
host = strings.TrimSpace(host)
|
|
// skip empty lines and comments
|
|
if host == "" || host[:1] == "#" {
|
|
continue
|
|
}
|
|
|
|
hosts = append(hosts, host)
|
|
}
|
|
return hosts, nil
|
|
}
|