UNPKG

6.2 kBJavaScriptView Raw
1'use strict'
2
3const cli = require('heroku-cli-util')
4const host = require('../lib/host')
5
6async 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 // Finds or creates a non-DATABASE attachment for the DB currently
17 // attached as DATABASE.
18 //
19 // If current DATABASE is attached by other names, return one of them.
20 // If current DATABASE is only attachment, create a new one and return it.
21 // If no current DATABASE, return nil.
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 // The current add-on occupying the DATABASE attachment has no
37 // other attachments. In order to promote this database without
38 // error, we can create a secondary attachment, just-in-time.
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 // 15 minutes from now
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
149module.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}