diff --git a/Cargo.toml b/Cargo.toml index 2be1fd8..eda2043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ required-features = ["editor"] [package.metadata.deb] name = "acr-mirror" -depends = "jq, libssl-dev" +depends = "jq" maintainer-scripts = "lib/debian" assets = [ [ diff --git a/src/bin/shared/cli.rs b/src/bin/shared/cli.rs index 36352e5..949abbd 100644 --- a/src/bin/shared/cli.rs +++ b/src/bin/shared/cli.rs @@ -103,7 +103,7 @@ pub struct MirrorSettings { /// If initializing settings, only initialize the hosts.toml file /// #[clap(long, action)] - pub hosts_config_only: bool, + pub init_hosts_config_only: bool, /// Root of the current filesystem, /// /// This is usually just `/` however when testing it's useful to specify since root is a privelaged folder. diff --git a/src/bin/shared/commands.rs b/src/bin/shared/commands.rs index 6434c9f..3a9c272 100644 --- a/src/bin/shared/commands.rs +++ b/src/bin/shared/commands.rs @@ -114,7 +114,7 @@ impl Commands { teleport_format, registry_host, fs_root, - hosts_config_only, + init_hosts_config_only: hosts_config_only, .. }) => { if mirror_runmd.exists() { diff --git a/src/config/hosts_config.rs b/src/config/hosts_config.rs index 55c39ad..8ef7e8b 100644 --- a/src/config/hosts_config.rs +++ b/src/config/hosts_config.rs @@ -2,12 +2,13 @@ use logos::Logos; use std::{ collections::{BTreeMap, BTreeSet}, fmt::Display, - path::PathBuf, io::Write + io::Write, + path::PathBuf, }; use tracing::error; /// Folder name of the default hosts_config folder used by containerd, -/// +/// const HOSTS_CONFIG_FOLDER: &'static str = "etc/containerd/certs.d"; /// Struct for creating a hosts.toml file for containerd hosts configuration, @@ -34,7 +35,7 @@ impl HostsConfig { } /// Enables legacy support for containerd version under 1.7 - /// + /// pub fn enable_legacy_support(mut self) -> Self { self.legacy_support = true; self @@ -48,16 +49,9 @@ impl HostsConfig { } /// Serializes and writes the current config to - /// + /// pub fn install(&self, root_dir: Option>) -> Result { - let path = root_dir.map(|r| r.into()).unwrap_or(PathBuf::from("/")); - let path = path.join(HOSTS_CONFIG_FOLDER); - - let path = if let Some(server) = self.server.as_ref() { - path.join(server) - } else { - path.join("_default") - }; + let path = self.install_dir(root_dir); std::fs::create_dir_all(&path)?; @@ -71,6 +65,49 @@ impl HostsConfig { Ok(path) } + + /// Uninstalls the config, + /// + pub fn uninstall(&self, root_dir: Option>) -> Result<(), std::io::Error> { + let path = self.install_dir(root_dir); + + std::fs::create_dir_all(&path)?; + + let path = path.join("hosts.toml"); + + if !path.exists() { + return Ok(()); + } else { + std::fs::remove_file(path)?; + } + + Ok(()) + } + + /// Returns true if the config is installed, + /// + pub fn installed(&self, root_dir: Option>) -> bool { + self.install_dir(root_dir) + .canonicalize() + .ok() + .map(|p| p.join("hosts.toml").exists()) + .unwrap_or_default() + } + + /// Returns the path to the config, + /// + fn install_dir(&self, root_dir: Option>) -> PathBuf { + let path = root_dir.map(|r| r.into()).unwrap_or(PathBuf::from("/")); + let path = path.join(HOSTS_CONFIG_FOLDER); + + let path = if let Some(server) = self.server.as_ref() { + path.join(server) + } else { + path.join("_default") + }; + + path + } } impl Display for HostsConfig { @@ -306,9 +343,11 @@ server = "https://test.azurecr.io" .trim_start(), format!("{}", host_config) ); - let location = host_config.install(Some(".test")).expect("should be able to install"); + let location = host_config + .install(Some(".test")) + .expect("should be able to install"); eprintln!("{:?}", location); - + // Test w/o server= let host_config = HostsConfig::new(None::); let host_config = host_config.add_host( @@ -334,7 +373,9 @@ server = "https://test.azurecr.io" format!("{}", host_config) ); - let location = host_config.install(Some(".test")).expect("should be able to install"); + let location = host_config + .install(Some(".test")) + .expect("should be able to install"); eprintln!("{:?}", location); } diff --git a/src/error.rs b/src/error.rs index 6cc98c6..b7568d2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -87,6 +87,15 @@ impl Error { } } + /// Returns true if the category is an invalid operation, + /// + pub fn is_invalid_operation(&self) -> bool { + match self.category { + ErrorCategory::InvalidOperation(_) => true, + _ => false, + } + } + /// Returns a composite error, /// pub fn also(&self, other: Self) -> Self { diff --git a/src/proxy.rs b/src/proxy.rs index 0d4bcce..41987e4 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,3 +1,15 @@ +use crate::default_access_provider; +use crate::Artifact; +use crate::ArtifactManifest; +use crate::Authenticate; +use crate::Descriptor; +use crate::Discover; +use crate::ImageIndex; +use crate::ImageManifest; +use crate::Login; +use crate::Mirror; +use crate::Resolve; +use crate::Teleport; use lifec::prelude::AttributeParser; use lifec::prelude::Block; use lifec::prelude::Host; @@ -13,29 +25,17 @@ use lifec::project::default_world; use lifec::project::Project; use lifec::runtime::Runtime; use lifec_poem::WebApp; -use poem::Route; -use poem::EndpointExt; -use poem::web::Data; -use poem::handler; use poem::get; +use poem::handler; +use poem::web::Data; +use poem::EndpointExt; +use poem::Route; use specs::WorldExt; use std::sync::Arc; -use crate::Teleport; -use crate::Resolve; -use crate::Mirror; -use crate::Login; -use crate::ImageManifest; -use crate::ImageIndex; -use crate::Discover; -use crate::Descriptor; -use crate::Authenticate; -use crate::ArtifactManifest; -use crate::Artifact; -use crate::default_access_provider; mod proxy_target; -pub use proxy_target::ProxyTarget; pub use proxy_target::Object; +pub use proxy_target::ProxyTarget; mod manifests; pub use manifests::Manifests; @@ -54,6 +54,9 @@ mod auth; use auth::handle_auth; pub use auth::OAuthToken; +mod config; +use config::handle_config; + /// Struct for creating a customizable registry proxy, /// /// This proxy is a server that intercepts registry requests intended for upstream registries, @@ -191,6 +194,12 @@ impl WebApp for RegistryProxy { .data(self.context.clone()) .data(default_access_provider(file_provider)), ) + .at( + "/config", + get(handle_config.data(self.context.clone())) + .put(handle_config.data(self.context.clone())) + .delete(handle_config.data(self.context.clone())), + ) .nest("/v2", route) } else { panic!("Cannot start w/o config") @@ -626,6 +635,12 @@ mod tests { resp.assert_status(StatusCode::SERVICE_UNAVAILABLE); }); + let test_5 = cli.clone(); + tokio::spawn(async move { + let resp = test_5.get("/config?ns=tenant.registry.io").send().await; + resp.assert_status(StatusCode::SERVICE_UNAVAILABLE); + }); + // It's important that all requests start before this line, otherwise the host will exit immediately b/c there will be no operations pending host.async_wait_for_exit( Some(Instant::now() + Duration::from_millis(100)), diff --git a/src/proxy/config.rs b/src/proxy/config.rs new file mode 100644 index 0000000..cbc17d8 --- /dev/null +++ b/src/proxy/config.rs @@ -0,0 +1,164 @@ +// Imports +use hyper::Method; +use lifec::state::AttributeIndex; +use lifec::prelude::ThunkContext; +use poem::IntoResponse; +use poem::web::Query; +use poem::web::Data; +use poem::handler; +use poem::error::IntoResult; +use serde::Serialize; +use serde::Deserialize; +use tracing::info; +use tracing::error; +use tracing::debug; +use crate::Error; +use crate::hosts_config::MirrorHost; + +// Exports +mod config_response; +pub use config_response::ConfigResponse; + +/// Struct for query parameters related to mirror config, +/// +#[derive(Serialize, Deserialize)] +pub struct ConfigRequest { + /// Namespace of the registry, + /// + ns: String, + /// Stream format to configure, + /// + stream_format: Option, +} + +/// Handler for /config requests +/// +#[handler] +pub async fn handle_config( + method: Method, + query: Query, + context: Data<&ThunkContext>, +) -> Result { + _handle_config(method, query, context).await +} + +/// Handler impl, seperated to test +/// +async fn _handle_config( + method: Method, + Query(ConfigRequest { ns, stream_format }): Query, + context: Data<&ThunkContext>, +) -> Result { + let app_host = context + .search() + .find_symbol("app_host") + .unwrap_or("http://localhost:8578".to_string()); + + let mirror_hosts_config = MirrorHost::get_hosts_config(&ns, app_host, true, stream_format); + + match method { + Method::GET => { + if mirror_hosts_config.installed(context.search().find_symbol("sysroot")) { + Ok(ConfigResponse::ok()) + } else { + Err(Error::recoverable_error("config is not installed")) + } + } + Method::PUT => { + info!("Configuring namespace {ns}"); + + if let Err(err) = mirror_hosts_config.install(context.search().find_symbol("sysroot")) { + error!("Unable to enable mirror host config for, {}, {:?}", ns, err); + Err(Error::system_environment()) + } else { + debug!("Enabled mirror host config for {}", ns); + Ok(ConfigResponse::ok()) + } + } + Method::DELETE => { + info!("Deleting config for namespace {ns}"); + if let Err(err) = mirror_hosts_config.uninstall(context.search().find_symbol("sysroot")) { + error!("Unable to enable mirror host config for, {}, {:?}", ns, err); + Err(Error::system_environment()) + } else { + debug!("Enabled mirror host config for {}", ns); + Ok(ConfigResponse::ok()) + } + } + _ => Err(Error::invalid_operation("unsupported method")), + } +} + +impl IntoResult for Result { + fn into_result(self) -> poem::Result { + match self { + Ok(resp) => Ok(resp), + Err(err) => { + let resp = ConfigResponse::error(err); + let resp = resp.into_response(); + + Err(poem::Error::from_response(resp)) + } + } + } +} + +#[allow(unused_imports)] +mod tests { + use hyper::Method; + use lifec::prelude::ThunkContext; + use lifec::state::AttributeIndex; + use poem::web::Data; + use poem::web::Query; + use poem::Endpoint; + + use crate::proxy::config::{ConfigRequest, _handle_config}; + + #[tokio::test] + async fn test_handler() { + let _ = _handle_config( + Method::GET, + Query(ConfigRequest { + ns: String::from("test.azurecr.io"), + stream_format: None, + }), + Data( + &ThunkContext::default() + .with_symbol("app_host", "test") + .with_symbol("sysroot", ".test_handle_config"), + ), + ) + .await + .expect_err("should return an error"); + + let _ = _handle_config( + Method::PUT, + Query(ConfigRequest { + ns: String::from("test.azurecr.io"), + stream_format: None, + }), + Data( + &ThunkContext::default() + .with_symbol("app_host", "test") + .with_symbol("sysroot", ".test_handle_config"), + ), + ) + .await + .expect("should put a config"); + + let _ = _handle_config( + Method::DELETE, + Query(ConfigRequest { + ns: String::from("test.azurecr.io"), + stream_format: None, + }), + Data( + &ThunkContext::default() + .with_symbol("app_host", "test") + .with_symbol("sysroot", ".test_handle_config"), + ), + ) + .await + .expect("should put a config"); + } +} diff --git a/src/proxy/config/config_response.rs b/src/proxy/config/config_response.rs new file mode 100644 index 0000000..f2018d4 --- /dev/null +++ b/src/proxy/config/config_response.rs @@ -0,0 +1,57 @@ +use hyper::StatusCode; +use poem::IntoResponse; + +use crate::Error; + +/// Struct to return in response to /config +/// +#[derive(Debug)] +pub struct ConfigResponse { + /// True if the config was installed, + /// + installed: bool, + /// Error + /// + error: Option, +} + +impl ConfigResponse { + /// Creates a new ok response, + /// + pub fn ok() -> Self { + ConfigResponse { installed: true, error: None } + } + + /// Creates a new ok uninstalled response, + /// + pub fn ok_uninstalled() -> Self { + ConfigResponse { installed: false, error: None } + } + + /// Creates a new error response + /// + pub fn error(error: Error) -> Self { + ConfigResponse { installed: false, error: Some(error) } + } +} + +impl IntoResponse for ConfigResponse { + fn into_response(self) -> poem::Response { + let response = poem::Response::builder() + .status(if self.installed && self.error.is_none() { + StatusCode::OK + } else if self.error.as_ref().map(|e| e.is_invalid_operation()).unwrap_or_default() { + StatusCode::METHOD_NOT_ALLOWED + } else if self.error.as_ref().map(|e| e.is_recoverable()).unwrap_or_default() { + StatusCode::NOT_FOUND + } else { + StatusCode::INTERNAL_SERVER_ERROR + }); + + if let Some(error) = self.error.as_ref() { + response.body(format!("{error}")) + } else { + response.finish() + } + } +} \ No newline at end of file