Compare commits
4 Commits
f988cf0f26
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba1cf0182b
|
|||
|
5cccec88c9
|
|||
|
bbcd3f00a9
|
|||
| 2e339dd73b |
@@ -0,0 +1,11 @@
|
||||
### AI assistance (if any):
|
||||
- List tools here and files touched by them
|
||||
|
||||
### Authorship & Understanding
|
||||
|
||||
- [ ] I wrote or heavily modified this code myself
|
||||
- [ ] I understand how it works end-to-end
|
||||
- [ ] I can maintain this code in the future
|
||||
- [ ] No undisclosed AI-generated code was used
|
||||
- [ ] If AI assistance was used, it is documented below
|
||||
|
||||
@@ -91,5 +91,12 @@ Then, you can run workflows locally without having to commit and see if the GitH
|
||||
act -W .github/workflows/release.yml --input_type bump=minor
|
||||
```
|
||||
|
||||
## Authorship Policy
|
||||
|
||||
All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
|
||||
Claude, etc.) is not permitted unless explicitly disclosed and approved.
|
||||
|
||||
Submissions must certify that the contributor understands and can maintain the code they submit.
|
||||
|
||||
## Questions? Reach out to me!
|
||||
If you encounter any questions while developing Managarr, please don't hesitate to reach out to me at alex.j.tusa@gmail.com. I'm happy to help contributors, new and experienced in any way I can!
|
||||
|
||||
@@ -21,6 +21,8 @@ RUN mv target/release/managarr .
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the compiled binary from the builder container
|
||||
COPY --from=builder --chown=nonroot:nonroot /usr/src/managarr-temp/managarr /usr/local/bin
|
||||
|
||||
|
||||
@@ -364,7 +364,16 @@ radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: someApiToken1234567890
|
||||
ssl_cert_path: /path/to/radarr.crt # Required to enable SSL
|
||||
ssl_cert_path: /path/to/radarr.crt # Use the specified SSL certificate to connect to this Servarr
|
||||
# Enables SSL regardless of the value of the 'ssl'
|
||||
# See the SSL Configuration section below for more information
|
||||
|
||||
- host: 192.168.0.79
|
||||
port: 7878
|
||||
api_token: someApiToken1234567890
|
||||
ssl: true # Use SSL to connect to this Servarr
|
||||
# This will assume that you have the SSL certificate installed to your system trust store
|
||||
# See the SSL Configuration section below for more information
|
||||
|
||||
- uri: http://htpc.local/radarr # Example of using the 'uri' key instead of 'host' and 'port'
|
||||
api_token: someApiToken1234567890
|
||||
@@ -400,6 +409,59 @@ lidarr:
|
||||
SOME-OTHER-CUSTOM-HEADER: ${MY_CUSTOM_HEADER_VALUE}
|
||||
```
|
||||
|
||||
### SSL Configuration
|
||||
If your Servarr is using SSL or self-signed certificates, you may need to specify additional configuration options to connect without issues.
|
||||
|
||||
|
||||
**If your Servarr's domain CA is installed in the system's trust store:**
|
||||
Then you can simply specify `ssl: true` and Managarr will be able to connect to your Servarr:
|
||||
|
||||
```yaml
|
||||
radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: yourApiTokenHere
|
||||
ssl: true
|
||||
```
|
||||
|
||||
|
||||
**If your Servarr's domain CA is not installed:**
|
||||
You'll either need to specify the path to the certificate via the `ssl_cert_path` property, or you'll need to install the certificate into your system store.
|
||||
|
||||
To acquire the cert for your Servarr's domain, you can use the following command:
|
||||
```shell
|
||||
openssl s_client -show-certs -connect <your-servarr-domain.com>:<port> </dev/null |\
|
||||
sed -n -e '/-.BEGIN/,/-.END/ p' > /path/to/your/servarr.pem
|
||||
```
|
||||
|
||||
Now, you can either specify `ssl_cert_path: /path/to/your/servarr.pem`:
|
||||
|
||||
Example configuration with a certificate that's not installed to the system trust store:
|
||||
```yaml
|
||||
radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: yourApiTokenHere
|
||||
ssl_cert_path: /path/to/your/certificate.crt
|
||||
```
|
||||
|
||||
Or install the certificate into your system's trust store.
|
||||
|
||||
For example, if you're on a Debian-based system and have `ca-certificates` installed (`sudo apt install ca-certificates`):
|
||||
```shell
|
||||
sudo mv /path/to/your/servarr.pem /usr/local/share/ca-certificates/servarr.pem
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
Example configuration with a certificate that is installed to the system trust store:
|
||||
```yaml
|
||||
radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
api_token: yourApiTokenHere
|
||||
ssl: true
|
||||
```
|
||||
|
||||
### Example Multi-Instance Configuration:
|
||||
```yaml
|
||||
theme: default
|
||||
|
||||
@@ -85,5 +85,5 @@ build build_type='debug':
|
||||
|
||||
# Build the docker image
|
||||
[group: 'build']
|
||||
build-docker:
|
||||
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMG_NAME}}:{{VERSION}} .
|
||||
build-docker version=VERSION:
|
||||
@DOCKER_BUILDKIT=1 docker build --rm -t {{IMG_NAME}}:{{version}} .
|
||||
|
||||
+74
-1
@@ -447,6 +447,78 @@ mod tests {
|
||||
assert_none!(config.port);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_bool() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: true
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_string() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: "true"
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_is_present() {
|
||||
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL", "true") };
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL}
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &true);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_bool_defaults_to_false() {
|
||||
unsafe { std::env::set_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY", "test") };
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
ssl: ${TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY}
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_some_eq_x!(&config.ssl, &false);
|
||||
unsafe { std::env::remove_var("TEST_VAR_DESERIALIZE_OPTION_BOOL_FALSEY") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_optional_env_var_bool_empty() {
|
||||
let yaml_data = r#"
|
||||
host: localhost
|
||||
api_token: "test123"
|
||||
"#;
|
||||
|
||||
let config: ServarrConfig = serde_yaml::from_str(yaml_data).unwrap();
|
||||
|
||||
assert_none!(config.ssl);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_deserialize_optional_env_var_header_map_is_present() {
|
||||
@@ -674,7 +746,7 @@ mod tests {
|
||||
let mut custom_headers = HeaderMap::new();
|
||||
custom_headers.insert("X-Custom-Header", "value".parse().unwrap());
|
||||
let expected_str = format!(
|
||||
"ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}"
|
||||
"ServarrConfig {{ name: Some(\"{name}\"), host: Some(\"{host}\"), port: Some({port}), uri: Some(\"{uri}\"), weight: Some({weight}), api_token: Some(\"***********\"), api_token_file: Some(\"{api_token_file}\"), ssl: Some(true), ssl_cert_path: Some(\"{ssl_cert_path}\"), custom_headers: Some({{\"x-custom-header\": \"value\"}}), monitored_storage_paths: Some([\"/path1\", \"/path2\"]) }}"
|
||||
);
|
||||
let servarr_config = ServarrConfig {
|
||||
name: Some(name),
|
||||
@@ -685,6 +757,7 @@ mod tests {
|
||||
api_token: Some(api_token),
|
||||
api_token_file: Some(api_token_file),
|
||||
ssl_cert_path: Some(ssl_cert_path),
|
||||
ssl: Some(true),
|
||||
custom_headers: Some(custom_headers),
|
||||
monitored_storage_paths: Some(monitored_storage),
|
||||
};
|
||||
|
||||
@@ -431,6 +431,8 @@ pub struct ServarrConfig {
|
||||
pub api_token: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
|
||||
pub api_token_file: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var_bool")]
|
||||
pub ssl: Option<bool>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_env_var")]
|
||||
pub ssl_cert_path: Option<String>,
|
||||
#[serde(
|
||||
@@ -486,6 +488,7 @@ impl Default for ServarrConfig {
|
||||
api_token: Some(String::new()),
|
||||
api_token_file: None,
|
||||
ssl_cert_path: None,
|
||||
ssl: None,
|
||||
custom_headers: None,
|
||||
monitored_storage_paths: None,
|
||||
}
|
||||
@@ -532,6 +535,29 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_env_var_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum StringOrBool {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
}
|
||||
|
||||
match StringOrBool::deserialize(deserializer)? {
|
||||
StringOrBool::Bool(b) => Ok(Some(b)),
|
||||
StringOrBool::String(s) => {
|
||||
let val = interpolate_env_vars(&s)
|
||||
.to_lowercase()
|
||||
.parse()
|
||||
.unwrap_or(false);
|
||||
Ok(Some(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_env_var_header_map<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<HeaderMap>, D::Error>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand, arg};
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::LidarrCommand;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, arg};
|
||||
use clap::Subcommand;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use add_command_handler::{LidarrAddCommand, LidarrAddCommandHandler};
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, arg};
|
||||
use clap::Subcommand;
|
||||
use delete_command_handler::{LidarrDeleteCommand, LidarrDeleteCommandHandler};
|
||||
use edit_command_handler::{LidarrEditCommand, LidarrEditCommandHandler};
|
||||
use get_command_handler::{LidarrGetCommand, LidarrGetCommandHandler};
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use clap_complete::Shell;
|
||||
use indoc::indoc;
|
||||
use lidarr::{LidarrCliHandler, LidarrCommand};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{ArgAction, Subcommand, arg, command};
|
||||
use clap::{ArgAction, Subcommand};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::RadarrCommand;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Subcommand, command};
|
||||
use clap::Subcommand;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
|
||||
+2
-1
@@ -229,6 +229,7 @@ impl<'a, 'b> Network<'a, 'b> {
|
||||
uri,
|
||||
api_token,
|
||||
ssl_cert_path,
|
||||
ssl,
|
||||
custom_headers: custom_headers_option,
|
||||
..
|
||||
} = app
|
||||
@@ -245,7 +246,7 @@ impl<'a, 'b> Network<'a, 'b> {
|
||||
let mut uri = if let Some(servarr_uri) = uri {
|
||||
format!("{servarr_uri}/api/{api_version}{resource}")
|
||||
} else {
|
||||
let protocol = if ssl_cert_path.is_some() {
|
||||
let protocol = if ssl_cert_path.is_some() || ssl.unwrap_or(false) {
|
||||
"https"
|
||||
} else {
|
||||
"http"
|
||||
|
||||
@@ -409,7 +409,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[should_panic(expected = "Servarr config is undefined")]
|
||||
#[rstest]
|
||||
async fn test_request_props_from_requires_radarr_config_to_be_present_for_all_network_events(
|
||||
async fn test_request_props_from_requires_config_to_be_present_for_all_network_events(
|
||||
#[values(RadarrEvent::HealthCheck, SonarrEvent::HealthCheck)] network_event: impl Into<NetworkEvent>
|
||||
+ NetworkResource,
|
||||
) {
|
||||
@@ -492,6 +492,82 @@ mod tests {
|
||||
assert!(request_props.custom_headers.is_empty());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_request_props_from_custom_config_ssl_doesnt_affect_ssl_cert_path(
|
||||
#[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into<NetworkEvent>
|
||||
+ NetworkResource,
|
||||
#[values(Some(true), Some(false), None)] ssl_option: Option<bool>,
|
||||
) {
|
||||
let api_token = "testToken1234".to_owned();
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let resource = network_event.resource();
|
||||
let servarr_config = ServarrConfig {
|
||||
host: Some("192.168.0.123".to_owned()),
|
||||
port: Some(8080),
|
||||
api_token: Some(api_token.clone()),
|
||||
ssl_cert_path: Some("/test/cert.crt".to_owned()),
|
||||
ssl: ssl_option,
|
||||
..ServarrConfig::default()
|
||||
};
|
||||
{
|
||||
let mut app = app_arc.lock().await;
|
||||
app.server_tabs.tabs[0].config = Some(servarr_config.clone());
|
||||
app.server_tabs.tabs[1].config = Some(servarr_config);
|
||||
}
|
||||
let network = test_network(&app_arc);
|
||||
|
||||
let request_props = network
|
||||
.request_props_from(network_event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
assert_str_eq!(
|
||||
request_props.uri,
|
||||
format!("https://192.168.0.123:8080/api/v3{resource}")
|
||||
);
|
||||
assert_eq!(request_props.method, RequestMethod::Get);
|
||||
assert_eq!(request_props.body, None);
|
||||
assert_str_eq!(request_props.api_token, api_token);
|
||||
assert!(request_props.custom_headers.is_empty());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_request_props_uses_ssl_property(
|
||||
#[values(RadarrEvent::GetMovies, SonarrEvent::ListSeries)] network_event: impl Into<NetworkEvent>
|
||||
+ NetworkResource,
|
||||
) {
|
||||
let api_token = "testToken1234".to_owned();
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let resource = network_event.resource();
|
||||
let servarr_config = ServarrConfig {
|
||||
host: Some("192.168.0.123".to_owned()),
|
||||
port: Some(8080),
|
||||
api_token: Some(api_token.clone()),
|
||||
ssl: Some(true),
|
||||
..ServarrConfig::default()
|
||||
};
|
||||
{
|
||||
let mut app = app_arc.lock().await;
|
||||
app.server_tabs.tabs[0].config = Some(servarr_config.clone());
|
||||
app.server_tabs.tabs[1].config = Some(servarr_config);
|
||||
}
|
||||
let network = test_network(&app_arc);
|
||||
|
||||
let request_props = network
|
||||
.request_props_from(network_event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
assert_str_eq!(
|
||||
request_props.uri,
|
||||
format!("https://192.168.0.123:8080/api/v3{resource}")
|
||||
);
|
||||
assert_eq!(request_props.method, RequestMethod::Get);
|
||||
assert_eq!(request_props.body, None);
|
||||
assert_str_eq!(request_props.api_token, api_token);
|
||||
assert!(request_props.custom_headers.is_empty());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_request_props_from_custom_config_custom_headers(
|
||||
|
||||
Reference in New Issue
Block a user