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  }