github.com/hernad/nomad@v1.6.112/e2e/e2eutil/allocs.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package e2eutil
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"reflect"
    10  	"strconv"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	api "github.com/hernad/nomad/api"
    16  	"github.com/hernad/nomad/testutil"
    17  	"github.com/kr/pretty"
    18  	"github.com/shoenig/test/must"
    19  	"github.com/shoenig/test/wait"
    20  )
    21  
    22  // AllocsByName sorts allocs by Name
    23  type AllocsByName []*api.AllocationListStub
    24  
    25  func (a AllocsByName) Len() int {
    26  	return len(a)
    27  }
    28  
    29  func (a AllocsByName) Less(i, j int) bool {
    30  	return a[i].Name < a[j].Name
    31  }
    32  
    33  func (a AllocsByName) Swap(i, j int) {
    34  	a[i], a[j] = a[j], a[i]
    35  }
    36  
    37  // WaitForAllocStatusExpected polls 'nomad job status' and exactly compares
    38  // the status of all allocations (including any previous versions) against the
    39  // expected list.
    40  func WaitForAllocStatusExpected(jobID, ns string, expected []string) error {
    41  	err := WaitForAllocStatusComparison(
    42  		func() ([]string, error) { return AllocStatuses(jobID, ns) },
    43  		func(got []string) bool { return reflect.DeepEqual(got, expected) },
    44  		nil,
    45  	)
    46  	if err != nil {
    47  		allocs, _ := AllocsForJob(jobID, ns)
    48  		err = fmt.Errorf("%v\nallocs: %v", err, pretty.Sprint(allocs))
    49  	}
    50  	return err
    51  }
    52  
    53  // WaitForAllocStatusComparison is a convenience wrapper that polls the query
    54  // function until the comparison function returns true.
    55  func WaitForAllocStatusComparison(query func() ([]string, error), comparison func([]string) bool, wc *WaitConfig) error {
    56  	var got []string
    57  	var err error
    58  	interval, retries := wc.OrDefault()
    59  	testutil.WaitForResultRetries(retries, func() (bool, error) {
    60  		time.Sleep(interval)
    61  		got, err = query()
    62  		if err != nil {
    63  			return false, err
    64  		}
    65  		return comparison(got), nil
    66  	}, func(e error) {
    67  		err = fmt.Errorf("alloc status check failed: got %#v", got)
    68  	})
    69  	return err
    70  }
    71  
    72  // SingleAllocID returns the ID for the first allocation found for jobID in namespace
    73  // at the specified job version number. Will retry for ten seconds before returning
    74  // an error.
    75  //
    76  // Should only be used with jobs containing a single task group.
    77  func SingleAllocID(t *testing.T, jobID, namespace string, version int) string {
    78  	var id string
    79  	f := func() error {
    80  		allocations, err := AllocsForJob(jobID, namespace)
    81  		if err != nil {
    82  			return err
    83  		}
    84  		for _, m := range allocations {
    85  			if m["Version"] == strconv.Itoa(version) {
    86  				id = m["ID"]
    87  				return nil
    88  			}
    89  		}
    90  		return fmt.Errorf("id not found for %s/%s/%d", namespace, jobID, version)
    91  	}
    92  	must.Wait(t, wait.InitialSuccess(
    93  		wait.ErrorFunc(f),
    94  		wait.Timeout(10*time.Second),
    95  		wait.Gap(1*time.Second),
    96  	))
    97  	return id
    98  }
    99  
   100  // AllocsForJob returns a slice of key->value maps, each describing the values
   101  // of the 'nomad job status' Allocations section (not actual
   102  // structs.Allocation objects, query the API if you want those)
   103  func AllocsForJob(jobID, ns string) ([]map[string]string, error) {
   104  	var nsArg = []string{}
   105  	if ns != "" {
   106  		nsArg = []string{"-namespace", ns}
   107  	}
   108  
   109  	cmd := []string{"nomad", "job", "status"}
   110  	params := []string{"-verbose", "-all-allocs", jobID}
   111  	cmd = append(cmd, nsArg...)
   112  	cmd = append(cmd, params...)
   113  
   114  	out, err := Command(cmd[0], cmd[1:]...)
   115  	if err != nil {
   116  		return nil, fmt.Errorf("'nomad job status' failed: %w", err)
   117  	}
   118  
   119  	section, err := GetSection(out, "Allocations")
   120  	if err != nil {
   121  		return nil, fmt.Errorf("could not find Allocations section: %w", err)
   122  	}
   123  
   124  	allocs, err := ParseColumns(section)
   125  	if err != nil {
   126  		return nil, fmt.Errorf("could not parse Allocations section: %w", err)
   127  	}
   128  	return allocs, nil
   129  }
   130  
   131  // AllocTaskEventsForJob returns a map of allocation IDs containing a map of
   132  // Task Event key value pairs
   133  func AllocTaskEventsForJob(jobID, ns string) (map[string][]map[string]string, error) {
   134  	allocs, err := AllocsForJob(jobID, ns)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	results := make(map[string][]map[string]string)
   140  	for _, alloc := range allocs {
   141  		results[alloc["ID"]] = make([]map[string]string, 0)
   142  
   143  		cmd := []string{"nomad", "alloc", "status", alloc["ID"]}
   144  		out, err := Command(cmd[0], cmd[1:]...)
   145  		if err != nil {
   146  			return nil, fmt.Errorf("querying alloc status: %w", err)
   147  		}
   148  
   149  		section, err := GetSection(out, "Recent Events:")
   150  		if err != nil {
   151  			return nil, fmt.Errorf("could not find Recent Events section: %w", err)
   152  		}
   153  
   154  		events, err := ParseColumns(section)
   155  		if err != nil {
   156  			return nil, fmt.Errorf("could not parse recent events section: %w", err)
   157  		}
   158  		results[alloc["ID"]] = events
   159  	}
   160  
   161  	return results, nil
   162  }
   163  
   164  // AllocsForNode returns a slice of key->value maps, each describing the values
   165  // of the 'nomad node status' Allocations section (not actual
   166  // structs.Allocation objects, query the API if you want those)
   167  func AllocsForNode(nodeID string) ([]map[string]string, error) {
   168  
   169  	out, err := Command("nomad", "node", "status", "-verbose", nodeID)
   170  	if err != nil {
   171  		return nil, fmt.Errorf("'nomad node status' failed: %w", err)
   172  	}
   173  
   174  	section, err := GetSection(out, "Allocations")
   175  	if err != nil {
   176  		return nil, fmt.Errorf("could not find Allocations section: %w", err)
   177  	}
   178  
   179  	allocs, err := ParseColumns(section)
   180  	if err != nil {
   181  		return nil, fmt.Errorf("could not parse Allocations section: %w", err)
   182  	}
   183  	return allocs, nil
   184  }
   185  
   186  // AllocStatuses returns a slice of client statuses
   187  func AllocStatuses(jobID, ns string) ([]string, error) {
   188  
   189  	allocs, err := AllocsForJob(jobID, ns)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	statuses := []string{}
   195  	for _, alloc := range allocs {
   196  		statuses = append(statuses, alloc["Status"])
   197  	}
   198  	return statuses, nil
   199  }
   200  
   201  // AllocStatusesRescheduled is a helper function that pulls
   202  // out client statuses only from rescheduled allocs.
   203  func AllocStatusesRescheduled(jobID, ns string) ([]string, error) {
   204  
   205  	var nsArg = []string{}
   206  	if ns != "" {
   207  		nsArg = []string{"-namespace", ns}
   208  	}
   209  
   210  	cmd := []string{"nomad", "job", "status"}
   211  	params := []string{"-verbose", jobID}
   212  	cmd = append(cmd, nsArg...)
   213  	cmd = append(cmd, params...)
   214  
   215  	out, err := Command(cmd[0], cmd[1:]...)
   216  	if err != nil {
   217  		return nil, fmt.Errorf("nomad job status failed: %w", err)
   218  	}
   219  
   220  	section, err := GetSection(out, "Allocations")
   221  	if err != nil {
   222  		return nil, fmt.Errorf("could not find Allocations section: %w", err)
   223  	}
   224  
   225  	allocs, err := ParseColumns(section)
   226  	if err != nil {
   227  		return nil, fmt.Errorf("could not parse Allocations section: %w", err)
   228  	}
   229  
   230  	statuses := []string{}
   231  	for _, alloc := range allocs {
   232  
   233  		allocID := alloc["ID"]
   234  
   235  		cmd := []string{"nomad", "alloc", "status"}
   236  		params := []string{"-json", allocID}
   237  		cmd = append(cmd, nsArg...)
   238  		cmd = append(cmd, params...)
   239  
   240  		// reschedule tracker isn't exposed in the normal CLI output
   241  		out, err := Command(cmd[0], cmd[1:]...)
   242  		if err != nil {
   243  			return nil, fmt.Errorf("nomad alloc status failed: %w", err)
   244  		}
   245  
   246  		dec := json.NewDecoder(strings.NewReader(out))
   247  		alloc := &api.Allocation{}
   248  		err = dec.Decode(alloc)
   249  		if err != nil {
   250  			return nil, fmt.Errorf("could not decode alloc status JSON: %w", err)
   251  		}
   252  
   253  		if (alloc.RescheduleTracker != nil &&
   254  			len(alloc.RescheduleTracker.Events) > 0) || alloc.FollowupEvalID != "" {
   255  			statuses = append(statuses, alloc.ClientStatus)
   256  		}
   257  	}
   258  	return statuses, nil
   259  }
   260  
   261  type LogStream int
   262  
   263  const (
   264  	LogsStdErr LogStream = iota
   265  	LogsStdOut
   266  )
   267  
   268  func AllocLogs(allocID, namespace string, logStream LogStream) (string, error) {
   269  	cmd := []string{"nomad", "alloc", "logs"}
   270  	if logStream == LogsStdErr {
   271  		cmd = append(cmd, "-stderr")
   272  	}
   273  	if namespace != "" {
   274  		cmd = append(cmd, "-namespace", namespace)
   275  	}
   276  	cmd = append(cmd, allocID)
   277  	return Command(cmd[0], cmd[1:]...)
   278  }
   279  
   280  // AllocChecks returns the CLI output from 'nomad alloc checks' on the given
   281  // alloc ID.
   282  func AllocChecks(allocID string) (string, error) {
   283  	cmd := []string{"nomad", "alloc", "checks", allocID}
   284  	return Command(cmd[0], cmd[1:]...)
   285  }
   286  
   287  func AllocTaskLogs(allocID, task string, logStream LogStream) (string, error) {
   288  	cmd := []string{"nomad", "alloc", "logs"}
   289  	if logStream == LogsStdErr {
   290  		cmd = append(cmd, "-stderr")
   291  	}
   292  	cmd = append(cmd, allocID, task)
   293  	return Command(cmd[0], cmd[1:]...)
   294  }
   295  
   296  // AllocExec is a convenience wrapper that runs 'nomad alloc exec' with the
   297  // passed execCmd via '/bin/sh -c', retrying if the task isn't ready
   298  func AllocExec(allocID, taskID, execCmd, ns string, wc *WaitConfig) (string, error) {
   299  	var got string
   300  	var err error
   301  	interval, retries := wc.OrDefault()
   302  
   303  	var nsArg = []string{}
   304  	if ns != "" {
   305  		nsArg = []string{"-namespace", ns}
   306  	}
   307  
   308  	cmd := []string{"nomad", "exec"}
   309  	params := []string{"-task", taskID, allocID, "/bin/sh", "-c", execCmd}
   310  	cmd = append(cmd, nsArg...)
   311  	cmd = append(cmd, params...)
   312  
   313  	testutil.WaitForResultRetries(retries, func() (bool, error) {
   314  		time.Sleep(interval)
   315  		got, err = Command(cmd[0], cmd[1:]...)
   316  		return err == nil, err
   317  	}, func(e error) {
   318  		err = fmt.Errorf("exec failed: '%s': %v\nGot: %v", strings.Join(cmd, " "), e, got)
   319  	})
   320  	return got, err
   321  }
   322  
   323  // WaitForAllocFile is a helper that grabs a file via alloc fs and tests its
   324  // contents; useful for checking the results of rendered templates
   325  func WaitForAllocFile(allocID, path string, test func(string) bool, wc *WaitConfig) error {
   326  	var err error
   327  	var out string
   328  	interval, retries := wc.OrDefault()
   329  
   330  	testutil.WaitForResultRetries(retries, func() (bool, error) {
   331  		time.Sleep(interval)
   332  		out, err = Command("nomad", "alloc", "fs", allocID, path)
   333  		if err != nil {
   334  			return false, fmt.Errorf("could not get file %q from allocation %q: %v",
   335  				path, allocID, err)
   336  		}
   337  		return test(out), nil
   338  	}, func(e error) {
   339  		err = fmt.Errorf("test for file content failed: got %#v\nerror: %v", out, e)
   340  	})
   341  	return err
   342  }