History

Fix Microsoft service split ownership

Rhys Sullivan committed 4 months ago

Commit 0ed72ef1 parent 56452be2 files · +467 −31Browse files

Showing the changes to scripts (1 of 2 files). Show all

Unified · Split

scripts/migration/service-split-planner.ts+190−31
@@ -337,6 +337,121 @@ const microsoftCatalogByMonolithPresetId: ReadonlyMap<string, OpenApiPreset> = n · 336 unmodified lines
337337 microsoftCatalog.map((preset) => [preset.id.replace(/^microsoft-/, ""), preset]),
338338 );
339339
340+const MICROSOFT_UNMIGRATABLE_UMBRELLA_PRESET_IDS = new Set(["me-surface", "users", "groups"]);
341+const MICROSOFT_TOOL_IMPLIED_PRESET_IDS = new Set(MICROSOFT_GRAPH_DEFAULT_PRESET_IDS);
342+const microsoftCatalogBackedPresetIds = new Set(
343+ microsoftCatalog
344+ .filter((preset) => preset.defaultSlug && preset.defaultSlug.length > 0)
345+ .map((preset) => preset.id.replace(/^microsoft-/, "")),
346+);
347+
348+export const microsoftToolFirstSegmentPresetIds: ReadonlyMap<string, readonly string[]> = new Map([
349+ ["chats", ["teams-chat"]],
350+ ["chatsChat", ["teams-chat"]],
351+ ["communications", ["meetings-calls"]],
352+ ["communicationsCall", ["meetings-calls"]],
353+ ["communicationsOnlineMeeting", ["meetings-calls"]],
354+ ["communicationsPresence", ["meetings-calls"]],
355+ ["drives", ["files"]],
356+ ["drivesDrive", ["files"]],
357+ ["drivesDriveItem", ["files"]],
358+ ["groupsDrive", ["files"]],
359+ ["groupsDrives", ["files"]],
360+ ["groupsOnenote", ["onenote"]],
361+ ["groupsTeam", ["teams-channels"]],
362+ ["meCalendar", ["calendar"]],
363+ ["meCalendarGroups", ["calendar"]],
364+ ["meCalendars", ["calendar"]],
365+ ["meCalendarView", ["calendar"]],
366+ ["meChat", ["teams-chat"]],
367+ ["meChats", ["teams-chat"]],
368+ ["meContact", ["contacts"]],
369+ ["meContactFolder", ["contacts"]],
370+ ["meContactFolders", ["contacts"]],
371+ ["meContacts", ["contacts"]],
372+ ["meDrive", ["files"]],
373+ ["meDrives", ["files"]],
374+ ["meEvent", ["calendar"]],
375+ ["meEvents", ["calendar"]],
376+ ["meFindMeetingTimes", ["calendar"]],
377+ ["meFollowedSites", ["files"]],
378+ ["meGetMailTips", ["mail"]],
379+ ["meInferenceClassification", ["mail"]],
380+ ["meJoinedTeams", ["teams-channels"]],
381+ ["meMailFolders", ["mail"]],
382+ ["meMailboxSettings", ["mail"]],
383+ ["meMessage", ["mail"]],
384+ ["meMessages", ["mail"]],
385+ ["meOnlineMeeting", ["meetings-calls"]],
386+ ["meOnlineMeetings", ["meetings-calls"]],
387+ ["meOnenote", ["onenote"]],
388+ ["meOutlook", ["mail"]],
389+ ["meOutlookUser", ["mail"]],
390+ ["mePeople", ["contacts"]],
391+ ["mePerson", ["contacts"]],
392+ ["mePhoto", ["profile"]],
393+ ["meProfilePhoto", ["profile"]],
394+ ["meReminderView", ["calendar"]],
395+ ["meSendMail", ["mail"]],
396+ ["meSite", ["files"]],
397+ ["meTeam", ["teams-channels"]],
398+ ["meTodo", ["tasks"]],
399+ ["meUser", ["profile"]],
400+ ["meUserActions", ["profile"]],
401+ ["meUserFunctions", ["profile"]],
402+ ["shares", ["files"]],
403+ ["sites", ["sites"]],
404+ ["sitesOnenote", ["onenote"]],
405+ ["sitesSite", ["sites"]],
406+ ["teams", ["teams-channels"]],
407+ ["teamsChannel", ["teams-channels"]],
408+ ["teamsTeam", ["teams-channels"]],
409+ ["teamsTemplates", ["teams-channels"]],
410+ ["teamwork", ["teams-channels"]],
411+ ["usersCalendar", ["calendar"]],
412+ ["usersCalendarGroups", ["calendar"]],
413+ ["usersCalendars", ["calendar"]],
414+ ["usersCalendarView", ["calendar"]],
415+ ["usersContact", ["contacts"]],
416+ ["usersContactFolder", ["contacts"]],
417+ ["usersContactFolders", ["contacts"]],
418+ ["usersContacts", ["contacts"]],
419+ ["usersDrive", ["files"]],
420+ ["usersDrives", ["files"]],
421+ ["usersEvent", ["calendar"]],
422+ ["usersEvents", ["calendar"]],
423+ ["usersFindMeetingTimes", ["calendar"]],
424+ ["usersMailFolders", ["mail"]],
425+ ["usersMessage", ["mail"]],
426+ ["usersMessages", ["mail"]],
427+ ["usersOnlineMeeting", ["meetings-calls"]],
428+ ["usersOnlineMeetings", ["meetings-calls"]],
429+ ["usersOnenote", ["onenote"]],
430+ ["usersOutlook", ["mail"]],
431+ ["usersPeople", ["contacts"]],
432+ ["usersPerson", ["contacts"]],
433+ ["usersReminderView", ["calendar"]],
434+ ["usersSendMail", ["mail"]],
435+ ["usersTodo", ["tasks"]],
436+]);
437+
438+const MICROSOFT_WORKBOOK_FIRST_SEGMENTS = new Set([
439+ "drivesDriveItem",
440+ "groupsDrive",
441+ "meDrive",
442+ "usersDrive",
443+]);
444+
445+const microsoftFirstSegmentOwnershipKey = (firstSegment: string): string | undefined =>
446+ [...microsoftToolFirstSegmentPresetIds.keys()]
447+ .filter(
448+ (key) =>
449+ firstSegment === key ||
450+ (firstSegment.startsWith(key) &&
451+ firstSegment[key.length]?.toUpperCase() === firstSegment[key.length]),
452+ )
453+ .sort((left, right) => right.length - left.length)[0];
454+
340455 const serviceSlugForPreset = (family: MonolithPluginId, presetId: string): string | undefined => {
341456 const preset =
342457 family === "google"
@@ -405,6 +520,13 @@ const microsoftPresetIdsFromConfig = (integration: IntegrationRow): readonly str · 62 unmodified lines
405520 );
406521 };
407522
523+const microsoftUnmigratablePresetIds = (presetIds: readonly string[]): readonly string[] =>
524+ presetIds.filter(
525+ (presetId) =>
526+ MICROSOFT_UNMIGRATABLE_UMBRELLA_PRESET_IDS.has(presetId) ||
527+ !microsoftCatalogBackedPresetIds.has(presetId),
528+ );
529+
408530 export const googlePresetIdForTool = (toolName: string): string | undefined => {
409531 const [first, second] = toolName.split(".");
410532 if (first === "admin") {
@@ -413,38 +535,20 @@ export const googlePresetIdForTool = (toolName: string): string | undefined => { · 2 unmodified lines
413535 return GOOGLE_TOOL_PREFIX_TO_PRESET_ID.get(first ?? "");
414536 };
415537
416−const microsoftPresetIdsForTool = (toolName: string): readonly string[] => {
538+export const microsoftPresetIdsForTool = (toolName: string): readonly string[] => {
417539 const segments = toolName.split(".");
418540 const first = segments[0] ?? "";
419− const presetIds: string[] = [];
420− const add = (presetId: string) => {
421− if (!presetIds.includes(presetId)) presetIds.push(presetId);
422− };
423−
424− if (/^me/.test(first)) add("me-surface");
425− if (/^users/.test(first)) add("users");
426− if (/^groups/.test(first)) add("groups");
427− if (/^(me|users).*Message|^meMail|^usersMail|^meMailbox|^meOutlook|^usersOutlook/.test(first)) {
428− add("mail");
429− }
430− if (/Calendar|Event|Reminder|findMeetingTimes/.test(first)) add("calendar");
431− if (/Contact|Person/.test(first)) add("contacts");
432− if (/Todo/.test(first)) add("tasks");
433− if (/Planner/.test(first)) add("planner");
434− if (/Drive|drives|shares/.test(first)) add("files");
435− if (segments.some((segment) => /workbook/i.test(segment)) || /Workbook|Excel/.test(first)) {
436− if (/^(me|users|groups)/.test(first) || /Drive|drives/.test(first)) add("files");
437− add("excel");
438− }
439− if (/sites|Site|List|SharePoint/.test(first)) add("sites");
440− if (/onenote/i.test(first) || segments.some((segment) => /onenote/i.test(segment))) {
441− add("onenote");
442− }
443− if (/chats|Chat/.test(first)) add("teams-chat");
444− if (/teams|Team|teamwork|Channel/.test(first)) add("teams-channels");
445− if (/communications|OnlineMeeting|Call|Presence/.test(first)) add("meetings-calls");
446− if (/^meUser|^usersUser|ProfilePhoto/.test(first)) add("profile");
447− return presetIds;
541+ const ownershipKey = microsoftFirstSegmentOwnershipKey(first);
542+ const fromFirstSegment = ownershipKey
543+ ? (microsoftToolFirstSegmentPresetIds.get(ownershipKey) ?? [])
544+ : [];
545+ const workbookOwned =
546+ ownershipKey !== undefined &&
547+ MICROSOFT_WORKBOOK_FIRST_SEGMENTS.has(ownershipKey) &&
548+ segments.some((segment) => segment.toLowerCase().includes("workbook"));
549+ return unique([...fromFirstSegment, ...(workbookOwned ? ["excel"] : [])]).filter((presetId) =>
550+ MICROSOFT_TOOL_IMPLIED_PRESET_IDS.has(presetId),
551+ );
448552 };
449553
450554 const presetIdsForTool = (pluginId: MonolithPluginId, toolName: string): readonly string[] => {
@@ -462,6 +566,14 @@ const deriveServices = ( · 11 unmodified lines
462566 pluginId === "google"
463567 ? googlePresetIdsFromConfig(integration)
464568 : microsoftPresetIdsFromConfig(integration);
569+ if (pluginId === "microsoft") {
570+ const unmigratablePresetIds = microsoftUnmigratablePresetIds(fromConfig);
571+ if (unmigratablePresetIds.length > 0) {
572+ throw new Error(
573+ `Monolith ${tenantHash(integration.tenant)}/${integration.plugin_id}/${integration.slug} references unmigratable-umbrella Microsoft Graph preset(s): ${unmigratablePresetIds.join(", ")}; org needs manual handling`,
574+ );
575+ }
576+ }
465577 const fromTools = unique(
466578 tools.flatMap((tool) => {
467579 const presetIds = presetIdsForTool(pluginId, tool.name);
@@ -578,7 +690,7 @@ const operationRowsForService = ( · 110 unmodified lines
578690 if (operationIntegration(row) !== monolith.slug) return false;
579691 const toolName = operationToolName(row);
580692 if (!toolName) return false;
581− if (monolith.plugin_id === "google" && toolName.startsWith("oauth2.")) return false;
693+ if (isIntentionallyDroppedTool(monolith.plugin_id as MonolithPluginId, toolName)) return false;
582694 return serviceForMatchedTool(monolith.plugin_id as MonolithPluginId, toolName, [
583695 target,
584696 ]).includes(target.slug);
@@ -602,6 +714,9 @@ const isConfigOnlyMonolith = (input: { · 17 unmodified lines
602714 }): boolean =>
603715 input.tools.length === 0 && input.connections.length === 0 && input.operations.length === 0;
604716
717+const isIntentionallyDroppedTool = (pluginId: MonolithPluginId, toolName: string): boolean =>
718+ pluginId === "google" && toolName.startsWith("oauth2.");
719+
605720 const serviceForMatchedTool = (
606721 pluginId: MonolithPluginId,
607722 toolName: string,
@@ -617,6 +732,43 @@ const serviceForMatchedTool = ( · 9 unmodified lines
617732 const allServiceSlugs = (services: readonly ServiceTarget[]): readonly string[] =>
618733 services.map((service) => service.slug);
619734
735+const unassignedToolNames = (
736+ pluginId: MonolithPluginId,
737+ tools: readonly { readonly name: string }[],
738+ services: readonly ServiceTarget[],
739+): readonly string[] =>
740+ unique(
741+ tools.flatMap((tool) => {
742+ if (isIntentionallyDroppedTool(pluginId, tool.name)) return [];
743+ return serviceForMatchedTool(pluginId, tool.name, services).length === 0 ? [tool.name] : [];
744+ }),
745+ );
746+
747+const assertNoUnassignedRows = (
748+ monolith: IntegrationRow,
749+ tools: readonly ToolRow[],
750+ operations: readonly PluginStorageRow[],
751+ services: readonly ServiceTarget[],
752+): void => {
753+ const pluginId = monolith.plugin_id as MonolithPluginId;
754+ const unassignedTools = unassignedToolNames(pluginId, tools, services);
755+ if (unassignedTools.length > 0) {
756+ throw new Error(
757+ `Monolith ${tenantHash(monolith.tenant)}/${monolith.plugin_id}/${monolith.slug} has tool row(s) with no target service: ${unassignedTools.slice(0, 20).join(", ")}`,
758+ );
759+ }
760+ const operationTools = operations.flatMap((row) => {
761+ const toolName = operationToolName(row);
762+ return toolName ? [{ name: toolName }] : [];
763+ });
764+ const unassignedOperations = unassignedToolNames(pluginId, operationTools, services);
765+ if (unassignedOperations.length > 0) {
766+ throw new Error(
767+ `Monolith ${tenantHash(monolith.tenant)}/${monolith.plugin_id}/${monolith.slug} has operation row(s) with no target service: ${unassignedOperations.slice(0, 20).join(", ")}`,
768+ );
769+ }
770+};
771+
620772 const serviceSlugsForToolPattern = (
621773 pluginId: MonolithPluginId,
622774 toolSegments: readonly string[],
@@ -880,6 +1032,13 @@ export const planMigration = (input: MigrationInput): MigrationPlan => { · 257 unmodified lines
8801032 ) {
8811033 continue;
8821034 }
1035+ try {
1036+ assertNoUnassignedRows(monolith, monolithTools, monolithOperations, services);
1037+ } catch (error) {
1038+ if (!input.collectPolicyErrors) throw error;
1039+ hardErrors.push(error instanceof Error ? error.message : String(error));
1040+ continue;
1041+ }
8831042 const specHash = specHashFor(monolith);
8841043 const namespace = pluginBlobNamespace(tenant, monolith.plugin_id);
8851044 const targetNamespace = pluginBlobNamespace(tenant, "openapi");