1 | 'use strict'
|
2 |
|
3 | const cli = require('heroku-cli-util')
|
4 | const host = require('../lib/host')
|
5 |
|
6 | async function run(context, heroku) {
|
7 | const fetcher = require('../lib/fetcher')(heroku)
|
8 | const { app, args, flags } = context
|
9 | const { force } = flags
|
10 | const attachment = await fetcher.attachment(app, args.database)
|
11 |
|
12 | let current
|
13 | let attachments
|
14 |
|
15 | await cli.action(`Ensuring an alternate alias for existing ${cli.color.configVar('DATABASE_URL')}`, async function () {
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 | attachments = await heroku.get(`/apps/${app}/addon-attachments`)
|
23 | current = attachments.find(a => a.name === 'DATABASE')
|
24 | if (!current) return
|
25 |
|
26 | if (current.addon.name === attachment.addon.name && current.namespace === attachment.namespace) {
|
27 | if (attachment.namespace) {
|
28 | throw new Error(`${cli.color.attachment(attachment.name)} is already promoted on ${cli.color.app(app)}`)
|
29 | } else {
|
30 | throw new Error(`${cli.color.addon(attachment.addon.name)} is already promoted on ${cli.color.app(app)}`)
|
31 | }
|
32 | }
|
33 | let existing = attachments.filter(a => a.addon.id === current.addon.id && a.namespace === current.namespace).find(a => a.name !== 'DATABASE')
|
34 | if (existing) return cli.action.done(cli.color.configVar(existing.name + '_URL'))
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | let backup = await heroku.post('/addon-attachments', {
|
41 | body: {
|
42 | app: { name: app },
|
43 | addon: { name: current.addon.name },
|
44 | namespace: current.namespace,
|
45 | confirm: app
|
46 | }
|
47 | })
|
48 | cli.action.done(cli.color.configVar(backup.name + '_URL'))
|
49 | }())
|
50 |
|
51 | if (!force) {
|
52 | let status = await heroku.request({
|
53 | host: host(attachment.addon),
|
54 | path: `/client/v11/databases/${attachment.addon.id}/wait_status`
|
55 | })
|
56 |
|
57 | if (status['waiting?']) {
|
58 | throw new Error(`Database cannot be promoted while in state: ${status['message']}
|
59 | \nPromoting this database can lead to application errors and outage. Please run pg:wait to wait for database to become available.
|
60 | \nTo ignore this error, you can pass the --force flag to promote the database and risk application issues.`)
|
61 | }
|
62 | }
|
63 |
|
64 | let promotionMessage
|
65 | if (attachment.namespace) {
|
66 | promotionMessage = `Promoting ${cli.color.attachment(attachment.name)} to ${cli.color.configVar('DATABASE_URL')} on ${cli.color.app(app)}`
|
67 | } else {
|
68 | promotionMessage = `Promoting ${cli.color.addon(attachment.addon.name)} to ${cli.color.configVar('DATABASE_URL')} on ${cli.color.app(app)}`
|
69 | }
|
70 |
|
71 | await cli.action(promotionMessage, async function () {
|
72 | await heroku.post('/addon-attachments', {
|
73 | body: {
|
74 | name: 'DATABASE',
|
75 | app: { name: app },
|
76 | addon: { name: attachment.addon.name },
|
77 | namespace: attachment.namespace,
|
78 | confirm: app
|
79 | }
|
80 | })
|
81 | }())
|
82 |
|
83 | let currentPooler = attachments.find(a => a.namespace === "connection-pooling:default" && a.addon.id == current.addon.id && a.name == "DATABASE_CONNECTION_POOL")
|
84 | if (currentPooler) {
|
85 | await cli.action(`Reattaching pooler to new leader`, async function () {
|
86 | await heroku.post('/addon-attachments', {
|
87 | body: {
|
88 | name: currentPooler.name,
|
89 | app: { name: app },
|
90 | addon: { name: attachment.addon.name },
|
91 | namespace: "connection-pooling:default",
|
92 | confirm: app
|
93 | }
|
94 | })
|
95 | }())
|
96 | return cli.action.done()
|
97 | }
|
98 |
|
99 | let releasePhase = ((await heroku.get(`/apps/${app}/formation`)))
|
100 | .find((formation) => formation.type === 'release')
|
101 |
|
102 | if (releasePhase) {
|
103 | await cli.action('Checking release phase', async function () {
|
104 | let releases = await heroku.request({
|
105 | path: `/apps/${app}/releases`,
|
106 | partial: true,
|
107 | headers: {
|
108 | 'Range': `version ..; max=5, order=desc`
|
109 | }
|
110 | })
|
111 | let attach = releases.find((release) => release.description.includes('Attach DATABASE'))
|
112 | let detach = releases.find((release) => release.description.includes('Detach DATABASE'))
|
113 |
|
114 | if (!attach || !detach) {
|
115 | throw new Error('Unable to check release phase. Check your Attach DATABASE release for failures.')
|
116 | }
|
117 |
|
118 | let endTime = Date.now() + 900000
|
119 | let [attachId, detachId] = [attach.id, detach.id]
|
120 | while (true) {
|
121 | let attach = await fetcher.release(app, attachId)
|
122 | if (attach && attach.status === 'succeeded') {
|
123 | let msg = 'pg:promote succeeded.'
|
124 | let detach = await fetcher.release(app, detachId)
|
125 | if (detach && detach.status === 'failed') {
|
126 | msg += ` It is safe to ignore the failed ${detach.description} release.`
|
127 | }
|
128 | return cli.action.done(msg)
|
129 | } else if (attach && attach.status === 'failed') {
|
130 | let msg = `pg:promote failed because ${attach.description} release was unsuccessful. Your application is currently running `
|
131 | let detach = await fetcher.release(app, detachId)
|
132 | if (detach && detach.status === 'succeeded') {
|
133 | msg += 'without an attached DATABASE_URL.'
|
134 | } else {
|
135 | msg += `with ${current.addon.name} attached as DATABASE_URL.`
|
136 | }
|
137 | msg += ' Check your release phase logs for failure causes.'
|
138 | return cli.action.done(msg)
|
139 | } else if (Date.now() > endTime) {
|
140 | return cli.action.done('timeout. Check your Attach DATABASE release for failures.')
|
141 | }
|
142 |
|
143 | await new Promise((resolve) => setTimeout(resolve, 5000))
|
144 | }
|
145 | }())
|
146 | }
|
147 | }
|
148 |
|
149 | module.exports = {
|
150 | topic: 'pg',
|
151 | command: 'promote',
|
152 | description: 'sets DATABASE as your DATABASE_URL',
|
153 | needsApp: true,
|
154 | needsAuth: true,
|
155 | flags: [{ name: 'force', char: 'f' }],
|
156 | args: [{ name: 'database' }],
|
157 | run: cli.command({ preauth: true }, run)
|
158 | }
|