mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[8.x][Fleet] Backport auto upgrade feature to 8.x (#217461)
## Summary Backport auto upgrade feature to 8.x --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jill Guyonnet <jill.guyonnet@elastic.co> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Mason Herron <46727170+Supplementing@users.noreply.github.com>
This commit is contained in:
parent
fd494ec39d
commit
bb1aede715
76 changed files with 3208 additions and 377 deletions
|
@ -18666,6 +18666,9 @@
|
|||
"hasRolloutPeriod": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_automatic": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"latestErrors": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
|
|
|
@ -14249,6 +14249,24 @@ paths:
|
|||
- created_at
|
||||
- created_by
|
||||
type: array
|
||||
required_versions:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
percentage:
|
||||
description: Target percentage of agents to auto upgrade
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
type: number
|
||||
version:
|
||||
description: Target version for automatic agent upgrade
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
- percentage
|
||||
nullable: true
|
||||
type: array
|
||||
revision:
|
||||
type: number
|
||||
schema_version:
|
||||
|
@ -14517,6 +14535,24 @@ paths:
|
|||
description: Override settings that are defined in the agent policy. Input settings cannot be overridden. The override option should be used only in unusual circumstances and not as a routine procedure.
|
||||
nullable: true
|
||||
type: object
|
||||
required_versions:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
percentage:
|
||||
description: Target percentage of agents to auto upgrade
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
type: number
|
||||
version:
|
||||
description: Target version for automatic agent upgrade
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
- percentage
|
||||
nullable: true
|
||||
type: array
|
||||
space_ids:
|
||||
items:
|
||||
type: string
|
||||
|
@ -15097,6 +15133,24 @@ paths:
|
|||
- created_at
|
||||
- created_by
|
||||
type: array
|
||||
required_versions:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
percentage:
|
||||
description: Target percentage of agents to auto upgrade
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
type: number
|
||||
version:
|
||||
description: Target version for automatic agent upgrade
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
- percentage
|
||||
nullable: true
|
||||
type: array
|
||||
revision:
|
||||
type: number
|
||||
schema_version:
|
||||
|
@ -15769,6 +15823,24 @@ paths:
|
|||
- created_at
|
||||
- created_by
|
||||
type: array
|
||||
required_versions:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
percentage:
|
||||
description: Target percentage of agents to auto upgrade
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
type: number
|
||||
version:
|
||||
description: Target version for automatic agent upgrade
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
- percentage
|
||||
nullable: true
|
||||
type: array
|
||||
revision:
|
||||
type: number
|
||||
schema_version:
|
||||
|
@ -16421,6 +16493,24 @@ paths:
|
|||
- created_at
|
||||
- created_by
|
||||
type: array
|
||||
required_versions:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
percentage:
|
||||
description: Target percentage of agents to auto upgrade
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
type: number
|
||||
version:
|
||||
description: Target version for automatic agent upgrade
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
- percentage
|
||||
nullable: true
|
||||
type: array
|
||||
revision:
|
||||
type: number
|
||||
schema_version:
|
||||
|
@ -16571,6 +16661,8 @@ paths:
|
|||
type: string
|
||||
memory:
|
||||
type: string
|
||||
bumpRevision:
|
||||
type: boolean
|
||||
data_output_id:
|
||||
nullable: true
|
||||
type: string
|
||||
|
@ -16688,6 +16780,24 @@ paths:
|
|||
description: Override settings that are defined in the agent policy. Input settings cannot be overridden. The override option should be used only in unusual circumstances and not as a routine procedure.
|
||||
nullable: true
|
||||
type: object
|
||||
required_versions:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
percentage:
|
||||
description: Target percentage of agents to auto upgrade
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
type: number
|
||||
version:
|
||||
description: Target version for automatic agent upgrade
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
- percentage
|
||||
nullable: true
|
||||
type: array
|
||||
space_ids:
|
||||
items:
|
||||
type: string
|
||||
|
@ -17268,6 +17378,24 @@ paths:
|
|||
- created_at
|
||||
- created_by
|
||||
type: array
|
||||
required_versions:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
percentage:
|
||||
description: Target percentage of agents to auto upgrade
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
type: number
|
||||
version:
|
||||
description: Target version for automatic agent upgrade
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
- percentage
|
||||
nullable: true
|
||||
type: array
|
||||
revision:
|
||||
type: number
|
||||
schema_version:
|
||||
|
@ -17940,6 +18068,24 @@ paths:
|
|||
- created_at
|
||||
- created_by
|
||||
type: array
|
||||
required_versions:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
percentage:
|
||||
description: Target percentage of agents to auto upgrade
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
type: number
|
||||
version:
|
||||
description: Target version for automatic agent upgrade
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
- percentage
|
||||
nullable: true
|
||||
type: array
|
||||
revision:
|
||||
type: number
|
||||
schema_version:
|
||||
|
@ -19186,6 +19332,11 @@ paths:
|
|||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
upgrade_attempts:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
upgrade_details:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -19650,6 +19801,11 @@ paths:
|
|||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
upgrade_attempts:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
upgrade_details:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -19993,6 +20149,11 @@ paths:
|
|||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
upgrade_attempts:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
upgrade_details:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -20611,6 +20772,8 @@ paths:
|
|||
type: string
|
||||
hasRolloutPeriod:
|
||||
type: boolean
|
||||
is_automatic:
|
||||
type: boolean
|
||||
latestErrors:
|
||||
items:
|
||||
additionalProperties: false
|
||||
|
|
|
@ -500,6 +500,7 @@
|
|||
"name",
|
||||
"namespace",
|
||||
"overrides",
|
||||
"required_versions",
|
||||
"revision",
|
||||
"schema_version",
|
||||
"status",
|
||||
|
@ -624,6 +625,7 @@
|
|||
"name",
|
||||
"namespace",
|
||||
"overrides",
|
||||
"required_versions",
|
||||
"revision",
|
||||
"schema_version",
|
||||
"status",
|
||||
|
|
|
@ -1685,6 +1685,10 @@
|
|||
"index": false,
|
||||
"type": "flattened"
|
||||
},
|
||||
"required_versions": {
|
||||
"index": false,
|
||||
"type": "flattened"
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
@ -2071,6 +2075,10 @@
|
|||
"index": false,
|
||||
"type": "flattened"
|
||||
},
|
||||
"required_versions": {
|
||||
"index": false,
|
||||
"type": "flattened"
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
|
|
@ -105,7 +105,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"file": "6b65ae5899b60ebe08656fd163ea532e557d3c98",
|
||||
"file-upload-usage-collection-telemetry": "06e0a8c04f991e744e09d03ab2bd7f86b2088200",
|
||||
"fileShare": "5be52de1747d249a221b5241af2838264e19aaa1",
|
||||
"fleet-agent-policies": "4a5c6477d2a61121e95ea9865ed1403a28c38706",
|
||||
"fleet-agent-policies": "f69f7c5639f4cf9e85077c904e161f3574ac3ca2",
|
||||
"fleet-fleet-server-host": "69be15f6b6f2a2875ad3c7050ddea7a87f505417",
|
||||
"fleet-message-signing-keys": "93421f43fed2526b59092a4e3c65d64bc2266c0f",
|
||||
"fleet-package-policies": "8173220091e28ff4afa8238bb37749599378f9e5",
|
||||
|
@ -121,7 +121,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"infra-custom-dashboards": "1a5994f2e05bb8a1609825ddbf5012f77c5c67f3",
|
||||
"infrastructure-monitoring-log-view": "5f86709d3c27aed7a8379153b08ee5d3d90d77f5",
|
||||
"infrastructure-ui-source": "113182d6895764378dfe7fa9fa027244f3a457c4",
|
||||
"ingest-agent-policies": "57ebfb047cf0b81c6fa0ceed8586fa7199c7c5e2",
|
||||
"ingest-agent-policies": "cfe66f4aeca8f53b26bd4ddb0e956de1637d774e",
|
||||
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
|
||||
"ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91",
|
||||
"ingest-package-policies": "870f8c21fe3602f31075430a1fdfb052c62d4a14",
|
||||
|
|
|
@ -20706,8 +20706,6 @@
|
|||
"xpack.fleet.addIntegration.standaloneWarning": "La configuration des intégrations en exécutant Elastic Agent en mode autonome est une opération avancée. Si possible, nous vous conseillons d'utiliser plutôt {link}.",
|
||||
"xpack.fleet.addIntegration.switchToManagedButton": "Enregistrer plutôt dans Fleet (recommandé)",
|
||||
"xpack.fleet.agent.metricsNotAvailableMonitoringNotEnabled": "Le monitoring des agents n'est pas activé pour cette politique d'agent. Visitez les paramètres de politique d'agent pour activer le monitoring.",
|
||||
"xpack.fleet.agentActivity.completedTitle": "{nbAgents} {agents} {completedText}{offlineText}",
|
||||
"xpack.fleet.agentActivity.inProgressTitle": "{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}{failuresText}",
|
||||
"xpack.fleet.agentActivity.policyChangeCompletedTitle": "Politique modifiée",
|
||||
"xpack.fleet.agentActivityButton.tourContent": "Consultez ici et à tout moment l'historique des activités des agents en cours, terminées et planifiées.",
|
||||
"xpack.fleet.agentActivityButton.tourTitle": "Historique des activités des agents",
|
||||
|
@ -20737,7 +20735,6 @@
|
|||
"xpack.fleet.agentActivityFlyout.startedDescription": "Démarré le {date}",
|
||||
"xpack.fleet.agentActivityFlyout.title": "Activité des agents",
|
||||
"xpack.fleet.agentActivityFlyout.todayTitle": "Aujourd'hui",
|
||||
"xpack.fleet.agentActivityFlyout.upgradeDescription": "{guideLink} concernant les mises à jour de l'agent.",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButton": "Afficher les agents",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonDisabledMaxTooltip": "La fonctionnalité d'affichage des agents est disponible uniquement pour une action affectant moins de {agentCount} agents",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonPolicyChangeTooltip": "Afficher les agents actuellement affectés à cette politique",
|
||||
|
|
|
@ -20679,8 +20679,6 @@
|
|||
"xpack.fleet.addIntegration.standaloneWarning": "スタンドアロンモードでElasticエージェントを実行して統合を設定する方法は、上級者向けです。可能なかぎり、{link}を使用することをお勧めします。",
|
||||
"xpack.fleet.addIntegration.switchToManagedButton": "Fleetで登録(推奨)",
|
||||
"xpack.fleet.agent.metricsNotAvailableMonitoringNotEnabled": "このエージェントポリシーのアラート監視が無効です。エージェントポリシー設定にアクセスし、監視を有効化してください。",
|
||||
"xpack.fleet.agentActivity.completedTitle": "{nbAgents} {agents} {completedText}{offlineText}",
|
||||
"xpack.fleet.agentActivity.inProgressTitle": "{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}{failuresText}",
|
||||
"xpack.fleet.agentActivity.policyChangeCompletedTitle": "ポリシーが変更されました",
|
||||
"xpack.fleet.agentActivityButton.tourContent": "実行中、完了、スケジュール済みのエージェントアクションアクティビティ履歴をいつでもここで確認できます。",
|
||||
"xpack.fleet.agentActivityButton.tourTitle": "エージェントアクティビティ履歴",
|
||||
|
@ -20710,7 +20708,6 @@
|
|||
"xpack.fleet.agentActivityFlyout.startedDescription": "{date}に開始しました。",
|
||||
"xpack.fleet.agentActivityFlyout.title": "エージェントアクティビティ",
|
||||
"xpack.fleet.agentActivityFlyout.todayTitle": "今日",
|
||||
"xpack.fleet.agentActivityFlyout.upgradeDescription": "エージェントアップグレードについては、{guideLink}。",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButton": "エージェントを表示",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonDisabledMaxTooltip": "エージェントの表示機能は、{agentCount}個未満のエージェントに影響するアクションでのみ利用可能です",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonPolicyChangeTooltip": "現在このポリシーに割り当てられているエージェントを表示",
|
||||
|
|
|
@ -20728,8 +20728,6 @@
|
|||
"xpack.fleet.addIntegration.standaloneWarning": "通过在独立模式下运行 Elastic 代理来设置集成为高级选项。如果可能,我们建议改用 {link}。",
|
||||
"xpack.fleet.addIntegration.switchToManagedButton": "改为在 Fleet 中注册(建议)",
|
||||
"xpack.fleet.agent.metricsNotAvailableMonitoringNotEnabled": "未对此代理策略启用代理监测。访问代理策略设置以启用监测。",
|
||||
"xpack.fleet.agentActivity.completedTitle": "{nbAgents} {agents} {completedText}{offlineText}",
|
||||
"xpack.fleet.agentActivity.inProgressTitle": "{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}{failuresText}",
|
||||
"xpack.fleet.agentActivity.policyChangeCompletedTitle": "策略已更改",
|
||||
"xpack.fleet.agentActivityButton.tourContent": "随时在此处查看正在进行、已完成和已计划的代理操作活动历史记录。",
|
||||
"xpack.fleet.agentActivityButton.tourTitle": "代理活动历史记录",
|
||||
|
@ -20759,7 +20757,6 @@
|
|||
"xpack.fleet.agentActivityFlyout.startedDescription": "已于 {date}启动。",
|
||||
"xpack.fleet.agentActivityFlyout.title": "代理活动",
|
||||
"xpack.fleet.agentActivityFlyout.todayTitle": "今日",
|
||||
"xpack.fleet.agentActivityFlyout.upgradeDescription": "有关代理升级的{guideLink}。",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButton": "查看代理",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonDisabledMaxTooltip": "查看代理功能仅适用于影响的代理数不超过 {agentCount} 个的操作",
|
||||
"xpack.fleet.agentActivityFlyout.viewAgentsButtonPolicyChangeTooltip": "查看当前分配给此策略的代理",
|
||||
|
|
|
@ -48,6 +48,16 @@ export const AgentStatuses = [
|
|||
'degraded',
|
||||
] as const;
|
||||
|
||||
export const ActiveAgentStatuses = [
|
||||
'online',
|
||||
'offline',
|
||||
'enrolling',
|
||||
'updating',
|
||||
'degraded',
|
||||
'error',
|
||||
'orphaned',
|
||||
]; // excluded: unenrolling, unenrolled, inactive, uninstalled
|
||||
|
||||
// Kueries for finding unprivileged and privileged agents
|
||||
// Privileged is `not` because the metadata field can be undefined
|
||||
export const UNPRIVILEGED_AGENT_KUERY = `${AGENTS_PREFIX}.local_metadata.elastic.agent.unprivileged: true`;
|
||||
|
|
|
@ -58,3 +58,5 @@ export const FLEET_ENROLLMENT_API_PREFIX = 'fleet-enrollment-api-keys';
|
|||
export const REQUEST_DIAGNOSTICS_TIMEOUT_MS = 3 * 60 * 60 * 1000; // 3 hours;
|
||||
|
||||
export * from './mappings';
|
||||
|
||||
export const AUTO_UPGRADE_DEFAULT_RETRIES = ['30m', '1h', '2h', '4h', '8h', '16h', '24h'];
|
||||
|
|
|
@ -363,6 +363,9 @@ export const AGENT_MAPPINGS = {
|
|||
},
|
||||
},
|
||||
},
|
||||
upgrade_attempts: {
|
||||
type: 'date',
|
||||
},
|
||||
// added to allow validation on status field
|
||||
status: {
|
||||
type: 'keyword',
|
||||
|
|
|
@ -81,6 +81,7 @@ export const AGENT_POLICY_API_ROUTES = {
|
|||
DELETE_PATTERN: `${AGENT_POLICY_API_ROOT}/delete`,
|
||||
FULL_INFO_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/full`,
|
||||
FULL_INFO_DOWNLOAD_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/download`,
|
||||
AUTO_UPGRADE_AGENTS_STATUS_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/auto_upgrade_agents_status`,
|
||||
};
|
||||
|
||||
// Kubernetes Manifest API routes
|
||||
|
|
|
@ -28,6 +28,7 @@ const _allowedExperimentalValues = {
|
|||
asyncDeployPolicies: true,
|
||||
enableExportCSV: true,
|
||||
enabledUpgradeAgentlessDeploymentsTask: false,
|
||||
enableAutomaticAgentUpgrades: false,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ActiveAgentStatuses } from '../constants';
|
||||
import type { Agent, AgentStatus, FleetServerAgent } from '../types';
|
||||
|
||||
export function getPreviousAgentStatusForOfflineAgents(
|
||||
|
@ -57,6 +58,10 @@ export function buildKueryForInactiveAgents() {
|
|||
return 'status:inactive';
|
||||
}
|
||||
|
||||
export function buildKueryForActiveAgents() {
|
||||
return `(${ActiveAgentStatuses.map((s) => `status:${s}`).join(' or ')})`;
|
||||
}
|
||||
|
||||
export const AGENT_UPDATING_TIMEOUT_HOURS = 2;
|
||||
|
||||
export function isStuckInUpdating(agent: Agent): boolean {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -155,6 +155,13 @@ export const agentPolicyRouteService = {
|
|||
return AGENT_POLICY_API_ROUTES.INFO_PATTERN.replace('{agentPolicyId}', agentPolicyId);
|
||||
},
|
||||
|
||||
getAutoUpgradeAgentsStatusPath: (agentPolicyId: string) => {
|
||||
return AGENT_POLICY_API_ROUTES.AUTO_UPGRADE_AGENTS_STATUS_PATTERN.replace(
|
||||
'{agentPolicyId}',
|
||||
agentPolicyId
|
||||
);
|
||||
},
|
||||
|
||||
getCreatePath: () => {
|
||||
return AGENT_POLICY_API_ROUTES.CREATE_PATTERN;
|
||||
},
|
||||
|
|
|
@ -83,6 +83,9 @@ export interface FleetConfigType {
|
|||
};
|
||||
};
|
||||
createArtifactsBulkBatchSize?: number;
|
||||
autoUpgrades?: {
|
||||
retryDelays?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// Calling Object.entries(PackagesGroupedByStatus) gave `status: string`
|
||||
|
|
|
@ -70,6 +70,8 @@ export interface NewAgentAction {
|
|||
rollout_duration_seconds?: number;
|
||||
source_uri?: string;
|
||||
total?: number;
|
||||
is_automatic?: boolean;
|
||||
policyId?: string;
|
||||
}
|
||||
|
||||
export interface AgentAction extends NewAgentAction {
|
||||
|
@ -95,6 +97,7 @@ interface AgentBase {
|
|||
upgraded_at?: string | null;
|
||||
upgrade_started_at?: string | null;
|
||||
upgrade_details?: AgentUpgradeDetails;
|
||||
upgrade_attempts?: string[] | null;
|
||||
access_api_key_id?: string;
|
||||
default_api_key?: string;
|
||||
default_api_key_id?: string;
|
||||
|
@ -182,6 +185,7 @@ export interface ActionStatus {
|
|||
latestErrors?: ActionErrorResult[];
|
||||
revision?: number;
|
||||
policyId?: string;
|
||||
is_automatic?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentDiagnostics {
|
||||
|
@ -267,6 +271,10 @@ export interface FleetServerAgent {
|
|||
* Upgrade state of the Elastic Agent
|
||||
*/
|
||||
upgrade_details?: AgentUpgradeDetails;
|
||||
/**
|
||||
* List of timestamps of attempts of Elastic Agent automatic upgrades
|
||||
*/
|
||||
upgrade_attempts?: string[] | null;
|
||||
access_api_key_id?: string;
|
||||
agent?: FleetServerAgentMetadata;
|
||||
/**
|
||||
|
@ -444,6 +452,14 @@ export interface FleetServerAgentAction {
|
|||
signature: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* True if action was generated by an automated task.
|
||||
*/
|
||||
is_automatic?: boolean;
|
||||
|
||||
// the id of the policy associated with the action
|
||||
policyId?: string;
|
||||
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,12 @@ export interface NewAgentPolicy {
|
|||
max_dur?: string;
|
||||
};
|
||||
};
|
||||
required_versions?: AgentTargetVersion[] | null;
|
||||
}
|
||||
|
||||
export interface AgentTargetVersion {
|
||||
version: string;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface AgentlessPolicy {
|
||||
|
|
|
@ -31,6 +31,17 @@ export interface GetOneAgentPolicyResponse {
|
|||
item: AgentPolicy;
|
||||
}
|
||||
|
||||
export interface CurrentVersionCount {
|
||||
version: string;
|
||||
agents: number;
|
||||
failedUpgradeAgents: number;
|
||||
}
|
||||
|
||||
export interface GetAutoUpgradeAgentsStatusResponse {
|
||||
currentVersions: CurrentVersionCount[];
|
||||
totalAgents: number;
|
||||
}
|
||||
|
||||
export interface CreateAgentPolicyRequest {
|
||||
body: NewAgentPolicy;
|
||||
}
|
||||
|
@ -40,7 +51,9 @@ export interface CreateAgentPolicyResponse {
|
|||
}
|
||||
|
||||
export type UpdateAgentPolicyRequest = GetOneAgentPolicyRequest & {
|
||||
body: NewAgentPolicy;
|
||||
body: NewAgentPolicy & {
|
||||
bumpRevision?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export interface UpdateAgentPolicyResponse {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -174,7 +174,7 @@ describe('SearchBar', () => {
|
|||
|
||||
describe('getFieldSpecs', () => {
|
||||
it('returns fieldSpecs for Fleet agents', () => {
|
||||
expect(getFieldSpecs(AGENTS_INDEX, AGENTS_PREFIX)).toHaveLength(73);
|
||||
expect(getFieldSpecs(AGENTS_INDEX, AGENTS_PREFIX)).toHaveLength(74);
|
||||
});
|
||||
|
||||
it('returns fieldSpecs for Fleet enrollment tokens', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { useLink, useConfig, useAuthz, useStartServices } from '../../hooks';
|
|||
import { WithHeaderLayout } from '../../../../layouts';
|
||||
|
||||
import { ExperimentalFeaturesService } from '../../services';
|
||||
import { AutoUpgradeAgentsTour } from '../../sections/agent_policy/components/auto_upgrade_agents_tour';
|
||||
|
||||
import { DefaultPageTitle } from './default_page_title';
|
||||
|
||||
|
@ -60,6 +61,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: (
|
||||
|
@ -145,6 +147,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, ExperimentalFeaturesService } from '../../../serv
|
|||
|
||||
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 { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();
|
||||
|
||||
|
@ -100,6 +105,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)}
|
||||
|
@ -189,6 +211,7 @@ export const AgentPolicyActionMenu = memo<{
|
|||
)}
|
||||
</EuiContextMenuItem>,
|
||||
viewPolicyItem,
|
||||
manageAutoUpgradeAgentsItem,
|
||||
copyPolicyItem,
|
||||
deletePolicyItem,
|
||||
];
|
||||
|
@ -278,6 +301,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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiNotificationBadge,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { AgentPolicy } from '../../../../../types';
|
||||
import { useGetAutoUpgradeAgentsStatusQuery } from '../../../../../hooks';
|
||||
|
||||
export interface Props {
|
||||
agentPolicy: AgentPolicy;
|
||||
isManageAutoUpgradeAgentsModalOpen: boolean;
|
||||
setIsManageAutoUpgradeAgentsModalOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const ManageAutoUpgradeAgentsBadge: React.FC<Props> = ({
|
||||
agentPolicy,
|
||||
isManageAutoUpgradeAgentsModalOpen,
|
||||
setIsManageAutoUpgradeAgentsModalOpen,
|
||||
}: Props) => {
|
||||
const { data: autoUpgradeAgentsStatus } = useGetAutoUpgradeAgentsStatusQuery(agentPolicy.id);
|
||||
const requiredVersions = (agentPolicy.required_versions ?? []).map(
|
||||
(reqVersion) => reqVersion.version
|
||||
);
|
||||
const hasErrors = useMemo(() => {
|
||||
return autoUpgradeAgentsStatus?.currentVersions
|
||||
.filter((value) => requiredVersions.includes(value.version))
|
||||
.some((value) => value.failedUpgradeAgents > 0);
|
||||
}, [autoUpgradeAgentsStatus, requiredVersions]);
|
||||
|
||||
return (
|
||||
<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>
|
||||
{hasErrors && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.failedUpgradeTooltip"
|
||||
defaultMessage="Some agents failed to upgrade, click on Manage to see details."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiIcon type="warning" color="danger" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -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,19 @@ import {
|
|||
EuiLink,
|
||||
EuiToolTip,
|
||||
EuiIconTip,
|
||||
EuiPortal,
|
||||
} 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';
|
||||
|
||||
import { ManageAutoUpgradeAgentsBadge } from './manage_auto_upgrade_agents';
|
||||
|
||||
export interface HeaderRightContentProps {
|
||||
isLoading: boolean;
|
||||
|
@ -55,6 +60,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 +92,194 @@ 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."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</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"
|
||||
<>
|
||||
<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 },
|
||||
{
|
||||
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>
|
||||
),
|
||||
},
|
||||
{ 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."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</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: (
|
||||
<ManageAutoUpgradeAgentsBadge
|
||||
agentPolicy={agentPolicy}
|
||||
isManageAutoUpgradeAgentsModalOpen={isManageAutoUpgradeAgentsModalOpen}
|
||||
setIsManageAutoUpgradeAgentsModalOpen={
|
||||
setIsManageAutoUpgradeAgentsModalOpen
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ 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>
|
||||
{isManageAutoUpgradeAgentsModalOpen && (
|
||||
<EuiPortal>
|
||||
<ManageAutoUpgradeAgentsModal
|
||||
agentPolicy={agentPolicy}
|
||||
agentCount={agentPolicy.agents || 0}
|
||||
onClose={(refreshPolicy: boolean) => {
|
||||
setIsManageAutoUpgradeAgentsModalOpen(false);
|
||||
if (refreshPolicy) {
|
||||
refreshAgentPolicy();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiPortal>
|
||||
)}
|
||||
<AutoUpgradeAgentsTour anchor="#auto-upgrade-manage-button" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,25 +16,36 @@ import {
|
|||
EuiText,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
useEuiTheme,
|
||||
EuiIconTip,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import type { ActionStatus } from '../../../../../types';
|
||||
|
||||
import { ViewErrors } from '../view_errors';
|
||||
|
||||
import {
|
||||
formattedTime,
|
||||
getAction,
|
||||
inProgressDescription,
|
||||
inProgressTitle,
|
||||
inProgressTitleColor,
|
||||
} from './helpers';
|
||||
import { formattedTime, getAction, inProgressDescription, inProgressTitle } from './helpers';
|
||||
import { ViewAgentsButton } from './view_agents_button';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
height: 50%;
|
||||
border-left: ${(props) => props.theme.euiTheme.border.thin};
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
||||
export const ActivityItem: React.FunctionComponent<{
|
||||
action: ActionStatus;
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, onClickViewAgents }) => {
|
||||
onClickManageAutoUpgradeAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, onClickViewAgents, onClickManageAutoUpgradeAgents }) => {
|
||||
const theme = useEuiTheme();
|
||||
|
||||
const isAutomaticUpgrade = action.is_automatic;
|
||||
const completeTitle =
|
||||
action.type === 'POLICY_CHANGE' && action.nbAgentsActioned === 0 ? (
|
||||
<EuiText>
|
||||
|
@ -44,26 +55,43 @@ export const ActivityItem: React.FunctionComponent<{
|
|||
/>
|
||||
</EuiText>
|
||||
) : (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.completedTitle"
|
||||
defaultMessage="{nbAgents} {agents} {completedText}{offlineText}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck === action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
completedText: getAction(action.type, action.actionId).completedText,
|
||||
offlineText:
|
||||
action.status === 'ROLLOUT_PASSED' && action.nbAgentsActioned - action.nbAgentsAck > 0
|
||||
? `, ${
|
||||
action.nbAgentsActioned - action.nbAgentsAck
|
||||
} agent(s) offline during the rollout period`
|
||||
: '',
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.completedTitle"
|
||||
defaultMessage="{nbAgents} {agents} {completedText}{versionText}{offlineText}{automaticIcon}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck === action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
completedText: getAction(action.type, action.actionId).completedText,
|
||||
offlineText:
|
||||
action.status === 'ROLLOUT_PASSED' &&
|
||||
action.nbAgentsActioned - action.nbAgentsAck > 0
|
||||
? `, ${
|
||||
action.nbAgentsActioned - action.nbAgentsAck
|
||||
} agent(s) offline during the rollout period`
|
||||
: '',
|
||||
versionText: action.version ? ` to version ${action.version}` : '',
|
||||
automaticIcon: action.is_automatic ? (
|
||||
<EuiIconTip
|
||||
anchorProps={{
|
||||
style: { display: 'flex', alignItems: 'center' },
|
||||
}}
|
||||
type="timeRefresh"
|
||||
content="Triggered by an automatic upgrade"
|
||||
/>
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
// TODO: investigate whether default completion is due to a bug
|
||||
|
@ -103,8 +131,8 @@ export const ActivityItem: React.FunctionComponent<{
|
|||
} = {
|
||||
IN_PROGRESS: {
|
||||
icon: <EuiLoadingSpinner size="m" />,
|
||||
title: <EuiText>{inProgressTitle(action)}</EuiText>,
|
||||
titleColor: inProgressTitleColor,
|
||||
title: <EuiText>{inProgressTitle(action, action.is_automatic)}</EuiText>,
|
||||
titleColor: theme.euiTheme.colors.textPrimary,
|
||||
description: <EuiText color="subdued">{inProgressDescription(action.creationTime)}</EuiText>,
|
||||
},
|
||||
ROLLOUT_PASSED: {
|
||||
|
@ -247,7 +275,31 @@ export const ActivityItem: React.FunctionComponent<{
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="xs" />
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
</EuiFlexItem>
|
||||
|
||||
{isAutomaticUpgrade && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="manageAutoUpgradesButton"
|
||||
onClick={() => onClickManageAutoUpgradeAgents(action)}
|
||||
size="m"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.manageAutoUpgradeAgents"
|
||||
defaultMessage="Manage auto-upgrade agents"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiText, EuiPanel } from '@elastic/eui';
|
||||
import { EuiText, EuiPanel, EuiHealth, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import type { ActionStatus } from '../../../../../types';
|
||||
|
||||
|
@ -19,12 +19,22 @@ export const ActivitySection: React.FunctionComponent<{
|
|||
actions: ActionStatus[];
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ title, actions, abortUpgrade, onClickViewAgents }) => {
|
||||
onClickManageAutoUpgradeAgents: (action: ActionStatus) => void;
|
||||
}> = ({ title, actions, abortUpgrade, onClickViewAgents, onClickManageAutoUpgradeAgents }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiPanel color="subdued" hasBorder={true} borderRadius="none">
|
||||
<EuiText>
|
||||
<b>{title}</b>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
{actions.some((action) => action.status === 'IN_PROGRESS') && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHealth color="success" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<b>{title}</b>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
{actions.map((currentAction) =>
|
||||
|
@ -34,12 +44,14 @@ export const ActivitySection: React.FunctionComponent<{
|
|||
abortUpgrade={abortUpgrade}
|
||||
key={currentAction.actionId}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
) : (
|
||||
<ActivityItem
|
||||
action={currentAction}
|
||||
key={currentAction.actionId}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
|
@ -47,6 +47,8 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
currentActions: ActionStatus[];
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
onClickViewAgents: (action: ActionStatus) => Promise<void>;
|
||||
onClickManageAutoUpgradeAgents: (action: ActionStatus) => void;
|
||||
|
||||
areActionsFullyLoaded: boolean;
|
||||
onClickShowMore: () => void;
|
||||
dateFilter: moment.Moment | null;
|
||||
|
@ -56,6 +58,7 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
currentActions,
|
||||
abortUpgrade,
|
||||
onClickViewAgents,
|
||||
onClickManageAutoUpgradeAgents,
|
||||
areActionsFullyLoaded,
|
||||
onClickShowMore,
|
||||
dateFilter,
|
||||
|
@ -140,7 +143,9 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
}
|
||||
|
||||
// Loaded actions
|
||||
|
||||
const inProgressActions = currentActions.filter((a) => a.status === 'IN_PROGRESS');
|
||||
|
||||
const completedActions = currentActions.filter((a) => a.status !== 'IN_PROGRESS');
|
||||
const todayActions = getTodayActions(completedActions);
|
||||
const otherDays = getOtherDaysActions(completedActions);
|
||||
|
@ -162,6 +167,7 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
actions={inProgressActions}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
) : null}
|
||||
{todayActions.length > 0 ? (
|
||||
|
@ -175,6 +181,7 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
actions={todayActions}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
) : null}
|
||||
{Object.keys(otherDays).map((day) => (
|
||||
|
@ -184,6 +191,7 @@ export const FlyoutBody: React.FunctionComponent<{
|
|||
actions={otherDays[day]}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react';
|
||||
import { EuiIconTip, EuiFlexGroup } from '@elastic/eui';
|
||||
|
||||
import type { ActionStatus } from '../../../../../types';
|
||||
|
||||
|
@ -73,25 +74,36 @@ export const getAction = (type?: string, actionId?: string) => {
|
|||
return actionNames[type ?? 'ACTION'] ?? actionNames.ACTION;
|
||||
};
|
||||
|
||||
export const inProgressTitle = (action: ActionStatus) => (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.inProgressTitle"
|
||||
defaultMessage="{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}{failuresText}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck >= action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck === 0
|
||||
? action.nbAgentsActioned
|
||||
: action.nbAgentsActioned - action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
inProgressText: getAction(action.type, action.actionId).inProgressText,
|
||||
reassignText:
|
||||
action.type === 'POLICY_REASSIGN' && action.newPolicyId ? `to ${action.newPolicyId}` : '',
|
||||
upgradeText: action.type === 'UPGRADE' ? `to version ${action.version}` : '',
|
||||
failuresText: action.nbAgentsFailed > 0 ? `, has ${action.nbAgentsFailed} failure(s)` : '',
|
||||
}}
|
||||
/>
|
||||
export const inProgressTitle = (action: ActionStatus, isAutomatic: boolean | undefined) => (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.inProgressTitle"
|
||||
defaultMessage="{inProgressText} {nbAgents} {agents}{reassignText}{upgradeText}{failuresText}{automaticIcon}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck >= action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck === 0
|
||||
? action.nbAgentsActioned
|
||||
: action.nbAgentsActioned - action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
inProgressText: getAction(action.type, action.actionId).inProgressText,
|
||||
reassignText:
|
||||
action.type === 'POLICY_REASSIGN' && action.newPolicyId ? `to ${action.newPolicyId}` : '',
|
||||
upgradeText: action.type === 'UPGRADE' ? ` to version ${action.version}` : '',
|
||||
failuresText: action.nbAgentsFailed > 0 ? `, has ${action.nbAgentsFailed} failure(s)` : '',
|
||||
automaticIcon: isAutomatic ? (
|
||||
<EuiIconTip
|
||||
anchorProps={{
|
||||
style: { display: 'flex', alignItems: 'center' },
|
||||
}}
|
||||
type="timeRefresh"
|
||||
content="Triggered by an automatic upgrade"
|
||||
/>
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
export const inProgressDescription = (time?: string) => (
|
||||
|
|
|
@ -6,17 +6,22 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act, render, fireEvent } from '@testing-library/react';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import type { TestRenderer } from '../../../../../../../mock';
|
||||
import { createFleetTestRendererMock } from '../../../../../../../mock';
|
||||
|
||||
import { useActionStatus } from '../../hooks';
|
||||
import { useGetAgentPolicies, useStartServices, useAuthz } from '../../../../../hooks';
|
||||
import { useGetAgentPolicies, useAuthz } from '../../../../../hooks';
|
||||
|
||||
import { AgentActivityFlyout } from '.';
|
||||
|
||||
jest.mock('../../hooks');
|
||||
jest.mock('../../../../../hooks');
|
||||
jest.mock('../../../../../../../hooks/use_request/agent_policy');
|
||||
jest.mock('../../../../../../../hooks/use_authz');
|
||||
|
||||
jest.mock('@kbn/shared-ux-link-redirect-app', () => ({
|
||||
RedirectAppLinks: ({ children }: { children: React.ReactNode }) => children,
|
||||
|
@ -24,7 +29,7 @@ jest.mock('@kbn/shared-ux-link-redirect-app', () => ({
|
|||
|
||||
const mockUseActionStatus = useActionStatus as jest.Mock;
|
||||
const mockUseGetAgentPolicies = useGetAgentPolicies as jest.Mock;
|
||||
const mockUseStartServices = useStartServices as jest.Mock;
|
||||
|
||||
const mockedUseAuthz = useAuthz as jest.Mock;
|
||||
|
||||
jest.mock('@kbn/logs-shared-plugin/common', () => {
|
||||
|
@ -43,6 +48,7 @@ describe('AgentActivityFlyout', () => {
|
|||
const mockAbortUpgrade = jest.fn();
|
||||
const mockSetSearch = jest.fn();
|
||||
const mockSetSelectedStatus = jest.fn();
|
||||
const mockOpenManageAutoUpgradeModal = jest.fn();
|
||||
|
||||
const component = (refreshAgentActivity: boolean = false) => (
|
||||
<IntlProvider timeZone="UTC" locale="en">
|
||||
|
@ -52,11 +58,13 @@ describe('AgentActivityFlyout', () => {
|
|||
refreshAgentActivity={refreshAgentActivity}
|
||||
setSearch={mockSetSearch}
|
||||
setSelectedStatus={mockSetSelectedStatus}
|
||||
openManageAutoUpgradeModal={mockOpenManageAutoUpgradeModal}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
let testRenderer: TestRenderer;
|
||||
beforeEach(() => {
|
||||
testRenderer = createFleetTestRendererMock();
|
||||
mockOnClose.mockReset();
|
||||
mockOnAbortSuccess.mockReset();
|
||||
mockAbortUpgrade.mockReset();
|
||||
|
@ -66,30 +74,18 @@ describe('AgentActivityFlyout', () => {
|
|||
mockUseGetAgentPolicies.mockReturnValue({
|
||||
data: {
|
||||
items: [
|
||||
{ id: 'policy1', name: 'Policy 1' },
|
||||
{ id: 'policy2', name: 'Policy 2' },
|
||||
{ id: 'Policy1', name: 'Policy 1' },
|
||||
{ id: 'Policy2', name: 'Policy 2' },
|
||||
],
|
||||
},
|
||||
});
|
||||
mockUseStartServices.mockReturnValue({
|
||||
docLinks: { links: { fleet: { upgradeElasticAgent: 'https://elastic.co' } } },
|
||||
application: { navigateToUrl: jest.fn() },
|
||||
http: { basePath: { prepend: jest.fn() } },
|
||||
share: {
|
||||
url: {
|
||||
locators: {
|
||||
get: () => ({
|
||||
useUrl: () => 'https://locator.url',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockedUseAuthz.mockReturnValue({
|
||||
fleet: {
|
||||
readAgents: true,
|
||||
allAgents: true,
|
||||
},
|
||||
integrations: {},
|
||||
} as any);
|
||||
});
|
||||
|
||||
|
@ -118,24 +114,22 @@ describe('AgentActivityFlyout', () => {
|
|||
hasRolloutPeriod: true,
|
||||
},
|
||||
];
|
||||
mockUseActionStatus
|
||||
.mockReturnValueOnce({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: true,
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
mockUseActionStatus.mockReturnValue({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: true,
|
||||
});
|
||||
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('loading')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('upgradeInProgressTitle')).not.toBeInTheDocument();
|
||||
|
||||
mockUseActionStatus.mockReturnValue({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
result.rerender(component());
|
||||
expect(result.queryByTestId('loading')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('upgradeInProgressTitle')).toBeInTheDocument();
|
||||
|
@ -147,7 +141,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.queryByText('No activity to display')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -174,7 +168,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
|
||||
|
@ -186,7 +180,7 @@ describe('AgentActivityFlyout', () => {
|
|||
result.container
|
||||
.querySelector('[data-test-subj="upgradeInProgressDescription"]')!
|
||||
.textContent?.replace(/\s/g, '')
|
||||
).toContain('Started on Sep 15, 2022 10:00 AM. Learn more'.replace(/\s/g, ''));
|
||||
).toContain('Started on Sep 15, 2022 10:00 AM.'.replace(/\s/g, ''));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(result.getByText('Cancel'));
|
||||
|
@ -217,7 +211,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
|
||||
|
@ -229,7 +223,7 @@ describe('AgentActivityFlyout', () => {
|
|||
result.container
|
||||
.querySelector('[data-test-subj="upgradeInProgressDescription"]')!
|
||||
.textContent?.replace(/\s/g, '')
|
||||
).toContain('Started on Sep 15, 2022 10:00 AM. Learn more'.replace(/\s/g, ''));
|
||||
).toContain('Started on Sep 15, 2022 10:00 AM.'.replace(/\s/g, ''));
|
||||
|
||||
expect(result.queryByText('Cancel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -255,7 +249,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
|
||||
|
@ -266,7 +260,7 @@ describe('AgentActivityFlyout', () => {
|
|||
result.container
|
||||
.querySelector('[data-test-subj="upgradeInProgressDescription"]')!
|
||||
.textContent?.replace(/\s/g, '')
|
||||
).toContain('Scheduled for Sep 16, 2022 10:00 AM. Learn more'.replace(/\s/g, ''));
|
||||
).toContain('Scheduled for Sep 16, 2022 10:00 AM.'.replace(/\s/g, ''));
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(result.getByText('Cancel'));
|
||||
|
@ -295,7 +289,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'2 agents upgraded'
|
||||
|
@ -326,7 +320,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'1 of 2 agents upgraded, 1 agent(s) offline during the rollout period'
|
||||
|
@ -357,7 +351,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'1 of 2 agents upgraded, 1 agent(s) offline during the rollout period'
|
||||
|
@ -393,7 +387,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'Agent unenrollment expired'
|
||||
|
@ -426,7 +420,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'Agent upgrade cancelled'
|
||||
|
@ -448,7 +442,7 @@ describe('AgentActivityFlyout', () => {
|
|||
nbAgentsActioned: 1,
|
||||
status: 'FAILED',
|
||||
expiration: '2099-09-16T10:00:00.000Z',
|
||||
newPolicyId: 'policy1',
|
||||
newPolicyId: 'Policy1',
|
||||
creationTime: '2022-09-15T10:00:00.000Z',
|
||||
nbAgentsFailed: 1,
|
||||
completionTime: '2022-09-15T11:00:00.000Z',
|
||||
|
@ -459,7 +453,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'0 of 1 agent assigned to a new policy'
|
||||
|
@ -496,7 +490,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'0 of 3 agents actioned'
|
||||
|
@ -518,7 +512,7 @@ describe('AgentActivityFlyout', () => {
|
|||
nbAgentsActioned: 0,
|
||||
status: 'COMPLETE',
|
||||
expiration: '2099-09-16T10:00:00.000Z',
|
||||
policyId: 'policy1',
|
||||
policyId: 'Policy1',
|
||||
revision: 2,
|
||||
creationTime: '2022-09-15T10:00:00.000Z',
|
||||
nbAgentsFailed: 0,
|
||||
|
@ -530,7 +524,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'Policy changed'
|
||||
|
@ -552,7 +546,7 @@ describe('AgentActivityFlyout', () => {
|
|||
nbAgentsActioned: 3,
|
||||
status: 'COMPLETE',
|
||||
expiration: '2099-09-16T10:00:00.000Z',
|
||||
policyId: 'policy1',
|
||||
policyId: 'Policy1',
|
||||
revision: 2,
|
||||
creationTime: '2022-09-15T10:00:00.000Z',
|
||||
nbAgentsFailed: 0,
|
||||
|
@ -564,7 +558,7 @@ describe('AgentActivityFlyout', () => {
|
|||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.container.querySelector('[data-test-subj="statusTitle"]')!.textContent).toEqual(
|
||||
'3 agents applied policy change'
|
||||
|
@ -576,6 +570,42 @@ describe('AgentActivityFlyout', () => {
|
|||
).toContain('Policy1 changed to revision 2 at Sep 15, 2022 10:00 AM.'.replace(/\s/g, ''));
|
||||
});
|
||||
|
||||
it('should render agent activity for an automatic upgrade', () => {
|
||||
const mockActionStatuses = [
|
||||
{
|
||||
actionId: 'action8',
|
||||
nbAgentsActionCreated: 3,
|
||||
nbAgentsAck: 0,
|
||||
type: 'UPGRADE',
|
||||
nbAgentsActioned: 3,
|
||||
is_automatic: true,
|
||||
status: 'IN_PROGRESS',
|
||||
expiration: '2099-09-16T10:00:00.000Z',
|
||||
policyId: 'Policy1',
|
||||
revision: 2,
|
||||
creationTime: '2022-09-15T10:00:00.000Z',
|
||||
nbAgentsFailed: 0,
|
||||
completionTime: '2022-09-15T11:00:00.000Z',
|
||||
version: '8.17.3',
|
||||
},
|
||||
];
|
||||
mockUseActionStatus.mockReturnValue({
|
||||
currentActions: mockActionStatuses,
|
||||
abortUpgrade: mockAbortUpgrade,
|
||||
isFirstLoading: false,
|
||||
});
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(
|
||||
result.container.querySelector('[data-test-subj="upgradeInProgressTitle"]')!.textContent
|
||||
).toContain('Upgrading 3 agents to version 8.17.3');
|
||||
expect(
|
||||
result.container
|
||||
.querySelector('[data-test-subj="manageAutoUpgradesButton"]')!
|
||||
.textContent?.replace(/\s/g, '')
|
||||
).toContain('Manage auto-upgrade agents'.replace(/\s/g, ''));
|
||||
});
|
||||
|
||||
it('should keep flyout state on new data', () => {
|
||||
const failedAction = {
|
||||
actionId: 'action1',
|
||||
|
@ -626,7 +656,7 @@ describe('AgentActivityFlyout', () => {
|
|||
};
|
||||
}
|
||||
});
|
||||
const result = render(component());
|
||||
const result = testRenderer.render(component());
|
||||
|
||||
expect(result.getByText('Agent activity')).toBeInTheDocument();
|
||||
expect(result.container.querySelector('[data-test-subj="upgradeInProgressTitle"]')).toBe(null);
|
||||
|
|
|
@ -46,7 +46,15 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
refreshAgentActivity: boolean;
|
||||
setSearch: (search: string) => void;
|
||||
setSelectedStatus: (status: string[]) => void;
|
||||
}> = ({ onClose, onAbortSuccess, refreshAgentActivity, setSearch, setSelectedStatus }) => {
|
||||
openManageAutoUpgradeModal: (policyId: string) => void;
|
||||
}> = ({
|
||||
onClose,
|
||||
onAbortSuccess,
|
||||
refreshAgentActivity,
|
||||
setSearch,
|
||||
setSelectedStatus,
|
||||
openManageAutoUpgradeModal,
|
||||
}) => {
|
||||
const { notifications } = useStartServices();
|
||||
const { data: agentPoliciesData } = useGetAgentPolicies({
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
|
@ -75,7 +83,7 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
currentActions.map((a) => ({
|
||||
...a,
|
||||
newPolicyId: getAgentPolicyName(a.newPolicyId ?? ''),
|
||||
policyId: getAgentPolicyName(a.policyId ?? ''),
|
||||
policyId: a.policyId ? a.policyId : getAgentPolicyName(a.newPolicyId ?? ''),
|
||||
})),
|
||||
[currentActions, getAgentPolicyName]
|
||||
);
|
||||
|
@ -100,6 +108,11 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
});
|
||||
}
|
||||
};
|
||||
const onClickManageAutoUpgradeAgents = async (action: ActionStatus) => {
|
||||
// use the policy id from the action to manage
|
||||
onClose();
|
||||
openManageAutoUpgradeModal(action.policyId!);
|
||||
};
|
||||
|
||||
const onClickShowMore = () => {
|
||||
setNActions(nActions + 10);
|
||||
|
@ -153,6 +166,7 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
currentActions={currentActionsEnriched}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
onClickManageAutoUpgradeAgents={onClickManageAutoUpgradeAgents}
|
||||
areActionsFullyLoaded={areActionsFullyLoaded}
|
||||
onClickShowMore={onClickShowMore}
|
||||
dateFilter={dateFilter}
|
||||
|
|
|
@ -15,9 +15,11 @@ import {
|
|||
EuiPanel,
|
||||
EuiButton,
|
||||
EuiLink,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import type { ActionStatus } from '../../../../../types';
|
||||
import { useStartServices } from '../../../../../hooks';
|
||||
|
@ -30,12 +32,23 @@ import {
|
|||
} from './helpers';
|
||||
import { ViewAgentsButton } from './view_agents_button';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
height: 50%;
|
||||
border-left: ${(props) => props.theme.euiTheme.border.thin};
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
||||
export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
||||
action: ActionStatus;
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, abortUpgrade, onClickViewAgents }) => {
|
||||
onClickManageAutoUpgradeAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, abortUpgrade, onClickViewAgents, onClickManageAutoUpgradeAgents }) => {
|
||||
const { docLinks } = useStartServices();
|
||||
const isAutomaticUpgrade = action.is_automatic;
|
||||
const [isAborting, setIsAborting] = useState(false);
|
||||
const onClickAbortUpgrade = useCallback(async () => {
|
||||
try {
|
||||
|
@ -63,6 +76,23 @@ export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
|||
return (
|
||||
<EuiPanel hasBorder={true} borderRadius="none">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" data-test-subj="upgradeInProgressDescription">
|
||||
<p>
|
||||
{isScheduled && action.startTime ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.scheduledDescription"
|
||||
defaultMessage="Scheduled for "
|
||||
/>
|
||||
<strong>{formattedTime(action.startTime)}</strong>.
|
||||
</>
|
||||
) : (
|
||||
<>{inProgressDescription(action.creationTime)} </>
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -80,7 +110,7 @@ export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
|||
}}
|
||||
/>
|
||||
) : (
|
||||
inProgressTitle(action)
|
||||
inProgressTitle(action, isAutomaticUpgrade)
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
@ -88,40 +118,45 @@ export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" data-test-subj="upgradeInProgressDescription">
|
||||
<p>
|
||||
{isScheduled && action.startTime ? (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
{isAutomaticUpgrade && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="manageAutoUpgradesButton"
|
||||
onClick={() => onClickManageAutoUpgradeAgents(action)}
|
||||
size="m"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.scheduledDescription"
|
||||
defaultMessage="Scheduled for "
|
||||
id="xpack.fleet.agentActivityFlyout.manageAutoUpgradeAgents"
|
||||
defaultMessage="Manage auto-upgrade agents"
|
||||
/>
|
||||
<strong>{formattedTime(action.startTime)}</strong>.
|
||||
</>
|
||||
) : (
|
||||
<>{inProgressDescription(action.creationTime)} </>
|
||||
)}
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.upgradeDescription"
|
||||
defaultMessage="{guideLink} about agent upgrades."
|
||||
values={{
|
||||
guideLink: (
|
||||
<EuiLink href={docLinks.links.fleet.upgradeElasticAgent} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.guideLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
</EuiFlexItem>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty>
|
||||
<EuiLink href={docLinks.links.fleet.upgradeElasticAgent} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.guideLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
{showCancelButton ? (
|
||||
<EuiButton
|
||||
|
|
|
@ -29,7 +29,6 @@ export const ViewAgentsButton: React.FunctionComponent<{
|
|||
<EuiButtonEmpty
|
||||
size="m"
|
||||
onClick={() => onClickViewAgents(action)}
|
||||
flush="left"
|
||||
data-test-subj="agentActivityFlyout.viewAgentsButton"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
|
|
|
@ -339,7 +339,10 @@ describe('AgentUpgradeStatus', () => {
|
|||
error_msg: 'Something went wrong',
|
||||
},
|
||||
},
|
||||
local_metadata: { elastic: { agent: { version: '8.11.0', upgradeable: true } } },
|
||||
},
|
||||
isAgentUpgradable: true,
|
||||
latestAgentVersion: '8.12.0',
|
||||
});
|
||||
|
||||
expectUpgradeStatusBadgeLabel(results, 'Upgrade failed');
|
||||
|
|
|
@ -17,6 +17,10 @@ import {
|
|||
isAgentUpgradeAvailable,
|
||||
} from '../../../../../../../common/services';
|
||||
|
||||
import { AUTO_UPGRADE_DEFAULT_RETRIES } from '../../../../../../../common/constants';
|
||||
|
||||
import { useConfig } from '../../../../hooks';
|
||||
|
||||
/**
|
||||
* Returns a user-friendly string for the estimated remaining time until the upgrade is scheduled.
|
||||
*/
|
||||
|
@ -282,17 +286,7 @@ export const AgentUpgradeStatus: React.FC<{
|
|||
const status = useMemo(() => getStatusComponents(agent.upgrade_details), [agent.upgrade_details]);
|
||||
const minVersion = '8.12';
|
||||
const notUpgradeableMessage = getNotUpgradeableMessage(agent, latestAgentVersion);
|
||||
|
||||
if (isAgentUpgradable && isAgentUpgradeAvailable(agent, latestAgentVersion)) {
|
||||
return (
|
||||
<EuiBadge color="hollow" iconType="sortUp">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUpgradeStatusBadge.upgradeAvailable"
|
||||
defaultMessage="Upgrade available"
|
||||
/>
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
||||
const retryDelays = useConfig().autoUpgrades?.retryDelays ?? AUTO_UPGRADE_DEFAULT_RETRIES;
|
||||
|
||||
if (agent.upgrade_details && status) {
|
||||
return (
|
||||
|
@ -311,6 +305,39 @@ export const AgentUpgradeStatus: React.FC<{
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
if (agent.upgrade_attempts && agent.upgrade_attempts.length > 1 && agent.status === 'updating') {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
type="warning"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUpgradeStatusTooltip.retryingUpgrade"
|
||||
defaultMessage="Retrying Upgrade ({retryCount}/{maxRetries} attempts)"
|
||||
values={{
|
||||
retryCount: agent.upgrade_attempts.length,
|
||||
maxRetries: retryDelays.length,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
color="subdued"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAgentUpgradable && isAgentUpgradeAvailable(agent, latestAgentVersion)) {
|
||||
return (
|
||||
<EuiBadge color="hollow" iconType="sortUp">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUpgradeStatusBadge.upgradeAvailable"
|
||||
defaultMessage="Upgrade available"
|
||||
/>
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAgentUpgrading) {
|
||||
return (
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
import { useFleetServerUnhealthy } from '../hooks/use_fleet_server_unhealthy';
|
||||
|
||||
import { AgentRequestDiagnosticsModal } from '../components/agent_request_diagnostics_modal';
|
||||
import { ManageAutoUpgradeAgentsModal } from '../components/manage_auto_upgrade_agents_modal';
|
||||
|
||||
import type { SelectionMode } from './components/types';
|
||||
|
||||
|
@ -62,6 +63,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
isOpen: false,
|
||||
});
|
||||
const [isAgentActivityFlyoutOpen, setAgentActivityFlyoutOpen] = useState(false);
|
||||
const [isManageAutoUpgradeModalOpen, setManageAutoUpgradeModalOpen] = useState(false);
|
||||
|
||||
const [selectedPolicyId, setSelectedPolicyId] = useState<string | undefined>();
|
||||
const flyoutContext = useFlyoutContext();
|
||||
|
||||
// Agent actions states
|
||||
|
@ -285,12 +289,26 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
<AgentActivityFlyout
|
||||
onAbortSuccess={fetchData}
|
||||
onClose={() => setAgentActivityFlyoutOpen(false)}
|
||||
openManageAutoUpgradeModal={(policyId: string) => {
|
||||
setSelectedPolicyId(policyId);
|
||||
|
||||
setManageAutoUpgradeModalOpen(true);
|
||||
}}
|
||||
refreshAgentActivity={isLoading}
|
||||
setSearch={setSearch}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
/>
|
||||
</EuiPortal>
|
||||
) : null}
|
||||
{isManageAutoUpgradeModalOpen ? (
|
||||
<EuiPortal>
|
||||
<ManageAutoUpgradeAgentsModal
|
||||
key={selectedPolicyId}
|
||||
onClose={() => setManageAutoUpgradeModalOpen(false)}
|
||||
agentPolicy={allAgentPolicies.find((p) => p.id === selectedPolicyId)!}
|
||||
/>
|
||||
</EuiPortal>
|
||||
) : null}
|
||||
{enrollmentFlyout.isOpen ? (
|
||||
<EuiPortal>
|
||||
<AgentEnrollmentFlyout
|
||||
|
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
* 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,
|
||||
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';
|
||||
|
||||
import { StatusColumn } from './status_column';
|
||||
|
||||
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,
|
||||
// required_versions are not sent to agents, so no need to bump revision
|
||||
bumpRevision: false,
|
||||
});
|
||||
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>
|
||||
<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
|
||||
)
|
||||
);
|
||||
}}
|
||||
agentPolicyId={agentPolicy.id}
|
||||
/>
|
||||
<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;
|
||||
agentPolicyId: string;
|
||||
}> = ({ agentsAvailableVersions, requiredVersion, onRemove, onUpdate, agentPolicyId }) => {
|
||||
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="stretch">
|
||||
<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"
|
||||
title={'Rounding Applied'}
|
||||
content={
|
||||
<FormattedMessage
|
||||
data-test-subj="percentageTooltip"
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.percentageTooltip"
|
||||
defaultMessage="The actual percentage of agents upgraded may vary slightly due to rounding. For example, selecting 30% of 25 agents may result in 8 agents being upgraded (32%)."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<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={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.statusTitle"
|
||||
defaultMessage="Status"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StatusColumn agentPolicyId={agentPolicyId} version={version} percentage={percentage} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ 'align-self': 'end' }}>
|
||||
<EuiFormRow label="">
|
||||
<EuiButton onClick={onRemove} color="text">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.removeVersionButton"
|
||||
defaultMessage="Remove"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useGetAutoUpgradeAgentsStatusQuery, useLink } from '../../../../../../hooks';
|
||||
|
||||
export const StatusColumn: React.FunctionComponent<{
|
||||
agentPolicyId: string;
|
||||
version: string;
|
||||
percentage: number;
|
||||
}> = ({ agentPolicyId, version, percentage }) => {
|
||||
const { getHref } = useLink();
|
||||
const { data: autoUpgradeAgentsStatus } = useGetAutoUpgradeAgentsStatusQuery(agentPolicyId);
|
||||
|
||||
const getAgentsHref = useCallback(
|
||||
(failed?: boolean): string => {
|
||||
const kuery = failed
|
||||
? `policy_id:"${agentPolicyId}" AND upgrade_details.state:"UPG_FAILED" AND upgrade_details.target_version:"${version}"`
|
||||
: `policy_id:"${agentPolicyId}" AND agent.version:"${version}"`;
|
||||
return getHref('agent_list', {
|
||||
kuery,
|
||||
});
|
||||
},
|
||||
[getHref, agentPolicyId, version]
|
||||
);
|
||||
|
||||
const calcPercentage = useCallback(
|
||||
(agents: number): number =>
|
||||
autoUpgradeAgentsStatus
|
||||
? Math.round((agents / autoUpgradeAgentsStatus.totalAgents) * 100)
|
||||
: 0,
|
||||
[autoUpgradeAgentsStatus]
|
||||
);
|
||||
|
||||
const agentVersionCounts = useMemo(() => {
|
||||
return (
|
||||
autoUpgradeAgentsStatus?.currentVersions.find((value) => value.version === version) ?? {
|
||||
version,
|
||||
agents: 0,
|
||||
failedUpgradeAgents: 0,
|
||||
}
|
||||
);
|
||||
}, [autoUpgradeAgentsStatus, version]);
|
||||
|
||||
const currentPercentage = useMemo(() => {
|
||||
const result = calcPercentage(agentVersionCounts.agents);
|
||||
return `${result}%`;
|
||||
}, [agentVersionCounts, calcPercentage]);
|
||||
|
||||
const currentStatus = useMemo(() => {
|
||||
const inProgressStatus = (
|
||||
<EuiButtonEmpty size="s" iconType="clock" href={getAgentsHref(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.inProgressText"
|
||||
defaultMessage="In progress"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
const failedStatus = (
|
||||
<EuiButtonEmpty size="s" iconType="errorFilled" color="danger" href={getAgentsHref(true)}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.failedText"
|
||||
defaultMessage="Upgrade failed"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
const completedStatus = (
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
iconType="checkInCircleFilled"
|
||||
color="success"
|
||||
href={getAgentsHref(false)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.completedText"
|
||||
defaultMessage="Completed"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
const notStartedStatus = (
|
||||
<EuiButtonEmpty size="s" iconType="minusInCircle" color="text">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.notStartedText"
|
||||
defaultMessage="Not started"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
let statusButton = inProgressStatus;
|
||||
|
||||
if (agentVersionCounts.failedUpgradeAgents > 0) {
|
||||
statusButton = failedStatus;
|
||||
} else if (agentVersionCounts.agents === 0) {
|
||||
statusButton = notStartedStatus;
|
||||
} else {
|
||||
const currPercentage = calcPercentage(agentVersionCounts.agents);
|
||||
if (currPercentage >= percentage) {
|
||||
statusButton = completedStatus;
|
||||
} else {
|
||||
statusButton = inProgressStatus;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={
|
||||
agentVersionCounts.agents > 0 ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.currentStatusTooltip"
|
||||
defaultMessage="{agents} agents on target version"
|
||||
values={{
|
||||
agents: agentVersionCounts.agents,
|
||||
}}
|
||||
/>
|
||||
) : agentVersionCounts.failedUpgradeAgents > 0 ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.manageAutoUpgradeAgents.failedStatusTooltip"
|
||||
defaultMessage="{failedUpgradeAgents} agents failed to upgrade"
|
||||
values={{
|
||||
failedUpgradeAgents: agentVersionCounts.failedUpgradeAgents,
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{statusButton}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}, [agentVersionCounts, percentage, calcPercentage, getAgentsHref]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">{currentPercentage}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem component="span">{currentStatus}</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 {
|
||||
|
|
|
@ -230,6 +230,8 @@ export const pagePathGetters: {
|
|||
? `?kuery=${kuery}&showInactive=true`
|
||||
: showInactive
|
||||
? '?showInactive=true'
|
||||
: kuery
|
||||
? `?kuery=${kuery}`
|
||||
: ''
|
||||
}`,
|
||||
],
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import type { GetAutoUpgradeAgentsStatusResponse } from '../../../common/types';
|
||||
|
||||
import { agentPolicyRouteService } from '../../services';
|
||||
import { API_VERSIONS } from '../../../common/constants';
|
||||
|
||||
|
@ -128,6 +130,16 @@ export const sendGetOneAgentPolicy = (agentPolicyId: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export function useGetAutoUpgradeAgentsStatusQuery(agentPolicyId: string) {
|
||||
return useQuery(['auto_upgrade_agents_status'], () =>
|
||||
sendRequestForRq<GetAutoUpgradeAgentsStatusResponse>({
|
||||
method: 'get',
|
||||
path: agentPolicyRouteService.getAutoUpgradeAgentsStatusPath(agentPolicyId),
|
||||
version: API_VERSIONS.public.v1,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const sendCreateAgentPolicy = (
|
||||
body: CreateAgentPolicyRequest['body'],
|
||||
{ withSysMonitoring }: { withSysMonitoring: boolean } = { withSysMonitoring: false }
|
||||
|
@ -153,6 +165,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',
|
||||
|
|
|
@ -283,6 +283,11 @@ export const config: PluginConfigDescriptor = {
|
|||
min: 400,
|
||||
})
|
||||
),
|
||||
autoUpgrades: schema.maybe(
|
||||
schema.object({
|
||||
retryDelays: schema.maybe(schema.arrayOf(schema.string())),
|
||||
})
|
||||
),
|
||||
},
|
||||
{
|
||||
validate: (configToValidate) => {
|
||||
|
|
|
@ -143,6 +143,7 @@ export const createAppContextStartContractMock = (
|
|||
unenrollInactiveAgentsTask: {} as any,
|
||||
deleteUnenrolledAgentsTask: {} as any,
|
||||
updateAgentlessDeploymentsTask: {} as any,
|
||||
automaticAgentUpgradeTask: {} as any,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -147,6 +147,7 @@ import { registerDeployAgentPoliciesTask } from './services/agent_policies/deplo
|
|||
import { DeleteUnenrolledAgentsTask } from './tasks/delete_unenrolled_agents_task';
|
||||
import { registerBumpAgentPoliciesTask } from './services/agent_policies/bump_agent_policies_task';
|
||||
import { UpgradeAgentlessDeploymentsTask } from './tasks/upgrade_agentless_deployment';
|
||||
import { AutomaticAgentUpgradeTask } from './tasks/automatic_agent_upgrade_task';
|
||||
|
||||
export interface FleetSetupDeps {
|
||||
security: SecurityPluginSetup;
|
||||
|
@ -199,6 +200,7 @@ export interface FleetAppContext {
|
|||
unenrollInactiveAgentsTask: UnenrollInactiveAgentsTask;
|
||||
deleteUnenrolledAgentsTask: DeleteUnenrolledAgentsTask;
|
||||
updateAgentlessDeploymentsTask: UpgradeAgentlessDeploymentsTask;
|
||||
automaticAgentUpgradeTask: AutomaticAgentUpgradeTask;
|
||||
taskManagerStart?: TaskManagerStartContract;
|
||||
}
|
||||
|
||||
|
@ -299,6 +301,7 @@ export class FleetPlugin
|
|||
private unenrollInactiveAgentsTask?: UnenrollInactiveAgentsTask;
|
||||
private deleteUnenrolledAgentsTask?: DeleteUnenrolledAgentsTask;
|
||||
private updateAgentlessDeploymentsTask?: UpgradeAgentlessDeploymentsTask;
|
||||
private automaticAgentUpgradeTask?: AutomaticAgentUpgradeTask;
|
||||
|
||||
private agentService?: AgentService;
|
||||
private packageService?: PackageService;
|
||||
|
@ -628,7 +631,7 @@ export class FleetPlugin
|
|||
registerRoutes(fleetAuthzRouter, config);
|
||||
|
||||
this.telemetryEventsSender.setup(deps.telemetry);
|
||||
// Register task
|
||||
// Register tasks
|
||||
registerUpgradeManagedPackagePoliciesTask(deps.taskManager);
|
||||
registerDeployAgentPoliciesTask(deps.taskManager);
|
||||
registerBumpAgentPoliciesTask(deps.taskManager);
|
||||
|
@ -654,6 +657,11 @@ export class FleetPlugin
|
|||
taskManager: deps.taskManager,
|
||||
logFactory: this.initializerContext.logger,
|
||||
});
|
||||
this.automaticAgentUpgradeTask = new AutomaticAgentUpgradeTask({
|
||||
core,
|
||||
taskManager: deps.taskManager,
|
||||
logFactory: this.initializerContext.logger,
|
||||
});
|
||||
|
||||
// Register fields metadata extractors
|
||||
registerFieldsMetadataExtractors({ core, fieldsMetadata: deps.fieldsMetadata });
|
||||
|
@ -702,6 +710,7 @@ export class FleetPlugin
|
|||
unenrollInactiveAgentsTask: this.unenrollInactiveAgentsTask!,
|
||||
deleteUnenrolledAgentsTask: this.deleteUnenrolledAgentsTask!,
|
||||
updateAgentlessDeploymentsTask: this.updateAgentlessDeploymentsTask!,
|
||||
automaticAgentUpgradeTask: this.automaticAgentUpgradeTask!,
|
||||
taskManagerStart: plugins.taskManager,
|
||||
});
|
||||
licenseService.start(plugins.licensing.license$);
|
||||
|
@ -714,7 +723,7 @@ export class FleetPlugin
|
|||
this.updateAgentlessDeploymentsTask
|
||||
?.start({ taskManager: plugins.taskManager })
|
||||
.catch(() => {});
|
||||
|
||||
this.automaticAgentUpgradeTask?.start({ taskManager: plugins.taskManager }).catch(() => {});
|
||||
startFleetUsageLogger(plugins.taskManager).catch(() => {});
|
||||
this.fleetMetricsTask
|
||||
?.start(plugins.taskManager, core.elasticsearch.client.asInternalUser)
|
||||
|
|
|
@ -33,6 +33,7 @@ import type {
|
|||
BulkGetAgentPoliciesRequestSchema,
|
||||
AgentPolicy,
|
||||
FleetRequestHandlerContext,
|
||||
GetAutoUpgradeAgentsStatusRequestSchema,
|
||||
} from '../../types';
|
||||
|
||||
import type {
|
||||
|
@ -52,6 +53,7 @@ import { AgentPolicyNotFoundError, FleetUnauthorizedError, FleetError } from '..
|
|||
import { createAgentPolicyWithPackages } from '../../services/agent_policy_create';
|
||||
import { updateAgentPolicySpaces } from '../../services/spaces/agent_policy';
|
||||
import { packagePolicyToSimplifiedPackagePolicy } from '../../../common/services/simplified_package_policy_helper';
|
||||
import { getAutoUpgradeAgentsStatus } from '../../services/agents';
|
||||
|
||||
export async function populateAssignedAgentsCount(
|
||||
agentClient: AgentClient,
|
||||
|
@ -268,6 +270,20 @@ export const getOneAgentPolicyHandler: FleetRequestHandler<
|
|||
}
|
||||
};
|
||||
|
||||
export const getAutoUpgradeAgentsStatusHandler: FleetRequestHandler<
|
||||
TypeOf<typeof GetAutoUpgradeAgentsStatusRequestSchema.params>,
|
||||
undefined
|
||||
> = async (context, request, response) => {
|
||||
const [_, fleetContext] = await Promise.all([context.core, context.fleet]);
|
||||
|
||||
const agentClient = fleetContext.agentClient.asCurrentUser;
|
||||
|
||||
const body = await getAutoUpgradeAgentsStatus(agentClient, request.params.agentPolicyId);
|
||||
return response.ok({
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
export const createAgentPolicyHandler: FleetRequestHandler<
|
||||
undefined,
|
||||
TypeOf<typeof CreateAgentPolicyRequestSchema.query>,
|
||||
|
@ -356,7 +372,7 @@ export const updateAgentPolicyHandler: FleetRequestHandler<
|
|||
const fleetContext = await context.fleet;
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
const user = appContextService.getSecurityCore().authc.getCurrentUser(request) || undefined;
|
||||
const { force, space_ids: spaceIds, ...data } = request.body;
|
||||
const { force, bumpRevision, space_ids: spaceIds, ...data } = request.body;
|
||||
|
||||
let spaceId = fleetContext.spaceId;
|
||||
|
||||
|
@ -382,7 +398,7 @@ export const updateAgentPolicyHandler: FleetRequestHandler<
|
|||
esClient,
|
||||
request.params.agentPolicyId,
|
||||
data,
|
||||
{ force, user, spaceId }
|
||||
{ force, bumpRevision, user, spaceId }
|
||||
);
|
||||
|
||||
let item: any = agentPolicy;
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { FleetAuthzRouter } from '../../services/security';
|
|||
import { API_VERSIONS } from '../../../common/constants';
|
||||
|
||||
import { AGENT_POLICY_API_ROUTES } from '../../constants';
|
||||
import { type FleetConfigType } from '../../config';
|
||||
import {
|
||||
GetAgentPoliciesRequestSchema,
|
||||
GetOneAgentPolicyRequestSchema,
|
||||
|
@ -20,9 +21,12 @@ import {
|
|||
GetFullAgentPolicyRequestSchema,
|
||||
GetK8sManifestRequestSchema,
|
||||
BulkGetAgentPoliciesRequestSchema,
|
||||
GetAutoUpgradeAgentsStatusRequestSchema,
|
||||
GetAutoUpgradeAgentsStatusResponseSchema,
|
||||
} from '../../types';
|
||||
|
||||
import { K8S_API_ROUTES } from '../../../common/constants';
|
||||
import { parseExperimentalConfigValue } from '../../../common/experimental_features';
|
||||
|
||||
import {
|
||||
getAgentPoliciesHandler,
|
||||
|
@ -36,9 +40,10 @@ import {
|
|||
downloadK8sManifest,
|
||||
getK8sManifest,
|
||||
bulkGetAgentPoliciesHandler,
|
||||
getAutoUpgradeAgentsStatusHandler,
|
||||
} from './handlers';
|
||||
|
||||
export const registerRoutes = (router: FleetAuthzRouter) => {
|
||||
export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => {
|
||||
// List - Fleet Server needs access to run setup
|
||||
router.versioned
|
||||
.get({
|
||||
|
@ -103,6 +108,36 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
|
|||
getOneAgentPolicyHandler
|
||||
);
|
||||
|
||||
const experimentalFeatures = parseExperimentalConfigValue(config.enableExperimental);
|
||||
if (experimentalFeatures.enableAutomaticAgentUpgrades) {
|
||||
router.versioned
|
||||
.get({
|
||||
path: AGENT_POLICY_API_ROUTES.AUTO_UPGRADE_AGENTS_STATUS_PATTERN,
|
||||
fleetAuthz: {
|
||||
fleet: { readAgents: true },
|
||||
},
|
||||
summary: `Get auto upgrade agent status`,
|
||||
description: `Get auto upgrade agent status`,
|
||||
options: {
|
||||
tags: ['oas-tag:Elastic Agent policies'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: GetAutoUpgradeAgentsStatusRequestSchema,
|
||||
response: {
|
||||
200: {
|
||||
body: () => GetAutoUpgradeAgentsStatusResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getAutoUpgradeAgentsStatusHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Create
|
||||
router.versioned
|
||||
.post({
|
||||
|
|
|
@ -37,7 +37,7 @@ export function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: Fleet
|
|||
registerEPMRoutes(fleetAuthzRouter, config);
|
||||
|
||||
registerSetupRoutes(fleetAuthzRouter, config);
|
||||
registerAgentPolicyRoutes(fleetAuthzRouter);
|
||||
registerAgentPolicyRoutes(fleetAuthzRouter, config);
|
||||
registerPackagePolicyRoutes(fleetAuthzRouter);
|
||||
registerOutputRoutes(fleetAuthzRouter);
|
||||
registerSettingsRoutes(fleetAuthzRouter, config);
|
||||
|
|
|
@ -254,6 +254,7 @@ export const getSavedObjectTypes = (
|
|||
monitoring_pprof_enabled: { type: 'boolean', index: false },
|
||||
monitoring_http: { type: 'flattened', index: false },
|
||||
monitoring_diagnostics: { type: 'flattened', index: false },
|
||||
required_versions: { type: 'flattened', index: false },
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
|
@ -331,6 +332,16 @@ export const getSavedObjectTypes = (
|
|||
},
|
||||
],
|
||||
},
|
||||
'7': {
|
||||
changes: [
|
||||
{
|
||||
type: 'mappings_addition',
|
||||
addedMappings: {
|
||||
required_versions: { type: 'flattened', index: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[AGENT_POLICY_SAVED_OBJECT_TYPE]: {
|
||||
|
@ -379,6 +390,7 @@ export const getSavedObjectTypes = (
|
|||
dynamic: false,
|
||||
properties: {},
|
||||
},
|
||||
required_versions: { type: 'flattened', index: false },
|
||||
},
|
||||
},
|
||||
modelVersions: {
|
||||
|
@ -390,6 +402,16 @@ export const getSavedObjectTypes = (
|
|||
},
|
||||
],
|
||||
},
|
||||
'2': {
|
||||
changes: [
|
||||
{
|
||||
type: 'mappings_addition',
|
||||
addedMappings: {
|
||||
required_versions: { type: 'flattened', index: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[OUTPUT_SAVED_OBJECT_TYPE]: {
|
||||
|
|
|
@ -11,3 +11,4 @@ export {
|
|||
storedPackagePoliciesToAgentInputs,
|
||||
} from './package_policies_to_agent_inputs';
|
||||
export { getDataOutputForAgentPolicy, validateOutputForPolicy } from './outputs_helpers';
|
||||
export { validateRequiredVersions } from './required_versions';
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { appContextService } from '..';
|
||||
import { AgentPolicyInvalidError } from '../../errors';
|
||||
|
||||
import { validateRequiredVersions } from './required_versions';
|
||||
|
||||
describe('validateRequiredVersions', () => {
|
||||
it('should throw error if feature flag is disabled', () => {
|
||||
jest
|
||||
.spyOn(appContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableAutomaticAgentUpgrades: false } as any);
|
||||
|
||||
expect(() => {
|
||||
validateRequiredVersions('test policy', [{ version: '9.0.0', percentage: 100 }]);
|
||||
}).toThrow(
|
||||
new AgentPolicyInvalidError(
|
||||
`Policy "test policy" failed validation: required_versions are not allowed when automatic upgrades feature is disabled`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
describe('feature flag enabled', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(appContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableAutomaticAgentUpgrades: true } as any);
|
||||
});
|
||||
|
||||
it('should throw error if duplicate versions', () => {
|
||||
expect(() => {
|
||||
validateRequiredVersions('test policy', [
|
||||
{ version: '9.0.0', percentage: 10 },
|
||||
{ version: '9.0.0', percentage: 10 },
|
||||
]);
|
||||
}).toThrow(
|
||||
new AgentPolicyInvalidError(
|
||||
`Policy "test policy" failed required_versions validation: duplicate versions not allowed`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if has invalid semver version', () => {
|
||||
expect(() => {
|
||||
validateRequiredVersions('test policy', [
|
||||
{ version: '9.0.0', percentage: 10 },
|
||||
{ version: '9.0.0invalid', percentage: 10 },
|
||||
]);
|
||||
}).toThrow(
|
||||
new AgentPolicyInvalidError(
|
||||
`Policy "test policy" failed required_versions validation: invalid semver version 9.0.0invalid`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if sum of percentages exceeds 100', () => {
|
||||
expect(() => {
|
||||
validateRequiredVersions('test policy', [
|
||||
{ version: '9.0.0', percentage: 100 },
|
||||
{ version: '9.1.0', percentage: 10 },
|
||||
]);
|
||||
}).toThrow(
|
||||
new AgentPolicyInvalidError(
|
||||
`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`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw error if valid required_versions', () => {
|
||||
validateRequiredVersions('test policy', [
|
||||
{ version: '9.0.0', percentage: 90 },
|
||||
{ version: '9.1.0', percentage: 10 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not throw error if required_versions undefined', () => {
|
||||
validateRequiredVersions('test policy');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 type { AgentTargetVersion } from '../../../common/types';
|
||||
|
||||
import { AgentPolicyInvalidError } from '../../errors';
|
||||
import { appContextService } from '..';
|
||||
import { checkTargetVersionsValidity } from '../../../common/services/agent_utils';
|
||||
|
||||
export function validateRequiredVersions(
|
||||
name: string,
|
||||
requiredVersions?: AgentTargetVersion[] | null
|
||||
): void {
|
||||
if (!requiredVersions) {
|
||||
return;
|
||||
}
|
||||
if (!appContextService.getExperimentalFeatures().enableAutomaticAgentUpgrades) {
|
||||
throw new AgentPolicyInvalidError(
|
||||
`Policy "${name}" failed validation: required_versions are not allowed when automatic upgrades feature is disabled`
|
||||
);
|
||||
}
|
||||
const error = checkTargetVersionsValidity(requiredVersions);
|
||||
if (error) {
|
||||
throw new AgentPolicyInvalidError(
|
||||
`Policy "${name}" failed required_versions validation: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -110,7 +110,11 @@ import { incrementPackagePolicyCopyName } from './package_policies';
|
|||
import { outputService } from './output';
|
||||
import { agentPolicyUpdateEventHandler } from './agent_policy_update';
|
||||
import { escapeSearchQueryPhrase, normalizeKuery as _normalizeKuery } from './saved_object';
|
||||
import { getFullAgentPolicy, validateOutputForPolicy } from './agent_policies';
|
||||
import {
|
||||
getFullAgentPolicy,
|
||||
validateOutputForPolicy,
|
||||
validateRequiredVersions,
|
||||
} from './agent_policies';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
import { licenseService } from './license';
|
||||
import { createSoFindIterable } from './utils/create_so_find_iterable';
|
||||
|
@ -408,6 +412,7 @@ class AgentPolicyService {
|
|||
{},
|
||||
getAllowedOutputTypesForAgentPolicy(agentPolicy)
|
||||
);
|
||||
validateRequiredVersions(agentPolicy.name, agentPolicy.required_versions);
|
||||
|
||||
const newSo = await soClient.create<AgentPolicySOAttributes>(
|
||||
savedObjectType,
|
||||
|
@ -690,6 +695,7 @@ class AgentPolicyService {
|
|||
spaceId?: string;
|
||||
authorizationHeader?: HTTPAuthorizationHeader | null;
|
||||
skipValidation?: boolean;
|
||||
bumpRevision?: boolean;
|
||||
}
|
||||
): Promise<AgentPolicy> {
|
||||
const logger = appContextService.getLogger();
|
||||
|
@ -708,6 +714,7 @@ class AgentPolicyService {
|
|||
namespace: agentPolicy.namespace,
|
||||
});
|
||||
}
|
||||
validateRequiredVersions(agentPolicy.name ?? id, agentPolicy.required_versions);
|
||||
|
||||
const existingAgentPolicy = await this.get(soClient, id, true);
|
||||
|
||||
|
@ -760,7 +767,7 @@ class AgentPolicyService {
|
|||
}
|
||||
|
||||
return this._update(soClient, esClient, id, agentPolicy, options?.user, {
|
||||
bumpRevision: true,
|
||||
bumpRevision: options?.bumpRevision ?? true,
|
||||
removeProtection: false,
|
||||
skipValidation: options?.skipValidation ?? false,
|
||||
}).then((updatedAgentPolicy) => {
|
||||
|
|
|
@ -165,7 +165,6 @@ async function getActionResults(
|
|||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
...action,
|
||||
nbAgentsAck: nbAgentsAck - errorCount,
|
||||
|
@ -276,6 +275,8 @@ async function getActions(
|
|||
creationTime: source['@timestamp']!,
|
||||
nbAgentsFailed: 0,
|
||||
hasRolloutPeriod: !!source.rollout_duration_seconds,
|
||||
is_automatic: source.is_automatic,
|
||||
policyId: source.policyId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -64,6 +64,8 @@ export async function createAgentAction(
|
|||
rollout_duration_seconds: newAgentAction.rollout_duration_seconds,
|
||||
total: newAgentAction.total,
|
||||
traceparent: apm.currentTraceparent,
|
||||
is_automatic: newAgentAction.is_automatic,
|
||||
policyId: newAgentAction.policyId,
|
||||
};
|
||||
|
||||
const messageSigningService = appContextService.getMessageSigningService();
|
||||
|
@ -83,7 +85,9 @@ export async function createAgentAction(
|
|||
});
|
||||
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User created Fleet action [id=${actionId}]`,
|
||||
message: `${
|
||||
newAgentAction.is_automatic ? 'Automatic Upgrade' : 'User'
|
||||
} created Fleet action [id=${actionId}]`,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { AGENTS_PREFIX } from '../../../common';
|
||||
import type {
|
||||
CurrentVersionCount,
|
||||
GetAutoUpgradeAgentsStatusResponse,
|
||||
} from '../../../common/types/rest_spec/agent_policy';
|
||||
|
||||
import type { AgentClient } from './agent_service';
|
||||
|
||||
export async function getAutoUpgradeAgentsStatus(
|
||||
agentClient: AgentClient,
|
||||
agentPolicyId: string
|
||||
): Promise<GetAutoUpgradeAgentsStatusResponse> {
|
||||
const currentVersionsMap: {
|
||||
[version: string]: CurrentVersionCount;
|
||||
} = {};
|
||||
let total = 0;
|
||||
|
||||
await agentClient
|
||||
.listAgents({
|
||||
showInactive: false,
|
||||
perPage: 0,
|
||||
kuery: `${AGENTS_PREFIX}.policy_id:"${agentPolicyId}"`,
|
||||
aggregations: {
|
||||
versions: {
|
||||
terms: {
|
||||
field: 'agent.version',
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
(result.aggregations?.versions as any)?.buckets.forEach(
|
||||
(bucket: { key: string; doc_count: number }) =>
|
||||
(currentVersionsMap[bucket.key] = {
|
||||
version: bucket.key,
|
||||
agents: bucket.doc_count,
|
||||
failedUpgradeAgents: 0,
|
||||
})
|
||||
);
|
||||
total = result.total;
|
||||
});
|
||||
|
||||
await agentClient
|
||||
.listAgents({
|
||||
showInactive: false,
|
||||
perPage: 0,
|
||||
kuery: `${AGENTS_PREFIX}.policy_id:"${agentPolicyId}" AND ${AGENTS_PREFIX}.upgrade_details.state:"UPG_FAILED"`,
|
||||
aggregations: {
|
||||
versions: {
|
||||
terms: {
|
||||
field: 'upgrade_details.target_version.keyword',
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
(result.aggregations?.versions as any)?.buckets.forEach(
|
||||
(bucket: { key: string; doc_count: number }) =>
|
||||
(currentVersionsMap[bucket.key] = {
|
||||
version: bucket.key,
|
||||
agents: currentVersionsMap[bucket.key]?.agents ?? 0,
|
||||
failedUpgradeAgents: bucket.doc_count,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
currentVersions: Object.values(currentVersionsMap),
|
||||
totalAgents: total,
|
||||
};
|
||||
}
|
|
@ -29,6 +29,7 @@ import {
|
|||
updateAgent,
|
||||
_joinFilters,
|
||||
getByIds,
|
||||
fetchAllAgentsByKuery,
|
||||
} from './crud';
|
||||
|
||||
jest.mock('../audit_logging');
|
||||
|
@ -570,4 +571,42 @@ describe('Agents CRUD test', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAllAgentsByKuery', () => {
|
||||
const createEsSearchResultMock = (ids: string[]) => {
|
||||
const mock = getEsResponse(ids, ids.length, 'online');
|
||||
return {
|
||||
...mock,
|
||||
hits: {
|
||||
...mock.hits,
|
||||
hits: mock.hits.hits.map((item) => ({ ...item, sort: ['enrolled_at'] })),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it('should return an iterator', async () => {
|
||||
expect(await fetchAllAgentsByKuery(esClientMock, soClientMock, {})).toEqual({
|
||||
[Symbol.asyncIterator]: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide agents on every iteration', async () => {
|
||||
const agentIds = [
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
];
|
||||
searchMock
|
||||
.mockResolvedValueOnce(createEsSearchResultMock(agentIds[0]))
|
||||
.mockResolvedValueOnce(createEsSearchResultMock(agentIds[1]))
|
||||
.mockResolvedValueOnce(createEsSearchResultMock([]));
|
||||
|
||||
let testCounter = 0;
|
||||
for await (const agents of await fetchAllAgentsByKuery(esClientMock, soClientMock, {})) {
|
||||
expect(agents.map((agent) => agent.id)).toEqual(agentIds[testCounter]);
|
||||
testCounter++;
|
||||
}
|
||||
|
||||
expect(searchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,6 +32,7 @@ import { getCurrentNamespace } from '../spaces/get_current_namespace';
|
|||
import { isSpaceAwarenessEnabled } from '../spaces/helpers';
|
||||
import { isAgentInNamespace } from '../spaces/agent_namespaces';
|
||||
import { addNamespaceFilteringToQuery } from '../spaces/query_namespaces_filtering';
|
||||
import { createEsSearchIterable } from '../utils/create_es_search_iterable';
|
||||
|
||||
import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers';
|
||||
import { buildAgentStatusRuntimeField } from './build_status_runtime_field';
|
||||
|
@ -229,16 +230,7 @@ export async function getAgentsByKuery(
|
|||
aggregations,
|
||||
spaceId,
|
||||
} = options;
|
||||
const filters = [];
|
||||
|
||||
const useSpaceAwareness = await isSpaceAwarenessEnabled();
|
||||
if (useSpaceAwareness && spaceId) {
|
||||
if (spaceId === DEFAULT_SPACE_ID) {
|
||||
filters.push(`namespaces:"${DEFAULT_SPACE_ID}" or not namespaces:*`);
|
||||
} else {
|
||||
filters.push(`namespaces:"${spaceId}"`);
|
||||
}
|
||||
}
|
||||
const filters = await _getSpaceAwarenessFilter(spaceId);
|
||||
|
||||
if (kuery && kuery !== '') {
|
||||
filters.push(kuery);
|
||||
|
@ -394,6 +386,59 @@ export async function getAllAgentsByKuery(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all agents by kuery in batches.
|
||||
* @param esClient
|
||||
* @param soClient
|
||||
* @param options
|
||||
*/
|
||||
export async function fetchAllAgentsByKuery(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
options: ListWithKuery & { spaceId?: string }
|
||||
): Promise<AsyncIterable<Agent[]>> {
|
||||
const {
|
||||
kuery = '',
|
||||
perPage = SO_SEARCH_LIMIT,
|
||||
sortField = 'enrolled_at',
|
||||
sortOrder = 'desc',
|
||||
spaceId,
|
||||
} = options;
|
||||
|
||||
const filters = await _getSpaceAwarenessFilter(spaceId);
|
||||
if (kuery && kuery !== '') {
|
||||
filters.push(kuery);
|
||||
}
|
||||
const kueryNode = _joinFilters(filters);
|
||||
const query = kueryNode ? { query: toElasticsearchQuery(kueryNode) } : {};
|
||||
const runtimeFields = await buildAgentStatusRuntimeField(soClient);
|
||||
const sort = getSortConfig(sortField, sortOrder);
|
||||
|
||||
try {
|
||||
return createEsSearchIterable<FleetServerAgent>({
|
||||
esClient,
|
||||
searchRequest: {
|
||||
index: AGENTS_INDEX,
|
||||
size: perPage,
|
||||
rest_total_hits_as_int: true,
|
||||
track_total_hits: true,
|
||||
runtime_mappings: runtimeFields,
|
||||
fields: Object.keys(runtimeFields),
|
||||
sort,
|
||||
...query,
|
||||
},
|
||||
resultsMapper: (data): Agent[] => {
|
||||
return data.hits.hits.map(searchHitToAgent);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
appContextService
|
||||
.getLogger()
|
||||
.error(`Error fetching all agents by kuery: ${JSON.stringify(err)}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAgentById(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
|
@ -718,3 +763,15 @@ export async function getAgentPolicyForAgent(
|
|||
return agentPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
async function _getSpaceAwarenessFilter(spaceId: string | undefined) {
|
||||
const useSpaceAwareness = await isSpaceAwarenessEnabled();
|
||||
if (!useSpaceAwareness || !spaceId) {
|
||||
return [];
|
||||
}
|
||||
if (spaceId === DEFAULT_SPACE_ID) {
|
||||
return [`namespaces:"${DEFAULT_SPACE_ID}" or not namespaces:*`];
|
||||
} else {
|
||||
return [`namespaces:"${spaceId}"`];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ export function searchHitToAgent(
|
|||
upgraded_at: hit._source?.upgraded_at,
|
||||
upgrade_started_at: hit._source?.upgrade_started_at,
|
||||
upgrade_details: hit._source?.upgrade_details,
|
||||
upgrade_attempts: hit._source?.upgrade_attempts,
|
||||
access_api_key_id: hit._source?.access_api_key_id,
|
||||
default_api_key_id: hit._source?.default_api_key_id,
|
||||
policy_id: hit._source?.policy_id,
|
||||
|
|
|
@ -20,3 +20,4 @@ export { AgentServiceImpl } from './agent_service';
|
|||
export type { AgentClient, AgentService } from './agent_service';
|
||||
export { BulkActionsResolver } from './bulk_actions_resolver';
|
||||
export { getAvailableVersions, getLatestAvailableAgentVersion } from './versions';
|
||||
export { getAutoUpgradeAgentsStatus } from './auto_upgrade_agents_status';
|
||||
|
|
|
@ -73,6 +73,7 @@ export async function sendUpgradeAgentsActions(
|
|||
upgradeDurationSeconds?: number;
|
||||
startTime?: string;
|
||||
batchSize?: number;
|
||||
isAutomatic?: boolean;
|
||||
}
|
||||
): Promise<{ actionId: string }> {
|
||||
const currentSpaceId = getCurrentNamespace(soClient);
|
||||
|
@ -124,3 +125,22 @@ export async function sendUpgradeAgentsActions(
|
|||
|
||||
return await upgradeBatch(esClient, givenAgents, outgoingErrors, options, currentSpaceId);
|
||||
}
|
||||
|
||||
export async function sendAutomaticUpgradeAgentsActions(
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
options: {
|
||||
agents: Agent[];
|
||||
version: string;
|
||||
upgradeDurationSeconds?: number;
|
||||
}
|
||||
): Promise<{ actionId: string }> {
|
||||
const currentSpaceId = getCurrentNamespace(soClient);
|
||||
return await upgradeBatch(
|
||||
esClient,
|
||||
options.agents,
|
||||
{},
|
||||
{ ...options, isAutomatic: true },
|
||||
currentSpaceId
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,11 +9,13 @@ import type { ElasticsearchClient } from '@kbn/core/server';
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import moment from 'moment';
|
||||
import semverGte from 'semver/functions/gte';
|
||||
|
||||
import {
|
||||
getRecentUpgradeInfoForAgent,
|
||||
getNotUpgradeableMessage,
|
||||
isAgentUpgradeableToVersion,
|
||||
AGENT_UPGARDE_DETAILS_SUPPORTED_VERSION,
|
||||
} from '../../../common/services';
|
||||
|
||||
import type { Agent } from '../../types';
|
||||
|
@ -70,6 +72,7 @@ export async function upgradeBatch(
|
|||
upgradeDurationSeconds?: number;
|
||||
startTime?: string;
|
||||
total?: number;
|
||||
isAutomatic?: boolean;
|
||||
},
|
||||
spaceId?: string
|
||||
): Promise<{ actionId: string }> {
|
||||
|
@ -167,6 +170,10 @@ export async function upgradeBatch(
|
|||
data: {
|
||||
upgraded_at: null,
|
||||
upgrade_started_at: now,
|
||||
...(options.isAutomatic &&
|
||||
semverGte(agent.agent?.version ?? '0.0.0', AGENT_UPGARDE_DETAILS_SUPPORTED_VERSION)
|
||||
? { upgrade_attempts: [now, ...(agent.upgrade_attempts ?? [])] }
|
||||
: {}),
|
||||
},
|
||||
})),
|
||||
errors
|
||||
|
@ -186,6 +193,8 @@ export async function upgradeBatch(
|
|||
agents: agentsToUpdate.map((agent) => agent.id),
|
||||
...rollingUpgradeOptions,
|
||||
namespaces,
|
||||
is_automatic: options.isAutomatic,
|
||||
policyId: agentsToUpdate[0]?.policy_id,
|
||||
});
|
||||
|
||||
await createErrorActionResults(
|
||||
|
|
|
@ -0,0 +1,526 @@
|
|||
/*
|
||||
* 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 type { CoreSetup } from '@kbn/core/server';
|
||||
import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server';
|
||||
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 {
|
||||
fetchAllAgentsByKuery,
|
||||
getAgentsByKuery,
|
||||
sendAutomaticUpgradeAgentsActions,
|
||||
} from '../services/agents';
|
||||
import { isAgentUpgradeable } from '../../common/services';
|
||||
import type { Agent, AgentPolicy } from '../types';
|
||||
|
||||
import { AutomaticAgentUpgradeTask, TYPE, VERSION } from './automatic_agent_upgrade_task';
|
||||
|
||||
jest.mock('../../common/services');
|
||||
jest.mock('../services');
|
||||
jest.mock('../services/agents');
|
||||
|
||||
const MOCK_TASK_INSTANCE = {
|
||||
id: `${TYPE}:${VERSION}`,
|
||||
runAt: new Date(),
|
||||
attempts: 0,
|
||||
ownerId: '',
|
||||
status: TaskStatus.Running,
|
||||
startedAt: new Date(),
|
||||
scheduledAt: new Date(),
|
||||
retryAt: new Date(),
|
||||
params: {},
|
||||
state: {},
|
||||
taskType: TYPE,
|
||||
};
|
||||
|
||||
const mockAgentPolicyService = agentPolicyService as jest.Mocked<typeof agentPolicyService>;
|
||||
const mockedFetchAllAgentsByKuery = fetchAllAgentsByKuery as jest.MockedFunction<
|
||||
typeof fetchAllAgentsByKuery
|
||||
>;
|
||||
const mockedGetAgentsByKuery = getAgentsByKuery as jest.MockedFunction<typeof getAgentsByKuery>;
|
||||
const mockedSendAutomaticUpgradeAgentsActions =
|
||||
sendAutomaticUpgradeAgentsActions as jest.MockedFunction<
|
||||
typeof sendAutomaticUpgradeAgentsActions
|
||||
>;
|
||||
const mockedIsAgentUpgradeable = isAgentUpgradeable as jest.MockedFunction<
|
||||
typeof isAgentUpgradeable
|
||||
>;
|
||||
|
||||
const getMockAgentPolicyFetchAllAgentPolicies = (items: AgentPolicy[]) =>
|
||||
jest.fn().mockResolvedValue(
|
||||
jest.fn(async function* () {
|
||||
yield items;
|
||||
})()
|
||||
);
|
||||
|
||||
const getMockFetchAllAgentsByKuery = (items: Agent[]) =>
|
||||
jest.fn(async function* () {
|
||||
yield items;
|
||||
})();
|
||||
|
||||
const mockDefaultAgentPolicy = () => {
|
||||
const agentPolicies = [
|
||||
{
|
||||
id: 'agent-policy-1',
|
||||
required_versions: [{ version: '8.18.0', percentage: 30 }],
|
||||
},
|
||||
] as AgentPolicy[];
|
||||
mockAgentPolicyService.fetchAllAgentPolicies =
|
||||
getMockAgentPolicyFetchAllAgentPolicies(agentPolicies);
|
||||
};
|
||||
|
||||
const generateAgents = (
|
||||
nAgents: number,
|
||||
agentPolicyId: string = 'agent-policy-1',
|
||||
version: string = '8.15.0',
|
||||
status: string = 'online'
|
||||
) => {
|
||||
return [
|
||||
...Array(nAgents)
|
||||
.fill({})
|
||||
.map((_, i) => ({
|
||||
id: `agent-${i}`,
|
||||
policy_id: agentPolicyId,
|
||||
status,
|
||||
agent: { version },
|
||||
})),
|
||||
] as Agent[];
|
||||
};
|
||||
|
||||
describe('AutomaticAgentUpgradeTask', () => {
|
||||
const { createSetup: coreSetupMock } = coreMock;
|
||||
const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock;
|
||||
|
||||
let mockContract: ReturnType<typeof createAppContextStartContractMock>;
|
||||
let mockTask: AutomaticAgentUpgradeTask;
|
||||
let mockCore: CoreSetup;
|
||||
let mockTaskManagerSetup: jest.Mocked<TaskManagerSetupContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContract = createAppContextStartContractMock();
|
||||
appContextService.start(mockContract);
|
||||
mockCore = coreSetupMock();
|
||||
mockTaskManagerSetup = tmSetupMock();
|
||||
mockTask = new AutomaticAgentUpgradeTask({
|
||||
core: mockCore,
|
||||
taskManager: mockTaskManagerSetup,
|
||||
logFactory: loggingSystemMock.create(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Task lifecycle', () => {
|
||||
it('Should create task', () => {
|
||||
expect(mockTask).toBeInstanceOf(AutomaticAgentUpgradeTask);
|
||||
});
|
||||
|
||||
it('Should register task', () => {
|
||||
expect(mockTaskManagerSetup.registerTaskDefinitions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should schedule task', async () => {
|
||||
const mockTaskManagerStart = tmStartMock();
|
||||
await mockTask.start({ taskManager: mockTaskManagerStart });
|
||||
expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task logic', () => {
|
||||
const runTask = async (taskInstance = MOCK_TASK_INSTANCE) => {
|
||||
const mockTaskManagerStart = tmStartMock();
|
||||
await mockTask.start({ taskManager: mockTaskManagerStart });
|
||||
const createTaskRunner =
|
||||
mockTaskManagerSetup.registerTaskDefinitions.mock.calls[0][0][TYPE].createTaskRunner;
|
||||
const taskRunner = createTaskRunner({ taskInstance });
|
||||
return taskRunner.run();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(appContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableAutomaticAgentUpgrades: true } as any);
|
||||
mockDefaultAgentPolicy();
|
||||
mockedIsAgentUpgradeable.mockReturnValue(true);
|
||||
mockedSendAutomaticUpgradeAgentsActions.mockResolvedValue({ actionId: 'action-1' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('Should not run if task is outdated', async () => {
|
||||
const result = await runTask({ ...MOCK_TASK_INSTANCE, id: 'old-id' });
|
||||
|
||||
expect(mockAgentPolicyService.fetchAllAgentPolicies).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(getDeleteTaskRunResult());
|
||||
});
|
||||
|
||||
it('Should exit if the enableAutomaticAgentUpgrades feature flag is disabled', async () => {
|
||||
jest
|
||||
.spyOn(appContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableAutomaticAgentUpgrades: false } as any);
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockAgentPolicyService.fetchAllAgentPolicies).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should upgrade eligible agents', async () => {
|
||||
const agents = generateAgents(10);
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: agents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: 0 } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery(agents)); // active agents
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: agents.slice(0, 3),
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should take agents already on target version into account', async () => {
|
||||
const agents = [
|
||||
...generateAgents(10),
|
||||
...generateAgents(1, 'agent-policy-1', '8.18.0', 'online'),
|
||||
];
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: agents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: 1 } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValue(getMockFetchAllAgentsByKuery(agents)); // active agents
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: agents.slice(0, 2), // As theres already one upgrading, and 30% of 11 is 3, we only want two items to be sent for upgrade
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should take agents already upgrading to target version into account', async () => {
|
||||
const agents = [
|
||||
...generateAgents(10),
|
||||
...generateAgents(1, 'agent-policy-1', '8.15.0', 'updating'),
|
||||
];
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: agents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: 1 } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValue(getMockFetchAllAgentsByKuery(agents));
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: agents.slice(0, 2),
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not attempt to upgrade already upgrading agents', async () => {
|
||||
const agents = generateAgents(10, 'agent-policy-1', '8.15.0', 'updating');
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: agents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: agents.length } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValue(getMockFetchAllAgentsByKuery(agents)); // active agents
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not take inactive agents into account when checking for upgrade eligibility', async () => {
|
||||
const activeAgents = generateAgents(10, 'agent-policy-1', '8.15.0');
|
||||
const uninstalledAgents = generateAgents(5, 'agent-policy-1', '8.17.3', 'uninstalled');
|
||||
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: activeAgents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: 0 } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery(activeAgents)); // active agents
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: activeAgents.slice(0, 3),
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).not.toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{ agents: uninstalledAgents, version: '8.18.0' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly round agent counts and not over or undershoot the target', async () => {
|
||||
// need to check that things are being rounded correctly. Each sub-array would be an automatic upgrade on a policy with a breakout of how many of each version should be upgraded
|
||||
// the target percentage is what we want to hit, but the count is what we have with the first step of normal rounding
|
||||
// this tests that the adjustment to ensure we dont go over or under the total amount of agents is working as intended
|
||||
const MOCK_VERSIONS_AND_COUNTS = [
|
||||
[
|
||||
{ version: '8.17.3', count: 1, targetPercentage: 33, alreadyUpgrading: 0 }, // 3 way split of 4 agents
|
||||
{ version: '8.17.2', count: 1, targetPercentage: 33, alreadyUpgrading: 0 },
|
||||
|
||||
{ version: '8.17.1', count: 1, targetPercentage: 34, alreadyUpgrading: 0 },
|
||||
],
|
||||
[
|
||||
{ version: '8.17.3', count: 33, targetPercentage: 33, alreadyUpgrading: 0 }, // 3 way split of 99 agents
|
||||
{ version: '8.17.2', count: 33, targetPercentage: 33, alreadyUpgrading: 0 },
|
||||
|
||||
{ version: '8.17.1', count: 34, targetPercentage: 34, alreadyUpgrading: 0 },
|
||||
],
|
||||
[{ version: '8.17.3', count: 60, targetPercentage: 60, alreadyUpgrading: 0 }], // 60% with 99 agents
|
||||
[
|
||||
{ version: '8.17.3', count: 13, targetPercentage: 50, alreadyUpgrading: 0 },
|
||||
{ version: '8.17.2', count: 13, targetPercentage: 50, alreadyUpgrading: 0 }, // 50% each with 25 agents
|
||||
],
|
||||
];
|
||||
const TOTAL_AGENTS_MOCKS = [4, 99, 99, 25]; // how many total agents each array should be using to recalculate the values
|
||||
|
||||
const MOCK_VERSIONS_AND_COUNTS_EXPECTED = [
|
||||
[
|
||||
{ version: '8.17.3', count: 2, targetPercentage: 33, alreadyUpgrading: 0 }, // since we were missing one
|
||||
{ version: '8.17.2', count: 1, targetPercentage: 33, alreadyUpgrading: 0 }, // we should add one to the lowest
|
||||
|
||||
{ version: '8.17.1', count: 1, targetPercentage: 34, alreadyUpgrading: 0 },
|
||||
],
|
||||
[
|
||||
{ version: '8.17.3', count: 32, targetPercentage: 33, alreadyUpgrading: 0 },
|
||||
{ version: '8.17.2', count: 33, targetPercentage: 33, alreadyUpgrading: 0 },
|
||||
|
||||
{ version: '8.17.1', count: 34, targetPercentage: 34, alreadyUpgrading: 0 },
|
||||
],
|
||||
[{ version: '8.17.3', count: 59, targetPercentage: 60, alreadyUpgrading: 0 }], // since the 60 above was over, this should come out to 59
|
||||
[
|
||||
{ version: '8.17.3', count: 12, targetPercentage: 50, alreadyUpgrading: 0 }, // as theres too many above, it should get reduced by 1
|
||||
{ version: '8.17.2', count: 13, targetPercentage: 50, alreadyUpgrading: 0 },
|
||||
],
|
||||
];
|
||||
// now assert on each item using the above mocks
|
||||
for (let i = 0; i < MOCK_VERSIONS_AND_COUNTS.length; i++) {
|
||||
const result = await mockTask.adjustAgentCounts(
|
||||
MOCK_VERSIONS_AND_COUNTS[i],
|
||||
TOTAL_AGENTS_MOCKS[i]
|
||||
);
|
||||
|
||||
expect(result).toEqual(MOCK_VERSIONS_AND_COUNTS_EXPECTED[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should set a rollout duration for upgrade batches bigger than 10 agents', async () => {
|
||||
const agents = generateAgents(100);
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: agents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: 0 } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValue(getMockFetchAllAgentsByKuery(agents)); // active agents
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: agents.slice(0, 30),
|
||||
version: '8.18.0',
|
||||
upgradeDurationSeconds: 600,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should process agent policies in batches', async () => {
|
||||
const firstAgentPoliciesBatch = [
|
||||
{
|
||||
id: 'agent-policy-1',
|
||||
required_versions: [{ version: '8.18.0', percentage: 30 }],
|
||||
},
|
||||
] as AgentPolicy[];
|
||||
const secondAgentPoliciesBatch = [
|
||||
{
|
||||
id: 'agent-policy-2',
|
||||
required_versions: [{ version: '8.18.0', percentage: 30 }],
|
||||
},
|
||||
] as AgentPolicy[];
|
||||
mockAgentPolicyService.fetchAllAgentPolicies = jest.fn().mockResolvedValue(
|
||||
jest.fn(async function* () {
|
||||
yield firstAgentPoliciesBatch;
|
||||
yield secondAgentPoliciesBatch;
|
||||
})()
|
||||
);
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: 0 } as any) // active agents for first policy batch
|
||||
.mockResolvedValueOnce({ total: 10 } as any) // active agents for second policy batch
|
||||
.mockResolvedValueOnce({ total: 0 } as any); // agents on or updating to target version (second policy batch)
|
||||
const agents = generateAgents(10, 'agent-policy-501', '8.15.0');
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValue(getMockFetchAllAgentsByKuery(agents)); // active agents
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: agents.slice(0, 3),
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should process agents in batches', async () => {
|
||||
const agentPolicies = [
|
||||
{
|
||||
id: 'agent-policy-1',
|
||||
required_versions: [{ version: '8.18.0', percentage: 70 }],
|
||||
},
|
||||
] as AgentPolicy[];
|
||||
mockAgentPolicyService.fetchAllAgentPolicies =
|
||||
getMockAgentPolicyFetchAllAgentPolicies(agentPolicies);
|
||||
const agents = generateAgents(20);
|
||||
const firstAgentsBatch = agents.slice(0, 10);
|
||||
const secondAgentsBatch = agents.slice(10);
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: agents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: 0 } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery([])) // agents marked for retry
|
||||
.mockResolvedValueOnce(
|
||||
jest.fn(async function* () {
|
||||
yield firstAgentsBatch;
|
||||
yield secondAgentsBatch;
|
||||
})()
|
||||
);
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: firstAgentsBatch,
|
||||
version: '8.18.0',
|
||||
upgradeDurationSeconds: 600,
|
||||
}
|
||||
);
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: secondAgentsBatch.slice(0, 4),
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should pick up agents in failed upgrade state for retry if they are ready', async () => {
|
||||
jest
|
||||
.spyOn(appContextService, 'getConfig')
|
||||
.mockReturnValue({ autoUpgrades: { retryDelays: ['10m', '20m'] } } as any);
|
||||
|
||||
const agentPolicies = [
|
||||
{
|
||||
id: 'agent-policy-1',
|
||||
required_versions: [{ version: '8.18.0', percentage: 100 }],
|
||||
},
|
||||
] as AgentPolicy[];
|
||||
mockAgentPolicyService.fetchAllAgentPolicies =
|
||||
getMockAgentPolicyFetchAllAgentPolicies(agentPolicies);
|
||||
|
||||
const getDate = (minutesAgo: number) => {
|
||||
return new Date(Date.now() - minutesAgo * 60000).toISOString();
|
||||
};
|
||||
|
||||
const agents = [
|
||||
{
|
||||
id: 'agent-1',
|
||||
policy_id: 'agent-policy-1',
|
||||
status: 'online',
|
||||
agent: { version: '8.15.0' },
|
||||
upgrade_details: {
|
||||
target_version: '8.18.0',
|
||||
state: 'UPG_FAILED',
|
||||
},
|
||||
upgrade_attempts: [getDate(20)], // should be picked up
|
||||
},
|
||||
{
|
||||
id: 'agent-2',
|
||||
policy_id: 'agent-policy-1',
|
||||
status: 'online',
|
||||
agent: { version: '8.15.0' },
|
||||
upgrade_details: {
|
||||
target_version: '8.18.0',
|
||||
state: 'UPG_FAILED',
|
||||
},
|
||||
upgrade_attempts: [getDate(5)], // should NOT be picked up (not ready yet)
|
||||
},
|
||||
{
|
||||
id: 'agent-3',
|
||||
policy_id: 'agent-policy-1',
|
||||
status: 'online',
|
||||
agent: { version: '8.15.0' },
|
||||
upgrade_details: {
|
||||
target_version: '8.18.0',
|
||||
state: 'UPG_FAILED',
|
||||
},
|
||||
upgrade_attempts: [getDate(20), getDate(10), getDate(5)], // should NOT be picked up (exceeded max attempts)
|
||||
},
|
||||
] as unknown as Agent[];
|
||||
|
||||
mockedGetAgentsByKuery
|
||||
.mockResolvedValueOnce({ total: agents.length } as any) // active agents
|
||||
.mockResolvedValueOnce({ total: 0 } as any); // agents on or updating to target version
|
||||
mockedFetchAllAgentsByKuery
|
||||
.mockResolvedValueOnce(getMockFetchAllAgentsByKuery(agents)) // agents marked for retry
|
||||
.mockResolvedValue(getMockFetchAllAgentsByKuery([]));
|
||||
|
||||
await runTask();
|
||||
|
||||
expect(mockedSendAutomaticUpgradeAgentsActions).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{
|
||||
agents: agents.slice(0, 1),
|
||||
version: '8.18.0',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,537 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClient } from '@kbn/core/server';
|
||||
import type {
|
||||
CoreSetup,
|
||||
ElasticsearchClient,
|
||||
Logger,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
import type {
|
||||
ConcreteTaskInstance,
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task';
|
||||
import type { LoggerFactory } from '@kbn/core/server';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import semverGt from 'semver/functions/gt';
|
||||
import moment from 'moment';
|
||||
|
||||
import { AUTO_UPGRADE_DEFAULT_RETRIES } from '../../common/constants';
|
||||
import type {
|
||||
Agent,
|
||||
AgentPolicy,
|
||||
AgentTargetVersion,
|
||||
FleetServerAgentMetadata,
|
||||
} from '../../common/types';
|
||||
|
||||
import { agentPolicyService, appContextService } from '../services';
|
||||
import {
|
||||
fetchAllAgentsByKuery,
|
||||
getAgentsByKuery,
|
||||
sendAutomaticUpgradeAgentsActions,
|
||||
} from '../services/agents';
|
||||
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants';
|
||||
import { AgentStatusKueryHelper, isAgentUpgradeable } from '../../common/services';
|
||||
|
||||
export const TYPE = 'fleet:automatic-agent-upgrade-task';
|
||||
export const VERSION = '1.0.0';
|
||||
const TITLE = 'Fleet Automatic agent upgrades';
|
||||
const SCOPE = ['fleet'];
|
||||
const INTERVAL = '30m';
|
||||
const TIMEOUT = '10m';
|
||||
const AGENT_POLICIES_BATCHSIZE = 500;
|
||||
const AGENTS_BATCHSIZE = 10000;
|
||||
const MIN_AGENTS_FOR_ROLLOUT = 10;
|
||||
const MIN_UPGRADE_DURATION_SECONDS = 600;
|
||||
type AgentWithDefinedVersion = Agent & { agent: FleetServerAgentMetadata };
|
||||
|
||||
interface AutomaticAgentUpgradeTaskSetupContract {
|
||||
core: CoreSetup;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
logFactory: LoggerFactory;
|
||||
}
|
||||
|
||||
interface AutomaticAgentUpgradeTaskStartContract {
|
||||
taskManager: TaskManagerStartContract;
|
||||
}
|
||||
interface UpgradeTargetForVersion {
|
||||
version: string;
|
||||
count: number;
|
||||
targetPercentage: number;
|
||||
alreadyUpgrading: number;
|
||||
}
|
||||
|
||||
export class AutomaticAgentUpgradeTask {
|
||||
private logger: Logger;
|
||||
private wasStarted: boolean = false;
|
||||
private abortController = new AbortController();
|
||||
private retryDelays: string[] = [];
|
||||
|
||||
constructor(setupContract: AutomaticAgentUpgradeTaskSetupContract) {
|
||||
const { core, taskManager, logFactory } = setupContract;
|
||||
this.logger = logFactory.get(this.taskId);
|
||||
|
||||
taskManager.registerTaskDefinitions({
|
||||
[TYPE]: {
|
||||
title: TITLE,
|
||||
timeout: TIMEOUT,
|
||||
createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
|
||||
return {
|
||||
run: async () => {
|
||||
return this.runTask(taskInstance, core);
|
||||
},
|
||||
cancel: async () => {
|
||||
this.abortController.abort('Task timed out');
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start = async ({ taskManager }: AutomaticAgentUpgradeTaskStartContract) => {
|
||||
if (!taskManager) {
|
||||
this.logger.error('[AutomaticAgentUpgradeTask] Missing required service during start');
|
||||
return;
|
||||
}
|
||||
|
||||
this.wasStarted = true;
|
||||
this.logger.info(`[AutomaticAgentUpgradeTask] Started with interval of [${INTERVAL}]`);
|
||||
|
||||
try {
|
||||
await taskManager.ensureScheduled({
|
||||
id: this.taskId,
|
||||
taskType: TYPE,
|
||||
scope: SCOPE,
|
||||
schedule: {
|
||||
interval: INTERVAL,
|
||||
},
|
||||
state: {},
|
||||
params: { version: VERSION },
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(`Error scheduling task AutomaticAgentUpgradeTask, error: ${e.message}`, e);
|
||||
}
|
||||
};
|
||||
|
||||
private get taskId(): string {
|
||||
return `${TYPE}:${VERSION}`;
|
||||
}
|
||||
|
||||
public runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => {
|
||||
if (!appContextService.getExperimentalFeatures().enableAutomaticAgentUpgrades) {
|
||||
this.logger.debug(
|
||||
'[AutomaticAgentUpgradeTask] Aborting runTask: automatic upgrades feature is disabled'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.wasStarted) {
|
||||
this.logger.debug('[AutomaticAgentUpgradeTask] Aborting runTask(): task not started yet');
|
||||
return;
|
||||
}
|
||||
// Check that this task is current
|
||||
if (taskInstance.id !== this.taskId) {
|
||||
this.logger.debug(
|
||||
`[AutomaticAgentUpgradeTask] Outdated task version: Got [${taskInstance.id}] from task instance. Current version is [${this.taskId}]`
|
||||
);
|
||||
return getDeleteTaskRunResult();
|
||||
}
|
||||
|
||||
this.logger.info('[AutomaticAgentUpgradeTask] runTask() started');
|
||||
|
||||
const [coreStart] = await core.getStartServices();
|
||||
const esClient = coreStart.elasticsearch.client.asInternalUser;
|
||||
const soClient = new SavedObjectsClient(coreStart.savedObjects.createInternalRepository());
|
||||
this.retryDelays =
|
||||
appContextService.getConfig()?.autoUpgrades?.retryDelays ?? AUTO_UPGRADE_DEFAULT_RETRIES;
|
||||
|
||||
try {
|
||||
await this.checkAgentPoliciesForAutomaticUpgrades(esClient, soClient);
|
||||
this.endRun('success');
|
||||
} catch (err) {
|
||||
if (err instanceof errors.RequestAbortedError) {
|
||||
this.logger.warn(`[AutomaticAgentUpgradeTask] Request aborted due to timeout: ${err}`);
|
||||
this.endRun();
|
||||
return;
|
||||
}
|
||||
this.logger.error(`[AutomaticAgentUpgradeTask] Error: ${err}`);
|
||||
this.endRun('error');
|
||||
}
|
||||
};
|
||||
|
||||
private endRun(msg: string = '') {
|
||||
this.logger.info(`[AutomaticAgentUpgradeTask] runTask() ended${msg ? ': ' + msg : ''}`);
|
||||
}
|
||||
|
||||
private throwIfAborted() {
|
||||
if (this.abortController.signal.aborted) {
|
||||
throw new Error('Task was aborted');
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAgentPoliciesForAutomaticUpgrades(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract
|
||||
) {
|
||||
// Fetch custom agent policies with set required_versions in batches.
|
||||
const agentPolicyFetcher = await agentPolicyService.fetchAllAgentPolicies(soClient, {
|
||||
kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_managed:false AND ${AGENT_POLICY_SAVED_OBJECT_TYPE}.required_versions:*`,
|
||||
perPage: AGENT_POLICIES_BATCHSIZE,
|
||||
fields: ['id', 'required_versions'],
|
||||
});
|
||||
for await (const agentPolicyPageResults of agentPolicyFetcher) {
|
||||
this.logger.debug(
|
||||
`[AutomaticAgentUpgradeTask] Found ${agentPolicyPageResults.length} agent policies with required_versions`
|
||||
);
|
||||
if (!agentPolicyPageResults.length) {
|
||||
this.endRun('Found no agent policies to process');
|
||||
return;
|
||||
}
|
||||
for (const agentPolicy of agentPolicyPageResults) {
|
||||
this.throwIfAborted();
|
||||
await this.checkAgentPolicyForAutomaticUpgrades(esClient, soClient, agentPolicy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAgentPolicyForAutomaticUpgrades(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentPolicy: AgentPolicy
|
||||
) {
|
||||
this.logger.debug(
|
||||
`[AutomaticAgentUpgradeTask] Processing agent policy ${
|
||||
agentPolicy.id
|
||||
} with required_versions ${JSON.stringify(agentPolicy.required_versions)}`
|
||||
);
|
||||
|
||||
// Get total number of active agents.
|
||||
// This is used to calculate how many agents should be selected for upgrade based on the target percentage.
|
||||
const totalActiveAgents = await this.getAgentCount(
|
||||
esClient,
|
||||
soClient,
|
||||
`policy_id:${agentPolicy.id} AND ${AgentStatusKueryHelper.buildKueryForActiveAgents()}`
|
||||
);
|
||||
if (totalActiveAgents === 0) {
|
||||
this.logger.debug(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id} has no active agents`
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Before processing each required version, we need to get the count of agents for each version so we know if we should round some up or down to make sure we arent overshooting the total number of agents.
|
||||
const versionAndCounts = await this.getVersionAndCounts(
|
||||
agentPolicy,
|
||||
totalActiveAgents,
|
||||
esClient,
|
||||
soClient
|
||||
);
|
||||
|
||||
for (const requiredVersion of agentPolicy.required_versions ?? []) {
|
||||
await this.processRequiredVersion(
|
||||
esClient,
|
||||
soClient,
|
||||
agentPolicy,
|
||||
requiredVersion,
|
||||
versionAndCounts
|
||||
);
|
||||
}
|
||||
}
|
||||
public async getVersionAndCounts(
|
||||
agentPolicy: AgentPolicy,
|
||||
totalActiveAgents: number,
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract
|
||||
) {
|
||||
let versionAndCounts: UpgradeTargetForVersion[] = [];
|
||||
for (const requiredVersion of agentPolicy.required_versions ?? []) {
|
||||
let numberOfAgentsForUpgrade = Math.round(
|
||||
(totalActiveAgents * requiredVersion.percentage) / 100
|
||||
);
|
||||
|
||||
// Subtract the total number of agents already or on or updating to target version.
|
||||
const updatingToKuery = `(upgrade_details.target_version:${requiredVersion.version} AND NOT upgrade_details.state:UPG_FAILED)`;
|
||||
const totalOnOrUpdatingToTargetVersionAgents = await this.getAgentCount(
|
||||
esClient,
|
||||
soClient,
|
||||
`((policy_id:${agentPolicy.id} AND agent.version:${
|
||||
requiredVersion.version
|
||||
}) OR ${updatingToKuery}) AND ${AgentStatusKueryHelper.buildKueryForActiveAgents()}`
|
||||
);
|
||||
|
||||
numberOfAgentsForUpgrade -= totalOnOrUpdatingToTargetVersionAgents;
|
||||
versionAndCounts.push({
|
||||
version: requiredVersion.version,
|
||||
count: numberOfAgentsForUpgrade,
|
||||
targetPercentage: requiredVersion.percentage,
|
||||
alreadyUpgrading: totalOnOrUpdatingToTargetVersionAgents,
|
||||
});
|
||||
}
|
||||
// Then we need to make adjustments based on the total to make sure we arent over or undershooting the total number of agents
|
||||
versionAndCounts = await this.adjustAgentCounts(versionAndCounts, totalActiveAgents);
|
||||
return versionAndCounts;
|
||||
}
|
||||
public async adjustAgentCounts(
|
||||
versionAndCounts: UpgradeTargetForVersion[],
|
||||
totalActiveAgents: number
|
||||
) {
|
||||
// Calculate what we actually have vs what we need to have.
|
||||
// First we need to get the total actual percentage if we actually added the new agents and considering the existing ones
|
||||
const totalActualPercentage =
|
||||
((versionAndCounts.reduce((acc, item) => acc + item.count, 0) +
|
||||
versionAndCounts.reduce((acc, item) => acc + item.alreadyUpgrading, 0)) /
|
||||
totalActiveAgents) *
|
||||
100;
|
||||
const totalNeededPercentage = versionAndCounts.reduce(
|
||||
(acc, item) => acc + item.targetPercentage,
|
||||
0
|
||||
);
|
||||
|
||||
// Now we have the total percentage after we add everything up, vs the total target percentage we have. Get the difference, then multiply that by the total active agents to get the delta we need to add or remove from the total count.
|
||||
const totalDeltaPercentage = totalActualPercentage - totalNeededPercentage;
|
||||
|
||||
// If we are over, we need to remove some from the count, and if we are under, we need to add some to the count. If we're spot on, all good.
|
||||
if (totalDeltaPercentage !== 0) {
|
||||
// get the actual count of agents we are off by using the percentage * the total active agents
|
||||
let deltaCount = Math.round((totalDeltaPercentage / 100) * totalActiveAgents);
|
||||
|
||||
// Now we need to add or remove from the versionAndCounts array
|
||||
let index = 0;
|
||||
// So long as we have more to add or remove, do so
|
||||
while (deltaCount !== 0 && index < versionAndCounts.length) {
|
||||
const item = versionAndCounts[index];
|
||||
if (deltaCount > 0) {
|
||||
// Still have too many, removing one
|
||||
item.count -= 1;
|
||||
deltaCount -= 1;
|
||||
} else if (deltaCount < 0) {
|
||||
// Still have too few, adding one
|
||||
if (item.count > 0) {
|
||||
item.count += 1;
|
||||
deltaCount += 1;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return versionAndCounts;
|
||||
}
|
||||
|
||||
private async getAgentCount(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
kuery: string
|
||||
) {
|
||||
const res = await getAgentsByKuery(esClient, soClient, {
|
||||
showInactive: false,
|
||||
perPage: 0,
|
||||
kuery,
|
||||
});
|
||||
return res.total;
|
||||
}
|
||||
|
||||
private async processRequiredVersion(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentPolicy: AgentPolicy,
|
||||
requiredVersion: AgentTargetVersion,
|
||||
versionAndCounts: UpgradeTargetForVersion[]
|
||||
) {
|
||||
this.logger.debug(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: checking candidate agents for upgrade (target version: ${requiredVersion.version}, percentage: ${requiredVersion.percentage})`
|
||||
);
|
||||
|
||||
let numberOfAgentsForUpgrade =
|
||||
versionAndCounts.find((item) => item.version === requiredVersion.version)?.count ?? 0;
|
||||
// Return if target is already met.
|
||||
if (numberOfAgentsForUpgrade <= 0) {
|
||||
this.logger.info(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: target percentage ${requiredVersion.percentage} already reached for version: ${requiredVersion.version})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle retries.
|
||||
const numberOfRetriedAgents = await this.processRetries(
|
||||
esClient,
|
||||
soClient,
|
||||
agentPolicy,
|
||||
requiredVersion.version
|
||||
);
|
||||
|
||||
numberOfAgentsForUpgrade -= numberOfRetriedAgents;
|
||||
if (numberOfAgentsForUpgrade <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch candidate agents assigned to the policy in batches.
|
||||
// NB: ideally, we would query active agents on or below the target version. Unfortunately, this is not possible because agent.version
|
||||
// is stored as text, so semver comparison cannot be done in the ES query (cf. https://github.com/elastic/kibana/issues/168604).
|
||||
// As an imperfect alternative, sort agents by version. Since versions sort alphabetically, this will not always result in ascending semver sorting.
|
||||
const statusKuery =
|
||||
'(status:online OR status:offline OR status:enrolling OR status:degraded OR status:error OR status:orphaned)'; // active status except updating
|
||||
const oldStuckInUpdatingKuery = `(NOT upgrade_details:* AND status:updating AND NOT upgraded_at:* AND upgrade_started_at < now-2h)`; // agents pre 8.12.0 (without upgrade_details)
|
||||
const newStuckInUpdatingKuery = `(upgrade_details.target_version:${requiredVersion.version} AND upgrade_details.state:UPG_FAILED)`;
|
||||
const agentsFetcher = await fetchAllAgentsByKuery(esClient, soClient, {
|
||||
kuery: `policy_id:${agentPolicy.id} AND (NOT upgrade_attempts:*) AND (${statusKuery} OR ${oldStuckInUpdatingKuery} OR ${newStuckInUpdatingKuery})`,
|
||||
perPage: AGENTS_BATCHSIZE,
|
||||
sortField: 'agent.version',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
let { done, agents } = await this.getNextAgentsBatch(agentsFetcher);
|
||||
if (agents.length === 0) {
|
||||
this.logger.debug(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: no candidate agents found for upgrade (target version: ${requiredVersion.version}, percentage: ${requiredVersion.percentage})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
let shouldProcessAgents = true;
|
||||
|
||||
while (shouldProcessAgents) {
|
||||
this.throwIfAborted();
|
||||
numberOfAgentsForUpgrade = await this.findAndUpgradeCandidateAgents(
|
||||
esClient,
|
||||
soClient,
|
||||
agentPolicy,
|
||||
numberOfAgentsForUpgrade,
|
||||
requiredVersion.version,
|
||||
agents
|
||||
);
|
||||
if (!done && numberOfAgentsForUpgrade > 0) {
|
||||
({ done, agents } = await this.getNextAgentsBatch(agentsFetcher));
|
||||
} else {
|
||||
shouldProcessAgents = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (numberOfAgentsForUpgrade > 0) {
|
||||
this.logger.info(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: not enough agents eligible for upgrade (target version: ${requiredVersion.version}, percentage: ${requiredVersion.percentage})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async processRetries(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentPolicy: AgentPolicy,
|
||||
version: string
|
||||
) {
|
||||
let retriedAgentsCounter = 0;
|
||||
|
||||
const retryingAgentsFetcher = await fetchAllAgentsByKuery(esClient, soClient, {
|
||||
kuery: `policy_id:${agentPolicy.id} AND upgrade_details.target_version:${version} AND upgrade_details.state:UPG_FAILED AND upgrade_attempts:*`,
|
||||
perPage: AGENTS_BATCHSIZE,
|
||||
sortField: 'agent.version',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
for await (const retryingAgentsPageResults of retryingAgentsFetcher) {
|
||||
this.throwIfAborted();
|
||||
// This function will return the total number of agents marked for retry so they're included in the count of agents for upgrade.
|
||||
retriedAgentsCounter += retryingAgentsPageResults.length;
|
||||
|
||||
const agentsReadyForRetry = retryingAgentsPageResults.filter((agent) =>
|
||||
this.isAgentReadyForRetry(agent, agentPolicy)
|
||||
);
|
||||
if (agentsReadyForRetry.length > 0) {
|
||||
this.logger.info(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: retrying upgrade to ${version} for ${agentsReadyForRetry.length} agents`
|
||||
);
|
||||
|
||||
await sendAutomaticUpgradeAgentsActions(soClient, esClient, {
|
||||
agents: agentsReadyForRetry,
|
||||
version,
|
||||
...this.getUpgradeDurationSeconds(agentsReadyForRetry.length),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return retriedAgentsCounter;
|
||||
}
|
||||
|
||||
private isAgentReadyForRetry(agent: Agent, agentPolicy: AgentPolicy) {
|
||||
if (!agent.upgrade_attempts) {
|
||||
return false;
|
||||
}
|
||||
if (agent.upgrade_attempts.length > this.retryDelays.length) {
|
||||
this.logger.debug(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: max retry attempts exceeded for agent ${agent.id}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const currentRetryDelay = moment
|
||||
.duration('PT' + this.retryDelays[agent.upgrade_attempts.length - 1].toUpperCase()) // https://momentjs.com/docs/#/durations/
|
||||
.asMilliseconds();
|
||||
const lastUpgradeAttempt = Date.parse(agent.upgrade_attempts[0]);
|
||||
return Date.now() - lastUpgradeAttempt >= currentRetryDelay;
|
||||
}
|
||||
|
||||
private async getNextAgentsBatch(agentsFetcher: AsyncIterable<Agent[]>) {
|
||||
const agentsFetcherIter = agentsFetcher[Symbol.asyncIterator]();
|
||||
const agentsBatch = await agentsFetcherIter.next();
|
||||
const agents: Agent[] = agentsBatch.value ?? [];
|
||||
return {
|
||||
done: agentsBatch.done,
|
||||
agents: agents.filter((agent): agent is AgentWithDefinedVersion => agent.agent !== undefined),
|
||||
};
|
||||
}
|
||||
|
||||
private async findAndUpgradeCandidateAgents(
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentPolicy: AgentPolicy,
|
||||
numberOfAgentsForUpgrade: number,
|
||||
version: string,
|
||||
agents: AgentWithDefinedVersion[]
|
||||
) {
|
||||
const agentsForUpgrade: AgentWithDefinedVersion[] = [];
|
||||
|
||||
for (const agent of agents) {
|
||||
if (agentsForUpgrade.length >= numberOfAgentsForUpgrade) {
|
||||
break;
|
||||
}
|
||||
if (this.isAgentEligibleForUpgrade(agent, version)) {
|
||||
agentsForUpgrade.push(agent);
|
||||
}
|
||||
}
|
||||
|
||||
// Send bulk upgrade action for selected agents.
|
||||
if (agentsForUpgrade.length > 0) {
|
||||
this.logger.info(
|
||||
`[AutomaticAgentUpgradeTask] Agent policy ${agentPolicy.id}: sending bulk upgrade to ${version} for ${agentsForUpgrade.length} agents`
|
||||
);
|
||||
await sendAutomaticUpgradeAgentsActions(soClient, esClient, {
|
||||
agents: agentsForUpgrade,
|
||||
version,
|
||||
...this.getUpgradeDurationSeconds(agentsForUpgrade.length),
|
||||
});
|
||||
}
|
||||
|
||||
return numberOfAgentsForUpgrade - agentsForUpgrade.length;
|
||||
}
|
||||
|
||||
private isAgentEligibleForUpgrade(agent: AgentWithDefinedVersion, version: string) {
|
||||
return isAgentUpgradeable(agent) && semverGt(version, agent.agent.version);
|
||||
}
|
||||
|
||||
private getUpgradeDurationSeconds(nAgents: number) {
|
||||
if (nAgents < MIN_AGENTS_FOR_ROLLOUT) {
|
||||
return {};
|
||||
}
|
||||
const upgradeDurationSeconds = Math.max(
|
||||
MIN_UPGRADE_DURATION_SECONDS,
|
||||
Math.round(nAgents * 0.03)
|
||||
);
|
||||
return { upgradeDurationSeconds };
|
||||
}
|
||||
}
|
|
@ -156,6 +156,27 @@ export const AgentPolicyBaseSchema = {
|
|||
),
|
||||
})
|
||||
),
|
||||
required_versions: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal(null),
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
version: schema.string({
|
||||
meta: {
|
||||
description: 'Target version for automatic agent upgrade',
|
||||
},
|
||||
}),
|
||||
percentage: schema.number({
|
||||
min: 0,
|
||||
max: 100,
|
||||
meta: {
|
||||
description: 'Target percentage of agents to auto upgrade',
|
||||
},
|
||||
}),
|
||||
})
|
||||
),
|
||||
])
|
||||
),
|
||||
};
|
||||
|
||||
function validateGlobalDataTagInput(tags: GlobalDataTag[]): string | undefined {
|
||||
|
@ -214,3 +235,14 @@ export const AgentPolicySchema = schema.object({
|
|||
updated_at: schema.string(),
|
||||
updated_by: schema.string(),
|
||||
});
|
||||
|
||||
export const GetAutoUpgradeAgentsStatusResponseSchema = schema.object({
|
||||
currentVersions: schema.arrayOf(
|
||||
schema.object({
|
||||
version: schema.string(),
|
||||
agents: schema.number(),
|
||||
failedUpgradeAgents: schema.number(),
|
||||
})
|
||||
),
|
||||
totalAgents: schema.number(),
|
||||
});
|
||||
|
|
|
@ -67,6 +67,12 @@ export const GetOneAgentPolicyRequestSchema = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const GetAutoUpgradeAgentsStatusRequestSchema = {
|
||||
params: schema.object({
|
||||
agentPolicyId: schema.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export const CreateAgentPolicyRequestSchema = {
|
||||
body: NewAgentPolicySchema,
|
||||
query: schema.object({
|
||||
|
@ -78,6 +84,7 @@ export const UpdateAgentPolicyRequestSchema = {
|
|||
...GetOneAgentPolicyRequestSchema,
|
||||
body: NewAgentPolicySchema.extends({
|
||||
force: schema.maybe(schema.boolean()),
|
||||
bumpRevision: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -1568,47 +1568,96 @@ export default function (providerContext: FtrProviderContext) {
|
|||
expect(updatedPolicy.global_data_tags).to.eql([{ name: 'newTag', value: 'newValue' }]);
|
||||
});
|
||||
|
||||
it('should return 400 if updating data output to non-local ES for agentless policy', async () => {
|
||||
const { body: outputResponse } = await supertest
|
||||
.post(`/api/fleet/outputs`)
|
||||
it('should allow to set required_versions', async () => {
|
||||
const {
|
||||
body: { item: originalPolicy },
|
||||
} = await supertest
|
||||
.post(`/api/fleet/agent_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'logstash-output',
|
||||
type: 'logstash',
|
||||
hosts: ['test.fr:443'],
|
||||
ssl: {
|
||||
certificate: 'CERTIFICATE',
|
||||
key: 'KEY',
|
||||
certificate_authorities: ['CA1', 'CA2'],
|
||||
name: `Override Test ${Date.now()}`,
|
||||
description: 'Initial description',
|
||||
namespace: 'default',
|
||||
})
|
||||
.expect(200);
|
||||
agentPolicyId = originalPolicy.id;
|
||||
createdPolicyIds.push(agentPolicyId as string);
|
||||
const {
|
||||
body: { item: updatedPolicy },
|
||||
} = await supertest
|
||||
.put(`/api/fleet/agent_policies/${agentPolicyId}`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: originalPolicy.name,
|
||||
description: originalPolicy.description,
|
||||
namespace: 'default',
|
||||
required_versions: [
|
||||
{
|
||||
version: '9.0.0',
|
||||
percentage: 10,
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { id, updated_at, version, ...newPolicy } = updatedPolicy;
|
||||
|
||||
expect(newPolicy).to.eql({
|
||||
status: 'active',
|
||||
name: originalPolicy.name,
|
||||
description: originalPolicy.description,
|
||||
namespace: 'default',
|
||||
is_managed: false,
|
||||
revision: 2,
|
||||
schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION,
|
||||
updated_by: 'elastic',
|
||||
inactivity_timeout: 1209600,
|
||||
package_policies: [],
|
||||
is_protected: false,
|
||||
space_ids: [],
|
||||
required_versions: [
|
||||
{
|
||||
version: '9.0.0',
|
||||
percentage: 10,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const agentPolicyResponse = await supertest
|
||||
.post(`/api/fleet/agent_policies?sys_monitoring=false`)
|
||||
it('should not allow to set invalid required_versions', async () => {
|
||||
const {
|
||||
body: { item: originalPolicy },
|
||||
} = await supertest
|
||||
.post(`/api/fleet/agent_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'test-agentless-policy',
|
||||
name: `Override Test ${Date.now()}`,
|
||||
description: 'Initial description',
|
||||
namespace: 'default',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const agentPolicy = agentPolicyResponse.body.item;
|
||||
|
||||
const response = await supertest
|
||||
.put(`/api/fleet/agent_policies/${agentPolicy.id}`)
|
||||
agentPolicyId = originalPolicy.id;
|
||||
createdPolicyIds.push(agentPolicyId as string);
|
||||
await supertest
|
||||
.put(`/api/fleet/agent_policies/${agentPolicyId}`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'test-agentless-policy',
|
||||
name: `Override Test ${Date.now()}`,
|
||||
description: 'Updated description',
|
||||
namespace: 'default',
|
||||
supports_agentless: true,
|
||||
data_output_id: outputResponse.item.id,
|
||||
required_versions: [
|
||||
{
|
||||
version: '9.0.0',
|
||||
percentage: 50,
|
||||
},
|
||||
{
|
||||
version: '9.1.0',
|
||||
percentage: 60,
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.message).to.eql(
|
||||
'Output of type "logstash" is not usable with policy "test-agentless-policy".'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1900,5 +1949,48 @@ export default function (providerContext: FtrProviderContext) {
|
|||
expect(items.length).equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/fleet/agent_policies/{id}/auto_upgrade_agents_status', () => {
|
||||
it('should get auto upgrade agents status', async () => {
|
||||
const {
|
||||
body: { item: policyWithAgents },
|
||||
} = await supertest
|
||||
.post(`/api/fleet/agent_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'Policy with agents 2',
|
||||
namespace: 'default',
|
||||
})
|
||||
.expect(200);
|
||||
await generateAgent(providerContext, 'healhty', 'agent-1', policyWithAgents.id, '8.16.1');
|
||||
await generateAgent(providerContext, 'healhty', 'agent-2', policyWithAgents.id, '8.16.1', {
|
||||
state: 'UPG_FAILED',
|
||||
target_version: '8.16.3',
|
||||
});
|
||||
const { body } = await supertest
|
||||
.get(`/api/fleet/agent_policies/${policyWithAgents.id}/auto_upgrade_agents_status`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
currentVersions: [
|
||||
{
|
||||
agents: 2,
|
||||
failedUpgradeAgents: 0,
|
||||
version: '8.16.1',
|
||||
},
|
||||
{
|
||||
agents: 0,
|
||||
failedUpgradeAgents: 1,
|
||||
version: '8.16.3',
|
||||
},
|
||||
],
|
||||
totalAgents: 2,
|
||||
});
|
||||
|
||||
await supertest.delete(`/api/fleet/agents/agent-1`).set('kbn-xsrf', 'xx').expect(200);
|
||||
await supertest.delete(`/api/fleet/agents/agent-2`).set('kbn-xsrf', 'xx').expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ export async function cleanFleetAgentPolicies(esClient: Client) {
|
|||
index: AGENT_POLICY_INDEX,
|
||||
q: '*',
|
||||
refresh: true,
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -85,12 +85,7 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext
|
|||
'./apis/fixtures/package_verification/signatures/fleet_test_key_public.asc'
|
||||
)}`,
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['endpointRbacEnabled'])}`,
|
||||
`--xpack.fleet.enableExperimental=${JSON.stringify([
|
||||
'outputSecretsStorage',
|
||||
'agentTamperProtectionEnabled',
|
||||
'enableStrictKQLValidation',
|
||||
'subfeaturePrivileges',
|
||||
])}`,
|
||||
`--xpack.fleet.enableExperimental=${JSON.stringify(['enableAutomaticAgentUpgrades'])}`,
|
||||
`--xpack.cloud.id='123456789'`,
|
||||
`--xpack.fleet.agentless.enabled=true`,
|
||||
`--xpack.fleet.agentless.api.url=https://api.agentless.url/api/v1/ess`,
|
||||
|
|
|
@ -65,7 +65,8 @@ export async function generateAgent(
|
|||
status: string,
|
||||
id: string,
|
||||
policyId: string,
|
||||
version?: string
|
||||
version?: string,
|
||||
upgradeDetails?: any
|
||||
) {
|
||||
let data: any = {};
|
||||
const { getService } = providerContext;
|
||||
|
@ -112,6 +113,9 @@ export async function generateAgent(
|
|||
last_checkin: new Date().toISOString(),
|
||||
policy_id: policyId,
|
||||
policy_revision: 1,
|
||||
agent: {
|
||||
version,
|
||||
},
|
||||
local_metadata: {
|
||||
elastic: {
|
||||
agent: {
|
||||
|
@ -121,6 +125,7 @@ export async function generateAgent(
|
|||
},
|
||||
},
|
||||
...data,
|
||||
...(upgradeDetails ? { upgrade_details: upgradeDetails } : {}),
|
||||
},
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
|
|
|
@ -145,6 +145,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'endpoint:user-artifact-packager',
|
||||
'entity_store:data_view:refresh',
|
||||
'entity_store:field_retention:enrichment',
|
||||
'fleet:automatic-agent-upgrade-task',
|
||||
'fleet:bump_agent_policies',
|
||||
'fleet:check-deleted-files-task',
|
||||
'fleet:delete-unenrolled-agents-task',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue