Skip to content

Commit

Permalink
Merge pull request #529 from FabianKramm/main
Browse files Browse the repository at this point in the history
user shell, inactivity timeout & web timeout
  • Loading branch information
FabianKramm authored Jul 18, 2023
2 parents 4a796e9 + 9dbf363 commit a0cb422
Show file tree
Hide file tree
Showing 29 changed files with 525 additions and 119 deletions.
27 changes: 19 additions & 8 deletions cmd/agent/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
// DaemonCmd holds the cmd flags
type DaemonCmd struct {
*flags.GlobalFlags

Interval string
}

// NewDaemonCmd creates a new command
Expand All @@ -38,6 +40,7 @@ func NewDaemonCmd(flags *flags.GlobalFlags) *cobra.Command {
return cmd.Run(context.Background())
},
}
daemonCmd.Flags().StringVar(&cmd.Interval, "interval", "", "The interval how to poll workspaces")
return daemonCmd
}

Expand All @@ -61,9 +64,18 @@ func (cmd *DaemonCmd) patrol(log log.Logger) {
// make sure we don't immediately resleep on startup
cmd.initialTouch(log)

// parse the daemon interval
interval := time.Second * 60
if cmd.Interval != "" {
parsed, err := time.ParseDuration(cmd.Interval)
if err == nil {
interval = parsed
}
}

// loop over workspace configs and check their last ModTime
for {
time.Sleep(time.Minute * 2)
time.Sleep(interval)
cmd.doOnce(log)
}
}
Expand Down Expand Up @@ -217,20 +229,19 @@ func getActivity(workspaceConfig string, log log.Logger) (*time.Time, *provider2
return nil, nil, nil
}

// check if it was actually ever executed fully
_, err = os.Stat(filepath.Join(filepath.Dir(workspaceConfig), agent.WorkspaceDevContainerResult))
if err != nil {
return nil, nil, nil
}

// check last access time
stat, err := os.Stat(workspaceConfig)
if err != nil {
return nil, nil, err
}

// check if timeout
// check if workspace is locked
t := stat.ModTime()
if agent.HasWorkspaceBusyFile(filepath.Dir(workspaceConfig)) {
t = t.Add(time.Minute * 20)
}

// check if timeout
return &t, workspace, nil
}

Expand Down
4 changes: 4 additions & 0 deletions cmd/agent/workspace/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ func (cmd *BuildCmd) Run(ctx context.Context) error {
return fmt.Errorf("repository needs to be specified")
}

// make sure daemon does shut us down while we are doing things
agent.CreateWorkspaceBusyFile(workspaceInfo.Origin)
defer agent.DeleteWorkspaceBusyFile(workspaceInfo.Origin)

// initialize the workspace
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
Expand Down
7 changes: 6 additions & 1 deletion cmd/agent/workspace/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ func (cmd *UpCmd) Run(ctx context.Context) error {
return nil
}

// make sure daemon does shut us down while we are doing things
agent.CreateWorkspaceBusyFile(workspaceInfo.Origin)
defer agent.DeleteWorkspaceBusyFile(workspaceInfo.Origin)

// initialize the workspace
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
Expand Down Expand Up @@ -329,7 +333,7 @@ func installDaemon(workspaceInfo *provider2.AgentWorkspaceInfo, log log.Logger)
}

log.Debugf("Installing DevPod daemon into server...")
err := daemon.InstallDaemon(workspaceInfo.Agent.DataPath, log)
err := daemon.InstallDaemon(workspaceInfo.Agent.DataPath, workspaceInfo.CLIOptions.DaemonInterval, log)
if err != nil {
return errors.Wrap(err, "install daemon")
}
Expand Down Expand Up @@ -385,6 +389,7 @@ func (cmd *UpCmd) devPodUp(ctx context.Context, workspaceInfo *provider2.AgentWo
return nil, err
}

// start the devcontainer
result, err := runner.Up(ctx, devcontainer.UpOptions{
CLIOptions: workspaceInfo.CLIOptions,
})
Expand Down
5 changes: 5 additions & 0 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ func NewUpCmd(flags *flags.GlobalFlags) *cobra.Command {

upCmd.Flags().StringVar(&cmd.Source, "source", "", "Optional source for the workspace. E.g. git:https://github.com/my-org/my-repo")
upCmd.Flags().BoolVar(&cmd.Proxy, "proxy", false, "If true will forward agent requests to stdio")

// testing
upCmd.Flags().StringVar(&cmd.DaemonInterval, "daemon-interval", "", "TESTING ONLY")
_ = upCmd.Flags().MarkHidden("daemon-interval")
return upCmd
}

