From 95491d2fa4608708027ea343f50bef9b687ba942 Mon Sep 17 00:00:00 2001 From: darylgraham Date: Tue, 5 Nov 2024 23:16:02 +0000 Subject: [PATCH 1/5] Extend Azure Org custom transformer docs to be more end-to-end Signed-off-by: darylgraham --- docs/integrations/azure/org.md | 139 +++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 7 deletions(-) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index e6fb69a520..6e8b9c84a3 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -258,21 +258,96 @@ The `myUserTransformer`, `myGroupTransformer`, `myOrganizationTransformer`, and The following provides an example of each kind of transformer. We recommend creating a `transformers.ts` file in your `packages/backend/src` folder for these. +First, lets set up the basic structure of the file, with functions for each kind of transformer that simply passes through the default transformer unchanged. + ```ts title="packages/backend/src/transformers.ts" import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import { defaultGroupTransformer, defaultUserTransformer, defaultOrganizationTransformer, + microsoftGraphOrgEntityProviderTransformExtensionPoint, MicrosoftGraphProviderConfig, } from '@backstage/plugin-catalog-backend-module-msgraph'; import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { createBackendModule } from '@backstage/backend-plugin-api'; -// This group transformer completely replaces the built in logic with custom logic. +// The Group transformer transforms Groups that are ingested from MS Graph export async function myGroupTransformer( group: MicrosoftGraph.Group, groupPhoto?: string, ): Promise { + const backstageGroup = await defaultGroupTransformer(group, groupPhoto); + return backstageGroup; +} + +// The User transformer transforms Users that are ingested from MS Graph +export async function myUserTransformer( + graphUser: MicrosoftGraph.User, + userPhoto?: string, +): Promise { + const backstageUser = await defaultUserTransformer(graphUser, userPhoto); + return backstageUser; +} + +// The Organization transformer transforms the root MS Graph Organization into a Group +export async function myOrganizationTransformer( + graphOrganization: MicrosoftGraph.Organization, +): Promise { + const backstageOrg = await defaultOrganizationTransformer(graphOrganization); + return backstageOrg; +} + +// The Provider Config transformer enables modification of the plugin config +export async function myProviderConfigTransformer( + provider: MicrosoftGraphProviderConfig, +): Promise { + return provider; +} + +// Wrapping these functions in a Module allows us to inject them into the Catalog plugin easily +export const myMsgraphTransformersModule = createBackendModule({ + pluginId: 'catalog', + moduleId: 'msgraph-org', + register(reg) { + reg.registerInit({ + deps: { + microsoftGraphTransformers: + microsoftGraphOrgEntityProviderTransformExtensionPoint, + }, + async init({ microsoftGraphTransformers }) { + // Set the transformers to our custom functions + microsoftGraphTransformers.setUserTransformer(myUserTransformer); + microsoftGraphTransformers.setGroupTransformer(myGroupTransformer); + microsoftGraphTransformers.setOrganizationTransformer( + myOrganizationTransformer, + ); + microsoftGraphTransformers.setProviderConfigTransformer( + myProviderConfigTransformer, + ); + }, + }); + }, +}); + +// Export a default to make importing into the backend simpler +export default myMsgraphTransformersModule; +``` + +Now lets customize each of the providers to suit our needs. + +The Group Transformer will have the default logic completely removed and replaced with our custom logic: + +```ts +export async function myGroupTransformer( + group: MicrosoftGraph.Group, + groupPhoto?: string, +): Promise { + // highlight-remove-start + const backstageGroup = await defaultGroupTransformer(group, groupPhoto); + return backstageGroup; + // highlight-remove-end + // highlight-add-start return { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', @@ -285,40 +360,90 @@ export async function myGroupTransformer( children: [], }, }; + // highlight-add-end } +``` -// This user transformer makes use of the built in logic, but also sets the description field +The User Transformer makes use of the built-in logic, but also modifies the username and sets a description + +```ts export async function myUserTransformer( graphUser: MicrosoftGraph.User, userPhoto?: string, ): Promise { const backstageUser = await defaultUserTransformer(graphUser, userPhoto); - + // highlight-add-start + // Make sure the default transformer returned an entity if (backstageUser) { - backstageUser.metadata.description = 'Loaded from Microsoft Entra ID'; + // Update the description to make it obvious where this entity came from + backstageUser.metadata.description = + 'Loaded from Microsoft Entra ID via MyCustomUserTransformer'; + + // The default transformer sets the username to the email address with invalid characters subbed out: 'user_domain.com' + // Set the username to the local part of the email address in lowercase without the domain + const newName = backstageUser.metadata.name.split('_')[0].toLowerCase(); + backstageUser.metadata.name = newName; + + return backstageUser; } - + return undefined; + // highlight-add-end + // highlight-remove-start return backstageUser; + // highlight-remove-end } +``` -// Example organization transformer that removes the organization group completely +The Organization Transformer removes the organization group completely by returning undefined + +```ts export async function myOrganizationTransformer( graphOrganization: MicrosoftGraph.Organization, ): Promise { + // highlight-remove-start + const backstageOrg = await defaultOrganizationTransformer(graphOrganization); + return backstageOrg; + // highlight-remove-end + // highlight-add-start return undefined; + // highlight-add-end } +``` -// Example config transformer that expands the group filter to also include 'azure-group-a' +The Config Transformer expands the group filter to also include 'azure-group-a' + +```ts export async function myProviderConfigTransformer( provider: MicrosoftGraphProviderConfig, ): Promise { + // highlight-add-start if (!provider.groupFilter?.includes('azure-group-a')) { provider.groupFilter = `${provider.groupFilter} or displayName eq 'azure-group-a'`; } + // highlight-add-end return provider; } ``` +Now we just need to add our new module to the Backend. + +```ts +// packages/backend/src/index.ts +// Your file will have more than this in it + +const backend = createBackend(); + +... + +// highlight-add-start +backend.add(import('./transformers')); +// highlight-add-end + +... + +backend.start(); +``` + ## Troubleshooting ### No data From b84fde2cc530d46a3b8410ea1a66cffd75ce3181 Mon Sep 17 00:00:00 2001 From: Daryl Graham Date: Fri, 29 Nov 2024 20:19:56 +1000 Subject: [PATCH 2/5] Update docs/integrations/azure/org.md Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Daryl Graham Signed-off-by: darylgraham --- docs/integrations/azure/org.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 6e8b9c84a3..aab6c47b5d 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -260,7 +260,7 @@ The following provides an example of each kind of transformer. We recommend crea First, lets set up the basic structure of the file, with functions for each kind of transformer that simply passes through the default transformer unchanged. -```ts title="packages/backend/src/transformers.ts" +```ts title="packages/backend/src/extensions/transformers.ts" import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import { defaultGroupTransformer, From 0ede7cc8fcd123e20f6e3f9aab1fde806e8cd9ab Mon Sep 17 00:00:00 2001 From: Daryl Graham Date: Fri, 29 Nov 2024 20:22:25 +1000 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Signed-off-by: Daryl Graham Signed-off-by: darylgraham --- docs/integrations/azure/org.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index aab6c47b5d..3773c664c2 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -306,7 +306,7 @@ export async function myProviderConfigTransformer( } // Wrapping these functions in a Module allows us to inject them into the Catalog plugin easily -export const myMsgraphTransformersModule = createBackendModule({ +export default createBackendModule({ pluginId: 'catalog', moduleId: 'msgraph-org', register(reg) { @@ -336,7 +336,7 @@ export default myMsgraphTransformersModule; Now lets customize each of the providers to suit our needs. -The Group Transformer will have the default logic completely removed and replaced with our custom logic: +This Group Transformer example will have the default logic completely removed and replaced with our custom logic: ```ts export async function myGroupTransformer( @@ -364,7 +364,7 @@ export async function myGroupTransformer( } ``` -The User Transformer makes use of the built-in logic, but also modifies the username and sets a description +This User Transformer example makes use of the built-in logic, but also modifies the username and sets a description ```ts export async function myUserTransformer( @@ -394,7 +394,7 @@ export async function myUserTransformer( } ``` -The Organization Transformer removes the organization group completely by returning undefined +This Organization Transformer example removes the organization group completely by returning undefined ```ts export async function myOrganizationTransformer( @@ -410,7 +410,7 @@ export async function myOrganizationTransformer( } ``` -The Config Transformer expands the group filter to also include 'azure-group-a' +This Config Transformer example expands the group filter to also include 'azure-group-a' ```ts export async function myProviderConfigTransformer( @@ -436,7 +436,7 @@ const backend = createBackend(); ... // highlight-add-start -backend.add(import('./transformers')); +backend.add(import('./extensions/transformers')); // highlight-add-end ... From 22da1177ff4dac312ecd079b5fb18ae98396aeb3 Mon Sep 17 00:00:00 2001 From: darylgraham Date: Mon, 2 Dec 2024 00:00:26 +0000 Subject: [PATCH 4/5] Update custom transformers with logic to explain why we would make the change Signed-off-by: darylgraham --- docs/integrations/azure/org.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 3773c664c2..bc2b35d8c5 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -329,9 +329,6 @@ export default createBackendModule({ }); }, }); - -// Export a default to make importing into the backend simpler -export default myMsgraphTransformersModule; ``` Now lets customize each of the providers to suit our needs. @@ -348,15 +345,26 @@ export async function myGroupTransformer( return backstageGroup; // highlight-remove-end // highlight-add-start + // All of our groups are prefixed with the organisational unit: 'Engineering - Team A' + // We want to drop the org unit from the group name and use it for the namespace instead + const groupNameArr = group.displayName.split(' - '); + const displayName = groupNameArr[1]; + // Standardise name and namespace by replacing spaces with hyphens and converting to lowercase + const namespace = groupNameArr[0].replace(' ', '-').toLowerCase(); + const groupName = groupNameArr[1].replace(' ', '-').toLowerCase(); + return { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', metadata: { - name: group.id!, + name: groupName, + description: group.description, annotations: {}, }, spec: { - type: 'Microsoft Entra ID', + type: 'team', + displayName: displayName, + email: group.mail, children: [], }, }; @@ -405,6 +413,8 @@ export async function myOrganizationTransformer( return backstageOrg; // highlight-remove-end // highlight-add-start + // The org transformer creates a group to be used as the base of the relationship tree for groups + // We don't need this to be created, so return undefined instead of an entity return undefined; // highlight-add-end } @@ -417,6 +427,8 @@ export async function myProviderConfigTransformer( provider: MicrosoftGraphProviderConfig, ): Promise { // highlight-add-start + // The filter in our config file relies on a property that has been intermittantly causing this important group to fail ingestion + // Ensure the group is always discovered by the filter if (!provider.groupFilter?.includes('azure-group-a')) { provider.groupFilter = `${provider.groupFilter} or displayName eq 'azure-group-a'`; } From 5e282bc5377508cf8c3f21c2800350eee80a5a3a Mon Sep 17 00:00:00 2001 From: darylgraham Date: Fri, 27 Dec 2024 11:59:21 +0000 Subject: [PATCH 5/5] codefence title added instead of a comment inside the block Signed-off-by: darylgraham --- docs/integrations/azure/org.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index bc2b35d8c5..068aa70101 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -439,8 +439,7 @@ export async function myProviderConfigTransformer( Now we just need to add our new module to the Backend. -```ts -// packages/backend/src/index.ts +```ts title="packages/backend/src/index.ts" // Your file will have more than this in it const backend = createBackend();