Skip to content

Commit f10d08a

Browse files
authored
Add proxy support in gitlab (#1032)
* basic proxy * use reflag * updated env var * linting * changeset * try changing var name * add env in turbo
1 parent 48e5f3a commit f10d08a

File tree

14 files changed

+156
-42
lines changed

14 files changed

+156
-42
lines changed

.changeset/young-carrots-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/integration-gitlab': minor
3+
---
4+
5+
Add support for custom proxy

.github/workflows/production.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ jobs:
4747
GITBOOK_ENDPOINT: https://api.gitbook.com
4848
GITBOOK_ORGANIZATION: gitbook
4949
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
50+
# Proxy
51+
GITBOOK_PROXY_URL: ${{ secrets.PROXY_URL }}
52+
GITBOOK_PROXY_SECRET: ${{ secrets.PROXY_SECRET }}
53+
REFLAG_SECRET_KEY: ${{ secrets.REFLAG_SECRET_KEY }}
5054
# GitHub Files
5155
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
5256
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}

.github/workflows/release.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ jobs:
6565
GITBOOK_ENDPOINT: https://api.gitbook-staging.com
6666
GITBOOK_ORGANIZATION: gitbookio
6767
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
68+
# Proxy
69+
GITBOOK_PROXY_URL: ${{ secrets.PROXY_STAGING_URL }}
70+
GITBOOK_PROXY_SECRET: ${{ secrets.PROXY_STAGING_SECRET }}
71+
REFLAG_SECRET_KEY: ${{ secrets.REFLAG_STAGING_SECRET_KEY }}
6872
# GitHub Files
6973
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
7074
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}

.github/workflows/test.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ jobs:
8181
run: bun run check
8282
env:
8383
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
84+
# Proxy
85+
GITBOOK_PROXY_URL: ${{ secrets.PROXY_STAGING_URL }}
86+
GITBOOK_PROXY_SECRET: ${{ secrets.PROXY_STAGING_SECRET }}
87+
REFLAG_SECRET_KEY: ${{ secrets.REFLAG_STAGING_SECRET_KEY }}
8488
# GitHub Files
8589
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
8690
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}

