Skip to content

Commit d6cc8af

Browse files
authored
feat: create pg:upgrade:cancel command (#3267)
2 parents 97798e2 + bf1818d commit d6cc8af

File tree

2 files changed

+259
-0
lines changed

2 files changed

+259
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
cancels a scheduled upgrade. You can't cancel a version upgrade that's in progress.
16+
`)
17+
18+
static flags = {
19+
confirm: flags.string({char: 'c'}),
20+
app: flags.app({required: true}),
21+
}
22+
23+
static args = {
24+
database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}),
25+
}
26+
27+
public async run(): Promise<void> {
28+
const {flags, args} = await this.parse(Upgrade)
29+
const {app, confirm} = flags
30+
const {database} = args
31+
32+
const db = await getAddon(this.heroku, app, database)
33+
if (legacyEssentialPlan(db))
34+
ux.error(`You can only use ${color.cmd('pg:upgrade:*')} commands on Essential-* and higher plans.`)
35+
36+
if (essentialNumPlan(db))
37+
ux.error(`You can't use ${color.cmd('pg:upgrade:cancel')} on Essential-tier databases. You can only use this command on Standard-tier and higher leader databases.`)
38+
39+
const {body: replica} = await this.heroku.get<PgDatabase>(`/client/v11/databases/${db.id}`, {hostname: pgHost()})
40+
if (replica.following)
41+
ux.error(`You can't use ${color.cmd('pg:upgrade:cancel')} on follower databases. You can only use this command on Standard-tier and higher leader databases.`)
42+
43+
await confirmCommand(app, confirm, heredoc(`
44+
Destructive action
45+
You're canceling the scheduled version upgrade for ${color.addon(db.name)}.
46+
47+
You can't undo this action.
48+
`))
49+
50+
try {
51+
ux.action.start(`Cancelling upgrade on ${color.addon(db.name)}`)
52+
const response = await this.heroku.post<PgUpgradeResponse>(`/client/v11/databases/${db.id}/upgrade/cancel`, {hostname: pgHost(), body: {}})
53+
ux.action.stop('done\n' + formatResponseWithCommands(response.body.message))
54+
} catch (error) {
55+
const response = error as PgUpgradeError
56+
ux.error(formatResponseWithCommands(response.body.message) + `\n\nError ID: ${response.body.id}`)
57+
}
58+
}
59+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import {stderr} from 'stdout-stderr'
2+
import Cmd from '../../../../../src/commands/pg/upgrade/cancel'
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+
13+
const stripAnsi = require('strip-ansi')
14+
15+
describe('pg:upgrade:cancel', function () {
16+
const addon = fixtures.addons['dwh-db']
17+
let uxWarnStub: sinon.SinonStub
18+
let uxPromptStub: sinon.SinonStub
19+
20+
before(function () {
21+
uxWarnStub = sinon.stub(ux, 'warn')
22+
uxPromptStub = sinon.stub(ux, 'prompt').resolves('myapp')
23+
})
24+
25+
beforeEach(async function () {
26+
uxWarnStub.resetHistory()
27+
uxPromptStub.resetHistory()
28+
})
29+
30+
afterEach(async function () {
31+
nock.cleanAll()
32+
})
33+
34+
after(function () {
35+
uxWarnStub.restore()
36+
uxPromptStub.restore()
37+
})
38+
39+
it('refuses to cancel upgrade on legacy essential dbs', async function () {
40+
const hobbyAddon = fixtures.addons['www-db']
41+
42+
nock('https://api.heroku.com')
43+
.post('/actions/addon-attachments/resolve')
44+
.reply(200, [{addon: hobbyAddon}])
45+
await runCommand(Cmd, [
46+
'--app',
47+
'myapp',
48+
'--confirm',
49+
'myapp',
50+
]).catch(error => {
51+
expectOutput(error.message, heredoc(`
52+
You can only use ${color.cmd('pg:upgrade:*')} commands on Essential-* and higher plans.
53+
`))
54+
})
55+
})
56+
57+
it('refuses to cancel upgrade on essential tier dbs', async function () {
58+
const essentialAddon = {
59+
name: 'postgres-1', plan: {name: 'heroku-postgresql:essential-0'},
60+
}
61+
62+
nock('https://api.heroku.com')
63+
.post('/actions/addon-attachments/resolve')
64+
.reply(200, [{addon: essentialAddon}])
65+
66+
await runCommand(Cmd, [
67+
'--app',
68+
'myapp',
69+
'--confirm',
70+
'myapp',
71+
]).catch(error => {
72+
expect(error.message).to.equal(`You can't use ${color.cmd('pg:upgrade:cancel')} on Essential-tier databases. You can only use this command on Standard-tier and higher leader databases.`)
73+
})
74+
})
75+
76+
it('refuses to cancel upgrade on follower dbs', async function () {
77+
nock('https://api.heroku.com')
78+
.post('/actions/addon-attachments/resolve')
79+
.reply(200, [{addon: addon}])
80+
nock('https://api.data.heroku.com')
81+
.get(`/client/v11/databases/${addon.id}`)
82+
.reply(200, {
83+
following: 'postgres://xxx.com:5432/abcdefghijklmn',
84+
leader: {
85+
addon_id: '5ba2ba8b-07a9-4a65-a808-585a50e37f98',
86+
name: 'postgresql-leader',
87+
},
88+
})
89+
await runCommand(Cmd, [
90+
'--app',
91+
'myapp',
92+
'--confirm',
93+
'myapp',
94+
]).catch(error => {
95+
expectOutput(error.message, heredoc(`
96+
You can't use ${color.cmd('pg:upgrade:cancel')} on follower databases. You can only use this command on Standard-tier and higher leader databases.
97+
`))
98+
})
99+
})
100+
101+
it('cancels upgrade on a leader db', async function () {
102+
nock('https://api.heroku.com')
103+
.post('/actions/addon-attachments/resolve')
104+
.reply(200, [{addon}])
105+
nock('https://api.heroku.com')
106+
.get('/apps/myapp/config-vars')
107+
.reply(200, {DATABASE_URL: 'postgres://db1'})
108+
nock('https://api.data.heroku.com')
109+
.get(`/client/v11/databases/${addon.id}`)
110+
.reply(200)
111+
nock('https://api.data.heroku.com')
112+
.post(`/client/v11/databases/${addon.id}/upgrade/cancel`)
113+
.reply(200, {message: 'You canceled the upgrade.'})
114+
115+
const message = heredoc(`
116+
Destructive action
117+
You're canceling the scheduled version upgrade for ${addon.name}.
118+
119+
You can't undo this action.
120+
`)
121+
122+
await runCommand(Cmd, [
123+
'--app',
124+
'myapp',
125+
])
126+
127+
expect(stripAnsi(uxPromptStub.args[0].toString())).contains('To proceed, type myapp')
128+
expect(stripAnsi(uxWarnStub.args[0].toString())).to.eq(message)
129+
130+
expectOutput(stderr.output, heredoc(`
131+
Cancelling upgrade on ${addon.name}...
132+
Cancelling upgrade on ${addon.name}... done
133+
You canceled the upgrade.
134+
`))
135+
})
136+
137+
it('errors when there is no upgrade prepared', async function () {
138+
nock('https://api.heroku.com')
139+
.post('/actions/addon-attachments/resolve')
140+
.reply(200, [{addon}])
141+
nock('https://api.heroku.com')
142+
.get('/apps/myapp/config-vars')
143+
.reply(200, {DATABASE_URL: 'postgres://db1'})
144+
nock('https://api.data.heroku.com')
145+
.get(`/client/v11/databases/${addon.id}`)
146+
.reply(200)
147+
nock('https://api.data.heroku.com')
148+
.post(`/client/v11/databases/${addon.id}/upgrade/cancel`)
149+
.reply(422, {id: 'bad_request', message: "You haven't scheduled an upgrade on your database. Run `pg:upgrade:prepare` to schedule an upgrade."})
150+
151+
await runCommand(Cmd, [
152+
'--app',
153+
'myapp',
154+
'--confirm',
155+
'myapp',
156+
]).catch(error => {
157+
expectOutput(error.message, heredoc(`
158+
You haven't scheduled an upgrade on your database. Run ${color.cmd('pg:upgrade:prepare')} to schedule an upgrade.
159+
160+
Error ID: bad_request
161+
`))
162+
163+
expectOutput(stderr.output, heredoc(`
164+
Cancelling upgrade on ${addon.name}...
165+
`))
166+
})
167+
})
168+
169+
it('errors when upgrade is not cancelable', 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/cancel`)
181+
.reply(422, {id: 'bad_request', message: "You can't cancel the upgrade because it's currently in progress."})
182+
183+
await runCommand(Cmd, [
184+
'--app',
185+
'myapp',
186+
'--confirm',
187+
'myapp',
188+
]).catch(error => {
189+
expectOutput(error.message, heredoc(`
190+
You can't cancel the upgrade because it's currently in progress.
191+
192+
Error ID: bad_request
193+
`))
194+
195+
expectOutput(stderr.output, heredoc(`
196+
Cancelling upgrade on ${addon.name}...
197+
`))
198+
})
199+
})
200+
})

0 commit comments

Comments
 (0)