catalog-github: add default namespace support to GithubMultiOrgEntityProvider

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2024-05-08 20:15:06 +02:00
parent a940cd9f11
commit 3e8d4ca63e
2 changed files with 318 additions and 16 deletions
@@ -68,26 +68,24 @@ describe('GithubMultiOrgEntityProvider', () => {
headers: { token: 'blah' },
type: 'app',
});
const githubCredentialsProvider: GithubCredentialsProvider = {
getCredentials: mockGetCredentials,
};
entityProvider = new GithubMultiOrgEntityProvider({
id: 'my-id',
gitHubConfig,
githubCredentialsProvider,
githubUrl: 'https://github.com',
logger,
orgs: ['orgA', 'orgB'], // only include for tests that require it
});
entityProvider.connect(entityProviderConnection);
});
afterEach(() => jest.resetAllMocks());
it('should read specified orgs', async () => {
entityProvider = new GithubMultiOrgEntityProvider({
id: 'my-id',
gitHubConfig,
githubCredentialsProvider: {
getCredentials: mockGetCredentials,
},
githubUrl: 'https://github.com',
logger,
orgs: ['orgA', 'orgB'],
});
await entityProvider.connect(entityProviderConnection);
mockClient
.mockResolvedValueOnce({
organization: {
@@ -348,6 +346,19 @@ describe('GithubMultiOrgEntityProvider', () => {
});
it('should read every accessible org', async () => {
entityProvider = new GithubMultiOrgEntityProvider({
id: 'my-id',
gitHubConfig,
githubCredentialsProvider: {
getCredentials: mockGetCredentials,
},
githubUrl: 'https://github.com',
logger,
orgs: ['orgA', 'orgB'],
});
await entityProvider.connect(entityProviderConnection);
getAllInstallationsMock.mockResolvedValue([
{
target_type: 'Organization',
@@ -636,7 +647,284 @@ describe('GithubMultiOrgEntityProvider', () => {
});
});
it('should use the default namespace if options.defaultNamespace is provided', async () => {
mockClient
.mockResolvedValueOnce({
organization: {
membersWithRole: {
pageInfo: { hasNextPage: false },
nodes: [
{
login: 'a',
name: 'b',
bio: 'c',
email: 'd',
avatarUrl: 'e',
},
{
login: 'x',
name: 'y',
bio: 'z',
email: 'w',
avatarUrl: 'v',
},
],
},
},
})
.mockResolvedValueOnce({
organization: {
teams: {
pageInfo: { hasNextPage: false },
nodes: [
{
slug: 'team',
combinedSlug: 'orgC/team',
name: 'Team',
description: 'The one and only team',
avatarUrl: 'http://example.com/team.jpeg',
parentTeam: {
slug: 'parent',
combinedSlug: '',
members: { pageInfo: { hasNextPage: false }, nodes: [] },
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }, { login: 'x' }],
},
},
],
},
},
})
.mockResolvedValueOnce({
organization: {
membersWithRole: {
pageInfo: { hasNextPage: false },
nodes: [
{
login: 'a',
name: 'b',
bio: 'c',
email: 'd',
avatarUrl: 'e',
},
{
login: 'x',
name: 'y',
bio: 'z',
email: 'w',
avatarUrl: 'v',
},
{
login: 'q',
name: 'r',
bio: 's',
email: 't',
avatarUrl: 'u',
},
],
},
},
})
.mockResolvedValueOnce({
organization: {
teams: {
pageInfo: { hasNextPage: false },
nodes: [
{
slug: 'team',
combinedSlug: 'orgD/team',
name: 'Team',
description: 'The one and only team',
avatarUrl: 'http://example.com/team.jpeg',
parentTeam: {
slug: 'parent',
combinedSlug: '',
members: { pageInfo: { hasNextPage: false }, nodes: [] },
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }, { login: 'q' }],
},
},
],
},
},
});
(graphql.defaults as jest.Mock).mockReturnValue(mockClient);
entityProvider = new GithubMultiOrgEntityProvider({
id: 'my-id',
gitHubConfig,
githubCredentialsProvider: {
getCredentials: mockGetCredentials,
},
githubUrl: 'https://github.com',
logger,
orgs: ['orgA', 'orgB'],
defaultNamespace: true,
});
await entityProvider.connect(entityProviderConnection);
await entityProvider.read();
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
entities: [
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/a',
'backstage.io/managed-by-origin-location':
'url:https://github.com/a',
'github.com/user-login': 'a',
},
description: 'c',
name: 'a',
},
spec: {
memberOf: ['team'],
profile: {
displayName: 'b',
email: 'd',
picture: 'e',
},
},
},
locationKey: 'github-multi-org-provider:my-id',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/x',
'backstage.io/managed-by-origin-location':
'url:https://github.com/x',
'github.com/user-login': 'x',
},
description: 'z',
name: 'x',
},
spec: {
memberOf: ['team'],
profile: {
displayName: 'y',
email: 'w',
picture: 'v',
},
},
},
locationKey: 'github-multi-org-provider:my-id',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/q',
'backstage.io/managed-by-origin-location':
'url:https://github.com/q',
'github.com/user-login': 'q',
},
description: 's',
name: 'q',
},
spec: {
memberOf: ['team'],
profile: {
displayName: 'r',
email: 't',
picture: 'u',
},
},
},
locationKey: 'github-multi-org-provider:my-id',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/orgs/orgC/teams/team',
'backstage.io/managed-by-origin-location':
'url:https://github.com/orgs/orgC/teams/team',
'github.com/team-slug': 'orgC/team',
},
name: 'team',
description: 'The one and only team',
},
spec: {
children: [],
parent: 'parent',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
type: 'team',
members: ['default/a', 'default/x'],
},
},
locationKey: 'github-multi-org-provider:my-id',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/orgs/orgD/teams/team',
'backstage.io/managed-by-origin-location':
'url:https://github.com/orgs/orgD/teams/team',
'github.com/team-slug': 'orgD/team',
},
name: 'team',
description: 'The one and only team',
},
spec: {
children: [],
parent: 'parent',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
type: 'team',
members: ['default/a', 'default/q'],
},
},
locationKey: 'github-multi-org-provider:my-id',
},
],
type: 'full',
});
});
it('should not call applyMutation if an error is thrown', async () => {
entityProvider = new GithubMultiOrgEntityProvider({
id: 'my-id',
gitHubConfig,
githubCredentialsProvider: {
getCredentials: mockGetCredentials,
},
githubUrl: 'https://github.com',
logger,
orgs: ['orgA', 'orgB'],
});
await entityProvider.connect(entityProviderConnection);
mockClient.mockImplementationOnce(() => {
throw new Error('Network Error');
});
@@ -143,6 +143,15 @@ export interface GithubMultiOrgEntityProviderOptions {
*/
githubCredentialsProvider?: GithubCredentialsProvider;
/**
* Use the default namespace for groups. By default, groups will be namespaced according to their GitHub org.
*
* @remarks
*
* If set to true, groups with the same name across different orgs will be considered the same group.
*/
defaultNamespace?: boolean;
/**
* Optionally include a user transformer for transforming from GitHub users to User Entities
*/
@@ -198,6 +207,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
userTransformer: options.userTransformer,
teamTransformer: options.teamTransformer,
events: options.events,
defaultNamespace: options.defaultNamespace,
});
provider.schedule(options.schedule);
@@ -216,6 +226,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
orgs?: string[];
userTransformer?: UserTransformer;
teamTransformer?: TeamTransformer;
defaultNamespace?: boolean;
},
) {}
@@ -846,7 +857,10 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
const result = await defaultOrganizationTeamTransformer(team, ctx);
if (result && result.spec) {
result.metadata.namespace = ctx.org.toLocaleLowerCase('en-US');
if (!this.options.defaultNamespace) {
result.metadata.namespace = ctx.org.toLocaleLowerCase('en-US');
}
// Group `spec.members` inherits the namespace of it's group so need to explicitly specify refs here
result.spec.members = team.members.map(
user => `${DEFAULT_NAMESPACE}/${user.login}`,