import * as azure from "@pulumi/azure" ; import * as cloud from "@pulumi/cloud" ; import * as pulumi from "@pulumi/pulumi" ; import { RunError } from "@pulumi/pulumi/errors" ; import * as shared from "./shared" ; import * as docker from "@pulumi/docker" ; import * as utils from "./utils" ; import * as azureContainerSDK from "azure-arm-containerinstance" ; import * as msrest from "ms-rest-azure" ; export class Service extends pulumi . ComponentResource implements cloud . Service { public readonly name : string ; public readonly group : azure . containerservice . Group ; public readonly endpoints : pulumi . Output < cloud . Endpoints > ; public readonly defaultEndpoint : pulumi . Output < cloud . Endpoint > ; public readonly getEndpoint : ( containerName ? : string , containerPort ? : number ) => Promise < cloud . Endpoint > ; constructor ( name , args , opts ? ) { let containers ; if ( args . image || args . build || args . function ) { if ( args . containers ) { throw new Error ( "Exactly one of image, build, function, or containers must be used, not multiple" ) ; } containers = { "default" : args } ; } else if ( args . containers ) { containers = args . containers ; } else { throw new Error ( "Missing one of image, build, function, or containers, specifying this service's containers" ) ; } const replicas = args . replicas === undefined ? 1 : args . replicas ; if ( replicas !== 1 ) { throw new RunError ( "Only a single replicable is supported in Azure currently." ) ; } super ( "cloud:service:Service" , name , { } , opts ) ; const { group , endpoints , defaultEndpoint } = createGroup ( this , name , args . host , containers ) ; this . group = group ; this . endpoints = endpoints ; this . defaultEndpoint = defaultEndpoint ; this . getEndpoint = async ( containerName , containerPort ) => { return getEndpointHelper ( endpoints . get ( ) , containerName , containerPort ) ; } ; this . registerOutputs ( ) ; } } function getEndpointHelper ( endpoints , containerName ? , containerPort ? ) { containerName = containerName || Object . keys ( endpoints ) [ 0 ] ; if ( ! containerName ) { throw new RunError ( `No containers available in this service` ) ; } const containerPorts = endpoints [ containerName ] || { } ; containerPort = containerPort || + Object . keys ( containerPorts ) [ 0 ] ; if ( ! containerPort ) { throw new RunError ( `No ports available in service container ${ containerName }` ) ; } const endpoint = containerPorts [ containerPort ] ; if ( ! endpoint ) { throw new RunError ( `No exposed port for ${ containerName } port ${ containerPort }` ) ; } return endpoint ; } interface AzureContainer { commands ? : pulumi . Input < string [ ] > ; cpu : pulumi . Input < number > ; environmentVariables ? : pulumi . Input < { [ key ] : any ; } > ; image : pulumi . Input < string > ; memory : pulumi . Input < number > ; name : pulumi . Input < string > ; ports ? : azure . types . input . containerservice . GroupContainer [ "ports" ] ; protocol ? : pulumi . Input < string > ; volumes ? : pulumi . Input < pulumi . Input < { mountPath : pulumi . Input < string > ; name : pulumi . Input < string > ; readOnly ? : pulumi . Input < boolean > ; shareName : pulumi . Input < string > ; storageAccountKey : pulumi . Input < string > ; storageAccountName : pulumi . Input < string > ; } > [ ] > ; } interface AzureCredentials { password : pulumi . Input < string > ; server : pulumi . Input < string > ; username : pulumi . Input < string > ; } interface GroupInfo { group : azure . containerservice . Group ; endpoints : pulumi . Output < cloud . Endpoints > ; defaultEndpoint : pulumi . Output < cloud . Endpoint > ; } interface ExposedPorts { [ name ] : { [ port ] : number ; } ; } function createGroup ( parent , name , props , containers ) { const disallowedChars = /[^-a-zA-Z0-9]/g ; props = props || { } ; const azureContainers = [ ] ; let credentials ; let firstContainerName ; let firstContainerPort ; const exposedPorts = { } ; for ( const containerName of Object . keys ( containers ) ) { const container = containers [ containerName ] ; if ( firstContainerName === undefined ) { firstContainerName = containerName ; } const ports = container . ports ; let hostPortNumber ; let targetPortNumber ; let protocol ; let isPublic ; if ( ports ) { if ( ports . length >= 2 ) { throw new RunError ( "Only zero or one port can be provided with a container: " + containerName ) ; } if ( ports . length === 1 ) { const port = ports [ 0 ] ; hostPortNumber = port . port ; targetPortNumber = port . targetPort !== undefined ? port . targetPort : hostPortNumber ; protocol = port . protocol ; if ( targetPortNumber !== hostPortNumber ) { throw new RunError ( "Mapping a host port to a different target port is not supported in Azure currently." ) ; } if ( containerName === firstContainerName ) { firstContainerPort = targetPortNumber ; } const external = port . external === undefined ? false : port . external ; if ( isPublic === undefined ) { isPublic = external ; } else if ( isPublic !== external ) { throw new RunError ( "All ports must have a matching [external] value." ) ; } } } if ( isPublic === false ) { throw new RunError ( "Only public ip address types are supported by Azure currently." ) ; } const { imageOptions , registry } = computeImageOptionsAndRegistry ( this , container ) ; const memoryInGB = pulumi . output ( container . memoryReservation ) . apply ( r => r === undefined ? 1 : r / 1024 ) ; const qualifiedName = ( name + "-" + containerName ) . replace ( disallowedChars , "-" ) ; azureContainers . push ( { name : qualifiedName , cpu : pulumi . output ( container . cpu ) . apply ( c => c === undefined ? 1 : c ) , memory : memoryInGB , ports : [ { port : targetPortNumber , } ] , protocol : protocol , image : imageOptions . image , environmentVariables : imageOptions . environment , commands : container . command , } ) ; credentials = credentials || ( registry ? [ { password : registry . adminPassword , server : registry . loginServer , username : registry . adminUsername } ] : undefined ) ; if ( targetPortNumber !== undefined ) { exposedPorts [ containerName ] = { [ targetPortNumber ] : hostPortNumber ! } ; } } const group = new azure . containerservice . Group ( name . replace ( disallowedChars , "-" ) , { containers : azureContainers , location : shared . location , resourceGroupName : shared . globalResourceGroupName , osType : getOS ( props ) , imageRegistryCredentials : credentials , ipAddressType : "Public" , } , { parent : parent } ) ; const endpoints = getEndpoints ( exposedPorts , group ) ; const defaultEndpoint = firstContainerName === undefined || firstContainerPort === undefined ? pulumi . output < cloud . Endpoint > ( undefined ! ) : endpoints . apply ( ep => getEndpointHelper ( ep ) ) ; return { group , endpoints , defaultEndpoint } ; } function getEndpoints ( ports , group ) { return pulumi . all ( utils . apply ( ports , targetPortToHostPort => { const inner = pulumi . all ( utils . apply ( targetPortToHostPort , hostPort => group . ipAddress . apply ( ip => ( { port : hostPort , hostname : ip , } ) ) ) ) ; return inner ; } ) ) ; } /**
 * A Task represents a container which can be [run] dynamically whenever (and as many times as)
 * needed.
 */ export class Task extends pulumi . ComponentResource implements cloud . Task { public readonly run : ( options ? : cloud . TaskRunOptions ) => Promise < void > ; constructor ( name , container , opts ? ) { super ( "cloud:task:Task" , name , { } , opts ) ; if ( container . ports && container . ports . length > 0 ) { throw new RunError ( "Tasks should not be given any [ports] in their Container definition." ) ; } const { imageOptions , registry } = computeImageOptionsAndRegistry ( this , container ) ; const globalResourceGroupName = shared . globalResourceGroupName ; const memory = pulumi . output ( container . memoryReservation ) ; const config = new pulumi . Config ( "cloud-azure" ) ; const subscriptionId = config . require ( "subscriptionId" ) ; const clientId = config . require ( "clientId" ) ; const clientSecret = config . require ( "clientSecret" ) ; const tenantId = config . require ( "tenantId" ) ; this . run = async ( options ) => { try { options = options || { } ; const clientCredentials = await new Promise ( ( resolve , reject ) => { msrest . loginWithServicePrincipalSecret ( clientId , clientSecret , tenantId , ( err , credentials ) => { if ( err ) { return reject ( err ) ; } resolve ( credentials ) ; } ) ; } ) ; const client = new azureContainerSDK . ContainerInstanceManagementClient ( clientCredentials , subscriptionId ) ; const imageOpts = imageOptions . get ( ) ; let envMap = imageOpts . environment ; if ( options . environment ) { envMap = Object . assign ( options . environment , envMap ) ; } const env = Object . keys ( envMap ) . map ( k => ( { name : k , value : envMap [ k ] } ) ) ; const containerCredentials = registry ? [ { server : registry . loginServer . get ( ) , username : registry . adminUsername . get ( ) , password : registry . adminPassword . get ( ) } ] : undefined ; const uniqueName = createUniqueContainerName ( name ) ; const group = await client . containerGroups . createOrUpdate ( globalResourceGroupName . get ( ) , uniqueName , { location : shared . location , osType : getOS ( options . host ) , containers : [ { name : uniqueName , image : imageOpts . image , environmentVariables : env , resources : { requests : { cpu : 1 , memoryInGB : memory . get ( ) || 1 , } , } , } ] , imageRegistryCredentials : containerCredentials , restartPolicy : "Never" , } ) ; } catch ( err ) { console . log ( "Error: " + JSON . stringify ( err , null , 2 ) ) ; throw err ; } } ; this . registerOutputs ( ) ; } } function getOS ( props ) { return props && props . os ? props . os : "Linux" ; } function createUniqueContainerName ( name ) { const uniqueName = name + shared . sha1hash ( Math . random ( ) . toString ( ) ) ; return uniqueName . toLowerCase ( ) ; } export class SharedVolume extends pulumi . ComponentResource implements cloud . SharedVolume { public readonly kind = "SharedVolume" ; public readonly name : string ; constructor ( name , opts ? ) { super ( "cloud:volume:Volume" , name , { } , opts ) ; throw new Error ( "Method not implemented." ) ; this . registerOutputs ( { kind : this . kind , name : this . name } ) ; } } export class HostPathVolume implements cloud . HostPathVolume { public readonly kind = "HostPathVolume" ; public readonly path : string ; constructor ( path ) { this . path = path ; } } function getBuildImageName ( build ) { let buildSig ; if ( typeof build === "string" ) { buildSig = build ; } else { buildSig = build . context || "." ; if ( build . dockerfile ) { buildSig += `;dockerfile=${ build . dockerfile }` ; } if ( build . args ) { for ( const arg of Object . keys ( build . args ) ) { buildSig += `;arg[${ arg }]=${ build . args [ arg ] }` ; } } } const imageName = shared . createNameWithStackInfo ( `container-${ shared . sha1hash ( buildSig ) }` , 63 , "-" ) ; const disallowedChars = /[^-a-zA-Z0-9]/g ; return imageName . replace ( disallowedChars , "-" ) ; } let globalRegistry ; function getOrCreateGlobalRegistry ( ) { if ( ! globalRegistry ) { globalRegistry = new azure . containerservice . Registry ( "global" , { resourceGroupName : shared . globalResourceGroupName , location : shared . location , adminEnabled : true , sku : "Standard" , } , { parent : shared . getGlobalInfrastructureResource ( ) } ) ; } return globalRegistry ; } const buildImageCache = new Map < string , pulumi . Output < string > > ( ) ; interface ImageOptions { image : string ; environment : Record < string , string > ; } function computeImageOptionsAndRegistry ( parent , container ) { const preEnv = Object . assign ( { } , container . environment || { } ) ; if ( container . build ) { return computeImageFromBuild ( parent , preEnv , container . build ) ; } else if ( container . image ) { return { imageOptions : createImageOptions ( container . image , preEnv ) , registry : undefined } ; } else if ( container . function ) { return { imageOptions : computeImageFromFunction ( container . function , preEnv ) , registry : undefined } ; } else { throw new RunError ( "Invalid container definition: `image`, `build`, or `function` must be provided" ) ; } } function computeImageFromBuild ( parent , preEnv , build ) { const imageName = getBuildImageName ( build ) ; const registry = getOrCreateGlobalRegistry ( ) ; pulumi . log . debug ( `Building container image at '${ build }'` , registry ) ; const dockerRegistry = pulumi . output ( { registry : registry . loginServer , username : registry . adminUsername , password : registry . adminPassword , } ) ; const imageOptions = pulumi . all ( [ registry . loginServer , dockerRegistry ] ) . apply ( ( [ loginServer , dockerRegistry ] ) => computeImageFromBuildWorker ( preEnv , build , imageName , loginServer + "/" + imageName , dockerRegistry , parent ) ) ; return { imageOptions , registry } ; } function computeImageFromBuildWorker ( preEnv , build , imageName , repositoryUrl , dockerRegistry , logResource ) { let uniqueImageName = buildImageCache . get ( imageName ) ; if ( uniqueImageName ) { uniqueImageName . apply ( d => pulumi . log . debug ( `    already built: ${ imageName } (${ d })` , logResource ) ) ; } else { uniqueImageName = docker . buildAndPushImage ( imageName , build , repositoryUrl , logResource , async ( ) => dockerRegistry ) ; uniqueImageName . apply ( d => pulumi . log . debug ( `    build complete: ${ imageName } (${ d })` , logResource ) ) ; } return createImageOptions ( uniqueImageName , preEnv ) ; } function computeImageFromFunction ( func , preEnv ) { preEnv . PULUMI_SRC = pulumi . runtime . serializeFunction ( func ) . then ( v => v . text ) ; return createImageOptions ( "lukehoban/nodejsrunner" , preEnv ) ; } function createImageOptions ( image , environment ) { return pulumi . output ( { image , environment } ) ; }