client/dragonfly-client-config/src/dfdaemon.rs

2265 lines
75 KiB
Rust

/*
* Copyright 2024 The Dragonfly Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use bytesize::ByteSize;
use dragonfly_client_core::{
error::{ErrorType, OrErr},
Result,
};
use dragonfly_client_util::{
http::basic_auth,
tls::{generate_ca_cert_from_pem, generate_cert_from_pem},
};
use local_ip_address::{local_ip, local_ipv6};
use rcgen::Certificate;
use regex::Regex;
use rustls_pki_types::CertificateDer;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::PathBuf;
use std::time::Duration;
use tokio::fs;
use tonic::transport::{
Certificate as TonicCertificate, ClientTlsConfig, Identity, ServerTlsConfig,
};
use tracing::{error, instrument};
use validator::Validate;
/// NAME is the name of dfdaemon.
pub const NAME: &str = "dfdaemon";
/// default_dfdaemon_config_path is the default config path for dfdaemon.
#[inline]
pub fn default_dfdaemon_config_path() -> PathBuf {
crate::default_config_dir().join("dfdaemon.yaml")
}
/// default_dfdaemon_log_dir is the default log directory for dfdaemon.
#[inline]
pub fn default_dfdaemon_log_dir() -> PathBuf {
crate::default_log_dir().join(NAME)
}
/// default_download_unix_socket_path is the default unix socket path for download gRPC service.
pub fn default_download_unix_socket_path() -> PathBuf {
crate::default_root_dir().join("dfdaemon.sock")
}
/// default_download_protocol is the default protocol of downloading.
#[inline]
fn default_download_protocol() -> String {
"tcp".to_string()
}
/// default_download_request_rate_limit is the default rate limit of the download request in the
/// download grpc server, default is 4000 req/s.
pub fn default_download_request_rate_limit() -> u64 {
4000
}
/// default_parent_selector_sync_interval is the default interval to sync host information.
#[inline]
fn default_parent_selector_sync_interval() -> Duration {
Duration::from_secs(3)
}
/// default_parent_selector_capacity is the default capacity of the parent selector's gRPC connections.
#[inline]
pub fn default_parent_selector_capacity() -> usize {
20
}
/// default_host_hostname is the default hostname of the host.
#[inline]
fn default_host_hostname() -> String {
hostname::get().unwrap().to_string_lossy().to_string()
}
/// default_dfdaemon_plugin_dir is the default plugin directory for dfdaemon.
#[inline]
fn default_dfdaemon_plugin_dir() -> PathBuf {
crate::default_plugin_dir().join(NAME)
}
/// default_dfdaemon_cache_dir is the default cache directory for dfdaemon.
#[inline]
fn default_dfdaemon_cache_dir() -> PathBuf {
crate::default_cache_dir().join(NAME)
}
/// default_upload_grpc_server_port is the default port of the upload gRPC server.
#[inline]
fn default_upload_grpc_server_port() -> u16 {
4000
}
/// default_upload_request_rate_limit is the default rate limit of the upload request in the
/// upload grpc server, default is 4000 req/s.
pub fn default_upload_request_rate_limit() -> u64 {
4000
}
/// default_upload_rate_limit is the default rate limit of the upload speed in GiB/Mib/Kib per second.
#[inline]
fn default_upload_rate_limit() -> ByteSize {
// Default rate limit is 10GiB/s.
ByteSize::gib(10)
}
/// default_health_server_port is the default port of the health server.
#[inline]
fn default_health_server_port() -> u16 {
4003
}
/// default_metrics_server_port is the default port of the metrics server.
#[inline]
fn default_metrics_server_port() -> u16 {
4002
}
/// default_stats_server_port is the default port of the stats server.
#[inline]
fn default_stats_server_port() -> u16 {
4004
}
/// default_download_rate_limit is the default rate limit of the download speed in GiB/Mib/Kib per second.
#[inline]
fn default_download_rate_limit() -> ByteSize {
// Default rate limit is 10GiB/s.
ByteSize::gib(10)
}
/// default_download_piece_timeout is the default timeout for downloading a piece from source.
#[inline]
fn default_download_piece_timeout() -> Duration {
Duration::from_secs(120)
}
/// default_collected_download_piece_timeout is the default timeout for collecting one piece from the parent in the stream.
#[inline]
fn default_collected_download_piece_timeout() -> Duration {
Duration::from_secs(10)
}
/// default_download_concurrent_piece_count is the default number of concurrent pieces to download.
#[inline]
fn default_download_concurrent_piece_count() -> u32 {
8
}
/// default_download_max_schedule_count is the default max count of schedule.
#[inline]
fn default_download_max_schedule_count() -> u32 {
5
}
/// default_tracing_path is the default tracing path for dfdaemon.
#[inline]
fn default_tracing_path() -> Option<PathBuf> {
Some(PathBuf::from("/v1/traces"))
}
/// default_scheduler_announce_interval is the default interval to announce peer to the scheduler.
#[inline]
fn default_scheduler_announce_interval() -> Duration {
Duration::from_secs(300)
}
/// default_scheduler_schedule_timeout is the default timeout for scheduling.
#[inline]
fn default_scheduler_schedule_timeout() -> Duration {
Duration::from_secs(3 * 60 * 60)
}
/// default_dynconfig_refresh_interval is the default interval to refresh dynamic configuration from manager.
#[inline]
fn default_dynconfig_refresh_interval() -> Duration {
Duration::from_secs(300)
}
/// default_storage_server_tcp_port is the default port of the storage tcp server.
#[inline]
fn default_storage_server_tcp_port() -> u16 {
4005
}
/// default_storage_server_quic_port is the default port of the storage quic server.
#[inline]
fn default_storage_server_quic_port() -> u16 {
4006
}
/// default_storage_keep is the default keep of the task's metadata and content when the dfdaemon restarts.
#[inline]
fn default_storage_keep() -> bool {
false
}
/// default_storage_directio is the default whether enable direct I/O when reading or writing piece to storage.
#[inline]
fn default_storage_directio() -> bool {
false
}
/// default_storage_write_piece_timeout is the default timeout for writing a piece to storage(e.g., disk
/// or cache).
#[inline]
fn default_storage_write_piece_timeout() -> Duration {
Duration::from_secs(90)
}
/// default_storage_write_buffer_size is the default buffer size for writing piece to disk, default is 4MB.
#[inline]
fn default_storage_write_buffer_size() -> usize {
4 * 1024 * 1024
}
/// default_storage_read_buffer_size is the default buffer size for reading piece from disk, default is 4MB.
#[inline]
fn default_storage_read_buffer_size() -> usize {
4 * 1024 * 1024
}
/// default_storage_cache_capacity is the default cache capacity for the storage server, default is
/// 64MiB.
#[inline]
fn default_storage_cache_capacity() -> ByteSize {
ByteSize::mib(64)
}
/// default_gc_interval is the default interval to do gc.
#[inline]
fn default_gc_interval() -> Duration {
Duration::from_secs(900)
}
/// default_gc_policy_task_ttl is the default ttl of the task.
#[inline]
fn default_gc_policy_task_ttl() -> Duration {
Duration::from_secs(21_600)
}
/// default_gc_policy_dist_threshold is the default threshold of the disk usage to do gc.
#[inline]
fn default_gc_policy_dist_threshold() -> ByteSize {
ByteSize::default()
}
/// default_gc_policy_dist_high_threshold_percent is the default high threshold percent of the disk usage.
#[inline]
fn default_gc_policy_dist_high_threshold_percent() -> u8 {
80
}
/// default_gc_policy_dist_low_threshold_percent is the default low threshold percent of the disk usage.
#[inline]
fn default_gc_policy_dist_low_threshold_percent() -> u8 {
60
}
/// default_proxy_server_port is the default port of the proxy server.
#[inline]
pub fn default_proxy_server_port() -> u16 {
4001
}
/// default_proxy_read_buffer_size is the default buffer size for reading piece, default is 4MB.
#[inline]
pub fn default_proxy_read_buffer_size() -> usize {
4 * 1024 * 1024
}
/// default_prefetch_rate_limit is the default rate limit of the prefetch speed in GiB/Mib/Kib per second. The prefetch request
/// has lower priority so limit the rate to avoid occupying the bandwidth impact other download tasks.
#[inline]
fn default_prefetch_rate_limit() -> ByteSize {
// Default rate limit is 2GiB/s.
ByteSize::gib(2)
}
/// default_s3_filtered_query_params is the default filtered query params with s3 protocol to generate the task id.
#[inline]
fn s3_filtered_query_params() -> Vec<String> {
vec![
"X-Amz-Algorithm".to_string(),
"X-Amz-Credential".to_string(),
"X-Amz-Date".to_string(),
"X-Amz-Expires".to_string(),
"X-Amz-SignedHeaders".to_string(),
"X-Amz-Signature".to_string(),
"X-Amz-Security-Token".to_string(),
"X-Amz-User-Agent".to_string(),
]
}
/// gcs_filtered_query_params is the filtered query params with gcs protocol to generate the task id.
#[inline]
fn gcs_filtered_query_params() -> Vec<String> {
vec![
"X-Goog-Algorithm".to_string(),
"X-Goog-Credential".to_string(),
"X-Goog-Date".to_string(),
"X-Goog-Expires".to_string(),
"X-Goog-SignedHeaders".to_string(),
"X-Goog-Signature".to_string(),
]
}
/// oss_filtered_query_params is the filtered query params with oss protocol to generate the task id.
#[inline]
fn oss_filtered_query_params() -> Vec<String> {
vec![
"OSSAccessKeyId".to_string(),
"Expires".to_string(),
"Signature".to_string(),
"SecurityToken".to_string(),
]
}
/// obs_filtered_query_params is the filtered query params with obs protocol to generate the task id.
#[inline]
fn obs_filtered_query_params() -> Vec<String> {
vec![
"AccessKeyId".to_string(),
"Signature".to_string(),
"Expires".to_string(),
"X-Obs-Date".to_string(),
"X-Obs-Security-Token".to_string(),
]
}
/// cos_filtered_query_params is the filtered query params with cos protocol to generate the task id.
#[inline]
fn cos_filtered_query_params() -> Vec<String> {
vec![
"q-sign-algorithm".to_string(),
"q-ak".to_string(),
"q-sign-time".to_string(),
"q-key-time".to_string(),
"q-header-list".to_string(),
"q-url-param-list".to_string(),
"q-signature".to_string(),
"x-cos-security-token".to_string(),
]
}
/// containerd_filtered_query_params is the filtered query params with containerd to generate the task id.
#[inline]
fn containerd_filtered_query_params() -> Vec<String> {
vec!["ns".to_string()]
}
/// default_proxy_rule_filtered_query_params is the default filtered query params to generate the task id.
#[inline]
pub fn default_proxy_rule_filtered_query_params() -> Vec<String> {
let mut visited = HashSet::new();
for query_param in s3_filtered_query_params() {
visited.insert(query_param);
}
for query_param in gcs_filtered_query_params() {
visited.insert(query_param);
}
for query_param in oss_filtered_query_params() {
visited.insert(query_param);
}
for query_param in obs_filtered_query_params() {
visited.insert(query_param);
}
for query_param in cos_filtered_query_params() {
visited.insert(query_param);
}
for query_param in containerd_filtered_query_params() {
visited.insert(query_param);
}
visited.into_iter().collect()
}
/// default_proxy_registry_mirror_addr is the default registry mirror address.
#[inline]
fn default_proxy_registry_mirror_addr() -> String {
"https://index.docker.io".to_string()
}
/// Host is the host configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Host {
/// idc is the idc of the host.
pub idc: Option<String>,
/// location is the location of the host.
pub location: Option<String>,
/// hostname is the hostname of the host.
#[serde(default = "default_host_hostname")]
pub hostname: String,
/// ip is the advertise ip of the host.
pub ip: Option<IpAddr>,
/// scheduler_cluster_id is the ID of the cluster to which the scheduler belongs.
/// NOTE: This field is used to identify the cluster to which the scheduler belongs.
/// If this flag is set, the idc, location, hostname and ip will be ignored when listing schedulers.
#[serde(rename = "schedulerClusterID")]
pub scheduler_cluster_id: Option<u64>,
}
/// Host implements Default.
impl Default for Host {
fn default() -> Self {
Host {
idc: None,
location: None,
hostname: default_host_hostname(),
ip: None,
scheduler_cluster_id: None,
}
}
}
/// Server is the server configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Server {
/// plugin_dir is the directory to store plugins.
#[serde(default = "default_dfdaemon_plugin_dir")]
pub plugin_dir: PathBuf,
/// cache_dir is the directory to store cache files.
#[serde(default = "default_dfdaemon_cache_dir")]
pub cache_dir: PathBuf,
}
/// Server implements Default.
impl Default for Server {
fn default() -> Self {
Server {
plugin_dir: default_dfdaemon_plugin_dir(),
cache_dir: default_dfdaemon_cache_dir(),
}
}
}
/// DownloadServer is the download server configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct DownloadServer {
/// socket_path is the unix socket path for dfdaemon gRPC service.
#[serde(default = "default_download_unix_socket_path")]
pub socket_path: PathBuf,
/// request_rate_limit is the rate limit of the download request in the download grpc server,
/// default is 4000 req/s.
#[serde(default = "default_download_request_rate_limit")]
pub request_rate_limit: u64,
}
/// DownloadServer implements Default.
impl Default for DownloadServer {
fn default() -> Self {
DownloadServer {
socket_path: default_download_unix_socket_path(),
request_rate_limit: default_download_request_rate_limit(),
}
}
}
/// Download is the download configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Download {
/// server is the download server configuration for dfdaemon.
pub server: DownloadServer,
/// Protocol that peers use to download piece (e.g., "tcp", "quic").
/// When dfdaemon acts as a parent, it announces this protocol so downstream
/// peers fetch pieces using it.
#[serde(default = "default_download_protocol")]
pub protocol: String,
/// parent_selector is the download parent selector configuration for dfdaemon.
pub parent_selector: ParentSelector,
/// rate_limit is the rate limit of the download speed in GiB/Mib/Kib per second.
#[serde(with = "bytesize_serde", default = "default_download_rate_limit")]
pub rate_limit: ByteSize,
/// piece_timeout is the timeout for downloading a piece from source.
#[serde(default = "default_download_piece_timeout", with = "humantime_serde")]
pub piece_timeout: Duration,
/// collected_piece_timeout is the timeout for collecting one piece from the parent in the
/// stream.
#[serde(
default = "default_collected_download_piece_timeout",
with = "humantime_serde"
)]
pub collected_piece_timeout: Duration,
/// concurrent_piece_count is the number of concurrent pieces to download.
#[serde(default = "default_download_concurrent_piece_count")]
#[validate(range(min = 1))]
pub concurrent_piece_count: u32,
}
/// Download implements Default.
impl Default for Download {
fn default() -> Self {
Download {
server: DownloadServer::default(),
protocol: default_download_protocol(),
parent_selector: ParentSelector::default(),
rate_limit: default_download_rate_limit(),
piece_timeout: default_download_piece_timeout(),
collected_piece_timeout: default_collected_download_piece_timeout(),
concurrent_piece_count: default_download_concurrent_piece_count(),
}
}
}
/// UploadServer is the upload server configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct UploadServer {
/// ip is the listen ip of the gRPC server.
pub ip: Option<IpAddr>,
/// port is the port to the gRPC server.
#[serde(default = "default_upload_grpc_server_port")]
pub port: u16,
/// ca_cert is the root CA cert path with PEM format for the upload server, and it is used
/// for mutual TLS.
pub ca_cert: Option<PathBuf>,
/// cert is the server cert path with PEM format for the upload server and it is used for
/// mutual TLS.
pub cert: Option<PathBuf>,
/// key is the server key path with PEM format for the upload server and it is used for
/// mutual TLS.
pub key: Option<PathBuf>,
/// request_rate_limit is the rate limit of the upload request in the upload grpc server,
/// default is 4000 req/s.
#[serde(default = "default_upload_request_rate_limit")]
pub request_rate_limit: u64,
}
/// UploadServer implements Default.
impl Default for UploadServer {
fn default() -> Self {
UploadServer {
ip: None,
port: default_upload_grpc_server_port(),
ca_cert: None,
cert: None,
key: None,
request_rate_limit: default_upload_request_rate_limit(),
}
}
}
/// UploadServer is the implementation of UploadServer.
impl UploadServer {
/// load_server_tls_config loads the server tls config.
pub async fn load_server_tls_config(&self) -> Result<Option<ServerTlsConfig>> {
if let (Some(ca_cert_path), Some(server_cert_path), Some(server_key_path)) =
(self.ca_cert.clone(), self.cert.clone(), self.key.clone())
{
let server_cert = fs::read(&server_cert_path).await?;
let server_key = fs::read(&server_key_path).await?;
let server_identity = Identity::from_pem(server_cert, server_key);
let ca_cert = fs::read(&ca_cert_path).await?;
let ca_cert = TonicCertificate::from_pem(ca_cert);
return Ok(Some(
ServerTlsConfig::new()
.identity(server_identity)
.client_ca_root(ca_cert),
));
}
Ok(None)
}
}
/// UploadClient is the upload client configuration for dfdaemon.
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct UploadClient {
/// ca_cert is the root CA cert path with PEM format for the upload client, and it is used
/// for mutual TLS.
pub ca_cert: Option<PathBuf>,
/// cert is the client cert path with PEM format for the upload client and it is used for
/// mutual TLS.
pub cert: Option<PathBuf>,
/// key is the client key path with PEM format for the upload client and it is used for
/// mutual TLS.
pub key: Option<PathBuf>,
}
/// UploadClient is the implementation of UploadClient.
impl UploadClient {
/// load_client_tls_config loads the client tls config.
pub async fn load_client_tls_config(
&self,
domain_name: &str,
) -> Result<Option<ClientTlsConfig>> {
if let (Some(ca_cert_path), Some(client_cert_path), Some(client_key_path)) =
(self.ca_cert.clone(), self.cert.clone(), self.key.clone())
{
let client_cert = fs::read(&client_cert_path).await?;
let client_key = fs::read(&client_key_path).await?;
let client_identity = Identity::from_pem(client_cert, client_key);
let ca_cert = fs::read(&ca_cert_path).await?;
let ca_cert = TonicCertificate::from_pem(ca_cert);
// TODO(gaius): Use trust_anchor to skip the verify of hostname.
return Ok(Some(
ClientTlsConfig::new()
.domain_name(domain_name)
.ca_certificate(ca_cert)
.identity(client_identity),
));
}
Ok(None)
}
}
/// ParentSelector is the download parent selector configuration for dfdaemon. It will synchronize
/// the host info in real-time from the parents and then select the parents for downloading.
///
/// The workflow diagram is as follows:
///
///```text
/// +----------+
/// ----------------| Parent |---------------
/// | +----------+ |
/// Host Info Piece Metadata
/// +------------|-----------------------------------------|------------+
/// | | | |
/// | | Peer | |
/// | v v |
/// | +------------------+ +------------------+ |
/// | | ParentSelector | ---Optimal Parent---> | PieceCollector | |
/// | +------------------+ +------------------+ |
/// | | |
/// | Piece Metadata |
/// | | |
/// | v |
/// | +------------+ |
/// | | Download | |
/// | +------------+ |
/// +-------------------------------------------------------------------+
/// ```
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ParentSelector {
/// enable indicates whether enable parent selector for downloading.
///
/// If `enable` is true, the `ParentSelector`'s sync loop will start. It will periodically fetch
/// host information from parents and use this information to calculate scores for selecting the
/// parents for downloading.
pub enable: bool,
/// sync_interval is the interval to sync parents' host info by gRPC streaming.
#[serde(
default = "default_parent_selector_sync_interval",
with = "humantime_serde"
)]
pub sync_interval: Duration,
/// capacity is the maximum number of gRPC connections that `DfdaemonUpload.SyncHost` maintains
/// in the `ParentSelector`, the default value is 20.
#[serde(default = "default_parent_selector_capacity")]
pub capacity: usize,
}
/// Upload is the upload configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Upload {
/// server is the upload server configuration for dfdaemon.
pub server: UploadServer,
/// client is the upload client configuration for dfdaemon.
pub client: UploadClient,
/// disable_shared indicates whether disable to share data for other peers.
pub disable_shared: bool,
/// rate_limit is the rate limit of the upload speed in GiB/Mib/Kib per second.
#[serde(with = "bytesize_serde", default = "default_upload_rate_limit")]
pub rate_limit: ByteSize,
}
/// Upload implements Default.
impl Default for Upload {
fn default() -> Self {
Upload {
server: UploadServer::default(),
client: UploadClient::default(),
disable_shared: false,
rate_limit: default_upload_rate_limit(),
}
}
}
/// Manager is the manager configuration for dfdaemon.
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Manager {
/// addr is the manager address.
pub addr: String,
/// ca_cert is the root CA cert path with PEM format for the manager, and it is used
/// for mutual TLS.
pub ca_cert: Option<PathBuf>,
/// cert is the client cert path with PEM format for the manager and it is used for
/// mutual TLS.
pub cert: Option<PathBuf>,
/// key is the client key path with PEM format for the manager and it is used for
/// mutual TLS.
pub key: Option<PathBuf>,
}
/// Manager is the implementation of Manager.
impl Manager {
/// load_client_tls_config loads the client tls config.
pub async fn load_client_tls_config(
&self,
domain_name: &str,
) -> Result<Option<ClientTlsConfig>> {
if let (Some(ca_cert_path), Some(client_cert_path), Some(client_key_path)) =
(self.ca_cert.clone(), self.cert.clone(), self.key.clone())
{
let client_cert = fs::read(&client_cert_path).await?;
let client_key = fs::read(&client_key_path).await?;
let client_identity = Identity::from_pem(client_cert, client_key);
let ca_cert = fs::read(&ca_cert_path).await?;
let ca_cert = TonicCertificate::from_pem(ca_cert);
// TODO(gaius): Use trust_anchor to skip the verify of hostname.
return Ok(Some(
ClientTlsConfig::new()
.domain_name(domain_name)
.ca_certificate(ca_cert)
.identity(client_identity),
));
}
Ok(None)
}
}
/// Scheduler is the scheduler configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Scheduler {
/// announce_interval is the interval to announce peer to the scheduler.
/// Announcer will provide the scheduler with peer information for scheduling,
/// peer information includes cpu, memory, etc.
#[serde(
default = "default_scheduler_announce_interval",
with = "humantime_serde"
)]
pub announce_interval: Duration,
/// schedule_timeout is timeout for the scheduler to respond to a scheduling request from dfdaemon, default is 3 hours.
///
/// If the scheduler's response time for a scheduling decision exceeds this timeout,
/// dfdaemon will encounter a `TokioStreamElapsed(Elapsed(()))` error.
///
/// Behavior upon timeout:
/// - If `enable_back_to_source` is `true`, dfdaemon will attempt to download directly
/// from the source.
/// - Otherwise (if `enable_back_to_source` is `false`), dfdaemon will report a download failure.
///
/// **Important Considerations Regarding Timeout Triggers**:
/// This timeout isn't solely for the scheduler's direct response. It can also be triggered
/// if the overall duration of the client's interaction with the scheduler for a task
/// (e.g., client downloading initial pieces and reporting their status back to the scheduler)
/// exceeds `schedule_timeout`. During such client-side processing and reporting,
/// the scheduler might be awaiting these updates before sending its comprehensive
/// scheduling response, and this entire period is subject to the `schedule_timeout`.
///
/// **Configuration Guidance**:
/// To prevent premature timeouts, `schedule_timeout` should be configured to a value
/// greater than the maximum expected time for the *entire scheduling interaction*.
/// This includes:
/// 1. The scheduler's own processing and response time.
/// 2. The time taken by the client to download any initial pieces and download all pieces finished,
/// as this communication is part of the scheduling phase.
///
/// Setting this value too low can lead to `TokioStreamElapsed` errors even if the
/// network and scheduler are functioning correctly but the combined interaction time
/// is longer than the configured timeout.
#[serde(
default = "default_scheduler_schedule_timeout",
with = "humantime_serde"
)]
pub schedule_timeout: Duration,
/// max_schedule_count is the max count of schedule.
#[serde(default = "default_download_max_schedule_count")]
#[validate(range(min = 1))]
pub max_schedule_count: u32,
/// ca_cert is the root CA cert path with PEM format for the scheduler, and it is used
/// for mutual TLS.
pub ca_cert: Option<PathBuf>,
/// cert is the client cert path with PEM format for the scheduler and it is used for
/// mutual TLS.
pub cert: Option<PathBuf>,
/// key is the client key path with PEM format for the scheduler and it is used for
/// mutual TLS.
pub key: Option<PathBuf>,
}
/// Scheduler implements Default.
impl Default for Scheduler {
fn default() -> Self {
Scheduler {
announce_interval: default_scheduler_announce_interval(),
schedule_timeout: default_scheduler_schedule_timeout(),
max_schedule_count: default_download_max_schedule_count(),
ca_cert: None,
cert: None,
key: None,
}
}
}
/// Scheduler is the implementation of Scheduler.
impl Scheduler {
/// load_client_tls_config loads the client tls config.
pub async fn load_client_tls_config(
&self,
domain_name: &str,
) -> Result<Option<ClientTlsConfig>> {
if let (Some(ca_cert_path), Some(client_cert_path), Some(client_key_path)) =
(self.ca_cert.clone(), self.cert.clone(), self.key.clone())
{
let client_cert = fs::read(&client_cert_path).await?;
let client_key = fs::read(&client_key_path).await?;
let client_identity = Identity::from_pem(client_cert, client_key);
let ca_cert = fs::read(&ca_cert_path).await?;
let ca_cert = TonicCertificate::from_pem(ca_cert);
// TODO(gaius): Use trust_anchor to skip the verify of hostname.
return Ok(Some(
ClientTlsConfig::new()
.domain_name(domain_name)
.ca_certificate(ca_cert)
.identity(client_identity),
));
}
Ok(None)
}
}
/// HostType is the type of the host.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
pub enum HostType {
/// Normal indicates the peer is normal peer.
#[serde(rename = "normal")]
Normal,
/// Super indicates the peer is super seed peer.
#[default]
#[serde(rename = "super")]
Super,
/// Strong indicates the peer is strong seed peer.
#[serde(rename = "strong")]
Strong,
/// Weak indicates the peer is weak seed peer.
#[serde(rename = "weak")]
Weak,
}
/// HostType implements Display.
impl fmt::Display for HostType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HostType::Normal => write!(f, "normal"),
HostType::Super => write!(f, "super"),
HostType::Strong => write!(f, "strong"),
HostType::Weak => write!(f, "weak"),
}
}
}
/// SeedPeer is the seed peer configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SeedPeer {
/// enable indicates whether enable seed peer.
pub enable: bool,
/// kind is the type of seed peer.
#[serde(default, rename = "type")]
pub kind: HostType,
}
/// SeedPeer implements Default.
impl Default for SeedPeer {
fn default() -> Self {
SeedPeer {
enable: false,
kind: HostType::Normal,
}
}
}
/// Dynconfig is the dynconfig configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Dynconfig {
/// refresh_interval is the interval to refresh dynamic configuration from manager.
#[serde(
default = "default_dynconfig_refresh_interval",
with = "humantime_serde"
)]
pub refresh_interval: Duration,
}
/// Dynconfig implements Default.
impl Default for Dynconfig {
fn default() -> Self {
Dynconfig {
refresh_interval: default_dynconfig_refresh_interval(),
}
}
}
/// StorageServer is the storage server configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct StorageServer {
/// ip is the listen ip of the storage server.
pub ip: Option<IpAddr>,
/// port is the port to the tcp server.
#[serde(default = "default_storage_server_tcp_port")]
pub tcp_port: u16,
/// port is the port to the quic server.
#[serde(default = "default_storage_server_quic_port")]
pub quic_port: u16,
}
/// Storage implements Default.
impl Default for StorageServer {
fn default() -> Self {
StorageServer {
ip: None,
tcp_port: default_storage_server_tcp_port(),
quic_port: default_storage_server_quic_port(),
}
}
}
/// Storage is the storage configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Storage {
/// server is the storage server configuration for dfdaemon.
pub server: StorageServer,
/// dir is the directory to store task's metadata and content.
#[serde(default = "crate::default_storage_dir")]
pub dir: PathBuf,
/// keep indicates whether keep the task's metadata and content when the dfdaemon restarts.
#[serde(default = "default_storage_keep")]
pub keep: bool,
/// directio indicates whether enable direct I/O when reading or writing piece to storage.
#[serde(default = "default_storage_directio")]
pub directio: bool,
/// write_piece_timeout is the timeout for writing a piece to storage(e.g., disk
/// or cache).
#[serde(
default = "default_storage_write_piece_timeout",
with = "humantime_serde"
)]
pub write_piece_timeout: Duration,
/// write_buffer_size is the buffer size for writing piece to disk, default is 4MiB.
#[serde(default = "default_storage_write_buffer_size")]
pub write_buffer_size: usize,
/// read_buffer_size is the buffer size for reading piece from disk, default is 4MiB.
#[serde(default = "default_storage_read_buffer_size")]
pub read_buffer_size: usize,
/// cache_capacity is the cache capacity for downloading, default is 100.
///
/// Cache storage:
/// 1. Users can preheat task by caching to memory (via CacheTask) or to disk (via Task).
/// For more details, refer to https://github.com/dragonflyoss/api/blob/main/proto/dfdaemon.proto#L174.
/// 2. If the download hits the memory cache, it will be faster than reading from the disk, because there is no
/// page cache for the first read.
///
///```text
/// +--------+
/// │ Source │
/// +--------+
/// ^ ^ Preheat
/// │ │ |
/// +-----------------+ │ │ +----------------------------+
/// │ Other Peers │ │ │ │ Peer | │
/// │ │ │ │ │ v │
/// │ +----------+ │ │ │ │ +----------+ │
/// │ │ Cache |<--|----------|<-Miss--| Cache |--Hit-->|<----Download CacheTask
/// │ +----------+ │ │ │ +----------+ │
/// │ │ │ │ │
/// │ +----------+ │ │ │ +----------+ │
/// │ │ Disk |<--|----------|<-Miss--| Disk |--Hit-->|<----Download Task
/// │ +----------+ │ │ +----------+ │
/// │ │ │ ^ │
/// │ │ │ | │
/// +-----------------+ +----------------------------+
/// |
/// Preheat
///```
#[serde(with = "bytesize_serde", default = "default_storage_cache_capacity")]
pub cache_capacity: ByteSize,
}
/// Storage implements Default.
impl Default for Storage {
fn default() -> Self {
Storage {
server: StorageServer::default(),
dir: crate::default_storage_dir(),
keep: default_storage_keep(),
directio: default_storage_directio(),
write_piece_timeout: default_storage_write_piece_timeout(),
write_buffer_size: default_storage_write_buffer_size(),
read_buffer_size: default_storage_read_buffer_size(),
cache_capacity: default_storage_cache_capacity(),
}
}
}
/// Policy is the policy configuration for gc.
#[derive(Debug, Clone, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Policy {
/// task_ttl is the ttl of the task.
#[serde(
default = "default_gc_policy_task_ttl",
rename = "taskTTL",
with = "humantime_serde"
)]
pub task_ttl: Duration,
/// dist_threshold optionally defines a specific disk capacity to be used as the base for
/// calculating GC trigger points with `dist_high_threshold_percent` and `dist_low_threshold_percent`.
///
/// - If a value is provided (e.g., "500GB"), the percentage-based thresholds (`dist_high_threshold_percent`,
/// `dist_low_threshold_percent`) are applied relative to this specified capacity.
/// - If not provided or set to 0 (the default behavior), these percentage-based thresholds are applied
/// relative to the total actual disk space.
///
/// This allows dfdaemon to effectively manage a logical portion of the disk for its cache,
/// rather than always considering the entire disk volume.
#[serde(with = "bytesize_serde", default = "default_gc_policy_dist_threshold")]
pub dist_threshold: ByteSize,
/// dist_high_threshold_percent is the high threshold percent of the disk usage.
/// If the disk usage is greater than the threshold, dfdaemon will do gc.
#[serde(default = "default_gc_policy_dist_high_threshold_percent")]
#[validate(range(min = 1, max = 99))]
pub dist_high_threshold_percent: u8,
/// dist_low_threshold_percent is the low threshold percent of the disk usage.
/// If the disk usage is less than the threshold, dfdaemon will stop gc.
#[serde(default = "default_gc_policy_dist_low_threshold_percent")]
#[validate(range(min = 1, max = 99))]
pub dist_low_threshold_percent: u8,
}
/// Policy implements Default.
impl Default for Policy {
fn default() -> Self {
Policy {
dist_threshold: default_gc_policy_dist_threshold(),
task_ttl: default_gc_policy_task_ttl(),
dist_high_threshold_percent: default_gc_policy_dist_high_threshold_percent(),
dist_low_threshold_percent: default_gc_policy_dist_low_threshold_percent(),
}
}
}
/// GC is the gc configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct GC {
/// interval is the interval to do gc.
#[serde(default = "default_gc_interval", with = "humantime_serde")]
pub interval: Duration,
/// policy is the gc policy.
pub policy: Policy,
}
/// GC implements Default.
impl Default for GC {
fn default() -> Self {
GC {
interval: default_gc_interval(),
policy: Policy::default(),
}
}
}
/// BasicAuth is the basic auth configuration for HTTP proxy in dfdaemon.
#[derive(Default, Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct BasicAuth {
/// username is the username of the basic auth.
#[validate(length(min = 1, max = 20))]
pub username: String,
/// passwork is the passwork of the basic auth.
#[validate(length(min = 1, max = 20))]
pub password: String,
}
impl BasicAuth {
/// credentials loads the credentials.
pub fn credentials(&self) -> basic_auth::Credentials {
basic_auth::Credentials::new(&self.username, &self.password)
}
}
/// ProxyServer is the proxy server configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ProxyServer {
/// ip is the listen ip of the proxy server.
pub ip: Option<IpAddr>,
/// port is the port to the proxy server.
#[serde(default = "default_proxy_server_port")]
pub port: u16,
/// ca_cert is the root CA cert path with PEM format for the proxy server to generate the server cert.
///
/// If ca_cert is empty, proxy will generate a smaple CA cert by rcgen::generate_simple_self_signed.
/// When client requests via the proxy, the client should not verify the server cert and set
/// insecure to true.
///
/// If ca_cert is not empty, proxy will sign the server cert with the CA cert. If openssl is installed,
/// you can use openssl to generate the root CA cert and make the system trust the root CA cert.
/// Then set the ca_cert and ca_key to the root CA cert and key path. Dfdaemon generates the server cert
/// and key, and signs the server cert with the root CA cert. When client requests via the proxy,
/// the proxy can intercept the request by the server cert.
pub ca_cert: Option<PathBuf>,
/// ca_key is the root CA key path with PEM format for the proxy server to generate the server cert.
///
/// If ca_key is empty, proxy will generate a smaple CA key by rcgen::generate_simple_self_signed.
/// When client requests via the proxy, the client should not verify the server cert and set
/// insecure to true.
///
/// If ca_key is not empty, proxy will sign the server cert with the CA cert. If openssl is installed,
/// you can use openssl to generate the root CA cert and make the system trust the root CA cert.
/// Then set the ca_cert and ca_key to the root CA cert and key path. Dfdaemon generates the server cert
/// and key, and signs the server cert with the root CA cert. When client requests via the proxy,
/// the proxy can intercept the request by the server cert.
pub ca_key: Option<PathBuf>,
/// basic_auth is the basic auth configuration for HTTP proxy in dfdaemon. If basic_auth is not
/// empty, the proxy will use the basic auth to authenticate the client by Authorization
/// header. The value of the Authorization header is "Basic base64(username:password)", refer
/// to https://en.wikipedia.org/wiki/Basic_access_authentication.
pub basic_auth: Option<BasicAuth>,
}
/// ProxyServer implements Default.
impl Default for ProxyServer {
fn default() -> Self {
Self {
ip: None,
port: default_proxy_server_port(),
ca_cert: None,
ca_key: None,
basic_auth: None,
}
}
}
/// ProxyServer is the implementation of ProxyServer.
impl ProxyServer {
/// load_cert loads the cert.
pub fn load_cert(&self) -> Result<Option<Certificate>> {
if let (Some(server_ca_cert_path), Some(server_ca_key_path)) =
(self.ca_cert.clone(), self.ca_key.clone())
{
match generate_ca_cert_from_pem(&server_ca_cert_path, &server_ca_key_path) {
Ok(server_ca_cert) => return Ok(Some(server_ca_cert)),
Err(err) => {
error!("generate ca cert and key from pem failed: {}", err);
return Err(err);
}
}
}
Ok(None)
}
}
/// Rule is the proxy rule configuration.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Rule {
/// regex is the regex of the request url.
#[serde(with = "serde_regex")]
pub regex: Regex,
/// use_tls indicates whether use tls for the proxy backend.
#[serde(rename = "useTLS")]
pub use_tls: bool,
/// redirect is the redirect url.
pub redirect: Option<String>,
/// filtered_query_params is the filtered query params to generate the task id.
/// When filter is ["Signature", "Expires", "ns"], for example:
/// http://example.com/xyz?Expires=e1&Signature=s1&ns=docker.io and http://example.com/xyz?Expires=e2&Signature=s2&ns=docker.io
/// will generate the same task id.
/// Default value includes the filtered query params of s3, gcs, oss, obs, cos.
#[serde(default = "default_proxy_rule_filtered_query_params")]
pub filtered_query_params: Vec<String>,
}
/// Rule implements Default.
impl Default for Rule {
fn default() -> Self {
Self {
regex: Regex::new(r".*").unwrap(),
use_tls: false,
redirect: None,
filtered_query_params: default_proxy_rule_filtered_query_params(),
}
}
}
/// RegistryMirror is the registry mirror configuration.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct RegistryMirror {
/// addr is the default address of the registry mirror. Proxy will start a registry mirror service for the
/// client to pull the image. The client can use the default address of the registry mirror in
/// configuration to pull the image. The `X-Dragonfly-Registry` header can instead of the default address
/// of registry mirror.
#[serde(default = "default_proxy_registry_mirror_addr")]
pub addr: String,
/// cert is the client cert path with PEM format for the registry.
/// If registry use self-signed cert, the client should set the
/// cert for the registry mirror.
pub cert: Option<PathBuf>,
}
/// RegistryMirror implements Default.
impl Default for RegistryMirror {
fn default() -> Self {
Self {
addr: default_proxy_registry_mirror_addr(),
cert: None,
}
}
}
/// RegistryMirror is the implementation of RegistryMirror.
impl RegistryMirror {
/// load_cert_ders loads the cert ders.
pub fn load_cert_der(&self) -> Result<Option<Vec<CertificateDer<'static>>>> {
if let Some(cert_path) = self.cert.clone() {
match generate_cert_from_pem(&cert_path) {
Ok(cert) => return Ok(Some(cert)),
Err(err) => {
error!("generate cert from pems failed: {}", err);
return Err(err);
}
}
};
Ok(None)
}
}
/// Proxy is the proxy configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Proxy {
/// server is the proxy server configuration for dfdaemon.
pub server: ProxyServer,
/// rules is the proxy rules.
pub rules: Option<Vec<Rule>>,
/// registry_mirror is implementation of the registry mirror in the proxy.
pub registry_mirror: RegistryMirror,
/// disable_back_to_source indicates whether disable to download back-to-source
/// when download failed.
pub disable_back_to_source: bool,
/// prefetch pre-downloads full of the task when download with range request.
pub prefetch: bool,
/// prefetch_rate_limit is the rate limit of the prefetch speed in GiB/Mib/Kib per second. The prefetch request
/// has lower priority so limit the rate to avoid occupying the bandwidth impact other download tasks.
#[serde(with = "bytesize_serde", default = "default_prefetch_rate_limit")]
pub prefetch_rate_limit: ByteSize,
/// read_buffer_size is the buffer size for reading piece from disk, default is 1KB.
#[serde(default = "default_proxy_read_buffer_size")]
pub read_buffer_size: usize,
}
/// Proxy implements Default.
impl Default for Proxy {
fn default() -> Self {
Self {
server: ProxyServer::default(),
rules: None,
registry_mirror: RegistryMirror::default(),
disable_back_to_source: false,
prefetch: false,
prefetch_rate_limit: default_prefetch_rate_limit(),
read_buffer_size: default_proxy_read_buffer_size(),
}
}
}
/// Security is the security configuration for dfdaemon.
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Security {
/// enable indicates whether enable security.
pub enable: bool,
}
/// Network is the network configuration for dfdaemon.
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Network {
/// enable_ipv6 indicates whether enable ipv6.
pub enable_ipv6: bool,
}
/// HealthServer is the health server configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HealthServer {
/// ip is the listen ip of the health server.
pub ip: Option<IpAddr>,
/// port is the port to the health server.
#[serde(default = "default_health_server_port")]
pub port: u16,
}
/// HealthServer implements Default.
impl Default for HealthServer {
fn default() -> Self {
Self {
ip: None,
port: default_health_server_port(),
}
}
}
/// Health is the health configuration for dfdaemon.
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Health {
/// server is the health server configuration for dfdaemon.
pub server: HealthServer,
}
/// MetricsServer is the metrics server configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct MetricsServer {
/// ip is the listen ip of the metrics server.
pub ip: Option<IpAddr>,
/// port is the port to the metrics server.
#[serde(default = "default_metrics_server_port")]
pub port: u16,
}
/// MetricsServer implements Default.
impl Default for MetricsServer {
fn default() -> Self {
Self {
ip: None,
port: default_metrics_server_port(),
}
}
}
/// Metrics is the metrics configuration for dfdaemon.
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Metrics {
/// server is the metrics server configuration for dfdaemon.
pub server: MetricsServer,
}
/// StatsServer is the stats server configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct StatsServer {
/// ip is the listen ip of the stats server.
pub ip: Option<IpAddr>,
/// port is the port to the stats server.
#[serde(default = "default_stats_server_port")]
pub port: u16,
}
/// StatsServer implements Default.
impl Default for StatsServer {
fn default() -> Self {
Self {
ip: None,
port: default_stats_server_port(),
}
}
}
/// Stats is the stats configuration for dfdaemon.
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Stats {
/// server is the stats server configuration for dfdaemon.
pub server: StatsServer,
}
/// Tracing is the tracing configuration for dfdaemon.
#[derive(Debug, Clone, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Tracing {
/// Protocol specifies the communication protocol for the tracing server.
/// Supported values: "http", "https", "grpc" (default: None).
/// This determines how tracing logs are transmitted to the server.
pub protocol: Option<String>,
/// endpoint is the endpoint to report tracing log, example: "localhost:4317".
pub endpoint: Option<String>,
/// path is the path to report tracing log, example: "/v1/traces" if the protocol is "http" or
/// "https".
#[serde(default = "default_tracing_path")]
pub path: Option<PathBuf>,
/// headers is the headers to report tracing log.
#[serde(with = "http_serde::header_map")]
pub headers: reqwest::header::HeaderMap,
}
/// Tracing implements Default.
impl Default for Tracing {
fn default() -> Self {
Self {
protocol: None,
endpoint: None,
path: default_tracing_path(),
headers: reqwest::header::HeaderMap::new(),
}
}
}
/// Config is the configuration for dfdaemon.
#[derive(Debug, Clone, Default, Validate, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Config {
/// host is the host configuration for dfdaemon.
#[validate]
pub host: Host,
/// server is the server configuration for dfdaemon.
#[validate]
pub server: Server,
/// download is the download configuration for dfdaemon.
#[validate]
pub download: Download,
/// upload is the upload configuration for dfdaemon.
#[validate]
pub upload: Upload,
/// manager is the manager configuration for dfdaemon.
#[validate]
pub manager: Manager,
/// scheduler is the scheduler configuration for dfdaemon.
#[validate]
pub scheduler: Scheduler,
/// seed_peer is the seed peer configuration for dfdaemon.
#[validate]
pub seed_peer: SeedPeer,
/// dynconfig is the dynconfig configuration for dfdaemon.
#[validate]
pub dynconfig: Dynconfig,
/// storage is the storage configuration for dfdaemon.
#[validate]
pub storage: Storage,
/// gc is the gc configuration for dfdaemon.
#[validate]
pub gc: GC,
/// proxy is the proxy configuration for dfdaemon.
#[validate]
pub proxy: Proxy,
/// security is the security configuration for dfdaemon.
#[validate]
pub security: Security,
/// health is the health configuration for dfdaemon.
#[validate]
pub health: Health,
/// metrics is the metrics configuration for dfdaemon.
#[validate]
pub metrics: Metrics,
/// stats is the stats configuration for dfdaemon.
#[validate]
pub stats: Stats,
/// tracing is the tracing configuration for dfdaemon.
#[validate]
pub tracing: Tracing,
/// network is the network configuration for dfdaemon.
#[validate]
pub network: Network,
}
/// Config implements the config operation of dfdaemon.
impl Config {
/// load loads configuration from file.
#[instrument(skip_all)]
pub async fn load(path: &PathBuf) -> Result<Config> {
// Load configuration from file.
let content = fs::read_to_string(path).await?;
let mut config: Config = serde_yaml::from_str(&content).or_err(ErrorType::ConfigError)?;
// Convert configuration.
config.convert();
// Validate configuration.
config.validate().or_err(ErrorType::ValidationError)?;
Ok(config)
}
/// convert converts the configuration.
#[instrument(skip_all)]
fn convert(&mut self) {
// Convert advertise ip.
if self.host.ip.is_none() {
self.host.ip = if self.network.enable_ipv6 {
Some(local_ipv6().unwrap())
} else {
Some(local_ip().unwrap())
}
}
// Convert upload gRPC server listen ip.
if self.upload.server.ip.is_none() {
self.upload.server.ip = if self.network.enable_ipv6 {
Some(Ipv6Addr::UNSPECIFIED.into())
} else {
Some(Ipv4Addr::UNSPECIFIED.into())
}
}
// Convert storage server listen ip.
if self.storage.server.ip.is_none() {
self.storage.server.ip = if self.network.enable_ipv6 {
Some(Ipv6Addr::UNSPECIFIED.into())
} else {
Some(Ipv4Addr::UNSPECIFIED.into())
}
}
// Convert metrics server listen ip.
if self.health.server.ip.is_none() {
self.health.server.ip = if self.network.enable_ipv6 {
Some(Ipv6Addr::UNSPECIFIED.into())
} else {
Some(Ipv4Addr::UNSPECIFIED.into())
}
}
// Convert metrics server listen ip.
if self.metrics.server.ip.is_none() {
self.metrics.server.ip = if self.network.enable_ipv6 {
Some(Ipv6Addr::UNSPECIFIED.into())
} else {
Some(Ipv4Addr::UNSPECIFIED.into())
}
}
// Convert stats server listen ip.
if self.stats.server.ip.is_none() {
self.stats.server.ip = if self.network.enable_ipv6 {
Some(Ipv6Addr::UNSPECIFIED.into())
} else {
Some(Ipv4Addr::UNSPECIFIED.into())
}
}
// Convert proxy server listen ip.
if self.proxy.server.ip.is_none() {
self.proxy.server.ip = if self.network.enable_ipv6 {
Some(Ipv6Addr::UNSPECIFIED.into())
} else {
Some(Ipv4Addr::UNSPECIFIED.into())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use tokio::fs;
#[test]
fn default_proxy_rule_filtered_query_params_contains_all_params() {
let mut expected = HashSet::new();
expected.extend(s3_filtered_query_params());
expected.extend(gcs_filtered_query_params());
expected.extend(oss_filtered_query_params());
expected.extend(obs_filtered_query_params());
expected.extend(cos_filtered_query_params());
expected.extend(containerd_filtered_query_params());
let actual = default_proxy_rule_filtered_query_params();
let actual_set: HashSet<_> = actual.into_iter().collect();
assert_eq!(actual_set, expected);
}
#[test]
fn default_proxy_rule_removes_duplicates() {
let params: Vec<String> = default_proxy_rule_filtered_query_params();
let param_count = params.len();
let unique_params: HashSet<_> = params.into_iter().collect();
assert_eq!(unique_params.len(), param_count);
}
#[test]
fn default_proxy_rule_filtered_query_params_contains_key_properties() {
let params = default_proxy_rule_filtered_query_params();
let param_set: HashSet<_> = params.into_iter().collect();
assert!(param_set.contains("X-Amz-Signature"));
assert!(param_set.contains("X-Goog-Signature"));
assert!(param_set.contains("OSSAccessKeyId"));
assert!(param_set.contains("X-Obs-Security-Token"));
assert!(param_set.contains("q-sign-algorithm"));
assert!(param_set.contains("ns"));
}
#[test]
fn deserialize_server_correctly() {
let json_data = r#"
{
"pluginDir": "/custom/plugin/dir",
"cacheDir": "/custom/cache/dir"
}"#;
let server: Server = serde_json::from_str(json_data).unwrap();
assert_eq!(server.plugin_dir, PathBuf::from("/custom/plugin/dir"));
assert_eq!(server.cache_dir, PathBuf::from("/custom/cache/dir"));
}
#[test]
fn deserialize_download_correctly() {
let json_data = r#"
{
"server": {
"socketPath": "/var/run/dragonfly/dfdaemon.sock",
"requestRateLimit": 4000
},
"protocol": "quic",
"rateLimit": "50GiB",
"pieceTimeout": "30s",
"concurrentPieceCount": 10
}"#;
let download: Download = serde_json::from_str(json_data).unwrap();
assert_eq!(
download.server.socket_path,
PathBuf::from("/var/run/dragonfly/dfdaemon.sock")
);
assert_eq!(download.server.request_rate_limit, 4000);
assert_eq!(download.protocol, "quic".to_string());
assert_eq!(download.rate_limit, ByteSize::gib(50));
assert_eq!(download.piece_timeout, Duration::from_secs(30));
assert_eq!(download.concurrent_piece_count, 10);
}
#[test]
fn deserialize_upload_correctly() {
let json_data = r#"
{
"server": {
"port": 4000,
"ip": "127.0.0.1",
"caCert": "/etc/ssl/certs/ca.crt",
"cert": "/etc/ssl/certs/server.crt",
"key": "/etc/ssl/private/server.pem"
},
"client": {
"caCert": "/etc/ssl/certs/ca.crt",
"cert": "/etc/ssl/certs/client.crt",
"key": "/etc/ssl/private/client.pem"
},
"disableShared": false,
"rateLimit": "10GiB"
}"#;
let upload: Upload = serde_json::from_str(json_data).unwrap();
assert_eq!(upload.server.port, 4000);
assert_eq!(
upload.server.ip,
Some("127.0.0.1".parse::<IpAddr>().unwrap())
);
assert_eq!(
upload.server.ca_cert,
Some(PathBuf::from("/etc/ssl/certs/ca.crt"))
);
assert_eq!(
upload.server.cert,
Some(PathBuf::from("/etc/ssl/certs/server.crt"))
);
assert_eq!(
upload.server.key,
Some(PathBuf::from("/etc/ssl/private/server.pem"))
);
assert_eq!(
upload.client.ca_cert,
Some(PathBuf::from("/etc/ssl/certs/ca.crt"))
);
assert_eq!(
upload.client.cert,
Some(PathBuf::from("/etc/ssl/certs/client.crt"))
);
assert_eq!(
upload.client.key,
Some(PathBuf::from("/etc/ssl/private/client.pem"))
);
assert!(!upload.disable_shared);
assert_eq!(upload.rate_limit, ByteSize::gib(10));
}
#[test]
fn upload_server_default() {
let server = UploadServer::default();
assert!(server.ip.is_none());
assert_eq!(server.port, default_upload_grpc_server_port());
assert!(server.ca_cert.is_none());
assert!(server.cert.is_none());
assert!(server.key.is_none());
assert_eq!(
server.request_rate_limit,
default_upload_request_rate_limit()
);
}
#[tokio::test]
async fn upload_load_server_tls_config_success() {
let (ca_file, cert_file, key_file) = create_temp_certs().await;
let server = UploadServer {
ca_cert: Some(ca_file.path().to_path_buf()),
cert: Some(cert_file.path().to_path_buf()),
key: Some(key_file.path().to_path_buf()),
..Default::default()
};
let tls_config = server.load_server_tls_config().await.unwrap();
assert!(tls_config.is_some());
}
#[tokio::test]
async fn load_server_tls_config_missing_certs() {
let server = UploadServer {
ca_cert: Some(PathBuf::from("/invalid/path")),
cert: None,
key: None,
..Default::default()
};
let tls_config = server.load_server_tls_config().await.unwrap();
assert!(tls_config.is_none());
}
#[test]
fn upload_client_default() {
let client = UploadClient::default();
assert!(client.ca_cert.is_none());
assert!(client.cert.is_none());
assert!(client.key.is_none());
}
#[tokio::test]
async fn upload_client_load_tls_config_success() {
let (ca_file, cert_file, key_file) = create_temp_certs().await;
let client = UploadClient {
ca_cert: Some(ca_file.path().to_path_buf()),
cert: Some(cert_file.path().to_path_buf()),
key: Some(key_file.path().to_path_buf()),
};
let tls_config = client.load_client_tls_config("example.com").await.unwrap();
assert!(tls_config.is_some());
let cfg_string = format!("{:?}", tls_config.unwrap());
assert!(
cfg_string.contains("example.com"),
"Domain name not found in TLS config"
);
}
#[tokio::test]
async fn upload_server_load_tls_config_invalid_path() {
let server = UploadServer {
ca_cert: Some(PathBuf::from("/invalid/ca.crt")),
cert: Some(PathBuf::from("/invalid/server.crt")),
key: Some(PathBuf::from("/invalid/server.key")),
..Default::default()
};
let result = server.load_server_tls_config().await;
assert!(result.is_err());
}
async fn create_temp_certs() -> (NamedTempFile, NamedTempFile, NamedTempFile) {
let ca = NamedTempFile::new().unwrap();
let cert = NamedTempFile::new().unwrap();
let key = NamedTempFile::new().unwrap();
fs::write(ca.path(), "-----BEGIN CERT-----\n...\n-----END CERT-----\n")
.await
.unwrap();
fs::write(
cert.path(),
"-----BEGIN CERT-----\n...\n-----END CERT-----\n",
)
.await
.unwrap();
fs::write(
key.path(),
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
)
.await
.unwrap();
(ca, cert, key)
}
#[tokio::test]
async fn manager_load_client_tls_config_success() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ca_path = temp_dir.path().join("ca.crt");
let cert_path = temp_dir.path().join("client.crt");
let key_path = temp_dir.path().join("client.key");
fs::write(&ca_path, "CA cert content").await.unwrap();
fs::write(&cert_path, "Client cert content").await.unwrap();
fs::write(&key_path, "Client key content").await.unwrap();
let manager = Manager {
addr: "http://example.com".to_string(),
ca_cert: Some(ca_path),
cert: Some(cert_path),
key: Some(key_path),
};
let result = manager.load_client_tls_config("example.com").await;
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.is_some());
}
#[test]
fn deserialize_optional_fields_correctly() {
let yaml = r#"
addr: http://another-service:8080
"#;
let manager: Manager = serde_yaml::from_str(yaml).unwrap();
assert_eq!(manager.addr, "http://another-service:8080");
assert!(manager.ca_cert.is_none());
assert!(manager.cert.is_none());
assert!(manager.key.is_none());
}
#[test]
fn deserialize_manager_correctly() {
let yaml = r#"
addr: http://manager-service:65003
caCert: /etc/ssl/certs/ca.crt
cert: /etc/ssl/certs/client.crt
key: /etc/ssl/private/client.pem
"#;
let manager: Manager = serde_yaml::from_str(yaml).expect("Failed to deserialize");
assert_eq!(manager.addr, "http://manager-service:65003");
assert_eq!(
manager.ca_cert,
Some(PathBuf::from("/etc/ssl/certs/ca.crt"))
);
assert_eq!(
manager.cert,
Some(PathBuf::from("/etc/ssl/certs/client.crt"))
);
assert_eq!(
manager.key,
Some(PathBuf::from("/etc/ssl/private/client.pem"))
);
}
#[test]
fn default_host_type_correctly() {
// Test whether the Display implementation is correct.
assert_eq!(HostType::Normal.to_string(), "normal");
assert_eq!(HostType::Super.to_string(), "super");
assert_eq!(HostType::Strong.to_string(), "strong");
assert_eq!(HostType::Weak.to_string(), "weak");
// Test if the default value is HostType::Super.
let default_host_type: HostType = Default::default();
assert_eq!(default_host_type, HostType::Super);
}
#[test]
fn serialize_host_type_correctly() {
let normal: HostType = serde_json::from_str("\"normal\"").unwrap();
let super_seed: HostType = serde_json::from_str("\"super\"").unwrap();
let strong_seed: HostType = serde_json::from_str("\"strong\"").unwrap();
let weak_seed: HostType = serde_json::from_str("\"weak\"").unwrap();
assert_eq!(normal, HostType::Normal);
assert_eq!(super_seed, HostType::Super);
assert_eq!(strong_seed, HostType::Strong);
assert_eq!(weak_seed, HostType::Weak);
}
#[test]
fn serialize_host_type() {
let normal_json = serde_json::to_string(&HostType::Normal).unwrap();
let super_json = serde_json::to_string(&HostType::Super).unwrap();
let strong_json = serde_json::to_string(&HostType::Strong).unwrap();
let weak_json = serde_json::to_string(&HostType::Weak).unwrap();
assert_eq!(normal_json, "\"normal\"");
assert_eq!(super_json, "\"super\"");
assert_eq!(strong_json, "\"strong\"");
assert_eq!(weak_json, "\"weak\"");
}
#[test]
fn default_seed_peer() {
let default_seed_peer = SeedPeer::default();
assert!(!default_seed_peer.enable);
assert_eq!(default_seed_peer.kind, HostType::Normal);
}
#[test]
fn validate_seed_peer() {
let valid_seed_peer = SeedPeer {
enable: true,
kind: HostType::Weak,
};
assert!(valid_seed_peer.validate().is_ok());
}
#[test]
fn deserialize_seed_peer_correctly() {
let json_data = r#"
{
"enable": true,
"type": "super",
"clusterID": 2,
"keepaliveInterval": "60s"
}"#;
let seed_peer: SeedPeer = serde_json::from_str(json_data).unwrap();
assert!(seed_peer.enable);
assert_eq!(seed_peer.kind, HostType::Super);
}
#[test]
fn default_dynconfig() {
let default_dynconfig = Dynconfig::default();
assert_eq!(default_dynconfig.refresh_interval, Duration::from_secs(300));
}
#[test]
fn deserialize_dynconfig_correctly() {
let json_data = r#"
{
"refreshInterval": "5m"
}"#;
let dynconfig: Dynconfig = serde_json::from_str(json_data).unwrap();
assert_eq!(dynconfig.refresh_interval, Duration::from_secs(300));
}
#[test]
fn deserialize_storage_correctly() {
let json_data = r#"
{
"server": {
"ip": "128.0.0.1",
"tcpPort": 4005,
"quicPort": 4006
},
"dir": "/tmp/storage",
"keep": true,
"writePieceTimeout": "20s",
"writeBufferSize": 8388608,
"readBufferSize": 8388608,
"cacheCapacity": "256MB"
}"#;
let storage: Storage = serde_json::from_str(json_data).unwrap();
assert_eq!(
storage.server.ip.unwrap().to_string(),
"128.0.0.1".to_string()
);
assert_eq!(storage.server.tcp_port, 4005);
assert_eq!(storage.server.quic_port, 4006);
assert_eq!(storage.dir, PathBuf::from("/tmp/storage"));
assert!(storage.keep);
assert_eq!(storage.write_piece_timeout, Duration::from_secs(20));
assert_eq!(storage.write_buffer_size, 8 * 1024 * 1024);
assert_eq!(storage.read_buffer_size, 8 * 1024 * 1024);
assert_eq!(storage.cache_capacity, ByteSize::mb(256));
}
#[test]
fn validate_policy() {
let valid_policy = Policy {
task_ttl: Duration::from_secs(12 * 3600),
dist_threshold: ByteSize::mb(100),
dist_high_threshold_percent: 90,
dist_low_threshold_percent: 70,
};
assert!(valid_policy.validate().is_ok());
let invalid_policy = Policy {
task_ttl: Duration::from_secs(12 * 3600),
dist_threshold: ByteSize::mb(100),
dist_high_threshold_percent: 100,
dist_low_threshold_percent: 70,
};
assert!(invalid_policy.validate().is_err());
}
#[test]
fn deserialize_gc_correctly() {
let json_data = r#"
{
"interval": "1h",
"policy": {
"taskTTL": "12h",
"distHighThresholdPercent": 90,
"distLowThresholdPercent": 70
}
}"#;
let gc: GC = serde_json::from_str(json_data).unwrap();
assert_eq!(gc.interval, Duration::from_secs(3600));
assert_eq!(gc.policy.task_ttl, Duration::from_secs(12 * 3600));
assert_eq!(gc.policy.dist_high_threshold_percent, 90);
assert_eq!(gc.policy.dist_low_threshold_percent, 70);
}
#[test]
fn deserialize_proxy_correctly() {
let json_data = r#"
{
"server": {
"port": 8080,
"caCert": "/path/to/ca_cert.pem",
"caKey": "/path/to/ca_key.pem",
"basicAuth": {
"username": "admin",
"password": "password"
}
},
"rules": [
{
"regex": "^https?://example\\.com/.*$",
"useTLS": true,
"redirect": "https://mirror.example.com",
"filteredQueryParams": ["Signature", "Expires"]
}
],
"registryMirror": {
"addr": "https://mirror.example.com",
"cert": "/path/to/cert.pem"
},
"disableBackToSource": true,
"prefetch": true,
"prefetchRateLimit": "1GiB",
"readBufferSize": 8388608
}"#;
let proxy: Proxy = serde_json::from_str(json_data).unwrap();
assert_eq!(proxy.server.port, 8080);
assert_eq!(
proxy.server.ca_cert,
Some(PathBuf::from("/path/to/ca_cert.pem"))
);
assert_eq!(
proxy.server.ca_key,
Some(PathBuf::from("/path/to/ca_key.pem"))
);
assert_eq!(
proxy.server.basic_auth.as_ref().unwrap().username,
"admin".to_string()
);
assert_eq!(
proxy.server.basic_auth.as_ref().unwrap().password,
"password".to_string()
);
let rule = &proxy.rules.as_ref().unwrap()[0];
assert_eq!(rule.regex.as_str(), "^https?://example\\.com/.*$");
assert!(rule.use_tls);
assert_eq!(
rule.redirect,
Some("https://mirror.example.com".to_string())
);
assert_eq!(rule.filtered_query_params, vec!["Signature", "Expires"]);
assert_eq!(proxy.registry_mirror.addr, "https://mirror.example.com");
assert_eq!(
proxy.registry_mirror.cert,
Some(PathBuf::from("/path/to/cert.pem"))
);
assert!(proxy.disable_back_to_source);
assert!(proxy.prefetch);
assert_eq!(proxy.prefetch_rate_limit, ByteSize::gib(1));
assert_eq!(proxy.read_buffer_size, 8 * 1024 * 1024);
}
#[test]
fn deserialize_tracing_correctly() {
let json_data = r#"
{
"protocol": "http",
"endpoint": "tracing.example.com",
"path": "/v1/traces",
"headers": {
"X-Custom-Header": "value"
}
}"#;
let tracing: Tracing = serde_json::from_str(json_data).unwrap();
assert_eq!(tracing.protocol, Some("http".to_string()));
assert_eq!(tracing.endpoint, Some("tracing.example.com".to_string()));
assert_eq!(tracing.path, Some(PathBuf::from("/v1/traces")));
assert!(tracing.headers.contains_key("X-Custom-Header"));
}
#[test]
fn deserialize_metrics_correctly() {
let json_data = r#"
{
"server": {
"port": 4002,
"ip": "127.0.0.1"
}
}"#;
let metrics: Metrics = serde_json::from_str(json_data).unwrap();
assert_eq!(metrics.server.port, 4002);
assert_eq!(
metrics.server.ip,
Some("127.0.0.1".parse::<IpAddr>().unwrap())
);
}
}