Skip to content

Commit

Permalink
Convert pg credentials destroy to oclif
Browse files Browse the repository at this point in the history
  • Loading branch information
eablack committed Mar 27, 2024
1 parent bdbbb7f commit d939de1
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 213 deletions.
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, 'WARNING: Destructive action')
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/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

0 comments on commit d939de1

Please sign in to comment.