diff --git a/Cargo.lock b/Cargo.lock index f11686b7..a815d304 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "activity-container" +version = "0.4.0" +dependencies = [ + "clap", + "hipcheck-sdk", + "jiff", + "log", + "schemars", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "addr2line" version = "0.22.0" diff --git a/Cargo.toml b/Cargo.toml index 44d743e2..c0df8b80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "plugins/typo", "test-plugins/dummy_rand_data", "test-plugins/dummy_sha256", + "test-plugins/activity-container", "xtask", ] diff --git a/sdk/rust/src/server.rs b/sdk/rust/src/server.rs index f4ebef01..56d4743a 100644 --- a/sdk/rust/src/server.rs +++ b/sdk/rust/src/server.rs @@ -40,7 +40,7 @@ impl PluginServer

{ /// Run the plugin server on the provided port. pub async fn listen(self, port: u16) -> Result<()> { let service = PluginServiceServer::new(self); - let host = format!("127.0.0.1:{}", port).parse().unwrap(); + let host = format!("0.0.0.0:{}", port).parse().unwrap(); Server::builder() .add_service(service) @@ -168,16 +168,25 @@ impl PluginService for PluginServer

{ ) -> QueryResult> { let rx = req.into_inner(); // TODO: - make channel size configurable - let (tx, out_rx) = mpsc::channel::>(10); + let (tx, out_rx) = mpsc::channel::>(100); let cloned_plugin = self.plugin.clone(); - + let tx_clone = tx.clone(); tokio::spawn(async move { let mut channel = HcSessionSocket::new(tx, rx); if let Err(e) = channel.run(cloned_plugin).await { - panic!("Error: {e}"); + eprintln!("Channel error: {e}"); + if !tx_clone.is_closed() { + if let Err(send_err) = tx_clone + .send(Err(tonic::Status::internal(format!("Session error: {e}")))) + .await + { + eprintln!("Failed to send error through channel: {send_err}"); + } + } } }); + Ok(Resp::new(RecvStream::new(out_rx))) } } diff --git a/site/content/docs/guide/making-plugins/release.md b/site/content/docs/guide/making-plugins/release.md index 2e832e00..b5d41d88 100644 --- a/site/content/docs/guide/making-plugins/release.md +++ b/site/content/docs/guide/making-plugins/release.md @@ -84,11 +84,13 @@ system path, followed by some number of arguments. For example, if your plugin is an executable file, the entrypoint string may be as simple as `" [ARGS}"`, as above. If your plugin were a Python script, the entrypoint string may be `"python3 " [ARGS]`. If your plugin -code is represented by a container image, you may use `"podman -[ARGS]"` or `"docker [ARGS]"`. Whatever it is, at runtime Hipcheck -will append ` --port ` to this string to tell the plugin which port to -listen on, so you will need to ensure that the behavior of your entrypoint -string can handle this addition. +code is represented by a container image, your entrypoint can be a shell script +that will load your image file and format your run command to include port +mapping as `docker run -p "$PORT":50051 [ARGS]` or +`podman run -p "$PORT":50051 [ARGS]`. Whatever it is, at runtime +Hipcheck will append ` --port ` to the entrypoint to tell the plugin +which port to listen on, so you will need to ensure that the behavior of your +entrypoint string can handle this addition. You may have as many or as few entries in the `entrypoint` section of the plugin manifest. If you are doing a [local deployment](#local-deployment), you may diff --git a/test-plugins/activity-container/Cargo.toml b/test-plugins/activity-container/Cargo.toml new file mode 100644 index 00000000..8b6bcb90 --- /dev/null +++ b/test-plugins/activity-container/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "activity-container" +version = "0.4.0" +license = "Apache-2.0" +edition = "2021" +publish = false + +[dependencies] +clap = { version = "4.5.27", features = ["derive"] } +hipcheck-sdk = { path = "../../sdk/rust", features = [ + "macros", +] } +jiff = { version = "0.1.16", features = ["serde"] } +log = "0.4.22" +schemars = { version = "0.8.21", features = ["url"] } +serde = { version = "1.0.215", features = ["derive", "rc"] } +serde_json = "1.0.134" +tokio = { version = "1.43.0", features = ["rt"] } + +[dev-dependencies] +hipcheck-sdk = { path = "../../sdk/rust", features = [ + "mock_engine", +] } diff --git a/test-plugins/activity-container/Containerfile b/test-plugins/activity-container/Containerfile new file mode 100644 index 00000000..39fe653d --- /dev/null +++ b/test-plugins/activity-container/Containerfile @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 + +FROM debian:bookworm-slim + +WORKDIR /app + +COPY ../../target/debug/activity /app/activity + +RUN chmod +x /app/activity + +EXPOSE 50051 + +ENTRYPOINT ["/app/activity", "--port", "50051"] diff --git a/test-plugins/activity-container/activity-container-deploy.sh b/test-plugins/activity-container/activity-container-deploy.sh new file mode 100755 index 00000000..a9ac9529 --- /dev/null +++ b/test-plugins/activity-container/activity-container-deploy.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Default values +IMAGE_TAR="./test-plugins/activity-container/activity-image.tar" +IMAGE_NAME="activity-image" +PORT=8888 + +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--port" && -n "$2" && "$2" =~ ^[0-9]+$ ]]; then + PORT="$2" + shift 2 + else + echo "Unknown or invalid argument: $1" + exit 1 + fi +done + +if [[ ! -f "$IMAGE_TAR" ]]; then + echo "Error: Image tar file '$IMAGE_TAR' not found!" + exit 1 +fi + + +# Check if the image is already loaded +if ! docker images | grep -q "$IMAGE_NAME"; then + echo "Image '$IMAGE_NAME' not found. Loading the image..." + docker load -i "$IMAGE_TAR" > /dev/null 2>&1 +fi +# Otherwise, the image is already loaded + +# Format the run statement for container port mapping +docker run --init -p "$PORT":50051 activity-image diff --git a/test-plugins/activity-container/local-plugin.kdl b/test-plugins/activity-container/local-plugin.kdl new file mode 100644 index 00000000..4be689ce --- /dev/null +++ b/test-plugins/activity-container/local-plugin.kdl @@ -0,0 +1,15 @@ +publisher "mitre" +name "activity-container" +version "0.0.0" +license "Apache-2.0" + +entrypoint { + on arch="aarch64-apple-darwin" "activity-container-deploy.sh" + on arch="x86_64-apple-darwin" "activity-container-deploy.sh" + on arch="x86_64-unknown-linux-gnu" "activity-container-deploy.sh" + on arch="x86_64-pc-windows-msvc" "activity-container-deploy.sh" +} + +dependencies { + plugin "mitre/git" version="0.0.0" manifest="./plugins/git/local-plugin.kdl" +} diff --git a/test-plugins/activity-container/src/main.rs b/test-plugins/activity-container/src/main.rs new file mode 100644 index 00000000..bfbee388 --- /dev/null +++ b/test-plugins/activity-container/src/main.rs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Plugin for querying how long it has been since a commit was last made to a repo + +use clap::Parser; +use hipcheck_sdk::{prelude::*, types::Target}; +use jiff::Timestamp; +use serde::Deserialize; +use std::{result::Result as StdResult, sync::OnceLock}; + +#[derive(Deserialize)] +struct Config { + weeks: Option, +} + +static CONFIG: OnceLock = OnceLock::new(); + +/// Returns the span of time since the most recent commit to a Git repo as `jiff:Span` displayed as a String +/// (Which means that anything expecting a `Span` must parse the output of this query appropriately) +#[query(default)] +async fn activity(engine: &mut PluginEngine, target: Target) -> Result { + log::debug!("running activity query"); + + let repo = target.local; + + // Get today's date + let today = Timestamp::now(); + + // Get the date of the most recent commit. + let value = engine + .query("mitre/git/last_commit_date", repo) + .await + .map_err(|e| { + log::error!("failed to get last commit date for activity metric: {}", e); + Error::UnspecifiedQueryState + })?; + + let Value::String(date_string) = value else { + return Err(Error::UnexpectedPluginQueryInputFormat); + }; + let last_commit_date: Timestamp = date_string.parse().map_err(|e| { + log::error!("{}", e); + Error::UnspecifiedQueryState + })?; + + // Get the time between the most recent commit and today. + let time_since_last_commit = today.since(last_commit_date).map_err(|e| { + log::error!("{}", e); + Error::UnspecifiedQueryState + })?; + + Ok(time_since_last_commit.to_string()) +} + +#[derive(Clone, Debug)] +struct ActivityPlugin; + +impl Plugin for ActivityPlugin { + const PUBLISHER: &'static str = "mitre"; + + const NAME: &'static str = "activity"; + + fn set_config(&self, config: Value) -> StdResult<(), ConfigError> { + let conf = + serde_json::from_value::(config).map_err(|e| ConfigError::Unspecified { + message: e.to_string(), + })?; + CONFIG.set(conf).map_err(|_e| ConfigError::Unspecified { + message: "config was already set".to_owned(), + }) + } + + fn default_policy_expr(&self) -> Result { + let Some(conf) = CONFIG.get() else { + log::error!("tried to access config before set by Hipcheck core!"); + return Err(Error::UnspecifiedQueryState); + }; + + Ok(format!("(lte $ P{}w)", conf.weeks.unwrap_or(71))) + } + + fn explain_default_query(&self) -> Result> { + Ok(Some( + "span of time that has elapsed since last activity in repo".to_string(), + )) + } + + queries! {} +} + +#[derive(Parser, Debug)] +struct Args { + #[arg(long)] + p: u16, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let args = Args::try_parse().unwrap(); + log::info!("Activity container plugin is registering {:?}", args); + PluginServer::register(ActivityPlugin {}) + .listen(args.p) + .await +} + +#[cfg(test)] +mod test { + use super::*; + + use hipcheck_sdk::types::LocalGitRepo; + use jiff::{Span, SpanRound, Unit}; + use std::result::Result as StdResult; + + fn repo() -> LocalGitRepo { + LocalGitRepo { + path: "/home/users/me/.cache/hipcheck/clones/github/expressjs/express/".to_string(), + git_ref: "main".to_string(), + } + } + + fn mock_responses() -> StdResult { + let repo = repo(); + let output = "2024-06-19T19:22:45Z".to_string(); + + // when calling into query, the input repo gets passed to `last_commit_date`, lets assume it returns the datetime `output` + let mut mock_responses = MockResponses::new(); + mock_responses.insert("mitre/git/last_commit_date", repo, Ok(output))?; + Ok(mock_responses) + } + + #[tokio::test] + async fn test_activity() { + let repo = repo(); + let target = Target { + specifier: "express".to_string(), + local: repo, + remote: None, + package: None, + }; + + let mut engine = PluginEngine::mock(mock_responses().unwrap()); + let output = activity(&mut engine, target).await.unwrap(); + let span: Span = output.parse().unwrap(); + let result = span.round(SpanRound::new().smallest(Unit::Day)).unwrap(); + + let today = Timestamp::now(); + let last_commit: Timestamp = "2024-06-19T19:22:45Z".parse().unwrap(); + let expected = today + .since(last_commit) + .unwrap() + .round(SpanRound::new().smallest(Unit::Day)) + .unwrap(); + + assert_eq!(result, expected); + } +}