Skip to content

Commit

Permalink
Add cli to generate and install plugin bundle
Browse files Browse the repository at this point in the history
  • Loading branch information
andreievg committed Feb 3, 2025
1 parent ac974dd commit e6ceb35
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 3 deletions.
10 changes: 10 additions & 0 deletions server/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ enum Action {
},
/// Will generate a plugin bundle
GeneratePluginBundle(GeneratePluginBundle),
/// Will insert generated plugin bundle
InstallPluginBundle(InstallPluginBundle),
/// Will generate and then install plugin bundle
GenerateAndInstallPluginBundle(GenerateAndInstallPluginBundle),
UpsertReports {
/// Optional reports json path. This needs to be of type ReportsData. If none supplied, will upload the standard generated reports
#[clap(short, long)]
Expand Down Expand Up @@ -536,6 +540,12 @@ async fn main() -> anyhow::Result<()> {
Action::GeneratePluginBundle(arguments) => {
generate_plugin_bundle(arguments)?;
}
Action::InstallPluginBundle(arguments) => {
install_plugin_bundle(arguments).await?;
}
Action::GenerateAndInstallPluginBundle(arguments) => {
generate_and_install_plugin_bundle(arguments).await?;
}
}

Ok(())
Expand Down
16 changes: 16 additions & 0 deletions server/cli/src/graphql/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pub(super) const AUTH_QUERY: &str = r#"
query AuthToken($username: String!, $password: String) {
root: authToken(password: $password, username: $username) {
... on AuthToken {
__typename
token
}
... on AuthTokenError {
__typename
error {
description
}
}
}
}
"#;
172 changes: 172 additions & 0 deletions server/cli/src/graphql/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use std::{
io::{self, Read},
path::PathBuf,
};

use auth::AUTH_QUERY;
use reqwest::{multipart, RequestBuilder, Url};
use serde::{de::DeserializeOwned, Deserialize, Serialize};

#[derive(Debug)]
pub struct Api {
url: Url,
token: String,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize, Serialize)]
struct GraphQlResponse {
data: Root,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize, Serialize)]
struct Root {
root: serde_json::Value,
}

mod auth;
pub mod queries_mutations;

