codius
Version:
Command Line Interface for Codius
177 lines (161 loc) • 6.17 kB
JavaScript
/**
* @fileOverview
* @name hosts-utils.js
* @author Travis Crist
*/
const fetch = require('ilp-fetch')
const logger = require('riverpig')('codius-cli:host-utils')
const config = require('../config.js')
const BigNumber = require('bignumber.js')
const sampleSize = require('lodash.samplesize')
const { getCurrencyDetails } = require('../common/price.js')
const { URL } = require('url')
const { fetchPromise } = require('../common/utils.js')
const moment = require('moment')
const BATCH_SIZE = 30
function cleanHostListUrls (hosts) {
let hostList
// Singular host options are a string so we have to make them into an array
if (typeof hosts === 'string') {
hostList = [hosts]
} else {
hostList = hosts
}
return hostList.map(host => {
if (!host.startsWith('http://') && !host.startsWith('https://')) {
host = `https://${host}`
}
try {
const url = new URL(host)
return url.origin
} catch (err) {
throw new Error(err)
}
})
}
async function fetchHostPrice (host, duration, manifestJson) {
const fetchFunction = fetch(`${host}/pods?duration=${duration}`, {
headers: {
Accept: `application/codius-v${config.version.codius.min}+json`,
'Content-Type': 'application/json'
},
method: 'OPTIONS',
body: JSON.stringify(manifestJson),
timeout: 10000 // 10s
})
return fetchPromise(fetchFunction, host)
}
async function checkHostsPrices (fetchHostPromises, maxMonthlyRate) {
logger.debug(`Fetching host prices from ${fetchHostPromises.length} host(s)`)
const responses = await Promise.all(fetchHostPromises)
const currency = await getCurrencyDetails()
const results = await responses.reduce((acc, curr) => {
if (curr.error) {
acc.failed.push(curr)
} else if (!new BigNumber(curr.response.price).lte(maxMonthlyRate)) {
const errorMessage = {
message: 'Quoted price exceeded specified max price, please increase your max price.',
host: curr.host,
quotedPrice: `${curr.response.price.toString()} ${currency}`,
maxPrice: `${maxMonthlyRate} ${currency}`
}
acc.failed.push(errorMessage)
} else {
acc.success.push(curr)
}
return acc
}, { success: [], failed: [] })
return results
}
async function gatherMatchingValidHosts ({ duration, hostCount = 1 }, hostList, maxMonthlyRate, manifestJson) {
let validHosts = []
const maxAttempts = hostList.length
let attemptCount = 0
let invalidHosts = []
while (validHosts.length < hostCount && attemptCount < maxAttempts) {
logger.debug(`Valid Hosts Found: ${validHosts.length}, attemptCount: ${attemptCount} need: ${hostCount} host(s) maxAttempts: ${maxAttempts}`)
const candidateHosts = sampleSize(hostList, hostCount < BATCH_SIZE ? hostCount : BATCH_SIZE).filter((host) => !invalidHosts.includes(host))
logger.debug(`Candidate Hosts: ${candidateHosts}`)
logger.debug(`InvalidHosts: ${invalidHosts}`)
attemptCount += candidateHosts.length
const fetchPromises = candidateHosts.map((host) => fetchHostPrice(host, duration, manifestJson))
const priceCheckResults = await checkHostsPrices(fetchPromises, maxMonthlyRate)
if (priceCheckResults.success.length > 0) {
validHosts = [...new Set([...validHosts, ...priceCheckResults.success.map((obj) => obj.host)])]
}
if (priceCheckResults.failed.length > 0) {
invalidHosts = [...new Set([...invalidHosts, ...priceCheckResults.failed.map((obj) => obj.host)])]
}
}
if (validHosts.length < hostCount) {
const error = {
message: `Unable to find ${hostCount} hosts with provided max price. Found ${validHosts.length} matching host(s)`,
invalidHosts: invalidHosts
}
throw new Error(JSON.stringify(error))
}
logger.debug(`Validated Price successfully against ${validHosts.length}`)
const uploadHosts = validHosts.slice(0, hostCount)
logger.debug(`Using ${uploadHosts.length} for upload`)
return uploadHosts
}
async function checkPricesOnHosts (hosts, duration, maxMonthlyRate, manifestJson) {
const fetchPromises = hosts.map((host) => fetchHostPrice(host, duration, manifestJson))
const priceCheckResults = await checkHostsPrices(fetchPromises, maxMonthlyRate)
if (priceCheckResults.failed.length !== 0) {
throw new Error(JSON.stringify(priceCheckResults.failed, null, 2))
}
return hosts
}
async function getValidHosts (options, hostOpts) {
let uploadHosts = []
if (options.host || (hostOpts.codiusHostsExists && !options.hostCount)) {
await checkPricesOnHosts(hostOpts.hostList, options.duration, hostOpts.maxMonthlyRate, hostOpts.manifestJson)
uploadHosts = hostOpts.hostList
} else {
uploadHosts = await gatherMatchingValidHosts(options, hostOpts.hostList, hostOpts.maxMonthlyRate, hostOpts.manifestJson)
}
return uploadHosts
}
function getHostsStatus (codiusStateJson) {
const hostList = codiusStateJson.hostList
const hostDetails = codiusStateJson.status ? codiusStateJson.status.hostDetails : null
return hostList.map(host => {
if (hostDetails && hostDetails[host]) {
const hostInfo = hostDetails[host]
return {
host,
expirationDate: hostInfo.expirationDate,
'expires/expired': moment().to(moment(hostInfo.expirationDate, 'MM-DD-YYYY HH:mm:ss Z')),
totalPricePaid: `${hostInfo.price.totalPaid} ${hostInfo.price.units}`
}
} else {
return {
host,
message: 'No Existing Host Details for this host.'
}
}
})
}
function getHostList ({ host, manifestHash }) {
let hostsArr = []
if (!host) {
const potentialHost = manifestHash.split('.')
potentialHost.shift()
if (potentialHost.length <= 0) {
throw new Error(`The end of ${manifestHash} is not a valid url. Please use the format <manifesth-hash.hostName> to specify the specific pod to extend or the --host parameter.`)
}
console.log(potentialHost)
hostsArr = [`https://${potentialHost.join('.')}`]
} else {
hostsArr = host
}
return cleanHostListUrls(hostsArr)
}
module.exports = {
cleanHostListUrls,
getValidHosts,
checkPricesOnHosts,
getHostsStatus,
getHostList
}