mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] added modal to manage auto upgrade agents (#206955)
## Summary Related to https://github.com/elastic/ingest-dev/issues/4721 Added Agent policy action `Manage auto-upgrade agents` to edit `required_versions` config of agent policy on the UI. Also added it to the Agent policy details header to open the modal. To test: - enable FF in `kibana.dev.yml` - `xpack.fleet.enableExperimental: ['enableAutomaticAgentUpgrades']` - navigate to an Agent policy, click Actions and click `Manage auto-upgrade agents` <img width="1197" alt="image" src="https://github.com/user-attachments/assets/de1afa92-c155-4072-8ed6-7e8263480199" /> Added validation on the UI (same as in the API) to prevent duplicate versions, more than 100 percentages, empty percentage. Added a callout to show how many agents are impacted by the update. <img width="1044" alt="image" src="https://github.com/user-attachments/assets/c889da73-ac8f-4c74-9bc5-113cb5b902c8" /> <img width="825" alt="image" src="https://github.com/user-attachments/assets/cd9d797d-4343-44e8-bf29-b8683d5f83d4" /> Added `Auto-upgrade agents` with a `Manage` link to Agent policy details header <img width="1029" alt="image" src="https://github.com/user-attachments/assets/733a21e6-4c78-4e83-b2cf-00a757921410" /> Moved the percentage helptext to a tooltip: <img width="794" alt="image" src="https://github.com/user-attachments/assets/3e133a50-e2aa-4de3-a49d-f312648fdc21" /> ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [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
This commit is contained in:
parent
3498d509ef
commit
ff85b0fbff
15 changed files with 700 additions and 174 deletions
|
@ -4,6 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import semverValid from 'semver/functions/valid';
|
||||
|
||||
import type { AgentTargetVersion } from '../types';
|
||||
|
||||
export function removeSOAttributes(kuery: string): string {
|
||||
return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, '');
|
||||
|
@ -20,3 +23,25 @@ export function getSortConfig(
|
|||
: [];
|
||||
return [{ [sortField]: { order: sortOrder } }, ...secondarySort];
|
||||
}
|
||||
|
||||
export function checkTargetVersionsValidity(
|
||||
requiredVersions: AgentTargetVersion[]
|
||||
): string | undefined {
|
||||
const versions = requiredVersions.map((v) => v.version);
|
||||
const uniqueVersions = new Set(versions);
|
||||
if (versions.length !== uniqueVersions.size) {
|
||||
return `duplicate versions not allowed`;
|
||||
}
|
||||
if (requiredVersions.some((item) => !item.percentage)) {
|
||||
return `percentage is required`;
|
||||
}
|
||||
for (const version of versions) {
|
||||
if (!semverValid(version)) {
|
||||
return `invalid semver version ${version}`;
|
||||
}
|
||||
}
|
||||
const sumOfPercentages = requiredVersions.reduce((acc, v) => acc + v.percentage, 0);
|
||||
if (sumOfPercentages > 100) {
|
||||
return `sum of percentages cannot exceed 100`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,4 +92,4 @@ export {
|
|||
isAgentVersionLessThanFleetServer,
|
||||
} from './check_fleet_server_versions';
|
||||
|
||||
export { removeSOAttributes, getSortConfig } from './agent_utils';
|
||||
export { removeSOAttributes, getSortConfig, checkTargetVersionsValidity } from './agent_utils';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { setupFleetServer } from '../tasks/fleet_server';
|
||||
import { AGENT_FLYOUT, AGENT_POLICY_DETAILS_PAGE } from '../screens/fleet';
|
||||
import { login } from '../tasks/login';
|
||||
import { visit } from '../tasks/common';
|
||||
|
||||
describe('Edit agent policy', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -37,7 +38,7 @@ describe('Edit agent policy', () => {
|
|||
});
|
||||
|
||||
it('should edit agent policy', () => {
|
||||
cy.visit('/app/fleet/policies/policy-1/settings');
|
||||
visit('/app/fleet/policies/policy-1/settings');
|
||||
cy.get('[placeholder="Optional description"').clear().type('desc');
|
||||
|
||||
cy.intercept('/api/fleet/agent_policies/policy-1', {
|
||||
|
@ -135,7 +136,7 @@ describe('Edit agent policy', () => {
|
|||
},
|
||||
});
|
||||
|
||||
cy.visit('/app/fleet/policies/policy-1');
|
||||
visit('/app/fleet/policies/policy-1');
|
||||
|
||||
cy.getBySel(AGENT_POLICY_DETAILS_PAGE.ADD_AGENT_LINK).click();
|
||||
cy.getBySel(AGENT_FLYOUT.KUBERNETES_PLATFORM_TYPE).click();
|
||||
|
|
|
@ -80,6 +80,10 @@ const disableNewFeaturesTours = (window: Window) => {
|
|||
});
|
||||
};
|
||||
|
||||
const disableFleetTours = (window: Window) => {
|
||||
window.localStorage.setItem('fleet.autoUpgradeAgentsTour', JSON.stringify({ active: false }));
|
||||
};
|
||||
|
||||
export const waitForPageToBeLoaded = () => {
|
||||
cy.get(LOADING_INDICATOR_HIDDEN).should('exist');
|
||||
cy.get(LOADING_INDICATOR).should('not.exist');
|
||||
|
@ -115,6 +119,7 @@ export const visit = (url: string, options: Partial<Cypress.VisitOptions> = {},
|
|||
options.onBeforeLoad?.(win);
|
||||
|
||||
disableNewFeaturesTours(win);
|
||||
disableFleetTours(win);
|
||||
},
|
||||
onLoad: (win) => {
|
||||
options.onLoad?.(win);
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { visit } from './common';
|
||||
|
||||
export const INTEGRATIONS = 'app/integrations#/';
|
||||
export const FLEET = 'app/fleet/';
|
||||
export const LOGIN_API_ENDPOINT = '/internal/security/login';
|
||||
|
@ -16,5 +18,5 @@ export const hostDetailsUrl = (hostName: string) =>
|
|||
`/app/security/hosts/${hostName}/authentications`;
|
||||
|
||||
export const navigateTo = (page: string) => {
|
||||
cy.visit(page);
|
||||
visit(page);
|
||||
};
|
||||
|
|
|
@ -14,6 +14,8 @@ import type { Section } from '../../sections';
|
|||
import { useLink, useConfig, useAuthz, useStartServices } from '../../hooks';
|
||||
import { WithHeaderLayout } from '../../../../layouts';
|
||||
|
||||
import { AutoUpgradeAgentsTour } from '../../sections/agent_policy/components/auto_upgrade_agents_tour';
|
||||
|
||||
import { DefaultPageTitle } from './default_page_title';
|
||||
|
||||
interface Props {
|
||||
|
@ -56,6 +58,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
|
|||
isSelected: section === 'agent_policies',
|
||||
href: getHref('policies_list'),
|
||||
'data-test-subj': 'fleet-agent-policies-tab',
|
||||
id: 'fleet-agent-policies-tab',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
|
@ -141,6 +144,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
|
|||
<WithHeaderLayout leftColumn={<DefaultPageTitle />} rightColumn={rightColumn} tabs={tabs}>
|
||||
{children}
|
||||
</WithHeaderLayout>
|
||||
<AutoUpgradeAgentsTour anchor="#fleet-agent-policies-tab" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
|
||||
|
||||
import type { AgentPolicy } from '../../../types';
|
||||
import { useAuthz } from '../../../hooks';
|
||||
import { useAgentPolicyRefresh, useAuthz } from '../../../hooks';
|
||||
import {
|
||||
AgentEnrollmentFlyout,
|
||||
ContextMenuActions,
|
||||
|
@ -22,6 +22,8 @@ import { policyHasFleetServer } from '../../../services';
|
|||
|
||||
import { AgentUpgradeAgentModal } from '../../agents/components';
|
||||
|
||||
import { ManageAutoUpgradeAgentsModal } from '../../agents/components/manage_auto_upgrade_agents_modal';
|
||||
|
||||
import { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout';
|
||||
import { AgentPolicyCopyProvider } from './agent_policy_copy_provider';
|
||||
import { AgentPolicyDeleteProvider } from './agent_policy_delete_provider';
|
||||
|
@ -49,6 +51,9 @@ export const AgentPolicyActionMenu = memo<{
|
|||
const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] =
|
||||
useState<boolean>(false);
|
||||
const [isUpgradeAgentsModalOpen, setIsUpgradeAgentsModalOpen] = useState<boolean>(false);
|
||||
const [isManageAutoUpgradeAgentsModalOpen, setIsManageAutoUpgradeAgentsModalOpen] =
|
||||
useState<boolean>(false);
|
||||
const refreshAgentPolicy = useAgentPolicyRefresh();
|
||||
|
||||
const isFleetServerPolicy = useMemo(
|
||||
() =>
|
||||
|
@ -98,6 +103,23 @@ export const AgentPolicyActionMenu = memo<{
|
|||
</EuiContextMenuItem>
|
||||
);
|
||||
|
||||
const manageAutoUpgradeAgentsItem = (
|
||||
<EuiContextMenuItem
|
||||
icon="gear"
|
||||
disabled={!authz.fleet.allAgentPolicies}
|
||||
onClick={() => {
|
||||
setIsContextMenuOpen(false);
|
||||
setIsManageAutoUpgradeAgentsModalOpen(!isManageAutoUpgradeAgentsModalOpen);
|
||||
}}
|
||||
key="manageAutoUpgradeAgents"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentPolicyActionMenu.manageAutoUpgradeAgentsText"
|
||||
defaultMessage="Manage auto-upgrade agents"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
|
||||
const deletePolicyItem = (
|
||||
<AgentPolicyDeleteProvider
|
||||
hasFleetServer={policyHasFleetServer(agentPolicy as AgentPolicy)}
|
||||
|
@ -187,6 +209,7 @@ export const AgentPolicyActionMenu = memo<{
|
|||
)}
|
||||
</EuiContextMenuItem>,
|
||||
viewPolicyItem,
|
||||
manageAutoUpgradeAgentsItem,
|
||||
copyPolicyItem,
|
||||
deletePolicyItem,
|
||||
];
|
||||
|
@ -273,6 +296,20 @@ export const AgentPolicyActionMenu = memo<{
|
|||
/>
|
||||
</EuiPortal>
|
||||
)}
|
||||
{isManageAutoUpgradeAgentsModalOpen && (
|
||||
<EuiPortal>
|
||||
<ManageAutoUpgradeAgentsModal
|
||||
agentPolicy={agentPolicy}
|
||||
agentCount={agentPolicy.agents || 0}
|
||||
onClose={(refreshPolicy: boolean) => {
|
||||
setIsManageAutoUpgradeAgentsModalOpen(false);
|
||||
if (refreshPolicy) {
|
||||
refreshAgentPolicy();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiPortal>
|
||||
)}
|
||||
{isUninstallCommandFlyoutOpen && (
|
||||
<UninstallCommandFlyout
|
||||
target="agent"
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { EuiText, EuiTourStep } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { TOUR_STORAGE_CONFIG } from '../../../constants';
|
||||
import { TOUR_STORAGE_KEYS } from '../../../constants';
|
||||
import { useStartServices } from '../../../hooks';
|
||||
|
||||
export const AutoUpgradeAgentsTour: React.FC<{ anchor: string }> = ({ anchor }) => {
|
||||
const { storage, uiSettings } = useStartServices();
|
||||
|
||||
const [tourState, setTourState] = useState({ isOpen: true });
|
||||
|
||||
const isTourHidden =
|
||||
uiSettings.get('hideAnnouncements', false) ||
|
||||
(
|
||||
storage.get(TOUR_STORAGE_KEYS.AUTO_UPGRADE_AGENTS) as
|
||||
| TOUR_STORAGE_CONFIG['AUTO_UPGRADE_AGENTS']
|
||||
| undefined
|
||||
)?.active === false;
|
||||
|
||||
const setTourAsHidden = () => {
|
||||
storage.set(TOUR_STORAGE_KEYS.AUTO_UPGRADE_AGENTS, {
|
||||
active: false,
|
||||
} as TOUR_STORAGE_CONFIG['AUTO_UPGRADE_AGENTS']);
|
||||
};
|
||||
|
||||
const onFinish = () => {
|
||||
setTourState({ isOpen: false });
|
||||
setTourAsHidden();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTourStep
|
||||
content={
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.autoUpgradeAgentsTour.tourContent"
|
||||
defaultMessage="Select your policy and configure target agent versions for automatic upgrades."
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
isStepOpen={!isTourHidden && tourState.isOpen}
|
||||
onFinish={onFinish}
|
||||
minWidth={360}
|
||||
maxWidth={360}
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.autoUpgradeAgentsTour.tourTitle"
|
||||
defaultMessage="Auto-upgrade agents"
|
||||
/>
|
||||
}
|
||||
anchorPosition="downLeft"
|
||||
anchor={anchor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedDate, FormattedMessage } from '@kbn/i18n-react';
|
||||
import styled from 'styled-components';
|
||||
|
@ -20,14 +20,18 @@ import {
|
|||
EuiLink,
|
||||
EuiToolTip,
|
||||
EuiIconTip,
|
||||
EuiPortal,
|
||||
EuiNotificationBadge,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useAuthz, useLink } from '../../../../../hooks';
|
||||
import { useAgentPolicyRefresh, useAuthz, useLink } from '../../../../../hooks';
|
||||
import type { AgentPolicy } from '../../../../../types';
|
||||
import { AgentPolicyActionMenu, LinkedAgentCount } from '../../../components';
|
||||
import { AddAgentHelpPopover } from '../../../../../components';
|
||||
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';
|
||||
|
||||
export interface HeaderRightContentProps {
|
||||
isLoading: boolean;
|
||||
|
@ -55,6 +59,9 @@ export const HeaderRightContent: React.FunctionComponent<HeaderRightContentProps
|
|||
const authz = useAuthz();
|
||||
const { getPath } = useLink();
|
||||
const history = useHistory();
|
||||
const [isManageAutoUpgradeAgentsModalOpen, setIsManageAutoUpgradeAgentsModalOpen] =
|
||||
useState<boolean>(false);
|
||||
const refreshAgentPolicy = useAgentPolicyRefresh();
|
||||
|
||||
const isFleetServerPolicy = useMemo(
|
||||
() =>
|
||||
|
@ -84,154 +91,215 @@ export const HeaderRightContent: React.FunctionComponent<HeaderRightContentProps
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup justifyContent={'flexEnd'} direction="row">
|
||||
{isLoading || !agentPolicy
|
||||
? null
|
||||
: [
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.revision', {
|
||||
defaultMessage: 'Revision',
|
||||
}),
|
||||
content: agentPolicy.revision ?? 0,
|
||||
},
|
||||
{ isDivider: true },
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.integrations', {
|
||||
defaultMessage: 'Integrations',
|
||||
}),
|
||||
content: (
|
||||
<EuiI18nNumber
|
||||
value={(agentPolicy.package_policies && agentPolicy.package_policies.length) || 0}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ isDivider: true },
|
||||
...(authz.fleet.readAgents && !agentPolicy?.supports_agentless
|
||||
? [
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.usedBy', {
|
||||
defaultMessage: 'Agents',
|
||||
}),
|
||||
content:
|
||||
!agentPolicy.agents && isFleetServerPolicy && authz.fleet.addFleetServers ? (
|
||||
<AddAgentHelpPopover
|
||||
button={addFleetServerLink}
|
||||
isOpen={isAddAgentHelpPopoverOpen}
|
||||
offset={15}
|
||||
closePopover={() => {
|
||||
setIsAddAgentHelpPopoverOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : !agentPolicy.agents && !isFleetServerPolicy && authz.fleet.addAgents ? (
|
||||
<AddAgentHelpPopover
|
||||
button={addAgentLink}
|
||||
isOpen={isAddAgentHelpPopoverOpen}
|
||||
offset={15}
|
||||
closePopover={() => {
|
||||
setIsAddAgentHelpPopoverOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.policyDetails.summary.usedByUnprivilegedTooltip"
|
||||
defaultMessage="{count, plural, one {# unprivileged agent} other {# unprivileged agents}}"
|
||||
values={{ count: agentPolicy.unprivileged_agents || 0 }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.policyDetails.summary.usedByPrivilegedTooltip"
|
||||
defaultMessage="{count, plural, one {# privileged agent} other {# privileged agents}}"
|
||||
values={{
|
||||
count:
|
||||
(agentPolicy.agents || 0) -
|
||||
(agentPolicy.unprivileged_agents || 0),
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<LinkedAgentCount
|
||||
count={agentPolicy.agents || 0}
|
||||
agentPolicyId={agentPolicy.id}
|
||||
showAgentText={true}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
{getRootIntegrations(agentPolicy.package_policies || []).length > 0 &&
|
||||
(agentPolicy.unprivileged_agents || 0) > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
type="warning"
|
||||
color="warning"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.policyDetails.summary.containsUnprivilegedAgentsWarning"
|
||||
defaultMessage="This agent policy contains integrations that require Elastic Agents to have root privileges. Some enrolled agents are running in unprivileged mode."
|
||||
/>
|
||||
}
|
||||
<>
|
||||
<EuiFlexGroup justifyContent={'flexEnd'} direction="row">
|
||||
{isLoading || !agentPolicy
|
||||
? null
|
||||
: [
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.revision', {
|
||||
defaultMessage: 'Revision',
|
||||
}),
|
||||
content: agentPolicy.revision ?? 0,
|
||||
},
|
||||
{ isDivider: true },
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.integrations', {
|
||||
defaultMessage: 'Integrations',
|
||||
}),
|
||||
content: (
|
||||
<EuiI18nNumber
|
||||
value={
|
||||
(agentPolicy.package_policies && agentPolicy.package_policies.length) || 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ isDivider: true },
|
||||
...(authz.fleet.readAgents && !agentPolicy?.supports_agentless
|
||||
? [
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.usedBy', {
|
||||
defaultMessage: 'Agents',
|
||||
}),
|
||||
content:
|
||||
!agentPolicy.agents &&
|
||||
isFleetServerPolicy &&
|
||||
authz.fleet.addFleetServers ? (
|
||||
<AddAgentHelpPopover
|
||||
button={addFleetServerLink}
|
||||
isOpen={isAddAgentHelpPopoverOpen}
|
||||
offset={15}
|
||||
closePopover={() => {
|
||||
setIsAddAgentHelpPopoverOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : !agentPolicy.agents && !isFleetServerPolicy && authz.fleet.addAgents ? (
|
||||
<AddAgentHelpPopover
|
||||
button={addAgentLink}
|
||||
isOpen={isAddAgentHelpPopoverOpen}
|
||||
offset={15}
|
||||
closePopover={() => {
|
||||
setIsAddAgentHelpPopoverOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.policyDetails.summary.usedByUnprivilegedTooltip"
|
||||
defaultMessage="{count, plural, one {# unprivileged agent} other {# unprivileged agents}}"
|
||||
values={{ count: agentPolicy.unprivileged_agents || 0 }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.policyDetails.summary.usedByPrivilegedTooltip"
|
||||
defaultMessage="{count, plural, one {# privileged agent} other {# privileged agents}}"
|
||||
values={{
|
||||
count:
|
||||
(agentPolicy.agents || 0) -
|
||||
(agentPolicy.unprivileged_agents || 0),
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<LinkedAgentCount
|
||||
count={agentPolicy.agents || 0}
|
||||
agentPolicyId={agentPolicy.id}
|
||||
showAgentText={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
{getRootIntegrations(agentPolicy.package_policies || []).length > 0 &&
|
||||
(agentPolicy.unprivileged_agents || 0) > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
type="warning"
|
||||
color="warning"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.policyDetails.summary.containsUnprivilegedAgentsWarning"
|
||||
defaultMessage="This agent policy contains integrations that require Elastic Agents to have root privileges. Some enrolled agents are running in unprivileged mode."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{ isDivider: true },
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.lastUpdated', {
|
||||
defaultMessage: 'Last updated on',
|
||||
}),
|
||||
content:
|
||||
(agentPolicy && (
|
||||
<FormattedDate
|
||||
value={agentPolicy?.updated_at}
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="2-digit"
|
||||
/>
|
||||
)) ||
|
||||
'',
|
||||
},
|
||||
{ isDivider: true },
|
||||
...(authz.fleet.allAgentPolicies
|
||||
? [
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.autoUpgrade', {
|
||||
defaultMessage: 'Auto-upgrade agents',
|
||||
}),
|
||||
content: (
|
||||
<EuiFlexGroup
|
||||
gutterSize="xs"
|
||||
justifyContent="flexEnd"
|
||||
alignItems="center"
|
||||
id="auto-upgrade-manage-button"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
setIsManageAutoUpgradeAgentsModalOpen(
|
||||
!isManageAutoUpgradeAgentsModalOpen
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.policyDetails.summary.autoUpgradeButton"
|
||||
defaultMessage="Manage"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiNotificationBadge
|
||||
color={agentPolicy.required_versions?.length ? 'accent' : 'subdued'}
|
||||
>
|
||||
{agentPolicy.required_versions?.length || 0}
|
||||
</EuiNotificationBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{ isDivider: true },
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: i18n.translate('xpack.fleet.policyDetails.summary.lastUpdated', {
|
||||
defaultMessage: 'Last updated on',
|
||||
}),
|
||||
content:
|
||||
(agentPolicy && (
|
||||
<FormattedDate
|
||||
value={agentPolicy?.updated_at}
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="2-digit"
|
||||
},
|
||||
{ isDivider: true },
|
||||
]
|
||||
: []),
|
||||
{
|
||||
content: agentPolicy && (
|
||||
<AgentPolicyActionMenu
|
||||
agentPolicy={agentPolicy}
|
||||
fullButton={true}
|
||||
onCopySuccess={(newAgentPolicy: AgentPolicy) => {
|
||||
history.push(getPath('policy_details', { policyId: newAgentPolicy.id }));
|
||||
}}
|
||||
onCancelEnrollment={onCancelEnrollment}
|
||||
/>
|
||||
)) ||
|
||||
'',
|
||||
},
|
||||
{ isDivider: true },
|
||||
{
|
||||
content: agentPolicy && (
|
||||
<AgentPolicyActionMenu
|
||||
agentPolicy={agentPolicy}
|
||||
fullButton={true}
|
||||
onCopySuccess={(newAgentPolicy: AgentPolicy) => {
|
||||
history.push(getPath('policy_details', { policyId: newAgentPolicy.id }));
|
||||
}}
|
||||
onCancelEnrollment={onCancelEnrollment}
|
||||
/>
|
||||
),
|
||||
},
|
||||
].map((item, index) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
{item.isDivider ?? false ? (
|
||||
<Divider />
|
||||
) : item.label ? (
|
||||
<EuiDescriptionList compressed textStyle="reverse" style={{ textAlign: 'right' }}>
|
||||
<EuiDescriptionListTitle className="eui-textNoWrap">
|
||||
{item.label}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
{item.content}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
) : (
|
||||
item.content
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
].map((item, index) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
{item.isDivider ?? false ? (
|
||||
<Divider />
|
||||
) : item.label ? (
|
||||
<EuiDescriptionList compressed textStyle="reverse" style={{ textAlign: 'right' }}>
|
||||
<EuiDescriptionListTitle className="eui-textNoWrap">
|
||||
{item.label}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
{item.content}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
) : (
|
||||
item.content
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
{isManageAutoUpgradeAgentsModalOpen && (
|
||||
<EuiPortal>
|
||||
<ManageAutoUpgradeAgentsModal
|
||||
agentPolicy={agentPolicy}
|
||||
agentCount={agentPolicy.agents || 0}
|
||||
onClose={(refreshPolicy: boolean) => {
|
||||
setIsManageAutoUpgradeAgentsModalOpen(false);
|
||||
if (refreshPolicy) {
|
||||
refreshAgentPolicy();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiPortal>
|
||||
)}
|
||||
<AutoUpgradeAgentsTour anchor="#auto-upgrade-manage-button" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiConfirmModal,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiSpacer,
|
||||
EuiSuperSelect,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { AgentTargetVersion } from '../../../../../../../common/types';
|
||||
|
||||
import type { AgentPolicy } from '../../../../../../../common';
|
||||
import { useGetAgentsAvailableVersionsQuery, useStartServices } from '../../../../../../hooks';
|
||||
import { checkTargetVersionsValidity } from '../../../../../../../common/services';
|
||||
import { sendUpdateAgentPolicyForRq } from '../../../../../../hooks/use_request/agent_policy';
|
||||
|
||||
export interface ManageAutoUpgradeAgentsModalProps {
|
||||
onClose: (refreshPolicy: boolean) => void;
|
||||
agentPolicy: AgentPolicy;
|
||||
agentCount: number;
|
||||
}
|
||||
|
||||
export const ManageAutoUpgradeAgentsModal: React.FunctionComponent<
|
||||
ManageAutoUpgradeAgentsModalProps
|
||||
> = ({ onClose, agentPolicy, agentCount }) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { notifications } = useStartServices();
|
||||
const [targetVersions, setTargetVersions] = useState(agentPolicy.required_versions || []);
|
||||
const { data: agentsAvailableVersions } = useGetAgentsAvailableVersionsQuery({
|
||||
enabled: true,
|
||||
});
|
||||
const latestVersion = agentsAvailableVersions?.items[0];
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
const submitUpdateAgentPolicy = async () => {
|
||||
setIsLoading(true);
|
||||
let isSuccess = false;
|
||||
try {
|
||||
await sendUpdateAgentPolicyForRq(agentPolicy.id, {
|
||||
name: agentPolicy.name,
|
||||
namespace: agentPolicy.namespace,
|
||||
required_versions: targetVersions,
|
||||
});
|
||||
notifications.toasts.addSuccess(
|
||||
i18n.translate('xpack.fleet.manageAutoUpgradeAgents.successNotificationTitle', {
|
||||
defaultMessage: "Successfully updated ''{name}'' auto-upgrade agents settings",
|
||||
values: { name: agentPolicy.name },
|
||||
})
|
||||
);
|
||||
isSuccess = true;
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.fleet.manageAutoUpgradeAgents.errorNotificationTitle', {
|
||||
defaultMessage: 'Unable to update agent policy',
|
||||
})
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
onClose(isSuccess);
|
||||
};
|
||||
|
||||
async function onSubmit() {
|
||||
await submitUpdateAgentPolicy();
|
||||
}
|
||||
|
||||
async function updateTargetVersions(newVersions: AgentTargetVersion[]) {
|
||||
const error = checkTargetVersionsValidity(newVersions);
|
||||
setErrors(error ? [error] : []);
|
||||
|
||||
setTargetVersions(newVersions);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
data-test-subj="manageAutoUpgradeAgentsModal"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.modalTitle"
|
||||
defaultMessage="Manage auto-upgrade agents"
|
||||
/>
|
||||
}
|
||||
onCancel={() => onClose(false)}
|
||||
onConfirm={onSubmit}
|
||||
confirmButtonDisabled={isLoading || errors.length > 0}
|
||||
cancelButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
}
|
||||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
}
|
||||
style={{ width: 1000 }}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
{agentCount > 0 ? (
|
||||
<>
|
||||
<EuiCallOut
|
||||
iconType="iInCircle"
|
||||
title={i18n.translate('xpack.fleet.manageAutoUpgradeAgents.calloutTitle', {
|
||||
defaultMessage:
|
||||
'This action will update {agentCount, plural, one {# agent} other {# agents}}',
|
||||
values: {
|
||||
agentCount,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.descriptionText"
|
||||
defaultMessage="Add the target agent version for automatic upgrades."
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiForm isInvalid={errors.length > 0} error={errors} component="form">
|
||||
{targetVersions.map((requiredVersion, index) => (
|
||||
<>
|
||||
<TargetVersionsRow
|
||||
agentsAvailableVersions={agentsAvailableVersions?.items || []}
|
||||
requiredVersion={requiredVersion}
|
||||
key={index}
|
||||
onRemove={() => {
|
||||
updateTargetVersions(targetVersions.filter((_, i) => i !== index));
|
||||
}}
|
||||
onUpdate={(version: string, percentage: number) => {
|
||||
updateTargetVersions(
|
||||
targetVersions.map((targetVersion, i) =>
|
||||
i === index ? { version, percentage } : targetVersion
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
))}
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
updateTargetVersions([
|
||||
...targetVersions,
|
||||
{
|
||||
version: latestVersion || '',
|
||||
percentage: targetVersions.length === 0 ? 100 : 1,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.addVersionButton"
|
||||
defaultMessage="Add target version"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
const TargetVersionsRow: React.FunctionComponent<{
|
||||
agentsAvailableVersions: string[];
|
||||
requiredVersion: AgentTargetVersion;
|
||||
onRemove: () => void;
|
||||
onUpdate: (version: string, percentage: number) => void;
|
||||
}> = ({ agentsAvailableVersions, requiredVersion, onRemove, onUpdate }) => {
|
||||
const options = agentsAvailableVersions.map((version) => ({
|
||||
value: version,
|
||||
inputDisplay: version,
|
||||
}));
|
||||
|
||||
const [version, setVersion] = useState(requiredVersion.version);
|
||||
|
||||
const onVersionChange = (value: string) => {
|
||||
setVersion(value);
|
||||
};
|
||||
|
||||
const [percentage, setPercentage] = useState(requiredVersion.percentage);
|
||||
|
||||
const onPercentageChange = (value: number) => {
|
||||
setPercentage(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" alignItems="flexEnd">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.targetAgentVersionTitle"
|
||||
defaultMessage="Target agent version"
|
||||
/>
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
content={
|
||||
<FormattedMessage
|
||||
data-test-subj="targetVersionTooltip"
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.targetVersionTooltip"
|
||||
defaultMessage="You can only downgrade agents manually."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
options={options}
|
||||
valueOfSelected={version}
|
||||
onChange={(value) => {
|
||||
onVersionChange(value);
|
||||
onUpdate(value, percentage);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.percentageTitle"
|
||||
defaultMessage="% of agents to upgrade"
|
||||
/>
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
content={
|
||||
<FormattedMessage
|
||||
data-test-subj="percentageTooltip"
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.percentageTooltip"
|
||||
defaultMessage="Set 100 to upgrade all agents in the policy."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
value={percentage}
|
||||
onChange={(e) => {
|
||||
const newValue = parseInt(e.target.value, 10);
|
||||
onPercentageChange(newValue);
|
||||
onUpdate(version, newValue);
|
||||
}}
|
||||
min={0}
|
||||
step={1}
|
||||
max={100}
|
||||
required
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label="">
|
||||
<EuiButton onClick={onRemove} color="text">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.removeVersionButton"
|
||||
defaultMessage="Remove"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -55,6 +55,7 @@ export const TOUR_STORAGE_KEYS = {
|
|||
INACTIVE_AGENTS: 'fleet.inactiveAgentsTour',
|
||||
GRANULAR_PRIVILEGES: 'fleet.granularPrivileges',
|
||||
AGENT_EXPORT_CSV: 'fleet.agentExportCSVTour',
|
||||
AUTO_UPGRADE_AGENTS: 'fleet.autoUpgradeAgentsTour',
|
||||
};
|
||||
|
||||
export interface TourConfig {
|
||||
|
|
|
@ -156,6 +156,18 @@ export const sendUpdateAgentPolicy = (
|
|||
});
|
||||
};
|
||||
|
||||
export const sendUpdateAgentPolicyForRq = (
|
||||
agentPolicyId: string,
|
||||
body: UpdateAgentPolicyRequest['body']
|
||||
) => {
|
||||
return sendRequestForRq<UpdateAgentPolicyResponse>({
|
||||
path: agentPolicyRouteService.getUpdatePath(agentPolicyId),
|
||||
method: 'put',
|
||||
body: JSON.stringify(body),
|
||||
version: API_VERSIONS.public.v1,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCopyAgentPolicy = (
|
||||
agentPolicyId: string,
|
||||
body: CopyAgentPolicyRequest['body']
|
||||
|
|
|
@ -351,6 +351,21 @@ export function sendGetAgentsAvailableVersions() {
|
|||
});
|
||||
}
|
||||
|
||||
export function useGetAgentsAvailableVersionsQuery(options: Partial<{ enabled: boolean }> = {}) {
|
||||
return useQuery(
|
||||
['available_versions'],
|
||||
() =>
|
||||
sendRequestForRq<GetAvailableVersionsResponse>({
|
||||
method: 'get',
|
||||
path: agentRouteService.getAvailableVersionsPath(),
|
||||
version: API_VERSIONS.public.v1,
|
||||
}),
|
||||
{
|
||||
enabled: options.enabled,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function sendGetAgentStatusRuntimeField() {
|
||||
return sendRequestForRq<string>({
|
||||
method: 'get',
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('validateRequiredVersions', () => {
|
|||
]);
|
||||
}).toThrow(
|
||||
new AgentPolicyInvalidError(
|
||||
`Policy "test policy" failed validation: duplicate versions not allowed in required_versions`
|
||||
`Policy "test policy" failed required_versions validation: duplicate versions not allowed`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
@ -53,7 +53,7 @@ describe('validateRequiredVersions', () => {
|
|||
]);
|
||||
}).toThrow(
|
||||
new AgentPolicyInvalidError(
|
||||
`Policy "test policy" failed validation: invalid semver version 9.0.0invalid in required_versions`
|
||||
`Policy "test policy" failed required_versions validation: invalid semver version 9.0.0invalid`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
@ -66,7 +66,20 @@ describe('validateRequiredVersions', () => {
|
|||
]);
|
||||
}).toThrow(
|
||||
new AgentPolicyInvalidError(
|
||||
`Policy "test policy" failed validation: sum of required_versions percentages cannot exceed 100`
|
||||
`Policy "test policy" failed required_versions validation: sum of percentages cannot exceed 100`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if percentage is 0 or undefined', () => {
|
||||
expect(() => {
|
||||
validateRequiredVersions('test policy', [
|
||||
{ version: '9.0.0', percentage: 100 },
|
||||
{ version: '9.1.0', percentage: 0 },
|
||||
]);
|
||||
}).toThrow(
|
||||
new AgentPolicyInvalidError(
|
||||
`Policy "test policy" failed required_versions validation: percentage is required`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -5,12 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import semverValid from 'semver/functions/valid';
|
||||
|
||||
import type { AgentTargetVersion } from '../../../common/types';
|
||||
|
||||
import { AgentPolicyInvalidError } from '../../errors';
|
||||
import { appContextService } from '..';
|
||||
import { checkTargetVersionsValidity } from '../../../common/services/agent_utils';
|
||||
|
||||
export function validateRequiredVersions(
|
||||
name: string,
|
||||
|
@ -24,24 +23,10 @@ export function validateRequiredVersions(
|
|||
`Policy "${name}" failed validation: required_versions are not allowed when automatic upgrades feature is disabled`
|
||||
);
|
||||
}
|
||||
const versions = requiredVersions.map((v) => v.version);
|
||||
const uniqueVersions = new Set(versions);
|
||||
if (versions.length !== uniqueVersions.size) {
|
||||
const error = checkTargetVersionsValidity(requiredVersions);
|
||||
if (error) {
|
||||
throw new AgentPolicyInvalidError(
|
||||
`Policy "${name}" failed validation: duplicate versions not allowed in required_versions`
|
||||
);
|
||||
}
|
||||
versions.forEach((version) => {
|
||||
if (!semverValid(version)) {
|
||||
throw new AgentPolicyInvalidError(
|
||||
`Policy "${name}" failed validation: invalid semver version ${version} in required_versions`
|
||||
);
|
||||
}
|
||||
});
|
||||
const sumOfPercentages = requiredVersions.reduce((acc, v) => acc + v.percentage, 0);
|
||||
if (sumOfPercentages > 100) {
|
||||
throw new AgentPolicyInvalidError(
|
||||
`Policy "${name}" failed validation: sum of required_versions percentages cannot exceed 100`
|
||||
`Policy "${name}" failed required_versions validation: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue