Skip to content

Commit e6fcd03

Browse files
committed
Copying and refactoring data:pg:quotas topic commands and tests
1 parent 0bca71f commit e6fcd03

File tree

5 files changed

+452
-0
lines changed

5 files changed

+452
-0
lines changed

cspell-dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ osslsigncode
241241
ossp
242242
otherapp
243243
otherdb
244+
otherquota
244245
otta
245246
otlpgrpc
246247
otlphttp
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {utils} from '@heroku/heroku-cli-util'
2+
import {flags as Flags} from '@heroku-cli/command'
3+
import {Args, ux} from '@oclif/core'
4+
5+
import type {Quota, Quotas} from '../../../../lib/data/types.js'
6+
7+
import BaseCommand from '../../../../lib/data/baseCommand.js'
8+
import {displayQuota} from '../../../../lib/data/displayQuota.js'
9+
10+
export default class DataPgQuotasIndex extends BaseCommand {
11+
static args = {
12+
database: Args.string({
13+
description: 'database name, database attachment name, or related config var on an app',
14+
required: true,
15+
}),
16+
}
17+
18+
static description = 'display quotas set on a Postgres Advanced database'
19+
20+
static examples = [
21+
'<%= config.bin %> <%= command.id %> database_name --app example-app',
22+
]
23+
24+
static flags = {
25+
app: Flags.app({required: true}),
26+
remote: Flags.remote(),
27+
type: Flags.string({description: 'type of quota', options: ['storage']}),
28+
}
29+
30+
async run() {
31+
const {args, flags} = await this.parse(DataPgQuotasIndex)
32+
const {app, type} = flags
33+
const {database} = args
34+
35+
const addonResolver = new utils.AddonResolver(this.heroku)
36+
const addon = await addonResolver.resolve(database, app, utils.pg.addonService())
37+
38+
if (!utils.pg.isAdvancedDatabase(addon)) {
39+
ux.error('You can only use this command on Advanced-tier databases')
40+
}
41+
42+
if (type) {
43+
const {body: quota} = await this.dataApi.get<Quota>(`/data/postgres/v1/${addon.id}/quotas/${type}`)
44+
displayQuota(quota)
45+
} else {
46+
const {body: quotas} = await this.dataApi.get<Quotas>(`/data/postgres/v1/${addon.id}/quotas`)
47+
48+
quotas.items.forEach(quota => {
49+
displayQuota(quota)
50+
ux.stdout('')
51+
})
52+
}
53+
}
54+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {color, utils} from '@heroku/heroku-cli-util'
2+
import {flags as Flags} from '@heroku-cli/command'
3+
import {Args, ux} from '@oclif/core'
4+
import tsheredoc from 'tsheredoc'
5+
6+
import type {Quota} from '../../../../lib/data/types.js'
7+
8+
import BaseCommand from '../../../../lib/data/baseCommand.js'
9+
import {displayQuota} from '../../../../lib/data/displayQuota.js'
10+
11+
type QuotaUpdate = {
12+
critical_gb?: null | number,
13+
enforcement_action?: string
14+
warning_gb?: null | number,
15+
}
16+
17+
const heredoc = tsheredoc.default
18+
const validateQuotaSetting = function (flagName: string, settingAmt: string | undefined) {
19+
if (settingAmt && settingAmt !== 'none' && !Number.parseInt(settingAmt, 10)) {
20+
ux.error(heredoc(`
21+
Parsing --${flagName}
22+
You can only enter an integer or "none" in the --${flagName} flag.
23+
See more help with --help
24+
`))
25+
}
26+
}
27+
28+
export default class DataPgQuotasUpdate extends BaseCommand {
29+
static args = {
30+
database: Args.string({
31+
description: 'database name, database attachment name, or related config var on an app',
32+
required: true,
33+
}),
34+
}
35+
36+
static description = 'update quota settings on a Postgres Advanced database'
37+
38+
static examples = ['<%= config.bin %> <%= command.id %> --app example-app --type storage --warning 12 --critical 15 --enforcement-action notify']
39+
40+
static flags = {
41+
app: Flags.app({required: true}),
42+
critical: Flags.string({description: 'set critical threshold in GB, set to "none" to remove threshold'}),
43+
'enforcement-action': Flags.string({
44+
description: 'set enforcement action for when database surpasses the critical threshold',
45+
options: ['notify', 'restrict', 'none'],
46+
}),
47+
remote: Flags.remote(),
48+
type: Flags.string({
49+
description: 'type of quota to update',
50+
options: ['storage'],
51+
required: true,
52+
}),
53+
warning: Flags.string({description: 'set warning threshold in GB, set to "none" to remove threshold'}),
54+
}
55+
56+
async run(): Promise<void> {
57+
const {args, flags} = await this.parse(DataPgQuotasUpdate)
58+
const {database} = args
59+
const {app, critical, 'enforcement-action': enforcementAction, type, warning} = flags
60+
61+
if (!warning && !critical && !enforcementAction) {
62+
ux.error('You must set a value for either the warning, critical, or enforcement-action flags')
63+
}
64+
65+
validateQuotaSetting('warning', warning)
66+
validateQuotaSetting('critical', critical)
67+
68+
const addonResolver = new utils.AddonResolver(this.heroku)
69+
const addon = await addonResolver.resolve(database, app, utils.pg.addonService())
70+
71+
if (!utils.pg.isAdvancedDatabase(addon)) {
72+
ux.error('You can only use this command on Advanced-tier databases.')
73+
}
74+
75+
const quotaUpdate: QuotaUpdate = {}
76+
if (warning) quotaUpdate.warning_gb = warning === 'none' ? null : Number.parseInt(warning, 10)
77+
if (critical) quotaUpdate.critical_gb = critical === 'none' ? null : Number.parseInt(critical, 10)
78+
if (enforcementAction) quotaUpdate.enforcement_action = enforcementAction
79+
80+
ux.action.start(`Updating ${type} quota on ${color.datastore(database)}`)
81+
const {body: updatedQuota} = await this.dataApi.patch<Quota>(`/data/postgres/v1/${addon.id}/quotas/${type}`, {
82+
body: quotaUpdate,
83+
})
84+
ux.action.stop()
85+
86+
displayQuota(updatedQuota)
87+
}
88+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import ansis from 'ansis'
2+
import {expect} from 'chai'
3+
import nock from 'nock'
4+
import {stderr, stdout} from 'stdout-stderr'
5+
import tsheredoc from 'tsheredoc'
6+
7+
import DataPgQuotasIndex from '../../../../../../src/commands/data/pg/quotas/index.js'
8+
import {
9+
addon,
10+
nonAdvancedAddon,
11+
quotasResponse,
12+
storageQuotaResponse,
13+
} from '../../../../../fixtures/data/pg/fixtures.js'
14+
import runCommand from '../../../../../helpers/runCommand.js'
15+
16+
const heredoc = tsheredoc.default
17+
18+
describe.only('data:pg:quotas', function () {
19+
let dataApi: nock.Scope
20+
let herokuApi: nock.Scope
21+
22+
beforeEach(function () {
23+
dataApi = nock('https://api.data.heroku.com')
24+
herokuApi = nock('https://api.heroku.com')
25+
})
26+
27+
afterEach(function () {
28+
dataApi.done()
29+
herokuApi.done()
30+
})
31+
32+
describe('without type flag', function () {
33+
it('returns info on all quotas', async function () {
34+
dataApi
35+
.get(`/data/postgres/v1/${addon.id}/quotas`)
36+
.reply(200, quotasResponse)
37+
herokuApi
38+
.post('/actions/addons/resolve')
39+
.reply(200, [addon])
40+
41+
await runCommand(DataPgQuotasIndex, [
42+
'advanced-horizontal-01234',
43+
'--app=myapp',
44+
])
45+
46+
expect(stderr.output).to.equal('')
47+
expect(ansis.strip(stdout.output)).to.equal(
48+
heredoc(`
49+
=== Storage
50+
51+
Warning: Not set
52+
Critical: Not set
53+
Enforcement Action: None
54+
Status: 1.10 GB (No quotas set)
55+
56+
=== Otherquota
57+
58+
Warning: 50.00 GB
59+
Critical: 100.00 GB
60+
Enforcement Action: Notify
61+
Status: 1.10 GB / 100.00 GB (1.10%) (Within configured quotas)
62+
63+
`),
64+
)
65+
})
66+
})
67+
68+
describe('with type flag', function () {
69+
it('returns info only on the specified type of quota', async function () {
70+
dataApi
71+
.get(`/data/postgres/v1/${addon.id}/quotas/storage`)
72+
.reply(200, storageQuotaResponse)
73+
herokuApi
74+
.post('/actions/addons/resolve')
75+
.reply(200, [addon])
76+
77+
await runCommand(DataPgQuotasIndex, [
78+
'advanced-horizontal-01234',
79+
'--app=myapp',
80+
'--type=storage',
81+
])
82+
83+
expect(stderr.output).to.equal('')
84+
expect(ansis.strip(stdout.output)).to.equal(
85+
heredoc(`
86+
=== Storage
87+
88+
Warning: 50.00 GB
89+
Critical: 100.00 GB
90+
Enforcement Action: None
91+
Status: 0.00 MB / 100.00 GB (Within configured quotas)
92+
`),
93+
)
94+
})
95+
})
96+
97+
describe('error handling', function () {
98+
it('errors when used with non-Advanced-tier add-ons', async function () {
99+
herokuApi
100+
.post('/actions/addons/resolve')
101+
.reply(200, [nonAdvancedAddon])
102+
103+
try {
104+
await runCommand(DataPgQuotasIndex, ['advanced-horizontal-01234', '--app=myapp'])
105+
} catch (error: unknown) {
106+
const err = error as Error
107+
108+
herokuApi.done()
109+
expect(ansis.strip(err.message)).to.equal('You can only use this command on Advanced-tier databases')
110+
}
111+
})
112+
})
113+
})

0 commit comments

Comments
 (0)