Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(spaces): Move command spaces:topology to oclif #2835

Merged
merged 2 commits into from
Apr 24, 2024
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
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
Loading