UNPKG

5.76 kBJavaScriptView Raw
1'use strict'
2
3const cli = require('heroku-cli-util')
4const debug = require('./debug')
5const { sortBy } = require('lodash')
6const printf = require('printf')
7const URL = require('url').URL
8const env = require('process').env
9
10function getConfigVarName (configVars) {
11 let connstringVars = configVars.filter((cv) => (cv.endsWith('_URL')))
12 if (connstringVars.length === 0) throw new Error('Database URL not found for this addon')
13 return connstringVars[0]
14}
15
16exports.getConfigVarName = getConfigVarName
17
18function formatAttachment (attachment) {
19 let attName = cli.color.addon(attachment.name)
20
21 let output = [cli.color.dim('as'), attName]
22 let appInfo = `on ${cli.color.app(attachment.app.name)} app`
23 output.push(cli.color.dim(appInfo))
24
25 return output.join(' ')
26}
27
28function renderAttachment (attachment, app, isLast) {
29 let line = isLast ? '└─' : '├─'
30 let attName = formatAttachment(attachment)
31 return printf(' %s %s', cli.color.dim(line), attName)
32}
33
34function presentCredentialAttachments (app, credAttachments, credentials, cred) {
35 let isForeignApp = (attOrAddon) => attOrAddon.app.name !== app
36 let atts = sortBy(credAttachments,
37 isForeignApp,
38 'name',
39 'app.name'
40 )
41 // render each attachment under the credential
42 let attLines = atts.map(function (attachment, idx) {
43 let isLast = (idx === credAttachments.length - 1)
44 return renderAttachment(attachment, app, isLast)
45 })
46
47 let rotationLines = []
48 let credentialStore = credentials.filter(a => a.name === cred)[0]
49 if (credentialStore.state === 'rotating') {
50 let formatted = credentialStore.credentials.map(function (credential, idx) {
51 return {
52 'user': credential.user,
53 'state': credential.state,
54 'connections': credential.connections
55 }
56 })
57 let connectionInformationAvailable = formatted.some((c) => c.connections != null)
58 if (connectionInformationAvailable) {
59 let prefix = ' '
60 rotationLines.push(`${prefix}Usernames currently active for this credential:`)
61 cli.table(formatted, {
62 printHeader: false,
63 printLine: function (line) { rotationLines.push(line) },
64 columns: [
65 { key: 'user', format: (v, r) => `${prefix}${v}` },
66 { key: 'state', format: (v, r) => (v === 'revoking') ? 'waiting for no connections to be revoked' : v },
67 { key: 'connections', format: (v, r) => `${v} connections` }
68 ]
69 })
70 }
71 }
72 return [cred].concat(attLines).concat(rotationLines).join('\n')
73}
74
75exports.presentCredentialAttachments = presentCredentialAttachments
76
77exports.getConnectionDetails = function (attachment, config) {
78 const {getBastion} = require('./bastion')
79 const url = require('url')
80 const configVars = attachment.config_vars.filter((cv) => {
81 return config[cv] && config[cv].startsWith('postgres://')
82 })
83 if (configVars.length === 0) {
84 throw new Error(`No config vars found for ${attachment.name}; perhaps they were removed as a side effect of ${cli.color.cmd('heroku rollback')}? Use ${cli.color.cmd('heroku addons:attach')} to create a new attachment and then ${cli.color.cmd('heroku addons:detach')} to remove the current attachment.`)
85 }
86 const connstringVar = getConfigVarName(configVars)
87
88 // remove _URL from the end of the config var name
89 const baseName = connstringVar.slice(0, -4)
90
91 // build the default payload for non-bastion dbs
92 debug(`Using "${connstringVar}" to connect to your database…`)
93 const target = url.parse(config[connstringVar])
94 let [user, password] = target.auth.split(':')
95
96 let payload = {
97 user,
98 password,
99 database: target.path.split('/', 2)[1],
100 host: target.hostname,
101 port: target.port,
102 attachment,
103 url: target
104 }
105
106 // If bastion creds exist, graft it into the payload
107 const bastion = getBastion(config, baseName)
108 if (bastion) {
109 Object.assign(payload, bastion)
110 }
111
112 return payload
113}
114
115exports.starterPlan = a => !!a.plan.name.match(/(dev|basic)$/)
116
117exports.legacyPlan = a => !!a.plan.name.match(/^legacy/)
118
119exports.bastionKeyPlan = a => !!a.plan.name.match(/private/)
120
121exports.configVarNamesFromValue = (config, value) => {
122 let keys = []
123 for (let key of Object.keys(config)) {
124 let configVal = config[key]
125 if (configVal === value) {
126 keys.push(key)
127 } else if (configVal.startsWith('postgres://')) {
128 try {
129 let configURL = new URL(configVal)
130 let ourURL = new URL(value)
131 let components = [ 'protocol', 'hostname', 'port', 'pathname' ]
132 if (components.every((component) => ourURL[component] === configURL[component])) {
133 keys.push(key)
134 }
135 } catch (err) {
136 // ignore -- this is not a valid URL so not a matching URL
137 }
138 }
139 }
140 return sortBy(keys, k => k !== 'DATABASE_URL', 'name')
141}
142
143exports.databaseNameFromUrl = (uri, config) => {
144 const url = require('url')
145
146 let names = exports.configVarNamesFromValue(config, uri)
147 let name = names.pop()
148 while (names.length > 0 && name === 'DATABASE_URL') name = names.pop()
149 if (name) return cli.color.configVar(name.replace(/_URL$/, ''))
150 uri = url.parse(uri)
151 return `${uri.hostname}:${uri.port || 5432}${uri.path}`
152}
153
154exports.parsePostgresConnectionString = (db) => {
155 const url = require('url')
156
157 db = url.parse(db.match(/:\/\//) ? db : `postgres:///${db}`)
158 const [user, password] = db.auth ? db.auth.split(':') : []
159 db.user = user
160 db.password = password
161 const databaseName = db.pathname || null
162 if (databaseName && databaseName.charAt(0) === '/') {
163 db.database = databaseName.slice(1) || null
164 } else {
165 db.database = databaseName
166 }
167 db.host = db.hostname
168 db.port = db.port || env.PGPORT
169 if (db.hostname) {
170 db.port = db.port || 5432
171 }
172 return db
173}