Skip to main content

security_client_rs/fleet/
actions.rs

1//! Progress of submitted agent actions and the files they upload.
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value, json};
4
5use super::{Fleet, Items};
6use crate::{
7    Scope,
8    http::{Method, Raw},
9    request::endpoint,
10};
11
12/// The state of an agent action. States added by newer Kibana versions are
13/// kept as [`Unknown`](Self::Unknown).
14#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
15#[serde(from = "String", into = "String")]
16#[non_exhaustive]
17pub enum ActionStatus {
18    InProgress,
19    Complete,
20    Failed,
21    Cancelled,
22    Expired,
23    RolloutPassed,
24    Unknown(String),
25}
26
27impl ActionStatus {
28    /// Kibana's name for the state, such as `IN_PROGRESS`.
29    pub fn as_str(&self) -> &str {
30        match self {
31            Self::InProgress => "IN_PROGRESS",
32            Self::Complete => "COMPLETE",
33            Self::Failed => "FAILED",
34            Self::Cancelled => "CANCELLED",
35            Self::Expired => "EXPIRED",
36            Self::RolloutPassed => "ROLLOUT_PASSED",
37            Self::Unknown(status) => status,
38        }
39    }
40
41    /// Whether the action can no longer progress. An unknown state counts as
42    /// finished, so waiting stops at a state this client cannot interpret
43    /// instead of running until the deadline.
44    /// This also returns true for known failures; inspect the status before
45    /// treating the action as successful.
46    ///
47    /// ```
48    /// use security_client_rs::fleet::ActionStatus;
49    ///
50    /// assert!(ActionStatus::Failed.is_finished());
51    /// assert!(!ActionStatus::InProgress.is_finished());
52    /// let added_by_kibana = ActionStatus::from("NEW_STATUS".to_owned());
53    /// assert!(added_by_kibana.is_finished());
54    /// assert!(matches!(added_by_kibana, ActionStatus::Unknown(_)));
55    /// ```
56    pub fn is_finished(&self) -> bool {
57        !matches!(self, Self::InProgress)
58    }
59}
60
61impl From<String> for ActionStatus {
62    fn from(status: String) -> Self {
63        match status.as_str() {
64            "IN_PROGRESS" => Self::InProgress,
65            "COMPLETE" => Self::Complete,
66            "FAILED" => Self::Failed,
67            "CANCELLED" => Self::Cancelled,
68            "EXPIRED" => Self::Expired,
69            "ROLLOUT_PASSED" => Self::RolloutPassed,
70            _ => Self::Unknown(status),
71        }
72    }
73}
74
75impl From<ActionStatus> for String {
76    fn from(status: ActionStatus) -> Self {
77        match status {
78            ActionStatus::Unknown(status) => status,
79            known => known.as_str().to_owned(),
80        }
81    }
82}
83
84/// The state of a file uploaded by an agent. States added by newer Kibana
85/// versions are kept as [`Unknown`](Self::Unknown).
86#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
87#[serde(from = "String", into = "String")]
88#[non_exhaustive]
89pub enum UploadStatus {
90    AwaitingUpload,
91    InProgress,
92    /// The file can be downloaded.
93    Ready,
94    Failed,
95    Expired,
96    Deleted,
97    Unknown(String),
98}
99
100impl UploadStatus {
101    /// Kibana's name for the state, such as `READY`.
102    pub fn as_str(&self) -> &str {
103        match self {
104            Self::AwaitingUpload => "AWAITING_UPLOAD",
105            Self::InProgress => "IN_PROGRESS",
106            Self::Ready => "READY",
107            Self::Failed => "FAILED",
108            Self::Expired => "EXPIRED",
109            Self::Deleted => "DELETED",
110            Self::Unknown(status) => status,
111        }
112    }
113
114    /// Whether the upload can no longer change. As with [`ActionStatus`], an
115    /// unknown state counts as finished.
116    pub fn is_finished(&self) -> bool {
117        !matches!(self, Self::AwaitingUpload | Self::InProgress)
118    }
119}
120
121impl From<String> for UploadStatus {
122    fn from(status: String) -> Self {
123        match status.as_str() {
124            "AWAITING_UPLOAD" => Self::AwaitingUpload,
125            "IN_PROGRESS" => Self::InProgress,
126            "READY" => Self::Ready,
127            "FAILED" => Self::Failed,
128            "EXPIRED" => Self::Expired,
129            "DELETED" => Self::Deleted,
130            _ => Self::Unknown(status),
131        }
132    }
133}
134
135impl From<UploadStatus> for String {
136    fn from(status: UploadStatus) -> Self {
137        match status {
138            UploadStatus::Unknown(status) => status,
139            known => known.as_str().to_owned(),
140        }
141    }
142}
143
144/// Inspect failure counts and errors, including while the action is in progress.
145#[derive(Clone, Debug, Deserialize, Serialize)]
146#[serde(rename_all = "camelCase")]
147#[non_exhaustive]
148pub struct AgentActionStatus {
149    pub action_id: String,
150    pub status: ActionStatus,
151    #[serde(rename = "type")]
152    pub action_type: String,
153    pub nb_agents_action_created: u64,
154    pub nb_agents_ack: u64,
155    pub nb_agents_failed: u64,
156    pub nb_agents_actioned: u64,
157    #[serde(default)]
158    pub latest_errors: Vec<Value>,
159    #[serde(flatten)]
160    pub extra: Map<String, Value>,
161}
162
163impl AgentActionStatus {
164    /// See [`ActionStatus::is_finished`]. Check the failure counts too.
165    pub fn is_finished(&self) -> bool {
166        self.status.is_finished()
167    }
168}
169
170#[derive(Clone, Debug, Deserialize, Serialize)]
171#[serde(rename_all = "camelCase")]
172#[non_exhaustive]
173pub struct AgentUpload {
174    pub id: String,
175    pub name: String,
176    pub action_id: String,
177    pub status: UploadStatus,
178    #[serde(default)]
179    pub error: Option<String>,
180    #[serde(flatten)]
181    pub extra: Map<String, Value>,
182}
183
184impl AgentUpload {
185    /// See [`UploadStatus::is_finished`].
186    pub fn is_finished(&self) -> bool {
187        self.status.is_finished()
188    }
189}
190
191impl<'a> Fleet<'a> {
192    /// Recent agent actions with completion and failure counts, newest first.
193    /// Pages start at zero. There is no filter by action ID, and a short or
194    /// empty page does not prove that older actions are absent.
195    pub fn agent_action_status(&self) -> AgentActionStatuses<'a> {
196        AgentActionStatuses(self.0.request(
197            Method::GET,
198            Scope::Space,
199            &["api", "fleet", "agents", "action_status"],
200        ))
201    }
202
203    /// Kibana supports cancellation of upgrade and unenrollment actions only.
204    pub fn cancel_agent_action(&self, action_id: &str) -> CancelAgentAction<'a> {
205        CancelAgentAction(
206            self.0
207                .request(
208                    Method::POST,
209                    Scope::Space,
210                    &["api", "fleet", "agents", "actions", action_id, "cancel"],
211                )
212                .json(&json!({})),
213        )
214    }
215
216    pub fn list_agent_uploads(&self, agent_id: &str) -> ListAgentUploads<'a> {
217        ListAgentUploads(self.0.request(
218            Method::GET,
219            Scope::Space,
220            &["api", "fleet", "agents", agent_id, "uploads"],
221        ))
222    }
223
224    /// Downloads an upload after it reports `READY`. Diagnostic archives can
225    /// contain sensitive configuration.
226    pub fn download_agent_file(&self, file_id: &str, file_name: &str) -> DownloadAgentFile<'a> {
227        DownloadAgentFile(self.0.request(
228            Method::GET,
229            Scope::Space,
230            &["api", "fleet", "agents", "files", file_id, file_name],
231        ))
232    }
233}
234
235endpoint! {
236    /// `GET /api/fleet/agents/action_status`
237    AgentActionStatuses => Items<AgentActionStatus>
238}
239
240impl AgentActionStatuses<'_> {
241    /// Zero-based page number, unlike Fleet resource collections.
242    pub fn page(self, page: u32) -> Self {
243        Self(self.0.param("page", page))
244    }
245
246    pub fn per_page(self, per_page: u32) -> Self {
247        Self(self.0.positive_param("perPage", per_page))
248    }
249
250    /// Only actions created after this ISO 8601 time.
251    pub fn date(self, date: &str) -> Self {
252        Self(self.0.param("date", date))
253    }
254
255    /// Only actions from the last `seconds`.
256    pub fn latest(self, seconds: u32) -> Self {
257        Self(self.0.param("latest", seconds))
258    }
259
260    /// How many recent errors to include per action.
261    pub fn error_size(self, size: u32) -> Self {
262        Self(self.0.param("errorSize", size))
263    }
264}
265
266endpoint! {
267    /// `POST /api/fleet/agents/actions/{actionId}/cancel`
268    CancelAgentAction => Value
269}
270
271endpoint! {
272    /// `GET /api/fleet/agents/{agentId}/uploads`
273    ListAgentUploads => Items<AgentUpload>
274}
275
276endpoint! {
277    /// `GET /api/fleet/agents/files/{fileId}/{fileName}`
278    DownloadAgentFile => Raw
279}