Store interactive setup certificates in the data folder. (#115981)
This commit is contained in:
parent
1732927fb1
commit
5947cee096
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getConfigPath } from '@kbn/utils';
|
||||
import { getConfigPath, getDataPath } from '@kbn/utils';
|
||||
import inquirer from 'inquirer';
|
||||
import { duration } from 'moment';
|
||||
import { merge } from 'lodash';
|
||||
|
@ -30,7 +30,7 @@ const logger: Logger = {
|
|||
get: () => logger,
|
||||
};
|
||||
|
||||
export const kibanaConfigWriter = new KibanaConfigWriter(getConfigPath(), logger);
|
||||
export const kibanaConfigWriter = new KibanaConfigWriter(getConfigPath(), getDataPath(), logger);
|
||||
export const elasticsearch = new ElasticsearchService(logger).setup({
|
||||
connectionCheckInterval: duration(Infinity),
|
||||
elasticsearch: {
|
||||
|
|
|
@ -30,6 +30,7 @@ describe('KibanaConfigWriter', () => {
|
|||
|
||||
kibanaConfigWriter = new KibanaConfigWriter(
|
||||
'/some/path/kibana.yml',
|
||||
'/data',
|
||||
loggingSystemMock.createLogger()
|
||||
);
|
||||
});
|
||||
|
@ -37,15 +38,15 @@ describe('KibanaConfigWriter', () => {
|
|||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('#isConfigWritable()', () => {
|
||||
it('returns `false` if config directory is not writable even if kibana yml is writable', async () => {
|
||||
it('returns `false` if data directory is not writable even if kibana yml is writable', async () => {
|
||||
mockFsAccess.mockImplementation((path, modifier) =>
|
||||
path === '/some/path' && modifier === constants.W_OK ? Promise.reject() : Promise.resolve()
|
||||
path === '/data' && modifier === constants.W_OK ? Promise.reject() : Promise.resolve()
|
||||
);
|
||||
|
||||
await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('returns `false` if kibana yml is NOT writable if even config directory is writable', async () => {
|
||||
it('returns `false` if kibana yml is NOT writable if even data directory is writable', async () => {
|
||||
mockFsAccess.mockImplementation((path, modifier) =>
|
||||
path === '/some/path/kibana.yml' && modifier === constants.W_OK
|
||||
? Promise.reject()
|
||||
|
@ -55,219 +56,208 @@ describe('KibanaConfigWriter', () => {
|
|||
await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('returns `true` if both kibana yml and config directory are writable', async () => {
|
||||
it('returns `true` if both kibana yml and data directory are writable', async () => {
|
||||
mockFsAccess.mockResolvedValue(undefined);
|
||||
|
||||
await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('returns `true` even if kibana yml does not exist when config directory is writable', async () => {
|
||||
it('returns `true` even if kibana yml does not exist even if data directory is writable', async () => {
|
||||
mockFsAccess.mockImplementation((path) =>
|
||||
path === '/some/path/kibana.yml' ? Promise.reject() : Promise.resolve()
|
||||
);
|
||||
|
||||
await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(true);
|
||||
await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#writeConfig()', () => {
|
||||
describe('without existing config', () => {
|
||||
beforeEach(() => {
|
||||
mockReadFile.mockResolvedValue('');
|
||||
});
|
||||
|
||||
it('throws if cannot write CA file', async () => {
|
||||
mockWriteFile.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: '',
|
||||
serviceAccountToken: { name: '', value: '' },
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`);
|
||||
|
||||
expect(mockWriteFile).toHaveBeenCalledTimes(1);
|
||||
expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content');
|
||||
});
|
||||
|
||||
it('throws if cannot write config to yaml file', async () => {
|
||||
mockWriteFile.mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`);
|
||||
|
||||
expect(mockWriteFile).toHaveBeenCalledTimes(2);
|
||||
expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content');
|
||||
expect(mockWriteFile).toHaveBeenCalledWith(
|
||||
'/some/path/kibana.yml',
|
||||
`
|
||||
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.serviceAccountToken: some-value
|
||||
elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt]
|
||||
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if cannot read existing config', async () => {
|
||||
mockReadFile.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`);
|
||||
|
||||
expect(mockWriteFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws if cannot parse existing config', async () => {
|
||||
mockReadFile.mockResolvedValue('foo: bar\nfoo: baz');
|
||||
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`
|
||||
[YAMLException: duplicated mapping key at line 2, column 1:
|
||||
foo: baz
|
||||
^]
|
||||
`);
|
||||
|
||||
expect(mockWriteFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can successfully write CA certificate and elasticsearch config with service token', async () => {
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockWriteFile).toHaveBeenCalledTimes(2);
|
||||
expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content');
|
||||
expect(mockWriteFile).toHaveBeenCalledWith(
|
||||
'/some/path/kibana.yml',
|
||||
`
|
||||
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.serviceAccountToken: some-value
|
||||
elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt]
|
||||
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('can successfully write CA certificate and elasticsearch config with credentials', async () => {
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockWriteFile).toHaveBeenCalledTimes(2);
|
||||
expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content');
|
||||
expect(mockWriteFile).toHaveBeenCalledWith(
|
||||
'/some/path/kibana.yml',
|
||||
`
|
||||
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.password: password
|
||||
elasticsearch.username: username
|
||||
elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt]
|
||||
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('can successfully write elasticsearch config without CA certificate', async () => {
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
host: 'some-host',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockWriteFile).toHaveBeenCalledTimes(1);
|
||||
expect(mockWriteFile).toHaveBeenCalledWith(
|
||||
'/some/path/kibana.yml',
|
||||
`
|
||||
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.password: password
|
||||
elasticsearch.username: username
|
||||
|
||||
`
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockReadFile.mockResolvedValue(
|
||||
'# Default Kibana configuration for docker target\nserver.host: "0.0.0.0"\nserver.shutdownTimeout: "5s"'
|
||||
);
|
||||
});
|
||||
|
||||
describe('with existing config (no conflicts)', () => {
|
||||
beforeEach(() => {
|
||||
mockReadFile.mockResolvedValue(
|
||||
'# Default Kibana configuration for docker target\nserver.host: "0.0.0.0"\nserver.shutdownTimeout: "5s"'
|
||||
);
|
||||
});
|
||||
it('throws if cannot write CA file', async () => {
|
||||
mockWriteFile.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
it('can successfully write CA certificate and elasticsearch config', async () => {
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: '',
|
||||
serviceAccountToken: { name: '', value: '' },
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`);
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledTimes(1);
|
||||
expect(mockReadFile).toHaveBeenCalledWith('/some/path/kibana.yml', 'utf-8');
|
||||
expect(mockWriteFile).toHaveBeenCalledTimes(1);
|
||||
expect(mockWriteFile).toHaveBeenCalledWith('/data/ca_1234.crt', 'ca-content');
|
||||
});
|
||||
|
||||
expect(mockWriteFile).toHaveBeenCalledTimes(2);
|
||||
expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(`
|
||||
it('throws if cannot write config to yaml file', async () => {
|
||||
mockWriteFile.mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`);
|
||||
|
||||
expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Array [
|
||||
"/some/path/ca_1234.crt",
|
||||
"ca-content",
|
||||
],
|
||||
Array [
|
||||
"/some/path/kibana.yml",
|
||||
"# Default Kibana configuration for docker target
|
||||
server.host: \\"0.0.0.0\\"
|
||||
server.shutdownTimeout: \\"5s\\"
|
||||
"/data/ca_1234.crt",
|
||||
"ca-content",
|
||||
],
|
||||
Array [
|
||||
"/some/path/kibana.yml",
|
||||
"# Default Kibana configuration for docker target
|
||||
server.host: \\"0.0.0.0\\"
|
||||
server.shutdownTimeout: \\"5s\\"
|
||||
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.serviceAccountToken: some-value
|
||||
elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt]
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.serviceAccountToken: some-value
|
||||
elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt]
|
||||
|
||||
",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('with existing config (with conflicts)', () => {
|
||||
it('throws if cannot read existing config', async () => {
|
||||
mockReadFile.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`);
|
||||
|
||||
expect(mockWriteFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws if cannot parse existing config', async () => {
|
||||
mockReadFile.mockResolvedValue('foo: bar\nfoo: baz');
|
||||
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`
|
||||
[YAMLException: duplicated mapping key at line 2, column 1:
|
||||
foo: baz
|
||||
^]
|
||||
`);
|
||||
|
||||
expect(mockWriteFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can successfully write CA certificate and elasticsearch config with credentials', async () => {
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/data/ca_1234.crt",
|
||||
"ca-content",
|
||||
],
|
||||
Array [
|
||||
"/some/path/kibana.yml",
|
||||
"# Default Kibana configuration for docker target
|
||||
server.host: \\"0.0.0.0\\"
|
||||
server.shutdownTimeout: \\"5s\\"
|
||||
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.password: password
|
||||
elasticsearch.username: username
|
||||
elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt]
|
||||
|
||||
",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('can successfully write elasticsearch config without CA certificate', async () => {
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
host: 'some-host',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/some/path/kibana.yml",
|
||||
"# Default Kibana configuration for docker target
|
||||
server.host: \\"0.0.0.0\\"
|
||||
server.shutdownTimeout: \\"5s\\"
|
||||
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.password: password
|
||||
elasticsearch.username: username
|
||||
|
||||
",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('can successfully write CA certificate and elasticsearch config with service token', async () => {
|
||||
await expect(
|
||||
kibanaConfigWriter.writeConfig({
|
||||
caCert: 'ca-content',
|
||||
host: 'some-host',
|
||||
serviceAccountToken: { name: 'some-token', value: 'some-value' },
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledTimes(1);
|
||||
expect(mockReadFile).toHaveBeenCalledWith('/some/path/kibana.yml', 'utf-8');
|
||||
|
||||
expect(mockWriteFile).toHaveBeenCalledTimes(2);
|
||||
expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/data/ca_1234.crt",
|
||||
"ca-content",
|
||||
],
|
||||
Array [
|
||||
"/some/path/kibana.yml",
|
||||
"# Default Kibana configuration for docker target
|
||||
server.host: \\"0.0.0.0\\"
|
||||
server.shutdownTimeout: \\"5s\\"
|
||||
|
||||
# This section was automatically generated during setup.
|
||||
elasticsearch.hosts: [some-host]
|
||||
elasticsearch.serviceAccountToken: some-value
|
||||
elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt]
|
||||
|
||||
",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('with conflicts', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Date.prototype, 'toISOString').mockReturnValue('some date');
|
||||
mockReadFile.mockResolvedValue(
|
||||
|
@ -291,7 +281,7 @@ elasticsearch.username: username
|
|||
expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/some/path/ca_1234.crt",
|
||||
"/data/ca_1234.crt",
|
||||
"ca-content",
|
||||
],
|
||||
Array [
|
||||
|
@ -312,7 +302,7 @@ elasticsearch.username: username
|
|||
elasticsearch.hosts: [some-host]
|
||||
monitoring.ui.container.elasticsearch.enabled: true
|
||||
elasticsearch.serviceAccountToken: some-value
|
||||
elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt]
|
||||
elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt]
|
||||
|
||||
",
|
||||
],
|
||||
|
|
|
@ -31,24 +31,23 @@ export type WriteConfigParameters = {
|
|||
);
|
||||
|
||||
export class KibanaConfigWriter {
|
||||
constructor(private readonly configPath: string, private readonly logger: Logger) {}
|
||||
constructor(
|
||||
private readonly configPath: string,
|
||||
private readonly dataDirectoryPath: string,
|
||||
private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks if we can write to the Kibana configuration file and configuration directory.
|
||||
* Checks if we can write to the Kibana configuration file and data directory.
|
||||
*/
|
||||
public async isConfigWritable() {
|
||||
try {
|
||||
// We perform two separate checks here:
|
||||
// 1. If we can write to config directory to add a new CA certificate file and potentially Kibana configuration
|
||||
// file if it doesn't exist for some reason.
|
||||
// 1. If we can write to data directory to add a new CA certificate file.
|
||||
// 2. If we can write to the Kibana configuration file if it exists.
|
||||
const canWriteToConfigDirectory = fs.access(path.dirname(this.configPath), constants.W_OK);
|
||||
await Promise.all([
|
||||
canWriteToConfigDirectory,
|
||||
fs.access(this.configPath, constants.F_OK).then(
|
||||
() => fs.access(this.configPath, constants.W_OK),
|
||||
() => canWriteToConfigDirectory
|
||||
),
|
||||
fs.access(this.dataDirectoryPath, constants.W_OK),
|
||||
fs.access(this.configPath, constants.W_OK),
|
||||
]);
|
||||
return true;
|
||||
} catch {
|
||||
|
@ -61,7 +60,7 @@ export class KibanaConfigWriter {
|
|||
* @param params
|
||||
*/
|
||||
public async writeConfig(params: WriteConfigParameters) {
|
||||
const caPath = path.join(path.dirname(this.configPath), `ca_${Date.now()}.crt`);
|
||||
const caPath = path.join(this.dataDirectoryPath, `ca_${Date.now()}.crt`);
|
||||
const config: Record<string, string | string[]> = { 'elasticsearch.hosts': [params.host] };
|
||||
if ('serviceAccountToken' in params) {
|
||||
config['elasticsearch.serviceAccountToken'] = params.serviceAccountToken.value;
|
||||
|
|
|
@ -10,6 +10,7 @@ import chalk from 'chalk';
|
|||
import type { Subscription } from 'rxjs';
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { getDataPath } from '@kbn/utils';
|
||||
import type { CorePreboot, Logger, PluginInitializerContext, PrebootPlugin } from 'src/core/server';
|
||||
|
||||
import { ElasticsearchConnectionStatus } from '../common';
|
||||
|
@ -146,7 +147,11 @@ Go to ${chalk.cyanBright.underline(url)} to get started.
|
|||
basePath: core.http.basePath,
|
||||
logger: this.#logger.get('routes'),
|
||||
preboot: { ...core.preboot, completeSetup },
|
||||
kibanaConfigWriter: new KibanaConfigWriter(configPath, this.#logger.get('kibana-config')),
|
||||
kibanaConfigWriter: new KibanaConfigWriter(
|
||||
configPath,
|
||||
getDataPath(),
|
||||
this.#logger.get('kibana-config')
|
||||
),
|
||||
elasticsearch,
|
||||
verificationCode,
|
||||
getConfig: this.#getConfig.bind(this),
|
||||
|
|
Loading…
Reference in a new issue