diff --git a/packages/browser-repl/package-lock.json b/packages/browser-repl/package-lock.json index c68b48d268..3bf2f452e7 100644 --- a/packages/browser-repl/package-lock.json +++ b/packages/browser-repl/package-lock.json @@ -23,6 +23,7 @@ "@leafygreen-ui/code": "^9.4.0", "@leafygreen-ui/icon": "^11.6.1", "@leafygreen-ui/palette": "^3.3.1", + "@leafygreen-ui/typography": "^8.1.0", "@storybook/addon-knobs": "^5.3.10", "@storybook/react": "^5.3.1", "@types/classnames": "^2.2.11", @@ -60,6 +61,7 @@ "@leafygreen-ui/code": "^9.4.0", "@leafygreen-ui/icon": "^11.6.1", "@leafygreen-ui/palette": "^3.3.1", + "@leafygreen-ui/typography": "^8.1.0", "mongodb-ace-theme": "^0.0.1", "prop-types": "^15.7.2", "react": "^16.12.0", @@ -2446,6 +2448,22 @@ "@leafygreen-ui/lib": "^9.0.0" } }, + "node_modules/@leafygreen-ui/typography": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/typography/-/typography-8.1.0.tgz", + "integrity": "sha512-zH+rDVaFzFnEjujtzsErFS4w1cX8x1/y9ALIEO2RtmHr4di7c6t/9UyKUueG8VWEtWOzBu+VjxSgtKbB+NkgOg==", + "dev": true, + "dependencies": { + "@leafygreen-ui/box": "^3.0.6", + "@leafygreen-ui/icon": "^11.3.0", + "@leafygreen-ui/lib": "^9.0.0", + "@leafygreen-ui/palette": "^3.2.2", + "@leafygreen-ui/tokens": "^0.5.3" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^2.1.3" + } + }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -20430,6 +20448,19 @@ "@leafygreen-ui/lib": "^9.0.0" } }, + "@leafygreen-ui/typography": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/typography/-/typography-8.1.0.tgz", + "integrity": "sha512-zH+rDVaFzFnEjujtzsErFS4w1cX8x1/y9ALIEO2RtmHr4di7c6t/9UyKUueG8VWEtWOzBu+VjxSgtKbB+NkgOg==", + "dev": true, + "requires": { + "@leafygreen-ui/box": "^3.0.6", + "@leafygreen-ui/icon": "^11.3.0", + "@leafygreen-ui/lib": "^9.0.0", + "@leafygreen-ui/palette": "^3.2.2", + "@leafygreen-ui/tokens": "^0.5.3" + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", diff --git a/packages/browser-repl/package.json b/packages/browser-repl/package.json index 30e57331f8..99cdb4617b 100644 --- a/packages/browser-repl/package.json +++ b/packages/browser-repl/package.json @@ -58,6 +58,7 @@ "@leafygreen-ui/code": "^9.4.0", "@leafygreen-ui/icon": "^11.6.1", "@leafygreen-ui/palette": "^3.3.1", + "@leafygreen-ui/typography": "^8.1.0", "@storybook/addon-knobs": "^5.3.10", "@storybook/react": "^5.3.1", "@types/classnames": "^2.2.11", @@ -92,6 +93,7 @@ "@leafygreen-ui/code": "^9.4.0", "@leafygreen-ui/icon": "^11.6.1", "@leafygreen-ui/palette": "^3.3.1", + "@leafygreen-ui/typography": "^8.1.0", "mongodb-ace-theme": "^0.0.1", "prop-types": "^15.7.2", "react": "^16.12.0", diff --git a/packages/browser-repl/src/components/shell-output-line.tsx b/packages/browser-repl/src/components/shell-output-line.tsx index 387022e909..695782a89b 100644 --- a/packages/browser-repl/src/components/shell-output-line.tsx +++ b/packages/browser-repl/src/components/shell-output-line.tsx @@ -8,6 +8,7 @@ import Icon from '@leafygreen-ui/icon'; import { LineWithIcon } from './utils/line-with-icon'; import { HelpOutput } from './types/help-output'; +import { ShowBannerResultOutput } from './types/show-banner-result-output'; import { ShowDbsOutput } from './types/show-dbs-output'; import { ShowCollectionsOutput } from './types/show-collections-output'; import { CursorOutput } from './types/cursor-output'; @@ -67,7 +68,7 @@ export class ShellOutputLine extends Component { return ; } - if (type === 'ListCommandsResult)') { + if (type === 'ListCommandsResult') { return ; } @@ -75,6 +76,10 @@ export class ShellOutputLine extends Component { return ; } + if (type === 'ShowBannerResult') { + return ; + } + if (type === 'Cursor' || type === 'AggregationCursor') { return ; } diff --git a/packages/browser-repl/src/components/types/show-banner-result-output.tsx b/packages/browser-repl/src/components/types/show-banner-result-output.tsx new file mode 100644 index 0000000000..9e208268e0 --- /dev/null +++ b/packages/browser-repl/src/components/types/show-banner-result-output.tsx @@ -0,0 +1,20 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { H3 } from '@leafygreen-ui/typography'; + +interface ShowBannerResultOutputProps { + value: null | { header?: string, content: string }; +} + +export class ShowBannerResultOutput extends Component { + static propTypes = { + value: PropTypes.any + }; + + render(): JSX.Element { + return (<> + {this.props.value?.header &&

{this.props.value.header}

} + {this.props.value?.content &&
{this.props.value.content}
} + ); + } +} diff --git a/packages/cli-repl/src/format-output.spec.ts b/packages/cli-repl/src/format-output.spec.ts index 084137818d..7eacd90189 100644 --- a/packages/cli-repl/src/format-output.spec.ts +++ b/packages/cli-repl/src/format-output.spec.ts @@ -371,5 +371,19 @@ test 558.79 GiB }); } }); + + context('when the result is ShowBannerResult', () => { + it('returns a formatted banner', () => { + const output = stripAnsiColors(format({ + value: { + header: 'Header', + content: 'foo\nbar\n' + }, + type: 'ShowBannerResult' + })); + + expect(output).to.equal('------\n Header\n foo\n bar\n------\n'); + }); + }); }); } diff --git a/packages/cli-repl/src/format-output.ts b/packages/cli-repl/src/format-output.ts index 1c68ea45f7..d27a22d95c 100644 --- a/packages/cli-repl/src/format-output.ts +++ b/packages/cli-repl/src/format-output.ts @@ -62,6 +62,10 @@ export default function formatOutput(evaluationResult: EvaluationResult, options return formatCollections(value, options); } + if (type === 'ShowBannerResult') { + return formatBanner(value, options); + } + if (type === 'StatsResult') { return formatStats(value, options); } @@ -147,6 +151,22 @@ function formatCollections(output: CollectionNamesWithTypes[], options: FormatOp return textTable(tableEntries, { align: ['l', 'l'] }); } +function formatBanner(output: null | { header?: string, content: string }, options: FormatOptions): string { + if (!output?.content) { + return ''; + } + + let text = ''; + text += `${clr('------', 'mongosh:section-header', options)}\n`; + if (output.header) { + text += ` ${clr(output.header, 'mongosh:section-header', options)}\n`; + } + // indent output.content with 3 spaces + text += output.content.trim().replace(/^/gm, ' ') + '\n'; + text += `${clr('------', 'mongosh:section-header', options)}\n`; + return text; +} + function formatDatabases(output: any[], options: FormatOptions): string { const tableEntries = output.map( (db) => [clr(db.name, 'bold', options), formatBytes(db.sizeOnDisk)] diff --git a/packages/cli-repl/src/mongosh-repl.spec.ts b/packages/cli-repl/src/mongosh-repl.spec.ts index b176fea98d..1d744ccf66 100644 --- a/packages/cli-repl/src/mongosh-repl.spec.ts +++ b/packages/cli-repl/src/mongosh-repl.spec.ts @@ -10,7 +10,7 @@ import { StubbedInstance, stubInterface } from 'ts-sinon'; import { promisify } from 'util'; import { expect, fakeTTYProps, tick, useTmpdir, waitEval } from '../test/repl-helpers'; import MongoshNodeRepl, { MongoshIOProvider, MongoshNodeReplOptions } from './mongosh-repl'; -import { parseAnyLogEntry } from './log-entry'; +import { parseAnyLogEntry } from '../../shell-api/src/log-entry'; import stripAnsi from 'strip-ansi'; const delay = promisify(setTimeout); @@ -64,6 +64,7 @@ describe('MongoshNodeRepl', () => { version: '4.4.1' } }); + sp.runCommandWithCheck.resolves({ ok: 1 }); serviceProvider = sp; mongoshReplOptions = { diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 470d8aebdc..d7a95c3f93 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -1,5 +1,5 @@ import completer from '@mongosh/autocomplete'; -import { MongoshCommandFailed, MongoshInternalError, MongoshWarning } from '@mongosh/errors'; +import { MongoshInternalError, MongoshWarning } from '@mongosh/errors'; import { changeHistory } from '@mongosh/history'; import type { AutoEncryptionOptions, ServiceProvider } from '@mongosh/service-provider-core'; import { EvaluationListener, OnLoadResult, ShellCliOptions, ShellInstanceState, getShellApiType, toShellResult } from '@mongosh/shell-api'; @@ -20,7 +20,6 @@ import { MONGOSH_WIKI, TELEMETRY_GREETING_MESSAGE } from './constants'; import formatOutput, { formatError } from './format-output'; import { makeMultilineJSIntoSingleLine } from '@mongosh/js-multiline-to-singleline'; import { LineByLineInput } from './line-by-line-input'; -import { LogEntry, parseAnyLogEntry } from './log-entry'; /** * All CLI flags that are useful for {@link MongoshNodeRepl}. @@ -186,7 +185,6 @@ class MongoshNodeRepl implements EvaluationListener { mongodVersion = (mongodVersion ? mongodVersion + ' ' : '') + `(API Version ${apiVersion})`; } await this.greet(mongodVersion); - await this.printStartupLog(instanceState); await this.printBasicConnectivityWarning(instanceState); this.inspectCompact = await this.getConfig('inspectCompact'); @@ -380,6 +378,24 @@ class MongoshNodeRepl implements EvaluationListener { }); instanceState.setCtx(repl.context); + + if (!this.shellCliOptions.nodb && !this.shellCliOptions.quiet) { + // cf. legacy shell: + // https://github.com/mongodb/mongo/blob/a6df396047a77b90bf1ce9463eecffbee16fb864/src/mongo/shell/mongo_main.cpp#L1003-L1026 + const { shellApi } = instanceState; + const banners = await Promise.all([ + (async() => await shellApi.show('startupWarnings'))(), + (async() => await shellApi.show('freeMonitoring'))(), + (async() => await shellApi.show('automationNotices'))() + ]); + for (const banner of banners) { + if (banner.value) { + await shellApi.print(banner); + } + } + // Omitted, see MONGOSH-57: 'show nonGenuineMongoDBCheck' + } + return { __initialized: 'yes' }; } @@ -421,46 +437,6 @@ class MongoshNodeRepl implements EvaluationListener { this.output.write(text); } - /** - * Print warnings from the server startup log, if any. - */ - async printStartupLog(instanceState: ShellInstanceState): Promise { - if (this.shellCliOptions.nodb || this.shellCliOptions.quiet) { - return; - } - - type GetLogResult = { ok: number, totalLinesWritten: number, log: string[] | undefined }; - let result; - try { - result = await instanceState.currentDb.adminCommand({ getLog: 'startupWarnings' }) as GetLogResult; - if (!result) { - throw new MongoshCommandFailed('adminCommand getLog unexpectedly returned no result'); - } - } catch (error: any) { - this.bus.emit('mongosh:error', error, 'repl'); - return; - } - - if (!result.log || !result.log.length) { - return; - } - - let text = ''; - text += `${this.clr('------', 'mongosh:section-header')}\n`; - text += ` ${this.clr('The server generated these startup warnings when booting:', 'mongosh:warning')}\n`; - result.log.forEach(logLine => { - try { - const entry: LogEntry = parseAnyLogEntry(logLine); - text += ` ${entry.timestamp}: ${entry.message}\n`; - } catch (e: any) { - text += ` Unexpected log line format: ${logLine}\n`; - } - }); - text += `${this.clr('------', 'mongosh:section-header')}\n`; - text += '\n'; - this.output.write(text); - } - /** * Print a warning if the server is not able to respond to commands. * This can happen in load balanced mode, for example. diff --git a/packages/cli-repl/test/e2e-banners.spec.ts b/packages/cli-repl/test/e2e-banners.spec.ts new file mode 100644 index 0000000000..6ad8d60aa4 --- /dev/null +++ b/packages/cli-repl/test/e2e-banners.spec.ts @@ -0,0 +1,125 @@ +import { serialize, Long } from 'bson'; +import { once } from 'events'; +import { createServer } from 'http'; +import { startTestServer, skipIfApiStrict } from '../../../testing/integration-testing-hooks'; +import { TestShell } from './test-shell'; + +describe('e2e startup banners', () => { + skipIfApiStrict(); + afterEach(TestShell.cleanup); + + const testServer = startTestServer('shared'); + + let freeMonitoringHttpServer; + before(async() => { + freeMonitoringHttpServer = createServer((req, res) => { + req.resume().on('end', () => { + res.end(serialize({ + version: new Long(1), + haltMetricsUploading: false, + id: 'mock123', + informationalURL: 'http://www.example.com', + message: 'Welcome to the Mock Free Monitoring Endpoint', + reportingInterval: new Long(1), + userReminder: 'Some user reminder about free monitoring' + })); + }); + }); + freeMonitoringHttpServer.listen(42123); + await once(freeMonitoringHttpServer, 'listening'); + }); + + after(() => { + freeMonitoringHttpServer.close(); + }); + + context('without special configuration', () => { + it('shows startup warnings', async() => { + const shell = TestShell.start({ args: [await testServer.connectionString()] }); + await shell.waitForPrompt(); + shell.assertContainsOutput('The server generated these startup warnings when booting'); + shell.assertContainsOutput('Access control is not enabled for the database.'); + shell.assertNoErrors(); + }); + }); + + context('with automation notices enabled', () => { + let helperShell: TestShell; + + beforeEach(async() => { + helperShell = TestShell.start({ args: [await testServer.connectionString()] }); + await helperShell.waitForPrompt(); + await helperShell.executeLine('db.adminCommand({setParameter: 1, automationServiceDescriptor: "automation service"})'); + }); + + afterEach(async() => { + await helperShell.executeLine('db.adminCommand({setParameter: 1, automationServiceDescriptor: ""})'); + helperShell.assertNoErrors(); + }); + + it('shows automation notices', async() => { + const shell = TestShell.start({ args: [await testServer.connectionString()] }); + await shell.waitForPrompt(); + shell.assertContainsOutput("This server is managed by automation service 'automation service'."); + shell.assertNoErrors(); + }); + }); + + context('with free monitoring', () => { + if ( + !process.env.MONGOSH_SERVER_TEST_VERSION?.includes('community') || + process.env.MONGOSH_SERVER_TEST_VERSION.match(/^4\.[0-3]/) || + process.platform === 'win32' + ) { + // Enterprise and 4.2/4.0 community servers do not know about the setParameter flags below. + // On Windows in CI, this fails as well (for unknown reasons). + before(function() { this.skip(); }); + } + + // Using a non-shared server so we can change the server configuration + // in isolation here. + const testServer = startTestServer( + 'not-shared', + '--setParameter', 'cloudFreeMonitoringEndpointURL=http://127.0.0.1:42123/', + '--setParameter', 'testingDiagnosticsEnabled=true'); + + it('shows free monitoring notice by default', async() => { + const shell = TestShell.start({ args: [await testServer.connectionString()] }); + await shell.waitForPrompt(); + shell.assertContainsOutput('To enable free monitoring, run the following command: db.enableFreeMonitoring()'); + shell.assertNoErrors(); + }); + + context('with free monitoring explicitly disabled', () => { + beforeEach(async() => { + const helperShell = TestShell.start({ args: [await testServer.connectionString()] }); + await helperShell.waitForPrompt(); + await helperShell.executeLine('db.disableFreeMonitoring()'); + helperShell.assertNoErrors(); + }); + + it('does not show a free monitoring notice', async() => { + const shell = TestShell.start({ args: [await testServer.connectionString()] }); + await shell.waitForPrompt(); + shell.assertNotContainsOutput('free monitoring'); + shell.assertNoErrors(); + }); + }); + + context('with free monitoring explicitly enabled', () => { + beforeEach(async() => { + const helperShell = TestShell.start({ args: [await testServer.connectionString()] }); + await helperShell.waitForPrompt(); + await helperShell.executeLine('db.enableFreeMonitoring()'); + helperShell.assertNoErrors(); + }); + + it('does not show a free monitoring notice', async() => { + const shell = TestShell.start({ args: [await testServer.connectionString()] }); + await shell.waitForPrompt(); + shell.assertContainsOutput('Some user reminder about free monitoring'); + shell.assertNoErrors(); + }); + }); + }); +}); diff --git a/packages/shell-api/src/helpers.ts b/packages/shell-api/src/helpers.ts index aa1725cc05..13fa149f74 100644 --- a/packages/shell-api/src/helpers.ts +++ b/packages/shell-api/src/helpers.ts @@ -794,3 +794,15 @@ export function getBadge(collections: Document[], index: number): string { return ''; } + +export const FREE_MONITORING_BANNER = `\ +Enable MongoDB's free cloud-based monitoring service, which will then receive and display +metrics about your deployment (disk utilization, CPU, operation statistics, etc). + +The monitoring data will be available on a MongoDB website with a unique URL accessible to you +and anyone you share the URL with. MongoDB may use this information to make product +improvements and to suggest MongoDB products and deployment options to you. + +To enable free monitoring, run the following command: db.enableFreeMonitoring() +To permanently disable this reminder, run the following command: db.disableFreeMonitoring() +`; diff --git a/packages/cli-repl/src/log-entry.ts b/packages/shell-api/src/log-entry.ts similarity index 100% rename from packages/cli-repl/src/log-entry.ts rename to packages/shell-api/src/log-entry.ts diff --git a/packages/shell-api/src/mongo.spec.ts b/packages/shell-api/src/mongo.spec.ts index 524a9b411d..a23fd6ed64 100644 --- a/packages/shell-api/src/mongo.spec.ts +++ b/packages/shell-api/src/mongo.spec.ts @@ -315,6 +315,131 @@ describe('Mongo', () => { expect(caughtError).to.equal(expectedError); }); }); + + describe('startupWarnings', () => { + it('calls database.adminCommand', async() => { + const expectedResult = { ok: 1, log: [] }; + database.adminCommand.resolves(expectedResult); + await mongo.show('startupWarnings'); + expect(database.adminCommand).to.have.been.calledWith( + { getLog: 'startupWarnings' } + ); + }); + + it('returns ShowBannerResult CommandResult', async() => { + const expectedResult = { + ok: 1, + log: [ + '{"t":{"$date":"2022-05-17T11:16:16.597+02:00"},"s":"I", "c":"STORAGE", "id":22297, "ctx":"initandlisten","msg":"Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem","tags":["startupWarnings"]}\n', + '{"t":{"$date":"2022-05-17T11:16:16.778+02:00"},"s":"W", "c":"CONTROL", "id":22120, "ctx":"initandlisten","msg":"Access control is not enabled for the database. Read and write access to data and configuration is unrestricted","tags":["startupWarnings"]}\n', + ] + }; + database.adminCommand.resolves(expectedResult); + const result = await mongo.show('startupWarnings'); + expect(result.value).to.deep.equal({ + header: 'The server generated these startup warnings when booting', + content: '2022-05-17T11:16:16.597+02:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem\n' + + '2022-05-17T11:16:16.778+02:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted' + }); + expect(result.type).to.equal('ShowBannerResult'); + }); + + it('returns null database.adminCommand rejects', async() => { + const expectedError = new Error(); + database.adminCommand.rejects(expectedError); + const result = await mongo.show('startupWarnings'); + expect(result.value).to.equal(null); + expect(result.type).to.equal('ShowBannerResult'); + }); + }); + + describe('freeMonitoring', () => { + it('calls database.adminCommand', async() => { + const expectedResult = { ok: 1, state: '' }; + database.adminCommand.resolves(expectedResult); + await mongo.show('freeMonitoring'); + expect(database.adminCommand).to.have.been.calledWith( + { getFreeMonitoringStatus: 1 } + ); + }); + + it('returns ShowBannerResult CommandResult (freeMonitoring enabled with notice)', async() => { + const expectedResult = { + ok: 1, + state: 'enabled', + userReminder: 'Reminder!' + }; + database.adminCommand.resolves(expectedResult); + const result = await mongo.show('freeMonitoring'); + expect(result.value).to.deep.equal({ + content: 'Reminder!' + }); + expect(result.type).to.equal('ShowBannerResult'); + }); + + it('returns ShowBannerResult CommandResult (freeMonitoring undecided)', async() => { + const expectedResult = { + ok: 1, + state: 'undecided' + }; + database.adminCommand.resolves(expectedResult); + const result = await mongo.show('freeMonitoring'); + expect((result.value as any).content).to.include('run the following command: db.enableFreeMonitoring'); + expect(result.type).to.equal('ShowBannerResult'); + }); + + it('returns ShowBannerResult CommandResult (freeMonitoring disabled)', async() => { + const expectedResult = { + ok: 1, + state: 'disabled' + }; + database.adminCommand.resolves(expectedResult); + const result = await mongo.show('freeMonitoring'); + expect(result.value).to.equal(null); + expect(result.type).to.equal('ShowBannerResult'); + }); + + it('returns null database.adminCommand rejects', async() => { + const expectedError = new Error(); + database.adminCommand.rejects(expectedError); + const result = await mongo.show('freeMonitoring'); + expect(result.value).to.equal(null); + expect(result.type).to.equal('ShowBannerResult'); + }); + }); + + describe('automationNotices', () => { + it('calls database.hello', async() => { + const expectedResult = { ok: 1 }; + database.hello.resolves(expectedResult); + await mongo.show('automationNotices'); + expect(database.hello).to.have.been.calledWith(); + }); + + it('returns ShowBannerResult CommandResult', async() => { + const expectedResult = { + ok: 1, + automationServiceDescriptor: 'some_service' + }; + database.hello.resolves(expectedResult); + const result = await mongo.show('automationNotices'); + expect(result.value).to.deep.equal({ + content: + "This server is managed by automation service 'some_service'.\n" + + 'Many administrative actions are inappropriate, and may be automatically reverted.' + }); + expect(result.type).to.equal('ShowBannerResult'); + }); + + it('returns null database.hello rejects', async() => { + const expectedError = new Error(); + database.hello.rejects(expectedError); + const result = await mongo.show('automationNotices'); + expect(result.value).to.equal(null); + expect(result.type).to.equal('ShowBannerResult'); + }); + }); + describe('invalid command', () => { it('throws an error', async() => { const caughtError = await mongo.show('aslkdjhekjghdskjhfds') diff --git a/packages/shell-api/src/mongo.ts b/packages/shell-api/src/mongo.ts index 1b3e14b54b..147730e986 100644 --- a/packages/shell-api/src/mongo.ts +++ b/packages/shell-api/src/mongo.ts @@ -1,6 +1,7 @@ /* eslint-disable complexity */ import { CommonErrors, + MongoshCommandFailed, MongoshDeprecatedError, MongoshInternalError, MongoshInvalidInputError, @@ -50,7 +51,12 @@ import { CommandResult } from './result'; import { redactURICredentials } from '@mongosh/history'; import { asPrintable, ServerVersions, Topologies } from './enums'; import Session from './session'; -import { assertArgsDefinedType, processFLEOptions, isValidDatabaseName } from './helpers'; +import { + assertArgsDefinedType, + processFLEOptions, + isValidDatabaseName, + FREE_MONITORING_BANNER +} from './helpers'; import ChangeStreamCursor from './change-stream-cursor'; import { blockedByDriverMetadata } from './error-codes'; import { @@ -59,6 +65,7 @@ import { ClientEncryption } from './field-level-encryption'; import { ShellApiErrors } from './error-codes'; +import { LogEntry, parseAnyLogEntry } from './log-entry'; @shellApiClassDefault @classPlatforms([ ReplPlatform.CLI ] ) @@ -309,6 +316,9 @@ export default class Mongo extends ShellApiClass { @returnsPromise @apiVersions([1]) async show(cmd: string, arg?: string): Promise { + const db = this._instanceState.currentDb; + // legacy shell: + // https://github.com/mongodb/mongo/blob/a6df396047a77b90bf1ce9463eecffbee16fb864/src/mongo/shell/utils.js#L900-L1226 this._instanceState.messageBus.emit('mongosh:show', { method: `show ${cmd}` }); switch (cmd) { @@ -318,10 +328,10 @@ export default class Mongo extends ShellApiClass { return new CommandResult('ShowDatabasesResult', result); case 'collections': case 'tables': - const collectionNames = await this._instanceState.currentDb._getCollectionNamesWithTypes({ readPreference: 'primaryPreferred', promoteLongs: true }); + const collectionNames = await db._getCollectionNamesWithTypes({ readPreference: 'primaryPreferred', promoteLongs: true }); return new CommandResult('ShowCollectionsResult', collectionNames); case 'profile': - const sysprof = this._instanceState.currentDb.getCollection('system.profile'); + const sysprof = db.getCollection('system.profile'); const profiles = { count: await sysprof.countDocuments({}) } as Document; if (profiles.count !== 0) { profiles.result = await (await sysprof.find({ millis: { $gt: 0 } })) @@ -331,17 +341,81 @@ export default class Mongo extends ShellApiClass { } return new CommandResult('ShowProfileResult', profiles); case 'users': - const users = await this._instanceState.currentDb.getUsers(); + const users = await db.getUsers(); return new CommandResult('ShowResult', users.users); case 'roles': - const roles = await this._instanceState.currentDb.getRoles({ showBuiltinRoles: true }); + const roles = await db.getRoles({ showBuiltinRoles: true }); return new CommandResult('ShowResult', roles.roles); case 'log': - const log = await this._instanceState.currentDb.adminCommand({ getLog: arg || 'global' }); + const log = await db.adminCommand({ getLog: arg || 'global' }); return new CommandResult('ShowResult', log.log); case 'logs': - const logs = await this._instanceState.currentDb.adminCommand({ getLog: '*' }); + const logs = await db.adminCommand({ getLog: '*' }); return new CommandResult('ShowResult', logs.names); + case 'startupWarnings': { + type GetLogResult = { ok: number, totalLinesWritten: number, log: string[] | undefined }; + let result; + try { + result = await db.adminCommand({ getLog: 'startupWarnings' }) as GetLogResult; + if (!result) { + throw new MongoshCommandFailed('adminCommand getLog unexpectedly returned no result'); + } + } catch (error: any) { + this._instanceState.messageBus.emit('mongosh:error', error, 'shell-api'); + return new CommandResult('ShowBannerResult', null); + } + + if (!result.log || !result.log.length) { + return new CommandResult('ShowBannerResult', null); + } + + const lines: string[] = result.log.map(logLine => { + try { + const entry: LogEntry = parseAnyLogEntry(logLine); + return `${entry.timestamp}: ${entry.message}`; + } catch (e: any) { + return `Unexpected log line format: ${logLine}`; + } + }); + return new CommandResult('ShowBannerResult', { + header: 'The server generated these startup warnings when booting', + content: lines.join('\n') + }); + } + case 'freeMonitoring': { + let freemonStatus; + try { + freemonStatus = await db.adminCommand({ getFreeMonitoringStatus: 1 }); + } catch (error: any) { + this._instanceState.messageBus.emit('mongosh:error', error, 'shell-api'); + return new CommandResult('ShowBannerResult', null); + } + + if (freemonStatus.state === 'enabled' && freemonStatus.userReminder) { + return new CommandResult('ShowBannerResult', { content: freemonStatus.userReminder }); + } else if (freemonStatus.state === 'undecided') { + return new CommandResult('ShowBannerResult', { content: FREE_MONITORING_BANNER }); + } + + return new CommandResult('ShowBannerResult', null); + } + case 'automationNotices': { + let helloResult; + try { + helloResult = await db.hello(); + } catch (error: any) { + this._instanceState.messageBus.emit('mongosh:error', error, 'shell-api'); + return new CommandResult('ShowBannerResult', null); + } + if (helloResult.automationServiceDescriptor) { + return new CommandResult('ShowBannerResult', { + content: + `This server is managed by automation service '${helloResult.automationServiceDescriptor}'.\n` + + 'Many administrative actions are inappropriate, and may be automatically reverted.' + }); + } + return new CommandResult('ShowBannerResult', null); + } default: const err = new MongoshInvalidInputError( `'${cmd}' is not a valid argument for "show".`, diff --git a/packages/shell-api/src/shell-api.ts b/packages/shell-api/src/shell-api.ts index 7bf595fcc3..6669af9cfc 100644 --- a/packages/shell-api/src/shell-api.ts +++ b/packages/shell-api/src/shell-api.ts @@ -112,7 +112,10 @@ async function showCompleter(params: ShellCommandAutocompleteParameters, args: s // Special-case: The user might want `show dbs` or `show databases`, but they won't care about which they get. return ['databases']; } - const candidates = ['databases', 'dbs', 'collections', 'tables', 'profile', 'users', 'roles', 'log', 'logs']; + const candidates = [ + 'databases', 'dbs', 'collections', 'tables', 'profile', 'users', 'roles', 'log', 'logs', + 'startupWarnings', 'freeMonitoring', 'automationNotices' + ]; return candidates.filter(str => str.startsWith(args[1] ?? '')); }