Skip to content

Commit e3c736d

Browse files
authored
Merge branch '8.19' into backport/8.19/pr-228707
2 parents 07929d0 + 18f8add commit e3c736d

8 files changed

Lines changed: 207 additions & 50 deletions

File tree

src/platform/plugins/shared/console/public/application/hooks/use_send_current_request/send_request.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,21 @@ const extractStatusCodeAndText = (response: Response | undefined, path: string)
5050
// For ES requests, we need to extract the status code and text from the response
5151
// headers, due to the way the proxy set up to avoid mirroring the status code which could be 401
5252
// and trigger a login prompt. See for more details: https://github.com/elastic/kibana/issues/140536
53-
const statusCode = parseInt(response?.headers.get('x-console-proxy-status-code') ?? '500', 10);
54-
const statusText = response?.headers.get('x-console-proxy-status-text') ?? 'error';
53+
const proxyStatusCode = response?.headers.get('x-console-proxy-status-code');
54+
const proxyStatusText = response?.headers.get('x-console-proxy-status-text');
5555

56-
return { statusCode, statusText };
56+
// If proxy headers are missing (e.g., validation errors), use the actual response status
57+
if (!proxyStatusCode) {
58+
return {
59+
statusCode: parseInt(String(response?.status ?? 500), 10),
60+
statusText: response?.statusText ?? 'error',
61+
};
62+
}
63+
64+
return {
65+
statusCode: parseInt(proxyStatusCode, 10),
66+
statusText: proxyStatusText ?? 'error',
67+
};
5768
};
5869

5970
let CURRENT_REQ_ID = 0;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { HttpSetup } from '@kbn/core/public';
11+
12+
jest.unmock('./send_request');
13+
14+
describe('Status Code Extraction in sendRequest', () => {
15+
let mockHttp: jest.Mocked<HttpSetup>;
16+
17+
beforeEach(() => {
18+
mockHttp = {
19+
post: jest.fn(),
20+
} as any;
21+
});
22+
23+
afterEach(() => {
24+
jest.clearAllMocks();
25+
});
26+
27+
describe('extractStatusCodeAndText function behavior', () => {
28+
it('should use proxy headers when available for ES requests', async () => {
29+
const { sendRequest } = await import('./send_request');
30+
31+
const mockResponse = {
32+
response: {
33+
status: 200, // Actual HTTP status
34+
statusText: 'OK',
35+
headers: new Map([
36+
['x-console-proxy-status-code', '404'], // ES status from proxy
37+
['x-console-proxy-status-text', 'Not Found'],
38+
['Content-Type', 'application/json'],
39+
]),
40+
},
41+
body: JSON.stringify({ error: 'index_not_found_exception' }),
42+
};
43+
44+
mockHttp.post.mockResolvedValue(mockResponse);
45+
46+
const result = await sendRequest({
47+
http: mockHttp,
48+
requests: [{ url: '/_search', method: 'GET', data: ['{}'] }],
49+
});
50+
51+
expect(result[0].response.statusCode).toBe(404); // Should use proxy header
52+
expect(result[0].response.statusText).toBe('Not Found');
53+
});
54+
55+
it('should fall back to actual response status when proxy headers are missing', async () => {
56+
const { sendRequest } = await import('./send_request');
57+
58+
const mockResponse = {
59+
response: {
60+
status: 400,
61+
statusText: 'Bad Request',
62+
headers: new Map([['Content-Type', 'application/json']]),
63+
},
64+
body: JSON.stringify({
65+
statusCode: 400,
66+
error: 'Bad Request',
67+
message:
68+
"Method must be one of, case insensitive ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH']. Received 'INVALIDMETHOD'.",
69+
}),
70+
};
71+
72+
mockHttp.post.mockResolvedValue(mockResponse);
73+
74+
const result = await sendRequest({
75+
http: mockHttp,
76+
requests: [{ url: '/_search', method: 'INVALIDMETHOD', data: ['{}'] }],
77+
});
78+
79+
expect(result[0].response.statusCode).toBe(400); // Should use actual response status, not 500
80+
expect(result[0].response.statusText).toBe('Bad Request');
81+
});
82+
83+
it('should handle empty proxy header as missing header', async () => {
84+
const { sendRequest } = await import('./send_request');
85+
86+
const mockResponse = {
87+
response: {
88+
status: 400,
89+
statusText: 'Bad Request',
90+
headers: new Map([
91+
['x-console-proxy-status-code', ''], // Empty header value
92+
['Content-Type', 'application/json'],
93+
]),
94+
},
95+
body: JSON.stringify({ error: 'validation error' }),
96+
};
97+
98+
mockHttp.post.mockResolvedValue(mockResponse);
99+
100+
const result = await sendRequest({
101+
http: mockHttp,
102+
requests: [{ url: '/_search', method: 'INVALID', data: ['{}'] }],
103+
});
104+
105+
expect(result[0].response.statusCode).toBe(400); // Should fall back to actual status
106+
expect(result[0].response.statusText).toBe('Bad Request');
107+
});
108+
});
109+
});

