Skip to content

Commit

Permalink
delete
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyjhuang committed Apr 27, 2024
1 parent 017693f commit 1c8d3aa
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 31 deletions.
38 changes: 38 additions & 0 deletions src/apphosting/index.ts
Expand Up @@ -422,3 +422,41 @@ 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,
): Promise<apphosting.Backend> {
let { unreachable, backends } = await apphosting.listBackends(projectId, "-");
if (unreachable && unreachable.length !== 0) {
logWarning(
`The following locations are currently unreachable: ${unreachable}.\n` +

Check warning on line 437 in src/apphosting/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "string[]" of template literal expression
"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({

Check warning on line 455 in src/apphosting/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
name: "location",
type: "list",
message: "Please select the location of the backend you'd like to delete:",
choices: [...backendsByLocation.keys()],
});
return backendsByLocation.get(location)!;
}
56 changes: 29 additions & 27 deletions src/commands/apphosting-backends-delete.ts
Expand Up @@ -6,36 +6,27 @@ 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(
projectId,
"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,
});
if (location === "-" || location === "") {
backend = await getBackendForAmbiguousLocation(projectId, backendId);
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 +38,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
63 changes: 63 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,62 @@ describe("apphosting setup functions", () => {
});
});
});

describe.only("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")).to.be.rejectedWith(
/No backend named "baz" found./,
);
});

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

await expect(getBackendForAmbiguousLocation(projectId, "foo")).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")).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 1c8d3aa

Please sign in to comment.