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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ FORGE_AUTH__BOOTSTRAP_ADMIN__PASSWORD=changeme
# ---------------------------------------------------------------------------
# WebAssembly format plugins (optional; docs/format-plugins.md)
# Load further package formats from <dir>/<id>/, each pinned to the SHA-256 of
# its component (a mismatch fails startup). The built-in formats (Helm) need
# its component (a mismatch fails startup). The built-in format (NuGet) needs
# nothing here. `cargo xtask plugins` builds components and prints digests.
# ---------------------------------------------------------------------------
# FORGE_PLUGINS__DIR=/etc/forge/plugins
Expand Down
53 changes: 50 additions & 3 deletions .github/scripts/oci-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
# 3. crane: ls, manifest, copy;
# 4. a proxy repository of a `registry:2` upstream (seeded with crane) and a
# group over the hosted and proxy repositories, pulled with docker;
# 5. cosign: a signature and an attestation pushed as OCI 1.1 referrers,
# 5. helm: registry login, a chart pushed, pulled, rendered and shown in
# the browse API, and pulled through the proxy and the group;
# 6. cosign: a signature and an attestation pushed as OCI 1.1 referrers,
# found through the referrers API and verified.
#
# Usage: oci-e2e.sh <forge-server binary> <results dir>
# Needs on PATH: docker (with buildx), go, crane, cosign, curl, jq, git.
# Needs on PATH: docker (with buildx), go, crane, helm, cosign, curl, jq, git.
# Postgres and S3 come from the environment (defaults match ci.yml):
# FORGE_E2E_DATABASE_URL (postgres://forge:forge@localhost:5432/forge)
# FORGE_E2E_S3_ENDPOINT (http://localhost:7070), access/secret forge-e2e-key
Expand Down Expand Up @@ -41,6 +43,7 @@ cleanup() {
docker rm -f forge-e2e-upstream >/dev/null 2>&1 || true
docker buildx rm forge-e2e >/dev/null 2>&1 || true
docker logout "$registry" >/dev/null 2>&1 || true
helm registry logout "$registry" >/dev/null 2>&1 || true
cp "$work/server.log" "$results/" 2>/dev/null || true
rm -rf "$work"
}
Expand Down Expand Up @@ -143,6 +146,50 @@ docker pull "$registry/docker-group/upstream/multi:1"
crane copy --insecure "$image" "$registry/docker-group/e2e/via-group:1"
crane digest --insecure "$registry/docker-hosted/e2e/via-group:1" >/dev/null || fail "group push"

step "helm: login, push, pull, template; through the proxy and the group"
# Helm keeps its registry login and cache under these, not in ~/.config.
export HELM_CONFIG_HOME="$work/helm/config" HELM_CACHE_HOME="$work/helm/cache" \
HELM_DATA_HOME="$work/helm/data"
echo "$token" | helm registry login "$registry" --username "$admin_user" --password-stdin --plain-http
mkdir -p "$work/charts" "$work/pulled"/{hosted,range,proxy,group-charts,group-upstream-charts}
helm create "$work/charts/e2e-chart" >/dev/null
sed -i 's/^version: .*/version: 1.2.3/; s/^appVersion: .*/appVersion: "4.5.6"/' \
"$work/charts/e2e-chart/Chart.yaml"
printf '# The e2e chart\n' > "$work/charts/e2e-chart/README.md"
helm package "$work/charts/e2e-chart" -d "$work/charts" >/dev/null
packaged="$work/charts/e2e-chart-1.2.3.tgz"
helm push "$packaged" "oci://$registry/docker-hosted/charts" --plain-http
chart="oci://$registry/docker-hosted/charts/e2e-chart"
helm pull "$chart" --version 1.2.3 --plain-http -d "$work/pulled/hosted"
cmp -s "$packaged" "$work/pulled/hosted/e2e-chart-1.2.3.tgz" || fail "helm pull: not the pushed chart"
# A version range resolves against the tags.
helm pull "$chart" --version '~1.2' --plain-http -d "$work/pulled/range"
[ -f "$work/pulled/range/e2e-chart-1.2.3.tgz" ] || fail "helm pull --version ~1.2"
helm show chart "$chart" --version 1.2.3 --plain-http | grep -qx 'appVersion: 4.5.6' \
|| fail "helm show chart"
helm template e2e "$chart" --version 1.2.3 --plain-http | grep -q '^kind: Deployment' \
|| fail "helm template"
# The browse API reads the chart's metadata, README and values.
chart_digest=$(crane digest --insecure "$registry/docker-hosted/charts/e2e-chart:1.2.3")
browsed=$(curl -fsS -b "$work/jar" \
"$base/browse/chart?repo=docker-hosted&image=charts/e2e-chart&digest=$chart_digest")
jq -e '.metadata.appVersion == "4.5.6" and .readme.text == "# The e2e chart\n"
and (.values.text | contains("replicaCount"))' <<<"$browsed" >/dev/null \
|| fail "browse a chart: $browsed"
# Through the proxy of the registry:2 upstream (no login there), and the
# group, which also takes the push to its writable member.
helm push "$packaged" "oci://localhost:$upstream_port/upstream-charts" --plain-http
helm pull "oci://$registry/docker-proxy/upstream-charts/e2e-chart" --version 1.2.3 \
--plain-http -d "$work/pulled/proxy"
cmp -s "$packaged" "$work/pulled/proxy/e2e-chart-1.2.3.tgz" || fail "helm pull through the proxy"
for path in charts upstream-charts; do
helm pull "oci://$registry/docker-group/$path/e2e-chart" --version 1.2.3 \
--plain-http -d "$work/pulled/group-$path"
done
helm push "$packaged" "oci://$registry/docker-group/via-group" --plain-http
crane digest --insecure "$registry/docker-hosted/via-group/e2e-chart:1.2.3" >/dev/null \
|| fail "helm push to the group"

