Skip to content

Commit 99af335

Browse files
authored
Add utility function to list GCB connections w/ well-known names. (#6535)
Update naming convention for GCB connection maintained by the frameworks service. In the meantime, add a small utility feature that can filters all GCB connections for frameworks-maintained ones.
1 parent eb92701 commit 99af335

File tree

3 files changed

+135
-38
lines changed

3 files changed

+135
-38
lines changed

src/gcp/cloudbuild.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Client } from "../apiv2";
22
import { cloudbuildOrigin } from "../api";
33

4+
const PAGE_SIZE_MAX = 100;
5+
46
const client = new Client({
57
urlPrefix: cloudbuildOrigin,
68
auth: true,
@@ -42,7 +44,7 @@ type InstallationStage =
4244
type ConnectionOutputOnlyFields = "createTime" | "updateTime" | "installationState" | "reconciling";
4345

4446
export interface Connection {
45-
name?: string;
47+
name: string;
4648
disabled?: boolean;
4749
annotations?: {
4850
[key: string]: string;
@@ -62,7 +64,7 @@ export interface Connection {
6264
type RepositoryOutputOnlyFields = "createTime" | "updateTime";
6365

6466
export interface Repository {
65-
name?: string;
67+
name: string;
6668
remoteUri: string;
6769
annotations?: {
6870
[key: string]: string;
@@ -85,7 +87,10 @@ export async function createConnection(
8587
location: string,
8688
connectionId: string
8789
): Promise<Operation> {
88-
const res = await client.post<Omit<Connection, ConnectionOutputOnlyFields>, Operation>(
90+
const res = await client.post<
91+
Omit<Omit<Connection, "name">, ConnectionOutputOnlyFields>,
92+
Operation
93+
>(
8994
`projects/${projectId}/locations/${location}/connections`,
9095
{ githubConfig: {} },
9196
{ queryParams: { connectionId } }
@@ -106,6 +111,32 @@ export async function getConnection(
106111
return res.body;
107112
}
108113

114+
/**
115+
* List metadata for a Cloud Build V2 Connection.
116+
*/
117+
export async function listConnections(projectId: string, location: string): Promise<Connection[]> {
118+
const conns: Connection[] = [];
119+
const getNextPage = async (pageToken = ""): Promise<void> => {
120+
const res = await client.get<{
121+
connections: Connection[];
122+
nextPageToken?: string;
123+
}>(`/projects/${projectId}/locations/${location}/connections`, {
124+
queryParams: {
125+
pageSize: PAGE_SIZE_MAX,
126+
pageToken,
127+
},
128+
});
129+
if (Array.isArray(res.body.connections)) {
130+
conns.push(...res.body.connections);
131+
}
132+
if (res.body.nextPageToken) {
133+
await getNextPage(res.body.nextPageToken);
134+
}
135+
};
136+
await getNextPage();
137+
return conns;
138+
}
139+
109140
/**
110141
* Deletes a Cloud Build V2 Connection.
111142
*/
@@ -142,7 +173,7 @@ export async function createRepository(
142173
repositoryId: string,
143174
remoteUri: string
144175
): Promise<Operation> {
145-
const res = await client.post<Omit<Repository, RepositoryOutputOnlyFields>, Operation>(
176+
const res = await client.post<Omit<Repository, RepositoryOutputOnlyFields | "name">, Operation>(
146177
`projects/${projectId}/locations/${location}/connections/${connectionId}/repositories`,
147178
{ remoteUri },
148179
{ queryParams: { repositoryId } }

src/init/features/frameworks/repo.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import * as utils from "../../../utils";
77
import { promptOnce } from "../../../prompt";
88
import * as clc from "colorette";
99

10+
const FRAMEWORKS_CONN_PATTERN = /.+\/frameworks-github-conn-.+$/;
11+
1012
const gcbPollerOptions: Omit<poller.OperationPollerOptions, "operationResourceName"> = {
1113
apiOrigin: cloudbuildOrigin,
1214
apiVersion: "v2",
@@ -188,3 +190,8 @@ export async function getOrCreateRepository(
188190
}
189191
return repo;
190192
}
193+
194+
export async function listFrameworksConnections(projectId: string) {
195+
const conns = await gcb.listConnections(projectId, "-");
196+
return conns.filter((conn) => FRAMEWORKS_CONN_PATTERN.test(conn.name));
197+
}

src/test/init/frameworks/repo.spec.ts

Lines changed: 93 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,51 @@ import { expect } from "chai";
44
import * as gcb from "../../../gcp/cloudbuild";
55
import * as prompt from "../../../prompt";
66
import * as poller from "../../../operation-poller";
7-
import { FirebaseError } from "../../../error";
87
import * as repo from "../../../init/features/frameworks/repo";
98
import * as utils from "../../../utils";
9+
import { Connection } from "../../../gcp/cloudbuild";
10+
import { FirebaseError } from "../../../error";
1011

1112
describe("composer", () => {
12-
const sandbox: sinon.SinonSandbox = sinon.createSandbox();
13-
14-
let promptOnceStub: sinon.SinonStub;
15-
let pollOperationStub: sinon.SinonStub;
16-
let getConnectionStub: sinon.SinonStub;
17-
let getRepositoryStub: sinon.SinonStub;
18-
let createConnectionStub: sinon.SinonStub;
19-
let createRepositoryStub: sinon.SinonStub;
20-
let fetchLinkableRepositoriesStub: sinon.SinonStub;
21-
22-
beforeEach(() => {
23-
promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call");
24-
pollOperationStub = sandbox
25-
.stub(poller, "pollOperation")
26-
.throws("Unexpected pollOperation call");
27-
getConnectionStub = sandbox.stub(gcb, "getConnection").throws("Unexpected getConnection call");
28-
getRepositoryStub = sandbox.stub(gcb, "getRepository").throws("Unexpected getRepository call");
29-
createConnectionStub = sandbox
30-
.stub(gcb, "createConnection")
31-
.throws("Unexpected createConnection call");
32-
createRepositoryStub = sandbox
33-
.stub(gcb, "createRepository")
34-
.throws("Unexpected createRepository call");
35-
fetchLinkableRepositoriesStub = sandbox
36-
.stub(gcb, "fetchLinkableRepositories")
37-
.throws("Unexpected fetchLinkableRepositories call");
38-
39-
sandbox.stub(utils, "openInBrowser").resolves();
40-
});
13+
describe("connect GitHub repo", () => {
14+
const sandbox: sinon.SinonSandbox = sinon.createSandbox();
4115

42-
afterEach(() => {
43-
sandbox.verifyAndRestore();
44-
});
16+
let promptOnceStub: sinon.SinonStub;
17+
let pollOperationStub: sinon.SinonStub;
18+
let getConnectionStub: sinon.SinonStub;
19+
let getRepositoryStub: sinon.SinonStub;
20+
let createConnectionStub: sinon.SinonStub;
21+
let createRepositoryStub: sinon.SinonStub;
22+
let fetchLinkableRepositoriesStub: sinon.SinonStub;
23+
24+
beforeEach(() => {
25+
promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call");
26+
pollOperationStub = sandbox
27+
.stub(poller, "pollOperation")
28+
.throws("Unexpected pollOperation call");
29+
getConnectionStub = sandbox
30+
.stub(gcb, "getConnection")
31+
.throws("Unexpected getConnection call");
32+
getRepositoryStub = sandbox
33+
.stub(gcb, "getRepository")
34+
.throws("Unexpected getRepository call");
35+
createConnectionStub = sandbox
36+
.stub(gcb, "createConnection")
37+
.throws("Unexpected createConnection call");
38+
createRepositoryStub = sandbox
39+
.stub(gcb, "createRepository")
40+
.throws("Unexpected createRepository call");
41+
fetchLinkableRepositoriesStub = sandbox
42+
.stub(gcb, "fetchLinkableRepositories")
43+
.throws("Unexpected fetchLinkableRepositories call");
44+
45+
sandbox.stub(utils, "openInBrowser").resolves();
46+
});
47+
48+
afterEach(() => {
49+
sandbox.verifyAndRestore();
50+
});
4551

46-
describe("connect GitHub repo", () => {
4752
const projectId = "projectId";
4853
const location = "us-central1";
4954
const connectionId = `frameworks-${location}`;
@@ -130,4 +135,58 @@ describe("composer", () => {
130135
await expect(repo.linkGitHubRepository(projectId, location)).to.be.rejected;
131136
});
132137
});
138+
139+
describe("listFrameworksConnections", () => {
140+
const sandbox: sinon.SinonSandbox = sinon.createSandbox();
141+
let listConnectionsStub: sinon.SinonStub;
142+
143+
const projectId = "projectId";
144+
const location = "us-central1";
145+
146+
function mockConn(id: string): Connection {
147+
return {
148+
name: `projects/${projectId}/locations/${location}/connections/${id}`,
149+
disabled: false,
150+
createTime: "0",
151+
updateTime: "1",
152+
installationState: {
153+
stage: "COMPLETE",
154+
message: "complete",
155+
actionUri: "https://google.com",
156+
},
157+
reconciling: false,
158+
};
159+
}
160+
161+
function extractId(name: string): string {
162+
const parts = name.split("/");
163+
return parts.pop() ?? "";
164+
}
165+
166+
beforeEach(() => {
167+
listConnectionsStub = sandbox
168+
.stub(gcb, "listConnections")
169+
.throws("Unexpected getConnection call");
170+
});
171+
172+
afterEach(() => {
173+
sandbox.verifyAndRestore();
174+
});
175+
176+
it("filters out non-frameworks connections", async () => {
177+
listConnectionsStub.resolves([
178+
mockConn("frameworks-github-conn-baddcafe"),
179+
mockConn("hooray-conn"),
180+
mockConn("frameworks-github-conn-deadbeef"),
181+
mockConn("frameworks-github-oauth"),
182+
]);
183+
184+
const conns = await repo.listFrameworksConnections(projectId);
185+
expect(conns).to.have.length(2);
186+
expect(conns.map((c) => extractId(c.name))).to.include.members([
187+
"frameworks-github-conn-baddcafe",
188+
"frameworks-github-conn-deadbeef",
189+
]);
190+
});
191+
});
133192
});

0 commit comments

Comments
 (0)