From 8dfe76f30b1bd783f0f8e1bbf214392eecd75b54 Mon Sep 17 00:00:00 2001 From: Alexandre Fournier Date: Mon, 24 Mar 2025 22:03:03 -0400 Subject: [PATCH 1/6] added support for multiple group patterns instead of a single one to increase flexibility when filtering groups from GitLab. Signed-off-by: Alexandre Fournier --- .changeset/chubby-results-relax.md | 5 + docs/integrations/gitlab/discovery.md | 3 + .../src/__testUtils__/handlers.ts | 21 +++ .../src/__testUtils__/mocks.ts | 101 +++++++++++++++ .../src/lib/types.ts | 10 +- .../GitlabDiscoveryEntityProvider.test.ts | 121 ++++++++++++++++++ .../GitlabDiscoveryEntityProvider.ts | 121 +++++++++++++++--- .../src/providers/config.test.ts | 6 + .../src/providers/config.ts | 5 + 9 files changed, 373 insertions(+), 20 deletions(-) create mode 100644 .changeset/chubby-results-relax.md diff --git a/.changeset/chubby-results-relax.md b/.changeset/chubby-results-relax.md new file mode 100644 index 0000000000..a668570694 --- /dev/null +++ b/.changeset/chubby-results-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': minor +--- + +Added support for multiple group patterns instead of a single one to increase flexibility when filtering groups from GitLab. diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index e0ededd1fa..85daea3649 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -151,6 +151,9 @@ catalog: skipForkedRepos: false # Optional. If the project is a fork, skip repository includeArchivedRepos: false # Optional. If project is archived, include repository group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned + groupPatterns: # Optional. Filters for groups based on a list of patterns. Default, no filters. + - '^somegroup$' + - 'anothergroup' entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided patter. Defaults to `[\s\S]*`, which means to not filter anything excludeRepos: [] # Optional. A list of project paths that should be excluded from discovery, e.g. group/subgroup/repo. Should not start or end with a slash. diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts index 0732bbc4cd..7549e6faca 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts @@ -229,6 +229,26 @@ const httpGroupListDescendantProjectsByName = all_groups_response.map(group => { }, ); }); + +const httpGroupListDescendantProjectsByFullPath = all_groups_response.map( + group => { + return rest.get( + `${apiBaseUrl}/groups/${encodeURIComponent(group.full_path)}/projects`, + (req, res, ctx) => { + const archived = req.url.searchParams.get('archived'); + + const projectsInGroup = all_projects_response.filter( + p => + p.path_with_namespace?.includes(group.full_path) && + (archived === 'false' ? !p.archived : true), + ); + + return res(ctx.json(projectsInGroup)); + }, + ); + }, +); + const httpGroupFindByNameDynamic = all_groups_response.map(group => { return rest.get(`${apiBaseUrl}/groups/${group.name}`, (_, res, ctx) => { return res(ctx.json(all_groups_response.find(g => g.name === group.name))); @@ -711,6 +731,7 @@ export const handlers = [ ...httpGroupFindByNameDynamic, ...httpGroupListDescendantProjectsById, ...httpGroupListDescendantProjectsByName, + ...httpGroupListDescendantProjectsByFullPath, ...graphqlHandlers, ...httpGroupFindByEncodedPathDynamic, ]; diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts index 2e74be66cc..768ccb416b 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts @@ -820,6 +820,95 @@ export const config_org_group_with_subgroups_sass = { }, }, }; + +export const config_groupPatterns_only_noMatch = { + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + groupPatterns: ['^group8$', '^group9$', '^group10$'], + }, + }, + }, + }, +}; + +export const config_groupPatterns_only_Match1Group = { + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + groupPatterns: ['^group1$', '^group9$', '^group10$'], + }, + }, + }, + }, +}; + +export const config_groupPatterns_multiple_matches = { + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + groupPatterns: ['^group1$', '^group2$', '^group10$'], + }, + }, + }, + }, +}; + +export const config_groupPatterns_duplicate_match = { + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + groupPatterns: ['^group1$', 'subgroup1'], // Both patterns match the same group + }, + }, + }, + }, +}; + /** * GitLab API responses */ @@ -919,6 +1008,18 @@ export const all_projects_response: GitLabProject[] = [ web_url: 'https://example.com/group1/test-repo8-archived', path_with_namespace: 'group1/test-repo8-archived', }, + // project in subgroup + { + id: 9, + description: 'Project Nine Description', + name: 'test-repo9', + default_branch: 'main', + path: 'test-repo9', + archived: false, + last_activity_at: new Date().toString(), + web_url: 'https://example.com/group1/subgroup1/test-repo9', + path_with_namespace: 'group1/subgroup1/test-repo9', + }, ]; export const all_users_response: GitLabUser[] = [ diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index 23b03ec366..a51e129fbf 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -184,12 +184,20 @@ export type GitlabProviderConfig = { * Defaults to `[\s\S]*`, which means to not filter anything */ userPattern: RegExp; + /** - * Filters found groups based on provided patter. + * Filters found groups based on provided pattern. * Defaults to `[\s\S]*`, which means to not filter anything + * + * @deprecated Use groupPatterns that provide a list instead. */ groupPattern: RegExp; + /** + * Optional, provide a list of pattern for each can match a list of groups. + */ + groupPatterns: RegExp[]; + /** * If true, the provider will also add inherited (ascendant) users to the ingested groups. * See: https://docs.gitlab.com/ee/api/graphql/reference/#groupmemberrelation diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts index dd7a4f0d6d..7d7bb699f5 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -558,4 +558,125 @@ describe('GitlabDiscoveryEntityProvider - events', () => { expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); }); // EventSupportChange: stop add tests >>> + + // add the following test: + // setup: 3 projects, 2 groups + // 1 test where none match --> OK + // 1 test where match only 1 group --> OK + // 1 test where match 2 groups + // 1 test where match 1 group with 2 identical regex + + it('should ignore projects when none of the groups regex patterns match', async () => { + const config = new ConfigReader(mock.config_groupPatterns_only_noMatch); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + await provider.connect(entityProviderConnection); + + await provider.refresh(logger); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [], + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); // No entities should be applied + }); +}); +it('should include only the project that matches one of the group regex patterns', async () => { + const config = new ConfigReader(mock.config_groupPatterns_only_Match1Group); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + await provider.connect(entityProviderConnection); + + await provider.refresh(logger); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_location_entities_default_branch.filter( + entity => + entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('/group1/'), // Only projects in 'group2' should match + ), + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); // Only one matching project should be applied +}); +it('should include projects that match multiple group regex patterns', async () => { + const config = new ConfigReader(mock.config_groupPatterns_multiple_matches); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + await provider.connect(entityProviderConnection); + + await provider.refresh(logger); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_location_entities_default_branch.filter( + entity => + entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('/group1/') || + entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('/group2/'), + ), + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); // Projects from both groups should be applied +}); +it('should not create duplicate locations when multiple groupPatterns match the same group', async () => { + const config = new ConfigReader(mock.config_groupPatterns_duplicate_match); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + await provider.connect(entityProviderConnection); + + await provider.refresh(logger); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_location_entities_default_branch.filter(entity => + entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('/group1/'), + ), + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); }); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 8bf749c275..3a7f1ab82d 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -33,6 +33,7 @@ import { WebhookProjectSchema, WebhookPushEventSchema } from '@gitbeaker/rest'; import * as uuid from 'uuid'; import { GitLabClient, + GitLabGroup, GitLabProject, GitlabProviderConfig, paginated, @@ -207,10 +208,107 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { ); } + const locations = await this.GetLocations(); + + await this.connection.applyMutation({ + type: 'full', + entities: locations.map(location => ({ + locationKey: this.getProviderName(), + entity: locationSpecToLocationEntity({ location }), + })), + }); + + logger.info(`Processed ${locations.length} locations`); + } + + /** + * Determine the location on GitLab to be ingested base on configured groups and filters. + * + * @returns A list of location to be ingested + */ + private async GetLocations() { + let res: Result = { + scanned: 0, + matches: [], + }; + + const groupToProcess = new Map(); + + if ( + this.config.groupPatterns !== undefined && + this.config.groupPatterns.length > 0 + ) { + for (const pattern of this.config.groupPatterns) { + const groups = paginated( + options => this.gitLabClient.listGroups(options), + { + page: 1, + per_page: 50, + }, + ); + + for await (const group of groups) { + if ( + pattern.test(group.full_path) && + !groupToProcess.has(group.full_path) + ) { + groupToProcess.set(group.full_path, group); + } + } + } + + for (const group of groupToProcess.values()) { + const tmpRes = await this.GetProjectsToProcess(group.full_path); + res.scanned += tmpRes.scanned; + res.matches.push(...tmpRes.matches); + } + } else { + res = await this.GetProjectsToProcess(this.config.group); + } + + const locations = this.deduplicateProjects(res.matches).map(p => + this.createLocationSpec(p), + ); + + this.logger.info( + `Processed ${locations.length} from scanned ${res.scanned} projects.`, + ); + + return locations; + } + + /** + * Deduplicate a list of projects based on their id. + * + * @param projects a list of projects to be deduplicated + * @returns a list of projects with unique id + */ + private deduplicateProjects(projects: GitLabProject[]): GitLabProject[] { + const uniqueProjects = new Map(); + + for (const project of projects) { + uniqueProjects.set(project.id, project); + } + + return Array.from(uniqueProjects.values()); + } + + /** + * Retrieve a list of projects that match configuration. + * + * @param group a full path of a GitLab group, can be empty + * @returns An array of project to be processed and the number of project scanned + */ + private async GetProjectsToProcess(group: string) { + const res: Result = { + scanned: 0, + matches: [], + }; + const projects = paginated( options => this.gitLabClient.listProjects(options), { - group: this.config.group, + group: group, page: 1, per_page: 50, ...(!this.config.includeArchivedRepos && { archived: false }), @@ -219,30 +317,15 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { }, ); - const res: Result = { - scanned: 0, - matches: [], - }; - for await (const project of projects) { + res.scanned++; + if (await this.shouldProcessProject(project, this.gitLabClient)) { - res.scanned++; res.matches.push(project); } } - const locations = res.matches.map(p => this.createLocationSpec(p)); - - logger.info( - `Processed ${locations.length} from scanned ${res.scanned} projects.`, - ); - await this.connection.applyMutation({ - type: 'full', - entities: locations.map(location => ({ - locationKey: this.getProviderName(), - entity: locationSpecToLocationEntity({ location }), - })), - }); + return res; } private createLocationSpec(project: GitLabProject): LocationSpec { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index 8790a3fe53..27dd32367a 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -55,6 +55,7 @@ describe('config', () => { catalogFile: 'catalog-info.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -101,6 +102,7 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -147,6 +149,7 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -193,6 +196,7 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -240,6 +244,7 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -288,6 +293,7 @@ describe('config', () => { catalogFile: 'catalog-info.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index 5b50fce5b1..639a1e1177 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -42,6 +42,10 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { const groupPattern = new RegExp( config.getOptionalString('groupPattern') ?? /[\s\S]*/, ); + const groupPatterns: Array = + config + .getOptionalStringArray('groupPatterns') + ?.map(pattern => new RegExp(pattern)) ?? []; const orgEnabled: boolean = config.getOptionalBoolean('orgEnabled') ?? false; const allowInherited: boolean = config.getOptionalBoolean('allowInherited') ?? false; @@ -81,6 +85,7 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { projectPattern, userPattern, groupPattern, + groupPatterns, schedule, orgEnabled, allowInherited, From 1d0c9ba472ff8285018519df9ced8623007ae429 Mon Sep 17 00:00:00 2001 From: Alexandre Fournier Date: Wed, 26 Mar 2025 20:10:53 -0400 Subject: [PATCH 2/6] Update the API Report to add the groupPatterns new configuration Signed-off-by: Alexandre Fournier --- plugins/catalog-backend-module-gitlab/report.api.md | 1 + .../src/providers/GitlabDiscoveryEntityProvider.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/report.api.md b/plugins/catalog-backend-module-gitlab/report.api.md index 5b7bfadf1d..896680b41f 100644 --- a/plugins/catalog-backend-module-gitlab/report.api.md +++ b/plugins/catalog-backend-module-gitlab/report.api.md @@ -112,6 +112,7 @@ export type GitlabProviderConfig = { projectPattern: RegExp; userPattern: RegExp; groupPattern: RegExp; + groupPatterns: RegExp[]; allowInherited?: boolean; relations?: string[]; orgEnabled?: boolean; diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 3a7f1ab82d..ae2a3d573f 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -280,7 +280,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { /** * Deduplicate a list of projects based on their id. * - * @param projects a list of projects to be deduplicated + * @param projects - a list of projects to be deduplicated * @returns a list of projects with unique id */ private deduplicateProjects(projects: GitLabProject[]): GitLabProject[] { @@ -296,7 +296,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { /** * Retrieve a list of projects that match configuration. * - * @param group a full path of a GitLab group, can be empty + * @param group - a full path of a GitLab group, can be empty * @returns An array of project to be processed and the number of project scanned */ private async GetProjectsToProcess(group: string) { From 82a3210d6f0fde36b297cfc772408db0d60a31f3 Mon Sep 17 00:00:00 2001 From: Alexandre Fournier Date: Wed, 26 Mar 2025 20:53:15 -0400 Subject: [PATCH 3/6] Fixed broken tests because of missing groupPatterns Signed-off-by: Alexandre Fournier --- .../src/providers/config.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index 27dd32367a..eb7f86a8cb 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -385,6 +385,7 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -431,6 +432,7 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -477,6 +479,7 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -523,6 +526,7 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, + groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, From f44dd042c7feeb6c470df5921423e136af032e37 Mon Sep 17 00:00:00 2001 From: Alexandre Fournier Date: Sun, 27 Apr 2025 18:42:30 -0400 Subject: [PATCH 4/6] Avoid calling paginated() multiple time and handle array merge safely Signed-off-by: Alexandre Fournier --- .../GitlabDiscoveryEntityProvider.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index ae2a3d573f..2428c17b62 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -208,7 +208,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { ); } - const locations = await this.GetLocations(); + const locations = await this.getEntities(); await this.connection.applyMutation({ type: 'full', @@ -226,7 +226,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { * * @returns A list of location to be ingested */ - private async GetLocations() { + private async getEntities() { let res: Result = { scanned: 0, matches: [], @@ -238,15 +238,15 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { this.config.groupPatterns !== undefined && this.config.groupPatterns.length > 0 ) { - for (const pattern of this.config.groupPatterns) { - const groups = paginated( - options => this.gitLabClient.listGroups(options), - { - page: 1, - per_page: 50, - }, - ); + const groups = paginated( + options => this.gitLabClient.listGroups(options), + { + page: 1, + per_page: 50, + }, + ); + for (const pattern of this.config.groupPatterns) { for await (const group of groups) { if ( pattern.test(group.full_path) && @@ -258,12 +258,15 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { } for (const group of groupToProcess.values()) { - const tmpRes = await this.GetProjectsToProcess(group.full_path); + const tmpRes = await this.getProjectsToProcess(group.full_path); res.scanned += tmpRes.scanned; - res.matches.push(...tmpRes.matches); + // merge both arrays safely + for (const project of tmpRes.matches) { + res.matches.push(project); + } } } else { - res = await this.GetProjectsToProcess(this.config.group); + res = await this.getProjectsToProcess(this.config.group); } const locations = this.deduplicateProjects(res.matches).map(p => @@ -299,7 +302,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { * @param group - a full path of a GitLab group, can be empty * @returns An array of project to be processed and the number of project scanned */ - private async GetProjectsToProcess(group: string) { + private async getProjectsToProcess(group: string) { const res: Result = { scanned: 0, matches: [], From a715103301c77ca5503043b17744eac89edad798 Mon Sep 17 00:00:00 2001 From: Alexandre Fournier Date: Sat, 10 May 2025 17:18:40 -0400 Subject: [PATCH 5/6] Documents groupPatten is using regex to filter Signed-off-by: Alexandre Fournier --- docs/integrations/gitlab/discovery.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 85daea3649..5b76a9597b 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -151,11 +151,11 @@ catalog: skipForkedRepos: false # Optional. If the project is a fork, skip repository includeArchivedRepos: false # Optional. If project is archived, include repository group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned - groupPatterns: # Optional. Filters for groups based on a list of patterns. Default, no filters. + groupPatterns: # Optional. Filters for groups based on a list of RegEx. Default, no filters. - '^somegroup$' - 'anothergroup' entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` - projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided patter. Defaults to `[\s\S]*`, which means to not filter anything + projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided pattern. Defaults to `[\s\S]*`, which means to not filter anything excludeRepos: [] # Optional. A list of project paths that should be excluded from discovery, e.g. group/subgroup/repo. Should not start or end with a slash. schedule: # Same options as in SchedulerServiceTaskScheduleDefinition. Optional for the Legacy Backend System # supports cron, ISO duration, "human duration" as used in code From e320e2e1837158e4aa675dfb8d99dc1634eb979e Mon Sep 17 00:00:00 2001 From: Alexandre Fournier Date: Sun, 17 Aug 2025 18:38:18 -0400 Subject: [PATCH 6/6] Revert change that introduce groupPatterns and reuse the config groupPattern insteand Signed-off-by: Alexandre Fournier --- .changeset/chubby-results-relax.md | 2 +- docs/integrations/gitlab/discovery.md | 2 +- .../catalog-backend-module-gitlab/config.d.ts | 2 +- .../report.api.md | 3 +- .../src/__testUtils__/handlers.ts | 7 + .../src/__testUtils__/mocks.ts | 14 +- .../src/lib/types.ts | 10 +- .../GitlabDiscoveryEntityProvider.test.ts | 155 +++++++++--------- .../GitlabDiscoveryEntityProvider.ts | 33 ++-- .../GitlabOrgDiscoveryEntityProvider.ts | 8 +- .../src/providers/config.test.ts | 10 -- .../src/providers/config.ts | 23 ++- 12 files changed, 140 insertions(+), 129 deletions(-) diff --git a/.changeset/chubby-results-relax.md b/.changeset/chubby-results-relax.md index a668570694..c20c79085f 100644 --- a/.changeset/chubby-results-relax.md +++ b/.changeset/chubby-results-relax.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend-module-gitlab': minor +'@backstage/plugin-catalog-backend-module-gitlab': patch --- Added support for multiple group patterns instead of a single one to increase flexibility when filtering groups from GitLab. diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 5b76a9597b..0fce7b864e 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -151,7 +151,7 @@ catalog: skipForkedRepos: false # Optional. If the project is a fork, skip repository includeArchivedRepos: false # Optional. If project is archived, include repository group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned - groupPatterns: # Optional. Filters for groups based on a list of RegEx. Default, no filters. + groupPattern: # Optional. Filters for groups based on a list of RegEx. Default, no filters. - '^somegroup$' - 'anothergroup' entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index 4cc7ebe844..8a2b6d8fa5 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -72,7 +72,7 @@ export interface Config { /** * (Optional) RegExp for the Group Name Pattern */ - groupPattern?: string; + groupPattern?: string | string[]; /** * Specifies the types of group membership relations that should be included when ingesting data. * diff --git a/plugins/catalog-backend-module-gitlab/report.api.md b/plugins/catalog-backend-module-gitlab/report.api.md index 896680b41f..f5048835c1 100644 --- a/plugins/catalog-backend-module-gitlab/report.api.md +++ b/plugins/catalog-backend-module-gitlab/report.api.md @@ -111,8 +111,7 @@ export type GitlabProviderConfig = { catalogFile: string; projectPattern: RegExp; userPattern: RegExp; - groupPattern: RegExp; - groupPatterns: RegExp[]; + groupPattern: RegExp | RegExp[]; allowInherited?: boolean; relations?: string[]; orgEnabled?: boolean; diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts index 7549e6faca..9af00d2564 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts @@ -81,6 +81,13 @@ const httpHandlers = [ ); }), + rest.get(`${apiBaseUrl}/groups/group1`, (_, res, ctx) => { + return res( + ctx.set('x-next-page', ''), + ctx.json(all_groups_response.find(g => g.full_path === 'group1')), + ); + }), + rest.get(`${apiBaseUrl}/groups/42`, (_, res, ctx) => { return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' })); }), diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts index 768ccb416b..d64b266183 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts @@ -836,7 +836,7 @@ export const config_groupPatterns_only_noMatch = { gitlab: { 'test-id': { host: 'example.com', - groupPatterns: ['^group8$', '^group9$', '^group10$'], + groupPattern: ['^group8$', '^group9$', '^group10$'], }, }, }, @@ -858,7 +858,7 @@ export const config_groupPatterns_only_Match1Group = { gitlab: { 'test-id': { host: 'example.com', - groupPatterns: ['^group1$', '^group9$', '^group10$'], + groupPattern: ['^group1$', '^group9$', '^group10$'], }, }, }, @@ -880,7 +880,7 @@ export const config_groupPatterns_multiple_matches = { gitlab: { 'test-id': { host: 'example.com', - groupPatterns: ['^group1$', '^group2$', '^group10$'], + groupPattern: ['^group1$', '^group2$', '^group10$'], }, }, }, @@ -902,7 +902,7 @@ export const config_groupPatterns_duplicate_match = { gitlab: { 'test-id': { host: 'example.com', - groupPatterns: ['^group1$', 'subgroup1'], // Both patterns match the same group + groupPattern: ['^group1$', 'subgroup1'], // Both patterns match the same group }, }, }, @@ -1312,6 +1312,12 @@ export const all_groups_response: GitLabGroup[] = [ description: '', full_path: 'group1/subgroup2', }, + { + id: 8, + name: 'awsome-group', + description: '', + full_path: 'awsome-group', + }, ]; export const group_with_subgroups_response: GitLabGroup[] = [ diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index a51e129fbf..aa2f73b35d 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -186,17 +186,11 @@ export type GitlabProviderConfig = { userPattern: RegExp; /** - * Filters found groups based on provided pattern. + * Filters found groups based on provided patterns. * Defaults to `[\s\S]*`, which means to not filter anything * - * @deprecated Use groupPatterns that provide a list instead. */ - groupPattern: RegExp; - - /** - * Optional, provide a list of pattern for each can match a list of groups. - */ - groupPatterns: RegExp[]; + groupPattern: RegExp | RegExp[]; /** * If true, the provider will also add inherited (ascendant) users to the ingested groups. diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts index 7d7bb699f5..f052434035 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -557,14 +557,6 @@ describe('GitlabDiscoveryEntityProvider - events', () => { }); expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); }); - // EventSupportChange: stop add tests >>> - - // add the following test: - // setup: 3 projects, 2 groups - // 1 test where none match --> OK - // 1 test where match only 1 group --> OK - // 1 test where match 2 groups - // 1 test where match 1 group with 2 identical regex it('should ignore projects when none of the groups regex patterns match', async () => { const config = new ConfigReader(mock.config_groupPatterns_only_noMatch); @@ -590,93 +582,96 @@ describe('GitlabDiscoveryEntityProvider - events', () => { expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); // No entities should be applied }); -}); -it('should include only the project that matches one of the group regex patterns', async () => { - const config = new ConfigReader(mock.config_groupPatterns_only_Match1Group); - const schedule = new PersistingTaskRunner(); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; + it('should include only the project that matches one of the group regex patterns', async () => { + const config = new ConfigReader(mock.config_groupPatterns_only_Match1Group); - await provider.connect(entityProviderConnection); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; - await provider.refresh(logger); + await provider.connect(entityProviderConnection); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ - type: 'full', - entities: mock.expected_location_entities_default_branch.filter( - entity => - entity.entity.metadata.annotations[ - 'backstage.io/managed-by-location' - ].includes('/group1/'), // Only projects in 'group2' should match - ), + await provider.refresh(logger); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_location_entities_default_branch.filter( + entity => + entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('/group1/'), // Only projects in 'group2' should match + ), + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); // Only one matching project should be applied }); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); // Only one matching project should be applied -}); -it('should include projects that match multiple group regex patterns', async () => { - const config = new ConfigReader(mock.config_groupPatterns_multiple_matches); + it('should include projects that match multiple group regex patterns', async () => { + const config = new ConfigReader(mock.config_groupPatterns_multiple_matches); - const schedule = new PersistingTaskRunner(); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; - await provider.connect(entityProviderConnection); + await provider.connect(entityProviderConnection); - await provider.refresh(logger); + await provider.refresh(logger); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ - type: 'full', - entities: mock.expected_location_entities_default_branch.filter( - entity => - entity.entity.metadata.annotations[ - 'backstage.io/managed-by-location' - ].includes('/group1/') || - entity.entity.metadata.annotations[ - 'backstage.io/managed-by-location' - ].includes('/group2/'), - ), + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_location_entities_default_branch.filter( + entity => + entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('/group1/') || + entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('/group2/'), + ), + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); // Projects from both groups should be applied }); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); // Projects from both groups should be applied -}); -it('should not create duplicate locations when multiple groupPatterns match the same group', async () => { - const config = new ConfigReader(mock.config_groupPatterns_duplicate_match); + it('should not create duplicate locations when multiple groupPatterns match the same group', async () => { + const config = new ConfigReader(mock.config_groupPatterns_duplicate_match); - const schedule = new PersistingTaskRunner(); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; - await provider.connect(entityProviderConnection); + await provider.connect(entityProviderConnection); - await provider.refresh(logger); + await provider.refresh(logger); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ - type: 'full', - entities: mock.expected_location_entities_default_branch.filter(entity => - entity.entity.metadata.annotations[ - 'backstage.io/managed-by-location' - ].includes('/group1/'), - ), + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_location_entities_default_branch.filter(entity => + entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('/group1/'), + ), + }); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); }); - - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); }); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 2428c17b62..08bd498615 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -233,11 +233,22 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { }; const groupToProcess = new Map(); + let groupFilters; - if ( - this.config.groupPatterns !== undefined && - this.config.groupPatterns.length > 0 - ) { + if (this.config.groupPattern !== undefined) { + const patterns = Array.isArray(this.config.groupPattern) + ? this.config.groupPattern + : [this.config.groupPattern]; + + if (patterns.length === 1 && patterns[0].source === '[\\s\\S]*') { + // if the pattern is a catch-all, we don't need to filter groups + groupFilters = new Array(); + } else { + groupFilters = patterns; + } + } + + if (groupFilters && groupFilters.length > 0) { const groups = paginated( options => this.gitLabClient.listGroups(options), { @@ -246,14 +257,12 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { }, ); - for (const pattern of this.config.groupPatterns) { - for await (const group of groups) { - if ( - pattern.test(group.full_path) && - !groupToProcess.has(group.full_path) - ) { - groupToProcess.set(group.full_path, group); - } + for await (const group of groups) { + if ( + groupFilters.some(groupFilter => groupFilter.test(group.full_path)) && + !groupToProcess.has(group.full_path) + ) { + groupToProcess.set(group.full_path, group); } } diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts index 3eec377e73..3b8720b5f2 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts @@ -111,6 +111,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { private groupEntitiesTransformer: GroupEntitiesTransformer; private groupNameTransformer: GroupNameTransformer; private readonly gitLabClient: GitLabClient; + private readonly groupPatterns: RegExp[]; static fromConfig( config: Config, @@ -198,6 +199,10 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { this.groupNameTransformer = options.groupNameTransformer ?? defaultGroupNameTransformer; + this.groupPatterns = Array.isArray(this.config.groupPattern) + ? this.config.groupPattern + : [this.config.groupPattern]; + this.gitLabClient = new GitLabClient({ config: this.integration.config, logger: this.logger, @@ -464,7 +469,6 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { for await (const group of groups) { groupRes.scanned++; - if (!this.shouldProcessGroup(group)) { logger.debug(`Skipped group: ${group.full_path}`); continue; @@ -798,7 +802,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { private shouldProcessGroup(group: GitLabGroup): boolean { return ( - this.config.groupPattern.test(group.full_path) && + this.groupPatterns.some(pattern => pattern.test(group.full_path)) && (!this.config.group || group.full_path.startsWith(`${this.config.group}/`) || group.full_path === this.config.group) diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index eb7f86a8cb..8790a3fe53 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -55,7 +55,6 @@ describe('config', () => { catalogFile: 'catalog-info.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -102,7 +101,6 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -149,7 +147,6 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -196,7 +193,6 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -244,7 +240,6 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -293,7 +288,6 @@ describe('config', () => { catalogFile: 'catalog-info.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -385,7 +379,6 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -432,7 +425,6 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -479,7 +471,6 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, @@ -526,7 +517,6 @@ describe('config', () => { catalogFile: 'custom-file.yaml', projectPattern: /[\s\S]*/, groupPattern: /[\s\S]*/, - groupPatterns: [], userPattern: /[\s\S]*/, orgEnabled: false, allowInherited: false, diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index 639a1e1177..f635d4c432 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -39,13 +39,21 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { const userPattern = new RegExp( config.getOptionalString('userPattern') ?? /[\s\S]*/, ); - const groupPattern = new RegExp( - config.getOptionalString('groupPattern') ?? /[\s\S]*/, - ); - const groupPatterns: Array = - config - .getOptionalStringArray('groupPatterns') - ?.map(pattern => new RegExp(pattern)) ?? []; + + const configValue = config.getOptional('groupPattern'); + let groupPattern; + + if ((configValue && typeof configValue === 'string') || !configValue) { + groupPattern = new RegExp( + config.getOptionalString('groupPattern') ?? /[\s\S]*/, + ); + } else if (configValue && Array.isArray(configValue)) { + const configPattern = config.getOptionalStringArray('groupPattern') ?? []; + groupPattern = configPattern.map(pattern => new RegExp(pattern)); + } else { + groupPattern = new RegExp(/[\s\S]*/); + } + const orgEnabled: boolean = config.getOptionalBoolean('orgEnabled') ?? false; const allowInherited: boolean = config.getOptionalBoolean('allowInherited') ?? false; @@ -85,7 +93,6 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { projectPattern, userPattern, groupPattern, - groupPatterns, schedule, orgEnabled, allowInherited,