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
172 changes: 172 additions & 0 deletions src/commands/engage/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import Command from '../../lib/command.js';
import { Args, Flags } from '@oclif/core';
import { commonFlags } from '../../lib/engage/flags.js';
import logger, { highlight } from '../../lib/logger.js';
import { YesNo, YesNoChoices } from '../../lib/engage/types.js';
import Renderer from '../../lib/engage/results/renderer.js';
import { deleteResources } from '../../lib/engage/services/delete-service.js';
import { askList } from '../../lib/engage/utils/basic-prompts.js';

export default class EngageDelete extends Command {
static override summary = 'Delete resources.';

static override aliases = [ 'central:delete' ];

static override description = `You must be authenticated to delete one or more resources.
Run ${highlight('"axway auth login"')} to authenticate.`;

static override examples = [
{
description: 'Delete resources from a file',
command: '<%= config.bin %> <%= command.id %> --file <FilePath>',
},
{
description: 'Delete a single resource',
command: '<%= config.bin %> <%= command.id %> <Resource> <Name>',
},
{
description: 'Delete a scoped resource in all scopes with a specific name',
command: '<%= config.bin %> <%= command.id %> <Resource> <Name> --scope <Scope Name> --yes',
},
{
description: 'Delete a scoped resource in a specific scope',
command: '<%= config.bin %> <%= command.id %> <Resource> <Name> --scope <Scope Kind>/<Scope Name>',
},
];

static override args = {
resource: Args.string({
description: 'Resource type to delete.',
required: false,
}),
name: Args.string({
description: 'Name of the resource to delete.',
required: false,
}),
};

static override flags = {
...commonFlags,
file: Flags.string({
char: 'f',
description: 'Filename to use to delete the resource.',
}),
scope: Flags.string({
char: 's',
description: 'Scope name or kind/name for scoped resources.',
}),
yes: Flags.boolean({
char: 'y',
description: 'Automatically reply "yes" to any command prompts.',
}),
wait: Flags.boolean({
description: 'Wait for the resources to be completely deleted.',
}),
forceDelete: Flags.boolean({
description: 'Force delete a resource (Warning: Ignores finalizers on the resource and the resources scoped under it)',
}),
};

private printInvalidArgsUsage() {
this.log(`\nUSAGE:
To delete resources by filenames:\t"axway engage delete -f/--file <path>
To delete a single resource:\t\t"axway engage delete <Resource> <Name>"`);
}

async run(): Promise<any> {
const log = logger('EngageDelete');
const { args, flags, account } = await this.parse(EngageDelete);

const typedResource = args.resource;
const typedName = args.name;
const render = new Renderer(console);

if (flags.file && typedResource) {
render.error('Error: Invalid command arguments, please provide a file path or resource type and name.');
this.printInvalidArgsUsage();
process.exit(1);
}

let isCmdError = true;
try {
render.startSpin(`Deleting resources${flags.wait ? ' and waiting for them to be deleted' : ''}`);

const result = await deleteResources({
account,
region: flags.region,
useCache: flags.cache,
resourceType: typedResource,
resourceName: typedName,
filePath: flags.file,
scopeParam: flags.scope,
wait: flags.wait,
forceDelete: flags.forceDelete,
skipConfirmation: flags.yes,
onConfirmSingleDelete: async (scopeProvided, matchingDefsLength) => {
render.stopSpin();
let answer = YesNo.Yes;
if (!scopeProvided) {
answer = await askList({
msg: 'Deleting this will delete all resources under its scope. Are you sure you want to do this?',
choices: YesNoChoices,
default: YesNo.No,
}) as YesNo;
} else if (matchingDefsLength > 1) {
answer = await askList({
msg: 'The resource may exist in many scopes, and multiple entities might be deleted. Do you want to continue?',
choices: YesNoChoices,
default: YesNo.No,
}) as YesNo;
}
render.startSpin(`Deleting resources${flags.wait ? ' and waiting for them to be deleted' : ''}`);
return answer !== YesNo.No;
},
onConfirmForceDelete: async () => {
render.stopSpin();
const answer = await askList({
msg: 'Are you sure you want to force delete this resource?',
choices: YesNoChoices,
default: YesNo.No,
}) as YesNo;
render.startSpin(`Deleting resources${flags.wait ? ' and waiting for them to be deleted' : ''}`);
return answer !== YesNo.No;
},
});

if (result.missingArgs) {
render.stopSpin();
render.error('Error: You must specify the type and name of the resource to delete or a file path.');
if (result.defsHelpTable) {
this.log(`\nThe server supports the following resources:\n\n${result.defsHelpTable}`);
}
process.exit(1);
}

isCmdError = result.hasErrors;

if (result.singleResults) {
result.singleResults.forEach((res) => {
if (isCmdError && res.error?.length) {
render.anyError(res.error[0]);
} else if (!res.error?.length && res.data) {
render.success(`${render.resourceAndScopeKinds(res.data)} has successfully been deleted.`);
}
});
}

if (result.bulkResults) {
render.bulkResult(result.bulkResults, 'has successfully been deleted.');
}
} catch (e: any) {
log('command error', e);
isCmdError = true;
render.anyError(e);
} finally {
log(`command finished, success = ${!isCmdError}`);
render.stopSpin();
if (isCmdError) {
process.exit(1);
}
}
}
}
170 changes: 170 additions & 0 deletions src/lib/engage/services/delete-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import logger from '../../logger.js';
import { ApiServerClient } from '../clients-external/apiserverclient.js';
import { DefinitionsManager } from '../results/DefinitionsManager.js';
import { loadAndVerifySpecs, parseScopeParam, verifyFile, verifyScopeParam } from '../utils/utils.js';
import {
ApiServerClientSingleResult,
DeleteCommandParams,
DeleteCommandResult,
GenericResource,
} from '../types.js';

