diff --git a/Cargo.lock b/Cargo.lock index bbd2410..e9f097f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,8 +63,10 @@ name = "autodok" version = "0.1.0" dependencies = [ "axum", + "base64 0.22.0", "bollard", "ctrlc", + "dotenv", "futures", "futures-core", "futures-util", @@ -155,6 +157,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bitflags" version = "1.3.2" @@ -173,7 +181,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f03db470b3c0213c47e978da93200259a1eb4dae2e5512cba9955e2b540a6fc6" dependencies = [ - "base64", + "base64 0.21.7", "bollard-stubs", "bytes", "futures-core", @@ -270,6 +278,12 @@ dependencies = [ "serde", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "equivalent" version = "1.0.1" @@ -1080,7 +1094,7 @@ version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" dependencies = [ - "base64", + "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", diff --git a/Cargo.toml b/Cargo.toml index bd6cebd..ddef099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,5 @@ lazy_static = "1.4.0" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing = "0.1.40" tower-http = { version = "0.5.2", features = ["trace"] } +base64 = "0.22.0" +dotenv = "0.15.0" diff --git a/README.md b/README.md index 03ba8d1..879df2c 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,6 @@ The following fields are supported in the request body: |------------------------|------------|-------------------------------------------------|-------------------------------------| | container | foo | yes | Name of a **running** container | | image | foo:latest | yes | Name and tag of an existing image | -| registry.serveraddress | ghcr.io | if the image is pushed to a private registry | Domain of a custom docker registry | -| registry.username | cars10 | if the image is pushed to a private registry | Username for custom docker registry | -| registry.password | foobar | if the image is pushed to a private registry | Password for custom docker registry | ### Examples @@ -100,17 +97,6 @@ curl -X POST "http://autodok.example.com/update" \ {"message":"Container 'elasticsearch' restarted with new image 'docker.elastic.co/elasticsearch/elasticsearch:8.8.0'"} ``` -Update container `secret` with new image from private registry `ghcr.io/nsa/secret:42` -```bash -curl -X POST "http://autodok.example.com/update" \ - -H "Authorization: $API_KEY" \ - -H "content-type: application/json" \ - -d '{"container": "secret", "image": "ghcr.io/nsa/secret:42", "registry": {"serveraddress": "ghcr.io", "username": "$USERNAME", "password": "$PASSWORD"}}' - -# response: -{"message":"Container 'secret' restarted with new image 'ghcr.io/nsa/secret:42'"} -``` - ## FAQ & possible issues ### Request timeouts @@ -119,7 +105,7 @@ Requests to `POST /update` might timeout depending on your setup. Please keep in ### Using a private registry -You can download images from private registries by providing the optional registry field in the request body: `"registry": {"serveraddress": "", "username": "", "password": ""}` +You can download images from private registries by providing a standard docker `config.json`. Uncomment the volume in `compose.yml` and adjust the path to your `config.json`. *autodok* will then use your existing credentials whenever needed. ### Restarting multiple containers at once diff --git a/compose.yml b/compose.yml index 395b352..dc85c59 100644 --- a/compose.yml +++ b/compose.yml @@ -8,6 +8,7 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock - ./data:/data + #- /home/user/.docker/config.json:/config.json healthcheck: test: curl --fail http://localhost:3000/health || exit 1 interval: 30s diff --git a/src/credentials.rs b/src/credentials.rs new file mode 100644 index 0000000..c3d4f59 --- /dev/null +++ b/src/credentials.rs @@ -0,0 +1,78 @@ +use base64::prelude::*; +use bollard::auth::DockerCredentials; +use serde::Deserialize; +use std::{collections::HashMap, env, fs}; + +#[derive(Debug)] +struct Repository { + registry: String, + _image: String, +} + +#[derive(Debug, Deserialize)] +struct DockerConfig { + auths: HashMap, +} + +#[derive(Debug, Deserialize)] +struct DockerConfigAuth { + auth: String, +} + +impl DockerConfig { + fn get(&self, registry: &str) -> Option<(String, String)> { + let encoded = self.auths.get(registry)?; + let decoded = BASE64_STANDARD.decode(&encoded.auth).ok()?; + let decoded_str = String::from_utf8(decoded).ok()?; + + decoded_str + .split_once(':') + .map(|(username, password)| (username.to_string(), password.to_string())) + } +} + +static DEFAULT_REGISTRY: &str = "docker.io"; + +pub fn registry_credentials(image: &str) -> Option { + let repository = parse_repository(image); + let config = docker_config()?; + + config + .get(&repository.registry) + .map(|(username, password)| DockerCredentials { + username: Some(username), + password: Some(password), + serveraddress: Some(repository.registry), + ..Default::default() + }) +} + +fn parse_repository(name: &str) -> Repository { + let i = name.find('/'); + match i { + Some(index) => { + if !name[..index].contains(&['.', ':'][..]) && &name[..index] != "localhost" { + Repository { + registry: DEFAULT_REGISTRY.to_string(), + _image: name.to_string(), + } + } else { + Repository { + registry: name[..index].to_string(), + _image: name[index + 1..].to_string(), + } + } + } + None => Repository { + registry: DEFAULT_REGISTRY.to_string(), + _image: name.to_string(), + }, + } +} + +fn docker_config() -> Option { + let path = env::var("DOCKER_CONFIG").unwrap_or("/config.json".to_string()); + let raw = fs::read_to_string(path).ok()?; + + serde_json::from_str(&raw).unwrap() +} diff --git a/src/docker.rs b/src/docker.rs index bbe7259..c39c1d4 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -1,6 +1,5 @@ use crate::error::AutodokError; use bollard::{ - auth::DockerCredentials, container::{Config, CreateContainerOptions, NetworkingConfig, StartContainerOptions}, image::CreateImageOptions, models::ContainerConfig, @@ -11,19 +10,14 @@ use bollard::{ use futures_util::stream::StreamExt; use log::debug; use std::collections::HashMap; -use std::env; -pub async fn pull_image( - docker: &Docker, - image: String, - credentials: Option, -) -> Result<(), AutodokError> { +pub async fn pull_image(docker: &Docker, image: String) -> Result<(), AutodokError> { let options = Some(CreateImageOptions { - from_image: image, + from_image: image.clone(), ..Default::default() }); - let credentials = credentials.or_else(default_credentials); + let credentials = crate::credentials::registry_credentials(&image); let mut stream = docker.create_image(options, None, credentials); while let Some(res) = stream.next().await { @@ -33,15 +27,6 @@ pub async fn pull_image( Ok(()) } -pub fn default_credentials() -> Option { - Some(DockerCredentials { - serveraddress: env::var("DEFAULT_REGISTRY_ADDRESS").ok(), - username: env::var("DEFAULT_REGISTRY_USERNAME").ok(), - password: env::var("DEFAULT_REGISTRY_PASSWORD").ok(), - ..Default::default() - }) -} - pub async fn stop_start_container( docker: &Docker, container: String, diff --git a/src/main.rs b/src/main.rs index 16b0a1d..8f9a0b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use tower_http::trace::TraceLayer; use tracing::{info_span, Span}; mod api_key; +mod credentials; mod docker; mod error; mod random; @@ -27,6 +28,8 @@ lazy_static! { #[tokio::main] async fn main() { + dotenv::dotenv().ok(); + let format = tracing_subscriber::fmt::format().with_target(false); tracing_subscriber::fmt().event_format(format).init(); diff --git a/src/routes.rs b/src/routes.rs index ca04e86..e637e4f 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -3,7 +3,7 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; -use bollard::{auth::DockerCredentials, Docker}; +use bollard::Docker; use log::info; use serde::{Deserialize, Serialize}; @@ -13,7 +13,6 @@ use crate::error::AutodokError; pub struct UpdateContainerImage { container: String, image: String, - registry: Option, } #[derive(Debug, Serialize)] @@ -32,7 +31,7 @@ pub async fn update_image( info!(" Container '{container}' found."); info!(" Pulling image '{image}'..."); - crate::docker::pull_image(&docker, image.clone(), payload.registry).await?; + crate::docker::pull_image(&docker, image.clone()).await?; info!(" Image pull done."); info!(" Restarting container...");