Skip to content

Commit

Permalink
improve private reg support
Browse files Browse the repository at this point in the history
  • Loading branch information
cars10 committed Mar 2, 2024
1 parent c0975e4 commit 835cedf
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 38 deletions.
18 changes: 16 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
16 changes: 1 addition & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions src/credentials.rs
Original file line number Diff line number Diff line change
@@ -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<String, DockerConfigAuth>,
}

#[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<DockerCredentials> {
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<DockerConfig> {
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()
}
21 changes: 3 additions & 18 deletions src/docker.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::error::AutodokError;
use bollard::{
auth::DockerCredentials,
container::{Config, CreateContainerOptions, NetworkingConfig, StartContainerOptions},
image::CreateImageOptions,
models::ContainerConfig,
Expand All @@ -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<DockerCredentials>,
) -> 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 {
Expand All @@ -33,15 +27,6 @@ pub async fn pull_image(
Ok(())
}

pub fn default_credentials() -> Option<DockerCredentials> {
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,
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand Down
5 changes: 2 additions & 3 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -13,7 +13,6 @@ use crate::error::AutodokError;
pub struct UpdateContainerImage {
container: String,
image: String,
registry: Option<DockerCredentials>,
}

#[derive(Debug, Serialize)]
Expand All @@ -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...");
Expand Down

0 comments on commit 835cedf

Please sign in to comment.