import { ImageInfo } from 'dockerode' ; import ApiStatusCodes from '../api/ApiStatusCodes' ; import DataStore from '../datastore/DataStore' ; import DockerApi , { IDockerUpdateOrders } from '../docker/DockerApi' ; import { PreDeployFunction } from '../models/OtherTypes' ; import CaptainConstants from '../utils/CaptainConstants' ; import Logger from '../utils/Logger' ; import Utils from '../utils/Utils' ; import Authenticator from './Authenticator' ; import DockerRegistryHelper from './DockerRegistryHelper' ; import ImageMaker , { BuildLogsManager } from './ImageMaker' ; import DomainResolveChecker from './system/DomainResolveChecker' ; import LoadBalancerManager from './system/LoadBalancerManager' ; import requireFromString = require ( 'require-from-string' ) ; const ERROR_FIRST_ENABLE_ROOT_SSL = 'You have to first enable SSL for your root domain' ; const serviceMangerCache = { } as IHashMapGeneric < ServiceManager > ; interface QueuedPromise { resolve : undefined | ( ( reason ? : unknown ) => void ) ; reject : undefined | ( ( reason ? : any ) => void ) ; promise : undefined | Promise < unknown > ; } interface QueuedBuild { appName : string ; source : IImageSource ; promiseToSave : QueuedPromise ; } class ServiceManager { static get ( namespace , authenticator , dataStore , dockerApi , loadBalancerManager , domainResolveChecker ) { if ( ! serviceMangerCache [ namespace ] ) { serviceMangerCache [ namespace ] = new ServiceManager ( dataStore , authenticator , dockerApi , loadBalancerManager , domainResolveChecker ) ; } return serviceMangerCache [ namespace ] ; } private activeOrScheduledBuilds : IHashMapGeneric < boolean > ; private buildLogsManager : BuildLogsManager ; private queuedBuilds : QueuedBuild [ ] ; private isReady : boolean ; private imageMaker : ImageMaker ; private dockerRegistryHelper : DockerRegistryHelper ; constructor ( private dataStore , private authenticator , private dockerApi , private loadBalancerManager , private domainResolveChecker ) { this . activeOrScheduledBuilds = { } ; this . queuedBuilds = [ ] ; this . buildLogsManager = new BuildLogsManager ( ) ; this . isReady = true ; this . dockerRegistryHelper = new DockerRegistryHelper ( this . dataStore , this . dockerApi ) ; this . imageMaker = new ImageMaker ( this . dockerRegistryHelper , this . dockerApi , this . dataStore . getNameSpace ( ) , this . buildLogsManager ) ; } getRegistryHelper ( ) { return this . dockerRegistryHelper ; } isInited ( ) { return this . isReady ; } scheduleDeployNewVersion ( appName , source ) { const self = this ; const activeBuildAppName = self . isAnyBuildRunning ( ) ; this . activeOrScheduledBuilds [ appName ] = true ; self . buildLogsManager . getAppBuildLogs ( appName ) . clear ( ) ; if ( activeBuildAppName ) { const existingBuildForTheSameApp = self . queuedBuilds . find ( ( v ) => v . appName === appName ) ; if ( existingBuildForTheSameApp ) { self . buildLogsManager . getAppBuildLogs ( appName ) . log ( `A build for ${ appName } was queued, it's now being replaced with a new build...` ) ; existingBuildForTheSameApp . source = source ; const existingPromise = existingBuildForTheSameApp . promiseToSave . promise ; if ( ! existingPromise ) throw new Error ( 'Existing promise for the queued app is NULL!!' ) ; return existingPromise ; } self . buildLogsManager . getAppBuildLogs ( appName ) . log ( `An active build (${ activeBuildAppName }) is in progress. This build is queued...` ) ; const promiseToSave = { resolve : undefined , reject : undefined , promise : undefined , } ; const promise = new Promise ( function ( resolve , reject ) { promiseToSave . resolve = resolve ; promiseToSave . reject = reject ; } ) ; promiseToSave . promise = promise ; self . queuedBuilds . push ( { appName , source , promiseToSave } ) ; return promise ; } return this . startDeployingNewVersion ( appName , source ) ; } startDeployingNewVersion ( appName , source ) { const self = this ; const dataStore = this . dataStore ; let deployedVersion ; return Promise . resolve ( ) . then ( function ( ) { return dataStore . getAppsDataStore ( ) . createNewVersion ( appName ) ; } ) . then ( function ( appVersion ) { deployedVersion = appVersion ; return dataStore . getAppsDataStore ( ) . getAppDefinition ( appName ) . then ( function ( app ) { const envVars = app . envVars || [ ] ; return self . imageMaker . ensureImage ( source , appName , app . captainDefinitionRelativeFilePath , appVersion , envVars ) ; } ) ; } ) . then ( function ( builtImage ) { return dataStore . getAppsDataStore ( ) . setDeployedVersionAndImage ( appName , deployedVersion , builtImage ) ; } ) . then ( function ( ) { self . onBuildFinished ( appName ) ; return self . ensureServiceInitedAndUpdated ( appName ) ; } ) . catch ( function ( error ) { self . onBuildFinished ( appName ) ; return new Promise < void > ( function ( resolve , reject ) { self . logBuildFailed ( appName , error ) ; reject ( error ) ; } ) ; } ) ; } onBuildFinished ( appName ) { const self = this ; self . activeOrScheduledBuilds [ appName ] = false ; Promise . resolve ( ) . then ( function ( ) { const newBuild = self . queuedBuilds . shift ( ) ; if ( newBuild ) self . startDeployingNewVersion ( newBuild . appName , newBuild . source ) ; } ) ; } enableCustomDomainSsl ( appName , customDomain ) { const self = this ; return Promise . resolve ( ) . then ( function ( ) { return self . dataStore . getHasRootSsl ( ) ; } ) . then ( function ( rootHasSsl ) { if ( ! rootHasSsl ) { throw ApiStatusCodes . createError ( ApiStatusCodes . STATUS_ERROR_GENERIC , ERROR_FIRST_ENABLE_ROOT_SSL ) ; } Logger . d ( `Verifying Captain owns domain: ${ customDomain }` ) ; return self . domainResolveChecker . verifyCaptainOwnsDomainOrThrow ( customDomain , undefined ) ; } ) . then ( function ( ) { Logger . d ( `Enabling SSL for: ${ appName } on ${ customDomain }` ) ; return self . dataStore . getAppsDataStore ( ) . verifyCustomDomainBelongsToApp ( appName , customDomain ) ; } ) . then ( function ( ) { return self . domainResolveChecker . requestCertificateForDomain ( customDomain ) ; } ) . then ( function ( ) { return self . dataStore . getAppsDataStore ( ) . enableCustomDomainSsl ( appName , customDomain ) ; } ) . then ( function ( ) { return self . reloadLoadBalancer ( ) ; } ) ; } addCustomDomain ( appName , customDomain ) { const self = this ; return Promise . resolve ( ) . then ( function ( ) { const rootDomain = self . dataStore . getRootDomain ( ) ; try { Utils . checkCustomDomain ( customDomain , appName , rootDomain ) ; } catch ( error ) { throw ApiStatusCodes . createError ( ApiStatusCodes . STATUS_ERROR_BAD_NAME , error ) ; } } ) . then ( function ( ) { return self . domainResolveChecker . verifyDomainResolvesToDefaultServerOnHost ( customDomain ) ; } ) . then ( function ( ) { Logger . d ( `Enabling custom domain for: ${ appName }` ) ; return self . dataStore . getAppsDataStore ( ) . addCustomDomainForApp ( appName , customDomain ) ; } ) . then ( function ( ) { return self . reloadLoadBalancer ( ) ; } ) ; } removeCustomDomain ( appName , customDomain ) { const self = this ; return Promise . resolve ( ) . then ( function ( ) { Logger . d ( `Removing custom domain for: ${ appName }` ) ; return self . dataStore . getAppsDataStore ( ) . removeCustomDomainForApp ( appName , customDomain ) ; } ) . then ( function ( ) { return self . reloadLoadBalancer ( ) ; } ) ; } enableSslForApp ( appName ) { const self = this ; let rootDomain ; return Promise . resolve ( ) . then ( function ( ) { return self . dataStore . getHasRootSsl ( ) ; } ) . then ( function ( rootHasSsl ) { if ( ! rootHasSsl ) { throw ApiStatusCodes . createError ( ApiStatusCodes . STATUS_ERROR_GENERIC , ERROR_FIRST_ENABLE_ROOT_SSL ) ; } return self . verifyCaptainOwnsGenericSubDomain ( appName ) ; } ) . then ( function ( ) { Logger . d ( `Enabling SSL for: ${ appName }` ) ; return self . dataStore . getRootDomain ( ) ; } ) . then ( function ( val ) { rootDomain = val ; if ( ! rootDomain ) { throw new Error ( 'No rootDomain! Cannot verify domain' ) ; } } ) . then ( function ( ) { return self . dataStore . getAppsDataStore ( ) . getAppDefinition ( appName ) ; } ) . then ( function ( ) { return `${ appName }.${ rootDomain }` ; } ) . then ( function ( domainName ) { return self . domainResolveChecker . requestCertificateForDomain ( domainName ) ; } ) . then ( function ( ) { return self . dataStore . getAppsDataStore ( ) . setSslForDefaultSubDomain ( appName , true ) ; } ) . then ( function ( ) { return self . reloadLoadBalancer ( ) ; } ) ; } verifyCaptainOwnsGenericSubDomain ( appName ) { const self = this ; let rootDomain ; return Promise . resolve ( ) . then ( function ( ) { return self . dataStore . getRootDomain ( ) ; } ) . then ( function ( val ) { rootDomain = val ; } ) . then ( function ( ) { return self . dataStore . getAppsDataStore ( ) . getAppDefinition ( appName ) ; } ) . then ( function ( ) { return `${ appName }.${ rootDomain }` ; } ) . then ( function ( domainName ) { Logger . d ( `Verifying Captain owns domain: ${ domainName }` ) ; return self . domainResolveChecker . verifyCaptainOwnsDomainOrThrow ( domainName , undefined ) ; } ) ; } renameApp ( oldAppName , newAppName ) { Logger . d ( `Renaming app: ${ oldAppName }` ) ; const self = this ; const oldServiceName = this . dataStore . getAppsDataStore ( ) . getServiceName ( oldAppName ) ; const dockerApi = this . dockerApi ; const dataStore = this . dataStore ; let defaultSslOn = false ; return Promise . resolve ( ) . then ( function ( ) { return dataStore . getAppsDataStore ( ) . getAppDefinition ( oldAppName ) ; } ) . then ( function ( appDef ) { defaultSslOn = ! ! appDef . hasDefaultSubDomainSsl ; dataStore . getAppsDataStore ( ) . nameAllowedOrThrow ( newAppName ) ; return self . ensureNotBuilding ( oldAppName ) ; } ) . then ( function ( ) { Logger . d ( `Check if service is running: ${ oldServiceName }` ) ; return dockerApi . isServiceRunningByName ( oldServiceName ) ; } ) . then ( function ( isRunning ) { if ( ! isRunning ) { throw ApiStatusCodes . createError ( ApiStatusCodes . STATUS_ERROR_GENERIC , 'Service is not running!' ) ; } return dockerApi . removeServiceByName ( oldServiceName ) ; } ) . then ( function ( ) { return dataStore . getAppsDataStore ( ) . renameApp ( self . authenticator , oldAppName , newAppName ) ; } ) . then ( function ( ) { return self . ensureServiceInitedAndUpdated ( newAppName ) ; } ) . then ( function ( ) { if ( defaultSslOn ) return self . enableSslForApp ( newAppName ) ; } ) ; } removeApp ( appName ) { Logger . d ( `Removing service for: ${ appName }` ) ; const self = this ; const serviceName = this . dataStore . getAppsDataStore ( ) . getServiceName ( appName ) ; const dockerApi = this . dockerApi ; const dataStore = this . dataStore ; return Promise . resolve ( ) . then ( function ( ) { return self . ensureNotBuilding ( appName ) ; } ) . then ( function ( ) { Logger . d ( `Check if service is running: ${ serviceName }` ) ; return dockerApi . isServiceRunningByName ( serviceName ) ; } ) . then ( function ( isRunning ) { if ( isRunning ) { return dockerApi . removeServiceByName ( serviceName ) ; } else { Logger . w ( `Cannot delete service... It is not running: ${ serviceName }` ) ; return true ; } } ) . then ( function ( ) { return dataStore . getAppsDataStore ( ) . deleteAppDefinition ( appName ) ; } ) . then ( function ( ) { return self . reloadLoadBalancer ( ) ; } ) ; } removeVolsSafe ( volumes ) { const dockerApi = this . dockerApi ; const dataStore = this . dataStore ; const volsFailedToDelete = { } ; return Promise . resolve ( ) . then ( function ( ) { return dataStore . getAppsDataStore ( ) . getAppDefinitions ( ) ; } ) . then ( function ( apps ) { Object . keys ( apps ) . forEach ( ( appName ) => { const app = apps [ appName ] ; const volsInApp = app . volumes || [ ] ; volsInApp . forEach ( ( v ) => { const volName = v . volumeName ; if ( ! volName ) return ; if ( volumes . indexOf ( volName ) >= 0 ) { volsFailedToDelete [ volName ] = true ; } } ) ; } ) ; const volumesTryToDelete = [ ] ; volumes . forEach ( ( v ) => { if ( ! volsFailedToDelete [ v ] ) { volumesTryToDelete . push ( dataStore . getAppsDataStore ( ) . getVolumeName ( v ) ) ; } } ) ; return dockerApi . deleteVols ( volumesTryToDelete ) ; } ) . then ( function ( failedVols ) { failedVols . forEach ( ( v ) => { volsFailedToDelete [ v ] = true ; } ) ; return Object . keys ( volsFailedToDelete ) ; } ) ; } getUnusedImages ( mostRecentLimit ) { Logger . d ( `Getting unused images, excluding most recent ones: ${ mostRecentLimit }` ) ; const dockerApi = this . dockerApi ; const dataStore = this . dataStore ; let allImages ; return Promise . resolve ( ) . then ( function ( ) { return dockerApi . getImages ( ) ; } ) . then ( function ( images ) { allImages = images ; return dataStore . getAppsDataStore ( ) . getAppDefinitions ( ) ; } ) . then ( function ( apps ) { const unusedImages = [ ] ; if ( mostRecentLimit < 0 ) { throw ApiStatusCodes . createError ( ApiStatusCodes . ILLEGAL_PARAMETER , 'Most Recent Limit cannot be negative' ) ; } for ( let i = 0 ; i < allImages . length ; i ++ ) { const currentImage = allImages [ i ] ; let imageInUse = false ; const repoTags = currentImage . RepoTags || [ ] ; Object . keys ( apps ) . forEach ( function ( appName ) { const app = apps [ appName ] ; for ( let k = 0 ; k < mostRecentLimit + 1 ; k ++ ) { const versionToCheck = Number ( app . deployedVersion ) - k ; if ( versionToCheck < 0 ) continue ; let deployedImage = '' ; app . versions . forEach ( ( v ) => { if ( v . version === versionToCheck ) { deployedImage = v . deployedImageName || '' ; } } ) ; if ( ! deployedImage ) continue ; if ( repoTags . indexOf ( deployedImage ) >= 0 ) { imageInUse = true ; } } } ) ; if ( ! imageInUse ) { unusedImages . push ( { id : currentImage . Id , tags : repoTags , } ) ; } } return unusedImages ; } ) ; } deleteImages ( imageIds ) { Logger . d ( 'Deleting images...' ) ; const dockerApi = this . dockerApi ; return Promise . resolve ( ) . then ( function ( ) { return dockerApi . deleteImages ( imageIds ) ; } ) ; } createPreDeployFunctionIfExist ( app ) { let preDeployFunction = app . preDeployFunction ; if ( ! preDeployFunction ) { return undefined ; } preDeployFunction = preDeployFunction + '\n\n module.exports = preDeployFunction' ; return requireFromString ( preDeployFunction ) ; } ensureNotBuilding ( appName ) { if ( this . activeOrScheduledBuilds [ appName ] ) throw ApiStatusCodes . createError ( ApiStatusCodes . ILLEGAL_OPERATION , `Build in-progress for ${ appName }. Please wait...` ) ; } updateAppDefinition ( appName , description , instanceCount , captainDefinitionRelativeFilePath , envVars , volumes , nodeId , notExposeAsWebApp , containerHttpPort , httpAuth , forceSsl , ports , repoInfo , customNginxConfig , preDeployFunction , serviceUpdateOverride , websocketSupport , appDeployTokenConfig ) { const self = this ; const dataStore = this . dataStore ; const dockerApi = this . dockerApi ; let serviceName ; const checkIfNodeIdExists = function ( nodeIdToCheck ) { return dockerApi . getNodesInfo ( ) . then ( function ( nodeInfo ) { for ( let i = 0 ; i < nodeInfo . length ; i ++ ) { if ( nodeIdToCheck === nodeInfo [ i ] . nodeId ) { return ; } } throw ApiStatusCodes . createError ( ApiStatusCodes . STATUS_ERROR_GENERIC , `Node ID you requested is not part of the swarm cluster: ${ nodeIdToCheck }` ) ; } ) ; } ; return Promise . resolve ( ) . then ( function ( ) { return self . ensureNotBuilding ( appName ) ; } ) . then ( function ( ) { return dataStore . getAppsDataStore ( ) . getAppDefinition ( appName ) ; } ) . then ( function ( app ) { serviceName = dataStore . getAppsDataStore ( ) . getServiceName ( appName ) ; if ( app . hasPersistentData ) { if ( nodeId ) { return checkIfNodeIdExists ( nodeId ) ; } else { if ( app . nodeId ) { nodeId = app . nodeId ; } else { return dockerApi . isServiceRunningByName ( serviceName ) . then ( function ( isRunning ) { if ( ! isRunning ) { throw ApiStatusCodes . createError ( ApiStatusCodes . STATUS_ERROR_GENERIC , 'Cannot find the service. Try again in a minute...' ) ; } return dockerApi . getNodeIdByServiceName ( serviceName , 0 ) ; } ) . then ( function ( nodeIdRunningService ) { if ( ! nodeIdRunningService ) { throw ApiStatusCodes . createError ( ApiStatusCodes . STATUS_ERROR_GENERIC , 'No NodeId was found. Try again in a minute...' ) ; } nodeId = nodeIdRunningService ; } ) ; } } } else { if ( volumes && volumes . length ) { throw ApiStatusCodes . createError ( ApiStatusCodes . ILLEGAL_OPERATION , 'Cannot set volumes for a non-persistent container!' ) ; } if ( nodeId ) { return checkIfNodeIdExists ( nodeId ) ; } } } ) . then ( function ( ) { serviceUpdateOverride = serviceUpdateOverride ? `${ serviceUpdateOverride }` . trim ( ) : '' ; if ( ! serviceUpdateOverride ) { return ; } if ( ! Utils . convertYamlOrJsonToObject ( serviceUpdateOverride ) ) { throw ApiStatusCodes . createError ( ApiStatusCodes . ILLEGAL_PARAMETER , 'serviceUpdateOverride must be either a valid JSON object starting with { or an equivalent yaml' ) ; } } ) . then ( function ( ) { return dataStore . getAppsDataStore ( ) . updateAppDefinitionInDb ( appName , description , instanceCount , captainDefinitionRelativeFilePath , envVars , volumes , nodeId , notExposeAsWebApp , containerHttpPort , httpAuth , forceSsl , ports , repoInfo , self . authenticator , customNginxConfig , preDeployFunction , serviceUpdateOverride , websocketSupport , appDeployTokenConfig ) ; } ) . then ( function ( ) { return self . ensureServiceInitedAndUpdated ( appName ) ; } ) . then ( function ( ) { return self . reloadLoadBalancer ( ) ; } ) ; } isAppBuilding ( appName ) { return ! ! this . activeOrScheduledBuilds [ appName ] ; } returns isAnyBuildRunning ( ) { const activeBuilds = this . activeOrScheduledBuilds ; for ( const appName in activeBuilds ) { if ( activeBuilds [ appName ] ) { return appName ; } } return undefined ; } getBuildStatus ( appName ) { const self = this ; return { isAppBuilding : self . isAppBuilding ( appName ) , logs : self . buildLogsManager . getAppBuildLogs ( appName ) . getLogs ( ) , isBuildFailed : self . buildLogsManager . getAppBuildLogs ( appName ) . isBuildFailed , } ; } logBuildFailed ( appName , error ) { error = ( error || '' ) + '' ; this . buildLogsManager . getAppBuildLogs ( appName ) . onBuildFailed ( error ) ; } getAppLogs ( appName , encoding ) { const serviceName = this . dataStore . getAppsDataStore ( ) . getServiceName ( appName ) ; const dockerApi = this . dockerApi ; return Promise . resolve ( ) . then ( function ( ) { return dockerApi . getLogForService ( serviceName , CaptainConstants . configs . appLogSize , encoding ) ; } ) ; } ensureServiceInitedAndUpdated ( appName ) { Logger . d ( `Ensure service inited and Updated for: ${ appName }` ) ; const self = this ; const serviceName = this . dataStore . getAppsDataStore ( ) . getServiceName ( appName ) ; let imageName ; const dockerApi = this . dockerApi ; const dataStore = this . dataStore ; let app ; let dockerAuthObject ; return Promise . resolve ( ) . then ( function ( ) { return dataStore . getAppsDataStore ( ) . getAppDefinition ( appName ) ; } ) . then ( function ( appFound ) { app = appFound ; Logger . d ( `Check if service is running: ${ serviceName }` ) ; return dockerApi . isServiceRunningByName ( serviceName ) ; } ) . then ( function ( isRunning ) { for ( let i = 0 ; i < app . versions . length ; i ++ ) { const element = app . versions [ i ] ; if ( element . version === app . deployedVersion ) { imageName = element . deployedImageName ; break ; } } if ( ! imageName ) { throw ApiStatusCodes . createError ( ApiStatusCodes . ILLEGAL_PARAMETER , 'ImageName for deployed version is not available, this version was probably failed due to an unsuccessful build!' ) ; } if ( isRunning ) { Logger . d ( `Service is already running: ${ serviceName }` ) ; return true ; } else { Logger . d ( `Creating service ${ serviceName } with default image, we will update image later` ) ; return dockerApi . createServiceOnNodeId ( CaptainConstants . configs . appPlaceholderImageName , serviceName , undefined , undefined , undefined , undefined , undefined ) . then ( ( ) => true ) ; } } ) . then ( function ( ) { return self . dockerRegistryHelper . getDockerAuthObjectForImageName ( imageName ! ) ; } ) . then ( function ( data ) { dockerAuthObject = data ; } ) . then ( function ( ) { return self . createPreDeployFunctionIfExist ( app ) ; } ) . then ( function ( preDeployFunction ) { Logger . d ( `Updating service ${ serviceName } with image ${ imageName }` ) ; return dockerApi . updateService ( serviceName , imageName , app . volumes , app . networks , app . envVars , undefined , dockerAuthObject , Number ( app . instanceCount ) , app . nodeId , dataStore . getNameSpace ( ) , app . ports , app , IDockerUpdateOrders . AUTO , Utils . convertYamlOrJsonToObject ( app . serviceUpdateOverride ) , preDeployFunction ) ; } ) . then ( function ( ) { return new Promise < void > ( function ( resolve ) { setTimeout ( resolve , 2000 ) ; } ) ; } ) . then ( function ( ) { return self . reloadLoadBalancer ( ) ; } ) ; } reloadLoadBalancer ( ) { Logger . d ( 'Updating Load Balancer - ServiceManager' ) ; const self = this ; return self . loadBalancerManager . rePopulateNginxConfigFile ( self . dataStore ) ; } } export default ServiceManager ;