Skip to content

Commit b879a9a

Browse files
thomheymannspalgerkibanamachine
authored
Add interactive setup CLI (elastic#114493)
* Add interactive setup CLI * Added tsconfig * ignore all CLI dev.js files when building * add cli_init to the root TS project and setup necessary ref * Fix type errors * Added suggestions from code review * ts fix * fixed build dependencies * Added suggestions from code review * fix type definitions * fix types * upgraded commander to fix ts issues * Revert "upgraded commander to fix ts issues" This reverts commit 52b8943. * upgraded commander Co-authored-by: spalger <[email protected]> Co-authored-by: Kibana Machine <[email protected]>
1 parent abd5e9f commit b879a9a

18 files changed

+442
-30
lines changed

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
"chroma-js": "^1.4.1",
198198
"classnames": "2.2.6",
199199
"color": "1.0.3",
200-
"commander": "^3.0.2",
200+
"commander": "^4.1.1",
201201
"compare-versions": "3.5.1",
202202
"concat-stream": "1.6.2",
203203
"constate": "^1.3.2",
@@ -248,6 +248,7 @@
248248
"idx": "^2.5.6",
249249
"immer": "^9.0.6",
250250
"inline-style": "^2.0.0",
251+
"inquirer": "^7.3.3",
251252
"intl": "^1.2.5",
252253
"intl-format-cache": "^2.1.0",
253254
"intl-messageformat": "^2.2.0",
@@ -297,6 +298,7 @@
297298
"object-hash": "^1.3.1",
298299
"object-path-immutable": "^3.1.1",
299300
"opn": "^5.5.0",
301+
"ora": "^4.0.4",
300302
"p-limit": "^3.0.1",
301303
"p-map": "^4.0.0",
302304
"p-retry": "^4.2.0",
@@ -720,7 +722,6 @@
720722
"html": "1.0.0",
721723
"html-loader": "^0.5.5",
722724
"http-proxy": "^1.18.1",
723-
"inquirer": "^7.3.3",
724725
"is-glob": "^4.0.1",
725726
"is-path-inside": "^3.0.2",
726727
"istanbul-instrumenter-loader": "^3.0.1",
@@ -762,7 +763,6 @@
762763
"null-loader": "^3.0.0",
763764
"nyc": "^15.0.1",
764765
"oboe": "^2.1.4",
765-
"ora": "^4.0.4",
766766
"parse-link-header": "^1.0.1",
767767
"pbf": "3.2.1",
768768
"pirates": "^4.0.1",

scripts/kibana_setup.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
require('../src/cli_setup/dev');

src/cli_plugin/lib/logger.d.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
interface LoggerOptions {
10+
silent?: boolean;
11+
quiet?: boolean;
12+
}
13+
14+
export declare class Logger {
15+
constructor(settings?: LoggerOptions);
16+
17+
log(data: string, sameLine?: boolean): void;
18+
19+
error(data: string): void;
20+
}

src/cli_setup/cli_setup.ts

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { kibanaPackageJson } from '@kbn/utils';
10+
import chalk from 'chalk';
11+
import ora from 'ora';
12+
import { Command } from 'commander';
13+
import { getConfigPath } from '@kbn/utils';
14+
15+
import {
16+
ElasticsearchService,
17+
EnrollResult,
18+
} from '../plugins/interactive_setup/server/elasticsearch_service';
19+
import { getDetailedErrorMessage } from '../plugins/interactive_setup/server/errors';
20+
import {
21+
promptToken,
22+
getCommand,
23+
decodeEnrollmentToken,
24+
kibanaConfigWriter,
25+
elasticsearch,
26+
} from './utils';
27+
import { Logger } from '../cli_plugin/lib/logger';
28+
29+
const program = new Command('bin/kibana-setup');
30+
31+
program
32+
.version(kibanaPackageJson.version)
33+
.description(
34+
'This command walks you through all required steps to securely connect Kibana with Elasticsearch'
35+
)
36+
.option('-t, --token <token>', 'Elasticsearch enrollment token')
37+
.option('-s, --silent', 'Prevent all logging');
38+
39+
program.parse(process.argv);
40+
41+
interface SetupOptions {
42+
token?: string;
43+
silent?: boolean;
44+
}
45+
46+
const options = program.opts() as SetupOptions;
47+
const spinner = ora();
48+
const logger = new Logger(options);
49+
50+
async function initCommand() {
51+
const token = decodeEnrollmentToken(
52+
options.token ?? (options.silent ? undefined : await promptToken())
53+
);
54+
if (!token) {
55+
logger.error(chalk.red('Invalid enrollment token provided.'));
56+
logger.error('');
57+
logger.error('To generate a new enrollment token run:');
58+
logger.error(` ${getCommand('elasticsearch-create-enrollment-token', '-s kibana')}`);
59+
process.exit(1);
60+
}
61+
62+
if (!(await kibanaConfigWriter.isConfigWritable())) {
63+
logger.error(chalk.red('Kibana does not have enough permissions to write to the config file.'));
64+
logger.error('');
65+
logger.error('To grant write access run:');
66+
logger.error(` chmod +w ${getConfigPath()}`);
67+
process.exit(1);
68+
}
69+
70+
logger.log('');
71+
if (!options.silent) {
72+
spinner.start(chalk.dim('Configuring Kibana...'));
73+
}
74+
75+
let configToWrite: EnrollResult;
76+
try {
77+
configToWrite = await elasticsearch.enroll({
78+
hosts: token.adr,
79+
apiKey: token.key,
80+
caFingerprint: ElasticsearchService.formatFingerprint(token.fgr),
81+
});
82+
} catch (error) {
83+
if (!options.silent) {
84+
spinner.fail(
85+
`${chalk.bold('Unable to enroll with Elasticsearch:')} ${chalk.red(
86+
`${getDetailedErrorMessage(error)}`
87+
)}`
88+
);
89+
}
90+
logger.error('');
91+
logger.error('To generate a new enrollment token run:');
92+
logger.error(` ${getCommand('elasticsearch-create-enrollment-token', '-s kibana')}`);
93+
process.exit(1);
94+
}
95+
96+
try {
97+
await kibanaConfigWriter.writeConfig(configToWrite);
98+
} catch (error) {
99+
if (!options.silent) {
100+
spinner.fail(
101+
`${chalk.bold('Unable to configure Kibana:')} ${chalk.red(
102+
`${getDetailedErrorMessage(error)}`
103+
)}`
104+
);
105+
}
106+
logger.error(chalk.red(`${getDetailedErrorMessage(error)}`));
107+
process.exit(1);
108+
}
109+
110+
if (!options.silent) {
111+
spinner.succeed(chalk.bold('Kibana configured successfully.'));
112+
}
113+
logger.log('');
114+
logger.log('To start Kibana run:');
115+
logger.log(` ${getCommand('kibana')}`);
116+
}
117+
118+
initCommand();

src/cli_setup/dev.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
require('../setup_node_env');
10+
require('./cli_setup');

src/cli_setup/dist.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
require('../setup_node_env/dist');
10+
require('./cli_setup');

src/cli_setup/jest.config.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
module.exports = {
10+
preset: '@kbn/test',
11+
rootDir: '../..',
12+
roots: ['<rootDir>/src/cli_setup'],
13+
};

src/cli_setup/utils.test.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { decodeEnrollmentToken, getCommand } from './utils';
10+
import type { EnrollmentToken } from '../plugins/interactive_setup/common';
11+
12+
describe('kibana setup cli', () => {
13+
describe('getCommand', () => {
14+
const originalPlatform = process.platform;
15+
16+
it('should format windows correctly', () => {
17+
Object.defineProperty(process, 'platform', {
18+
value: 'win32',
19+
});
20+
expect(getCommand('kibana')).toEqual('bin\\kibana.bat');
21+
expect(getCommand('kibana', '--silent')).toEqual('bin\\kibana.bat --silent');
22+
});
23+
24+
it('should format unix correctly', () => {
25+
Object.defineProperty(process, 'platform', {
26+
value: 'linux',
27+
});
28+
expect(getCommand('kibana')).toEqual('bin/kibana');
29+
expect(getCommand('kibana', '--silent')).toEqual('bin/kibana --silent');
30+
});
31+
32+
afterAll(function () {
33+
Object.defineProperty(process, 'platform', {
34+
value: originalPlatform,
35+
});
36+
});
37+
});
38+
39+
describe('decodeEnrollmentToken', () => {
40+
const token: EnrollmentToken = {
41+
ver: '8.0.0',
42+
adr: ['localhost:9200'],
43+
fgr: 'AA:C8:2C:2E:09:58:F4:FE:A1:D2:AB:7F:13:70:C2:7D:EB:FD:A2:23:88:13:E4:DA:3A:D0:59:D0:09:00:07:36',
44+
key: 'JH-36HoBo4EYIoVhHh2F:uEo4dksARMq_BSHaAHUr8Q',
45+
};
46+
47+
it('should decode a valid token', () => {
48+
expect(decodeEnrollmentToken(btoa(JSON.stringify(token)))).toEqual({
49+
adr: ['https://localhost:9200'],
50+
fgr: 'AA:C8:2C:2E:09:58:F4:FE:A1:D2:AB:7F:13:70:C2:7D:EB:FD:A2:23:88:13:E4:DA:3A:D0:59:D0:09:00:07:36',
51+
key: 'SkgtMzZIb0JvNEVZSW9WaEhoMkY6dUVvNGRrc0FSTXFfQlNIYUFIVXI4UQ==',
52+
ver: '8.0.0',
53+
});
54+
});
55+
56+
it('should not decode an invalid token', () => {
57+
expect(decodeEnrollmentToken(JSON.stringify(token))).toBeUndefined();
58+
expect(
59+
decodeEnrollmentToken(
60+
btoa(
61+
JSON.stringify({
62+
ver: [''],
63+
adr: null,
64+
fgr: false,
65+
key: undefined,
66+
})
67+
)
68+
)
69+
).toBeUndefined();
70+
expect(decodeEnrollmentToken(btoa(JSON.stringify({})))).toBeUndefined();
71+
expect(decodeEnrollmentToken(btoa(JSON.stringify([])))).toBeUndefined();
72+
expect(decodeEnrollmentToken(btoa(JSON.stringify(null)))).toBeUndefined();
73+
expect(decodeEnrollmentToken(btoa(JSON.stringify('')))).toBeUndefined();
74+
});
75+
});
76+
});

src/cli_setup/utils.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { getConfigPath } from '@kbn/utils';
10+
import inquirer from 'inquirer';
11+
import { duration } from 'moment';
12+
import { merge } from 'lodash';
13+
14+
import { Logger } from '../core/server';
15+
import { ClusterClient } from '../core/server/elasticsearch/client';
16+
import { configSchema } from '../core/server/elasticsearch';
17+
import { ElasticsearchService } from '../plugins/interactive_setup/server/elasticsearch_service';
18+
import { KibanaConfigWriter } from '../plugins/interactive_setup/server/kibana_config_writer';
19+
import type { EnrollmentToken } from '../plugins/interactive_setup/common';
20+
21+
const noop = () => {};
22+
const logger: Logger = {
23+
debug: noop,
24+
error: noop,
25+
warn: noop,
26+
trace: noop,
27+
info: noop,
28+
fatal: noop,
29+
log: noop,
30+
get: () => logger,
31+
};
32+
33+
export const kibanaConfigWriter = new KibanaConfigWriter(getConfigPath(), logger);
34+
export const elasticsearch = new ElasticsearchService(logger).setup({
35+
connectionCheckInterval: duration(Infinity),
36+
elasticsearch: {
37+
createClient: (type, config) => {
38+
const defaults = configSchema.validate({});
39+
return new ClusterClient(
40+
merge(
41+
defaults,
42+
{
43+
hosts: Array.isArray(defaults.hosts) ? defaults.hosts : [defaults.hosts],
44+
},
45+
config
46+
),
47+
logger,
48+
type
49+
);
50+
},
51+
},
52+
});
53+
54+
export async function promptToken() {
55+
const answers = await inquirer.prompt({
56+
type: 'input',
57+
name: 'token',
58+
message: 'Enter enrollment token:',
59+
validate: (value = '') => (decodeEnrollmentToken(value) ? true : 'Invalid enrollment token'),
60+
});
61+
return answers.token;
62+
}
63+
64+
export function decodeEnrollmentToken(enrollmentToken: string): EnrollmentToken | undefined {
65+
try {
66+
const json = JSON.parse(atob(enrollmentToken)) as EnrollmentToken;
67+
if (
68+
!Array.isArray(json.adr) ||
69+
json.adr.some((adr) => typeof adr !== 'string') ||
70+
typeof json.fgr !== 'string' ||
71+
typeof json.key !== 'string' ||
72+
typeof json.ver !== 'string'
73+
) {
74+
return;
75+
}
76+
return { ...json, adr: json.adr.map((adr) => `https://${adr}`), key: btoa(json.key) };
77+
} catch (error) {} // eslint-disable-line no-empty
78+
}
79+
80+
function btoa(str: string) {
81+
return Buffer.from(str, 'binary').toString('base64');
82+
}
83+
84+
function atob(str: string) {
85+
return Buffer.from(str, 'base64').toString('binary');
86+
}
87+
88+
export function getCommand(command: string, args?: string) {
89+
const isWindows = process.platform === 'win32';
90+
return `${isWindows ? `bin\\${command}.bat` : `bin/${command}`}${args ? ` ${args}` : ''}`;
91+
}

0 commit comments

Comments
 (0)