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

migrate(W-14169186): spaces: Upgrade spaces:ps #2836

Merged
merged 4 commits into from
Apr 29, 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
93 changes: 93 additions & 0 deletions packages/cli/src/commands/spaces/ps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import {ago} from '../../lib/time'

const getProcessNum = (s: string) => Number.parseInt(s.split('.', 2)[1], 10)

type SpaceDynosInfo = {
app_name: string,
dynos: Required<Heroku.Dyno>[]
}
export default class Ps extends Command {
static topic = 'spaces';
static description = 'list dynos for a space';
static flags = {
space: flags.string({char: 's', description: 'space to get dynos 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(Ps)
const spaceName = flags.space || args.space
if (!spaceName) {
throw new Error('Space name required.\nUSAGE: heroku spaces:ps my-space')
}

const [{body: spaceDynos}, {body: space}] = await Promise.all([
this.heroku.get<SpaceDynosInfo[]>(`/spaces/${spaceName}/dynos`),
this.heroku.get<Heroku.Space>(`/spaces/${spaceName}`),
])

if (space.shield) {
spaceDynos.forEach(spaceDyno => {
spaceDyno.dynos.forEach(d => {
if (d.size?.startsWith('Private')) {
d.size = d.size.replace('Private-', 'Shield-')
}
})
})
}

if (flags.json) {
ux.styledJSON(spaceDynos)
} else {
this.render(spaceDynos)
}
}

private render(spaceDynos?: SpaceDynosInfo[]) {
spaceDynos?.forEach(spaceDyno => {
this.printDynos(spaceDyno.app_name, spaceDyno.dynos)
})
}

private printDynos(appName: string, dynos: Required<Heroku.Dyno>[]) {
const dynosByCommand = new Map<string, string[]>()
for (const dyno of dynos) {
const since = ago(new Date(dyno.updated_at))
const size = dyno.size ?? '1X'
let key = ''
let item = ''
if (dyno.type === 'run') {
key = 'run: one-off processes'
item = `${dyno.name} (${size}): ${dyno.state} ${since}: ${dyno.command}`
} else {
key = `${color.green(dyno.type)} (${color.cyan(size)}): ${dyno.command}`
const state = dyno.state === 'up' ? color.green(dyno.state) : color.yellow(dyno.state)
item = `${dyno.name}: ${color.green(state)} ${color.dim(since)}`
}

if (!dynosByCommand.has(key)) {
dynosByCommand.set(key, [])
}

dynosByCommand.get(key)?.push(item)
}

for (const [key, dynos] of dynosByCommand) {
ux.styledHeader(`${appName} ${key} (${color.yellow(dynos.length)})`)
dynos.sort((a, b) => getProcessNum(a) - getProcessNum(b))
for (const dyno of dynos) {
ux.log(dyno)
}

ux.log()
}
}
}
4 changes: 2 additions & 2 deletions packages/cli/src/lib/time.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as strftime from 'strftime'

export function ago(since: any) {
const elapsed = Math.floor((Date.now() - since) / 1000)
export function ago(since: Date) {
const elapsed = Math.floor((Date.now() - since.getTime()) / 1000)
const message = strftime('%Y/%m/%d %H:%M:%S %z', since)
if (elapsed < 60) return `${message} (~ ${Math.floor(elapsed)}s ago)`
if (elapsed < 60 * 60) return `${message} (~ ${Math.floor(elapsed / 60)}m ago)`
Expand Down
135 changes: 135 additions & 0 deletions packages/cli/test/unit/commands/ps.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {expect} from '@oclif/test'
import * as nock from 'nock'
import {stdout} from 'stdout-stderr'
import * as strftime from 'strftime'
import heredoc from 'tsheredoc'
import Cmd from '../../../src/commands/spaces/ps'
import runCommand from '../../helpers/runCommand'

const hourAgo = new Date(Date.now() - (60 * 60 * 1000))
const hourAgoStr = strftime('%Y/%m/%d %H:%M:%S %z', hourAgo)
const spaceDynos = [
{
app_id: 'app_id1', app_name: 'app_name1', dynos: [
{
command: 'npm start',
size: 'Free',
name: 'web.1',
type: 'web',
updated_at: hourAgoStr,
state: 'up',
}, {command: 'bash', size: 'Free', name: 'run.1', type: 'run', updated_at: hourAgoStr, state: 'up'},
],
}, {
app_id: 'app_id2', app_name: 'app_name2', dynos: [
{
command: 'npm start',
size: 'Free',
name: 'web.1',
type: 'web',
updated_at: hourAgoStr,
state: 'up',
}, {command: 'bash', size: 'Free', name: 'run.1', type: 'run', updated_at: hourAgoStr, state: 'up'},
],
},
]
const privateDynos = [
{
app_id: 'app_id1', app_name: 'app_name1', dynos: [
{command: 'npm start', size: 'Private-M', name: 'web.1', type: 'web', updated_at: hourAgoStr, state: 'up'},
],
},
]
describe('spaces:ps', function () {
let api: nock.Scope
let apiSpace: nock.Scope

afterEach(function () {
api.done()
apiSpace.done()
})

it('shows space dynos', async () => {
api = nock('https://api.heroku.com:443')
.get('/spaces/my-space/dynos')
.reply(200, spaceDynos)
apiSpace = nock('https://api.heroku.com:443')
.get('/spaces/my-space')
.reply(200, {shield: false})
await runCommand(Cmd, [
'--space',
'my-space',
])
expect(stdout.output).to.equal(heredoc(`
=== app_name1 web (Free): npm start (1)

web.1: up ${hourAgoStr} (~ 1h ago)

=== app_name1 run: one-off processes (1)

run.1 (Free): up ${hourAgoStr} (~ 1h ago): bash

=== app_name2 web (Free): npm start (1)

web.1: up ${hourAgoStr} (~ 1h ago)

=== app_name2 run: one-off processes (1)

run.1 (Free): up ${hourAgoStr} (~ 1h ago): bash

`))
})

it('shows shield space dynos', async () => {
api = nock('https://api.heroku.com:443')
.get('/spaces/my-space/dynos')
.reply(200, privateDynos)
apiSpace = nock('https://api.heroku.com:443')
.get('/spaces/my-space')
.reply(200, {shield: true})
await runCommand(Cmd, [
'--space',
'my-space',
])
expect(stdout.output).to.equal(heredoc(`
=== app_name1 web (Shield-M): npm start (1)

web.1: up ${hourAgoStr} (~ 1h ago)

`))
})

it('shows private space dynos', async () => {
api = nock('https://api.heroku.com:443')
.get('/spaces/my-space/dynos')
.reply(200, privateDynos)
apiSpace = nock('https://api.heroku.com:443')
.get('/spaces/my-space')
.reply(200, {shield: false})
await runCommand(Cmd, [
'--space',
'my-space',
])
expect(stdout.output).to.equal(heredoc(`
=== app_name1 web (Private-M): npm start (1)

web.1: up ${hourAgoStr} (~ 1h ago)

`))
})

it('shows space dynos with --json', async () => {
api = nock('https://api.heroku.com:443')
.get('/spaces/my-space/dynos')
.reply(200, spaceDynos)
apiSpace = nock('https://api.heroku.com:443')
.get('/spaces/my-space')
.reply(200, {shield: false})
await runCommand(Cmd, [
'--space',
'my-space',
'--json',
])
expect(JSON.parse(stdout.output)).to.eql(spaceDynos)
})
})
90 changes: 0 additions & 90 deletions packages/spaces/commands/ps.js

This file was deleted.

1 change: 0 additions & 1 deletion packages/spaces/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ exports.commands = [
require('./commands/vpn/wait'),
require('./commands/vpn/destroy'),
require('./commands/vpn/update'),
require('./commands/ps'),
require('./commands/transfer'),
require('./commands/drains/get'),
require('./commands/drains/set'),
Expand Down
Loading
Loading