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
337 microsoftCatalog.map((preset) => [preset.id.replace(/^microsoft-/, ""), preset]),337 microsoftCatalog.map((preset) => [preset.id.replace(/^microsoft-/, ""), preset]),
338);338);
339 339
340const MICROSOFT_UNMIGRATABLE_UMBRELLA_PRESET_IDS = new Set(["me-surface", "users", "groups"]);
341const MICROSOFT_TOOL_IMPLIED_PRESET_IDS = new Set(MICROSOFT_GRAPH_DEFAULT_PRESET_IDS);
342const microsoftCatalogBackedPresetIds = new Set(
343 microsoftCatalog
344 .filter((preset) => preset.defaultSlug && preset.defaultSlug.length > 0)
345 .map((preset) => preset.id.replace(/^microsoft-/, "")),
346);
347
348export 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
438const MICROSOFT_WORKBOOK_FIRST_SEGMENTS = new Set([
439 "drivesDriveItem",
440 "groupsDrive",
441 "meDrive",
442 "usersDrive",
443]);
444
445const 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
340const serviceSlugForPreset = (family: MonolithPluginId, presetId: string): string | undefined => {455const serviceSlugForPreset = (family: MonolithPluginId, presetId: string): string | undefined => {
341 const preset =456 const preset =
342 family === "google"457 family === "google"
@@ -405,6 +520,13 @@ const microsoftPresetIdsFromConfig = (integration: IntegrationRow): readonly str · 62 unmodified lines
405 );520 );
406};521};
407 522
523const 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
408export const googlePresetIdForTool = (toolName: string): string | undefined => {530export const googlePresetIdForTool = (toolName: string): string | undefined => {
409 const [first, second] = toolName.split(".");531 const [first, second] = toolName.split(".");
410 if (first === "admin") {532 if (first === "admin") {
@@ -413,38 +535,20 @@ export const googlePresetIdForTool = (toolName: string): string | undefined => { · 2 unmodified lines
413 return GOOGLE_TOOL_PREFIX_TO_PRESET_ID.get(first ?? "");535 return GOOGLE_TOOL_PREFIX_TO_PRESET_ID.get(first ?? "");
414};536};
415 537
416const microsoftPresetIdsForTool = (toolName: string): readonly string[] => {538export const microsoftPresetIdsForTool = (toolName: string): readonly string[] => {
417 const segments = toolName.split(".");539 const segments = toolName.split(".");
418 const first = segments[0] ?? "";540 const first = segments[0] ?? "";
419 const presetIds: string[] = [];541 const ownershipKey = microsoftFirstSegmentOwnershipKey(first);
420 const add = (presetId: string) => {542 const fromFirstSegment = ownershipKey
421 if (!presetIds.includes(presetId)) presetIds.push(presetId);543 ? (microsoftToolFirstSegmentPresetIds.get(ownershipKey) ?? [])
422 };544 : [];
423 545 const workbookOwned =
424 if (/^me/.test(first)) add("me-surface");546 ownershipKey !== undefined &&
425 if (/^users/.test(first)) add("users");547 MICROSOFT_WORKBOOK_FIRST_SEGMENTS.has(ownershipKey) &&
426 if (/^groups/.test(first)) add("groups");548 segments.some((segment) => segment.toLowerCase().includes("workbook"));
427 if (/^(me|users).*Message|^meMail|^usersMail|^meMailbox|^meOutlook|^usersOutlook/.test(first)) {549 return unique([...fromFirstSegment, ...(workbookOwned ? ["excel"] : [])]).filter((presetId) =>
428 add("mail");550 MICROSOFT_TOOL_IMPLIED_PRESET_IDS.has(presetId),
429 }551 );
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;
448};552};
449 553
450const presetIdsForTool = (pluginId: MonolithPluginId, toolName: string): readonly string[] => {554const presetIdsForTool = (pluginId: MonolithPluginId, toolName: string): readonly string[] => {
@@ -462,6 +566,14 @@ const deriveServices = ( · 11 unmodified lines
462 pluginId === "google"566 pluginId === "google"
463 ? googlePresetIdsFromConfig(integration)567 ? googlePresetIdsFromConfig(integration)
464 : microsoftPresetIdsFromConfig(integration);568 : 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 }
465 const fromTools = unique(577 const fromTools = unique(
466 tools.flatMap((tool) => {578 tools.flatMap((tool) => {
467 const presetIds = presetIdsForTool(pluginId, tool.name);579 const presetIds = presetIdsForTool(pluginId, tool.name);
@@ -578,7 +690,7 @@ const operationRowsForService = ( · 110 unmodified lines
578 if (operationIntegration(row) !== monolith.slug) return false;690 if (operationIntegration(row) !== monolith.slug) return false;
579 const toolName = operationToolName(row);691 const toolName = operationToolName(row);
580 if (!toolName) return false;692 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;
582 return serviceForMatchedTool(monolith.plugin_id as MonolithPluginId, toolName, [694 return serviceForMatchedTool(monolith.plugin_id as MonolithPluginId, toolName, [
583 target,695 target,
584 ]).includes(target.slug);696 ]).includes(target.slug);
@@ -602,6 +714,9 @@ const isConfigOnlyMonolith = (input: { · 17 unmodified lines
602}): boolean =>714}): boolean =>
603 input.tools.length === 0 && input.connections.length === 0 && input.operations.length === 0;715 input.tools.length === 0 && input.connections.length === 0 && input.operations.length === 0;
604 716
717const isIntentionallyDroppedTool = (pluginId: MonolithPluginId, toolName: string): boolean =>
718 pluginId === "google" && toolName.startsWith("oauth2.");
719
605const serviceForMatchedTool = (720const serviceForMatchedTool = (
606 pluginId: MonolithPluginId,721 pluginId: MonolithPluginId,
607 toolName: string,722 toolName: string,
@@ -617,6 +732,43 @@ const serviceForMatchedTool = ( · 9 unmodified lines
617const allServiceSlugs = (services: readonly ServiceTarget[]): readonly string[] =>732const allServiceSlugs = (services: readonly ServiceTarget[]): readonly string[] =>
618 services.map((service) => service.slug);733 services.map((service) => service.slug);
619 734
735const 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
747const 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
620const serviceSlugsForToolPattern = (772const serviceSlugsForToolPattern = (
621 pluginId: MonolithPluginId,773 pluginId: MonolithPluginId,
622 toolSegments: readonly string[],774 toolSegments: readonly string[],
@@ -880,6 +1032,13 @@ export const planMigration = (input: MigrationInput): MigrationPlan => { · 257 unmodified lines
880 ) {1032 ) {
881 continue;1033 continue;
882 }1034 }
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 }
883 const specHash = specHashFor(monolith);1042 const specHash = specHashFor(monolith);
884 const namespace = pluginBlobNamespace(tenant, monolith.plugin_id);1043 const namespace = pluginBlobNamespace(tenant, monolith.plugin_id);
885 const targetNamespace = pluginBlobNamespace(tenant, "openapi");1044 const targetNamespace = pluginBlobNamespace(tenant, "openapi");