Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add developer connect debug commands #7007

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/apphosting/githubConnections.ts
Expand Up @@ -22,7 +22,7 @@
const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/;
const APPHOSTING_OAUTH_CONN_NAME = "apphosting-github-oauth";
const CONNECTION_NAME_REGEX =
/^projects\/(?<projectId>[^\/]+)\/locations\/(?<location>[^\/]+)\/connections\/(?<id>[^\/]+)$/;

Check warning on line 25 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unnecessary escape character: \/

Check warning on line 25 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unnecessary escape character: \/

Check warning on line 25 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unnecessary escape character: \/

/**
* Exported for unit testing.
Expand Down Expand Up @@ -127,7 +127,7 @@
} while (repoCloneUri === ADD_CONN_CHOICE);

// Ensure that the selected connection exists in the same region as the backend
const { id: connectionId } = parseConnectionName(connection.name)!;

Check warning on line 130 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
await getOrCreateConnection(projectId, location, connectionId, {
authorizerCredential: connection.githubConfig?.authorizerCredential,
appInstallationId: connection.githubConfig?.appInstallationId,
Expand Down Expand Up @@ -192,7 +192,7 @@
try {
conn = await devConnect.getConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
} catch (err: unknown) {
if ((err as any).status === 404) {

Check warning on line 195 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .status on an `any` value

Check warning on line 195 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
// Cloud build P4SA requires the secret manager admin role.
// This is required when creating an initial connection which is the Oauth connection in our case.
await ensureSecretManagerAdminGrant(projectId);
Expand All @@ -215,7 +215,7 @@
message: "Press Enter once you have authorized the app",
});
cleanup();
const { projectId, location, id } = parseConnectionName(conn.name)!;

Check warning on line 218 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
conn = await devConnect.getConnection(projectId, location, id);
}

Expand All @@ -231,7 +231,7 @@
type: "autocomplete",
name: "cloneUri",
message: "Which repository would you like to deploy?",
source: (_: any, input = ""): Promise<(inquirer.DistinctChoice | inquirer.Separator)[]> => {

Check warning on line 234 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
return new Promise((resolve) =>
resolve([
new inquirer.Separator(),
Expand Down Expand Up @@ -301,9 +301,9 @@
["roles/secretmanager.admin"],
/* skipAccountLookup= */ true,
);
} catch (e: any) {

Check warning on line 304 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
// if the dev connect P4SA doesn't exist in the project, generate one
if (e?.code === 400 || e?.status === 400) {

Check warning on line 306 in src/apphosting/githubConnections.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .code on an `any` value
await devConnect.generateP4SA(projectNumber);
await rm.addServiceAccountToRoles(
projectId,
Expand Down Expand Up @@ -443,3 +443,17 @@
}
return { cloneUris: Object.keys(cloneUriToConnection), cloneUriToConnection };
}

/**
* checks if the given connection name is an apphosting connection
*/
export function isApphostingConnection(name: string): boolean {
const match = CONNECTION_NAME_REGEX.exec(name);

if (!match || typeof match.groups === undefined) {
return false;
}

const { id } = match.groups as unknown as ConnectionNameParts;
return APPHOSTING_CONN_PATTERN.test(id) || id === APPHOSTING_OAUTH_CONN_NAME;
}
59 changes: 58 additions & 1 deletion src/apphosting/index.ts
Expand Up @@ -21,7 +21,13 @@ import { DEFAULT_REGION } from "./constants";
import { ensure } from "../ensureApiEnabled";
import * as deploymentTool from "../deploymentTool";
import { DeepOmit } from "../metaprogramming";
import { GitRepositoryLink } from "../gcp/devConnect";
import {
Connection,
GitRepositoryLink,
deleteConnection,
getConnection,
listAllConnections,
} from "../gcp/devConnect";

const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";

Expand Down Expand Up @@ -357,3 +363,54 @@ export async function orchestrateRollout(
}
return { rollout, build };
}

