-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from heroku/zw/models-destroy
Add 'ai:models:destroy' command
- Loading branch information
Showing
7 changed files
with
225 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import {Args} from '@oclif/core' | ||
import {flags} from '@heroku-cli/command' | ||
import destroyAddon from '../../../lib/ai/models/destroy_addon' | ||
import confirmCommand from '../../../lib/confirmCommand' | ||
import Command from '../../../lib/base' | ||
|
||
export default class Destroy extends Command { | ||
static description = 'destroy an existing AI model resource' | ||
|
||
static flags = { | ||
app: flags.app({required: true, description: 'app to run command against'}), | ||
confirm: flags.string({char: 'c'}), | ||
force: flags.boolean({char: 'f', description: 'allow destruction even if connected to other apps'}), | ||
remote: flags.remote({description: 'git remote of app to use'}), | ||
} | ||
|
||
static args = { | ||
modelResource: Args.string({required: true, description: 'The resource ID or alias of the model resource to destroy.'}), | ||
} | ||
|
||
static examples = [ | ||
'$ heroku ai:models:destroy claude-3-5-sonnet-acute-43973', | ||
] | ||
|
||
public async run(): Promise<void> { | ||
const {flags, args} = await this.parse(Destroy) | ||
const {app, confirm} = flags | ||
const {modelResource} = args | ||
const force = flags.force || process.env.HEROKU_FORCE === '1' | ||
|
||
await this.configureHerokuAIClient(modelResource, app) | ||
|
||
const aiAddon = this.addon | ||
|
||
await confirmCommand(app, confirm) | ||
await destroyAddon(this.config, aiAddon, force) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import color from '@heroku-cli/color' | ||
import {ux} from '@oclif/core' | ||
import {Config} from '@oclif/core' | ||
import {APIClient} from '@heroku-cli/command' | ||
import * as Heroku from '@heroku-cli/schema' | ||
|
||
export default async function (config: Config, addon: Heroku.AddOn, force = false) { | ||
const addonName = addon.name || '' | ||
const herokuClient = new APIClient(config) | ||
|
||
ux.action.start(`Destroying ${color.addon(addonName)} in the background.\nThe app will restart when complete...`) | ||
|
||
await herokuClient.delete<Heroku.AddOn>(`/apps/${addon.app?.id}/addons/${addon.id}`, { | ||
headers: {'Accept-Expansion': 'plan'}, | ||
body: {force}, | ||
}).catch(error => { | ||
ux.action.stop('') | ||
const error_ = error.body && error.body.message ? new Error(`The add-on was unable to be destroyed: ${error.body.message}.`) : new Error(`The add-on was unable to be destroyed: ${error}.`) | ||
throw error_ | ||
}) | ||
|
||
ux.action.stop() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import {expect} from 'chai' | ||
import {stderr, stdout} from 'stdout-stderr' | ||
import Cmd from '../../../../src/commands/ai/models/destroy' | ||
import stripAnsi from '../../../helpers/strip-ansi' | ||
import {runCommand} from '../../../run-command' | ||
import {mockConfigVars, mockAPIErrors, addon1, addon1Attachment1} from '../../../helpers/fixtures' | ||
import {CLIError} from '@oclif/core/lib/errors' | ||
import nock from 'nock' | ||
|
||
describe('ai:models:destroy', function () { | ||
const {env} = process | ||
let api: nock.Scope | ||
|
||
beforeEach(function () { | ||
process.env = {} | ||
api = nock('https://api.heroku.com:443') | ||
}) | ||
|
||
afterEach(function () { | ||
process.env = env | ||
api.done() | ||
nock.cleanAll() | ||
}) | ||
|
||
context('no model resource is provided', function () { | ||
it('errors when no model resource is provided', async function () { | ||
try { | ||
await runCommand(Cmd) | ||
} catch (error) { | ||
const {message} = error as CLIError | ||
expect(stripAnsi(message)).contains('Missing 1 required arg:') | ||
expect(stripAnsi(message)).contains('modelResource The resource ID or alias of the model resource to destroy.') | ||
} | ||
}) | ||
}) | ||
|
||
it('displays confirmation of AI addon destruction', async function () { | ||
const addonAppId = addon1.app?.id | ||
const addonId = addon1.id | ||
const addonName = addon1.name | ||
const appName = addon1.app?.name | ||
|
||
api | ||
.post('/actions/addons/resolve', {app: `${appName}`, addon: `${addonName}`}) | ||
.reply(200, [addon1]) | ||
.get(`/addons/${addonId}/addon-attachments`) | ||
.reply(200, [addon1Attachment1]) | ||
.get(`/apps/${addonAppId}/config-vars`) | ||
.reply(200, mockConfigVars) | ||
.delete(`/apps/${addonAppId}/addons/${addonId}`, {force: false}) | ||
.reply(200, {...addon1, state: 'deprovisioned'}) | ||
|
||
await runCommand(Cmd, [`${addonName}`, '--app', `${appName}`, '--confirm', `${appName}`]) | ||
expect(stderr.output).contains(`Destroying ${addonName} in the background.`) | ||
expect(stderr.output).contains('The app will restart when complete...') | ||
expect(stdout.output).to.eq('') | ||
}) | ||
|
||
it('displays API error message if destroy request fails', async function () { | ||
const addonAppId = addon1.app?.id | ||
const addonId = addon1.id | ||
const addonName = addon1.name | ||
const appName = addon1.app?.name | ||
|
||
api | ||
.post('/actions/addons/resolve', {app: `${appName}`, addon: `${addonName}`}) | ||
.reply(200, [addon1]) | ||
.get(`/addons/${addonId}/addon-attachments`) | ||
.reply(200, [addon1Attachment1]) | ||
.get(`/apps/${addonAppId}/config-vars`) | ||
.reply(200, mockConfigVars) | ||
.delete(`/apps/${addonAppId}/addons/${addonId}`, {force: false}) | ||
.reply(500, mockAPIErrors.modelsDestroyErrorResponse) | ||
|
||
try { | ||
await runCommand(Cmd, [`${addonName}`, '--app', `${appName}`, '--confirm', `${appName}`]) | ||
} catch (error) { | ||
const {message} = error as CLIError | ||
expect(stripAnsi(message)).to.contains('The add-on was unable to be destroyed:') | ||
expect(stripAnsi(message)).to.contains(mockAPIErrors.modelsDestroyErrorResponse.message) | ||
} | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters