Skip to content

Commit a6352c1

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

5 files changed

Lines changed: 125 additions & 86 deletions

File tree

plugins/gitlab/src/class.ts

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
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, AllRepositoryTreesOptions, CondensedProjectSchema, Gitlab, ProjectSchema, RepositoryFileExpandedSchema } from '@gitbeaker/core'
54
import { AccessLevel } from '@gitbeaker/core'
65
import type { VaultProjectApi } from '@cpn-console/vault-plugin/types/vault-project-api.js'
76
import { objectEntries } from '@cpn-console/shared'
87
import type { GitbeakerRequestError } from '@gitbeaker/requester-utils'
9-
import { getApi, getGroupRootId, infraAppsRepoName, internalMirrorRepoName } from './utils.js'
8+
import { find, getApi, getAll, getGroupRootId, infraAppsRepoName, internalMirrorRepoName, offsetPaginate } from './utils.js'
109
import config from './config.js'
1110

1211
type setVariableResult = 'created' | 'updated' | 'already up-to-date'
@@ -69,8 +68,8 @@ export class GitlabApi extends PluginApi {
6968
): Promise<boolean> {
7069
let action: CommitAction['action'] = 'create'
7170

72-
const branches = await this.api.Branches.all(repoId)
73-
if (branches.some(b => b.name === branch)) {
71+
const existingBranch = await find(offsetPaginate(opts => this.api.Branches.all(repoId, opts)), b => b.name === branch)
72+
if (existingBranch) {
7473
let actualFile: RepositoryFileExpandedSchema | undefined
7574
try {
7675
actualFile = await this.api.RepositoryFiles.show(repoId, filePath, branch)
@@ -152,12 +151,12 @@ export class GitlabApi extends PluginApi {
152151
return filesUpdated
153152
}
154153

155-
public async listFiles(repoId: number, options: AllRepositoryTreesOptions & PaginationRequestOptions<'keyset'> = {}) {
154+
public async listFiles(repoId: number, options: AllRepositoryTreesOptions = {}) {
156155
options.path = options?.path ?? '/'
157156
options.ref = options?.ref ?? 'main'
158157
options.recursive = options?.recursive ?? false
159158
try {
160-
const files: RepositoryTreeSchema[] = await this.api.Repositories.allRepositoryTrees(repoId, options)
159+
const files = await this.api.Repositories.allRepositoryTrees(repoId, options)
161160
// if (depth >= 0) {
162161
// for (const file of files) {
163162
// if (file.type !== 'tree') {
@@ -199,8 +198,11 @@ export class GitlabZoneApi extends GitlabApi {
199198
public async getOrCreateInfraGroup(): Promise<GroupSchema> {
200199
const rootId = await getGroupRootId()
201200
// 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)
201+
const existingParentGroup = await find(offsetPaginate(opts => this.api.Groups.all({
202+
search: infraGroupName,
203+
orderBy: 'id',
204+
...opts,
205+
})), group => group.parent_id === rootId && group.name === infraGroupName)
204206
return existingParentGroup || await this.api.Groups.create(infraGroupName, infraGroupPath, {
205207
parentId: rootId,
206208
projectCreationLevel: 'maintainer',
@@ -216,26 +218,24 @@ export class GitlabZoneApi extends GitlabApi {
216218
}
217219
const infraGroup = await this.getOrCreateInfraGroup()
218220
// Get or create projects_root_dir/infra/zone
219-
const infraProjects = await this.api.Groups.allProjects(infraGroup.id, {
221+
const project = await find(offsetPaginate(opts => this.api.Groups.allProjects(infraGroup.id, {
220222
search: zone,
221223
simple: true,
222-
perPage: 100,
223-
})
224-
const project: ProjectSchema = infraProjects.find(repo => repo.name === zone) ?? await this.createEmptyRepository({
224+
...opts,
225+
})), repo => repo.name === zone) ?? await this.createEmptyRepository({
225226
repoName: zone,
226227
groupId: infraGroup.id,
227228
description: 'Repository hosting deployment files for this zone.',
228229
createFirstCommit: true,
229-
},
230-
)
230+
})
231231
this.infraProjectsByZoneSlug.set(zone, project)
232232
return project
233233
}
234234
}
235235

236236
export class GitlabProjectApi extends GitlabApi {
237237
private project: Project | UniqueRepo
238-
private gitlabGroup: GroupSchema & { statistics: GroupStatisticsSchema } | undefined
238+
private gitlabGroup: GroupSchema | undefined
239239
private specialRepositories: string[] = [infraAppsRepoName, internalMirrorRepoName]
240240
private zoneApi: GitlabZoneApi
241241

@@ -248,9 +248,12 @@ export class GitlabProjectApi extends GitlabApi {
248248

249249
// Group Project
250250
private async createProjectGroup(): Promise<GroupSchema> {
251-
const searchResult = await this.api.Groups.search(this.project.slug)
252251
const parentId = await getGroupRootId()
253-
const existingGroup = searchResult.find(group => group.parent_id === parentId && group.name === this.project.slug)
252+
const existingGroup = await find(offsetPaginate(opts => this.api.Groups.all({
253+
search: this.project.slug,
254+
orderBy: 'id',
255+
...opts,
256+
})), group => group.parent_id === parentId && group.name === this.project.slug)
254257

255258
if (existingGroup) return existingGroup
256259

@@ -265,8 +268,7 @@ export class GitlabProjectApi extends GitlabApi {
265268
public async getProjectGroup(): Promise<GroupSchema | undefined> {
266269
if (this.gitlabGroup) return this.gitlabGroup
267270
const parentId = await getGroupRootId()
268-
const searchResult = await this.api.Groups.allSubgroups(parentId)
269-
this.gitlabGroup = searchResult.find(group => group.name === this.project.slug)
271+
this.gitlabGroup = await find(offsetPaginate(opts => this.api.Groups.allSubgroups(parentId, opts)), group => group.name === this.project.slug)
270272
return this.gitlabGroup
271273
}
272274

@@ -323,21 +325,15 @@ export class GitlabProjectApi extends GitlabApi {
323325

324326
public async getProjectId(projectName: string) {
325327
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, {
328+
if (!projectGroup) throw new Error(`Gitlab inaccessible, impossible de trouver le groupe ${this.project.slug}`)
329+
330+
const project = await find(offsetPaginate(opts => this.api.Groups.allProjects(projectGroup.id, {
330331
search: projectName,
331332
simple: true,
332-
perPage: 100,
333-
})
334-
const project = projectsInGroup.find(p => p.path === projectName)
333+
...opts,
334+
})), repo => repo.name === projectName)
335335

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
336+
return project?.id
341337
}
342338

343339
public async getProjectById(projectId: number) {
@@ -351,8 +347,7 @@ export class GitlabProjectApi extends GitlabApi {
351347
public async getProjectToken(tokenName: string) {
352348
const group = await this.getProjectGroup()
353349
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)
350+
return find(offsetPaginate(opts => this.api.GroupAccessTokens.all(group.id, opts)), token => token.name === tokenName)
356351
}
357352

358353
public async createProjectToken(tokenName: string, scopes: AccessTokenScopes[]) {
@@ -375,8 +370,7 @@ export class GitlabProjectApi extends GitlabApi {
375370
const gitlabRepositories = await this.listRepositories()
376371
const mirrorRepo = gitlabRepositories.find(repo => repo.name === internalMirrorRepoName)
377372
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)
373+
const currentTriggerToken = await find(offsetPaginate(opts => this.api.PipelineTriggerTokens.all(mirrorRepo.id, opts)), token => token.description === tokenDescription)
380374

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

@@ -398,7 +392,7 @@ export class GitlabProjectApi extends GitlabApi {
398392

399393
public async listRepositories() {
400394
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
395+
const projects = await getAll(offsetPaginate(opts => this.api.Groups.allProjects(group.id, { simple: false, ...opts }))) // to refactor with https://github.com/jdalrymple/gitbeaker/pull/3624
402396
return Promise.all(projects.map(async (project) => {
403397
if (this.specialRepositories.includes(project.name) && (!project.topics || !project.topics.includes(pluginManagedTopic))) {
404398
return this.api.Projects.edit(project.id, { topics: project.topics ? [...project.topics, pluginManagedTopic] : [pluginManagedTopic] })
@@ -432,7 +426,7 @@ export class GitlabProjectApi extends GitlabApi {
432426
// Group members
433427
public async getGroupMembers() {
434428
const group = await this.getOrCreateProjectGroup()
435-
return this.api.GroupMembers.all(group.id)
429+
return getAll(offsetPaginate(opts => this.api.GroupMembers.all(group.id, opts)))
436430
}
437431

438432
public async addGroupMember(userId: number, accessLevel: AccessLevelAllowed = AccessLevel.DEVELOPER): Promise<MemberSchema> {
@@ -448,7 +442,7 @@ export class GitlabProjectApi extends GitlabApi {
448442
// CI Variables
449443
public async getGitlabGroupVariables(): Promise<VariableSchema[]> {
450444
const group = await this.getOrCreateProjectGroup()
451-
return await this.api.GroupVariables.all(group.id)
445+
return await getAll(offsetPaginate(opts => this.api.GroupVariables.all(group.id, opts)))
452446
}
453447

454448
public async setGitlabGroupVariable(listVars: VariableSchema[], toSetVariable: VariableSchema): Promise<setVariableResult> {
@@ -491,7 +485,7 @@ export class GitlabProjectApi extends GitlabApi {
491485
}
492486

493487
public async getGitlabRepoVariables(repoId: number): Promise<VariableSchema[]> {
494-
return await this.api.ProjectVariables.all(repoId)
488+
return await getAll(offsetPaginate(opts => this.api.ProjectVariables.all(repoId, opts)))
495489
}
496490

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

plugins/gitlab/src/user.ts

Lines changed: 13 additions & 18 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, offsetPaginate } 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+
offsetPaginate(opts => api.Users.all({ ...opts, asAdmin: true })),
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> {
@@ -57,7 +48,11 @@ export async function upsertUser(user: UserObject): Promise<SimpleUserSchema> {
5748
console.log(`Gitlab plugin: Updating user: ${user.email}`)
5849
console.log(incorrectProps)
5950
}
60-
await api.Users.edit(existingUser.id, userDefinitionBase)
51+
try {
52+
await api.Users.edit(existingUser.id, userDefinitionBase)
53+
} catch (err) {
54+
console.error(`Gitlab plugin: Failed to update user: ${user.email} for ${err}`)
55+
}
6156
}
6257
return existingUser
6358
}

plugins/gitlab/src/utils.ts

Lines changed: 54 additions & 17 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, BaseRequestOptions, PaginationRequestOptions, OffsetPagination } from '@gitbeaker/core'
33
import { GitbeakerRequestError } from '@gitbeaker/requester-utils'
44
import config from './config.js'
55

@@ -13,8 +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
16+
const groupRoot = await find(offsetPaginate(opts => gitlabApi.Groups.all({
17+
search: projectRootDir,
18+
orderBy: 'id',
19+
...opts,
20+
})), grp => grp.full_path === projectRootDir)
21+
const searchId = groupRoot?.id
1822
if (typeof searchId === 'undefined') {
1923
if (throwIfNotFound) {
2024
throw new Error(`Gitlab inaccessible, impossible de trouver le groupe ${projectRootDir}`)
@@ -35,19 +39,23 @@ async function createGroupRoot(): Promise<number> {
3539
throw new Error('No projectRootDir available')
3640
}
3741

38-
let parentGroup = (await gitlabApi.Groups.search(rootGroupPath))
39-
.find(grp => grp.full_path === rootGroupPath)
40-
?? await gitlabApi.Groups.create(rootGroupPath, rootGroupPath)
42+
let parentGroup = await find(offsetPaginate(opts => gitlabApi.Groups.all({
43+
search: rootGroupPath,
44+
orderBy: 'id',
45+
...opts,
46+
})), grp => grp.full_path === rootGroupPath) ?? await gitlabApi.Groups.create(rootGroupPath, rootGroupPath)
4147

4248
if (parentGroup.full_path === projectRootDir) {
4349
return parentGroup.id
4450
}
4551

4652
for (const path of projectRootDirArray) {
4753
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' })
54+
parentGroup = await find(offsetPaginate(opts => gitlabApi.Groups.all({
55+
search: futureFullPath,
56+
orderBy: 'id',
57+
...opts,
58+
})), grp => grp.full_path === futureFullPath) ?? await gitlabApi.Groups.create(path, path, { parentId: parentGroup.id, visibility: 'internal' })
5159

5260
if (parentGroup.full_path === projectRootDir) {
5361
return parentGroup.id
@@ -57,17 +65,11 @@ async function createGroupRoot(): Promise<number> {
5765
}
5866

5967
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
68+
return await getGroupRootId(false) ?? createGroupRoot()
6569
}
6670

6771
export function getApi(): IGitlab {
68-
if (!api) {
69-
api = new Gitlab({ token: config().token, host: config().internalUrl })
70-
}
72+
api ??= new Gitlab({ token: config().token, host: config().internalUrl })
7173
return api
7274
}
7375

@@ -89,3 +91,38 @@ export function cleanGitlabError<T>(error: T): T {
8991
}
9092
return error
9193
}
94+
95+
export async function* offsetPaginate<T>(
96+
request: (options: PaginationRequestOptions<'offset'> & BaseRequestOptions<true>) => Promise<{ data: T[], paginationInfo: OffsetPagination }>,
97+
): AsyncGenerator<T> {
98+
let page: number | null = 1
99+
while (page !== null) {
100+
const { data, paginationInfo } = await request({ page, showExpanded: true, pagination: 'offset' })
101+
for (const item of data) {
102+
yield item
103+
}
104+
page = paginationInfo.next
105+
}
106+
}
107+
108+
export async function getAll<T>(
109+
iterable: AsyncIterable<T>,
110+
): Promise<T[]> {
111+
const items: T[] = []
112+
for await (const item of iterable) {
113+
items.push(item)
114+
}
115+
return items
116+
}
117+
118+
export async function find<T>(
119+
iterable: AsyncIterable<T>,
120+
predicate: (item: T) => boolean,
121+
): Promise<T | undefined> {
122+
for await (const item of iterable) {
123+
if (predicate(item)) {
124+
return item
125+
}
126+
}
127+
return undefined
128+
}

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)