package nomadexec import ( "bytes" "context" "fmt" "io" "reflect" "regexp" "strings" "testing" "time" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/e2e/e2eutil" "github.com/hashicorp/nomad/e2e/framework" "github.com/hashicorp/nomad/helper/uuid" dtestutils "github.com/hashicorp/nomad/plugins/drivers/testutils" "github.com/stretchr/testify/assert" ) type NomadExecE2ETest struct { framework.TC name string jobFilePath string jobID string alloc api.Allocation } func init() { framework.AddSuites(&framework.TestSuite{ Component: "Nomad exec", CanRunLocal: true, Cases: []framework.TestCase{ newNomadExecE2eTest("docker", "./nomadexec/testdata/docker.nomad"), }, }) } func newNomadExecE2eTest(name, jobFilePath string) *NomadExecE2ETest { return &NomadExecE2ETest{ name: name, jobFilePath: jobFilePath, } } func (tc *NomadExecE2ETest) Name() string { return fmt.Sprintf("%v (%v)", tc.TC.Name(), tc.name) } func (tc *NomadExecE2ETest) BeforeAll(f *framework.F) { // Ensure cluster has leader before running tests e2eutil.WaitForLeader(f.T(), tc.Nomad()) e2eutil.WaitForNodesReady(f.T(), tc.Nomad(), 1) // register a job for execing into tc.jobID = "nomad-exec" + uuid.Generate()[:8] allocs := e2eutil.RegisterAndWaitForAllocs(f.T(), tc.Nomad(), tc.jobFilePath, tc.jobID, "") f.Len(allocs, 1) e2eutil.WaitForAllocRunning(f.T(), tc.Nomad(), allocs[0].ID) tc.alloc = api.Allocation{ ID: allocs[0].ID, Namespace: allocs[0].Namespace, NodeID: allocs[0].NodeID, } } func (tc *NomadExecE2ETest) TestExecBasicResponses(f *framework.F) { for _, c := range dtestutils.ExecTaskStreamingBasicCases { f.T().Run(c.Name, func(t *testing.T) { stdin := newTestStdin(c.Tty, c.Stdin) var stdout, stderr bytes.Buffer resizeCh := make(chan api.TerminalSize) go func() { resizeCh <- api.TerminalSize{Height: 100, Width: 100} }() ctx, cancelFn := context.WithTimeout(context.Background(), 15*time.Second) defer cancelFn() exitCode, err := tc.Nomad().Allocations().Exec(ctx, &tc.alloc, "task", c.Tty, []string{"/bin/sh", "-c", c.Command}, stdin, &stdout, &stderr, resizeCh, nil) // TODO: Occasionally, we get "Unexpected EOF" error, but with the correct output. // investigate why if err != nil && strings.Contains(err.Error(), io.ErrUnexpectedEOF.Error()) { f.T().Logf("got unexpected EOF error, ignoring: %v", err) } else { assert.NoError(t, err) } assert.Equal(t, c.ExitCode, exitCode) switch s := c.Stdout.(type) { case string: assert.Equal(t, s, stdout.String()) case *regexp.Regexp: assert.Regexp(t, s, stdout.String()) case nil: assert.Empty(t, stdout.String()) default: assert.Fail(t, "unexpected stdout type", "found %v (%v), but expected string or regexp", s, reflect.TypeOf(s)) } switch s := c.Stderr.(type) { case string: assert.Equal(t, s, stderr.String()) case *regexp.Regexp: assert.Regexp(t, s, stderr.String()) case nil: assert.Empty(t, stderr.String()) default: assert.Fail(t, "unexpected stderr type", "found %v (%v), but expected string or regexp", s, reflect.TypeOf(s)) } }) } } func (tc *NomadExecE2ETest) AfterAll(f *framework.F) { jobs := tc.Nomad().Jobs() if tc.jobID != "" { jobs.Deregister(tc.jobID, true, nil) } tc.Nomad().System().GarbageCollect() } func newTestStdin(tty bool, d string) io.Reader { pr, pw := io.Pipe() go func() { pw.Write([]byte(d)) // when testing TTY, leave connection open for the entire duration of command // closing stdin may cause TTY session prematurely before command completes if !tty { pw.Close() } }() return pr }