Skip to content

Add experimental oci repo plugin#591

Draft
lcarva wants to merge 1 commit intorpm-software-management:masterfrom
lcarva:oci-repo
Draft

Add experimental oci repo plugin#591
lcarva wants to merge 1 commit intorpm-software-management:masterfrom
lcarva:oci-repo

Conversation

@lcarva
Copy link
Copy Markdown

@lcarva lcarva commented Sep 3, 2025

This plugin allows using a YUM repo that is represented as an OCI Artifact by adding support to the oci:// protocol. Example repo file:

[oci-repo-test]
name=oci-repo-test
baseurl=oci://quay.io/lucarval/yum-repo:latest
enabled=1
gpgcheck=1

When enabled, the plugin is activated for any enabled repo that uses the oci:// protocol in its baseurl. The baseurl reference is expected to point to an OCI Artifact.

During the config phase, it downloads the repodata from the OCI Artifact into a local temporary directory. This allows DNF to determine which packages can be installed from that repo.

During the resolved phase, the plugin then downloads each RPM package that is marked for installation. It places those files alongside the repodata in the same temporary directory. This allows DNF to install those packages.

Finally, during the transaction phase, the local temporary directory is removed as a clean up step.

If you're interested in trying out this plugin locally:

  1. Copy the oci_repo.py file to /usr/lib/python3.12/site-packages/dnf-plugins/, python version should match your system's python version.
  2. Enable the plugin by creating /etc/dnf/plugins/oci_repo.conf with the following contents:
[main]
enabled=1
  1. Create yum repo file under /etc/yum.repos.d/ that uses the oci:// protocol. The example above should work.
  2. Install the python3-oras RPM package.
  3. Run dnf install as you would normally. NOTE: If using the example YUM repo above, you can install cowsay and lolcat. Be sure to disable other repos, e.g. dnf install --disablerepo '*' --enablerepo oci-test-repo.

If you want to create your own OCI Artifact yum repo, start with an empty directory, copy/download all the RPMs you want to include into that directory, run createrepo_c . to create the repo metadata, then use oras push <oci-ref> * to create the OCI Artifact. The OCI Artifact is simply an OCI Image Manifest that contains each file as a separate blob/layer. Directories, i.e. repodata, are combined into a single blob/layer.

The approach of downloading RPMs and repodata is not ideal since it does not leverage the existing DNF caching system. Also, for RPMs in particular, packages are downloaded if marked for installation, not if they are actually being installed. For example,if I choose to type "N" and not install them, they have already been downloaded.

This plugin allows using a YUM repo that is represented as an OCI
Artifact by adding support to the `oci://` protocol. Example repo file:

```
[oci-repo-test]
name=oci-repo-test
baseurl=oci://quay.io/lucarval/yum-repo:latest
enabled=1
gpgcheck=1
```

When enabled, the plugin is activated for any enabled repo that uses the
`oci://` protocol in its baseurl. The baseurl reference is expected to
point to an OCI Artifact.

During the `config` phase, it downloads the `repodata` from the OCI
Artifact into a local temporary directory. This allows DNF to determine
which packages can be installed from that repo.

During the `resolved` phase, the plugin then downloads each RPM package
that is marked for installation. It places those files alongside the
repodata in the same temporary directory. This allows DNF to install
those packages.

Finally, during the `transaction` phase, the local temporary directory
is removed as a clean up step.

If you're interested in trying out this plugin locally:

1. Copy the `oci_repo.py` file to
   `/usr/lib/python3.12/site-packages/dnf-plugins/`, python version
   should match your system's python version.
2. Enable the plugin by creating `/etc/dnf/plugins/oci_repo.conf` with
   the following contents:
```
[main]
enabled=1
```
3. Create yum repo file under `/etc/yum.repos.d/` that uses the `oci://`
   protocol. The example above should work.
4. Install the `python3-oras` RPM package.
5. Run `dnf install` as you would normally. NOTE: If using the example
   YUM repo above, you can install `cowsay` and `lolcat`. Be sure to
   disable other repos, e.g. `dnf install --disablerepo '*' --enablerepo
   oci-test-repo`.

If you want to create your own OCI Artifact yum repo, start with an
empty directory, copy/download all the RPMs you want to include into
that directory, run `createrepo_c .` to create the repo metadata, then
use `oras push <oci-ref> *` to create the OCI Artifact. The OCI Artifact
is simply an OCI Image Manifest that contains each file as a separate
blob/layer. Directories, i.e. `repodata`, are combined into a single
blob/layer.

The approach of downloading RPMs and repodata is not ideal since it does
not leverage the existing DNF caching system. Also, for RPMs in
particular, packages are downloaded if marked for installation, not if
they are actually being installed. For example,if I choose to type "N"
and not install them, they have already been downloaded.

Signed-off-by: Luiz Carvalho <[email protected]>
Comment on lines +32 to +33
import oras.client # Install python3-oras RPM
import oras.defaults
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this stack does not share code with the podman stack, and in particular doesn't honor things in /etc/containers or handle auth files the same way.

That's not fatal, but https://github.com/containers/skopeo/blob/749370dd999e034d89227e1ca9e1391eb12ad58e/docs-experimental/skopeo-experimental-image-proxy.1.md#L4 was designed with use cases like this in mind and I'm of a mind to remove the experimental label from it.

client = oras.client.OrasClient(hostname=hostname, insecure=insecure)

authfile = os.environ.get(
"REGSITRY_AUTH", os.path.expanduser("~/.docker/config.json")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the env var name

Comment on lines +109 to +112
last_slash = url.rfind("/")
last_colon = url.rfind(":")
if last_colon > last_slash:
url = url[:last_colon]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are subtle cases where this image parsing will fail

tar_stream.seek(0)

with tarfile.open(fileobj=tar_stream, mode="r") as tar:
tar.extractall(path=dest, filter="data")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this python library robust against potentially malicious input? This would be a good thing to sandbox in a restricted subprocess (which is what podman/containers-storage does) or parse the tar to an in-memory VFS and defer regfiles to out of band (what composefs-rs does, see https://github.com/containers/composefs-rs/blob/main/crates/composefs-oci/src/tar.rs etc.)

Comment on lines +160 to +165
if unpack:
tar_stream = io.BytesIO()
for chunk in r.iter_content(chunk_size=8192):
if chunk:
tar_stream.write(chunk)
tar_stream.seek(0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confused by streaming the whole thing to memory only to unpack to the fs, surely this can be done in a piped fashion?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants