Skip to content

Commit

Permalink
backends:delete improvements (#7059)
Browse files Browse the repository at this point in the history
* delete

* pass message

* tests
  • Loading branch information
tonyjhuang committed Apr 29, 2024
1 parent 180a562 commit 540aaab
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 31 deletions.
41 changes: 40 additions & 1 deletion src/apphosting/index.ts
Expand Up @@ -410,7 +410,7 @@ export async function deleteBackendAndPoll(
*/
export async function promptLocation(
projectId: string,
prompt = "Please select a location:",
prompt: string = "Please select a location:",
): Promise<string> {
const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);

Expand All @@ -422,3 +422,42 @@ export async function promptLocation(
choices: allowedLocations,
})) as string;
}

/**
* Fetches a backend from the server. If there are multiple backends with that name (ie multi-regional backends),
* prompts the user to disambiguate.
*/
export async function getBackendForAmbiguousLocation(
projectId: string,
backendId: string,
locationDisambugationPrompt: string,
): Promise<apphosting.Backend> {
let { unreachable, backends } = await apphosting.listBackends(projectId, "-");
if (unreachable && unreachable.length !== 0) {
logWarning(
`The following locations are currently unreachable: ${unreachable}.\n` +
"If your backend is in one of these regions, please try again later.",
);
}
backends = backends.filter(
(backend) => apphosting.parseBackendName(backend.name).id === backendId,
);
if (backends.length === 0) {
throw new FirebaseError(`No backend named "${backendId}" found.`);
}
if (backends.length === 1) {
return backends[0];
}

const backendsByLocation = new Map<string, apphosting.Backend>();
backends.forEach((backend) =>
backendsByLocation.set(apphosting.parseBackendName(backend.name).location, backend),
);
const location = await promptOnce({
name: "location",
type: "list",
message: locationDisambugationPrompt,
choices: [...backendsByLocation.keys()],
});
return backendsByLocation.get(location)!;
}
58 changes: 32 additions & 26 deletions src/commands/apphosting-backends-delete.ts
Expand Up @@ -6,36 +6,31 @@ import { promptOnce } from "../prompt";
import * as utils from "../utils";
import * as apphosting from "../gcp/apphosting";
import { printBackendsTable } from "./apphosting-backends-list";
import { deleteBackendAndPoll, promptLocation } from "../apphosting";
import { deleteBackendAndPoll, getBackendForAmbiguousLocation } from "../apphosting";
import * as ora from "ora";

