From 4d843d2bde236ec23810d0904dfb8aebbc53a37b Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Fri, 24 Nov 2017 01:14:30 +0100 Subject: New dashboard improvements. - add build buttons - add build settings support and backup into project clientData - improved async alert - fixed project dropdown Signed-off-by: Sebastien Douheret --- lib/agent/apiv1-exec.go | 18 ++++++++----- lib/agent/apiv1-projects.go | 28 ++++++++++++++++++-- lib/agent/apiv1.go | 7 ++--- lib/agent/events.go | 9 ++++--- lib/agent/project-interface.go | 15 ++++++----- lib/agent/project-pathmap.go | 22 +++++++++++++--- lib/agent/project-st.go | 33 ++++++++++++++++++------ lib/agent/projects.go | 58 ++++++++++++++++++++++++++++++++++++++---- lib/agent/sessions.go | 8 ++++++ lib/agent/xdsserver.go | 23 +++++++++++++---- lib/apiv1/events.go | 7 ++--- lib/apiv1/projects.go | 6 +++++ 12 files changed, 186 insertions(+), 48 deletions(-) (limited to 'lib') diff --git a/lib/agent/apiv1-exec.go b/lib/agent/apiv1-exec.go index c199267..3cb4d23 100644 --- a/lib/agent/apiv1-exec.go +++ b/lib/agent/apiv1-exec.go @@ -5,15 +5,13 @@ import ( "io/ioutil" "net/http" + "github.com/franciscocpg/reflectme" "github.com/gin-gonic/gin" "github.com/iotbzh/xds-agent/lib/apiv1" common "github.com/iotbzh/xds-common/golib" uuid "github.com/satori/go.uuid" ) -var execCmdID = 1 -var fwdFuncID []uuid.UUID - // ExecCmd executes remotely a command func (s *APIService) execCmd(c *gin.Context) { s._execRequest("/exec", c) @@ -81,6 +79,7 @@ func (s *APIService) _execRequest(cmd string, c *gin.Context) { apiv1.ExecInferiorOutEvent, } + var fwdFuncID []uuid.UUID for _, evName := range evtList { evN := evName fwdFunc := func(pData interface{}, evData interface{}) error { @@ -92,6 +91,9 @@ func (s *APIService) _execRequest(cmd string, c *gin.Context) { return nil } + // Add sessionID to event Data + reflectme.SetField(evData, "sessionID", sid) + // Forward event to Client/Dashboard (*so).Emit(evN, evData) return nil @@ -110,15 +112,17 @@ func (s *APIService) _execRequest(cmd string, c *gin.Context) { evN := apiv1.ExecExitEvent sid := pData.(string) + // Add sessionID to event Data + reflectme.SetField(evData, "sessionID", sid) + // IO socket can be nil when disconnected so := s.sessions.IOSocketGet(sid) - if so == nil { + if so != nil { + (*so).Emit(evN, evData) + } else { s.Log.Infof("%s not emitted: WS closed (sid:%s)", evN, sid) - return nil } - (*so).Emit(evN, evData) - // cleanup listener for i, evName := range evtList { svr.EventOff(evName, fwdFuncID[i]) diff --git a/lib/agent/apiv1-projects.go b/lib/agent/apiv1-projects.go index c835967..5784896 100644 --- a/lib/agent/apiv1-projects.go +++ b/lib/agent/apiv1-projects.go @@ -39,7 +39,7 @@ func (s *APIService) addProject(c *gin.Context) { s.Log.Debugln("Add project config: ", cfgArg) - newFld, err := s.projects.Add(cfgArg) + newFld, err := s.projects.Add(cfgArg, s.sessions.GetID(c)) if err != nil { common.APIError(c, err.Error()) return @@ -77,10 +77,34 @@ func (s *APIService) delProject(c *gin.Context) { s.Log.Debugln("Delete project id ", id) - delEntry, err := s.projects.Delete(id) + delEntry, err := s.projects.Delete(id, s.sessions.GetID(c)) if err != nil { common.APIError(c, err.Error()) return } c.JSON(http.StatusOK, delEntry) } + +// updateProject Update some field of a specific project +func (s *APIService) updateProject(c *gin.Context) { + id, err := s.projects.ResolveID(c.Param("id")) + if err != nil { + common.APIError(c, err.Error()) + return + } + + var cfgArg apiv1.ProjectConfig + if c.BindJSON(&cfgArg) != nil { + common.APIError(c, "Invalid arguments") + return + } + + s.Log.Debugln("Update project id ", id) + + upPrj, err := s.projects.Update(id, cfgArg, s.sessions.GetID(c)) + if err != nil { + common.APIError(c, err.Error()) + return + } + c.JSON(http.StatusOK, upPrj) +} diff --git a/lib/agent/apiv1.go b/lib/agent/apiv1.go index 3e742f5..36e5a54 100644 --- a/lib/agent/apiv1.go +++ b/lib/agent/apiv1.go @@ -8,7 +8,7 @@ import ( "github.com/iotbzh/xds-agent/lib/xdsconfig" ) -const apiBaseUrl = "/api/v1" +const apiBaseURL = "/api/v1" // APIService . type APIService struct { @@ -21,7 +21,7 @@ type APIService struct { func NewAPIV1(ctx *Context) *APIService { s := &APIService{ Context: ctx, - apiRouter: ctx.webServer.router.Group(apiBaseUrl), + apiRouter: ctx.webServer.router.Group(apiBaseURL), serverIndex: 0, } @@ -34,6 +34,7 @@ func NewAPIV1(ctx *Context) *APIService { s.apiRouter.GET("/projects", s.getProjects) s.apiRouter.GET("/projects/:id", s.getProject) + s.apiRouter.PUT("/projects/:id", s.updateProject) s.apiRouter.POST("/projects", s.addProject) s.apiRouter.POST("/projects/sync/:id", s.syncProject) s.apiRouter.DELETE("/projects/:id", s.delProject) @@ -80,7 +81,7 @@ func (s *APIService) AddXdsServer(cfg xdsconfig.XDSServerConf) (*XdsServer, erro // Create a new server object if cfg.APIBaseURL == "" { - cfg.APIBaseURL = apiBaseUrl + cfg.APIBaseURL = apiBaseURL } if cfg.APIPartialURL == "" { cfg.APIPartialURL = "/server/" + strconv.Itoa(s.serverIndex) diff --git a/lib/agent/events.go b/lib/agent/events.go index 9ff72ac..ccf8ddc 100644 --- a/lib/agent/events.go +++ b/lib/agent/events.go @@ -71,7 +71,7 @@ func (e *Events) UnRegister(evName, sessionID string) error { } // Emit Used to manually emit an event -func (e *Events) Emit(evName string, data interface{}) error { +func (e *Events) Emit(evName string, data interface{},fromSid string) error { var firstErr error if _, ok := e.eventsMap[evName]; !ok { @@ -93,9 +93,10 @@ func (e *Events) Emit(evName string, data interface{}) error { continue } msg := apiv1.EventMsg{ - Time: time.Now().String(), - Type: evName, - Data: data, + Time: time.Now().String(), + FromSessionID: fromSid, + Type: evName, + Data: data, } e.Log.Debugf("Emit Event %s: %v", evName, sid) if err := (*so).Emit(evName, msg); err != nil { diff --git a/lib/agent/project-interface.go b/lib/agent/project-interface.go index c9e9ec5..0d6bb1a 100644 --- a/lib/agent/project-interface.go +++ b/lib/agent/project-interface.go @@ -4,11 +4,12 @@ import "github.com/iotbzh/xds-agent/lib/apiv1" // IPROJECT Project interface type IPROJECT interface { - Add(cfg apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) // Add a new project - Delete() error // Delete a project - GetProject() *apiv1.ProjectConfig // Get project public configuration - UpdateProject(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) // Update project configuration - GetServer() *XdsServer // Get XdsServer that holds this project - Sync() error // Force project files synchronization - IsInSync() (bool, error) // Check if project files are in-sync + Add(cfg apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) // Add a new project + Setup(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) // Local setup of the project + Delete() error // Delete a project + GetProject() *apiv1.ProjectConfig // Get project public configuration + Update(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) // Update project configuration + GetServer() *XdsServer // Get XdsServer that holds this project + Sync() error // Force project files synchronization + IsInSync() (bool, error) // Check if project files are in-sync } diff --git a/lib/agent/project-pathmap.go b/lib/agent/project-pathmap.go index 7a96e6e..3c87770 100644 --- a/lib/agent/project-pathmap.go +++ b/lib/agent/project-pathmap.go @@ -69,7 +69,7 @@ func (p *PathMap) Add(cfg apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { // Send request to create folder on XDS server side err = p.server.FolderAdd(fld, p.folder) if err != nil { - return nil, fmt.Errorf("Folders mapping verification failure.\n%v", err) + return nil, err } // 2nd part of sanity checker @@ -98,16 +98,30 @@ func (p *PathMap) GetProject() *apiv1.ProjectConfig { return &prj } -// UpdateProject Set project config -func (p *PathMap) UpdateProject(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { +// Setup Setup local project config +func (p *PathMap) Setup(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { p.folder = p.server.ProjectToFolder(prj) np := p.GetProject() - if err := p.events.Emit(apiv1.EVTProjectChange, np); err != nil { + if err := p.events.Emit(apiv1.EVTProjectChange, np, ""); err != nil { return np, err } return np, nil } +// Update Update some field of a project +func (p *PathMap) Update(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { + if p.folder.ID != prj.ID { + return nil, fmt.Errorf("Invalid id") + } + + err := p.server.FolderUpdate(p.server.ProjectToFolder(prj), p.folder) + if err != nil { + return nil, err + } + + return p.GetProject(), nil +} + // GetServer Get the XdsServer that holds this project func (p *PathMap) GetServer() *XdsServer { return p.server diff --git a/lib/agent/project-st.go b/lib/agent/project-st.go index e2cd3cb..c4e8fce 100644 --- a/lib/agent/project-st.go +++ b/lib/agent/project-st.go @@ -1,6 +1,8 @@ package agent import ( + "fmt" + "github.com/iotbzh/xds-agent/lib/apiv1" st "github.com/iotbzh/xds-agent/lib/syncthing" ) @@ -56,8 +58,8 @@ func (p *STProject) Add(cfg apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { p.Log.Errorf("Project ID in XDSServer and local ST differ: %s != %s", svrPrj.ID, locPrj.ID) } - // Use Update function to setup remains fields - return p.UpdateProject(*svrPrj) + // Use Setup function to setup remains fields + return p.Setup(*svrPrj) } // Delete a project @@ -77,16 +79,16 @@ func (p *STProject) GetProject() *apiv1.ProjectConfig { return &prj } -// UpdateProject Update project config -func (p *STProject) UpdateProject(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { +// Setup Setup local project config +func (p *STProject) Setup(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { // Update folder p.folder = p.server.ProjectToFolder(prj) svrPrj := p.GetProject() // Register events to update folder status // Register to XDS Server events - p.server.EventOn("event:FolderStateChanged", "", p._cbServerFolderChanged) - if err := p.server.EventRegister("FolderStateChanged", svrPrj.ID); err != nil { + p.server.EventOn("event:folder-state-change", "", p._cbServerFolderChanged) + if err := p.server.EventRegister("folder-state-change", svrPrj.ID); err != nil { p.Log.Warningf("XDS Server EventRegister failed: %v", err) return svrPrj, err } @@ -103,6 +105,21 @@ func (p *STProject) UpdateProject(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig return svrPrj, nil } +// Update Update some field of a project +func (p *STProject) Update(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { + + if p.folder.ID != prj.ID { + return nil, fmt.Errorf("Invalid id") + } + + err := p.server.FolderUpdate(p.server.ProjectToFolder(prj), p.folder) + if err != nil { + return nil, err + } + + return p.GetProject(), nil +} + // GetServer Get the XdsServer that holds this project func (p *STProject) GetServer() *XdsServer { return p.server @@ -142,7 +159,7 @@ func (p *STProject) _cbServerFolderChanged(pData interface{}, data interface{}) p.folder.DataCloudSync.STSvrIsInSync = evt.Folder.IsInSync p.folder.DataCloudSync.STSvrStatus = evt.Folder.Status - if err := p.events.Emit(apiv1.EVTProjectChange, p.server.FolderToProject(*p.folder)); err != nil { + if err := p.events.Emit(apiv1.EVTProjectChange, p.server.FolderToProject(*p.folder), ""); err != nil { p.Log.Warningf("Cannot notify project change (from server): %v", err) } } @@ -181,7 +198,7 @@ func (p *STProject) _cbLocalSTEvents(ev st.Event, data *st.EventsCBData) { p.folder.DataCloudSync.STLocIsInSync = inSync p.folder.DataCloudSync.STLocStatus = sts - if err := p.events.Emit(apiv1.EVTProjectChange, p.server.FolderToProject(*p.folder)); err != nil { + if err := p.events.Emit(apiv1.EVTProjectChange, p.server.FolderToProject(*p.folder), ""); err != nil { p.Log.Warningf("Cannot notify project change (local): %v", err) } } diff --git a/lib/agent/projects.go b/lib/agent/projects.go index f089882..966c231 100644 --- a/lib/agent/projects.go +++ b/lib/agent/projects.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/franciscocpg/reflectme" "github.com/iotbzh/xds-agent/lib/apiv1" "github.com/iotbzh/xds-agent/lib/syncthing" "github.com/syncthing/syncthing/lib/sync" @@ -119,14 +120,14 @@ func (p *Projects) GetProjectArrUnsafe() []apiv1.ProjectConfig { } // Add adds a new folder -func (p *Projects) Add(newF apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) { +func (p *Projects) Add(newF apiv1.ProjectConfig, fromSid string) (*apiv1.ProjectConfig, error) { prj, err := p.createUpdate(newF, true, false) if err != nil { return prj, err } // Notify client with event - if err := p.events.Emit(apiv1.EVTProjectAdd, *prj); err != nil { + if err := p.events.Emit(apiv1.EVTProjectAdd, *prj, fromSid); err != nil { p.Log.Warningf("Cannot notify project deletion: %v", err) } @@ -190,7 +191,7 @@ func (p *Projects) createUpdate(newF apiv1.ProjectConfig, create bool, initial b } } else { // Just update project config - if newPrj, err = fld.UpdateProject(newF); err != nil { + if newPrj, err = fld.Setup(newF); err != nil { newF.Status = apiv1.StatusErrorConfig log.Printf("ERROR Updating project: %v\n", err) return newPrj, err @@ -217,7 +218,7 @@ func (p *Projects) createUpdate(newF apiv1.ProjectConfig, create bool, initial b } // Delete deletes a specific folder -func (p *Projects) Delete(id string) (apiv1.ProjectConfig, error) { +func (p *Projects) Delete(id, fromSid string) (apiv1.ProjectConfig, error) { var err error pjMutex.Lock() @@ -238,7 +239,7 @@ func (p *Projects) Delete(id string) (apiv1.ProjectConfig, error) { delete(p.projects, id) // Notify client with event - if err := p.events.Emit(apiv1.EVTProjectDelete, *prj); err != nil { + if err := p.events.Emit(apiv1.EVTProjectDelete, *prj, fromSid); err != nil { p.Log.Warningf("Cannot notify project deletion: %v", err) } @@ -262,3 +263,50 @@ func (p *Projects) IsProjectInSync(id string) (bool, error) { } return (*fc).IsInSync() } + +// Update Update some field of a project +func (p *Projects) Update(id string, prj apiv1.ProjectConfig, fromSid string) (*apiv1.ProjectConfig, error) { + + pjMutex.Lock() + defer pjMutex.Unlock() + + fc, exist := p.projects[id] + if !exist { + return nil, fmt.Errorf("Unknown id") + } + + // Copy current in a new object to change nothing in case of an error rises + newFld := apiv1.ProjectConfig{} + reflectme.Copy((*fc).GetProject(), &newFld) + + // Only update some fields + dirty := false + for _, fieldName := range apiv1.ProjectConfigUpdatableFields { + valNew, err := reflectme.GetField(prj, fieldName) + if err == nil { + valCur, err := reflectme.GetField(newFld, fieldName) + if err == nil && valNew != valCur { + err = reflectme.SetField(&newFld, fieldName, valNew) + if err != nil { + return nil, err + } + dirty = true + } + } + } + + if !dirty { + return &newFld, nil + } + + upPrj, err := (*fc).Update(newFld) + if err != nil { + return nil, err + } + + // Notify client with event + if err := p.events.Emit(apiv1.EVTProjectChange, *upPrj, fromSid); err != nil { + p.Log.Warningf("Cannot notify project change: %v", err) + } + return upPrj, err +} diff --git a/lib/agent/sessions.go b/lib/agent/sessions.go index 7347480..3d8b0f4 100644 --- a/lib/agent/sessions.go +++ b/lib/agent/sessions.go @@ -125,6 +125,14 @@ func (s *Sessions) Get(c *gin.Context) *ClientSession { return nil } +// GetID returns the session or an empty string +func (s *Sessions) GetID(c *gin.Context) string { + if sess := s.Get(c); sess != nil { + return sess.ID + } + return "" +} + // IOSocketGet Get socketio definition from sid func (s *Sessions) IOSocketGet(sid string) *socketio.Socket { s.mutex.Lock() diff --git a/lib/agent/xdsserver.go b/lib/agent/xdsserver.go index 73a5bd9..7b03579 100644 --- a/lib/agent/xdsserver.go +++ b/lib/agent/xdsserver.go @@ -64,9 +64,12 @@ type XdsBuilderConfig struct { type XdsFolderType string const ( - XdsTypePathMap = "PathMap" + // XdsTypePathMap Path Mapping folder type + XdsTypePathMap = "PathMap" + // XdsTypeCloudSync Cloud synchronization (AKA syncthing) folder type XdsTypeCloudSync = "CloudSync" - XdsTypeCifsSmb = "CIFS" + // XdsTypeCifsSmb CIFS (AKA samba) folder type + XdsTypeCifsSmb = "CIFS" ) // XdsFolderConfig XdsServer folder config @@ -78,6 +81,8 @@ type XdsFolderConfig struct { Status string `json:"status"` IsInSync bool `json:"isInSync"` DefaultSdk string `json:"defaultSdk"` + ClientData string `json:"clientData"` // free form field that can used by client + // Specific data depending on which Type is used DataPathMap XdsPathMapConfig `json:"dataPathMap,omitempty"` DataCloudSync XdsCloudSyncConfig `json:"dataCloudSync,omitempty"` @@ -112,7 +117,7 @@ type XdsEventFolderChange struct { Folder XdsFolderConfig `json:"folder"` } -// Event emitter callback +// EventCB Event emitter callback type EventCB func(privData interface{}, evtData interface{}) error // caller Used to chain event listeners @@ -241,6 +246,11 @@ func (xs *XdsServer) FolderSync(id string) error { return xs.client.HTTPPost("/folders/sync/"+id, "") } +// FolderUpdate Send PUT request to update a folder +func (xs *XdsServer) FolderUpdate(fld *XdsFolderConfig, resFld *XdsFolderConfig) error { + return xs.client.Put("/folders/"+fld.ID, fld, resFld) +} + // SetAPIRouterGroup . func (xs *XdsServer) SetAPIRouterGroup(r *gin.RouterGroup) { xs.apiRouter = r @@ -334,7 +344,7 @@ func (xs *XdsServer) EventOn(evName string, privData interface{}, f EventCB) (uu // FIXME: use generic type: data interface{} instead of data XdsEventFolderChange var err error - if evName == "event:FolderStateChanged" { + if evName == "event:folder-state-change" { err = xs.ioSock.On(evn, func(data XdsEventFolderChange) error { xs.sockEventsLock.Lock() sEvts := make([]*caller, len(xs.sockEvents[evn])) @@ -400,6 +410,7 @@ func (xs *XdsServer) ProjectToFolder(pPrj apiv1.ProjectConfig) *XdsFolderConfig if pPrj.Type == XdsTypeCloudSync { stID, _ = xs.SThg.IDGet() } + // TODO: limit ClientData size and gzip it (see https://golang.org/pkg/compress/gzip/) fPrj := XdsFolderConfig{ ID: pPrj.ID, Label: pPrj.Label, @@ -408,6 +419,7 @@ func (xs *XdsServer) ProjectToFolder(pPrj apiv1.ProjectConfig) *XdsFolderConfig Status: pPrj.Status, IsInSync: pPrj.IsInSync, DefaultSdk: pPrj.DefaultSdk, + ClientData: pPrj.ClientData, DataPathMap: XdsPathMapConfig{ ServerPath: pPrj.ServerPath, }, @@ -457,6 +469,7 @@ func (xs *XdsServer) FolderToProject(fPrj XdsFolderConfig) apiv1.ProjectConfig { Status: sts, IsInSync: inSync, DefaultSdk: fPrj.DefaultSdk, + ClientData: fPrj.ClientData, } return pPrj } @@ -628,7 +641,7 @@ func (xs *XdsServer) _NotifyState() { ConnRetry: xs.ConnRetry, Connected: xs.Connected, } - if err := xs.events.Emit(apiv1.EVTServerConfig, evSts); err != nil { + if err := xs.events.Emit(apiv1.EVTServerConfig, evSts, ""); err != nil { xs.Log.Warningf("Cannot notify XdsServer state change: %v", err) } } diff --git a/lib/apiv1/events.go b/lib/apiv1/events.go index cdd0889..b2fda62 100644 --- a/lib/apiv1/events.go +++ b/lib/apiv1/events.go @@ -40,9 +40,10 @@ var EVTAllList = []string{ // EventMsg Event message send over Websocket, data format depend to Type (see DecodeXXX function) type EventMsg struct { - Time string `json:"time"` - Type string `json:"type"` - Data interface{} `json:"data"` + Time string `json:"time"` // Timestamp + FromSessionID string `json:"sessionID"` // Session ID of client that emits this event + Type string `json:"type"` // Data type + Data interface{} `json:"data"` // Data } // DecodeServerCfg Helper to decode Data field type ServerCfg diff --git a/lib/apiv1/projects.go b/lib/apiv1/projects.go index d76fa09..b1e64c8 100644 --- a/lib/apiv1/projects.go +++ b/lib/apiv1/projects.go @@ -29,4 +29,10 @@ type ProjectConfig struct { Status string `json:"status"` IsInSync bool `json:"isInSync"` DefaultSdk string `json:"defaultSdk"` + ClientData string `json:"clientData"` // free form field that can used by client +} + +// ProjectConfigUpdatableFields List fields that can be updated using Update function +var ProjectConfigUpdatableFields = []string{ + "Label", "DefaultSdk", "ClientData", } -- cgit 1.2.3-korg