Skip to content
This repository has been archived by the owner on Sep 10, 2024. It is now read-only.

Commit

Permalink
Add interactive setup CLI (elastic#114493)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Oct 20, 2021
1 parent abd5e9f commit b879a9a
Show file tree
Hide file tree
Showing 18 changed files with 442 additions and 30 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
"chroma-js": "^1.4.1",
"classnames": "2.2.6",
"color": "1.0.3",
"commander": "^3.0.2",
"commander": "^4.1.1",
"compare-versions": "3.5.1",
"concat-stream": "1.6.2",
"constate": "^1.3.2",
Expand Down Expand Up @@ -248,6 +248,7 @@
"idx": "^2.5.6",
"immer": "^9.0.6",
"inline-style": "^2.0.0",
"inquirer": "^7.3.3",
"intl": "^1.2.5",
"intl-format-cache": "^2.1.0",
"intl-messageformat": "^2.2.0",
Expand Down Expand Up @@ -297,6 +298,7 @@
"object-hash": "^1.3.1",
"object-path-immutable": "^3.1.1",
"opn": "^5.5.0",
"ora": "^4.0.4",
"p-limit": "^3.0.1",
"p-map": "^4.0.0",
"p-retry": "^4.2.0",
Expand Down Expand Up @@ -720,7 +722,6 @@
"html": "1.0.0",
"html-loader": "^0.5.5",
"http-proxy": "^1.18.1",
"inquirer": "^7.3.3",
"is-glob": "^4.0.1",
"is-path-inside": "^3.0.2",
"istanbul-instrumenter-loader": "^3.0.1",
Expand Down Expand Up @@ -762,7 +763,6 @@
"null-loader": "^3.0.0",
"nyc": "^15.0.1",
"oboe": "^2.1.4",
"ora": "^4.0.4",
"parse-link-header": "^1.0.1",
"pbf": "3.2.1",
"pirates": "^4.0.1",
Expand Down
9 changes: 9 additions & 0 deletions scripts/kibana_setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

require('../src/cli_setup/dev');
20 changes: 20 additions & 0 deletions src/cli_plugin/lib/logger.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

interface LoggerOptions {
silent?: boolean;
quiet?: boolean;
}

export declare class Logger {
constructor(settings?: LoggerOptions);

log(data: string, sameLine?: boolean): void;

error(data: string): void;
}
118 changes: 118 additions & 0 deletions src/cli_setup/cli_setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { kibanaPackageJson } from '@kbn/utils';
import chalk from 'chalk';
import ora from 'ora';
import { Command } from 'commander';
import { getConfigPath } from '@kbn/utils';

import {
ElasticsearchService,
EnrollResult,
} from '../plugins/interactive_setup/server/elasticsearch_service';
import { getDetailedErrorMessage } from '../plugins/interactive_setup/server/errors';
import {
promptToken,
getCommand,
decodeEnrollmentToken,
kibanaConfigWriter,
elasticsearch,
} from './utils';
import { Logger } from '../cli_plugin/lib/logger';

const program = new Command('bin/kibana-setup');

program
.version(kibanaPackageJson.version)
.description(
'This command walks you through all required steps to securely connect Kibana with Elasticsearch'
)
.option('-t, --token <token>', 'Elasticsearch enrollment token')
.option('-s, --silent', 'Prevent all logging');

program.parse(process.argv);

interface SetupOptions {
token?: string;
silent?: boolean;
}

const options = program.opts() as SetupOptions;
const spinner = ora();
const logger = new Logger(options);

async function initCommand() {
const token = decodeEnrollmentToken(
options.token ?? (options.silent ? undefined : await promptToken())
);
if (!token) {
logger.error(chalk.red('Invalid enrollment token provided.'));
logger.error('');
logger.error('To generate a new enrollment token run:');
logger.error(` ${getCommand('elasticsearch-create-enrollment-token', '-s kibana')}`);
process.exit(1);
}

if (!(await kibanaConfigWriter.isConfigWritable())) {
logger.error(chalk.red('Kibana does not have enough permissions to write to the config file.'));
logger.error('');
logger.error('To grant write access run:');
logger.error(` chmod +w ${getConfigPath()}`);
process.exit(1);
}

logger.log('');
if (!options.silent) {
spinner.start(chalk.dim('Configuring Kibana...'));
}

let configToWrite: EnrollResult;
try {
configToWrite = await elasticsearch.enroll({
hosts: token.adr,
apiKey: token.key,
caFingerprint: ElasticsearchService.formatFingerprint(token.fgr),
});
} catch (error) {
if (!options.silent) {
spinner.fail(
`${chalk.bold('Unable to enroll with Elasticsearch:')} ${chalk.red(
`${getDetailedErrorMessage(error)}`
)}`
);
}
logger.error('');
logger.error('To generate a new enrollment token run:');
logger.error(` ${getCommand('elasticsearch-create-enrollment-token', '-s kibana')}`);
process.exit(1);
}

try {
await kibanaConfigWriter.writeConfig(configToWrite);
} catch (error) {
if (!options.silent) {
spinner.fail(
`${chalk.bold('Unable to configure Kibana:')} ${chalk.red(
`${getDetailedErrorMessage(error)}`
)}`
);
}
logger.error(chalk.red(`${getDetailedErrorMessage(error)}`));
process.exit(1);
}

if (!options.silent) {
spinner.succeed(chalk.bold('Kibana configured successfully.'));
}
logger.log('');
logger.log('To start Kibana run:');
logger.log(` ${getCommand('kibana')}`);
}

initCommand();
10 changes: 10 additions & 0 deletions src/cli_setup/dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

require('../setup_node_env');
require('./cli_setup');
10 changes: 10 additions & 0 deletions src/cli_setup/dist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

require('../setup_node_env/dist');
require('./cli_setup');
13 changes: 13 additions & 0 deletions src/cli_setup/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/src/cli_setup'],
};
76 changes: 76 additions & 0 deletions src/cli_setup/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { decodeEnrollmentToken, getCommand } from './utils';
import type { EnrollmentToken } from '../plugins/interactive_setup/common';

