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 }