[8.8] Fix config stacking order (#158827) (#159025)

# Backport

This will backport the following commits from `main` to `8.8`:
- [Fix config stacking order
(#158827)](https://github.com/elastic/kibana/pull/158827)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Alex
Szabo","email":"alex.szabo@elastic.co"},"sourceCommit":{"committedDate":"2023-06-05T13:15:07Z","message":"Fix
config stacking order (#158827)\n\n## Summary\r\nFixes: #155154
(introduced in #149878), builds on #155436 .\r\n\r\n- Adds tests to
ensure the configuration merging order, check those
for\r\nreference.\r\n- Updates the README to explain the intention\r\n
\r\nFor the tests, I needed to output something to the logs. I hope it's
not\r\na big issue to log it. If needed, I might hide that behind a
verbose- or\r\nfeature flag.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"c57589ec57e5e8265a66cd9c8c2102005736f6d8","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Operations","release_note:fix","backport:prev-minor","v8.9.0"],"number":158827,"url":"https://github.com/elastic/kibana/pull/158827","mergeCommit":{"message":"Fix
config stacking order (#158827)\n\n## Summary\r\nFixes: #155154
(introduced in #149878), builds on #155436 .\r\n\r\n- Adds tests to
ensure the configuration merging order, check those
for\r\nreference.\r\n- Updates the README to explain the intention\r\n
\r\nFor the tests, I needed to output something to the logs. I hope it's
not\r\na big issue to log it. If needed, I might hide that behind a
verbose- or\r\nfeature flag.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"c57589ec57e5e8265a66cd9c8c2102005736f6d8"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/158827","number":158827,"mergeCommit":{"message":"Fix
config stacking order (#158827)\n\n## Summary\r\nFixes: #155154
(introduced in #149878), builds on #155436 .\r\n\r\n- Adds tests to
ensure the configuration merging order, check those
for\r\nreference.\r\n- Updates the README to explain the intention\r\n
\r\nFor the tests, I needed to output something to the logs. I hope it's
not\r\na big issue to log it. If needed, I might hide that behind a
verbose- or\r\nfeature flag.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine
<42973632+kibanamachine@users.noreply.github.com>","sha":"c57589ec57e5e8265a66cd9c8c2102005736f6d8"}}]}]
BACKPORT-->

---------

Co-authored-by: Alex Szabo <alex.szabo@elastic.co>
This commit is contained in:
Kibana Machine 2023-06-05 14:05:34 -04:00 committed by GitHub
parent 9b343b4a78
commit 419cd20b4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 261 additions and 5 deletions

View file

@ -5,9 +5,12 @@ this configuration, pass `--serverless={mode}` or run `yarn serverless-{mode}`
valid modes are currently: `es`, `oblt`, and `security`
configuration is applied in the following order, later values override
1. kibana.yml
2. serverless.yml
3. serverless.{mode}.yml
1. serverless.yml (serverless configs go first)
2. serverless.{mode}.yml (serverless configs go first)
3. base config, in this preference order:
- my-config.yml(s) (set by --config)
- env-config.yml (described by `env.KBN_CONFIG_PATHS`)
- kibana.yml (default @ `env.KBN_PATH_CONF`/kibana.yml)
4. kibana.dev.yml
5. serverless.dev.yml
6. serverless.{mode}.dev.yml

View file

@ -21,5 +21,6 @@ jest.doMock('@kbn/config', () => ({
jest.doMock('./root', () => ({
Root: jest.fn(() => ({
shutdown: jest.fn(),
logger: { get: () => ({ info: jest.fn(), debug: jest.fn() }) },
})),
}));

View file

@ -78,6 +78,9 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
}
const root = new Root(rawConfigService, env, onRootShutdown);
const cliLogger = root.logger.get('cli');
cliLogger.debug('Kibana configurations evaluated in this order: ' + env.configs.join(', '));
process.on('SIGHUP', () => reloadConfiguration());
@ -93,7 +96,6 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
});
function reloadConfiguration(reason = 'SIGHUP signal received') {
const cliLogger = root.logger.get('cli');
cliLogger.info(`Reloading Kibana configuration (reason: ${reason}).`, { tags: ['config'] });
try {

View file

@ -0,0 +1,243 @@
/*
* 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 * as Fs from 'fs';
import * as Path from 'path';
import * as Os from 'os';
import * as Child from 'child_process';
import Del from 'del';
import * as Rx from 'rxjs';
import { filter, map, take, timeout } from 'rxjs/operators';
const tempDir = Path.join(Os.tmpdir(), 'kbn-config-test');
const kibanaPath = follow('../../../../scripts/kibana.js');
const TIMEOUT_MS = 20000;
const envForTempDir = {
env: { KBN_PATH_CONF: tempDir },
};
const TestFiles = {
fileList: [] as string[],
createEmptyConfigFiles(fileNames: string[], root: string = tempDir): string[] {
const configFiles = [];
for (const fileName of fileNames) {
const filePath = Path.resolve(root, fileName);
if (!Fs.existsSync(filePath)) {
Fs.writeFileSync(filePath, 'dummy');
TestFiles.fileList.push(filePath);
}
configFiles.push(filePath);
}
return configFiles;
},
cleanUpEmptyConfigFiles() {
for (const filePath of TestFiles.fileList) {
Del.sync(filePath);
}
TestFiles.fileList.length = 0;
},
};
describe('Server configuration ordering', () => {
let kibanaProcess: Child.ChildProcessWithoutNullStreams;
beforeEach(() => {
Fs.mkdirSync(tempDir, { recursive: true });
});
afterEach(async () => {
if (kibanaProcess !== undefined) {
const exitPromise = new Promise((resolve) => kibanaProcess?.once('exit', resolve));
kibanaProcess.kill('SIGKILL');
await exitPromise;
}
Del.sync(tempDir, { force: true });
TestFiles.cleanUpEmptyConfigFiles();
});
it('loads default config set without any options', async function () {
TestFiles.createEmptyConfigFiles(['kibana.yml']);
kibanaProcess = Child.spawn(process.execPath, [kibanaPath, '--verbose'], envForTempDir);
const configList = await extractConfigurationOrder(kibanaProcess);
expect(configList).toEqual(['kibana.yml']);
});
it('loads serverless configs when --serverless is set', async () => {
TestFiles.createEmptyConfigFiles([
'serverless.yml',
'serverless.oblt.yml',
'kibana.yml',
'serverless.recent.yml',
]);
kibanaProcess = Child.spawn(
process.execPath,
[kibanaPath, '--verbose', '--serverless', 'oblt'],
envForTempDir
);
const configList = await extractConfigurationOrder(kibanaProcess);
expect(configList).toEqual([
'serverless.yml',
'serverless.oblt.yml',
'kibana.yml',
'serverless.recent.yml',
]);
});
it('prefers --config options over default', async () => {
const [configPath] = TestFiles.createEmptyConfigFiles([
'potato.yml',
'serverless.yml',
'serverless.oblt.yml',
'kibana.yml',
'serverless.recent.yml',
]);
kibanaProcess = Child.spawn(
process.execPath,
[kibanaPath, '--verbose', '--serverless', 'oblt', '--config', configPath],
envForTempDir
);
const configList = await extractConfigurationOrder(kibanaProcess);
expect(configList).toEqual([
'serverless.yml',
'serverless.oblt.yml',
'potato.yml',
'serverless.recent.yml',
]);
});
it('defaults to "es" if --serverless and --dev are there', async () => {
TestFiles.createEmptyConfigFiles([
'serverless.yml',
'serverless.es.yml',
'kibana.yml',
'kibana.dev.yml',
'serverless.dev.yml',
]);
kibanaProcess = Child.spawn(
process.execPath,
[kibanaPath, '--verbose', '--serverless', '--dev'],
envForTempDir
);
const configList = await extractConfigurationOrder(kibanaProcess);
expect(configList).toEqual([
'serverless.yml',
'serverless.es.yml',
'kibana.yml',
'serverless.recent.yml',
'kibana.dev.yml',
'serverless.dev.yml',
]);
});
it('adds dev configs to the stack', async () => {
TestFiles.createEmptyConfigFiles([
'serverless.yml',
'serverless.security.yml',
'serverless.recent.yml',
'kibana.yml',
'kibana.dev.yml',
'serverless.dev.yml',
]);
kibanaProcess = Child.spawn(
process.execPath,
[kibanaPath, '--verbose', '--serverless', 'security', '--dev'],
envForTempDir
);
const configList = await extractConfigurationOrder(kibanaProcess);
expect(configList).toEqual([
'serverless.yml',
'serverless.security.yml',
'kibana.yml',
'serverless.recent.yml',
'kibana.dev.yml',
'serverless.dev.yml',
]);
});
});
async function extractConfigurationOrder(
proc: Child.ChildProcessWithoutNullStreams
): Promise<string[] | undefined> {
const configMessage = await waitForMessage(proc, /[Cc]onfig.*order:/, TIMEOUT_MS);
const configList = configMessage
.match(/order: (.*)$/)
?.at(1)
?.split(', ')
?.map((path) => Path.basename(path));
return configList;
}
async function waitForMessage(
proc: Child.ChildProcessWithoutNullStreams,
expression: string | RegExp,
timeoutMs: number
): Promise<string> {
const message$ = Rx.fromEvent(proc.stdout!, 'data').pipe(
map((messages) => String(messages).split('\n').filter(Boolean))
);
const trackedExpression$ = message$.pipe(
// We know the sighup handler will be registered before this message logged
filter((messages: string[]) => messages.some((m) => m.match(expression))),
take(1)
);
const error$ = message$.pipe(
filter((messages: string[]) => messages.some((line) => line.match(/fatal/i))),
take(1),
map((line) => new Error(line.join('\n')))
);
const value = await Rx.firstValueFrom(
Rx.race(trackedExpression$, error$).pipe(
timeout({
first: timeoutMs,
with: () =>
Rx.throwError(
() => new Error(`Config options didn't appear in logs for ${timeoutMs / 1000}s...`)
),
})
)
);
if (value instanceof Error) {
throw value;
}
if (Array.isArray(value)) {
return value[0];
} else {
return value;
}
}
function follow(file: string) {
return Path.relative(process.cwd(), Path.resolve(__dirname, file));
}

View file

@ -20,6 +20,8 @@ import { readKeystore } from '../keystore/read_keystore';
/** @type {ServerlessProjectMode[]} */
const VALID_SERVERLESS_PROJECT_MODE = ['es', 'oblt', 'security'];
const isNotEmpty = _.negate(_.isEmpty);
/**
* @param {Record<string, unknown>} opts
* @returns {ServerlessProjectMode | true | null}
@ -311,8 +313,13 @@ export default function (program) {
}
command.action(async function (opts) {
const cliConfigs = opts.config || [];
const envConfigs = getEnvConfigs();
const defaultConfig = getConfigPath();
const configs = [cliConfigs, envConfigs, [defaultConfig]].find(isNotEmpty);
const unknownOptions = this.getUnknownOptions();
const configs = [getConfigPath(), ...getEnvConfigs(), ...(opts.config || [])];
const serverlessMode = getServerlessProjectMode(opts);
if (serverlessMode) {