9dcdafe4cf
When a command fails, it's nice to have the full output, as it contains diagnostic information. The status code isn't sufficient for debugging.
106 lines
3.1 KiB
Go
106 lines
3.1 KiB
Go
package e2eutil
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Command sends a command line argument to Nomad and returns the unbuffered
|
|
// stdout as a string (or, if there's an error, the stderr)
|
|
func Command(cmd string, args ...string) (string, error) {
|
|
bytes, err := exec.Command(cmd, args...).CombinedOutput()
|
|
out := string(bytes)
|
|
if err != nil {
|
|
return out, fmt.Errorf("command %v %v failed: %v\nOutput: %v", cmd, args, err, out)
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// GetField returns the value of an output field (ex. the "Submit Date" field
|
|
// of `nomad job status :id`)
|
|
func GetField(output, key string) (string, error) {
|
|
re := regexp.MustCompile(`(?m)^` + key + ` += (.*)$`)
|
|
match := re.FindStringSubmatch(output)
|
|
if match == nil {
|
|
return "", fmt.Errorf("could not find field %q", key)
|
|
}
|
|
return match[1], nil
|
|
}
|
|
|
|
// GetSection returns a section, with its field header but without its title.
|
|
// (ex. the Allocations section of `nomad job status :id`)
|
|
func GetSection(output, key string) (string, error) {
|
|
|
|
// golang's regex engine doesn't support negative lookahead, so
|
|
// we can't stop at 2 newlines if we also want a section that includes
|
|
// single newlines. so split on the section title, and then split a second time
|
|
// on \n\n
|
|
re := regexp.MustCompile(`(?ms)^` + key + `\n(.*)`)
|
|
match := re.FindStringSubmatch(output)
|
|
if match == nil {
|
|
return "", fmt.Errorf("could not find section %q", key)
|
|
}
|
|
tail := match[1]
|
|
return strings.Split(tail, "\n\n")[0], nil
|
|
}
|
|
|
|
// ParseColumns maps the CLI output for a columized section (without title) to
|
|
// a slice of key->value pairs for each row in that section.
|
|
// (ex. the Allocations section of `nomad job status :id`)
|
|
func ParseColumns(section string) ([]map[string]string, error) {
|
|
parsed := []map[string]string{}
|
|
|
|
// field names and values are deliminated by two or more spaces, but can have a
|
|
// single space themselves. compress all the delimiters into a tab so we can
|
|
// break the fields on that
|
|
re := regexp.MustCompile(" {2,}")
|
|
section = re.ReplaceAllString(section, "\t")
|
|
rows := strings.Split(section, "\n")
|
|
|
|
breakFields := func(row string) []string {
|
|
return strings.FieldsFunc(row, func(c rune) bool { return c == '\t' })
|
|
}
|
|
|
|
fieldNames := breakFields(rows[0])
|
|
|
|
for _, row := range rows[1:] {
|
|
if row == "" {
|
|
continue
|
|
}
|
|
r := map[string]string{}
|
|
vals := breakFields(row)
|
|
for i, val := range vals {
|
|
if i >= len(fieldNames) {
|
|
return parsed, fmt.Errorf("section is misaligned with header\n%v", section)
|
|
}
|
|
|
|
r[fieldNames[i]] = val
|
|
}
|
|
parsed = append(parsed, r)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
// ParseFields maps the CLI output for a key-value section (without title) to
|
|
// map of the key->value pairs in that section
|
|
// (ex. the Latest Deployment section of `nomad job status :id`)
|
|
func ParseFields(section string) (map[string]string, error) {
|
|
parsed := map[string]string{}
|
|
rows := strings.Split(strings.TrimSpace(section), "\n")
|
|
for _, row := range rows {
|
|
kv := strings.Split(row, "=")
|
|
if len(kv) == 0 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(kv[0])
|
|
if len(kv) == 1 {
|
|
parsed[key] = ""
|
|
} else {
|
|
parsed[key] = strings.TrimSpace(strings.Join(kv[1:], " "))
|
|
}
|
|
}
|
|
return parsed, nil
|
|
}
|