// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package e2eutil import ( "context" "fmt" "os/exec" "regexp" "strings" "time" ) // 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) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() bytes, err := exec.CommandContext(ctx, 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 }