import { DigitalOceanSession , DropletInfo , RestApiSession } from '../cloud/digitalocean_api' ; import * as crypto from '../infrastructure/crypto' ; import * as do_install_script from '../install_scripts/do_install_script' ; import * as digitalocean from '../model/digitalocean' ; import * as server from '../model/server' ; import { DigitalOceanServer } from './digitalocean_server' ; import { getShellExportCommands , ShadowboxSettings } from "./server_install" ; const SHADOWBOX_TAG = 'shadowbox' ; const MACHINE_SIZE = 's-1vcpu-1gb' ; export class DigitalOceanAccount implements digitalocean . Account { private readonly digitalOcean : DigitalOceanSession ; private servers : DigitalOceanServer [ ] = [ ] ; constructor ( private id , private accessToken , private shadowboxSettings , private debugMode ) { this . digitalOcean = new RestApiSession ( accessToken ) ; } getId ( ) { return this . id ; } async getName ( ) { return ( await this . digitalOcean . getAccount ( ) ) ?. email ; } async getStatus ( ) { const account = await this . digitalOcean . getAccount ( ) ; if ( account . status === 'active' ) { return digitalocean . Status . ACTIVE ; } if ( ! account . email_verified ) { return digitalocean . Status . EMAIL_UNVERIFIED ; } return digitalocean . Status . MISSING_BILLING_INFORMATION ; } async listLocations ( ) { const regions = await this . digitalOcean . getRegionInfo ( ) ; return regions . map ( info => ( { cloudLocation : new digitalocean . Region ( info . slug ) , available : info . available && info . sizes . indexOf ( MACHINE_SIZE ) !== - 1 } ) ) ; } async createServer ( region , name ) { console . time ( 'activeServer' ) ; console . time ( 'servingServer' ) ; const keyPair = await crypto . generateKeyPair ( ) ; const installCommand = getInstallScript ( this . digitalOcean . accessToken , name , this . shadowboxSettings ) ; const dropletSpec = { installCommand , size : MACHINE_SIZE , image : 'docker-18-04' , tags : [ SHADOWBOX_TAG ] , } ; if ( this . debugMode ) { console . debug ( `private key for SSH access to new droplet:\n${ keyPair . private . replace ( /\r/g , '' ) }\n\n` + 'Use "ssh -i keyfile root@[ip_address]" to connect to the machine' ) ; } const response = await this . digitalOcean . createDroplet ( name , region . id , keyPair . public , dropletSpec ) ; const server = this . createDigitalOceanServer ( this . digitalOcean , response . droplet ) ; server . onceDropletActive . then ( async ( ) => { console . timeEnd ( 'activeServer' ) ; for await ( const _ of server . monitorInstallProgress ( ) ) { } console . timeEnd ( 'servingServer' ) ; } ) . catch ( e => console . log ( 'Couldn\'t time installation' , e ) ) ; return server ; } listServers ( fetchFromHost = true ) { if ( ! fetchFromHost ) { return Promise . resolve ( this . servers ) ; } return this . digitalOcean . getDropletsByTag ( SHADOWBOX_TAG ) . then ( ( droplets ) => { this . servers = [ ] ; return droplets . map ( ( droplet ) => { return this . createDigitalOceanServer ( this . digitalOcean , droplet ) ; } ) ; } ) ; } getAccessToken ( ) { return this . accessToken ; } private createDigitalOceanServer ( digitalOcean , dropletInfo ) { const server = new DigitalOceanServer ( `${ this . id }:${ dropletInfo . id }` , digitalOcean , dropletInfo ) ; this . servers . push ( server ) ; return server ; } } function sanitizeDigitalOceanToken ( input ) { const sanitizedInput = input . trim ( ) ; const pattern = /^[A-Za-z0-9_\/-]+$/ ; if ( ! pattern . test ( sanitizedInput ) ) { throw new Error ( 'Invalid DigitalOcean Token' ) ; } return sanitizedInput ; } function getInstallScript ( accessToken , name , shadowboxSettings ) { const sanitizedAccessToken = sanitizeDigitalOceanToken ( accessToken ) ; return '#!/bin/bash -eu\n' + `export DO_ACCESS_TOKEN='${ sanitizedAccessToken }'\n` + getShellExportCommands ( shadowboxSettings , name ) + do_install_script . SCRIPT ; }