step "cosign: signature and attestation as referrers"
# cosign 3 stores both as Sigstore bundles through the OCI 1.1 referrers
# API. Signed with a local key and no transparency log, so verification
Expand All @@ -167,4 +214,4 @@ verify=(--key "$work/cosign.pub" --insecure-ignore-tlog --private-infrastructure
cosign verify "${verify[@]}" "$signed" >/dev/null
cosign verify-attestation "${verify[@]}" --type slsaprovenance "$signed" >/dev/null

step "OK: conformance, docker, crane, proxy, group and cosign"
step "OK: conformance, docker, crane, proxy, group, helm and cosign"
26 changes: 21 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ jobs:
# validates the output against the Kubernetes schemas. kustomize and
# kubeconform are pinned by checksum, Helm is built from its pinned Go
# module (verified by the Go checksum database; keep HELM_VERSION in step
# with release.yml). Runs when the change touches any of these (the
# kubernetes group, #353).
# with oci-e2e and release.yml). Runs when the change touches any of these
# (the kubernetes group, #353).
kubernetes:
name: kubernetes
runs-on: ubuntu-latest
Expand Down Expand Up @@ -448,8 +448,9 @@ jobs:
# ── Docker / OCI registry e2e (real clients against a live server) ─────────
# Boots forge-server and drives the registry with the OCI distribution-spec
# conformance suite, docker (buildx multi-arch), crane, a proxy of a
# registry:2 upstream, a group, and cosign signatures/attestations through
# the referrers API (.github/scripts/oci-e2e.sh). Runs on pull requests too:
# registry:2 upstream, a group, Helm charts (helm push, pull, template), and
# cosign signatures/attestations through the referrers API
# (.github/scripts/oci-e2e.sh). Runs on pull requests too:
# it is the only check that the registry still speaks to real clients.
oci-e2e:
name: oci-e2e
Expand All @@ -473,6 +474,9 @@ jobs:
env:
SQLX_OFFLINE: "true"
CRANE_VERSION: v0.20.2
# Built from its pinned Go module, as in the kubernetes job (keep it in
# step with that job and release.yml).
HELM_VERSION: v4.3.1
COSIGN_VERSION: v3.0.2
COSIGN_SHA256: 46dbdcb5467a3dfec2526923d0b3365e40c8d9dc00ec23d5aca3437449e8cbfd
# The upstream registry the proxy repository caches, and buildx's
Expand Down Expand Up @@ -500,6 +504,18 @@ jobs:
echo "${COSIGN_SHA256} $RUNNER_TEMP/bin/cosign" | sha256sum -c -
chmod +x "$RUNNER_TEMP/bin/cosign"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Cache the Helm build
id: helm-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/helm
key: ${{ runner.os }}-helm-${{ env.HELM_VERSION }}
- name: Install Helm
run: |
if [ "${{ steps.helm-cache.outputs.cache-hit }}" != "true" ]; then
GOBIN="$RUNNER_TEMP/helm" GOTOOLCHAIN=auto GOFLAGS=-trimpath go install "helm.sh/helm/v4/cmd/helm@${HELM_VERSION}"
fi
echo "$RUNNER_TEMP/helm" >> "$GITHUB_PATH"
- name: Start the S3 gateway and pull the upstream registry image
run: |
docker run -d --name forge-e2e-s3 -p 7070:7070 \
Expand All @@ -508,7 +524,7 @@ jobs:
docker pull "$FORGE_E2E_UPSTREAM_IMAGE"
- name: cargo build -p forge-server
run: cargo build -p forge-server --bin forge-server
- name: Registry e2e (conformance, docker, crane, proxy, group, cosign)
- name: Registry e2e (conformance, docker, crane, proxy, group, helm, cosign)
env:
GOTOOLCHAIN: auto
run: .github/scripts/oci-e2e.sh target/debug/forge-server oci-e2e-results
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ its pull request's description says why.
A package format is a crate in `plugins/<id>/`, the directory named after its
format id, whether it runs built in only, as a WebAssembly component, or
both. Built in only is no reason to put it elsewhere: NuGet runs built in
only and lives in `plugins/nuget`, like Helm. Never add a format as a crate
only and lives in `plugins/nuget`. Never add a format as a crate
under `crates/` (no `crates/format-<id>`) or as a module of the server: it
implements `FormatPlugin` with the layout
[plugins/README.md](plugins/README.md) describes, and has a `component`
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

30 changes: 16 additions & 14 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Developing & running Forge locally

Forge is a NuGet, Docker / OCI, Helm and raw file registry written in Rust.
Forge is a NuGet, Docker / OCI (Helm charts included) and raw file registry
written in Rust.
Locally it runs as a single `forge-server` process backed by **PostgreSQL**
(metadata) and an **S3-compatible** object store (blobs) — both provided by
`docker-compose.yml` (versitygw stands in for S3).
Expand Down Expand Up @@ -253,23 +254,23 @@ groups. See [docs/raw.md](docs/raw.md).
Every package format is a format plugin, a crate in
[`plugins/`](./plugins) (see [docs/format-plugins.md](./docs/format-plugins.md)),
whether it runs built in only, as a WebAssembly component, or both: NuGet runs
built in only, Helm either way. A new format goes there too, never in a
`crates/format-<id>` crate or the server. Docker / OCI (`crates/format-oci`)
and raw (`crates/server/src/raw/`) are not there yet and are to move to
`plugins/docker` and `plugins/raw`.
built in only. `plugins/helm`, classic Helm chart repositories, is the example
plugin the plugin tests run; it is not built in (Helm charts are pushed to
docker repositories, see [docs/docker.md](docs/docker.md#helm-charts)). A new
format goes in `plugins/` too, never in a `crates/format-<id>` crate or the
server. Docker / OCI (`crates/format-oci`) and raw (`crates/server/src/raw/`)
are not there yet and are to move to `plugins/docker` and `plugins/raw`.

Helm is built in: create a repository with `"format": "helm"` (hosted, proxy or
group; a plugin format has proxies when it implements the upstream side). It is
served under `/repository/<name>/`, e.g.
`helm repo add local http://localhost:8421/repository/charts`.

To run a plugin as a WebAssembly component instead:
To run a plugin as a WebAssembly component:

```bash
cargo xtask plugins # builds plugins/* as components, prints the [plugins] section for forge.toml
```

A built-in plugin's component loads only under another id (rename its
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. A built-in plugin's component loads only under another id (rename its
installed directory and the `id` in its `plugin.toml`).

## Configuration
Expand Down Expand Up @@ -616,8 +617,9 @@ cargo xtask plugins-check # the format plugins' component builds: clippy and b
```

The format-plugin tests (`crates/plugin/tests`, `crates/server/tests/plugins.rs`)
build the Helm plugin as a component themselves into `target/plugins/`; they
need the `wasm32-wasip2` target.
run the example plugin, `plugins/helm`: built in for the test, and built as a
component themselves into `target/plugins/`; they need the `wasm32-wasip2`
target.

The e2e suite includes an optional `dotnet` smoke test that is skipped when the
.NET SDK isn't installed.
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ formats come as **format plugins**, see below):
priority; tolerates a failing member and, by default, keeps public feeds
from shadowing internal package ids (dependency-confusion guard).

The same three types exist for container images: hosted image repositories,
pull-through proxies of Docker Hub, GHCR, Quay or any OCI registry, and
groups — see **[docs/docker.md](./docs/docker.md)** — and for plain files
The same three types exist for container images and other OCI artifacts,
Helm charts included (`helm push`, `helm install oci://…`): hosted image
repositories, pull-through proxies of Docker Hub, GHCR, Quay or any OCI
registry, and groups — see **[docs/docker.md](./docs/docker.md)** — and for plain files
under arbitrary paths (binaries, installers, fixtures), like Nexus' raw
repositories, with ranges, checksum sidecars and proxies of any HTTP(S)
download URL such as GitHub release assets — see
Expand All @@ -35,8 +36,7 @@ token, cache, and audit limitations are documented below.
A **retention** engine reaps cold versions and garbage-collects dereferenced
blobs. **Format plugins** (preview) add further package formats that reuse all
of the above, either built into the server or loaded as sandboxed WebAssembly
components; **Helm chart repositories** are built in
([docs/format-plugins.md](docs/format-plugins.md)). The admin + browse UI is
components ([docs/format-plugins.md](docs/format-plugins.md)). The admin + browse UI is
a **TanStack Router SPA** served by the same Rust process on a single origin —
there is no Node server in production.

Expand All @@ -56,7 +56,7 @@ A Cargo workspace of small crates plus the web SPA:
| `server` | `forge-server` | Axum HTTP server: the format plugins' feeds (NuGet at `/v3`), the Docker registry, raw file repositories (`/raw`), `/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. |
| `plugins/` | `forge-format-*` | Format plugins, built in or as WebAssembly components: `nuget` (built in only), `helm`. |
| `plugins/` | `forge-format-*` | Format plugins, built in or as WebAssembly components: `nuget` (built in only), and `helm`, the example plugin the plugin tests run (not built in). |
| `web/` | — | TanStack Router SPA (pnpm + Vite), built into `web/dist/`. |

**Every format is a plugin in `plugins/`.** A package format is a crate in
Expand All @@ -66,7 +66,7 @@ a WebAssembly component, or both. Docker / OCI (`crates/format-oci`) and raw
`plugins/raw`.

**Single-origin serving.** `forge-server` mounts each format plugin's feed
(NuGet at `/v3`, Helm at `/repository`), the Docker / OCI registry (`/v2`),
(NuGet at `/v3`, others at `/repository`), the Docker / OCI registry (`/v2`),
raw file repositories (`/raw`),
the admin/browse APIs (`/admin`, `/browse`, `/auth`) and then serves the built SPA
from `web/dist/` via a `ServeDir` SPA fallback. The browser talks to the API on
Expand Down Expand Up @@ -211,11 +211,11 @@ curl -u admin@example.com:<TOKEN> http://localhost:8421/raw/binaries/tools/1.0/t
- **[Trusted publishing](./docs/trusted-publishing.md)** — configure short-lived
workload identity for GitHub Actions, GitLab CI, and compatible OIDC issuers,
and exchange a job's OIDC token for a short-lived API key (`POST /api/v2/token`).
- **[Format plugins](./docs/format-plugins.md)** — further package formats
(Helm charts), built in or as sandboxed WebAssembly components: design,
operation, and writing one.
- **[Format plugins](./docs/format-plugins.md)** — further package formats,
built in or as sandboxed WebAssembly components: design, operation, and
writing one.
- **[Importing from Nexus](./docs/nexus-import.md)** — copy a Nexus hosted
repository (NuGet, Helm, Docker) into Forge, with its publish dates:
repository (NuGet, Docker) into Forge, with its publish dates:
dry run, conflicts, resuming, and the cutover.

Run `cargo xtask help` for the complete command list.
Expand Down
2 changes: 0 additions & 2 deletions crates/e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ aws-sigv4 = "1.6"
aws-credential-types = { workspace = true }

[dev-dependencies]
# Builds the Helm chart archive the Nexus import scenario uploads to Nexus.
flate2 = { version = "1.1", default-features = false, features = ["zlib-rs"] }
serde_json = { workspace = true }
sqlx = { workspace = true }
reqwest = { workspace = true, features = ["multipart"] }
Expand Down
Loading
Loading