x-pack/platform/plugins/shared/alerting/server/lib/rule_gaps/update/apply_scheduled_backfills_to_gap.test.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ const gap = new Gap({
5252
},
5353
});
5454

55+
const testToHaveBeenCalledBefore = (
56+
calledFirst: jest.Mock,
57+
calledSecond: jest.Mock,
58+
timesCalled = 1
59+
) => {
60+
const calledFirstOrder = calledFirst.mock.invocationCallOrder;
61+
const calledSecondOrder = calledSecond.mock.invocationCallOrder;
62+
63+
expect(calledFirstOrder).toHaveLength(timesCalled);
64+
expect(calledSecondOrder).toHaveLength(timesCalled);
65+
66+
calledFirstOrder.forEach((order, idx) => {
67+
expect(order).toBeLessThan(calledSecondOrder[idx]);
68+
});
69+
};
70+
5571
describe('applyScheduledBackfillsToGap', () => {
5672
beforeEach(() => {
5773
jest.clearAllMocks();
@@ -68,6 +84,8 @@ describe('applyScheduledBackfillsToGap', () => {
6884
ruleId,
6985
});
7086

87+
expect(updateGapFromScheduleMock).toHaveBeenCalledWith({ gap, scheduledItems: [] });
88+
7189
expect(calculateGapStateFromAllBackfillsMock).toHaveBeenCalledWith({
7290
gap,
7391
savedObjectsRepository,
@@ -76,7 +94,8 @@ describe('applyScheduledBackfillsToGap', () => {
7694
actionsClient,
7795
logger: mockLogger,
7896
});
79-
expect(updateGapFromScheduleMock).not.toHaveBeenCalled();
97+
98+
testToHaveBeenCalledBefore(updateGapFromScheduleMock, calculateGapStateFromAllBackfillsMock);
8099
});
81100

82101
test('when there is a scheduled item with an errored task', async () => {
@@ -96,6 +115,11 @@ describe('applyScheduledBackfillsToGap', () => {
96115
ruleId,
97116
});
98117

118+
expect(updateGapFromScheduleMock).toHaveBeenCalledWith({
119+
gap,
120+
scheduledItems: scheduledItemsWithFailedTask,
121+
});
122+
99123
expect(calculateGapStateFromAllBackfillsMock).toHaveBeenCalledWith({
100124
gap,
101125
savedObjectsRepository,
@@ -104,7 +128,8 @@ describe('applyScheduledBackfillsToGap', () => {
104128
actionsClient,
105129
logger: mockLogger,
106130
});
107-
expect(updateGapFromScheduleMock).not.toHaveBeenCalled();
131+
132+
testToHaveBeenCalledBefore(updateGapFromScheduleMock, calculateGapStateFromAllBackfillsMock);
108133
});
109134

110135
test('when there is a scheduled item with a task that timed out', async () => {
@@ -124,6 +149,11 @@ describe('applyScheduledBackfillsToGap', () => {
124149
ruleId,
125150
});
126151

152+
expect(updateGapFromScheduleMock).toHaveBeenCalledWith({
153+
gap,
154+
scheduledItems: scheduledItemsWithFailedTask,
155+
});
156+
127157
expect(calculateGapStateFromAllBackfillsMock).toHaveBeenCalledWith({
128158
gap,
129159
savedObjectsRepository,
@@ -132,7 +162,8 @@ describe('applyScheduledBackfillsToGap', () => {
132162
actionsClient,
133163
logger: mockLogger,
134164
});
135-
expect(updateGapFromScheduleMock).not.toHaveBeenCalled();
165+
166+
testToHaveBeenCalledBefore(updateGapFromScheduleMock, calculateGapStateFromAllBackfillsMock);
136167
});
137168

138169
test('when shouldRefetchAllBackfills is true', async () => {
@@ -147,6 +178,8 @@ describe('applyScheduledBackfillsToGap', () => {
147178
shouldRefetchAllBackfills: true,
148179
});
149180

181+
expect(updateGapFromScheduleMock).toHaveBeenCalledWith({ gap, scheduledItems });
182+
150183
expect(calculateGapStateFromAllBackfillsMock).toHaveBeenCalledWith({
151184
gap,
152185
savedObjectsRepository,
@@ -155,7 +188,8 @@ describe('applyScheduledBackfillsToGap', () => {
155188
actionsClient,
156189
logger: mockLogger,
157190
});
158-
expect(updateGapFromScheduleMock).not.toHaveBeenCalled();
191+
192+
testToHaveBeenCalledBefore(updateGapFromScheduleMock, calculateGapStateFromAllBackfillsMock);
159193
});
160194
});
161195

@@ -171,8 +205,8 @@ describe('applyScheduledBackfillsToGap', () => {
171205
ruleId,
172206
});
173207

174-
expect(calculateGapStateFromAllBackfillsMock).not.toHaveBeenCalled();
175208
expect(updateGapFromScheduleMock).toHaveBeenCalledWith({ gap, scheduledItems });
209+
expect(calculateGapStateFromAllBackfillsMock).not.toHaveBeenCalled();
176210
});
177211
});
178212
});

x-pack/platform/plugins/shared/alerting/server/lib/rule_gaps/update/apply_scheduled_backfills_to_gap.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export const applyScheduledBackfillsToGap = async ({
4040
scheduleItem.status === adHocRunStatus.ERROR || scheduleItem.status === adHocRunStatus.TIMEOUT
4141
);
4242

43+
// Although calculateGapStateFromAllBackfills also calls updateGapFromSchedule,
44+
// it's crucial to call updateGapFromSchedule first with the current scheduled items.
45+
// This ensures that if a backfill has been deleted, we still update gaps based on any
46+
// completed scheduled items it contained. Since deleted backfills aren't returned on refetch,
47+
// calculateGapStateFromAllBackfills can't account for them.
48+
updateGapFromSchedule({
49+
gap,
50+
scheduledItems,
51+
});
52+
4353
if (hasFailedBackfillTask || scheduledItems.length === 0 || shouldRefetchAllBackfills) {
4454
await calculateGapStateFromAllBackfills({
4555
gap,
@@ -49,11 +59,5 @@ export const applyScheduledBackfillsToGap = async ({
4959
actionsClient,
5060
logger,
5161
});
52-
return;
5362
}
54-
55-
updateGapFromSchedule({
56-
gap,
57-
scheduledItems,
58-
});
5963
};

x-pack/platform/plugins/shared/index_management/common/lib/component_template_serialization.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,25 +72,27 @@ export function deserializeComponentTemplate(
7272
}
7373

7474
export function deserializeComponentTemplateList(
75-
componentTemplateEs: ComponentTemplateFromEs,
75+
componentTemplatesEs: ComponentTemplateFromEs[],
7676
indexTemplatesEs: TemplateFromEs[]
7777
) {
78-
const { name, component_template: componentTemplate } = componentTemplateEs;
79-
const { template, _meta, deprecated } = componentTemplate;
80-
8178
const indexTemplatesToUsedBy = getIndexTemplatesToUsedBy(indexTemplatesEs);
8279

83-
const componentTemplateListItem: ComponentTemplateListItem = {
84-
name,
85-
usedBy: indexTemplatesToUsedBy[name] || [],
86-
isDeprecated: Boolean(deprecated === true),
87-
isManaged: Boolean(_meta?.managed === true),
88-
hasSettings: hasEntries(template.settings),
89-
hasMappings: hasEntries(template.mappings),
90-
hasAliases: hasEntries(template.aliases),
91-
};
80+
return componentTemplatesEs.map((componentTemplateEs) => {
81+
const { name, component_template: componentTemplate } = componentTemplateEs;
82+
const { template, _meta, deprecated } = componentTemplate;
83+
84+
const componentTemplateListItem: ComponentTemplateListItem = {
85+
name,
86+
usedBy: indexTemplatesToUsedBy[name] || [],
87+
isDeprecated: Boolean(deprecated === true),
88+
isManaged: Boolean(_meta?.managed === true),
89+
hasSettings: hasEntries(template.settings),
90+
hasMappings: hasEntries(template.mappings),
91+
hasAliases: hasEntries(template.aliases),
92+
};
9293

93-
return componentTemplateListItem;
94+
return componentTemplateListItem;
95+
});
9496
}
9597

9698
export function serializeComponentTemplate(

x-pack/platform/plugins/shared/index_management/server/routes/api/component_templates/register_get_route.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,17 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep
3636
const { client } = (await context.core).elasticsearch;
3737

3838
try {
39-
const { component_templates: componentTemplates } =
40-
await client.asCurrentUser.cluster.getComponentTemplate();
39+
const [{ component_templates: componentTemplates }, { index_templates: indexTemplates }] =
40+
await Promise.all([
41+
client.asCurrentUser.cluster.getComponentTemplate(),
42+
client.asCurrentUser.indices.getIndexTemplate(),
43+
]);
4144

42-
const { index_templates: indexTemplates } =
43-
await client.asCurrentUser.indices.getIndexTemplate();
44-
45-
const body = componentTemplates.map((componentTemplate) => {
46-
const deserializedComponentTemplateListItem = deserializeComponentTemplateList(
47-
componentTemplate as ComponentTemplateFromEs,
48-
// @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns
49-
indexTemplates
50-
);
51-
return deserializedComponentTemplateListItem;
52-
});
45+
const body = deserializeComponentTemplateList(
46+
componentTemplates as ComponentTemplateFromEs[],
47+
// @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns
48+
indexTemplates
49+
);
5350

5451
return response.ok({ body });
5552
} catch (error) {
@@ -77,13 +74,11 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep
7774
const { name } = request.params;
7875

7976
try {
80-
const { component_templates: componentTemplates } =
81-
await client.asCurrentUser.cluster.getComponentTemplate({
82-
name,
83-
});
84-
85-
const { index_templates: indexTemplates } =
86-
await client.asCurrentUser.indices.getIndexTemplate();
77+
const [{ component_templates: componentTemplates }, { index_templates: indexTemplates }] =
78+
await Promise.all([
79+
client.asCurrentUser.cluster.getComponentTemplate({ name }),
80+
client.asCurrentUser.indices.getIndexTemplate(),
81+
]);
8782

8883
return response.ok({
8984
// @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns

x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import { expectManagementTableRules } from '../../../../tasks/alerts_detection_r
1717

1818
const PREBUILT_RULES_PACKAGE_INSTALLATION_TIMEOUT_MS = 120000; // 2 minutes
1919

20-
describe(
20+
// Failing: See https://github.com/elastic/kibana/issues/228945
21+
describe.skip(
2122
'Detection rules, Prebuilt Rules Installation and Update workflow',
2223
{ tags: ['@ess', '@serverless'] },
2324
() => {

x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ import { TOOLTIP } from '../../../../../screens/common';
4343

4444
const RULES_TO_IMPORT_FILENAME = 'cypress/fixtures/7_16_rules.ndjson';
4545

46-
describe('rule snoozing', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => {
46+
// Failing: See https://github.com/elastic/kibana/issues/228942
47+
describe.skip('rule snoozing', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => {
4748
beforeEach(() => {
4849
login();
4950
deleteAlertsAndRules();

0 commit comments

Comments
 (0)