summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2017-11-24 01:14:30 +0100
committerSebastien Douheret <sebastien.douheret@iot.bzh>2017-11-24 01:37:24 +0100
commit4d843d2bde236ec23810d0904dfb8aebbc53a37b (patch)
tree84c01452f01620cedb7bf6bcb608a0eade82161b /lib
parent38c0c21a969e621c725245ce91c78e77076c5ce7 (diff)
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 <sebastien.douheret@iot.bzh>
Diffstat (limited to 'lib')
-rw-r--r--lib/agent/apiv1-exec.go18
-rw-r--r--lib/agent/apiv1-projects.go28
-rw-r--r--lib/agent/apiv1.go7
-rw-r--r--lib/agent/events.go9
-rw-r--r--lib/agent/project-interface.go15
-rw-r--r--lib/agent/project-pathmap.go22
-rw-r--r--lib/agent/project-st.go33
-rw-r--r--lib/agent/projects.go58
-rw-r--r--lib/agent/sessions.go8
-rw-r--r--lib/agent/xdsserver.go23
-rw-r--r--lib/apiv1/events.go7
-rw-r--r--lib/apiv1/projects.go6
12 files changed, 186 insertions, 48 deletions
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",
}