diff --git a/Cargo.lock b/Cargo.lock index e26b2e4..f221869 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3275,7 +3275,7 @@ dependencies = [ [[package]] name = "trow" -version = "0.5.2" +version = "0.6.0" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 421055c..ad3b7d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trow" -version = "0.5.2" +version = "0.6.0" authors = [] edition = "2021" diff --git a/charts/trow/Chart.yaml b/charts/trow/Chart.yaml index f53dcda..6c43489 100644 --- a/charts/trow/Chart.yaml +++ b/charts/trow/Chart.yaml @@ -3,5 +3,5 @@ name: trow description: Helm chart for Trow registry type: application -version: 0.5.3 -appVersion: 0.5.2 +version: 0.6.0 +appVersion: 0.6.0 diff --git a/charts/trow/values.yaml b/charts/trow/values.yaml index e8d6de3..3f36ced 100644 --- a/charts/trow/values.yaml +++ b/charts/trow/values.yaml @@ -32,14 +32,16 @@ trow: ## Ignore or Fail onWebhookFailure: Ignore config: - - alias: docker - host: registry-1.docker.io - - alias: nvcr - host: https://nvcr.io - - alias: quay - host: quay.io - # - alias: toto - # host: http://toto.land + offline: false + registries: + - alias: docker + host: registry-1.docker.io + - alias: nvcr + host: https://nvcr.io + - alias: quay + host: quay.io + # - alias: toto + # host: http://toto.land ## For more info on log levels see https://docs.rs/tracing-subscriber/0.3.17/tracing_subscriber/filter/struct.EnvFilter.html logLevel: info @@ -60,11 +62,12 @@ service: ingress: enabled: false gke: false - annotations: {} + annotations: + {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - - paths: ['/'] + - paths: ["/"] # use "none" to not set a host (otherwise defaults to trow.domain) host: tls: [] @@ -72,7 +75,8 @@ ingress: # hosts: # - chart-example.local -resources: {} +resources: + {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following @@ -91,7 +95,7 @@ tolerations: [] affinity: {} volumeClaim: - accessModes: [ "ReadWriteOnce" ] + accessModes: ["ReadWriteOnce"] resources: requests: storage: 20Gi diff --git a/src/lib.rs b/src/lib.rs index f7f3410..f0d9d79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use client_interface::ClientInterface; use futures::Future; use thiserror::Error; use tracing::{event, Level}; -use trow_server::{ImageValidationConfig, RegistryProxyConfig}; +use trow_server::{ImageValidationConfig, RegistryProxiesConfig}; use uuid::Uuid; //TODO: Make this take a cause or description @@ -58,7 +58,7 @@ pub struct TrowConfig { tls: Option, grpc: GrpcConfig, service_name: String, - proxy_registry_config: Vec, + proxy_registry_config: Option, image_validation_config: Option, dry_run: bool, token_secret: String, @@ -128,7 +128,7 @@ impl TrowBuilder { tls: None, grpc: GrpcConfig { listen }, service_name, - proxy_registry_config: Vec::new(), + proxy_registry_config: None, image_validation_config: None, dry_run, token_secret: Uuid::new_v4().to_string(), @@ -142,9 +142,9 @@ impl TrowBuilder { let config_file = config_file.as_ref(); let config_str = fs::read_to_string(config_file) .with_context(|| format!("Could not read file `{}`", config_file))?; - let config = serde_yaml::from_str::>(&config_str) + let config = serde_yaml::from_str::(&config_str) .with_context(|| format!("Could not parse file `{}`", config_file))?; - self.config.proxy_registry_config = config; + self.config.proxy_registry_config = Some(config); Ok(self) } @@ -197,9 +197,9 @@ impl TrowBuilder { } None => println!("Image validation webhook not configured"), } - if !self.config.proxy_registry_config.is_empty() { + if let Some(proxy_config) = &self.config.proxy_registry_config { println!("Proxy registries configured:"); - for config in &self.config.proxy_registry_config { + for config in &proxy_config.registries { println!(" - {}: {}", config.alias, config.host); } } else { diff --git a/tests/admission_mutation.rs b/tests/admission_mutation.rs index 72c1500..03a9ef5 100644 --- a/tests/admission_mutation.rs +++ b/tests/admission_mutation.rs @@ -14,7 +14,7 @@ mod admission_mutation_tests { use k8s_openapi::api::core::v1::Pod; use kube::core::admission::AdmissionReview; use reqwest::StatusCode; - use trow_server::RegistryProxyConfig; + use trow_server::{RegistryProxiesConfig, SingleRegistryProxyConfig}; use crate::common; @@ -29,20 +29,23 @@ mod admission_mutation_tests { /// Call out to cargo to start trow. /// Seriously considering moving to docker run. async fn start_trow() -> TrowInstance { - let config_file = common::get_file(vec![ - RegistryProxyConfig { - alias: "docker".to_string(), - host: "registry-1.docker.io".to_string(), - username: None, - password: None, - }, - RegistryProxyConfig { - alias: "ecr".to_string(), - host: "1234.dkr.ecr.saturn-5.amazonaws.com".to_string(), - username: Some("AWS".to_string()), - password: None, - }, - ]); + let config_file = common::get_file(RegistryProxiesConfig { + offline: false, + registries: vec![ + SingleRegistryProxyConfig { + alias: "docker".to_string(), + host: "registry-1.docker.io".to_string(), + username: None, + password: None, + }, + SingleRegistryProxyConfig { + alias: "ecr".to_string(), + host: "1234.dkr.ecr.saturn-5.amazonaws.com".to_string(), + username: Some("AWS".to_string()), + password: None, + }, + ], + }); let mut child = Command::new("cargo") .arg("run") diff --git a/tests/cli.rs b/tests/cli.rs index 6252b6c..fe97c0f 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -4,7 +4,7 @@ mod common; #[cfg(test)] mod cli { use predicates::prelude::*; - use trow_server::{ImageValidationConfig, RegistryProxyConfig}; + use trow_server::{ImageValidationConfig, RegistryProxiesConfig, SingleRegistryProxyConfig}; use crate::common::get_file; @@ -101,20 +101,23 @@ mod cli { "Image validation webhook not configured", )); - let file = get_file::>(vec![ - RegistryProxyConfig { - alias: "lovni".to_string(), - host: "jul.example.com".to_string(), - username: Some("robert".to_string()), - password: Some("1234".to_string()), - }, - RegistryProxyConfig { - alias: "trow".to_string(), - host: "127.0.0.1".to_string(), - username: None, - password: None, - }, - ]); + let file = get_file(RegistryProxiesConfig { + offline: true, + registries: vec![ + SingleRegistryProxyConfig { + alias: "lovni".to_string(), + host: "jul.example.com".to_string(), + username: Some("robert".to_string()), + password: Some("1234".to_string()), + }, + SingleRegistryProxyConfig { + alias: "trow".to_string(), + host: "127.0.0.1".to_string(), + username: None, + password: None, + }, + ], + }); get_command() .args([ diff --git a/tests/proxy.rs b/tests/proxy.rs index 6d44bb2..a85753e 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -10,7 +10,7 @@ mod interface_tests { use environment::Environment; use reqwest::StatusCode; - use trow_server::{manifest, RegistryProxyConfig}; + use trow_server::{manifest, RegistryProxiesConfig, SingleRegistryProxyConfig}; use crate::common; @@ -22,26 +22,29 @@ mod interface_tests { } async fn start_trow() -> TrowInstance { - let config_file = common::get_file(vec![ - RegistryProxyConfig { - alias: "docker".to_string(), - host: "registry-1.docker.io".to_string(), - username: None, - password: None, - }, - RegistryProxyConfig { - alias: "nvcr".to_string(), - host: "nvcr.io".to_string(), - username: None, - password: None, - }, - RegistryProxyConfig { - alias: "quay".to_string(), - host: "quay.io".to_string(), - username: None, - password: None, - }, - ]); + let config_file = common::get_file(RegistryProxiesConfig { + offline: false, + registries: vec![ + SingleRegistryProxyConfig { + alias: "docker".to_string(), + host: "registry-1.docker.io".to_string(), + username: None, + password: None, + }, + SingleRegistryProxyConfig { + alias: "nvcr".to_string(), + host: "nvcr.io".to_string(), + username: None, + password: None, + }, + SingleRegistryProxyConfig { + alias: "quay".to_string(), + host: "quay.io".to_string(), + username: None, + password: None, + }, + ], + }); let mut child = Command::new("cargo") .arg("run") diff --git a/trow-server/src/admission.rs b/trow-server/src/admission.rs index d8e213d..e2b704d 100644 --- a/trow-server/src/admission.rs +++ b/trow-server/src/admission.rs @@ -97,6 +97,14 @@ impl AdmissionController for TrowServer { ) -> Result, Status> { let ar = ar.into_inner(); let mut patch_operations = Vec::::new(); + let proxy_config = match self.proxy_registry_config.as_ref() { + Some(s) => s, + None => { + return Err(Status::internal( + "Proxy registry config not set, cannot mutate image references", + )) + } + }; for (raw_image, image_path) in ar.images.iter().zip(ar.image_paths.iter()) { let image = match RemoteImage::try_from_str(raw_image) { @@ -104,7 +112,7 @@ impl AdmissionController for TrowServer { Err(_) => continue, }; - for cfg in self.proxy_registry_config.iter() { + for cfg in proxy_config.registries.iter() { if image.get_host() == cfg.host { event!( Level::INFO, diff --git a/trow-server/src/lib.rs b/trow-server/src/lib.rs index 6e0cdbe..4a43fda 100644 --- a/trow-server/src/lib.rs +++ b/trow-server/src/lib.rs @@ -10,7 +10,7 @@ mod temporary_file; use std::future::Future; pub use admission::ImageValidationConfig; -pub use proxy_auth::RegistryProxyConfig; +pub use proxy_auth::{RegistryProxiesConfig, SingleRegistryProxyConfig}; use server::trow_server::admission_controller_server::AdmissionControllerServer; use server::trow_server::registry_server::RegistryServer; use server::TrowServer; @@ -19,7 +19,7 @@ use tonic::transport::Server; pub struct TrowServerBuilder { data_path: String, listen_addr: std::net::SocketAddr, - proxy_registry_config: Vec, + proxy_registry_config: Option, image_validation_config: Option, tls_cert: Option>, tls_key: Option>, @@ -29,7 +29,7 @@ pub struct TrowServerBuilder { pub fn build_server( data_path: &str, listen_addr: std::net::SocketAddr, - proxy_registry_config: Vec, + proxy_registry_config: Option, image_validation_config: Option, ) -> TrowServerBuilder { TrowServerBuilder { diff --git a/trow-server/src/proxy_auth.rs b/trow-server/src/proxy_auth.rs index 1e1e045..88e3ae9 100644 --- a/trow-server/src/proxy_auth.rs +++ b/trow-server/src/proxy_auth.rs @@ -25,7 +25,14 @@ pub enum HttpAuth { } #[derive(Serialize, Deserialize, Clone, Debug)] -pub struct RegistryProxyConfig { +pub struct RegistryProxiesConfig { + pub registries: Vec, + #[serde(default)] + pub offline: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SingleRegistryProxyConfig { pub alias: String, /// This field is unvalidated and may contain a scheme or not. /// eg: `http://example.com` and `example.com` @@ -43,7 +50,7 @@ pub struct ProxyClient { impl ProxyClient { pub async fn try_new( - mut proxy_cfg: RegistryProxyConfig, + mut proxy_cfg: SingleRegistryProxyConfig, proxy_image: &RemoteImage, ) -> Result { let base_client = reqwest::ClientBuilder::new() @@ -83,7 +90,7 @@ impl ProxyClient { } async fn try_new_with_basic_auth( - proxy_cfg: &RegistryProxyConfig, + proxy_cfg: &SingleRegistryProxyConfig, cl: reqwest::Client, ) -> Result { if proxy_cfg.username.is_none() { @@ -102,7 +109,7 @@ impl ProxyClient { } async fn try_new_with_bearer_auth( - proxy_cfg: &RegistryProxyConfig, + proxy_cfg: &SingleRegistryProxyConfig, cl: reqwest::Client, authn_header: &str, ) -> Result { @@ -218,7 +225,7 @@ fn get_bearer_param_map(www_authenticate_header: &str) -> HashMap Result { let mut bearer_param_map = get_bearer_param_map(www_authenticate_header); event!(Level::DEBUG, "bearer param map: {:?}", bearer_param_map); @@ -270,10 +277,10 @@ mod tests { const AUTHZ_HEADER: &str = "Authorization"; - fn get_basic_setup() -> (MockServer, RegistryProxyConfig, RemoteImage) { + fn get_basic_setup() -> (MockServer, SingleRegistryProxyConfig, RemoteImage) { let server = MockServer::start(); - let proxy_cfg = RegistryProxyConfig { + let proxy_cfg = SingleRegistryProxyConfig { host: format!("http://{}", server.address()), alias: "toto".to_string(), username: None, diff --git a/trow-server/src/server.rs b/trow-server/src/server.rs index e74ff60..8f35bd6 100644 --- a/trow-server/src/server.rs +++ b/trow-server/src/server.rs @@ -25,10 +25,10 @@ use self::trow_server::*; use crate::digest::sha256_tag_digest; use crate::image::RemoteImage; use crate::manifest::{manifest_media_type, FromJson, Manifest}; -use crate::proxy_auth::ProxyClient; +use crate::proxy_auth::{ProxyClient, SingleRegistryProxyConfig}; use crate::server::trow_server::registry_server::Registry; use crate::temporary_file::TemporaryFile; -use crate::{metrics, ImageValidationConfig, RegistryProxyConfig}; +use crate::{metrics, ImageValidationConfig, RegistryProxiesConfig}; pub mod trow_server { include!("../../trow-protobuf/out/trow.rs"); @@ -58,7 +58,7 @@ pub struct TrowServer { manifests_path: PathBuf, blobs_path: PathBuf, scratch_path: PathBuf, - pub proxy_registry_config: Vec, + pub proxy_registry_config: Option, pub image_validation_config: Option, } @@ -229,7 +229,7 @@ fn get_digest_from_manifest_path>(path: P) -> Result { impl TrowServer { pub fn new( data_path: &str, - proxy_registry_config: Vec, + proxy_registry_config: Option, image_validation_config: Option, ) -> Result { let manifests_path = create_path(data_path, MANIFESTS_DIR)?; @@ -338,15 +338,17 @@ impl TrowServer { &self, repo_name: &str, reference: &str, - ) -> Option<(RemoteImage, RegistryProxyConfig)> { - //All proxies are under "f_" - if repo_name.starts_with(PROXY_DIR) { + ) -> Option<(RemoteImage, SingleRegistryProxyConfig)> { + // All proxies are under "f_" + if repo_name.starts_with(PROXY_DIR) && self.proxy_registry_config.is_some() { + let proxy_config = self.proxy_registry_config.as_ref().unwrap(); + let segments = repo_name.splitn(3, '/').collect::>(); debug_assert_eq!(segments[0], "f"); let proxy_alias = segments[1].to_string(); let repo = segments[2].to_string(); - for proxy in self.proxy_registry_config.iter() { + for proxy in proxy_config.registries.iter() { if proxy.alias == proxy_alias { let image = RemoteImage::new(&proxy.host, repo, reference.into()); return Some((image, proxy.clone())); @@ -496,7 +498,7 @@ impl TrowServer { async fn download_remote_image( &self, remote_image: RemoteImage, - proxy_cfg: RegistryProxyConfig, + proxy_cfg: SingleRegistryProxyConfig, ) -> Result { // Replace eg f/docker/alpine by f/docker/library/alpine let repo_name = format!("f/{}/{}", proxy_cfg.alias, remote_image.get_repo()); @@ -602,12 +604,16 @@ impl TrowServer { reference, remote_image ); - let digest = self.download_remote_image(remote_image, proxy_cfg).await?; // These are not up to date and should not be used ! drop(repo_name); drop(reference); - - self.get_catalog_path_for_blob(&digest)? + if self.proxy_registry_config.as_ref().unwrap().offline { + let repo_name = format!("f/{}/{}", proxy_cfg.alias, remote_image.get_repo()); + self.get_path_for_manifest(&repo_name, &remote_image.reference)? + } else { + let digest = self.download_remote_image(remote_image, proxy_cfg).await?; + self.get_catalog_path_for_blob(&digest)? + } } else { self.get_path_for_manifest(&repo_name, &reference)? };