import { UserError } from "@adpt/utils" ; import { cloneDeep , isError } from "lodash" ; import * as randomstring from "randomstring" ; import { DeployStatus } from "../deploy" ; import { DeploymentNotActive , DeploymentOpIDNotActive , DeployStepIDNotFound , InternalError } from "../error" ; import { ElementID } from "../jsx" ; import { DeploymentInfo } from "../ops/listDeployments" ; import { DeploymentStored , DeployOpID , DeployStepID , DeployStepInfo , ElementStatus , ElementStatusMap , } from "./deployment_data" ; import { HistoryEntry , HistoryName , HistoryStatus , HistoryStore } from "./history" ; import { AdaptServer , isServerPathExists , ServerLock , withLock } from "./server" ; export interface Deployment { readonly deployID : string ; getDataDir ( withStatus ) : Promise < string > ; releaseDataDir ( ) : Promise < void > ; commitEntry ( toStore ) : Promise < void > ; historyEntry ( historyName ) : Promise < HistoryEntry > ; lastEntry ( withStatus ? ) : Promise < HistoryEntry | undefined > ; currentOpID ( ) : Promise < DeployOpID > ; newOpID ( ) : Promise < DeployOpID > ; currentStepID ( opID ) : Promise < DeployStepID > ; newStepID ( opID ) : Promise < DeployStepID > ; status ( stepID ) : Promise < DeployStepInfo > ; status ( stepID , info ) : Promise < void > ; elementStatus ( stepID , elementID ) : Promise < ElementStatus > ; elementStatus ( stepID , statusMap ) : Promise < void > ; } const deploymentPath = "/deployments" ; const maxTries = 100 ; const invalidChars = RegExp ( "[:/]" , "g" ) ; export function encodePathComponent ( comp ) { return comp . replace ( invalidChars , "_" ) ; } function dpath ( deployID ) { deployID = encodePathComponent ( deployID ) ; return `${ deploymentPath }/${ deployID }` ; } function makeName ( base ) { const rand = randomstring . generate ( { length : 4 , charset : "alphabetic" , readable : true , capitalization : "lowercase" , } ) ; return `${ base }-${ rand }` ; } function isPathNotFound ( err ) { return isError ( err ) && err . name === "DataError" ; } export interface DeploymentOptions { deployID ? : string ; } export async function createDeployment ( server , projectName , stackName , options = { } ) { const baseName = `${ projectName }::${ stackName }` ; let deployID = "" ; for ( let i = 0 ; i < maxTries ; i ++ ) { deployID = options . deployID || makeName ( baseName ) ; const deployData = { deployID , currentOpID : null , deployOpInfo : { } , stateDirs : [ ] , } ; try { await server . set ( dpath ( deployID ) , deployData , { mustCreate : true } ) ; break ; } catch ( err ) { if ( isServerPathExists ( err ) && options . deployID ) { throw new UserError ( `DeployID '${ deployID }' already exists` ) ; } if ( ! isPathNotFound ( err ) ) throw err ; } } const deployment = new DeploymentImpl ( deployID , server ) ; await deployment . init ( ) ; return deployment ; } export async function loadDeployment ( server , deployID ) { try { await server . get ( dpath ( deployID ) ) ; } catch ( err ) { if ( ! isPathNotFound ( err ) ) throw err ; throw new UserError ( `Deployment '${ deployID }' does not exist` ) ; } const deployment = new DeploymentImpl ( deployID , server ) ; await deployment . init ( ) ; return deployment ; } export async function destroyDeployment ( server , deployID ) { let history ; const errs = [ ] ; try { history = await server . historyStore ( dpath ( deployID ) , false ) ; } catch ( err ) { if ( ! isPathNotFound ( err ) ) throw err ; } try { await server . delete ( dpath ( deployID ) ) ; } catch ( err ) { errs . push ( err . message || err . toString ( ) ) ; } try { if ( history ) await history . destroy ( ) ; } catch ( err ) { errs . push ( err . message || err . toString ( ) ) ; } if ( errs . length > 0 ) { const msg = `Error deleting deployment '${ deployID }': ` + errs . join ( " and " ) ; throw new Error ( msg ) ; } } export async function listDeploymentIDs ( server ) { try { const deps = await listDeployments ( server ) ; return deps . map ( ( dep ) => dep . deployID ) ; } catch ( err ) { throw new Error ( `Error listing deployments: ${ err }` ) ; } } export async function listDeployments ( server ) { try { const deps = await server . get ( deploymentPath ) ; return Object . keys ( deps ) . map ( ( key ) => ( { deployID : deps [ key ] . deployID } ) ) ; } catch ( err ) { if ( isPathNotFound ( err ) ) return [ ] ; throw new Error ( `Error listing deployments: ${ err }` ) ; } } class DeploymentImpl implements Deployment { private historyStore_ ? : HistoryStore ; private basePath_ : string ; constructor ( public deployID , private server ) { this . basePath_ = dpath ( this . deployID ) ; } init = async ( ) => { this . historyStore_ = await this . server . historyStore ( this . basePath_ , true ) ; } ; getDataDir = ( withStatus ) => this . historyStore . getDataDir ( withStatus ) ; releaseDataDir = ( ) => this . historyStore . releaseDataDir ( ) ; commitEntry = ( toStore ) => this . historyStore . commitEntry ( toStore ) ; historyEntry = ( name ) => this . historyStore . historyEntry ( name ) ; lastEntry = ( withStatus ) => this . historyStore . last ( withStatus ) ; async status ( stepID ) ; async status ( stepID , info ) ; async status ( stepID , info ? ) { const path = this . stepInfoPath ( stepID ) ; if ( info !== undefined ) { return withLock ( this . server , async ( lock ) => { await this . assertCurrent ( stepID , lock ) ; const cur = await this . server . get ( path , { lock } ) ; await this . server . set ( path , { ... cur , ... info } , { lock } ) ; return ; } ) ; } try { const i = await this . server . get ( path ) ; return cloneDeep ( i ) ; } catch ( err ) { if ( ! isPathNotFound ( err ) ) throw err ; throw new DeployStepIDNotFound ( stepID . deployOpID , stepID . deployStepNum ) ; } } async elementStatus ( stepID , elementID ) ; async elementStatus ( stepID , statusMap ) ; async elementStatus ( stepID , idOrMap ) { const path = this . stepInfoPath ( stepID ) + `/elementStatus` ; return withLock ( this . server , async ( lock ) => { if ( typeof idOrMap === "string" ) { await this . currentOpID ( lock ) ; try { return await this . server . get ( `${ path }/${ idOrMap }` , { lock } ) ; } catch ( err ) { if ( ! isPathNotFound ( err ) ) throw err ; throw new Error ( `ElementID '${ idOrMap }' not found` ) ; } } await this . assertCurrent ( stepID , lock ) ; let old = { } ; try { old = await this . server . get ( path , { lock } ) ; } catch ( err ) { if ( ! isPathNotFound ( err ) ) throw err ; } await this . server . set ( path , { ... old , ... idOrMap } , { lock } ) ; } ) ; } async newOpID ( ) { return withLock ( this . server , async ( lock ) => { const opPath = this . basePath_ + "/currentOpID" ; const cur = await this . server . get ( opPath , { lock } ) ; const next = cur == null ? 0 : cur + 1 ; const stepPath = this . stepNumPath ( next ) ; await this . server . set ( stepPath , null , { lock } ) ; await this . server . set ( opPath , next , { lock } ) ; return next ; } ) ; } async currentOpID ( lock ? ) { const path = this . basePath_ + "/currentOpID" ; const cur = await this . server . get ( path , { lock } ) ; if ( cur == null ) throw new DeploymentNotActive ( this . deployID ) ; return cur ; } async newStepID ( deployOpID ) { return withLock ( this . server , async ( lock ) => { await this . assertCurrentOpID ( deployOpID , lock ) ; const cur = await this . currentStepNum ( deployOpID , lock ) ; const deployStepNum = cur == null ? 0 : cur + 1 ; const info = { deployStatus : DeployStatus . Initial , goalStatus : DeployStatus . Initial , elementStatus : { } , } ; const step = { deployOpID , deployStepNum , } ; await this . server . set ( this . stepInfoPath ( step ) , info , { lock } ) ; await this . server . set ( this . stepNumPath ( deployOpID ) , deployStepNum , { lock } ) ; return step ; } ) ; } async currentStepID ( deployOpID ) { return withLock ( this . server , async ( lock ) => { await this . assertCurrentOpID ( deployOpID , lock ) ; const deployStepNum = await this . currentStepNum ( deployOpID , lock ) ; if ( deployStepNum == null ) { throw new DeploymentOpIDNotActive ( this . deployID , deployOpID ) ; } return { deployOpID , deployStepNum , } ; } ) ; } private stepInfoPath ( stepID ) { return `${ this . basePath_ }/deployOpInfo/` + `${ stepID . deployOpID }/${ stepID . deployStepNum }` ; } private stepNumPath ( opID ) { return this . basePath_ + `/deployOpInfo/${ opID }/currentStepNum` ; } private get historyStore ( ) { if ( this . historyStore_ == null ) throw new InternalError ( `null historyStore` ) ; return this . historyStore_ ; } private async currentStepNum ( opID , lock ) { const path = this . stepNumPath ( opID ) ; return this . server . get ( path , { lock } ) ; } private async assertCurrentOpID ( opID , lock ) { const current = await this . currentOpID ( lock ) ; if ( opID !== current ) { throw new Error ( `Requested DeployOpID (${ opID }) is not current (${ current })` ) ; } } private async assertCurrent ( stepID , lock ) { await this . assertCurrentOpID ( stepID . deployOpID , lock ) ; const current = await this . currentStepNum ( stepID . deployOpID , lock ) ; if ( current == null ) { throw new DeploymentOpIDNotActive ( this . deployID , stepID . deployOpID ) ; } if ( stepID . deployStepNum !== current ) { throw new Error ( `Requested DeployStepID ` + `(${ stepID . deployOpID }.${ stepID . deployStepNum }) is not ` + `current (${ stepID . deployOpID }.${ current })` ) ; } } }