Skip to content

Commit

Permalink
Fix(pg:promote): returns early and does not promote when no database …
Browse files Browse the repository at this point in the history
…with name exits. (#3002)
  • Loading branch information
justinwilaby authored Sep 3, 2024
1 parent 85f36c5 commit 39b4d08
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 35 deletions.
64 changes: 32 additions & 32 deletions packages/cli/src/commands/pg/promote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,34 @@ export default class Promote extends Command {
ux.action.start(`Ensuring an alternate alias for existing ${color.green('DATABASE_URL')}`)
const {body: attachments} = await this.heroku.get<Heroku.AddOnAttachment[]>(`/apps/${app}/addon-attachments`)
const current = attachments.find(a => a.name === 'DATABASE')
if (!current)
return
// eslint-disable-next-line eqeqeq
if (current.addon?.name === attachment.addon.name && current.namespace == attachment.namespace) {
if (attachment.namespace) {
ux.error(`${color.cyan(attachment.name)} is already promoted on ${color.app(app)}`)
} else {
ux.error(`${color.addon(attachment.addon.name)} is already promoted on ${color.app(app)}`)
if (current) {
// eslint-disable-next-line eqeqeq
if (current.addon?.name === attachment.addon.name && current.namespace == attachment.namespace) {
if (attachment.namespace) {
ux.error(`${color.cyan(attachment.name)} is already promoted on ${color.app(app)}`)
} else {
ux.error(`${color.addon(attachment.addon.name)} is already promoted on ${color.app(app)}`)
}
}
}

const existing = attachments.filter(a => a.addon?.id === current.addon?.id && a.namespace === current.namespace)
.find(a => a.name !== 'DATABASE')
if (existing) {
ux.action.stop(color.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<Heroku.AddOnAttachment>('/addon-attachments', {
body: {
app: {name: app},
addon: {name: current.addon?.name},
namespace: current.namespace,
confirm: app,
},
})
ux.action.stop(color.green(backup.name + '_URL'))
const existing = attachments.filter(a => a.addon?.id === current.addon?.id && a.namespace === current.namespace)
.find(a => a.name !== 'DATABASE')
if (existing) {
ux.action.stop(color.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<Heroku.AddOnAttachment>('/addon-attachments', {
body: {
app: {name: app},
addon: {name: current.addon?.name},
namespace: current.namespace,
confirm: app,
},
})
ux.action.stop(color.green(backup.name + '_URL'))
}
}

if (!force) {
Expand All @@ -66,9 +66,9 @@ export default class Promote extends Command {
if (status['waiting?']) {
ux.error(heredoc(`
Database cannot be promoted while in state: ${status.message}
Promoting this database can lead to application errors and outage. Please run ${color.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.
`))
}
Expand All @@ -92,7 +92,7 @@ export default class Promote extends Command {
},
})
ux.action.stop()
const currentPooler = attachments.find(a => a.namespace === 'connection-pooling:default' && a.addon?.id === current.addon?.id && a.name === 'DATABASE_CONNECTION_POOL')
const currentPooler = attachments.find(a => a.namespace === 'connection-pooling:default' && a.addon?.id === current?.addon?.id && a.name === 'DATABASE_CONNECTION_POOL')
if (currentPooler) {
ux.action.start('Reattaching pooler to new leader')
await this.heroku.post('/addon-attachments', {
Expand All @@ -114,9 +114,9 @@ export default class Promote extends Command {
const unfollowLeaderCmd = `heroku pg:unfollow ${attachment.addon.name}`
ux.warn(heredoc(`
Your database has been promoted but it is currently a follower database in read-only mode.
Promoting a database with ${color.cmd('heroku pg:promote')} doesn't automatically unfollow its leader.
Use ${color.cmd(unfollowLeaderCmd)} to stop this follower from replicating from its leader (${color.yellow(promotedDatabaseDetails.leader as string)}) and convert it into a writable database.
`))
}
Expand Down Expand Up @@ -159,7 +159,7 @@ export default class Promote extends Command {
if (detach && detach.status === 'succeeded') {
msg += 'without an attached DATABASE_URL.'
} else {
msg += `with ${current.addon?.name} attached as DATABASE_URL.`
msg += `with ${current?.addon?.name} attached as DATABASE_URL.`
}

msg += ' Check your release phase logs for failure causes.'
Expand Down
58 changes: 55 additions & 3 deletions packages/cli/test/unit/commands/pg/promote.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {stderr} from 'stdout-stderr'
import {stderr, stdout} from 'stdout-stderr'
import Cmd from '../../../../src/commands/pg/promote'
import runCommand from '../../../helpers/runCommand'
import expectOutput from '../../../helpers/utils/expectOutput'
Expand All @@ -16,8 +16,6 @@ describe('pg:promote when argument is database', function () {
nock('https://api.heroku.com')
.post('/actions/addon-attachments/resolve')
.reply(200, [{addon}])
.get('/apps/myapp/formation')
.reply(200, [])
nock('https://api.data.heroku.com')
.get(`/client/v11/databases/${addon.id}/wait_status`)
.reply(200, {message: 'available', 'waiting?': false})
Expand All @@ -31,6 +29,8 @@ describe('pg:promote when argument is database', function () {

it('promotes db and attaches pgbouncer if DATABASE_CONNECTION_POOL is an attachment', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/formation')
.reply(200, [])
.get('/apps/myapp/addon-attachments').reply(200, [
{
name: 'DATABASE',
Expand Down Expand Up @@ -83,6 +83,8 @@ describe('pg:promote when argument is database', function () {

it('promotes db and does not detach pgbouncers attached to new leader under other name than DATABASE_CONNECTION_POOL', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/formation')
.reply(200, [])
.get('/apps/myapp/addon-attachments')
.reply(200, [
{
Expand Down Expand Up @@ -121,6 +123,8 @@ describe('pg:promote when argument is database', function () {

it('promotes db and does not reattach pgbouncer if DATABASE_CONNECTION_POOL attached to database being promoted, but not old leader', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/formation')
.reply(200, [])
.get('/apps/myapp/addon-attachments')
.reply(200, [
{
Expand Down Expand Up @@ -159,6 +163,8 @@ describe('pg:promote when argument is database', function () {

it('promotes the db and creates another attachment if current DATABASE does not have another', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/formation')
.reply(200, [])
.get('/apps/myapp/addon-attachments')
.reply(200, [
{name: 'DATABASE', addon: {name: 'postgres-2'}, namespace: null},
Expand Down Expand Up @@ -194,6 +200,8 @@ describe('pg:promote when argument is database', function () {

it('promotes the db and does not create another attachment if current DATABASE has another', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/formation')
.reply(200, [])
.get('/apps/myapp/addon-attachments')
.reply(200, [
{
Expand Down Expand Up @@ -231,6 +239,8 @@ describe('pg:promote when argument is database', function () {

it('does not promote the db if is already is DATABASE', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/formation')
.reply(200, [])
.get('/apps/myapp/addon-attachments')
.reply(200, [
{name: 'DATABASE', addon: {name: addon.name}, namespace: null},
Expand All @@ -245,6 +255,48 @@ describe('pg:promote when argument is database', function () {
expect(stripAnsi(error.message)).to.equal(err)
})
})

it('promotes when the db is not a follower and has no DATABASE attachment exists', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/addon-attachments')
.reply(200, [
{name: 'PURPLE', addon: {name: addon.name}, namespace: null},
])
.post('/addon-attachments', {
name: 'DATABASE',
app: {name: 'myapp'},
addon: {name: addon.name},
namespace: null,
confirm: 'myapp',
})
.reply(201)
.get('/apps/myapp/formation')
.reply(200, [
{type: 'release'},
])
.get('/apps/myapp/releases')
.reply(200, [
{description: 'Attach DATABASE', id: 1},
{description: 'Detach DATABASE', id: 2},
])
.get('/apps/myapp/releases/1')
.reply(200, {status: 'succeeded'})
.get('/apps/myapp/releases/2')
.reply(200, {status: 'succeeded'})

await runCommand(Cmd, [
'--app',
'myapp',
'dwh-db',
])
expectOutput(stderr.output, heredoc(`
Ensuring an alternate alias for existing DATABASE_URL...
Promoting dwh-db to DATABASE_URL on ⬢ myapp...
Promoting dwh-db to DATABASE_URL on ⬢ myapp... done
Checking release phase...
Checking release phase... pg:promote succeeded.
`))
})
})

describe('pg:promote when argument is a credential attachment', function () {
Expand Down

0 comments on commit 39b4d08

Please sign in to comment.