UNPKG

heroku

Version:

CLI to interact with Heroku

171 lines (166 loc) 9.44 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /* eslint-disable complexity */ const color_1 = require("@heroku-cli/color"); const command_1 = require("@heroku-cli/command"); const core_1 = require("@oclif/core"); const tsheredoc_1 = require("tsheredoc"); const fetcher_1 = require("../../lib/pg/fetcher"); const host_1 = require("../../lib/pg/host"); const nls_1 = require("../../nls"); class Promote extends command_1.Command { async run() { var _a, _b, _c; const { flags, args } = await this.parse(Promote); const { force, app } = flags; const { database } = args; const attachment = await (0, fetcher_1.getAttachment)(this.heroku, app, database); core_1.ux.action.start(`Ensuring an alternate alias for existing ${color_1.default.green('DATABASE_URL')}`); const { body: attachments } = await this.heroku.get(`/apps/${app}/addon-attachments`); const current = attachments.find(a => a.name === 'DATABASE'); if (current) { // eslint-disable-next-line eqeqeq if (((_a = current.addon) === null || _a === void 0 ? void 0 : _a.name) === attachment.addon.name && current.namespace == attachment.namespace) { if (attachment.namespace) { core_1.ux.error(`${color_1.default.cyan(attachment.name)} is already promoted on ${color_1.default.app(app)}`); } else { core_1.ux.error(`${color_1.default.addon(attachment.addon.name)} is already promoted on ${color_1.default.app(app)}`); } } const existing = attachments.filter(a => { var _a, _b; return ((_a = a.addon) === null || _a === void 0 ? void 0 : _a.id) === ((_b = current.addon) === null || _b === void 0 ? void 0 : _b.id) && a.namespace === current.namespace; }) .find(a => a.name !== 'DATABASE'); if (existing) { core_1.ux.action.stop(color_1.default.green(existing.name + '_URL')); } else { // The current add-on occupying the DATABASE attachment has no // other attachments. In order to promote this database without // error, we can create a secondary attachment, just-in-time. const { body: backup } = await this.heroku.post('/addon-attachments', { body: { app: { name: app }, addon: { name: (_b = current.addon) === null || _b === void 0 ? void 0 : _b.name }, namespace: current.namespace, confirm: app, }, }); core_1.ux.action.stop(color_1.default.green(backup.name + '_URL')); } } if (!force) { const { body: status } = await this.heroku.get(`/client/v11/databases/${attachment.addon.id}/wait_status`, { hostname: (0, host_1.default)(), }); if (status['waiting?']) { core_1.ux.error((0, tsheredoc_1.default)(` Database cannot be promoted while in state: ${status.message} Promoting this database can lead to application errors and outage. Please run ${color_1.default.cmd('heroku pg:wait')} to wait for database to become available. To ignore this error, you can pass the --force flag to promote the database and risk application issues. `)); } } let promotionMessage; if (attachment.namespace) { promotionMessage = `Promoting ${color_1.default.cyan(attachment.name)} to ${color_1.default.green('DATABASE_URL')} on ${color_1.default.app(app)}`; } else { promotionMessage = `Promoting ${color_1.default.addon(attachment.addon.name)} to ${color_1.default.green('DATABASE_URL')} on ${color_1.default.app(app)}`; } core_1.ux.action.start(promotionMessage); await this.heroku.post('/addon-attachments', { body: { name: 'DATABASE', app: { name: app }, addon: { name: attachment.addon.name }, namespace: attachment.namespace || null, confirm: app, }, }); core_1.ux.action.stop(); const currentPooler = attachments.find(a => { var _a, _b; return a.namespace === 'connection-pooling:default' && ((_a = a.addon) === null || _a === void 0 ? void 0 : _a.id) === ((_b = current === null || current === void 0 ? void 0 : current.addon) === null || _b === void 0 ? void 0 : _b.id) && a.name === 'DATABASE_CONNECTION_POOL'; }); if (currentPooler) { core_1.ux.action.start('Reattaching pooler to new leader'); await this.heroku.post('/addon-attachments', { body: { name: currentPooler.name, app: { name: app }, addon: { name: attachment.addon.name }, namespace: 'connection-pooling:default', confirm: app, }, }); core_1.ux.action.stop(); } const { body: promotedDatabaseDetails } = await this.heroku.get(`/client/v11/databases/${attachment.addon.id}`, { hostname: (0, host_1.default)(), }); if (promotedDatabaseDetails.following) { const unfollowLeaderCmd = `heroku pg:unfollow ${attachment.addon.name}`; core_1.ux.warn((0, tsheredoc_1.default)(` Your database has been promoted but it is currently a follower database in read-only mode. Promoting a database with ${color_1.default.cmd('heroku pg:promote')} doesn't automatically unfollow its leader. Use ${color_1.default.cmd(unfollowLeaderCmd)} to stop this follower from replicating from its leader (${color_1.default.yellow(promotedDatabaseDetails.leader)}) and convert it into a writable database. `)); } const { body: formation } = await this.heroku.get(`/apps/${app}/formation`); const releasePhase = formation.find(process => process.type === 'release'); if (releasePhase) { core_1.ux.action.start('Checking release phase'); const { body: releases } = await this.heroku.get(`/apps/${app}/releases`, { partial: true, headers: { Range: 'version ..; max=5, order=desc', }, }); const attach = releases.find(release => { var _a; return (_a = release.description) === null || _a === void 0 ? void 0 : _a.includes('Attach DATABASE'); }); const detach = releases.find(release => { var _a; return (_a = release.description) === null || _a === void 0 ? void 0 : _a.includes('Detach DATABASE'); }); if (!attach || !detach) { core_1.ux.error('Unable to check release phase. Check your Attach DATABASE release for failures.'); } const endTime = Date.now() + 900000; // 15 minutes from now const [attachId, detachId] = [attach === null || attach === void 0 ? void 0 : attach.id, detach === null || detach === void 0 ? void 0 : detach.id]; while (true) { const attach = await (0, fetcher_1.getRelease)(this.heroku, app, attachId); if (attach && attach.status === 'succeeded') { let msg = 'pg:promote succeeded.'; const detach = await (0, fetcher_1.getRelease)(this.heroku, app, detachId); if (detach && detach.status === 'failed') { msg += ` It is safe to ignore the failed ${detach.description} release.`; } core_1.ux.action.stop(msg); return; } if (attach && attach.status === 'failed') { let msg = `pg:promote failed because ${attach.description} release was unsuccessful. Your application is currently running `; const detach = await (0, fetcher_1.getRelease)(this.heroku, app, detachId); if (detach && detach.status === 'succeeded') { msg += 'without an attached DATABASE_URL.'; } else { msg += `with ${(_c = current === null || current === void 0 ? void 0 : current.addon) === null || _c === void 0 ? void 0 : _c.name} attached as DATABASE_URL.`; } msg += ' Check your release phase logs for failure causes.'; core_1.ux.action.stop(msg); return; } if (Date.now() > endTime) { core_1.ux.action.stop('timeout. Check your Attach DATABASE release for failures.'); return; } await new Promise(resolve => setTimeout(resolve, 5000)); } } } } exports.default = Promote; Promote.topic = 'pg'; Promote.description = 'sets DATABASE as your DATABASE_URL'; Promote.flags = { force: command_1.flags.boolean({ char: 'f' }), app: command_1.flags.app({ required: true }), remote: command_1.flags.remote(), }; Promote.args = { database: core_1.Args.string({ required: true, description: (0, nls_1.nls)('pg:database:arg:description') }), };