export const command = new Command("apphosting:backends:delete <backend>")
.description("delete a Firebase App Hosting backend")
.option("-l, --location <location>", "specify the location of the backend", "")
.option("-l, --location <location>", "specify the location of the backend", "-")
.withForce()
.before(apphosting.ensureApiEnabled)
.action(async (backendId: string, options: Options) => {
const projectId = needProjectId(options);
let location = options.location as string;

location =
location ||
(await promptLocation(
let backend: apphosting.Backend;
if (location === "-" || location === "") {
backend = await getBackendForAmbiguousLocation(
projectId,
backendId,
"Please select the location of the backend you'd like to delete:",
));

let backend: apphosting.Backend;
try {
backend = await apphosting.getBackend(projectId, location, backendId);
} catch (err: any) {
throw new FirebaseError(`No backends found with given parameters. Command aborted.`, {
original: err,
});
);
location = apphosting.parseBackendName(backend.name).location;
} else {
backend = await getBackendForLocation(projectId, location, backendId);
}

utils.logWarning("You are about to permanently delete the backend:");
const backends: apphosting.Backend[] = [backend];
printBackendsTable(backends);
utils.logWarning("You are about to permanently delete this backend:");
printBackendsTable([backend]);

const confirmDeletion = await promptOnce(
{
Expand All @@ -47,18 +42,29 @@ export const command = new Command("apphosting:backends:delete <backend>")
options,
);
if (!confirmDeletion) {
throw new FirebaseError("Deletion Aborted");
return;
}

const spinner = ora("Deleting backend...").start();
try {
await deleteBackendAndPoll(projectId, location, backendId);
utils.logSuccess(`Successfully deleted the backend: ${backendId}`);
spinner.succeed(`Successfully deleted the backend: ${backendId}`);
} catch (err: any) {
throw new FirebaseError(
`Failed to delete backend: ${backendId}. Please check the parameters you have provided.`,
{ original: err },
);
spinner.stop();
throw new FirebaseError(`Failed to delete backend: ${backendId}.`, { original: err });
}

return backend;
});

async function getBackendForLocation(
projectId: string,
location: string,
backendId: string,
): Promise<apphosting.Backend> {
try {
return await apphosting.getBackend(projectId, location, backendId);
} catch (err: any) {
throw new FirebaseError(`No backend named "${backendId}" found in ${location}.`, {
original: err,
});
}
}
7 changes: 3 additions & 4 deletions src/commands/apphosting-backends-list.ts
Expand Up @@ -42,14 +42,13 @@ export function printBackendsTable(backends: apphosting.Backend[]): void {
});

for (const backend of backends) {
// sample backend.name value: "projects/<project-name>/locations/us-central1/backends/<backend-id>"
const [backendLocation, , backendId] = backend.name.split("/").slice(3, 6);
const { location, id } = apphosting.parseBackendName(backend.name);
table.push([
backendId,
id,
// sample repository value: "projects/<project-name>/locations/us-central1/connections/<connection-id>/repositories/<repository-name>"
backend.codebase?.repository?.split("/").pop() ?? "",
backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri,
backendLocation,
location,
datetimeString(new Date(backend.updateTime)),
]);
}
Expand Down
11 changes: 11 additions & 0 deletions src/gcp/apphosting.ts
Expand Up @@ -273,6 +273,17 @@ export function serviceAgentEmail(projectNumber: string): string {
return `service-${projectNumber}@${P4SA_DOMAIN}`;
}

/** Splits a backend resource name into its parts. */
export function parseBackendName(backendName: string): {
projectName: string;
location: string;
id: string;
} {
// sample value: "projects/<project-name>/locations/us-central1/backends/<backend-id>"
const [, projectName, , location, , id] = backendName.split("/");
return { projectName, location, id };
}

/**
* Creates a new Backend in a given project and location.
*/
Expand Down
67 changes: 67 additions & 0 deletions src/test/apphosting/index.spec.ts
Expand Up @@ -12,6 +12,7 @@ import {
promptLocation,
setDefaultTrafficPolicy,
ensureAppHostingComputeServiceAccount,
getBackendForAmbiguousLocation,
} from "../../apphosting/index";
import * as deploymentTool from "../../deploymentTool";
import { FirebaseError } from "../../error";
Expand All @@ -24,6 +25,7 @@ describe("apphosting setup functions", () => {
let promptOnceStub: sinon.SinonStub;
let pollOperationStub: sinon.SinonStub;
let createBackendStub: sinon.SinonStub;
let listBackendsStub: sinon.SinonStub;
let deleteBackendStub: sinon.SinonStub;
let updateTrafficStub: sinon.SinonStub;
let listLocationsStub: sinon.SinonStub;
Expand All @@ -37,6 +39,9 @@ describe("apphosting setup functions", () => {
createBackendStub = sinon
.stub(apphosting, "createBackend")
.throws("Unexpected createBackend call");
listBackendsStub = sinon
.stub(apphosting, "listBackends")
.throws("Unexpected listBackends call");
deleteBackendStub = sinon
.stub(apphosting, "deleteBackend")
.throws("Unexpected deleteBackend call");
Expand Down Expand Up @@ -250,4 +255,66 @@ describe("apphosting setup functions", () => {
});
});
});

describe("getBackendForAmbiguousLocation", () => {
const backendFoo = {
name: `projects/${projectId}/locations/${location}/backends/foo`,
labels: {},
createTime: "0",
updateTime: "1",
uri: "https://placeholder.com",
};

const backendFooOtherRegion = {
name: `projects/${projectId}/locations/otherRegion/backends/foo`,
labels: {},
createTime: "0",
updateTime: "1",
uri: "https://placeholder.com",
};

const backendBar = {
name: `projects/${projectId}/locations/${location}/backends/bar`,
labels: {},
createTime: "0",
updateTime: "1",
uri: "https://placeholder.com",
};

it("throws if there are no matching backends", async () => {
listBackendsStub.resolves({ backends: [] });

await expect(
getBackendForAmbiguousLocation(projectId, "baz", /* prompt= */ ""),
).to.be.rejectedWith(/No backend named "baz" found./);
});

it("returns unambiguous backend", async () => {
listBackendsStub.resolves({ backends: [backendFoo, backendBar] });

await expect(
getBackendForAmbiguousLocation(projectId, "foo", /* prompt= */ ""),
).to.eventually.equal(backendFoo);
});

it("prompts for location if backend is ambiguous", async () => {
listBackendsStub.resolves({ backends: [backendFoo, backendFooOtherRegion, backendBar] });
promptOnceStub.resolves(location);

await expect(
getBackendForAmbiguousLocation(
projectId,
"foo",
/* prompt= */ "Please select the location of the backend you'd like to delete:",
),
).to.eventually.equal(backendFoo);

expect(promptOnceStub).to.be.calledWith({
name: "location",
type: "list",
message: "Please select the location of the backend you'd like to delete:",
choices: [location, "otherRegion"],
});
});
});
});

0 comments on commit 540aaab

Please sign in to comment.