Files
nomad/e2e/e2eutil/cli.go
Daniel Bennett f7adcefbb3 e2e: refactor vault secrets test (#19152)
fixes VaultSecrets test - it was failing due to a
regex mismatch (`^job` stopped matching when
copywrite headers got prepended to the jobspec).

but RegisterFromJobspec (which had the bug)
was only used in the one spot, so instead this
refactors the whole test to the v3 format
with testing.T and some additional fun stuff
that we can take advantage of with it.

some improvements:
* use a namespace
* use and extend existing test helpers
* add more test helpers
2023-11-28 10:00:27 -06:00

149 lines
4.3 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package e2eutil
import (
"context"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"testing"
"time"
"github.com/hashicorp/nomad/e2e/v3/util3"
"github.com/shoenig/test"
"github.com/shoenig/test/must"
)
// 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
}
// Commandf runs a Command but with a Sprintf-style string and args
func Commandf(format string, args ...any) (string, error) {
cmd := fmt.Sprintf(format, args...)
parts := strings.Split(cmd, " ")
return Command(parts[0], parts[1:]...)
}
// MustCommand runs a Commandf and must run without error
func MustCommand(t *testing.T, format string, args ...any) {
t.Helper()
util3.Log3(t, false, "must command: "+format, args...)
_, err := Commandf(format, args...)
must.NoError(t, err)
}
// CleanupCommand adds a Commandf to t.Cleanup
func CleanupCommand(t *testing.T, format string, args ...any) {
if os.Getenv("NOMAD_TEST_SKIPCLEANUP") == "1" {
return
}
t.Helper()
t.Cleanup(func() {
t.Helper() // yes, another Helper() because this is another nested func
util3.Log3(t, false, "cleanup command: "+format, args...)
_, err := Commandf(format, args...)
test.NoError(t, 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
}