diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/layouts/default/default.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/layouts/default/default.tsx index cc722771da6df..dc6e7cfc255d4 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/layouts/default/default.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/layouts/default/default.tsx @@ -15,8 +15,7 @@ import { useLink, useConfig, useAuthz, useStartServices } from '../../hooks'; import { WithHeaderLayout } from '../../../../layouts'; import { AutoUpgradeAgentsTour } from '../../sections/agent_policy/components/auto_upgrade_agents_tour'; - -import { ExperimentalFeaturesService } from '../../services'; +import { useCanEnableAutomaticAgentUpgrades } from '../../../../hooks/use_can_enable_auto_upgrades'; import { DefaultPageTitle } from './default_page_title'; @@ -36,7 +35,7 @@ export const DefaultLayout: React.FunctionComponent = ({ const authz = useAuthz(); const { docLinks } = useStartServices(); const granularPrivilegesCallout = useDismissableTour('GRANULAR_PRIVILEGES'); - const { enableAutomaticAgentUpgrades } = ExperimentalFeaturesService.get(); + const canEnableAutomaticAgentUpgrades = useCanEnableAutomaticAgentUpgrades(); const tabs = [ { @@ -147,7 +146,7 @@ export const DefaultLayout: React.FunctionComponent = ({ } rightColumn={rightColumn} tabs={tabs}> {children} - {enableAutomaticAgentUpgrades ? ( + {canEnableAutomaticAgentUpgrades ? ( ) : null} diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx index 1dece14fca5a6..c2b5627707ad6 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx @@ -18,12 +18,14 @@ import { } from '../../../components'; import { FLEET_SERVER_PACKAGE } from '../../../constants'; -import { ExperimentalFeaturesService, policyHasFleetServer } from '../../../services'; +import { policyHasFleetServer } from '../../../services'; import { AgentUpgradeAgentModal } from '../../agents/components'; import { ManageAutoUpgradeAgentsModal } from '../../agents/components/manage_auto_upgrade_agents_modal'; +import { useCanEnableAutomaticAgentUpgrades } from '../../../../../hooks/use_can_enable_auto_upgrades'; + import { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout'; import { AgentPolicyCopyProvider } from './agent_policy_copy_provider'; import { AgentPolicyDeleteProvider } from './agent_policy_delete_provider'; @@ -54,7 +56,7 @@ export const AgentPolicyActionMenu = memo<{ const [isManageAutoUpgradeAgentsModalOpen, setIsManageAutoUpgradeAgentsModalOpen] = useState(false); const refreshAgentPolicy = useAgentPolicyRefresh(); - const { enableAutomaticAgentUpgrades } = ExperimentalFeaturesService.get(); + const canEnableAutomaticAgentUpgrades = useCanEnableAutomaticAgentUpgrades(); const isFleetServerPolicy = useMemo( () => @@ -210,7 +212,7 @@ export const AgentPolicyActionMenu = memo<{ )} , viewPolicyItem, - ...(enableAutomaticAgentUpgrades ? [manageAutoUpgradeAgentsItem] : []), + ...(canEnableAutomaticAgentUpgrades ? [manageAutoUpgradeAgentsItem] : []), copyPolicyItem, deletePolicyItem, ]; diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx index e0f325ef68238..1f7e3217fd7df 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx @@ -31,7 +31,8 @@ import { FLEET_SERVER_PACKAGE } from '../../../../../../../../common/constants'; import { getRootIntegrations } from '../../../../../../../../common/services'; import { ManageAutoUpgradeAgentsModal } from '../../../../agents/components/manage_auto_upgrade_agents_modal'; import { AutoUpgradeAgentsTour } from '../../../components/auto_upgrade_agents_tour'; -import { ExperimentalFeaturesService } from '../../../../../services'; + +import { useCanEnableAutomaticAgentUpgrades } from '../../../../../../../hooks/use_can_enable_auto_upgrades'; import { ManageAutoUpgradeAgentsBadge } from './manage_auto_upgrade_agents'; @@ -64,7 +65,7 @@ export const HeaderRightContent: React.FunctionComponent(false); const refreshAgentPolicy = useAgentPolicyRefresh(); - const { enableAutomaticAgentUpgrades } = ExperimentalFeaturesService.get(); + const canEnableAutomaticAgentUpgrades = useCanEnableAutomaticAgentUpgrades(); const isFleetServerPolicy = useMemo( () => @@ -217,7 +218,7 @@ export const HeaderRightContent: React.FunctionComponent ) : item.label ? ( - + {item.label} @@ -281,7 +282,7 @@ export const HeaderRightContent: React.FunctionComponent )} - {enableAutomaticAgentUpgrades ? ( + {canEnableAutomaticAgentUpgrades ? ( ) : null} diff --git a/x-pack/platform/plugins/shared/fleet/public/hooks/use_can_enable_auto_upgrades.ts b/x-pack/platform/plugins/shared/fleet/public/hooks/use_can_enable_auto_upgrades.ts new file mode 100644 index 0000000000000..3c3b0dd55a174 --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/public/hooks/use_can_enable_auto_upgrades.ts @@ -0,0 +1,15 @@ +/* + * 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 { ExperimentalFeaturesService } from '../services'; + +import { licenseService } from './use_license'; + +export function useCanEnableAutomaticAgentUpgrades() { + const { enableAutomaticAgentUpgrades } = ExperimentalFeaturesService.get(); + return enableAutomaticAgentUpgrades && licenseService.isEnterprise(); +} diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts b/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts index 0cd892b0feef2..82260ee6efe8b 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/agent_policy/handlers.ts @@ -17,7 +17,12 @@ import { inputsFormat } from '../../../common/constants'; import { HTTPAuthorizationHeader } from '../../../common/http_authorization_header'; import { fullAgentPolicyToYaml } from '../../../common/services'; -import { appContextService, agentPolicyService, packagePolicyService } from '../../services'; +import { + appContextService, + agentPolicyService, + packagePolicyService, + licenseService, +} from '../../services'; import { type AgentClient, getLatestAvailableAgentVersion } from '../../services/agents'; import { AGENTS_PREFIX, @@ -310,6 +315,12 @@ export const getAutoUpgradeAgentsStatusHandler: FleetRequestHandler< const agentClient = fleetContext.agentClient.asCurrentUser; + if (!licenseService.isEnterprise()) { + throw new FleetUnauthorizedError( + 'Auto-upgrade agents feature requires at least Enterprise license' + ); + } + const body = await getAutoUpgradeAgentsStatus(agentClient, request.params.agentPolicyId); return response.ok({ body, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/required_versions.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/required_versions.test.ts index 35c413be4fafa..57f7e715a32dc 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/required_versions.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/required_versions.test.ts @@ -6,7 +6,9 @@ */ import { appContextService } from '..'; -import { AgentPolicyInvalidError } from '../../errors'; +import { AgentPolicyInvalidError, FleetUnauthorizedError } from '../../errors'; + +import { licenseService } from '..'; import { validateRequiredVersions } from './required_versions'; @@ -30,6 +32,10 @@ describe('validateRequiredVersions', () => { jest .spyOn(appContextService, 'getExperimentalFeatures') .mockReturnValue({ enableAutomaticAgentUpgrades: true } as any); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(true); + }); + afterEach(() => { + jest.spyOn(licenseService, 'isEnterprise').mockClear(); }); it('should throw error if duplicate versions', () => { @@ -45,6 +51,20 @@ describe('validateRequiredVersions', () => { ); }); + it('should throw error if license is not at least Enterprise', () => { + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); + expect(() => { + validateRequiredVersions('test policy', [ + { version: '9.0.0', percentage: 10 }, + { version: '9.0.0', percentage: 10 }, + ]); + }).toThrow( + new FleetUnauthorizedError( + `Agents auto upgrades feature requires at least Enterprise license` + ) + ); + }); + it('should throw error if has invalid semver version', () => { expect(() => { validateRequiredVersions('test policy', [ diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/required_versions.ts b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/required_versions.ts index f2b93c77e3a04..766d9bda3223e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/required_versions.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agent_policies/required_versions.ts @@ -7,8 +7,8 @@ import type { AgentTargetVersion } from '../../../common/types'; -import { AgentPolicyInvalidError } from '../../errors'; -import { appContextService } from '..'; +import { AgentPolicyInvalidError, FleetUnauthorizedError } from '../../errors'; +import { appContextService, licenseService } from '..'; import { checkTargetVersionsValidity } from '../../../common/services/agent_utils'; export function validateRequiredVersions( @@ -23,6 +23,11 @@ export function validateRequiredVersions( `Policy "${name}" failed validation: required_versions are not allowed when automatic upgrades feature is disabled` ); } + if (requiredVersions && !licenseService.isEnterprise()) { + throw new FleetUnauthorizedError( + 'Agents auto upgrades feature requires at least Enterprise license' + ); + } const error = checkTargetVersionsValidity(requiredVersions); if (error) { throw new AgentPolicyInvalidError( diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/automatic_agent_upgrade_task.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/automatic_agent_upgrade_task.test.ts index c3f55818a09c0..f625857f3b24a 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/automatic_agent_upgrade_task.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/automatic_agent_upgrade_task.test.ts @@ -13,7 +13,7 @@ import { TaskStatus } from '@kbn/task-manager-plugin/server'; import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; import { createAppContextStartContractMock } from '../mocks'; -import { agentPolicyService, appContextService } from '../services'; +import { agentPolicyService, appContextService, licenseService } from '../services'; import { fetchAllAgentsByKuery, getAgentsByKuery, @@ -106,6 +106,8 @@ describe('AutomaticAgentUpgradeTask', () => { let mockTaskManagerSetup: jest.Mocked; beforeEach(() => { + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(true); + mockContract = createAppContextStartContractMock(); appContextService.start(mockContract); mockCore = coreSetupMock(); @@ -123,6 +125,7 @@ describe('AutomaticAgentUpgradeTask', () => { afterEach(() => { jest.clearAllMocks(); + jest.spyOn(licenseService, 'isEnterprise').mockClear(); }); describe('Task lifecycle', () => { @@ -181,6 +184,14 @@ describe('AutomaticAgentUpgradeTask', () => { expect(mockAgentPolicyService.fetchAllAgentPolicies).not.toHaveBeenCalled(); }); + it('Should exit if the license is not at least Enterprise', async () => { + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); + + await runTask(); + + expect(mockAgentPolicyService.fetchAllAgentPolicies).not.toHaveBeenCalled(); + }); + it('Should upgrade eligible agents', async () => { const agents = generateAgents(10); mockedGetAgentsByKuery diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/automatic_agent_upgrade_task.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/automatic_agent_upgrade_task.ts index 8248ad071eefa..16f10bcd9aba6 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/automatic_agent_upgrade_task.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/automatic_agent_upgrade_task.ts @@ -31,7 +31,7 @@ import type { FleetServerAgentMetadata, } from '../../common/types'; -import { agentPolicyService, appContextService } from '../services'; +import { agentPolicyService, appContextService, licenseService } from '../services'; import { fetchAllAgentsByKuery, getAgentsByKuery, @@ -141,6 +141,12 @@ export class AutomaticAgentUpgradeTask { ); return; } + if (!licenseService.isEnterprise()) { + this.logger.debug( + '[AutomaticAgentUpgradeTask] Aborting runTask: automatic upgrades feature requires at least Enterprise license' + ); + return; + } if (!this.wasStarted) { this.logger.debug('[AutomaticAgentUpgradeTask] Aborting runTask(): task not started yet'); @@ -376,6 +382,9 @@ export class AutomaticAgentUpgradeTask { numberOfAgentsForUpgrade -= numberOfRetriedAgents; if (numberOfAgentsForUpgrade <= 0) { + this.logger.debug( + `[AutomaticAgentUpgradeTask] Number of agents ${numberOfAgentsForUpgrade}: no candidate agents found for upgrade (target version: ${requiredVersion.version}, percentage: ${requiredVersion.percentage})` + ); return; }