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  }