feat: add callback transformers to GitlabOrgDiscoveryEntityProvider

Signed-off-by: Markus <markus.siebert@deutschebahn.com>
This commit is contained in:
Markus
2023-12-18 07:48:43 +01:00
parent ccc4862007
commit 60e4c2a86d
4 changed files with 211 additions and 100 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-gitlab': patch
---
add `groupTransformer`, `userTransformer` and `groupNameTransformer` to allow custom transformations when groups and users are created
@@ -0,0 +1,125 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import {
GitLabGroup,
GitLabUser,
GitlabProviderConfig,
GroupNameTransformer,
} from './types';
import { GitLabIntegrationConfig } from '@backstage/integration';
export function defaultGroupNameTransformer(
group: GitLabGroup,
config: GitlabProviderConfig,
): string {
if (config.group && group.full_path.startsWith(`${config.group}/`)) {
return group.full_path.replace(`${config.group}/`, '').replaceAll('/', '-');
}
return group.full_path.replaceAll('/', '-');
}
export function defaultGroupTransformer(
group: GitLabGroup,
provConfig: GitlabProviderConfig,
groupNameTransformer: GroupNameTransformer,
): GroupEntity {
const annotations: { [annotationName: string]: string } = {};
annotations[`${provConfig.host}/team-path`] = group.full_path;
const entity: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: groupNameTransformer(group, provConfig),
annotations: annotations,
},
spec: {
type: 'team',
children: [],
profile: {
displayName: group.name,
},
},
};
if (group.description) {
entity.metadata.description = group.description;
}
return entity;
}
/**
* The default implementation of the transformation from a graph user entry to
* a User entity.
*
* @public
*/
export function defaultUserTransformer(
user: GitLabUser,
intConfig: GitLabIntegrationConfig,
provConfig: GitlabProviderConfig,
groupNameTransformer: GroupNameTransformer,
): UserEntity {
const annotations: { [annotationName: string]: string } = {};
annotations[`${intConfig.host}/user-login`] = user.web_url;
if (user?.group_saml_identity?.extern_uid) {
annotations[`${intConfig.host}/saml-external-uid`] =
user.group_saml_identity.extern_uid;
}
const entity: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: user.username,
annotations: annotations,
},
spec: {
profile: {
displayName: user.name || undefined,
picture: user.avatar_url || undefined,
},
memberOf: [],
},
};
if (user.email) {
if (!entity.spec) {
entity.spec = {};
}
if (!entity.spec.profile) {
entity.spec.profile = {};
}
entity.spec.profile.email = user.email;
}
if (user.groups) {
for (const group of user.groups) {
if (!entity.spec.memberOf) {
entity.spec.memberOf = [];
}
entity.spec.memberOf.push(groupNameTransformer(group, provConfig));
}
}
return entity;
}
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
import { GitLabIntegrationConfig } from '@backstage/integration';
export type PagedResponse<T> = {
items: T[];
@@ -116,6 +118,10 @@ export type GitLabDescendantGroupsResponse = {
export type GitlabProviderConfig = {
host: string;
/**
* Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned.
* If present this if present, the discovered groups won't contain this prefix
*/
group: string;
id: string;
/**
@@ -136,3 +142,36 @@ export type GitlabProviderConfig = {
schedule?: TaskScheduleDefinition;
skipForkedRepos?: boolean;
};
/**
* Customize how group names are generated
*
* @public
*/
export type GroupNameTransformer = (
group: GitLabGroup,
config: GitlabProviderConfig,
) => string;
/**
* Customize the ingested User entity
*
* @public
*/
export type UserTransformer = (
user: GitLabUser,
intConfig: GitLabIntegrationConfig,
provConfig: GitlabProviderConfig,
groupNameTransformer: GroupNameTransformer,
) => UserEntity;
/**
* Customize the ingested Group entity
*
* @public
*/
export type GroupTransformer = (
group: GitLabGroup,
provConfig: GitlabProviderConfig,
groupNameTransformer: GroupNameTransformer,
) => GroupEntity;
@@ -19,7 +19,6 @@ import {
ANNOTATION_ORIGIN_LOCATION,
Entity,
GroupEntity,
UserEntity,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { GitLabIntegration, ScmIntegrations } from '@backstage/integration';
@@ -37,7 +36,19 @@ import {
paginated,
readGitlabConfigs,
} from '../lib';
import { GitLabGroup, GitLabUser, PagedResponse } from '../lib/types';
import {
GitLabGroup,
GitLabUser,
PagedResponse,
UserTransformer,
GroupTransformer,
GroupNameTransformer,
} from '../lib/types';
import {
defaultGroupNameTransformer,
defaultGroupTransformer,
defaultUserTransformer,
} from '../lib/defaultTransformers';
type Result = {
scanned: number;
@@ -59,6 +70,9 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
private readonly logger: Logger;
private readonly scheduleFn: () => Promise<void>;
private connection?: EntityProviderConnection;
private userTransformer: UserTransformer;
private groupTransformer: GroupTransformer;
private groupNameTransformer: GroupNameTransformer;
static fromConfig(
config: Config,
@@ -66,6 +80,9 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
logger: Logger;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
userTransformer?: UserTransformer;
groupTransformer?: GroupTransformer;
groupNameTransformer?: GroupNameTransformer;
},
): GitlabOrgDiscoveryEntityProvider[] {
if (!options.schedule && !options.scheduler) {
@@ -122,6 +139,9 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
integration: GitLabIntegration;
logger: Logger;
taskRunner: TaskRunner;
userTransformer?: UserTransformer;
groupTransformer?: GroupTransformer;
groupNameTransformer?: GroupNameTransformer;
}) {
this.config = options.config;
this.integration = options.integration;
@@ -129,6 +149,10 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
target: this.getProviderName(),
});
this.scheduleFn = this.createScheduleFn(options.taskRunner);
this.userTransformer = options.userTransformer ?? defaultUserTransformer;
this.groupTransformer = options.groupTransformer ?? defaultGroupTransformer;
this.groupNameTransformer =
options.groupNameTransformer ?? defaultGroupNameTransformer;
}
getProviderName(): string {
@@ -271,12 +295,14 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
});
const userEntities = res.matches.map(p =>
this.createUserEntity(p, this.integration.config.host),
);
const groupEntities = this.createGroupEntities(
groupsWithUsers,
this.integration.config.host,
this.userTransformer(
p,
this.integration.config,
this.config,
this.groupNameTransformer,
),
);
const groupEntities = this.createGroupEntities(groupsWithUsers);
await this.connection.applyMutation({
type: 'full',
@@ -291,10 +317,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
});
}
private createGroupEntities(
groupResult: GitLabGroup[],
host: string,
): GroupEntity[] {
private createGroupEntities(groupResult: GitLabGroup[]): GroupEntity[] {
const idMapped: { [groupId: number]: GitLabGroup } = {};
const entities: GroupEntity[] = [];
@@ -303,11 +326,16 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
}
for (const group of groupResult) {
const entity = this.createGroupEntity(group, host);
const entity = this.groupTransformer(
group,
this.config,
this.groupNameTransformer,
);
if (group.parent_id && idMapped.hasOwnProperty(group.parent_id)) {
entity.spec.parent = this.groupName(
idMapped[group.parent_id].full_path,
entity.spec.parent = this.groupNameTransformer(
idMapped[group.parent_id],
this.config,
);
}
@@ -334,90 +362,4 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
entity,
) as Entity;
}
private createUserEntity(user: GitLabUser, host: string): UserEntity {
const annotations: { [annotationName: string]: string } = {};
annotations[`${host}/user-login`] = user.web_url;
if (user?.group_saml_identity?.extern_uid) {
annotations[`${host}/saml-external-uid`] =
user.group_saml_identity.extern_uid;
}
const entity: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: user.username,
annotations: annotations,
},
spec: {
profile: {
displayName: user.name || undefined,
picture: user.avatar_url || undefined,
},
memberOf: [],
},
};
if (user.email) {
if (!entity.spec) {
entity.spec = {};
}
if (!entity.spec.profile) {
entity.spec.profile = {};
}
entity.spec.profile.email = user.email;
}
if (user.groups) {
for (const group of user.groups) {
if (!entity.spec.memberOf) {
entity.spec.memberOf = [];
}
entity.spec.memberOf.push(this.groupName(group.full_path));
}
}
return entity;
}
private groupName(full_path: string): string {
if (this.config.group && full_path.startsWith(`${this.config.group}/`)) {
return full_path
.replace(`${this.config.group}/`, '')
.replaceAll('/', '-');
}
return full_path.replaceAll('/', '-');
}
private createGroupEntity(group: GitLabGroup, host: string): GroupEntity {
const annotations: { [annotationName: string]: string } = {};
annotations[`${host}/team-path`] = group.full_path;
const entity: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: this.groupName(group.full_path),
annotations: annotations,
},
spec: {
type: 'team',
children: [],
profile: {
displayName: group.name,
},
},
};
if (group.description) {
entity.metadata.description = group.description;
}
return entity;
}
}