[Fleet] Implement active agent soft limit (#161289)

This commit is contained in:
Nicolas Chaulet 2023-07-06 14:50:33 -04:00 committed by GitHub
parent 2c01f7e290
commit 7709670d92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 168 additions and 3 deletions

View file

@ -2,9 +2,11 @@ interactiveSetup.enabled: false
newsfeed.enabled: false
xpack.security.showNavLinks: false
xpack.serverless.plugin.enabled: true
# Fleet settings
xpack.fleet.internal.fleetServerStandalone: true
xpack.fleet.internal.disableILMPolicies: true
xpack.fleet.internal.disableProxies: true
xpack.fleet.internal.activeAgentsSoftLimit: 25000
# Enable ZDT migration algorithm
migrations.algorithm: zdt
@ -13,7 +15,7 @@ migrations.algorithm: zdt
# until the controller is able to spawn the migrator job/pod
migrations.zdt:
metaPickupSyncDelaySec: 5
runOnRoles: ["ui"]
runOnRoles: ['ui']
# Ess plugins
xpack.securitySolutionEss.enabled: false

View file

@ -221,6 +221,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean)',
'xpack.fleet.agents.enabled (boolean)',
'xpack.fleet.enableExperimental (array)',
'xpack.fleet.internal.activeAgentsSoftLimit (number)',
'xpack.fleet.internal.disableProxies (boolean)',
'xpack.fleet.internal.fleetServerStandalone (boolean)',
'xpack.fleet.developer.maxAgentPoliciesWithInactivityTimeout (number)',

View file

@ -49,6 +49,7 @@ export interface FleetConfigType {
disableILMPolicies: boolean;
disableProxies: boolean;
fleetServerStandalone: boolean;
activeAgentsSoftLimit?: number;
};
createArtifactsBulkBatchSize?: number;
}

View file

@ -0,0 +1,37 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiCallOut } from '@elastic/eui';
import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
import { useConfig } from '../../../../hooks';
export const AgentSoftLimitCallout = () => {
const config = useConfig();
return (
<EuiCallOut
iconType="warning"
color="warning"
title={
<FormattedMessage
id="xpack.fleet.agentSoftLimitCallout.calloutTitle"
defaultMessage="Max number of online agents reached"
/>
}
>
<FormattedMessage
id="xpack.fleet.agentSoftLimitCallout.calloutDescription"
defaultMessage="Fleet supports a maximum of {nbAgents} active agents. You need to unenroll some agents to ensure that all active agents are able to connect and new agents can be enrolled."
values={{
nbAgents: <FormattedNumber value={config.internal?.activeAgentsSoftLimit ?? 25000} />,
}}
/>
</EuiCallOut>
);
};

View file

@ -7,3 +7,4 @@
export { AgentActivityFlyout } from './agent_activity_flyout';
export { AgentActivityButton } from './agent_activity_button';
export { AgentSoftLimitCallout } from './agent_soft_limit_callout';

View file

@ -9,3 +9,4 @@ export { useUpdateTags } from './use_update_tags';
export { useActionStatus } from './use_action_status';
export { useLastSeenInactiveAgentsCount } from './use_last_seen_inactive_agents_count';
export { useInactiveAgentsCalloutHasBeenDismissed } from './use_inactive_agents_callout_has_been_dismissed';
export { useAgentSoftLimit } from './use_agent_soft_limit';

View file

@ -0,0 +1,62 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createFleetTestRendererMock } from '../../../../../../mock';
import { useConfig, sendGetAgents } from '../../../../hooks';
import { useAgentSoftLimit } from './use_agent_soft_limit';
jest.mock('../../../../hooks');
const mockedSendGetAgents = jest.mocked(sendGetAgents);
const mockedUseConfig = jest.mocked(useConfig);
describe('useAgentSoftLimit', () => {
beforeEach(() => {
mockedSendGetAgents.mockReset();
mockedUseConfig.mockReset();
});
it('should return shouldDisplayAgentSoftLimit:false if soft limit is not enabled in config', async () => {
const renderer = createFleetTestRendererMock();
mockedUseConfig.mockReturnValue({} as any);
const { result } = renderer.renderHook(() => useAgentSoftLimit());
expect(result.current.shouldDisplayAgentSoftLimit).toEqual(false);
expect(mockedSendGetAgents).not.toBeCalled();
});
it('should return shouldDisplayAgentSoftLimit:false if soft limit is enabled in config and there is less online agents than the limit', async () => {
const renderer = createFleetTestRendererMock();
mockedUseConfig.mockReturnValue({ internal: { activeAgentsSoftLimit: 10 } } as any);
mockedSendGetAgents.mockResolvedValue({
data: {
total: 5,
},
} as any);
const { result, waitForNextUpdate } = renderer.renderHook(() => useAgentSoftLimit());
await waitForNextUpdate();
expect(mockedSendGetAgents).toBeCalled();
expect(result.current.shouldDisplayAgentSoftLimit).toEqual(false);
});
it('should return shouldDisplayAgentSoftLimit:true if soft limit is enabled in config and there is more online agents than the limit', async () => {
const renderer = createFleetTestRendererMock();
mockedUseConfig.mockReturnValue({ internal: { activeAgentsSoftLimit: 10 } } as any);
mockedSendGetAgents.mockResolvedValue({
data: {
total: 15,
},
} as any);
const { result, waitForNextUpdate } = renderer.renderHook(() => useAgentSoftLimit());
await waitForNextUpdate();
expect(mockedSendGetAgents).toBeCalled();
expect(result.current.shouldDisplayAgentSoftLimit).toEqual(true);
});
});

