github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/command/helpers_test.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "os" 10 "reflect" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/hashicorp/nomad/api" 16 "github.com/hashicorp/nomad/helper" 17 "github.com/hashicorp/nomad/helper/flatmap" 18 "github.com/kr/pretty" 19 "github.com/mitchellh/cli" 20 "github.com/stretchr/testify/require" 21 ) 22 23 func TestHelpers_FormatKV(t *testing.T) { 24 t.Parallel() 25 in := []string{"alpha|beta", "charlie|delta", "echo|"} 26 out := formatKV(in) 27 28 expect := "alpha = beta\n" 29 expect += "charlie = delta\n" 30 expect += "echo = <none>" 31 32 if out != expect { 33 t.Fatalf("expect: %s, got: %s", expect, out) 34 } 35 } 36 37 func TestHelpers_FormatList(t *testing.T) { 38 t.Parallel() 39 in := []string{"alpha|beta||delta"} 40 out := formatList(in) 41 42 expect := "alpha beta <none> delta" 43 44 if out != expect { 45 t.Fatalf("expect: %s, got: %s", expect, out) 46 } 47 } 48 49 func TestHelpers_NodeID(t *testing.T) { 50 t.Parallel() 51 srv, _, _ := testServer(t, false, nil) 52 defer srv.Shutdown() 53 54 meta := Meta{Ui: cli.NewMockUi()} 55 client, err := meta.Client() 56 if err != nil { 57 t.FailNow() 58 } 59 60 // This is because there is no client 61 if _, err := getLocalNodeID(client); err == nil { 62 t.Fatalf("getLocalNodeID() should fail") 63 } 64 } 65 66 func TestHelpers_LineLimitReader_NoTimeLimit(t *testing.T) { 67 t.Parallel() 68 helloString := `hello 69 world 70 this 71 is 72 a 73 test` 74 75 noLines := "jskdfhjasdhfjkajkldsfdlsjkahfkjdsafa" 76 77 cases := []struct { 78 Input string 79 Output string 80 Lines int 81 SearchLimit int 82 }{ 83 { 84 Input: helloString, 85 Output: helloString, 86 Lines: 6, 87 SearchLimit: 1000, 88 }, 89 { 90 Input: helloString, 91 Output: `world 92 this 93 is 94 a 95 test`, 96 Lines: 5, 97 SearchLimit: 1000, 98 }, 99 { 100 Input: helloString, 101 Output: `test`, 102 Lines: 1, 103 SearchLimit: 1000, 104 }, 105 { 106 Input: helloString, 107 Output: "", 108 Lines: 0, 109 SearchLimit: 1000, 110 }, 111 { 112 Input: helloString, 113 Output: helloString, 114 Lines: 6, 115 SearchLimit: 1, // Exceed the limit 116 }, 117 { 118 Input: noLines, 119 Output: noLines, 120 Lines: 10, 121 SearchLimit: 1000, 122 }, 123 { 124 Input: noLines, 125 Output: noLines, 126 Lines: 10, 127 SearchLimit: 2, 128 }, 129 } 130 131 for i, c := range cases { 132 in := ioutil.NopCloser(strings.NewReader(c.Input)) 133 limit := NewLineLimitReader(in, c.Lines, c.SearchLimit, 0) 134 outBytes, err := ioutil.ReadAll(limit) 135 if err != nil { 136 t.Fatalf("case %d failed: %v", i, err) 137 } 138 139 out := string(outBytes) 140 if out != c.Output { 141 t.Fatalf("case %d: got %q; want %q", i, out, c.Output) 142 } 143 } 144 } 145 146 type testReadCloser struct { 147 data chan []byte 148 } 149 150 func (t *testReadCloser) Read(p []byte) (n int, err error) { 151 select { 152 case b, ok := <-t.data: 153 if !ok { 154 return 0, io.EOF 155 } 156 157 return copy(p, b), nil 158 case <-time.After(10 * time.Millisecond): 159 return 0, nil 160 } 161 } 162 163 func (t *testReadCloser) Close() error { 164 close(t.data) 165 return nil 166 } 167 168 func TestHelpers_LineLimitReader_TimeLimit(t *testing.T) { 169 t.Parallel() 170 // Create the test reader 171 in := &testReadCloser{data: make(chan []byte)} 172 173 // Set up the reader such that it won't hit the line/buffer limit and could 174 // only terminate if it hits the time limit 175 limit := NewLineLimitReader(in, 1000, 1000, 100*time.Millisecond) 176 177 expected := []byte("hello world") 178 179 errCh := make(chan error) 180 resultCh := make(chan []byte) 181 go func() { 182 defer close(resultCh) 183 defer close(errCh) 184 outBytes, err := ioutil.ReadAll(limit) 185 if err != nil { 186 errCh <- fmt.Errorf("ReadAll failed: %v", err) 187 return 188 } 189 resultCh <- outBytes 190 }() 191 192 // Send the data 193 in.data <- expected 194 in.Close() 195 196 select { 197 case err := <-errCh: 198 if err != nil { 199 t.Fatalf("ReadAll: %v", err) 200 } 201 case outBytes := <-resultCh: 202 if !reflect.DeepEqual(outBytes, expected) { 203 t.Fatalf("got:%s, expected,%s", string(outBytes), string(expected)) 204 } 205 case <-time.After(1 * time.Second): 206 t.Fatalf("did not exit by time limit") 207 } 208 } 209 210 const ( 211 job = `job "job1" { 212 type = "service" 213 datacenters = ["dc1"] 214 group "group1" { 215 count = 1 216 task "task1" { 217 driver = "exec" 218 resources {} 219 } 220 restart { 221 attempts = 10 222 mode = "delay" 223 interval = "15s" 224 } 225 } 226 }` 227 ) 228 229 var ( 230 expectedApiJob = &api.Job{ 231 ID: helper.StringToPtr("job1"), 232 Name: helper.StringToPtr("job1"), 233 Type: helper.StringToPtr("service"), 234 Datacenters: []string{"dc1"}, 235 TaskGroups: []*api.TaskGroup{ 236 { 237 Name: helper.StringToPtr("group1"), 238 Count: helper.IntToPtr(1), 239 RestartPolicy: &api.RestartPolicy{ 240 Attempts: helper.IntToPtr(10), 241 Interval: helper.TimeToPtr(15 * time.Second), 242 Mode: helper.StringToPtr("delay"), 243 }, 244 245 Tasks: []*api.Task{ 246 { 247 Driver: "exec", 248 Name: "task1", 249 Resources: &api.Resources{}, 250 }, 251 }, 252 }, 253 }, 254 } 255 ) 256 257 // Test APIJob with local jobfile 258 func TestJobGetter_LocalFile(t *testing.T) { 259 t.Parallel() 260 fh, err := ioutil.TempFile("", "nomad") 261 if err != nil { 262 t.Fatalf("err: %s", err) 263 } 264 defer os.Remove(fh.Name()) 265 _, err = fh.WriteString(job) 266 if err != nil { 267 t.Fatalf("err: %s", err) 268 } 269 270 j := &JobGetter{} 271 aj, err := j.ApiJob(fh.Name()) 272 if err != nil { 273 t.Fatalf("err: %s", err) 274 } 275 276 if !reflect.DeepEqual(expectedApiJob, aj) { 277 eflat := flatmap.Flatten(expectedApiJob, nil, false) 278 aflat := flatmap.Flatten(aj, nil, false) 279 t.Fatalf("got:\n%v\nwant:\n%v", aflat, eflat) 280 } 281 } 282 283 // TestJobGetter_LocalFile_InvalidHCL2 asserts that a custom message is emited 284 // if the file is a valid HCL1 but not HCL2 285 func TestJobGetter_LocalFile_InvalidHCL2(t *testing.T) { 286 t.Parallel() 287 288 cases := []struct { 289 name string 290 hcl string 291 expectHCL1Message bool 292 }{ 293 { 294 "invalid HCL", 295 "nothing", 296 false, 297 }, 298 { 299 "invalid HCL2", 300 `job "example" { 301 meta { "key.with.dot" = "b" } 302 }`, 303 true, 304 }, 305 } 306 307 for _, c := range cases { 308 t.Run(c.name, func(t *testing.T) { 309 fh, err := ioutil.TempFile("", "nomad") 310 require.NoError(t, err) 311 defer os.Remove(fh.Name()) 312 defer fh.Close() 313 314 _, err = fh.WriteString(c.hcl) 315 require.NoError(t, err) 316 317 j := &JobGetter{} 318 _, err = j.ApiJob(fh.Name()) 319 require.Error(t, err) 320 321 exptMessage := "Failed to parse using HCL 2. Use the HCL 1" 322 if c.expectHCL1Message { 323 require.Contains(t, err.Error(), exptMessage) 324 } else { 325 require.NotContains(t, err.Error(), exptMessage) 326 } 327 }) 328 } 329 } 330 331 // TestJobGetter_HCL2_Variables asserts variable arguments from CLI 332 // and varfiles are both honored 333 func TestJobGetter_HCL2_Variables(t *testing.T) { 334 t.Parallel() 335 336 hcl := ` 337 variables { 338 var1 = "default-val" 339 var2 = "default-val" 340 var3 = "default-val" 341 var4 = "default-val" 342 } 343 344 job "example" { 345 datacenters = ["${var.var1}", "${var.var2}", "${var.var3}", "${var.var4}"] 346 } 347 ` 348 349 os.Setenv("NOMAD_VAR_var4", "from-envvar") 350 defer os.Unsetenv("NOMAD_VAR_var4") 351 352 cliArgs := []string{`var2=from-cli`} 353 fileVars := `var3 = "from-varfile"` 354 expected := []string{"default-val", "from-cli", "from-varfile", "from-envvar"} 355 356 hclf, err := ioutil.TempFile("", "hcl") 357 require.NoError(t, err) 358 defer os.Remove(hclf.Name()) 359 defer hclf.Close() 360 361 _, err = hclf.WriteString(hcl) 362 require.NoError(t, err) 363 364 vf, err := ioutil.TempFile("", "var.hcl") 365 require.NoError(t, err) 366 defer os.Remove(vf.Name()) 367 defer vf.Close() 368 369 _, err = vf.WriteString(fileVars + "\n") 370 require.NoError(t, err) 371 372 j, err := (&JobGetter{}).ApiJobWithArgs(hclf.Name(), cliArgs, []string{vf.Name()}) 373 require.NoError(t, err) 374 375 require.NotNil(t, j) 376 require.Equal(t, expected, j.Datacenters) 377 } 378 379 // Test StructJob with jobfile from HTTP Server 380 func TestJobGetter_HTTPServer(t *testing.T) { 381 t.Parallel() 382 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 383 fmt.Fprintf(w, job) 384 }) 385 go http.ListenAndServe("127.0.0.1:12345", nil) 386 387 // Wait until HTTP Server starts certainly 388 time.Sleep(100 * time.Millisecond) 389 390 j := &JobGetter{} 391 aj, err := j.ApiJob("http://127.0.0.1:12345/") 392 if err != nil { 393 t.Fatalf("err: %s", err) 394 } 395 if !reflect.DeepEqual(expectedApiJob, aj) { 396 for _, d := range pretty.Diff(expectedApiJob, aj) { 397 t.Logf(d) 398 } 399 t.Fatalf("Unexpected file") 400 } 401 } 402 403 func TestPrettyTimeDiff(t *testing.T) { 404 // Grab the time and truncate to the nearest second. This allows our tests 405 // to be deterministic since we don't have to worry about rounding. 406 now := time.Now().Truncate(time.Second) 407 408 test_cases := []struct { 409 t1 time.Time 410 t2 time.Time 411 exp string 412 }{ 413 {now, time.Unix(0, 0), ""}, // This is the upgrade path case 414 {now, now.Add(-10 * time.Millisecond), "0s ago"}, 415 {now, now.Add(-740 * time.Second), "12m20s ago"}, 416 {now, now.Add(-12 * time.Minute), "12m ago"}, 417 {now, now.Add(-60 * time.Minute), "1h ago"}, 418 {now, now.Add(-80 * time.Minute), "1h20m ago"}, 419 {now, now.Add(-6 * time.Hour), "6h ago"}, 420 {now.Add(-6 * time.Hour), now, "6h from now"}, 421 {now, now.Add(-22165 * time.Second), "6h9m ago"}, 422 {now, now.Add(-100 * time.Hour), "4d4h ago"}, 423 {now, now.Add(-438000 * time.Minute), "10mo4d ago"}, 424 {now, now.Add(-20460 * time.Hour), "2y4mo ago"}, 425 } 426 for _, tc := range test_cases { 427 t.Run(tc.exp, func(t *testing.T) { 428 out := prettyTimeDiff(tc.t2, tc.t1) 429 if out != tc.exp { 430 t.Fatalf("expected :%v but got :%v", tc.exp, out) 431 } 432 }) 433 } 434 435 var t1 time.Time 436 out := prettyTimeDiff(t1, time.Now()) 437 438 if out != "" { 439 t.Fatalf("Expected empty output but got:%v", out) 440 } 441 442 } 443 444 // TestUiErrorWriter asserts that writer buffers and 445 func TestUiErrorWriter(t *testing.T) { 446 t.Parallel() 447 448 var outBuf, errBuf bytes.Buffer 449 ui := &cli.BasicUi{ 450 Writer: &outBuf, 451 ErrorWriter: &errBuf, 452 } 453 454 w := &uiErrorWriter{ui: ui} 455 456 inputs := []string{ 457 "some line\n", 458 "multiple\nlines\r\nhere", 459 " with followup\nand", 460 " more lines ", 461 " without new line ", 462 "until here\nand then", 463 "some more", 464 } 465 466 partialAcc := "" 467 for _, in := range inputs { 468 n, err := w.Write([]byte(in)) 469 require.NoError(t, err) 470 require.Equal(t, len(in), n) 471 472 // assert that writer emits partial result until last new line 473 partialAcc += strings.ReplaceAll(in, "\r\n", "\n") 474 lastNL := strings.LastIndex(partialAcc, "\n") 475 require.Equal(t, partialAcc[:lastNL+1], errBuf.String()) 476 } 477 478 require.Empty(t, outBuf.String()) 479 480 // note that the \r\n got replaced by \n 481 expectedErr := "some line\nmultiple\nlines\nhere with followup\nand more lines without new line until here\n" 482 require.Equal(t, expectedErr, errBuf.String()) 483 484 // close emits the final line 485 err := w.Close() 486 require.NoError(t, err) 487 488 expectedErr += "and thensome more\n" 489 require.Equal(t, expectedErr, errBuf.String()) 490 }