-
Notifications
You must be signed in to change notification settings - Fork 236
Expand file tree
/
Copy pathcreate.ts
More file actions
223 lines (196 loc) · 8.06 KB
/
create.ts
File metadata and controls
223 lines (196 loc) · 8.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/* eslint-disable no-await-in-loop */
import {flags as Flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {color, utils} from '@heroku/heroku-cli-util'
import {ux} from '@oclif/core/ux'
import inquirer from 'inquirer'
import tsheredoc from 'tsheredoc'
import createAddon from '../../../lib/addons/create-addon.js'
import BaseCommand from '../../../lib/data/base-command.js'
import createPool from '../../../lib/data/create-pool.js'
import {parseProvisionOpts} from '../../../lib/data/parse-provision-opts.js'
import PoolConfig from '../../../lib/data/pool-config.js'
import {ExtendedPostgresLevelInfo} from '../../../lib/data/types.js'
import {fetchLevelsAndPricing} from '../../../lib/data/utils.js'
import notify from '../../../lib/notify.js'
const heredoc = tsheredoc.default
const {prompt} = inquirer
export default class DataPgCreate extends BaseCommand {
static baseFlags = BaseCommand.baseFlagsWithoutPrompt()
static description = 'create a Postgres Advanced database'
static examples = ['<%= config.bin %> <%= command.id %> --level 4G-Performance -a example-app']
static flags = {
app: Flags.app({
required: true,
}),
as: Flags.string({description: 'name for the initial database attachment'}),
confirm: Flags.string({char: 'c', description: 'pass in the app name to skip confirmation prompts'}),
followers: Flags.integer({
dependsOn: ['level'],
description: 'provision a follower instance pool with the specified number of instances',
max: 13,
min: 1,
}),
'high-availability': Flags.boolean({
allowNo: true,
dependsOn: ['level'],
description: 'enable or disable high availability on the leader pool by provisioning a warm standby instance',
}),
level: Flags.string({
description: 'set compute scale',
}),
name: Flags.string({description: 'name for the database'}),
network: Flags.string({description: 'set network for the database', options: ['private', 'shield']}),
'provision-option': Flags.string({
description: 'additional options for provisioning in KEY:VALUE or KEY format, and VALUE defaults to "true" (example: \'foo:bar\' or \'foo\')',
multiple: true,
}),
remote: Flags.remote(),
version: Flags.string({description: 'Postgres version for the database'}),
wait: Flags.boolean({
dependsOn: ['level'],
description: 'watch database creation status and exit when complete',
}),
}
static promptFlagActive = false
private addon: Heroku.AddOn | undefined
private extendedLevelsInfo: ExtendedPostgresLevelInfo[] | undefined
private followerInstanceCount: number = 0
private highAvailability: boolean | undefined
private leaderLevel: string | undefined
public async prompt<T extends inquirer.Answers>(...args: Parameters<typeof inquirer.prompt<T>>): Promise<T> {
return prompt<T>(...args)
}
public async run(): Promise<void> {
const {flags} = await this.parse(DataPgCreate)
const {app, as, confirm, name, network, 'provision-option': provisionOpts, version, wait} = flags
const {followers, 'high-availability': highAvailability, level} = flags
const service = utils.pg.addonService()
const plan = `advanced${network ? `-${network}` : ''}`
const servicePlan = `${service}:${plan}`
// Parse provision options
let provisionConfig: Record<string, string> = {}
if (provisionOpts) {
try {
provisionConfig = parseProvisionOpts(provisionOpts)
} catch (error) {
ux.error(error instanceof Error ? error.message : String(error))
}
}
// Leader Pool Configuration Stage
if (level) {
this.leaderLevel = level
this.highAvailability = highAvailability
} else {
// Fetch the information on levels and pricing for rendering choices
const {extendedLevelsInfo} = await fetchLevelsAndPricing(plan, this.dataApi)
this.extendedLevelsInfo = extendedLevelsInfo
// Start the interactive mode
await this.leaderPoolConfig()
}
// Database cluster provisioning (leader pool)
const config: Record<string, boolean | string | undefined> = {
'high-availability': this.highAvailability,
level: this.leaderLevel,
version,
...provisionConfig,
}
try {
this.addon = await createAddon(this.heroku, app, servicePlan, confirm, wait, {
actionStartMessage: `Creating a ${color.addon(this.leaderLevel || '')} database on ${color.app(app)}`,
actionStopMessage: 'done',
as, config, name,
})
if (wait) {
notify('heroku data:pg:create', 'We successfully provisioned the database')
}
} catch (error) {
ux.action.stop()
if (wait) {
notify(
'heroku data:pg:create',
'We can’t provision the database. Try again or open a ticket with Heroku Support: https://help.heroku.com/.',
false,
)
}
throw error
}
// Follower Pool(s) Configuration Stage
if (!level) {
// Interactive mode
await this.followerPoolConfigLoop()
process.stderr.write(`Running ${color.code(`heroku data:pg:info ${this.addon.name!} --app=${app}`)}...\n\n`)
await this.runCommand('data:pg:info', [this.addon.name!, `--app=${app}`])
} else if (followers && followers > 0) {
const poolInfo = await createPool(this.dataApi, this.addon!, {
count: followers,
level: this.leaderLevel!,
})
ux.stdout(heredoc`
${color.green('Success:')} we're provisioning ${color.bold(poolInfo.name)} follower pool on ${color.addon(this.addon.name!)}.
Run ${color.code(`heroku data:pg:info ${this.addon!.name} -a ${this.addon!.app?.name}`)} to check creation progress.
`)
}
}
public async runCommand(command: string, args: string[]): Promise<void> {
await this.config.runCommand(command, args)
}
private async followerPoolConfigLoop(): Promise<void> {
process.stderr.write('\n')
const {action} = await this.prompt<{action: string}>({
choices: [
{name: 'Configure a follower pool', value: 'configure'},
{name: 'Exit', value: 'exit'},
],
message: 'You can configure a follower pool while the leader pool is being configured.',
name: 'action',
type: 'list',
})
let oneMore: boolean = false
do {
process.stderr.write('\n')
if (action === 'configure') {
const poolConfig = new PoolConfig(this.extendedLevelsInfo!, this.followerInstanceCount)
const {count, level, name} = await poolConfig.followerInteractiveConfig()
try {
ux.action.start('Configuring follower pool')
const poolInfo = await createPool(this.dataApi, this.addon!, {count, level, name})
ux.action.stop()
ux.stdout(heredoc`
${color.green('Success:')} we're provisioning ${color.bold(poolInfo.name)} follower pool on ${color.addon(this.addon!.name!)}.
Run ${color.code(`heroku data:pg:info ${this.addon!.name} -a ${this.addon!.app?.name}`)} to check creation progress.
`)
} catch (error) {
ux.action.stop()
throw error
}
process.stderr.write('\n')
this.followerInstanceCount += count
oneMore = this.followerInstanceCount >= 13
? false
: (await this.prompt<{oneMore: boolean}>({
default: false,
message: 'Configure another follower pool?',
name: 'oneMore',
type: 'confirm',
})).oneMore
} else {
oneMore = false
}
} while (oneMore)
}
private async leaderPoolConfig(): Promise<void> {
const poolConfig = new PoolConfig(this.extendedLevelsInfo!, this.followerInstanceCount)
process.stderr.write(heredoc`
Create a Heroku Postgres Advanced database
${color.gray('Press Ctrl+C to cancel')}
`)
process.stderr.write(heredoc`
→ Configure Leader Pool
${color.gray(' Configure Follower Pool(s)')}\n
`)
const {highAvailability, level} = await poolConfig.leaderInteractiveConfig()
this.leaderLevel = level
this.highAvailability = highAvailability
}
}