Skip to content

Distribution Support

Rubhan Azeem edited this page Aug 27, 2026 · 9 revisions

Distribution Support gives OBS a first-class way to describe the Linux distributions an instance knows about, replacing a flat admin-curated list with a hierarchy that distribution maintainers own themselves. The hierarchy has three levels — Vendor, Distro, DistroRelease — anchored to a Project at the top and terminating in the RepositoryArchitecture records that a release is actually built from.

The whole feature sits behind the :enhanced_distribution_support feature toggle.

Distribution Support architecture


Why not the existing Distribution model?

OBS already has a Distribution model, and the new hierarchy deliberately does not reuse or extend it. The two solve different problems:

Distribution (legacy) Distribution Support
Shape Flat — vendor is a string column on each row Three linked models
Ownership Admin-only (DistributionsController restricts create/update/destroy) Project maintainers, via the policy chain
Scope Instance-global catalog Rooted in a Project
Purpose Populates the "Add repository" wizard Describes what a vendor ships, and what it is built from

Because a legacy Distribution carries its vendor as a bare string, there is nowhere to hang releases, no identity for a vendor, and no way for anyone but an admin to curate it. The new models are additive — the legacy table and the repository wizard are untouched.

Note the naming: the new model is Distro, not Distribution, precisely so the two can coexist. There is no namespace involved; the class names are distinct.


Components

The organisation that publishes distributions — openSUSE, SUSE, Debian.

A Vendor belongs to exactly one Project, and a Project has at most one Vendor (has_one :vendor, dependent: :destroy). That project is the vendor's home in OBS and the source of all authorization for the subtree beneath it. The one-to-one is enforced by a UNIQUE index on vendors.project_id.

Fields: name, description, url.

belongs_to :project, optional: false
has_many :distros, dependent: :destroy

A product line published by a vendor — Leap, Tumbleweed, SLES, Debian GNU/Linux.

Names are unique per vendor, not globally, so two vendors may both ship something called "Micro". Enforced both in the model and by a composite UNIQUE index on (vendor_id, name).

Fields: name, description, url.

belongs_to :vendor, optional: false
has_many :distro_releases, dependent: :destroy
has_one :project, through: :vendor

A specific shipped version of a distro — Leap 15.6, SLES 15 SP6, Debian 13 "Trixie".

Names are unique per distro, via (distro_id, name). Fields: name, description, url.

belongs_to :distro, optional: false
has_many :distro_release_repository_architectures, dependent: :destroy
has_many :repository_architectures, through: :distro_release_repository_architectures
has_many :repositories, through: :repository_architectures
has_many :projects, through: :repositories

The join model that says what a release is actually made of. A release links to many RepositoryArchitectures, and one RepositoryArchitecture may serve many releases. A UNIQUE index on (distro_release_id, repository_architecture_id) keeps the pairing from being recorded twice.


Why RepositoryArchitecture and not Project or Repository?

This is the central modelling decision, and it is easy to get wrong in either direction.

Not projects. The obvious framing — "a release is a set of projects" — cannot express reality, because projects are shared across releases. openSUSE:Maintenance feeds several Leap versions simultaneously. Given only a project, there is no way to say which release a given piece of content belongs to, so any per-release query built on projects returns answers that are silently wrong.

Not repositories alone. Repository grain fixes that, but a repository is still a set of architectures, and a release does not necessarily ship all of them. A repository may build x86_64, aarch64 and ppc64le while the release in question only ships the first two.

RepositoryArchitecture is the finest grain that is still a real, existing OBS object — it is the row that a build actually targets. Anchoring there means the composition of a release is exact, and the coarser views come for free by traversal.

Gotcha: the derived associations return duplicates

repositories and projects on DistroRelease are conveniences reached through the join. If a release links three architectures of the same repository, that repository appears three times:

release.repositories.count          # => 3, for one repository
release.repositories.distinct.count # => 1

The same applies to projects, compounded — a project with two repositories at three arches each yields six rows. Always add .distinct when you want the set rather than the multiset, and treat both associations as read-only views; writes belong on repository_architectures.


Authorization

Every policy delegates upward, so permission is decided once, at the project:

DistroReleasePolicy#create? → DistroPolicy#create? → VendorPolicy#create? → ProjectPolicy#update?

new?, update? and destroy? all alias create? at each level. The practical rule: if you can modify the project, you can manage its entire distribution tree. There is no separate vendor-maintainer role.

Reads are public — VendorPolicy#show? and DistroPolicy#show? both return true. DistroReleasePolicy has no show? because releases have no show action; they are rendered inside the distro page.

All three controllers use after_action :verify_authorized, and VendorsController#show calls skip_authorization when the project has no vendor yet, since there is no record to authorize.


Data integrity

Constraints are declared at both layers — model validation for usable error messages, database index for the actual guarantee.

Table Unique index Foreign keys
vendors (project_id) projects
distros (vendor_id, name) vendors
distro_releases (distro_id, name) distros
distro_release_repository_architectures (distro_release_id, repository_architecture_id) distro_releases, repository_architectures

Deletion cascades through dependent: :destroy at every level, so destroying a vendor removes its distros, their releases, and the join rows — but never touches the RepositoryArchitecture records themselves, which belong to the repositories.

Clone this wiki locally