diff --git a/src/gcp/cloudbuild.ts b/src/gcp/cloudbuild.ts index 488d806ab94..ce1e238fa52 100644 --- a/src/gcp/cloudbuild.ts +++ b/src/gcp/cloudbuild.ts @@ -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, @@ -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; @@ -62,7 +64,7 @@ export interface Connection { type RepositoryOutputOnlyFields = "createTime" | "updateTime"; export interface Repository { - name?: string; + name: string; remoteUri: string; annotations?: { [key: string]: string; @@ -85,7 +87,10 @@ export async function createConnection( location: string, connectionId: string ): Promise { - const res = await client.post, Operation>( + const res = await client.post< + Omit, ConnectionOutputOnlyFields>, + Operation + >( `projects/${projectId}/locations/${location}/connections`, { githubConfig: {} }, { queryParams: { connectionId } } @@ -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 { + const conns: Connection[] = []; + const getNextPage = async (pageToken = ""): Promise => { + 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. */ @@ -142,7 +173,7 @@ export async function createRepository( repositoryId: string, remoteUri: string ): Promise { - const res = await client.post, Operation>( + const res = await client.post, Operation>( `projects/${projectId}/locations/${location}/connections/${connectionId}/repositories`, { remoteUri }, { queryParams: { repositoryId } } diff --git a/src/init/features/frameworks/repo.ts b/src/init/features/frameworks/repo.ts index 6991bd832f0..7d77d7667f7 100644 --- a/src/init/features/frameworks/repo.ts +++ b/src/init/features/frameworks/repo.ts @@ -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 = { apiOrigin: cloudbuildOrigin, apiVersion: "v2", @@ -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)); +} diff --git a/src/test/init/frameworks/repo.spec.ts b/src/test/init/frameworks/repo.spec.ts index 9b80636762f..159e1063fe0 100644 --- a/src/test/init/frameworks/repo.spec.ts +++ b/src/test/init/frameworks/repo.spec.ts @@ -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}`; @@ -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", + ]); + }); + }); });