Skip to content

Commit

Permalink
feat(utils): add a getGeneration helper function and apply it across …
Browse files Browse the repository at this point in the history
…the CLI
  • Loading branch information
justinwilaby committed Feb 6, 2025
1 parent 51ad3ef commit 6c50633
Show file tree
Hide file tree
Showing 17 changed files with 607 additions and 556 deletions.
5 changes: 3 additions & 2 deletions packages/cli/src/commands/apps/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Heroku from '@heroku-cli/schema'
import * as util from 'util'
import * as _ from 'lodash'
import {filesize} from 'filesize'
import {getGeneration} from '../../lib/apps/generation'
const {countBy, snakeCase} = _

function formatDate(date: Date) {
Expand Down Expand Up @@ -71,7 +72,7 @@ function print(info: Heroku.App, addons: Heroku.AddOn[], collaborators: Heroku.C
data['Git URL'] = info.app.git_url
data['Web URL'] = info.app.web_url
data['Repo Size'] = filesize(info.app.repo_size, {standard: 'jedec', round: 0})
if (info.app.generation.name !== 'fir') data['Slug Size'] = filesize(info.app.slug_size, {standard: 'jedec', round: 0})
if (getGeneration(info.app) !== 'fir') data['Slug Size'] = filesize(info.app.slug_size, {standard: 'jedec', round: 0})
data.Owner = info.app.owner.email
data.Region = info.app.region.name
data.Dynos = countBy(info.dynos, 'type')
Expand Down Expand Up @@ -159,7 +160,7 @@ repo_size=5000000
print('git_url', info.app.git_url)
print('web_url', info.app.web_url)
print('repo_size', filesize(info.app.repo_size, {standard: 'jedec', round: 0}))
if (info.app.generation.name !== 'fir') print('slug_size', filesize(info.app.slug_size, {standard: 'jedec', round: 0}))
if (getGeneration(info.app) !== 'fir') print('slug_size', filesize(info.app.slug_size, {standard: 'jedec', round: 0}))
print('owner', info.app.owner.email)
print('region', info.app.region.name)
print('dynos', util.inspect(countBy(info.dynos, 'type')))
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/buildpacks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {App} from '../../lib/types/fir'
import color from '@heroku-cli/color'

import {BuildpackCommand} from '../../lib/buildpacks/buildpacks'
import {getGeneration} from '../../lib/apps/generation'

export default class Index extends Command {
static description = 'display the buildpacks for an app'
Expand All @@ -21,7 +22,7 @@ export default class Index extends Command {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})
const buildpacks = await buildpacksCommand.fetch(flags.app, app.generation === 'fir')
const buildpacks = await buildpacksCommand.fetch(flags.app, getGeneration(app) === 'fir')
if (buildpacks.length === 0) {
this.log(`${color.app(flags.app)} has no Buildpacks.`)
} else {
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/pipelines/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {getCoupling, getPipeline, getReleases, listPipelineApps, SDK_HEADER} fro
import KolkrabbiAPI from '../../lib/pipelines/kolkrabbi-api'
import type {OciImage, Slug, PipelineCoupling} from '../../lib/types/fir'
import type {Commit, GitHubDiff} from '../../lib/types/github'
import {GenerationKind, getGeneration} from '../../lib/apps/generation'

interface AppInfo {
name: string;
Expand Down Expand Up @@ -87,7 +88,7 @@ export default class PipelinesDiff extends Command {

kolkrabbi: KolkrabbiAPI = new KolkrabbiAPI(this.config.userAgent, () => this.heroku.auth)

getAppInfo = async (appName: string, appId: string, generation: string): Promise<AppInfo> => {
getAppInfo = async (appName: string, appId: string, generation: GenerationKind): Promise<AppInfo> => {
// Find GitHub connection for the app
const githubApp = await this.kolkrabbi.getAppLink(appId)
.catch(() => {
Expand Down Expand Up @@ -139,7 +140,7 @@ export default class PipelinesDiff extends Command {
const {body: pipeline} = await getPipeline(this.heroku, coupling.pipeline!.id!)

const targetAppId = coupling!.app!.id!
const generation = pipeline!.generation!.name!
const generation = getGeneration(pipeline)!

ux.action.start('Fetching apps from pipeline')
const allApps = await listPipelineApps(this.heroku, coupling!.pipeline!.id!)
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/ps/autoscale/enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {ux} from '@oclif/core'
import {App, Formation} from '../../../lib/types/fir'
import {getGeneration} from '../../../lib/apps/generation'

const METRICS_HOST = 'api.metrics.heroku.com'

Expand Down Expand Up @@ -42,7 +43,7 @@ export default class Enable extends Command {
const formations = formationResponse.body
const webFormation = formations.find((f: any) => f.type === 'web')

if (app.generation === 'fir') {
if (getGeneration(app) === 'fir') {
throw new Error('Autoscaling is unavailable for apps in this space. See https://devcenter.heroku.com/articles/generations.')
}

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/spaces/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import heredoc from 'tsheredoc'
import {displayShieldState} from '../../lib/spaces/spaces'
import {RegionCompletion} from '../../lib/autocomplete/completions'
import {splitCsv} from '../../lib/spaces/parsers'
import {getGeneration} from '../../lib/apps/generation'

export default class Create extends Command {
static topic = 'spaces'
Expand Down Expand Up @@ -103,7 +104,7 @@ export default class Create extends Command {

ux.styledHeader(space.name)
ux.styledObject({
ID: space.id, Team: space.team.name, Region: space.region.name, CIDR: space.cidr, 'Data CIDR': space.data_cidr, State: space.state, Shield: displayShieldState(space), Generation: space.generation, 'Created at': space.created_at,
ID: space.id, Team: space.team.name, Region: space.region.name, CIDR: space.cidr, 'Data CIDR': space.data_cidr, State: space.state, Shield: displayShieldState(space), Generation: getGeneration(space), 'Created at': space.created_at,
}, ['ID', 'Team', 'Region', 'CIDR', 'Data CIDR', 'State', 'Shield', 'Generation', 'Created at'])
}
}
3 changes: 2 additions & 1 deletion packages/cli/src/commands/spaces/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import confirmCommand from '../../lib/confirmCommand'
import {displayNat} from '../../lib/spaces/spaces'
import color from '@heroku-cli/color'
import {Space} from '../../lib/types/fir'
import {getGeneration} from '../../lib/apps/generation'

type RequiredSpaceWithNat = Required<Space> & {outbound_ips?: Required<Heroku.SpaceNetworkAddressTranslation>}

Expand Down Expand Up @@ -45,7 +46,7 @@ export default class Destroy extends Command {
if (space.state === 'allocated') {
({body: space.outbound_ips} = await this.heroku.get<Required<Heroku.SpaceNetworkAddressTranslation>>(`/spaces/${spaceName}/nat`))
if (space.outbound_ips && space.outbound_ips.state === 'enabled') {
const ipv6 = space.generation?.name === 'fir' ? ' and IPv6' : ''
const ipv6 = getGeneration(space) === 'fir' ? ' and IPv6' : ''
natWarning = heredoc`
${color.dim('===')} ${color.bold('WARNING: Outbound IPs Will Be Reused')}
${color.yellow(`⚠️ Deleting this space frees up the following outbound IPv4${ipv6} IPs for reuse:`)}
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/spaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import color from '@heroku-cli/color'
import {Command, flags as Flags} from '@heroku-cli/command'
import {ux} from '@oclif/core'
import {Space} from '../../lib/types/fir'
import {getGeneration} from '../../lib/apps/generation'

type SpaceArray = Array<Required<Space>>

Expand Down Expand Up @@ -58,7 +59,7 @@ export default class Index extends Command {
Team: {get: space => space.team.name},
Region: {get: space => space.region.name},
State: {get: space => space.state},
Generation: {get: space => space.generation},
Generation: {get: space => getGeneration(space)},
createdAt: {
header: 'Created At',
get: space => space.created_at,
Expand Down
74 changes: 55 additions & 19 deletions packages/cli/src/lib/apps/generation.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,63 @@
import {APIClient} from '@heroku-cli/command'
import {App} from '../types/fir'

async function getApp(appOrName: App | string, herokuApi?: APIClient): Promise<App> {
if (typeof appOrName === 'string') {
if (herokuApi === undefined)
throw new Error('herokuApi parameter is required when passing an app name')

const {body: app} = await herokuApi.get<App>(
`/apps/${appOrName}`, {
headers: {Accept: 'application/vnd.heroku+json; version=3.sdk'},
})
return app
import {App, Space, DynoSize, TeamApp, Pipeline, Generation, AppGeneration, DynoSizeGeneration, PipelineGeneration} from '../types/fir'
import Dyno from '../run/dyno'

export type GenerationKind = 'fir' | 'cedar';
// web.1 web-1234abcde-123ab
export type GenerationLike = Generation | AppGeneration | DynoSizeGeneration | PipelineGeneration | Dyno
export type GenerationCapable = App | Space | DynoSize | TeamApp | Pipeline

function getGenerationFromGenerationLike(generation: string | GenerationLike | undefined): GenerationKind | undefined {
let maybeGeneration = ''

if (typeof generation === 'string') {
maybeGeneration = generation
} else if (generation && 'name' in generation) {
maybeGeneration = generation.name ?? ''
}

return appOrName
if (/(fir|cedar)/.test(maybeGeneration)) {
return generation as GenerationKind
}

// web-1234abcde44-123ab etc. fir
if (/^web-[0-9a-z]+-[0-9a-z]{5}$/.test(maybeGeneration)) {
return 'fir'
}

// web.n cedar
if (/^web\.[0-9]+$/.test(maybeGeneration)) {
return 'cedar'
}

return undefined
}

export async function isFirApp(appOrName: App | string, herokuApi?: APIClient) {
const app = await getApp(appOrName, herokuApi)
return app.generation.name === 'fir'
/**
* Get the generation of an object
*
* @param source The object to get the generation from
* @returns The generation of the object
*/
export function getGeneration(source: GenerationLike | GenerationCapable | string): GenerationKind | undefined {
if (typeof source === 'object' && 'generation' in source) {
return getGenerationFromGenerationLike(source.generation)
}

return getGenerationFromGenerationLike(source)
}

export async function isCedarApp(appOrName: App | string, herokuApi?: APIClient) {
const app = await getApp(appOrName, herokuApi)
return app.generation.name === 'cedar'
/**
* Get the generation of an app by id or name
*
* @param appIdOrName The id or name of the app to get the generation for
* @param herokuApi The Heroku API client to use
* @returns The generation of the app
*/
export async function getGenerationByAppId(appIdOrName: string, herokuApi: APIClient) {
const {body: app} = await herokuApi.get<App>(
`/apps/${appIdOrName}`, {
headers: {Accept: 'application/vnd.heroku+json; version=3.sdk'},
})
return getGeneration(app)
}
4 changes: 2 additions & 2 deletions packages/cli/src/lib/run/log-displayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {APIClient} from '@heroku-cli/command'
import {ux} from '@oclif/core'
import color from '@heroku-cli/color'
import colorize from './colorize'
import {isFirApp} from '../apps/generation'
import {LogSession} from '../types/fir'
import {getGenerationByAppId} from '../apps/generation'

const EventSource = require('@heroku/eventsource')

Expand Down Expand Up @@ -68,7 +68,7 @@ async function logDisplayer(heroku: APIClient, options: LogDisplayerOptions) {
}
})

const firApp = await isFirApp(options.app, heroku)
const firApp = (await getGenerationByAppId(options.app, heroku)) === 'fir'
const isTail = firApp || options.tail

const requestBodyParameters = {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/lib/spaces/spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Heroku from '@heroku-cli/schema'
import {ux} from '@oclif/core'
import {SpaceNat} from '../types/fir'
import {SpaceWithOutboundIps} from '../types/spaces'
import {getGeneration} from '../apps/generation'

export function displayShieldState(space: Heroku.Space) {
return space.shield ? 'on' : 'off'
Expand All @@ -28,7 +29,7 @@ export function renderInfo(space: SpaceWithOutboundIps, json: boolean) {
State: space.state,
Shield: displayShieldState(space),
'Outbound IPs': displayNat(space.outbound_ips),
Generation: space.generation.name,
Generation: getGeneration(space),
'Created at': space.created_at,
},
['ID', 'Team', 'Region', 'CIDR', 'Data CIDR', 'State', 'Shield', 'Outbound IPs', 'Generation', 'Created at'],
Expand Down
Loading

0 comments on commit 6c50633

Please sign in to comment.