create dockerfile, create more configuration options

This commit is contained in:
SeanOMik 2023-06-16 01:15:12 -04:00
parent b9e41f6d6e
commit 0546b71c5e
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
10 changed files with 115 additions and 23 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
/target
.env
.vscode
test.db
/registry
config.toml

1
.gitignore vendored
View File

@ -1,6 +1,5 @@
/target /target
.env .env
.vscode .vscode
test.db
/registry /registry
config.toml config.toml

45
Dockerfile Normal file
View File

@ -0,0 +1,45 @@
FROM rust:alpine3.14 as builder
# update packages
RUN apk update
RUN apk add build-base openssl-dev ca-certificates
# create root application folder
WORKDIR /app
COPY ./ /app/src
# Install rust toolchains
RUN rustup toolchain install stable
RUN rustup default stable
WORKDIR /app/src
# Build dependencies only. Separate these for caches
RUN cargo install cargo-build-deps
RUN cargo build-deps --release
# Build the release executable.
RUN cargo build --release
# Runner stage. I tried using distroless (gcr.io/distroless/static-debian11), but the image was only ~3MBs smaller than
# alpine. I chose to use alpine since a user can easily be added to the image.
FROM alpine:3.17
ARG UNAME=orca-registry
ARG UID=1000
ARG GID=1000
# Add user and copy the executable from the build stage.
RUN adduser --disabled-password --gecos "" $UNAME -s -G $GID -u $UID
COPY --from=builder --chown=$UID:$GID /app/src/target/release/orca-registry /app/orca-registry
RUN chown -R $UID:$GID /app
USER $UNAME
WORKDIR /app/
EXPOSE 3000
ENTRYPOINT [ "/app/orca-registry" ]

View File

@ -2,16 +2,24 @@ listen_address = "127.0.0.1"
listen_port = "3000" listen_port = "3000"
url = "http://localhost:3000/" url = "http://localhost:3000/"
[ldap] [storage]
connection_url = "ldap://localhost:389" driver = "filesystem"
bind_dn = "cn=admin,dc=planetexpress,dc=com" path = "/app/blobs"
bind_password = "GoodNewsEveryone"
user_base_dn = "ou=people,dc=planetexpress,dc=com"
group_base_dn = "ou=people,dc=planetexpress,dc=com"
user_search_filter = "(&(objectClass=person)(mail=%s))" [database]
group_search_filter = "(&(objectclass=groupOfNames)(member=%d))" type = "sqlite"
path = "/app/orca.db"
admin_filter = "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)" #[ldap]
#connection_url = "ldap://localhost:389"
#bind_dn = "cn=admin,dc=planetexpress,dc=com"
#bind_password = "GoodNewsEveryone"
#user_base_dn = "ou=people,dc=planetexpress,dc=com"
#group_base_dn = "ou=people,dc=planetexpress,dc=com"
#
#user_search_filter = "(&(objectClass=person)(mail=%s))"
#group_search_filter = "(&(objectclass=groupOfNames)(member=%d))"
#
#admin_filter = "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)"
#login_attribute = "mail" #login_attribute = "mail"
#display_name_attribute = "displayName" #display_name_attribute = "displayName"

View File

