github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/java/driver_test.go (about)

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