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 }