Skip to content
21 changes: 18 additions & 3 deletions packages/cli/src/commands/spaces/destroy.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Args, ux} from '@oclif/core'
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import heredoc from 'tsheredoc'
import confirmCommand from '../../lib/confirmCommand'
import {displayNat} from '../../lib/spaces/spaces'
import color from '@heroku-cli/color'
import {Space} from '../../lib/types/fir'

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

export default class Destroy extends Command {
static topic = 'spaces'
Expand Down Expand Up @@ -40,10 +41,24 @@ export default class Destroy extends Command {

let natWarning = ''
const {body: space} = await this.heroku.get<RequiredSpaceWithNat>(`/spaces/${spaceName}`)

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') {
natWarning = `The Outbound IPs for this space will be reused!\nEnsure that external services no longer allow these Outbound IPs: ${displayNat(space.outbound_ips)}\n`
const ipv6 = space.generation?.name === '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:`)}
${color.bold(displayNat(space.outbound_ips) ?? '')}

${color.dim('Update the following configurations:')}
${color.dim('=')} IP allowlists
${color.dim('=')} Firewall rules
${color.dim('=')} Security group configurations
${color.dim('=')} Network ACLs

${color.yellow(`Ensure that you remove the listed IPv4${ipv6} addresses from your security configurations.`)}
`
}
}

Expand Down
85 changes: 78 additions & 7 deletions packages/cli/test/unit/commands/spaces/destroy.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,101 @@ import runCommand from '../../../helpers/runCommand'
import * as nock from 'nock'
import {expect} from 'chai'
import heredoc from 'tsheredoc'
import {ux} from '@oclif/core'
import * as sinon from 'sinon'

describe('spaces:destroy', function () {
const now = new Date()

beforeEach(function () {
sinon.stub(ux, 'prompt').resolves('my-space')
})

afterEach(function () {
nock.cleanAll()
sinon.restore()
})

it('destroys a space', async function () {
it('shows extended NAT warning for fir generation space', async function () {
const api = nock('https://api.heroku.com')
.get('/spaces/my-space')
.reply(200, {name: 'my-space', team: {name: 'my-team'}, region: {name: 'my-region'}, state: 'allocated', created_at: now})
.reply(200, {
name: 'my-space',
team: {name: 'my-team'},
region: {name: 'my-region'},
state: 'allocated',
created_at: now,
generation: {name: 'fir'},
})
.get('/spaces/my-space/nat')
.reply(200, {state: 'enabled', sources: ['1.1.1.1', '2.2.2.2']})
.delete('/spaces/my-space')
.reply(200)

await runCommand(Cmd, ['--space', 'my-space', '--confirm', 'my-space'])
await runCommand(Cmd, ['--space', 'my-space'])
api.done()
const replacer = /([»›])/g
expect(stderr.output.replace(replacer, '')).to.eq(heredoc(` › Warning: Destructive Action
› This command will destroy the space my-space
› === WARNING: Outbound IPs Will Be Reused
› ⚠️ Deleting this space frees up the following outbound IPv4 and IPv6 IPs
› for reuse:
› 1.1.1.1, 2.2.2.2
› Update the following configurations:
› = IP allowlists
› = Firewall rules
› = Security group configurations
› = Network ACLs
› Ensure that you remove the listed IPv4 and IPv6 addresses from your
› security configurations.

Destroying space my-space...
Destroying space my-space... done
`.replace(replacer, '')))
})

it('shows simple NAT warning for non-fir generation space', async function () {
const api = nock('https://api.heroku.com')
.get('/spaces/my-space')
.reply(200, {
name: 'my-space',
team: {name: 'my-team'},
region: {name: 'my-region'},
state: 'allocated',
created_at: now,
generation: {name: 'cedar'},
})
.get('/spaces/my-space/nat')
.reply(200, {state: 'enabled', sources: ['1.1.1.1', '2.2.2.2']})
.delete('/spaces/my-space')
.reply(200)

await runCommand(Cmd, ['--space', 'my-space'])
api.done()
const replacer = /([»›])/g
expect(stderr.output.replace(replacer, '')).to.eq(heredoc(` › Warning: Destructive Action
› This command will destroy the space my-space
› === WARNING: Outbound IPs Will Be Reused
› ⚠️ Deleting this space frees up the following outbound IPv4 IPs for reuse:
› 1.1.1.1, 2.2.2.2
› Update the following configurations:
› = IP allowlists
› = Firewall rules
› = Security group configurations
› = Network ACLs
› Ensure that you remove the listed IPv4 addresses from your security
› configurations.

expect(stderr.output).to.eq(heredoc`
Destroying space my-space...
Destroying space my-space... done
`)
Destroying space my-space...
Destroying space my-space... done
`.replace(replacer, '')))
})
})