#[derive(thiserror::Error, Debug)]
pub enum ApiError {
#[error("Error while sending request to {1}")]
SendingRequest(#[source] reqwest::Error, Url),
#[error("Error while getting text, status {1:?}")]
GettingText(#[source] reqwest::Error, reqwest::StatusCode),
#[error("Error parsing gql response: {1}")]
ParsingJson(#[source] serde_json::Error, String),
#[error("Error validating typename, expected typename {expected_typename}, result: {json}")]
ValidatingTypename {
expected_typename: String,
json: String,
},
#[error("Error opening file, {1}")]
OpeningFile(#[source] io::Error, PathBuf),
#[error("Error reading file, {1}")]
ReadingFile(#[source] io::Error, PathBuf),
}
use service::UploadedFile;
use ApiError as Error;

impl Api {
pub async fn new_with_token(
url: Url,
username: String,
password: String,
) -> Result<Self, Error> {
let result = _gql(
&url.join("graphql").unwrap(),
AUTH_QUERY,
serde_json::json! ({
"username": username,
"password": password,
}),
None,
Some("AuthToken"),
)
.await?;

let token = result["token"].as_str().unwrap().to_string();

Ok(Api { url, token })
}

pub async fn gql(
&self,
query: &str,
variables: serde_json::Value,
expected_typename: Option<&str>,
) -> Result<serde_json::Value, Error> {
_gql(
&self.url.join("graphql").unwrap(),
query,
variables,
Some(&self.token),
expected_typename,
)
.await
}

pub async fn upload_file(&self, path: PathBuf) -> Result<UploadedFile, Error> {
let url = self.url.join("upload").unwrap();

println!("{}", url);
let auth_cooke_value = format!(r#"auth={{"token": "{}"}}"#, self.token);
let built_request = reqwest::Client::new()
.post(url.clone())
.header("Cookie", auth_cooke_value);

// Add file to request
let mut file_handle =
std::fs::File::open(path.clone()).map_err(|e| Error::OpeningFile(e, path.clone()))?;
let mut file_bytes = Vec::new();
file_handle
.read_to_end(&mut file_bytes)
.map_err(|e| Error::ReadingFile(e, path))?;
let file_part = multipart::Part::bytes(file_bytes).file_name("upload".to_string());
let multipart_form = multipart::Form::new().part("files", file_part);
let built_request = built_request.multipart(multipart_form);

// Send and return file_id

send_and_parse(built_request, url).await
}
}

async fn _gql(
url: &Url,
query: &str,
variables: serde_json::Value,
token: Option<&str>,
expected_typename: Option<&str>,
) -> Result<serde_json::Value, Error> {
let body = serde_json::json!({
"query": query,
"variables": variables
});

println!("{}", url);
let mut client = reqwest::Client::new().post(url.clone());

if let Some(token) = token {
client = client.bearer_auth(token)
};

let built_request = client.json(&body);

let json_result: GraphQlResponse = send_and_parse(built_request, url.clone()).await?;

let result = json_result.data.root;

let Some(expected_typename) = expected_typename else {
return Ok(result);
};

if result["__typename"] != expected_typename {
return Err(Error::ValidatingTypename {
expected_typename: expected_typename.to_string(),
json: serde_json::to_string(&result).unwrap(),
});
}

Ok(result)
}

async fn send_and_parse<T: DeserializeOwned>(
built_request: RequestBuilder,
url: Url,
) -> Result<T, Error> {
let response = built_request
.send()
.await
.map_err(|e| Error::SendingRequest(e, url))?;

let status = response.status();
let text_result = response
.text()
.await
.map_err(|e| Error::GettingText(e, status))?;

Ok(serde_json::from_str(&text_result).map_err(|e| Error::ParsingJson(e, text_result))?)
}
11 changes: 11 additions & 0 deletions server/cli/src/graphql/queries_mutations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub const INSTALL_PLUGINS: &'static str = r#"
mutation Query($fileId: String!) {
root: centralServer {
__typename
plugins {
installUploadedPlugin(fileId: $fileId) {
backendPluginCodes
}
}
}
}"#;
2 changes: 2 additions & 0 deletions server/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ mod refresh_dates;
pub use refresh_dates::*;
mod report_utils;
pub use report_utils::*;
mod graphql;
pub use graphql::*;
107 changes: 106 additions & 1 deletion server/cli/src/plugins.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::{ffi::OsStr, fs, path::PathBuf};

use base64::{prelude::BASE64_STANDARD, Engine};
use cli::{queries_mutations::INSTALL_PLUGINS, Api, ApiError};
use repository::{BackendPluginRow, PluginTypes, PluginVariantType};
use reqwest::Url;
use serde::Deserialize;
use serde_json::json;
use service::backend_plugin::plugin_provider::PluginBundle;
use thiserror::Error as ThisError;

use thiserror::Error as ThisError;
#[derive(ThisError, Debug)]
pub(super) enum Error {
#[error("Failed to read dir {0}")]
Expand All @@ -24,6 +27,10 @@ pub(super) enum Error {
FailedToSerializeBundle(#[source] serde_json::Error),
#[error("Failed to write bundle file {0}")]
FailedToWriteBundleFile(PathBuf, #[source] std::io::Error),
#[error(transparent)]
GqlError(#[from] ApiError),
#[error("Failed to remove temp file {1}")]
FiledToRemoveTempFile(std::io::Error, PathBuf),
}

#[derive(clap::Parser, Debug)]
Expand All @@ -36,6 +43,37 @@ pub(super) struct GeneratePluginBundle {
out_file: PathBuf,
}

#[derive(clap::Parser, Debug)]
pub(super) struct InstallPluginBundle {
/// Path to bundle
#[clap(short, long)]
path: PathBuf,
/// Server url
#[clap(short, long)]
url: Url,
/// Username
#[clap(long)]
username: String,
/// Password
#[clap(long)]
password: String,
}

#[derive(clap::Parser, Debug)]
pub(super) struct GenerateAndInstallPluginBundle {
/// Directory in which to search for plugins
#[clap(short, long)]
in_dir: PathBuf,
/// Server url
#[clap(short, long)]
url: Url,
/// Username
#[clap(long)]
username: String,
/// Password
#[clap(long)]
password: String,
}
#[derive(Deserialize)]
struct ManifestJson {
code: String,
Expand Down Expand Up @@ -125,3 +163,70 @@ fn process_manifest(bundle: &mut PluginBundle, path: &PathBuf) -> Result<(), Err

Ok(())
}

/// username, password, url should come from config, like in reports (in the new show command)
pub(super) async fn install_plugin_bundle(
InstallPluginBundle {
path,
url,
username,
password,
}: InstallPluginBundle,
) -> Result<(), Error> {
let api = Api::new_with_token(url.clone(), username, password).await?;

let uploaded_file = api.upload_file(path).await?;

let upload_result = api
.gql(
INSTALL_PLUGINS,
json!( { "fileId": uploaded_file.file_id} ),
Some("CentralServerMutationNode"),
)
.await?;

println!(
"Result:{}",
serde_json::to_string_pretty(&upload_result).unwrap()
);

Ok(())
}

/// username and password should come from config, like in reports (and url too)
pub(super) async fn generate_and_install_plugin_bundle(
GenerateAndInstallPluginBundle {
in_dir,
url,
username,
password,
}: GenerateAndInstallPluginBundle,
) -> Result<(), Error> {
let out_file = PathBuf::from("report_temp.json");

generate_plugin_bundle(GeneratePluginBundle {
in_dir,
out_file: out_file.clone(),
})?;

let api = Api::new_with_token(url.clone(), username, password).await?;

let uploaded_file = api.upload_file(out_file.clone()).await?;

fs::remove_file(out_file.clone()).map_err(|e| Error::FiledToRemoveTempFile(e, out_file))?;

let upload_result = api
.gql(
INSTALL_PLUGINS,
json!( { "fileId": uploaded_file.file_id} ),
Some("CentralServerMutationNode"),
)
.await?;

println!(
"Result:{}",
serde_json::to_string_pretty(&upload_result).unwrap()
);

Ok(())
}
9 changes: 9 additions & 0 deletions server/service/src/backend_plugin/examples/amc/amc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
// To upload to server (from this dir):
// cargo run --bin remote_server_cli -- generate-and-install-plugin-bundle -i './service/src/backend_plugin/examples/amc' -u 'http://localhost:8000' --username 'test' --password 'pass'
// OR
// cargo run --bin remote_server_cli -- generate-plugin-bundle -i './service/src/backend_plugin/examples/amc' -o 'check.json'
// Can install via CLI
// cargo run --bin remote_server_cli -- install-plugin-bundle --path 'check.json' -u 'http://localhost:8000' --username 'test' --password 'pass'
// Or can install via curl
// cargo run --bin remote_server_cli -- generate-plugin-bundle -i './service/src/backend_plugin/examples/amc' -o 'check.json'
// curl -H 'Content-Type: application/json' --data '{"query":"query MyQuery {authToken(password: \"pass\", username: \"Admin\") {... on AuthToken {token}... on AuthTokenError {error {description}}}}","variables":{}}' 'http://localhost:8000/graphql'
// TOKEN=token from above
Expand Down Expand Up @@ -36,6 +42,9 @@ let plugins = {
const sql_result = sql(sql_statement);
const response = {};

// Fill all item_ids with default
item_ids.forEach((itemId) => (response[itemId] = { average_monthly_consumption: 1 }));

sql_result.forEach(({ item_id, consumption }) => {
response[item_id] = {
average_monthly_consumption: consumption / (DAY_LOOKBACK / DAYS_IN_MONTH),
Expand Down
Loading

0 comments on commit e6ceb35

Please sign in to comment.