Repository: tensorchord/envd (language: python)

Issue:
feat(CLI): Support image remove in envd destroy command
## Description

`envd image rm` should be supported to remove unused envd images.

---
<!-- Issue Author: Don't delete this message to encourage other users to support your issue! -->
**Message from the maintainers**:

Love this enhancement proposal? Give it a 👍. We prioritise the proposals with the most 👍.



Discussion / hints:
Ref https://github.com/tensorchord/envd/issues/621#issuecomment-1189753075

To avoid `docker rmi`
Implement in envd destroy command

Fix patch:
diff --git a/pkg/app/build.go b/pkg/app/build.go
index 55514b5a3..f85fa25f8 100644
--- a/pkg/app/build.go
+++ b/pkg/app/build.go
@@ -171,7 +171,7 @@ func ParseBuildOpt(clicontext *cli.Context) (builder.Options, error) {
 		logrus.Debug("tag not specified, using default")
 		tag = fmt.Sprintf("%s:%s", filepath.Base(buildContext), "dev")
 	}
-	// The current container engine is only Docker. It should be expaned to support other container engines.
+	// The current container engine is only Docker. It should be expanded to support other container engines.
 	tag, err = docker.NormalizeNamed(tag)
 	if err != nil {
 		return builder.Options{}, err
diff --git a/pkg/app/destroy.go b/pkg/app/destroy.go
index a7ea64acb..0624994c8 100644
--- a/pkg/app/destroy.go
+++ b/pkg/app/destroy.go
@@ -22,6 +22,7 @@ import (
 	"github.com/urfave/cli/v2"
 
 	"github.com/tensorchord/envd/pkg/docker"
+	"github.com/tensorchord/envd/pkg/envd"
 	sshconfig "github.com/tensorchord/envd/pkg/ssh/config"
 )
 
@@ -70,15 +71,40 @@ func destroy(clicontext *cli.Context) error {
 		}
 		ctrName = filepath.Base(buildContext)
 	}
+
+	tag, err := getContainerTag(clicontext, ctrName)
+	if err != nil {
+		return err
+	}
+
 	if ctrName, err := dockerClient.Destroy(clicontext.Context, ctrName); err != nil {
 		return errors.Wrapf(err, "failed to destroy the environment: %s", ctrName)
 	} else if ctrName != "" {
 		logrus.Infof("%s is destroyed", ctrName)
 	}
-
+	if err := dockerClient.RemoveImage(clicontext.Context, tag); err != nil {
+		return errors.Wrapf(err, "failed to remove the image: %s", tag)
+	}
 	if err = sshconfig.RemoveEntry(ctrName); err != nil {
 		logrus.Infof("failed to remove entry %s from your SSH config file: %s", ctrName, err)
 		return errors.Wrap(err, "failed to remove entry from your SSH config file")
 	}
 	return nil
 }
+
+func getContainerTag(clicontext *cli.Context, name string) (string, error) {
+	envdEngine, err := envd.New(clicontext.Context)
+	if err != nil {
+		return "", err
+	}
+	envs, err := envdEngine.ListEnvironment(clicontext.Context)
+	if err != nil {
+		return "", err
+	}
+	for _, env := range envs {
+		if env.Name == name {
+			return env.Container.Image, nil
+		}
+	}
+	return "", errors.Newf("cannot find the image of %s", name)
+}


Test patch:
diff --git a/e2e/cli/init_test.go b/e2e/cli/init_test.go
index e9d020e74..89c6b985a 100644
--- a/e2e/cli/init_test.go
+++ b/e2e/cli/init_test.go
@@ -60,12 +60,8 @@ var _ = Describe("init project", Ordered, func() {
 		It("exec installed command inside container", func() {
 			_, err := e.Exec("via --help")
 			Expect(err).To(Succeed())
+			e.DestroyContainer()
 		})
-		AfterAll(func() {
-			e.DestroyContainer()()
-			e.RemoveImage()()
-		})
-
 	})
 
 	AfterAll(func() {
diff --git a/e2e/cli/quick_start_test.go b/e2e/cli/quick_start_test.go
index 56a77a6b0..f4583fb10 100644
--- a/e2e/cli/quick_start_test.go
+++ b/e2e/cli/quick_start_test.go
@@ -33,5 +33,4 @@ var _ = Describe("e2e quickstart", Ordered, func() {
 		Expect(res).To(Equal("[2 3 4]"))
 	})
 	AfterEach(e.DestroyContainer())
-	AfterAll(e.RemoveImage())
 })
