heroku
Version:
CLI to interact with Heroku
250 lines (248 loc) • 10.6 kB
JavaScript
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`,
];
;