diff --git a/cmd/logs.go b/cmd/logs.go index 43963c79a..fabea88a4 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -13,6 +13,7 @@ import ( "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/ssh" + "github.com/devsy-org/devsy/pkg/tunnel" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" ) @@ -70,84 +71,71 @@ func (cmd *LogsCmd) Run(ctx context.Context, args []string) error { if !ok { return fmt.Errorf("this command is not supported for proxy providers") } - // create readers - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - return err - } - stdinReader, stdinWriter, err := os.Pipe() - if err != nil { - return err - } - defer func() { _ = stdoutWriter.Close() }() - defer func() { _ = stdinWriter.Close() }() - // ssh tunnel command + sshServerCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", client.AgentPath()) if log.DebugEnabled() { sshServerCmd += " --debug" } - // Get the timeout from the context options timeout := config.ParseTimeOption(devsyConfig, config.ContextOptionAgentInjectTimeout) - // start ssh server in background - errChan := make(chan error, 1) - go func() { - stderr := log.Writer(log.LevelDebug) - defer func() { _ = stderr.Close() }() - - errChan <- agent.InjectAgent(&agent.InjectOptions{ - Ctx: ctx, - Exec: func(ctx context.Context, command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { - return client.Command(ctx, clientpkg.CommandOptions{ - Command: command, - Stdin: stdin, - Stdout: stdout, - Stderr: stderr, - }) - }, - IsLocal: client.AgentLocal(), - RemoteAgentPath: client.AgentPath(), - DownloadURL: client.AgentURL(), - Command: sshServerCmd, - Stdin: stdinReader, - Stdout: stdoutWriter, - Stderr: stderr, - Timeout: timeout, - }) - }() - - // create agent command - agentCommand := fmt.Sprintf( - "'%s' agent workspace logs --context '%s' --id '%s'", - client.AgentPath(), - client.Context(), - client.Workspace(), - ) - if log.DebugEnabled() { - agentCommand += " --debug" - } - - // create new ssh client - // start ssh client as root / default user - sshClient, err := ssh.StdioClientWithUser(stdoutReader, stdinWriter, "" /* default */, false) - if err != nil { - return err - } - defer func() { _ = sshClient.Close() }() - - session, err := sshClient.NewSession() - if err != nil { - return err - } - defer func() { _ = session.Close() }() - - session.Stdout = os.Stdout - session.Stderr = os.Stderr - err = session.Run(agentCommand) + pb, err := tunnel.NewPipeBridge() if err != nil { return err } + defer pb.Close() + + return pb.RunPair(ctx, + func(ctx context.Context, stdin, stdout *os.File) error { + stderr := log.Writer(log.LevelDebug) + defer func() { _ = stderr.Close() }() + + return agent.InjectAgent(&agent.InjectOptions{ + Ctx: ctx, + Exec: func(ctx context.Context, command string, stdinR io.Reader, stdoutW io.Writer, stderrW io.Writer) error { + return client.Command(ctx, clientpkg.CommandOptions{ + Command: command, + Stdin: stdinR, + Stdout: stdoutW, + Stderr: stderrW, + }) + }, + IsLocal: client.AgentLocal(), + RemoteAgentPath: client.AgentPath(), + DownloadURL: client.AgentURL(), + Command: sshServerCmd, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + Timeout: timeout, + }) + }, + func(ctx context.Context, stdout, stdin *os.File) error { + sshClient, err := ssh.StdioClientWithUser(stdout, stdin, "", false) + if err != nil { + return err + } + defer func() { _ = sshClient.Close() }() + + session, err := sshClient.NewSession() + if err != nil { + return err + } + defer func() { _ = session.Close() }() + + agentCommand := fmt.Sprintf( + "'%s' agent workspace logs --context '%s' --id '%s'", + client.AgentPath(), + client.Context(), + client.Workspace(), + ) + if log.DebugEnabled() { + agentCommand += " --debug" + } - return nil + session.Stdout = os.Stdout + session.Stderr = os.Stderr + return session.Run(agentCommand) + }, + ) } diff --git a/e2e/e2e_suite_test.go b/e2e/e2e_suite_test.go index 4460e21ed..3554d1526 100644 --- a/e2e/e2e_suite_test.go +++ b/e2e/e2e_suite_test.go @@ -13,6 +13,7 @@ import ( _ "github.com/devsy-org/devsy/e2e/tests/dockerinstall" _ "github.com/devsy-org/devsy/e2e/tests/ide" _ "github.com/devsy-org/devsy/e2e/tests/integration" + _ "github.com/devsy-org/devsy/e2e/tests/logs" _ "github.com/devsy-org/devsy/e2e/tests/machine" _ "github.com/devsy-org/devsy/e2e/tests/machineprovider" _ "github.com/devsy-org/devsy/e2e/tests/provider" diff --git a/e2e/framework/command.go b/e2e/framework/command.go index 5f2a53a43..ca79b7692 100644 --- a/e2e/framework/command.go +++ b/e2e/framework/command.go @@ -465,6 +465,15 @@ func (f *Framework) DevsyIDEUse(ctx context.Context, ide string, extraArgs ...st return nil } +func (f *Framework) DevsyLogs(ctx context.Context, workspace string) (string, error) { + args := []string{"logs", workspace} + stdout, _, err := f.ExecCommandCapture(ctx, args) + if err != nil { + return "", fmt.Errorf("devsy logs failed: %s", err.Error()) + } + return stdout, nil +} + func (f *Framework) DevsyIDEList(ctx context.Context, extraArgs ...string) (string, error) { baseArgs := []string{"ide", "list"} return f.ExecCommandOutput(ctx, append(baseArgs, extraArgs...)) diff --git a/e2e/tests/logs/logs.go b/e2e/tests/logs/logs.go new file mode 100644 index 000000000..738c3e466 --- /dev/null +++ b/e2e/tests/logs/logs.go @@ -0,0 +1,67 @@ +package logs + +import ( + "context" + "os" + "path/filepath" + "strings" + + "github.com/devsy-org/devsy/e2e/framework" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +var _ = ginkgo.Describe( + "devsy logs test suite", + ginkgo.Label("logs"), + ginkgo.Ordered, + func() { + var initialDir string + + ginkgo.BeforeEach(func() { + var err error + initialDir, err = os.Getwd() + framework.ExpectNoError(err) + }) + + ginkgo.It("should return workspace container logs", + ginkgo.SpecTimeout(framework.TimeoutLong()), + func(ctx context.Context) { + f := framework.NewDefaultFramework(initialDir + "/bin") + + tempDir, err := framework.CopyToTempDirWithoutChdir( + initialDir + "/tests/tunnel/testdata/tunnel", + ) + framework.ExpectNoError(err) + ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir) + + _ = f.DevsyProviderDelete(ctx, "docker123") + err = f.DevsyProviderAdd(ctx, filepath.Join(tempDir, "provider.yaml")) + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func(cleanupCtx context.Context) { + err = f.DevsyWorkspaceDelete(cleanupCtx, tempDir) + framework.ExpectNoError(err) + err = f.DevsyProviderDelete(cleanupCtx, "docker123") + framework.ExpectNoError(err) + }) + + err = f.DevsyUp(ctx, tempDir, "--debug") + framework.ExpectNoError(err) + + status, err := f.DevsyStatus(ctx, tempDir, "--container-status=false") + framework.ExpectNoError(err) + framework.ExpectEqual( + strings.ToUpper(status.State), + "RUNNING", + "workspace should be running before fetching logs", + ) + + out, err := f.DevsyLogs(ctx, tempDir) + framework.ExpectNoError(err) + gomega.Expect(out).ToNot( + gomega.BeEmpty(), + "devsy logs should return non-empty output for a running workspace", + ) + }) + }, +)