Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

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

6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ after its format id, linked into the server like `crates/formats/nuget`
under `crates/` (no `crates/format-<id>`) or as a module of the server: it
implements `FormatPlugin` with the layout
[crates/formats/README.md](crates/formats/README.md) describes. Docker / OCI
is in `crates/formats/docker` but still native: the server's registry serves
it under `/v2`, not through `FormatPlugin`. Until it implements
`FormatPlugin`, it is not the model for a new format.
(`crates/formats/docker`) is one too, an images format: it speaks the
registry protocol, and the server's image store (`crates/server/src/images`)
keeps the images.

## Changelog: a fragment per change, never CHANGELOG.md

Expand Down
22 changes: 2 additions & 20 deletions Cargo.lock

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

10 changes: 5 additions & 5 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,12 @@ groups. See [docs/raw.md](docs/raw.md).

### Repositories of other formats (format plugins)

Every package format is a format plugin, a crate in
Every format is a format plugin, a crate in
[`crates/formats/`](./crates/formats) linked into the server (see
[docs/format-plugins.md](./docs/format-plugins.md)): NuGet, npm and raw. A
new format goes in `crates/formats/<id>` too, never in a `crates/format-<id>`
crate or the server. Docker / OCI is in `crates/formats/docker` but still
native, served by the server's registry under `/v2`. A repository of a plugin
[docs/format-plugins.md](./docs/format-plugins.md)): Docker / OCI, NuGet, npm
and raw. A new format goes in `crates/formats/<id>` too, never in a
`crates/format-<id>` crate or the server. Docker / OCI is an images format,
served under `/v2` over the server's image store. A repository of a plugin
format is created with `"format": "<id>"` (hosted, proxy or group; a plugin
format has proxies when it implements the upstream side) and served under
`/repository/<name>/` unless the plugin names another mount (NuGet's is
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,17 @@ A Cargo workspace of small crates plus the web SPA:
| `core` | `forge-core` | Domain logic: repo dispatch, auth chain, authz/RBAC, sessions, OIDC, retention. |
| `plugin-api` | `forge-plugin-api` | The format-plugin contract: the `FormatPlugin` trait and its types. |
| `plugin` | `forge-plugin` | The host side of format plugins: an installed plugin and its adapter to the repository machinery. |
| `formats/<id>` | `forge-format-*` | Format plugins, built into the server: `nuget`, `npm`, `raw`. |
| `formats/docker` | `forge-format-docker` | Docker / OCI registry, still native: the distribution-spec `/v2` API, manifests, registry tokens, hosted/proxy/group resolution. |
| `server` | `forge-server` | Axum HTTP server: the format plugins' feeds (NuGet at `/v3`, npm at `/npm`, raw files at `/raw`), the file store of files formats, the Docker registry, `/admin` + `/auth` REST, background workers, static SPA serving. |
| `formats/<id>` | `forge-format-*` | Format plugins, built into the server: `docker` (the distribution-spec `/v2` API, manifests, upstream registries), `nuget`, `npm`, `raw`. |
| `server` | `forge-server` | Axum HTTP server: the format plugins' feeds (NuGet at `/v3`, npm at `/npm`, raw files at `/raw`, the Docker / OCI registry at `/v2`), the file store of files formats, the image store and registry tokens of images formats, `/admin` + `/auth` REST, background workers, static SPA serving. |
| `e2e` | `forge-e2e` | End-to-end suite (testcontainers + mock upstream/IdP). |
| `xtask` | — | The task runner (`cargo xtask …`); replaces Make/Just. |
| `web/` | — | TanStack Router SPA (pnpm + Vite), built into `web/dist/`. |

**Every format is a plugin in `crates/formats/`.** A package format is a
crate in `crates/formats/<id>/` implementing `FormatPlugin`, linked into the
server. Docker / OCI is in `crates/formats/docker` but still native: the
server's registry serves it under `/v2`, not through `FormatPlugin`.
**Every format is a plugin in `crates/formats/`.** A format is a crate in
`crates/formats/<id>/` implementing `FormatPlugin`, linked into the server.
Docker / OCI is one too, an images format: the plugin speaks the protocol,
and the server's image store (`crates/server/src/images`) keeps the
images.

**Single-origin serving.** `forge-server` mounts each format plugin's feed
(NuGet at `/v3`, npm at `/npm`, others at `/repository`), the Docker / OCI registry (`/v2`),
Expand Down
7 changes: 7 additions & 0 deletions crates/core/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ pub trait Format: Send + Sync {
false
}

/// Whether the format's repositories hold images (Docker / OCI): their
/// contents are the image store's (`oci_*`), served by the registry, and
/// none of the package seams below apply to them.
fn stores_images(&self) -> bool {
false
}

/// Parse an uploaded package into format-neutral [`ParsedUpload`] data.
fn parse_upload(&self, file: &mut dyn ReadSeek) -> Result<ParsedUpload, FormatError>;

Expand Down
8 changes: 7 additions & 1 deletion crates/core/src/repo/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,13 @@ impl ProxyRepo {
} else {
(self.refuse(repo, &fetch.url), Vec::new(), Trouble::None)
};
(api::FetchResult { fetch, outcome }, named, down)
let result = api::FetchResult {
fetch,
outcome,
status: None,
headers: Vec::new(),
};
(result, named, down)
}
})
.buffered(FETCH_CONCURRENCY)
Expand Down
7 changes: 6 additions & 1 deletion crates/core/src/retention/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ impl RetentionWorker {
run_id: uuid::Uuid,
) -> Result<RunSummary, WorkerError> {
let kind = RepoKind::from(repo.kind.as_str());
if repo.format == forge_db::model::DOCKER_FORMAT {
// An images format's repositories (Docker) hold images.
if self
.formats
.resolve(&repo.format)
.is_some_and(|format| format.stores_images())
{
return self.process_images(repo, kind, run_id).await;
}
// A files format's repositories (raw) hold files under paths.
Expand Down
8 changes: 0 additions & 8 deletions crates/db/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,6 @@ impl Repository {
/// `repository.format` of NuGet repositories (the column default).
pub const NUGET_FORMAT: &str = "nuget";

/// `repository.format` of Docker / OCI registry repositories.
pub const DOCKER_FORMAT: &str = "docker";

/// `repository.config` key of [`Repository::docker_host`].
pub const DOCKER_HOST_KEY: &str = "host";

Expand All @@ -221,11 +218,6 @@ pub const WRITABLE_MEMBER_ID_KEY: &str = "writable_member_id";
pub const MAX_UPLOAD_BYTES_KEY: &str = "max_upload_bytes";

impl Repository {
/// Whether this is a Docker / OCI registry repository.
pub fn is_docker(&self) -> bool {
self.format == DOCKER_FORMAT
}

/// Docker option (`config.host`): the host name this repository answers
/// on by itself (host-based routing), so its images keep short names.
pub fn docker_host(&self) -> Option<&str> {
Expand Down
76 changes: 40 additions & 36 deletions crates/db/src/oci_dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ const TOUCH_INTERVAL_SECS: f64 = 3600.0;

// ── Repositories ────────────────────────────────────────────────────────────

/// The docker repository that serves the host name `host` (host-based
/// routing, `config.host`), case-insensitively.
/// The repository of the images format `format` that serves the host name
/// `host` (host-based routing, `config.host`), case-insensitively.
pub async fn find_repository_by_host(
pool: &PgPool,
host: &str,
format: &str,
) -> sqlx::Result<Option<Repository>> {
sqlx::query_as!(
Repository,
Expand All @@ -36,9 +37,10 @@ pub async fn find_repository_by_host(
upstream_password_enc, metadata_ttl_seconds, not_found_ttl_seconds,
allow_redeploy, created_at, updated_at
FROM repository
WHERE format = 'docker' AND config ? 'host' AND lower(config->>'host') = lower($1)
WHERE format = $2 AND config ? 'host' AND lower(config->>'host') = lower($1)
"#,
host,
format,
)
.fetch_optional(pool)
.await
Expand Down Expand Up @@ -838,57 +840,59 @@ pub async fn set_manifest_summary(
Ok(())
}

/// Record the platform of the image config `config_digest` on the image's
/// manifests that name it and have none yet (a proxy caches a manifest
/// before its config).
pub async fn set_config_platform(
/// The media type of the config `config_digest`, as a manifest of the image
/// that names it as its config says (its summary's `config_digest` and
/// `config_media_type`), if one does.
pub async fn config_media_type(
pool: &PgPool,
repository_id: i64,
image: &str,
config_digest: &str,
platform: &str,
) -> sqlx::Result<()> {
sqlx::query!(
) -> sqlx::Result<Option<String>> {
sqlx::query_scalar(
r#"
UPDATE oci_manifest
SET summary = summary || jsonb_build_object('platform', $4::text)
SELECT summary->>'config_media_type' FROM oci_manifest
WHERE repository_id = $1 AND image = $2
AND summary->>'config_digest' = $3
AND NOT summary ? 'platform'
AND summary ? 'config_media_type'
LIMIT 1
"#,
repository_id,
image,
config_digest,
platform,
)
.execute(pool)
.await?;
Ok(())
.bind(repository_id)
.bind(image)
.bind(config_digest)
.fetch_optional(pool)
.await
}

/// Whether a manifest of the image names `config_digest` as its config and
/// has no platform recorded yet.
pub async fn config_needs_platform(
/// Merge `part`, what the config `config_digest` holds, into the summaries
/// of the image's manifests that name it as their config and hold none of
/// `keys` yet (a proxy caches a manifest before its config).
pub async fn complete_config_summary(
pool: &PgPool,
repository_id: i64,
image: &str,
config_digest: &str,
) -> sqlx::Result<bool> {
sqlx::query_scalar!(
part: &JsonValue,
keys: &[String],
) -> sqlx::Result<()> {
sqlx::query(
r#"
SELECT EXISTS (
SELECT 1 FROM oci_manifest
WHERE repository_id = $1 AND image = $2
AND summary->>'config_digest' = $3
AND NOT summary ? 'platform'
) AS "exists!"
UPDATE oci_manifest
SET summary = summary || $4
WHERE repository_id = $1 AND image = $2
AND summary->>'config_digest' = $3
AND NOT summary ?| $5
"#,
repository_id,
image,
config_digest,
)
.fetch_one(pool)
.await
.bind(repository_id)
.bind(image)
.bind(config_digest)
.bind(part)
.bind(keys)
.execute(pool)
.await?;
Ok(())
}

/// Which of `digests` are manifests of the image.
Expand Down
12 changes: 5 additions & 7 deletions crates/formats/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,22 @@ the design, the contract and how to write one.

| Plugin | Format id | What it serves |
| --- | --- | --- |
| [`docker`](docker) | `docker` | Docker / OCI registries at `/v2`: push and pull images, indexes and artifacts (Helm charts, signatures, SBOMs); hosted, proxy and group repositories ([docs/docker.md](../../docs/docker.md)) |
| [`nuget`](nuget) | `nuget` | NuGet V3 feeds at `/v3`: push, restore, search, unlist and relist; hosted, proxy and group repositories |
| [`npm`](npm) | `npm` | npm registries at `/npm` for npm, pnpm, Yarn and Bun: publish, install, dist-tags, deprecations, unpublishing, search, audits and logins; hosted, proxy and group repositories ([docs/npm.md](../../docs/npm.md)) |
| [`raw`](raw) | `raw` | Raw files at `/raw`: files under paths, uploads with `PUT` or a form, ranges, checksum sidecars, listings; hosted, proxy and group repositories ([docs/raw.md](../../docs/raw.md)) |

[`docker`](docker) holds Docker / OCI (`forge-format-docker`), which is still
native: the server's registry serves it under `/v2`, not through
`FormatPlugin` ([docs/docker.md](../../docs/docker.md)).

A plugin directory holds:

- `Cargo.toml`: a member of the workspace; its `version` is the plugin's;
- `src/`: `impl FormatPlugin`, and the format's `pub const DESCRIPTOR:
Descriptor` that its `descriptor()` returns: the format id (also the
directory's name), display name, version (`env!("CARGO_PKG_VERSION")`),
description, mount, repository kinds (`Proxy` when it implements
`upstream`), its `storage` (`Packages`, or `Files` for a files format such
as raw), its asset names, and how a proxy sends a token
(`upstream_token`: `Bearer` for npm).
`upstream`), its `storage` (`Packages`, `Files` for a files format such
as raw, or `Images` for an images format such as Docker), its asset
names, and how a proxy sends a token (`upstream_token`: `Bearer` for
npm).

The server installs it in `plugins::builtin()`
([`crates/server/src/plugins.rs`](../server/src/plugins.rs)), whose tests
Expand Down
Loading
Loading