/**
* lists dev connect connections
*/
export async function listDeveloperConnectAppHostingConnections(
projectId: string,
location: string,
): Promise<Connection[]> {
const connections = await listAllConnections(projectId, location);
const appHostingConnections = connections.filter((connection) =>
githubConnections.isApphostingConnection(connection.name),
);

return appHostingConnections;
}

/**
* delete a dev connect apphosting connections
*/
export async function deleteDeveloperConnectAppHostingConnection(
projectId: string,
location: string,
connectionId: string,
): Promise<void> {
const connection = await getConnection(projectId, location, connectionId);

if (!githubConnections.isApphostingConnection(connection.name)) {
throw new Error(
`Unable to delete connection "${connection.name}" as it is not an apphosting conneciton`,
);
}

await deleteConnection(projectId, location, connectionId);
}

/**
* deletes all dev connect apphosting connections
*/
export async function deleteAllDeveloperConnectAppHostingConnection(
projectId: string,
location: string,
): Promise<void> {
const connections = await listDeveloperConnectAppHostingConnections(projectId, location);
for (let i = 0; i < connections.length; i++) {
const connection = connections[i];
const { id } = githubConnections.parseConnectionName(connection.name)!;

logBullet(`Deleting connection ${id}`);
await deleteConnection(projectId, location, id);
}
}
40 changes: 40 additions & 0 deletions src/commands/apphosting-connections-delete.ts
@@ -0,0 +1,40 @@
import { Command } from "../command";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import requireInteractive from "../requireInteractive";
import {
deleteAllDeveloperConnectAppHostingConnection,
deleteDeveloperConnectAppHostingConnection,
} from "../apphosting";
import { FirebaseError } from "../error";

export const command = new Command("apphosting:connections:delete")
.description("deletes all connections for the current project")
.option("-l, --location <location>", "specify the region of the connection")
.option("-c, --connectionId <connectionId>", "specify the id of connection you want to delete")
.option("-a, --all", "deletes all apphosting connections for this project", false)
.before(requireInteractive)
.action(async (options: Options) => {
const projectId = needProjectId(options);
const location = options.location as string | null;
const connectionId = options.connectionId as string | null;
const deleteAll = options.all as boolean;

if (!connectionId && deleteAll === false) {
throw new FirebaseError(
"To delete a connection a connectionId is required. See `firebase apphosting:backends:delete --help`",
);
}

if (!location) {
throw new FirebaseError(
"A location is required. See `firebase apphosting:backends:delete --help`",
);
}

if (connectionId) {
await deleteDeveloperConnectAppHostingConnection(projectId, location, connectionId);
} else if (deleteAll) {
await deleteAllDeveloperConnectAppHostingConnection(projectId, location);
}
});
29 changes: 29 additions & 0 deletions src/commands/apphosting-connections-list.ts
@@ -0,0 +1,29 @@
import { Command } from "../command";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import requireInteractive from "../requireInteractive";
import { listDeveloperConnectAppHostingConnections } from "../apphosting";
import { logBullet } from "../utils";
import { FirebaseError } from "../error";

export const command = new Command("apphosting:connections:list")
.description("lists all dev connect connections for the current project")
.option("-l, --location <location>", "specify the region of the connection")
.before(requireInteractive)
.action(async (options: Options) => {
const projectId = needProjectId(options);
const location = options.location as string | null;

if (!location) {
throw new FirebaseError(
"A location is requried. See `firebase apphosting:connections:list --help`",
);
}

const connections = await listDeveloperConnectAppHostingConnections(projectId, location);

for (let i = 0; i < connections.length; i++) {
const connection = connections[i];
logBullet(connection.name);
}
});
3 changes: 3 additions & 0 deletions src/commands/index.ts
Expand Up @@ -180,6 +180,9 @@ export function load(client: any): any {
client.apphosting.rollouts = {};
client.apphosting.rollouts.create = loadCommand("apphosting-rollouts-create");
client.apphosting.rollouts.list = loadCommand("apphosting-rollouts-list");
client.apphosting.connections = {};
client.apphosting.connections.delete = loadCommand("apphosting-connections-delete");
client.apphosting.connections.list = loadCommand("apphosting-connections-list");
}
}
client.login = loadCommand("login");
Expand Down