New dashboard improvements.
[src/xds/xds-agent.git] / webapp / src / app / @core-xds / services / xdsagent.service.ts
1 import { Injectable, Inject, isDevMode } from '@angular/core';
2 import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
3 import { DOCUMENT } from '@angular/common';
4 import { Observable } from 'rxjs/Observable';
5 import { Subject } from 'rxjs/Subject';
6 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
7 import * as io from 'socket.io-client';
8
9 import { AlertService } from './alert.service';
10 import { ISdk } from './sdk.service';
11 import { ProjectType, ProjectTypeEnum } from './project.service';
12
13 // Import RxJs required methods
14 import 'rxjs/add/operator/map';
15 import 'rxjs/add/operator/catch';
16 import 'rxjs/add/observable/throw';
17 import 'rxjs/add/operator/mergeMap';
18 import 'rxjs/add/observable/of';
19 import 'rxjs/add/operator/retryWhen';
20
21
22 export interface IXDSConfigProject {
23   id: string;
24   path: string;
25   clientSyncThingID: string;
26   type: string;
27   label?: string;
28   defaultSdkID?: string;
29 }
30
31 interface IXDSBuilderConfig {
32   ip: string;
33   port: string;
34   syncThingID: string;
35 }
36
37 export interface IXDSProjectConfig {
38   id: string;
39   serverId: string;
40   label: string;
41   clientPath: string;
42   serverPath?: string;
43   type: ProjectTypeEnum;
44   status?: string;
45   isInSync?: boolean;
46   defaultSdkID: string;
47   clientData?: string;
48 }
49
50 export interface IXDSVer {
51   id: string;
52   version: string;
53   apiVersion: string;
54   gitTag: string;
55 }
56
57 export interface IXDSVersions {
58   client: IXDSVer;
59   servers: IXDSVer[];
60 }
61
62 export interface IXDServerCfg {
63   id: string;
64   url: string;
65   apiUrl?: string;
66   partialUrl?: string;
67   connRetry: number;
68   connected: boolean;
69 }
70
71 export interface IXDSConfig {
72   servers: IXDServerCfg[];
73 }
74
75 export interface ISdkMessage {
76   wsID: string;
77   msgType: string;
78   data: any;
79 }
80
81 export interface ICmdOutput {
82   cmdID: string;
83   timestamp: string;
84   stdout: string;
85   stderr: string;
86 }
87
88 export interface ICmdExit {
89   cmdID: string;
90   timestamp: string;
91   code: number;
92   error: string;
93 }
94
95 export interface IServerStatus {
96   id: string;
97   connected: boolean;
98 }
99
100 export interface IAgentStatus {
101   connected: boolean;
102   servers: IServerStatus[];
103 }
104
105
106 @Injectable()
107 export class XDSAgentService {
108
109   public XdsConfig$: Observable<IXDSConfig>;
110   public Status$: Observable<IAgentStatus>;
111   public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
112   public CmdExit$ = <Subject<ICmdExit>>new Subject();
113
114   protected projectAdd$ = new Subject<IXDSProjectConfig>();
115   protected projectDel$ = new Subject<IXDSProjectConfig>();
116   protected projectChange$ = new Subject<IXDSProjectConfig>();
117
118   private baseUrl: string;
119   private wsUrl: string;
120   private httpSessionID: string;
121   private _config = <IXDSConfig>{ servers: [] };
122   private _status = { connected: false, servers: [] };
123
124   private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config);
125   private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status);
126
127   private socket: SocketIOClient.Socket;
128
129   constructor( @Inject(DOCUMENT) private document: Document,
130     private http: HttpClient, private alert: AlertService) {
131
132     this.XdsConfig$ = this.configSubject.asObservable();
133     this.Status$ = this.statusSubject.asObservable();
134
135     const originUrl = this.document.location.origin;
136     this.baseUrl = originUrl + '/api/v1';
137
138     // Retrieve Session ID / token
139     this.http.get(this.baseUrl + '/version', { observe: 'response' })
140       .subscribe(
141       resp => {
142         this.httpSessionID = resp.headers.get('xds-agent-sid');
143
144         const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/);
145         if (re === null || re.length < 2) {
146           console.error('ERROR: cannot determine Websocket url');
147         } else {
148           this.wsUrl = 'ws://' + re[1];
149           this._handleIoSocket();
150           this._RegisterEvents();
151         }
152       },
153       err => {
154         /* tslint:disable:no-console */
155         console.error('ERROR while retrieving session id:', err);
156       });
157   }
158
159   private _NotifyXdsAgentState(sts: boolean) {
160     this._status.connected = sts;
161     this.statusSubject.next(Object.assign({}, this._status));
162
163     // Update XDS config including XDS Server list when connected
164     if (sts) {
165       this.getConfig().subscribe(c => {
166         this._config = c;
167         this._NotifyXdsServerState();
168         this.configSubject.next(Object.assign({ servers: [] }, this._config));
169       });
170     }
171   }
172
173   private _NotifyXdsServerState() {
174     this._status.servers = this._config.servers.map(svr => {
175       return { id: svr.id, connected: svr.connected };
176     });
177     this.statusSubject.next(Object.assign({}, this._status));
178   }
179
180   private _handleIoSocket() {
181     this.socket = io(this.wsUrl, { transports: ['websocket'] });
182
183     this.socket.on('connect_error', (res) => {
184       this._NotifyXdsAgentState(false);
185       console.error('XDS Agent WebSocket Connection error !');
186     });
187
188     this.socket.on('connect', (res) => {
189       this._NotifyXdsAgentState(true);
190     });
191
192     this.socket.on('disconnection', (res) => {
193       this._NotifyXdsAgentState(false);
194       this.alert.error('WS disconnection: ' + res);
195     });
196
197     this.socket.on('error', (err) => {
198       console.error('WS error:', err);
199     });
200
201     // XDS Events decoding
202
203     this.socket.on('make:output', data => {
204       this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
205     });
206
207     this.socket.on('make:exit', data => {
208       this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
209     });
210
211     this.socket.on('exec:output', data => {
212       this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
213     });
214
215     this.socket.on('exec:exit', data => {
216       this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
217     });
218
219     this.socket.on('event:server-config', ev => {
220       if (ev && ev.data) {
221         const cfg: IXDServerCfg = ev.data;
222         const idx = this._config.servers.findIndex(el => el.id === cfg.id);
223         if (idx >= 0) {
224           this._config.servers[idx] = Object.assign({}, cfg);
225           this._NotifyXdsServerState();
226         }
227         this.configSubject.next(Object.assign({}, this._config));
228       }
229     });
230
231     this.socket.on('event:project-add', (ev) => {
232       if (ev && ev.data && ev.data.id) {
233         this.projectAdd$.next(Object.assign({}, ev.data));
234         if (ev.sessionID !== this.httpSessionID && ev.data.label) {
235           this.alert.info('Project "' + ev.data.label + '" has been added by another tool.');
236         }
237       } else if (isDevMode) {
238         /* tslint:disable:no-console */
239         console.log('Warning: received event:project-add with unknown data: ev=', ev);
240       }
241     });
242
243     this.socket.on('event:project-delete', (ev) => {
244       if (ev && ev.data && ev.data.id) {
245         this.projectDel$.next(Object.assign({}, ev.data));
246         if (ev.sessionID !== this.httpSessionID && ev.data.label) {
247           this.alert.info('Project "' + ev.data.label + '" has been deleted by another tool.');
248         }
249       } else if (isDevMode) {
250         console.log('Warning: received event:project-delete with unknown data: ev=', ev);
251       }
252     });
253
254     this.socket.on('event:project-state-change', ev => {
255       if (ev && ev.data) {
256         this.projectChange$.next(Object.assign({}, ev.data));
257       } else if (isDevMode) {
258         console.log('Warning: received event:project-state-change with unknown data: ev=', ev);
259       }
260     });
261
262   }
263
264   /**
265   ** Events registration
266   ***/
267   onProjectAdd(): Observable<IXDSProjectConfig> {
268     return this.projectAdd$.asObservable();
269   }
270
271   onProjectDelete(): Observable<IXDSProjectConfig> {
272     return this.projectDel$.asObservable();
273   }
274
275   onProjectChange(): Observable<IXDSProjectConfig> {
276     return this.projectChange$.asObservable();
277   }
278
279   /**
280   ** Misc / Version
281   ***/
282   getVersion(): Observable<IXDSVersions> {
283     return this._get('/version');
284   }
285
286   /***
287   ** Config
288   ***/
289   getConfig(): Observable<IXDSConfig> {
290     return this._get('/config');
291   }
292
293   setConfig(cfg: IXDSConfig): Observable<IXDSConfig> {
294     return this._post('/config', cfg);
295   }
296
297   setServerRetry(serverID: string, retry: number): Observable<IXDSConfig> {
298     const svr = this._getServer(serverID);
299     if (!svr) {
300       return Observable.throw('Unknown server ID');
301     }
302     if (retry < 0 || Number.isNaN(retry) || retry == null) {
303       return Observable.throw('Not a valid number');
304     }
305     svr.connRetry = retry;
306     return this._setConfig();
307   }
308
309   setServerUrl(serverID: string, url: string, retry: number): Observable<IXDSConfig> {
310     const svr = this._getServer(serverID);
311     if (!svr) {
312       return Observable.throw('Unknown server ID');
313     }
314     svr.connected = false;
315     svr.url = url;
316     if (!Number.isNaN(retry) && retry > 0) {
317       svr.connRetry = retry;
318     }
319     this._NotifyXdsServerState();
320     return this._setConfig();
321   }
322
323   private _setConfig(): Observable<IXDSConfig> {
324     return this.setConfig(this._config)
325       .map(newCfg => {
326         this._config = newCfg;
327         this.configSubject.next(Object.assign({}, this._config));
328         return this._config;
329       });
330   }
331
332   /***
333   ** SDKs
334   ***/
335   getSdks(serverID: string): Observable<ISdk[]> {
336     const svr = this._getServer(serverID);
337     if (!svr || !svr.connected) {
338       return Observable.of([]);
339     }
340
341     return this._get(svr.partialUrl + '/sdks');
342   }
343
344   /***
345   ** Projects
346   ***/
347   getProjects(): Observable<IXDSProjectConfig[]> {
348     return this._get('/projects');
349   }
350
351   addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
352     return this._post('/projects', cfg);
353   }
354
355   deleteProject(id: string): Observable<IXDSProjectConfig> {
356     return this._delete('/projects/' + id);
357   }
358
359   updateProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
360     return this._put('/projects/' + cfg.id, cfg);
361   }
362
363   syncProject(id: string): Observable<string> {
364     return this._post('/projects/sync/' + id, {});
365   }
366
367   /***
368   ** Exec
369   ***/
370   exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
371     return this._post('/exec',
372       {
373         id: prjID,
374         rpath: dir,
375         cmd: cmd,
376         sdkID: sdkid || '',
377         args: args || [],
378         env: env || [],
379       });
380   }
381
382   /**
383   ** Private functions
384   ***/
385
386   private _RegisterEvents() {
387     // Register to all existing events
388     this._post('/events/register', { 'name': 'event:all' })
389       .subscribe(
390       res => { },
391       error => {
392         this.alert.error('ERROR while registering to all events: ' + error);
393       },
394     );
395   }
396
397   private _getServer(ID: string): IXDServerCfg {
398     const svr = this._config.servers.filter(item => item.id === ID);
399     if (svr.length < 1) {
400       return null;
401     }
402     return svr[0];
403   }
404
405   private _attachAuthHeaders(options?: any) {
406     options = options || {};
407     const headers = options.headers || new HttpHeaders();
408     // headers.append('Authorization', 'Basic ' + btoa('username:password'));
409     headers.append('Accept', 'application/json');
410     headers.append('Content-Type', 'application/json');
411     // headers.append('Access-Control-Allow-Origin', '*');
412
413     options.headers = headers;
414     return options;
415   }
416
417   private _get(url: string): Observable<any> {
418     return this.http.get(this.baseUrl + url, this._attachAuthHeaders())
419       .catch(this._decodeError);
420   }
421   private _post(url: string, body: any): Observable<any> {
422     return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
423       .catch((error) => {
424         return this._decodeError(error);
425       });
426   }
427   private _put(url: string, body: any): Observable<any> {
428     return this.http.put(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
429       .catch((error) => {
430         return this._decodeError(error);
431       });
432   }
433   private _delete(url: string): Observable<any> {
434     return this.http.delete(this.baseUrl + url, this._attachAuthHeaders())
435       .catch(this._decodeError);
436   }
437
438   private _decodeError(err: any) {
439     let e: string;
440     if (err instanceof HttpErrorResponse) {
441       e = (err.error && err.error.error) ? err.error.error : err.message || 'Unknown error';
442     } else if (typeof err === 'object') {
443       if (err.statusText) {
444         e = err.statusText;
445       } else if (err.error) {
446         e = String(err.error);
447       } else {
448         e = JSON.stringify(err);
449       }
450     } else {
451       e = err.message ? err.message : err.toString();
452     }
453     /* tslint:disable:no-console */
454     if (isDevMode) {
455       console.log('xdsagent.service - ERROR: ', e);
456     }
457     return Observable.throw(e);
458   }
459 }