github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/plugins/drivers/testutils/testing.go (about)

     1  package testutils
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  	"time"
    12  
    13  	hclog "github.com/hashicorp/go-hclog"
    14  	plugin "github.com/hashicorp/go-plugin"
    15  	"github.com/hashicorp/nomad/ci"
    16  	"github.com/hashicorp/nomad/client/allocdir"
    17  	"github.com/hashicorp/nomad/client/config"
    18  	"github.com/hashicorp/nomad/client/lib/cgutil"
    19  	"github.com/hashicorp/nomad/client/logmon"
    20  	"github.com/hashicorp/nomad/client/taskenv"
    21  	"github.com/hashicorp/nomad/helper/testlog"
    22  	"github.com/hashicorp/nomad/helper/uuid"
    23  	"github.com/hashicorp/nomad/nomad/mock"
    24  	"github.com/hashicorp/nomad/nomad/structs"
    25  	"github.com/hashicorp/nomad/plugins/base"
    26  	"github.com/hashicorp/nomad/plugins/drivers"
    27  	"github.com/hashicorp/nomad/plugins/shared/hclspec"
    28  	testing "github.com/mitchellh/go-testing-interface"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  type DriverHarness struct {
    33  	drivers.DriverPlugin
    34  	client *plugin.GRPCClient
    35  	server *plugin.GRPCServer
    36  	t      testing.T
    37  	logger hclog.Logger
    38  	impl   drivers.DriverPlugin
    39  	cgroup string
    40  }
    41  
    42  func (h *DriverHarness) Impl() drivers.DriverPlugin {
    43  	return h.impl
    44  }
    45  func NewDriverHarness(t testing.T, d drivers.DriverPlugin) *DriverHarness {
    46  	logger := testlog.HCLogger(t).Named("driver_harness")
    47  	pd := drivers.NewDriverPlugin(d, logger)
    48  
    49  	client, server := plugin.TestPluginGRPCConn(t,
    50  		map[string]plugin.Plugin{
    51  			base.PluginTypeDriver: pd,
    52  			base.PluginTypeBase:   &base.PluginBase{Impl: d},
    53  			"logmon":              logmon.NewPlugin(logmon.NewLogMon(logger.Named("logmon"))),
    54  		},
    55  	)
    56  
    57  	raw, err := client.Dispense(base.PluginTypeDriver)
    58  	require.NoError(t, err, "failed to dispense plugin")
    59  
    60  	dClient := raw.(drivers.DriverPlugin)
    61  	return &DriverHarness{
    62  		client:       client,
    63  		server:       server,
    64  		DriverPlugin: dClient,
    65  		logger:       logger,
    66  		t:            t,
    67  		impl:         d,
    68  	}
    69  }
    70  
    71  // setupCgroupV2 creates a v2 cgroup for the task, as if a Client were initialized
    72  // and managing the cgroup as it normally would via the cpuset manager.
    73  //
    74  // Note that we are being lazy and trying to avoid importing cgutil because
    75  // currently plugins/drivers/testutils is platform agnostic-ish.
    76  //
    77  // Some drivers (raw_exec) setup their own cgroup, while others (exec, java, docker)
    78  // would otherwise depend on the Nomad cpuset manager (and docker daemon) to create
    79  // one, which isn't available here in testing, and so we create one via the harness.
    80  // Plumbing such metadata through to the harness is a mind bender, so we just always
    81  // create the cgroup, but at least put it under 'testing.slice'.
    82  //
    83  // tl;dr raw_exec tests should ignore this cgroup.
    84  func (h *DriverHarness) setupCgroupV2(allocID, task string) {
    85  	if cgutil.UseV2 {
    86  		h.cgroup = filepath.Join(cgutil.CgroupRoot, "testing.slice", cgutil.CgroupScope(allocID, task))
    87  		h.logger.Trace("create cgroup for test", "parent", "testing.slice", "id", allocID, "task", task, "path", h.cgroup)
    88  		if err := os.MkdirAll(h.cgroup, 0755); err != nil {
    89  			panic(err)
    90  		}
    91  	}
    92  }
    93  
    94  func (h *DriverHarness) Kill() {
    95  	_ = h.client.Close()
    96  	h.server.Stop()
    97  	h.cleanupCgroup()
    98  }
    99  
   100  // cleanupCgroup might cleanup a cgroup that may or may not be tricked by DriverHarness.
   101  func (h *DriverHarness) cleanupCgroup() {
   102  	// some [non-exec] tests don't bother with MkAllocDir which is what would create
   103  	// the cgroup, but then do call Kill, so in that case skip the cgroup cleanup
   104  	if cgutil.UseV2 && h.cgroup != "" {
   105  		if err := os.Remove(h.cgroup); err != nil && !os.IsNotExist(err) {
   106  			// in some cases the driver will cleanup the cgroup itself, in which
   107  			// case we do not care about the cgroup not existing at cleanup time
   108  			h.t.Fatalf("failed to cleanup cgroup: %v", err)
   109  		}
   110  	}
   111  }
   112  
   113  // MkAllocDir creates a temporary directory and allocdir structure.
   114  // If enableLogs is set to true a logmon instance will be started to write logs
   115  // to the LogDir of the task
   116  // A cleanup func is returned and should be deferred so as to not leak dirs
   117  // between tests.
   118  func (h *DriverHarness) MkAllocDir(t *drivers.TaskConfig, enableLogs bool) func() {
   119  	dir, err := ioutil.TempDir("", "nomad_driver_harness-")
   120  	require.NoError(h.t, err)
   121  
   122  	allocDir := allocdir.NewAllocDir(h.logger, dir, t.AllocID)
   123  	require.NoError(h.t, allocDir.Build())
   124  
   125  	t.AllocDir = allocDir.AllocDir
   126  
   127  	taskDir := allocDir.NewTaskDir(t.Name)
   128  
   129  	caps, err := h.Capabilities()
   130  	require.NoError(h.t, err)
   131  
   132  	fsi := caps.FSIsolation
   133  	h.logger.Trace("FS isolation", "fsi", fsi)
   134  	require.NoError(h.t, taskDir.Build(fsi == drivers.FSIsolationChroot, ci.TinyChroot))
   135  
   136  	task := &structs.Task{
   137  		Name: t.Name,
   138  		Env:  t.Env,
   139  	}
   140  
   141  	// Create the mock allocation
   142  	alloc := mock.Alloc()
   143  	alloc.ID = t.AllocID
   144  	if t.Resources != nil {
   145  		alloc.AllocatedResources.Tasks[task.Name] = t.Resources.NomadResources
   146  	}
   147  
   148  	taskBuilder := taskenv.NewBuilder(mock.Node(), alloc, task, "global")
   149  	SetEnvvars(taskBuilder, fsi, taskDir, config.DefaultConfig())
   150  
   151  	taskEnv := taskBuilder.Build()
   152  	if t.Env == nil {
   153  		t.Env = taskEnv.Map()
   154  	} else {
   155  		for k, v := range taskEnv.Map() {
   156  			if _, ok := t.Env[k]; !ok {
   157  				t.Env[k] = v
   158  			}
   159  		}
   160  	}
   161  
   162  	// setup a v2 cgroup for test cases that assume one exists
   163  	h.setupCgroupV2(alloc.ID, task.Name)
   164  
   165  	//logmon
   166  	if enableLogs {
   167  		lm := logmon.NewLogMon(h.logger.Named("logmon"))
   168  		if runtime.GOOS == "windows" {
   169  			id := uuid.Generate()[:8]
   170  			t.StdoutPath = fmt.Sprintf("//./pipe/%s-%s.stdout", t.Name, id)
   171  			t.StderrPath = fmt.Sprintf("//./pipe/%s-%s.stderr", t.Name, id)
   172  		} else {
   173  			t.StdoutPath = filepath.Join(taskDir.LogDir, fmt.Sprintf(".%s.stdout.fifo", t.Name))
   174  			t.StderrPath = filepath.Join(taskDir.LogDir, fmt.Sprintf(".%s.stderr.fifo", t.Name))
   175  		}
   176  		err = lm.Start(&logmon.LogConfig{
   177  			LogDir:        taskDir.LogDir,
   178  			StdoutLogFile: fmt.Sprintf("%s.stdout", t.Name),
   179  			StderrLogFile: fmt.Sprintf("%s.stderr", t.Name),
   180  			StdoutFifo:    t.StdoutPath,
   181  			StderrFifo:    t.StderrPath,
   182  			MaxFiles:      10,
   183  			MaxFileSizeMB: 10,
   184  		})
   185  		require.NoError(h.t, err)
   186  
   187  		return func() {
   188  			lm.Stop()
   189  			h.client.Close()
   190  			allocDir.Destroy()
   191  		}
   192  	}
   193  
   194  	return func() {
   195  		h.client.Close()
   196  		allocDir.Destroy()
   197  		h.cleanupCgroup()
   198  	}
   199  }
   200  
   201  // WaitUntilStarted will block until the task for the given ID is in the running
   202  // state or the timeout is reached
   203  func (h *DriverHarness) WaitUntilStarted(taskID string, timeout time.Duration) error {
   204  	deadline := time.Now().Add(timeout)
   205  	var lastState drivers.TaskState
   206  	for {
   207  		status, err := h.InspectTask(taskID)
   208  		if err != nil {
   209  			return err
   210  		}
   211  		if status.State == drivers.TaskStateRunning {
   212  			return nil
   213  		}
   214  		lastState = status.State
   215  		if time.Now().After(deadline) {
   216  			return fmt.Errorf("task never transitioned to running, currently '%s'", lastState)
   217  		}
   218  		time.Sleep(100 * time.Millisecond)
   219  	}
   220  }
   221  
   222  // MockDriver is used for testing.
   223  // Each function can be set as a closure to make assertions about how data
   224  // is passed through the base plugin layer.
   225  type MockDriver struct {
   226  	base.MockPlugin
   227  	TaskConfigSchemaF  func() (*hclspec.Spec, error)
   228  	FingerprintF       func(context.Context) (<-chan *drivers.Fingerprint, error)
   229  	CapabilitiesF      func() (*drivers.Capabilities, error)
   230  	RecoverTaskF       func(*drivers.TaskHandle) error
   231  	StartTaskF         func(*drivers.TaskConfig) (*drivers.TaskHandle, *drivers.DriverNetwork, error)
   232  	WaitTaskF          func(context.Context, string) (<-chan *drivers.ExitResult, error)
   233  	StopTaskF          func(string, time.Duration, string) error
   234  	DestroyTaskF       func(string, bool) error
   235  	InspectTaskF       func(string) (*drivers.TaskStatus, error)
   236  	TaskStatsF         func(context.Context, string, time.Duration) (<-chan *drivers.TaskResourceUsage, error)
   237  	TaskEventsF        func(context.Context) (<-chan *drivers.TaskEvent, error)
   238  	SignalTaskF        func(string, string) error
   239  	ExecTaskF          func(string, []string, time.Duration) (*drivers.ExecTaskResult, error)
   240  	ExecTaskStreamingF func(context.Context, string, *drivers.ExecOptions) (*drivers.ExitResult, error)
   241  	MockNetworkManager
   242  }
   243  
   244  type MockNetworkManager struct {
   245  	CreateNetworkF  func(string, *drivers.NetworkCreateRequest) (*drivers.NetworkIsolationSpec, bool, error)
   246  	DestroyNetworkF func(string, *drivers.NetworkIsolationSpec) error
   247  }
   248  
   249  func (m *MockNetworkManager) CreateNetwork(allocID string, req *drivers.NetworkCreateRequest) (*drivers.NetworkIsolationSpec, bool, error) {
   250  	return m.CreateNetworkF(allocID, req)
   251  }
   252  func (m *MockNetworkManager) DestroyNetwork(id string, spec *drivers.NetworkIsolationSpec) error {
   253  	return m.DestroyNetworkF(id, spec)
   254  }
   255  
   256  func (d *MockDriver) TaskConfigSchema() (*hclspec.Spec, error) { return d.TaskConfigSchemaF() }
   257  func (d *MockDriver) Fingerprint(ctx context.Context) (<-chan *drivers.Fingerprint, error) {
   258  	return d.FingerprintF(ctx)
   259  }
   260  func (d *MockDriver) Capabilities() (*drivers.Capabilities, error) { return d.CapabilitiesF() }
   261  func (d *MockDriver) RecoverTask(h *drivers.TaskHandle) error      { return d.RecoverTaskF(h) }
   262  func (d *MockDriver) StartTask(c *drivers.TaskConfig) (*drivers.TaskHandle, *drivers.DriverNetwork, error) {
   263  	return d.StartTaskF(c)
   264  }
   265  func (d *MockDriver) WaitTask(ctx context.Context, id string) (<-chan *drivers.ExitResult, error) {
   266  	return d.WaitTaskF(ctx, id)
   267  }
   268  func (d *MockDriver) StopTask(taskID string, timeout time.Duration, signal string) error {
   269  	return d.StopTaskF(taskID, timeout, signal)
   270  }
   271  func (d *MockDriver) DestroyTask(taskID string, force bool) error {
   272  	return d.DestroyTaskF(taskID, force)
   273  }
   274  func (d *MockDriver) InspectTask(taskID string) (*drivers.TaskStatus, error) {
   275  	return d.InspectTaskF(taskID)
   276  }
   277  func (d *MockDriver) TaskStats(ctx context.Context, taskID string, i time.Duration) (<-chan *drivers.TaskResourceUsage, error) {
   278  	return d.TaskStatsF(ctx, taskID, i)
   279  }
   280  func (d *MockDriver) TaskEvents(ctx context.Context) (<-chan *drivers.TaskEvent, error) {
   281  	return d.TaskEventsF(ctx)
   282  }
   283  func (d *MockDriver) SignalTask(taskID string, signal string) error {
   284  	return d.SignalTaskF(taskID, signal)
   285  }
   286  func (d *MockDriver) ExecTask(taskID string, cmd []string, timeout time.Duration) (*drivers.ExecTaskResult, error) {
   287  	return d.ExecTaskF(taskID, cmd, timeout)
   288  }
   289  
   290  func (d *MockDriver) ExecTaskStreaming(ctx context.Context, taskID string, execOpts *drivers.ExecOptions) (*drivers.ExitResult, error) {
   291  	return d.ExecTaskStreamingF(ctx, taskID, execOpts)
   292  }
   293  
   294  // SetEnvvars sets path and host env vars depending on the FS isolation used.
   295  func SetEnvvars(envBuilder *taskenv.Builder, fsi drivers.FSIsolation, taskDir *allocdir.TaskDir, conf *config.Config) {
   296  
   297  	envBuilder.SetClientTaskRoot(taskDir.Dir)
   298  	envBuilder.SetClientSharedAllocDir(taskDir.SharedAllocDir)
   299  	envBuilder.SetClientTaskLocalDir(taskDir.LocalDir)
   300  	envBuilder.SetClientTaskSecretsDir(taskDir.SecretsDir)
   301  
   302  	// Set driver-specific environment variables
   303  	switch fsi {
   304  	case drivers.FSIsolationNone:
   305  		// Use host paths
   306  		envBuilder.SetAllocDir(taskDir.SharedAllocDir)
   307  		envBuilder.SetTaskLocalDir(taskDir.LocalDir)
   308  		envBuilder.SetSecretsDir(taskDir.SecretsDir)
   309  	default:
   310  		// filesystem isolation; use container paths
   311  		envBuilder.SetAllocDir(allocdir.SharedAllocContainerPath)
   312  		envBuilder.SetTaskLocalDir(allocdir.TaskLocalContainerPath)
   313  		envBuilder.SetSecretsDir(allocdir.TaskSecretsContainerPath)
   314  	}
   315  
   316  	// Set the host environment variables for non-image based drivers
   317  	if fsi != drivers.FSIsolationImage {
   318  		// COMPAT(1.0) using inclusive language, blacklist is kept for backward compatibility.
   319  		filter := strings.Split(conf.ReadAlternativeDefault(
   320  			[]string{"env.denylist", "env.blacklist"},
   321  			config.DefaultEnvDenylist,
   322  		), ",")
   323  		envBuilder.SetHostEnvvars(filter)
   324  	}
   325  }