Expand Down Expand Up @@ -579,6 +583,7 @@ func startBrowserTunnel(ctx context.Context, devPodConfig *config.Config, client

return nil
}

func configureSSH(client client2.BaseWorkspaceClient, configPath, user string) error {
err := devssh.ConfigureSSHConfig(configPath, client.Context(), client.Workspace(), user, log.Default)
if err != nil {
Expand Down
9 changes: 1 addition & 8 deletions e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import (
"testing"
"time"

"github.com/loft-sh/devpod/e2e/framework"

"github.com/onsi/ginkgo/v2"

"github.com/onsi/gomega"

// Register tests
_ "github.com/loft-sh/devpod/e2e/tests/integration"
_ "github.com/loft-sh/devpod/e2e/tests/machine"
_ "github.com/loft-sh/devpod/e2e/tests/machineprovider"
_ "github.com/loft-sh/devpod/e2e/tests/provider"
_ "github.com/loft-sh/devpod/e2e/tests/proxyprovider"
_ "github.com/loft-sh/devpod/e2e/tests/ssh"
Expand All @@ -28,11 +27,5 @@ import (
func TestRunE2ETests(t *testing.T) {
rand.Seed(time.Now().UTC().UnixNano())
gomega.RegisterFailHandler(ginkgo.Fail)
go func() {
err := framework.StartAgentServer()
if err != nil {
t.Error(err)
}
}()
ginkgo.RunSpecs(t, "DevPod e2e suite")
}
4 changes: 2 additions & 2 deletions e2e/framework/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ func (f *Framework) DevPodProviderUse(ctx context.Context, provider string, extr
return nil
}

func (f *Framework) DevPodStatus(ctx context.Context, workspace string) (client.WorkspaceStatus, error) {
func (f *Framework) DevPodStatus(ctx context.Context, extraArgs ...string) (client.WorkspaceStatus, error) {
baseArgs := []string{"status", "--output", "json"}
baseArgs = append(baseArgs, workspace)
baseArgs = append(baseArgs, extraArgs...)
stdout, err := f.ExecCommandOutput(ctx, baseArgs)
if err != nil {
return client.WorkspaceStatus{}, fmt.Errorf("devpod stop failed: %s", err.Error())
Expand Down
8 changes: 8 additions & 0 deletions e2e/tests/machineprovider/framework.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package machineprovider

import "github.com/onsi/ginkgo/v2"

// DevPodDescribe annotates the test with the label.
func DevPodDescribe(text string, body func()) bool {
return ginkgo.Describe("[machineprovider] "+text, body)
}
156 changes: 156 additions & 0 deletions e2e/tests/machineprovider/machineprovider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package machineprovider

import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/loft-sh/devpod/e2e/framework"
"github.com/onsi/ginkgo/v2"
)

var _ = DevPodDescribe("devpod machine provider test suite", func() {
var initialDir string

ginkgo.BeforeEach(func() {
var err error
initialDir, err = os.Getwd()
framework.ExpectNoError(err)
})

ginkgo.It("test start / stop / status", func(ctx context.Context) {
f := framework.NewDefaultFramework(initialDir + "/bin")

// copy test dir
tempDir, err := framework.CopyToTempDirWithoutChdir(initialDir + "/tests/machineprovider/testdata/machineprovider")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func() {
_ = os.RemoveAll(tempDir)
})

tempDirLocation, err := os.MkdirTemp("", "")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func() {
_ = os.RemoveAll(tempDirLocation)
})

// create docker provider
err = f.DevPodProviderAdd(ctx, filepath.Join(tempDir, "provider.yaml"), "-o", "LOCATION="+tempDirLocation)
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func() {
err = f.DevPodProviderDelete(context.Background(), "docker123")
framework.ExpectNoError(err)
})

// wait for devpod workspace to come online (deadline: 30s)
err = f.DevPodUp(ctx, tempDir, "--debug")
framework.ExpectNoError(err)

// expect workspace
workspace, err := f.FindWorkspace(ctx, tempDir)
framework.ExpectNoError(err)

// check status
status, err := f.DevPodStatus(ctx, tempDir)
framework.ExpectNoError(err)
framework.ExpectEqual(strings.ToUpper(status.State), "RUNNING", "workspace status did not match")

// stop container
err = f.DevPodStop(ctx, tempDir)
framework.ExpectNoError(err)

// check status
status, err = f.DevPodStatus(ctx, tempDir)
framework.ExpectNoError(err)
framework.ExpectEqual(strings.ToUpper(status.State), "STOPPED", "workspace status did not match")

// wait for devpod workspace to come online (deadline: 30s)
err = f.DevPodUp(ctx, tempDir)
framework.ExpectNoError(err)

// check if ssh works as it should start the container
out, err := f.DevPodSSH(ctx, tempDir, fmt.Sprintf("cat /workspaces/%s/test.txt", workspace.ID))
framework.ExpectNoError(err)
framework.ExpectEqual(out, "Test123", "workspace content does not match")

// delete workspace
err = f.DevPodWorkspaceDelete(ctx, tempDir)
framework.ExpectNoError(err)
}, ginkgo.SpecTimeout(60*time.Second))

ginkgo.It("test devpod inactivity timeout", func(ctx context.Context) {
f := framework.NewDefaultFramework(initialDir + "/bin")

// copy test dir
tempDir, err := framework.CopyToTempDirWithoutChdir(initialDir + "/tests/machineprovider/testdata/machineprovider2")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func() {
err = os.RemoveAll(tempDir)
framework.ExpectNoError(err)
})

tempDirLocation, err := os.MkdirTemp("", "")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func() {
err = os.RemoveAll(tempDirLocation)
framework.ExpectNoError(err)
})

// create provider
_ = f.DevPodProviderDelete(ctx, "docker123")
err = f.DevPodProviderAdd(ctx, filepath.Join(tempDir, "provider.yaml"))
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func() {
err = f.DevPodProviderDelete(context.Background(), "docker123")
framework.ExpectNoError(err)
})

// wait for devpod workspace to come online (deadline: 30s)
err = f.DevPodUp(ctx, tempDir, "--debug", "--daemon-interval=3s")
framework.ExpectNoError(err)
ginkgo.DeferCleanup(func() {
// delete workspace
err = f.DevPodWorkspaceDelete(context.Background(), tempDir)
framework.ExpectNoError(err)
})

// check status
status, err := f.DevPodStatus(ctx, tempDir, "--container-status=false")
framework.ExpectNoError(err)
framework.ExpectEqual(strings.ToUpper(status.State), "RUNNING", "workspace status did not match")

// stop container
err = f.DevPodStop(ctx, tempDir)
framework.ExpectNoError(err)

// check status
status, err = f.DevPodStatus(ctx, tempDir, "--container-status=false")
framework.ExpectNoError(err)
framework.ExpectEqual(strings.ToUpper(status.State), "STOPPED", "workspace status did not match")

// wait for devpod workspace to come online (deadline: 30s)
err = f.DevPodUp(ctx, tempDir, "--daemon-interval=3s")
framework.ExpectNoError(err)

// check status
status, err = f.DevPodStatus(ctx, tempDir, "--container-status=false")
framework.ExpectNoError(err)
framework.ExpectEqual(strings.ToUpper(status.State), "RUNNING", "workspace status did not match")

// wait until workspace is stopped again
now := time.Now()
for {
status, err := f.DevPodStatus(ctx, tempDir, "--container-status=false")
framework.ExpectNoError(err)
framework.ExpectEqual(time.Since(now) < time.Minute*2, true, "machine did not shutdown in time")
if status.State == "Stopped" {
break
}

time.Sleep(time.Second * 2)
}
}, ginkgo.SpecTimeout(300*time.Second))
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Go",
"image": "mcr.microsoft.com/devcontainers/go:0-1.19-bullseye"
}
30 changes: 30 additions & 0 deletions e2e/tests/machineprovider/testdata/machineprovider/provider.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: docker123
version: v0.0.1
description: |-
DevPod on Docker
options:
LOCATION:
description: The location DevPod should use
agent:
local: true
docker:
install: false
exec:
create: |-
mkdir -p ${LOCATION}/${MACHINE_ID}
echo "RUNNING" > ${LOCATION}/${MACHINE_ID}/status.txt
delete: |-
rm -R ${LOCATION}/${MACHINE_ID}
start: |-
echo "RUNNING" > ${LOCATION}/${MACHINE_ID}/status.txt
stop: |-
echo "STOPPED" > ${LOCATION}/${MACHINE_ID}/status.txt
status: |-
cat ${LOCATION}/${MACHINE_ID}/status.txt 2>/dev/null || echo "NOTFOUND"
command: |-
"${DEVPOD}" helper sh -c "${COMMAND}"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test123
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Go",
"image": "ubuntu"
}
Loading

0 comments on commit a0cb422

Please sign in to comment.