/**
 * Copyright 2020 Opstrace, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import { call , delay , CallEffect } from "redux-saga/effects" ; import { google , sql_v1beta4 } from "googleapis" ; import { log , SECOND , sleep } from "@opstrace/utils" ; import { ensureSQLInstanceDoesNotExist , ensureSQLInstanceExists } from "./cloudSQLInstance" ; import { ensureSQLDatabaseDoesNotExist , ensureSQLDatabaseExists } from "./cloudSQLDatabase" ; import { ensureAddressDoesNotExist , ensureAddressExists } from "./globalAddress" ; const snclient = google . servicenetworking ( "v1" ) ; async function listServiceConnections ( network ) { let result ; try { result = await snclient . services . connections . list ( { parent : "services/servicenetworking.googleapis.com" , network : network } ) ; } catch ( err ) { log . warning ( `error during services.connections.list: ${ err } -- ignore` ) ; return ; } const pfx = `services.connections.list for services/servicenetworking.googleapis.com ` + `and network ${ network }:` ; if ( result . data !== undefined ) { log . info ( `${ pfx }\nt${ JSON . stringify ( result . data , null , 2 ) }` ) ; return ; } log . warning ( `${ pfx }: unexpected result obj: ${ JSON . stringify ( result , null , 2 ) }` ) ; } /**
 * Set up VPC Network Peering connection between a service producer's VPC
 * network and a service consumer's VPC network.
 *
 * The VPC peering creation is a long-running operation that is followed with
 * individual API calls until it either has succeeded or failed. VPC peering
 * creation is retried upon failure.
 *
 * The parameters are explained here:
 * https://cloud.google.com/service-infrastructure/docs/service-networking/reference/rest/v1/services.connections#Connection
 *
 * `network`: The name of service consumer's VPC network that's connected with
 * service producer network, in the following format:
 * projects/{project}/global/networks/{network}
 *
 * `addressName`: "The name of one IP address range for the service producer",
 * "of type PEERING."
 */ async function createServiceConnection ( { addressName , network } ) { const logpfx = "set up peering between CloudSQL VPC and Opstrace instance VPC" ; let operationName ; let attempt = 0 ; const requestparams = { parent : "services/servicenetworking.googleapis.com" , requestBody : { network , reservedPeeringRanges : [ addressName ] , service : "servicenetworking.googleapis.com" } } ; while ( true ) { attempt += 1 ; await listServiceConnections ( network ) ; log . info ( `${ logpfx }: create with params: ${ requestparams }` ) ; let result ; try { result = await snclient . services . connections . create ( requestparams ) ; } catch ( err ) { log . error ( `${ logpfx }: error during services.connections.create: ${ err }` ) ; } log . debug ( `${ logpfx }: services.connections.create result: ${ JSON . stringify ( result , null , 2 ) }` ) ; if ( result !== undefined && result . data !== undefined ) { const response = result . data ; if ( response . name !== undefined ) { log . info ( `${ logpfx }: started log-running operation: ${ response . name }` ) ; operationName = response . name ; if ( await waitForLongrunningOperationToSucceed ( snclient , operationName , logpfx ) ) { log . info ( `${ logpfx }: operation completed, leave peerVpcs()` ) ; return ; } else { log . info ( `${ logpfx }: operation resulted in permanent error, retry creation from scratch` ) ; } } } const delaySeconds = 45 ; log . info ( `${ logpfx }: connections.create(): attempt ${ attempt } failed, retry in ${ delaySeconds } s` ) ; await sleep ( delaySeconds ) ; } } param operationName param logpfx returns async function waitForLongrunningOperationToSucceed ( apiclient , operationName , logpfx ) { log . info ( `${ logpfx }: follow long-running operation ${ operationName }` ) ; let attempt = 0 ; while ( true ) { attempt += 1 ; let result ; try { result = await apiclient . operations . get ( { name : operationName } ) ; } catch ( err ) { log . error ( `${ logpfx }: error during operations.get: ${ err }` ) ; } log . debug ( `${ logpfx }: operations.get result: ${ result }` ) ; if ( result !== undefined && result . data !== undefined ) { const operation = result . data ; log . info ( `${ logpfx }: current operation status: ${ JSON . stringify ( operation , null , 2 ) }` ) ; if ( operation . error !== undefined ) { log . info ( `${ logpfx }: operation failed: ${ JSON . stringify ( operation . error , null , 2 ) }` ) ; log . debug ( `${ logpfx }: operation failed, err.details: ${ JSON . stringify ( operation . error . details , null , 2 ) }` ) ; return false ; } if ( operation . response !== undefined ) { log . info ( `${ logpfx }: operation seems to have successfully completed. ` + `Response: ${ JSON . stringify ( operation . response , null , 2 ) }` ) ; if ( ! operation . done ) { log . warning ( `${ logpfx }: operation has response, but 'done' is not true yet` ) ; } return true ; } } const delaySeconds = 10 ; log . info ( `${ logpfx }: follow operation ${ operationName }: attempt ${ attempt } done, retry in ${ delaySeconds } s` ) ; await sleep ( delaySeconds ) ; } } export function * ensureCloudSQLExists ( { instance , addressName , network , region , ipCidrRange , opstraceClusterName } ) { log . info ( `Ensuring CloudSQL exists` ) ; const auth = new google . auth . GoogleAuth ( { scopes : [ "https://www.googleapis.com/auth/cloud-platform" , "https://www.googleapis.com/auth/sqlservice.admin" , "https://www.googleapis.com/auth/compute" , "https://www.googleapis.com/auth/compute.readonly" , "https://www.googleapis.com/auth/service.management" ] } ) ; google . options ( { auth } ) ; log . info ( `Ensure Address exists` ) ; yield call ( ensureAddressExists , { region , addressName , network , ipCidrRange } ) ; log . info ( `Peering cluster VPC with CloudSQL services VPC: ${ JSON . stringify ( { network , addressName } , null , 2 ) }` ) ; yield call ( createServiceConnection , { network , addressName } ) ; yield call ( listServiceConnections , network ) ; let attemptNumber = 0 ; const sqlInstanceCreationDeadline = Date . now ( ) + 15 * 60 * SECOND ; log . info ( `Ensure SQLInstance exists` ) ; while ( sqlInstanceCreationDeadline > Date . now ( ) ) { log . info ( `Attempt ${ attemptNumber ++ } to create SQLInstance` ) ; yield delay ( 15 * SECOND ) ; try { const existingInstance = yield call ( ensureSQLInstanceExists , { instance , opstraceClusterName } ) ; if ( ! existingInstance . name ) { throw Error ( "SQLInstance did not return a name" ) ; } log . info ( `Ensure SQLDatabase exists` ) ; yield call ( ensureSQLDatabaseExists , { opstraceClusterName } ) ; return existingInstance ; } catch ( err ) { log . debug ( "Creating SQLInstance failed, retrying: %s" , err ) ; } } throw Error ( `SQLInstance creation deadline hit after ${ attemptNumber } attempts` ) ; } export function * ensureCloudSQLDoesNotExist ( { opstraceClusterName , addressName } ) { const auth = new google . auth . GoogleAuth ( { scopes : [ "https://www.googleapis.com/auth/cloud-platform" , "https://www.googleapis.com/auth/sqlservice.admin" , "https://www.googleapis.com/auth/compute" , "https://www.googleapis.com/auth/compute.readonly" , "https://www.googleapis.com/auth/service.management" ] } ) ; google . options ( { auth } ) ; log . info ( `Ensure SQLDatabase deletion` ) ; yield call ( ensureSQLDatabaseDoesNotExist , opstraceClusterName ) ; log . info ( `Ensure SQLInstance deletion` ) ; yield call ( ensureSQLInstanceDoesNotExist , opstraceClusterName ) ; log . info ( `Ensure Address deletion` ) ; yield call ( ensureAddressDoesNotExist , { addressName } ) ; }