diff --git a/packages/cli/src/commands/spaces/hosts.ts b/packages/cli/src/commands/spaces/hosts.ts new file mode 100644 index 0000000000..035a07d3df --- /dev/null +++ b/packages/cli/src/commands/spaces/hosts.ts @@ -0,0 +1,39 @@ +import {Command, flags} from '@heroku-cli/command' +import {Args, ux} from '@oclif/core' +import heredoc from 'tsheredoc' +import {displayHostsAsJSON, displayHosts, type Host} from '../../lib/spaces/hosts' + +export default class Hosts extends Command { + static topic = 'spaces' + static hidden = true + static description = 'list dedicated hosts for a space' + static flags = { + space: flags.string({char: 's', description: 'space to get host list from'}), + json: flags.boolean({description: 'output in json format'}), + } + + static args = { + space: Args.string({hidden: true}), + } + + public async run(): Promise { + const {flags, args} = await this.parse(Hosts) + const spaceName = flags.space || args.space + if (!spaceName) { + ux.error(heredoc(` + Error: Missing 1 required arg: + space + See more help with --help + `)) + } + + const {body: hosts} = await this.heroku.get( + `/spaces/${spaceName}/hosts`, { + headers: {Accept: 'application/vnd.heroku+json; version=3.dogwood'}, + }) + if (flags.json) + displayHostsAsJSON(hosts) + else + displayHosts(spaceName as string, hosts) + } +} diff --git a/packages/cli/src/lib/spaces/format.ts b/packages/cli/src/lib/spaces/format.ts index 7873932e88..9c4a92ad71 100644 --- a/packages/cli/src/lib/spaces/format.ts +++ b/packages/cli/src/lib/spaces/format.ts @@ -1,3 +1,21 @@ +import color from '@heroku-cli/color' + export function displayCIDR(cidr: string[] | undefined) { return cidr?.join(', ') ?? '' } + +export function hostStatus(s: string) { + switch (s) { + case 'available': + return `${color.green(s)}` + case 'under-assessment': + return `${color.yellow(s)}` + case 'permanent-failure': + case 'released-permanent-failure': + return `${color.red(s)}` + case 'released': + return `${color.gray(s)}` + default: + return s + } +} diff --git a/packages/cli/src/lib/spaces/hosts.ts b/packages/cli/src/lib/spaces/hosts.ts new file mode 100644 index 0000000000..30927ad357 --- /dev/null +++ b/packages/cli/src/lib/spaces/hosts.ts @@ -0,0 +1,39 @@ +import {ux} from '@oclif/core' +import {hostStatus} from './format' + +export type Host = { + host_id: string, + state: string, + available_capacity_percentage: number, + allocated_at: string, + released_at: string, +} + +export function displayHosts(space: string, hosts: Host[]) { + ux.styledHeader(`${space} Hosts`) + ux.table(hosts, { + host_id: { + header: 'Host ID', + }, + state: { + header: 'State', + get: host => hostStatus(host.state), + }, + available_capacity_percentage: { + header: 'Available Capacity', + get: host => `${host.available_capacity_percentage}%`, + }, + allocated_at: { + header: 'Allocated At', + get: host => host.allocated_at || '', + }, + released_at: { + header: 'Released At', + get: host => host.released_at || '', + }, + }) +} + +export function displayHostsAsJSON(hosts: Host[]) { + ux.log(JSON.stringify(hosts, null, 2)) +} diff --git a/packages/cli/test/unit/commands/spaces/hosts.unit.test.ts b/packages/cli/test/unit/commands/spaces/hosts.unit.test.ts new file mode 100644 index 0000000000..de7ac02c8b --- /dev/null +++ b/packages/cli/test/unit/commands/spaces/hosts.unit.test.ts @@ -0,0 +1,64 @@ +import {stdout} from 'stdout-stderr' +import Cmd from '../../../../src/commands/spaces/hosts' +import runCommand from '../../../helpers/runCommand' +import * as nock from 'nock' +import heredoc from 'tsheredoc' +import expectOutput from '../../../helpers/utils/expectOutput' +import {expect} from 'chai' + +describe('spaces:hosts', function () { + const hosts = [ + { + host_id: 'h-0f927460a59aac18e', + state: 'available', + available_capacity_percentage: 72, + allocated_at: '2020-05-28T04:15:59Z', + released_at: null, + }, { + host_id: 'h-0e927460a59aac18f', + state: 'released', + available_capacity_percentage: 0, + allocated_at: '2020-03-28T04:15:59Z', + released_at: '2020-04-28T04:15:59Z', + }, + ] + + it('lists space hosts', async function () { + nock('https://api.heroku.com', { + reqheaders: { + Accept: 'application/vnd.heroku+json; version=3.dogwood', + }, + }) + .get('/spaces/my-space/hosts') + .reply(200, hosts) + await runCommand(Cmd, [ + '--space', + 'my-space', + ]) + + expectOutput(stdout.output, heredoc(` + === my-space Hosts + Host ID State Available Capacity Allocated At Released At + ─────────────────── ───────── ────────────────── ──────────────────── ──────────────────── + h-0f927460a59aac18e available 72% 2020-05-28T04:15:59Z + h-0e927460a59aac18f released 0% 2020-03-28T04:15:59Z 2020-04-28T04:15:59Z + `)) + }) + + it('shows hosts:info --json', async function () { + nock('https://api.heroku.com', { + reqheaders: { + Accept: 'application/vnd.heroku+json; version=3.dogwood', + }, + }) + .get('/spaces/my-space/hosts') + .reply(200, hosts) + + await runCommand(Cmd, [ + '--space', + 'my-space', + '--json', + ]) + expect(JSON.parse(stdout.output)).to.eql(hosts) + }) +}) diff --git a/packages/cli/test/unit/commands/spaces/topology.unit.test.ts b/packages/cli/test/unit/commands/spaces/topology.unit.test.ts index f01f88702b..84739262f9 100644 --- a/packages/cli/test/unit/commands/spaces/topology.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/topology.unit.test.ts @@ -6,12 +6,21 @@ import heredoc from 'tsheredoc' import expectOutput from '../../../helpers/utils/expectOutput' import * as fixtures from '../../../fixtures/spaces/fixtures' import {expect} from 'chai' +import type {SpaceTopology} from '../../../../src/commands/spaces/topology' +import {App} from '@heroku-cli/schema' 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 + let topo1: SpaceTopology + let topo2: SpaceTopology + let topo3: SpaceTopology + let app: App + + beforeEach(function () { + topo1 = fixtures.topologies['topology-one'] + topo2 = fixtures.topologies['topology-two'] + topo3 = fixtures.topologies['topology-three'] + app = fixtures.apps.www + }) it('shows space topology', async function () { nock('https://api.heroku.com') diff --git a/packages/cli/test/unit/commands/spaces/wait.unit.test.ts b/packages/cli/test/unit/commands/spaces/wait.unit.test.ts index 7a332bb02c..943329da9a 100644 --- a/packages/cli/test/unit/commands/spaces/wait.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/wait.unit.test.ts @@ -7,19 +7,22 @@ import {expect} from 'chai' import expectOutput from '../../../helpers/utils/expectOutput' import * as fixtures from '../../../fixtures/spaces/fixtures' import * as sinon from 'sinon' +import {Space} from '@heroku-cli/schema' describe('spaces:wait', function () { - const allocatingSpace = fixtures.spaces['allocating-space'] - const allocatedSpace = fixtures.spaces['non-shield-space'] + let allocatingSpace: Required + let allocatedSpace: Required let sandbox: sinon.SinonSandbox let notifySpy: sinon.SinonSpy - beforeEach(() => { + beforeEach(function () { sandbox = sinon.createSandbox() notifySpy = sandbox.spy(require('@heroku-cli/notifications'), 'notify') + allocatingSpace = fixtures.spaces['allocating-space'] + allocatedSpace = fixtures.spaces['non-shield-space'] }) - afterEach(() => { + afterEach(function () { sandbox.restore() }) diff --git a/packages/spaces/commands/hosts/index.js b/packages/spaces/commands/hosts/index.js deleted file mode 100644 index d24e468fb1..0000000000 --- a/packages/spaces/commands/hosts/index.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict' - -let cli = require('heroku-cli-util') - -function displayJSON(hosts) { - cli.log(JSON.stringify(hosts, null, 2)) -} - -async function run(context, heroku) { - let lib = require('../../lib/hosts')(heroku) - let space = context.flags.space || context.args.space - if (!space) throw new Error('Space name required.\nUSAGE: heroku spaces:hosts --space my-space') - let hosts = await lib.getHosts(space) - if (context.flags.json) displayJSON(hosts) - else lib.displayHosts(space, hosts) -} - -module.exports = { - topic: 'spaces', - command: 'hosts', - hidden: true, - description: 'list dedicated hosts for a space', - needsApp: false, - needsAuth: true, - args: [{name: 'space', optional: true, hidden: true}], - flags: [ - {name: 'space', char: 's', hasValue: true, description: 'space to get host list from'}, - {name: 'json', description: 'output in json format'}, - ], - run: cli.command(run), -} diff --git a/packages/spaces/index.js b/packages/spaces/index.js index b7f6995519..75152ce987 100644 --- a/packages/spaces/index.js +++ b/packages/spaces/index.js @@ -16,5 +16,4 @@ exports.commands = [ require('./commands/drains/get'), require('./commands/trusted-ips'), require('./commands/trusted-ips/remove'), - require('./commands/hosts'), ] diff --git a/packages/spaces/test/unit/commands/hosts/index.unit.test.js b/packages/spaces/test/unit/commands/hosts/index.unit.test.js deleted file mode 100644 index 6ee3560ae7..0000000000 --- a/packages/spaces/test/unit/commands/hosts/index.unit.test.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict' -/* globals beforeEach */ - -let nock = require('nock') -let cmd = require('../../../../commands/hosts/index') -let expect = require('chai').expect -let cli = require('heroku-cli-util') -let hosts = [ - { - host_id: 'h-0f927460a59aac18e', - state: 'available', - available_capacity_percentage: 72, - allocated_at: '2020-05-28T04:15:59Z', - released_at: null, - }, - { - host_id: 'h-0e927460a59aac18f', - state: 'released', - available_capacity_percentage: 0, - allocated_at: '2020-03-28T04:15:59Z', - released_at: '2020-04-28T04:15:59Z', - }, -] - -describe('spaces:hosts', function () { - beforeEach(() => cli.mockConsole()) - - it('lists space hosts', function () { - let api = nock('https://api.heroku.com:443') - .get('/spaces/my-space/hosts') - .reply(200, - hosts, - ) - return cmd.run({flags: {space: 'my-space'}}) - .then(() => expect(cli.stdout).to.equal( - `=== my-space Hosts -Host ID State Available Capacity Allocated At Released At -─────────────────── ───────── ────────────────── ──────────────────── ──────────────────── -h-0f927460a59aac18e available 72% 2020-05-28T04:15:59Z -h-0e927460a59aac18f released 0% 2020-03-28T04:15:59Z 2020-04-28T04:15:59Z -`)) - .then(() => api.done()) - }) - - it('shows hosts:info --json', function () { - let api = nock('https://api.heroku.com:443') - .get('/spaces/my-space/hosts') - .reply(200, hosts) - - return cmd.run({flags: {space: 'my-space', json: true}}) - .then(() => expect(JSON.parse(cli.stdout)).to.eql(hosts)) - .then(() => api.done()) - }) -})