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