create dockerfile, create more configuration options
This commit is contained in:
parent
b9e41f6d6e
commit
0546b71c5e
|
@ -0,0 +1,6 @@
|
||||||
|
/target
|
||||||
|
.env
|
||||||
|
.vscode
|
||||||
|
test.db
|
||||||
|
/registry
|
||||||
|
config.toml
|
|
@ -1,6 +1,5 @@
|
||||||
/target
|
/target
|
||||||
.env
|
.env
|
||||||
.vscode
|
.vscode
|
||||||
test.db
|
|
||||||
/registry
|
/registry
|
||||||
config.toml
|
config.toml
|
|
@ -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" ]
|
|
@ -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"
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
[
|
[
|
||||||
|
|
|
@ -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,
|
||||||
[
|
[
|
||||||
|
|
|
@ -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)
|
||||||
|
|
20
src/main.rs
20
src/main.rs
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue