From f26fc4ce7e42ed0e567cb492c41551f6c2f2c3b4 Mon Sep 17 00:00:00 2001 From: Ce Gao Date: Wed, 12 Oct 2022 09:49:59 +0800 Subject: [PATCH] feat(CLI): Support create command (#1001) * feat(CLI): Add create command Signed-off-by: Ce Gao * fix: Fix import order Signed-off-by: Ce Gao * fix: Move waitingInterval to types Signed-off-by: Ce Gao Signed-off-by: Ce Gao --- go.mod | 13 +++- go.sum | 30 +++++++- pkg/app/app.go | 3 +- pkg/app/create.go | 123 ++++++++++++++++++++++++++++++ pkg/app/k8s.go | 53 ------------- pkg/app/ssh.go | 70 ----------------- pkg/app/up.go | 102 ++++++++++++------------- pkg/envd/docker.go | 98 ++++++++++++++---------- pkg/envd/engine.go | 11 +-- pkg/envd/envdserver.go | 69 +++++++++++++++-- pkg/envd/factory.go | 9 ++- pkg/envd/types.go | 65 ++++++++++++++++ pkg/lang/ir/compile.go | 2 +- pkg/ssh/config/entry.go | 117 ++++++++++++++++++++++++++++ pkg/ssh/config/ssh_config.go | 74 +----------------- pkg/ssh/config/ssh_config_test.go | 10 ++- 16 files changed, 535 insertions(+), 314 deletions(-) create mode 100644 pkg/app/create.go delete mode 100644 pkg/app/k8s.go delete mode 100644 pkg/app/ssh.go create mode 100644 pkg/envd/types.go create mode 100644 pkg/ssh/config/entry.go diff --git a/go.mod b/go.mod index df9560cac..a4633e6ec 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/spf13/viper v1.13.0 github.com/stretchr/testify v1.8.0 - github.com/tensorchord/envd-server v0.0.2 + github.com/tensorchord/envd-server v0.0.3 github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f github.com/urfave/cli/v2 v2.19.2 @@ -37,11 +37,22 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/term v0.0.0-20220919170432-7a66f970e087 golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45 + k8s.io/api v0.25.2 ) require ( + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 // indirect github.com/pkg/errors v0.9.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/apimachinery v0.25.2 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73 // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) require ( diff --git a/go.sum b/go.sum index 66de66af8..4d6a53d8f 100644 --- a/go.sum +++ b/go.sum @@ -236,6 +236,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -312,6 +314,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -374,6 +378,8 @@ github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c/go.m github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -463,9 +469,12 @@ github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf/go.mod h1:FBS0z0QWA44HXy github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= @@ -574,6 +583,7 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -607,8 +617,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tensorchord/envd-server v0.0.2 h1:GJbRydV3xfo1TxUWGreoY3nECgUDOEpVTHdTPpAhH2g= -github.com/tensorchord/envd-server v0.0.2/go.mod h1:GXzQ2sZX5zxRODSDW9Fcdp6VltEprgyVp7HSug2t7bM= +github.com/tensorchord/envd-server v0.0.3 h1:4QzvkM6f3nJRqFN/aVsDkEp1F/vxAEJ3xF1wNhDF9qc= +github.com/tensorchord/envd-server v0.0.3/go.mod h1:AWUk101rs4xl7wHmQmafZ8X5TSV/ZR9AD7BysLkIMIo= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tonistiigi/fsutil v0.0.0-20220115021204-b19f7f9cb274 h1:wbyZxD6IPFp0sl5uscMOJRsz5UKGFiNiD16e+MVfKZY= github.com/tonistiigi/fsutil v0.0.0-20220115021204-b19f7f9cb274/go.mod h1:oPAfvw32vlUJSjyDcQ3Bu0nb2ON2B+G0dtVN/SZNJiA= @@ -1088,6 +1098,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -1123,6 +1135,20 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8= +k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0= +k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= +k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73 h1:H9TCJUUx+2VA0ZiD9lvtaX8fthFsMoD+Izn93E/hm8U= +k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/pkg/app/app.go b/pkg/app/app.go index 722ab0d10..c08f33370 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -61,6 +61,7 @@ func New() EnvdApp { internalApp.Commands = []*cli.Command{ CommandBootstrap, + CommandCreate, CommandContext, CommandBuild, CommandDestroy, @@ -68,8 +69,6 @@ func New() EnvdApp { CommandImage, CommandInit, CommandLogin, - CommandK8s, - CommandSSH, CommandPause, CommandPrune, CommandRun, diff --git a/pkg/app/create.go b/pkg/app/create.go new file mode 100644 index 000000000..131e37bfb --- /dev/null +++ b/pkg/app/create.go @@ -0,0 +1,123 @@ +// 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 app + +import ( + "time" + + "github.com/cockroachdb/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + + "github.com/tensorchord/envd/pkg/envd" + "github.com/tensorchord/envd/pkg/home" + "github.com/tensorchord/envd/pkg/ssh" + sshconfig "github.com/tensorchord/envd/pkg/ssh/config" + "github.com/tensorchord/envd/pkg/types" +) + +var CommandCreate = &cli.Command{ + Name: "create", + Category: CategoryBasic, + Aliases: []string{"c"}, + Usage: "Create the envd environment from the existing image", + Hidden: true, + Description: ``, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "image", + Usage: "image name", + DefaultText: "PROJECT:dev", + Required: true, + }, + &cli.DurationFlag{ + Name: "timeout", + Usage: "Timeout of container creation", + Value: time.Second * 30, + }, + &cli.BoolFlag{ + Name: "detach", + Usage: "Detach from the container", + Value: false, + }, + &cli.PathFlag{ + Name: "private-key", + Usage: "Path to the private key", + Aliases: []string{"k"}, + Value: sshconfig.GetPrivateKeyOrPanic(), + Hidden: true, + }, + }, + Action: create, +} + +func create(clicontext *cli.Context) error { + c, err := home.GetManager().ContextGetCurrent() + if err != nil { + return err + } + + engine, err := envd.New(clicontext.Context, envd.Options{ + Context: c, + }) + if err != nil { + return err + } + + opt := envd.StartOptions{ + Image: clicontext.String("image"), + Timeout: clicontext.Duration("timeout"), + } + if c.Runner == types.RunnerTypeEnvdServer { + opt.EnvdServerSource = &envd.EnvdServerSource{} + } + res, err := engine.StartEnvd(clicontext.Context, opt) + if err != nil { + return err + } + + logrus.Debugf("container %s is running", res.Name) + + logrus.Debugf("add entry %s to SSH config.", res.Name) + eo := sshconfig.EntryOptions{ + Name: res.Name, + IFace: localhost, + Port: res.SSHPort, + PrivateKeyPath: clicontext.Path("private-key"), + EnableHostKeyCheck: false, + EnableAgentForward: false, + User: res.Name, + } + if err = sshconfig.AddEntry(eo); err != nil { + logrus.Infof("failed to add entry %s to your SSH config file: %s", res.Name, err) + return errors.Wrap(err, "failed to add entry to your SSH config file") + } + + if !clicontext.Bool("detach") { + opt := ssh.DefaultOptions() + opt.PrivateKeyPath = clicontext.Path("private-key") + opt.Port = res.SSHPort + opt.AgentForwarding = false + opt.User = res.Name + sshClient, err := ssh.NewClient(opt) + if err != nil { + return errors.Wrap(err, "failed to create the ssh client") + } + if err := sshClient.Attach(); err != nil { + return errors.Wrap(err, "failed to attach to the container") + } + } + return nil +} diff --git a/pkg/app/k8s.go b/pkg/app/k8s.go deleted file mode 100644 index 5356bdaf0..000000000 --- a/pkg/app/k8s.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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 app - -import ( - servertypes "github.com/tensorchord/envd-server/api/types" - "github.com/tensorchord/envd-server/client" - "github.com/urfave/cli/v2" - - "github.com/tensorchord/envd/pkg/home" -) - -var CommandK8s = &cli.Command{ - Name: "k8s", - Category: CategoryBasic, - Hidden: true, - Usage: "TestK8s", - Action: k8s, -} - -func k8s(clicontext *cli.Context) error { - ac, err := home.GetManager().AuthGetCurrent() - if err != nil { - return err - } - it := ac.IdentityToken - c, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - return err - } - req := servertypes.EnvironmentCreateRequest{ - IdentityToken: it, - Image: "gaocegege/test-envd", - } - _, err = c.EnvironmentCreate(clicontext.Context, req) - if err != nil { - return err - } - - return nil -} diff --git a/pkg/app/ssh.go b/pkg/app/ssh.go deleted file mode 100644 index 9f3562ffe..000000000 --- a/pkg/app/ssh.go +++ /dev/null @@ -1,70 +0,0 @@ -// 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 app - -import ( - "github.com/cockroachdb/errors" - "github.com/urfave/cli/v2" - - "github.com/tensorchord/envd/pkg/home" - "github.com/tensorchord/envd/pkg/ssh" - sshconfig "github.com/tensorchord/envd/pkg/ssh/config" -) - -var CommandSSH = &cli.Command{ - Name: "ssh", - Category: CategoryBasic, - Hidden: true, - Usage: "TestK8s", - Flags: []cli.Flag{ - &cli.PathFlag{ - Name: "private-key", - Usage: "Path to the private key", - Aliases: []string{"k"}, - Value: sshconfig.GetPrivateKeyOrPanic(), - Hidden: true, - }, - &cli.PathFlag{ - Name: "public-key", - Usage: "Path to the public key", - Aliases: []string{"pubk"}, - Value: sshconfig.GetPublicKeyOrPanic(), - Hidden: true, - }, - }, - Action: sshc, -} - -func sshc(clicontext *cli.Context) error { - ac, err := home.GetManager().AuthGetCurrent() - if err != nil { - return err - } - it := ac.IdentityToken - - opt := ssh.DefaultOptions() - opt.User = it - opt.PrivateKeyPath = clicontext.Path("private-key") - opt.Port = 2222 - opt.AgentForwarding = false - sshClient, err := ssh.NewClient(opt) - if err != nil { - return errors.Wrap(err, "failed to create the ssh client") - } - if err := sshClient.Attach(); err != nil { - return errors.Wrap(err, "failed to attach to the container") - } - return nil -} diff --git a/pkg/app/up.go b/pkg/app/up.go index bb7eb8358..dd9f3dfd2 100644 --- a/pkg/app/up.go +++ b/pkg/app/up.go @@ -22,13 +22,12 @@ import ( "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" - "github.com/tensorchord/envd/pkg/builder" "github.com/tensorchord/envd/pkg/envd" "github.com/tensorchord/envd/pkg/home" "github.com/tensorchord/envd/pkg/lang/ir" "github.com/tensorchord/envd/pkg/ssh" sshconfig "github.com/tensorchord/envd/pkg/ssh/config" - "github.com/tensorchord/envd/pkg/util/netutil" + "github.com/tensorchord/envd/pkg/types" ) const ( @@ -157,77 +156,72 @@ func up(clicontext *cli.Context) error { } else { gpu = builder.GPUEnabled() } - numGPUs := builder.NumGPUs() - - sshPortInHost, error := StartEnvd(clicontext, buildOpt, gpu, numGPUs) - if error != nil { - return error - } - - if !detach { - opt := ssh.DefaultOptions() - opt.PrivateKeyPath = clicontext.Path("private-key") - opt.Port = sshPortInHost - sshClient, err := ssh.NewClient(opt) - if err != nil { - return errors.Wrap(err, "failed to create the ssh client") - } - if err := sshClient.Attach(); err != nil { - return errors.Wrap(err, "failed to attach to the container") - } + numGPU := 0 + if gpu { + numGPU = 1 } - return nil -} - -func StartEnvd(clicontext *cli.Context, buildOpt builder.Options, gpu bool, numGPUs int) (int, error) { context, err := home.GetManager().ContextGetCurrent() if err != nil { - return 0, errors.Wrap(err, "failed to get the current context") + return errors.Wrap(err, "failed to get the current context") } opt := envd.Options{ Context: context, } engine, err := envd.New(clicontext.Context, opt) if err != nil { - return 0, errors.Wrap(err, "failed to create the docker client") - } - - if gpu { - nvruntimeExists, err := engine.GPUEnabled(clicontext.Context) - if err != nil { - return 0, errors.Wrap(err, "failed to check if nvidia-runtime is installed") - } - if !nvruntimeExists { - return 0, errors.New("GPU is required but nvidia container runtime is not installed, please refer to https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker") + return errors.Wrap(err, "failed to create the docker client") + } + + startOptions := envd.StartOptions{ + EnvironmentName: filepath.Base(buildOpt.BuildContextDir), + BuildContext: buildOpt.BuildContextDir, + Image: buildOpt.Tag, + NumGPU: numGPU, + Forced: clicontext.Bool("force"), + Timeout: clicontext.Duration("timeout"), + } + if context.Runner != types.RunnerTypeEnvdServer { + startOptions.EngineSource = envd.EngineSource{ + DockerSource: &envd.DockerSource{ + Graph: *ir.DefaultGraph, + MountOptions: clicontext.StringSlice("volume"), + }, } } - sshPortInHost, err := netutil.GetFreePort() + res, err := engine.StartEnvd(clicontext.Context, startOptions) if err != nil { - return 0, errors.Wrap(err, "failed to get a free port") + return errors.Wrap(err, "failed to start the envd environment") } + logrus.Debugf("container %s is running", res.Name) - ctr := filepath.Base(buildOpt.BuildContextDir) - force := clicontext.Bool("force") - err = engine.CleanEnvdIfExists(clicontext.Context, ctr, force) - if err != nil { - return 0, errors.Wrap(err, "failed to clean the envd environment") + logrus.Debugf("add entry %s to SSH config.", ctr) + eo := sshconfig.EntryOptions{ + Name: ctr, + IFace: localhost, + Port: res.SSHPort, + PrivateKeyPath: clicontext.Path("private-key"), + EnableHostKeyCheck: false, + EnableAgentForward: true, } - containerID, containerIP, err := engine.StartEnvd(clicontext.Context, - buildOpt.Tag, ctr, buildOpt.BuildContextDir, gpu, numGPUs, sshPortInHost, *ir.DefaultGraph, clicontext.Duration("timeout"), - clicontext.StringSlice("volume")) - if err != nil { - return 0, errors.Wrap(err, "failed to start the envd environment") + if err = sshconfig.AddEntry(eo); err != nil { + logrus.Infof("failed to add entry %s to your SSH config file: %s", ctr, err) + return errors.Wrap(err, "failed to add entry to your SSH config file") } - logrus.Debugf("container %s is running", containerID) - logrus.Debugf("Add entry %s to SSH config. at %s", buildOpt.BuildContextDir, containerIP) - if err = sshconfig.AddEntry( - ctr, localhost, sshPortInHost, clicontext.Path("private-key")); err != nil { - logrus.Infof("failed to add entry %s to your SSH config file: %s", ctr, err) - return 0, errors.Wrap(err, "failed to add entry to your SSH config file") + if !detach { + opt := ssh.DefaultOptions() + opt.PrivateKeyPath = clicontext.Path("private-key") + opt.Port = res.SSHPort + sshClient, err := ssh.NewClient(opt) + if err != nil { + return errors.Wrap(err, "failed to create the ssh client") + } + if err := sshClient.Attach(); err != nil { + return errors.Wrap(err, "failed to attach to the container") + } } - return sshPortInHost, nil + return nil } diff --git a/pkg/envd/docker.go b/pkg/envd/docker.go index 8c5ce6ed9..5b820e538 100644 --- a/pkg/envd/docker.go +++ b/pkg/envd/docker.go @@ -39,14 +39,6 @@ import ( "github.com/tensorchord/envd/pkg/util/netutil" ) -const ( - localhost = "127.0.0.1" -) - -var ( - waitingInternal = 1 * time.Second -) - type dockerEngine struct { *client.Client } @@ -246,29 +238,51 @@ func (e dockerEngine) IsRunning(ctx context.Context, cname string) (bool, error) } // StartEnvd creates the container for the given tag and container name. -func (e dockerEngine) StartEnvd(ctx context.Context, tag, name, buildContext string, - gpuEnabled bool, numGPUs int, sshPortInHost int, g ir.Graph, timeout time.Duration, - mountOptionsStr []string) (string, string, error) { +func (e dockerEngine) StartEnvd(ctx context.Context, so StartOptions) (*StartResult, error) { logger := logrus.WithFields(logrus.Fields{ - "tag": tag, - "container": name, - "gpu": gpuEnabled, - "numGPUs": numGPUs, - "build-context": buildContext, + "tag": so.Image, + "environment": so.EnvironmentName, + "gpu": so.NumGPU, + "build-context": so.BuildContext, }) + if so.NumGPU != 0 { + nvruntimeExists, err := e.GPUEnabled(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to check if nvidia-runtime is installed") + } + if !nvruntimeExists { + return nil, errors.New("GPU is required but nvidia container runtime is not installed, please refer to https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker") + } + } + + sshPortInHost, err := netutil.GetFreePort() + if err != nil { + return nil, errors.Wrap(err, "failed to get a free port") + } + + err = e.CleanEnvdIfExists(ctx, so.EnvironmentName, so.Forced) + if err != nil { + return nil, errors.Wrap(err, "failed to clean the envd environment") + } config := &container.Config{ - Image: tag, + Image: so.Image, User: "envd", ExposedPorts: nat.PortSet{}, } - base := fileutil.EnvdHomeDir(filepath.Base(buildContext)) + base := fileutil.EnvdHomeDir(filepath.Base(so.BuildContext)) config.WorkingDir = base - mountOption := make([]mount.Mount, 0, len(mountOptionsStr)+len(g.Mount)+1) - for _, option := range mountOptionsStr { + if so.DockerSource == nil { + return nil, errors.New("failed to get the docker-specific options") + } + g := so.DockerSource.Graph + + mountOption := make([]mount.Mount, 0, + len(so.DockerSource.MountOptions)+len(g.Mount)+1) + for _, option := range so.DockerSource.MountOptions { mStr := strings.Split(option, ":") if len(mStr) != 2 { - return "", "", errors.Newf("Invalid mount options %s", option) + return nil, errors.Newf("Invalid mount options %s", option) } logger.WithFields(logrus.Fields{ @@ -295,12 +309,12 @@ func (e dockerEngine) StartEnvd(ctx context.Context, tag, name, buildContext str mountOption = append(mountOption, mount.Mount{ Type: mount.TypeBind, - Source: buildContext, + Source: so.BuildContext, Target: base, }) logger.WithFields(logrus.Fields{ - "mount-path": buildContext, + "mount-path": so.BuildContext, "working-dir": base, }).Debug("setting up container working directory") @@ -324,6 +338,7 @@ func (e dockerEngine) StartEnvd(ctx context.Context, tag, name, buildContext str var jupyterPortInHost int // TODO(gaocegege): Avoid specific logic to set the port. + // Add a func to builder to generate all the ports from the build process. if g.JupyterConfig != nil { if g.JupyterConfig.Port != 0 { jupyterPortInHost = int(g.JupyterConfig.Port) @@ -331,7 +346,7 @@ func (e dockerEngine) StartEnvd(ctx context.Context, tag, name, buildContext str var err error jupyterPortInHost, err = netutil.GetFreePort() if err != nil { - return "", "", errors.Wrap(err, "failed to get a free port") + return nil, errors.Wrap(err, "failed to get a free port") } } natPort := nat.Port(fmt.Sprintf("%d/tcp", envdconfig.JupyterPortInContainer)) @@ -348,7 +363,7 @@ func (e dockerEngine) StartEnvd(ctx context.Context, tag, name, buildContext str var err error rStudioPortInHost, err = netutil.GetFreePort() if err != nil { - return "", "", errors.Wrap(err, "failed to get a free port") + return nil, errors.Wrap(err, "failed to get a free port") } natPort := nat.Port(fmt.Sprintf("%d/tcp", envdconfig.RStudioServerPortInContainer)) hostConfig.PortBindings[natPort] = []nat.PortBinding{ @@ -366,7 +381,7 @@ func (e dockerEngine) StartEnvd(ctx context.Context, tag, name, buildContext str if item.HostPort == 0 { item.HostPort, err = netutil.GetFreePort() if err != nil { - return "", "", errors.Wrap(err, "failed to get a free port") + return nil, errors.Wrap(err, "failed to get a free port") } } natPort := nat.Port(fmt.Sprintf("%d/tcp", item.EnvdPort)) @@ -380,23 +395,23 @@ func (e dockerEngine) StartEnvd(ctx context.Context, tag, name, buildContext str } } - if gpuEnabled { + if so.NumGPU != 0 { logger.Debug("GPU is enabled.") - hostConfig.DeviceRequests = deviceRequests(numGPUs) + hostConfig.DeviceRequests = deviceRequests(so.NumGPU) } - config.Labels = labels(name, g, + config.Labels = labels(so.EnvironmentName, g, sshPortInHost, jupyterPortInHost, rStudioPortInHost) logger = logger.WithFields(logrus.Fields{ "entrypoint": config.Entrypoint, "working-dir": config.WorkingDir, }) - logger.Debugf("starting %s container", name) + logger.Debugf("starting %s container", so.EnvironmentName) - resp, err := e.ContainerCreate(ctx, config, hostConfig, nil, nil, name) + resp, err := e.ContainerCreate(ctx, config, hostConfig, nil, nil, so.EnvironmentName) if err != nil { - return "", "", errors.Wrap(err, "failed to create the container") + return nil, errors.Wrap(err, "failed to create the container") } for _, w := range resp.Warnings { @@ -409,22 +424,27 @@ func (e dockerEngine) StartEnvd(ctx context.Context, tag, name, buildContext str // Hack to check if the port is already allocated. if strings.Contains(errCause.Error(), "port is already allocated") { logrus.Debugf("failed to allocate the port: %s", err) - return "", "", errors.New("port is already allocated in the host") + return nil, errors.New("port is already allocated in the host") } - return "", "", errors.Wrap(err, "failed to run the container") + return nil, errors.Wrap(err, "failed to run the container") } container, err := e.ContainerInspect(ctx, resp.ID) if err != nil { - return "", "", errors.Wrap(err, "failed to inspect the container") + return nil, errors.Wrap(err, "failed to inspect the container") } if err := e.WaitUntilRunning( - ctx, container.Name, timeout); err != nil { - return "", "", errors.Wrap(err, "failed to wait until the container is running") + ctx, container.Name, so.Timeout); err != nil { + return nil, errors.Wrap(err, "failed to wait until the container is running") } - return container.Name, container.NetworkSettings.IPAddress, nil + result := &StartResult{ + SSHPort: sshPortInHost, + Address: container.NetworkSettings.IPAddress, + Name: container.Name, + } + return result, nil } func (e dockerEngine) WaitUntilRunning(ctx context.Context, @@ -437,7 +457,7 @@ func (e dockerEngine) WaitUntilRunning(ctx context.Context, defer cancel() for { select { - case <-time.After(waitingInternal): + case <-time.After(waitingInterval): isRunning, err := e.IsRunning(ctxTimeout, name) if err != nil { // Has not yet started. Keep waiting. diff --git a/pkg/envd/engine.go b/pkg/envd/engine.go index a6a771048..82d022044 100644 --- a/pkg/envd/engine.go +++ b/pkg/envd/engine.go @@ -20,16 +20,9 @@ import ( dockertypes "github.com/docker/docker/api/types" - "github.com/tensorchord/envd/pkg/lang/ir" "github.com/tensorchord/envd/pkg/types" ) -type Engine interface { - ImageClient - EnvironmentClient - VersionClient -} - type EnvironmentClient interface { PauseEnvironment(ctx context.Context, env string) (string, error) ResumeEnvironment(ctx context.Context, env string) (string, error) @@ -39,9 +32,7 @@ type EnvironmentClient interface { CleanEnvdIfExists(ctx context.Context, name string, force bool) error // StartEnvd creates the container for the given tag and container name. - StartEnvd(ctx context.Context, tag, name, buildContext string, - gpuEnabled bool, numGPUs int, sshPort int, g ir.Graph, timeout time.Duration, - mountOptionsStr []string) (string, string, error) + StartEnvd(ctx context.Context, so StartOptions) (*StartResult, error) IsRunning(ctx context.Context, name string) (bool, error) Exists(ctx context.Context, name string) (bool, error) diff --git a/pkg/envd/envdserver.go b/pkg/envd/envdserver.go index dc273c9aa..fed8e3690 100644 --- a/pkg/envd/envdserver.go +++ b/pkg/envd/envdserver.go @@ -20,14 +20,17 @@ import ( "github.com/cockroachdb/errors" dockertypes "github.com/docker/docker/api/types" + "github.com/sirupsen/logrus" + servertypes "github.com/tensorchord/envd-server/api/types" "github.com/tensorchord/envd-server/client" + v1 "k8s.io/api/core/v1" - "github.com/tensorchord/envd/pkg/lang/ir" "github.com/tensorchord/envd/pkg/types" ) type envdServerEngine struct { *client.Client + IdentityToken string } func (e *envdServerEngine) ListImage(ctx context.Context) ([]types.EnvdImage, error) { @@ -75,14 +78,44 @@ func (e *envdServerEngine) CleanEnvdIfExists(ctx context.Context, name string, f } // StartEnvd creates the container for the given tag and container name. -func (e *envdServerEngine) StartEnvd(ctx context.Context, tag, name, buildContext string, - gpuEnabled bool, numGPUs int, sshPort int, g ir.Graph, timeout time.Duration, - mountOptionsStr []string) (string, string, error) { - return "", "", errors.New("not implemented") +func (e *envdServerEngine) StartEnvd(ctx context.Context, so StartOptions) (*StartResult, error) { + if so.EnvdServerSource == nil { + return nil, errors.New("failed to get the envd server specific options") + } + + req := servertypes.EnvironmentCreateRequest{ + IdentityToken: e.IdentityToken, + Image: so.Image, + } + + resp, err := e.EnvironmentCreate(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "failed to create the environment") + } + + if err := e.WaitUntilRunning( + ctx, resp.ID, so.Timeout); err != nil { + return nil, errors.Wrap(err, "failed to wait until the container is running") + } + + result := &StartResult{ + SSHPort: 2222, + Address: "", + Name: resp.ID, + } + return result, nil } func (e *envdServerEngine) IsRunning(ctx context.Context, name string) (bool, error) { - return false, errors.New("not implemented") + req := servertypes.EnvironmentListRequest{ + IdentityToken: e.IdentityToken, + } + + resp, err := e.EnvironmentList(ctx, req) + if err != nil { + return false, errors.Wrap(err, "failed to list the environment") + } + return resp.Pod.Status.Phase == v1.PodRunning, nil } func (e *envdServerEngine) Exists(ctx context.Context, name string) (bool, error) { @@ -90,5 +123,27 @@ func (e *envdServerEngine) Exists(ctx context.Context, name string) (bool, error } func (e *envdServerEngine) WaitUntilRunning(ctx context.Context, name string, timeout time.Duration) error { - return errors.New("not implemented") + logger := logrus.WithField("container", name) + logger.Debug("waiting to start") + + // First, wait for the container to be marked as started. + ctxTimeout, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + for { + select { + case <-time.After(waitingInterval): + isRunning, err := e.IsRunning(ctxTimeout, name) + if err != nil { + // Has not yet started. Keep waiting. + return errors.Wrap(err, "failed to check if environment is running") + } + if isRunning { + logger.Debug("the environment is running") + return nil + } + + case <-ctxTimeout.Done(): + return errors.Errorf("timeout %s: environment did not start", timeout) + } + } } diff --git a/pkg/envd/factory.go b/pkg/envd/factory.go index 2053493b4..56c932093 100644 --- a/pkg/envd/factory.go +++ b/pkg/envd/factory.go @@ -21,6 +21,7 @@ import ( "github.com/docker/docker/client" envdclient "github.com/tensorchord/envd-server/client" + "github.com/tensorchord/envd/pkg/home" "github.com/tensorchord/envd/pkg/types" ) @@ -33,12 +34,18 @@ func New(ctx context.Context, opt Options) (Engine, error) { return nil, errors.New("failed to get the context") } if opt.Context.Runner == types.RunnerTypeEnvdServer { + ac, err := home.GetManager().AuthGetCurrent() + if err != nil { + return nil, err + } + cli, err := envdclient.NewClientWithOpts(envdclient.FromEnv) if err != nil { return nil, err } return &envdServerEngine{ - Client: cli, + Client: cli, + IdentityToken: ac.IdentityToken, }, nil } else { cli, err := client.NewClientWithOpts( diff --git a/pkg/envd/types.go b/pkg/envd/types.go new file mode 100644 index 000000000..887df97c1 --- /dev/null +++ b/pkg/envd/types.go @@ -0,0 +1,65 @@ +// 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 envd + +import ( + "time" + + "github.com/tensorchord/envd/pkg/lang/ir" +) + +const ( + localhost = "127.0.0.1" +) + +var ( + waitingInterval = 1 * time.Second +) + +type StartOptions struct { + Image string + EnvironmentName string + BuildContext string + NumGPU int + Timeout time.Duration + Forced bool + + EngineSource +} + +type EngineSource struct { + DockerSource *DockerSource + EnvdServerSource *EnvdServerSource +} + +type DockerSource struct { + Graph ir.Graph + MountOptions []string +} + +type EnvdServerSource struct{} + +type Engine interface { + ImageClient + EnvironmentClient + VersionClient +} + +type StartResult struct { + // TODO(gaocegege): Make result a chan, to send running status to the receiver. + SSHPort int + Address string + Name string +} diff --git a/pkg/lang/ir/compile.go b/pkg/lang/ir/compile.go index 9a5ec4ece..2f6d148eb 100644 --- a/pkg/lang/ir/compile.go +++ b/pkg/lang/ir/compile.go @@ -50,7 +50,7 @@ func NewGraph() *Graph { }, CUDA: nil, CUDNN: CUDNNVersionDefault, - NumGPUs: -1, + NumGPUs: 0, PyPIPackages: []string{}, RPackages: []string{}, diff --git a/pkg/ssh/config/entry.go b/pkg/ssh/config/entry.go new file mode 100644 index 000000000..752431718 --- /dev/null +++ b/pkg/ssh/config/entry.go @@ -0,0 +1,117 @@ +// 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 config + +import ( + "strconv" + + "github.com/sirupsen/logrus" + + "github.com/tensorchord/envd/pkg/util/osutil" +) + +type EntryOptions struct { + Name string + IFace string + Port int + PrivateKeyPath string + + EnableHostKeyCheck bool + EnableAgentForward bool + User string +} + +// AddEntry adds an entry to the user's sshconfig +func AddEntry(eo EntryOptions) error { + eo.Name = buildHostname(eo.Name) + err := add(getSSHConfigPath(), eo) + if err != nil { + return err + } + if osutil.IsWsl() { + logrus.Debug("Try adding entry to WSL's ssh-agent") + winSshConfig, err := osutil.GetWslHostSshConfig() + if err != nil { + return err + } + winKeyPath, err := osutil.CopyToWinEnvdHome(eo.PrivateKeyPath, 0600) + if err != nil { + return err + } + // Add the entry to the WSL host SSH config + logrus.Debugf("Adding entry to WSL's ssh-agent: %s", winSshConfig) + eo.PrivateKeyPath = winKeyPath + err = add(winSshConfig, eo) + if err != nil { + return err + } + } + return nil +} + +func add(path string, eo EntryOptions) error { + cfg, err := getConfig(path) + if err != nil { + return err + } + + _ = removeHost(cfg, eo.Name) + + host := newHost([]string{eo.Name}, []string{"entry generated by envd"}) + host.params = []*param{ + newParam(forwardAgentKeyword, []string{"yes"}, nil), + newParam(pubkeyAcceptedKeyTypesKeyword, []string{"+ssh-rsa"}, nil), + newParam(hostKeyAlgorithms, []string{"+ssh-rsa"}, nil), + newParam(hostNameKeyword, []string{eo.IFace}, nil), + newParam(portKeyword, []string{strconv.Itoa(eo.Port)}, nil), + newParam(userKnownHostsFileKeyword, []string{"/dev/null"}, nil), + newParam(identityFile, []string{"\"" + eo.PrivateKeyPath + "\""}, nil), + } + if !eo.EnableHostKeyCheck { + host.params = append(host.params, + newParam(strictHostKeyCheckingKeyword, []string{"no"}, nil)) + } + if eo.EnableAgentForward { + host.params = append(host.params, + newParam(forwardAgentKeyword, []string{"yes"}, nil)) + } + if eo.User != "" { + host.params = append(host.params, + newParam(userKeyword, []string{eo.User}, nil)) + } + + cfg.hosts = append(cfg.hosts, host) + return save(cfg, path) +} + +// RemoveEntry removes the entry to the user's sshconfig if found +func RemoveEntry(name string) error { + err := remove(getSSHConfigPath(), buildHostname(name)) + if err != nil { + return err + } + if osutil.IsWsl() { + logrus.Debug("Try removing entry from WSL's ssh-agent") + winSshConfig, err := osutil.GetWslHostSshConfig() + if err != nil { + return err + } + err = remove(winSshConfig, buildHostname(name)) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/ssh/config/ssh_config.go b/pkg/ssh/config/ssh_config.go index 4865d36e5..418757255 100644 --- a/pkg/ssh/config/ssh_config.go +++ b/pkg/ssh/config/ssh_config.go @@ -61,6 +61,7 @@ const ( hostKeyAlgorithms = "HostKeyAlgorithms" userKnownHostsFileKeyword = "UserKnownHostsFile" identityFile = "IdentityFile" + userKeyword = "User" ) func newHost(hostnames, comments []string) *host { @@ -304,32 +305,6 @@ func buildHostname(name string) string { return fmt.Sprintf("%s.envd", name) } -// AddEntry adds an entry to the user's sshconfig -func AddEntry(name, iface string, port int, privateKeyPath string) error { - err := add(getSSHConfigPath(), buildHostname(name), iface, port, privateKeyPath) - if err != nil { - return err - } - if osutil.IsWsl() { - logrus.Debug("Try adding entry to WSL's ssh-agent") - winSshConfig, err := osutil.GetWslHostSshConfig() - if err != nil { - return err - } - winKeyPath, err := osutil.CopyToWinEnvdHome(privateKeyPath, 0600) - if err != nil { - return err - } - // Add the entry to the WSL host SSH config - logrus.Debugf("Adding entry to WSL's ssh-agent: %s", winSshConfig) - err = add(winSshConfig, buildHostname(name), iface, port, winKeyPath) - if err != nil { - return err - } - } - return nil -} - func ReplaceKeyManagedByEnvd(oldKey string, newKey string) error { cfg, err := getConfig(getSSHConfigPath()) if err != nil { @@ -404,53 +379,6 @@ func ReplaceKeyManagedByEnvd(oldKey string, newKey string) error { return nil } -func add(path, name, iface string, port int, privateKeyPath string) error { - cfg, err := getConfig(path) - if err != nil { - return err - } - - _ = removeHost(cfg, name) - - // TODO: Use private key to authenticate ssh - // _, privateKey := getKeyPaths() - - host := newHost([]string{name}, []string{"entry generated by envd"}) - host.params = []*param{ - newParam(forwardAgentKeyword, []string{"yes"}, nil), - newParam(pubkeyAcceptedKeyTypesKeyword, []string{"+ssh-rsa"}, nil), - newParam(hostKeyAlgorithms, []string{"+ssh-rsa"}, nil), - newParam(hostNameKeyword, []string{iface}, nil), - newParam(portKeyword, []string{strconv.Itoa(port)}, nil), - newParam(strictHostKeyCheckingKeyword, []string{"no"}, nil), - newParam(userKnownHostsFileKeyword, []string{"/dev/null"}, nil), - newParam(identityFile, []string{"\"" + privateKeyPath + "\""}, nil), - } - - cfg.hosts = append(cfg.hosts, host) - return save(cfg, path) -} - -// RemoveEntry removes the entry to the user's sshconfig if found -func RemoveEntry(name string) error { - err := remove(getSSHConfigPath(), buildHostname(name)) - if err != nil { - return err - } - if osutil.IsWsl() { - logrus.Debug("Try removing entry from WSL's ssh-agent") - winSshConfig, err := osutil.GetWslHostSshConfig() - if err != nil { - return err - } - err = remove(winSshConfig, buildHostname(name)) - if err != nil { - return err - } - } - return nil -} - // GetPort returns the corresponding SSH port for the dev env func GetPort(name string) (int, error) { cfg, err := getConfig(getSSHConfigPath()) diff --git a/pkg/ssh/config/ssh_config_test.go b/pkg/ssh/config/ssh_config_test.go index eeffe127b..5e731e1b2 100644 --- a/pkg/ssh/config/ssh_config_test.go +++ b/pkg/ssh/config/ssh_config_test.go @@ -26,7 +26,15 @@ var _ = Describe("ssh config", func() { iface := "localhost" port := 8888 keyPath := "key" - err := add(getSSHConfigPath(), buildHostname(env), iface, port, keyPath) + eo := EntryOptions{ + Name: buildHostname(env), + IFace: iface, + Port: port, + PrivateKeyPath: keyPath, + EnableHostKeyCheck: false, + EnableAgentForward: true, + } + err := add(getSSHConfigPath(), eo) Expect(err).NotTo(HaveOccurred()) actual, err := GetPort(env)