@ -36,7 +36,7 @@ pub async fn list_repositories(Query(params): Query<ListRepositoriesParams>, sta
let last_repo = repos.last().and_then(|s| Some(s.clone())); let last_repo = repos.last().and_then(|s| Some(s.clone()));
// Construct the link header // Construct the link header
let url = &state.config.get_url(); let url = &state.config.url();
let mut url = format!("<{}/v2/_catalog?n={}", url, limit); let mut url = format!("<{}/v2/_catalog?n={}", url, limit);
if let Some(last_repo) = last_repo { if let Some(last_repo) = last_repo {
url += &format!("&limit={}", last_repo); url += &format!("&limit={}", last_repo);

View File

@ -37,7 +37,7 @@ pub async fn list_tags(Path((name, )): Path<(String, )>, Query(params): Query<Li
let last_tag = tags.last(); let last_tag = tags.last();
// Construct the link header // Construct the link header
let url = &state.config.get_url(); let url = &state.config.url();
let mut url = format!("<{}/v2/{}/tags/list?n={}", url, name, limit); let mut url = format!("<{}/v2/{}/tags/list?n={}", url, name, limit);
if let Some(last_tag) = last_tag { if let Some(last_tag) = last_tag {
url += &format!("&limit={}", last_tag.name); url += &format!("&limit={}", last_tag.name);

View File

@ -85,7 +85,7 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
(0, written_size) (0, written_size)
}; };
let full_uri = format!("{}/v2/{}/blobs/uploads/{}", state.config.get_url(), name, layer_uuid); let full_uri = format!("{}/v2/{}/blobs/uploads/{}", state.config.url(), name, layer_uuid);
Ok(( Ok((
StatusCode::ACCEPTED, StatusCode::ACCEPTED,
[ [

View File

@ -91,7 +91,7 @@ impl Deref for AuthToken {
type Rejection = (StatusCode, HeaderMap); type Rejection = (StatusCode, HeaderMap);
pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Request<B>, next: Next<B>) -> Result<Response, Rejection> { pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Request<B>, next: Next<B>) -> Result<Response, Rejection> {
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url()); let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url());
let mut failure_headers = HeaderMap::new(); let mut failure_headers = HeaderMap::new();
failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap()); failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap());
failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap()); failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap());
@ -119,7 +119,7 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
Ok(next.run(request).await) Ok(next.run(request).await)
} else { } else {
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url()); let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url());
Ok(( Ok((
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
[ [
@ -134,7 +134,7 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
/// The www-authenticate header is set to notify the client of where to authorize with. /// The www-authenticate header is set to notify the client of where to authorize with.
#[inline(always)] #[inline(always)]
pub fn unauthenticated_response(config: &Config) -> Response { pub fn unauthenticated_response(config: &Config) -> Response {
let bearer = format!("Bearer realm=\"{}/auth\"", config.get_url()); let bearer = format!("Bearer realm=\"{}/auth\"", config.url());
( (
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
[ [

View File

@ -29,12 +29,36 @@ fn default_display_name_attribute() -> String {
"displayName".to_string() "displayName".to_string()
} }
#[derive(Deserialize, Clone)]
pub struct FilesystemDriverConfig {
pub path: String,
}
#[derive(Deserialize, Clone)]
#[serde(tag = "driver", rename_all = "snake_case")]
pub enum StorageConfig {
Filesystem(FilesystemDriverConfig),
}
#[derive(Deserialize, Clone)]
pub struct SqliteDbConfig {
pub path: String,
}
#[derive(Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DatabaseConfig {
Sqlite(SqliteDbConfig),
}
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct Config { pub struct Config {
pub listen_address: String, pub listen_address: String,
pub listen_port: String, pub listen_port: String,
pub url: Option<String>, url: Option<String>,
pub ldap: Option<LdapConnectionConfig>, pub ldap: Option<LdapConnectionConfig>,
pub database: DatabaseConfig,
pub storage: StorageConfig,
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -72,7 +96,7 @@ impl Config {
Ok(config) Ok(config)
} }
pub fn get_url(&self) -> String { pub fn url(&self) -> String {
match &self.url { match &self.url {
Some(u) => u.clone(), Some(u) => u.clone(),
None => format!("http://{}:{}", self.listen_address, self.listen_port) None => format!("http://{}:{}", self.listen_address, self.listen_port)

View File

@ -18,9 +18,10 @@ use axum::middleware::Next;
use axum::response::Response; use axum::response::Response;
use axum::{Router, routing}; use axum::{Router, routing};
use axum::ServiceExt; use axum::ServiceExt;
use sqlx::ConnectOptions;
use tower_layer::Layer; use tower_layer::Layer;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tower_http::normalize_path::NormalizePathLayer; use tower_http::normalize_path::NormalizePathLayer;
use tracing::{debug, Level}; use tracing::{debug, Level};
@ -31,7 +32,7 @@ use database::Database;
use crate::storage::StorageDriver; use crate::storage::StorageDriver;
use crate::storage::filesystem::FilesystemDriver; use crate::storage::filesystem::FilesystemDriver;
use crate::config::Config; use crate::config::{Config, DatabaseConfig, StorageConfig};
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
@ -68,12 +69,22 @@ async fn main() -> anyhow::Result<()> {
let config = Config::new() let config = Config::new()
.expect("Failure to parse config!"); .expect("Failure to parse config!");
let sqlite_config = match &config.database {
DatabaseConfig::Sqlite(sqlite) => sqlite,
};
let connection_options = SqliteConnectOptions::from_str(&format!("sqlite://{}", &sqlite_config.path))?
.journal_mode(SqliteJournalMode::Wal);
let pool = SqlitePoolOptions::new() let pool = SqlitePoolOptions::new()
.max_connections(15) .max_connections(15)
.connect("test.db").await?; .connect_with(connection_options).await?;
pool.create_schema().await?; pool.create_schema().await?;
let storage_driver: Mutex<Box<dyn StorageDriver>> = Mutex::new(Box::new(FilesystemDriver::new("registry/blobs"))); let storage_driver: Mutex<Box<dyn StorageDriver>> = match &config.storage {
StorageConfig::Filesystem(fs) => {
Mutex::new(Box::new(FilesystemDriver::new(&fs.path)))
}
};
// figure out the auth driver depending on whats specified in the config, // figure out the auth driver depending on whats specified in the config,
// the fallback is a database auth driver. // the fallback is a database auth driver.
@ -91,7 +102,6 @@ async fn main() -> anyhow::Result<()> {
let state = Arc::new(AppState::new(pool, storage_driver, config, auth_driver)); let state = Arc::new(AppState::new(pool, storage_driver, config, auth_driver));
let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::require_auth); let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::require_auth);
let path_middleware = axum::middleware::from_fn(change_request_paths); let path_middleware = axum::middleware::from_fn(change_request_paths);