github.com/hernad/nomad@v1.6.112/e2e/e2eutil/job.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  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/shoenig/test"
    19  )
    20  
    21  // Register registers a jobspec from a file but with a unique ID.
    22  // The caller is responsible for recording that ID for later cleanup.
    23  func Register(jobID, jobFilePath string) error {
    24  	_, err := RegisterGetOutput(jobID, jobFilePath)
    25  	return err
    26  }
    27  
    28  // RegisterGetOutput registers a jobspec from a file but with a unique ID.
    29  // The caller is responsible for recording that ID for later cleanup.
    30  // Also returns the CLI output from running 'job run'.
    31  func RegisterGetOutput(jobID, jobFilePath string) (string, error) {
    32  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    33  	defer cancel()
    34  	b, err := execCmd(jobID, jobFilePath, exec.CommandContext(ctx, "nomad", "job", "run", "-detach", "-"))
    35  	return string(b), err
    36  }
    37  
    38  // RegisterWithArgs registers a jobspec from a file but with a unique ID. The
    39  // optional args are added to the run command. The caller is responsible for
    40  // recording that ID for later cleanup.
    41  func RegisterWithArgs(jobID, jobFilePath string, args ...string) error {
    42  
    43  	baseArgs := []string{"job", "run", "-detach"}
    44  	baseArgs = append(baseArgs, args...)
    45  	baseArgs = append(baseArgs, "-")
    46  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    47  	defer cancel()
    48  	_, err := execCmd(jobID, jobFilePath, exec.CommandContext(ctx, "nomad", baseArgs...))
    49  	return err
    50  }
    51  
    52  // Revert reverts the job to the given version.
    53  func Revert(jobID, jobFilePath string, version int) error {
    54  	args := []string{"job", "revert", "-detach", jobID, strconv.Itoa(version)}
    55  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    56  	defer cancel()
    57  	_, err := execCmd(jobID, jobFilePath, exec.CommandContext(ctx, "nomad", args...))
    58  	return err
    59  }
    60  
    61  func execCmd(jobID, jobFilePath string, cmd *exec.Cmd) ([]byte, error) {
    62  	stdin, err := cmd.StdinPipe()
    63  	if err != nil {
    64  		return nil, fmt.Errorf("could not open stdin?: %w", err)
    65  	}
    66  
    67  	content, err := os.ReadFile(jobFilePath)
    68  	if err != nil {
    69  		return nil, fmt.Errorf("could not open job file: %w", err)
    70  	}
    71  
    72  	// hack off the job block to replace with our unique ID
    73  	var re = regexp.MustCompile(`(?m)^job ".*" \{`)
    74  	jobspec := re.ReplaceAllString(string(content),
    75  		fmt.Sprintf("job \"%s\" {", jobID))
    76  
    77  	go func() {
    78  		defer func() {
    79  			_ = stdin.Close()
    80  		}()
    81  		_, _ = io.WriteString(stdin, jobspec)
    82  	}()
    83  
    84  	out, err := cmd.CombinedOutput()
    85  	if err != nil {
    86  		return out, fmt.Errorf("could not register job: %w\n%v", err, string(out))
    87  	}
    88  	return out, nil
    89  }
    90  
    91  // PeriodicForce forces a periodic job to dispatch
    92  func PeriodicForce(jobID string) error {
    93  	// nomad job periodic force
    94  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    95  	defer cancel()
    96  	cmd := exec.CommandContext(ctx, "nomad", "job", "periodic", "force", jobID)
    97  
    98  	out, err := cmd.CombinedOutput()
    99  	if err != nil {
   100  		return fmt.Errorf("could not register job: %w\n%v", err, string(out))
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  // Dispatch dispatches a parameterized job
   107  func Dispatch(jobID string, meta map[string]string, payload string) error {
   108  	// nomad job periodic force
   109  	args := []string{"job", "dispatch"}
   110  	for k, v := range meta {
   111  		args = append(args, "-meta", fmt.Sprintf("%v=%v", k, v))
   112  	}
   113  	args = append(args, jobID)
   114  	if payload != "" {
   115  		args = append(args, "-")
   116  	}
   117  
   118  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
   119  	defer cancel()
   120  	cmd := exec.CommandContext(ctx, "nomad", args...)
   121  	cmd.Stdin = strings.NewReader(payload)
   122  
   123  	out, err := cmd.CombinedOutput()
   124  	if err != nil {
   125  		return fmt.Errorf("could not dispatch job: %w\n%v", err, string(out))
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  // JobInspectTemplate runs nomad job inspect and formats the output
   132  // using the specified go template
   133  func JobInspectTemplate(jobID, template string) (string, error) {
   134  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
   135  	defer cancel()
   136  	cmd := exec.CommandContext(ctx, "nomad", "job", "inspect", "-t", template, jobID)
   137  	out, err := cmd.CombinedOutput()
   138  	if err != nil {
   139  		return "", fmt.Errorf("could not inspect job: %w\n%v", err, string(out))
   140  	}
   141  	outStr := string(out)
   142  	outStr = strings.TrimSuffix(outStr, "\n")
   143  	return outStr, nil
   144  }
   145  
   146  // RegisterFromJobspec registers a jobspec from a string, also with a unique
   147  // ID. The caller is responsible for recording that ID for later cleanup.
   148  func RegisterFromJobspec(jobID, jobspec string) error {
   149  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
   150  	defer cancel()
   151  	cmd := exec.CommandContext(ctx, "nomad", "job", "run", "-detach", "-")
   152  	stdin, err := cmd.StdinPipe()
   153  	if err != nil {
   154  		return fmt.Errorf("could not open stdin?: %w", err)
   155  	}
   156  
   157  	// hack off the first line to replace with our unique ID
   158  	var re = regexp.MustCompile(`^job "\w+" \{`)
   159  	jobspec = re.ReplaceAllString(jobspec,
   160  		fmt.Sprintf("job \"%s\" {", jobID))
   161  
   162  	go func() {
   163  		defer stdin.Close()
   164  		io.WriteString(stdin, jobspec)
   165  	}()
   166  
   167  	out, err := cmd.CombinedOutput()
   168  	if err != nil {
   169  		return fmt.Errorf("could not register job: %w\n%v", err, string(out))
   170  	}
   171  	return nil
   172  }
   173  
   174  func ChildrenJobSummary(jobID string) ([]map[string]string, error) {
   175  	out, err := Command("nomad", "job", "status", jobID)
   176  	if err != nil {
   177  		return nil, fmt.Errorf("nomad job status failed: %w", err)
   178  	}
   179  
   180  	section, err := GetSection(out, "Children Job Summary")
   181  	if err != nil {
   182  		section, err = GetSection(out, "Parameterized Job Summary")
   183  		if err != nil {
   184  			return nil, fmt.Errorf("could not find children job summary section: %w", err)
   185  		}
   186  	}
   187  
   188  	summary, err := ParseColumns(section)
   189  	if err != nil {
   190  		return nil, fmt.Errorf("could not parse children job summary section: %w", err)
   191  	}
   192  
   193  	return summary, nil
   194  }
   195  
   196  func PreviouslyLaunched(jobID string) ([]map[string]string, error) {
   197  	out, err := Command("nomad", "job", "status", jobID)
   198  	if err != nil {
   199  		return nil, fmt.Errorf("nomad job status failed: %w", err)
   200  	}
   201  
   202  	section, err := GetSection(out, "Previously Launched Jobs")
   203  	if err != nil {
   204  		return nil, fmt.Errorf("could not find previously launched jobs section: %w", err)
   205  	}
   206  
   207  	summary, err := ParseColumns(section)
   208  	if err != nil {
   209  		return nil, fmt.Errorf("could not parse previously launched jobs section: %w", err)
   210  	}
   211  
   212  	return summary, nil
   213  }
   214  
   215  func DispatchedJobs(jobID string) ([]map[string]string, error) {
   216  	out, err := Command("nomad", "job", "status", jobID)
   217  	if err != nil {
   218  		return nil, fmt.Errorf("nomad job status failed: %w", err)
   219  	}
   220  
   221  	section, err := GetSection(out, "Dispatched Jobs")
   222  	if err != nil {
   223  		return nil, fmt.Errorf("could not find previously launched jobs section: %w", err)
   224  	}
   225  
   226  	summary, err := ParseColumns(section)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("could not parse previously launched jobs section: %w", err)
   229  	}
   230  
   231  	return summary, nil
   232  }
   233  
   234  func StopJob(jobID string, args ...string) error {
   235  
   236  	// Build our argument list in the correct order, ensuring the jobID is last
   237  	// and the Nomad subcommand are first.
   238  	baseArgs := []string{"job", "stop"}
   239  	baseArgs = append(baseArgs, args...)
   240  	baseArgs = append(baseArgs, jobID)
   241  
   242  	// Execute the command. We do not care about the stdout, only stderr.
   243  	_, err := Command("nomad", baseArgs...)
   244  
   245  	if err != nil {
   246  		// When stopping a job and monitoring the resulting deployment, we
   247  		// expect that the monitor fails and exits with status code one because
   248  		// technically the deployment has failed. Overwrite the error to be
   249  		// nil.
   250  		if strings.Contains(err.Error(), "Description = Cancelled because job is stopped") ||
   251  			strings.Contains(err.Error(), "Description = Failed due to progress deadline") {
   252  			err = nil
   253  		}
   254  	}
   255  	return err
   256  }
   257  
   258  // CleanupJobsAndGC stops and purges the list of jobIDs and runs a
   259  // system gc. Returns a func so that the return value can be used
   260  // in t.Cleanup
   261  func CleanupJobsAndGC(t *testing.T, jobIDs *[]string) func() {
   262  	return func() {
   263  		for _, jobID := range *jobIDs {
   264  			err := StopJob(jobID, "-purge", "-detach")
   265  			test.NoError(t, err)
   266  		}
   267  		_, err := Command("nomad", "system", "gc")
   268  		test.NoError(t, err)
   269  	}
   270  }
   271  
   272  // MaybeCleanupJobsAndGC stops and purges the list of jobIDs and runs a
   273  // system gc. Returns a func so that the return value can be used
   274  // in t.Cleanup. Similar to CleanupJobsAndGC, but this one does not assert
   275  // on a successful stop and gc, which is useful for tests that want to stop and
   276  // gc the jobs themselves but we want a backup Cleanup just in case.
   277  func MaybeCleanupJobsAndGC(jobIDs *[]string) func() {
   278  	return func() {
   279  		for _, jobID := range *jobIDs {
   280  			_ = StopJob(jobID, "-purge", "-detach")
   281  		}
   282  		_, _ = Command("nomad", "system", "gc")
   283  	}
   284  }
   285  
   286  // CleanupJobsAndGCWithContext stops and purges the list of jobIDs and runs a
   287  // system gc. The passed context allows callers to cancel the execution of the
   288  // cleanup as they desire. This is useful for tests which attempt to remove the
   289  // job as part of their run, but may fail before that point is reached.
   290  func CleanupJobsAndGCWithContext(t *testing.T, ctx context.Context, jobIDs *[]string) {
   291  
   292  	// Check the context before continuing. If this has been closed return,
   293  	// otherwise fallthrough and complete the work.
   294  	select {
   295  	case <-ctx.Done():
   296  		return
   297  	default:
   298  	}
   299  	for _, jobID := range *jobIDs {
   300  		err := StopJob(jobID, "-purge", "-detach")
   301  		test.NoError(t, err)
   302  	}
   303  	_, err := Command("nomad", "system", "gc")
   304  	test.NoError(t, err)
   305  }