198 lines
6.2 KiB
Rust
198 lines
6.2 KiB
Rust
/**
|
|
* The main server entrypoint for Contaminate
|
|
*/
|
|
|
|
extern crate config;
|
|
extern crate pretty_env_logger;
|
|
#[macro_use]
|
|
extern crate serde;
|
|
#[macro_use]
|
|
extern crate serde_json;
|
|
extern crate surf;
|
|
extern crate tide;
|
|
|
|
use async_std::task;
|
|
use log::*;
|
|
use tide::{Request, Response};
|
|
|
|
use std::path::Path;
|
|
|
|
mod models;
|
|
|
|
/**
|
|
* Load the settings based on the hierarchy.
|
|
*
|
|
* First we load the configuration file `contaminate.yml` if it exists
|
|
* Then we look at environment variables.
|
|
*/
|
|
fn load_settings() -> config::Config {
|
|
let mut settings = config::Config::default();
|
|
settings.set_default("registry", "https://registry-1.docker.io")
|
|
.expect("Could not set the default for `registry`");
|
|
settings.set_default("layers_dir", "./layers.d")
|
|
.expect("Could not set the default for `layers_dir`");
|
|
|
|
settings
|
|
.merge(config::File::with_name("contaminate").required(false))
|
|
.expect("Failed to load settings in contaminate.ymll")
|
|
.merge(config::Environment::with_prefix("CT"))
|
|
.expect("Failed to load settings defined by CT_* env vars");
|
|
|
|
debug!("Loaded configuration: {:?}", settings);
|
|
|
|
return settings;
|
|
}
|
|
|
|
/**
|
|
* AppState is a simple struct to carry information into request handlers
|
|
*/
|
|
struct AppState {
|
|
conf: config::Config,
|
|
upstream: String,
|
|
}
|
|
|
|
impl AppState {
|
|
/**
|
|
* This function returns a true if the configured `layers_dir` has an override
|
|
* for the given triplet of org/image:digest
|
|
*
|
|
* For example, if we have a `<layers_dir>/library/alpine/latest/` directory
|
|
* with `*.tar.gz` files within it, then the function would return true.
|
|
*/
|
|
fn override_exists(&self, org: String, image: String, digest: String) -> bool {
|
|
let layers_dir = self.conf.get_str("layers_dir")
|
|
.expect("Unable to access `layers_dir` conf variable");
|
|
|
|
info!("Looking in directory: {}", layers_dir);
|
|
let layers_dir = Path::new(&layers_dir);
|
|
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Proxy the given response to the upstream registry and return the response
|
|
* back to the client request it.
|
|
*/
|
|
async fn proxy_upstream(req: Request<AppState>) -> Response {
|
|
let full_url = format!("{}{}", req.state().upstream, req.uri());
|
|
info!("Proxying request upstream to {}", full_url);
|
|
/*
|
|
* We need to send the Authorization header along as well, otherwise
|
|
* the upstream repository might complain that we're not authorized
|
|
*/
|
|
let token = req.header("Authorization").unwrap_or("");
|
|
let accepts = req.header("Accept").unwrap_or("");
|
|
|
|
let outbound = surf::get(full_url)
|
|
.set_header("Authorization", token)
|
|
.set_header("Accept", accepts);
|
|
|
|
if let Ok(mut u_res) = outbound.await {
|
|
let status = u_res.status().as_u16();
|
|
let body = u_res.body_string().await;
|
|
match body {
|
|
Ok(body) => {
|
|
/*
|
|
* If we don't explicitly set the content type here, the client will think
|
|
* that we're sending back a v1 manifest schema and complain about a "missing
|
|
* signature key"
|
|
*/
|
|
debug!("upstream headers: {:?}", u_res.headers());
|
|
debug!("upstream response for {}:\n{}", req.uri(), body);
|
|
let content_type = u_res.header("Content-Type").unwrap_or("text/plain");
|
|
Response::new(status)
|
|
.set_header("Content-Type", content_type)
|
|
.body_string(body)
|
|
},
|
|
Err(err) => {
|
|
error!("Failed to make upstream request: {:?}", err);
|
|
Response::new(500)
|
|
},
|
|
}
|
|
}
|
|
else {
|
|
error!("Failed to make request upstream to {}", req.uri());
|
|
Response::new(500)
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* This function will fetch and manipulate the upstream manifest, typically
|
|
* located at `/v2/myorg/myimage/manifests/latest`
|
|
*
|
|
* This will return a Response to the client which conforms to the manifest
|
|
* specification.
|
|
*/
|
|
async fn fetch_digest(req: Request<AppState>) -> Response {
|
|
let org: String = req.param("org").unwrap_or("".to_string());
|
|
let image: String = req.param("image").unwrap_or("".to_string());
|
|
let digest: String = req.param("digest").unwrap_or("".to_string());
|
|
if req.state().override_exists(org, image, digest) {
|
|
error!("We should not proxy");
|
|
Response::new(200)
|
|
}
|
|
else {
|
|
error!("We SHOULD proxy");
|
|
Response::new(200)
|
|
}
|
|
}
|
|
|
|
async fn fetch_blob(req: Request<AppState>) -> Response {
|
|
info!("fetch_blob: {}", req.uri());
|
|
Response::new(200)
|
|
}
|
|
|
|
fn main() -> Result<(), std::io::Error> {
|
|
pretty_env_logger::init();
|
|
let conf = load_settings();
|
|
let upstream_url = conf.get_str("registry")
|
|
.expect("`registry` not properly configured, must be a string");
|
|
|
|
info!("Starting with the following upstream: {}", upstream_url);
|
|
|
|
let layers_dir = conf.get_str("layers_dir")
|
|
.expect("`layers_dir` not properly configured, must be a string");
|
|
let layers_dir = Path::new(&layers_dir);
|
|
|
|
if ! layers_dir.is_dir() {
|
|
error!("The `layers_dir` ({}) does not appear to be a directory", layers_dir.display());
|
|
panic!("`layers_dir` must be a directory");
|
|
}
|
|
|
|
let state = AppState {
|
|
conf: conf,
|
|
upstream: upstream_url,
|
|
};
|
|
|
|
task::block_on(async {
|
|
let mut app = tide::with_state(state);
|
|
app.at("/").get(|_| async move { "Hello, world!" });
|
|
/*
|
|
* This route works for "normal" images, which have name of org/image
|
|
*/
|
|
app.at("/v2/:org/:image/manifests/:digest").get(fetch_digest);
|
|
/*
|
|
* This route works handles images which look like "official" images,
|
|
* such as `alpine:latest`, which _actually_ maps to `library/alpine:latest`
|
|
* in DockerHub
|
|
*/
|
|
//app.at("/v2/:image/manifests/:digest").get(fetch_digest);
|
|
app.at("/v2/:org/:image/blobs/:sha").get(fetch_blob);
|
|
/*
|
|
* The catch-all for the remainder of the v2 API calls should proxy to
|
|
* the upstream repository, since Contaminate does not implement a full
|
|
* registry API
|
|
*/
|
|
app.at("/v2/*").get(proxy_upstream);
|
|
app.listen("127.0.0.1:9090").await?;
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
}
|