1 | 'use strict'
|
2 |
|
3 | const cli = require('heroku-cli-util')
|
4 | const debug = require('./debug')
|
5 | const { sortBy } = require('lodash')
|
6 | const printf = require('printf')
|
7 | const URL = require('url').URL
|
8 | const env = require('process').env
|
9 |
|
10 | function 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 |
|
16 | exports.getConfigVarName = getConfigVarName
|
17 |
|
18 | function 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 |
|
28 | function 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 |
|
34 | function 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 |
|
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 |
|
75 | exports.presentCredentialAttachments = presentCredentialAttachments
|
76 |
|
77 | exports.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 |
|
89 | const baseName = connstringVar.slice(0, -4)
|
90 |
|
91 |
|
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 |
|
107 | const bastion = getBastion(config, baseName)
|
108 | if (bastion) {
|
109 | Object.assign(payload, bastion)
|
110 | }
|
111 |
|
112 | return payload
|
113 | }
|
114 |
|
115 | exports.starterPlan = a => !!a.plan.name.match(/(dev|basic)$/)
|
116 |
|
117 | exports.legacyPlan = a => !!a.plan.name.match(/^legacy/)
|
118 |
|
119 | exports.bastionKeyPlan = a => !!a.plan.name.match(/private/)
|
120 |
|
121 | exports.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 |
|
137 | }
|
138 | }
|
139 | }
|
140 | return sortBy(keys, k => k !== 'DATABASE_URL', 'name')
|
141 | }
|
142 |
|
143 | exports.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 |
|
154 | exports.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 | }
|