From 61ca475685c6b7b33654edaad637c7d53bdf8d34 Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Mon, 22 May 2017 18:45:46 +0200 Subject: [PATCH] Add XDS-agent tarball download feature Signed-off-by: Sebastien Douheret --- .gitignore | 1 + lib/apiv1/agent.go | 37 +++++ lib/apiv1/apiv1.go | 1 + webapp/assets/xds-agent-tarballs/.gitkeep | 0 webapp/src/app/alert/alert.component.ts | 2 +- webapp/src/app/app.module.ts | 4 + webapp/src/app/common/alert.service.ts | 2 +- webapp/src/app/common/config.service.ts | 75 ++++++++-- webapp/src/app/common/utils.service.ts | 23 +++ webapp/src/app/common/xdsagent.service.ts | 213 ++++++++++++++++++++++++++++ webapp/src/app/common/xdsserver.service.ts | 13 ++ webapp/src/app/config/config.component.html | 25 +++- webapp/src/app/config/config.component.ts | 32 +++-- 13 files changed, 401 insertions(+), 27 deletions(-) create mode 100644 lib/apiv1/agent.go create mode 100644 webapp/assets/xds-agent-tarballs/.gitkeep create mode 100644 webapp/src/app/common/utils.service.ts create mode 100644 webapp/src/app/common/xdsagent.service.ts diff --git a/.gitignore b/.gitignore index 6602391..500d93b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ cmd/*/debug webapp/dist webapp/node_modules +webapp/assets/xds-agent-tarballs/*.zip # private (prefixed by 2 underscores) directories or files __*/ diff --git a/lib/apiv1/agent.go b/lib/apiv1/agent.go new file mode 100644 index 0000000..7434545 --- /dev/null +++ b/lib/apiv1/agent.go @@ -0,0 +1,37 @@ +package apiv1 + +import ( + "net/http" + + "path/filepath" + + "github.com/gin-gonic/gin" +) + +type XDSAgentTarball struct { + OS string `json:"os"` + FileURL string `json:"fileUrl"` +} +type XDSAgentInfo struct { + Tarballs []XDSAgentTarball `json:"tarballs"` +} + +// getXdsAgentInfo : return various information about Xds Agent +func (s *APIService) getXdsAgentInfo(c *gin.Context) { + // TODO: retrieve link dynamically by reading assets/xds-agent-tarballs + tarballDir := "assets/xds-agent-tarballs" + response := XDSAgentInfo{ + Tarballs: []XDSAgentTarball{ + XDSAgentTarball{ + OS: "linux", + FileURL: filepath.Join(tarballDir, "xds-agent_linux-amd64-v0.0.1_3cdf92c.zip"), + }, + XDSAgentTarball{ + OS: "windows", + FileURL: filepath.Join(tarballDir, "xds-agent_windows-386-v0.0.1_3cdf92c.zip"), + }, + }, + } + + c.JSON(http.StatusOK, response) +} diff --git a/lib/apiv1/apiv1.go b/lib/apiv1/apiv1.go index 7359266..2df8ea7 100644 --- a/lib/apiv1/apiv1.go +++ b/lib/apiv1/apiv1.go @@ -34,6 +34,7 @@ func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolder * } s.apiRouter.GET("/version", s.getVersion) + s.apiRouter.GET("/xdsagent/info", s.getXdsAgentInfo) s.apiRouter.GET("/config", s.getConfig) s.apiRouter.POST("/config", s.setConfig) diff --git a/webapp/assets/xds-agent-tarballs/.gitkeep b/webapp/assets/xds-agent-tarballs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/webapp/src/app/alert/alert.component.ts b/webapp/src/app/alert/alert.component.ts index e9d7629..449506f 100644 --- a/webapp/src/app/alert/alert.component.ts +++ b/webapp/src/app/alert/alert.component.ts @@ -9,7 +9,7 @@ import {AlertService, IAlert} from '../common/alert.service';
- +
` diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index d4a6918..1abcf0c 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -26,9 +26,11 @@ import { SdkSelectDropdownComponent } from "./sdks/sdkSelectDropdown.component"; import { HomeComponent } from "./home/home.component"; import { BuildComponent } from "./build/build.component"; import { XDSServerService } from "./common/xdsserver.service"; +import { XDSAgentService } from "./common/xdsagent.service"; import { SyncthingService } from "./common/syncthing.service"; import { ConfigService } from "./common/config.service"; import { AlertService } from './common/alert.service'; +import { UtilsService } from './common/utils.service'; import { SdkService } from "./common/sdk.service"; @@ -67,9 +69,11 @@ import { SdkService } from "./common/sdk.service"; useValue: window }, XDSServerService, + XDSAgentService, ConfigService, SyncthingService, AlertService, + UtilsService, SdkService, ], bootstrap: [AppComponent] diff --git a/webapp/src/app/common/alert.service.ts b/webapp/src/app/common/alert.service.ts index 710046f..9dab36a 100644 --- a/webapp/src/app/common/alert.service.ts +++ b/webapp/src/app/common/alert.service.ts @@ -39,7 +39,7 @@ export class AlertService { } public info(msg: string) { - this.add({ type: "warning", msg: msg, dismissible: true, dismissTimeout: this.defaultDissmissTmo }); + this.add({ type: "info", msg: msg, dismissible: true, dismissTimeout: this.defaultDissmissTmo }); } public add(al: IAlert) { diff --git a/webapp/src/app/common/config.service.ts b/webapp/src/app/common/config.service.ts index 201ee8b..a04ac13 100644 --- a/webapp/src/app/common/config.service.ts +++ b/webapp/src/app/common/config.service.ts @@ -14,8 +14,10 @@ import 'rxjs/add/operator/mergeMap'; import { XDSServerService, IXDSConfigProject } from "../common/xdsserver.service"; +import { XDSAgentService } from "../common/xdsagent.service"; import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../common/syncthing.service"; import { AlertService, IAlert } from "../common/alert.service"; +import { UtilsService } from "../common/utils.service"; export enum ProjectType { NATIVE = 1, @@ -38,6 +40,11 @@ export interface IProject { defaultSdkID?: string; } +export interface IXDSAgentConfig { + URL: string; + retry: number; +} + export interface ILocalSTConfig { ID: string; URL: string; @@ -47,6 +54,8 @@ export interface ILocalSTConfig { export interface IConfig { xdsServerURL: string; + xdsAgent: IXDSAgentConfig; + xdsAgentZipUrl: string; projectsRootDir: string; projects: IProject[]; localSThg: ILocalSTConfig; @@ -59,13 +68,17 @@ export class ConfigService { private confSubject: BehaviorSubject; private confStore: IConfig; + private AgentConnectObs = null; private stConnectObs = null; + private xdsAgentZipUrl = ""; constructor(private _window: Window, private cookie: CookieService, - private sdkSvr: XDSServerService, + private xdsServerSvr: XDSServerService, + private xdsAgentSvr: XDSAgentService, private stSvr: SyncthingService, private alert: AlertService, + private utils: UtilsService, ) { this.load(); this.confSubject = >new BehaviorSubject(this.confStore); @@ -85,6 +98,11 @@ export class ConfigService { // Set default config this.confStore = { xdsServerURL: this._window.location.origin + '/api/v1', + xdsAgent: { + URL: 'http://localhost:8000', + retry: 10, + }, + xdsAgentZipUrl: "", projectsRootDir: "", projects: [], localSThg: { @@ -95,6 +113,13 @@ export class ConfigService { } }; } + + // Update XDS Agent tarball url + this.xdsServerSvr.getXdsAgentInfo().subscribe(nfo => { + let os = this.utils.getOSName(true); + let zurl = nfo.tarballs.filter(elem => elem.os === os); + this.confStore.xdsAgentZipUrl = zurl && zurl[0].fileUrl; + }); } // Save config into cookie @@ -104,11 +129,26 @@ export class ConfigService { // Don't save projects in cookies (too big!) let cfg = this.confStore; - delete(cfg.projects); + delete (cfg.projects); this.cookie.putObject("xds-config", cfg); } loadProjects() { + // Setup connection with local XDS agent + if (this.AgentConnectObs) { + try { + this.AgentConnectObs.unsubscribe(); + } catch (err) { } + this.AgentConnectObs = null; + } + + let cfg = this.confStore.xdsAgent; + this.AgentConnectObs = this.xdsAgentSvr.connect(cfg.retry, cfg.URL) + .subscribe((sts) => { + console.log("Agent sts", sts); + }, error => this.alert.error(error) + ); + // Remove previous subscriber if existing if (this.stConnectObs) { try { @@ -117,7 +157,8 @@ export class ConfigService { this.stConnectObs = null; } - // First setup connection with local SyncThing + // FIXME: move this code and all logic about syncthing inside XDS Agent + // Setup connection with local SyncThing let retry = this.confStore.localSThg.retry; let url = this.confStore.localSThg.URL; this.stConnectObs = this.stSvr.connect(retry, url).subscribe((sts) => { @@ -130,7 +171,7 @@ export class ConfigService { // Rebuild projects definition from local and remote syncthing this.confStore.projects = []; - this.sdkSvr.getProjects().subscribe(remotePrj => { + this.xdsServerSvr.getProjects().subscribe(remotePrj => { this.stSvr.getProjects().subscribe(localPrj => { remotePrj.forEach(rPrj => { let lPrj = localPrj.filter(item => item.id === rPrj.id); @@ -150,7 +191,18 @@ export class ConfigService { }), error => this.alert.error('Could not load initial state of local projects.'); }), error => this.alert.error('Could not load initial state of remote projects.'); - }, error => this.alert.error(error)); + }, error => { + if (error.indexOf("Syncthing local daemon not responding") !== -1) { + let msg = "" + error + "
"; + msg += "You may need to download and execute XDS-Agent.
"; + msg += ""; + msg += " Download XDS-Agent tarball."; + msg += "
"; + this.alert.error(msg); + } else { + this.alert.error(error); + } + }); } set syncToolURL(url: string) { @@ -158,11 +210,18 @@ export class ConfigService { this.save(); } - set syncToolRetry(r: number) { + set xdsAgentRetry(r: number) { this.confStore.localSThg.retry = r; + this.confStore.xdsAgent.retry = r; this.save(); } + set xdsAgentUrl(url: string) { + this.confStore.xdsAgent.URL = url; + this.save(); + } + + set projectsRootDir(p: string) { if (p.charAt(0) === '~') { p = this.confStore.localSThg.tilde + p.substring(1); @@ -219,7 +278,7 @@ export class ConfigService { // Send config to XDS server let newPrj = prj; - this.sdkSvr.addProject(sdkPrj) + this.xdsServerSvr.addProject(sdkPrj) .subscribe(resStRemotePrj => { newPrj.remotePrjDef = resStRemotePrj; @@ -258,7 +317,7 @@ export class ConfigService { if (idx === -1) { throw new Error("Invalid project id (id=" + prj.id + ")"); } - this.sdkSvr.deleteProject(prj.id) + this.xdsServerSvr.deleteProject(prj.id) .subscribe(res => { this.stSvr.deleteProject(prj.id) .subscribe(res => { diff --git a/webapp/src/app/common/utils.service.ts b/webapp/src/app/common/utils.service.ts new file mode 100644 index 0000000..291ffd3 --- /dev/null +++ b/webapp/src/app/common/utils.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class UtilsService { + constructor() { } + + getOSName(lowerCase?: boolean): string { + let OSName = "Unknown OS"; + if (navigator.appVersion.indexOf("Linux") !== -1) { + OSName = "Linux"; + } else if (navigator.appVersion.indexOf("Win") !== -1) { + OSName = "Windows"; + } else if (navigator.appVersion.indexOf("Mac") !== -1) { + OSName = "MacOS"; + } else if (navigator.appVersion.indexOf("X11") !== -1) { + OSName = "UNIX"; + } + if (lowerCase) { + return OSName.toLowerCase(); + } + return OSName; + } +} \ No newline at end of file diff --git a/webapp/src/app/common/xdsagent.service.ts b/webapp/src/app/common/xdsagent.service.ts new file mode 100644 index 0000000..4d9aadc --- /dev/null +++ b/webapp/src/app/common/xdsagent.service.ts @@ -0,0 +1,213 @@ +import { Injectable } from '@angular/core'; +import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http'; +import { Location } from '@angular/common'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import * as io from 'socket.io-client'; + +import { AlertService } from './alert.service'; + + +// Import RxJs required methods +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/observable/throw'; + +export interface IXDSVersion { + version: string; + apiVersion: string; + gitTag: string; + +} + +export interface IAgentStatus { + baseURL: string; + connected: boolean; + WS_connected: boolean; + connectionRetry: number; + version: string; +} + +// Default settings +const DEFAULT_PORT = 8010; +const DEFAULT_API_KEY = "1234abcezam"; +const API_VERSION = "v1"; + +@Injectable() +export class XDSAgentService { + public Status$: Observable; + + private baseRestUrl: string; + private wsUrl: string; + private connectionMaxRetry: number; + private apikey: string; + private _status: IAgentStatus = { + baseURL: "", + connected: false, + WS_connected: false, + connectionRetry: 0, + version: "", + }; + private statusSubject = >new BehaviorSubject(this._status); + + + private socket: SocketIOClient.Socket; + + constructor(private http: Http, private _window: Window, private alert: AlertService) { + + this.Status$ = this.statusSubject.asObservable(); + + this.apikey = DEFAULT_API_KEY; // FIXME Add dynamic allocated key + this._status.baseURL = 'http://localhost:' + DEFAULT_PORT; + this.baseRestUrl = this._status.baseURL + '/api/' + API_VERSION; + let re = this._window.location.origin.match(/http[s]?:\/\/([^\/]*)[\/]?/); + if (re === null || re.length < 2) { + console.error('ERROR: cannot determine Websocket url'); + } else { + this.wsUrl = 'ws://' + re[1]; + } + } + + connect(retry: number, url?: string): Observable { + if (url) { + this._status.baseURL = url; + this.baseRestUrl = this._status.baseURL + '/api/' + API_VERSION; + } + //FIXME [XDS-Agent]: not implemented yet, set always as connected + //this._status.connected = false; + this._status.connected = true; + this._status.connectionRetry = 0; + this.connectionMaxRetry = retry || 3600; // 1 hour + + // Init IO Socket connection + this._handleIoSocket(); + + // Get Version in order to check connection via a REST request + return this.getVersion() + .map((v) => { + this._status.version = v.version; + this.statusSubject.next(Object.assign({}, this._status)); + return this._status; + }); + } + + public getVersion(): Observable { + /*FIXME [XDS-Agent]: Not implemented for now + return this._get('/version'); + */ + return Observable.of({ + version: "NOT_IMPLEMENTED", + apiVersion: "NOT_IMPLEMENTED", + gitTag: "NOT_IMPLEMENTED" + }); + } + + private _WSState(sts: boolean) { + this._status.WS_connected = sts; + this.statusSubject.next(Object.assign({}, this._status)); + } + + private _handleIoSocket() { + this.socket = io(this.wsUrl, { transports: ['websocket'] }); + + this.socket.on('connect_error', (res) => { + this._WSState(false); + console.error('WS Connect_error ', res); + }); + + this.socket.on('connect', (res) => { + this._WSState(true); + }); + + this.socket.on('disconnection', (res) => { + this._WSState(false); + this.alert.error('WS disconnection: ' + res); + }); + + this.socket.on('error', (err) => { + console.error('WS error:', err); + }); + + } + + private _attachAuthHeaders(options?: any) { + options = options || {}; + let headers = options.headers || new Headers(); + // headers.append('Authorization', 'Basic ' + btoa('username:password')); + headers.append('Access-Control-Allow-Origin', '*'); + headers.append('Accept', 'application/json'); + headers.append('Content-Type', 'application/json'); + if (this.apikey !== "") { + headers.append('X-API-Key', this.apikey); + + } + + options.headers = headers; + return options; + } + + private _checkAlive(): Observable { + if (this._status.connected) { + return Observable.of(true); + } + + return this.http.get(this._status.baseURL, this._attachAuthHeaders()) + .map((r) => this._status.connected = true) + .retryWhen((attempts) => { + this._status.connectionRetry = 0; + return attempts.flatMap(error => { + this._status.connected = false; + if (++this._status.connectionRetry >= this.connectionMaxRetry) { + return Observable.throw("XDS local Agent not responding (url=" + this._status.baseURL + ")"); + } else { + return Observable.timer(1000); + } + }); + }); + } + + private _get(url: string): Observable { + return this._checkAlive() + .flatMap(() => this.http.get(this.baseRestUrl + url, this._attachAuthHeaders())) + .map((res: Response) => res.json()) + .catch(this._decodeError); + } + private _post(url: string, body: any): Observable { + return this._checkAlive() + .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders())) + .map((res: Response) => res.json()) + .catch((error) => { + return this._decodeError(error); + }); + } + private _delete(url: string): Observable { + return this._checkAlive() + .flatMap(() => this.http.delete(this.baseRestUrl + url, this._attachAuthHeaders())) + .map((res: Response) => res.json()) + .catch(this._decodeError); + } + + private _decodeError(err: Response | any) { + let e: string; + if (this._status) { + this._status.connected = false; + } + if (typeof err === "object") { + if (err.statusText) { + e = err.statusText; + } else if (err.error) { + e = String(err.error); + } else { + e = JSON.stringify(err); + } + } else if (err instanceof Response) { + const body = err.json() || 'Server error'; + const error = body.error || JSON.stringify(body); + e = `${err.status} - ${err.statusText || ''} ${error}`; + } else { + e = err.message ? err.message : err.toString(); + } + return Observable.throw(e); + } +} diff --git a/webapp/src/app/common/xdsserver.service.ts b/webapp/src/app/common/xdsserver.service.ts index 6cd9ba3..49c2d37 100644 --- a/webapp/src/app/common/xdsserver.service.ts +++ b/webapp/src/app/common/xdsserver.service.ts @@ -48,6 +48,15 @@ interface IXDSConfig { folders: IXDSFolderConfig[]; } +export interface IXDSAgentTarball { + os: string; + fileUrl: string; +} + +export interface IXDSAgentInfo { + tarballs: IXDSAgentTarball[]; +} + export interface ISdkMessage { wsID: string; msgType: string; @@ -144,6 +153,10 @@ export class XDSServerService { return this._get('/sdks'); } + getXdsAgentInfo(): Observable { + return this._get('/xdsagent/info'); + } + getProjects(): Observable { return this._get('/folders'); } diff --git a/webapp/src/app/config/config.component.html b/webapp/src/app/config/config.component.html index 2a3d322..77d90c5 100644 --- a/webapp/src/app/config/config.component.html +++ b/webapp/src/app/config/config.component.html @@ -2,7 +2,7 @@

Global Configuration

- +
@@ -10,20 +10,33 @@
+ - - + + - - + + + + + + + diff --git a/webapp/src/app/config/config.component.ts b/webapp/src/app/config/config.component.ts index 745e9f6..1e1e9c2 100644 --- a/webapp/src/app/config/config.component.ts +++ b/webapp/src/app/config/config.component.ts @@ -8,7 +8,8 @@ import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/debounceTime'; import { ConfigService, IConfig, IProject, ProjectType } from "../common/config.service"; -import { XDSServerService, IServerStatus } from "../common/xdsserver.service"; +import { XDSServerService, IServerStatus, IXDSAgentInfo } from "../common/xdsserver.service"; +import { XDSAgentService, IAgentStatus } from "../common/xdsagent.service"; import { SyncthingService, ISyncThingStatus } from "../common/syncthing.service"; import { AlertService } from "../common/alert.service"; import { ISdk, SdkService } from "../common/sdk.service"; @@ -25,15 +26,18 @@ export class ConfigComponent implements OnInit { config$: Observable; sdks$: Observable; - severStatus$: Observable; + serverStatus$: Observable; + agentStatus$: Observable; localSTStatus$: Observable; curProj: number; userEditedLabel: boolean = false; + xdsAgentZipUrl: string = ""; // TODO replace by reactive FormControl + add validation syncToolUrl: string; - syncToolRetry: string; + xdsAgentUrl: string; + xdsAgentRetry: string; projectsRootDir: string; showApplyBtn = { // Used to show/hide Apply buttons "retry": false, @@ -46,7 +50,8 @@ export class ConfigComponent implements OnInit { constructor( private configSvr: ConfigService, - private xdsSvr: XDSServerService, + private xdsServerSvr: XDSServerService, + private xdsAgentSvr: XDSAgentService, private stSvr: SyncthingService, private sdkSvr: SdkService, private alert: AlertService, @@ -63,14 +68,17 @@ export class ConfigComponent implements OnInit { ngOnInit() { this.config$ = this.configSvr.conf; this.sdks$ = this.sdkSvr.Sdks$; - this.severStatus$ = this.xdsSvr.Status$; + this.serverStatus$ = this.xdsServerSvr.Status$; + this.agentStatus$ = this.xdsAgentSvr.Status$; this.localSTStatus$ = this.stSvr.Status$; - // Bind syncToolUrl to baseURL + // Bind xdsAgentUrl to baseURL this.config$.subscribe(cfg => { this.syncToolUrl = cfg.localSThg.URL; - this.syncToolRetry = String(cfg.localSThg.retry); + this.xdsAgentUrl = cfg.xdsAgent.URL; + this.xdsAgentRetry = String(cfg.xdsAgent.retry); this.projectsRootDir = cfg.projectsRootDir; + this.xdsAgentZipUrl = cfg.xdsAgentZipUrl; }); // Auto create label name @@ -93,9 +101,9 @@ export class ConfigComponent implements OnInit { switch (field) { case "retry": let re = new RegExp('^[0-9]+$'); - let rr = parseInt(this.syncToolRetry, 10); - if (re.test(this.syncToolRetry) && rr >= 0) { - this.configSvr.syncToolRetry = rr; + let rr = parseInt(this.xdsAgentRetry, 10); + if (re.test(this.xdsAgentRetry) && rr >= 0) { + this.configSvr.xdsAgentRetry = rr; } else { this.alert.warning("Not a valid number", true); } @@ -109,8 +117,10 @@ export class ConfigComponent implements OnInit { this.showApplyBtn[field] = false; } - syncToolRestartConn() { + xdsAgentRestartConn() { + let aurl = this.xdsAgentUrl; this.configSvr.syncToolURL = this.syncToolUrl; + this.configSvr.xdsAgentUrl = aurl; this.configSvr.loadProjects(); } -- 2.16.6
- + + +
+ +