Skip to content
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
104 changes: 104 additions & 0 deletions src/commands/data/maintenances/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {color, hux, utils} from '@heroku/heroku-cli-util'
import {flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import {addSeconds, formatDistance} from 'date-fns'

import BaseCommand from '../../../lib/data/baseCommand.js'
import {Maintenance} from '../../../lib/data/types.js'

interface StyledMaintenance extends Maintenance {
[key: string]: any;
duration_approximate?: string;
}

export default class DataMaintenancesInfo extends BaseCommand {
static args = {
addon: Args.string({
description: 'data addon to show maintenance for',
required: true,
}),
}

static description = 'display details of the most recent maintenance for an addon'

static examples = [
'$ heroku data:maintenances:info postgresql-sinuous-83720',
'$ heroku data:maintenances:info postgresql-sinuous-83720 --json',
'$ heroku data:maintenances:info DATABASE --app test-app',
]

static flags = {
app: flags.app({description: 'app to list addon maintenances for'}),
json: flags.boolean({char: 'j', description: 'output result in json'}),
remote: flags.remote(),
}

// a prettier display of the information
protected createStyledMaintenance(maintenance: Maintenance) {
// make a copy of the maintenance
const styledMaintenance: StyledMaintenance = {
...maintenance,
addon: {
...maintenance.addon,
},
app: {
...maintenance.app,
},
}

// remove app uuid
if (styledMaintenance.app && styledMaintenance.app.uuid) {
delete styledMaintenance.app.uuid
}

// remove addon uuid
if (styledMaintenance.addon && styledMaintenance.addon.uuid) {
delete styledMaintenance.addon.uuid
}

['app', 'addon'].forEach((key: string) => {
Object.keys(styledMaintenance[key]).forEach(childKey => {
const composedKey = `${key}_${childKey}`
const childValue = styledMaintenance[key][childKey]

if (childValue !== undefined) {
styledMaintenance[composedKey] = childValue
}
})

// after flattening the child keys from `key`, we can remove `key`
// off the of object so that it isn't shown
delete styledMaintenance[key]
})

if (maintenance.duration_seconds) {
const startDuration = Date.now()
const endDuration = addSeconds(startDuration, Number(maintenance.duration_seconds!))
styledMaintenance.duration_approximate = `~ ${formatDistance(endDuration, startDuration)}`
}

return styledMaintenance
}

// create new maintenance-ish object for the purpose of
async run() {
const {args, flags} = await this.parse(DataMaintenancesInfo)
const addonResolver = new utils.AddonResolver(this.heroku)
const {app, json} = flags
const addon = await addonResolver.resolve(args.addon, app, utils.pg.addonService())

ux.action.start(`Fetching maintenance for ${color.addon(addon.name!)}`)
const {body: maintenance} = await this.dataApi.get<Maintenance>(
`/data/maintenances/v1/${addon!.id}`,
this.dataApi.defaults,
)
ux.action.stop()

if (json) {
hux.styledJSON(maintenance)
} else {
const styledMaintenance = this.createStyledMaintenance(maintenance)
hux.styledObject(styledMaintenance)
}
}
}
81 changes: 81 additions & 0 deletions src/commands/data/maintenances/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {color, utils} from '@heroku/heroku-cli-util'
import {flags as Flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'

import BaseCommand from '../../../lib/data/baseCommand.js'
import {waitUntilMaintenanceComplete} from '../../../lib/data/utils.js'

export default class DataMaintenancesRun extends BaseCommand {
static args = {
addon: Args.string({
description: 'data addon to run maintenance on',
required: true,
}),
}

static description = 'triggers a scheduled maintenance for a data add-on'

static examples = [
'$ heroku data:maintenances:run postgresql-sinuous-92834',
'$ heroku data:maintenances:run postgresql-sinuous-92834 --confirm production-app',
'$ heroku data:maintenances:run postgresql-sinuous-92834 --wait',
'$ heroku data:maintenances:run DATABASE --app production-app',
]

static flags = {
app: Flags.app({description: 'app to run addon maintenance for'}),
confirm: Flags.string({
char: 'c',
description: 'confirms running maintenance without entering application maintenance mode if the app name matches',
}),
force: Flags.boolean({
char: 'f',
description: 'start maintenance without entering application maintenance mode',
hidden: true,
}),
remote: Flags.remote(),
wait: Flags.boolean({
char: 'w',
description: 'wait for maintenance to complete before exiting',
}),
}

async confirmMaintenanceMode(addon: Heroku.AddOn, confirm: string | undefined, force: boolean) {
const {body: app} = await this.heroku.get<Heroku.App>(`/apps/${addon!.app!.id}`)
const appName = app.name || ''
if (Boolean(app.maintenance) || Boolean(force)) {
// app is in maintenance mode, or it was forced
} else if (!confirm || confirm !== appName) {
ux.warn('Application is not in maintenance mode.')
this.error(`To proceed, put the application into maintenance mode or re-run the command with ${color.bold.red(`--confirm ${appName}`)}`)
}
}

async run() {
const {args, flags} = await this.parse(DataMaintenancesRun)
const addonResolver = new utils.AddonResolver(this.heroku)
const {app, confirm, force, wait} = flags
const addon = await addonResolver.resolve(args.addon, app, utils.pg.addonService())

const isEssentialTier = utils.pg.isEssentialDatabase(addon) || utils.pg.isLegacyEssentialDatabase(addon)
if (isEssentialTier) {
this.error('You can\'t trigger maintenance on an Essential tier database.')
}

await this.confirmMaintenanceMode(addon, confirm, force || false)

ux.action.start('Triggering maintenance')
await this.dataApi.post(
`/data/maintenances/v1/${addon.id}/run`,
this.dataApi.defaults,
)
ux.action.stop('maintenance triggered')

if (wait) {
ux.action.start('Waiting for maintenance to complete')
await waitUntilMaintenanceComplete(addon.id!, this.dataApi)
ux.action.stop('maintenance completed')
}
}
}
95 changes: 95 additions & 0 deletions src/commands/data/maintenances/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {color, utils} from '@heroku/heroku-cli-util'
import {flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'
import {differenceInCalendarWeeks} from 'date-fns'

import BaseCommand from '../../../lib/data/baseCommand.js'
import {Maintenance} from '../../../lib/data/types.js'

export default class DataMaintenancesSchedule extends BaseCommand {
static args = {
addon: Args.string({
description: 'addon to schedule or re-schedule maintenance for',
required: true,
}),
}

static description = 'schedule or re-schedule maintenance for an add-on'

static examples = [
'$ heroku data:maintenances:schedule postgresql-sinuous-83910',
'$ heroku data:maintenances:schedule postgresql-sinuous-83910 --weeks 3',
'$ heroku data:maintenances:schedule postgresql-sinuous-83910 --weeks -2',
'$ heroku data:maintenances:schedule postgresql-sinuous-83910 --week 2020-02-23',
'$ heroku data:maintenances:schedule HEROKU_POSTGRESQL_RED --app test-app',
]

static flags = {
app: flags.app(),
remote: flags.remote(),
week: flags.string({
description: 'desired week to run maintenance in',
exclusive: ['weeks'],
}),
weeks: flags.string({
default: '2',
description: 'the number of weeks to delay maintenance for',
exclusive: ['week'],
}),
}

protected async computeDelayWeeks(addon: Heroku.AddOn, week: string) {
const {body: maintenance} = await this.dataApi.get<Maintenance>(
`/data/maintenances/v1/${addon!.id}`,
this.dataApi.defaults,
)

const scheduled = (maintenance.status === 'completed' || maintenance.scheduled_for === null)
? Date.now()
: Date.parse(maintenance.scheduled_for)

const weeks = differenceInCalendarWeeks(
Date.parse(week),
scheduled,
)

return weeks.toString()
}

async run() {
const {args, flags} = await this.parse(DataMaintenancesSchedule)
const addonResolver = new utils.AddonResolver(this.heroku)
const {app, week, weeks} = flags

const addon = await addonResolver.resolve(args.addon, app, utils.pg.addonService())

const delayWeeks = week === undefined
? weeks
: await this.computeDelayWeeks(addon, week)

await this.scheduleMaintenance(addon, delayWeeks)
}

protected async scheduleMaintenance(addon: Heroku.AddOn, delayWeeks: string) {
ux.action.start(`Scheduling maintenance for ${color.addon(addon.name!)}`)
const {body: schedule} = await this.dataApi.post<Maintenance>(
`/data/maintenances/v1/${addon.id}/schedule`,
{
...this.dataApi.defaults,
body: {
delay_weeks: delayWeeks,
},
},
)
ux.action.stop('maintenance scheduled')

const alreadyScheduled = schedule.previously_scheduled_for !== null

if (alreadyScheduled) {
this.log(`Scheduled maintenance for ${color.addon(addon.name!)} changed from ${schedule.previously_scheduled_for} to ${schedule.scheduled_for}`)
} else {
this.log(`Maintenance for ${color.addon(addon.name!)} scheduled for ${schedule.scheduled_for}`)
}
}
}
49 changes: 49 additions & 0 deletions src/commands/data/maintenances/wait.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {color, utils} from '@heroku/heroku-cli-util'
import {flags as Flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'

import BaseCommand from '../../../lib/data/baseCommand.js'
import {Maintenance, MaintenanceStatus} from '../../../lib/data/types.js'
import {waitUntilMaintenanceComplete} from '../../../lib/data/utils.js'

export default class DataMaintenancesWait extends BaseCommand {
static args = {
addon: Args.string({description: 'data addon', required: true}),
}

static description = 'blocks until the maintenance process has completed'

static examples = [
'$ heroku data:maintenances:wait postgresql-sinuous-83720',
'$ heroku data:maintenances:wait DATABASE --app production-app',
]

static flags = {
app: Flags.app(),
remote: Flags.remote(),
}

async run() {
const {args, flags} = await this.parse(DataMaintenancesWait)
const addonResolver = new utils.AddonResolver(this.heroku)
const addon = await addonResolver.resolve(args.addon, flags.app, utils.pg.addonService())

const isEssentialTier = utils.pg.isEssentialDatabase(addon) || utils.pg.isLegacyEssentialDatabase(addon)
if (isEssentialTier) {
this.error('You can\'t await maintenance on an Essential tier database.')
}

const {body: maintenance} = await this.dataApi.get<Maintenance>(
`/data/maintenances/v1/${addon.id}`,
this.dataApi.defaults,
)

if (maintenance.status !== MaintenanceStatus.running) {
this.error(`There currently isn't any maintenance in progress for ${color.addon(addon.name!)}`)
}

ux.action.start(`Waiting for maintenance on ${color.addon(addon.name!)} to complete`)
await waitUntilMaintenanceComplete(addon.id!, this.dataApi)
ux.action.stop('maintenance completed')
}
}
47 changes: 47 additions & 0 deletions src/commands/data/maintenances/window/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {color, hux, utils} from '@heroku/heroku-cli-util'
import {flags as Flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'

import BaseCommand from '../../../../lib/data/baseCommand.js'
import {Window} from '../../../../lib/data/types.js'

export default class DataMaintenancesWindow extends BaseCommand {
static args = {
addon: Args.string({
description: 'addon to show window for',
required: true,
}),
}

static description = 'describe the maintenance window on an add-on'

static examples = [
'$ heroku data:maintenances:window postgresql-sinuous-92834',
'$ heroku data:maintenances:window DATABASE --app production-app',
]

static flags = {
app: Flags.app({description: 'app to show addon maintenance window for'}),
json: Flags.boolean({char: 'j', description: 'output result in json'}),
remote: Flags.remote(),
}

async run() {
const {args, flags} = await this.parse(DataMaintenancesWindow)
const addonResolver = new utils.AddonResolver(this.heroku)
const addon = await addonResolver.resolve(args.addon, flags.app, utils.pg.addonService())

ux.action.start(`Fetching maintenance window for ${color.addon(addon.name!)}`)
const {body: window} = await this.dataApi.get<Window>(
`/data/maintenances/v1/${addon.id}/window`,
this.dataApi.defaults,
)
ux.action.stop()

if (flags.json) {
hux.styledJSON(window)
} else {
hux.styledObject(window)
}
}
}
Loading
Loading