diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index d29358f1e..000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,28 +0,0 @@ -# https://github.com/riyadhalnur/issuelabeler - -# Number of labels to fetch (optional). Defaults to 100 -numLabels: 100 -# These labels will not be used even if the issue contains them (optional). -# Pass a blank array if no labels are to be excluded. -# excludeLabels: [] -excludeLabels: [] -# custom configuration to override default behaviour -# control explicitly what gets added and when -custom: - - location: title - keywords: - - 'feat' - - 'feature' - labels: - - type/feature 💡 - - location: body - keywords: - - 'feat' - - 'feature' - labels: - - type/feature 💡 - - location: title - keywords: - - 'chore' - labels: - - type/maintenance 🚧 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 67815ee1c..0edd208ed 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -65,6 +65,9 @@ jobs: ${{ runner.OS }}-build-${{ env.cache-name }}- ${{ runner.OS }}-build- ${{ runner.OS }}- + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 - name: Verify mockgen run: | make generate && git add pkg diff --git a/Makefile b/Makefile index dcfb9f86e..b6560fc4f 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,7 @@ addlicense: addlicense -c "The envd Authors" **/*.go **/**/*.go **/**/**/*.go test: generate - @go test -race -coverprofile=coverage.out ./... + @go test -v -race -coverprofile=coverage.out ./... @go tool cover -func coverage.out | tail -n 1 | awk '{ print "Total coverage: " $$3 }' clean: diff --git a/cmd/envd/build.go b/cmd/envd/build.go index 3d4e6d962..ceee49fcd 100644 --- a/cmd/envd/build.go +++ b/cmd/envd/build.go @@ -19,9 +19,11 @@ import ( "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" + "github.com/spf13/viper" cli "github.com/urfave/cli/v2" "github.com/tensorchord/envd/pkg/builder" + "github.com/tensorchord/envd/pkg/flag" "github.com/tensorchord/envd/pkg/home" "github.com/tensorchord/envd/pkg/util/fileutil" ) @@ -76,12 +78,15 @@ func build(clicontext *cli.Context) error { } logger := logrus.WithFields(logrus.Fields{ - "build-context": buildContext, - "build-file": manifest, - "config": config, - "tag": tag, + "build-context": buildContext, + "build-file": manifest, + "config": config, + "tag": tag, + flag.FlagBuildkitdImage: viper.GetString(flag.FlagBuildkitdImage), + flag.FlagBuildkitdContainer: viper.GetString(flag.FlagBuildkitdContainer), + flag.FlagSSHImage: viper.GetString(flag.FlagSSHImage), }) - logger.Debug("starting build") + logger.Debug("starting build command") builder, err := builder.New(clicontext.Context, config, manifest, buildContext, tag) if err != nil { diff --git a/cmd/envd/build_test.go b/cmd/envd/build_test.go new file mode 100644 index 000000000..e9de16090 --- /dev/null +++ b/cmd/envd/build_test.go @@ -0,0 +1,42 @@ +// Copyright 2022 The envd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/tensorchord/envd/pkg/docker" +) + +var _ = Describe("build command", func() { + buildContext := "testdata" + args := []string{ + "envd.test", "--debug", "build", "--path", buildContext, + } + BeforeEach(func() { + cli, err := docker.NewClient(context.TODO()) + Expect(err).NotTo(HaveOccurred()) + _ = cli.Destroy(context.TODO(), buildContext) + }) + When("given the right arguments", func() { + It("should build successfully", func() { + _, err := run(args) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/cmd/envd/main.go b/cmd/envd/main.go index c6b9d08b2..dc2fa3066 100644 --- a/cmd/envd/main.go +++ b/cmd/envd/main.go @@ -31,7 +31,7 @@ import ( "github.com/tensorchord/envd/pkg/version" ) -func main() { +func run(args []string) (bool, error) { cli.VersionPrinter = func(c *cli.Context) { fmt.Println(c.App.Name, version.Package, c.App.Version, version.Revision) } @@ -94,7 +94,7 @@ func main() { viper.Set(flag.FlagSSHImage, context.String(flag.FlagSSHImage)) return nil } - handleErr(debugEnabled, app.Run(os.Args)) + return debugEnabled, app.Run(args) } func handleErr(debug bool, err error) { @@ -110,3 +110,8 @@ func handleErr(debug bool, err error) { } os.Exit(1) } + +func main() { + debug, err := run(os.Args) + handleErr(debug, err) +} diff --git a/cmd/envd/suite_test.go b/cmd/envd/suite_test.go new file mode 100644 index 000000000..380eb9dc3 --- /dev/null +++ b/cmd/envd/suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2022 The envd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMain(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "envd Suite") +} diff --git a/cmd/envd/testdata/build.envd b/cmd/envd/testdata/build.envd new file mode 100644 index 000000000..353b338cb --- /dev/null +++ b/cmd/envd/testdata/build.envd @@ -0,0 +1,4 @@ +base(os="ubuntu20.04", language="python3") +pip_package(name = [ + "ormb", +]) diff --git a/cmd/envd/up.go b/cmd/envd/up.go index 1f688ae0d..6b145640b 100644 --- a/cmd/envd/up.go +++ b/cmd/envd/up.go @@ -21,10 +21,12 @@ import ( "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" + "github.com/spf13/viper" cli "github.com/urfave/cli/v2" "github.com/tensorchord/envd/pkg/builder" "github.com/tensorchord/envd/pkg/docker" + "github.com/tensorchord/envd/pkg/flag" "github.com/tensorchord/envd/pkg/home" "github.com/tensorchord/envd/pkg/lang/ir" "github.com/tensorchord/envd/pkg/ssh" @@ -75,6 +77,11 @@ var CommandUp = &cli.Command{ Usage: "Timeout of container creation", Value: time.Second * 30, }, + &cli.BoolFlag{ + Name: "detach", + Usage: "detach from the container", + Value: false, + }, }, Action: up, @@ -103,14 +110,20 @@ func up(clicontext *cli.Context) error { } ctr := fileutil.Base(buildContext) + detach := clicontext.Bool("detach") + logger := logrus.WithFields(logrus.Fields{ - "build-context": buildContext, - "build-file": manifest, - "config": config, - "tag": tag, - "container-name": ctr, + "build-context": buildContext, + "build-file": manifest, + "config": config, + "tag": tag, + "container-name": ctr, + "detach": detach, + flag.FlagBuildkitdImage: viper.GetString(flag.FlagBuildkitdImage), + flag.FlagBuildkitdContainer: viper.GetString(flag.FlagBuildkitdContainer), + flag.FlagSSHImage: viper.GetString(flag.FlagSSHImage), }) - logger.Debug("starting up") + logger.Debug("starting up command") builder, err := builder.New(clicontext.Context, config, manifest, buildContext, tag) if err != nil { @@ -134,13 +147,15 @@ func up(clicontext *cli.Context) error { } logrus.Debugf("container %s is running", containerID) - sshClient, err := ssh.NewClient( - containerIP, "envd", 2222, clicontext.Bool("auth"), clicontext.Path("private-key"), "") - if err != nil { - return err - } - if err := sshClient.Attach(); err != nil { - return err + if !detach { + sshClient, err := ssh.NewClient( + containerIP, "root", 2222, clicontext.Bool("auth"), clicontext.Path("private-key"), "") + if err != nil { + return err + } + if err := sshClient.Attach(); err != nil { + return err + } } return nil diff --git a/cmd/envd/up_test.go b/cmd/envd/up_test.go new file mode 100644 index 000000000..1cf509a97 --- /dev/null +++ b/cmd/envd/up_test.go @@ -0,0 +1,47 @@ +// Copyright 2022 The envd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/tensorchord/envd/pkg/docker" +) + +var _ = Describe("up command", func() { + buildContext := "testdata" + args := []string{ + "envd.test", "--debug", "up", "--path", buildContext, "--detach", + } + BeforeEach(func() { + cli, err := docker.NewClient(context.TODO()) + Expect(err).NotTo(HaveOccurred()) + _ = cli.Destroy(context.TODO(), buildContext) + }) + When("given the right arguments", func() { + It("should up and destroy successfully", func() { + _, err := run(args) + Expect(err).NotTo(HaveOccurred()) + destroyArgs := []string{ + "envd.test", "--debug", "destroy", "--path", buildContext, + } + _, err = run(destroyArgs) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/pkg/lang/ir/graph.go b/pkg/lang/ir/graph.go index dc7601056..ea02ee543 100644 --- a/pkg/lang/ir/graph.go +++ b/pkg/lang/ir/graph.go @@ -122,14 +122,10 @@ func (g *Graph) compileBase() llb.State { var base llb.State if g.CUDA == nil && g.CUDNN == nil { base = llb.Image("docker.io/library/python:3.8") + } else { + base = g.compileCUDAPackages() } - base = g.compileCUDAPackages() - // TODO(gaocegege): Refactor user to a seperate stage. - run := base. - Run(llb.Shlex("groupadd -g 1000 envd")). - Run(llb.Shlex("useradd -p \"\" -u 1000 -g envd -s /bin/sh -m envd")). - Run(llb.Shlex("adduser envd sudo")) - return llb.User("envd")(run.Root()) + return base } func (g *Graph) compileCUDAPackages() llb.State { @@ -201,7 +197,13 @@ func (g Graph) compileBuiltinSystemPackages(root llb.State) llb.State { llb.AsPersistentCacheDir("/"+cacheDir, llb.CacheMountShared)) run.AddMount(cacheLibDir, llb.Scratch(), llb.AsPersistentCacheDir("/"+cacheLibDir, llb.CacheMountShared)) - return run.Root() + + // TODO(gaocegege): Refactor user to a seperate stage. + run = run. + Run(llb.Shlex("groupadd -g 1000 envd"), llb.WithCustomName("create user group envd")). + Run(llb.Shlex("useradd -p \"\" -u 1000 -g envd -s /bin/sh -m envd"), llb.WithCustomName("create user envd")). + Run(llb.Shlex("adduser envd sudo"), llb.WithCustomName("add user envd to sudoers")) + return llb.User("envd")(run.Root()) } func (g Graph) compileSystemPackages(root llb.State) llb.State { diff --git a/pkg/progress/compileui/display.go b/pkg/progress/compileui/display.go index 2435795a4..4e543bff1 100644 --- a/pkg/progress/compileui/display.go +++ b/pkg/progress/compileui/display.go @@ -34,46 +34,58 @@ type Writer interface { } type generalWriter struct { - console console.Console - phase string - trace *trace - doneCh chan bool - repeatd bool - result *Result - lineCount int + console console.Console + modeConsole bool + phase string + trace *trace + doneCh chan bool + repeatd bool + result *Result + lineCount int } func New(ctx context.Context, out console.File, mode string) (Writer, error) { var c console.Console switch mode { - case "auto": + case "auto", "tty", "": if cons, err := console.ConsoleFromFile(out); err == nil { c = cons } else { - return nil, errors.Wrap(err, "failed to get console") + if mode == "tty" { + return nil, errors.Wrap(err, "failed to get console") + } } case "plain": default: return nil, errors.Errorf("invalid progress mode %s", mode) } - t := newTrace(out, true) + modeConsole := c != nil + t := newTrace(out, modeConsole) t.init() w := &generalWriter{ - console: c, - phase: "parse build.envd and download/cache dependencies", - trace: t, - doneCh: make(chan bool), - repeatd: false, + console: c, + modeConsole: modeConsole, + phase: "parse build.envd and download/cache dependencies", + trace: t, + doneCh: make(chan bool), + repeatd: false, result: &Result{ plugins: make([]*PluginInfo, 0), }, lineCount: 0, } - // TODO(gaocegege): Have a result chan - //nolint - go w.run(ctx) + go func() { + // TODO(gaocegege): Print in text. + if modeConsole { + // TODO(gaocegege): Have a result chan + //nolint + w.run(ctx) + } else { + <-ctx.Done() + } + }() return w, nil } @@ -114,23 +126,28 @@ func (w *generalWriter) LogZSH(action Action, cached bool) { } func (w generalWriter) Finish() { - w.doneCh <- true + if w.modeConsole { + w.doneCh <- true + } } func (w *generalWriter) run(ctx context.Context) error { - displayTimeout := 100 * time.Millisecond - ticker := time.NewTicker(displayTimeout) - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-w.doneCh: - w.output(true) - return nil - case <-ticker.C: - w.output(false) + if w.modeConsole { + displayTimeout := 100 * time.Millisecond + ticker := time.NewTicker(displayTimeout) + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-w.doneCh: + w.output(true) + return nil + case <-ticker.C: + w.output(false) + } } } + return nil } func (w *generalWriter) output(finished bool) {