Skip to content

Commit b248ed7

Browse files
committed
fix(gitlab): iterate over pagination
Current implementation assume that the API return the whole list, which is not truth. Related: #1916 Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
1 parent 4623cc4 commit b248ed7

File tree

5 files changed

+121
-86
lines changed

5 files changed

+121
-86
lines changed

plugins/gitlab/src/class.ts

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { createHash } from 'node:crypto'
22
import { PluginApi, type Project, type UniqueRepo } from '@cpn-console/hooks'
3-
import type { AccessTokenScopes, CommitAction, GroupSchema, GroupStatisticsSchema, MemberSchema, ProjectVariableSchema, VariableSchema } from '@gitbeaker/rest'
4-
import type { AllRepositoryTreesOptions, CondensedProjectSchema, Gitlab, PaginationRequestOptions, ProjectSchema, RepositoryFileExpandedSchema, RepositoryTreeSchema } from '@gitbeaker/core'
3+
import type { AccessTokenScopes, CommitAction, GroupSchema, MemberSchema, ProjectVariableSchema, VariableSchema } from '@gitbeaker/rest'
4+
import type { AllRepositoryTreesOptions, CondensedProjectSchema, Gitlab, ProjectSchema, RepositoryFileExpandedSchema } from '@gitbeaker/core'
55
import { AccessLevel } from '@gitbeaker/core'
66
import type { VaultProjectApi } from '@cpn-console/vault-plugin/types/vault-project-api.js'
77
import { objectEntries } from '@cpn-console/shared'
88
import type { GitbeakerRequestError } from '@gitbeaker/requester-utils'
9-
import { getApi, getGroupRootId, infraAppsRepoName, internalMirrorRepoName } from './utils.js'
9+
import { find, getApi, getAll, getGroupRootId, infraAppsRepoName, internalMirrorRepoName, iter } from './utils.js'
1010
import config from './config.js'
1111