const log = logger('engage:delete-service');

export async function deleteResources(params: DeleteCommandParams): Promise<DeleteCommandResult> {
const {
account,
region,
useCache,
resourceType,
resourceName,
filePath,
scopeParam,
wait,
forceDelete,
skipConfirmation,
onConfirmSingleDelete,
onConfirmForceDelete,
} = params;

const client = new ApiServerClient({ account, region, useCache });
const defsManager = await new DefinitionsManager(client).init();

if (!filePath && !resourceType) {
return {
hasErrors: true,
missingArgs: true,
defsHelpTable: defsManager.getDefsTableForHelpMsg(),
};
}

const scope = parseScopeParam(scopeParam);

if (resourceType) {
log('executing api calls in single delete mode');
return runSingleDelete({
resourceType,
resourceName,
scope,
wait,
forceDelete,
skipConfirmation,
defsManager,
client,
onConfirmSingleDelete,
onConfirmForceDelete,
});
}

if (!filePath) {
throw new Error('file path is required for bulk delete.');
}

log('executing api calls in bulk delete mode');
return runBulkDelete({ filePath, defsManager, client, wait, forceDelete });
}

async function runSingleDelete({
resourceType,
resourceName,
scope,
wait,
forceDelete,
skipConfirmation,
defsManager,
client,
onConfirmSingleDelete,
onConfirmForceDelete,
}: {
resourceType: string;
resourceName?: string;
scope: ReturnType<typeof parseScopeParam>;
wait?: boolean;
forceDelete?: boolean;
skipConfirmation?: boolean;
defsManager: DefinitionsManager;
client: ApiServerClient;
onConfirmSingleDelete?: (scopeProvided: boolean, matchingDefsLength: number) => Promise<boolean>;
onConfirmForceDelete?: () => Promise<boolean>;
}): Promise<DeleteCommandResult> {
const defs = defsManager.findDefsByWord(resourceType);
if (!defs) {
throw new Error(`the server doesn't have a resource type "${resourceType}"`);
}
if (!resourceName) {
throw new Error('resource name is required.');
}
if (defs.every((def) => !!def.scope) && !scope) {
throw new Error(
`scope name param (-s/--scope) is required for the scoped "${defs[0].resource.spec.kind}" resource.`
);
}
verifyScopeParam(defsManager.getAllKindsList(), defs, scope);

const matchingDefs = defs.filter(
(def) =>
(scope && ((scope.kind && scope.kind === def.scope?.spec.kind) || (!scope.kind && !!def.scope))) ||
(!scope && !def.scope)
);

if (!matchingDefs.length) {
throw new Error('can\'t find matching resource definitions.');
}

if (!skipConfirmation && onConfirmSingleDelete) {
const shouldContinue = await onConfirmSingleDelete(!!scope, matchingDefs.length);
if (!shouldContinue) {
return { hasErrors: true };
}
}

if (!skipConfirmation && forceDelete && onConfirmForceDelete) {
const shouldContinue = await onConfirmForceDelete();
if (!shouldContinue) {
return { hasErrors: true };
}
}

const singleResults: ApiServerClientSingleResult[] = await Promise.all(
matchingDefs.map(async (def) =>
client.deleteResourceByName({
resourceDef: def.resource,
resourceName: resourceName,
scopeDef: def.scope,
scopeName: scope?.name,
wait,
forceDelete,
})
)
);

const hasErrors = !singleResults.filter((res) => res.data !== null).length;
log('single delete complete, hasErrors:', hasErrors);
return { hasErrors, singleResults };
}

async function runBulkDelete({
filePath,
defsManager,
client,
wait,
forceDelete,
}: {
filePath: string;
defsManager: DefinitionsManager;
client: ApiServerClient;
wait?: boolean;
forceDelete?: boolean;
}): Promise<DeleteCommandResult> {
verifyFile(filePath);
const { docs } = await loadAndVerifySpecs(filePath, defsManager.getAllKindsList());
const bulkResults = await client.bulkDelete(
docs as GenericResource[],
defsManager.getSortedKindsMap(),
wait,
forceDelete
);
const hasErrors = !!bulkResults.error.length;
log('bulk delete complete, hasErrors:', hasErrors);
return { hasErrors, bulkResults };
}
23 changes: 23 additions & 0 deletions src/lib/engage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,29 @@ export interface ApplyCommandResult {
hasErrors: boolean;
}

export interface DeleteCommandParams extends EngageCommandParams {
resourceType?: string;
resourceName?: string;
filePath?: string;
scopeParam?: string;
wait?: boolean;
forceDelete?: boolean;
skipConfirmation?: boolean;
/** Called to confirm a single delete (scope or multi-scope). Return false to abort. */
onConfirmSingleDelete?: (scopeProvided: boolean, matchingDefsLength: number) => Promise<boolean>;
/** Called to confirm a force delete. Return false to abort. */
onConfirmForceDelete?: () => Promise<boolean>;
}

export interface DeleteCommandResult {
hasErrors: boolean;
singleResults?: ApiServerClientSingleResult[];
bulkResults?: ApiServerClientBulkResult;
/** True when neither resource type nor file was provided. */
missingArgs?: boolean;
defsHelpTable?: string;
}

export interface CreateCommandParams extends EngageCommandParams {
filePath: string;
/** Called when file contains resources with missing names. Return false to abort. */
Expand Down
Loading