diff --git a/src/commands/data/pg/upgrade/run.ts b/src/commands/data/pg/upgrade/run.ts new file mode 100644 index 0000000000..89777d9c1d --- /dev/null +++ b/src/commands/data/pg/upgrade/run.ts @@ -0,0 +1,77 @@ +import {color, hux, utils} from '@heroku/heroku-cli-util' +import {flags as Flags} from '@heroku-cli/command' +import {Args, ux} from '@oclif/core' +import tsheredoc from 'tsheredoc' + +import BaseCommand from '../../../../lib/data/baseCommand.js' +import {InfoResponse, UpgradeResponse} from '../../../../lib/data/types.js' + +const heredoc = tsheredoc.default + +export default class DataPgUpgradeRun extends BaseCommand { + static args = { + database: Args.string({ + description: 'database name, database attachment name, or related config var on an app', + required: true, + }), + } + + static description = 'upgrade the Postgres version on a Heroku Postgres Advanced database' + + static examples = [ + heredoc` + # Upgrade a Heroku Postgres Advanced database to version 17 + <%= config.bin %> <%= command.id %> DATABASE --version 17 --app my-app + `, + ] + + static flags = { + app: Flags.app({required: true}), + confirm: Flags.string({char: 'c', description: 'pass in the app name to skip confirmation prompts'}), + remote: Flags.remote(), + version: Flags.string({char: 'v', description: 'Postgres version to upgrade to'}), + } + + public async run(): Promise { + const {args, flags} = await this.parse(DataPgUpgradeRun) + const {app, confirm, version} = flags + const {database} = args + + const dbResolver = new utils.pg.DatabaseResolver(this.heroku) + const {addon} = await dbResolver.getAttachment(app, database) + + if (!utils.pg.isAdvancedDatabase(addon)) { + ux.error( + 'You can only use this command on Advanced-tier databases.\n' + + `Run ${color.code(`heroku pg:upgrade:run ${addon.name} --app ${app}`)} instead.`, + ) + } + + const {body: databaseInfo} = await this.dataApi.get(`/data/postgres/v1/${addon.id}/info`) + const {version: currentVersion} = databaseInfo + const newVersion = version ?? 'the latest supported Postgres version' + await hux.confirmCommand({ + comparison: app, + confirmation: confirm, + warningMessage: heredoc(` + This command immediately upgrades your ${color.datastore(addon.name)} database from ${currentVersion} to ${newVersion}. + Your database will be unavailable until the upgrade is complete.`), + }) + + try { + ux.action.start(`Upgrading your ${color.datastore(addon.name)} database from ${currentVersion} to ${newVersion}`) + const {body: {message}} = await this.dataApi.post( + `/data/postgres/v1/${addon.id}/upgrade/run`, + {body: {version}}, + ) + ux.action.stop(heredoc(` + done + + ${color.info(message)} + `)) + } catch (error: unknown) { + ux.action.stop(color.red('!')) + throw error + } + } +} diff --git a/src/lib/data/types.ts b/src/lib/data/types.ts index 897a699d06..9e7b27f626 100644 --- a/src/lib/data/types.ts +++ b/src/lib/data/types.ts @@ -263,3 +263,7 @@ export enum MaintenanceStatus { ready = 'ready', running = 'running', } + +export type UpgradeResponse = { + message: string +} diff --git a/test/unit/commands/data/pg/upgrade/run.unit.test.ts b/test/unit/commands/data/pg/upgrade/run.unit.test.ts new file mode 100644 index 0000000000..715ee89361 --- /dev/null +++ b/test/unit/commands/data/pg/upgrade/run.unit.test.ts @@ -0,0 +1,118 @@ +import ansis from 'ansis' +import {expect} from 'chai' +import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' +import tsheredoc from 'tsheredoc' + +import DataPgUpgradeRun from '../../../../../../src/commands/data/pg/upgrade/run.js' +import { + advancedAddonAttachment, + nonAdvancedAddonAttachment, + pgInfo, +} from '../../../../../fixtures/data/pg/fixtures.js' +import runCommand from '../../../../../helpers/runCommand.js' + +const heredoc = tsheredoc.default + +describe('data:pg:upgrade:run', function () { + it('upgrades an advanced database to the latest version', async function () { + const resolveApi = nock('https://api.heroku.com') + .post('/actions/addon-attachments/resolve') + .reply(200, [advancedAddonAttachment]) + const {addon, name: attachmentName} = advancedAddonAttachment + const dataApi = nock('https://api.data.heroku.com') + .get(`/data/postgres/v1/${addon.id}/info`) + .reply(200, {...pgInfo, version: '16.10'}) + .post(`/data/postgres/v1/${addon.id}/upgrade/run`, {}) + .reply(200, {message: 'Upgrade started. Monitor progress with heroku data:pg:info.'}) + + await runCommand(DataPgUpgradeRun, [ + attachmentName, + '--app=myapp', + '--confirm=myapp', + ]) + + resolveApi.done() + dataApi.done() + + expect(ansis.strip(stderr.output)).to.equal( + heredoc(` + Upgrading your ⛁ ${addon.name} database from 16.10 to the latest supported Postgres version... done + + Upgrade started. Monitor progress with heroku data:pg:info. + + `), + ) + expect(stdout.output).to.equal('') + }) + + it('upgrades an advanced database to a specific version with --version', async function () { + const resolveApi = nock('https://api.heroku.com') + .post('/actions/addon-attachments/resolve') + .reply(200, [advancedAddonAttachment]) + const {addon, name: attachmentName} = advancedAddonAttachment + const dataApi = nock('https://api.data.heroku.com') + .get(`/data/postgres/v1/${addon.id}/info`) + .reply(200, {...pgInfo, version: '16.10'}) + .post(`/data/postgres/v1/${addon.id}/upgrade/run`, {version: '17.5'}) + .reply(200, {message: 'Upgrade to 17.5 started.'}) + + await runCommand(DataPgUpgradeRun, [ + attachmentName, + '--app=myapp', + '--confirm=myapp', + '--version=17.5', + ]) + + resolveApi.done() + dataApi.done() + + expect(ansis.strip(stderr.output)).to.include('from 16.10 to 17.5') + expect(ansis.strip(stderr.output)).to.include('Upgrade to 17.5 started.') + }) + + it('errors if database is not Advanced-tier', async function () { + const resolveApi = nock('https://api.heroku.com') + .post('/actions/addon-attachments/resolve') + .reply(200, [nonAdvancedAddonAttachment]) + const {addon, name: attachmentName} = nonAdvancedAddonAttachment + + try { + await runCommand(DataPgUpgradeRun, [ + attachmentName, + '--app=myapp', + '--confirm=myapp', + ]) + } catch (error: unknown) { + resolveApi.done() + expect(ansis.strip((error as Error).message)).to.equal( + 'You can only use this command on Advanced-tier databases.\n' + + `Run heroku pg:upgrade:run ${addon.name} --app myapp instead.`, + ) + } + }) + + it('displays the correct error when the upgrade API fails', async function () { + const resolveApi = nock('https://api.heroku.com') + .post('/actions/addon-attachments/resolve') + .reply(200, [advancedAddonAttachment]) + const {addon, name: attachmentName} = advancedAddonAttachment + const dataApi = nock('https://api.data.heroku.com') + .get(`/data/postgres/v1/${addon.id}/info`) + .reply(200, {...pgInfo, version: '16.10'}) + .post(`/data/postgres/v1/${addon.id}/upgrade/run`) + .reply(422, {message: 'Database is not ready for upgrade.'}) + + try { + await runCommand(DataPgUpgradeRun, [ + attachmentName, + '--app=myapp', + '--confirm=myapp', + ]) + } catch (error: unknown) { + resolveApi.done() + dataApi.done() + expect(ansis.strip((error as Error).message)).to.include('Database is not ready for upgrade.') + } + }) +})