diff --git a/config/serverless.yml b/config/serverless.yml index 897b168340cd5..02e7a7ea07fb8 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -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 diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 8ec34f1b91ec4..e050ecf0a3ba8 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -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)', diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 6f8b643b44a74..a35760e35ba17 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -49,6 +49,7 @@ export interface FleetConfigType { disableILMPolicies: boolean; disableProxies: boolean; fleetServerStandalone: boolean; + activeAgentsSoftLimit?: number; }; createArtifactsBulkBatchSize?: number; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_soft_limit_callout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_soft_limit_callout.tsx new file mode 100644 index 0000000000000..68f986fc8c0f4 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_soft_limit_callout.tsx @@ -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 ( + + } + > + , + }} + /> + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/index.tsx index 05b481fb692b5..94b8565b83704 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/index.tsx @@ -7,3 +7,4 @@ export { AgentActivityFlyout } from './agent_activity_flyout'; export { AgentActivityButton } from './agent_activity_button'; +export { AgentSoftLimitCallout } from './agent_soft_limit_callout'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/index.tsx index a71ad1891b890..a270aadcd72e4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/index.tsx @@ -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'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.test.tsx new file mode 100644 index 0000000000000..4b9d908baa84d --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.test.tsx @@ -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); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.tsx new file mode 100644 index 0000000000000..e693838ca47bf --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.tsx @@ -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, + }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index cec075b4a8f7a..bfb728c792a93 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -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<{}> = () => { )} + {shouldDisplayAgentSoftLimit && ( + <> + + + + )} {/* TODO serverless agent soft limit */} {showUnhealthyCallout && ( <> diff --git a/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx index c562510634022..708bab59f1d79 100644 --- a/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx +++ b/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx @@ -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 ( - {children} + + + {children} + + ); diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index 9f499f52e0713..eaee8ec0cd3be 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -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 }),