[8.7] [Fleet] Bugfix: prevent status runtime query going over character limit (#150910) (#151138)

# Backport

This will backport the following commits from `main` to `8.7`:
- [[Fleet] Bugfix: prevent status runtime query going over character
limit (#150910)](https://github.com/elastic/kibana/pull/150910)

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

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

<!--BACKPORT [{"author":{"name":"Mark
Hopkin","email":"mark.hopkin@elastic.co"},"sourceCommit":{"committedDate":"2023-02-14T14:05:34Z","message":"[Fleet]
Bugfix: prevent status runtime query going over character limit
(#150910)\n\n## Summary\r\n\r\nCloses #150577 \r\n\r\nif there are too
many agent policies in a system, we were creating too\r\nbig a runtime
query for elastic and the query would be rejected.\r\n\r\nThis PR adds a
limit, if the user has more than 750 agent policies then\r\nagents will
not be marked as inactive anymore. If the user reaches the\r\nlimit then
a warning badge is displayed (the text underlined has also\r\nbeen
added):\r\n\r\n<img width=\"968\" alt=\"Screenshot 2023-02-13 at 20 14
31\"\r\nsrc=\"https://user-images.githubusercontent.com/3315046/218565456-f5758e4b-74f6-4e7c-9b49-22f0fd6f9102.png\">\r\n\r\n\r\nIntegration
test
added.","sha":"687294d4d30e936aa95b4a4c566b29c4462ab362","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:Fleet","v8.7.0","v8.8.0"],"number":150910,"url":"https://github.com/elastic/kibana/pull/150910","mergeCommit":{"message":"[Fleet]
Bugfix: prevent status runtime query going over character limit
(#150910)\n\n## Summary\r\n\r\nCloses #150577 \r\n\r\nif there are too
many agent policies in a system, we were creating too\r\nbig a runtime
query for elastic and the query would be rejected.\r\n\r\nThis PR adds a
limit, if the user has more than 750 agent policies then\r\nagents will
not be marked as inactive anymore. If the user reaches the\r\nlimit then
a warning badge is displayed (the text underlined has also\r\nbeen
added):\r\n\r\n<img width=\"968\" alt=\"Screenshot 2023-02-13 at 20 14
31\"\r\nsrc=\"https://user-images.githubusercontent.com/3315046/218565456-f5758e4b-74f6-4e7c-9b49-22f0fd6f9102.png\">\r\n\r\n\r\nIntegration
test
added.","sha":"687294d4d30e936aa95b4a4c566b29c4462ab362"}},"sourceBranch":"main","suggestedTargetBranches":["8.7"],"targetPullRequestStates":[{"branch":"8.7","label":"v8.7.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/150910","number":150910,"mergeCommit":{"message":"[Fleet]
Bugfix: prevent status runtime query going over character limit
(#150910)\n\n## Summary\r\n\r\nCloses #150577 \r\n\r\nif there are too
many agent policies in a system, we were creating too\r\nbig a runtime
query for elastic and the query would be rejected.\r\n\r\nThis PR adds a
limit, if the user has more than 750 agent policies then\r\nagents will
not be marked as inactive anymore. If the user reaches the\r\nlimit then
a warning badge is displayed (the text underlined has also\r\nbeen
added):\r\n\r\n<img width=\"968\" alt=\"Screenshot 2023-02-13 at 20 14
31\"\r\nsrc=\"https://user-images.githubusercontent.com/3315046/218565456-f5758e4b-74f6-4e7c-9b49-22f0fd6f9102.png\">\r\n\r\n\r\nIntegration
test added.","sha":"687294d4d30e936aa95b4a4c566b29c4462ab362"}}]}]
BACKPORT-->

Co-authored-by: Mark Hopkin <mark.hopkin@elastic.co>
This commit is contained in:
Kibana Machine 2023-02-14 10:14:12 -05:00 committed by GitHub
parent 28fb73eefe
commit 6050429f09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 239 additions and 251 deletions

View file

@ -190,6 +190,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean)',
'xpack.fleet.agents.enabled (boolean)',
'xpack.fleet.enableExperimental (array)',
'xpack.fleet.developer.maxAgentPoliciesWithInactivityTimeout (number)',
'xpack.global_search.search_timeout (duration)',
'xpack.graph.canEditDrillDownUrls (boolean)',
'xpack.graph.savePolicy (alternatives)',

View file

@ -40,6 +40,7 @@ export interface FleetConfigType {
agentPolicySchemaUpgradeBatchSize?: number;
};
developer?: {
maxAgentPoliciesWithInactivityTimeout?: number;
disableRegistryVersionCheck?: boolean;
bundledPackageLocation?: string;
};

View file

@ -24,13 +24,14 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiBetaBadge,
EuiBadge,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { dataTypes } from '../../../../../../../common/constants';
import { AGENT_POLICY_SAVED_OBJECT_TYPE, dataTypes } from '../../../../../../../common/constants';
import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
import { useStartServices } from '../../../../hooks';
import { useStartServices, useConfig, useGetAgentPolicies } from '../../../../hooks';
import { AgentPolicyPackageBadge } from '../../../../components';
@ -63,12 +64,26 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
}) => {
const { agentFqdnMode: agentFqdnModeEnabled } = ExperimentalFeaturesService.get();
const { docLinks } = useStartServices();
const config = useConfig();
const maxAgentPoliciesWithInactivityTimeout =
config.developer?.maxAgentPoliciesWithInactivityTimeout ?? 0;
const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({});
const {
dataOutputOptions,
monitoringOutputOptions,
isLoading: isLoadingOptions,
} = useOutputOptions(agentPolicy);
const { data: agentPoliciesData } = useGetAgentPolicies({
page: 1,
perPage: 0,
kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.inactivity_timeout:*`,
});
const totalAgentPoliciesWithInactivityTimeout = agentPoliciesData?.total ?? 0;
const tooManyAgentPoliciesForInactivityTimeout =
maxAgentPoliciesWithInactivityTimeout !== undefined &&
totalAgentPoliciesWithInactivityTimeout > (maxAgentPoliciesWithInactivityTimeout ?? 0);
const { dataDownloadSourceOptions, isLoading: isLoadingDownloadSources } =
useDownloadSourcesOptions(agentPolicy);
@ -273,12 +288,32 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
id="xpack.fleet.agentPolicyForm.inactivityTimeoutLabel"
defaultMessage="Inactivity timeout"
/>
{tooManyAgentPoliciesForInactivityTimeout && (
<>
&nbsp;
<EuiToolTip
content={
<FormattedMessage
id="xpack.fleet.agentPolicyForm.inactivityTimeoutTooltip"
defaultMessage="The maximum of 750 agent policies with an inactivity timeout has been exceeded. Remove inactivity timeouts or agent policies to allow agents to become inactive again."
/>
}
>
<EuiBadge color="warning">
<FormattedMessage
id="xpack.fleet.agentPolicyForm.inactivityTimeoutBadge"
defaultMessage="Warning"
/>
</EuiBadge>
</EuiToolTip>
</>
)}
</h4>
}
description={
<FormattedMessage
id="xpack.fleet.agentPolicyForm.inactivityTimeoutDescription"
defaultMessage="An optional timeout in seconds. If provided, an agent will automatically change to inactive status and be filtered out of the agents list."
defaultMessage="An optional timeout in seconds. If provided, an agent will automatically change to inactive status and be filtered out of the agents list. A maximum of 750 agent policies can have an inactivity timeout."
/>
}
>

View file

@ -35,6 +35,9 @@ export const config: PluginConfigDescriptor = {
enabled: true,
},
enableExperimental: true,
developer: {
maxAgentPoliciesWithInactivityTimeout: true,
},
},
deprecations: ({ renameFromRoot, unused, unusedFromRoot }) => [
// Unused settings before Fleet server exists
@ -126,6 +129,7 @@ export const config: PluginConfigDescriptor = {
})
),
developer: schema.object({
maxAgentPoliciesWithInactivityTimeout: schema.maybe(schema.number()),
disableRegistryVersionCheck: schema.boolean({ defaultValue: false }),
allowAgentUpgradeSourceUri: schema.boolean({ defaultValue: false }),
bundledPackageLocation: schema.string({ defaultValue: DEFAULT_BUNDLED_PACKAGE_LOCATION }),

View file

@ -15,54 +15,13 @@ describe('buildStatusRuntimeField', () => {
});
it('should build the correct runtime field if there are no inactivity timeouts', () => {
const inactivityTimeouts: InactivityTimeouts = [];
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts);
const runtimeField = _buildStatusRuntimeField({ inactivityTimeouts });
expect(runtimeField).toMatchInlineSnapshot(`
Object {
"status": Object {
"script": Object {
"lang": "painless",
"source": "
long lastCheckinMillis = doc['last_checkin'].size() > 0
? doc['last_checkin'].value.toInstant().toEpochMilli()
: (
doc['enrolled_at'].size() > 0
? doc['enrolled_at'].value.toInstant().toEpochMilli()
: -1
);
if (doc['active'].size() > 0 && doc['active'].value == false) {
emit('unenrolled');
} else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && false) {
emit('inactive');
} else if (
lastCheckinMillis > 0
&& lastCheckinMillis
< (1234567590123L)
) {
emit('offline');
} else if (
doc['policy_revision_idx'].size() == 0 || (
doc['upgrade_started_at'].size() > 0 &&
doc['upgraded_at'].size() == 0
)
) {
emit('updating');
} else if (doc['last_checkin'].size() == 0) {
emit('enrolling');
} else if (doc['unenrollment_started_at'].size() > 0) {
emit('unenrolling');
} else if (
doc['last_checkin_status'].size() > 0 &&
doc['last_checkin_status'].value.toLowerCase() == 'error'
) {
emit('error');
} else if (
doc['last_checkin_status'].size() > 0 &&
doc['last_checkin_status'].value.toLowerCase() == 'degraded'
) {
emit('degraded');
} else {
emit('online');
}",
"source": " long lastCheckinMillis = doc['last_checkin'].size() > 0 ? doc['last_checkin'].value.toInstant().toEpochMilli() : ( doc['enrolled_at'].size() > 0 ? doc['enrolled_at'].value.toInstant().toEpochMilli() : -1 ); if (doc['active'].size() > 0 && doc['active'].value == false) { emit('unenrolled'); } else if ( lastCheckinMillis > 0 && lastCheckinMillis < 1234567590123L ) { emit('offline'); } else if ( doc['policy_revision_idx'].size() == 0 || ( doc['upgrade_started_at'].size() > 0 && doc['upgraded_at'].size() == 0 ) ) { emit('updating'); } else if (doc['last_checkin'].size() == 0) { emit('enrolling'); } else if (doc['unenrollment_started_at'].size() > 0) { emit('unenrolling'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'error' ) { emit('error'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'degraded' ) { emit('degraded'); } else { emit('online'); }",
},
"type": "keyword",
},
@ -71,54 +30,13 @@ describe('buildStatusRuntimeField', () => {
});
it('should build the correct runtime field if there are no inactivity timeouts (prefix)', () => {
const inactivityTimeouts: InactivityTimeouts = [];
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts, 'my.prefix.');
const runtimeField = _buildStatusRuntimeField({ inactivityTimeouts, pathPrefix: 'my.prefix.' });
expect(runtimeField).toMatchInlineSnapshot(`
Object {
"status": Object {
"script": Object {
"lang": "painless",
"source": "
long lastCheckinMillis = doc['my.prefix.last_checkin'].size() > 0
? doc['my.prefix.last_checkin'].value.toInstant().toEpochMilli()
: (
doc['my.prefix.enrolled_at'].size() > 0
? doc['my.prefix.enrolled_at'].value.toInstant().toEpochMilli()
: -1
);
if (doc['my.prefix.active'].size() > 0 && doc['my.prefix.active'].value == false) {
emit('unenrolled');
} else if (lastCheckinMillis > 0 && doc['my.prefix.policy_id'].size() > 0 && false) {
emit('inactive');
} else if (
lastCheckinMillis > 0
&& lastCheckinMillis
< (1234567590123L)
) {
emit('offline');
} else if (
doc['my.prefix.policy_revision_idx'].size() == 0 || (
doc['my.prefix.upgrade_started_at'].size() > 0 &&
doc['my.prefix.upgraded_at'].size() == 0
)
) {
emit('updating');
} else if (doc['my.prefix.last_checkin'].size() == 0) {
emit('enrolling');
} else if (doc['my.prefix.unenrollment_started_at'].size() > 0) {
emit('unenrolling');
} else if (
doc['my.prefix.last_checkin_status'].size() > 0 &&
doc['my.prefix.last_checkin_status'].value.toLowerCase() == 'error'
) {
emit('error');
} else if (
doc['my.prefix.last_checkin_status'].size() > 0 &&
doc['my.prefix.last_checkin_status'].value.toLowerCase() == 'degraded'
) {
emit('degraded');
} else {
emit('online');
}",
"source": " long lastCheckinMillis = doc['my.prefix.last_checkin'].size() > 0 ? doc['my.prefix.last_checkin'].value.toInstant().toEpochMilli() : ( doc['my.prefix.enrolled_at'].size() > 0 ? doc['my.prefix.enrolled_at'].value.toInstant().toEpochMilli() : -1 ); if (doc['my.prefix.active'].size() > 0 && doc['my.prefix.active'].value == false) { emit('unenrolled'); } else if ( lastCheckinMillis > 0 && lastCheckinMillis < 1234567590123L ) { emit('offline'); } else if ( doc['my.prefix.policy_revision_idx'].size() == 0 || ( doc['my.prefix.upgrade_started_at'].size() > 0 && doc['my.prefix.upgraded_at'].size() == 0 ) ) { emit('updating'); } else if (doc['my.prefix.last_checkin'].size() == 0) { emit('enrolling'); } else if (doc['my.prefix.unenrollment_started_at'].size() > 0) { emit('unenrolling'); } else if ( doc['my.prefix.last_checkin_status'].size() > 0 && doc['my.prefix.last_checkin_status'].value.toLowerCase() == 'error' ) { emit('error'); } else if ( doc['my.prefix.last_checkin_status'].size() > 0 && doc['my.prefix.last_checkin_status'].value.toLowerCase() == 'degraded' ) { emit('degraded'); } else { emit('online'); }",
},
"type": "keyword",
},
@ -132,54 +50,13 @@ describe('buildStatusRuntimeField', () => {
policyIds: ['policy-1'],
},
];
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts);
const runtimeField = _buildStatusRuntimeField({ inactivityTimeouts });
expect(runtimeField).toMatchInlineSnapshot(`
Object {
"status": Object {
"script": Object {
"lang": "painless",
"source": "
long lastCheckinMillis = doc['last_checkin'].size() > 0
? doc['last_checkin'].value.toInstant().toEpochMilli()
: (
doc['enrolled_at'].size() > 0
? doc['enrolled_at'].value.toInstant().toEpochMilli()
: -1
);
if (doc['active'].size() > 0 && doc['active'].value == false) {
emit('unenrolled');
} else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && (doc['policy_id'].value == 'policy-1') && lastCheckinMillis < 1234567590123L) {
emit('inactive');
} else if (
lastCheckinMillis > 0
&& lastCheckinMillis
< (1234567590123L)
) {
emit('offline');
} else if (
doc['policy_revision_idx'].size() == 0 || (
doc['upgrade_started_at'].size() > 0 &&
doc['upgraded_at'].size() == 0
)
) {
emit('updating');
} else if (doc['last_checkin'].size() == 0) {
emit('enrolling');
} else if (doc['unenrollment_started_at'].size() > 0) {
emit('unenrolling');
} else if (
doc['last_checkin_status'].size() > 0 &&
doc['last_checkin_status'].value.toLowerCase() == 'error'
) {
emit('error');
} else if (
doc['last_checkin_status'].size() > 0 &&
doc['last_checkin_status'].value.toLowerCase() == 'degraded'
) {
emit('degraded');
} else {
emit('online');
}",
"source": " long lastCheckinMillis = doc['last_checkin'].size() > 0 ? doc['last_checkin'].value.toInstant().toEpochMilli() : ( doc['enrolled_at'].size() > 0 ? doc['enrolled_at'].value.toInstant().toEpochMilli() : -1 ); if (doc['active'].size() > 0 && doc['active'].value == false) { emit('unenrolled'); } else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && ['policy-1'].contains(doc['policy_id'].value) && lastCheckinMillis < 1234567590123L) {emit('inactive');} else if ( lastCheckinMillis > 0 && lastCheckinMillis < 1234567590123L ) { emit('offline'); } else if ( doc['policy_revision_idx'].size() == 0 || ( doc['upgrade_started_at'].size() > 0 && doc['upgraded_at'].size() == 0 ) ) { emit('updating'); } else if (doc['last_checkin'].size() == 0) { emit('enrolling'); } else if (doc['unenrollment_started_at'].size() > 0) { emit('unenrolling'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'error' ) { emit('error'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'degraded' ) { emit('degraded'); } else { emit('online'); }",
},
"type": "keyword",
},
@ -193,54 +70,37 @@ describe('buildStatusRuntimeField', () => {
policyIds: ['policy-1', 'policy-2'],
},
];
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts);
const runtimeField = _buildStatusRuntimeField({ inactivityTimeouts });
expect(runtimeField).toMatchInlineSnapshot(`
Object {
"status": Object {
"script": Object {
"lang": "painless",
"source": "
long lastCheckinMillis = doc['last_checkin'].size() > 0
? doc['last_checkin'].value.toInstant().toEpochMilli()
: (
doc['enrolled_at'].size() > 0
? doc['enrolled_at'].value.toInstant().toEpochMilli()
: -1
);
if (doc['active'].size() > 0 && doc['active'].value == false) {
emit('unenrolled');
} else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && (doc['policy_id'].value == 'policy-1' || doc['policy_id'].value == 'policy-2') && lastCheckinMillis < 1234567590123L) {
emit('inactive');
} else if (
lastCheckinMillis > 0
&& lastCheckinMillis
< (1234567590123L)
) {
emit('offline');
} else if (
doc['policy_revision_idx'].size() == 0 || (
doc['upgrade_started_at'].size() > 0 &&
doc['upgraded_at'].size() == 0
)
) {
emit('updating');
} else if (doc['last_checkin'].size() == 0) {
emit('enrolling');
} else if (doc['unenrollment_started_at'].size() > 0) {
emit('unenrolling');
} else if (
doc['last_checkin_status'].size() > 0 &&
doc['last_checkin_status'].value.toLowerCase() == 'error'
) {
emit('error');
} else if (
doc['last_checkin_status'].size() > 0 &&
doc['last_checkin_status'].value.toLowerCase() == 'degraded'
) {
emit('degraded');
} else {
emit('online');
}",
"source": " long lastCheckinMillis = doc['last_checkin'].size() > 0 ? doc['last_checkin'].value.toInstant().toEpochMilli() : ( doc['enrolled_at'].size() > 0 ? doc['enrolled_at'].value.toInstant().toEpochMilli() : -1 ); if (doc['active'].size() > 0 && doc['active'].value == false) { emit('unenrolled'); } else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && ['policy-1','policy-2'].contains(doc['policy_id'].value) && lastCheckinMillis < 1234567590123L) {emit('inactive');} else if ( lastCheckinMillis > 0 && lastCheckinMillis < 1234567590123L ) { emit('offline'); } else if ( doc['policy_revision_idx'].size() == 0 || ( doc['upgrade_started_at'].size() > 0 && doc['upgraded_at'].size() == 0 ) ) { emit('updating'); } else if (doc['last_checkin'].size() == 0) { emit('enrolling'); } else if (doc['unenrollment_started_at'].size() > 0) { emit('unenrolling'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'error' ) { emit('error'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'degraded' ) { emit('degraded'); } else { emit('online'); }",
},
"type": "keyword",
},
}
`);
});
it('should not perform inactivity check if there are too many agent policies with an inactivity timeout', () => {
const inactivityTimeouts: InactivityTimeouts = [
{
inactivityTimeout: 300,
// default max is 750
policyIds: new Array(1000).fill(0).map((_, i) => `policy-${i}`),
},
];
const runtimeField = _buildStatusRuntimeField({ inactivityTimeouts });
expect(runtimeField).not.toContain('policy-');
expect(runtimeField).toMatchInlineSnapshot(`
Object {
"status": Object {
"script": Object {
"lang": "painless",
"source": " long lastCheckinMillis = doc['last_checkin'].size() > 0 ? doc['last_checkin'].value.toInstant().toEpochMilli() : ( doc['enrolled_at'].size() > 0 ? doc['enrolled_at'].value.toInstant().toEpochMilli() : -1 ); if (doc['active'].size() > 0 && doc['active'].value == false) { emit('unenrolled'); } else if ( lastCheckinMillis > 0 && lastCheckinMillis < 1234567590123L ) { emit('offline'); } else if ( doc['policy_revision_idx'].size() == 0 || ( doc['upgrade_started_at'].size() > 0 && doc['upgraded_at'].size() == 0 ) ) { emit('updating'); } else if (doc['last_checkin'].size() == 0) { emit('enrolling'); } else if (doc['unenrollment_started_at'].size() > 0) { emit('unenrolling'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'error' ) { emit('error'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'degraded' ) { emit('degraded'); } else { emit('online'); }",
},
"type": "keyword",
},
@ -258,54 +118,13 @@ describe('buildStatusRuntimeField', () => {
policyIds: ['policy-3'],
},
];
const runtimeField = _buildStatusRuntimeField(inactivityTimeouts);
const runtimeField = _buildStatusRuntimeField({ inactivityTimeouts });
expect(runtimeField).toMatchInlineSnapshot(`
Object {
"status": Object {
"script": Object {
"lang": "painless",
"source": "
long lastCheckinMillis = doc['last_checkin'].size() > 0
? doc['last_checkin'].value.toInstant().toEpochMilli()
: (
doc['enrolled_at'].size() > 0
? doc['enrolled_at'].value.toInstant().toEpochMilli()
: -1
);
if (doc['active'].size() > 0 && doc['active'].value == false) {
emit('unenrolled');
} else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && (doc['policy_id'].value == 'policy-1' || doc['policy_id'].value == 'policy-2') && lastCheckinMillis < 1234567590123L || (doc['policy_id'].value == 'policy-3') && lastCheckinMillis < 1234567490123L) {
emit('inactive');
} else if (
lastCheckinMillis > 0
&& lastCheckinMillis
< (1234567590123L)
) {
emit('offline');
} else if (
doc['policy_revision_idx'].size() == 0 || (
doc['upgrade_started_at'].size() > 0 &&
doc['upgraded_at'].size() == 0
)
) {
emit('updating');
} else if (doc['last_checkin'].size() == 0) {
emit('enrolling');
} else if (doc['unenrollment_started_at'].size() > 0) {
emit('unenrolling');
} else if (
doc['last_checkin_status'].size() > 0 &&
doc['last_checkin_status'].value.toLowerCase() == 'error'
) {
emit('error');
} else if (
doc['last_checkin_status'].size() > 0 &&
doc['last_checkin_status'].value.toLowerCase() == 'degraded'
) {
emit('degraded');
} else {
emit('online');
}",
"source": " long lastCheckinMillis = doc['last_checkin'].size() > 0 ? doc['last_checkin'].value.toInstant().toEpochMilli() : ( doc['enrolled_at'].size() > 0 ? doc['enrolled_at'].value.toInstant().toEpochMilli() : -1 ); if (doc['active'].size() > 0 && doc['active'].value == false) { emit('unenrolled'); } else if (lastCheckinMillis > 0 && doc['policy_id'].size() > 0 && ['policy-1','policy-2'].contains(doc['policy_id'].value) && lastCheckinMillis < 1234567590123L || ['policy-3'].contains(doc['policy_id'].value) && lastCheckinMillis < 1234567490123L) {emit('inactive');} else if ( lastCheckinMillis > 0 && lastCheckinMillis < 1234567590123L ) { emit('offline'); } else if ( doc['policy_revision_idx'].size() == 0 || ( doc['upgrade_started_at'].size() > 0 && doc['upgraded_at'].size() == 0 ) ) { emit('updating'); } else if (doc['last_checkin'].size() == 0) { emit('enrolling'); } else if (doc['unenrollment_started_at'].size() > 0) { emit('unenrolling'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'error' ) { emit('error'); } else if ( doc['last_checkin_status'].size() > 0 && doc['last_checkin_status'].value.toLowerCase() == 'degraded' ) { emit('degraded'); } else { emit('online'); }",
},
"type": "keyword",
},
@ -314,8 +133,14 @@ describe('buildStatusRuntimeField', () => {
});
it('should build the same runtime field if path ends with. or not', () => {
const inactivityTimeouts: InactivityTimeouts = [];
const runtimeFieldWithDot = _buildStatusRuntimeField(inactivityTimeouts, 'my.prefix.');
const runtimeFieldNoDot = _buildStatusRuntimeField(inactivityTimeouts, 'my.prefix');
const runtimeFieldWithDot = _buildStatusRuntimeField({
inactivityTimeouts,
pathPrefix: 'my.prefix.',
});
const runtimeFieldNoDot = _buildStatusRuntimeField({
inactivityTimeouts,
pathPrefix: 'my.prefix',
});
expect(runtimeFieldWithDot).toEqual(runtimeFieldNoDot);
});
});

View file

@ -7,41 +7,88 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/types';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { Logger } from '@kbn/core/server';
import { AGENT_POLLING_THRESHOLD_MS } from '../../constants';
import { agentPolicyService } from '../agent_policy';
import { appContextService } from '../app_context';
const MISSED_INTERVALS_BEFORE_OFFLINE = 10;
const MS_BEFORE_OFFLINE = MISSED_INTERVALS_BEFORE_OFFLINE * AGENT_POLLING_THRESHOLD_MS;
const DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT = 750;
export type InactivityTimeouts = Awaited<
ReturnType<typeof agentPolicyService['getInactivityTimeouts']>
>;
const _buildInactiveClause = (
now: number,
inactivityTimeouts: InactivityTimeouts,
field: (path: string) => string
) => {
let inactivityTimeoutsDisabled = false;
const _buildInactiveCondition = (opts: {
now: number;
inactivityTimeouts: InactivityTimeouts;
maxAgentPoliciesWithInactivityTimeout: number;
field: (path: string) => string;
logger?: Logger;
}): string | null => {
const { now, inactivityTimeouts, maxAgentPoliciesWithInactivityTimeout, field, logger } = opts;
// if there are no policies with inactivity timeouts, then no agents are inactive
if (inactivityTimeouts.length === 0) {
return null;
}
const totalAgentPoliciesWithInactivityTimeouts = inactivityTimeouts.reduce(
(total, { policyIds }) => total + policyIds.length,
0
);
// if too many agent policies have inactivity timeouts, then we can't use the inactivity timeout
// as the query becomes too large see github.com/elastic/kibana/issues/150577
if (totalAgentPoliciesWithInactivityTimeouts > maxAgentPoliciesWithInactivityTimeout) {
if (!inactivityTimeoutsDisabled) {
// only log this once as this function is executed a lot
logger?.warn(
`There are ${totalAgentPoliciesWithInactivityTimeouts} agent policies with an inactivity timeout set but the maximum allowed is ${maxAgentPoliciesWithInactivityTimeout}. Agents will not be marked as inactive.`
);
inactivityTimeoutsDisabled = true;
}
return null;
}
if (inactivityTimeoutsDisabled) {
logger?.info(
`There are ${totalAgentPoliciesWithInactivityTimeouts} agent policies which is now below the maximum allowed of ${maxAgentPoliciesWithInactivityTimeout}. Agents will now be marked as inactive again.`
);
inactivityTimeoutsDisabled = false;
}
const policyClauses = inactivityTimeouts
.map(({ inactivityTimeout, policyIds }) => {
const inactivityTimeoutMs = inactivityTimeout * 1000;
const policyOrs = policyIds
.map((policyId) => `${field('policy_id')}.value == '${policyId}'`)
.join(' || ');
const policyIdMatches = `[${policyIds.map((id) => `'${id}'`).join(',')}].contains(${field(
'policy_id'
)}.value)`;
return `(${policyOrs}) && lastCheckinMillis < ${now - inactivityTimeoutMs}L`;
return `${policyIdMatches} && lastCheckinMillis < ${now - inactivityTimeoutMs}L`;
})
.join(' || ');
const agentIsInactive = policyClauses.length ? `${policyClauses}` : 'false'; // if no policies have inactivity timeouts, then no agents are inactive
return `lastCheckinMillis > 0 && ${field('policy_id')}.size() > 0 && ${agentIsInactive}`;
return `lastCheckinMillis > 0 && ${field('policy_id')}.size() > 0 && ${policyClauses}`;
};
function _buildSource(inactivityTimeouts: InactivityTimeouts, pathPrefix?: string) {
function _buildSource(
inactivityTimeouts: InactivityTimeouts,
maxAgentPoliciesWithInactivityTimeout: number,
pathPrefix?: string,
logger?: Logger
) {
const normalizedPrefix = pathPrefix ? `${pathPrefix}${pathPrefix.endsWith('.') ? '' : '.'}` : '';
const field = (path: string) => `doc['${normalizedPrefix + path}']`;
const now = Date.now();
const agentIsInactiveCondition = _buildInactiveCondition({
now,
inactivityTimeouts,
maxAgentPoliciesWithInactivityTimeout,
field,
logger,
});
return `
long lastCheckinMillis = ${field('last_checkin')}.size() > 0
? ${field('last_checkin')}.value.toInstant().toEpochMilli()
@ -52,12 +99,11 @@ function _buildSource(inactivityTimeouts: InactivityTimeouts, pathPrefix?: strin
);
if (${field('active')}.size() > 0 && ${field('active')}.value == false) {
emit('unenrolled');
} else if (${_buildInactiveClause(now, inactivityTimeouts, field)}) {
emit('inactive');
} else if (
} ${agentIsInactiveCondition ? `else if (${agentIsInactiveCondition}) {emit('inactive');}` : ''}
else if (
lastCheckinMillis > 0
&& lastCheckinMillis
< (${now - MS_BEFORE_OFFLINE}L)
< ${now - MS_BEFORE_OFFLINE}L
) {
emit('offline');
} else if (
@ -83,15 +129,28 @@ function _buildSource(inactivityTimeouts: InactivityTimeouts, pathPrefix?: strin
emit('degraded');
} else {
emit('online');
}`;
}`.replace(/\s{2,}/g, ' '); // replace newlines and double spaces to save characters
}
// exported for testing
export function _buildStatusRuntimeField(
inactivityTimeouts: InactivityTimeouts,
pathPrefix?: string
): NonNullable<estypes.SearchRequest['runtime_mappings']> {
const source = _buildSource(inactivityTimeouts, pathPrefix);
export function _buildStatusRuntimeField(opts: {
inactivityTimeouts: InactivityTimeouts;
maxAgentPoliciesWithInactivityTimeout?: number;
pathPrefix?: string;
logger?: Logger;
}): NonNullable<estypes.SearchRequest['runtime_mappings']> {
const {
inactivityTimeouts,
maxAgentPoliciesWithInactivityTimeout = DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT,
pathPrefix,
logger,
} = opts;
const source = _buildSource(
inactivityTimeouts,
maxAgentPoliciesWithInactivityTimeout,
pathPrefix,
logger
);
return {
status: {
type: 'keyword',
@ -111,7 +170,23 @@ export async function buildAgentStatusRuntimeField(
soClient: SavedObjectsClientContract,
pathPrefix?: string
) {
const config = appContextService.getConfig();
let logger: Logger | undefined;
try {
logger = appContextService.getLogger();
} catch (e) {
// ignore, logger is optional
// this code can be used and tested without an app context
}
const maxAgentPoliciesWithInactivityTimeout =
config?.developer?.maxAgentPoliciesWithInactivityTimeout;
const inactivityTimeouts = await agentPolicyService.getInactivityTimeouts(soClient);
return _buildStatusRuntimeField(inactivityTimeouts, pathPrefix);
return _buildStatusRuntimeField({
inactivityTimeouts,
maxAgentPoliciesWithInactivityTimeout,
pathPrefix,
logger,
});
}

View file

@ -16,14 +16,11 @@ import { updateAgentTags } from './update_agent_tags';
import { UpdateAgentTagsActionRunner, updateTagsBatch } from './update_agent_tags_action_runner';
jest.mock('../app_context', () => {
const { loggerMock } = jest.requireActual('@kbn/logging-mocks');
return {
appContextService: {
getLogger: jest.fn().mockReturnValue({
debug: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
} as any),
getLogger: () => loggerMock.create(),
getConfig: () => {},
},
};
});

View file

@ -257,5 +257,54 @@ export default function ({ getService }: FtrProviderContext) {
statusCode: 403,
});
});
it('should not perform inactivity check if there are too many agent policies with inactivity timeout', async () => {
// the test server is started with --xpack.fleet.developer.maxAgentPoliciesWithInactivityTimeout=10
// so we create 11 policies with inactivity timeout then no agents should turn inactive
const policiesToAdd = new Array(11).fill(0).map((_, i) => `policy-inactivity-timeout-${i}`);
await Promise.all(
policiesToAdd.map((policyId) =>
es.create({
id: 'ingest-agent-policies:' + policyId,
index: '.kibana',
refresh: 'wait_for',
document: {
type: 'ingest-agent-policies',
'ingest-agent-policies': {
name: policyId,
namespace: 'default',
description: 'Policy with inactivity timeout',
status: 'active',
is_default: true,
monitoring_enabled: ['logs', 'metrics'],
revision: 2,
updated_at: '2020-05-07T19:34:42.533Z',
updated_by: 'system',
inactivity_timeout: 60,
},
migrationVersion: {
'ingest-agent-policies': '7.10.0',
},
},
})
)
);
const { body: apiResponse } = await supertest.get(`/api/fleet/agent_status`).expect(200);
expect(apiResponse).to.eql({
results: {
events: 0,
other: 0,
total: 10,
online: 3,
error: 2,
offline: 1,
updating: 4,
inactive: 0,
unenrolled: 1,
},
});
});
});
}

View file

@ -69,6 +69,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
...(registryPort ? [`--xpack.fleet.registryUrl=http://localhost:${registryPort}`] : []),
`--xpack.fleet.developer.bundledPackageLocation=${BUNDLED_PACKAGE_DIR}`,
'--xpack.cloudSecurityPosture.enabled=true',
`--xpack.fleet.developer.maxAgentPoliciesWithInactivityTimeout=10`,
`--xpack.fleet.packageVerification.gpgKeyPath=${getFullPath(
'./apis/fixtures/package_verification/signatures/fleet_test_key_public.asc'
)}`,