12 "github.com/gin-gonic/gin"
13 "github.com/iotbzh/xds-agent/lib/xdsconfig"
14 common "github.com/iotbzh/xds-common/golib"
15 uuid "github.com/satori/go.uuid"
16 sio_client "github.com/sebd71/go-socket.io-client"
20 type XdsServer struct {
29 ServerConfig *xdsServerConfig
33 CBOnDisconnect func(error)
36 client *common.HTTPClient
37 ioSock *sio_client.Client
39 apiRouter *gin.RouterGroup
42 // xdsServerConfig Data return by GET /config
43 type xdsServerConfig struct {
45 Version string `json:"version"`
46 APIVersion string `json:"apiVersion"`
47 VersionGitTag string `json:"gitTag"`
48 SupportedSharing map[string]bool `json:"supportedSharing"`
49 Builder xdsBuilderConfig `json:"builder"`
52 // xdsBuilderConfig represents the builder container configuration
53 type xdsBuilderConfig struct {
55 Port string `json:"port"`
56 SyncThingID string `json:"syncThingID"`
59 // FolderType XdsServer folder type
60 type FolderType string
63 XdsTypePathMap = "PathMap"
64 XdsTypeCloudSync = "CloudSync"
65 XdsTypeCifsSmb = "CIFS"
68 // FolderConfig XdsServer folder config
69 type FolderConfig struct {
71 Label string `json:"label"`
72 ClientPath string `json:"path"`
73 Type FolderType `json:"type"`
74 Status string `json:"status"`
75 IsInSync bool `json:"isInSync"`
76 DefaultSdk string `json:"defaultSdk"`
77 // Specific data depending on which Type is used
78 DataPathMap PathMapConfig `json:"dataPathMap,omitempty"`
79 DataCloudSync CloudSyncConfig `json:"dataCloudSync,omitempty"`
82 // PathMapConfig Path mapping specific data
83 type PathMapConfig struct {
84 ServerPath string `json:"serverPath"`
87 // CloudSyncConfig CloudSync (AKA Syncthing) specific data
88 type CloudSyncConfig struct {
89 SyncThingID string `json:"syncThingID"`
92 const _IDTempoPrefix = "tempo-"
94 // NewXdsServer creates an instance of XdsServer
95 func NewXdsServer(ctx *Context, conf xdsconfig.XDSServerConf) *XdsServer {
98 ID: _IDTempoPrefix + uuid.NewV1().String(),
100 APIURL: conf.APIBaseURL + conf.APIPartialURL,
101 PartialURL: conf.APIPartialURL,
102 ConnRetry: conf.ConnRetry,
110 // Close Free and close XDS Server connection
111 func (xs *XdsServer) Close() error {
119 // Connect Establish HTTP connection with XDS Server
120 func (xs *XdsServer) Connect() error {
128 for retry = xs.ConnRetry; retry > 0; retry-- {
129 if err = xs._CreateConnectHTTP(); err == nil {
132 if retry == xs.ConnRetry {
133 // Notify only on the first conn error
134 // doing that avoid 2 notifs (conn false; conn true) on startup
137 xs.Log.Infof("Establishing connection to XDS Server (retry %d/%d)", retry, xs.ConnRetry)
138 time.Sleep(time.Second)
141 // FIXME SEB: re-use _reconnect to wait longer in background
142 return fmt.Errorf("Connection to XDS Server failure")
148 // Check HTTP connection and establish WS connection
149 err = xs._connect(false)
154 // IsTempoID returns true when server as a temporary id
155 func (xs *XdsServer) IsTempoID() bool {
156 return strings.HasPrefix(xs.ID, _IDTempoPrefix)
159 // SetLoggerOutput Set logger ou
160 func (xs *XdsServer) SetLoggerOutput(out io.Writer) {
164 // FolderAdd Send POST request to add a folder
165 func (xs *XdsServer) FolderAdd(prj *FolderConfig, res interface{}) error {
166 response, err := xs.HTTPPost("/folder", prj)
170 if response.StatusCode != 200 {
171 return fmt.Errorf("FolderAdd error status=%s", response.Status)
173 // Result is a FolderConfig that is equivalent to ProjectConfig
174 err = json.Unmarshal(xs.client.ResponseToBArray(response), res)
179 // FolderDelete Send DELETE request to delete a folder
180 func (xs *XdsServer) FolderDelete(id string) error {
181 return xs.client.HTTPDelete("/folder/" + id)
185 func (xs *XdsServer) HTTPGet(url string, data interface{}) error {
187 if err := xs.client.HTTPGet(url, &dd); err != nil {
190 return json.Unmarshal(dd, &data)
194 func (xs *XdsServer) HTTPPost(url string, data interface{}) (*http.Response, error) {
195 body, err := json.Marshal(data)
199 return xs.HTTPPostBody(url, string(body))
203 func (xs *XdsServer) HTTPPostBody(url string, body string) (*http.Response, error) {
204 return xs.client.HTTPPostWithRes(url, body)
207 // SetAPIRouterGroup .
208 func (xs *XdsServer) SetAPIRouterGroup(r *gin.RouterGroup) {
212 // PassthroughGet Used to declare a route that sends directly a GET request to XDS Server
213 func (xs *XdsServer) PassthroughGet(url string) {
214 if xs.apiRouter == nil {
215 xs.Log.Errorf("apiRouter not set !")
219 xs.apiRouter.GET(url, func(c *gin.Context) {
221 if err := xs.HTTPGet(url, &data); err != nil {
222 if strings.Contains(err.Error(), "connection refused") {
226 common.APIError(c, err.Error())
230 c.JSON(http.StatusOK, data)
234 // PassthroughPost Used to declare a route that sends directly a POST request to XDS Server
235 func (xs *XdsServer) PassthroughPost(url string) {
236 if xs.apiRouter == nil {
237 xs.Log.Errorf("apiRouter not set !")
241 xs.apiRouter.POST(url, func(c *gin.Context) {
243 n, err := c.Request.Body.Read(bodyReq)
245 common.APIError(c, err.Error())
249 response, err := xs.HTTPPostBody(url, string(bodyReq[:n]))
251 common.APIError(c, err.Error())
254 bodyRes, err := ioutil.ReadAll(response.Body)
256 common.APIError(c, "Cannot read response body")
259 c.JSON(http.StatusOK, string(bodyRes))
263 // EventOn Register a callback on events reception
264 func (xs *XdsServer) EventOn(message string, f interface{}) (err error) {
265 if xs.ioSock == nil {
266 return fmt.Errorf("Io.Socket not initialized")
268 // FIXME SEB: support chain / multiple listeners
269 /* sockEvents map[string][]*caller
270 xs.sockEventsLock.Lock()
271 xs.sockEvents[message] = append(xs.sockEvents[message], f)
272 xs.sockEventsLock.Unlock()
273 xs.ioSock.On(message, func(ev) {
277 return xs.ioSock.On(message, f)
281 func (xs *XdsServer) ProjectToFolder(pPrj ProjectConfig) *FolderConfig {
283 if pPrj.Type == XdsTypeCloudSync {
284 stID, _ = xs.SThg.IDGet()
286 fPrj := FolderConfig{
289 ClientPath: pPrj.ClientPath,
290 Type: FolderType(pPrj.Type),
292 IsInSync: pPrj.IsInSync,
293 DefaultSdk: pPrj.DefaultSdk,
294 DataPathMap: PathMapConfig{
295 ServerPath: pPrj.ServerPath,
297 DataCloudSync: CloudSyncConfig{
305 func (xs *XdsServer) FolderToProject(fPrj FolderConfig) ProjectConfig {
306 pPrj := ProjectConfig{
310 ClientPath: fPrj.ClientPath,
311 ServerPath: fPrj.DataPathMap.ServerPath,
312 Type: ProjectType(fPrj.Type),
314 IsInSync: fPrj.IsInSync,
315 DefaultSdk: fPrj.DefaultSdk,
324 // Create HTTP client
325 func (xs *XdsServer) _CreateConnectHTTP() error {
327 xs.client, err = common.HTTPNewClient(xs.BaseURL,
328 common.HTTPClientConfig{
329 URLPrefix: "/api/v1",
330 HeaderClientKeyName: "Xds-Sid",
333 LogPrefix: "XDSSERVER: ",
334 LogLevel: common.HTTPLogLevelWarning,
337 xs.client.SetLogLevel(xs.Log.Level.String())
340 msg := ": " + err.Error()
341 if strings.Contains(err.Error(), "connection refused") {
342 msg = fmt.Sprintf("(url: %s)", xs.BaseURL)
344 return fmt.Errorf("ERROR: cannot connect to XDS Server %s", msg)
346 if xs.client == nil {
347 return fmt.Errorf("ERROR: cannot connect to XDS Server (null client)")
353 // Re-established connection
354 func (xs *XdsServer) _reconnect() error {
355 err := xs._connect(true)
357 // Reload projects list for this server
358 err = xs.projects.Init(xs)
363 // Established HTTP and WS connection and retrieve XDSServer config
364 func (xs *XdsServer) _connect(reConn bool) error {
366 xdsCfg := xdsServerConfig{}
367 if err := xs.HTTPGet("/config", &xdsCfg); err != nil {
375 if reConn && xs.ID != xdsCfg.ID {
376 xs.Log.Warningf("Reconnected to server but ID differs: old=%s, new=%s", xs.ID, xdsCfg.ID)
379 // Update local XDS config
381 xs.ServerConfig = &xdsCfg
383 // Establish WS connection and register listen
384 if err := xs._SocketConnect(); err != nil {
395 // Create WebSocket (io.socket) connection
396 func (xs *XdsServer) _SocketConnect() error {
398 xs.Log.Infof("Connecting IO.socket for server %s (url %s)", xs.ID, xs.BaseURL)
400 opts := &sio_client.Options{
401 Transport: "websocket",
402 Header: make(map[string][]string),
404 opts.Header["XDS-SID"] = []string{xs.client.GetClientID()}
406 iosk, err := sio_client.NewClient(xs.BaseURL, opts)
408 return fmt.Errorf("IO.socket connection error for server %s: %v", xs.ID, err)
412 // Register some listeners
414 iosk.On("error", func(err error) {
415 xs.Log.Infof("IO.socket Error server %s; err: %v", xs.ID, err)
416 if xs.CBOnError != nil {
421 iosk.On("disconnection", func(err error) {
422 xs.Log.Infof("IO.socket disconnection server %s", xs.ID)
423 if xs.CBOnDisconnect != nil {
424 xs.CBOnDisconnect(err)
429 // Try to reconnect during 15min (or at least while not disabled)
433 for !xs.Disabled && !xs.Connected {
438 if waitTime > 15*60 {
439 xs.Log.Infof("Stop reconnection to server url=%s id=%s !", xs.BaseURL, xs.ID)
442 time.Sleep(time.Second * time.Duration(waitTime))
443 xs.Log.Infof("Try to reconnect to server %s (%d)", xs.BaseURL, count)
450 // XXX - There is no connection event generated so, just consider that
451 // we are connected when NewClient return successfully
452 /* iosk.On("connection", func() { ... }) */
453 xs.Log.Infof("IO.socket connected server url=%s id=%s", xs.BaseURL, xs.ID)
458 // Send event to notify changes
459 func (xs *XdsServer) _NotifyState() {
465 PartialURL: xs.PartialURL,
466 ConnRetry: xs.ConnRetry,
467 Connected: xs.Connected,
469 if err := xs.events.Emit(EVTServerConfig, evSts); err != nil {
470 xs.Log.Warningf("Cannot notify XdsServer state change: %v", err)