import * as errors from '../infrastructure/errors' ; export interface DigitalOceanDropletSpecification { installCommand : string ; size : string ; image : string ; tags : string [ ] ; } export type DropletInfo = Readonly < { id : number ; status : 'new' | 'active' ; tags : string [ ] ; region : { readonly slug : string ; } ; size : Readonly < { transfer : number ; price_monthly : number ; } > ; networks : Readonly < { v4 : ReadonlyArray < Readonly < { type : string ; ip_address : string ; } > > ; } > ; } > ; export type Account = Readonly < { email : string ; uuid : string ; email_verified : boolean ; status : string ; } > ; export type RegionInfo = Readonly < { slug : string ; name : string ; sizes : string [ ] ; available : boolean ; features : string [ ] ; } > ; export class XhrError extends errors . OutlineError { constructor ( ) { super ( ) ; } } export interface DigitalOceanSession { accessToken : string ; getAccount ( ) : Promise < Account > ; createDroplet ( displayName , region , publicKeyForSSH , dropletSpec ) : Promise < { droplet : DropletInfo ; } > ; deleteDroplet ( dropletId ) : Promise < void > ; getRegionInfo ( ) : Promise < RegionInfo [ ] > ; getDroplet ( dropletId ) : Promise < DropletInfo > ; getDropletTags ( dropletId ) : Promise < string [ ] > ; getDropletsByTag ( tag ) : Promise < DropletInfo [ ] > ; getDroplets ( ) : Promise < DropletInfo [ ] > ; } export class RestApiSession implements DigitalOceanSession { constructor ( public accessToken ) { } public getAccount ( ) { console . info ( 'Requesting account' ) ; return this . request < { account : Account ; } > ( 'GET' , 'account' ) . then ( ( response ) => { return response . account ; } ) ; } public createDroplet ( displayName , region , publicKeyForSSH , dropletSpec ) { const dropletName = makeValidDropletName ( displayName ) ; return this . registerKey_ ( dropletName , publicKeyForSSH ) . then ( ( keyId ) => { return this . makeCreateDropletRequest ( dropletName , region , keyId , dropletSpec ) ; } ) ; } private makeCreateDropletRequest ( dropletName , region , keyId , dropletSpec ) { let requestCount = 0 ; const MAX_REQUESTS = 10 ; const RETRY_TIMEOUT_MS = 5000 ; return new Promise ( ( fulfill , reject ) => { const makeRequestRecursive = ( ) => { ++ requestCount ; console . info ( `Requesting droplet creation ${ requestCount }/${ MAX_REQUESTS }` ) ; this . request < { droplet : DropletInfo ; } > ( 'POST' , 'droplets' , { name : dropletName , region , size : dropletSpec . size , image : dropletSpec . image , ssh_keys : [ keyId ] , user_data : dropletSpec . installCommand , tags : dropletSpec . tags , ipv6 : true , } ) . then ( fulfill ) . catch ( ( e ) => { if ( e . message . toLowerCase ( ) . indexOf ( 'finalizing' ) >= 0 && requestCount < MAX_REQUESTS ) { setTimeout ( makeRequestRecursive , RETRY_TIMEOUT_MS ) ; } else { reject ( e ) ; } } ) ; } ; makeRequestRecursive ( ) ; } ) ; } public deleteDroplet ( dropletId ) { console . info ( 'Requesting droplet deletion' ) ; return this . request < void > ( 'DELETE' , 'droplets/' + dropletId ) ; } public getRegionInfo ( ) { console . info ( 'Requesting region info' ) ; return this . request < { regions : RegionInfo [ ] ; } > ( 'GET' , 'regions' ) . then ( ( response ) => { return response . regions ; } ) ; } private registerKey_ ( keyName , publicKeyForSSH ) { console . info ( 'Requesting key registration' ) ; return this . request < { ssh_key : { id : number ; } ; } > ( 'POST' , 'account/keys' , { name : keyName , public_key : publicKeyForSSH } ) . then ( ( response ) => { return response . ssh_key . id ; } ) ; } public getDroplet ( dropletId ) { console . info ( 'Requesting droplet' ) ; return this . request < { droplet : DropletInfo ; } > ( 'GET' , 'droplets/' + dropletId ) . then ( ( response ) => { return response . droplet ; } ) ; } public getDropletTags ( dropletId ) { return this . getDroplet ( dropletId ) . then ( ( droplet ) => { return droplet . tags ; } ) ; } public getDropletsByTag ( tag ) { console . info ( 'Requesting droplet by tag' ) ; return this . request < { droplets : DropletInfo [ ] ; } > ( 'GET' , `droplets?tag_name=${ encodeURI ( tag ) }` ) . then ( ( response ) => { return response . droplets ; } ) ; } public getDroplets ( ) { console . info ( 'Requesting droplets' ) ; return this . request < { droplets : DropletInfo [ ] ; } > ( 'GET' , 'droplets' ) . then ( ( response ) => { return response . droplets ; } ) ; } private request < T > ( method , actionPath , data ? ) { return new Promise < T > ( ( resolve , reject ) => { const xhr = new XMLHttpRequest ( ) ; xhr . open ( method , `https://api.digitalocean.com/v2/${ actionPath }` ) ; xhr . setRequestHeader ( 'Authorization' , `Bearer ${ this . accessToken }` ) ; xhr . setRequestHeader ( 'Content-Type' , 'application/json' ) ; xhr . onload = ( ) => { if ( xhr . status >= 200 && xhr . status <= 299 ) { const responseObj = ( xhr . response ? JSON . parse ( xhr . response ) : { } ) ; resolve ( responseObj ) ; } else if ( xhr . status === 401 ) { console . error ( 'DigitalOcean request failed with Unauthorized error' ) ; reject ( new XhrError ( ) ) ; } else { const responseJson = JSON . parse ( xhr . response ) ; console . error ( `DigitalOcean request failed with status ${ xhr . status }` ) ; reject ( new Error ( `XHR ${ responseJson . id } failed with ${ xhr . status }: ${ responseJson . message }` ) ) ; } } ; xhr . onerror = ( ) => { console . error ( 'Failed to perform DigitalOcean request' ) ; reject ( new XhrError ( ) ) ; } ; xhr . send ( data ? JSON . stringify ( data ) : undefined ) ; } ) ; } } function makeValidDropletName ( name ) { return name . replace ( /[^A-Za-z0-9\-]/g , '' ) ; }