Skip to content

Commit e4ea87e

Browse files
[EDR Workflows][Investigation] Telemetry config watcher fix (elastic#210406)
## Summary To update the `global_telemetry_config` flag in Defend package policies, we subscribe to the Telemetry plugin's `isOptedIn$` observable during Kibana's `start()` phase, and receive the initial value immediately. This feature is used for 'migrating' existing package policies: after stack upgrade, when Kibana starts up, this subscription mechanism makes sure that existing policies are backfilled with the new field. But not on cloud and serverless instances. It turned out, that while this works on local instances, on cloud and serverless instances, at the very moment we receive the value during `start()`, some mechanisms are not yet green, and this resulted in `security_exception: missing authentication credentials for REST request` when trying to read Saved Objects. As subscribing to`core.status.core$`, and waiting until `ServiceStatus` for `elasticsearch` and `savedObjects` is `available` didn't solve the issue, I simply added a retry mechanism, which, at least, protects against other temporary issues as well. Some additional logging is added as well. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [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: Elastic Machine <[email protected]>
1 parent 151fa26 commit e4ea87e

File tree

3 files changed

+206
-65
lines changed

3 files changed

+206
-65
lines changed

x-pack/solutions/security/plugins/security_solution/server/endpoint/lib/policy/telemetry_watch.test.ts

Lines changed: 127 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,49 @@
66
*/
77

88
import { Subject } from 'rxjs';
9-
import { elasticsearchServiceMock, savedObjectsServiceMock } from '@kbn/core/server/mocks';
9+
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
1010
import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks';
1111
import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
12-
import type { PackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common';
12+
import {
13+
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
14+
type PackagePolicy,
15+
type UpdatePackagePolicy,
16+
} from '@kbn/fleet-plugin/common';
1317
import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks';
1418
import { policyFactory } from '../../../../common/endpoint/models/policy_config';
1519
import type { PolicyConfig } from '../../../../common/endpoint/types';
1620
import { TelemetryConfigWatcher } from './telemetry_watch';
1721
import { TelemetryConfigProvider } from '../../../../common/telemetry_config/telemetry_config_provider';
1822
import { createMockEndpointAppContextService } from '../../mocks';
23+
import type { MockedLogger } from '@kbn/logging-mocks';
24+
import { loggerMock } from '@kbn/logging-mocks';
25+
import type { NewPackagePolicyWithId } from '@kbn/fleet-plugin/server/services/package_policy';
1926

2027
const MockPackagePolicyWithEndpointPolicy = (
2128
cb?: (p: PolicyConfig) => PolicyConfig
2229
): PackagePolicy => {
2330
const packagePolicy = createPackagePolicyMock();
24-
if (!cb) {
25-
// eslint-disable-next-line no-param-reassign
26-
cb = (p) => p;
27-
}
28-
const policyConfig = cb(policyFactory());
31+
32+
const policyConfig = cb?.(policyFactory()) ?? policyFactory();
33+
2934
packagePolicy.inputs[0].config = { policy: { value: policyConfig } };
3035

3136
return packagePolicy;
3237
};
3338

3439
describe('Telemetry config watcher', () => {
35-
const soStartMock = savedObjectsServiceMock.createStartContract();
3640
const esStartMock = elasticsearchServiceMock.createStart();
37-
let packagePolicySvcMock: jest.Mocked<PackagePolicyClient>;
41+
42+
let mockedLogger: MockedLogger;
43+
let packagePolicyServiceMock: jest.Mocked<PackagePolicyClient>;
3844
let telemetryWatcher: TelemetryConfigWatcher;
3945

4046
const preparePackagePolicyMock = ({
4147
isGlobalTelemetryEnabled,
4248
}: {
4349
isGlobalTelemetryEnabled: boolean;
4450
}) => {
45-
packagePolicySvcMock.list.mockResolvedValueOnce({
51+
packagePolicyServiceMock.list.mockResolvedValueOnce({
4652
items: [
4753
MockPackagePolicyWithEndpointPolicy((pc: PolicyConfig): PolicyConfig => {
4854
pc.global_telemetry_enabled = isGlobalTelemetryEnabled;
@@ -56,13 +62,21 @@ describe('Telemetry config watcher', () => {
5662
};
5763

5864
beforeEach(() => {
59-
packagePolicySvcMock = createPackagePolicyServiceMock();
65+
packagePolicyServiceMock = createPackagePolicyServiceMock();
66+
packagePolicyServiceMock.bulkUpdate.mockResolvedValue({
67+
updatedPolicies: [],
68+
failedPolicies: [],
69+
});
70+
71+
mockedLogger = loggerMock.create();
72+
const endpointAppContextServiceMock = createMockEndpointAppContextService();
73+
endpointAppContextServiceMock.createLogger = jest.fn().mockReturnValue(mockedLogger);
6074

6175
telemetryWatcher = new TelemetryConfigWatcher(
62-
packagePolicySvcMock,
63-
soStartMock,
76+
packagePolicyServiceMock,
6477
esStartMock,
65-
createMockEndpointAppContextService()
78+
endpointAppContextServiceMock,
79+
{ immediateRetry: true }
6680
);
6781
});
6882

@@ -90,7 +104,7 @@ describe('Telemetry config watcher', () => {
90104
const TOTAL = 247;
91105

92106
// set up the mocked package policy service to return and do what we want
93-
packagePolicySvcMock.list
107+
packagePolicyServiceMock.list
94108
.mockResolvedValueOnce({
95109
items: Array.from({ length: 100 }, () => MockPackagePolicyWithEndpointPolicy()),
96110
total: TOTAL,
@@ -112,12 +126,101 @@ describe('Telemetry config watcher', () => {
112126

113127
await telemetryWatcher.watch(true); // manual trigger
114128

115-
expect(packagePolicySvcMock.list).toBeCalledTimes(3);
129+
expect(packagePolicyServiceMock.list).toBeCalledTimes(3);
116130

117131
// Assert: on the first call to packagePolicy.list, we asked for page 1
118-
expect(packagePolicySvcMock.list.mock.calls[0][1].page).toBe(1);
119-
expect(packagePolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2
120-
expect(packagePolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc
132+
expect(packagePolicyServiceMock.list.mock.calls[0][1].page).toBe(1);
133+
expect(packagePolicyServiceMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2
134+
expect(packagePolicyServiceMock.list.mock.calls[2][1].page).toBe(3); // etc
135+
136+
expect(mockedLogger.warn).not.toHaveBeenCalled();
137+
expect(mockedLogger.error).not.toHaveBeenCalled();
138+
});
139+
140+
describe('error handling', () => {
141+
it('retries fetching package policies', async () => {
142+
packagePolicyServiceMock.list.mockRejectedValueOnce(new Error());
143+
144+
packagePolicyServiceMock.list.mockResolvedValueOnce({
145+
items: Array.from({ length: 6 }, () => MockPackagePolicyWithEndpointPolicy()),
146+
total: 6,
147+
page: 1,
148+
perPage: 100,
149+
});
150+
151+
await telemetryWatcher.watch(true);
152+
153+
expect(packagePolicyServiceMock.list).toBeCalledTimes(2);
154+
const expectedParams = {
155+
page: 1,
156+
perPage: 100,
157+
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`,
158+
};
159+
expect(packagePolicyServiceMock.list.mock.calls[0][1]).toStrictEqual(expectedParams);
160+
expect(packagePolicyServiceMock.list.mock.calls[1][1]).toStrictEqual(expectedParams);
161+
162+
expect(mockedLogger.warn).not.toHaveBeenCalled();
163+
expect(mockedLogger.error).not.toHaveBeenCalled();
164+
});
165+
166+
it('retries fetching package policies maximum 5 times', async () => {
167+
packagePolicyServiceMock.list.mockRejectedValue(new Error());
168+
169+
await telemetryWatcher.watch(true);
170+
171+
expect(packagePolicyServiceMock.list).toBeCalledTimes(5);
172+
expect(mockedLogger.warn).toHaveBeenCalledTimes(1);
173+
expect(mockedLogger.error).not.toHaveBeenCalled();
174+
});
175+
176+
it('retries bulk updating package policies', async () => {
177+
preparePackagePolicyMock({ isGlobalTelemetryEnabled: true });
178+
packagePolicyServiceMock.bulkUpdate.mockRejectedValueOnce(new Error());
179+
180+
await telemetryWatcher.watch(false);
181+
182+
expect(packagePolicyServiceMock.bulkUpdate).toHaveBeenCalledTimes(2);
183+
expect(mockedLogger.warn).not.toHaveBeenCalled();
184+
expect(mockedLogger.error).not.toHaveBeenCalled();
185+
});
186+
187+
it('retries bulk updating package policies maximum 5 times', async () => {
188+
preparePackagePolicyMock({ isGlobalTelemetryEnabled: true });
189+
packagePolicyServiceMock.bulkUpdate.mockRejectedValue(new Error());
190+
191+
await telemetryWatcher.watch(false);
192+
193+
expect(packagePolicyServiceMock.bulkUpdate).toHaveBeenCalledTimes(5);
194+
expect(mockedLogger.warn).toHaveBeenCalledTimes(1);
195+
expect(mockedLogger.error).not.toHaveBeenCalled();
196+
});
197+
198+
it('logs the ids of package policies that are failed to be updated', async () => {
199+
preparePackagePolicyMock({ isGlobalTelemetryEnabled: true });
200+
packagePolicyServiceMock.bulkUpdate.mockResolvedValueOnce({
201+
updatedPolicies: [],
202+
failedPolicies: [
203+
{
204+
error: new Error('error message 1'),
205+
packagePolicy: { id: 'policy-id-1' } as NewPackagePolicyWithId,
206+
},
207+
{
208+
error: new Error('error message 2'),
209+
packagePolicy: { id: 'policy-id-2' } as NewPackagePolicyWithId,
210+
},
211+
],
212+
});
213+
214+
await telemetryWatcher.watch(false);
215+
216+
expect(packagePolicyServiceMock.bulkUpdate).toHaveBeenCalledTimes(1);
217+
expect(mockedLogger.warn).toHaveBeenCalledTimes(1);
218+
expect(mockedLogger.error).not.toHaveBeenCalled();
219+
220+
const logMessage = mockedLogger.warn.mock.calls[0][0] as string;
221+
expect(logMessage).toMatch(/- id: policy-id-1, error:.+error message 1/);
222+
expect(logMessage).toMatch(/- id: policy-id-2, error:.+error message 2/);
223+
});
121224
});
122225

123226
it.each([true, false])(
@@ -127,17 +230,18 @@ describe('Telemetry config watcher', () => {
127230

128231
await telemetryWatcher.watch(value);
129232

130-
expect(packagePolicySvcMock.bulkUpdate).not.toHaveBeenCalled();
233+
expect(packagePolicyServiceMock.bulkUpdate).not.toHaveBeenCalled();
131234
}
132235
);
133236

134-
it.each([true, false])('updates `global_telemetry_config` field to %s', async (value) => {
237+
it.each([true, false])('updates `global_telemetry_enabled` field to %s', async (value) => {
135238
preparePackagePolicyMock({ isGlobalTelemetryEnabled: !value });
136239

137240
await telemetryWatcher.watch(value);
138241

139-
expect(packagePolicySvcMock.bulkUpdate).toHaveBeenCalled();
140-
const policyUpdates: UpdatePackagePolicy[] = packagePolicySvcMock.bulkUpdate.mock.calls[0][2];
242+
expect(packagePolicyServiceMock.bulkUpdate).toHaveBeenCalled();
243+
const policyUpdates: UpdatePackagePolicy[] =
244+
packagePolicyServiceMock.bulkUpdate.mock.calls[0][2];
141245
expect(policyUpdates.length).toBe(1);
142246
const updatedPolicyConfigs: PolicyConfig = policyUpdates[0].inputs[0].config?.policy.value;
143247
expect(updatedPolicyConfigs.global_telemetry_enabled).toBe(value);

0 commit comments

Comments
 (0)