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
41 changes: 20 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/commands/addons/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default class Create extends Command {
const config = parseConfig(argv)
let addon: Heroku.AddOn
try {
addon = await createAddon(this.heroku, app, servicePlan, confirm, wait, {as, config, name})
addon = await createAddon(app, servicePlan, confirm, wait, {as, config, name})
if (wait) {
Create.notifier(`heroku addons:create ${addon.name}`, 'Add-on successfully provisioned')
}
Expand Down
13 changes: 7 additions & 6 deletions src/commands/addons/detach.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import * as color from '@heroku/heroku-cli-util/color'
import {HerokuSDK} from '@heroku/sdk'
import {Args, ux} from '@oclif/core'

export default class Detach extends Command {
Expand All @@ -17,19 +17,20 @@ export default class Detach extends Command {
public async run(): Promise<void> {
const {args, flags} = await this.parse(Detach)
const {app} = flags
const {body: attachment} = await this.heroku.get<Heroku.AddOnAttachment>(`/apps/${app}/addon-attachments/${args.attachment_name}`)
const {platform} = new HerokuSDK()
const attachment = await platform.addOnAttachment.infoByApp(app, args.attachment_name)

ux.action.start(`Detaching ${color.attachment(attachment.name || '')} to ${color.addon(attachment.addon?.name || '')} from ${color.app(app)}`)

await this.heroku.delete(`/addon-attachments/${attachment.id}`)
await platform.addOnAttachment.delete(attachment.id!)

ux.action.stop()

ux.action.start(`Unsetting ${color.attachment(attachment.name || '')} config vars and restarting ${color.app(app)}`)

const {body: releases} = await this.heroku.get<Heroku.Release[]>(`/apps/${app}/releases`, {
headers: {Range: 'version ..; max=1, order=desc'}, partial: true,
})
const releases = await platform
.withHeaders({Range: 'version ..; max=1, order=desc'})
.release.list(app)

ux.action.stop(`done, v${releases[0]?.version || ''}`)
}
Expand Down
40 changes: 21 additions & 19 deletions src/commands/addons/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import {APIClient, Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {color, hux} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {ux} from '@oclif/core/ux'
import _ from 'lodash'

import {formatPrice, formatState, grandfatheredPrice} from '../../lib/addons/util.js'
import {huxTableNoWrapOptions} from '../../lib/utils/table-utils.js'

// The Platform expands nested addon_service and plan when this header is set.
const ADDON_EXPANSION_HEADERS = {
Accept: 'application/vnd.heroku+json; version=3.sdk',
'Accept-Expansion': 'addon_service,plan',
}

const topic = 'addons'

export default class Addons extends Command {
Expand Down Expand Up @@ -58,43 +65,38 @@ export function renderAttachment(attachment: Heroku.AddOnAttachment, app: string
}

async function addonGetter(api: APIClient, app?: string) {
let attachmentsResponse: null | ReturnType<typeof api.get<Heroku.AddOnAttachment>> = null
let addonsResponse: ReturnType<typeof api.get<Heroku.AddOn[]>>
const {platform} = new HerokuSDK()
// Apply Accept-Expansion only on add-on list calls (the global list
// endpoint rejects it; the attachments endpoints don't need it).
const platformWithExpansion = platform.withHeaders(ADDON_EXPANSION_HEADERS)
let attachmentsResponse: null | Promise<Heroku.AddOnAttachment[]> = null
let addonsResponse: Promise<Heroku.AddOn[]>
if (app) { // don't display attachments globally
addonsResponse = api.get<Heroku.AddOn[]>(`/apps/${app}/addons`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
'Accept-Expansion': 'addon_service,plan',
},
})
addonsResponse = platformWithExpansion.addOn.listByApp(app) as unknown as Promise<Heroku.AddOn[]>
const sudoHeaders = JSON.parse(process.env.HEROKU_HEADERS || '{}')
// eslint-disable-next-line unicorn/prefer-ternary
if (sudoHeaders['X-Heroku-Sudo'] && !sudoHeaders['X-Heroku-Sudo-User']) {
// because the root /addon-attachments endpoint won't include relevant
// attachments when sudo-ing for another app, we will use the more
// specific API call and sacrifice listing foreign attachments.
attachmentsResponse = api.get<Heroku.AddOnAttachment>(`/apps/${app}/addon-attachments`)
attachmentsResponse = platform.addOnAttachment.listByApp(app) as unknown as Promise<Heroku.AddOnAttachment[]>
} else {
// In order to display all foreign attachments, we'll get out entire
// attachment list
attachmentsResponse = api.get<Heroku.AddOnAttachment>('/addon-attachments')
attachmentsResponse = platform.addOnAttachment.list() as unknown as Promise<Heroku.AddOnAttachment[]>
}
} else {
addonsResponse = api.get<Heroku.AddOn[]>('/addons', {
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
'Accept-Expansion': 'addon_service,plan',
},
})
// The global /addons endpoint doesn't support Accept-Expansion.
addonsResponse = platform.addOn.list() as unknown as Promise<Heroku.AddOn[]>
}

// Get addons and attachments in parallel
const [{body: addonsRaw}, potentialAttachments] = await Promise.all([addonsResponse, attachmentsResponse])
const [addonsRaw, potentialAttachments] = await Promise.all([addonsResponse, attachmentsResponse])
function isRelevantToApp(addon: Heroku.AddOn) {
return !app || addon.app?.name === app || _.some(addon.attachments, att => att.app.name === app)
}

const groupedAttachments = _.groupBy<Heroku.AddOnAttachment>(potentialAttachments?.body, 'addon.id')
const groupedAttachments = _.groupBy<Heroku.AddOnAttachment>(potentialAttachments ?? [], 'addon.id')
const addons: Heroku.AddOn[] = []
addonsRaw.forEach((addon: Heroku.AddOn) => {
addon.attachments = groupedAttachments[addon.id as string] || []
Expand All @@ -114,7 +116,7 @@ async function addonGetter(api: APIClient, app?: string) {
// if the attachment looks relevant to the app, and then render whatever
for (const atts of _.values(groupedAttachments)) {
const inaccessibleAddon = {
addon_service: {}, app: atts[0].addon.app, attachments: atts, name: atts[0].addon.name, plan: {},
addon_service: {}, app: atts[0].addon!.app, attachments: atts, name: atts[0].addon!.name, plan: {},
}
if (isRelevantToApp(inaccessibleAddon)) {
addons.push(inaccessibleAddon)
Expand Down
22 changes: 11 additions & 11 deletions src/commands/addons/info.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {color, hux} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {addOnExtensions} from '@heroku/sdk/extensions/platform'
import {Args} from '@oclif/core'

import {resolveAddon} from '../../lib/addons/resolve.js'
import {formatPrice, formatState, grandfatheredPrice} from '../../lib/addons/util.js'
import {formatPrice, formatState} from '../../lib/addons/util.js'

const topic = 'addons'

Expand All @@ -24,21 +25,20 @@ export default class Info extends Command {
const {args, flags} = await this.parse(Info)
const {app} = flags

const addon = await resolveAddon(this.heroku, app, args.addon)
const {body: attachments} = await this.heroku.get<Heroku.AddOnAttachment[]>(`/addons/${addon.id}/addon-attachments`)
const {platform} = new HerokuSDK({extensions: [addOnExtensions]})
const addon = await platform.addOn.describe(args.addon, {appIdentity: app})
const plan = addon.plan as undefined | {name?: string; price?: Heroku.AddOn['price']}

addon.plan.price = grandfatheredPrice(addon)
addon.attachments = attachments
hux.styledHeader(color.addon(addon.name ?? ''))
/* eslint-disable perfectionist/sort-objects */
hux.styledObject({
Plan: addon.plan.name,
Price: formatPrice({hourly: true, price: addon.plan.price}),
'Max Price': formatPrice({hourly: false, price: addon.plan.price}),
Attachments: addon.attachments.map((att: Heroku.AddOnAttachment) => [
Plan: plan?.name,
Price: formatPrice({hourly: true, price: plan?.price}),
'Max Price': formatPrice({hourly: false, price: plan?.price}),
Attachments: addon.attachments.map(att => [
color.app(att.app?.name || ''), color.attachment(att.name || ''),
].join('::')).sort(),
'Owning app': color.app(addon.app?.name ?? ''),
'Owning app': color.app(addon.app.name ?? ''),
'Installed at': (new Date(addon.created_at ?? ''))
.toString(),
State: formatState(addon.state),
Expand Down
16 changes: 9 additions & 7 deletions src/commands/addons/plans.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Command, flags} from '@heroku-cli/command'
import {Plan} from '@heroku-cli/schema'
import {hux} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {addOnExtensions} from '@heroku/sdk/extensions/platform'
import {Args} from '@oclif/core'
import _ from 'lodash'
import printf from 'printf'

import {formatPrice} from '../../lib/addons/util.js'
Expand All @@ -29,12 +30,13 @@ export default class Plans extends Command {
public async run(): Promise<void> {
const {args, flags} = await this.parse(Plans)
const {service} = args
let {body: plans} = await this.heroku.get<PlanWithMeteredPrice[]>(`/addon-services/${service}/plans`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})
plans = _.sortBy(plans, ['price.contract', 'price.cents'])
const {platform} = new HerokuSDK({extensions: [addOnExtensions]})
const plans = ((await platform.addOn.listPlans(service)) as unknown as PlanWithMeteredPrice[])
.sort((a, b) => {
const contractDelta = Number(a.price?.contract ?? false) - Number(b.price?.contract ?? false)
if (contractDelta !== 0) return contractDelta
return (a.price?.cents ?? 0) - (b.price?.cents ?? 0)
})
if (flags.json) {
hux.styledJSON(plans)
} else {
Expand Down
7 changes: 4 additions & 3 deletions src/commands/addons/rename.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import * as color from '@heroku/heroku-cli-util/color'
import {HerokuSDK} from '@heroku/sdk'
import {Args, ux} from '@oclif/core'

export default class Rename extends Command {
Expand All @@ -13,8 +13,9 @@ export default class Rename extends Command {

public async run(): Promise<void> {
const {args} = await this.parse(Rename)
const {body: addon} = await this.heroku.get<Heroku.AddOn>(`/addons/${encodeURIComponent(args.addon_name)}`)
await this.heroku.patch<Heroku.AddOn>(`/apps/${addon.app?.id}/addons/${addon.id}`, {body: {name: args.new_name}})
const {platform} = new HerokuSDK()
const addon = await platform.addOn.info(args.addon_name)
await platform.addOn.update(addon.app!.id!, addon.id!, {name: args.new_name, plan: addon.plan!.name!})
ux.stdout(`${color.addon(args.addon_name)} successfully renamed to ${color.info(args.new_name)}.`)
}
}
7 changes: 4 additions & 3 deletions src/commands/addons/services.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {color, hux} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {ux} from '@oclif/core/ux'

export default class Services extends Command {
Expand All @@ -12,12 +12,13 @@ export default class Services extends Command {

public async run(): Promise<void> {
const {flags} = await this.parse(Services)
const {body: services} = await this.heroku.get<Heroku.AddOnService[]>('/addon-services')
const {platform} = new HerokuSDK()
const services = await platform.addOnService.list()
if (flags.json) {
hux.styledJSON(services)
} else {
/* eslint-disable perfectionist/sort-objects */
hux.table(services, {
hux.table(services as Array<Record<string, unknown>>, {
name: {
header: 'Slug',
},
Expand Down
Loading
Loading