diff --git a/package-lock.json b/package-lock.json index 3cfca4a376d..70ac8689cb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47188,7 +47188,6 @@ "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.16.0", - "mongodb-connection-string-url": "^3.0.1", "mongodb-data-service": "^22.27.0", "mongodb-log-writer": "^2.3.4", "mongodb-ns": "^2.4.2", @@ -59000,7 +58999,6 @@ "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.16.0", - "mongodb-connection-string-url": "^3.0.1", "mongodb-data-service": "^22.27.0", "mongodb-log-writer": "^2.3.4", "mongodb-ns": "^2.4.2", diff --git a/packages/compass-web/package.json b/packages/compass-web/package.json index 30aab3a643d..bc658ddc87b 100644 --- a/packages/compass-web/package.json +++ b/packages/compass-web/package.json @@ -119,7 +119,6 @@ "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.16.0", - "mongodb-connection-string-url": "^3.0.1", "mongodb-data-service": "^22.27.0", "mongodb-log-writer": "^2.3.4", "mongodb-ns": "^2.4.2", diff --git a/packages/compass-web/sandbox/index.tsx b/packages/compass-web/sandbox/index.tsx index 35dc4780dcb..242035a6c6b 100644 --- a/packages/compass-web/sandbox/index.tsx +++ b/packages/compass-web/sandbox/index.tsx @@ -109,13 +109,6 @@ const App = () => { return ( - > = { - Cluster0: { - '@provider': 'AWS', - groupId: 'abc', - name: 'Cluster0', - clusterType: 'REPLICASET', - srvAddress: 'test', - state: 'test', - deploymentItemName: 'replicaSet-xxx', - dataProcessingRegion: { regionalUrl: 'test' }, - }, - NoDeploymentItem: { - '@provider': 'AWS', - groupId: 'abc', - name: 'NoDeploymentItem', - clusterType: 'REPLICASET', - srvAddress: 'test', - state: 'test', - deploymentItemName: 'not-found', - dataProcessingRegion: { regionalUrl: 'test' }, - }, - NoSrvAddress: { - '@provider': 'AWS', - name: 'NoSrvAddress', - }, - Paused: { - '@provider': 'AWS', - name: 'Paused', - isPaused: true, - }, - WillThrowOnFetch: { - '@provider': 'AWS', - name: 'WillThrowOnFetch', - }, - }; - describe('#loadAll', function () { - it('should load connection descriptions filtering out the ones that failed to fetch', async function () { + it('should load connection descriptions filtering out the ones that are in the unsupported state', async function () { const atlasService = { cloudEndpoint(path: string) { return path; @@ -293,25 +13,30 @@ describe('AtlasCloudConnectionStorage', function () { return path; }, authenticatedFetch(path: string) { - let payload: any; - if (path === '/deployment/abc') { - payload = deployment; - } - if (path === '/nds/clusters/abc') { - payload = Array.from(Object.values(testClusters)); - } - const { groups } = - /^\/nds\/clusters\/abc\/(?.+?)\/.+?$/.exec(path) ?? { - groups: undefined, - }; - if (groups?.clusterName) { - if (groups?.clusterName === 'WillThrowOnFetch') { - return Promise.reject( - new Error('Failed to fetch cluster description') - ); - } - payload = testClusters[groups.clusterName]; + if (!path.endsWith('/clusters/connectionInfo')) { + throw new Error('Unsupported URL'); } + const payload = [ + { + id: 'foo', + connectionOptions: {}, + atlasMetadata: { clusterName: 'Cluster0', clusterState: 'IDLE' }, + }, + // No metadata, will filter this out + { + id: 'bar', + connectionOptions: {}, + }, + // Cluster state not supported + { + id: 'buz', + connectionOptions: {}, + atlasMetadata: { + clusterName: 'Cluster2', + clusterState: 'WEIRD_ONE', + }, + }, + ]; return Promise.resolve({ json() { return payload; @@ -339,7 +64,10 @@ describe('AtlasCloudConnectionStorage', function () { // We expect all other clusters to be filtered out for one reason or // another expect(connections).to.have.lengthOf(1); - expect(connections[0]).to.have.property('id', 'Cluster0'); + expect(connections[0]).to.have.nested.property( + 'atlasMetadata.clusterName', + 'Cluster0' + ); }); }); }); diff --git a/packages/compass-web/src/connection-storage.tsx b/packages/compass-web/src/connection-storage.tsx index 8c42fad9bac..f9b4584c152 100644 --- a/packages/compass-web/src/connection-storage.tsx +++ b/packages/compass-web/src/connection-storage.tsx @@ -8,7 +8,6 @@ import { ConnectionStorageProvider, InMemoryConnectionStorage, } from '@mongodb-js/connection-storage/provider'; -import ConnectionString from 'mongodb-connection-string-url'; import { createServiceProvider } from 'hadron-app-registry'; import type { AtlasService } from '@mongodb-js/atlas-service/provider'; import { atlasServiceLocator } from '@mongodb-js/atlas-service/provider'; @@ -53,197 +52,6 @@ export type ClusterDescriptionWithDataProcessingRegion = ClusterDescription & { dataProcessingRegion: { regionalUrl: string }; }; -type ReplicaSetDeploymentItem = { - _id: string; - state: { - clusterId: string; - }; -}; - -type ShardingDeploymentItem = { - name: string; - state: { - clusterId: string; - }; -}; - -type Deployment = { - replicaSets?: ReplicaSetDeploymentItem[]; - sharding?: ShardingDeploymentItem[]; -}; - -function findDeploymentItemByClusterName( - description: ClusterDescription, - deployment: Deployment -): ReplicaSetDeploymentItem | ShardingDeploymentItem | undefined { - if (isSharded(description)) { - return (deployment.sharding ?? []).find((item) => { - return item.name === description.deploymentItemName; - }); - } - - return (deployment.replicaSets ?? []).find((item) => { - return item._id === description.deploymentItemName; - }); -} - -function isServerless(clusterDescription: ClusterDescription) { - return clusterDescription['@provider'] === 'SERVERLESS'; -} - -function isFlex(clusterDescription: ClusterDescription) { - return clusterDescription['@provider'] === 'FLEX'; -} - -function isSharded(clusterDescription: ClusterDescription) { - return ( - clusterDescription.clusterType === 'SHARDED' || - clusterDescription.clusterType === 'GEOSHARDED' - ); -} - -function getMetricsIdAndType( - clusterDescription: ClusterDescription, - deploymentItem?: ReplicaSetDeploymentItem | ShardingDeploymentItem -): Pick { - if (isServerless(clusterDescription)) { - return { metricsId: clusterDescription.name, metricsType: 'serverless' }; - } - - if (isFlex(clusterDescription)) { - return { metricsId: clusterDescription.name, metricsType: 'flex' }; - } - - if (!deploymentItem) { - throw new Error( - "Can't build metrics info when deployment item is not found" - ); - } - - return { - metricsId: deploymentItem.state.clusterId, - metricsType: isSharded(clusterDescription) ? 'cluster' : 'replicaSet', - }; -} - -function getInstanceSize( - clusterDescription: ClusterDescription -): string | undefined { - return getFirstInstanceSize(clusterDescription.replicationSpecList); -} - -function getFirstInstanceSize( - replicationSpecs?: ReplicationSpec[] -): string | undefined { - if (!replicationSpecs || replicationSpecs.length === 0) { - return undefined; - } - const preferredRegion = getPreferredRegion(replicationSpecs[0]); - if (!preferredRegion) { - return undefined; - } - return preferredRegion.electableSpecs.instanceSize; -} - -function getPreferredRegion( - replicationSpec: ReplicationSpec -): RegionConfig | undefined { - let regionConfig: RegionConfig | undefined = undefined; - - // find the RegionConfig in replicationSpec with the highest priority - for (const r of replicationSpec.regionConfigs) { - if (!regionConfig || r.priority > regionConfig.priority) { - regionConfig = r; - } - } - - return regionConfig; -} - -export function buildConnectionInfoFromClusterDescription( - driverProxyEndpoint: string, - orgId: string, - projectId: string, - description: ClusterDescriptionWithDataProcessingRegion, - deployment: Deployment, - extraConnectionOptions?: Record -): ConnectionInfo { - const connectionString = new ConnectionString( - `mongodb+srv://${description.srvAddress}` - ); - - // Special connection options for cloud env, we will not actually pass - // certs when establishing the connection on the client, but the proxy - // server will provide cert resolved from cloud backend - connectionString.searchParams.set('tls', 'true'); - connectionString.searchParams.set('authMechanism', 'MONGODB-X509'); - connectionString.searchParams.set('authSource', '$external'); - - // Make sure server monitoring is done without streaming - connectionString.searchParams.set('serverMonitoringMode', 'poll'); - // Allow driver to clean up idle connections from the pool - connectionString.searchParams.set('maxIdleTimeMS', '30000'); - - // Limit connection pool for replicas and sharded - connectionString.searchParams.set('minPoolSize', '0'); - connectionString.searchParams.set('maxPoolSize', '5'); - if (isSharded(description)) { - connectionString.searchParams.set('srvMaxHosts', '1'); - } - - for (const [key, value] of Object.entries(extraConnectionOptions ?? {})) { - connectionString.searchParams.set(key, String(value)); - } - - const deploymentItem = findDeploymentItemByClusterName( - description, - deployment - ); - - const { metricsId, metricsType } = getMetricsIdAndType( - description, - deploymentItem - ); - const instanceSize = getInstanceSize(description); - - return { - id: description.uniqueId, - connectionOptions: { - connectionString: connectionString.toString(), - lookup: () => { - return { - wsURL: driverProxyEndpoint, - projectId: projectId, - clusterName: description.name, - srvAddress: description.srvAddress, - }; - }, - }, - atlasMetadata: { - orgId: orgId, - projectId: projectId, - clusterUniqueId: description.uniqueId, - clusterType: description.clusterType, - 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 VISIBLE_CLUSTER_STATES: AtlasClusterMetadata['clusterState'][] = [ 'IDLE', 'REPAIRING', @@ -262,13 +70,11 @@ export class AtlasCloudConnectionStorage implements ConnectionStorage { private loadAllPromise: Promise | undefined; - private useNewConnectionInfoEndpoint = true; constructor( private atlasService: AtlasService, private orgId: string, private projectId: string, - private logger: Logger, - private extraConnectionOptions?: Record + private logger: Logger ) { super(); } @@ -278,16 +84,28 @@ export class AtlasCloudConnectionStorage }); } - private async _loadAndNormalizeClusterDescriptionInfoV2(): Promise< + private async _loadAndNormalizeClusterDescriptionInfo(): Promise< ConnectionInfo[] > { - const res = await this.atlasService.authenticatedFetch( - this.atlasService.cloudEndpoint( - `/explorer/v1/groups/${this.projectId}/clusters/connectionInfo` - ) - ); + let connectionInfoList: ConnectionInfo[] = []; - const connectionInfoList = (await res.json()) as ConnectionInfo[]; + try { + const res = await this.atlasService.authenticatedFetch( + this.atlasService.cloudEndpoint( + `/explorer/v1/groups/${this.projectId}/clusters/connectionInfo` + ) + ); + + connectionInfoList = await res.json(); + } catch (err) { + this.logger.log.error( + mongoLogId(1_001_000_357), + 'LoadAndNormalizeClusterDescriptionInfo', + 'Failed to load connection list', + { error: (err as Error).message } + ); + throw err; + } return connectionInfoList .map((connectionInfo: ConnectionInfo): ConnectionInfo | null => { @@ -297,6 +115,12 @@ export class AtlasCloudConnectionStorage connectionInfo.atlasMetadata.clusterState ) ) { + this.logger.log.warn( + mongoLogId(1_001_000_358), + 'LoadAndNormalizeClusterDescriptionInfo', + 'Skipping connection info due to unsupported cluster state or missing metadata', + { connectionInfo } + ); return null; } @@ -323,107 +147,11 @@ export class AtlasCloudConnectionStorage }); } - /** - * 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( - this.atlasService.cloudEndpoint(`/nds/clusters/${this.projectId}`) - ) - .then((res) => { - return res.json() as Promise; - }) - .then((descriptions) => { - return Promise.all( - descriptions.map(async (description) => { - // Even though nds/clusters will list serverless clusters, to get - // the regional description we need to change the url - const clusterDescriptionType = isServerless(description) - ? 'serverless' - : 'clusters'; - try { - const res = await this.atlasService.authenticatedFetch( - this.atlasService.cloudEndpoint( - `/nds/${clusterDescriptionType}/${this.projectId}/${description.name}/regional/clusterDescription` - ) - ); - return await (res.json() as Promise); - } catch (err) { - this.logger.log.error( - mongoLogId(1_001_000_303), - 'LoadAndNormalizeClusterDescriptionInfo', - 'Failed to fetch cluster description for cluster', - { clusterName: description.name, error: (err as Error).stack } - ); - return null; - } - }) - ); - }), - this.atlasService - .authenticatedFetch( - this.atlasService.cloudEndpoint(`/deployment/${this.projectId}`) - ) - .then((res) => { - return res.json() as Promise; - }), - ]); - - return clusterDescriptions - .map((description) => { - // Clear cases where cluster doesn't have enough metadata - // - Failed to get the description - // - Cluster is paused - // - Cluster is missing an srv address (happens during deployment / - // termination) - if (!description || !!description.isPaused || !description.srvAddress) { - return null; - } - - try { - // We will always try to build the metadata, it can fail if deployment - // item for the cluster is missing even when description exists - // (happens during deployment / termination / weird corner cases of - // atlas cluster state) - return buildConnectionInfoFromClusterDescription( - this.atlasService.driverProxyEndpoint( - `/clusterConnection/${this.projectId}` - ), - this.orgId, - this.projectId, - description, - deployment, - this.extraConnectionOptions - ); - } catch (err) { - this.logger.log.error( - mongoLogId(1_001_000_304), - 'LoadAndNormalizeClusterDescriptionInfo', - 'Failed to build connection info from cluster description', - { clusterName: description.name, error: (err as Error).stack } - ); - - return null; - } - }) - .filter((connectionInfo): connectionInfo is ConnectionInfo => { - return !!connectionInfo; - }); - } - loadAll(): Promise { - this.loadAllPromise ??= (async () => { - if (this.useNewConnectionInfoEndpoint === false) { - return this._loadAndNormalizeClusterDescriptionInfoV1(); - } - return await this._loadAndNormalizeClusterDescriptionInfoV2(); - })().finally(() => { - delete this.loadAllPromise; - }); + this.loadAllPromise ??= + this._loadAndNormalizeClusterDescriptionInfo().finally(() => { + delete this.loadAllPromise; + }); return this.loadAllPromise; } } @@ -431,10 +159,6 @@ export class AtlasCloudConnectionStorage const SandboxConnectionStorageContext = React.createContext(null); -const SandboxExtraConnectionOptionsContext = React.createContext< - Record | undefined ->(undefined); - /** * Only used in the sandbox to provide connection info when connecting to the * non-Atlas deployment @@ -442,20 +166,14 @@ const SandboxExtraConnectionOptionsContext = React.createContext< */ export const SandboxConnectionStorageProvider = ({ value, - extraConnectionOptions, children, }: { value: ConnectionStorage | null; - extraConnectionOptions?: Record; children: React.ReactNode; }) => { return ( - - {children} - + {children} ); }; @@ -470,19 +188,10 @@ export const AtlasCloudConnectionStorageProvider = createServiceProvider( projectId: string; children: React.ReactChild; }) { - const extraConnectionOptions = useContext( - SandboxExtraConnectionOptionsContext - ); const logger = useLogger('ATLAS-CLOUD-CONNECTION-STORAGE'); const atlasService = atlasServiceLocator(); const storage = useRef( - new AtlasCloudConnectionStorage( - atlasService, - orgId, - projectId, - logger, - extraConnectionOptions - ) + new AtlasCloudConnectionStorage(atlasService, orgId, projectId, logger) ); const sandboxConnectionStorage = useContext( SandboxConnectionStorageContext