Skip to content
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
3 changes: 3 additions & 0 deletions cspell-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ mworks
myapp
mybillingapp
mycert
mycredential
mydb
myfeature
myhost
Expand All @@ -209,6 +210,7 @@ mylowercaseapp
myotherapp
mypath
myplugin
mypool
myschema
myscript
myteam
Expand Down Expand Up @@ -239,6 +241,7 @@ osslsigncode
ossp
otherapp
otherdb
otherquota
otta
otlpgrpc
otlphttp
Expand Down
125 changes: 125 additions & 0 deletions src/commands/data/pg/attachments/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {color, utils} from '@heroku/heroku-cli-util'
import {flags as Flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'
import tsheredoc from 'tsheredoc'

import type {DeepRequired} from '../../../../lib/data/types.js'

import {trapConfirmationRequired} from '../../../../lib/addons/util.js'
import BaseCommand from '../../../../lib/data/baseCommand.js'

const heredoc = tsheredoc.default

export default class DataPgAttachmentsCreate extends BaseCommand {
static args = {
database: Args.string({
description: 'database name, database attachment name, or related config var on an app',
required: true,
}),
}

static description = 'attach an existing Postgres Advanced database to an app'

static flags = {
app: Flags.app({required: true}),
as: Flags.string({description: 'name for Postgres database attachment'}),
confirm: Flags.string({char: 'c', description: 'pass in the app name to skip confirmation prompts'}),
credential: Flags.string({
description: 'credential to use for database',
exclusive: ['pool'],
}),
pool: Flags.string({description: 'instance pool to attach'}),
remote: Flags.remote(),
}

public async run(): Promise<void> {
const {args, flags} = await this.parse(DataPgAttachmentsCreate)
const {database: databaseArg} = args
const {app, as, confirm, credential, pool} = flags

const {body: addon} = await this.heroku.get<DeepRequired<Heroku.AddOn>>(`/addons/${databaseArg}`)

if (!utils.pg.isAdvancedDatabase(addon)) {
const cmd = `heroku addons:attach ${addon.name} -a ${app}${as ? ` --as ${as}` : ''}`
+ `${credential ? ` --credential ${credential}` : ''}`
ux.error(
'You can only use this command on Advanced-tier databases.\n'
+ `Use ${color.code(cmd)} instead.`,
)
}

const createAttachment = async (confirmed?: string): Promise<Required<Heroku.AddOnAttachment>> => {
let namespace: string | undefined
let attachMessage: string | undefined
if (credential) {
namespace = 'role:' + credential
attachMessage = `Attaching ${color.yellow(credential) + ' on '}${color.addon(addon.name)}`
+ `${as ? ' as ' + color.attachment(as) : ''} to ${color.app(app)}`
} else if (pool) {
namespace = 'pool:' + pool
attachMessage = `Attaching ${color.yellow(pool) + ' on '}${color.addon(addon.name)}`
+ `${as ? ' as ' + color.attachment(as) : ''} to ${color.app(app)}`
} else {
attachMessage = `Attaching ${color.addon(addon.name)}`
+ `${as ? ' as ' + color.attachment(as) : ''} to ${color.app(app)}`
}

const body = {
addon: {name: addon.name},
app: {name: app},
confirm: confirmed,
name: as,
namespace,
}

try {
ux.action.start(attachMessage)
const {body: attachment} = await this.heroku.post<Required<Heroku.AddOnAttachment>>('/addon-attachments', {body})
ux.action.stop()

return attachment
} catch (error) {
ux.action.stop(color.red('!'))
throw error
}
}

if (credential) {
const {body: credentialConfig} = await this.heroku.get<Required<Heroku.AddOnConfig>[]>(
`/addons/${addon.name}/config/role:${encodeURIComponent(credential)}`,
)
if (credentialConfig.length === 0) {
ux.error(heredoc`
The credential ${color.yellow(credential)} doesn't exist on the database ${color.addon(addon.name)}.
Use ${color.code(`heroku data:pg:credentials ${addon.name} -a ${app}`)} to list the credentials on the database.`,
{exit: 1},
)
}
} else if (pool) {
const {body: poolConfig} = await this.heroku.get<Required<Heroku.AddOnConfig>[]>(
`/addons/${addon.name}/config/pool:${encodeURIComponent(pool)}`,
)
if (poolConfig.length === 0) {
ux.error(heredoc`
The pool ${color.yellow(pool)} doesn't exist on the database ${color.addon(addon.name)}.
Use ${color.code(`heroku data:pg:info ${addon.name} -a ${app}`)} to list the pools on the database.`,
{exit: 1},
)
}
}

const attachment = await trapConfirmationRequired<Required<Heroku.AddOnAttachment>>(app, confirm, (confirmed?: string) => createAttachment(confirmed))

try {
ux.action.start(`Setting ${color.attachment(attachment.name)} config vars and restarting ${color.app(app)}`)
const {body: releases} = await this.heroku.get<Required<Heroku.Release>[]>(`/apps/${app}/releases`, {
headers: {Range: 'version ..; max=1, order=desc'}, partial: true,
})
ux.action.stop(`done, v${releases[0].version}`)
} catch (error) {
ux.action.stop(color.red('!'))
throw error
}
}
}
65 changes: 65 additions & 0 deletions src/commands/data/pg/attachments/destroy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {color, hux, utils} from '@heroku/heroku-cli-util'
import {flags as Flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'

import BaseCommand from '../../../../lib/data/baseCommand.js'

export default class DataPgAttachmentsDestroy extends BaseCommand {
static args = {
attachment_name: Args.string({
description: 'unique identifier of the database attachment',
required: true,
}),
}

static description = 'detach an existing database attachment from an app'

static flags = {
app: Flags.app({required: true}),
confirm: Flags.string({char: 'c', description: 'pass in the app name to skip confirmation prompts'}),
remote: Flags.remote(),
}

public async run(): Promise<void> {
const {args, flags} = await this.parse(DataPgAttachmentsDestroy)
const {attachment_name: attachmentName} = args
const {app, confirm} = flags
const {body: attachment} = await this.heroku.get<Required<Heroku.AddOnAttachment>>(
`/apps/${app}/addon-attachments/${attachmentName}`,
)
const addonResolver = new utils.AddonResolver(this.heroku)
const addon = await addonResolver.resolve(attachment.addon.name, app, utils.pg.addonService())

if (!utils.pg.isAdvancedDatabase(addon)) {
ux.error(
'You can only use this command on Advanced-tier databases.\n'
+ `Use ${color.code(`heroku addons:detach ${attachmentName} -a ${app}`)} instead.`,
)
}

await hux.confirmCommand({comparison: app, confirmation: confirm})

try {
ux.action.start(
`Detaching ${color.attachment(attachmentName)} on ${color.datastore(addon.name)} from ${color.app(app)}`,
)
await this.heroku.delete(`/addon-attachments/${attachment.id}`)
ux.action.stop()
} catch (error) {
ux.action.stop(color.red('!'))
throw error
}

try {
ux.action.start(`Unsetting ${color.attachment(attachmentName)} config vars and restarting ${color.app(app)}`)
const {body: releases} = await this.heroku.get<Required<Heroku.Release>[]>(`/apps/${app}/releases`, {
headers: {Range: 'version ..; max=1, order=desc'}, partial: true,
})
ux.action.stop(`done, v${releases[0].version}`)
} catch (error) {
ux.action.stop(color.red('!'))
throw error
}
}
}
78 changes: 78 additions & 0 deletions src/commands/data/pg/attachments/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {color, hux, utils} from '@heroku/heroku-cli-util'
import {flags as Flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'

import type {CredentialInfo, CredentialsInfo} from '../../../../lib/data/types.js'

import BaseCommand from '../../../../lib/data/baseCommand.js'

export default class DataPgAttachmentsIndex extends BaseCommand {
static args = {
database: Args.string({
description: 'database name, database attachment name, or related config var on an app',
required: true,
}),
}

static description = 'list attachments on a Postgres Advanced database'

static examples = [
'<%= config.bin %> <%= command.id %> database_name -a example-app',
]

static flags = {
app: Flags.app({required: true}),
remote: Flags.remote(),
}

async run() {
const {args, flags} = await this.parse(DataPgAttachmentsIndex)
const {app} = flags
const {database} = args

const addonResolver = new utils.AddonResolver(this.heroku)
const addon = await addonResolver.resolve(database, app, utils.pg.addonService())

if (!utils.pg.isAdvancedDatabase(addon)) {
ux.error(
'You can only use this command on Advanced-tier databases.\n'
+ `Use ${color.code(`heroku addons:info ${addon.name} -a ${app}`)} instead.`,
)
}

const [{body: {items: credentials}}, {body: attachments}] = await Promise.all([
this.dataApi.get<CredentialsInfo>(`/data/postgres/v1/${addon.id}/credentials`),
this.heroku.get<Required<Heroku.AddOnAttachment>[]>(`/addons/${addon.id}/addon-attachments`),
])
const ownerCred = credentials.find((cred: CredentialInfo) => cred.type === 'owner')

if (attachments.length === 0) {
ux.stdout('No attachments found for this database.')
return
}

hux.styledHeader(`Attachments for ${color.datastore(addon.name)}`)
hux.table(attachments, {
Attachment: {
get: attachment => `${color.attachment(attachment.app.name! + '::' + attachment.name)}`,
},
Credential: {
get(attachment) {
if (attachment.namespace?.startsWith('role:')) {
return color.name(attachment.namespace.split(':')[1])
}

return `${ownerCred?.name ? `${color.name(ownerCred.name)} (owner)` : ''}`
},
},
Pool: {
get(attachment) {
return color.name(attachment.namespace?.startsWith('pool:')
? attachment.namespace.split(':')[1]
: 'leader')
},
},
})
}
}
2 changes: 1 addition & 1 deletion src/commands/data/pg/fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default class Fork extends BaseCommand {
static flags = {
app: Flags.app({required: true}),
as: Flags.string({description: 'name for the initial database attachment'}),
confirm: Flags.string({hidden: true}),
confirm: Flags.string({char: 'c', description: 'pass in the app name to skip confirmation prompts'}),
level: Flags.string({description: 'set compute scale'}),
name: Flags.string({char: 'n', description: 'name for the database'}),
'provision-option': Flags.string({
Expand Down
54 changes: 54 additions & 0 deletions src/commands/data/pg/quotas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {utils} from '@heroku/heroku-cli-util'
import {flags as Flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'

import type {Quota, Quotas} from '../../../../lib/data/types.js'

import BaseCommand from '../../../../lib/data/baseCommand.js'
import {displayQuota} from '../../../../lib/data/displayQuota.js'

export default class DataPgQuotasIndex extends BaseCommand {
static args = {
database: Args.string({
description: 'database name, database attachment name, or related config var on an app',
required: true,
}),
}

static description = 'display quotas set on a Postgres Advanced database'

static examples = [
'<%= config.bin %> <%= command.id %> database_name --app example-app',
]

static flags = {
app: Flags.app({required: true}),
remote: Flags.remote(),
type: Flags.string({description: 'type of quota', options: ['storage']}),
}

async run() {
const {args, flags} = await this.parse(DataPgQuotasIndex)
const {app, type} = flags
const {database} = args

const addonResolver = new utils.AddonResolver(this.heroku)
const addon = await addonResolver.resolve(database, app, utils.pg.addonService())

if (!utils.pg.isAdvancedDatabase(addon)) {
ux.error('You can only use this command on Advanced-tier databases')
}

if (type) {
const {body: quota} = await this.dataApi.get<Quota>(`/data/postgres/v1/${addon.id}/quotas/${type}`)
displayQuota(quota)
} else {
const {body: quotas} = await this.dataApi.get<Quotas>(`/data/postgres/v1/${addon.id}/quotas`)

quotas.items.forEach(quota => {
displayQuota(quota)
ux.stdout('')
})
}
}
}
Loading
Loading