github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/e2e/e2eutil/cli.go (about) 1 package e2eutil 2 3 import ( 4 "fmt" 5 "os/exec" 6 "regexp" 7 "strings" 8 ) 9 10 // Command sends a command line argument to Nomad and returns the unbuffered 11 // stdout as a string (or, if there's an error, the stderr) 12 func Command(cmd string, args ...string) (string, error) { 13 bytes, err := exec.Command(cmd, args...).CombinedOutput() 14 out := string(bytes) 15 if err != nil { 16 return out, fmt.Errorf("command %v %v failed: %v\nOutput: %v", cmd, args, err, out) 17 } 18 return out, err 19 } 20 21 // GetField returns the value of an output field (ex. the "Submit Date" field 22 // of `nomad job status :id`) 23 func GetField(output, key string) (string, error) { 24 re := regexp.MustCompile(`(?m)^` + key + ` += (.*)$`) 25 match := re.FindStringSubmatch(output) 26 if match == nil { 27 return "", fmt.Errorf("could not find field %q", key) 28 } 29 return match[1], nil 30 } 31 32 // GetSection returns a section, with its field header but without its title. 33 // (ex. the Allocations section of `nomad job status :id`) 34 func GetSection(output, key string) (string, error) { 35 36 // golang's regex engine doesn't support negative lookahead, so 37 // we can't stop at 2 newlines if we also want a section that includes 38 // single newlines. so split on the section title, and then split a second time 39 // on \n\n 40 re := regexp.MustCompile(`(?ms)^` + key + `\n(.*)`) 41 match := re.FindStringSubmatch(output) 42 if match == nil { 43 return "", fmt.Errorf("could not find section %q", key) 44 } 45 tail := match[1] 46 return strings.Split(tail, "\n\n")[0], nil 47 } 48 49 // ParseColumns maps the CLI output for a columized section (without title) to 50 // a slice of key->value pairs for each row in that section. 51 // (ex. the Allocations section of `nomad job status :id`) 52 func ParseColumns(section string) ([]map[string]string, error) { 53 parsed := []map[string]string{} 54 55 // field names and values are deliminated by two or more spaces, but can have a 56 // single space themselves. compress all the delimiters into a tab so we can 57 // break the fields on that 58 re := regexp.MustCompile(" {2,}") 59 section = re.ReplaceAllString(section, "\t") 60 rows := strings.Split(section, "\n") 61 62 breakFields := func(row string) []string { 63 return strings.FieldsFunc(row, func(c rune) bool { return c == '\t' }) 64 } 65 66 fieldNames := breakFields(rows[0]) 67 68 for _, row := range rows[1:] { 69 if row == "" { 70 continue 71 } 72 r := map[string]string{} 73 vals := breakFields(row) 74 for i, val := range vals { 75 if i >= len(fieldNames) { 76 return parsed, fmt.Errorf("section is misaligned with header\n%v", section) 77 } 78 79 r[fieldNames[i]] = val 80 } 81 parsed = append(parsed, r) 82 } 83 return parsed, nil 84 } 85 86 // ParseFields maps the CLI output for a key-value section (without title) to 87 // map of the key->value pairs in that section 88 // (ex. the Latest Deployment section of `nomad job status :id`) 89 func ParseFields(section string) (map[string]string, error) { 90 parsed := map[string]string{} 91 rows := strings.Split(strings.TrimSpace(section), "\n") 92 for _, row := range rows { 93 kv := strings.Split(row, "=") 94 if len(kv) == 0 { 95 continue 96 } 97 key := strings.TrimSpace(kv[0]) 98 if len(kv) == 1 { 99 parsed[key] = "" 100 } else { 101 parsed[key] = strings.TrimSpace(strings.Join(kv[1:], " ")) 102 } 103 } 104 return parsed, nil 105 }