Skip to main content

regent_sdk/
task.rs

1//! Task distribution module
2//!
3//! This module provides the [`RegentTask`] type for distributing configuration management
4//! workloads across multiple workers. A `RegentTask` is a self-contained unit of work
5//! that can be serialized and sent across a network (via gRPC, AMQP, REST, etc.) to be
6//! processed by a worker node.
7//!
8//! ## Idempotency: Attribute-level vs Task-level
9//!
10//! Regent SDK implements idempotency at two distinct levels:
11//!
12//! - **Attribute-level idempotency**: Each `Attribute` is designed to be
13//!   idempotent when applied to a host. For example, a service attribute that ensures nginx is
14//!   running will only start the service if it's not already running, and will not cause errors
15//!   if applied multiple times. This is the core idempotency of the configuration management system.
16//!
17//! - **Task-level idempotency**: The idempotency key in [`RegentTask`] is a helper that allows
18//!   external middleware to implement task-level idempotency. If the same task is delivered
19//!   multiple times (due to network retries, message queue redelivery, etc.), external systems
20//!   can use this key to deduplicate the task execution. Note: **The regent-sdk crate itself does
21//!   not handle task idempotency** — it only provides the key. You must implement deduplication
22//!   logic in your message queue, API gateway, or other middleware.
23//!
24//! ## Features
25//!
26//! - **Serializable**: Tasks can be serialized as JSON or YAML for network transport
27//! - **Self-contained**: Each task includes all information needed for execution
28//! - **Task-level idempotency keys**: Unique identifiers that allow external systems to deduplicate task execution
29//! - **Result reporting**: Structured results with compliance status and actions taken
30//!
31//! ## Quick Start
32//!
33//! ```no_run
34//! use regent_sdk::task::{RegentTask, Job};
35//! use regent_sdk::hosts::managed_host::ManagedHostBuilder;
36//! use regent_sdk::state::ExpectedState;
37//! use regent_sdk::hosts::handlers::ConnectionMethod;
38//!
39//! // Create a task
40//! let managed_host_builder = ManagedHostBuilder::new(
41//!     "web-server-01",
42//!     "192.168.1.100:22",
43//!     Some(ConnectionMethod::Localhost(TargetUser::CurrentUser)),
44//! );
45//!
46//! let expected_state = ExpectedState::new();
47//!
48//! let task = RegentTask::from(
49//!     managed_host_builder,
50//!     expected_state,
51//!     Job::Assess, // or Job::Reach for remediation
52//! );
53//!
54//! // Serialize and send across network
55//! let json = serde_json::to_string(&task).unwrap();
56//!
57//! // On worker: deserialize and execute
58//! let mut task: RegentTask = serde_json::from_str(&json).unwrap();
59//! let result = task.run(Some(secrets_pool)).await.unwrap();
60//! ```
61
62use crate::secrets::SecretProvidersPool;
63use crate::state::ExpectedState;
64use crate::state::compliance::ManagedHostStatus;
65use crate::{error::RegentError, hosts::managed_host::ManagedHostBuilder};
66
67use nanoid::nanoid;
68use serde::{Deserialize, Serialize};
69
70/// A unit of work for distributed configuration management.
71///
72/// A `RegentTask` is a self-contained task that can be serialized and sent across
73/// a network to be processed by a worker node. It contains all the information needed
74/// to connect to a host, assess or remediate its compliance with an expected state,
75/// and return the results.
76///
77/// Each task has a unique idempotency key that enables task-level idempotency.
78/// If the same task is delivered multiple times (e.g., due to message queue redelivery),
79/// external systems can use this key to deduplicate the task execution. This is separate from
80/// attribute-level idempotency, which is inherent to each attribute's design.
81///
82/// # Serialization
83///
84/// Tasks implement `Serialize` and `Deserialize`, allowing them to be transmitted
85/// as JSON or YAML:
86///
87/// ```no_run
88/// use regent_sdk::task::RegentTask;
89///
90/// let task = /* create task */;
91/// let json = serde_json::to_string(&task).unwrap();
92/// let yaml = serde_yaml::to_string(&task).unwrap();
93/// ```
94///
95/// # Example
96///
97/// ```no_run
98/// use regent_sdk::task::{RegentTask, Job};
99/// use regent_sdk::hosts::managed_host::ManagedHostBuilder;
100/// use regent_sdk::state::ExpectedState;
101/// use regent_sdk::hosts::handlers::{ConnectionMethod, TargetUser};
102///
103/// let host_builder = ManagedHostBuilder::new(
104///     "server-01",
105///     "192.168.1.100:22",
106///     Some(ConnectionMethod::Localhost(TargetUser::current_user())),
107/// );
108///
109/// let expected_state = ExpectedState::new();
110/// let task = RegentTask::from(host_builder, expected_state, Job::Assess);
111///
112/// println!("Task idempotency key: {}", task.idempotency_key());
113/// ```
114#[derive(Serialize, Deserialize)]
115pub struct RegentTask {
116    managed_host_builder: ManagedHostBuilder,
117    expected_state: ExpectedState,
118    job: Job,
119    idempotency_key: String,
120}
121
122impl RegentTask {
123    /// Create a new `RegentTask` from a host builder, expected state, and job type.
124    ///
125    /// # Arguments
126    ///
127    /// * `managed_host_builder` - Builder for the target host
128    /// * `expected_state` - The expected state to assess/remedy
129    /// * `job` - The type of job to perform (`Assess` or `Reach`)
130    ///
131    /// # Returns
132    ///
133    /// A new `RegentTask` with a randomly generated idempotency key for task-level idempotency.
134    /// This allows external systems to detect and skip duplicate task deliveries.
135    ///
136    /// # Example
137    ///
138    /// ```no_run
139    /// use regent_sdk::task::{RegentTask, Job};
140    /// use regent_sdk::hosts::managed_host::ManagedHostBuilder;
141    /// use regent_sdk::state::ExpectedState;
142    /// use regent_sdk::hosts::handlers::{ConnectionMethod, TargetUser};
143    ///
144    /// let host_builder = ManagedHostBuilder::new(
145    ///     "my-host",
146    ///     "localhost",
147    ///     Some(ConnectionMethod::Localhost(TargetUser::current_user())),
148    /// );
149    ///
150    /// let expected_state = ExpectedState::new();
151    /// let task = RegentTask::from(host_builder, expected_state, Job::Assess);
152    /// ```
153    pub fn from(
154        managed_host_builder: ManagedHostBuilder,
155        expected_state: ExpectedState,
156        job: Job,
157    ) -> Self {
158        Self {
159            managed_host_builder,
160            expected_state,
161            job,
162            idempotency_key: nanoid!(
163                16,
164                &[
165                    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
166                ]
167            ),
168        }
169    }
170
171    /// Get the task-level idempotency key for this task.
172    ///
173    /// This key is a unique identifier that enables task-level idempotency. When the same
174    /// task is delivered multiple times to a worker, external systems can use this key to
175    /// deduplicate the execution. Note: this is separate from attribute-level idempotency,
176    /// which ensures each configuration change is safely repeatable.
177    ///
178    /// # Returns
179    ///
180    /// A reference to the idempotency key string.
181    ///
182    /// # Example
183    ///
184    /// ```no_run
185    /// use regent_sdk::task::RegentTask;
186    ///
187    /// let task = /* create task */;
188    /// println!("Idempotency key: {}", task.idempotency_key());
189    /// ```
190    pub fn idempotency_key(&self) -> &str {
191        &self.idempotency_key
192    }
193
194    /// Execute the task.
195    ///
196    /// This method builds the managed host, connects to it, and performs the
197    /// specified job (assess or reach compliance).
198    ///
199    /// # Arguments
200    ///
201    /// * `optional_secret_provider` - Optional secret providers pool for retrieving secrets
202    ///
203    /// # Returns
204    ///
205    /// A [`RegentTaskResult`] containing the idempotency key and host status,
206    /// or a [`RegentError`] if execution failed.
207    ///
208    /// # Example
209    ///
210    /// ```no_run
211    /// use regent_sdk::task::RegentTask;
212    /// use regent_sdk::secrets::{SecretProvider, SecretProvidersPoolBuilder};
213    ///
214    /// let mut task = /* create task */;
215    /// let secrets_pool = SecretProvidersPoolBuilder::new()
216    ///     .add_default_provider("files", SecretProvider::files())
217    ///     .build()
218    ///     .unwrap();
219    ///
220    /// let result = task.run(Some(secrets_pool)).await.unwrap();
221    /// ```
222    pub async fn run(
223        &mut self,
224        optional_secret_provider: Option<SecretProvidersPool>,
225    ) -> Result<RegentTaskResult, RegentError> {
226        // Build a ManagedHost
227        let mut managed_host = self
228            .managed_host_builder
229            .clone()
230            .build(optional_secret_provider)
231            .await?;
232
233        managed_host.connect().await?;
234
235        let host_status = match self.job {
236            Job::Assess => managed_host.assess_compliance(&self.expected_state).await?,
237            Job::Reach => managed_host.reach_compliance(&self.expected_state).await?,
238        };
239
240        Ok(RegentTaskResult::from(
241            self.idempotency_key.clone(),
242            host_status,
243        ))
244    }
245}
246
247/// The type of job for a [`RegentTask`] to perform.
248///
249/// # Variants
250///
251/// - `Assess`: Only assess compliance and return the current state (read-only)
252/// - `Reach`: Assess compliance and automatically perform remediation to reach the expected state
253///
254/// # Example
255///
256/// ```no_run
257/// use regent_sdk::task::Job;
258///
259/// // For read-only compliance checking
260/// let job = Job::Assess;
261///
262/// // For automatic remediation
263/// let job = Job::Reach;
264/// ```
265#[derive(Serialize, Deserialize)]
266pub enum Job {
267    /// Assess compliance only (read-only operation).
268    ///
269    /// This will check if the host is compliant with the expected state
270    /// and return the compliance status without making any changes.
271    Assess,
272    /// Assess and remediate compliance (read-write operation).
273    ///
274    /// This will check compliance and automatically perform the necessary
275    /// remediations to bring the host into the expected state.
276    Reach,
277}
278
279/// Result of executing a [`RegentTask`].
280///
281/// Contains the task-level idempotency key for deduplication purposes, along with the
282/// host's compliance status. This key allows external systems to identify duplicate
283/// task deliveries, which is separate from attribute-level idempotency.
284///
285/// # Example
286///
287/// ```no_run
288/// use regent_sdk::task::RegentTaskResult;
289/// use regent_sdk::state::compliance::ManagedHostStatus;
290///
291/// let result = RegentTaskResult::from(
292///     "abc123".to_string(),
293///     ManagedHostStatus::already_compliant(),
294/// );
295///
296/// assert_eq!(result.idempotency_key(), "abc123");
297/// assert!(result.host_status().is_already_compliant());
298/// ```
299#[derive(Serialize, Deserialize, Debug)]
300pub struct RegentTaskResult {
301    /// The idempotency key of the task that produced this result.
302    idempotency_key: String,
303    /// The compliance status of the host after task execution.
304    host_status: ManagedHostStatus,
305}
306
307impl RegentTaskResult {
308    /// Create a new task result.
309    ///
310    /// # Arguments
311    ///
312    /// * `idempotency_key` - The task-level idempotency key for deduplication of task deliveries
313    /// * `host_status` - The host's compliance status
314    ///
315    /// # Returns
316    ///
317    /// A new [`RegentTaskResult`] instance.
318    ///
319    /// # Example
320    ///
321    /// ```no_run
322    /// use regent_sdk::task::RegentTaskResult;
323    /// use regent_sdk::state::compliance::ManagedHostStatus;
324    ///
325    /// let result = RegentTaskResult::from(
326    ///     "task-123".to_string(),
327    ///     ManagedHostStatus::already_compliant(),
328    /// );
329    /// ```
330    pub fn from(idempotency_key: String, host_status: ManagedHostStatus) -> Self {
331        Self {
332            idempotency_key,
333            host_status,
334        }
335    }
336}