diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4ebe4f9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +/target +.env +.vscode +test.db +/registry +config.toml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4ebe4f9..fdfb5d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ /target .env .vscode -test.db /registry config.toml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..001ed98 --- /dev/null +++ b/Dockerfile @@ -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" ] \ No newline at end of file diff --git a/config-example.toml b/config-example.toml index 06335b0..0981690 100644 --- a/config-example.toml +++ b/config-example.toml @@ -2,16 +2,24 @@ listen_address = "127.0.0.1" listen_port = "3000" url = "http://localhost:3000/" -[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" +[storage] +driver = "filesystem" +path = "/app/blobs" -user_search_filter = "(&(objectClass=person)(mail=%s))" -group_search_filter = "(&(objectclass=groupOfNames)(member=%d))" +[database] +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" #display_name_attribute = "displayName" \ No newline at end of file diff --git a/src/api/catalog.rs b/src/api/catalog.rs index dff2215..c2f5c32 100644 --- a/src/api/catalog.rs +++ b/src/api/catalog.rs @@ -36,7 +36,7 @@ pub async fn list_repositories(Query(params): Query, sta let last_repo = repos.last().and_then(|s| Some(s.clone())); // Construct the link header - let url = &state.config.get_url(); + let url = &state.config.url(); let mut url = format!("<{}/v2/_catalog?n={}", url, limit); if let Some(last_repo) = last_repo { url += &format!("&limit={}", last_repo); diff --git a/src/api/tags.rs b/src/api/tags.rs index 6207880..83dd6e2 100644 --- a/src/api/tags.rs +++ b/src/api/tags.rs @@ -37,7 +37,7 @@ pub async fn list_tags(Path((name, )): Path<(String, )>, Query(params): Query
  • (State(state): State>, mut request: Request, next: Next) -> Result { - 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(); failure_headers.append(header::WWW_AUTHENTICATE, bearer.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(State(state): State>, mut request: Re Ok(next.run(request).await) } else { - let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url()); + let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url()); Ok(( StatusCode::UNAUTHORIZED, [ @@ -134,7 +134,7 @@ pub async fn require_auth(State(state): State>, mut request: Re /// The www-authenticate header is set to notify the client of where to authorize with. #[inline(always)] 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, [ diff --git a/src/config.rs b/src/config.rs index 57c5029..fb40188 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,12 +29,36 @@ fn default_display_name_attribute() -> 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)] pub struct Config { pub listen_address: String, pub listen_port: String, - pub url: Option, + url: Option, pub ldap: Option, + pub database: DatabaseConfig, + pub storage: StorageConfig, } #[allow(dead_code)] @@ -72,7 +96,7 @@ impl Config { Ok(config) } - pub fn get_url(&self) -> String { + pub fn url(&self) -> String { match &self.url { Some(u) => u.clone(), None => format!("http://{}:{}", self.listen_address, self.listen_port) diff --git a/src/main.rs b/src/main.rs index 729a814..fa39dc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,9 +18,10 @@ use axum::middleware::Next; use axum::response::Response; use axum::{Router, routing}; use axum::ServiceExt; +use sqlx::ConnectOptions; use tower_layer::Layer; -use sqlx::sqlite::SqlitePoolOptions; +use sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode}; use tokio::sync::Mutex; use tower_http::normalize_path::NormalizePathLayer; use tracing::{debug, Level}; @@ -31,7 +32,7 @@ use database::Database; use crate::storage::StorageDriver; use crate::storage::filesystem::FilesystemDriver; -use crate::config::Config; +use crate::config::{Config, DatabaseConfig, StorageConfig}; use tower_http::trace::TraceLayer; @@ -68,12 +69,22 @@ async fn main() -> anyhow::Result<()> { let config = Config::new() .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() .max_connections(15) - .connect("test.db").await?; + .connect_with(connection_options).await?; pool.create_schema().await?; - let storage_driver: Mutex> = Mutex::new(Box::new(FilesystemDriver::new("registry/blobs"))); + let storage_driver: Mutex> = 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, // 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 auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::require_auth); let path_middleware = axum::middleware::from_fn(change_request_paths);