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