UNPKG

heroku

Version:

CLI to interact with Heroku

250 lines (248 loc) 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderAttachment = void 0; const color_1 = require("@heroku-cli/color"); const command_1 = require("@heroku-cli/command"); const core_1 = require("@oclif/core"); const heroku_cli_util_1 = require("@heroku/heroku-cli-util"); const util_1 = require("../../lib/addons/util"); const lodash_1 = require("lodash"); const printf = require('printf'); const topic = 'addons'; async function addonGetter(api, app) { let attachmentsResponse = null; let addonsResponse; if (app) { // don't display attachments globally addonsResponse = api.get(`/apps/${app}/addons`, { headers: { 'Accept-Expansion': 'addon_service,plan', Accept: 'application/vnd.heroku+json; version=3.sdk', }, }); const sudoHeaders = JSON.parse(process.env.HEROKU_HEADERS || '{}'); if (sudoHeaders['X-Heroku-Sudo'] && !sudoHeaders['X-Heroku-Sudo-User']) { // because the root /addon-attachments endpoint won't include relevant // attachments when sudo-ing for another app, we will use the more // specific API call and sacrifice listing foreign attachments. attachmentsResponse = api.get(`/apps/${app}/addon-attachments`); } else { // In order to display all foreign attachments, we'll get out entire // attachment list attachmentsResponse = api.get('/addon-attachments'); } } else { addonsResponse = api.get('/addons', { headers: { 'Accept-Expansion': 'addon_service,plan', Accept: 'application/vnd.heroku+json; version=3.sdk', }, }); } // Get addons and attachments in parallel const [{ body: addonsRaw }, potentialAttachments] = await Promise.all([addonsResponse, attachmentsResponse]); function isRelevantToApp(addon) { var _a; return !app || ((_a = addon.app) === null || _a === void 0 ? void 0 : _a.name) === app || (0, lodash_1.some)(addon.attachments, att => att.app.name === app); } const groupedAttachments = (0, lodash_1.groupBy)(potentialAttachments === null || potentialAttachments === void 0 ? void 0 : potentialAttachments.body, 'addon.id'); const addons = []; addonsRaw.forEach(function (addon) { addon.attachments = groupedAttachments[addon.id] || []; delete groupedAttachments[addon.id]; if (isRelevantToApp(addon)) { addons.push(addon); } if (addon.plan) { addon.plan.price = (0, util_1.grandfatheredPrice)(addon); } }); // Any attachments left didn't have a corresponding add-on record in API. // This is probably normal (because we are asking API for all attachments) // but it could also be due to certain types of permissions issues, so check // if the attachment looks relevant to the app, and then render whatever (0, lodash_1.values)(groupedAttachments) .forEach(function (atts) { const inaccessibleAddon = { app: atts[0].addon.app, name: atts[0].addon.name, addon_service: {}, plan: {}, attachments: atts, }; if (isRelevantToApp(inaccessibleAddon)) { addons.push(inaccessibleAddon); } }); return addons; } function displayAll(addons) { addons = (0, lodash_1.sortBy)(addons, 'app.name', 'plan.name', 'addon.name'); if (addons.length === 0) { core_1.ux.log('No add-ons.'); return; } heroku_cli_util_1.hux.table(addons, { 'Owning App': { get: ({ app }) => color_1.default.cyan((app === null || app === void 0 ? void 0 : app.name) || ''), }, 'Add-on': { get: ({ name }) => color_1.default.magenta(name || ''), }, Plan: { get: function ({ plan }) { if (typeof plan === 'undefined') return color_1.default.dim('?'); return plan.name; }, }, Price: { get: function ({ plan }) { if (typeof (plan === null || plan === void 0 ? void 0 : plan.price) === 'undefined') return color_1.default.dim('?'); return (0, util_1.formatPrice)({ price: plan === null || plan === void 0 ? void 0 : plan.price, hourly: true }); }, }, 'Max Price': { get: function ({ plan }) { if (typeof (plan === null || plan === void 0 ? void 0 : plan.price) === 'undefined') return color_1.default.dim('?'); return (0, util_1.formatPrice)({ price: plan === null || plan === void 0 ? void 0 : plan.price, hourly: false }); }, }, State: { get: function ({ state }) { let result = state || ''; switch (state) { case 'provisioned': result = 'created'; break; case 'provisioning': result = 'creating'; break; case 'deprovisioned': result = 'errored'; } return result; }, }, }); } function formatAttachment(attachment, showApp = true) { var _a; const attName = color_1.default.green(attachment.name || ''); const output = [color_1.default.dim('as'), attName]; if (showApp) { const appInfo = `on ${color_1.default.cyan(((_a = attachment.app) === null || _a === void 0 ? void 0 : _a.name) || '')} app`; output.push(color_1.default.dim(appInfo)); } return output.join(' '); } function renderAttachment(attachment, app, isFirst = false) { var _a; const line = isFirst ? '\u2514\u2500' : '\u251C\u2500'; const attName = formatAttachment(attachment, ((_a = attachment.app) === null || _a === void 0 ? void 0 : _a.name) !== app); return printf(' %s %s', color_1.default.dim(line), attName); } exports.renderAttachment = renderAttachment; function displayForApp(app, addons) { if (addons.length === 0) { core_1.ux.log(`No add-ons for app ${app}.`); return; } const isForeignApp = (attOrAddon) => { var _a; return ((_a = attOrAddon.app) === null || _a === void 0 ? void 0 : _a.name) !== app; }; function presentAddon(addon) { var _a; const name = color_1.default.magenta(addon.name || ''); let service = (_a = addon.addon_service) === null || _a === void 0 ? void 0 : _a.name; if (service === undefined) { service = color_1.default.dim('?'); } const addonLine = `${service} (${name})`; const atts = (0, lodash_1.sortBy)(addon.attachments, isForeignApp, 'app.name', 'name'); // render each attachment under the add-on const attLines = atts.map(function (attachment, idx) { const isFirst = (idx === addon.attachments.length - 1); return renderAttachment(attachment, app, isFirst); }); return [addonLine].concat(attLines) .join('\n') + '\n'; // Separate each add-on row by a blank line } addons = (0, lodash_1.sortBy)(addons, isForeignApp, 'plan.name', 'name'); core_1.ux.log(); heroku_cli_util_1.hux.table(addons, { 'Add-on': { get: presentAddon }, Plan: { get: ({ plan }) => plan && plan.name !== undefined ? plan.name.replace(/^[^:]+:/, '') : color_1.default.dim('?'), }, Price: { get: function (addon) { var _a, _b, _c; if (((_a = addon.app) === null || _a === void 0 ? void 0 : _a.name) === app) { return (0, util_1.formatPrice)({ price: (_b = addon.plan) === null || _b === void 0 ? void 0 : _b.price, hourly: true }); } return color_1.default.dim(printf('(billed to %s app)', color_1.default.cyan(((_c = addon.app) === null || _c === void 0 ? void 0 : _c.name) || ''))); }, }, 'Max Price': { get: function (addon) { var _a, _b, _c; if (((_a = addon.app) === null || _a === void 0 ? void 0 : _a.name) === app) { return (0, util_1.formatPrice)({ price: (_b = addon.plan) === null || _b === void 0 ? void 0 : _b.price, hourly: false }); } return color_1.default.dim(printf('(billed to %s app)', color_1.default.cyan(((_c = addon.app) === null || _c === void 0 ? void 0 : _c.name) || ''))); }, }, State: { get: ({ state }) => (0, util_1.formatState)(state || ''), }, }, { // Separate each add-on row by a blank line // printLine: (s: string) => { // ux.log(s) // ux.log('\n') // }, }); core_1.ux.log(`The table above shows ${color_1.default.magenta('add-ons')} and the ${color_1.default.green('attachments')} to the current app (${app}) or other ${color_1.default.cyan('apps')}.\n `); } function displayJSON(addons) { core_1.ux.log(JSON.stringify(addons, null, 2)); } class Addons extends command_1.Command { async run() { const { flags } = await this.parse(Addons); const { app, all, json } = flags; if (!all && app) { const addons = await addonGetter(this.heroku, app); if (json) displayJSON(addons); else displayForApp(app, addons); } else { const addons = await addonGetter(this.heroku); if (json) displayJSON(addons); else displayAll(addons); } } } exports.default = Addons; Addons.topic = topic; Addons.usage = 'addons [--all|--app APP]'; Addons.description = `Lists your add-ons and attachments. The default filter applied depends on whether you are in a Heroku app directory. If so, the --app flag is implied. If not, the default of --all is implied. Explicitly providing either flag overrides the default behavior. `; Addons.flags = { all: command_1.flags.boolean({ char: 'A', description: 'show add-ons and attachments for all accessible apps' }), json: command_1.flags.boolean({ description: 'return add-ons in json format' }), app: command_1.flags.app(), remote: command_1.flags.remote(), }; Addons.examples = [ `$ heroku ${topic} --all`, `$ heroku ${topic} --app acme-inc-www`, ];