diff --git a/cmd/skywire-cli/commands/node/app.go b/cmd/skywire-cli/commands/node/app.go index 00f267fd0..f7afa616a 100644 --- a/cmd/skywire-cli/commands/node/app.go +++ b/cmd/skywire-cli/commands/node/app.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strconv" + "strings" "text/tabwriter" "github.com/spf13/cobra" @@ -18,6 +19,7 @@ func init() { startAppCmd, stopAppCmd, setAppAutostartCmd, + execCmd, ) } @@ -82,3 +84,14 @@ var setAppAutostartCmd = &cobra.Command{ fmt.Println("OK") }, } + +var execCmd = &cobra.Command{ + Use: "exec ", + Short: "Executes the given command", + Args: cobra.MinimumNArgs(1), + Run: func(_ *cobra.Command, args []string) { + out, err := rpcClient().Exec(strings.Join(args, " ")) + internal.Catch(err) + fmt.Println(string(out)) + }, +} diff --git a/pkg/hypervisor/hypervisor.go b/pkg/hypervisor/hypervisor.go index d7203b5b6..b88ebdf25 100644 --- a/pkg/hypervisor/hypervisor.go +++ b/pkg/hypervisor/hypervisor.go @@ -129,6 +129,7 @@ func (m *Node) ServeHTTP(w http.ResponseWriter, req *http.Request) { } r.Get("/user", m.users.UserInfo()) r.Post("/change-password", m.users.ChangePassword()) + r.Post("/exec", m.exec()) r.Get("/nodes", m.getNodes()) r.Get("/nodes/{pk}", m.getNode()) r.Get("/nodes/{pk}/apps", m.getApps()) @@ -150,6 +151,30 @@ func (m *Node) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.ServeHTTP(w, req) } +// executes a command and returns its output +func (m *Node) exec() http.HandlerFunc { + return m.withCtx(m.nodeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { + var reqBody struct { + Command string `json:"command"` + } + if err := httputil.ReadJSON(r, &reqBody); err != nil { + httputil.WriteJSON(w, r, http.StatusBadRequest, err) + return + } + + out, err := ctx.RPC.Exec(reqBody.Command) + if err != nil { + httputil.WriteJSON(w, r, http.StatusInternalServerError, err) + return + } + + output := struct { + Output string `json:"output"` + }{string(out)} + httputil.WriteJSON(w, r, http.StatusOK, output) + }) +} + type summaryResp struct { TCPAddr string `json:"tcp_addr"` *visor.Summary diff --git a/pkg/visor/rpc.go b/pkg/visor/rpc.go index 2f79d9d99..6b3113d75 100644 --- a/pkg/visor/rpc.go +++ b/pkg/visor/rpc.go @@ -92,6 +92,13 @@ func (r *RPC) Summary(_ *struct{}, out *Summary) error { return nil } +// Exec executes a given command in cmd and writes its output to out. +func (r *RPC) Exec(cmd *string, out *[]byte) error { + var err error + *out, err = r.node.Exec(*cmd) + return err +} + /* <<< APP MANAGEMENT >>> */ diff --git a/pkg/visor/rpc_client.go b/pkg/visor/rpc_client.go index 9296e0d10..f9dee8b6a 100644 --- a/pkg/visor/rpc_client.go +++ b/pkg/visor/rpc_client.go @@ -19,6 +19,7 @@ import ( // RPCClient represents a RPC Client implementation. type RPCClient interface { Summary() (*Summary, error) + Exec(command string) ([]byte, error) Apps() ([]*AppState, error) StartApp(appName string) error @@ -64,6 +65,13 @@ func (rc *rpcClient) Summary() (*Summary, error) { return out, err } +// Exec calls Exec. +func (rc *rpcClient) Exec(command string) ([]byte, error) { + output := make([]byte, 0) + err := rc.Call("Exec", &command, &output) + return output, err +} + // Apps calls Apps. func (rc *rpcClient) Apps() ([]*AppState, error) { states := make([]*AppState, 0) @@ -277,6 +285,11 @@ func (mc *mockRPCClient) Summary() (*Summary, error) { return &out, err } +// Exec implements RPCClient. +func (mc *mockRPCClient) Exec(command string) ([]byte, error) { + return []byte("mock"), nil +} + // Apps implements RPCClient. func (mc *mockRPCClient) Apps() ([]*AppState, error) { var apps []*AppState diff --git a/pkg/visor/rpc_test.go b/pkg/visor/rpc_test.go index 3f151ea60..9472d4f36 100644 --- a/pkg/visor/rpc_test.go +++ b/pkg/visor/rpc_test.go @@ -180,6 +180,22 @@ func TestRPC(t *testing.T) { // }) }) + t.Run("Exec", func(t *testing.T) { + command := "echo 1" + + t.Run("RPCServer", func(t *testing.T) { + var out []byte + require.NoError(t, gateway.Exec(&command, &out)) + assert.Equal(t, []byte("1\n"), out) + }) + + // t.Run("RPCClient", func(t *testing.T) { + // out, err := client.Exec(command) + // require.NoError(t, err) + // assert.Equal(t, []byte("1\n"), out) + // }) + }) + t.Run("Apps", func(t *testing.T) { test := func(t *testing.T, apps []*AppState) { assert.Len(t, apps, 2) diff --git a/pkg/visor/visor.go b/pkg/visor/visor.go index f8696f528..addb4ae99 100644 --- a/pkg/visor/visor.go +++ b/pkg/visor/visor.go @@ -355,6 +355,13 @@ func (node *Node) Close() (err error) { return err } +// Exec executes a shell command. It returns combined stdout and stderr output and an error. +func (node *Node) Exec(command string) ([]byte, error) { + args := strings.Split(command, " ") + cmd := exec.Command(args[0], args[1:]...) // nolint: gosec + return cmd.CombinedOutput() +} + // Apps returns list of AppStates for all registered apps. func (node *Node) Apps() []*AppState { res := make([]*AppState, 0)