import * as gcp_api from '../cloud/gcp_api' ; import * as errors from '../infrastructure/errors' ; import { sleep } from '../infrastructure/sleep' ; import { ValueStream } from "../infrastructure/value_stream" ; import { Zone } from '../model/gcp' ; import * as server from '../model/server' ; import { DataAmount , ManagedServerHost , MonetaryCost } from '../model/server' ; import { ShadowboxServer } from './shadowbox_server' ; enum InstallState { UNKNOWN = 0 , INSTANCE_CREATED , IP_ALLOCATED , INSTANCE_RUNNING , CERTIFICATE_CREATED , COMPLETED , FAILED , CANCELED } function getCompletionFraction ( state ) { switch ( state ) { case InstallState . UNKNOWN : return 0.01 ; case InstallState . INSTANCE_CREATED : return 0.12 ; case InstallState . IP_ALLOCATED : return 0.14 ; case InstallState . INSTANCE_RUNNING : return 0.4 ; case InstallState . CERTIFICATE_CREATED : return 0.7 ; case InstallState . COMPLETED : return 1.0 ; default : return 0 ; } } function isFinal ( state ) { return state === InstallState . COMPLETED || state === InstallState . FAILED || state === InstallState . CANCELED ; } export class GcpServer extends ShadowboxServer implements server . ManagedServer { private static readonly GUEST_ATTRIBUTES_POLLING_INTERVAL_MS = 5 * 1000 ; private readonly instanceReadiness : Promise < void > ; private readonly gcpHost : GcpHost ; private readonly installState = new ValueStream < InstallState > ( InstallState . UNKNOWN ) ; constructor ( id , private locator , private gcpInstanceName , instanceCreation , private apiClient ) { super ( id ) ; const hasStaticIp = this . hasStaticIp ( ) ; this . instanceReadiness = instanceCreation . then ( async ( ) => { if ( this . installState . isClosed ( ) ) { return ; } this . setInstallState ( InstallState . INSTANCE_CREATED ) ; if ( ! await hasStaticIp ) { await this . promoteEphemeralIp ( ) ; } if ( this . installState . isClosed ( ) ) { return ; } this . setInstallState ( InstallState . IP_ALLOCATED ) ; this . pollInstallState ( ) ; } ) . catch ( ( e ) => { this . setInstallState ( InstallState . FAILED ) ; throw e ; } ) ; this . gcpHost = new GcpHost ( locator , gcpInstanceName , this . instanceReadiness , apiClient , this . onDelete . bind ( this ) ) ; } private getRegionLocator ( ) { return { regionId : new Zone ( this . locator . zoneId ) . regionId , projectId : this . locator . projectId } ; } private async hasStaticIp ( ) { try { await this . apiClient . getStaticIp ( this . getRegionLocator ( ) , this . gcpInstanceName ) ; return true ; } catch ( e ) { if ( is404 ( e ) ) { return false ; } throw new errors . ServerInstallFailedError ( `Static IP check failed: ${ e }` ) ; } } private async promoteEphemeralIp ( ) { const instance = await this . apiClient . getInstance ( this . locator ) ; const ipAddress = instance . networkInterfaces [ 0 ] . accessConfigs [ 0 ] . natIP ; const createStaticIpData = { name : instance . name , description : instance . description , address : ipAddress , } ; const createStaticIpOperation = await this . apiClient . createStaticIp ( this . getRegionLocator ( ) , createStaticIpData ) ; const operationErrors = createStaticIpOperation . error ?. errors ; if ( operationErrors ) { throw new errors . ServerInstallFailedError ( `Firewall creation failed: ${ operationErrors }` ) ; } } getHost ( ) { return this . gcpHost ; } async * monitorInstallProgress ( ) { for await ( const state of this . installState . watch ( ) ) { yield getCompletionFraction ( state ) ; } if ( this . installState . get ( ) === InstallState . FAILED ) { throw new errors . ServerInstallFailedError ( ) ; } else if ( this . installState . get ( ) === InstallState . CANCELED ) { throw new errors . ServerInstallCanceledError ( ) ; } yield getCompletionFraction ( this . installState . get ( ) ) ; } private async pollInstallState ( ) { while ( ! this . installState . isClosed ( ) ) { const outlineGuestAttributes = await this . getOutlineGuestAttributes ( ) ; if ( outlineGuestAttributes . has ( 'apiUrl' ) && outlineGuestAttributes . has ( 'certSha256' ) ) { const certSha256 = outlineGuestAttributes . get ( 'certSha256' ) ; const apiUrl = outlineGuestAttributes . get ( 'apiUrl' ) ; trustCertificate ( certSha256 ) ; this . setManagementApiUrl ( apiUrl ) ; this . setInstallState ( InstallState . COMPLETED ) ; break ; } else if ( outlineGuestAttributes . has ( 'install-error' ) ) { this . setInstallState ( InstallState . FAILED ) ; break ; } else if ( outlineGuestAttributes . has ( 'certSha256' ) ) { this . setInstallState ( InstallState . CERTIFICATE_CREATED ) ; } else if ( outlineGuestAttributes . has ( 'install-started' ) ) { this . setInstallState ( InstallState . INSTANCE_RUNNING ) ; } await sleep ( GcpServer . GUEST_ATTRIBUTES_POLLING_INTERVAL_MS ) ; } } private async getOutlineGuestAttributes ( ) { const result = new Map < string , string > ( ) ; const guestAttributes = await this . apiClient . getGuestAttributes ( this . locator , 'outline/' ) ; const attributes = guestAttributes ?. queryValue ?. items ?? [ ] ; attributes . forEach ( ( entry ) => { result . set ( entry . key , entry . value ) ; } ) ; return result ; } private setInstallState ( newState ) { console . debug ( InstallState [ newState ] ) ; this . installState . set ( newState ) ; if ( isFinal ( newState ) ) { this . installState . close ( ) ; } } private onDelete ( ) { if ( ! this . installState . isClosed ( ) ) { this . setInstallState ( InstallState . CANCELED ) ; } } } class GcpHost implements server . ManagedServerHost { constructor ( private readonly locator , private readonly gcpInstanceName , private readonly instanceReadiness , private readonly apiClient , private readonly deleteCallback ) { } async delete ( ) { this . deleteCallback ( ) ; try { await this . instanceReadiness ; } catch ( e ) { console . warn ( `Attempting deletion of server that failed setup: ${ e }` ) ; } const regionLocator = { regionId : this . getCloudLocation ( ) . regionId , projectId : this . locator . projectId } ; await this . waitForDelete ( this . apiClient . deleteStaticIp ( regionLocator , this . gcpInstanceName ) , 'Deleted server did not have a static IP' ) ; await this . waitForDelete ( this . apiClient . deleteInstance ( this . locator ) , 'No instance for deleted server' ) ; } private async waitForDelete ( deletion , msg404 ) { try { await deletion ; } catch ( e ) { if ( is404 ( e ) ) { console . warn ( msg404 ) ; return ; } throw e ; } } getHostId ( ) { return this . locator . instanceId ; } getMonthlyCost ( ) { return undefined ; } getMonthlyOutboundTransferLimit ( ) { return undefined ; } getCloudLocation ( ) { return new Zone ( this . locator . zoneId ) ; } } function is404 ( error ) { return error instanceof gcp_api . HttpError && error . getStatusCode ( ) === 404 ; }