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