diff options
Diffstat (limited to 'lib/apiv1')
-rw-r--r-- | lib/apiv1/apiv1.go | 49 | ||||
-rw-r--r-- | lib/apiv1/config.go | 45 | ||||
-rw-r--r-- | lib/apiv1/exec.go | 154 | ||||
-rw-r--r-- | lib/apiv1/folders.go | 77 | ||||
-rw-r--r-- | lib/apiv1/make.go | 151 | ||||
-rw-r--r-- | lib/apiv1/version.go | 24 |
6 files changed, 500 insertions, 0 deletions
diff --git a/lib/apiv1/apiv1.go b/lib/apiv1/apiv1.go new file mode 100644 index 0000000..56c7503 --- /dev/null +++ b/lib/apiv1/apiv1.go @@ -0,0 +1,49 @@ +package apiv1 + +import ( + "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" + + "github.com/iotbzh/xds-server/lib/session" + "github.com/iotbzh/xds-server/lib/xdsconfig" +) + +// APIService . +type APIService struct { + router *gin.Engine + apiRouter *gin.RouterGroup + sessions *session.Sessions + cfg xdsconfig.Config + log *logrus.Logger +} + +// New creates a new instance of API service +func New(sess *session.Sessions, cfg xdsconfig.Config, r *gin.Engine) *APIService { + s := &APIService{ + router: r, + sessions: sess, + apiRouter: r.Group("/api/v1"), + cfg: cfg, + log: cfg.Log, + } + + s.apiRouter.GET("/version", s.getVersion) + + s.apiRouter.GET("/config", s.getConfig) + s.apiRouter.POST("/config", s.setConfig) + + s.apiRouter.GET("/folders", s.getFolders) + s.apiRouter.GET("/folder/:id", s.getFolder) + s.apiRouter.POST("/folder", s.addFolder) + s.apiRouter.DELETE("/folder/:id", s.delFolder) + + s.apiRouter.POST("/make", s.buildMake) + s.apiRouter.POST("/make/:id", s.buildMake) + + /* TODO: to be tested and then enabled + s.apiRouter.POST("/exec", s.execCmd) + s.apiRouter.POST("/exec/:id", s.execCmd) + */ + + return s +} diff --git a/lib/apiv1/config.go b/lib/apiv1/config.go new file mode 100644 index 0000000..a2817a0 --- /dev/null +++ b/lib/apiv1/config.go @@ -0,0 +1,45 @@ +package apiv1 + +import ( + "net/http" + "sync" + + "github.com/gin-gonic/gin" + "github.com/iotbzh/xds-server/lib/common" + "github.com/iotbzh/xds-server/lib/xdsconfig" +) + +var confMut sync.Mutex + +// GetConfig returns server configuration +func (s *APIService) getConfig(c *gin.Context) { + confMut.Lock() + defer confMut.Unlock() + + c.JSON(http.StatusOK, s.cfg) +} + +// SetConfig sets server configuration +func (s *APIService) setConfig(c *gin.Context) { + // FIXME - must be tested + c.JSON(http.StatusNotImplemented, "Not implemented") + + var cfgArg xdsconfig.Config + + if c.BindJSON(&cfgArg) != nil { + common.APIError(c, "Invalid arguments") + return + } + + confMut.Lock() + defer confMut.Unlock() + + s.log.Debugln("SET config: ", cfgArg) + + if err := s.cfg.UpdateAll(cfgArg); err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, s.cfg) +} diff --git a/lib/apiv1/exec.go b/lib/apiv1/exec.go new file mode 100644 index 0000000..f7beea6 --- /dev/null +++ b/lib/apiv1/exec.go @@ -0,0 +1,154 @@ +package apiv1 + +import ( + "net/http" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/iotbzh/xds-server/lib/common" +) + +// ExecArgs JSON parameters of /exec command +type ExecArgs struct { + ID string `json:"id"` + RPath string `json:"rpath"` // relative path into project + Cmd string `json:"cmd" binding:"required"` + Args []string `json:"args"` + CmdTimeout int `json:"timeout"` // command completion timeout in Second +} + +// ExecOutMsg Message send on each output (stdout+stderr) of executed command +type ExecOutMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:timestamp` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +// ExecExitMsg Message send when executed command exited +type ExecExitMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:timestamp` + Code int `json:"code"` + Error error `json:"error"` +} + +// Event name send in WS +const ExecOutEvent = "exec:output" +const ExecExitEvent = "exec:exit" + +var execCommandID = 1 + +// ExecCmd executes remotely a command +func (s *APIService) execCmd(c *gin.Context) { + var args ExecArgs + if c.BindJSON(&args) != nil { + common.APIError(c, "Invalid arguments") + return + } + + // TODO: add permission + + // Retrieve session info + sess := s.sessions.Get(c) + if sess == nil { + common.APIError(c, "Unknown sessions") + return + } + sop := sess.IOSocket + if sop == nil { + common.APIError(c, "Websocket not established") + return + } + + // Allow to pass id in url (/exec/:id) or as JSON argument + id := c.Param("id") + if id == "" { + id = args.ID + } + if id == "" { + common.APIError(c, "Invalid id") + return + } + + prj := s.cfg.GetFolderFromID(id) + if prj == nil { + common.APIError(c, "Unknown id") + return + } + + execTmo := args.CmdTimeout + if execTmo == 0 { + // TODO get default timeout from config.json file + execTmo = 24 * 60 * 60 // 1 day + } + + // Define callback for output + var oCB common.EmitOutputCB + oCB = func(sid string, id int, stdout, stderr string) { + // IO socket can be nil when disconnected + so := s.sessions.IOSocketGet(sid) + if so == nil { + s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id) + return + } + s.log.Debugf("%s emitted - WS sid %s - id:%d", ExecOutEvent, sid, id) + + // FIXME replace by .BroadcastTo a room + err := (*so).Emit(ExecOutEvent, ExecOutMsg{ + CmdID: strconv.Itoa(id), + Timestamp: time.Now().String(), + Stdout: stdout, + Stderr: stderr, + }) + if err != nil { + s.log.Errorf("WS Emit : %v", err) + } + } + + // Define callback for output + eCB := func(sid string, id int, code int, err error) { + s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err) + + // IO socket can be nil when disconnected + so := s.sessions.IOSocketGet(sid) + if so == nil { + s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id) + return + } + + // FIXME replace by .BroadcastTo a room + e := (*so).Emit(ExecExitEvent, ExecExitMsg{ + CmdID: strconv.Itoa(id), + Timestamp: time.Now().String(), + Code: code, + Error: err, + }) + if e != nil { + s.log.Errorf("WS Emit : %v", e) + } + } + + cmdID := execCommandID + execCommandID++ + + cmd := "cd " + prj.GetFullPath(args.RPath) + " && " + args.Cmd + if len(args.Args) > 0 { + cmd += " " + strings.Join(args.Args, " ") + } + + s.log.Debugf("Execute [Cmd ID %d]: %v %v", cmdID, cmd) + err := common.ExecPipeWs(cmd, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB) + if err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, + gin.H{ + "status": "OK", + "cmdID": cmdID, + }) +} diff --git a/lib/apiv1/folders.go b/lib/apiv1/folders.go new file mode 100644 index 0000000..b1864a2 --- /dev/null +++ b/lib/apiv1/folders.go @@ -0,0 +1,77 @@ +package apiv1 + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/iotbzh/xds-server/lib/common" + "github.com/iotbzh/xds-server/lib/xdsconfig" +) + +// getFolders returns all folders configuration +func (s *APIService) getFolders(c *gin.Context) { + confMut.Lock() + defer confMut.Unlock() + + c.JSON(http.StatusOK, s.cfg.Folders) +} + +// getFolder returns a specific folder configuration +func (s *APIService) getFolder(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil || id < 0 || id > len(s.cfg.Folders) { + common.APIError(c, "Invalid id") + return + } + + confMut.Lock() + defer confMut.Unlock() + + c.JSON(http.StatusOK, s.cfg.Folders[id]) +} + +// addFolder adds a new folder to server config +func (s *APIService) addFolder(c *gin.Context) { + var cfgArg xdsconfig.FolderConfig + if c.BindJSON(&cfgArg) != nil { + common.APIError(c, "Invalid arguments") + return + } + + confMut.Lock() + defer confMut.Unlock() + + s.log.Debugln("Add folder config: ", cfgArg) + + newFld, err := s.cfg.UpdateFolder(cfgArg) + if err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, newFld) +} + +// delFolder deletes folder from server config +func (s *APIService) delFolder(c *gin.Context) { + id := c.Param("id") + if id == "" { + common.APIError(c, "Invalid id") + return + } + + confMut.Lock() + defer confMut.Unlock() + + s.log.Debugln("Delete folder id ", id) + + var delEntry xdsconfig.FolderConfig + var err error + if delEntry, err = s.cfg.DeleteFolder(id); err != nil { + common.APIError(c, err.Error()) + return + } + c.JSON(http.StatusOK, delEntry) + +} diff --git a/lib/apiv1/make.go b/lib/apiv1/make.go new file mode 100644 index 0000000..eac6210 --- /dev/null +++ b/lib/apiv1/make.go @@ -0,0 +1,151 @@ +package apiv1 + +import ( + "net/http" + + "time" + + "strconv" + + "github.com/gin-gonic/gin" + "github.com/iotbzh/xds-server/lib/common" +) + +// MakeArgs is the parameters (json format) of /make command +type MakeArgs struct { + ID string `json:"id"` + RPath string `json:"rpath"` // relative path into project + Args string `json:"args"` + CmdTimeout int `json:"timeout"` // command completion timeout in Second +} + +// MakeOutMsg Message send on each output (stdout+stderr) of make command +type MakeOutMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:timestamp` + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +// MakeExitMsg Message send on make command exit +type MakeExitMsg struct { + CmdID string `json:"cmdID"` + Timestamp string `json:timestamp` + Code int `json:"code"` + Error error `json:"error"` +} + +// Event name send in WS +const MakeOutEvent = "make:output" +const MakeExitEvent = "make:exit" + +var makeCommandID = 1 + +func (s *APIService) buildMake(c *gin.Context) { + var args MakeArgs + + if c.BindJSON(&args) != nil { + common.APIError(c, "Invalid arguments") + return + } + + sess := s.sessions.Get(c) + if sess == nil { + common.APIError(c, "Unknown sessions") + return + } + sop := sess.IOSocket + if sop == nil { + common.APIError(c, "Websocket not established") + return + } + + // Allow to pass id in url (/make/:id) or as JSON argument + id := c.Param("id") + if id == "" { + id = args.ID + } + if id == "" { + common.APIError(c, "Invalid id") + return + } + + prj := s.cfg.GetFolderFromID(id) + if prj == nil { + common.APIError(c, "Unknown id") + return + } + + execTmo := args.CmdTimeout + if execTmo == 0 { + // TODO get default timeout from config.json file + execTmo = 24 * 60 * 60 // 1 day + } + + cmd := "cd " + prj.GetFullPath(args.RPath) + " && make" + if args.Args != "" { + cmd += " " + args.Args + } + + // Define callback for output + var oCB common.EmitOutputCB + oCB = func(sid string, id int, stdout, stderr string) { + // IO socket can be nil when disconnected + so := s.sessions.IOSocketGet(sid) + if so == nil { + s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", MakeOutEvent, sid, id) + return + } + s.log.Debugf("%s emitted - WS sid %s - id:%d", MakeOutEvent, sid, id) + + // FIXME replace by .BroadcastTo a room + err := (*so).Emit(MakeOutEvent, MakeOutMsg{ + CmdID: strconv.Itoa(id), + Timestamp: time.Now().String(), + Stdout: stdout, + Stderr: stderr, + }) + if err != nil { + s.log.Errorf("WS Emit : %v", err) + } + } + + // Define callback for output + eCB := func(sid string, id int, code int, err error) { + s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err) + + // IO socket can be nil when disconnected + so := s.sessions.IOSocketGet(sid) + if so == nil { + s.log.Infof("%s not emitted - WS closed (id:%d", MakeExitEvent, id) + return + } + + // FIXME replace by .BroadcastTo a room + e := (*so).Emit(MakeExitEvent, MakeExitMsg{ + CmdID: strconv.Itoa(id), + Timestamp: time.Now().String(), + Code: code, + Error: err, + }) + if e != nil { + s.log.Errorf("WS Emit : %v", e) + } + } + + cmdID := makeCommandID + makeCommandID++ + + s.log.Debugf("Execute [Cmd ID %d]: %v", cmdID, cmd) + err := common.ExecPipeWs(cmd, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB) + if err != nil { + common.APIError(c, err.Error()) + return + } + + c.JSON(http.StatusOK, + gin.H{ + "status": "OK", + "cmdID": cmdID, + }) +} diff --git a/lib/apiv1/version.go b/lib/apiv1/version.go new file mode 100644 index 0000000..e022441 --- /dev/null +++ b/lib/apiv1/version.go @@ -0,0 +1,24 @@ +package apiv1 + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type version struct { + Version string `json:"version"` + APIVersion string `json:"apiVersion"` + VersionGitTag string `json:"gitTag"` +} + +// getInfo : return various information about server +func (s *APIService) getVersion(c *gin.Context) { + response := version{ + Version: s.cfg.Version, + APIVersion: s.cfg.APIVersion, + VersionGitTag: s.cfg.VersionGitTag, + } + + c.JSON(http.StatusOK, response) +} |