Skip to content

Commit 0db3c9e

Browse files
authored
feat(connections): warn then connecting to an end-of-life server COMPASS-9083 (#6888)
* Propagate isEndOfLife via BuildInfo on data-service instance * Add end-of-life modal * Show end-of-life warning modal on connection * Hide cancel buttons * Update link in the modal * Request EOL server version with a fallback * Show end-of-life warning for genuine servers only * Update API route * Check networkTraffic preference and move into compass-connections * Fix tests by delaying promise resolution by a tick * Use server version 100.0.0 when testing to avoid triggering the end-of-life modal everywhere * Update log ids
1 parent 12e2aa3 commit 0db3c9e

File tree

12 files changed

+225
-7
lines changed

12 files changed

+225
-7
lines changed

configs/testing-library-compass/src/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ export class MockDataService
181181
},
182182
build: {
183183
isEnterprise: false,
184-
version: '0.0.0',
184+
// Picking a large version to avoid the end-of-life confirmation modal
185+
version: '100.0.0',
185186
},
186187
host: {},
187188
genuineMongoDB: {

package-lock.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-app-stores/src/provider.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ describe('NamespaceProvider', function () {
7676
const instance = instanceManager.getMongoDBInstanceForConnection();
7777
sandbox.stub(instance, 'fetchDatabases').callsFake(() => {
7878
instance.databases.add({ _id: 'foo' });
79-
return Promise.resolve();
79+
// Wait a tick before resolving the promise to simulate async behavior
80+
return new Promise((resolve) => {
81+
setTimeout(resolve);
82+
});
8083
});
8184

8285
await renderWithActiveConnection(

packages/compass-app-stores/src/stores/instance-store.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ describe('InstanceStore [Store]', function () {
125125
const instance = instancesManager.getMongoDBInstanceForConnection(
126126
connectedConnectionInfoId
127127
);
128-
expect(instance).to.have.nested.property('build.version', '0.0.0');
128+
expect(instance).to.have.nested.property('build.version', '100.0.0');
129129
globalAppRegistry.emit('refresh-data');
130130
await waitForInstanceRefresh(instance);
131131
});

packages/compass-connections/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@
6969
"react": "^17.0.2",
7070
"react-redux": "^8.1.3",
7171
"redux": "^4.2.1",
72-
"redux-thunk": "^2.4.2"
72+
"redux-thunk": "^2.4.2",
73+
"semver": "^7.6.2"
7374
},
7475
"devDependencies": {
7576
"@mongodb-js/eslint-config-compass": "^1.3.8",
@@ -82,6 +83,7 @@
8283
"@types/mocha": "^9.0.0",
8384
"@types/react": "^17.0.5",
8485
"@types/react-dom": "^17.0.10",
86+
"@types/semver": "^7.3.9",
8587
"@types/sinon-chai": "^3.2.5",
8688
"chai": "^4.3.4",
8789
"depcheck": "^1.4.1",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import {
3+
css,
4+
Banner,
5+
Link,
6+
spacing,
7+
Body,
8+
BannerVariant,
9+
showConfirmation,
10+
} from '@mongodb-js/compass-components';
11+
import {
12+
getConnectionTitle,
13+
type ConnectionInfo,
14+
} from '@mongodb-js/connection-info';
15+
16+
const modalBodyStyles = css({
17+
marginTop: spacing[400],
18+
marginBottom: spacing[200],
19+
});
20+
21+
export function showEndOfLifeMongoDBWarningModal(
22+
connectionInfo?: ConnectionInfo,
23+
version?: string
24+
) {
25+
return showConfirmation({
26+
title: 'End-of-life MongoDB Detected',
27+
hideCancelButton: true,
28+
description: (
29+
<>
30+
<Banner variant={BannerVariant.Warning}>
31+
{connectionInfo
32+
? `Server or service "${getConnectionTitle(connectionInfo)}"`
33+
: 'This server or service'}{' '}
34+
appears to be running a version of MongoDB that is no longer
35+
supported.
36+
</Banner>
37+
<Body className={modalBodyStyles}>
38+
Server version{version ? ` (${version})` : ''} is considered
39+
end-of-life, consider upgrading to get the latest features and
40+
performance improvements.{' '}
41+
</Body>
42+
<Link
43+
href="https://www.mongodb.com/legal/support-policy/lifecycles"
44+
target="_blank"
45+
data-testid="end-of-life-warning-modal-learn-more-link"
46+
>
47+
Learn more from the MongoDB Lifecycle Schedules.
48+
</Link>
49+
</>
50+
),
51+
});
52+
}

packages/compass-connections/src/components/non-genuine-connection-modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export function showNonGenuineMongoDBWarningModal(
2323
) {
2424
return showConfirmation({
2525
title: 'Non-Genuine MongoDB Detected',
26+
hideCancelButton: true,
2627
description: (
2728
<>
2829
<Banner variant={BannerVariant.Warning}>

packages/compass-connections/src/stores/connections-store-redux.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ import { adjustConnectionOptionsBeforeConnect } from '@mongodb-js/connection-for
3030
import mongodbBuildInfo, { getGenuineMongoDB } from 'mongodb-build-info';
3131
import EventEmitter from 'events';
3232
import { showNonGenuineMongoDBWarningModal as _showNonGenuineMongoDBWarningModal } from '../components/non-genuine-connection-modal';
33+
import { showEndOfLifeMongoDBWarningModal as _showEndOfLifeMongoDBWarningModal } from '../components/end-of-life-connection-modal';
3334
import ConnectionString from 'mongodb-connection-string-url';
3435
import type { ExtraConnectionData as ExtraConnectionDataForTelemetry } from '@mongodb-js/compass-telemetry';
3536
import { connectable } from '../utils/connection-supports';
37+
import {
38+
getLatestEndOfLifeServerVersion,
39+
isEndOfLifeVersion,
40+
} from '../utils/end-of-life-server';
3641

3742
export type ConnectionsEventMap = {
3843
connected: (
@@ -1818,6 +1823,28 @@ const connectWithOptions = (
18181823
.isGenuine === false
18191824
) {
18201825
dispatch(showNonGenuineMongoDBWarningModal(connectionInfo.id));
1826+
} else if (preferences.getPreferences().networkTraffic) {
1827+
void dataService
1828+
.instance()
1829+
.then(async (instance) => {
1830+
const { version } = instance.build;
1831+
const latestEndOfLifeServerVersion =
1832+
await getLatestEndOfLifeServerVersion();
1833+
if (isEndOfLifeVersion(version, latestEndOfLifeServerVersion)) {
1834+
dispatch(
1835+
showEndOfLifeMongoDBWarningModal(
1836+
connectionInfo.id,
1837+
instance.build.version
1838+
)
1839+
);
1840+
}
1841+
})
1842+
.catch((err) => {
1843+
debug(
1844+
'failed to get instance details to determine if the server version is end-of-life',
1845+
err
1846+
);
1847+
});
18211848
}
18221849
} catch (err) {
18231850
dispatch(connectionAttemptError(connectionInfo, err));
@@ -2142,6 +2169,17 @@ export const showNonGenuineMongoDBWarningModal = (
21422169
};
21432170
};
21442171

2172+
export const showEndOfLifeMongoDBWarningModal = (
2173+
connectionId: string,
2174+
version: string
2175+
): ConnectionsThunkAction<void> => {
2176+
return (_dispatch, getState, { track }) => {
2177+
const connectionInfo = getCurrentConnectionInfo(getState(), connectionId);
2178+
track('Screen', { name: 'end_of_life_mongodb_modal' }, connectionInfo);
2179+
void _showEndOfLifeMongoDBWarningModal(connectionInfo, version);
2180+
};
2181+
};
2182+
21452183
type ImportConnectionsFn = Required<ConnectionStorage>['importConnections'];
21462184

21472185
export const importConnections = (
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect } from 'chai';
2+
import { isEndOfLifeVersion } from './end-of-life-server';
3+
4+
describe('isEndOfLifeVersion', function () {
5+
const LATEST_END_OF_LIFE_VERSION = '4.4.x';
6+
7+
function expectVersions(versions: string[], expected: boolean) {
8+
for (const version of versions) {
9+
expect(isEndOfLifeVersion(version, LATEST_END_OF_LIFE_VERSION)).to.equal(
10+
expected,
11+
`Expected ${version} to be ${
12+
expected ? 'end of life' : 'not end of life'
13+
}`
14+
);
15+
}
16+
}
17+
18+
it('returns true for v4.4 and below', () => {
19+
expectVersions(
20+
['4.4.0', '4.3.0', '4.0', '4.0-beta.0', '1.0.0', '0.0.1', '3.999.0'],
21+
true
22+
);
23+
});
24+
25+
it('returns true for v4.5 and above', () => {
26+
expectVersions(
27+
['4.5.0', '5.0.0', '5.0.25', '6.0.0', '7.0.0', '8.0.0'],
28+
false
29+
);
30+
});
31+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import semverSatisfies from 'semver/functions/satisfies';
2+
import semverCoerce from 'semver/functions/coerce';
3+
4+
import { createLogger } from '@mongodb-js/compass-logging';
5+
6+
const { mongoLogId, log, debug } = createLogger('END-OF-LIFE-SERVER');
7+
8+
const FALLBACK_END_OF_LIFE_SERVER_VERSION = '4.4';
9+
const {
10+
HADRON_AUTO_UPDATE_ENDPOINT = process.env
11+
.HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE,
12+
} = process.env;
13+
14+
let latestEndOfLifeServerVersion: Promise<string> | null = null;
15+
16+
export async function getLatestEndOfLifeServerVersion(): Promise<string> {
17+
if (!HADRON_AUTO_UPDATE_ENDPOINT) {
18+
log.debug(
19+
mongoLogId(1_001_000_352),
20+
'getLatestEndOfLifeServerVersion',
21+
'HADRON_AUTO_UPDATE_ENDPOINT is not set'
22+
);
23+
return FALLBACK_END_OF_LIFE_SERVER_VERSION;
24+
}
25+
26+
if (!latestEndOfLifeServerVersion) {
27+
// Setting module scoped variable to avoid repeated fetches.
28+
log.debug(
29+
mongoLogId(1_001_000_353),
30+
'getLatestEndOfLifeServerVersion',
31+
'Fetching EOL server version'
32+
);
33+
latestEndOfLifeServerVersion = fetch(
34+
`${HADRON_AUTO_UPDATE_ENDPOINT}/api/v2/eol-server`
35+
)
36+
.then(async (response) => {
37+
if (response.ok) {
38+
const result = await response.text();
39+
log.debug(
40+
mongoLogId(1_001_000_354),
41+
'getLatestEndOfLifeServerVersion',
42+
'Got EOL server version response',
43+
{ result }
44+
);
45+
return result;
46+
} else {
47+
// Reset the cached value to null so that we can try again next time.
48+
latestEndOfLifeServerVersion = null;
49+
throw new Error(
50+
`Expected an OK response, got ${response.status} '${response.statusText}'`
51+
);
52+
}
53+
})
54+
.catch((error) => {
55+
log.error(
56+
mongoLogId(1_001_000_355),
57+
'getLatestEndOfLifeServerVersion',
58+
'Failed to fetch EOL server version',
59+
{ error }
60+
);
61+
// We don't want any downstream code to fail just because we can't fetch the EOL server version.
62+
return FALLBACK_END_OF_LIFE_SERVER_VERSION;
63+
});
64+
}
65+
// Return a cached or in-flight value
66+
return latestEndOfLifeServerVersion;
67+
}
68+
69+
export function isEndOfLifeVersion(
70+
version: string,
71+
latestEndOfLifeServerVersion: string
72+
) {
73+
try {
74+
const coercedVersion = semverCoerce(version);
75+
return coercedVersion
76+
? semverSatisfies(coercedVersion, `<=${latestEndOfLifeServerVersion}`)
77+
: false;
78+
} catch (error) {
79+
debug('Error comparing versions', { error });
80+
// If the version is not a valid semver, we can't reliably determine if it's EOL
81+
return false;
82+
}
83+
}

packages/compass-telemetry/src/telemetry-events.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2653,7 +2653,8 @@ type ScreenEvent = ConnectionScopedEvent<{
26532653
| 'restore_pipeline_modal'
26542654
| 'save_pipeline_modal'
26552655
| 'shell_info_modal'
2656-
| 'update_search_index_modal';
2656+
| 'update_search_index_modal'
2657+
| 'end_of_life_mongodb_modal';
26572658
};
26582659
}>;
26592660

packages/data-service/src/instance-detail-helper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,9 @@ function adaptHostInfo(rawHostInfo: Partial<HostInfo>): HostInfoDetails {
350350
};
351351
}
352352

353-
function adaptBuildInfo(rawBuildInfo: Partial<BuildInfo>) {
353+
export function adaptBuildInfo(
354+
rawBuildInfo: Partial<BuildInfo>
355+
): BuildInfoDetails {
354356
return {
355357
version: rawBuildInfo.version ?? '',
356358
// Cover both cases of detecting enterprise module, see SERVER-18099.

0 commit comments

Comments
 (0)