diff --git a/e2e/docs/docs_test.go b/e2e/docs/docs_test.go
index 3b59d19ef..affbcd8ef 100644
--- a/e2e/docs/docs_test.go
+++ b/e2e/docs/docs_test.go
@@ -27,8 +27,6 @@ import (
 )
 
 var _ = Describe("check examples in documentation", func() {
-	buildContext := "doctest"
-	// env := "up-test"
 	baseArgs := []string{
 		"envd.test", "--debug",
 	}
@@ -67,12 +65,12 @@ var _ = Describe("check examples in documentation", func() {
 		Expect(err).NotTo(HaveOccurred())
 	})
 
-	up_tests := []string{"testdata/minimal", "testdata/getting_started", "testdata/jupyter", "testsdata/complex"}
+	up_tests := []string{"testdata/minimal", "testdata/getting_started", "testdata/jupyter", "testdata/complex"}
 
 	for _, v := range up_tests {
 		It(fmt.Sprintf("can up %s environment", v), func() {
 			args := append(baseArgs, []string{
-				"up", "--path", "testdata/getting_started", "-f", "build.envd", "--detach", "--force",
+				"up", "--path", v, "-f", "build.envd", "--detach", "--force",
 			}...)
 			e2e.ResetEnvdApp()
 			envdApp := app.New()
@@ -80,7 +78,7 @@ var _ = Describe("check examples in documentation", func() {
 			Expect(err).NotTo(HaveOccurred())
 
 			destroyArgs := append(baseArgs, []string{
-				"destroy", "--path", buildContext,
+				"destroy", "--path", v,
 			}...)
 			err = envdApp.Run(destroyArgs)
 			Expect(err).NotTo(HaveOccurred())
diff --git a/e2e/docs/testdata/complex/build.envd b/e2e/docs/testdata/complex/build.envd
index 4595cdafa..893c797b6 100644
--- a/e2e/docs/testdata/complex/build.envd
+++ b/e2e/docs/testdata/complex/build.envd
@@ -18,7 +18,7 @@ deb https://mirror.sjtu.edu.cn/ubuntu focal-security main restricted universe mu
     install.python_packages(name = [
         "numpy",
     ])
-    install.cuda(version="11.6", cudnn="8")
+    install.cuda(version="11.2", cudnn="8")
     shell("zsh")
     install.apt_packages(name = [
         "htop"
diff --git a/e2e/language/python_test.go b/e2e/language/python_test.go
index 0b41197f4..4f6a9f506 100644
--- a/e2e/language/python_test.go
+++ b/e2e/language/python_test.go
@@ -28,7 +28,6 @@ var _ = Describe("python", Ordered, func() {
 		e.BuildImage(true)()
 		e.RunContainer()()
 		e.DestroyContainer()()
-		e.RemoveImage()()
 	})
 	It("Should build requirements successfully", func() {
 		exampleName := "python/requirements"
@@ -37,7 +36,6 @@ var _ = Describe("python", Ordered, func() {
 		e.BuildImage(true)()
 		e.RunContainer()()
 		e.DestroyContainer()()
-		e.RemoveImage()()
 	})
 	It("Should build hybrid successfully", func() {
 		exampleName := "python/hybrid"
@@ -46,7 +44,6 @@ var _ = Describe("python", Ordered, func() {
 		e.BuildImage(true)()
 		e.RunContainer()()
 		e.DestroyContainer()()
-		e.RemoveImage()()
 	})
 
 	It("Should build conda with channel successfully", func() {
@@ -56,6 +53,5 @@ var _ = Describe("python", Ordered, func() {
 		e.BuildImage(true)()
 		e.RunContainer()()
 		e.DestroyContainer()()
-		e.RemoveImage()()
 	})
 })
diff --git a/e2e/language/runtime_test.go b/e2e/language/runtime_test.go
index 8a247440c..29764cc6b 100644
--- a/e2e/language/runtime_test.go
+++ b/e2e/language/runtime_test.go
@@ -33,5 +33,4 @@ var _ = Describe("runtime", Ordered, func() {
 		Expect(res).To(Equal("[2 3 4]"))
 	})
 	AfterEach(e.DestroyContainer())
-	AfterAll(e.RemoveImage())
 })