Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ jobs:
install-kind: false
requires-secret: false

- label: down
runner: ubuntu-latest
free-disk-space: false
install-kind: false
requires-secret: false

# Up tests

- label: up-workspaces
Expand Down
129 changes: 129 additions & 0 deletions cmd/down.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cmd

import (
"context"
"fmt"

"github.com/devsy-org/devsy/cmd/completion"
"github.com/devsy-org/devsy/cmd/flags"
client2 "github.com/devsy-org/devsy/pkg/client"
"github.com/devsy-org/devsy/pkg/client/clientimplementation"
"github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/log"
"github.com/devsy-org/devsy/pkg/workspace"
"github.com/spf13/cobra"
)

// DownCmd holds the down cmd flags.
type DownCmd struct {
*flags.GlobalFlags
client2.DeleteOptions
}

// NewDownCmd creates a new down command that stops and deletes a workspace.
func NewDownCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &DownCmd{
GlobalFlags: flags,
}
downCmd := &cobra.Command{
Use: "down [flags] [workspace-path|workspace-name]",
Short: "Stops and deletes an existing workspace",
RunE: func(cobraCmd *cobra.Command, args []string) error {
return cmd.Run(cobraCmd.Context(), args)
},
ValidArgsFunction: func(
rootCmd *cobra.Command, args []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
return completion.GetWorkspaceSuggestions(
rootCmd,
cmd.Context,
cmd.Provider,
args,
toComplete,
cmd.Owner,
)
},
}

downCmd.Flags().
BoolVar(&cmd.Force, "force", false, "Delete workspace even if it is not found remotely anymore")
downCmd.Flags().
StringVar(&cmd.GracePeriod, "grace-period", "", "The amount of time to give the command to delete the workspace")
return downCmd
}

// Run stops and then deletes the workspace.
func (cmd *DownCmd) Run(ctx context.Context, args []string) error {
devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
if err != nil {
return err
}

if _, err := clientimplementation.DecodeOptionsFromEnv(
config.EnvFlagsDelete,
&cmd.DeleteOptions,
); err != nil {
return fmt.Errorf("decode delete options: %w", err)
}

if err := clientimplementation.DecodePlatformOptionsFromEnv(&cmd.Platform); err != nil {
return fmt.Errorf("decode platform options: %w", err)
}

client, err := workspace.Get(ctx, workspace.GetOptions{
DevsyConfig: devsyConfig,
Args: args,
Owner: cmd.Owner,
})
if err != nil {
return err
}

if err := cmd.stop(ctx, client); err != nil {
return err
}

_, err = workspace.Delete(ctx, workspace.DeleteOptions{
DevsyConfig: devsyConfig,
Args: args,
ClientDelete: cmd.DeleteOptions,
Force: cmd.Force,
Owner: cmd.Owner,
})
if err != nil {
return err
}

log.Infof("successfully stopped and deleted workspace")
return nil
}

