import * as gcp_api from '../cloud/gcp_api' ; import { ServerInstallFailedError } from '../infrastructure/errors' ; import { sleep } from '../infrastructure/sleep' ; import { SCRIPT } from '../install_scripts/gcp_install_script' ; import * as gcp from '../model/gcp' ; import { BillingAccount , Project } from '../model/gcp' ; import * as server from '../model/server' ; import { GcpServer } from './gcp_server' ; import * as server_install from "./server_install" ; /** Returns a unique, RFC1035-style name as required by GCE. */ function makeGcpInstanceName ( ) { function pad2 ( val ) { return val . toString ( ) . padStart ( 2 , '0' ) ; } const now = new Date ( ) ; const year = now . getUTCFullYear ( ) . toString ( ) ; const month = pad2 ( now . getUTCMonth ( ) + 1 ) ; const day = pad2 ( now . getUTCDate ( ) ) ; const hour = pad2 ( now . getUTCHours ( ) ) ; const minute = pad2 ( now . getUTCMinutes ( ) ) ; const second = pad2 ( now . getUTCSeconds ( ) ) ; return `outline-${ year }${ month }${ day }-${ hour }${ minute }${ second }` ; } const FREE_TIER_REGIONS = new Set < string > ( [ 'us-west1' , 'us-central1' , 'us-east1' ] ) ; export function isInFreeTier ( zone ) { return FREE_TIER_REGIONS . has ( zone . regionId ) ; } /**
 * The Google Cloud Platform account model.
 */ export class GcpAccount implements gcp . Account { private static readonly OUTLINE_PROJECT_NAME = 'Outline servers' ; private static readonly OUTLINE_FIREWALL_NAME = 'outline' ; private static readonly OUTLINE_FIREWALL_TAG = 'outline' ; private static readonly MACHINE_SIZE = 'e2-micro' ; private static readonly REQUIRED_GCP_SERVICES = [ 'compute.googleapis.com' ] ; private readonly apiClient : gcp_api . RestApiClient ; constructor ( private id , private refreshToken , private shadowboxSettings ) { this . apiClient = new gcp_api . RestApiClient ( refreshToken ) ; } getId ( ) { return this . id ; } see Account .  async getName ( ) { const userInfo = await this . apiClient . getUserInfo ( ) ; return userInfo ?. email ; } /** Returns the refresh token. */ getRefreshToken ( ) { return this . refreshToken ; } see Account .  async listServers ( projectId ) { const result = [ ] ; const filter = 'labels.outline=true' ; const listAllInstancesResponse = await this . apiClient . listAllInstances ( projectId , filter ) ; const instanceMap = listAllInstancesResponse ?. items ?? { } ; Object . values ( instanceMap ) . forEach ( ( { instances } ) => { instances ?. forEach ( instance => { const { zoneId } = gcp_api . parseZoneUrl ( instance . zone ) ; const locator = { projectId , zoneId , instanceId : instance . id } ; const id = `${ this . id }:${ instance . id }` ; result . push ( new GcpServer ( id , locator , instance . name , Promise . resolve ( ) , this . apiClient ) ) ; } ) ; } ) ; return result ; } see Account .  async listLocations ( projectId ) { const listZonesResponse = await this . apiClient . listZones ( projectId ) ; const zones = listZonesResponse . items ?? [ ] ; return zones . map ( zoneInfo => ( { cloudLocation : new gcp . Zone ( zoneInfo . name ) , available : zoneInfo . status === 'UP' } ) ) ; } see Account .  async listProjects ( ) { const filter = 'labels.outline=true AND lifecycleState=ACTIVE' ; const response = await this . apiClient . listProjects ( filter ) ; if ( response . projects ?. length > 0 ) { return response . projects . map ( project => { return { id : project . projectId , name : project . name , } ; } ) ; } return [ ] ; } see Account .  async createProject ( projectId , billingAccountId ) { const createProjectData = { projectId , name : GcpAccount . OUTLINE_PROJECT_NAME , labels : { outline : 'true' , } , } ; const createProjectResponse = await this . apiClient . createProject ( createProjectData ) ; let createProjectOperation = null ; while ( ! createProjectOperation ?. done ) { await sleep ( 2 * 1000 ) ; createProjectOperation = await this . apiClient . resourceManagerOperationGet ( createProjectResponse . name ) ; } if ( createProjectOperation . error ) { } await this . configureProject ( projectId , billingAccountId ) ; return { id : projectId , name : GcpAccount . OUTLINE_PROJECT_NAME , } ; } async isProjectHealthy ( projectId ) { const projectBillingInfo = await this . apiClient . getProjectBillingInfo ( projectId ) ; if ( ! projectBillingInfo . billingEnabled ) { return false ; } const listEnabledServicesResponse = await this . apiClient . listEnabledServices ( projectId ) ; for ( const requiredService of GcpAccount . REQUIRED_GCP_SERVICES ) { const found = listEnabledServicesResponse . services . find ( service => service . config . name === requiredService ) ; if ( ! found ) { return false ; } } return true ; } async repairProject ( projectId , billingAccountId ) { return await this . configureProject ( projectId , billingAccountId ) ; } see Account .  async listOpenBillingAccounts ( ) { const response = await this . apiClient . listBillingAccounts ( ) ; if ( response . billingAccounts ?. length > 0 ) { return response . billingAccounts . filter ( billingAccount => billingAccount . open ) . map ( billingAccount => ( { id : billingAccount . name . substring ( billingAccount . name . lastIndexOf ( '/' ) + 1 ) , name : billingAccount . displayName , } ) ) ; } return [ ] ; } private async createFirewallIfNeeded ( projectId ) { const getFirewallResponse = await this . apiClient . listFirewalls ( projectId , GcpAccount . OUTLINE_FIREWALL_NAME ) ; if ( ! getFirewallResponse ?. items || getFirewallResponse ?. items ?. length === 0 ) { const createFirewallData = { name : GcpAccount . OUTLINE_FIREWALL_NAME , direction : 'INGRESS' , priority : 1000 , targetTags : [ GcpAccount . OUTLINE_FIREWALL_TAG ] , allowed : [ { IPProtocol : 'all' , } , ] , sourceRanges : [ '0.0.0.0/0' ] , } ; const createFirewallOperation = await this . apiClient . createFirewall ( projectId , createFirewallData ) ; const errors = createFirewallOperation . error ?. errors ; if ( errors ) { throw new ServerInstallFailedError ( `Firewall creation failed: ${ errors }` ) ; } } } see Account .  async createServer ( projectId , name , zone ) { await this . createFirewallIfNeeded ( projectId ) ; const gcpInstanceName = makeGcpInstanceName ( ) ; const createInstanceData = { name : gcpInstanceName , description : name , machineType : `zones/${ zone . id }/machineTypes/${ GcpAccount . MACHINE_SIZE }` , disks : [ { boot : true , initializeParams : { sourceImage : 'projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts' , } , } , ] , networkInterfaces : [ { network : 'global/networks/default' , accessConfigs : [ { } ] , } , ] , labels : { outline : 'true' , } , tags : { items : [ GcpAccount . OUTLINE_FIREWALL_TAG ] , } , metadata : { items : [ { key : 'enable-guest-attributes' , value : 'TRUE' , } , { key : 'user-data' , value : this . getInstallScript ( name ) , } , ] , } , } ; const zoneLocator = { projectId , zoneId : zone . id } ; const createInstanceOperation = await this . apiClient . createInstance ( zoneLocator , createInstanceData ) ; const errors = createInstanceOperation . error ?. errors ; if ( errors ) { throw new ServerInstallFailedError ( `Instance creation failed: ${ errors }` ) ; } const instanceId = createInstanceOperation . targetId ; const instanceLocator = { instanceId , ... zoneLocator } ; const instanceCreation = this . apiClient . computeEngineOperationZoneWait ( zoneLocator , createInstanceOperation . name ) ; const id = `${ this . id }:${ instanceId }` ; return new GcpServer ( id , instanceLocator , gcpInstanceName , instanceCreation , this . apiClient ) ; } private async configureProject ( projectId , billingAccountId ) { const updateProjectBillingInfoData = { name : `projects/${ projectId }/billingInfo` , projectId , billingAccountName : `billingAccounts/${ billingAccountId }` , } ; await this . apiClient . updateProjectBillingInfo ( projectId , updateProjectBillingInfoData ) ; const enableServicesData = { serviceIds : GcpAccount . REQUIRED_GCP_SERVICES , } ; const enableServicesResponse = await this . apiClient . enableServices ( projectId , enableServicesData ) ; let enableServicesOperation = null ; while ( ! enableServicesOperation ?. done ) { await sleep ( 2 * 1000 ) ; enableServicesOperation = await this . apiClient . serviceUsageOperationGet ( enableServicesResponse . name ) ; } if ( enableServicesResponse . error ) { } } private getInstallScript ( serverName ) { return '#!/bin/bash -eu\n' + server_install . getShellExportCommands ( this . shadowboxSettings , serverName ) + SCRIPT ; } }