Skip to content

Add utility function to list GCB connections w/ well-known names. #6535

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

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
39 changes: 35 additions & 4 deletions src/gcp/cloudbuild.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Client } from "../apiv2";
import { cloudbuildOrigin } from "../api";

const PAGE_SIZE_MAX = 100;

const client = new Client({
urlPrefix: cloudbuildOrigin,
auth: true,
Expand Down Expand Up @@ -42,7 +44,7 @@ type InstallationStage =
type ConnectionOutputOnlyFields = "createTime" | "updateTime" | "installationState" | "reconciling";

export interface Connection {
name?: string;
name: string;
disabled?: boolean;
annotations?: {
[key: string]: string;
Expand All @@ -62,7 +64,7 @@ export interface Connection {
type RepositoryOutputOnlyFields = "createTime" | "updateTime";

export interface Repository {
name?: string;
name: string;
remoteUri: string;
annotations?: {
[key: string]: string;
Expand All @@ -85,7 +87,10 @@ export async function createConnection(
location: string,
connectionId: string
): Promise<Operation> {
const res = await client.post<Omit<Connection, ConnectionOutputOnlyFields>, Operation>(
const res = await client.post<
Omit<Omit<Connection, "name">, ConnectionOutputOnlyFields>,
Operation
>(
`projects/${projectId}/locations/${location}/connections`,
{ githubConfig: {} },
{ queryParams: { connectionId } }
Expand All @@ -106,6 +111,32 @@ export async function getConnection(
return res.body;
}

/**
* List metadata for a Cloud Build V2 Connection.
*/
export async function listConnections(projectId: string, location: string): Promise<Connection[]> {
const conns: Connection[] = [];
const getNextPage = async (pageToken = ""): Promise<void> => {
const res = await client.get<{
connections: Connection[];
nextPageToken?: string;
}>(`/projects/${projectId}/locations/${location}/connections`, {
queryParams: {
pageSize: PAGE_SIZE_MAX,
pageToken,
},
});
if (Array.isArray(res.body.connections)) {
conns.push(...res.body.connections);
}
if (res.body.nextPageToken) {
await getNextPage(res.body.nextPageToken);
}
};
await getNextPage();
return conns;
}

/**
* Deletes a Cloud Build V2 Connection.
*/
Expand Down Expand Up @@ -142,7 +173,7 @@ export async function createRepository(
repositoryId: string,
remoteUri: string
): Promise<Operation> {
const res = await client.post<Omit<Repository, RepositoryOutputOnlyFields>, Operation>(
const res = await client.post<Omit<Repository, RepositoryOutputOnlyFields | "name">, Operation>(
`projects/${projectId}/locations/${location}/connections/${connectionId}/repositories`,
{ remoteUri },
{ queryParams: { repositoryId } }
Expand Down
7 changes: 7 additions & 0 deletions src/init/features/frameworks/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import * as utils from "../../../utils";
import { promptOnce } from "../../../prompt";
import * as clc from "colorette";

const FRAMEWORKS_CONN_PATTERN = /.+\/frameworks-github-conn-.+$/;

const gcbPollerOptions: Omit<poller.OperationPollerOptions, "operationResourceName"> = {
apiOrigin: cloudbuildOrigin,
apiVersion: "v2",
Expand Down Expand Up @@ -188,3 +190,8 @@ export async function getOrCreateRepository(
}
return repo;
}

export async function listFrameworksConnections(projectId: string) {
const conns = await gcb.listConnections(projectId, "-");
return conns.filter((conn) => FRAMEWORKS_CONN_PATTERN.test(conn.name));
}
127 changes: 93 additions & 34 deletions src/test/init/frameworks/repo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,51 @@ import { expect } from "chai";
import * as gcb from "../../../gcp/cloudbuild";
import * as prompt from "../../../prompt";
import * as poller from "../../../operation-poller";
import { FirebaseError } from "../../../error";
import * as repo from "../../../init/features/frameworks/repo";
import * as utils from "../../../utils";
import { Connection } from "../../../gcp/cloudbuild";
import { FirebaseError } from "../../../error";

describe("composer", () => {
const sandbox: sinon.SinonSandbox = sinon.createSandbox();

let promptOnceStub: sinon.SinonStub;
let pollOperationStub: sinon.SinonStub;
let getConnectionStub: sinon.SinonStub;
let getRepositoryStub: sinon.SinonStub;
let createConnectionStub: sinon.SinonStub;
let createRepositoryStub: sinon.SinonStub;
let fetchLinkableRepositoriesStub: sinon.SinonStub;

beforeEach(() => {
promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call");
pollOperationStub = sandbox
.stub(poller, "pollOperation")
.throws("Unexpected pollOperation call");
getConnectionStub = sandbox.stub(gcb, "getConnection").throws("Unexpected getConnection call");
getRepositoryStub = sandbox.stub(gcb, "getRepository").throws("Unexpected getRepository call");
createConnectionStub = sandbox
.stub(gcb, "createConnection")
.throws("Unexpected createConnection call");
createRepositoryStub = sandbox
.stub(gcb, "createRepository")
.throws("Unexpected createRepository call");
fetchLinkableRepositoriesStub = sandbox
.stub(gcb, "fetchLinkableRepositories")
.throws("Unexpected fetchLinkableRepositories call");

sandbox.stub(utils, "openInBrowser").resolves();
});
describe("connect GitHub repo", () => {
const sandbox: sinon.SinonSandbox = sinon.createSandbox();

afterEach(() => {
sandbox.verifyAndRestore();
});
let promptOnceStub: sinon.SinonStub;
let pollOperationStub: sinon.SinonStub;
let getConnectionStub: sinon.SinonStub;
let getRepositoryStub: sinon.SinonStub;
let createConnectionStub: sinon.SinonStub;
let createRepositoryStub: sinon.SinonStub;
let fetchLinkableRepositoriesStub: sinon.SinonStub;

beforeEach(() => {
promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call");
pollOperationStub = sandbox
.stub(poller, "pollOperation")
.throws("Unexpected pollOperation call");
getConnectionStub = sandbox
.stub(gcb, "getConnection")
.throws("Unexpected getConnection call");
getRepositoryStub = sandbox
.stub(gcb, "getRepository")
.throws("Unexpected getRepository call");
createConnectionStub = sandbox
.stub(gcb, "createConnection")
.throws("Unexpected createConnection call");
createRepositoryStub = sandbox
.stub(gcb, "createRepository")
.throws("Unexpected createRepository call");
fetchLinkableRepositoriesStub = sandbox
.stub(gcb, "fetchLinkableRepositories")
.throws("Unexpected fetchLinkableRepositories call");

sandbox.stub(utils, "openInBrowser").resolves();
});

afterEach(() => {
sandbox.verifyAndRestore();
});

describe("connect GitHub repo", () => {
const projectId = "projectId";
const location = "us-central1";
const connectionId = `frameworks-${location}`;
Expand Down Expand Up @@ -130,4 +135,58 @@ describe("composer", () => {
await expect(repo.linkGitHubRepository(projectId, location)).to.be.rejected;
});
});

describe("listFrameworksConnections", () => {
const sandbox: sinon.SinonSandbox = sinon.createSandbox();
let listConnectionsStub: sinon.SinonStub;

const projectId = "projectId";
const location = "us-central1";

function mockConn(id: string): Connection {
return {
name: `projects/${projectId}/locations/${location}/connections/${id}`,
disabled: false,
createTime: "0",
updateTime: "1",
installationState: {
stage: "COMPLETE",
message: "complete",
actionUri: "https://google.com",
},
reconciling: false,
};
}

function extractId(name: string): string {
const parts = name.split("/");
return parts.pop() ?? "";
}

beforeEach(() => {
listConnectionsStub = sandbox
.stub(gcb, "listConnections")
.throws("Unexpected getConnection call");
});

afterEach(() => {
sandbox.verifyAndRestore();
});

it("filters out non-frameworks connections", async () => {
listConnectionsStub.resolves([
mockConn("frameworks-github-conn-baddcafe"),
mockConn("hooray-conn"),
mockConn("frameworks-github-conn-deadbeef"),
mockConn("frameworks-github-oauth"),
]);

const conns = await repo.listFrameworksConnections(projectId);
expect(conns).to.have.length(2);
expect(conns.map((c) => extractId(c.name))).to.include.members([
"frameworks-github-conn-baddcafe",
"frameworks-github-conn-deadbeef",
]);
});
});
});