func (cmd *DownCmd) stop(
ctx context.Context,
client client2.BaseWorkspaceClient,
) error {
if !cmd.Platform.Enabled {
if err := client.Lock(ctx); err != nil {
return err
}
defer client.Unlock()
}

status, err := client.Status(ctx, client2.StatusOptions{})
if err != nil {
return err
}

if status != client2.StatusRunning {
log.Infof("workspace is %s, skipping stop", status)
return nil
}

// Single-machine stop is not needed here: workspace.Delete handles
// single-machine cleanup via deleteSingleMachine.
if err := client.Stop(ctx, client2.StopOptions{}); err != nil {
return fmt.Errorf("stop workspace: %w", err)
}

return nil
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func BuildRoot() *cobra.Command {
rootCmd.AddCommand(NewSSHCmd(globalFlags))
rootCmd.AddCommand(NewVersionCmd())
rootCmd.AddCommand(NewStopCmd(globalFlags))
rootCmd.AddCommand(NewDownCmd(globalFlags))
rootCmd.AddCommand(NewListCmd(globalFlags))
rootCmd.AddCommand(NewStatusCmd(globalFlags))
rootCmd.AddCommand(NewBuildCmd(globalFlags))
Expand Down
5 changes: 2 additions & 3 deletions cmd/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ func NewStopCmd(flags *flags.GlobalFlags) *cobra.Command {
GlobalFlags: flags,
}
stopCmd := &cobra.Command{
Use: "stop [flags] [workspace-path|workspace-name]",
Aliases: []string{"down"},
Short: "Stops an existing workspace",
Use: "stop [flags] [workspace-path|workspace-name]",
Short: "Stops an existing workspace",
RunE: func(cobraCmd *cobra.Command, args []string) error {
ctx := cobraCmd.Context()
devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
Expand Down
2 changes: 1 addition & 1 deletion e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import (
_ "github.com/devsy-org/devsy/e2e/tests/build"
_ "github.com/devsy-org/devsy/e2e/tests/context"
_ "github.com/devsy-org/devsy/e2e/tests/dockerinstall"
_ "github.com/devsy-org/devsy/e2e/tests/down"
_ "github.com/devsy-org/devsy/e2e/tests/ide"
_ "github.com/devsy-org/devsy/e2e/tests/integration"
_ "github.com/devsy-org/devsy/e2e/tests/machine"
_ "github.com/devsy-org/devsy/e2e/tests/machineprovider"
_ "github.com/devsy-org/devsy/e2e/tests/provider"
_ "github.com/devsy-org/devsy/e2e/tests/readconfiguration"
_ "github.com/devsy-org/devsy/e2e/tests/ssh"
_ "github.com/devsy-org/devsy/e2e/tests/tunnel"
_ "github.com/devsy-org/devsy/e2e/tests/up"
Expand Down
9 changes: 9 additions & 0 deletions e2e/framework/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,15 @@ func (f *Framework) DevsyStop(ctx context.Context, workspace string) error {
return nil
}

func (f *Framework) DevsyDown(ctx context.Context, workspace string) error {
baseArgs := []string{"down", workspace}
err := f.ExecCommand(ctx, false, false, "", baseArgs)
if err != nil {
return fmt.Errorf("devsy down failed: %s", err.Error())
}
return nil
}

func (f *Framework) DevsyProviderAdd(ctx context.Context, args ...string) error {
baseArgs := []string{"provider", "add"}
baseArgs = append(baseArgs, args...)
Expand Down
105 changes: 105 additions & 0 deletions e2e/tests/down/down.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package down

import (
"context"
"fmt"
"os"
"strings"

"github.com/devsy-org/devsy/e2e/framework"
"github.com/devsy-org/devsy/pkg/devcontainer/config"
docker "github.com/devsy-org/devsy/pkg/docker"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)

var _ = ginkgo.Describe("testing down command", ginkgo.Label("down"), func() {
var dockerHelper *docker.DockerHelper
var initialDir string

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

dockerHelper = &docker.DockerHelper{DockerCommand: "docker"}
})

ginkgo.It("down stops and deletes workspace", func(ctx context.Context) {
f, err := framework.SetupDockerProvider(initialDir+"/bin", "docker")
framework.ExpectNoError(err)

name := "vscode-remote-try-python"

err = f.DevsyUp(ctx, "https://github.com/microsoft/vscode-remote-try-python.git")
framework.ExpectNoError(err)

// Verify workspace is running
status, err := f.DevsyStatus(ctx, name)
framework.ExpectNoError(err)
gomega.Expect(strings.ToUpper(status.State)).To(gomega.Equal("RUNNING"))

// Get workspace UID for container lookup
workspace, err := f.FindWorkspace(ctx, name)
framework.ExpectNoError(err)

containerIDs, err := dockerHelper.FindContainer(ctx, []string{
fmt.Sprintf("%s=%s", config.DockerIDLabel, workspace.UID),
})
framework.ExpectNoError(err)
gomega.Expect(containerIDs).NotTo(gomega.BeEmpty(), "container should exist before down")

// Run devsy down
err = f.DevsyDown(ctx, name)
framework.ExpectNoError(err)

// Verify container is gone
containerIDs, err = dockerHelper.FindContainer(ctx, []string{
fmt.Sprintf("%s=%s", config.DockerIDLabel, workspace.UID),
})
framework.ExpectNoError(err)
gomega.Expect(containerIDs).To(gomega.BeEmpty(), "container should be deleted after down")

// Verify workspace no longer appears in list
_, err = f.FindWorkspace(ctx, name)
gomega.Expect(err).To(gomega.HaveOccurred(), "workspace should not be in list after down")
}, ginkgo.SpecTimeout(framework.TimeoutModerate()))

ginkgo.It("stop only stops and does not delete workspace", func(ctx context.Context) {
f, err := framework.SetupDockerProvider(initialDir+"/bin", "docker")
framework.ExpectNoError(err)

name := "vscode-remote-try-python"
ginkgo.DeferCleanup(f.DevsyWorkspaceDelete, name)

err = f.DevsyUp(ctx, "https://github.com/microsoft/vscode-remote-try-python.git")
framework.ExpectNoError(err)

// Verify workspace is running
status, err := f.DevsyStatus(ctx, name)
framework.ExpectNoError(err)
gomega.Expect(strings.ToUpper(status.State)).To(gomega.Equal("RUNNING"))

// Get workspace UID for container lookup
workspace, err := f.FindWorkspace(ctx, name)
framework.ExpectNoError(err)

// Run devsy stop
err = f.DevsyStop(ctx, name)
framework.ExpectNoError(err)

// Verify workspace still exists in list (just stopped, not deleted)
_, err = f.FindWorkspace(ctx, name)
framework.ExpectNoError(err)

// Verify container still exists (stopped but not removed)
containerIDs, err := dockerHelper.FindContainer(ctx, []string{
fmt.Sprintf("%s=%s", config.DockerIDLabel, workspace.UID),
})
framework.ExpectNoError(err)
gomega.Expect(containerIDs).NotTo(
gomega.BeEmpty(),
"container should still exist after stop (only stopped, not deleted)",
)
}, ginkgo.SpecTimeout(framework.TimeoutModerate()))
})
Loading
Loading