1 | 'use strict'
|
2 |
|
3 | const cli = require('heroku-cli-util')
|
4 |
|
5 | async function run(context, heroku) {
|
6 | const url = require('url')
|
7 | const host = require('../lib/host')
|
8 | const pgbackups = require('../lib/pgbackups')(context, heroku)
|
9 | const fetcher = require('../lib/fetcher')(heroku)
|
10 | const { app, args, flags } = context
|
11 | const interval = Math.max(3, parseInt(flags['wait-interval'])) || 3
|
12 |
|
13 | const upperCaseConfig = function (config) {
|
14 | const output = {}
|
15 | for (const key in config) {
|
16 | output[key.toUpperCase()] = config[key]
|
17 | }
|
18 | return output
|
19 | }
|
20 |
|
21 | let resolve = async function (db) {
|
22 | if (db.match(/^postgres:\/\//)) {
|
23 |
|
24 | let uri = url.parse(db)
|
25 | let dbname = uri.path ? uri.path.slice(1) : ''
|
26 | let host = `${uri.hostname}:${uri.port || 5432}`
|
27 | return {
|
28 | name: dbname ? `database ${dbname} on ${host}` : `database on ${host}`,
|
29 | url: db,
|
30 | confirm: dbname || uri.host
|
31 | }
|
32 | } else {
|
33 |
|
34 | let attachment = await fetcher.attachment(app, db)
|
35 | if (!attachment) throw new Error(`${db} not found on ${cli.color.app(app)}`)
|
36 | let [addon, config] = await Promise.all([
|
37 | heroku.get(`/addons/${attachment.addon.name}`),
|
38 | heroku.get(`/apps/${attachment.app.name}/config-vars`)
|
39 | ])
|
40 | attachment.addon = addon
|
41 | config = upperCaseConfig(config)
|
42 | return {
|
43 | name: attachment.name.replace(/^HEROKU_POSTGRESQL_/, '').replace(/_URL$/, ''),
|
44 | url: config[attachment.name.toUpperCase() + '_URL'],
|
45 | attachment,
|
46 | confirm: app
|
47 | }
|
48 | }
|
49 | }
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | let [source, target] = await Promise.all([resolve(args.source), resolve(args.target)])
|
59 | if (source.url === target.url) throw new Error('Cannot copy database onto itself')
|
60 |
|
61 | await cli.confirmApp(target.confirm, flags.confirm, `WARNING: Destructive action
|
62 | This command will remove all data from ${cli.color.yellow(target.name)}
|
63 | Data from ${cli.color.yellow(source.name)} will then be transferred to ${cli.color.yellow(target.name)}`)
|
64 |
|
65 | let copy
|
66 | let attachment
|
67 | await cli.action(`Starting copy of ${cli.color.yellow(source.name)} to ${cli.color.yellow(target.name)}`, async function () {
|
68 | attachment = target.attachment || source.attachment
|
69 | if (!attachment) throw new Error('Heroku PostgreSQL database must be source or target')
|
70 | copy = await heroku.post(`/client/v11/databases/${attachment.addon.id}/transfers`, {
|
71 | body: {
|
72 | from_name: source.name,
|
73 | from_url: source.url,
|
74 | to_name: target.name,
|
75 | to_url: target.url
|
76 | },
|
77 | host: host(attachment.addon)
|
78 | })
|
79 | }())
|
80 |
|
81 | if (source.attachment) {
|
82 | let credentials = await heroku.get(`/postgres/v0/databases/${source.attachment.addon.name}/credentials`,
|
83 | { host: host(source.attachment.addon) })
|
84 | if (credentials.length > 1) {
|
85 | cli.action.warn(`pg:copy will only copy your default credential and the data it has access to. Any additional credentials and data that only they can access will not be copied.`)
|
86 | }
|
87 | }
|
88 | await pgbackups.wait('Copying', copy.uuid, interval, flags.verbose, attachment.addon.app.name)
|
89 | }
|
90 |
|
91 | module.exports = {
|
92 | topic: 'pg',
|
93 | command: 'copy',
|
94 | needsApp: true,
|
95 | needsAuth: true,
|
96 | description: 'copy all data from source db to target',
|
97 | help: 'at least one of the databases must be a Heroku PostgreSQL DB',
|
98 | args: [
|
99 | { name: 'source' },
|
100 | { name: 'target' }
|
101 | ],
|
102 | flags: [
|
103 | { name: 'wait-interval', hasValue: true },
|
104 | { name: 'verbose' },
|
105 | { name: 'confirm', hasValue: true }
|
106 | ],
|
107 | run: cli.command({ preauth: true }, run)
|
108 | }
|