Merge pull request #18889 from angom1/GD-294-users-groups-ingestion
Fix user ingestion for GitlabOrgDiscoveryEntityProvider for GitLab.com
This commit is contained in:
@@ -398,17 +398,29 @@ describe('GitLabClient', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
describe('getGroupMembers', () => {
|
||||
it('gets member IDs', async () => {
|
||||
describe('get gitlab.com users', () => {
|
||||
it('gets all users under group', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.operation((_, res, ctx) =>
|
||||
.query('getGroupMembers', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
groupMembers: {
|
||||
nodes: [{ user: { id: 'gid://gitlab/User/1' } }],
|
||||
nodes: [
|
||||
{
|
||||
user: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
username: 'user1',
|
||||
commitEmail: 'user1@example.com',
|
||||
name: 'user1',
|
||||
state: 'active',
|
||||
webUrl: 'user1.com',
|
||||
avatarUrl: 'user1',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: 'end',
|
||||
hasNextPage: false,
|
||||
@@ -424,16 +436,30 @@ describe('GitLabClient', () => {
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const members = await client.getGroupMembers('group1');
|
||||
const saasMembers = (
|
||||
await client.getGroupMembers('group1', ['DIRECT, DESCENDANTS'])
|
||||
).items;
|
||||
const expectedSaasMember = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
email: 'user1@example.com',
|
||||
name: 'user1',
|
||||
state: 'active',
|
||||
web_url: 'user1.com',
|
||||
avatar_url: 'user1',
|
||||
},
|
||||
];
|
||||
|
||||
expect(members).toEqual([1]);
|
||||
expect(saasMembers.length).toEqual(1);
|
||||
expect(saasMembers).toEqual(expectedSaasMember);
|
||||
});
|
||||
|
||||
it('gets member IDs with token without full permissions', async () => {
|
||||
it('gets all users with token without full permissions', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.operation((_, res, ctx) =>
|
||||
.query('getGroupMembers', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {},
|
||||
@@ -446,16 +472,18 @@ describe('GitLabClient', () => {
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const members = await client.getGroupMembers('group1');
|
||||
const saasMembers = (
|
||||
await client.getGroupMembers('group1', ['DIRECT, DESCENDANTS'])
|
||||
).items;
|
||||
|
||||
expect(members).toEqual([]);
|
||||
expect(saasMembers).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects when GraphQL returns errors', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.operation((_, res, ctx) =>
|
||||
.query('getGroupMembers', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.errors([
|
||||
{ message: 'Unexpected end of document', locations: [] },
|
||||
@@ -468,7 +496,354 @@ describe('GitLabClient', () => {
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
await expect(() => client.getGroupMembers('group1')).rejects.toThrow(
|
||||
await expect(() =>
|
||||
client.getGroupMembers('group1', ['DIRECT, DESCENDANTS']),
|
||||
).rejects.toThrow(
|
||||
'GraphQL errors: [{"message":"Unexpected end of document","locations":[]}]',
|
||||
);
|
||||
});
|
||||
it('traverses multi-page results', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.query('getGroupMembers', async (req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
groupMembers: {
|
||||
nodes: req.variables.endCursor
|
||||
? [
|
||||
{
|
||||
user: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
username: 'user1',
|
||||
commitEmail: 'user1@example.com',
|
||||
name: 'user1',
|
||||
state: 'active',
|
||||
webUrl: 'user1.com',
|
||||
avatarUrl: 'user1',
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
user: {
|
||||
id: 'gid://gitlab/User/2',
|
||||
username: 'user2',
|
||||
commitEmail: 'user2@example.com',
|
||||
name: 'user2',
|
||||
state: 'active',
|
||||
webUrl: 'user2.com',
|
||||
avatarUrl: 'user2',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: req.variables.endCursor ? 'end' : 'next',
|
||||
hasNextPage: !req.variables.endCursor,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const client = new GitLabClient({
|
||||
config: MOCK_CONFIG,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const saasMembers = (
|
||||
await client.getGroupMembers('group1', ['DIRECT, DESCENDANTS'])
|
||||
).items;
|
||||
|
||||
const expectedSaasMember1 = {
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
email: 'user1@example.com',
|
||||
name: 'user1',
|
||||
state: 'active',
|
||||
web_url: 'user1.com',
|
||||
avatar_url: 'user1',
|
||||
};
|
||||
|
||||
const expectedSaasMember2 = {
|
||||
id: 2,
|
||||
username: 'user2',
|
||||
email: 'user2@example.com',
|
||||
name: 'user2',
|
||||
state: 'active',
|
||||
web_url: 'user2.com',
|
||||
avatar_url: 'user2',
|
||||
};
|
||||
|
||||
expect(saasMembers.length).toEqual(2);
|
||||
expect(saasMembers[0]).toEqual(expectedSaasMember2);
|
||||
expect(saasMembers[1]).toEqual(expectedSaasMember1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listDescendantGroups', () => {
|
||||
it('gets all groups under root', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.query('listDescendantGroups', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
descendantGroups: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Group/1',
|
||||
name: 'group1',
|
||||
description: 'description1',
|
||||
fullPath: 'path/group1',
|
||||
parent: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: 'end',
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const client = new GitLabClient({
|
||||
config: MOCK_CONFIG,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const saasGroups = (await client.listDescendantGroups('group1')).items;
|
||||
|
||||
const expectedSaasGroup = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'group1',
|
||||
description: 'description1',
|
||||
full_path: 'path/group1',
|
||||
parent_id: 123,
|
||||
},
|
||||
];
|
||||
|
||||
expect(saasGroups.length).toEqual(1);
|
||||
expect(saasGroups).toEqual(expectedSaasGroup);
|
||||
});
|
||||
|
||||
it('gets all descendant groups with token without full permissions', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.query('listDescendantGroups', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const client = new GitLabClient({
|
||||
config: MOCK_CONFIG,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const saasGroups = (await client.listDescendantGroups('group1')).items;
|
||||
|
||||
expect(saasGroups).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects when GraphQL returns errors', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.query('listDescendantGroups', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.errors([
|
||||
{ message: 'Unexpected end of document', locations: [] },
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
const client = new GitLabClient({
|
||||
config: MOCK_CONFIG,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
await expect(() => client.listDescendantGroups('group1')).rejects.toThrow(
|
||||
'GraphQL errors: [{"message":"Unexpected end of document","locations":[]}]',
|
||||
);
|
||||
});
|
||||
it('traverses multi-page results', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.query('listDescendantGroups', async (req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
descendantGroups: {
|
||||
nodes: req.variables.endCursor
|
||||
? [
|
||||
{
|
||||
id: 'gid://gitlab/Group/1',
|
||||
name: 'group1',
|
||||
description: 'description1',
|
||||
fullPath: 'path/group1',
|
||||
parent: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'gid://gitlab/Group/2',
|
||||
name: 'group2',
|
||||
description: 'description2',
|
||||
fullPath: 'path/group2',
|
||||
parent: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: req.variables.endCursor ? 'end' : 'next',
|
||||
hasNextPage: !req.variables.endCursor,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const client = new GitLabClient({
|
||||
config: MOCK_CONFIG,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const saasGroups = (await client.listDescendantGroups('root')).items;
|
||||
|
||||
const expectedSaasGroup1 = {
|
||||
id: 1,
|
||||
name: 'group1',
|
||||
description: 'description1',
|
||||
full_path: 'path/group1',
|
||||
parent_id: 123,
|
||||
};
|
||||
|
||||
const expectedSaasGroup2 = {
|
||||
id: 2,
|
||||
name: 'group2',
|
||||
description: 'description2',
|
||||
full_path: 'path/group2',
|
||||
parent_id: 123,
|
||||
};
|
||||
|
||||
expect(saasGroups.length).toEqual(2);
|
||||
expect(saasGroups[0]).toEqual(expectedSaasGroup2);
|
||||
expect(saasGroups[1]).toEqual(expectedSaasGroup1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupMembers', () => {
|
||||
it('gets member IDs', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.query('getGroupMembers', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
groupMembers: {
|
||||
nodes: [
|
||||
{
|
||||
user: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
username: 'user1',
|
||||
commitEmail: 'user1@example.com',
|
||||
name: 'user1',
|
||||
state: 'active',
|
||||
webUrl: 'user1.com',
|
||||
avatarUrl: 'user1',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: 'end',
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const client = new GitLabClient({
|
||||
config: MOCK_CONFIG,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const members = await client.getGroupMembers('group1', ['DIRECT']);
|
||||
|
||||
const user = {
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
email: 'user1@example.com',
|
||||
name: 'user1',
|
||||
state: 'active',
|
||||
web_url: 'user1.com',
|
||||
avatar_url: 'user1',
|
||||
};
|
||||
|
||||
expect(members.items).toEqual([user]);
|
||||
});
|
||||
|
||||
it('gets member IDs with token without full permissions', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.query('getGroupMembers', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const client = new GitLabClient({
|
||||
config: MOCK_CONFIG,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const members = await client.getGroupMembers('group1', ['DIRECT']);
|
||||
|
||||
expect(members.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects when GraphQL returns errors', async () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.query('getGroupMembers', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.errors([
|
||||
{ message: 'Unexpected end of document', locations: [] },
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
const client = new GitLabClient({
|
||||
config: MOCK_CONFIG,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
await expect(() =>
|
||||
client.getGroupMembers('group1', ['DIRECT']),
|
||||
).rejects.toThrow(
|
||||
'GraphQL errors: [{"message":"Unexpected end of document","locations":[]}]',
|
||||
);
|
||||
});
|
||||
@@ -477,7 +852,7 @@ describe('GitLabClient', () => {
|
||||
server.use(
|
||||
graphql
|
||||
.link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
|
||||
.operation((req, res, ctx) =>
|
||||
.query('getGroupMembers', async (req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
@@ -500,9 +875,10 @@ describe('GitLabClient', () => {
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
const members = await client.getGroupMembers('group1');
|
||||
const members = await client.getGroupMembers('group1', ['DIRECT']);
|
||||
|
||||
expect(members).toEqual([1, 2]);
|
||||
expect(members.items[0].id).toEqual(1);
|
||||
expect(members.items[1].id).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,12 @@ import {
|
||||
GitLabIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { Logger } from 'winston';
|
||||
import { GitLabGroup, GitLabGroupMembersResponse, GitLabUser } from './types';
|
||||
import {
|
||||
GitLabGroup,
|
||||
GitLabDescendantGroupsResponse,
|
||||
GitLabGroupMembersResponse,
|
||||
GitLabUser,
|
||||
} from './types';
|
||||
|
||||
export type CommonListOptions = {
|
||||
[key: string]: string | number | boolean | undefined;
|
||||
@@ -92,12 +97,15 @@ export class GitLabClient {
|
||||
return this.pagedRequest(`/groups`, options);
|
||||
}
|
||||
|
||||
async getGroupMembers(groupPath: string): Promise<number[]> {
|
||||
const memberIds = [];
|
||||
async listDescendantGroups(
|
||||
groupPath: string,
|
||||
): Promise<PagedResponse<GitLabGroup>> {
|
||||
const items: GitLabGroup[] = [];
|
||||
let hasNextPage: boolean = false;
|
||||
let endCursor: string | null = null;
|
||||
|
||||
do {
|
||||
const response: GitLabGroupMembersResponse = await fetch(
|
||||
const response: GitLabDescendantGroupsResponse = await fetch(
|
||||
`${this.config.baseUrl}/api/graphql`,
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -107,21 +115,111 @@ export class GitLabClient {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
variables: { group: groupPath, endCursor },
|
||||
query: `query($group: ID!, $endCursor: String) {
|
||||
group(fullPath: $group) {
|
||||
groupMembers(first: 100, relations: [DIRECT], after: $endCursor) {
|
||||
nodes {
|
||||
user {
|
||||
query: /* GraphQL */ `
|
||||
query listDescendantGroups($group: ID!, $endCursor: String) {
|
||||
group(fullPath: $group) {
|
||||
descendantGroups(first: 100, after: $endCursor) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
description
|
||||
fullPath
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
`,
|
||||
}),
|
||||
},
|
||||
).then(r => r.json());
|
||||
if (response.errors) {
|
||||
throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
|
||||
}
|
||||
|
||||
if (!response.data.group?.descendantGroups?.nodes) {
|
||||
this.logger.warn(
|
||||
`Couldn't get groups under ${groupPath}. The provided token might not have sufficient permissions`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const groupItem of response.data.group.descendantGroups.nodes.filter(
|
||||
group => group?.id,
|
||||
)) {
|
||||
const formattedGroupResponse = {
|
||||
id: Number(groupItem.id.replace(/^gid:\/\/gitlab\/Group\//, '')),
|
||||
name: groupItem.name,
|
||||
description: groupItem.description,
|
||||
full_path: groupItem.fullPath,
|
||||
parent_id: Number(
|
||||
groupItem.parent.id.replace(/^gid:\/\/gitlab\/Group\//, ''),
|
||||
),
|
||||
};
|
||||
|
||||
items.push(formattedGroupResponse);
|
||||
}
|
||||
({ hasNextPage, endCursor } =
|
||||
response.data.group.descendantGroups.pageInfo);
|
||||
} while (hasNextPage);
|
||||
return { items };
|
||||
}
|
||||
|
||||
async getGroupMembers(
|
||||
groupPath: string,
|
||||
relations: string[],
|
||||
): Promise<PagedResponse<GitLabUser>> {
|
||||
const items: GitLabUser[] = [];
|
||||
let hasNextPage: boolean = false;
|
||||
let endCursor: string | null = null;
|
||||
do {
|
||||
const response: GitLabGroupMembersResponse = await fetch(
|
||||
`${this.config.baseUrl}/api/graphql`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getGitLabRequestOptions(this.config).headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
variables: { group: groupPath, relations: relations, endCursor },
|
||||
query: /* GraphQL */ `
|
||||
query getGroupMembers(
|
||||
$group: ID!
|
||||
$relations: [GroupMemberRelation!]
|
||||
$endCursor: String
|
||||
) {
|
||||
group(fullPath: $group) {
|
||||
groupMembers(
|
||||
first: 100
|
||||
relations: $relations
|
||||
after: $endCursor
|
||||
) {
|
||||
nodes {
|
||||
user {
|
||||
id
|
||||
username
|
||||
commitEmail
|
||||
name
|
||||
state
|
||||
webUrl
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
},
|
||||
).then(r => r.json());
|
||||
@@ -136,16 +234,24 @@ export class GitLabClient {
|
||||
continue;
|
||||
}
|
||||
|
||||
memberIds.push(
|
||||
...response.data.group.groupMembers.nodes
|
||||
.filter(n => n.user)
|
||||
.map(node =>
|
||||
Number(node.user.id.replace(/^gid:\/\/gitlab\/User\//, '')),
|
||||
),
|
||||
);
|
||||
for (const userItem of response.data.group.groupMembers.nodes.filter(
|
||||
user => user.user?.id,
|
||||
)) {
|
||||
const formattedUserResponse = {
|
||||
id: Number(userItem.user.id.replace(/^gid:\/\/gitlab\/User\//, '')),
|
||||
username: userItem.user.username,
|
||||
email: userItem.user.commitEmail,
|
||||
name: userItem.user.name,
|
||||
state: userItem.user.state,
|
||||
web_url: userItem.user.webUrl,
|
||||
avatar_url: userItem.user.avatarUrl,
|
||||
};
|
||||
|
||||
items.push(formattedUserResponse);
|
||||
}
|
||||
({ hasNextPage, endCursor } = response.data.group.groupMembers.pageInfo);
|
||||
} while (hasNextPage);
|
||||
return memberIds;
|
||||
return { items };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,7 +60,17 @@ export type GitLabGroupMembersResponse = {
|
||||
data: {
|
||||
group: {
|
||||
groupMembers: {
|
||||
nodes: { user: { id: string } }[];
|
||||
nodes: {
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
commitEmail: string;
|
||||
name: string;
|
||||
state: string;
|
||||
webUrl: string;
|
||||
avatarUrl: string;
|
||||
};
|
||||
}[];
|
||||
pageInfo: {
|
||||
endCursor: string;
|
||||
hasNextPage: boolean;
|
||||
@@ -70,6 +80,31 @@ export type GitLabGroupMembersResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type GitLabDescendantGroupsResponse = {
|
||||
errors: { message: string }[];
|
||||
data: {
|
||||
group: {
|
||||
descendantGroups: {
|
||||
nodes: [
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
fullPath: string;
|
||||
parent: {
|
||||
id: string;
|
||||
};
|
||||
},
|
||||
];
|
||||
pageInfo: {
|
||||
endCursor: string;
|
||||
hasNextPage: false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type GitlabProviderConfig = {
|
||||
host: string;
|
||||
group: string;
|
||||
|
||||
+289
@@ -475,6 +475,259 @@ describe('GitlabOrgDiscoveryEntityProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('apply full update on scheduled execution for gitlab.com', async () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
apiBaseUrl: 'https://gitlab.com/api/v4',
|
||||
token: '1234',
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
gitlab: {
|
||||
'test-id': {
|
||||
host: 'gitlab.com',
|
||||
group: 'group1',
|
||||
orgEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
};
|
||||
const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
schedule,
|
||||
})[0];
|
||||
expect(provider.getProviderName()).toEqual(
|
||||
'GitlabOrgDiscoveryEntityProvider:test-id',
|
||||
);
|
||||
|
||||
server.use(
|
||||
graphql
|
||||
.link('https://gitlab.com/api/graphql')
|
||||
.query('listDescendantGroups', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
descendantGroups: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Group/456',
|
||||
name: 'group2',
|
||||
description: 'Group2',
|
||||
fullPath: 'group1/group2',
|
||||
parent: {
|
||||
id: 'gid://gitlab/Group/123',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Group/789',
|
||||
name: 'group3',
|
||||
description: 'Group3',
|
||||
fullPath: 'group1/group3',
|
||||
parent: {
|
||||
id: 'gid://gitlab/Group/123',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: 'end',
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
graphql
|
||||
.link('https://gitlab.com/api/graphql')
|
||||
.query('getGroupMembers', async (_, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
groupMembers: {
|
||||
nodes: [
|
||||
{
|
||||
user: {
|
||||
id: 'gid://gitlab/User/12',
|
||||
username: 'testuser1',
|
||||
commitEmail: 'testuser1@example.com',
|
||||
state: 'active',
|
||||
name: 'Test User 1',
|
||||
webUrl: 'https://gitlab.com/testuser1',
|
||||
avatarUrl: 'https://secure.gravatar.com/',
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
id: 'gid://gitlab/User/34',
|
||||
username: 'testuser2',
|
||||
commitEmail: 'testuser2@example.com',
|
||||
state: 'active',
|
||||
name: 'Test User 2',
|
||||
webUrl: 'https://gitlab.com/testuser2',
|
||||
avatarUrl: 'https://secure.gravatar.com/',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: 'end',
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
graphql
|
||||
.link('https://gitlab.com/api/graphql')
|
||||
.query('getGroupMembers', async (req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
group: {
|
||||
groupMembers: {
|
||||
nodes:
|
||||
req.variables.group === 'group1/group2'
|
||||
? [{ user: { id: 'gid://gitlab/User/12' } }]
|
||||
: [{ user: { id: 'gid://gitlab/User/34' } }],
|
||||
pageInfo: {
|
||||
endCursor: 'end',
|
||||
hasNextPage: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
const taskDef = schedule.getTasks()[0];
|
||||
expect(taskDef.id).toEqual(
|
||||
'GitlabOrgDiscoveryEntityProvider:test-id:refresh',
|
||||
);
|
||||
await (taskDef.fn as () => Promise<void>)();
|
||||
|
||||
const expectedEntities = [
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://gitlab.com/testuser1',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser1',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser1',
|
||||
},
|
||||
name: 'testuser1',
|
||||
},
|
||||
spec: {
|
||||
memberOf: ['group2', 'group3'],
|
||||
profile: {
|
||||
displayName: 'Test User 1',
|
||||
email: 'testuser1@example.com',
|
||||
picture: 'https://secure.gravatar.com/',
|
||||
},
|
||||
},
|
||||
},
|
||||
locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id',
|
||||
},
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://gitlab.com/testuser2',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/testuser2',
|
||||
'gitlab.com/user-login': 'https://gitlab.com/testuser2',
|
||||
},
|
||||
name: 'testuser2',
|
||||
},
|
||||
spec: {
|
||||
memberOf: ['group2', 'group3'],
|
||||
profile: {
|
||||
displayName: 'Test User 2',
|
||||
email: 'testuser2@example.com',
|
||||
picture: 'https://secure.gravatar.com/',
|
||||
},
|
||||
},
|
||||
},
|
||||
locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id',
|
||||
},
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://gitlab.com/group1/group2',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/group1/group2',
|
||||
'gitlab.com/team-path': 'group1/group2',
|
||||
},
|
||||
description: 'Group2',
|
||||
name: 'group2',
|
||||
},
|
||||
spec: {
|
||||
children: [],
|
||||
profile: {
|
||||
displayName: 'group2',
|
||||
},
|
||||
type: 'team',
|
||||
},
|
||||
},
|
||||
locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id',
|
||||
},
|
||||
{
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Group',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://gitlab.com/group1/group3',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://gitlab.com/group1/group3',
|
||||
'gitlab.com/team-path': 'group1/group3',
|
||||
},
|
||||
description: 'Group3',
|
||||
name: 'group3',
|
||||
},
|
||||
spec: {
|
||||
children: [],
|
||||
profile: {
|
||||
displayName: 'group3',
|
||||
},
|
||||
type: 'team',
|
||||
},
|
||||
},
|
||||
locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id',
|
||||
},
|
||||
];
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
|
||||
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
|
||||
type: 'full',
|
||||
entities: expectedEntities,
|
||||
});
|
||||
});
|
||||
|
||||
it('fail without schedule and scheduler', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
@@ -543,6 +796,42 @@ describe('GitlabOrgDiscoveryEntityProvider', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('fail with scheduler but no group config when host is gitlab.com', () => {
|
||||
const scheduler = {
|
||||
createScheduledTaskRunner: (_: any) => jest.fn(),
|
||||
} as unknown as PluginTaskScheduler;
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
apiBaseUrl: 'https://gitlab.com/api/v4',
|
||||
token: '1234',
|
||||
},
|
||||
],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
gitlab: {
|
||||
'test-id': {
|
||||
host: 'gitlab.com',
|
||||
orgEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
GitlabOrgDiscoveryEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
}),
|
||||
).toThrow(
|
||||
`Missing 'group' value for GitlabOrgDiscoveryEntityProvider:test-id`,
|
||||
);
|
||||
});
|
||||
|
||||
it('single simple provider config with schedule in config', async () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const scheduler = {
|
||||
|
||||
+32
-12
@@ -89,6 +89,12 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
|
||||
);
|
||||
}
|
||||
|
||||
if (!providerConfig.group && providerConfig.host === 'gitlab.com') {
|
||||
throw new Error(
|
||||
`Missing 'group' value for GitlabOrgDiscoveryEntityProvider:${providerConfig.id}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!options.schedule && !providerConfig.schedule) {
|
||||
throw new Error(
|
||||
`No schedule provided neither via code nor config for GitlabOrgDiscoveryEntityProvider:${providerConfig.id}.`,
|
||||
@@ -168,19 +174,29 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
|
||||
logger: logger,
|
||||
});
|
||||
|
||||
const users = paginated<GitLabUser>(options => client.listUsers(options), {
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
active: true,
|
||||
});
|
||||
let groups;
|
||||
let users;
|
||||
|
||||
const groups = paginated<GitLabGroup>(
|
||||
options => client.listGroups(options),
|
||||
{
|
||||
if (client.isSelfManaged()) {
|
||||
groups = paginated<GitLabGroup>(options => client.listGroups(options), {
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
users = paginated<GitLabUser>(options => client.listUsers(options), {
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
active: true,
|
||||
});
|
||||
} else {
|
||||
groups = (await client.listDescendantGroups(this.config.group)).items;
|
||||
users = (
|
||||
await client.getGroupMembers(this.config.group.split('/')[0], [
|
||||
'DIRECT',
|
||||
'DESCENDANTS',
|
||||
])
|
||||
).items;
|
||||
}
|
||||
|
||||
const idMappedUser: { [userId: number]: GitLabUser } = {};
|
||||
|
||||
@@ -224,8 +240,12 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
|
||||
groupRes.scanned++;
|
||||
groupRes.matches.push(group);
|
||||
|
||||
for (const id of await client.getGroupMembers(group.full_path)) {
|
||||
const user = idMappedUser[id];
|
||||
const groupUsers = await client.getGroupMembers(group.full_path, [
|
||||
'DIRECT',
|
||||
]);
|
||||
|
||||
for (const groupUser of groupUsers.items) {
|
||||
const user = idMappedUser[groupUser.id];
|
||||
if (user) {
|
||||
user.groups = (user.groups ?? []).concat(group);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user