github.com/hernad/nomad@v1.6.112/drivers/java/driver_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package java 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "testing" 14 "time" 15 16 "github.com/hernad/nomad/client/lib/cgutil" 17 18 "github.com/hernad/nomad/ci" 19 ctestutil "github.com/hernad/nomad/client/testutil" 20 "github.com/hernad/nomad/helper/pluginutils/hclutils" 21 "github.com/hernad/nomad/helper/testlog" 22 "github.com/hernad/nomad/helper/uuid" 23 "github.com/hernad/nomad/nomad/structs" 24 "github.com/hernad/nomad/plugins/drivers" 25 dtestutil "github.com/hernad/nomad/plugins/drivers/testutils" 26 "github.com/hernad/nomad/testutil" 27 "github.com/stretchr/testify/require" 28 ) 29 30 func javaCompatible(t *testing.T) { 31 ctestutil.JavaCompatible(t) 32 33 _, _, _, err := javaVersionInfo() 34 if err != nil { 35 t.Skipf("java not found; skipping: %v", err) 36 } 37 } 38 39 func TestJavaDriver_Fingerprint(t *testing.T) { 40 ci.Parallel(t) 41 javaCompatible(t) 42 43 ctx, cancel := context.WithCancel(context.Background()) 44 defer cancel() 45 46 d := NewDriver(ctx, testlog.HCLogger(t)) 47 harness := dtestutil.NewDriverHarness(t, d) 48 49 fpCh, err := harness.Fingerprint(context.Background()) 50 require.NoError(t, err) 51 52 select { 53 case fp := <-fpCh: 54 require.Equal(t, drivers.HealthStateHealthy, fp.Health) 55 detected, _ := fp.Attributes["driver.java"].GetBool() 56 require.True(t, detected) 57 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 58 require.Fail(t, "timeout receiving fingerprint") 59 } 60 } 61 62 func TestJavaDriver_Jar_Start_Wait(t *testing.T) { 63 ci.Parallel(t) 64 javaCompatible(t) 65 66 ctx, cancel := context.WithCancel(context.Background()) 67 defer cancel() 68 69 d := NewDriver(ctx, testlog.HCLogger(t)) 70 harness := dtestutil.NewDriverHarness(t, d) 71 72 tc := &TaskConfig{ 73 JarPath: "demoapp.jar", 74 Args: []string{"1"}, 75 JvmOpts: []string{"-Xmx64m", "-Xms32m"}, 76 } 77 78 task := basicTask(t, "demo-app", tc) 79 80 cleanup := harness.MkAllocDir(task, true) 81 defer cleanup() 82 83 copyFile("./test-resources/demoapp.jar", filepath.Join(task.TaskDir().Dir, "demoapp.jar"), t) 84 85 handle, _, err := harness.StartTask(task) 86 require.NoError(t, err) 87 88 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 89 require.NoError(t, err) 90 result := <-ch 91 require.Nil(t, result.Err) 92 93 require.Zero(t, result.ExitCode) 94 95 // Get the stdout of the process and assert that it's not empty 96 stdout, err := os.ReadFile(filepath.Join(task.TaskDir().LogDir, "demo-app.stdout.0")) 97 require.NoError(t, err) 98 require.Contains(t, string(stdout), "Hello") 99 100 require.NoError(t, harness.DestroyTask(task.ID, true)) 101 } 102 103 func TestJavaDriver_Jar_Stop_Wait(t *testing.T) { 104 ci.Parallel(t) 105 javaCompatible(t) 106 107 ctx, cancel := context.WithCancel(context.Background()) 108 defer cancel() 109 110 d := NewDriver(ctx, testlog.HCLogger(t)) 111 harness := dtestutil.NewDriverHarness(t, d) 112 113 tc := &TaskConfig{ 114 JarPath: "demoapp.jar", 115 Args: []string{"600"}, 116 JvmOpts: []string{"-Xmx64m", "-Xms32m"}, 117 } 118 task := basicTask(t, "demo-app", tc) 119 120 cleanup := harness.MkAllocDir(task, true) 121 defer cleanup() 122 123 copyFile("./test-resources/demoapp.jar", filepath.Join(task.TaskDir().Dir, "demoapp.jar"), t) 124 125 handle, _, err := harness.StartTask(task) 126 require.NoError(t, err) 127 128 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 129 require.NoError(t, err) 130 131 require.NoError(t, harness.WaitUntilStarted(task.ID, 1*time.Second)) 132 133 go func() { 134 time.Sleep(10 * time.Millisecond) 135 harness.StopTask(task.ID, 2*time.Second, "SIGINT") 136 }() 137 138 select { 139 case result := <-ch: 140 require.False(t, result.Successful()) 141 case <-time.After(10 * time.Second): 142 require.Fail(t, "timeout waiting for task to shutdown") 143 } 144 145 // Ensure that the task is marked as dead, but account 146 // for WaitTask() closing channel before internal state is updated 147 testutil.WaitForResult(func() (bool, error) { 148 status, err := harness.InspectTask(task.ID) 149 if err != nil { 150 return false, fmt.Errorf("inspecting task failed: %v", err) 151 } 152 if status.State != drivers.TaskStateExited { 153 return false, fmt.Errorf("task hasn't exited yet; status: %v", status.State) 154 } 155 156 return true, nil 157 }, func(err error) { 158 require.NoError(t, err) 159 }) 160 161 require.NoError(t, harness.DestroyTask(task.ID, true)) 162 } 163 164 func TestJavaDriver_Class_Start_Wait(t *testing.T) { 165 ci.Parallel(t) 166 javaCompatible(t) 167 168 ctx, cancel := context.WithCancel(context.Background()) 169 defer cancel() 170 171 d := NewDriver(ctx, testlog.HCLogger(t)) 172 harness := dtestutil.NewDriverHarness(t, d) 173 174 tc := &TaskConfig{ 175 Class: "Hello", 176 Args: []string{"1"}, 177 } 178 task := basicTask(t, "demo-app", tc) 179 180 cleanup := harness.MkAllocDir(task, true) 181 defer cleanup() 182 183 copyFile("./test-resources/Hello.class", filepath.Join(task.TaskDir().Dir, "Hello.class"), t) 184 185 handle, _, err := harness.StartTask(task) 186 require.NoError(t, err) 187 188 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 189 require.NoError(t, err) 190 result := <-ch 191 require.Nil(t, result.Err) 192 193 require.Zero(t, result.ExitCode) 194 195 // Get the stdout of the process and assert that it's not empty 196 stdout, err := os.ReadFile(filepath.Join(task.TaskDir().LogDir, "demo-app.stdout.0")) 197 require.NoError(t, err) 198 require.Contains(t, string(stdout), "Hello") 199 200 require.NoError(t, harness.DestroyTask(task.ID, true)) 201 } 202 203 func TestJavaCmdArgs(t *testing.T) { 204 ci.Parallel(t) 205 206 cases := []struct { 207 name string 208 cfg TaskConfig 209 expected []string 210 }{ 211 { 212 "jar_path_full", 213 TaskConfig{ 214 JvmOpts: []string{"-Xmx512m", "-Xms128m"}, 215 JarPath: "/jar-path.jar", 216 Args: []string{"hello", "world"}, 217 }, 218 []string{"-Xmx512m", "-Xms128m", "-jar", "/jar-path.jar", "hello", "world"}, 219 }, 220 { 221 "class_full", 222 TaskConfig{ 223 JvmOpts: []string{"-Xmx512m", "-Xms128m"}, 224 Class: "ClassName", 225 ClassPath: "/classpath", 226 Args: []string{"hello", "world"}, 227 }, 228 []string{"-Xmx512m", "-Xms128m", "-cp", "/classpath", "ClassName", "hello", "world"}, 229 }, 230 { 231 "jar_path_slim", 232 TaskConfig{ 233 JarPath: "/jar-path.jar", 234 }, 235 []string{"-jar", "/jar-path.jar"}, 236 }, 237 { 238 "class_slim", 239 TaskConfig{ 240 Class: "ClassName", 241 }, 242 []string{"ClassName"}, 243 }, 244 } 245 246 for _, c := range cases { 247 t.Run(c.name, func(t *testing.T) { 248 found := javaCmdArgs(c.cfg) 249 require.Equal(t, c.expected, found) 250 }) 251 } 252 } 253 254 func TestJavaDriver_ExecTaskStreaming(t *testing.T) { 255 ci.Parallel(t) 256 javaCompatible(t) 257 258 ctx, cancel := context.WithCancel(context.Background()) 259 defer cancel() 260 261 d := NewDriver(ctx, testlog.HCLogger(t)) 262 harness := dtestutil.NewDriverHarness(t, d) 263 defer harness.Kill() 264 265 tc := &TaskConfig{ 266 Class: "Hello", 267 Args: []string{"900"}, 268 } 269 task := basicTask(t, "demo-app", tc) 270 271 cleanup := harness.MkAllocDir(task, true) 272 defer cleanup() 273 274 copyFile("./test-resources/Hello.class", filepath.Join(task.TaskDir().Dir, "Hello.class"), t) 275 276 _, _, err := harness.StartTask(task) 277 require.NoError(t, err) 278 defer d.DestroyTask(task.ID, true) 279 280 dtestutil.ExecTaskStreamingConformanceTests(t, harness, task.ID) 281 282 } 283 func basicTask(t *testing.T, name string, taskConfig *TaskConfig) *drivers.TaskConfig { 284 t.Helper() 285 286 allocID := uuid.Generate() 287 task := &drivers.TaskConfig{ 288 AllocID: allocID, 289 ID: uuid.Generate(), 290 Name: name, 291 Resources: &drivers.Resources{ 292 NomadResources: &structs.AllocatedTaskResources{ 293 Memory: structs.AllocatedMemoryResources{ 294 MemoryMB: 128, 295 }, 296 Cpu: structs.AllocatedCpuResources{ 297 CpuShares: 100, 298 }, 299 }, 300 LinuxResources: &drivers.LinuxResources{ 301 MemoryLimitBytes: 134217728, 302 CPUShares: 100, 303 }, 304 }, 305 } 306 307 if cgutil.UseV2 { 308 task.Resources.LinuxResources.CpusetCgroupPath = filepath.Join(cgutil.CgroupRoot, "testing.slice", cgutil.CgroupScope(allocID, name)) 309 } 310 311 require.NoError(t, task.EncodeConcreteDriverConfig(&taskConfig)) 312 return task 313 } 314 315 // copyFile moves an existing file to the destination 316 func copyFile(src, dst string, t *testing.T) { 317 in, err := os.Open(src) 318 if err != nil { 319 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 320 } 321 defer in.Close() 322 out, err := os.Create(dst) 323 if err != nil { 324 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 325 } 326 defer func() { 327 if err := out.Close(); err != nil { 328 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 329 } 330 }() 331 if _, err = io.Copy(out, in); err != nil { 332 t.Fatalf("copying %v -> %v failed: %v", src, dst, err) 333 } 334 } 335 336 func TestConfig_ParseAllHCL(t *testing.T) { 337 ci.Parallel(t) 338 339 cfgStr := ` 340 config { 341 class = "java.main" 342 class_path = "/tmp/cp" 343 jar_path = "/tmp/jar.jar" 344 jvm_options = ["-Xmx600"] 345 args = ["arg1", "arg2"] 346 }` 347 348 expected := &TaskConfig{ 349 Class: "java.main", 350 ClassPath: "/tmp/cp", 351 JarPath: "/tmp/jar.jar", 352 JvmOpts: []string{"-Xmx600"}, 353 Args: []string{"arg1", "arg2"}, 354 } 355 356 var tc *TaskConfig 357 hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc) 358 359 require.EqualValues(t, expected, tc) 360 } 361 362 // Tests that a given DNSConfig properly configures dns 363 func Test_dnsConfig(t *testing.T) { 364 ci.Parallel(t) 365 ctestutil.RequireRoot(t) 366 javaCompatible(t) 367 require := require.New(t) 368 ctx, cancel := context.WithCancel(context.Background()) 369 defer cancel() 370 371 d := NewDriver(ctx, testlog.HCLogger(t)) 372 harness := dtestutil.NewDriverHarness(t, d) 373 defer harness.Kill() 374 375 cases := []struct { 376 name string 377 cfg *drivers.DNSConfig 378 }{ 379 { 380 name: "nil DNSConfig", 381 }, 382 { 383 name: "basic", 384 cfg: &drivers.DNSConfig{ 385 Servers: []string{"1.1.1.1", "1.0.0.1"}, 386 }, 387 }, 388 { 389 name: "full", 390 cfg: &drivers.DNSConfig{ 391 Servers: []string{"1.1.1.1", "1.0.0.1"}, 392 Searches: []string{"local.test", "node.consul"}, 393 Options: []string{"ndots:2", "edns0"}, 394 }, 395 }, 396 } 397 398 for _, c := range cases { 399 tc := &TaskConfig{ 400 Class: "Hello", 401 Args: []string{"900"}, 402 } 403 task := basicTask(t, "demo-app", tc) 404 task.DNS = c.cfg 405 406 cleanup := harness.MkAllocDir(task, false) 407 defer cleanup() 408 409 _, _, err := harness.StartTask(task) 410 require.NoError(err) 411 defer d.DestroyTask(task.ID, true) 412 413 dtestutil.TestTaskDNSConfig(t, harness, task.ID, c.cfg) 414 } 415 416 } 417 418 func TestDriver_Config_validate(t *testing.T) { 419 ci.Parallel(t) 420 421 t.Run("pid/ipc", func(t *testing.T) { 422 for _, tc := range []struct { 423 pidMode, ipcMode string 424 exp error 425 }{ 426 {pidMode: "host", ipcMode: "host", exp: nil}, 427 {pidMode: "private", ipcMode: "host", exp: nil}, 428 {pidMode: "host", ipcMode: "private", exp: nil}, 429 {pidMode: "private", ipcMode: "private", exp: nil}, 430 {pidMode: "other", ipcMode: "private", exp: errors.New(`default_pid_mode must be "private" or "host", got "other"`)}, 431 {pidMode: "private", ipcMode: "other", exp: errors.New(`default_ipc_mode must be "private" or "host", got "other"`)}, 432 } { 433 require.Equal(t, tc.exp, (&Config{ 434 DefaultModePID: tc.pidMode, 435 DefaultModeIPC: tc.ipcMode, 436 }).validate()) 437 } 438 }) 439 440 t.Run("allow_caps", func(t *testing.T) { 441 for _, tc := range []struct { 442 ac []string 443 exp error 444 }{ 445 {ac: []string{}, exp: nil}, 446 {ac: []string{"all"}, exp: nil}, 447 {ac: []string{"chown", "sys_time"}, exp: nil}, 448 {ac: []string{"CAP_CHOWN", "cap_sys_time"}, exp: nil}, 449 {ac: []string{"chown", "not_valid", "sys_time"}, exp: errors.New("allow_caps configured with capabilities not supported by system: not_valid")}, 450 } { 451 require.Equal(t, tc.exp, (&Config{ 452 DefaultModePID: "private", 453 DefaultModeIPC: "private", 454 AllowCaps: tc.ac, 455 }).validate()) 456 } 457 }) 458 } 459 460 func TestDriver_TaskConfig_validate(t *testing.T) { 461 ci.Parallel(t) 462 463 t.Run("pid/ipc", func(t *testing.T) { 464 for _, tc := range []struct { 465 pidMode, ipcMode string 466 exp error 467 }{ 468 {pidMode: "host", ipcMode: "host", exp: nil}, 469 {pidMode: "host", ipcMode: "private", exp: nil}, 470 {pidMode: "host", ipcMode: "", exp: nil}, 471 {pidMode: "host", ipcMode: "other", exp: errors.New(`ipc_mode must be "private" or "host", got "other"`)}, 472 473 {pidMode: "host", ipcMode: "host", exp: nil}, 474 {pidMode: "private", ipcMode: "host", exp: nil}, 475 {pidMode: "", ipcMode: "host", exp: nil}, 476 {pidMode: "other", ipcMode: "host", exp: errors.New(`pid_mode must be "private" or "host", got "other"`)}, 477 } { 478 require.Equal(t, tc.exp, (&TaskConfig{ 479 ModePID: tc.pidMode, 480 ModeIPC: tc.ipcMode, 481 }).validate()) 482 } 483 }) 484 485 t.Run("cap_add", func(t *testing.T) { 486 for _, tc := range []struct { 487 adds []string 488 exp error 489 }{ 490 {adds: nil, exp: nil}, 491 {adds: []string{"chown"}, exp: nil}, 492 {adds: []string{"CAP_CHOWN"}, exp: nil}, 493 {adds: []string{"chown", "sys_time"}, exp: nil}, 494 {adds: []string{"chown", "not_valid", "sys_time"}, exp: errors.New("cap_add configured with capabilities not supported by system: not_valid")}, 495 } { 496 require.Equal(t, tc.exp, (&TaskConfig{ 497 CapAdd: tc.adds, 498 }).validate()) 499 } 500 }) 501 502 t.Run("cap_drop", func(t *testing.T) { 503 for _, tc := range []struct { 504 drops []string 505 exp error 506 }{ 507 {drops: nil, exp: nil}, 508 {drops: []string{"chown"}, exp: nil}, 509 {drops: []string{"CAP_CHOWN"}, exp: nil}, 510 {drops: []string{"chown", "sys_time"}, exp: nil}, 511 {drops: []string{"chown", "not_valid", "sys_time"}, exp: errors.New("cap_drop configured with capabilities not supported by system: not_valid")}, 512 } { 513 require.Equal(t, tc.exp, (&TaskConfig{ 514 CapDrop: tc.drops, 515 }).validate()) 516 } 517 }) 518 }