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

refactor(pg-v5): Move command pg:credentials:destroy to oclif #2751

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
51 changes: 51 additions & 0 deletions packages/cli/src/commands/pg/credentials/destroy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import {essentialPlan} from '../../../lib/pg/util'
import {getAddon} from '../../../lib/pg/fetcher'
import pgHost from '../../../lib/pg/host'
import confirmApp from '../../../lib/apps/confirm-app'

export default class Destroy extends Command {
static topic = 'pg';
static description = 'destroy credential within database';
static example = '$ heroku pg:credentials:destroy postgresql-transparent-56874 --name cred-name -a woodstock-production';
static flags = {
name: flags.string({char: 'n', required: true, description: 'unique identifier for the credential'}),
confirm: flags.string({char: 'c'}),
app: flags.app({required: true}),
};

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

public async run(): Promise<void> {
const {flags, args} = await this.parse(Destroy)
const {database} = args
const {app, name, confirm} = flags
if (name === 'default') {
throw new Error('Default credential cannot be destroyed.')
}

const db = await getAddon(this.heroku, app, database)
if (essentialPlan(db)) {
throw new Error("You can't destroy the default credential on Essential-tier databases.")
}

const {body: attachments} = await this.heroku.get<Heroku.AddOnAttachment[]>(`/addons/${db.name}/addon-attachments`)
const credAttachments = attachments.filter(a => a.namespace === `credential:${name}`)
const credAttachmentApps = Array.from(new Set(credAttachments.map(a => a.app?.name)))
if (credAttachmentApps.length > 0)
throw new Error(`Credential ${name} must be detached from the app${credAttachmentApps.length > 1 ? 's' : ''} ${credAttachmentApps.map(appName => color.app(appName || ''))
.join(', ')} before destroying.`)

await confirmApp(app, confirm)
ux.action.start(`Destroying credential ${color.cyan.bold(name)}`)
await this.heroku.delete(`/postgres/v0/databases/${db.name}/credentials/${encodeURIComponent(name)}`, {hostname: pgHost()})
ux.action.stop()
ux.log(`The credential has been destroyed within ${db.name}.`)
ux.log(`Database objects owned by ${name} will be assigned to the default credential.`)
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/lib/pg/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@ async function allAttachments(heroku: APIClient, app: string) {
return attachments.filter((a: AddOnAttachmentWithConfigVarsAndPlan) => a.addon.plan?.name?.startsWith('heroku-postgresql'))
}

export async function getAddon(heroku: APIClient, app: string, db: string) {
export async function getAddon(heroku: APIClient, app: string, db = 'DATABASE_URL') {
return ((await attachment(heroku, app, db))).addon
}
142 changes: 142 additions & 0 deletions packages/cli/test/unit/commands/pg/credentials/destroy.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {stderr, stdout} from 'stdout-stderr'
import Cmd from '../../../../../src/commands/pg/credentials/destroy'
import runCommand from '../../../../helpers/runCommand'
import * as nock from 'nock'
import expectOutput from '../../../../helpers/utils/expectOutput'
import {expect} from 'chai'
import heredoc from 'tsheredoc'
import stripAnsi = require('strip-ansi')

describe('pg:credentials:destroy', () => {
const addon = {
name: 'postgres-1', plan: {name: 'heroku-postgresql:standard-0'},
}
afterEach(() => {
nock.cleanAll()
})

it('destroys the credential', async () => {
nock('https://api.heroku.com')
.post('/actions/addon-attachments/resolve')
.reply(200, [{addon}])
nock('https://api.data.heroku.com')
.delete('/postgres/v0/databases/postgres-1/credentials/credname')
.reply(200)
const attachments = [
{
app: {name: 'myapp'}, addon: {id: 100, name: 'postgres-1'}, config_vars: ['HEROKU_POSTGRESQL_PINK_URL'],
},
]
nock('https://api.heroku.com')
.get('/addons/postgres-1/addon-attachments')
.reply(200, attachments)

await runCommand(Cmd, [
'--app',
'myapp',
'--name',
'credname',
'--confirm',
'myapp',
])
expectOutput(stderr.output, heredoc(`
Destroying credential credname...
Destroying credential credname... done
`))
expectOutput(stdout.output, heredoc(`
The credential has been destroyed within postgres-1.
Database objects owned by credname will be assigned to the default credential.
`))
})

it('throws an error when the db is starter plan', async () => {
const hobbyAddon = {
name: 'postgres-1', plan: {name: 'heroku-postgresql:hobby-dev'},
}
nock('https://api.heroku.com')
.post('/actions/addon-attachments/resolve')
.reply(200, [{addon: hobbyAddon}])

const err = "You can't destroy the default credential on Essential-tier databases."
await runCommand(Cmd, [
'--app',
'myapp',
'--name',
'jeff',
]).catch((error: Error) => {
expect(error.message).to.equal(err)
})
})

it('throws an error when the db is numbered essential plan', async () => {
const essentialAddon = {
name: 'postgres-1', plan: {name: 'heroku-postgresql:essential-0'},
}
nock('https://api.heroku.com')
.post('/actions/addon-attachments/resolve')
.reply(200, [{addon: essentialAddon}])
const err = "You can't destroy the default credential on Essential-tier databases."
await runCommand(Cmd, [
'--app',
'myapp',
'--name',
'gandalf',
]).catch((error: Error) => {
expect(error.message).to.equal(err)
})
})

it('throws an error when the credential is still used for an attachment', async () => {
const attachments = [
{
app: {name: 'myapp'}, addon: {id: 100, name: 'postgres-1'}, config_vars: ['HEROKU_POSTGRESQL_PINK_URL'],
}, {
app: {name: 'otherapp'}, addon: {id: 100, name: 'postgres-1'}, namespace: 'credential:gandalf', config_vars: ['HEROKU_POSTGRESQL_PURPLE_URL'],
},
]
nock('https://api.heroku.com')
.post('/actions/addon-attachments/resolve')
.reply(200, [{addon}])
nock('https://api.heroku.com')
.get('/addons/postgres-1/addon-attachments')
.reply(200, attachments)
const err = 'Credential gandalf must be detached from the app ⬢ otherapp before destroying.'
await runCommand(Cmd, [
'--app',
'myapp',
'--name',
'gandalf',
]).catch((error: Error) => {
expect(stripAnsi(error.message)).to.equal(err)
})
})

it('only mentions an app with multiple attachments once', async () => {
const attachments = [
{
app: {name: 'myapp'}, addon: {id: 100, name: 'postgres-1'}, config_vars: ['HEROKU_POSTGRESQL_PINK_URL'],
}, {
app: {name: 'otherapp'}, addon: {id: 100, name: 'postgres-1'}, namespace: 'credential:gandalf', config_vars: ['HEROKU_POSTGRESQL_PURPLE_URL'],
}, {
app: {name: 'otherapp'}, addon: {id: 100, name: 'postgres-1'}, namespace: 'credential:gandalf', config_vars: ['HEROKU_POSTGRESQL_RED_URL'],
}, {
app: {name: 'yetanotherapp'}, addon: {id: 100, name: 'postgres-1'}, namespace: 'credential:gandalf', config_vars: ['HEROKU_POSTGRESQL_BLUE_URL'],
},
]
nock('https://api.heroku.com')
.post('/actions/addon-attachments/resolve')
.reply(200, [{addon}])
nock('https://api.heroku.com')
.get('/addons/postgres-1/addon-attachments')
.reply(200, attachments)
const err = 'Credential gandalf must be detached from the apps ⬢ otherapp, ⬢ yetanotherapp before destroying.'
await runCommand(Cmd, [
'--app',
'myapp',
'--name',
'gandalf',
]).catch((error: Error) => {
expect(stripAnsi(error.message)).to.equal(err)
})
})
})
53 changes: 0 additions & 53 deletions packages/pg-v5/commands/credentials/destroy.js

This file was deleted.

1 change: 0 additions & 1 deletion packages/pg-v5/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ exports.commands = flatten([
require('./commands/connection_pooling'),
require('./commands/copy'),
require('./commands/credentials'),
require('./commands/credentials/destroy'),
require('./commands/credentials/repair_default'),
require('./commands/credentials/rotate'),
require('./commands/credentials/url'),
Expand Down
Loading
Loading