1212
type setVariableResult = 'created' | 'updated' | 'already up-to-date'
@@ -69,7 +69,7 @@ export class GitlabApi extends PluginApi {
6969
): Promise<boolean> {
7070
let action: CommitAction['action'] = 'create'
7171

72-
const branches = await this.api.Branches.all(repoId)
72+
const branches = await getAll(iter(opts => this.api.Branches.all(repoId, opts)))
7373
if (branches.some(b => b.name === branch)) {
7474
let actualFile: RepositoryFileExpandedSchema | undefined
7575
try {
@@ -152,12 +152,12 @@ export class GitlabApi extends PluginApi {
152152
return filesUpdated
153153
}
154154

155-
public async listFiles(repoId: number, options: AllRepositoryTreesOptions & PaginationRequestOptions<'keyset'> = {}) {
155+
public async listFiles(repoId: number, options: AllRepositoryTreesOptions = {}) {
156156
options.path = options?.path ?? '/'
157157
options.ref = options?.ref ?? 'main'
158158
options.recursive = options?.recursive ?? false
159159
try {
160-
const files: RepositoryTreeSchema[] = await this.api.Repositories.allRepositoryTrees(repoId, options)
160+
const files = await getAll(iter(opts => this.api.Repositories.allRepositoryTrees(repoId, { ...options, ...opts })))
161161
// if (depth >= 0) {
162162
// for (const file of files) {
163163
// if (file.type !== 'tree') {
@@ -199,8 +199,7 @@ export class GitlabZoneApi extends GitlabApi {
199199
public async getOrCreateInfraGroup(): Promise<GroupSchema> {
200200
const rootId = await getGroupRootId()
201201
// Get or create projects_root_dir/infra group
202-
const searchResult = await this.api.Groups.search(infraGroupName)
203-
const existingParentGroup = searchResult.find(group => group.parent_id === rootId && group.name === infraGroupName)
202+
const existingParentGroup = await find(iter(opts => this.api.Groups.all({ ...opts, search: infraGroupName })), group => group.parent_id === rootId && group.name === infraGroupName)
204203
return existingParentGroup || await this.api.Groups.create(infraGroupName, infraGroupPath, {
205204
parentId: rootId,
206205
projectCreationLevel: 'maintainer',
@@ -216,26 +215,24 @@ export class GitlabZoneApi extends GitlabApi {
216215
}
217216
const infraGroup = await this.getOrCreateInfraGroup()
218217
// Get or create projects_root_dir/infra/zone
219-
const infraProjects = await this.api.Groups.allProjects(infraGroup.id, {
218+
const project = await find(iter(opts => this.api.Groups.allProjects(infraGroup.id, {
220219
search: zone,
221220
simple: true,
222-
perPage: 100,
223-
})
224-
const project: ProjectSchema = infraProjects.find(repo => repo.name === zone) ?? await this.createEmptyRepository({
221+
...opts,
222+
})), repo => repo.name === zone) ?? await this.createEmptyRepository({
225223
repoName: zone,
226224
groupId: infraGroup.id,
227225
description: 'Repository hosting deployment files for this zone.',
228226
createFirstCommit: true,
229-
},
230-
)
227+
})
231228
this.infraProjectsByZoneSlug.set(zone, project)
232229
return project
233230
}
234231
}
235232

236233
export class GitlabProjectApi extends GitlabApi {
237234
private project: Project | UniqueRepo
238-
private gitlabGroup: GroupSchema & { statistics: GroupStatisticsSchema } | undefined
235+
private gitlabGroup: GroupSchema | undefined
239236
private specialRepositories: string[] = [infraAppsRepoName, internalMirrorRepoName]
240237
private zoneApi: GitlabZoneApi
241238

@@ -248,9 +245,8 @@ export class GitlabProjectApi extends GitlabApi {
248245

249246
// Group Project
250247
private async createProjectGroup(): Promise<GroupSchema> {
251-
const searchResult = await this.api.Groups.search(this.project.slug)
252248
const parentId = await getGroupRootId()
253-
const existingGroup = searchResult.find(group => group.parent_id === parentId && group.name === this.project.slug)
249+
const existingGroup = await find(iter(opts => this.api.Groups.all({ ...opts, search: this.project.slug })), group => group.parent_id === parentId && group.name === this.project.slug)
254250

255251
if (existingGroup) return existingGroup
256252

@@ -265,8 +261,7 @@ export class GitlabProjectApi extends GitlabApi {
265261
public async getProjectGroup(): Promise<GroupSchema | undefined> {
266262
if (this.gitlabGroup) return this.gitlabGroup
267263
const parentId = await getGroupRootId()
268-
const searchResult = await this.api.Groups.allSubgroups(parentId)
269-
this.gitlabGroup = searchResult.find(group => group.name === this.project.slug)
264+
this.gitlabGroup = await find(iter(opts => this.api.Groups.allSubgroups(parentId, opts)), group => group.name === this.project.slug)
270265
return this.gitlabGroup
271266
}
272267

@@ -323,21 +318,15 @@ export class GitlabProjectApi extends GitlabApi {
323318

324319
public async getProjectId(projectName: string) {
325320
const projectGroup = await this.getProjectGroup()
326-
if (!projectGroup) {
327-
throw new Error('Parent DSO Project group has not been created yet')
328-
}
329-
const projectsInGroup = await this.api.Groups.allProjects(projectGroup.id, {
321+
if (!projectGroup) throw new Error(`Gitlab inaccessible, impossible de trouver le groupe ${this.project.slug}`)
322+
323+
const project = await find(iter(opts => this.api.Groups.allProjects(projectGroup.id, {
330324
search: projectName,
331325
simple: true,
332-
perPage: 100,
333-
})
334-
const project = projectsInGroup.find(p => p.path === projectName)
326+
...opts,
327+
})), repo => repo.name === projectName)
335328

336-
if (!project) {
337-
const pathProjectName = `${config().projectsRootDir}/${this.project.slug}/${projectName}`
338-
throw new Error(`Gitlab project "${pathProjectName}" not found`)
339-
}
340-
return project.id
329+
return project?.id
341330
}
342331

343332
public async getProjectById(projectId: number) {
@@ -351,8 +340,7 @@ export class GitlabProjectApi extends GitlabApi {
351340
public async getProjectToken(tokenName: string) {
352341
const group = await this.getProjectGroup()
353342
if (!group) throw new Error('Unable to retrieve gitlab project group')
354-
const groupTokens = await this.api.GroupAccessTokens.all(group.id)
355-
return groupTokens.find(token => token.name === tokenName)
343+
return find(iter(opts => this.api.GroupAccessTokens.all(group.id, opts)), token => token.name === tokenName)
356344
}
357345

358346
public async createProjectToken(tokenName: string, scopes: AccessTokenScopes[]) {
@@ -375,8 +363,7 @@ export class GitlabProjectApi extends GitlabApi {
375363
const gitlabRepositories = await this.listRepositories()
376364
const mirrorRepo = gitlabRepositories.find(repo => repo.name === internalMirrorRepoName)
377365
if (!mirrorRepo) throw new Error('Don\'t know how mirror repo could not exist')
378-
const allTriggerTokens = await this.api.PipelineTriggerTokens.all(mirrorRepo.id)
379-
const currentTriggerToken = allTriggerTokens.find(token => token.description === tokenDescription)
366+
const currentTriggerToken = await find(iter(opts => this.api.PipelineTriggerTokens.all(mirrorRepo.id, opts)), token => token.description === tokenDescription)
380367

381368
const tokenVaultSecret = await vaultApi.read('GITLAB', { throwIfNoEntry: false })
382369

@@ -398,7 +385,7 @@ export class GitlabProjectApi extends GitlabApi {
398385

399386
public async listRepositories() {
400387
const group = await this.getOrCreateProjectGroup()
401-
const projects = await this.api.Groups.allProjects(group.id, { simple: false }) // to refactor with https://github.com/jdalrymple/gitbeaker/pull/3624
388+
const projects = await getAll(iter(opts => this.api.Groups.allProjects(group.id, { simple: false, ...opts })))
402389
return Promise.all(projects.map(async (project) => {
403390
if (this.specialRepositories.includes(project.name) && (!project.topics || !project.topics.includes(pluginManagedTopic))) {
404391
return this.api.Projects.edit(project.id, { topics: project.topics ? [...project.topics, pluginManagedTopic] : [pluginManagedTopic] })
@@ -432,7 +419,7 @@ export class GitlabProjectApi extends GitlabApi {
432419
// Group members
433420
public async getGroupMembers() {
434421
const group = await this.getOrCreateProjectGroup()
435-
return this.api.GroupMembers.all(group.id)
422+
return getAll(iter(opts => this.api.GroupMembers.all(group.id, opts)))
436423
}
437424

438425
public async addGroupMember(userId: number, accessLevel: AccessLevelAllowed = AccessLevel.DEVELOPER): Promise<MemberSchema> {
@@ -448,7 +435,7 @@ export class GitlabProjectApi extends GitlabApi {
448435
// CI Variables
449436
public async getGitlabGroupVariables(): Promise<VariableSchema[]> {
450437
const group = await this.getOrCreateProjectGroup()
451-
return await this.api.GroupVariables.all(group.id)
438+
return await getAll(iter(opts => this.api.GroupVariables.all(group.id, opts)))
452439
}
453440

454441
public async setGitlabGroupVariable(listVars: VariableSchema[], toSetVariable: VariableSchema): Promise<setVariableResult> {
@@ -491,7 +478,7 @@ export class GitlabProjectApi extends GitlabApi {
491478
}
492479

493480
public async getGitlabRepoVariables(repoId: number): Promise<VariableSchema[]> {
494-
return await this.api.ProjectVariables.all(repoId)
481+
return await getAll(iter(opts => this.api.ProjectVariables.all(repoId, opts)))
495482
}
496483

497484
public async setGitlabRepoVariable(repoId: number, listVars: VariableSchema[], toSetVariable: ProjectVariableSchema): Promise<setVariableResult | 'repository not found'> {

plugins/gitlab/src/user.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,19 @@
11
import type { UserObject } from '@cpn-console/hooks'
22
import type { CreateUserOptions, SimpleUserSchema } from '@gitbeaker/rest'
3-
import { getApi } from './utils.js'
3+
import { getApi, find, iter } from './utils.js'
44

55
export const createUsername = (email: string) => email.replace('@', '.')
66

77
export async function getUser(user: { email: string, username: string, id: string }): Promise<SimpleUserSchema | undefined> {
88
const api = getApi()
99

10-
let gitlabUser: SimpleUserSchema | undefined
11-
12-
// test finding by extern_uid by searching with email
13-
const usersByEmail = await api.Users.all({ search: user.email })
14-
gitlabUser = usersByEmail.find(gitlabUser => gitlabUser?.externUid === user.id)
15-
if (gitlabUser) return gitlabUser
16-
17-
// if not found, test finding by extern_uid by searching with username
18-
const usersByUsername = await api.Users.all({ username: user.username })
19-
gitlabUser = usersByUsername.find(gitlabUser => gitlabUser?.externUid === user.id)
20-
if (gitlabUser) return gitlabUser
21-
22-
// if not found, test finding by email or username
23-
const allUsers = [...usersByEmail, ...usersByUsername]
24-
return allUsers.find(gitlabUser => gitlabUser.email === user.email)
25-
|| allUsers.find(gitlabUser => gitlabUser.username === user.username)
10+
return find(
11+
iter(opts => api.Users.all(opts)),
12+
gitlabUser =>
13+
gitlabUser?.externUid === user.id
14+
|| gitlabUser.email === user.email
15+
|| gitlabUser.username === user.username,
16+
)
2617
}
2718

2819
export async function upsertUser(user: UserObject): Promise<SimpleUserSchema> {

plugins/gitlab/src/utils.ts

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Gitlab } from '@gitbeaker/rest'
2-
import type { Gitlab as IGitlab } from '@gitbeaker/core'
2+
import type { Gitlab as IGitlab, ShowExpanded, Sudo } from '@gitbeaker/core'
33
import { GitbeakerRequestError } from '@gitbeaker/requester-utils'
44
import config from './config.js'
55

@@ -13,9 +13,12 @@ export async function getGroupRootId(throwIfNotFound?: boolean): Promise<number
1313
const gitlabApi = getApi()
1414
const projectRootDir = config().projectsRootDir
1515
if (groupRootId) return groupRootId
16-
const groupRootSearch = await gitlabApi.Groups.search(projectRootDir)
17-
const searchId = (groupRootSearch.find(grp => grp.full_path === projectRootDir))?.id
18-
if (typeof searchId === 'undefined') {
16+
const searchGroup = await find<{ id: number, full_path: string }>(
17+
iter(opts => gitlabApi.Groups.all(opts), { search: projectRootDir }),
18+
grp => grp.full_path === projectRootDir,
19+
)
20+
const searchId = searchGroup?.id
21+
if (searchId === undefined) {
1922
if (throwIfNotFound) {
2023
throw new Error(`Gitlab inaccessible, impossible de trouver le groupe ${projectRootDir}`)
2124
}
@@ -35,19 +38,22 @@ async function createGroupRoot(): Promise<number> {
3538
throw new Error('No projectRootDir available')
3639
}
3740

38-
let parentGroup = (await gitlabApi.Groups.search(rootGroupPath))
39-
.find(grp => grp.full_path === rootGroupPath)
40-
?? await gitlabApi.Groups.create(rootGroupPath, rootGroupPath)
41+
let parentGroup: { id: number, full_path: string } | undefined = await find(
42+
iter(opts => gitlabApi.Groups.all(opts), { search: rootGroupPath }),
43+
grp => grp.full_path === rootGroupPath,
44+
)
45+
parentGroup ??= await gitlabApi.Groups.create(rootGroupPath, rootGroupPath)
4146

4247
if (parentGroup.full_path === projectRootDir) {
4348
return parentGroup.id
4449
}
4550

4651
for (const path of projectRootDirArray) {
47-
const futureFullPath = `${parentGroup.full_path}/${path}`
48-
parentGroup = (await gitlabApi.Groups.search(futureFullPath))
49-
.find(grp => grp.full_path === futureFullPath)
50-
?? await gitlabApi.Groups.create(path, path, { parentId: parentGroup.id, visibility: 'internal' })
52+
const futureFullPath: string = `${parentGroup.full_path}/${path}`
53+
parentGroup = await find(
54+
iter(opts => gitlabApi.Groups.all(opts), { search: futureFullPath }),
55+
grp => grp.full_path === futureFullPath,
56+
) ?? await gitlabApi.Groups.create(path, path, { parentId: parentGroup.id, visibility: 'internal' })
5157

5258
if (parentGroup.full_path === projectRootDir) {
5359
return parentGroup.id
@@ -57,17 +63,11 @@ async function createGroupRoot(): Promise<number> {
5763
}
5864

5965
export async function getOrCreateGroupRoot(): Promise<number> {
60-
let rootId = await getGroupRootId(false)
61-
if (typeof rootId === 'undefined') {
62-
rootId = await createGroupRoot()
63-
}
64-
return rootId
66+
return await getGroupRootId(false) ?? createGroupRoot()
6567
}
6668

6769
export function getApi(): IGitlab {
68-
if (!api) {
69-
api = new Gitlab({ token: config().token, host: config().internalUrl })
70-
}
70+
api ??= new Gitlab({ token: config().token, host: config().internalUrl })
7171
return api
7272
}
7373

@@ -89,3 +89,44 @@ export function cleanGitlabError<T>(error: T): T {
8989
}
9090
return error
9191
}
92+
93+
export async function* iter<T>(
94+
request: (options?: Sudo & ShowExpanded<true>) => Promise<{ data: T[], paginationInfo: any }>,
95+
options: any = {},
96+
): AsyncGenerator<T> {
97+
let page = 1
98+
let hasNext = true
99+
while (hasNext) {
100+
const { data, paginationInfo } = await request({ ...options, page, showExpanded: true })
101+
for (const item of data) {
102+
yield item
103+
}
104+
if (paginationInfo.next) {
105+
page = paginationInfo.next
106+
} else {
107+
hasNext = false
108+
}
109+
}
110+
}
111+
112+
export async function getAll<T>(
113+
iterable: AsyncIterable<T>,
114+
): Promise<T[]> {
115+
const items: T[] = []
116+
for await (const item of iterable) {
117+
items.push(item)
118+
}
119+
return items
120+
}
121+
122+
export async function find<T>(
123+
iterable: AsyncIterable<T>,
124+
predicate: (item: T) => boolean,
125+
): Promise<T | undefined> {
126+
for await (const item of iterable) {
127+
if (predicate(item)) {
128+
return item
129+
}
130+
}
131+
return undefined
132+
}

plugins/sonarqube/src/functions.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,13 @@ export const upsertProject: StepCall<Project> = async (payload) => {
119119

120120
// Remove excess repositories
121121
...sonarRepositories
122-
.filter(sonarRepository => !project.repositories.find(repo => repo.internalRepoName === sonarRepository.repository))
122+
.filter(sonarRepository => !project.repositories.some(repo => repo.internalRepoName === sonarRepository.repository))
123123
.map(sonarRepository => deleteDsoRepository(sonarRepository.key)),
124124

125125
// Create or configure needed repos
126126
...project.repositories.map(async (repository) => {
127127
const projectKey = generateProjectKey(projectSlug, repository.internalRepoName)
128-
if (!sonarRepositories.find(sonarRepository => sonarRepository.repository === repository.internalRepoName)) {
128+
if (!sonarRepositories.some(sonarRepository => sonarRepository.repository === repository.internalRepoName)) {
129129
await createDsoRepository(projectSlug, repository.internalRepoName)
130130
}
131131
await ensureRepositoryConfiguration(projectKey, username, keycloakGroupPath)
@@ -166,6 +166,7 @@ export const setVariables: StepCall<Project> = async (payload) => {
166166
...project.repositories.map(async (repo) => {
167167
const projectKey = generateProjectKey(projectSlug, repo.internalRepoName)
168168
const repoId = await payload.apis.gitlab.getProjectId(repo.internalRepoName)
169+
if (!repoId) return
169170
const listVars = await gitlabApi.getGitlabRepoVariables(repoId)
170171
return [
171172
await gitlabApi.setGitlabRepoVariable(repoId, listVars, {
@@ -193,9 +194,9 @@ export const setVariables: StepCall<Project> = async (payload) => {
193194
environment_scope: '*',
194195
}),
195196
]
196-
}).flat(),
197+
}),
197198
// Sonar vars saving in CI (group)
198-
await gitlabApi.setGitlabGroupVariable(listGroupVars, {
199+
gitlabApi.setGitlabGroupVariable(listGroupVars, {
199200
key: 'SONAR_TOKEN',
200201
masked: true,
201202
protected: false,

0 commit comments

Comments
 (0)