Skip to main content

hipcheck_sdk/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use hipcheck_common::proto::{
4	ConfigurationStatus, InitiateQueryProtocolResponse, SetConfigurationResponse,
5};
6use std::{convert::Infallible, error::Error as StdError, ops::Not, result::Result as StdResult};
7use tokio::sync::mpsc::error::SendError as TokioMpscSendError;
8use tonic::Status as TonicStatus;
9
10/// An enumeration of errors that can occur in a Hipcheck plugin
11#[derive(Debug, thiserror::Error)]
12pub enum Error {
13	/// An unknown error occurred, the query is in an unspecified state
14	#[error("unknown error")]
15	UnspecifiedQueryState,
16
17	/// The `PluginEngine` received a message with the unexpected status `ReplyInProgress`
18	#[error("unexpected ReplyInProgress state for query")]
19	UnexpectedReplyInProgress,
20
21	#[error("invalid JSON in query key")]
22	InvalidJsonInQueryKey(#[source] Box<serde_json::Error>),
23
24	#[error("invalid JSON in query output")]
25	InvalidJsonInQueryOutput(#[source] Box<serde_json::Error>),
26
27	#[error("session channel closed unexpectedly")]
28	SessionChannelClosed,
29
30	#[error("failed to send query from session to server")]
31	FailedToSendQueryFromSessionToServer(
32		#[source] Box<TokioMpscSendError<StdResult<InitiateQueryProtocolResponse, TonicStatus>>>,
33	),
34
35	/// The `PluginEngine` received a message with a reply-type status when it expected a request
36	#[error("plugin sent QueryReply when server was expecting a request")]
37	ReceivedReplyWhenExpectingRequest,
38
39	/// The `PluginEngine` received a message with a request-type status when it expected a reply
40	#[error("plugin sent QuerySubmit when server was expecting a reply chunk")]
41	ReceivedSubmitWhenExpectingReplyChunk,
42
43	/// The `PluginEngine` received additional messages when it did not expect any
44	#[error("received additional message for ID '{id}' after query completion")]
45	MoreAfterQueryComplete { id: usize },
46
47	#[error("failed to start server")]
48	FailedToStartServer(#[source] Box<tonic::transport::Error>),
49
50	/// The `Query::run` function implementation received an incorrectly-typed JSON Value key
51	#[error("unexpected JSON value from plugin")]
52	UnexpectedPluginQueryInputFormat,
53
54	/// The `Query::run` function implementation produced an output that cannot be serialized to JSON
55	#[error("plugin output could not be serialized to JSON")]
56	UnexpectedPluginQueryOutputFormat,
57
58	/// The `PluginEngine` received a request for an unknown query endpoint
59	#[error("could not determine which plugin query to run for '{0}'")]
60	UnknownPluginQuery(Box<str>),
61
62	#[error("invalid format for QueryTarget")]
63	InvalidQueryTargetFormat,
64
65	#[error("the plugin was called with a default query, but none is defined")]
66	NoDefaultQuery,
67
68	#[error("the query is unsupported for the target")]
69	QueryUnsupportedForTarget,
70
71	#[error(transparent)]
72	Unspecified { source: DynError },
73}
74
75impl From<hipcheck_common::error::Error> for Error {
76	fn from(value: hipcheck_common::error::Error) -> Self {
77		use hipcheck_common::error::Error::*;
78		match value {
79			UnspecifiedQueryState => Error::UnspecifiedQueryState,
80			UnexpectedRequestInProgress => Error::UnexpectedReplyInProgress,
81			UnexpectedReplyInProgress => Error::UnexpectedReplyInProgress,
82			ReceivedSubmitWhenExpectingReplyChunk => Error::ReceivedSubmitWhenExpectingReplyChunk,
83			ReceivedReplyWhenExpectingSubmitChunk => Error::ReceivedReplyWhenExpectingRequest,
84			MoreAfterQueryComplete { id } => Error::MoreAfterQueryComplete { id },
85			InvalidJsonInQueryKey(s) => Error::InvalidJsonInQueryKey(Box::new(s)),
86			InvalidJsonInQueryOutput(s) => Error::InvalidJsonInQueryOutput(Box::new(s)),
87			QueryUnsupportedForTarget => Error::QueryUnsupportedForTarget,
88		}
89	}
90}
91
92impl From<anyhow::Error> for Error {
93	fn from(value: anyhow::Error) -> Self {
94		Error::Unspecified {
95			source: value.into(),
96		}
97	}
98}
99
100impl Error {
101	pub fn any<E: StdError + 'static + Send + Sync>(source: E) -> Self {
102		Error::Unspecified {
103			source: Box::new(source),
104		}
105	}
106}
107
108/// A thread-safe error trait object.
109pub type DynError = Box<dyn StdError + 'static + Send + Sync>;
110
111// this will never happen, but is needed to enable passing QueryTarget to PluginEngine::query
112impl From<Infallible> for Error {
113	fn from(_value: Infallible) -> Self {
114		Error::UnspecifiedQueryState
115	}
116}
117
118/// A Result type using `hipcheck_sdk::Error`
119pub type Result<T> = StdResult<T, Error>;
120
121/// A Result type using `hipcheck_sdk::ConfigError`
122pub type ConfigResult<T> = StdResult<T, ConfigError>;
123
124/// Errors specific to the execution of `Plugin::set_configuration()` to configure a Hipcheck
125/// plugin.
126#[derive(Debug)]
127pub enum ConfigError {
128	/// The config key was valid, but the associated value was invalid
129	InvalidConfigValue {
130		field_name: Box<str>,
131		value: Box<str>,
132		reason: Box<str>,
133	},
134
135	/// The config was missing an expected field
136	MissingRequiredConfig {
137		field_name: Box<str>,
138		field_type: Box<str>,
139		possible_values: Vec<Box<str>>,
140	},
141
142	/// The config included an unrecognized field
143	UnrecognizedConfig {
144		field_name: Box<str>,
145		field_value: Box<str>,
146		possible_confusables: Vec<Box<str>>,
147	},
148
149	/// An unspecified error
150	Unspecified { message: Box<str> },
151
152	/// The plugin encountered an error, probably due to incorrect assumptions.
153	InternalError { message: Box<str> },
154
155	/// A necessary plugin input file was not found.
156	FileNotFound { file_path: Box<str> },
157
158	/// The plugin's input data could not be parsed correctly.
159	ParseError {
160		// A short name or description of the data source.
161		source: Box<str>,
162		message: Box<str>,
163	},
164
165	/// An environment variable needed by the plugin was not set.
166	EnvVarNotSet {
167		/// Name of the environment variable
168		env_var_name: Box<str>,
169		/// Message describing what the environment variable should contain
170		purpose: Box<str>,
171	},
172
173	/// The plugin could not run a needed program.
174	MissingProgram { program_name: Box<str> },
175}
176
177impl From<ConfigError> for SetConfigurationResponse {
178	fn from(value: ConfigError) -> Self {
179		match value {
180			ConfigError::InvalidConfigValue {
181				field_name,
182				value,
183				reason,
184			} => SetConfigurationResponse {
185				status: ConfigurationStatus::InvalidConfigurationValue as i32,
186				message: format!("'{value}' for '{field_name}', reason: '{reason}'"),
187			},
188			ConfigError::MissingRequiredConfig {
189				field_name,
190				field_type,
191				possible_values,
192			} => SetConfigurationResponse {
193				status: ConfigurationStatus::MissingRequiredConfiguration as i32,
194				message: {
195					let mut message = format!("'{field_name}' of type '{field_type}'");
196
197					if possible_values.is_empty().not() {
198						message.push_str("; possible values: ");
199						message.push_str(&possible_values.join(", "));
200					}
201
202					message
203				},
204			},
205			ConfigError::UnrecognizedConfig {
206				field_name,
207				field_value,
208				possible_confusables,
209			} => SetConfigurationResponse {
210				status: ConfigurationStatus::UnrecognizedConfiguration as i32,
211				message: {
212					let mut message = format!("'{field_name}' with value '{field_value}'");
213
214					if possible_confusables.is_empty().not() {
215						message.push_str("; possible field names: ");
216						message.push_str(&possible_confusables.join(", "));
217					}
218
219					message
220				},
221			},
222			ConfigError::Unspecified { message } => SetConfigurationResponse {
223				status: ConfigurationStatus::Unspecified as i32,
224				message: message.to_string(),
225			},
226			ConfigError::InternalError { message } => SetConfigurationResponse {
227				status: ConfigurationStatus::InternalError as i32,
228				message: format!(
229					"the plugin encountered an error, probably due to incorrect assumptions: {message}"
230				),
231			},
232			ConfigError::FileNotFound { file_path } => SetConfigurationResponse {
233				status: ConfigurationStatus::FileNotFound as i32,
234				message: file_path.to_string(),
235			},
236			ConfigError::ParseError { source, message } => SetConfigurationResponse {
237				status: ConfigurationStatus::ParseError as i32,
238				message: format!("{source} could not be parsed correctly: {message}"),
239			},
240			ConfigError::EnvVarNotSet {
241				env_var_name,
242				purpose,
243			} => SetConfigurationResponse {
244				status: ConfigurationStatus::EnvVarNotSet as i32,
245				message: format!("\"{env_var_name}\". Purpose: {purpose}"),
246			},
247			ConfigError::MissingProgram { program_name } => SetConfigurationResponse {
248				status: ConfigurationStatus::MissingProgram as i32,
249				message: program_name.to_string(),
250			},
251		}
252	}
253}