diff --git a/configs/testing-library-compass/src/index.tsx b/configs/testing-library-compass/src/index.tsx index a9cad9faa4a..6f3bf3bb79d 100644 --- a/configs/testing-library-compass/src/index.tsx +++ b/configs/testing-library-compass/src/index.tsx @@ -181,7 +181,8 @@ export class MockDataService }, build: { isEnterprise: false, - version: '0.0.0', + // Picking a large version to avoid the end-of-life confirmation modal + version: '100.0.0', }, host: {}, genuineMongoDB: { diff --git a/package-lock.json b/package-lock.json index f0b1aedda62..af0d03e7398 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43968,7 +43968,8 @@ "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", - "redux-thunk": "^2.4.2" + "redux-thunk": "^2.4.2", + "semver": "^7.6.2" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.3.8", @@ -43981,6 +43982,7 @@ "@types/mocha": "^9.0.0", "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", + "@types/semver": "^7.3.9", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", "depcheck": "^1.4.1", @@ -56395,6 +56397,7 @@ "@types/mocha": "^9.0.0", "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", + "@types/semver": "^7.3.9", "@types/sinon-chai": "^3.2.5", "bson": "^6.10.3", "chai": "^4.3.4", @@ -56414,6 +56417,7 @@ "react-redux": "^8.1.3", "redux": "^4.2.1", "redux-thunk": "^2.4.2", + "semver": "^7.6.2", "sinon": "^9.2.3", "xvfb-maybe": "^0.2.1" }, diff --git a/packages/compass-app-stores/src/provider.spec.tsx b/packages/compass-app-stores/src/provider.spec.tsx index e979c2ddcd3..646ffac645e 100644 --- a/packages/compass-app-stores/src/provider.spec.tsx +++ b/packages/compass-app-stores/src/provider.spec.tsx @@ -76,7 +76,10 @@ describe('NamespaceProvider', function () { const instance = instanceManager.getMongoDBInstanceForConnection(); sandbox.stub(instance, 'fetchDatabases').callsFake(() => { instance.databases.add({ _id: 'foo' }); - return Promise.resolve(); + // Wait a tick before resolving the promise to simulate async behavior + return new Promise((resolve) => { + setTimeout(resolve); + }); }); await renderWithActiveConnection( diff --git a/packages/compass-app-stores/src/stores/instance-store.spec.ts b/packages/compass-app-stores/src/stores/instance-store.spec.ts index b33c7355007..768a609945e 100644 --- a/packages/compass-app-stores/src/stores/instance-store.spec.ts +++ b/packages/compass-app-stores/src/stores/instance-store.spec.ts @@ -125,7 +125,7 @@ describe('InstanceStore [Store]', function () { const instance = instancesManager.getMongoDBInstanceForConnection( connectedConnectionInfoId ); - expect(instance).to.have.nested.property('build.version', '0.0.0'); + expect(instance).to.have.nested.property('build.version', '100.0.0'); globalAppRegistry.emit('refresh-data'); await waitForInstanceRefresh(instance); }); diff --git a/packages/compass-connections/package.json b/packages/compass-connections/package.json index ef1388c8466..11dc3de1db0 100644 --- a/packages/compass-connections/package.json +++ b/packages/compass-connections/package.json @@ -69,7 +69,8 @@ "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", - "redux-thunk": "^2.4.2" + "redux-thunk": "^2.4.2", + "semver": "^7.6.2" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.3.8", @@ -82,6 +83,7 @@ "@types/mocha": "^9.0.0", "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", + "@types/semver": "^7.3.9", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", "depcheck": "^1.4.1", diff --git a/packages/compass-connections/src/components/end-of-life-connection-modal.tsx b/packages/compass-connections/src/components/end-of-life-connection-modal.tsx new file mode 100644 index 00000000000..616f335ba10 --- /dev/null +++ b/packages/compass-connections/src/components/end-of-life-connection-modal.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { + css, + Banner, + Link, + spacing, + Body, + BannerVariant, + showConfirmation, +} from '@mongodb-js/compass-components'; +import { + getConnectionTitle, + type ConnectionInfo, +} from '@mongodb-js/connection-info'; + +const modalBodyStyles = css({ + marginTop: spacing[400], + marginBottom: spacing[200], +}); + +export function showEndOfLifeMongoDBWarningModal( + connectionInfo?: ConnectionInfo, + version?: string +) { + return showConfirmation({ + title: 'End-of-life MongoDB Detected', + hideCancelButton: true, + description: ( + <> + + {connectionInfo + ? `Server or service "${getConnectionTitle(connectionInfo)}"` + : 'This server or service'}{' '} + appears to be running a version of MongoDB that is no longer + supported. + + + Server version{version ? ` (${version})` : ''} is considered + end-of-life, consider upgrading to get the latest features and + performance improvements.{' '} + + + Learn more from the MongoDB Lifecycle Schedules. + + + ), + }); +} diff --git a/packages/compass-connections/src/components/non-genuine-connection-modal.tsx b/packages/compass-connections/src/components/non-genuine-connection-modal.tsx index 20eb41af456..eada1e085c9 100644 --- a/packages/compass-connections/src/components/non-genuine-connection-modal.tsx +++ b/packages/compass-connections/src/components/non-genuine-connection-modal.tsx @@ -23,6 +23,7 @@ export function showNonGenuineMongoDBWarningModal( ) { return showConfirmation({ title: 'Non-Genuine MongoDB Detected', + hideCancelButton: true, description: ( <> diff --git a/packages/compass-connections/src/stores/connections-store-redux.ts b/packages/compass-connections/src/stores/connections-store-redux.ts index e6b0f8a7c4d..339f823cacf 100644 --- a/packages/compass-connections/src/stores/connections-store-redux.ts +++ b/packages/compass-connections/src/stores/connections-store-redux.ts @@ -30,9 +30,14 @@ import { adjustConnectionOptionsBeforeConnect } from '@mongodb-js/connection-for import mongodbBuildInfo, { getGenuineMongoDB } from 'mongodb-build-info'; import EventEmitter from 'events'; import { showNonGenuineMongoDBWarningModal as _showNonGenuineMongoDBWarningModal } from '../components/non-genuine-connection-modal'; +import { showEndOfLifeMongoDBWarningModal as _showEndOfLifeMongoDBWarningModal } from '../components/end-of-life-connection-modal'; import ConnectionString from 'mongodb-connection-string-url'; import type { ExtraConnectionData as ExtraConnectionDataForTelemetry } from '@mongodb-js/compass-telemetry'; import { connectable } from '../utils/connection-supports'; +import { + getLatestEndOfLifeServerVersion, + isEndOfLifeVersion, +} from '../utils/end-of-life-server'; export type ConnectionsEventMap = { connected: ( @@ -1818,6 +1823,28 @@ const connectWithOptions = ( .isGenuine === false ) { dispatch(showNonGenuineMongoDBWarningModal(connectionInfo.id)); + } else if (preferences.getPreferences().networkTraffic) { + void dataService + .instance() + .then(async (instance) => { + const { version } = instance.build; + const latestEndOfLifeServerVersion = + await getLatestEndOfLifeServerVersion(); + if (isEndOfLifeVersion(version, latestEndOfLifeServerVersion)) { + dispatch( + showEndOfLifeMongoDBWarningModal( + connectionInfo.id, + instance.build.version + ) + ); + } + }) + .catch((err) => { + debug( + 'failed to get instance details to determine if the server version is end-of-life', + err + ); + }); } } catch (err) { dispatch(connectionAttemptError(connectionInfo, err)); @@ -2142,6 +2169,17 @@ export const showNonGenuineMongoDBWarningModal = ( }; }; +export const showEndOfLifeMongoDBWarningModal = ( + connectionId: string, + version: string +): ConnectionsThunkAction => { + return (_dispatch, getState, { track }) => { + const connectionInfo = getCurrentConnectionInfo(getState(), connectionId); + track('Screen', { name: 'end_of_life_mongodb_modal' }, connectionInfo); + void _showEndOfLifeMongoDBWarningModal(connectionInfo, version); + }; +}; + type ImportConnectionsFn = Required['importConnections']; export const importConnections = ( diff --git a/packages/compass-connections/src/utils/end-of-life-server.spec.ts b/packages/compass-connections/src/utils/end-of-life-server.spec.ts new file mode 100644 index 00000000000..ad0e0271008 --- /dev/null +++ b/packages/compass-connections/src/utils/end-of-life-server.spec.ts @@ -0,0 +1,31 @@ +import { expect } from 'chai'; +import { isEndOfLifeVersion } from './end-of-life-server'; + +describe('isEndOfLifeVersion', function () { + const LATEST_END_OF_LIFE_VERSION = '4.4.x'; + + function expectVersions(versions: string[], expected: boolean) { + for (const version of versions) { + expect(isEndOfLifeVersion(version, LATEST_END_OF_LIFE_VERSION)).to.equal( + expected, + `Expected ${version} to be ${ + expected ? 'end of life' : 'not end of life' + }` + ); + } + } + + it('returns true for v4.4 and below', () => { + expectVersions( + ['4.4.0', '4.3.0', '4.0', '4.0-beta.0', '1.0.0', '0.0.1', '3.999.0'], + true + ); + }); + + it('returns true for v4.5 and above', () => { + expectVersions( + ['4.5.0', '5.0.0', '5.0.25', '6.0.0', '7.0.0', '8.0.0'], + false + ); + }); +}); diff --git a/packages/compass-connections/src/utils/end-of-life-server.ts b/packages/compass-connections/src/utils/end-of-life-server.ts new file mode 100644 index 00000000000..fd8f06fb245 --- /dev/null +++ b/packages/compass-connections/src/utils/end-of-life-server.ts @@ -0,0 +1,83 @@ +import semverSatisfies from 'semver/functions/satisfies'; +import semverCoerce from 'semver/functions/coerce'; + +import { createLogger } from '@mongodb-js/compass-logging'; + +const { mongoLogId, log, debug } = createLogger('END-OF-LIFE-SERVER'); + +const FALLBACK_END_OF_LIFE_SERVER_VERSION = '4.4'; +const { + HADRON_AUTO_UPDATE_ENDPOINT = process.env + .HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE, +} = process.env; + +let latestEndOfLifeServerVersion: Promise | null = null; + +export async function getLatestEndOfLifeServerVersion(): Promise { + if (!HADRON_AUTO_UPDATE_ENDPOINT) { + log.debug( + mongoLogId(1_001_000_352), + 'getLatestEndOfLifeServerVersion', + 'HADRON_AUTO_UPDATE_ENDPOINT is not set' + ); + return FALLBACK_END_OF_LIFE_SERVER_VERSION; + } + + if (!latestEndOfLifeServerVersion) { + // Setting module scoped variable to avoid repeated fetches. + log.debug( + mongoLogId(1_001_000_353), + 'getLatestEndOfLifeServerVersion', + 'Fetching EOL server version' + ); + latestEndOfLifeServerVersion = fetch( + `${HADRON_AUTO_UPDATE_ENDPOINT}/api/v2/eol-server` + ) + .then(async (response) => { + if (response.ok) { + const result = await response.text(); + log.debug( + mongoLogId(1_001_000_354), + 'getLatestEndOfLifeServerVersion', + 'Got EOL server version response', + { result } + ); + return result; + } else { + // Reset the cached value to null so that we can try again next time. + latestEndOfLifeServerVersion = null; + throw new Error( + `Expected an OK response, got ${response.status} '${response.statusText}'` + ); + } + }) + .catch((error) => { + log.error( + mongoLogId(1_001_000_355), + 'getLatestEndOfLifeServerVersion', + 'Failed to fetch EOL server version', + { error } + ); + // We don't want any downstream code to fail just because we can't fetch the EOL server version. + return FALLBACK_END_OF_LIFE_SERVER_VERSION; + }); + } + // Return a cached or in-flight value + return latestEndOfLifeServerVersion; +} + +export function isEndOfLifeVersion( + version: string, + latestEndOfLifeServerVersion: string +) { + try { + const coercedVersion = semverCoerce(version); + return coercedVersion + ? semverSatisfies(coercedVersion, `<=${latestEndOfLifeServerVersion}`) + : false; + } catch (error) { + debug('Error comparing versions', { error }); + // If the version is not a valid semver, we can't reliably determine if it's EOL + return false; + } +} diff --git a/packages/compass-telemetry/src/telemetry-events.ts b/packages/compass-telemetry/src/telemetry-events.ts index 91e53ba04e0..3a5182ac913 100644 --- a/packages/compass-telemetry/src/telemetry-events.ts +++ b/packages/compass-telemetry/src/telemetry-events.ts @@ -2653,7 +2653,8 @@ type ScreenEvent = ConnectionScopedEvent<{ | 'restore_pipeline_modal' | 'save_pipeline_modal' | 'shell_info_modal' - | 'update_search_index_modal'; + | 'update_search_index_modal' + | 'end_of_life_mongodb_modal'; }; }>; diff --git a/packages/data-service/src/instance-detail-helper.ts b/packages/data-service/src/instance-detail-helper.ts index 2040d89aedd..ce84cf7396f 100644 --- a/packages/data-service/src/instance-detail-helper.ts +++ b/packages/data-service/src/instance-detail-helper.ts @@ -350,7 +350,9 @@ function adaptHostInfo(rawHostInfo: Partial): HostInfoDetails { }; } -function adaptBuildInfo(rawBuildInfo: Partial) { +export function adaptBuildInfo( + rawBuildInfo: Partial +): BuildInfoDetails { return { version: rawBuildInfo.version ?? '', // Cover both cases of detecting enterprise module, see SERVER-18099.