View file

@ -0,0 +1,38 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useQuery } from '@tanstack/react-query';
import { useConfig, sendGetAgents } from '../../../../hooks';
async function fetchTotalOnlineAgents() {
const response = await sendGetAgents({
kuery: 'status:online',
perPage: 0,
showInactive: false,
});
if (response.error) {
throw new Error(response.error.message);
}
return response.data?.total ?? 0;
}
export function useAgentSoftLimit() {
const config = useConfig();
const softLimit = config.internal?.activeAgentsSoftLimit;
const { data: totalAgents } = useQuery(['fetch-total-online-agents'], fetchTotalOnlineAgents, {
enabled: softLimit !== undefined,
});
return {
shouldDisplayAgentSoftLimit: softLimit && totalAgents ? totalAgents > softLimit : false,
};
}

View file

@ -47,10 +47,11 @@ import { AgentTableHeader } from './components/table_header';
import type { SelectionMode } from './components/types';
import { SearchAndFilterBar } from './components/search_and_filter_bar';
import { TagsAddRemove } from './components/tags_add_remove';
import { AgentActivityFlyout } from './components';
import { AgentActivityFlyout, AgentSoftLimitCallout } from './components';
import { TableRowActions } from './components/table_row_actions';
import { AgentListTable } from './components/agent_list_table';
import { getKuery } from './utils/get_kuery';
import { useAgentSoftLimit } from './hooks';
const REFRESH_INTERVAL_MS = 30000;
@ -396,6 +397,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
const { isFleetServerStandalone } = useFleetServerStandalone();
const showUnhealthyCallout = isFleetServerUnhealthy && !isFleetServerStandalone;
const { shouldDisplayAgentSoftLimit } = useAgentSoftLimit();
const onClickAddFleetServer = useCallback(() => {
flyoutContext.openFleetServerFlyout();
}, [flyoutContext]);
@ -520,6 +523,12 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
<EuiSpacer size="l" />
</>
)}
{shouldDisplayAgentSoftLimit && (
<>
<AgentSoftLimitCallout />
<EuiSpacer size="l" />
</>
)}
{/* TODO serverless agent soft limit */}
{showUnhealthyCallout && (
<>

View file

@ -13,6 +13,7 @@ import { render as reactRender, act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import type { RenderHookResult } from '@testing-library/react-hooks';
import { Router } from '@kbn/shared-ux-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { themeServiceMock } from '@kbn/core/public/mocks';
@ -59,6 +60,8 @@ export interface TestRenderer {
setHeaderActionMenu: Function;
}
const queryClient = new QueryClient();
export const createFleetTestRendererMock = (): TestRenderer => {
const basePath = '/mock';
const extensions: UIExtensionsStorage = {};
@ -72,7 +75,11 @@ export const createFleetTestRendererMock = (): TestRenderer => {
return (
<startServices.i18n.Context>
<Router history={mountHistory}>
<KibanaContextProvider services={{ ...startServices }}>{children}</KibanaContextProvider>
<QueryClientProvider client={queryClient}>
<KibanaContextProvider services={{ ...startServices }}>
{children}
</KibanaContextProvider>
</QueryClientProvider>
</Router>
</startServices.i18n.Context>
);

View file

@ -42,6 +42,7 @@ export const config: PluginConfigDescriptor = {
internal: {
fleetServerStandalone: true,
disableProxies: true,
activeAgentsSoftLimit: true,
},
},
deprecations: ({ renameFromRoot, unused, unusedFromRoot }) => [
@ -176,6 +177,11 @@ export const config: PluginConfigDescriptor = {
fleetServerStandalone: schema.boolean({
defaultValue: false,
}),
activeAgentsSoftLimit: schema.maybe(
schema.number({
min: 0,
})
),
})
),
enabled: schema.boolean({ defaultValue: true }),