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 }