Skip to content

Commit

Permalink
refactor(spaces): Move command spaces:topology to oclif (#2835)
Browse files Browse the repository at this point in the history
* Convert spaces:topology to oclif

* No need to set .sort as it is an in-place operation
  • Loading branch information
eablack authored Apr 24, 2024
1 parent b0499dc commit 3fad2f1
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 264 deletions.
114 changes: 114 additions & 0 deletions packages/cli/src/commands/spaces/topology.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import heredoc from 'tsheredoc'
import color from '@heroku-cli/color'

export type SpaceTopology = {
version: number,
apps: Array<{
id?: string
domains: string[]
formations: Array<{
process_type: string
dynos: Array<{
number: number
private_ip: string
hostname: string
}>
}>
}>
}

export default class Topology extends Command {
static topic = 'spaces';
static description = 'show space topology';
static flags = {
space: flags.string({char: 's', description: 'space to get topology of'}),
json: flags.boolean({description: 'output in json format'}),
};

static args = {
space: Args.string({hidden: true}),
};

public async run(): Promise<void> {
const {flags, args} = await this.parse(Topology)
const spaceName = flags.space || args.space
if (!spaceName) {
ux.error(heredoc(`
Error: Missing 1 required arg:
space
See more help with --help
`))
}

const {body: topology} = await this.heroku.get<SpaceTopology>(`/spaces/${spaceName}/topology`)
let appInfo: Heroku.App[] = []
if (topology.apps) {
appInfo = await Promise.all(topology.apps.map(async topologyApp => {
const {body: app} = await this.heroku.get<Heroku.App>(`/apps/${topologyApp.id}`)
return app
}))
}

this.render(topology, appInfo, flags.json)
}

protected render(topology: SpaceTopology, appInfo: Heroku.App[], json: boolean) {
if (json) {
ux.styledJSON(topology)
} else if (topology.apps) {
topology.apps.forEach(app => {
const formations: string[] = []
const dynos: string[] = []
if (app.formations) {
app.formations.forEach(formation => {
formations.push(formation.process_type)
if (formation.dynos) {
formation.dynos.forEach(dyno => {
const dynoS = [`${formation.process_type}.${dyno.number}`, dyno.private_ip, dyno.hostname].filter(Boolean)
dynos.push(dynoS.join(' - '))
})
}
})
}

const domains = app.domains.sort()
formations.sort()
dynos.sort((a, b) => {
const apt = this.getProcessType(a)
const bpt = this.getProcessType(b)
if (apt > bpt) {
return 1
}

if (apt < bpt) {
return -1
}

return this.getProcessNum(a) - this.getProcessNum(b)
})
const info = appInfo.find(info => info.id === app.id)
let header = info?.name
if (formations.length > 0) {
header += ` (${color.cyan(formations.join(', '))})`
}

ux.styledHeader(header || '')
ux.styledObject({
Domains: domains, Dynos: dynos,
}, ['Domains', 'Dynos'])
ux.log()
})
}
}

protected getProcessType(s: string) {
return s.split('-', 2)[0].split('.', 2)[0]
}

protected getProcessNum(s: string) {
return Number.parseInt(s.split('-', 2)[0].split('.', 2)[1], 10)
}
}
89 changes: 89 additions & 0 deletions packages/cli/test/fixtures/spaces/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Heroku from '@heroku-cli/schema'
import type {SpaceTopology} from '../../../src/commands/spaces/topology'

export const spaces: Record<string, Required<Heroku.Space>> = {
'non-shield-space': {
Expand Down Expand Up @@ -42,3 +43,91 @@ export const spaces: Record<string, Required<Heroku.Space>> = {
updated_at: '2016-01-06T03:23:13Z',
},
}

export const apps: Record<string, Heroku.App> = {
www: {
name: 'acme-inc-www',
id: 'a84b035c-4c83-11e5-9bda-2cf0ee2c94de',
},
}

export const topologies: Record<string, SpaceTopology> = {
'topology-one': {
version: 1,
apps: [
{
id: apps.www.id,
domains: ['example.com', 'example.net'],
formations: [
{
process_type: 'web',
dynos: [
{
number: 1,
private_ip: '10.0.134.42',
hostname: '1.example-app-90210.app.localspace',
},
{
number: 2,
private_ip: '10.0.134.42',
hostname: '1.example-app-90210.app.localspace',
},
],
},
],
},
],
},
'topology-two': {
version: 1,
apps: [
{
id: apps.www.id,
domains: ['example.com', 'example.net'],
formations: [
{
process_type: 'web',
dynos: [
{
number: 2,
private_ip: '10.0.134.42',
hostname: '1.example-app-90210.app.localspace',
},
{
number: 1,
private_ip: '10.0.134.42',
hostname: '1.example-app-90210.app.localspace',
},
],
},
],
},
],
},
'topology-three': {
version: 1,
apps: [
{
id: apps.www.id,
domains: ['example.com', 'example.net'],
formations: [
{
process_type: 'web',
dynos: [
{
number: 1,
private_ip: '10.0.134.42',
hostname: '1.example-app-90210.app.localspace',
},
{
number: 1,
private_ip: '10.0.134.42',
hostname: '1.example-app-90210.app.localspace',
},
],
},
],
},
],
},
}
90 changes: 90 additions & 0 deletions packages/cli/test/unit/commands/spaces/topology.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {stdout} from 'stdout-stderr'
import Cmd from '../../../../src/commands/spaces/topology'
import runCommand from '../../../helpers/runCommand'
import * as nock from 'nock'
import heredoc from 'tsheredoc'
import expectOutput from '../../../helpers/utils/expectOutput'
import * as fixtures from '../../../fixtures/spaces/fixtures'
import {expect} from 'chai'

describe('spaces:topology', function () {
const topo1 = fixtures.topologies['topology-one']
const topo2 = fixtures.topologies['topology-two']
const topo3 = fixtures.topologies['topology-three']
const app = fixtures.apps.www

it('shows space topology', async function () {
nock('https://api.heroku.com')
.get('/spaces/my-space/topology')
.reply(200, topo1)
.get(`/apps/${app.id}`)
.reply(200, app)

await runCommand(Cmd, [
'--space',
'my-space',
])
expectOutput(stdout.output, heredoc(`
=== ${app.name} (web)
Domains: example.com
example.net
Dynos: web.1 - 10.0.134.42 - 1.example-app-90210.app.localspace
web.2 - 10.0.134.42 - 1.example-app-90210.app.localspace
`))
})

it('shows space topology with first dyno having higher process number', async function () {
nock('https://api.heroku.com')
.get('/spaces/my-space/topology')
.reply(200, topo2)
.get(`/apps/${app.id}`)
.reply(200, app)

await runCommand(Cmd, [
'--space',
'my-space',
])
expectOutput(stdout.output, heredoc(`
=== ${app.name} (web)
Domains: example.com
example.net
Dynos: web.1 - 10.0.134.42 - 1.example-app-90210.app.localspace
web.2 - 10.0.134.42 - 1.example-app-90210.app.localspace
`))
})

it('shows space topology with dynos having same process number', async function () {
nock('https://api.heroku.com')
.get('/spaces/my-space/topology')
.reply(200, topo3)
.get(`/apps/${app.id}`)
.reply(200, app)

await runCommand(Cmd, [
'--space',
'my-space',
])
expectOutput(stdout.output, heredoc(`
=== ${app.name} (web)
Domains: example.com
example.net
Dynos: web.1 - 10.0.134.42 - 1.example-app-90210.app.localspace
web.1 - 10.0.134.42 - 1.example-app-90210.app.localspace
`))
})

it('shows space topology --json', async function () {
nock('https://api.heroku.com')
.get('/spaces/my-space/topology')
.reply(200, topo1)
.get(`/apps/${app.id}`)
.reply(200, app)

await runCommand(Cmd, [
'--space',
'my-space',
'--json',
])
expect(JSON.parse(stdout.output)).to.eql(topo1)
})
})
89 changes: 0 additions & 89 deletions packages/spaces/commands/topology.js

This file was deleted.

Loading

0 comments on commit 3fad2f1

Please sign in to comment.