bun.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@
313313
},
314314
"integrations/intercom-conversations": {
315315
"name": "@gitbook/integration-intercom-conversations",
316-
"version": "0.1.0",
316+
"version": "0.2.0",
317317
"dependencies": {
318318
"@gitbook/api": "*",
319319
"@gitbook/runtime": "*",
@@ -726,7 +726,7 @@
726726
},
727727
"packages/api": {
728728
"name": "@gitbook/api",
729-
"version": "0.145.0",
729+
"version": "0.147.0",
730730
"dependencies": {
731731
"event-iterator": "^2.0.0",
732732
"eventsource-parser": "^3.0.0",
@@ -741,7 +741,7 @@
741741
},
742742
"packages/cli": {
743743
"name": "@gitbook/cli",
744-
"version": "0.26.0",
744+
"version": "0.26.2",
745745
"bin": {
746746
"gitbook": "./cli.js",
747747
},

integrations/gitlab/gitbook-manifest.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,8 @@ scopes:
4141
configurations:
4242
space:
4343
componentId: configure
44+
secrets:
45+
PROXY_URL: ${{ env.GITBOOK_PROXY_URL }}
46+
PROXY_SECRET: ${{ env.GITBOOK_PROXY_SECRET }}
47+
REFLAG_SECRET_KEY: ${{ env.REFLAG_SECRET_KEY }}
4448
target: space

integrations/gitlab/src/api.ts

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import LinkHeader from 'http-link-header';
22

33
import { Logger, ExposableError } from '@gitbook/runtime';
44

5-
import type { GitLabSpaceConfiguration } from './types';
5+
import type { GitLabRuntimeContext, GitLabSpaceConfiguration } from './types';
6+
import { signResponse } from './utils';
67

78
const logger = Logger('gitlab:api');
89

@@ -38,8 +39,11 @@ interface GLFetchOptions {
3839
/**
3940
* Fetch the current GitLab user. It will use the access token from the environment.
4041
*/
41-
export async function getCurrentUser(config: GitLabSpaceConfiguration) {
42-
const user = await gitlabAPI<GLUser>(config, {
42+
export async function getCurrentUser(
43+
context: GitLabRuntimeContext,
44+
config: GitLabSpaceConfiguration,
45+
) {
46+
const user = await gitlabAPI<GLUser>(context, config, {
4347
path: '/user',
4448
});
4549

@@ -51,10 +55,11 @@ export async function getCurrentUser(config: GitLabSpaceConfiguration) {
5155
* the access token from the environment.
5256
*/
5357
export async function fetchProjects(
58+
context: GitLabRuntimeContext,
5459
config: GitLabSpaceConfiguration,
5560
options: GLFetchOptions = {},
5661
) {
57-
const projects = await gitlabAPI<Array<GLProject>>(config, {
62+
const projects = await gitlabAPI<Array<GLProject>>(context, config, {
5863
path: '/projects',
5964
params: {
6065
membership: true,
@@ -71,11 +76,12 @@ export async function fetchProjects(
7176
* Search currently authenticated user projects for a given query.
7277
*/
7378
export async function searchUserProjects(
79+
context: GitLabRuntimeContext,
7480
config: GitLabSpaceConfiguration,
7581
search: string,
7682
options: GLFetchOptions = {},
7783
) {
78-
const projects = await gitlabAPI<Array<GLProject>>(config, {
84+
const projects = await gitlabAPI<Array<GLProject>>(context, config, {
7985
path: `/users/${config.userId}/projects`,
8086
params: {
8187
search,
@@ -91,8 +97,12 @@ export async function searchUserProjects(
9197
/**
9298
* Fetch a GitLab project by its ID.
9399
*/
94-
export async function fetchProject(config: GitLabSpaceConfiguration, projectId: number) {
95-
const project = await gitlabAPI<GLProject>(config, {
100+
export async function fetchProject(
101+
context: GitLabRuntimeContext,
102+
config: GitLabSpaceConfiguration,
103+
projectId: number,
104+
) {
105+
const project = await gitlabAPI<GLProject>(context, config, {
96106
path: `/projects/${projectId}`,
97107
});
98108

@@ -102,8 +112,12 @@ export async function fetchProject(config: GitLabSpaceConfiguration, projectId:
102112
/**
103113
* Fetch all branches for a given project repository.
104114
*/
105-
export async function fetchProjectBranches(config: GitLabSpaceConfiguration, projectId: number) {
106-
const branches = await gitlabAPI<Array<GLBranch>>(config, {
115+
export async function fetchProjectBranches(
116+
context: GitLabRuntimeContext,
117+
config: GitLabSpaceConfiguration,
118+
projectId: number,
119+
) {
120+
const branches = await gitlabAPI<Array<GLBranch>>(context, config, {
107121
path: `/projects/${projectId}/repository/branches`,
108122
params: {
109123
per_page: 100,
@@ -118,12 +132,13 @@ export async function fetchProjectBranches(config: GitLabSpaceConfiguration, pro
118132
* Configure a GitLab webhook for a given project.
119133
*/
120134
export async function addProjectWebhook(
135+
context: GitLabRuntimeContext,
121136
config: GitLabSpaceConfiguration,
122137
projectId: number,
123138
webhookUrl: string,
124139
webhookToken: string,
125140
) {
126-
const { id } = await gitlabAPI<{ id: number }>(config, {
141+
const { id } = await gitlabAPI<{ id: number }>(context, config, {
127142
method: 'POST',
128143
path: `/projects/${projectId}/hooks`,
129144
body: {
@@ -141,11 +156,12 @@ export async function addProjectWebhook(
141156
* Delete a GitLab webhook for a given project.
142157
*/
143158
export async function deleteProjectWebhook(
159+
context: GitLabRuntimeContext,
144160
config: GitLabSpaceConfiguration,
145161
projectId: number,
146162
webhookId: number,
147163
) {
148-
await gitlabAPI(config, {
164+
await gitlabAPI(context, config, {
149165
method: 'DELETE',
150166
path: `/projects/${projectId}/hooks/${webhookId}`,
151167
});
@@ -155,12 +171,13 @@ export async function deleteProjectWebhook(
155171
* Create a commit status for a commit SHA.
156172
*/
157173
export async function editCommitStatus(
174+
context: GitLabRuntimeContext,
158175
config: GitLabSpaceConfiguration,
159176
projectId: number,
160177
sha: string,
161178
status: object,
162179
): Promise<void> {
163-
await gitlabAPI(config, {
180+
await gitlabAPI(context, config, {
164181
method: 'POST',
165182
path: `/projects/${projectId}/statuses/${sha}`,
166183
body: status,
@@ -171,6 +188,7 @@ export async function editCommitStatus(
171188
* Execute a GitLab API request.
172189
*/
173190
export async function gitlabAPI<T>(
191+
context: GitLabRuntimeContext,
174192
config: GitLabSpaceConfiguration,
175193
request: {
176194
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
@@ -209,7 +227,7 @@ export async function gitlabAPI<T>(
209227
body: body ? JSON.stringify(body) : undefined,
210228
};
211229

212-
const response = await requestGitLab(token, url, options);
230+
const response = await requestGitLab(context, token, url, options);
213231

214232
const isJSONResponse = response.headers.get('Content-Type')?.includes('application/json');
215233
if (!isJSONResponse) {
@@ -234,7 +252,7 @@ export async function gitlabAPI<T>(
234252
const nextURLSearchParams = Object.fromEntries(nextURL.searchParams);
235253
if (nextURLSearchParams.page) {
236254
url.searchParams.set('page', nextURLSearchParams.page as string);
237-
const nextResponse = await requestGitLab(token, url, options);
255+
const nextResponse = await requestGitLab(context, token, url, options);
238256
const nextData = await nextResponse.json();
239257
// @ts-ignore
240258
data = [...data, ...(paginatedListProperty ? nextData[listProperty] : nextData)];
@@ -253,21 +271,35 @@ export async function gitlabAPI<T>(
253271
* It will throw an error if the response is not ok.
254272
*/
255273
async function requestGitLab(
274+
context: GitLabRuntimeContext,
256275
token: string,
257276
url: URL,
258277
options: RequestInit = {},
259278
): Promise<Response> {
260279
logger.debug(`GitLab API -> [${options.method}] ${url.toString()}`);
261-
const response = await fetch(url.toString(), {
262-
...options,
263-
headers: {
264-
...options.headers,
265-
...(options.body ? { 'Content-Type': 'application/json' } : {}),
266-
Accept: 'application/json',
267-
Authorization: `Bearer ${token}`,
268-
'User-Agent': 'GitLab-Integration-Worker',
269-
},
270-
});
280+
// Hardcoded test org, will need to switch to use Reflag for that.
281+
const useProxy = await shouldUseProxy(context);
282+
const response = useProxy
283+
? await proxyRequest(context, url.toString(), {
284+
...options,
285+
headers: {
286+
...options.headers,
287+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
288+
Accept: 'application/json',
289+
Authorization: `Bearer ${token}`,
290+
'User-Agent': 'GitLab-Integration-Worker',
291+
},
292+
})
293+
: await fetch(url.toString(), {
294+
...options,
295+
headers: {
296+
...options.headers,
297+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
298+
Accept: 'application/json',
299+
Authorization: `Bearer ${token}`,
300+
'User-Agent': 'GitLab-Integration-Worker',
301+
},
302+
});
271303

272304
if (!response.ok) {
273305
const text = await response.text();
@@ -303,3 +335,51 @@ export function getAccessTokenOrThrow(config: GitLabSpaceConfiguration): string
303335

304336
return accessToken;
305337
}
338+
339+
export async function proxyRequest(
340+
context: GitLabRuntimeContext,
341+
url: string,
342+
options: RequestInit = {},
343+
): Promise<Response> {
344+
const signature = await signResponse(url, context.environment.secrets.PROXY_SECRET);
345+
const proxyUrl = new URL(context.environment.secrets.PROXY_URL);
346+
347+
proxyUrl.searchParams.set('target', url);
348+
349+
return fetch(proxyUrl.toString(), {
350+
...options,
351+
headers: {
352+
...options.headers,
353+
'X-Gitbook-Proxy-Signature': signature,
354+
},
355+
});
356+
}
357+
358+
export async function shouldUseProxy(context: GitLabRuntimeContext): Promise<boolean> {
359+
const companyId = context.environment.installation?.target.organization;
360+
if (!companyId) {
361+
return false;
362+
}
363+
try {
364+
const response = await fetch(
365+
`https://front.reflag.com/features/enabled?context.company.id=${companyId}&key=GIT_SYNC_STATIC_IP`,
366+
{
367+
method: 'GET',
368+
headers: {
369+
Authorization: `Bearer ${context.environment.secrets.REFLAG_SECRET_KEY}`,
370+
'Content-Type': 'application/json',
371+
},
372+
},
373+
);
374+
375+
const json = (await response.json()) as {
376+
features: { GIT_SYNC_STATIC_IP: { isEnabled: boolean } };
377+
};
378+
const flag = json.features.GIT_SYNC_STATIC_IP;
379+
380+
return flag.isEnabled;
381+
} catch (e) {
382+
logger.error('Error checking Reflag feature flag:', e);
383+
return false;
384+
}
385+
}

integrations/gitlab/src/components.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export const configBlock = createComponent<
6868
: undefined,
6969
};
7070

71-
const glUser = await getCurrentUser(config);
71+
const glUser = await getCurrentUser(context, config);
7272

7373
await context.api.integrations.updateIntegrationSpaceInstallation(
7474
spaceInstallation.integration,
@@ -149,7 +149,7 @@ export const configBlock = createComponent<
149149

150150
case 'save.configuration': {
151151
await saveSpaceConfiguration(context, element.state);
152-
return { type: 'complete' };
152+
return { type: 'complete' as const };
153153
}
154154
}
155155
},

integrations/gitlab/src/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ const handleFetchEvent: FetchEventCallback<GitLabRuntimeContext> = async (reques
164164
if (querySelectedProject) {
165165
try {
166166
const selectedProject = await fetchProject(
167+
context,
167168
spaceConfig,
168169
parseInt(querySelectedProject, 10),
169170
);
@@ -180,7 +181,7 @@ const handleFetchEvent: FetchEventCallback<GitLabRuntimeContext> = async (reques
180181

181182
if (queryProject) {
182183
const q = encodeURIComponent(queryProject);
183-
const searchedProjects = await searchUserProjects(spaceConfig, q, {
184+
const searchedProjects = await searchUserProjects(context, spaceConfig, q, {
184185
page: 1,
185186
per_page: 100,
186187
walkPagination: false,
@@ -201,7 +202,7 @@ const handleFetchEvent: FetchEventCallback<GitLabRuntimeContext> = async (reques
201202
});
202203
} else {
203204
const page = pageNumber || 1;
204-
const projects = await fetchProjects(spaceConfig, {
205+
const projects = await fetchProjects(context, spaceConfig, {
205206
page,
206207
per_page: 100,
207208
walkPagination: false,
@@ -251,7 +252,7 @@ const handleFetchEvent: FetchEventCallback<GitLabRuntimeContext> = async (reques
251252
const querySelectedBranch =
252253
selectedBranch && typeof selectedBranch === 'string' ? selectedBranch : undefined;
253254

254-
const branches = projectId ? await fetchProjectBranches(config, projectId) : [];
255+
const branches = projectId ? await fetchProjectBranches(context, config, projectId) : [];
255256

256257
const data = branches.map(
257258
(branch): ContentKitSelectOption => ({
@@ -405,7 +406,7 @@ const handleSpaceInstallationDeleted: EventCallback<
405406
return;
406407
}
407408

408-
await uninstallWebhook(configuration);
409+
await uninstallWebhook(context, configuration);
409410
};
410411

411412
export default createIntegration({

0 commit comments

Comments
 (0)