Skip to content

Commit 78a5f72

Browse files
authored
feat: create pg:upgrade:dryrun command (#3271)
2 parents d6cc8af + e9472e1 commit 78a5f72

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

cspell-dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ direwolf
6767
displayer
6868
dockerfiles
6969
dropdb
70+
dryrun
7071
DVCS
7172
dxxxxxxxxxxxx
7273
dyno
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import color from '@heroku-cli/color'
2+
import {Command, flags} from '@heroku-cli/command'
3+
import {Args, ux} from '@oclif/core'
4+
import heredoc from 'tsheredoc'
5+
import {getAddon} from '../../../lib/pg/fetcher'
6+
import pgHost from '../../../lib/pg/host'
7+
import {legacyEssentialPlan, essentialNumPlan, formatResponseWithCommands} from '../../../lib/pg/util'
8+
import {PgDatabase, PgUpgradeError, PgUpgradeResponse} from '../../../lib/pg/types'
9+
import confirmCommand from '../../../lib/confirmCommand'
10+
import {nls} from '../../../nls'
11+
12+
export default class Upgrade extends Command {
13+
static topic = 'pg';
14+
static description = heredoc(`
15+
simulates a Postgres version upgrade on a Standard-tier and higher leader database by creating and upgrading a follower database.
16+
Heroku sends the results of the test upgrade via email.
17+
`)
18+
19+
static flags = {
20+
confirm: flags.string({char: 'c'}),
21+
version: flags.string({char: 'v', description: 'Postgres version to upgrade to'}),
22+
app: flags.app({required: true}),
23+
}
24+
25+
static args = {
26+
database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}),
27+
}
28+
29+
public async run(): Promise<void> {
30+
const {flags, args} = await this.parse(Upgrade)
31+
const {app, version, confirm} = flags
32+
const {database} = args
33+
34+
const db = await getAddon(this.heroku, app, database)
35+
if (legacyEssentialPlan(db))
36+
ux.error(`You can only use ${color.cmd('pg:upgrade:*')} commands on Essential-* and higher plans.`)
37+
38+
if (essentialNumPlan(db))
39+
ux.error(`You can't use ${color.cmd('pg:upgrade:dryrun')} on Essential-tier databases. You can only use this command on Standard-tier and higher leader databases.`)
40+
41+
const versionPhrase = version ? heredoc(`Postgres version ${version}`) : heredoc('the latest supported Postgres version')
42+
const {body: replica} = await this.heroku.get<PgDatabase>(`/client/v11/databases/${db.id}`, {hostname: pgHost()})
43+
if (replica.following)
44+
ux.error(`You can't use ${color.cmd('pg:upgrade:dryrun')} on follower databases. You can only use this command on Standard-tier and higher leader databases.`)
45+
46+
await confirmCommand(app, confirm, heredoc(`
47+
Destructive action
48+
This command starts a test upgrade for ${color.addon(db.name)} to ${versionPhrase}.
49+
`))
50+
51+
try {
52+
const data = {version}
53+
ux.action.start(`Starting a test upgrade on ${color.addon(db.name)}`)
54+
const response = await this.heroku.post<PgUpgradeResponse>(`/client/v11/databases/${db.id}/upgrade/dry_run`, {hostname: pgHost(), body: data})
55+
ux.action.stop('done\n' + formatResponseWithCommands(response.body.message))
56+
} catch (error) {
57+
const response = error as PgUpgradeError
58+
ux.error(formatResponseWithCommands(response.body.message) + `\n\nError ID: ${response.body.id}`)
59+
}
60+
}
61+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import {stderr} from 'stdout-stderr'
2+
import Cmd from '../../../../../src/commands/pg/upgrade/dryrun'
3+
import runCommand from '../../../../helpers/runCommand'
4+
import expectOutput from '../../../../helpers/utils/expectOutput'
5+
import {expect} from 'chai'
6+
import * as nock from 'nock'
7+
import heredoc from 'tsheredoc'
8+
import * as fixtures from '../../../../fixtures/addons/fixtures'
9+
import color from '@heroku-cli/color'
10+
import {ux} from '@oclif/core'
11+
import * as sinon from 'sinon'
12+
const stripAnsi = require('strip-ansi')
13+
14+
describe('pg:upgrade:dryrun', function () {
15+
const addon = fixtures.addons['dwh-db']
16+
let uxWarnStub: sinon.SinonStub
17+
let uxPromptStub: sinon.SinonStub
18+
19+
before(function () {
20+
uxWarnStub = sinon.stub(ux, 'warn')
21+
uxPromptStub = sinon.stub(ux, 'prompt').resolves('myapp')
22+
})
23+
24+
beforeEach(async function () {
25+
uxWarnStub.resetHistory()
26+
uxPromptStub.resetHistory()
27+
})
28+
29+
afterEach(async function () {
30+
nock.cleanAll()
31+
})
32+
33+
after(function () {
34+
uxWarnStub.restore()
35+
uxPromptStub.restore()
36+
})
37+
38+
it('refuses to start test upgrade on legacy essential dbs', async function () {
39+
const hobbyAddon = fixtures.addons['www-db']
40+
41+
nock('https://api.heroku.com')
42+
.post('/actions/addon-attachments/resolve')
43+
.reply(200, [{addon: hobbyAddon}])
44+
await runCommand(Cmd, [
45+
'--app',
46+
'myapp',
47+
'--confirm',
48+
'myapp',
49+
]).catch(error => {
50+
expectOutput(error.message, heredoc(`
51+
You can only use ${color.cmd('pg:upgrade:*')} commands on Essential-* and higher plans.
52+
`))
53+
})
54+
})
55+
56+
it('refuses to start test upgrade on essential tier dbs', async function () {
57+
const essentialAddon = {
58+
name: 'postgres-1', plan: {name: 'heroku-postgresql:essential-0'},
59+
}
60+
61+
nock('https://api.heroku.com')
62+
.post('/actions/addon-attachments/resolve')
63+
.reply(200, [{addon: essentialAddon}])
64+
65+
await runCommand(Cmd, [
66+
'--app',
67+
'myapp',
68+
'--confirm',
69+
'myapp',
70+
]).catch(error => {
71+
expect(error.message).to.equal(`You can't use ${color.cmd('pg:upgrade:dryrun')} on Essential-tier databases. You can only use this command on Standard-tier and higher leader databases.`)
72+
})
73+
})
74+
75+
it('refuses to start test upgrade on follower dbs', async function () {
76+
nock('https://api.heroku.com')
77+
.post('/actions/addon-attachments/resolve')
78+
.reply(200, [{addon: addon}])
79+
nock('https://api.data.heroku.com')
80+
.get(`/client/v11/databases/${addon.id}`)
81+
.reply(200, {
82+
following: 'postgres://xxx.com:5432/abcdefghijklmn',
83+
leader: {
84+
addon_id: '5ba2ba8b-07a9-4a65-a808-585a50e37f98',
85+
name: 'postgresql-leader',
86+
},
87+
})
88+
await runCommand(Cmd, [
89+
'--app',
90+
'myapp',
91+
'--confirm',
92+
'myapp',
93+
]).catch(error => {
94+
expectOutput(error.message, heredoc(`
95+
You can't use ${color.cmd('pg:upgrade:dryrun')} on follower databases. You can only use this command on Standard-tier and higher leader databases.`))
96+
})
97+
})
98+
99+
it('starts test upgrade on a db with version flag', async function () {
100+
nock('https://api.heroku.com')
101+
.post('/actions/addon-attachments/resolve')
102+
.reply(200, [{addon}])
103+
nock('https://api.heroku.com')
104+
.get('/apps/myapp/config-vars')
105+
.reply(200, {DATABASE_URL: 'postgres://db1'})
106+
nock('https://api.data.heroku.com')
107+
.get(`/client/v11/databases/${addon.id}`)
108+
.reply(200)
109+
nock('https://api.data.heroku.com')
110+
.post(`/client/v11/databases/${addon.id}/upgrade/dry_run`)
111+
.reply(200, {message: "Started test upgrade. We'll notify you via email when it's complete."})
112+
113+
const message = heredoc(`
114+
Destructive action
115+
This command starts a test upgrade for ${addon.name} to Postgres version 15.
116+
`)
117+
118+
await runCommand(Cmd, [
119+
'--app',
120+
'myapp',
121+
'--version',
122+
'15',
123+
])
124+
125+
expect(stripAnsi(uxPromptStub.args[0].toString())).contains('To proceed, type myapp')
126+
expect(stripAnsi(uxWarnStub.args[0].toString())).to.eq(message)
127+
128+
expectOutput(stderr.output, heredoc(`
129+
Starting a test upgrade on ${addon.name}...
130+
Starting a test upgrade on ${addon.name}... done
131+
Started test upgrade. We'll notify you via email when it's complete.
132+
`))
133+
})
134+
135+
it('starts test upgrade on a db without a version flag', async function () {
136+
nock('https://api.heroku.com')
137+
.post('/actions/addon-attachments/resolve')
138+
.reply(200, [{addon}])
139+
nock('https://api.heroku.com')
140+
.get('/apps/myapp/config-vars')
141+
.reply(200, {DATABASE_URL: 'postgres://db1'})
142+
nock('https://api.data.heroku.com')
143+
.get(`/client/v11/databases/${addon.id}`)
144+
.reply(200)
145+
nock('https://api.data.heroku.com')
146+
.post(`/client/v11/databases/${addon.id}/upgrade/dry_run`)
147+
.reply(200, {message: "Started test upgrade. We'll notify you via email when it's complete."})
148+
149+
const message = heredoc(`
150+
Destructive action
151+
This command starts a test upgrade for ${addon.name} to the latest supported Postgres version.
152+
`)
153+
154+
await runCommand(Cmd, [
155+
'--app',
156+
'myapp',
157+
])
158+
159+
expect(stripAnsi(uxPromptStub.args[0].toString())).contains('To proceed, type myapp')
160+
expect(stripAnsi(uxWarnStub.args[0].toString())).to.eq(message)
161+
162+
expectOutput(stderr.output, heredoc(`
163+
Starting a test upgrade on ${addon.name}...
164+
Starting a test upgrade on ${addon.name}... done
165+
Started test upgrade. We'll notify you via email when it's complete.
166+
`))
167+
})
168+
169+
it('errors if version upgrade task is running', async function () {
170+
nock('https://api.heroku.com')
171+
.post('/actions/addon-attachments/resolve')
172+
.reply(200, [{addon}])
173+
nock('https://api.heroku.com')
174+
.get('/apps/myapp/config-vars')
175+
.reply(200, {DATABASE_URL: 'postgres://db1'})
176+
nock('https://api.data.heroku.com')
177+
.get(`/client/v11/databases/${addon.id}`)
178+
.reply(200)
179+
nock('https://api.data.heroku.com')
180+
.post(`/client/v11/databases/${addon.id}/upgrade/dry_run`)
181+
.reply(422, {id: 'unprocessable_entity', message: 'database is in the middle of a version upgrade. To perform this action, wait until the upgrade is complete and try again.'})
182+
183+
await runCommand(Cmd, [
184+
'--app',
185+
'myapp',
186+
'--confirm',
187+
'myapp',
188+
]).catch(error => {
189+
expect(error.message).to.equal(heredoc(`
190+
database is in the middle of a version upgrade. To perform this action, wait until the upgrade is complete and try again.
191+
192+
Error ID: unprocessable_entity`))
193+
})
194+
})
195+
})

0 commit comments

Comments
 (0)