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