diff --git a/packages/compass-connections/src/hooks/use-connection-supports.spec.ts b/packages/compass-connections/src/hooks/use-connection-supports.spec.ts index 88ec3ec4015..5e4bca3ce30 100644 --- a/packages/compass-connections/src/hooks/use-connection-supports.spec.ts +++ b/packages/compass-connections/src/hooks/use-connection-supports.spec.ts @@ -19,12 +19,17 @@ const mockConnections: ConnectionInfo[] = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', - metricsType: 'host', + metricsType: 'replicaSet', instanceSize: 'M10', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: false, + }, }, }, { @@ -36,12 +41,17 @@ const mockConnections: ConnectionInfo[] = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'replicaSet', instanceSize: 'M0', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: false, + }, }, }, { @@ -53,12 +63,17 @@ const mockConnections: ConnectionInfo[] = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'serverless', instanceSize: 'SERVERLESS_V2', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: false, + }, }, }, { @@ -70,12 +85,17 @@ const mockConnections: ConnectionInfo[] = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'replicaSet', instanceSize: 'M10', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: true, + }, }, }, { @@ -87,12 +107,17 @@ const mockConnections: ConnectionInfo[] = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'cluster', instanceSize: 'M10', clusterType: 'SHARDED', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: true, + }, }, }, { @@ -104,12 +129,17 @@ const mockConnections: ConnectionInfo[] = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'cluster', instanceSize: 'M30', clusterType: 'GEOSHARDED', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: true, + rollingIndexes: true, + }, }, }, ]; diff --git a/packages/compass-connections/src/utils/connection-supports.spec.ts b/packages/compass-connections/src/utils/connection-supports.spec.ts index c568a5ef3e6..51fe384c39d 100644 --- a/packages/compass-connections/src/utils/connection-supports.spec.ts +++ b/packages/compass-connections/src/utils/connection-supports.spec.ts @@ -18,12 +18,17 @@ const mockConnections = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', - metricsType: 'host', + metricsType: 'replicaSet', instanceSize: 'M10', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: false, + }, }, }, { @@ -35,12 +40,17 @@ const mockConnections = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'replicaSet', instanceSize: 'M0', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: false, + }, }, }, { @@ -52,12 +62,17 @@ const mockConnections = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'serverless', instanceSize: 'SERVERLESS_V2', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: false, + }, }, }, { @@ -69,12 +84,17 @@ const mockConnections = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'replicaSet', instanceSize: 'M10', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: true, + }, }, }, { @@ -86,14 +106,16 @@ const mockConnections = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'cluster', instanceSize: 'M10', clusterType: 'SHARDED', clusterUniqueId: 'clusterUniqueId', - geoSharding: { - selfManagedSharding: false, + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: true, }, }, }, @@ -106,12 +128,17 @@ const mockConnections = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'cluster', instanceSize: 'M30', clusterType: 'GEOSHARDED', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: true, + rollingIndexes: true, + }, }, }, { @@ -123,14 +150,16 @@ const mockConnections = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'cluster', instanceSize: 'M30', clusterType: 'GEOSHARDED', clusterUniqueId: 'clusterUniqueId', - geoSharding: { - selfManagedSharding: true, + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: true, }, }, }, @@ -143,12 +172,17 @@ const mockConnections = [ orgId: 'orgId', projectId: 'projectId', clusterName: 'clusterName', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'flex', instanceSize: 'FLEX', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: false, + }, }, }, ] as const; diff --git a/packages/compass-connections/src/utils/connection-supports.ts b/packages/compass-connections/src/utils/connection-supports.ts index d5b22d518f0..c70388f5b68 100644 --- a/packages/compass-connections/src/utils/connection-supports.ts +++ b/packages/compass-connections/src/utils/connection-supports.ts @@ -1,14 +1,6 @@ import type { ConnectionInfo } from '@mongodb-js/connection-info'; export type ConnectionFeature = 'rollingIndexCreation' | 'globalWrites'; -function isFreeOrSharedTierCluster(instanceSize: string | undefined): boolean { - if (!instanceSize) { - return false; - } - - return ['M0', 'M2', 'M5'].includes(instanceSize); -} - function supportsRollingIndexCreation(connectionInfo: ConnectionInfo) { const atlasMetadata = connectionInfo.atlasMetadata; @@ -16,11 +8,7 @@ function supportsRollingIndexCreation(connectionInfo: ConnectionInfo) { return false; } - const { metricsType, instanceSize } = atlasMetadata; - return ( - (metricsType === 'cluster' || metricsType === 'replicaSet') && - !isFreeOrSharedTierCluster(instanceSize) - ); + return atlasMetadata.supports.rollingIndexes; } function supportsGlobalWrites(connectionInfo: ConnectionInfo) { @@ -30,10 +18,7 @@ function supportsGlobalWrites(connectionInfo: ConnectionInfo) { return false; } - return ( - atlasMetadata.clusterType === 'GEOSHARDED' && - !atlasMetadata.geoSharding?.selfManagedSharding - ); + return atlasMetadata.supports.globalWrites; } export function connectionSupports( diff --git a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts index 841cb3a203f..46e190b742b 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts @@ -26,12 +26,17 @@ const mockConnectionInfo: ConnectionInfo = { orgId: 'testOrg', projectId: 'testProject', clusterName: 'pineapple', - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, metricsId: 'metricsId', metricsType: 'replicaSet', instanceSize: 'M10', clusterType: 'REPLICASET', clusterUniqueId: 'clusterUniqueId', + clusterState: 'IDLE', + supports: { + globalWrites: false, + rollingIndexes: false, + }, }, }; diff --git a/packages/compass-indexes/src/index.spec.tsx b/packages/compass-indexes/src/index.spec.tsx index c03f1e1155f..49fc9210b31 100644 --- a/packages/compass-indexes/src/index.spec.tsx +++ b/packages/compass-indexes/src/index.spec.tsx @@ -59,6 +59,9 @@ describe('CompassIndexesPlugin', function () { atlasMetadata: { instanceSize: 'VERY BIG', metricsType: 'replicaSet', + supports: { + rollingIndexes: true, + }, } as any, }, { diff --git a/packages/compass-indexes/src/modules/regular-indexes.spec.ts b/packages/compass-indexes/src/modules/regular-indexes.spec.ts index 288fba06256..23f9075eab5 100644 --- a/packages/compass-indexes/src/modules/regular-indexes.spec.ts +++ b/packages/compass-indexes/src/modules/regular-indexes.spec.ts @@ -68,6 +68,9 @@ describe('regular-indexes module', function () { atlasMetadata: { metricsType: 'cluster', instanceSize: 'M10', + supports: { + rollingIndexes: true, + }, }, }, } as ConnectionInfoRef; diff --git a/packages/compass-schema/src/utils.spec.ts b/packages/compass-schema/src/utils.spec.ts index c5b1ec1fa82..ba2f791d524 100644 --- a/packages/compass-schema/src/utils.spec.ts +++ b/packages/compass-schema/src/utils.spec.ts @@ -32,10 +32,10 @@ describe('compass-schema utils', function () { expect( getAtlasPerformanceAdvisorLink({ metricsId: '123456', - metricsType: 'host', + metricsType: 'replicaSet', clusterName: 'Cluster0', }) - ).to.equal('#/metrics/host/123456/advisor'); + ).to.equal('#/metrics/replicaSet/123456/advisor'); }); it('encodes the parameters', function () { expect( diff --git a/packages/compass-web/src/connection-storage.spec.ts b/packages/compass-web/src/connection-storage.spec.ts index 577691e114b..6253b48add7 100644 --- a/packages/compass-web/src/connection-storage.spec.ts +++ b/packages/compass-web/src/connection-storage.spec.ts @@ -186,6 +186,11 @@ describe('buildConnectionInfoFromClusterDescription', function () { clusterDescription.replicationSpecList?.[0].regionConfigs.slice().pop() ?.electableSpecs.instanceSize; + // We test these separately in another test + if (connectionInfo.atlasMetadata?.supports) { + delete (connectionInfo.atlasMetadata as { supports?: any }).supports; + } + expect(connectionInfo) .to.have.property('atlasMetadata') .deep.eq({ @@ -199,12 +204,9 @@ describe('buildConnectionInfoFromClusterDescription', function () { clusterUniqueId: '123abc', metricsType: type === 'sharded' ? 'cluster' : type, instanceSize: expectedInstanceSize, - regionalBaseUrl: 'https://example.com', + regionalBaseUrl: null, clusterType: clusterDescription.clusterType, - geoSharding: { - selfManagedSharding: - clusterDescription.geoSharding?.selfManagedSharding, - }, + clusterState: 'IDLE', }); }); } diff --git a/packages/compass-web/src/connection-storage.tsx b/packages/compass-web/src/connection-storage.tsx index acaf36e4820..a51889c2275 100644 --- a/packages/compass-web/src/connection-storage.tsx +++ b/packages/compass-web/src/connection-storage.tsx @@ -200,6 +200,12 @@ export function buildConnectionInfoFromClusterDescription( deployment ); + const { metricsId, metricsType } = getMetricsIdAndType( + description, + deploymentItem + ); + const instanceSize = getInstanceSize(description); + return { // Cluster name is unique inside the project (hence using it in the backend // urls as identifier) and using it as an id makes our job of mapping routes @@ -220,18 +226,33 @@ export function buildConnectionInfoFromClusterDescription( orgId: orgId, projectId: projectId, clusterUniqueId: description.uniqueId, - clusterName: description.name, - regionalBaseUrl: description.dataProcessingRegion.regionalUrl, - ...getMetricsIdAndType(description, deploymentItem), - instanceSize: getInstanceSize(description), clusterType: description.clusterType, - geoSharding: { - selfManagedSharding: description.geoSharding?.selfManagedSharding, + clusterName: description.name, + clusterState: description.state as AtlasClusterMetadata['clusterState'], + regionalBaseUrl: null, + metricsId, + metricsType, + instanceSize, + supports: { + globalWrites: + description.clusterType === 'GEOSHARDED' && + !description.geoSharding?.selfManagedSharding, + rollingIndexes: Boolean( + ['cluster', 'replicaSet'].includes(metricsType) && + instanceSize && + !['M0', 'M2', 'M5'].includes(instanceSize) + ), }, }, }; } +const CONNECTABLE_CLUSTER_STATES: AtlasClusterMetadata['clusterState'][] = [ + 'IDLE', + 'REPARING', + 'UPDATING', +]; + /** * @internal exported for testing purposes */ @@ -240,6 +261,7 @@ export class AtlasCloudConnectionStorage implements ConnectionStorage { private loadAllPromise: Promise | undefined; + private canUseNewConnectionInfoEndpoint = true; constructor( private atlasService: AtlasService, private orgId: string, @@ -255,15 +277,63 @@ export class AtlasCloudConnectionStorage }); } - private async _loadAndNormalizeClusterDescriptionInfo(): Promise< + private async _loadAndNormalizeClusterDescriptionInfoV2(): Promise< + ConnectionInfo[] + > { + const res = await this.atlasService.authenticatedFetch( + this.atlasService.cloudEndpoint( + `/explorer/v1/groups/${this.projectId}/clusters/connectionInfo` + ) + ); + + const connectionInfoList = (await res.json()) as ConnectionInfo[]; + + return connectionInfoList + .map((connectionInfo: ConnectionInfo): ConnectionInfo | null => { + if ( + !connectionInfo.connectionOptions.connectionString || + !connectionInfo.atlasMetadata || + // TODO(COMPASS-8228): do not filter out those connections, display + // them in navigation, but in a way that doesn't allow connecting + !CONNECTABLE_CLUSTER_STATES.includes( + connectionInfo.atlasMetadata.clusterState + ) + ) { + return null; + } + + const clusterName = connectionInfo.atlasMetadata.clusterName; + + return { + ...connectionInfo, + connectionOptions: { + ...connectionInfo.connectionOptions, + lookup: () => { + return { + wsURL: this.atlasService.driverProxyEndpoint( + `/clusterConnection/${this.projectId}` + ), + projectId: this.projectId, + clusterName, + }; + }, + }, + }; + }) + .filter((connectionInfo): connectionInfo is ConnectionInfo => { + return !!connectionInfo; + }); + } + + /** + * TODO(COMPASS-9263): clean-up when new endpoint is fully rolled out + */ + private async _loadAndNormalizeClusterDescriptionInfoV1(): Promise< ConnectionInfo[] > { const [clusterDescriptions, deployment] = await Promise.all([ this.atlasService .authenticatedFetch( - // TODO(CLOUDP-249088): replace with the list request that already - // contains regional data when it exists instead of fetching - // one-by-one after the list fetch this.atlasService.cloudEndpoint(`/nds/clusters/${this.projectId}`) ) .then((res) => { @@ -348,10 +418,19 @@ export class AtlasCloudConnectionStorage } loadAll(): Promise { - this.loadAllPromise ??= - this._loadAndNormalizeClusterDescriptionInfo().finally(() => { - delete this.loadAllPromise; - }); + this.loadAllPromise ??= (async () => { + if (this.canUseNewConnectionInfoEndpoint === false) { + return this._loadAndNormalizeClusterDescriptionInfoV1(); + } + try { + return await this._loadAndNormalizeClusterDescriptionInfoV2(); + } catch (err) { + this.canUseNewConnectionInfoEndpoint = false; + return this._loadAndNormalizeClusterDescriptionInfoV1(); + } + })().finally(() => { + delete this.loadAllPromise; + }); return this.loadAllPromise; } } diff --git a/packages/connection-info/src/connection-info.ts b/packages/connection-info/src/connection-info.ts index f8b92b88576..d04b3fc67bf 100644 --- a/packages/connection-info/src/connection-info.ts +++ b/packages/connection-info/src/connection-info.ts @@ -1,21 +1,57 @@ import type { ConnectionOptions } from 'mongodb-data-service'; +/** + * Atlas metadata for clusters, refer to the backend implementation to see how + * the values are derived + * + * https://github.com/10gen/mms/blob/1efe59a9bb4646635d946d979e5c9f4423f95b10/server/src/main/com/xgen/svc/mms/res/view/explorer/DataExplorerConnectionInfoView.java#L223-L302 + */ export interface AtlasClusterMetadata { + /** + * Atlas organization id + */ orgId: string; + /** - * Project ID that uniquely identifies an Atlas project. Legacy name is "groupId" - * as projects were previously identified as "groups". + * Project ID that uniquely identifies an Atlas project. Legacy name is + * "groupId" as projects were previously identified as "groups". + * * https://www.mongodb.com/docs/atlas/api/atlas-admin-api-ref/#project-id */ projectId: string; + /** * Unique id returned with the clusterDescription */ clusterUniqueId: string; + /** * Cluster name, unique inside same project */ clusterName: string; + + /** + * Possible types of Atlas clusters. + * + * https://github.com/10gen/mms/blob/9e6bf2d81d4d85b5ac68a15bf471dcddc5922323/client/packages/types/nds/clusterDescription.ts#L12-L16 + */ + clusterType: 'REPLICASET' | 'SHARDED' | 'GEOSHARDED'; + + /** + * Cluster states + * + * `DELETED` is never returned from backend, but can be derived by connection + * missing when polling connection info list + */ + clusterState: + | 'CREATING' + | 'UPDATING' + | 'PAUSED' + | 'IDLE' + | 'REPARING' + | 'DELETING' + | 'DELETED'; + /** * A special id and type that are only relevant in context of mms metrics * features. These are deployment items props (with a special exception for @@ -25,12 +61,12 @@ export interface AtlasClusterMetadata { * https://github.com/10gen/mms/blob/43b0049a85196b44e465feb9b96ef942d6f2c8f4/client/js/legacy/core/models/deployment */ metricsId: string; + /** * Somewhat related to the clusterType provided as part of clusterDescription, - * but way less granular: + * but accounting for special shared cluster types. Used for `/metrics/*` + * routing and automation agent jobs * - * - `host`: CM/OM clusters not managed by Atlas (in theory should - * never appear in our runtime) * - `cluster`: any sharded cluster type (sharded or geo sharded / * "global writes" one) * - `replicaSet`: anything that is not sharded (both dedicated or "free @@ -39,28 +75,36 @@ export interface AtlasClusterMetadata { * - `flex`: new type that replaces serverless and some shared * clusters */ - metricsType: 'host' | 'replicaSet' | 'cluster' | 'serverless' | 'flex'; + metricsType: 'replicaSet' | 'cluster' | 'serverless' | 'flex'; + /** * Atlas API base url to be used when making control plane requests for a - * regionalized cluster + * regionalized cluster. Always `null` while compass-web is disabled for + * those types of clusters */ - regionalBaseUrl: string; + regionalBaseUrl: null; + /* * At the time of writing these are the possible instance sizes. If we include * the list in the type here , then we'll have to maintain it.. + * * https://github.com/10gen/mms/blob/9e6bf2d81d4d85b5ac68a15bf471dcddc5922323/client/packages/types/nds/provider.ts#L60-L107 */ instanceSize?: string; /** - * Possible types of Atlas clusters. - * Copied from: - * https://github.com/10gen/mms/blob/9e6bf2d81d4d85b5ac68a15bf471dcddc5922323/client/packages/types/nds/clusterDescription.ts#L12-L16 + * Flags indicating Atlas cluster-level control plane feature support */ - clusterType: 'REPLICASET' | 'SHARDED' | 'GEOSHARDED'; + supports: { + /** + * True if cluster is geo sharded and not self managed + */ + globalWrites: boolean; - geoSharding?: { - selfManagedSharding?: boolean; + /** + * True for dedicated clusters + */ + rollingIndexes: boolean; }; } diff --git a/scripts/changed.js b/scripts/changed.js index b262bb32cfc..69e09109fca 100644 --- a/scripts/changed.js +++ b/scripts/changed.js @@ -124,6 +124,7 @@ async function main() { const args = process.argv.slice(2); const interactive = args.includes('--interactive'); const command = args.find((arg) => !arg.startsWith('-')); + const since = args.includes('--force-all') ? '' : ' --since origin/HEAD'; /** @type {{ name: string }[]} */ const changedPackages = await withProgress( @@ -131,7 +132,7 @@ async function main() { async function () { const spinner = this; const { stdout } = await runInDir( - 'npx lerna list --all --since origin/HEAD --json --exclude-dependents' + `npx lerna list --all${since} --json --exclude-dependents --toposort` ); const result = JSON.parse(stdout); spinner.text =