Skip to content

Commit c57589e

Browse files
Fix config stacking order (#158827)
## Summary Fixes: #155154 (introduced in #149878), builds on #155436 . - Adds tests to ensure the configuration merging order, check those for reference. - Updates the README to explain the intention For the tests, I needed to output something to the logs. I hope it's not a big issue to log it. If needed, I might hide that behind a verbose- or feature flag. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent fd068da commit c57589e

File tree

5 files changed

+260
-5
lines changed

5 files changed

+260
-5
lines changed

config/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ this configuration, pass `--serverless={mode}` or run `yarn serverless-{mode}`
55
valid modes are currently: `es`, `oblt`, and `security`
66

77
configuration is applied in the following order, later values override
8-
1. kibana.yml
9-
2. serverless.yml
10-
3. serverless.{mode}.yml
8+
1. serverless.yml (serverless configs go first)
9+
2. serverless.{mode}.yml (serverless configs go first)
10+
3. base config, in this preference order:
11+
- my-config.yml(s) (set by --config)
12+
- env-config.yml (described by `env.KBN_CONFIG_PATHS`)
13+
- kibana.yml (default @ `env.KBN_PATH_CONF`/kibana.yml)
1114
4. kibana.dev.yml
1215
5. serverless.dev.yml
1316
6. serverless.{mode}.dev.yml

packages/core/root/core-root-server-internal/src/bootstrap.test.mocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ jest.doMock('@kbn/config', () => ({
2121
jest.doMock('./root', () => ({
2222
Root: jest.fn(() => ({
2323
shutdown: jest.fn(),
24+
logger: { get: () => ({ info: jest.fn(), debug: jest.fn() }) },
2425
})),
2526
}));

packages/core/root/core-root-server-internal/src/bootstrap.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
7878
}
7979

8080
const root = new Root(rawConfigService, env, onRootShutdown);
81+
const cliLogger = root.logger.get('cli');
82+
83+
cliLogger.debug('Kibana configurations evaluated in this order: ' + env.configs.join(', '));
8184

8285
process.on('SIGHUP', () => reloadConfiguration());
8386

@@ -93,7 +96,6 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
9396
});
9497

9598
function reloadConfiguration(reason = 'SIGHUP signal received') {
96-
const cliLogger = root.logger.get('cli');
9799
cliLogger.info(`Reloading Kibana configuration (reason: ${reason}).`, { tags: ['config'] });
98100

99101
try {
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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 * as Fs from 'fs';
10+
import * as Path from 'path';
11+
import * as Os from 'os';
12+
import * as Child from 'child_process';
13+
import Del from 'del';
14+
import * as Rx from 'rxjs';
15+
import { filter, map, take, timeout } from 'rxjs/operators';
16+
17+
const tempDir = Path.join(Os.tmpdir(), 'kbn-config-test');
18+
19+
const kibanaPath = follow('../../../../scripts/kibana.js');
20+
21+
const TIMEOUT_MS = 20000;
22+
23+
const envForTempDir = {
24+
env: { KBN_PATH_CONF: tempDir },
25+
};
26+
27+
const TestFiles = {
28+
fileList: [] as string[],
29+
30+
createEmptyConfigFiles(fileNames: string[], root: string = tempDir): string[] {
31+
const configFiles = [];
32+
for (const fileName of fileNames) {
33+
const filePath = Path.resolve(root, fileName);
34+
35+
if (!Fs.existsSync(filePath)) {
36+
Fs.writeFileSync(filePath, 'dummy');
37+
38+
TestFiles.fileList.push(filePath);
39+
}
40+
41+
configFiles.push(filePath);
42+
}
43+
44+
return configFiles;
45+
},
46+
cleanUpEmptyConfigFiles() {
47+
for (const filePath of TestFiles.fileList) {
48+
Del.sync(filePath);
49+
}
50+
TestFiles.fileList.length = 0;
51+
},
52+
};
53+
54+
describe('Server configuration ordering', () => {
55+
let kibanaProcess: Child.ChildProcessWithoutNullStreams;
56+
57+
beforeEach(() => {
58+
Fs.mkdirSync(tempDir, { recursive: true });
59+
});
60+
61+
afterEach(async () => {
62+
if (kibanaProcess !== undefined) {
63+
const exitPromise = new Promise((resolve) => kibanaProcess?.once('exit', resolve));
64+
kibanaProcess.kill('SIGKILL');
65+
await exitPromise;
66+
}
67+
68+
Del.sync(tempDir, { force: true });
69+
TestFiles.cleanUpEmptyConfigFiles();
70+
});
71+
72+
it('loads default config set without any options', async function () {
73+
TestFiles.createEmptyConfigFiles(['kibana.yml']);
74+
75+
kibanaProcess = Child.spawn(process.execPath, [kibanaPath, '--verbose'], envForTempDir);
76+
const configList = await extractConfigurationOrder(kibanaProcess);
77+
78+
expect(configList).toEqual(['kibana.yml']);
79+
});
80+
81+
it('loads serverless configs when --serverless is set', async () => {
82+
TestFiles.createEmptyConfigFiles([
83+
'serverless.yml',
84+
'serverless.oblt.yml',
85+
'kibana.yml',
86+
'serverless.recent.yml',
87+
]);
88+
89+
kibanaProcess = Child.spawn(
90+
process.execPath,
91+
[kibanaPath, '--verbose', '--serverless', 'oblt'],
92+
envForTempDir
93+
);
94+
const configList = await extractConfigurationOrder(kibanaProcess);
95+
96+
expect(configList).toEqual([
97+
'serverless.yml',
98+
'serverless.oblt.yml',
99+
'kibana.yml',
100+
'serverless.recent.yml',
101+
]);
102+
});
103+
104+
it('prefers --config options over default', async () => {
105+
const [configPath] = TestFiles.createEmptyConfigFiles([
106+
'potato.yml',
107+
'serverless.yml',
108+
'serverless.oblt.yml',
109+
'kibana.yml',
110+
'serverless.recent.yml',
111+
]);
112+
113+
kibanaProcess = Child.spawn(
114+
process.execPath,
115+
[kibanaPath, '--verbose', '--serverless', 'oblt', '--config', configPath],
116+
envForTempDir
117+
);
118+
const configList = await extractConfigurationOrder(kibanaProcess);
119+
120+
expect(configList).toEqual([
121+
'serverless.yml',
122+
'serverless.oblt.yml',
123+
'potato.yml',
124+
'serverless.recent.yml',
125+
]);
126+
});
127+
128+
it('defaults to "es" if --serverless and --dev are there', async () => {
129+
TestFiles.createEmptyConfigFiles([
130+
'serverless.yml',
131+
'serverless.es.yml',
132+
'kibana.yml',
133+
'kibana.dev.yml',
134+
'serverless.dev.yml',
135+
]);
136+
137+
kibanaProcess = Child.spawn(
138+
process.execPath,
139+
[kibanaPath, '--verbose', '--serverless', '--dev'],
140+
envForTempDir
141+
);
142+
const configList = await extractConfigurationOrder(kibanaProcess);
143+
144+
expect(configList).toEqual([
145+
'serverless.yml',
146+
'serverless.es.yml',
147+
'kibana.yml',
148+
'serverless.recent.yml',
149+
'kibana.dev.yml',
150+
'serverless.dev.yml',
151+
]);
152+
});
153+
154+
it('adds dev configs to the stack', async () => {
155+
TestFiles.createEmptyConfigFiles([
156+
'serverless.yml',
157+
'serverless.security.yml',
158+
'kibana.yml',
159+
'kibana.dev.yml',
160+
'serverless.dev.yml',
161+
]);
162+
163+
kibanaProcess = Child.spawn(
164+
process.execPath,
165+
[kibanaPath, '--verbose', '--serverless', 'security', '--dev'],
166+
envForTempDir
167+
);
168+
169+
const configList = await extractConfigurationOrder(kibanaProcess);
170+
171+
expect(configList).toEqual([
172+
'serverless.yml',
173+
'serverless.security.yml',
174+
'kibana.yml',
175+
'serverless.recent.yml',
176+
'kibana.dev.yml',
177+
'serverless.dev.yml',
178+
]);
179+
});
180+
});
181+
182+
async function extractConfigurationOrder(
183+
proc: Child.ChildProcessWithoutNullStreams
184+
): Promise<string[] | undefined> {
185+
const configMessage = await waitForMessage(proc, /[Cc]onfig.*order:/, TIMEOUT_MS);
186+
187+
const configList = configMessage
188+
.match(/order: (.*)$/)
189+
?.at(1)
190+
?.split(', ')
191+
?.map((path) => Path.basename(path));
192+
193+
return configList;
194+
}
195+
196+
async function waitForMessage(
197+
proc: Child.ChildProcessWithoutNullStreams,
198+
expression: string | RegExp,
199+
timeoutMs: number
200+
): Promise<string> {
201+
const message$ = Rx.fromEvent(proc.stdout!, 'data').pipe(
202+
map((messages) => String(messages).split('\n').filter(Boolean))
203+
);
204+
205+
const trackedExpression$ = message$.pipe(
206+
// We know the sighup handler will be registered before this message logged
207+
filter((messages: string[]) => messages.some((m) => m.match(expression))),
208+
take(1)
209+
);
210+
211+
const error$ = message$.pipe(
212+
filter((messages: string[]) => messages.some((line) => line.match(/fatal/i))),
213+
take(1),
214+
map((line) => new Error(line.join('\n')))
215+
);
216+
217+
const value = await Rx.firstValueFrom(
218+
Rx.race(trackedExpression$, error$).pipe(
219+
timeout({
220+
first: timeoutMs,
221+
with: () =>
222+
Rx.throwError(
223+
() => new Error(`Config options didn't appear in logs for ${timeoutMs / 1000}s...`)
224+
),
225+
})
226+
)
227+
);
228+
229+
if (value instanceof Error) {
230+
throw value;
231+
}
232+
233+
if (Array.isArray(value)) {
234+
return value[0];
235+
} else {
236+
return value;
237+
}
238+
}
239+
240+
function follow(file: string) {
241+
return Path.relative(process.cwd(), Path.resolve(__dirname, file));
242+
}

src/cli/serve/serve.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { readKeystore } from '../keystore/read_keystore';
2020
/** @type {ServerlessProjectMode[]} */
2121
const VALID_SERVERLESS_PROJECT_MODE = ['es', 'oblt', 'security'];
2222

23+
const isNotEmpty = _.negate(_.isEmpty);
24+
2325
/**
2426
* @param {Record<string, unknown>} opts
2527
* @returns {ServerlessProjectMode | true | null}
@@ -305,8 +307,13 @@ export default function (program) {
305307
}
306308

307309
command.action(async function (opts) {
310+
const cliConfigs = opts.config || [];
311+
const envConfigs = getEnvConfigs();
312+
const defaultConfig = getConfigPath();
313+
314+
const configs = [cliConfigs, envConfigs, [defaultConfig]].find(isNotEmpty);
315+
308316
const unknownOptions = this.getUnknownOptions();
309-
const configs = [getConfigPath(), ...getEnvConfigs(), ...(opts.config || [])];
310317
const serverlessMode = getServerlessProjectMode(opts);
311318

312319
if (serverlessMode) {

0 commit comments

Comments
 (0)