Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

migrate(W-14170430): Upgrade pg:backups:restore #2752

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"import/no-unresolved": "error",
"indent": ["error", 2, {"MemberExpression": 1}],
"func-names":"warn", // TODO: fix issues and turn this back on
"no-await-in-loop": "warn", // TODO: fix issues and turn this back on
"no-await-in-loop": "off", // Perfect legit to use await in loops, we should leave it off
justinwilaby marked this conversation as resolved.
Show resolved Hide resolved
"no-constant-condition": ["error", {"checkLoops": false }],
"no-else-return": "warn", // TODO: fix issues and turn this back on
"no-negated-condition":"warn", // TODO: fix issues and turn this back on
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/commands/addons/destroy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable no-await-in-loop */

import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args} from '@oclif/core'
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/commands/addons/wait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export default class Wait extends Command {
if (addon.state === 'provisioning') {
let addonResponse
try {
// eslint-disable-next-line no-await-in-loop
addonResponse = await waitForAddonProvisioning(this.heroku, addon, interval)
} catch (error) {
notify(`heroku addons:wait ${addonName}`, 'Add-on failed to provision', false)
Expand All @@ -63,7 +62,6 @@ export default class Wait extends Command {
notify(`heroku addons:wait ${addonName}`, 'Add-on successfully provisioned')
}
} else if (addon.state === 'deprovisioning') {
// eslint-disable-next-line no-await-in-loop
await waitForAddonDeprovisioning(this.heroku, addon, interval)
if (Date.now() - startTime.valueOf() >= 1000 * 5) {
notify(`heroku addons:wait ${addonName}`, 'Add-on successfully deprovisioned')
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/commands/container/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export default class Pull extends Command {
for (const process of argv as string[]) {
const tag = `${registry}/${app}/${process}`
ux.styledHeader(`Pulling ${process} as ${tag}`)
// eslint-disable-next-line no-await-in-loop
await DockerHelper.pullImage(tag)
}
}
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/commands/container/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ export default class Push extends Command {
ux.styledHeader(`Building ${job.name} (${job.dockerfile})`)
}

// eslint-disable-next-line no-await-in-loop
await DockerHelper.buildImage(job.dockerfile, job.resource, buildArgs, contextPath)
}
} catch (error) {
Expand All @@ -102,7 +101,6 @@ export default class Push extends Command {
ux.styledHeader(`Pushing ${job.name} (${job.dockerfile})`)
}

// eslint-disable-next-line no-await-in-loop
await DockerHelper.pushImage(job.resource)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/pg/backups/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class Delete extends Command {
await confirmApp(app, confirm)
ux.action.start(`Deleting backup ${color.cyan(backup_id)} on ${color.app(app)}`)

const num = await pgbackups.transfer.num(backup_id)
const num = await pgbackups.num(backup_id)
if (!num) {
throw new Error(`Invalid Backup: ${backup_id}`)
}
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/pg/backups/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import pgHost from '../../../lib/pg/host'
import pgBackupsApi, {BackupTransfer, PublicUrlResponse} from '../../../lib/pg/backups'
import pgBackupsApi from '../../../lib/pg/backups'
import {sortBy} from 'lodash'
import download from '../../../lib/pg/download'
import * as fs from 'fs-extra'
import type {BackupTransfer, PublicUrlResponse} from '../../../lib/pg/types'

function defaultFilename() {
let f = 'latest.dump'
Expand Down Expand Up @@ -39,7 +40,7 @@ export default class Download extends Command {
let num
ux.action.start(`Getting backup from ${color.magenta(app)}`)
if (backup_id) {
num = await pgBackupsApi(app, this.heroku).transfer.num(backup_id)
num = await pgBackupsApi(app, this.heroku).num(backup_id)
if (!num)
throw new Error(`Invalid Backup: ${backup_id}`)
} else {
Expand Down
9 changes: 5 additions & 4 deletions packages/cli/src/commands/pg/backups/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {ux} from '@oclif/core'
import backupsFactory, {type BackupTransfer} from '../../../lib/pg/backups'
import backupsFactory from '../../../lib/pg/backups'
import host from '../../../lib/pg/host'
import type {BackupTransfer} from '../../../lib/pg/types'

export default class Index extends Command {
static topic = 'pg'
Expand Down Expand Up @@ -42,7 +43,7 @@ export default class Index extends Command {

private displayBackups(transfers: BackupTransfer[], app: string) {
const backups = transfers.filter(backupTransfer => backupTransfer.from_type === 'pg_dump' && backupTransfer.to_type === 'gof3r')
const {transfer: {name, status}, filesize} = backupsFactory(app, this.heroku)
const {name, status, filesize} = backupsFactory(app, this.heroku)
ux.styledHeader('Backups')
if (backups.length === 0) {
ux.log(`No backups. Capture one with ${color.cyan.bold('heroku pg:backups:capture')}`)
Expand Down Expand Up @@ -73,7 +74,7 @@ export default class Index extends Command {
const restores = transfers
.filter(t => t.from_type !== 'pg_dump' && t.to_type === 'pg_restore')
.slice(0, 10) // first 10 only
const {transfer: {name, status}, filesize} = backupsFactory(app, this.heroku)
const {name, status, filesize} = backupsFactory(app, this.heroku)
ux.styledHeader('Restores')
if (restores.length === 0) {
ux.log(`No restores found. Use ${color.cyan.bold('heroku pg:backups:restore')} to restore a backup`)
Expand Down Expand Up @@ -101,7 +102,7 @@ export default class Index extends Command {
}

private displayCopies(transfers: BackupTransfer[], app: string) {
const {transfer: {name, status}, filesize} = backupsFactory(app, this.heroku)
const {name, status, filesize} = backupsFactory(app, this.heroku)
const copies = transfers.filter(t => t.from_type === 'pg_dump' && t.to_type === 'pg_restore').slice(0, 10)
ux.styledHeader('Copies')
if (copies.length === 0) {
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/src/commands/pg/backups/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import pgHost from '../../../lib/pg/host'
import pgBackupsApi, {BackupTransfer} from '../../../lib/pg/backups'
import pgBackupsApi from '../../../lib/pg/backups'
import {sortBy} from 'lodash'
import type {BackupTransfer} from '../../../lib/pg/types'

function status(backup: BackupTransfer) {
if (backup.succeeded) {
Expand Down Expand Up @@ -45,8 +46,8 @@ export default class Info extends Command {
getBackup = async (id: string | undefined, app: string) => {
let backupID
if (id) {
const {transfer} = pgBackupsApi(app, this.heroku)
backupID = await transfer.num(id)
const {num} = pgBackupsApi(app, this.heroku)
backupID = await num(id)
if (!backupID)
throw new Error(`Invalid ID: ${id}`)
} else {
Expand All @@ -64,8 +65,8 @@ export default class Info extends Command {
}

displayBackup = (backup: BackupTransfer, app: string) => {
const {filesize, transfer} = pgBackupsApi(app, this.heroku)
ux.styledHeader(`Backup ${color.cyan(transfer.name(backup))}`)
const {filesize, name} = pgBackupsApi(app, this.heroku)
ux.styledHeader(`Backup ${color.cyan(name(backup))}`)
ux.styledObject({
Database: color.green(backup.from_name),
'Started at': backup.started_at,
Expand Down
117 changes: 117 additions & 0 deletions packages/cli/src/commands/pg/backups/restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import heredoc from 'tsheredoc'
import confirmApp from '../../../lib/apps/confirm-app'
import backupsFactory from '../../../lib/pg/backups'
import {attachment} from '../../../lib/pg/fetcher'
import host from '../../../lib/pg/host'
import type {BackupTransfer} from '../../../lib/pg/types'

function dropboxURL(url: string) {
eablack marked this conversation as resolved.
Show resolved Hide resolved
if (url.match(/^https?:\/\/www\.dropbox\.com/) && !url.endsWith('dl=1')) {
if (url.endsWith('dl=0'))
url = url.replace('dl=0', 'dl=1')
else if (url.includes('?'))
url += '&dl=1'
else
url += '?dl=1'
}

return url
}

export default class Restore extends Command {
static topic = 'pg'
static description = 'restore a backup (default latest) to a database'
static flags = {
'wait-interval': flags.integer({default: 3}),
extensions: flags.string({
char: 'e',
description: heredoc(`
comma-separated list of extensions to pre-install in the public schema
defaults to saving the latest database to DATABASE_URL
`),
}),
verbose: flags.boolean({char: 'v'}),
confirm: flags.string({char: 'c'}),
app: flags.app({required: true}),
}

static args = {
backup: Args.string(),
database: Args.string(),
}

public async run(): Promise<void> {
const {flags, args} = await this.parse(Restore)
const {app, 'wait-interval': waitInterval, extensions, confirm, verbose} = flags
const interval = Math.max(3, waitInterval)
const {addon: db} = await attachment(this.heroku, app as string, args.database)
const {name, wait} = backupsFactory(app, this.heroku)
let backupURL
let backupName = args.backup

if (backupName && backupName.match(/^https?:\/\//)) {
backupURL = dropboxURL(backupName)
} else {
let backupApp
if (backupName && backupName.match(/::/)) {
[backupApp, backupName] = backupName.split('::')
} else {
backupApp = app
}

const {body: transfers} = await this.heroku.get<BackupTransfer[]>(`/client/v11/apps/${backupApp}/transfers`, {hostname: host()})
const backups = transfers.filter(t => t.from_type === 'pg_dump' && t.to_type === 'gof3r')

let backup
if (backupName) {
backup = backups.find(b => name(b) === backupName)
if (!backup)
throw new Error(`Backup ${color.cyan(backupName)} not found for ${color.app(backupApp)}`)
if (!backup.succeeded)
throw new Error(`Backup ${color.cyan(backupName)} for ${color.app(backupApp)} did not complete successfully`)
} else {
backup = backups.filter(b => b.succeeded).sort((a, b) => {
if (a.finished_at < b.finished_at) {
return -1
}

if (a.finished_at > b.finished_at) {
return 1
}

return 0
}).pop()
if (!backup) {
throw new Error(`No backups for ${color.app(backupApp)}. Capture one with ${color.cyan.bold('heroku pg:backups:capture')}`)
}

backupName = name(backup)
}

backupURL = backup.to_url
}

await confirmApp(app, confirm)
ux.action.start(`Starting restore of ${color.cyan(backupName)} to ${color.yellow(db.name)}`)
ux.log(heredoc(`

Use Ctrl-C at any time to stop monitoring progress; the backup will continue restoring.
Use ${color.cyan.bold('heroku pg:backups')} to check progress.
Stop a running restore with ${color.cyan.bold('heroku pg:backups:cancel')}.
`))

const {body: restore} = await this.heroku.post<{uuid: string}>(`/client/v11/databases/${db.id}/restores`, {
body: {backup_url: backupURL, extensions: this.getSortedExtensions(extensions as string)}, hostname: host(),
})

ux.action.stop()
await wait('Restoring', restore.uuid, interval, verbose, db.app.id as string)
}

protected getSortedExtensions(extensions: string | null | undefined): string[] | undefined {
return extensions?.split(',').map(ext => ext.trim().toLowerCase()).sort()
justinwilaby marked this conversation as resolved.
Show resolved Hide resolved
}
}
5 changes: 3 additions & 2 deletions packages/cli/src/commands/pg/backups/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import host from '../../../lib/pg/host'
import pgBackupsApi, {BackupTransfer, PublicUrlResponse} from '../../../lib/pg/backups'
import pgBackupsApi from '../../../lib/pg/backups'
import {sortBy} from 'lodash'
import type {BackupTransfer, PublicUrlResponse} from '../../../lib/pg/types'

export default class Url extends Command {
static topic = 'pg';
Expand All @@ -24,7 +25,7 @@ export default class Url extends Command {

let num
if (backup_id) {
num = await pgBackupsApi(app, this.heroku).transfer.num(backup_id)
num = await pgBackupsApi(app, this.heroku).num(backup_id)
if (!num)
throw new Error(`Invalid Backup: ${backup_id}`)
} else {
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/commands/redis/wait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export default class Wait extends Command {
let waiting = false
while (true) {
try {
// eslint-disable-next-line no-await-in-loop
status = await api.request<RedisFormationWaitResponse>(`/redis/v0/databases/${addon.name}/wait`, 'GET').then(response => response.body)
} catch (error) {
const httpError = error as HTTPError
Expand All @@ -60,7 +59,6 @@ export default class Wait extends Command {

ux.action.status = status.message

// eslint-disable-next-line no-await-in-loop
await wait(interval * 1000)
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/lib/addons/addons_wait.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-await-in-loop */
import {ux} from '@oclif/core'
import color from '@heroku-cli/color'
import * as Heroku from '@heroku-cli/schema'
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/lib/certs/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import {Domain} from '../types/domain'
async function * customDomainCreationComplete(app: string, heroku: APIClient): AsyncGenerator<Domain[] | null> {
let retries = 30
while (retries--) {
// eslint-disable-next-line no-await-in-loop
const {body: apiDomains} = await heroku.get<Domain[]>(`/apps/${app}/domains`)
const someNull = apiDomains.some((domain: Domain) => domain.kind === 'custom' && !domain.cname)
if (!someNull) {
yield apiDomains
break
}

// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => {
setTimeout(resolve, 1000)
})
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/lib/container/docker_helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ export const chooseJobs = async function (jobs: groupedDockerJobs) {
message: `Found multiple Dockerfiles with process type ${processType}. Please choose one to build and push `,
}] as inquirer.QuestionCollection

// eslint-disable-next-line no-await-in-loop
const answer = await inquirer.prompt(prompt)
const found = group.find(o => o.dockerfile === answer[processType])

Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/lib/domains/domains.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable no-await-in-loop */

import {APIClient} from '@heroku-cli/command'
import {parse, ParsedDomain, ParseError} from 'psl'
import * as Heroku from '@heroku-cli/schema'
Expand Down
Loading
Loading