describe('kibana setup cli', () => {
describe('getCommand', () => {
const originalPlatform = process.platform;

it('should format windows correctly', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
});
expect(getCommand('kibana')).toEqual('bin\\kibana.bat');
expect(getCommand('kibana', '--silent')).toEqual('bin\\kibana.bat --silent');
});

it('should format unix correctly', () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
});
expect(getCommand('kibana')).toEqual('bin/kibana');
expect(getCommand('kibana', '--silent')).toEqual('bin/kibana --silent');
});

afterAll(function () {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
});
});

describe('decodeEnrollmentToken', () => {
const token: EnrollmentToken = {
ver: '8.0.0',
adr: ['localhost:9200'],
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',
key: 'JH-36HoBo4EYIoVhHh2F:uEo4dksARMq_BSHaAHUr8Q',
};

it('should decode a valid token', () => {
expect(decodeEnrollmentToken(btoa(JSON.stringify(token)))).toEqual({
adr: ['https://localhost:9200'],
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',
key: 'SkgtMzZIb0JvNEVZSW9WaEhoMkY6dUVvNGRrc0FSTXFfQlNIYUFIVXI4UQ==',
ver: '8.0.0',
});
});

it('should not decode an invalid token', () => {
expect(decodeEnrollmentToken(JSON.stringify(token))).toBeUndefined();
expect(
decodeEnrollmentToken(
btoa(
JSON.stringify({
ver: [''],
adr: null,
fgr: false,
key: undefined,
})
)
)
).toBeUndefined();
expect(decodeEnrollmentToken(btoa(JSON.stringify({})))).toBeUndefined();
expect(decodeEnrollmentToken(btoa(JSON.stringify([])))).toBeUndefined();
expect(decodeEnrollmentToken(btoa(JSON.stringify(null)))).toBeUndefined();
expect(decodeEnrollmentToken(btoa(JSON.stringify('')))).toBeUndefined();
});
});
});
91 changes: 91 additions & 0 deletions src/cli_setup/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { getConfigPath } from '@kbn/utils';
import inquirer from 'inquirer';
import { duration } from 'moment';
import { merge } from 'lodash';

import { Logger } from '../core/server';
import { ClusterClient } from '../core/server/elasticsearch/client';
import { configSchema } from '../core/server/elasticsearch';
import { ElasticsearchService } from '../plugins/interactive_setup/server/elasticsearch_service';
import { KibanaConfigWriter } from '../plugins/interactive_setup/server/kibana_config_writer';
import type { EnrollmentToken } from '../plugins/interactive_setup/common';

const noop = () => {};
const logger: Logger = {
debug: noop,
error: noop,
warn: noop,
trace: noop,
info: noop,
fatal: noop,
log: noop,
get: () => logger,
};

export const kibanaConfigWriter = new KibanaConfigWriter(getConfigPath(), logger);
export const elasticsearch = new ElasticsearchService(logger).setup({
connectionCheckInterval: duration(Infinity),
elasticsearch: {
createClient: (type, config) => {
const defaults = configSchema.validate({});
return new ClusterClient(
merge(
defaults,
{
hosts: Array.isArray(defaults.hosts) ? defaults.hosts : [defaults.hosts],
},
config
),
logger,
type
);
},
},
});

export async function promptToken() {
const answers = await inquirer.prompt({
type: 'input',
name: 'token',
message: 'Enter enrollment token:',
validate: (value = '') => (decodeEnrollmentToken(value) ? true : 'Invalid enrollment token'),
});
return answers.token;
}

export function decodeEnrollmentToken(enrollmentToken: string): EnrollmentToken | undefined {
try {
const json = JSON.parse(atob(enrollmentToken)) as EnrollmentToken;
if (
!Array.isArray(json.adr) ||
json.adr.some((adr) => typeof adr !== 'string') ||
typeof json.fgr !== 'string' ||
typeof json.key !== 'string' ||
typeof json.ver !== 'string'
) {
return;
}
return { ...json, adr: json.adr.map((adr) => `https://${adr}`), key: btoa(json.key) };
} catch (error) {} // eslint-disable-line no-empty
}

function btoa(str: string) {
return Buffer.from(str, 'binary').toString('base64');
}

function atob(str: string) {
return Buffer.from(str, 'base64').toString('binary');
}

export function getCommand(command: string, args?: string) {
const isWindows = process.platform === 'win32';
return `${isWindows ? `bin\\${command}.bat` : `bin/${command}`}${args ? ` ${args}` : ''}`;
}
Loading

0 comments on commit b879a9a

Please sign in to comment.