diff --git a/flytectl/cmd/sandbox/exec.go b/flytectl/cmd/sandbox/exec.go new file mode 100644 index 0000000000..804d54b6c6 --- /dev/null +++ b/flytectl/cmd/sandbox/exec.go @@ -0,0 +1,45 @@ +package sandbox + +import ( + "context" + "fmt" + + cmdCore "github.com/flyteorg/flytectl/cmd/core" + "github.com/flyteorg/flytectl/pkg/docker" +) + +const ( + execShort = "Execute any command in sandbox" + execLong = ` +Execute command will Will run non-interactive commands and return immediately with the output. + +:: + bin/flytectl sandbox exec -- ls -al + +Usage` +) + +func sandboxClusterExec(ctx context.Context, args []string, cmdCtx cmdCore.CommandContext) error { + cli, err := docker.GetDockerClient() + if err != nil { + return err + } + if len(args) > 0 { + return Execute(ctx, cli, args) + } + return fmt.Errorf("missing argument. Please check usage examples by running flytectl sandbox exec --help") +} + +func Execute(ctx context.Context, cli docker.Docker, args []string) error { + c := docker.GetSandbox(ctx, cli) + if c != nil { + exec, err := docker.ExecCommend(ctx, cli, c.ID, args) + if err != nil { + return err + } + if err := docker.InspectExecResp(ctx, cli, exec.ID); err != nil { + return err + } + } + return nil +} diff --git a/flytectl/cmd/sandbox/exec_test.go b/flytectl/cmd/sandbox/exec_test.go new file mode 100644 index 0000000000..4aa0e2148a --- /dev/null +++ b/flytectl/cmd/sandbox/exec_test.go @@ -0,0 +1,72 @@ +package sandbox + +import ( + "bufio" + "context" + "fmt" + "io" + "strings" + "testing" + + cmdCore "github.com/flyteorg/flytectl/cmd/core" + "github.com/stretchr/testify/assert" + + "github.com/docker/docker/api/types" + "github.com/flyteorg/flytectl/pkg/docker" + "github.com/flyteorg/flytectl/pkg/docker/mocks" + "github.com/stretchr/testify/mock" +) + +func TestSandboxClusterExec(t *testing.T) { + mockDocker := &mocks.Docker{} + mockOutStream := new(io.Writer) + ctx := context.Background() + cmdCtx := cmdCore.NewCommandContext(nil, *mockOutStream) + reader := bufio.NewReader(strings.NewReader("test")) + + mockDocker.OnContainerList(ctx, types.ContainerListOptions{All: true}).Return([]types.Container{ + { + ID: docker.FlyteSandboxClusterName, + Names: []string{ + docker.FlyteSandboxClusterName, + }, + }, + }, nil) + docker.ExecConfig.Cmd = []string{"ls -al"} + mockDocker.OnContainerExecCreateMatch(ctx, mock.Anything, docker.ExecConfig).Return(types.IDResponse{}, nil) + mockDocker.OnContainerExecInspectMatch(ctx, mock.Anything).Return(types.ContainerExecInspect{}, nil) + mockDocker.OnContainerExecAttachMatch(ctx, mock.Anything, types.ExecStartCheck{}).Return(types.HijackedResponse{ + Reader: reader, + }, fmt.Errorf("Test")) + docker.Client = mockDocker + err := sandboxClusterExec(ctx, []string{"ls -al"}, cmdCtx) + + assert.NotNil(t, err) +} + +func TestSandboxClusterExecWithoutCmd(t *testing.T) { + mockDocker := &mocks.Docker{} + mockOutStream := new(io.Writer) + ctx := context.Background() + cmdCtx := cmdCore.NewCommandContext(nil, *mockOutStream) + reader := bufio.NewReader(strings.NewReader("test")) + + mockDocker.OnContainerList(ctx, types.ContainerListOptions{All: true}).Return([]types.Container{ + { + ID: docker.FlyteSandboxClusterName, + Names: []string{ + docker.FlyteSandboxClusterName, + }, + }, + }, nil) + docker.ExecConfig.Cmd = []string{} + mockDocker.OnContainerExecCreateMatch(ctx, mock.Anything, docker.ExecConfig).Return(types.IDResponse{}, nil) + mockDocker.OnContainerExecInspectMatch(ctx, mock.Anything).Return(types.ContainerExecInspect{}, nil) + mockDocker.OnContainerExecAttachMatch(ctx, mock.Anything, types.ExecStartCheck{}).Return(types.HijackedResponse{ + Reader: reader, + }, fmt.Errorf("Test")) + docker.Client = mockDocker + err := sandboxClusterExec(ctx, []string{}, cmdCtx) + + assert.NotNil(t, err) +} diff --git a/flytectl/cmd/sandbox/sandbox.go b/flytectl/cmd/sandbox/sandbox.go index 7ee1f37cb6..233f4f0eba 100644 --- a/flytectl/cmd/sandbox/sandbox.go +++ b/flytectl/cmd/sandbox/sandbox.go @@ -41,6 +41,9 @@ func CreateSandboxCommand() *cobra.Command { "status": {CmdFunc: sandboxClusterStatus, Aliases: []string{}, ProjectDomainNotRequired: true, Short: statusShort, Long: statusLong}, + "exec": {CmdFunc: sandboxClusterExec, Aliases: []string{}, ProjectDomainNotRequired: true, + Short: execShort, + Long: execLong, PFlagProvider: sandboxConfig.DefaultConfig}, } cmdcore.AddCommands(sandbox, sandboxResourcesFuncs) diff --git a/flytectl/cmd/sandbox/sandbox_test.go b/flytectl/cmd/sandbox/sandbox_test.go index 8537f434e9..640390c137 100644 --- a/flytectl/cmd/sandbox/sandbox_test.go +++ b/flytectl/cmd/sandbox/sandbox_test.go @@ -13,23 +13,27 @@ func TestCreateSandboxCommand(t *testing.T) { assert.Equal(t, sandboxCommand.Use, "sandbox") assert.Equal(t, sandboxCommand.Short, "Used for testing flyte sandbox.") fmt.Println(sandboxCommand.Commands()) - assert.Equal(t, len(sandboxCommand.Commands()), 3) + assert.Equal(t, len(sandboxCommand.Commands()), 4) cmdNouns := sandboxCommand.Commands() // Sort by Use value. sort.Slice(cmdNouns, func(i, j int) bool { return cmdNouns[i].Use < cmdNouns[j].Use }) - assert.Equal(t, cmdNouns[0].Use, "start") - assert.Equal(t, cmdNouns[0].Short, startShort) - assert.Equal(t, cmdNouns[0].Long, startLong) + assert.Equal(t, cmdNouns[0].Use, "exec") + assert.Equal(t, cmdNouns[0].Short, execShort) + assert.Equal(t, cmdNouns[0].Long, execLong) - assert.Equal(t, cmdNouns[1].Use, "status") - assert.Equal(t, cmdNouns[1].Short, statusShort) - assert.Equal(t, cmdNouns[1].Long, statusLong) + assert.Equal(t, cmdNouns[1].Use, "start") + assert.Equal(t, cmdNouns[1].Short, startShort) + assert.Equal(t, cmdNouns[1].Long, startLong) - assert.Equal(t, cmdNouns[2].Use, "teardown") - assert.Equal(t, cmdNouns[2].Short, teardownShort) - assert.Equal(t, cmdNouns[2].Long, teardownLong) + assert.Equal(t, cmdNouns[2].Use, "status") + assert.Equal(t, cmdNouns[2].Short, statusShort) + assert.Equal(t, cmdNouns[2].Long, statusLong) + + assert.Equal(t, cmdNouns[3].Use, "teardown") + assert.Equal(t, cmdNouns[3].Short, teardownShort) + assert.Equal(t, cmdNouns[3].Long, teardownLong) } diff --git a/flytectl/go.mod b/flytectl/go.mod index 93af1fc05b..f944444102 100644 --- a/flytectl/go.mod +++ b/flytectl/go.mod @@ -13,9 +13,11 @@ require ( github.com/flyteorg/flytestdlib v0.3.24 github.com/ghodss/yaml v1.0.0 github.com/golang/protobuf v1.4.3 + github.com/google/go-cmp v0.5.6 // indirect github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.2.0 + github.com/gorilla/mux v1.8.0 // indirect github.com/hashicorp/go-version v1.3.0 github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 github.com/kr/text v0.2.0 // indirect diff --git a/flytectl/go.sum b/flytectl/go.sum index d07374e6d8..6658e96614 100644 --- a/flytectl/go.sum +++ b/flytectl/go.sum @@ -438,8 +438,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -479,8 +480,9 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/flytectl/pkg/docker/docker.go b/flytectl/pkg/docker/docker.go index f33b3b5219..1cc53b6285 100644 --- a/flytectl/pkg/docker/docker.go +++ b/flytectl/pkg/docker/docker.go @@ -21,6 +21,9 @@ type Docker interface { ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) ContainerRemove(ctx context.Context, containerID string, options types.ContainerRemoveOptions) error ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) + ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error) + ContainerExecAttach(ctx context.Context, execID string, config types.ExecStartCheck) (types.HijackedResponse, error) + ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) } type FlyteDocker struct { diff --git a/flytectl/pkg/docker/sandbox_util.go b/flytectl/pkg/docker/docker_util.go similarity index 87% rename from flytectl/pkg/docker/sandbox_util.go rename to flytectl/pkg/docker/docker_util.go index 4ce230581b..03e220879f 100644 --- a/flytectl/pkg/docker/sandbox_util.go +++ b/flytectl/pkg/docker/docker_util.go @@ -40,6 +40,15 @@ var ( Target: K3sDir, }, } + ExecConfig = types.ExecConfig{ + AttachStderr: true, + Tty: true, + WorkingDir: FlyteSnackDir, + AttachStdout: true, + Cmd: []string{}, + } + StdWriterPrefixLen = 8 + StartingBufLen = 32*1024 + StdWriterPrefixLen + 1 ) // SetupFlyteDir will create .flyte dir if not exist @@ -192,3 +201,25 @@ func GetDockerClient() (Docker, error) { } return Client, nil } + +// ExecCommend will execute a command in container and returns an execution id +func ExecCommend(ctx context.Context, cli Docker, containerID string, command []string) (types.IDResponse, error) { + ExecConfig.Cmd = command + r, err := cli.ContainerExecCreate(ctx, containerID, ExecConfig) + if err != nil { + return types.IDResponse{}, err + } + return r, err +} + +func InspectExecResp(ctx context.Context, cli Docker, containerID string) error { + resp, err := cli.ContainerExecAttach(ctx, containerID, types.ExecStartCheck{}) + if err != nil { + return err + } + s := bufio.NewScanner(resp.Reader) + for s.Scan() { + fmt.Println(s.Text()) + } + return nil +} diff --git a/flytectl/pkg/docker/sandbox_util_test.go b/flytectl/pkg/docker/docker_util_test.go similarity index 83% rename from flytectl/pkg/docker/sandbox_util_test.go rename to flytectl/pkg/docker/docker_util_test.go index b39e03f4f6..65004a2b0b 100644 --- a/flytectl/pkg/docker/sandbox_util_test.go +++ b/flytectl/pkg/docker/docker_util_test.go @@ -311,5 +311,62 @@ func TestDockerClient(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, cli) }) +} + +func TestDockerExec(t *testing.T) { + t.Run("Successfully exec command in container", func(t *testing.T) { + ctx := context.Background() + mockDocker := &mocks.Docker{} + Client = mockDocker + c := ExecConfig + c.Cmd = []string{"ls"} + mockDocker.OnContainerExecCreateMatch(ctx, mock.Anything, c).Return(types.IDResponse{}, nil) + _, err := ExecCommend(ctx, mockDocker, "test", []string{"ls"}) + assert.Nil(t, err) + }) + t.Run("Failed exec command in container", func(t *testing.T) { + ctx := context.Background() + mockDocker := &mocks.Docker{} + Client = mockDocker + c := ExecConfig + c.Cmd = []string{"ls"} + mockDocker.OnContainerExecCreateMatch(ctx, mock.Anything, c).Return(types.IDResponse{}, fmt.Errorf("test")) + _, err := ExecCommend(ctx, mockDocker, "test", []string{"ls"}) + assert.NotNil(t, err) + }) +} + +func TestInspectExecResp(t *testing.T) { + t.Run("Failed exec command in container", func(t *testing.T) { + ctx := context.Background() + mockDocker := &mocks.Docker{} + Client = mockDocker + c := ExecConfig + c.Cmd = []string{"ls"} + reader := bufio.NewReader(strings.NewReader("test")) + + mockDocker.OnContainerExecInspectMatch(ctx, mock.Anything).Return(types.ContainerExecInspect{}, nil) + mockDocker.OnContainerExecAttachMatch(ctx, mock.Anything, types.ExecStartCheck{}).Return(types.HijackedResponse{ + Reader: reader, + }, fmt.Errorf("err")) + + err := InspectExecResp(ctx, mockDocker, "test") + assert.NotNil(t, err) + }) + t.Run("Successfully exec command in container", func(t *testing.T) { + ctx := context.Background() + mockDocker := &mocks.Docker{} + Client = mockDocker + c := ExecConfig + c.Cmd = []string{"ls"} + reader := bufio.NewReader(strings.NewReader("test")) + + mockDocker.OnContainerExecAttachMatch(ctx, mock.Anything, types.ExecStartCheck{}).Return(types.HijackedResponse{ + Reader: reader, + }, nil) + + err := InspectExecResp(ctx, mockDocker, "test") + assert.Nil(t, err) + }) } diff --git a/flytectl/pkg/docker/mocks/docker.go b/flytectl/pkg/docker/mocks/docker.go index 917546fbee..9655d46a9c 100644 --- a/flytectl/pkg/docker/mocks/docker.go +++ b/flytectl/pkg/docker/mocks/docker.go @@ -62,6 +62,123 @@ func (_m *Docker) ContainerCreate(ctx context.Context, config *container.Config, return r0, r1 } +type Docker_ContainerExecAttach struct { + *mock.Call +} + +func (_m Docker_ContainerExecAttach) Return(_a0 types.HijackedResponse, _a1 error) *Docker_ContainerExecAttach { + return &Docker_ContainerExecAttach{Call: _m.Call.Return(_a0, _a1)} +} + +func (_m *Docker) OnContainerExecAttach(ctx context.Context, execID string, config types.ExecStartCheck) *Docker_ContainerExecAttach { + c := _m.On("ContainerExecAttach", ctx, execID, config) + return &Docker_ContainerExecAttach{Call: c} +} + +func (_m *Docker) OnContainerExecAttachMatch(matchers ...interface{}) *Docker_ContainerExecAttach { + c := _m.On("ContainerExecAttach", matchers...) + return &Docker_ContainerExecAttach{Call: c} +} + +// ContainerExecAttach provides a mock function with given fields: ctx, execID, config +func (_m *Docker) ContainerExecAttach(ctx context.Context, execID string, config types.ExecStartCheck) (types.HijackedResponse, error) { + ret := _m.Called(ctx, execID, config) + + var r0 types.HijackedResponse + if rf, ok := ret.Get(0).(func(context.Context, string, types.ExecStartCheck) types.HijackedResponse); ok { + r0 = rf(ctx, execID, config) + } else { + r0 = ret.Get(0).(types.HijackedResponse) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, types.ExecStartCheck) error); ok { + r1 = rf(ctx, execID, config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type Docker_ContainerExecCreate struct { + *mock.Call +} + +func (_m Docker_ContainerExecCreate) Return(_a0 types.IDResponse, _a1 error) *Docker_ContainerExecCreate { + return &Docker_ContainerExecCreate{Call: _m.Call.Return(_a0, _a1)} +} + +func (_m *Docker) OnContainerExecCreate(ctx context.Context, _a1 string, config types.ExecConfig) *Docker_ContainerExecCreate { + c := _m.On("ContainerExecCreate", ctx, _a1, config) + return &Docker_ContainerExecCreate{Call: c} +} + +func (_m *Docker) OnContainerExecCreateMatch(matchers ...interface{}) *Docker_ContainerExecCreate { + c := _m.On("ContainerExecCreate", matchers...) + return &Docker_ContainerExecCreate{Call: c} +} + +// ContainerExecCreate provides a mock function with given fields: ctx, _a1, config +func (_m *Docker) ContainerExecCreate(ctx context.Context, _a1 string, config types.ExecConfig) (types.IDResponse, error) { + ret := _m.Called(ctx, _a1, config) + + var r0 types.IDResponse + if rf, ok := ret.Get(0).(func(context.Context, string, types.ExecConfig) types.IDResponse); ok { + r0 = rf(ctx, _a1, config) + } else { + r0 = ret.Get(0).(types.IDResponse) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, types.ExecConfig) error); ok { + r1 = rf(ctx, _a1, config) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type Docker_ContainerExecInspect struct { + *mock.Call +} + +func (_m Docker_ContainerExecInspect) Return(_a0 types.ContainerExecInspect, _a1 error) *Docker_ContainerExecInspect { + return &Docker_ContainerExecInspect{Call: _m.Call.Return(_a0, _a1)} +} + +func (_m *Docker) OnContainerExecInspect(ctx context.Context, execID string) *Docker_ContainerExecInspect { + c := _m.On("ContainerExecInspect", ctx, execID) + return &Docker_ContainerExecInspect{Call: c} +} + +func (_m *Docker) OnContainerExecInspectMatch(matchers ...interface{}) *Docker_ContainerExecInspect { + c := _m.On("ContainerExecInspect", matchers...) + return &Docker_ContainerExecInspect{Call: c} +} + +// ContainerExecInspect provides a mock function with given fields: ctx, execID +func (_m *Docker) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) { + ret := _m.Called(ctx, execID) + + var r0 types.ContainerExecInspect + if rf, ok := ret.Get(0).(func(context.Context, string) types.ContainerExecInspect); ok { + r0 = rf(ctx, execID) + } else { + r0 = ret.Get(0).(types.ContainerExecInspect) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, execID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + type Docker_ContainerList struct { *mock.Call }